export class MoltZapService {
private client: MoltZapAgentClient | null = null;
private _connected = false;
/**
* Service-owned scope. Opened in `connect()`, owns the
* `subscribeAll → Stream.runForEach` fan-out fiber. Closed in `close()` so
* the fiber terminates with the service.
*
* Held off the public `connect()` signature so callers do not need to
* thread a `Scope` requirement.
*/
private serviceScope: Scope.CloseableScope | null = null;
private readonly conversationsRef: Ref.Ref<
HashMap.HashMap<string, ConversationMeta>
> = Effect.runSync(Ref.make(HashMap.empty<string, ConversationMeta>()));
private readonly messagesRef: Ref.Ref<
HashMap.HashMap<string, ReadonlyArray<Message>>
> = Effect.runSync(Ref.make(HashMap.empty<string, ReadonlyArray<Message>>()));
private readonly agentNamesRef: Ref.Ref<HashMap.HashMap<string, string>> =
Effect.runSync(Ref.make(HashMap.empty<string, string>()));
private readonly agentConversationCacheRef: Ref.Ref<
HashMap.HashMap<
string,
{ readonly taskId: TaskId; readonly conversationId: ConversationId }
>
> = Effect.runSync(
Ref.make(
HashMap.empty<
string,
{ readonly taskId: TaskId; readonly conversationId: ConversationId }
>(),
),
);
private readonly lastNotifiedRef: Ref.Ref<
HashMap.HashMap<string, HashMap.HashMap<string, string>>
> = Effect.runSync(
Ref.make(HashMap.empty<string, HashMap.HashMap<string, string>>()),
);
private readonly lastReadRef: Ref.Ref<
HashMap.HashMap<string, HashMap.HashMap<string, ReadonlySet<string>>>
> = Effect.runSync(
Ref.make(
HashMap.empty<string, HashMap.HashMap<string, ReadonlySet<string>>>(),
),
);
private readonly archivedConversationIds = new Set<string>();
/**
* The branded outer and inner keys keep conversation and message ids from
* crossing accidentally while each conversation owns its eviction window.
*/
private readonly seenMessageIds = new Map<
ConversationId,
BoundedMap<MessageId, true>
>();
private readonly handlers: {
[K in ServiceHandlerName]: Array<
NotificationHandler<ServiceHandlerPayloads[K]>
>;
} = {
message: [],
rawNotification: [],
disconnect: [],
reconnect: [],
conversationArchived: [],
conversationUnarchived: [],
dispatchRelease: [],
dispatchLeaseConsumed: [],
dispatchLeaseExpired: [],
};
private _ownAgentId: AgentId;
protected constructor(private opts: ServiceOptions) {
// The empty HelloOk carries no identity; `ownAgentId` is the client's
// registered/stored id, available before the handshake.
this._ownAgentId = opts.agentId;
}
static fromConfig(config: MoltzapServiceConfig): MoltZapService {
return new MoltZapService(config);
}
static make(
profileName: string,
): Effect.Effect<MoltZapService, ServiceConfigError> {
return loadServiceConfig(profileName).pipe(
Effect.map(MoltZapService.fromConfig),
);
}
static startDaemon(
profileName: string,
): Effect.Effect<
MoltZapService,
ServiceConfigError | ServiceRpcError | unknown
> {
return Effect.gen(function* () {
const service = yield* MoltZapService.make(profileName);
yield* service.connect();
yield* service.startSocketServer();
return service;
}).pipe(Effect.withSpan("MoltZapService.startDaemon"));
}
get connected(): boolean {
return this._connected;
}
get ownAgentId(): AgentId | undefined {
return this._ownAgentId;
}
/** Effect-native: compose via `yield*` or bridge at the edge via `Effect.runPromise`. */
connect(): Effect.Effect<HelloOk, ServiceRpcError> {
return Effect.gen(this, function* () {
const client = new MoltZapAgentClient({
serverUrl: this.opts.serverUrl,