@earendil-works/pi-server 0.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +50 -0
- package/dist/connection.d.ts +28 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +4 -0
- package/dist/connection.js.map +1 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +45 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/listener.d.ts +10 -0
- package/dist/listener.d.ts.map +1 -0
- package/dist/listener.js +2 -0
- package/dist/listener.js.map +1 -0
- package/dist/protocol.d.ts +22 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +266 -0
- package/dist/protocol.js.map +1 -0
- package/dist/server.d.ts +36 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +365 -0
- package/dist/server.js.map +1 -0
- package/dist/sessions.d.ts +2174 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +305 -0
- package/dist/sessions.js.map +1 -0
- package/dist/snapshots.d.ts +24 -0
- package/dist/snapshots.d.ts.map +1 -0
- package/dist/snapshots.js +39 -0
- package/dist/snapshots.js.map +1 -0
- package/dist/testing/client.d.ts +31 -0
- package/dist/testing/client.d.ts.map +1 -0
- package/dist/testing/client.js +118 -0
- package/dist/testing/client.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/server.d.ts +12 -0
- package/dist/testing/server.d.ts.map +1 -0
- package/dist/testing/server.js +17 -0
- package/dist/testing/server.js.map +1 -0
- package/dist/testing/service.d.ts +59 -0
- package/dist/testing/service.d.ts.map +1 -0
- package/dist/testing/service.js +240 -0
- package/dist/testing/service.js.map +1 -0
- package/dist/transports/unix/index.d.ts +4 -0
- package/dist/transports/unix/index.d.ts.map +1 -0
- package/dist/transports/unix/index.js +3 -0
- package/dist/transports/unix/index.js.map +1 -0
- package/dist/transports/unix/listener.d.ts +25 -0
- package/dist/transports/unix/listener.d.ts.map +1 -0
- package/dist/transports/unix/listener.js +432 -0
- package/dist/transports/unix/listener.js.map +1 -0
- package/dist/transports/unix/preset.d.ts +6 -0
- package/dist/transports/unix/preset.d.ts.map +1 -0
- package/dist/transports/unix/preset.js +21 -0
- package/dist/transports/unix/preset.js.map +1 -0
- package/dist/transports/unix/types.d.ts +15 -0
- package/dist/transports/unix/types.d.ts.map +1 -0
- package/dist/transports/unix/types.js +2 -0
- package/dist/transports/unix/types.js.map +1 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAmB,MAAM,6BAA6B,CAAC;AAC5G,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,KAAK,EAAwB,eAAe,EAA2C,MAAM,YAAY,CAAC;AAajH,UAAU,yBAAyB;IAClC,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,WAAW,EAAE,CAAC,UAAU,EAAE,eAAe,EAAE,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvF,eAAe,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,UAAU,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,uBAAuB,EAAE,MAAM,IAAI,CAAC;IACpC,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACtC;AAYD,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkC;IAC/D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA2C;IAE3E,YAAY,OAAO,EAAE,yBAAyB,EAE7C;IAEK,cAAc,CAAC,UAAU,EAAE,eAAe,EAAE,OAAO,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAyEjE;IAEK,UAAU,CAAC,UAAU,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAU3D;IAEK,YAAY,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAgB/C;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAiB3B;YAEa,YAAY;YAeZ,OAAO;YAuBP,MAAM;IAuCpB,OAAO,CAAC,kBAAkB;YAiBZ,SAAS;YAWT,kBAAkB;IAahC,OAAO,CAAC,aAAa;YAIP,iBAAiB;YAOjB,MAAM;IASpB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,oBAAoB;YAId,YAAY;CAsB1B","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Command, EventEnvelope, SessionMetadata, SessionSnapshot } from \"@earendil-works/pi-protocol\";\nimport type { ByteConnection, ConnectionState } from \"./connection.ts\";\nimport { PiServerError } from \"./errors.ts\";\nimport type { CreateSessionOptions, PiServerService, PiSessionRuntime, PiSessionRuntimeEvent } from \"./types.ts\";\n\ninterface LiveSession {\n\tid: string;\n\truntime: PiSessionRuntime;\n\tconnections: Set<ConnectionState>;\n\tunsubscribe: () => void;\n\toperationCount: number;\n\tready: boolean;\n\tterminal: boolean;\n\tdisposing?: Promise<void>;\n}\n\ninterface LiveSessionManagerOptions {\n\tservice: PiServerService;\n\tisClosing: () => boolean;\n\tsendMessage: (connection: ConnectionState, message: EventEnvelope) => Promise<boolean>;\n\tcloseConnection: (connection: ByteConnection) => Promise<void>;\n\tdisconnect: (connection: ConnectionState) => Promise<void>;\n\tbroadcastServerSnapshot: () => void;\n\treportError: (error: unknown) => void;\n}\n\nfunction toMetadata(snapshot: SessionSnapshot): SessionMetadata {\n\treturn {\n\t\tid: snapshot.id,\n\t\tcreatedAt: snapshot.createdAt,\n\t\tupdatedAt: snapshot.updatedAt,\n\t\tsessionName: snapshot.name,\n\t\tcwd: snapshot.cwd,\n\t};\n}\n\nexport class LiveSessionManager {\n\tprivate readonly options: LiveSessionManagerOptions;\n\tprivate readonly liveSessions = new Map<string, LiveSession>();\n\tprivate readonly openingSessions = new Map<string, Promise<LiveSession>>();\n\n\tconstructor(options: LiveSessionManagerOptions) {\n\t\tthis.options = options;\n\t}\n\n\tasync executeCommand(connection: ConnectionState, command: Command) {\n\t\tswitch (command.command) {\n\t\t\tcase \"list\":\n\t\t\t\treturn { command: \"list\" as const, sessions: await this.listMetadata() };\n\t\t\tcase \"create\": {\n\t\t\t\tconst id = randomUUID();\n\t\t\t\tconst options: CreateSessionOptions = {\n\t\t\t\t\tid,\n\t\t\t\t\tcwd: command.cwd,\n\t\t\t\t\tname: command.name,\n\t\t\t\t\tmodel: command.model,\n\t\t\t\t\tthinkingLevel: command.thinkingLevel,\n\t\t\t\t};\n\t\t\t\tconst live = await this.acquire(id, () => this.options.service.createSession(options));\n\t\t\t\tawait this.attach(connection, live);\n\t\t\t\tconst session = this.forConnection(await this.broadcastSnapshot(live), connection);\n\t\t\t\tthis.options.broadcastServerSnapshot();\n\t\t\t\treturn { command: \"create\" as const, session };\n\t\t\t}\n\t\t\tcase \"attach\": {\n\t\t\t\tconst live = await this.acquire(command.sessionId, () =>\n\t\t\t\t\tthis.options.service.openSession(command.sessionId),\n\t\t\t\t);\n\t\t\t\tawait this.attach(connection, live);\n\t\t\t\tconst session = this.forConnection(await this.broadcastSnapshot(live), connection);\n\t\t\t\tthis.options.broadcastServerSnapshot();\n\t\t\t\treturn { command: \"attach\" as const, session };\n\t\t\t}\n\t\t\tcase \"detach\": {\n\t\t\t\tconst live = this.liveSessions.get(command.sessionId);\n\t\t\t\tif (connection.sessionIds.has(command.sessionId)) {\n\t\t\t\t\tconnection.sessionIds.delete(command.sessionId);\n\t\t\t\t\tif (live) {\n\t\t\t\t\t\tlive.connections.delete(connection);\n\t\t\t\t\t\tif (live.connections.size > 0 && !live.terminal && !live.disposing) {\n\t\t\t\t\t\t\tawait this.broadcastSnapshot(live);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait this.maybeDispose(live);\n\t\t\t\t\t}\n\t\t\t\t\tthis.options.broadcastServerSnapshot();\n\t\t\t\t}\n\t\t\t\treturn { command: \"detach\" as const, sessionId: command.sessionId };\n\t\t\t}\n\t\t\tcase \"prompt\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () =>\n\t\t\t\t\tlive.runtime.prompt({ text: command.text }),\n\t\t\t\t);\n\t\t\t\treturn { command: \"prompt\" as const, session };\n\t\t\t}\n\t\t\tcase \"steer\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () => live.runtime.steer({ text: command.text }));\n\t\t\t\treturn { command: \"steer\" as const, session };\n\t\t\t}\n\t\t\tcase \"abort\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () => live.runtime.abort());\n\t\t\t\treturn { command: \"abort\" as const, session };\n\t\t\t}\n\t\t\tcase \"set_model\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () => live.runtime.setModel(command.model));\n\t\t\t\treturn { command: \"set_model\" as const, session };\n\t\t\t}\n\t\t\tcase \"set_thinking\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () =>\n\t\t\t\t\tlive.runtime.setThinking(command.thinkingLevel),\n\t\t\t\t);\n\t\t\t\treturn { command: \"set_thinking\" as const, session };\n\t\t\t}\n\t\t}\n\t}\n\n\tasync disconnect(connection: ConnectionState): Promise<void> {\n\t\tconst sessions = [...connection.sessionIds]\n\t\t\t.map((id) => this.liveSessions.get(id))\n\t\t\t.filter((live): live is LiveSession => live !== undefined);\n\t\tconnection.sessionIds.clear();\n\t\tfor (const live of sessions) live.connections.delete(connection);\n\t\tconst results = await Promise.allSettled(sessions.map((live) => this.maybeDispose(live)));\n\t\tfor (const result of results) {\n\t\t\tif (result.status === \"rejected\") this.options.reportError(result.reason);\n\t\t}\n\t}\n\n\tasync listMetadata(): Promise<SessionMetadata[]> {\n\t\tconst stored = await this.options.service.listSessions();\n\t\tconst liveSnapshots = await Promise.all(\n\t\t\t[...this.liveSessions.values()]\n\t\t\t\t.filter((live) => !live.disposing)\n\t\t\t\t.map(async (live) => [live.id, await this.normalizedSnapshot(live)] as const),\n\t\t);\n\t\tconst liveById = new Map(liveSnapshots);\n\t\tconst metadata = stored.map((item) => {\n\t\t\tconst snapshot = liveById.get(item.id);\n\t\t\tif (!snapshot) return item;\n\t\t\tliveById.delete(item.id);\n\t\t\treturn { ...item, ...toMetadata(snapshot) };\n\t\t});\n\t\tfor (const snapshot of liveById.values()) metadata.push(toMetadata(snapshot));\n\t\treturn metadata;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tconst openingResults = await Promise.allSettled([...this.openingSessions.values()]);\n\t\tfor (const result of openingResults) {\n\t\t\tif (result.status === \"rejected\") this.options.reportError(result.reason);\n\t\t}\n\t\tconst sessions = [...this.liveSessions.values()];\n\t\tthis.liveSessions.clear();\n\t\tawait Promise.all(\n\t\t\tsessions.map(async (live) => {\n\t\t\t\tif (live.disposing) {\n\t\t\t\t\tawait live.disposing;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlive.unsubscribe();\n\t\t\t\tawait live.runtime.dispose();\n\t\t\t}),\n\t\t);\n\t}\n\n\tprivate async runOperation(\n\t\tconnection: ConnectionState,\n\t\tlive: LiveSession,\n\t\toperation: () => Promise<void>,\n\t): Promise<SessionSnapshot> {\n\t\tlive.operationCount += 1;\n\t\ttry {\n\t\t\tawait operation();\n\t\t\treturn this.forConnection(await this.broadcastSnapshot(live), connection);\n\t\t} finally {\n\t\t\tlive.operationCount -= 1;\n\t\t\tthis.scheduleMaybeDispose(live);\n\t\t}\n\t}\n\n\tprivate async acquire(id: string, acquireRuntime: () => Promise<PiSessionRuntime>): Promise<LiveSession> {\n\t\tfor (;;) {\n\t\t\tconst existing = this.liveSessions.get(id);\n\t\t\tif (existing) {\n\t\t\t\tif (existing.terminal) throw new PiServerError(\"session_locked\", `Session runtime is terminating: ${id}`);\n\t\t\t\tif (existing.disposing) {\n\t\t\t\t\tawait existing.disposing;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\treturn existing;\n\t\t\t}\n\t\t\tconst opening = this.openingSessions.get(id);\n\t\t\tif (opening) return opening;\n\t\t\tconst pending = this.create(id, acquireRuntime);\n\t\t\tthis.openingSessions.set(id, pending);\n\t\t\ttry {\n\t\t\t\treturn await pending;\n\t\t\t} finally {\n\t\t\t\tif (this.openingSessions.get(id) === pending) this.openingSessions.delete(id);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async create(id: string, acquireRuntime: () => Promise<PiSessionRuntime>): Promise<LiveSession> {\n\t\tconst runtime = await acquireRuntime();\n\t\tif (this.options.isClosing()) {\n\t\t\tawait runtime.dispose();\n\t\t\tthrow new Error(\"PiServer closed while acquiring a session runtime\");\n\t\t}\n\t\tlet live: LiveSession | undefined;\n\t\ttry {\n\t\t\tconst snapshot = await runtime.snapshot();\n\t\t\tif (snapshot.id !== id) {\n\t\t\t\tthrow new PiServerError(\n\t\t\t\t\t\"invalid_request\",\n\t\t\t\t\t`Service returned session ${snapshot.id} for server-assigned session ${id}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tlive = {\n\t\t\t\tid,\n\t\t\t\truntime,\n\t\t\t\tconnections: new Set(),\n\t\t\t\tunsubscribe: () => {},\n\t\t\t\toperationCount: 0,\n\t\t\t\tready: false,\n\t\t\t\tterminal: false,\n\t\t\t};\n\t\t\tlive.unsubscribe = runtime.subscribe((event) => this.handleRuntimeEvent(live!, event));\n\t\t\tthis.liveSessions.set(id, live);\n\t\t\tlive.ready = true;\n\t\t\treturn live;\n\t\t} catch (error) {\n\t\t\tif (live) live.unsubscribe();\n\t\t\ttry {\n\t\t\t\tawait runtime.dispose();\n\t\t\t} catch (disposeError) {\n\t\t\t\tthis.options.reportError(disposeError);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate handleRuntimeEvent(live: LiveSession, event: PiSessionRuntimeEvent): void {\n\t\tif (event.type === \"error\") {\n\t\t\tvoid this.terminate(live, event.error).catch((error: unknown) => this.options.reportError(error));\n\t\t\treturn;\n\t\t}\n\t\tif (event.type === \"progress\") {\n\t\t\tconst envelope: EventEnvelope = {\n\t\t\t\ttype: \"event\",\n\t\t\t\tevent: { type: \"session_progress\", sessionId: live.id, progress: event.progress },\n\t\t\t};\n\t\t\tfor (const connection of live.connections) void this.options.sendMessage(connection, envelope);\n\t\t} else {\n\t\t\tvoid this.broadcastSnapshot(live).catch((error: unknown) => this.options.reportError(error));\n\t\t}\n\t\tthis.scheduleMaybeDispose(live);\n\t}\n\n\tprivate async terminate(live: LiveSession, error: PiServerError): Promise<void> {\n\t\tif (live.terminal) return;\n\t\tlive.terminal = true;\n\t\tthis.options.reportError(error);\n\t\tlive.unsubscribe();\n\t\tconst connections = [...live.connections];\n\t\tawait Promise.all(connections.map((connection) => this.options.closeConnection(connection.connection)));\n\t\tawait Promise.all(connections.map((connection) => this.options.disconnect(connection)));\n\t\tawait this.maybeDispose(live);\n\t}\n\n\tprivate async normalizedSnapshot(live: LiveSession): Promise<SessionSnapshot> {\n\t\tconst snapshot = await live.runtime.snapshot();\n\t\tif (snapshot.id !== live.id) {\n\t\t\tthrow new PiServerError(\"invalid_request\", `Runtime session ID changed from ${live.id} to ${snapshot.id}`);\n\t\t}\n\t\treturn {\n\t\t\t...snapshot,\n\t\t\tphase: live.runtime.getPhase(),\n\t\t\tattached: live.connections.size > 0,\n\t\t\tlocked: true,\n\t\t};\n\t}\n\n\tprivate forConnection(snapshot: SessionSnapshot, connection: ConnectionState): SessionSnapshot {\n\t\treturn { ...snapshot, attached: connection.sessionIds.has(snapshot.id) };\n\t}\n\n\tprivate async broadcastSnapshot(live: LiveSession): Promise<SessionSnapshot> {\n\t\tconst snapshot = await this.normalizedSnapshot(live);\n\t\tconst envelope: EventEnvelope = { type: \"event\", event: { type: \"session_snapshot\", snapshot } };\n\t\tfor (const connection of live.connections) void this.options.sendMessage(connection, envelope);\n\t\treturn snapshot;\n\t}\n\n\tprivate async attach(connection: ConnectionState, live: LiveSession): Promise<void> {\n\t\tif (connection.disconnected || connection.stage !== \"ready\" || connection.connection.closed) {\n\t\t\tawait this.maybeDispose(live);\n\t\t\tthrow new PiServerError(\"invalid_request\", \"Connection closed while attaching to a session\");\n\t\t}\n\t\tconnection.sessionIds.add(live.id);\n\t\tlive.connections.add(connection);\n\t}\n\n\tprivate requireAttached(connection: ConnectionState, sessionId: string): LiveSession {\n\t\tif (!connection.sessionIds.has(sessionId)) {\n\t\t\tthrow new PiServerError(\"invalid_request\", `Connection is not attached to session ${sessionId}`);\n\t\t}\n\t\tconst live = this.liveSessions.get(sessionId);\n\t\tif (!live || live.terminal || live.disposing) {\n\t\t\tthrow new PiServerError(\"not_found\", `Session is not live: ${sessionId}`);\n\t\t}\n\t\treturn live;\n\t}\n\n\tprivate scheduleMaybeDispose(live: LiveSession): void {\n\t\tvoid this.maybeDispose(live).catch((error: unknown) => this.options.reportError(error));\n\t}\n\n\tprivate async maybeDispose(live: LiveSession): Promise<void> {\n\t\tif (\n\t\t\tthis.options.isClosing() ||\n\t\t\t!live.ready ||\n\t\t\tlive.disposing ||\n\t\t\tlive.connections.size > 0 ||\n\t\t\tlive.operationCount > 0 ||\n\t\t\t(!live.terminal && live.runtime.getPhase() !== \"idle\")\n\t\t) {\n\t\t\treturn live.disposing;\n\t\t}\n\t\tlive.unsubscribe();\n\t\tlive.disposing = (async () => {\n\t\t\ttry {\n\t\t\t\tawait live.runtime.dispose();\n\t\t\t} finally {\n\t\t\t\tif (this.liveSessions.get(live.id) === live) this.liveSessions.delete(live.id);\n\t\t\t}\n\t\t})();\n\t\tawait live.disposing;\n\t\tif (!this.options.isClosing()) this.options.broadcastServerSnapshot();\n\t}\n}\n"]}
|
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { PiServerError } from "./errors.js";
|
|
3
|
+
function toMetadata(snapshot) {
|
|
4
|
+
return {
|
|
5
|
+
id: snapshot.id,
|
|
6
|
+
createdAt: snapshot.createdAt,
|
|
7
|
+
updatedAt: snapshot.updatedAt,
|
|
8
|
+
sessionName: snapshot.name,
|
|
9
|
+
cwd: snapshot.cwd,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export class LiveSessionManager {
|
|
13
|
+
options;
|
|
14
|
+
liveSessions = new Map();
|
|
15
|
+
openingSessions = new Map();
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
async executeCommand(connection, command) {
|
|
20
|
+
switch (command.command) {
|
|
21
|
+
case "list":
|
|
22
|
+
return { command: "list", sessions: await this.listMetadata() };
|
|
23
|
+
case "create": {
|
|
24
|
+
const id = randomUUID();
|
|
25
|
+
const options = {
|
|
26
|
+
id,
|
|
27
|
+
cwd: command.cwd,
|
|
28
|
+
name: command.name,
|
|
29
|
+
model: command.model,
|
|
30
|
+
thinkingLevel: command.thinkingLevel,
|
|
31
|
+
};
|
|
32
|
+
const live = await this.acquire(id, () => this.options.service.createSession(options));
|
|
33
|
+
await this.attach(connection, live);
|
|
34
|
+
const session = this.forConnection(await this.broadcastSnapshot(live), connection);
|
|
35
|
+
this.options.broadcastServerSnapshot();
|
|
36
|
+
return { command: "create", session };
|
|
37
|
+
}
|
|
38
|
+
case "attach": {
|
|
39
|
+
const live = await this.acquire(command.sessionId, () => this.options.service.openSession(command.sessionId));
|
|
40
|
+
await this.attach(connection, live);
|
|
41
|
+
const session = this.forConnection(await this.broadcastSnapshot(live), connection);
|
|
42
|
+
this.options.broadcastServerSnapshot();
|
|
43
|
+
return { command: "attach", session };
|
|
44
|
+
}
|
|
45
|
+
case "detach": {
|
|
46
|
+
const live = this.liveSessions.get(command.sessionId);
|
|
47
|
+
if (connection.sessionIds.has(command.sessionId)) {
|
|
48
|
+
connection.sessionIds.delete(command.sessionId);
|
|
49
|
+
if (live) {
|
|
50
|
+
live.connections.delete(connection);
|
|
51
|
+
if (live.connections.size > 0 && !live.terminal && !live.disposing) {
|
|
52
|
+
await this.broadcastSnapshot(live);
|
|
53
|
+
}
|
|
54
|
+
await this.maybeDispose(live);
|
|
55
|
+
}
|
|
56
|
+
this.options.broadcastServerSnapshot();
|
|
57
|
+
}
|
|
58
|
+
return { command: "detach", sessionId: command.sessionId };
|
|
59
|
+
}
|
|
60
|
+
case "prompt": {
|
|
61
|
+
const live = this.requireAttached(connection, command.sessionId);
|
|
62
|
+
const session = await this.runOperation(connection, live, () => live.runtime.prompt({ text: command.text }));
|
|
63
|
+
return { command: "prompt", session };
|
|
64
|
+
}
|
|
65
|
+
case "steer": {
|
|
66
|
+
const live = this.requireAttached(connection, command.sessionId);
|
|
67
|
+
const session = await this.runOperation(connection, live, () => live.runtime.steer({ text: command.text }));
|
|
68
|
+
return { command: "steer", session };
|
|
69
|
+
}
|
|
70
|
+
case "abort": {
|
|
71
|
+
const live = this.requireAttached(connection, command.sessionId);
|
|
72
|
+
const session = await this.runOperation(connection, live, () => live.runtime.abort());
|
|
73
|
+
return { command: "abort", session };
|
|
74
|
+
}
|
|
75
|
+
case "set_model": {
|
|
76
|
+
const live = this.requireAttached(connection, command.sessionId);
|
|
77
|
+
const session = await this.runOperation(connection, live, () => live.runtime.setModel(command.model));
|
|
78
|
+
return { command: "set_model", session };
|
|
79
|
+
}
|
|
80
|
+
case "set_thinking": {
|
|
81
|
+
const live = this.requireAttached(connection, command.sessionId);
|
|
82
|
+
const session = await this.runOperation(connection, live, () => live.runtime.setThinking(command.thinkingLevel));
|
|
83
|
+
return { command: "set_thinking", session };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async disconnect(connection) {
|
|
88
|
+
const sessions = [...connection.sessionIds]
|
|
89
|
+
.map((id) => this.liveSessions.get(id))
|
|
90
|
+
.filter((live) => live !== undefined);
|
|
91
|
+
connection.sessionIds.clear();
|
|
92
|
+
for (const live of sessions)
|
|
93
|
+
live.connections.delete(connection);
|
|
94
|
+
const results = await Promise.allSettled(sessions.map((live) => this.maybeDispose(live)));
|
|
95
|
+
for (const result of results) {
|
|
96
|
+
if (result.status === "rejected")
|
|
97
|
+
this.options.reportError(result.reason);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async listMetadata() {
|
|
101
|
+
const stored = await this.options.service.listSessions();
|
|
102
|
+
const liveSnapshots = await Promise.all([...this.liveSessions.values()]
|
|
103
|
+
.filter((live) => !live.disposing)
|
|
104
|
+
.map(async (live) => [live.id, await this.normalizedSnapshot(live)]));
|
|
105
|
+
const liveById = new Map(liveSnapshots);
|
|
106
|
+
const metadata = stored.map((item) => {
|
|
107
|
+
const snapshot = liveById.get(item.id);
|
|
108
|
+
if (!snapshot)
|
|
109
|
+
return item;
|
|
110
|
+
liveById.delete(item.id);
|
|
111
|
+
return { ...item, ...toMetadata(snapshot) };
|
|
112
|
+
});
|
|
113
|
+
for (const snapshot of liveById.values())
|
|
114
|
+
metadata.push(toMetadata(snapshot));
|
|
115
|
+
return metadata;
|
|
116
|
+
}
|
|
117
|
+
async close() {
|
|
118
|
+
const openingResults = await Promise.allSettled([...this.openingSessions.values()]);
|
|
119
|
+
for (const result of openingResults) {
|
|
120
|
+
if (result.status === "rejected")
|
|
121
|
+
this.options.reportError(result.reason);
|
|
122
|
+
}
|
|
123
|
+
const sessions = [...this.liveSessions.values()];
|
|
124
|
+
this.liveSessions.clear();
|
|
125
|
+
await Promise.all(sessions.map(async (live) => {
|
|
126
|
+
if (live.disposing) {
|
|
127
|
+
await live.disposing;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
live.unsubscribe();
|
|
131
|
+
await live.runtime.dispose();
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
async runOperation(connection, live, operation) {
|
|
135
|
+
live.operationCount += 1;
|
|
136
|
+
try {
|
|
137
|
+
await operation();
|
|
138
|
+
return this.forConnection(await this.broadcastSnapshot(live), connection);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
live.operationCount -= 1;
|
|
142
|
+
this.scheduleMaybeDispose(live);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async acquire(id, acquireRuntime) {
|
|
146
|
+
for (;;) {
|
|
147
|
+
const existing = this.liveSessions.get(id);
|
|
148
|
+
if (existing) {
|
|
149
|
+
if (existing.terminal)
|
|
150
|
+
throw new PiServerError("session_locked", `Session runtime is terminating: ${id}`);
|
|
151
|
+
if (existing.disposing) {
|
|
152
|
+
await existing.disposing;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
return existing;
|
|
156
|
+
}
|
|
157
|
+
const opening = this.openingSessions.get(id);
|
|
158
|
+
if (opening)
|
|
159
|
+
return opening;
|
|
160
|
+
const pending = this.create(id, acquireRuntime);
|
|
161
|
+
this.openingSessions.set(id, pending);
|
|
162
|
+
try {
|
|
163
|
+
return await pending;
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
if (this.openingSessions.get(id) === pending)
|
|
167
|
+
this.openingSessions.delete(id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async create(id, acquireRuntime) {
|
|
172
|
+
const runtime = await acquireRuntime();
|
|
173
|
+
if (this.options.isClosing()) {
|
|
174
|
+
await runtime.dispose();
|
|
175
|
+
throw new Error("PiServer closed while acquiring a session runtime");
|
|
176
|
+
}
|
|
177
|
+
let live;
|
|
178
|
+
try {
|
|
179
|
+
const snapshot = await runtime.snapshot();
|
|
180
|
+
if (snapshot.id !== id) {
|
|
181
|
+
throw new PiServerError("invalid_request", `Service returned session ${snapshot.id} for server-assigned session ${id}`);
|
|
182
|
+
}
|
|
183
|
+
live = {
|
|
184
|
+
id,
|
|
185
|
+
runtime,
|
|
186
|
+
connections: new Set(),
|
|
187
|
+
unsubscribe: () => { },
|
|
188
|
+
operationCount: 0,
|
|
189
|
+
ready: false,
|
|
190
|
+
terminal: false,
|
|
191
|
+
};
|
|
192
|
+
live.unsubscribe = runtime.subscribe((event) => this.handleRuntimeEvent(live, event));
|
|
193
|
+
this.liveSessions.set(id, live);
|
|
194
|
+
live.ready = true;
|
|
195
|
+
return live;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (live)
|
|
199
|
+
live.unsubscribe();
|
|
200
|
+
try {
|
|
201
|
+
await runtime.dispose();
|
|
202
|
+
}
|
|
203
|
+
catch (disposeError) {
|
|
204
|
+
this.options.reportError(disposeError);
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
handleRuntimeEvent(live, event) {
|
|
210
|
+
if (event.type === "error") {
|
|
211
|
+
void this.terminate(live, event.error).catch((error) => this.options.reportError(error));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (event.type === "progress") {
|
|
215
|
+
const envelope = {
|
|
216
|
+
type: "event",
|
|
217
|
+
event: { type: "session_progress", sessionId: live.id, progress: event.progress },
|
|
218
|
+
};
|
|
219
|
+
for (const connection of live.connections)
|
|
220
|
+
void this.options.sendMessage(connection, envelope);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
void this.broadcastSnapshot(live).catch((error) => this.options.reportError(error));
|
|
224
|
+
}
|
|
225
|
+
this.scheduleMaybeDispose(live);
|
|
226
|
+
}
|
|
227
|
+
async terminate(live, error) {
|
|
228
|
+
if (live.terminal)
|
|
229
|
+
return;
|
|
230
|
+
live.terminal = true;
|
|
231
|
+
this.options.reportError(error);
|
|
232
|
+
live.unsubscribe();
|
|
233
|
+
const connections = [...live.connections];
|
|
234
|
+
await Promise.all(connections.map((connection) => this.options.closeConnection(connection.connection)));
|
|
235
|
+
await Promise.all(connections.map((connection) => this.options.disconnect(connection)));
|
|
236
|
+
await this.maybeDispose(live);
|
|
237
|
+
}
|
|
238
|
+
async normalizedSnapshot(live) {
|
|
239
|
+
const snapshot = await live.runtime.snapshot();
|
|
240
|
+
if (snapshot.id !== live.id) {
|
|
241
|
+
throw new PiServerError("invalid_request", `Runtime session ID changed from ${live.id} to ${snapshot.id}`);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
...snapshot,
|
|
245
|
+
phase: live.runtime.getPhase(),
|
|
246
|
+
attached: live.connections.size > 0,
|
|
247
|
+
locked: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
forConnection(snapshot, connection) {
|
|
251
|
+
return { ...snapshot, attached: connection.sessionIds.has(snapshot.id) };
|
|
252
|
+
}
|
|
253
|
+
async broadcastSnapshot(live) {
|
|
254
|
+
const snapshot = await this.normalizedSnapshot(live);
|
|
255
|
+
const envelope = { type: "event", event: { type: "session_snapshot", snapshot } };
|
|
256
|
+
for (const connection of live.connections)
|
|
257
|
+
void this.options.sendMessage(connection, envelope);
|
|
258
|
+
return snapshot;
|
|
259
|
+
}
|
|
260
|
+
async attach(connection, live) {
|
|
261
|
+
if (connection.disconnected || connection.stage !== "ready" || connection.connection.closed) {
|
|
262
|
+
await this.maybeDispose(live);
|
|
263
|
+
throw new PiServerError("invalid_request", "Connection closed while attaching to a session");
|
|
264
|
+
}
|
|
265
|
+
connection.sessionIds.add(live.id);
|
|
266
|
+
live.connections.add(connection);
|
|
267
|
+
}
|
|
268
|
+
requireAttached(connection, sessionId) {
|
|
269
|
+
if (!connection.sessionIds.has(sessionId)) {
|
|
270
|
+
throw new PiServerError("invalid_request", `Connection is not attached to session ${sessionId}`);
|
|
271
|
+
}
|
|
272
|
+
const live = this.liveSessions.get(sessionId);
|
|
273
|
+
if (!live || live.terminal || live.disposing) {
|
|
274
|
+
throw new PiServerError("not_found", `Session is not live: ${sessionId}`);
|
|
275
|
+
}
|
|
276
|
+
return live;
|
|
277
|
+
}
|
|
278
|
+
scheduleMaybeDispose(live) {
|
|
279
|
+
void this.maybeDispose(live).catch((error) => this.options.reportError(error));
|
|
280
|
+
}
|
|
281
|
+
async maybeDispose(live) {
|
|
282
|
+
if (this.options.isClosing() ||
|
|
283
|
+
!live.ready ||
|
|
284
|
+
live.disposing ||
|
|
285
|
+
live.connections.size > 0 ||
|
|
286
|
+
live.operationCount > 0 ||
|
|
287
|
+
(!live.terminal && live.runtime.getPhase() !== "idle")) {
|
|
288
|
+
return live.disposing;
|
|
289
|
+
}
|
|
290
|
+
live.unsubscribe();
|
|
291
|
+
live.disposing = (async () => {
|
|
292
|
+
try {
|
|
293
|
+
await live.runtime.dispose();
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
if (this.liveSessions.get(live.id) === live)
|
|
297
|
+
this.liveSessions.delete(live.id);
|
|
298
|
+
}
|
|
299
|
+
})();
|
|
300
|
+
await live.disposing;
|
|
301
|
+
if (!this.options.isClosing())
|
|
302
|
+
this.options.broadcastServerSnapshot();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwB5C,SAAS,UAAU,CAAC,QAAyB,EAAmB;IAC/D,OAAO;QACN,EAAE,EAAE,QAAQ,CAAC,EAAE;QACf,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,WAAW,EAAE,QAAQ,CAAC,IAAI;QAC1B,GAAG,EAAE,QAAQ,CAAC,GAAG;KACjB,CAAC;AAAA,CACF;AAED,MAAM,OAAO,kBAAkB;IACb,OAAO,CAA4B;IACnC,YAAY,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC9C,eAAe,GAAG,IAAI,GAAG,EAAgC,CAAC;IAE3E,YAAY,OAAkC,EAAE;QAC/C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAAA,CACvB;IAED,KAAK,CAAC,cAAc,CAAC,UAA2B,EAAE,OAAgB,EAAE;QACnE,QAAQ,OAAO,CAAC,OAAO,EAAE,CAAC;YACzB,KAAK,MAAM;gBACV,OAAO,EAAE,OAAO,EAAE,MAAe,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;YAC1E,KAAK,QAAQ,EAAE,CAAC;gBACf,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;gBACxB,MAAM,OAAO,GAAyB;oBACrC,EAAE;oBACF,GAAG,EAAE,OAAO,CAAC,GAAG;oBAChB,IAAI,EAAE,OAAO,CAAC,IAAI;oBAClB,KAAK,EAAE,OAAO,CAAC,KAAK;oBACpB,aAAa,EAAE,OAAO,CAAC,aAAa;iBACpC,CAAC;gBACF,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;gBACvF,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;gBACpC,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,CAAC;gBACnF,IAAI,CAAC,OAAO,CAAC,uBAAuB,EAAE,CAAC;gBACvC,OAAO,EAAE,OAAO,EAAE,QAAiB,EAAE,OAAO,EAAE,CAAC;YAChD,CAAC;YACD,KAAK,QAAQ,EAAE,CAAC;gBACf,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,EAAE,CACvD,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,CACnD,CAAC;gBACF,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;gBACpC,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,CAAC;gBACnF,IAAI,CAAC,OAAO,CAAC,uBAAuB,EAAE,CAAC;gBACvC,OAAO,EAAE,OAAO,EAAE,QAAiB,EAAE,OAAO,EAAE,CAAC;YAChD,CAAC;YACD,KAAK,QAAQ,EAAE,CAAC;gBACf,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;gBACtD,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;oBAClD,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;oBAChD,IAAI,IAAI,EAAE,CAAC;wBACV,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;wBACpC,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;4BACpE,MAAM,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;wBACpC,CAAC;wBACD,MAAM,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;oBAC/B,CAAC;oBACD,IAAI,CAAC,OAAO,CAAC,uBAAuB,EAAE,CAAC;gBACxC,CAAC;gBACD,OAAO,EAAE,OAAO,EAAE,QAAiB,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC;YACrE,CAAC;YACD,KAAK,QAAQ,EAAE,CAAC;gBACf,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;gBACjE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,CAC9D,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAC3C,CAAC;gBACF,OAAO,EAAE,OAAO,EAAE,QAAiB,EAAE,OAAO,EAAE,CAAC;YAChD,CAAC;YACD,KAAK,OAAO,EAAE,CAAC;gBACd,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;gBACjE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;gBAC5G,OAAO,EAAE,OAAO,EAAE,OAAgB,EAAE,OAAO,EAAE,CAAC;YAC/C,CAAC;YACD,KAAK,OAAO,EAAE,CAAC;gBACd,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;gBACjE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;gBACtF,OAAO,EAAE,OAAO,EAAE,OAAgB,EAAE,OAAO,EAAE,CAAC;YAC/C,CAAC;YACD,KAAK,WAAW,EAAE,CAAC;gBAClB,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;gBACjE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;gBACtG,OAAO,EAAE,OAAO,EAAE,WAAoB,EAAE,OAAO,EAAE,CAAC;YACnD,CAAC;YACD,KAAK,cAAc,EAAE,CAAC;gBACrB,MAAM,IAAI,GAAG,IAAI,CAAC,eAAe,CAAC,UAAU,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;gBACjE,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,CAC9D,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,aAAa,CAAC,CAC/C,CAAC;gBACF,OAAO,EAAE,OAAO,EAAE,cAAuB,EAAE,OAAO,EAAE,CAAC;YACtD,CAAC;QACF,CAAC;IAAA,CACD;IAED,KAAK,CAAC,UAAU,CAAC,UAA2B,EAAiB;QAC5D,MAAM,QAAQ,GAAG,CAAC,GAAG,UAAU,CAAC,UAAU,CAAC;aACzC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;aACtC,MAAM,CAAC,CAAC,IAAI,EAAuB,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;QAC5D,UAAU,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QAC9B,KAAK,MAAM,IAAI,IAAI,QAAQ;YAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC1F,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC9B,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU;gBAAE,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3E,CAAC;IAAA,CACD;IAED,KAAK,CAAC,YAAY,GAA+B;QAChD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;QACzD,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CACtC,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;aAC7B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;aACjC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAU,CAAC,CAC9E,CAAC;QACF,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;QACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YACrC,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACvC,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC3B,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACzB,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAA,CAC5C,CAAC,CAAC;QACH,KAAK,MAAM,QAAQ,IAAI,QAAQ,CAAC,MAAM,EAAE;YAAE,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC9E,OAAO,QAAQ,CAAC;IAAA,CAChB;IAED,KAAK,CAAC,KAAK,GAAkB;QAC5B,MAAM,cAAc,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QACpF,KAAK,MAAM,MAAM,IAAI,cAAc,EAAE,CAAC;YACrC,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU;gBAAE,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3E,CAAC;QACD,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC1B,MAAM,OAAO,CAAC,GAAG,CAChB,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;YAC5B,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBACpB,MAAM,IAAI,CAAC,SAAS,CAAC;gBACrB,OAAO;YACR,CAAC;YACD,IAAI,CAAC,WAAW,EAAE,CAAC;YACnB,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QAAA,CAC7B,CAAC,CACF,CAAC;IAAA,CACF;IAEO,KAAK,CAAC,YAAY,CACzB,UAA2B,EAC3B,IAAiB,EACjB,SAA8B,EACH;QAC3B,IAAI,CAAC,cAAc,IAAI,CAAC,CAAC;QACzB,IAAI,CAAC;YACJ,MAAM,SAAS,EAAE,CAAC;YAClB,OAAO,IAAI,CAAC,aAAa,CAAC,MAAM,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,CAAC;QAC3E,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,cAAc,IAAI,CAAC,CAAC;YACzB,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;QACjC,CAAC;IAAA,CACD;IAEO,KAAK,CAAC,OAAO,CAAC,EAAU,EAAE,cAA+C,EAAwB;QACxG,SAAS,CAAC;YACT,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC3C,IAAI,QAAQ,EAAE,CAAC;gBACd,IAAI,QAAQ,CAAC,QAAQ;oBAAE,MAAM,IAAI,aAAa,CAAC,gBAAgB,EAAE,mCAAmC,EAAE,EAAE,CAAC,CAAC;gBAC1G,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;oBACxB,MAAM,QAAQ,CAAC,SAAS,CAAC;oBACzB,SAAS;gBACV,CAAC;gBACD,OAAO,QAAQ,CAAC;YACjB,CAAC;YACD,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC7C,IAAI,OAAO;gBAAE,OAAO,OAAO,CAAC;YAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC;YAChD,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;YACtC,IAAI,CAAC;gBACJ,OAAO,MAAM,OAAO,CAAC;YACtB,CAAC;oBAAS,CAAC;gBACV,IAAI,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,OAAO;oBAAE,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC/E,CAAC;QACF,CAAC;IAAA,CACD;IAEO,KAAK,CAAC,MAAM,CAAC,EAAU,EAAE,cAA+C,EAAwB;QACvG,MAAM,OAAO,GAAG,MAAM,cAAc,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;YAC9B,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;QACtE,CAAC;QACD,IAAI,IAA6B,CAAC;QAClC,IAAI,CAAC;YACJ,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC;YAC1C,IAAI,QAAQ,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;gBACxB,MAAM,IAAI,aAAa,CACtB,iBAAiB,EACjB,4BAA4B,QAAQ,CAAC,EAAE,gCAAgC,EAAE,EAAE,CAC3E,CAAC;YACH,CAAC;YACD,IAAI,GAAG;gBACN,EAAE;gBACF,OAAO;gBACP,WAAW,EAAE,IAAI,GAAG,EAAE;gBACtB,WAAW,EAAE,GAAG,EAAE,CAAC,EAAC,CAAC;gBACrB,cAAc,EAAE,CAAC;gBACjB,KAAK,EAAE,KAAK;gBACZ,QAAQ,EAAE,KAAK;aACf,CAAC;YACF,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,kBAAkB,CAAC,IAAK,EAAE,KAAK,CAAC,CAAC,CAAC;YACvF,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YAChC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAClB,OAAO,IAAI,CAAC;QACb,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,IAAI;gBAAE,IAAI,CAAC,WAAW,EAAE,CAAC;YAC7B,IAAI,CAAC;gBACJ,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;YACzB,CAAC;YAAC,OAAO,YAAY,EAAE,CAAC;gBACvB,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;YACxC,CAAC;YACD,MAAM,KAAK,CAAC;QACb,CAAC;IAAA,CACD;IAEO,kBAAkB,CAAC,IAAiB,EAAE,KAA4B,EAAQ;QACjF,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC5B,KAAK,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;YAClG,OAAO;QACR,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,QAAQ,GAAkB;gBAC/B,IAAI,EAAE,OAAO;gBACb,KAAK,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE;aACjF,CAAC;YACF,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC,WAAW;gBAAE,KAAK,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAChG,CAAC;aAAM,CAAC;YACP,KAAK,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9F,CAAC;QACD,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAAA,CAChC;IAEO,KAAK,CAAC,SAAS,CAAC,IAAiB,EAAE,KAAoB,EAAiB;QAC/E,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC1B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,MAAM,WAAW,GAAG,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;QAC1C,MAAM,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACxG,MAAM,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACxF,MAAM,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAAA,CAC9B;IAEO,KAAK,CAAC,kBAAkB,CAAC,IAAiB,EAA4B;QAC7E,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QAC/C,IAAI,QAAQ,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;YAC7B,MAAM,IAAI,aAAa,CAAC,iBAAiB,EAAE,mCAAmC,IAAI,CAAC,EAAE,OAAO,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5G,CAAC;QACD,OAAO;YACN,GAAG,QAAQ;YACX,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE;YAC9B,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC;YACnC,MAAM,EAAE,IAAI;SACZ,CAAC;IAAA,CACF;IAEO,aAAa,CAAC,QAAyB,EAAE,UAA2B,EAAmB;QAC9F,OAAO,EAAE,GAAG,QAAQ,EAAE,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;IAAA,CACzE;IAEO,KAAK,CAAC,iBAAiB,CAAC,IAAiB,EAA4B;QAC5E,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACrD,MAAM,QAAQ,GAAkB,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,QAAQ,EAAE,EAAE,CAAC;QACjG,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC,WAAW;YAAE,KAAK,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAC/F,OAAO,QAAQ,CAAC;IAAA,CAChB;IAEO,KAAK,CAAC,MAAM,CAAC,UAA2B,EAAE,IAAiB,EAAiB;QACnF,IAAI,UAAU,CAAC,YAAY,IAAI,UAAU,CAAC,KAAK,KAAK,OAAO,IAAI,UAAU,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;YAC7F,MAAM,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;YAC9B,MAAM,IAAI,aAAa,CAAC,iBAAiB,EAAE,gDAAgD,CAAC,CAAC;QAC9F,CAAC;QACD,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACnC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAAA,CACjC;IAEO,eAAe,CAAC,UAA2B,EAAE,SAAiB,EAAe;QACpF,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3C,MAAM,IAAI,aAAa,CAAC,iBAAiB,EAAE,yCAAyC,SAAS,EAAE,CAAC,CAAC;QAClG,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAC9C,MAAM,IAAI,aAAa,CAAC,WAAW,EAAE,wBAAwB,SAAS,EAAE,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,IAAI,CAAC;IAAA,CACZ;IAEO,oBAAoB,CAAC,IAAiB,EAAQ;QACrD,KAAK,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAAA,CACxF;IAEO,KAAK,CAAC,YAAY,CAAC,IAAiB,EAAiB;QAC5D,IACC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE;YACxB,CAAC,IAAI,CAAC,KAAK;YACX,IAAI,CAAC,SAAS;YACd,IAAI,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC;YACzB,IAAI,CAAC,cAAc,GAAG,CAAC;YACvB,CAAC,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,MAAM,CAAC,EACrD,CAAC;YACF,OAAO,IAAI,CAAC,SAAS,CAAC;QACvB,CAAC;QACD,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;YAC7B,IAAI,CAAC;gBACJ,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YAC9B,CAAC;oBAAS,CAAC;gBACV,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,IAAI;oBAAE,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAChF,CAAC;QAAA,CACD,CAAC,EAAE,CAAC;QACL,MAAM,IAAI,CAAC,SAAS,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE;YAAE,IAAI,CAAC,OAAO,CAAC,uBAAuB,EAAE,CAAC;IAAA,CACtE;CACD","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Command, EventEnvelope, SessionMetadata, SessionSnapshot } from \"@earendil-works/pi-protocol\";\nimport type { ByteConnection, ConnectionState } from \"./connection.ts\";\nimport { PiServerError } from \"./errors.ts\";\nimport type { CreateSessionOptions, PiServerService, PiSessionRuntime, PiSessionRuntimeEvent } from \"./types.ts\";\n\ninterface LiveSession {\n\tid: string;\n\truntime: PiSessionRuntime;\n\tconnections: Set<ConnectionState>;\n\tunsubscribe: () => void;\n\toperationCount: number;\n\tready: boolean;\n\tterminal: boolean;\n\tdisposing?: Promise<void>;\n}\n\ninterface LiveSessionManagerOptions {\n\tservice: PiServerService;\n\tisClosing: () => boolean;\n\tsendMessage: (connection: ConnectionState, message: EventEnvelope) => Promise<boolean>;\n\tcloseConnection: (connection: ByteConnection) => Promise<void>;\n\tdisconnect: (connection: ConnectionState) => Promise<void>;\n\tbroadcastServerSnapshot: () => void;\n\treportError: (error: unknown) => void;\n}\n\nfunction toMetadata(snapshot: SessionSnapshot): SessionMetadata {\n\treturn {\n\t\tid: snapshot.id,\n\t\tcreatedAt: snapshot.createdAt,\n\t\tupdatedAt: snapshot.updatedAt,\n\t\tsessionName: snapshot.name,\n\t\tcwd: snapshot.cwd,\n\t};\n}\n\nexport class LiveSessionManager {\n\tprivate readonly options: LiveSessionManagerOptions;\n\tprivate readonly liveSessions = new Map<string, LiveSession>();\n\tprivate readonly openingSessions = new Map<string, Promise<LiveSession>>();\n\n\tconstructor(options: LiveSessionManagerOptions) {\n\t\tthis.options = options;\n\t}\n\n\tasync executeCommand(connection: ConnectionState, command: Command) {\n\t\tswitch (command.command) {\n\t\t\tcase \"list\":\n\t\t\t\treturn { command: \"list\" as const, sessions: await this.listMetadata() };\n\t\t\tcase \"create\": {\n\t\t\t\tconst id = randomUUID();\n\t\t\t\tconst options: CreateSessionOptions = {\n\t\t\t\t\tid,\n\t\t\t\t\tcwd: command.cwd,\n\t\t\t\t\tname: command.name,\n\t\t\t\t\tmodel: command.model,\n\t\t\t\t\tthinkingLevel: command.thinkingLevel,\n\t\t\t\t};\n\t\t\t\tconst live = await this.acquire(id, () => this.options.service.createSession(options));\n\t\t\t\tawait this.attach(connection, live);\n\t\t\t\tconst session = this.forConnection(await this.broadcastSnapshot(live), connection);\n\t\t\t\tthis.options.broadcastServerSnapshot();\n\t\t\t\treturn { command: \"create\" as const, session };\n\t\t\t}\n\t\t\tcase \"attach\": {\n\t\t\t\tconst live = await this.acquire(command.sessionId, () =>\n\t\t\t\t\tthis.options.service.openSession(command.sessionId),\n\t\t\t\t);\n\t\t\t\tawait this.attach(connection, live);\n\t\t\t\tconst session = this.forConnection(await this.broadcastSnapshot(live), connection);\n\t\t\t\tthis.options.broadcastServerSnapshot();\n\t\t\t\treturn { command: \"attach\" as const, session };\n\t\t\t}\n\t\t\tcase \"detach\": {\n\t\t\t\tconst live = this.liveSessions.get(command.sessionId);\n\t\t\t\tif (connection.sessionIds.has(command.sessionId)) {\n\t\t\t\t\tconnection.sessionIds.delete(command.sessionId);\n\t\t\t\t\tif (live) {\n\t\t\t\t\t\tlive.connections.delete(connection);\n\t\t\t\t\t\tif (live.connections.size > 0 && !live.terminal && !live.disposing) {\n\t\t\t\t\t\t\tawait this.broadcastSnapshot(live);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait this.maybeDispose(live);\n\t\t\t\t\t}\n\t\t\t\t\tthis.options.broadcastServerSnapshot();\n\t\t\t\t}\n\t\t\t\treturn { command: \"detach\" as const, sessionId: command.sessionId };\n\t\t\t}\n\t\t\tcase \"prompt\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () =>\n\t\t\t\t\tlive.runtime.prompt({ text: command.text }),\n\t\t\t\t);\n\t\t\t\treturn { command: \"prompt\" as const, session };\n\t\t\t}\n\t\t\tcase \"steer\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () => live.runtime.steer({ text: command.text }));\n\t\t\t\treturn { command: \"steer\" as const, session };\n\t\t\t}\n\t\t\tcase \"abort\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () => live.runtime.abort());\n\t\t\t\treturn { command: \"abort\" as const, session };\n\t\t\t}\n\t\t\tcase \"set_model\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () => live.runtime.setModel(command.model));\n\t\t\t\treturn { command: \"set_model\" as const, session };\n\t\t\t}\n\t\t\tcase \"set_thinking\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () =>\n\t\t\t\t\tlive.runtime.setThinking(command.thinkingLevel),\n\t\t\t\t);\n\t\t\t\treturn { command: \"set_thinking\" as const, session };\n\t\t\t}\n\t\t}\n\t}\n\n\tasync disconnect(connection: ConnectionState): Promise<void> {\n\t\tconst sessions = [...connection.sessionIds]\n\t\t\t.map((id) => this.liveSessions.get(id))\n\t\t\t.filter((live): live is LiveSession => live !== undefined);\n\t\tconnection.sessionIds.clear();\n\t\tfor (const live of sessions) live.connections.delete(connection);\n\t\tconst results = await Promise.allSettled(sessions.map((live) => this.maybeDispose(live)));\n\t\tfor (const result of results) {\n\t\t\tif (result.status === \"rejected\") this.options.reportError(result.reason);\n\t\t}\n\t}\n\n\tasync listMetadata(): Promise<SessionMetadata[]> {\n\t\tconst stored = await this.options.service.listSessions();\n\t\tconst liveSnapshots = await Promise.all(\n\t\t\t[...this.liveSessions.values()]\n\t\t\t\t.filter((live) => !live.disposing)\n\t\t\t\t.map(async (live) => [live.id, await this.normalizedSnapshot(live)] as const),\n\t\t);\n\t\tconst liveById = new Map(liveSnapshots);\n\t\tconst metadata = stored.map((item) => {\n\t\t\tconst snapshot = liveById.get(item.id);\n\t\t\tif (!snapshot) return item;\n\t\t\tliveById.delete(item.id);\n\t\t\treturn { ...item, ...toMetadata(snapshot) };\n\t\t});\n\t\tfor (const snapshot of liveById.values()) metadata.push(toMetadata(snapshot));\n\t\treturn metadata;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tconst openingResults = await Promise.allSettled([...this.openingSessions.values()]);\n\t\tfor (const result of openingResults) {\n\t\t\tif (result.status === \"rejected\") this.options.reportError(result.reason);\n\t\t}\n\t\tconst sessions = [...this.liveSessions.values()];\n\t\tthis.liveSessions.clear();\n\t\tawait Promise.all(\n\t\t\tsessions.map(async (live) => {\n\t\t\t\tif (live.disposing) {\n\t\t\t\t\tawait live.disposing;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlive.unsubscribe();\n\t\t\t\tawait live.runtime.dispose();\n\t\t\t}),\n\t\t);\n\t}\n\n\tprivate async runOperation(\n\t\tconnection: ConnectionState,\n\t\tlive: LiveSession,\n\t\toperation: () => Promise<void>,\n\t): Promise<SessionSnapshot> {\n\t\tlive.operationCount += 1;\n\t\ttry {\n\t\t\tawait operation();\n\t\t\treturn this.forConnection(await this.broadcastSnapshot(live), connection);\n\t\t} finally {\n\t\t\tlive.operationCount -= 1;\n\t\t\tthis.scheduleMaybeDispose(live);\n\t\t}\n\t}\n\n\tprivate async acquire(id: string, acquireRuntime: () => Promise<PiSessionRuntime>): Promise<LiveSession> {\n\t\tfor (;;) {\n\t\t\tconst existing = this.liveSessions.get(id);\n\t\t\tif (existing) {\n\t\t\t\tif (existing.terminal) throw new PiServerError(\"session_locked\", `Session runtime is terminating: ${id}`);\n\t\t\t\tif (existing.disposing) {\n\t\t\t\t\tawait existing.disposing;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\treturn existing;\n\t\t\t}\n\t\t\tconst opening = this.openingSessions.get(id);\n\t\t\tif (opening) return opening;\n\t\t\tconst pending = this.create(id, acquireRuntime);\n\t\t\tthis.openingSessions.set(id, pending);\n\t\t\ttry {\n\t\t\t\treturn await pending;\n\t\t\t} finally {\n\t\t\t\tif (this.openingSessions.get(id) === pending) this.openingSessions.delete(id);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async create(id: string, acquireRuntime: () => Promise<PiSessionRuntime>): Promise<LiveSession> {\n\t\tconst runtime = await acquireRuntime();\n\t\tif (this.options.isClosing()) {\n\t\t\tawait runtime.dispose();\n\t\t\tthrow new Error(\"PiServer closed while acquiring a session runtime\");\n\t\t}\n\t\tlet live: LiveSession | undefined;\n\t\ttry {\n\t\t\tconst snapshot = await runtime.snapshot();\n\t\t\tif (snapshot.id !== id) {\n\t\t\t\tthrow new PiServerError(\n\t\t\t\t\t\"invalid_request\",\n\t\t\t\t\t`Service returned session ${snapshot.id} for server-assigned session ${id}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tlive = {\n\t\t\t\tid,\n\t\t\t\truntime,\n\t\t\t\tconnections: new Set(),\n\t\t\t\tunsubscribe: () => {},\n\t\t\t\toperationCount: 0,\n\t\t\t\tready: false,\n\t\t\t\tterminal: false,\n\t\t\t};\n\t\t\tlive.unsubscribe = runtime.subscribe((event) => this.handleRuntimeEvent(live!, event));\n\t\t\tthis.liveSessions.set(id, live);\n\t\t\tlive.ready = true;\n\t\t\treturn live;\n\t\t} catch (error) {\n\t\t\tif (live) live.unsubscribe();\n\t\t\ttry {\n\t\t\t\tawait runtime.dispose();\n\t\t\t} catch (disposeError) {\n\t\t\t\tthis.options.reportError(disposeError);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate handleRuntimeEvent(live: LiveSession, event: PiSessionRuntimeEvent): void {\n\t\tif (event.type === \"error\") {\n\t\t\tvoid this.terminate(live, event.error).catch((error: unknown) => this.options.reportError(error));\n\t\t\treturn;\n\t\t}\n\t\tif (event.type === \"progress\") {\n\t\t\tconst envelope: EventEnvelope = {\n\t\t\t\ttype: \"event\",\n\t\t\t\tevent: { type: \"session_progress\", sessionId: live.id, progress: event.progress },\n\t\t\t};\n\t\t\tfor (const connection of live.connections) void this.options.sendMessage(connection, envelope);\n\t\t} else {\n\t\t\tvoid this.broadcastSnapshot(live).catch((error: unknown) => this.options.reportError(error));\n\t\t}\n\t\tthis.scheduleMaybeDispose(live);\n\t}\n\n\tprivate async terminate(live: LiveSession, error: PiServerError): Promise<void> {\n\t\tif (live.terminal) return;\n\t\tlive.terminal = true;\n\t\tthis.options.reportError(error);\n\t\tlive.unsubscribe();\n\t\tconst connections = [...live.connections];\n\t\tawait Promise.all(connections.map((connection) => this.options.closeConnection(connection.connection)));\n\t\tawait Promise.all(connections.map((connection) => this.options.disconnect(connection)));\n\t\tawait this.maybeDispose(live);\n\t}\n\n\tprivate async normalizedSnapshot(live: LiveSession): Promise<SessionSnapshot> {\n\t\tconst snapshot = await live.runtime.snapshot();\n\t\tif (snapshot.id !== live.id) {\n\t\t\tthrow new PiServerError(\"invalid_request\", `Runtime session ID changed from ${live.id} to ${snapshot.id}`);\n\t\t}\n\t\treturn {\n\t\t\t...snapshot,\n\t\t\tphase: live.runtime.getPhase(),\n\t\t\tattached: live.connections.size > 0,\n\t\t\tlocked: true,\n\t\t};\n\t}\n\n\tprivate forConnection(snapshot: SessionSnapshot, connection: ConnectionState): SessionSnapshot {\n\t\treturn { ...snapshot, attached: connection.sessionIds.has(snapshot.id) };\n\t}\n\n\tprivate async broadcastSnapshot(live: LiveSession): Promise<SessionSnapshot> {\n\t\tconst snapshot = await this.normalizedSnapshot(live);\n\t\tconst envelope: EventEnvelope = { type: \"event\", event: { type: \"session_snapshot\", snapshot } };\n\t\tfor (const connection of live.connections) void this.options.sendMessage(connection, envelope);\n\t\treturn snapshot;\n\t}\n\n\tprivate async attach(connection: ConnectionState, live: LiveSession): Promise<void> {\n\t\tif (connection.disconnected || connection.stage !== \"ready\" || connection.connection.closed) {\n\t\t\tawait this.maybeDispose(live);\n\t\t\tthrow new PiServerError(\"invalid_request\", \"Connection closed while attaching to a session\");\n\t\t}\n\t\tconnection.sessionIds.add(live.id);\n\t\tlive.connections.add(connection);\n\t}\n\n\tprivate requireAttached(connection: ConnectionState, sessionId: string): LiveSession {\n\t\tif (!connection.sessionIds.has(sessionId)) {\n\t\t\tthrow new PiServerError(\"invalid_request\", `Connection is not attached to session ${sessionId}`);\n\t\t}\n\t\tconst live = this.liveSessions.get(sessionId);\n\t\tif (!live || live.terminal || live.disposing) {\n\t\t\tthrow new PiServerError(\"not_found\", `Session is not live: ${sessionId}`);\n\t\t}\n\t\treturn live;\n\t}\n\n\tprivate scheduleMaybeDispose(live: LiveSession): void {\n\t\tvoid this.maybeDispose(live).catch((error: unknown) => this.options.reportError(error));\n\t}\n\n\tprivate async maybeDispose(live: LiveSession): Promise<void> {\n\t\tif (\n\t\t\tthis.options.isClosing() ||\n\t\t\t!live.ready ||\n\t\t\tlive.disposing ||\n\t\t\tlive.connections.size > 0 ||\n\t\t\tlive.operationCount > 0 ||\n\t\t\t(!live.terminal && live.runtime.getPhase() !== \"idle\")\n\t\t) {\n\t\t\treturn live.disposing;\n\t\t}\n\t\tlive.unsubscribe();\n\t\tlive.disposing = (async () => {\n\t\t\ttry {\n\t\t\t\tawait live.runtime.dispose();\n\t\t\t} finally {\n\t\t\t\tif (this.liveSessions.get(live.id) === live) this.liveSessions.delete(live.id);\n\t\t\t}\n\t\t})();\n\t\tawait live.disposing;\n\t\tif (!this.options.isClosing()) this.options.broadcastServerSnapshot();\n\t}\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type EventEnvelope, type ModelMetadata, type ServerSnapshot, type SessionMetadata } from "@earendil-works/pi-protocol";
|
|
2
|
+
import type { ConnectionState } from "./connection.ts";
|
|
3
|
+
import type { PiServerService } from "./types.ts";
|
|
4
|
+
interface ServerSnapshotPublisherOptions {
|
|
5
|
+
serverId: string;
|
|
6
|
+
service: PiServerService;
|
|
7
|
+
connections: Set<ConnectionState>;
|
|
8
|
+
isClosing: () => boolean;
|
|
9
|
+
listSessions: () => Promise<SessionMetadata[]>;
|
|
10
|
+
sendMessage: (connection: ConnectionState, message: EventEnvelope) => Promise<boolean>;
|
|
11
|
+
reportError: (error: unknown) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare class ServerSnapshotPublisher {
|
|
14
|
+
private readonly options;
|
|
15
|
+
private revision;
|
|
16
|
+
private broadcastQueue;
|
|
17
|
+
constructor(options: ServerSnapshotPublisherOptions);
|
|
18
|
+
get currentRevision(): number;
|
|
19
|
+
get(models?: ModelMetadata[]): Promise<ServerSnapshot>;
|
|
20
|
+
broadcast(): Promise<void>;
|
|
21
|
+
private performBroadcast;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=snapshots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshots.d.ts","sourceRoot":"","sources":["../src/snapshots.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,aAAa,EAElB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,UAAU,8BAA8B;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,eAAe,CAAC;IACzB,WAAW,EAAE,GAAG,CAAC,eAAe,CAAC,CAAC;IAClC,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,YAAY,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAC/C,WAAW,EAAE,CAAC,UAAU,EAAE,eAAe,EAAE,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvF,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACtC;AAED,qBAAa,uBAAuB;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,cAAc,CAAoC;IAE1D,YAAY,OAAO,EAAE,8BAA8B,EAElD;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAEK,GAAG,CAAC,MAAM,CAAC,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAQ3D;IAED,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAIzB;YAEa,gBAAgB;CAY9B","sourcesContent":["import {\n\ttype EventEnvelope,\n\ttype ModelMetadata,\n\tPROTOCOL_VERSION,\n\ttype ServerSnapshot,\n\ttype SessionMetadata,\n} from \"@earendil-works/pi-protocol\";\nimport type { ConnectionState } from \"./connection.ts\";\nimport type { PiServerService } from \"./types.ts\";\n\ninterface ServerSnapshotPublisherOptions {\n\tserverId: string;\n\tservice: PiServerService;\n\tconnections: Set<ConnectionState>;\n\tisClosing: () => boolean;\n\tlistSessions: () => Promise<SessionMetadata[]>;\n\tsendMessage: (connection: ConnectionState, message: EventEnvelope) => Promise<boolean>;\n\treportError: (error: unknown) => void;\n}\n\nexport class ServerSnapshotPublisher {\n\tprivate readonly options: ServerSnapshotPublisherOptions;\n\tprivate revision = 0;\n\tprivate broadcastQueue: Promise<void> = Promise.resolve();\n\n\tconstructor(options: ServerSnapshotPublisherOptions) {\n\t\tthis.options = options;\n\t}\n\n\tget currentRevision(): number {\n\t\treturn this.revision;\n\t}\n\n\tasync get(models?: ModelMetadata[]): Promise<ServerSnapshot> {\n\t\treturn {\n\t\t\tserverId: this.options.serverId,\n\t\t\tprotocolVersion: PROTOCOL_VERSION,\n\t\t\trevision: this.revision,\n\t\t\tsessions: await this.options.listSessions(),\n\t\t\tmodels: models ?? (await this.options.service.listModels()),\n\t\t};\n\t}\n\n\tbroadcast(): Promise<void> {\n\t\tconst broadcast = this.broadcastQueue.then(() => this.performBroadcast());\n\t\tthis.broadcastQueue = broadcast.catch((error: unknown) => this.options.reportError(error));\n\t\treturn broadcast;\n\t}\n\n\tprivate async performBroadcast(): Promise<void> {\n\t\tconst readyConnections = [...this.options.connections].filter(\n\t\t\t(connection) => connection.stage === \"ready\" && !connection.disconnected,\n\t\t);\n\t\tif (readyConnections.length === 0 || this.options.isClosing()) return;\n\t\tconst revision = ++this.revision;\n\t\tconst models = await this.options.service.listModels();\n\t\tconst current = await this.get(models);\n\t\tconst snapshot: ServerSnapshot = { ...current, revision };\n\t\tconst envelope: EventEnvelope = { type: \"event\", event: { type: \"server_snapshot\", snapshot } };\n\t\tfor (const connection of readyConnections) await this.options.sendMessage(connection, envelope);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION, } from "@earendil-works/pi-protocol";
|
|
2
|
+
export class ServerSnapshotPublisher {
|
|
3
|
+
options;
|
|
4
|
+
revision = 0;
|
|
5
|
+
broadcastQueue = Promise.resolve();
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
get currentRevision() {
|
|
10
|
+
return this.revision;
|
|
11
|
+
}
|
|
12
|
+
async get(models) {
|
|
13
|
+
return {
|
|
14
|
+
serverId: this.options.serverId,
|
|
15
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
16
|
+
revision: this.revision,
|
|
17
|
+
sessions: await this.options.listSessions(),
|
|
18
|
+
models: models ?? (await this.options.service.listModels()),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
broadcast() {
|
|
22
|
+
const broadcast = this.broadcastQueue.then(() => this.performBroadcast());
|
|
23
|
+
this.broadcastQueue = broadcast.catch((error) => this.options.reportError(error));
|
|
24
|
+
return broadcast;
|
|
25
|
+
}
|
|
26
|
+
async performBroadcast() {
|
|
27
|
+
const readyConnections = [...this.options.connections].filter((connection) => connection.stage === "ready" && !connection.disconnected);
|
|
28
|
+
if (readyConnections.length === 0 || this.options.isClosing())
|
|
29
|
+
return;
|
|
30
|
+
const revision = ++this.revision;
|
|
31
|
+
const models = await this.options.service.listModels();
|
|
32
|
+
const current = await this.get(models);
|
|
33
|
+
const snapshot = { ...current, revision };
|
|
34
|
+
const envelope = { type: "event", event: { type: "server_snapshot", snapshot } };
|
|
35
|
+
for (const connection of readyConnections)
|
|
36
|
+
await this.options.sendMessage(connection, envelope);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=snapshots.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshots.js","sourceRoot":"","sources":["../src/snapshots.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,gBAAgB,GAGhB,MAAM,6BAA6B,CAAC;AAcrC,MAAM,OAAO,uBAAuB;IAClB,OAAO,CAAiC;IACjD,QAAQ,GAAG,CAAC,CAAC;IACb,cAAc,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAE1D,YAAY,OAAuC,EAAE;QACpD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAAA,CACvB;IAED,IAAI,eAAe,GAAW;QAC7B,OAAO,IAAI,CAAC,QAAQ,CAAC;IAAA,CACrB;IAED,KAAK,CAAC,GAAG,CAAC,MAAwB,EAA2B;QAC5D,OAAO;YACN,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ;YAC/B,eAAe,EAAE,gBAAgB;YACjC,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,QAAQ,EAAE,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE;YAC3C,MAAM,EAAE,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;SAC3D,CAAC;IAAA,CACF;IAED,SAAS,GAAkB;QAC1B,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC;QAC1E,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3F,OAAO,SAAS,CAAC;IAAA,CACjB;IAEO,KAAK,CAAC,gBAAgB,GAAkB;QAC/C,MAAM,gBAAgB,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,MAAM,CAC5D,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,KAAK,OAAO,IAAI,CAAC,UAAU,CAAC,YAAY,CACxE,CAAC;QACF,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE;YAAE,OAAO;QACtE,MAAM,QAAQ,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC;QACjC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;QACvD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,QAAQ,GAAmB,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,CAAC;QAC1D,MAAM,QAAQ,GAAkB,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,EAAE,CAAC;QAChG,KAAK,MAAM,UAAU,IAAI,gBAAgB;YAAE,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAAA,CAChG;CACD","sourcesContent":["import {\n\ttype EventEnvelope,\n\ttype ModelMetadata,\n\tPROTOCOL_VERSION,\n\ttype ServerSnapshot,\n\ttype SessionMetadata,\n} from \"@earendil-works/pi-protocol\";\nimport type { ConnectionState } from \"./connection.ts\";\nimport type { PiServerService } from \"./types.ts\";\n\ninterface ServerSnapshotPublisherOptions {\n\tserverId: string;\n\tservice: PiServerService;\n\tconnections: Set<ConnectionState>;\n\tisClosing: () => boolean;\n\tlistSessions: () => Promise<SessionMetadata[]>;\n\tsendMessage: (connection: ConnectionState, message: EventEnvelope) => Promise<boolean>;\n\treportError: (error: unknown) => void;\n}\n\nexport class ServerSnapshotPublisher {\n\tprivate readonly options: ServerSnapshotPublisherOptions;\n\tprivate revision = 0;\n\tprivate broadcastQueue: Promise<void> = Promise.resolve();\n\n\tconstructor(options: ServerSnapshotPublisherOptions) {\n\t\tthis.options = options;\n\t}\n\n\tget currentRevision(): number {\n\t\treturn this.revision;\n\t}\n\n\tasync get(models?: ModelMetadata[]): Promise<ServerSnapshot> {\n\t\treturn {\n\t\t\tserverId: this.options.serverId,\n\t\t\tprotocolVersion: PROTOCOL_VERSION,\n\t\t\trevision: this.revision,\n\t\t\tsessions: await this.options.listSessions(),\n\t\t\tmodels: models ?? (await this.options.service.listModels()),\n\t\t};\n\t}\n\n\tbroadcast(): Promise<void> {\n\t\tconst broadcast = this.broadcastQueue.then(() => this.performBroadcast());\n\t\tthis.broadcastQueue = broadcast.catch((error: unknown) => this.options.reportError(error));\n\t\treturn broadcast;\n\t}\n\n\tprivate async performBroadcast(): Promise<void> {\n\t\tconst readyConnections = [...this.options.connections].filter(\n\t\t\t(connection) => connection.stage === \"ready\" && !connection.disconnected,\n\t\t);\n\t\tif (readyConnections.length === 0 || this.options.isClosing()) return;\n\t\tconst revision = ++this.revision;\n\t\tconst models = await this.options.service.listModels();\n\t\tconst current = await this.get(models);\n\t\tconst snapshot: ServerSnapshot = { ...current, revision };\n\t\tconst envelope: EventEnvelope = { type: \"event\", event: { type: \"server_snapshot\", snapshot } };\n\t\tfor (const connection of readyConnections) await this.options.sendMessage(connection, envelope);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ClientMessage, type Command, type ResponseEnvelope, type ServerMessage } from "@earendil-works/pi-protocol";
|
|
2
|
+
export interface WireChannel {
|
|
3
|
+
send(chunk: Uint8Array): Promise<void>;
|
|
4
|
+
sendFragmented(chunk: Uint8Array, splitAt: number): Promise<void>;
|
|
5
|
+
close(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare class ProtocolTestClient {
|
|
8
|
+
readonly messages: ServerMessage[];
|
|
9
|
+
private readonly channel;
|
|
10
|
+
private readonly decoder;
|
|
11
|
+
private readonly waiters;
|
|
12
|
+
private readonly closedDeferred;
|
|
13
|
+
private requestSequence;
|
|
14
|
+
private closedValue;
|
|
15
|
+
constructor(channel: WireChannel);
|
|
16
|
+
get closed(): boolean;
|
|
17
|
+
hello(version?: number): Promise<ServerMessage>;
|
|
18
|
+
request(command: Command, id?: string): Promise<ResponseEnvelope>;
|
|
19
|
+
sendMessage(message: ClientMessage): Promise<void>;
|
|
20
|
+
sendBytes(chunk: Uint8Array): Promise<void>;
|
|
21
|
+
sendFragmentedMessage(message: ClientMessage, splitAt: number): Promise<void>;
|
|
22
|
+
next(predicate: (message: ServerMessage) => boolean): Promise<ServerMessage>;
|
|
23
|
+
nextFrom(index: number, predicate: (message: ServerMessage) => boolean): Promise<ServerMessage>;
|
|
24
|
+
waitForClose(): Promise<void>;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
receive(chunk: Uint8Array): void;
|
|
27
|
+
markClosed(): void;
|
|
28
|
+
fail(error: Error): void;
|
|
29
|
+
}
|
|
30
|
+
export declare function connectUnixTestClient(path: string): Promise<ProtocolTestClient>;
|
|
31
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/testing/client.ts"],"names":[],"mappings":"AAEA,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,OAAO,EAGZ,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAElB,MAAM,6BAA6B,CAAC;AASrC,MAAM,WAAW,WAAW;IAC3B,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,qBAAa,kBAAkB;IAC9B,QAAQ,CAAC,QAAQ,EAAE,aAAa,EAAE,CAAM;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8B;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwB;IACvD,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,WAAW,CAAS;IAE5B,YAAY,OAAO,EAAE,WAAW,EAE/B;IAED,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,KAAK,CAAC,OAAO,GAAE,MAAyB,GAAG,OAAO,CAAC,aAAa,CAAC,CAIhE;IAEK,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,SAAsC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAMnG;IAED,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAEjD;IAED,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1C;IAED,qBAAqB,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5E;IAED,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAE3E;IAED,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAK9F;IAED,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAE5B;IAED,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAErB;IAED,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAa/B;IAED,UAAU,IAAI,IAAI,CAKjB;IAED,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAGvB;CACD;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsBrF","sourcesContent":["import { once } from \"node:events\";\nimport { createConnection, type Socket } from \"node:net\";\nimport {\n\ttype ClientMessage,\n\ttype Command,\n\tencodeClientMessage,\n\tPROTOCOL_VERSION,\n\ttype ResponseEnvelope,\n\ttype ServerMessage,\n\tServerMessageDecoder,\n} from \"@earendil-works/pi-protocol\";\nimport { Deferred } from \"./service.ts\";\n\ninterface MessageWaiter {\n\tpredicate: (message: ServerMessage) => boolean;\n\tresolve: (message: ServerMessage) => void;\n\treject: (error: Error) => void;\n}\n\nexport interface WireChannel {\n\tsend(chunk: Uint8Array): Promise<void>;\n\tsendFragmented(chunk: Uint8Array, splitAt: number): Promise<void>;\n\tclose(): Promise<void>;\n}\n\nexport class ProtocolTestClient {\n\treadonly messages: ServerMessage[] = [];\n\tprivate readonly channel: WireChannel;\n\tprivate readonly decoder = new ServerMessageDecoder();\n\tprivate readonly waiters = new Set<MessageWaiter>();\n\tprivate readonly closedDeferred = new Deferred<void>();\n\tprivate requestSequence = 0;\n\tprivate closedValue = false;\n\n\tconstructor(channel: WireChannel) {\n\t\tthis.channel = channel;\n\t}\n\n\tget closed(): boolean {\n\t\treturn this.closedValue;\n\t}\n\n\thello(version: number = PROTOCOL_VERSION): Promise<ServerMessage> {\n\t\tconst response = this.next((message) => message.type === \"hello\" || message.type === \"hello_error\");\n\t\tvoid this.sendMessage({ type: \"hello\", version });\n\t\treturn response;\n\t}\n\n\tasync request(command: Command, id = `request-${++this.requestSequence}`): Promise<ResponseEnvelope> {\n\t\tconst response = this.next(\n\t\t\t(message): message is ResponseEnvelope => message.type === \"response\" && message.id === id,\n\t\t);\n\t\tawait this.sendMessage({ type: \"request\", id, request: command });\n\t\treturn (await response) as ResponseEnvelope;\n\t}\n\n\tsendMessage(message: ClientMessage): Promise<void> {\n\t\treturn this.channel.send(encodeClientMessage(message));\n\t}\n\n\tsendBytes(chunk: Uint8Array): Promise<void> {\n\t\treturn this.channel.send(chunk);\n\t}\n\n\tsendFragmentedMessage(message: ClientMessage, splitAt: number): Promise<void> {\n\t\treturn this.channel.sendFragmented(encodeClientMessage(message), splitAt);\n\t}\n\n\tnext(predicate: (message: ServerMessage) => boolean): Promise<ServerMessage> {\n\t\treturn this.nextFrom(0, predicate);\n\t}\n\n\tnextFrom(index: number, predicate: (message: ServerMessage) => boolean): Promise<ServerMessage> {\n\t\tconst existing = this.messages.slice(index).find(predicate);\n\t\tif (existing) return Promise.resolve(existing);\n\t\tif (this.closedValue) return Promise.reject(new Error(\"Wire client is closed\"));\n\t\treturn new Promise((resolve, reject) => this.waiters.add({ predicate, resolve, reject }));\n\t}\n\n\twaitForClose(): Promise<void> {\n\t\treturn this.closedValue ? Promise.resolve() : this.closedDeferred.promise;\n\t}\n\n\tclose(): Promise<void> {\n\t\treturn this.channel.close();\n\t}\n\n\treceive(chunk: Uint8Array): void {\n\t\ttry {\n\t\t\tfor (const message of this.decoder.push(chunk)) {\n\t\t\t\tthis.messages.push(message);\n\t\t\t\tfor (const waiter of this.waiters) {\n\t\t\t\t\tif (!waiter.predicate(message)) continue;\n\t\t\t\t\tthis.waiters.delete(waiter);\n\t\t\t\t\twaiter.resolve(message);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.fail(error instanceof Error ? error : new Error(String(error)));\n\t\t}\n\t}\n\n\tmarkClosed(): void {\n\t\tif (this.closedValue) return;\n\t\tthis.closedValue = true;\n\t\tthis.closedDeferred.resolve(undefined);\n\t\tthis.fail(new Error(\"Wire connection closed\"));\n\t}\n\n\tfail(error: Error): void {\n\t\tfor (const waiter of this.waiters) waiter.reject(error);\n\t\tthis.waiters.clear();\n\t}\n}\n\nexport async function connectUnixTestClient(path: string): Promise<ProtocolTestClient> {\n\tconst socket = createConnection(path);\n\tawait once(socket, \"connect\");\n\tconst client = new ProtocolTestClient({\n\t\tsend: (chunk) => writeSocket(socket, chunk),\n\t\tasync sendFragmented(chunk, splitAt) {\n\t\t\tawait writeSocket(socket, chunk.subarray(0, splitAt));\n\t\t\tawait writeSocket(socket, chunk.subarray(splitAt));\n\t\t},\n\t\tasync close() {\n\t\t\tif (socket.destroyed) return;\n\t\t\tconst closed = once(socket, \"close\");\n\t\t\tsocket.destroy();\n\t\t\tawait closed;\n\t\t},\n\t});\n\tsocket.on(\"data\", (chunk) => {\n\t\tclient.receive(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));\n\t});\n\tsocket.on(\"error\", (error) => client.fail(error));\n\tsocket.once(\"close\", () => client.markClosed());\n\treturn client;\n}\n\nfunction writeSocket(socket: Socket, chunk: Uint8Array): Promise<void> {\n\treturn new Promise<void>((resolve, reject) => {\n\t\tsocket.write(chunk, (error) => {\n\t\t\tif (error) reject(error);\n\t\t\telse resolve();\n\t\t});\n\t});\n}\n"]}
|