@llui/agent 0.0.30 → 0.0.31

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.
Files changed (93) hide show
  1. package/dist/server/cloudflare/durable-object.d.ts +58 -0
  2. package/dist/server/cloudflare/durable-object.d.ts.map +1 -0
  3. package/dist/server/cloudflare/durable-object.js +33 -0
  4. package/dist/server/cloudflare/durable-object.js.map +1 -0
  5. package/dist/server/cloudflare/index.d.ts +49 -0
  6. package/dist/server/cloudflare/index.d.ts.map +1 -0
  7. package/dist/server/cloudflare/index.js +49 -0
  8. package/dist/server/cloudflare/index.js.map +1 -0
  9. package/dist/server/cloudflare/worker.d.ts +40 -0
  10. package/dist/server/cloudflare/worker.d.ts.map +1 -0
  11. package/dist/server/cloudflare/worker.js +55 -0
  12. package/dist/server/cloudflare/worker.js.map +1 -0
  13. package/dist/server/core-entry.d.ts +27 -0
  14. package/dist/server/core-entry.d.ts.map +1 -0
  15. package/dist/server/core-entry.js +19 -0
  16. package/dist/server/core-entry.js.map +1 -0
  17. package/dist/server/core.d.ts +78 -0
  18. package/dist/server/core.d.ts.map +1 -0
  19. package/dist/server/core.js +84 -0
  20. package/dist/server/core.js.map +1 -0
  21. package/dist/server/factory.d.ts +5 -3
  22. package/dist/server/factory.d.ts.map +1 -1
  23. package/dist/server/factory.js +18 -58
  24. package/dist/server/factory.js.map +1 -1
  25. package/dist/server/http/mint.d.ts.map +1 -1
  26. package/dist/server/http/mint.js +2 -3
  27. package/dist/server/http/mint.js.map +1 -1
  28. package/dist/server/http/resume.js +1 -1
  29. package/dist/server/http/resume.js.map +1 -1
  30. package/dist/server/identity.d.ts +5 -1
  31. package/dist/server/identity.d.ts.map +1 -1
  32. package/dist/server/identity.js +49 -11
  33. package/dist/server/identity.js.map +1 -1
  34. package/dist/server/index.d.ts +16 -1
  35. package/dist/server/index.d.ts.map +1 -1
  36. package/dist/server/index.js +13 -1
  37. package/dist/server/index.js.map +1 -1
  38. package/dist/server/lap/confirm-result.d.ts +2 -2
  39. package/dist/server/lap/confirm-result.d.ts.map +1 -1
  40. package/dist/server/lap/confirm-result.js +1 -1
  41. package/dist/server/lap/confirm-result.js.map +1 -1
  42. package/dist/server/lap/describe.d.ts +4 -4
  43. package/dist/server/lap/describe.d.ts.map +1 -1
  44. package/dist/server/lap/describe.js +4 -4
  45. package/dist/server/lap/describe.js.map +1 -1
  46. package/dist/server/lap/forward.d.ts +2 -2
  47. package/dist/server/lap/forward.d.ts.map +1 -1
  48. package/dist/server/lap/forward.js +1 -1
  49. package/dist/server/lap/forward.js.map +1 -1
  50. package/dist/server/lap/message.d.ts +2 -2
  51. package/dist/server/lap/message.d.ts.map +1 -1
  52. package/dist/server/lap/message.js +1 -1
  53. package/dist/server/lap/message.js.map +1 -1
  54. package/dist/server/lap/observe.d.ts +2 -2
  55. package/dist/server/lap/observe.d.ts.map +1 -1
  56. package/dist/server/lap/observe.js +1 -1
  57. package/dist/server/lap/observe.js.map +1 -1
  58. package/dist/server/lap/wait.d.ts +2 -2
  59. package/dist/server/lap/wait.d.ts.map +1 -1
  60. package/dist/server/lap/wait.js +1 -1
  61. package/dist/server/lap/wait.js.map +1 -1
  62. package/dist/server/options.d.ts +25 -1
  63. package/dist/server/options.d.ts.map +1 -1
  64. package/dist/server/options.js.map +1 -1
  65. package/dist/server/token.d.ts +7 -3
  66. package/dist/server/token.d.ts.map +1 -1
  67. package/dist/server/token.js +66 -26
  68. package/dist/server/token.js.map +1 -1
  69. package/dist/server/web/adapter.d.ts +16 -0
  70. package/dist/server/web/adapter.d.ts.map +1 -0
  71. package/dist/server/web/adapter.js +45 -0
  72. package/dist/server/web/adapter.js.map +1 -0
  73. package/dist/server/web/index.d.ts +12 -0
  74. package/dist/server/web/index.d.ts.map +1 -0
  75. package/dist/server/web/index.js +12 -0
  76. package/dist/server/web/index.js.map +1 -0
  77. package/dist/server/web/upgrade.d.ts +41 -0
  78. package/dist/server/web/upgrade.d.ts.map +1 -0
  79. package/dist/server/web/upgrade.js +96 -0
  80. package/dist/server/web/upgrade.js.map +1 -0
  81. package/dist/server/ws/pairing-registry.d.ts +84 -21
  82. package/dist/server/ws/pairing-registry.d.ts.map +1 -1
  83. package/dist/server/ws/pairing-registry.js +89 -151
  84. package/dist/server/ws/pairing-registry.js.map +1 -1
  85. package/dist/server/ws/rpc.d.ts +39 -0
  86. package/dist/server/ws/rpc.d.ts.map +1 -0
  87. package/dist/server/ws/rpc.js +126 -0
  88. package/dist/server/ws/rpc.js.map +1 -0
  89. package/dist/server/ws/upgrade.d.ts +3 -3
  90. package/dist/server/ws/upgrade.d.ts.map +1 -1
  91. package/dist/server/ws/upgrade.js +2 -2
  92. package/dist/server/ws/upgrade.js.map +1 -1
  93. package/package.json +13 -1
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Web-runtime adapters. Use this sub-path from Cloudflare Workers,
3
+ * Deno, Bun, or any other runtime that speaks WHATWG `Request` /
4
+ * `Response` and exposes native WebSocket upgrade primitives.
5
+ *
6
+ * Pair with `@llui/agent/server/core`'s `createLluiAgentCore` — that
7
+ * builds the runtime-neutral router and registry; the handlers
8
+ * exported here handle the WebSocket upgrade half.
9
+ */
10
+ export { createWHATWGPairingConnection } from './adapter.js';
11
+ export { handleCloudflareUpgrade, handleDenoUpgrade, extractToken } from './upgrade.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/web/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,6BAA6B,EAAE,MAAM,cAAc,CAAA;AAC5D,OAAO,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Web-runtime adapters. Use this sub-path from Cloudflare Workers,
3
+ * Deno, Bun, or any other runtime that speaks WHATWG `Request` /
4
+ * `Response` and exposes native WebSocket upgrade primitives.
5
+ *
6
+ * Pair with `@llui/agent/server/core`'s `createLluiAgentCore` — that
7
+ * builds the runtime-neutral router and registry; the handlers
8
+ * exported here handle the WebSocket upgrade half.
9
+ */
10
+ export { createWHATWGPairingConnection } from './adapter.js';
11
+ export { handleCloudflareUpgrade, handleDenoUpgrade, extractToken } from './upgrade.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/web/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,6BAA6B,EAAE,MAAM,cAAc,CAAA;AAC5D,OAAO,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA","sourcesContent":["/**\n * Web-runtime adapters. Use this sub-path from Cloudflare Workers,\n * Deno, Bun, or any other runtime that speaks WHATWG `Request` /\n * `Response` and exposes native WebSocket upgrade primitives.\n *\n * Pair with `@llui/agent/server/core`'s `createLluiAgentCore` — that\n * builds the runtime-neutral router and registry; the handlers\n * exported here handle the WebSocket upgrade half.\n */\nexport { createWHATWGPairingConnection } from './adapter.js'\nexport { handleCloudflareUpgrade, handleDenoUpgrade, extractToken } from './upgrade.js'\n"]}
@@ -0,0 +1,41 @@
1
+ import type { AgentCoreHandle } from '../core.js';
2
+ /**
3
+ * Extract the bearer token from a LAP WebSocket upgrade request.
4
+ * Accepts the token on either `?token=` or `Authorization: Bearer` —
5
+ * query-string is the common pattern because browsers can't set
6
+ * arbitrary headers on WebSocket construction.
7
+ */
8
+ export declare function extractToken(req: Request): string | null;
9
+ /**
10
+ * Cloudflare Workers handler. Accepts a WebSocket upgrade using
11
+ * `WebSocketPair`, validates the token via
12
+ * `agent.acceptConnection`, and returns the 101 upgrade Response.
13
+ *
14
+ * Usage:
15
+ * ```ts
16
+ * const agent = createLluiAgentCore({ signingKey: env.AGENT_KEY })
17
+ * export default {
18
+ * async fetch(req, env) {
19
+ * const url = new URL(req.url)
20
+ * if (url.pathname === '/agent/ws') return handleCloudflareUpgrade(req, agent)
21
+ * return (await agent.router(req)) ?? new Response('Not Found', { status: 404 })
22
+ * },
23
+ * }
24
+ * ```
25
+ */
26
+ export declare function handleCloudflareUpgrade(req: Request, agent: AgentCoreHandle): Promise<Response>;
27
+ /**
28
+ * Deno handler. Uses `Deno.upgradeWebSocket(req)` to produce the
29
+ * response + socket pair, then plugs the socket into the registry.
30
+ *
31
+ * Usage:
32
+ * ```ts
33
+ * Deno.serve(async (req) => {
34
+ * const url = new URL(req.url)
35
+ * if (url.pathname === '/agent/ws') return handleDenoUpgrade(req, agent)
36
+ * return (await agent.router(req)) ?? new Response('Not Found', { status: 404 })
37
+ * })
38
+ * ```
39
+ */
40
+ export declare function handleDenoUpgrade(req: Request, agent: AgentCoreHandle): Promise<Response>;
41
+ //# sourceMappingURL=upgrade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../../src/server/web/upgrade.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAGjD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAOxD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,QAAQ,CAAC,CAmCnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CA+B/F"}
@@ -0,0 +1,96 @@
1
+ import { createWHATWGPairingConnection } from './adapter.js';
2
+ /**
3
+ * Extract the bearer token from a LAP WebSocket upgrade request.
4
+ * Accepts the token on either `?token=` or `Authorization: Bearer` —
5
+ * query-string is the common pattern because browsers can't set
6
+ * arbitrary headers on WebSocket construction.
7
+ */
8
+ export function extractToken(req) {
9
+ const url = new URL(req.url);
10
+ const q = url.searchParams.get('token');
11
+ if (q)
12
+ return q;
13
+ const auth = req.headers.get('authorization');
14
+ if (auth?.startsWith('Bearer '))
15
+ return auth.slice('Bearer '.length);
16
+ return null;
17
+ }
18
+ /**
19
+ * Cloudflare Workers handler. Accepts a WebSocket upgrade using
20
+ * `WebSocketPair`, validates the token via
21
+ * `agent.acceptConnection`, and returns the 101 upgrade Response.
22
+ *
23
+ * Usage:
24
+ * ```ts
25
+ * const agent = createLluiAgentCore({ signingKey: env.AGENT_KEY })
26
+ * export default {
27
+ * async fetch(req, env) {
28
+ * const url = new URL(req.url)
29
+ * if (url.pathname === '/agent/ws') return handleCloudflareUpgrade(req, agent)
30
+ * return (await agent.router(req)) ?? new Response('Not Found', { status: 404 })
31
+ * },
32
+ * }
33
+ * ```
34
+ */
35
+ export async function handleCloudflareUpgrade(req, agent) {
36
+ if (req.headers.get('upgrade') !== 'websocket') {
37
+ return new Response('Expected upgrade: websocket', { status: 426 });
38
+ }
39
+ const token = extractToken(req);
40
+ if (!token)
41
+ return new Response('Unauthorized', { status: 401 });
42
+ // `WebSocketPair` is a Cloudflare Workers global. We reference it
43
+ // through `globalThis` so importing this module in non-CF runtimes
44
+ // (e.g. during type-checking on Node) doesn't crash.
45
+ const Pair = globalThis.WebSocketPair;
46
+ if (!Pair) {
47
+ return new Response('WebSocketPair unavailable in this runtime', { status: 501 });
48
+ }
49
+ const pair = new Pair();
50
+ const client = pair[0];
51
+ const server = pair[1];
52
+ server.accept();
53
+ const conn = createWHATWGPairingConnection(server);
54
+ const result = await agent.acceptConnection(token, conn);
55
+ if (!result.ok) {
56
+ conn.close();
57
+ return new Response(result.code, { status: result.status });
58
+ }
59
+ // `webSocket` on ResponseInit is Cloudflare-specific; cast to satisfy
60
+ // the standard lib types.
61
+ return new Response(null, { status: 101, webSocket: client });
62
+ }
63
+ /**
64
+ * Deno handler. Uses `Deno.upgradeWebSocket(req)` to produce the
65
+ * response + socket pair, then plugs the socket into the registry.
66
+ *
67
+ * Usage:
68
+ * ```ts
69
+ * Deno.serve(async (req) => {
70
+ * const url = new URL(req.url)
71
+ * if (url.pathname === '/agent/ws') return handleDenoUpgrade(req, agent)
72
+ * return (await agent.router(req)) ?? new Response('Not Found', { status: 404 })
73
+ * })
74
+ * ```
75
+ */
76
+ export async function handleDenoUpgrade(req, agent) {
77
+ const token = extractToken(req);
78
+ if (!token)
79
+ return new Response('Unauthorized', { status: 401 });
80
+ const Deno_ = globalThis.Deno;
81
+ if (!Deno_) {
82
+ return new Response('Deno.upgradeWebSocket unavailable in this runtime', { status: 501 });
83
+ }
84
+ const { socket, response } = Deno_.upgradeWebSocket(req);
85
+ const conn = createWHATWGPairingConnection(socket);
86
+ // Deno opens the socket asynchronously; validate the token first,
87
+ // then register on `open` so frames aren't missed.
88
+ socket.addEventListener('open', () => {
89
+ void agent.acceptConnection(token, conn).then((result) => {
90
+ if (!result.ok)
91
+ conn.close();
92
+ });
93
+ }, { once: true });
94
+ return response;
95
+ }
96
+ //# sourceMappingURL=upgrade.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../../src/server/web/upgrade.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentCoreHandle } from '../core.js'\nimport { createWHATWGPairingConnection } from './adapter.js'\n\n/**\n * Extract the bearer token from a LAP WebSocket upgrade request.\n * Accepts the token on either `?token=` or `Authorization: Bearer` —\n * query-string is the common pattern because browsers can't set\n * arbitrary headers on WebSocket construction.\n */\nexport function extractToken(req: Request): string | null {\n const url = new URL(req.url)\n const q = url.searchParams.get('token')\n if (q) return q\n const auth = req.headers.get('authorization')\n if (auth?.startsWith('Bearer ')) return auth.slice('Bearer '.length)\n return null\n}\n\n/**\n * Cloudflare Workers handler. Accepts a WebSocket upgrade using\n * `WebSocketPair`, validates the token via\n * `agent.acceptConnection`, and returns the 101 upgrade Response.\n *\n * Usage:\n * ```ts\n * const agent = createLluiAgentCore({ signingKey: env.AGENT_KEY })\n * export default {\n * async fetch(req, env) {\n * const url = new URL(req.url)\n * if (url.pathname === '/agent/ws') return handleCloudflareUpgrade(req, agent)\n * return (await agent.router(req)) ?? new Response('Not Found', { status: 404 })\n * },\n * }\n * ```\n */\nexport async function handleCloudflareUpgrade(\n req: Request,\n agent: AgentCoreHandle,\n): Promise<Response> {\n if (req.headers.get('upgrade') !== 'websocket') {\n return new Response('Expected upgrade: websocket', { status: 426 })\n }\n const token = extractToken(req)\n if (!token) return new Response('Unauthorized', { status: 401 })\n\n // `WebSocketPair` is a Cloudflare Workers global. We reference it\n // through `globalThis` so importing this module in non-CF runtimes\n // (e.g. during type-checking on Node) doesn't crash.\n const Pair = (\n globalThis as unknown as { WebSocketPair?: new () => { 0: WebSocket; 1: WebSocket } }\n ).WebSocketPair\n if (!Pair) {\n return new Response('WebSocketPair unavailable in this runtime', { status: 501 })\n }\n const pair = new Pair()\n const client = pair[0]\n const server = pair[1]!\n // `accept()` on the server half is Cloudflare-specific — it tells\n // the runtime the Worker will handle the WebSocket itself.\n ;(server as unknown as { accept: () => void }).accept()\n\n const conn = createWHATWGPairingConnection(server)\n const result = await agent.acceptConnection(token, conn)\n if (!result.ok) {\n conn.close()\n return new Response(result.code, { status: result.status })\n }\n\n // `webSocket` on ResponseInit is Cloudflare-specific; cast to satisfy\n // the standard lib types.\n return new Response(null, { status: 101, webSocket: client } as ResponseInit & {\n webSocket: WebSocket\n })\n}\n\n/**\n * Deno handler. Uses `Deno.upgradeWebSocket(req)` to produce the\n * response + socket pair, then plugs the socket into the registry.\n *\n * Usage:\n * ```ts\n * Deno.serve(async (req) => {\n * const url = new URL(req.url)\n * if (url.pathname === '/agent/ws') return handleDenoUpgrade(req, agent)\n * return (await agent.router(req)) ?? new Response('Not Found', { status: 404 })\n * })\n * ```\n */\nexport async function handleDenoUpgrade(req: Request, agent: AgentCoreHandle): Promise<Response> {\n const token = extractToken(req)\n if (!token) return new Response('Unauthorized', { status: 401 })\n\n const Deno_ = (\n globalThis as unknown as {\n Deno?: {\n upgradeWebSocket: (req: Request) => { socket: WebSocket; response: Response }\n }\n }\n ).Deno\n if (!Deno_) {\n return new Response('Deno.upgradeWebSocket unavailable in this runtime', { status: 501 })\n }\n\n const { socket, response } = Deno_.upgradeWebSocket(req)\n const conn = createWHATWGPairingConnection(socket)\n\n // Deno opens the socket asynchronously; validate the token first,\n // then register on `open` so frames aren't missed.\n socket.addEventListener(\n 'open',\n () => {\n void agent.acceptConnection(token, conn).then((result) => {\n if (!result.ok) conn.close()\n })\n },\n { once: true },\n )\n\n return response\n}\n"]}
@@ -1,7 +1,11 @@
1
1
  import type { ClientFrame, ServerFrame, HelloFrame, LogEntry } from '../../protocol.js';
2
+ import { type RpcOptions, type RpcError } from './rpc.js';
3
+ export type { RpcOptions, RpcError };
2
4
  /**
3
- * Thin abstraction over a WebSocket so the registry is testable with
4
- * a fake EventEmitter-style mock.
5
+ * Thin abstraction over a single paired WebSocket. Consumed by the
6
+ * registry implementations; runtime-specific adapters (`ws`-lib,
7
+ * `WebSocketPair`, `Deno.upgradeWebSocket`, `Bun.serve` upgrade) build
8
+ * one of these and pass it to `registry.register()`.
5
9
  */
6
10
  export interface PairingConnection {
7
11
  send(frame: ServerFrame): void;
@@ -9,35 +13,85 @@ export interface PairingConnection {
9
13
  onClose(handler: () => void): void;
10
14
  close(): void;
11
15
  }
12
- export type RpcError = {
13
- code: 'paused' | 'invalid' | 'timeout' | 'schema-error' | 'internal' | string;
14
- detail?: string;
15
- };
16
- export type RpcOptions = {
17
- timeoutMs?: number;
18
- };
19
16
  /**
20
- * Tracks live browser pairings and correlates rpc requests with replies.
21
- * One instance per server; shared by all LAP handlers + the upgrade
22
- * handler. Spec §10.4–§10.5.
17
+ * A per-call frame subscriber. Return `true` to remove this
18
+ * subscriber (one-shot), or `false` to keep receiving. The registry
19
+ * dispatches every inbound `ClientFrame` to every active subscriber
20
+ * for the given `tid`; subscribers filter by `frame.t` + identifiers
21
+ * (correlation id, confirm id, state path) to find the one that
22
+ * belongs to their request.
23
23
  */
24
- export declare class WsPairingRegistry {
24
+ export type FrameSubscriber = (frame: ClientFrame) => boolean;
25
+ /**
26
+ * Registry of live browser pairings. Pure routing + hello cache —
27
+ * request-lifecycle state (in-flight RPC promises, confirm waits,
28
+ * long-polls) lives in the LAP handlers that need it, not here.
29
+ *
30
+ * Two implementations ship today:
31
+ * - `InMemoryPairingRegistry` for long-lived server processes
32
+ * (Node, Bun, Deno, Deno Deploy).
33
+ * - A Cloudflare Durable Object implementation (see
34
+ * `server/cloudflare`) for stateless Worker runtimes.
35
+ *
36
+ * Other runtimes can implement this interface the same way; the
37
+ * contract is intentionally small.
38
+ */
39
+ export interface PairingRegistry {
40
+ register(tid: string, conn: PairingConnection): void;
41
+ unregister(tid: string): void;
42
+ isPaired(tid: string): boolean;
43
+ getHello(tid: string): HelloFrame | null;
44
+ /** Send a frame. No-op when the pairing is absent or closed. */
45
+ send(tid: string, frame: ServerFrame): void;
46
+ /**
47
+ * Subscribe to frames from the paired browser. Returns an
48
+ * unsubscribe function. A subscriber can remove itself mid-dispatch
49
+ * by returning `true` from its callback — useful for one-shot
50
+ * request/response correlation.
51
+ */
52
+ subscribe(tid: string, handler: FrameSubscriber): () => void;
53
+ /**
54
+ * Observe the pairing closing (WebSocket drop, `unregister`, etc.).
55
+ * Handlers registered before close fire; handlers registered after
56
+ * close fire synchronously. Returns an unsubscribe function.
57
+ */
58
+ onClose(tid: string, handler: () => void): () => void;
59
+ /**
60
+ * Send a typed rpc frame and await its matching reply. See
61
+ * `./rpc.ts::rpc` for the full contract.
62
+ */
63
+ rpc(tid: string, tool: string, args: unknown, opts?: RpcOptions): Promise<unknown>;
64
+ /** See `./rpc.ts::waitForConfirm`. */
65
+ waitForConfirm(tid: string, confirmId: string, timeoutMs: number): Promise<{
66
+ outcome: 'confirmed' | 'user-cancelled';
67
+ stateAfter?: unknown;
68
+ }>;
69
+ /** See `./rpc.ts::waitForChange`. */
70
+ waitForChange(tid: string, path: string | undefined, timeoutMs: number): Promise<{
71
+ status: 'changed' | 'timeout';
72
+ stateAfter: unknown;
73
+ }>;
74
+ }
75
+ /**
76
+ * Single-process in-memory registry. Correct for Node/Bun/Deno/Deno
77
+ * Deploy — anywhere the server process can hold a long-lived
78
+ * WebSocket. Not suitable for stateless Worker isolates; use the
79
+ * Durable Object registry for Cloudflare.
80
+ */
81
+ export declare class InMemoryPairingRegistry implements PairingRegistry {
25
82
  private pairings;
26
- private now;
27
83
  private onLogAppend;
28
84
  constructor(opts?: {
29
- now?: () => number;
30
85
  onLogAppend?: (tid: string, entry: LogEntry) => void;
31
86
  });
32
87
  register(tid: string, conn: PairingConnection): void;
33
88
  unregister(tid: string): void;
34
89
  isPaired(tid: string): boolean;
35
- /**
36
- * Send a ServerFrame to the paired browser connection, if one is live.
37
- * No-op when unpaired or closed.
38
- */
39
- notify(tid: string, frame: ServerFrame): void;
40
90
  getHello(tid: string): HelloFrame | null;
91
+ send(tid: string, frame: ServerFrame): void;
92
+ subscribe(tid: string, handler: FrameSubscriber): () => void;
93
+ onClose(tid: string, handler: () => void): () => void;
94
+ private dispatch;
41
95
  rpc(tid: string, tool: string, args: unknown, opts?: RpcOptions): Promise<unknown>;
42
96
  waitForConfirm(tid: string, confirmId: string, timeoutMs: number): Promise<{
43
97
  outcome: 'confirmed' | 'user-cancelled';
@@ -47,7 +101,16 @@ export declare class WsPairingRegistry {
47
101
  status: 'changed' | 'timeout';
48
102
  stateAfter: unknown;
49
103
  }>;
50
- private handleClientFrame;
104
+ /** @deprecated Use `send(tid, frame)` directly; semantics are identical. */
105
+ notify(tid: string, frame: ServerFrame): void;
51
106
  private handleClose;
52
107
  }
108
+ /**
109
+ * Back-compat alias for the prior class name. New code should use
110
+ * `InMemoryPairingRegistry`. Removed in a future major.
111
+ *
112
+ * @deprecated Use `InMemoryPairingRegistry` directly.
113
+ */
114
+ export declare const WsPairingRegistry: typeof InMemoryPairingRegistry;
115
+ export type WsPairingRegistry = InMemoryPairingRegistry;
53
116
  //# sourceMappingURL=pairing-registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pairing-registry.d.ts","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAEvF;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IAC9B,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI,CAAA;IAChD,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAClC,KAAK,IAAI,IAAI,CAAA;CACd;AA4BD,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,GAAG,UAAU,GAAG,MAAM,CAAA;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE/C;;;;GAIG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,GAAG,CAAc;IACzB,OAAO,CAAC,WAAW,CAAiD;gBAGlE,IAAI,GAAE;QACJ,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;QAClB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAA;KAChD;IAMR,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI;IAcpD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAM7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAK9B;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI;IAU7C,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAIlC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,GAAE,UAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IA6BtF,cAAc,CAClB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,OAAO,EAAE,WAAW,GAAG,gBAAgB,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAiBvE,aAAa,CACjB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC;IAiBlE,OAAO,CAAC,iBAAiB;IAmDzB,OAAO,CAAC,WAAW;CAqBpB"}
1
+ {"version":3,"file":"pairing-registry.d.ts","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"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"}
@@ -1,45 +1,39 @@
1
- import { randomUUID } from 'node:crypto';
1
+ import { rpc as rpcHelper, waitForConfirm as waitForConfirmHelper, waitForChange as waitForChangeHelper, } from './rpc.js';
2
2
  /**
3
- * Tracks live browser pairings and correlates rpc requests with replies.
4
- * One instance per server; shared by all LAP handlers + the upgrade
5
- * handler. Spec §10.4–§10.5.
3
+ * Single-process in-memory registry. Correct for Node/Bun/Deno/Deno
4
+ * Deploy anywhere the server process can hold a long-lived
5
+ * WebSocket. Not suitable for stateless Worker isolates; use the
6
+ * Durable Object registry for Cloudflare.
6
7
  */
7
- export class WsPairingRegistry {
8
+ export class InMemoryPairingRegistry {
8
9
  pairings = new Map();
9
- now;
10
10
  onLogAppend;
11
11
  constructor(opts = {}) {
12
- this.now = opts.now ?? (() => Date.now());
13
12
  this.onLogAppend = opts.onLogAppend ?? null;
14
13
  }
15
14
  register(tid, conn) {
16
15
  const p = {
17
16
  conn,
18
17
  hello: null,
19
- pendingRpc: new Map(),
20
- pendingConfirm: new Map(),
21
- pendingWait: [],
18
+ subscribers: new Set(),
19
+ closeHandlers: new Set(),
22
20
  closed: false,
23
21
  };
24
22
  this.pairings.set(tid, p);
25
- conn.onFrame((frame) => this.handleClientFrame(tid, frame));
23
+ conn.onFrame((frame) => this.dispatch(tid, frame));
26
24
  conn.onClose(() => this.handleClose(tid));
27
25
  }
28
26
  unregister(tid) {
29
- const p = this.pairings.get(tid);
30
- if (!p)
31
- return;
32
27
  this.handleClose(tid);
33
28
  }
34
29
  isPaired(tid) {
35
30
  const p = this.pairings.get(tid);
36
31
  return !!p && !p.closed;
37
32
  }
38
- /**
39
- * Send a ServerFrame to the paired browser connection, if one is live.
40
- * No-op when unpaired or closed.
41
- */
42
- notify(tid, frame) {
33
+ getHello(tid) {
34
+ return this.pairings.get(tid)?.hello ?? null;
35
+ }
36
+ send(tid, frame) {
43
37
  const p = this.pairings.get(tid);
44
38
  if (!p || p.closed)
45
39
  return;
@@ -47,159 +41,103 @@ export class WsPairingRegistry {
47
41
  p.conn.send(frame);
48
42
  }
49
43
  catch {
50
- // connection may have dropped between isPaired and notify; ignore
44
+ // Connection may have dropped between isPaired() and send(); no-op.
51
45
  }
52
46
  }
53
- getHello(tid) {
54
- return this.pairings.get(tid)?.hello ?? null;
55
- }
56
- async rpc(tid, tool, args, opts = {}) {
47
+ subscribe(tid, handler) {
57
48
  const p = this.pairings.get(tid);
58
- if (!p || p.closed) {
59
- const err = { code: 'paused' };
60
- throw err;
61
- }
62
- const id = randomUUID();
63
- const timeoutMs = opts.timeoutMs ?? 15_000;
64
- return new Promise((resolve, reject) => {
65
- const entry = {
66
- resolve,
67
- reject,
68
- timer: setTimeout(() => {
69
- p.pendingRpc.delete(id);
70
- reject({ code: 'timeout' });
71
- }, timeoutMs),
72
- };
73
- p.pendingRpc.set(id, entry);
74
- const frame = { t: 'rpc', id, tool, args };
75
- try {
76
- p.conn.send(frame);
77
- }
78
- catch (e) {
79
- p.pendingRpc.delete(id);
80
- if (entry.timer)
81
- clearTimeout(entry.timer);
82
- reject({ code: 'internal', detail: String(e) });
83
- }
84
- });
49
+ if (!p || p.closed)
50
+ return () => { };
51
+ p.subscribers.add(handler);
52
+ return () => {
53
+ p.subscribers.delete(handler);
54
+ };
85
55
  }
86
- async waitForConfirm(tid, confirmId, timeoutMs) {
56
+ onClose(tid, handler) {
87
57
  const p = this.pairings.get(tid);
88
58
  if (!p || p.closed) {
89
- return { outcome: 'user-cancelled' };
59
+ // Already closed — fire synchronously so callers don't hang
60
+ // waiting for a close that already happened.
61
+ queueMicrotask(handler);
62
+ return () => { };
90
63
  }
91
- return new Promise((resolve) => {
92
- const entry = {
93
- resolve,
94
- timer: setTimeout(() => {
95
- p.pendingConfirm.delete(confirmId);
96
- resolve({ outcome: 'user-cancelled' });
97
- }, timeoutMs),
98
- };
99
- p.pendingConfirm.set(confirmId, entry);
100
- });
101
- }
102
- async waitForChange(tid, path, timeoutMs) {
103
- const p = this.pairings.get(tid);
104
- if (!p || p.closed)
105
- return { status: 'timeout', stateAfter: null };
106
- return new Promise((resolve) => {
107
- const entry = {
108
- path,
109
- resolve,
110
- timer: setTimeout(() => {
111
- const idx = p.pendingWait.indexOf(entry);
112
- if (idx >= 0)
113
- p.pendingWait.splice(idx, 1);
114
- resolve({ status: 'timeout', stateAfter: null });
115
- }, timeoutMs),
116
- };
117
- p.pendingWait.push(entry);
118
- });
64
+ p.closeHandlers.add(handler);
65
+ return () => {
66
+ p.closeHandlers.delete(handler);
67
+ };
119
68
  }
120
- handleClientFrame(tid, frame) {
69
+ dispatch(tid, frame) {
121
70
  const p = this.pairings.get(tid);
122
71
  if (!p || p.closed)
123
72
  return;
124
- switch (frame.t) {
125
- case 'hello': {
126
- p.hello = frame;
127
- break;
128
- }
129
- case 'rpc-reply': {
130
- const e = p.pendingRpc.get(frame.id);
131
- if (!e)
132
- break;
133
- p.pendingRpc.delete(frame.id);
134
- if (e.timer)
135
- clearTimeout(e.timer);
136
- e.resolve(frame.result);
137
- break;
138
- }
139
- case 'rpc-error': {
140
- const e = p.pendingRpc.get(frame.id);
141
- if (!e)
142
- break;
143
- p.pendingRpc.delete(frame.id);
144
- if (e.timer)
145
- clearTimeout(e.timer);
146
- e.reject({ code: frame.code, detail: frame.detail });
147
- break;
148
- }
149
- case 'confirm-resolved': {
150
- const e = p.pendingConfirm.get(frame.confirmId);
151
- if (!e)
152
- break;
153
- p.pendingConfirm.delete(frame.confirmId);
154
- if (e.timer)
155
- clearTimeout(e.timer);
156
- e.resolve({ outcome: frame.outcome, stateAfter: frame.stateAfter });
157
- break;
158
- }
159
- case 'state-update': {
160
- for (let i = p.pendingWait.length - 1; i >= 0; i--) {
161
- const w = p.pendingWait[i];
162
- if (w === undefined)
163
- continue;
164
- if (w.path === undefined || w.path === frame.path || frame.path.startsWith(w.path)) {
165
- p.pendingWait.splice(i, 1);
166
- if (w.timer)
167
- clearTimeout(w.timer);
168
- w.resolve({ status: 'changed', stateAfter: frame.stateAfter });
169
- }
170
- }
171
- break;
73
+ // hello and log-append are registry-owned side effects — handled
74
+ // here so no per-call subscriber has to pick them up.
75
+ if (frame.t === 'hello') {
76
+ p.hello = frame;
77
+ return;
78
+ }
79
+ if (frame.t === 'log-append') {
80
+ this.onLogAppend?.(tid, frame.entry);
81
+ return;
82
+ }
83
+ // Iterate over a snapshot because subscribers may self-remove
84
+ // mid-iteration by returning true.
85
+ const snapshot = Array.from(p.subscribers);
86
+ for (const sub of snapshot) {
87
+ try {
88
+ if (sub(frame))
89
+ p.subscribers.delete(sub);
172
90
  }
173
- case 'log-append': {
174
- this.onLogAppend?.(tid, frame.entry);
175
- break;
91
+ catch {
92
+ // One bad subscriber shouldn't break the others.
93
+ p.subscribers.delete(sub);
176
94
  }
177
95
  }
178
96
  }
97
+ // ── Convenience wrappers ───────────────────────────────────────
98
+ // The following methods delegate to the free-function helpers in
99
+ // `./rpc.ts`. They're here so the in-memory registry remains a
100
+ // one-stop testing surface (spy on `registry.rpc`, etc.) without
101
+ // couping the `PairingRegistry` interface to request-lifecycle
102
+ // details. External implementations (e.g. the Cloudflare Durable
103
+ // Object registry) are NOT required to provide these; the LAP
104
+ // handlers always go through the free helpers.
105
+ rpc(tid, tool, args, opts = {}) {
106
+ return rpcHelper(this, tid, tool, args, opts);
107
+ }
108
+ waitForConfirm(tid, confirmId, timeoutMs) {
109
+ return waitForConfirmHelper(this, tid, confirmId, timeoutMs);
110
+ }
111
+ waitForChange(tid, path, timeoutMs) {
112
+ return waitForChangeHelper(this, tid, path, timeoutMs);
113
+ }
114
+ /** @deprecated Use `send(tid, frame)` directly; semantics are identical. */
115
+ notify(tid, frame) {
116
+ this.send(tid, frame);
117
+ }
179
118
  handleClose(tid) {
180
119
  const p = this.pairings.get(tid);
181
- if (!p)
120
+ if (!p || p.closed)
182
121
  return;
183
122
  p.closed = true;
184
- for (const [, e] of p.pendingRpc) {
185
- if (e.timer)
186
- clearTimeout(e.timer);
187
- e.reject({ code: 'paused' });
188
- }
189
- p.pendingRpc.clear();
190
- for (const [, e] of p.pendingConfirm) {
191
- if (e.timer)
192
- clearTimeout(e.timer);
193
- e.resolve({ outcome: 'user-cancelled' });
194
- }
195
- p.pendingConfirm.clear();
196
- for (const w of p.pendingWait) {
197
- if (w.timer)
198
- clearTimeout(w.timer);
199
- w.resolve({ status: 'timeout', stateAfter: null });
123
+ for (const h of Array.from(p.closeHandlers)) {
124
+ try {
125
+ h();
126
+ }
127
+ catch {
128
+ // Swallow — handlers run best-effort.
129
+ }
200
130
  }
201
- p.pendingWait.length = 0;
131
+ p.closeHandlers.clear();
132
+ p.subscribers.clear();
202
133
  this.pairings.delete(tid);
203
134
  }
204
135
  }
136
+ /**
137
+ * Back-compat alias for the prior class name. New code should use
138
+ * `InMemoryPairingRegistry`. Removed in a future major.
139
+ *
140
+ * @deprecated Use `InMemoryPairingRegistry` directly.
141
+ */
142
+ export const WsPairingRegistry = InMemoryPairingRegistry;
205
143
  //# sourceMappingURL=pairing-registry.js.map