@fairfox/polly 0.26.0 → 0.27.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.
@@ -342,9 +342,25 @@ function signalingServer(options = {}) {
342
342
  }
343
343
  };
344
344
  const handleJoin = (ws, peerId) => {
345
- peerSockets.set(peerId, ws);
345
+ const newcomer = ws;
346
+ const incumbents = [];
347
+ for (const [existingPeerId, existingSocket] of peerSockets) {
348
+ if (existingPeerId === peerId)
349
+ continue;
350
+ incumbents.push({ peerId: existingPeerId, socket: existingSocket });
351
+ }
352
+ peerSockets.set(peerId, newcomer);
346
353
  const wsWithData = ws;
347
354
  wsWithData.data.peerId = peerId;
355
+ newcomer.send({
356
+ type: "peers-present",
357
+ peerIds: incumbents.map((i) => i.peerId)
358
+ });
359
+ for (const incumbent of incumbents) {
360
+ try {
361
+ incumbent.socket.send({ type: "peer-joined", peerId });
362
+ } catch {}
363
+ }
348
364
  };
349
365
  const sendUnknownTarget = (ws, targetPeerId) => {
350
366
  ws.send({
@@ -409,10 +425,20 @@ function signalingServer(options = {}) {
409
425
  },
410
426
  close(ws) {
411
427
  const peerId = ws.data.peerId;
412
- if (peerId) {
413
- if (peerSockets.get(peerId) === ws) {
414
- peerSockets.delete(peerId);
415
- }
428
+ if (!peerId) {
429
+ return;
430
+ }
431
+ const mapped = peerSockets.get(peerId);
432
+ const wsData = ws.data;
433
+ const mappedData = mapped?.data;
434
+ if (mapped === undefined || mappedData !== wsData) {
435
+ return;
436
+ }
437
+ peerSockets.delete(peerId);
438
+ for (const [_incumbentId, incumbentSocket] of peerSockets) {
439
+ try {
440
+ incumbentSocket.send({ type: "peer-left", peerId });
441
+ } catch {}
416
442
  }
417
443
  }
418
444
  });
@@ -423,4 +449,4 @@ export {
423
449
  peerRepo
424
450
  };
425
451
 
426
- //# debugId=ABCACD6B736E00DA64756E2164756E21
452
+ //# debugId=3F789E611CD3A45864756E2164756E21
@@ -7,9 +7,9 @@
7
7
  "// @ts-nocheck - Optional peer dependencies (elysia, @elysiajs/eden)\nimport type { Signal } from \"@preact/signals-core\";\nimport { Elysia } from \"elysia\";\nimport { createLamportClock } from \"../core/clock\";\nimport { serializeFunction } from \"../utils/function-serialization\";\nimport type { PollyConfig, PollyResponseMetadata } from \"./types\";\n\n/**\n * Broadcast message sent to connected clients\n */\ninterface BroadcastMessage {\n type: \"effect\";\n path: string;\n method: string;\n result: unknown;\n clock: { tick: number; contextId: string };\n}\n\n/**\n * Minimal WebSocket interface for broadcasting\n */\ninterface MinimalWebSocket {\n readyState: number;\n send(data: string): void;\n}\n\n/**\n * Route pattern matcher\n * Supports:\n * - Exact match: 'POST /todos'\n * - Param match: 'GET /todos/:id'\n * - Wildcard: '/todos/*'\n */\nfunction matchRoute(pattern: string, method: string, path: string): boolean {\n // Split pattern into method + path or just path\n const hasMethod = pattern.includes(\" \");\n const patternMethod = hasMethod ? pattern.split(\" \")[0] : null;\n const patternPath = hasMethod ? pattern.split(\" \")[1] : pattern;\n\n // Check method\n if (patternMethod && patternMethod !== method) {\n return false;\n }\n\n // Check path\n const patternSegments = patternPath.split(\"/\").filter(Boolean);\n const pathSegments = path.split(\"/\").filter(Boolean);\n\n if (patternSegments.length !== pathSegments.length && !patternPath.includes(\"*\")) {\n return false;\n }\n\n for (let i = 0; i < patternSegments.length; i++) {\n const patternSeg = patternSegments[i];\n const pathSeg = pathSegments[i];\n\n if (patternSeg === \"*\") return true;\n if (patternSeg.startsWith(\":\")) continue; // Param match\n if (patternSeg !== pathSeg) return false;\n }\n\n return true;\n}\n\n/**\n * Find matching config for a route\n */\nfunction findMatchingConfig<T>(\n configs: Record<string, T> | undefined,\n method: string,\n path: string\n): T | undefined {\n if (!configs) return undefined;\n\n for (const [pattern, config] of Object.entries(configs)) {\n if (matchRoute(pattern, method, path)) {\n return config;\n }\n }\n\n return undefined;\n}\n\n/**\n * WebSocket broadcast manager\n */\nclass BroadcastManager {\n private connections = new Map<string, MinimalWebSocket>();\n\n register(clientId: string, ws: MinimalWebSocket) {\n this.connections.set(clientId, ws);\n }\n\n unregister(clientId: string) {\n this.connections.delete(clientId);\n }\n\n broadcast(message: BroadcastMessage, filter?: (clientId: string) => boolean) {\n const payload = JSON.stringify(message);\n\n for (const [clientId, ws] of this.connections.entries()) {\n if (filter && !filter(clientId)) continue;\n if (ws.readyState === 1) {\n // WebSocket.OPEN = 1\n ws.send(payload);\n }\n }\n }\n}\n\n/**\n * Main Polly Elysia plugin\n */\nexport function polly(config: PollyConfig = {}) {\n const isDev = process.env.NODE_ENV !== \"production\";\n const clock = createLamportClock(\"server\");\n const broadcaster = new BroadcastManager();\n const clientStateByConnection = new Map<string, Record<string, Signal<unknown>>>();\n\n const app = new Elysia({ name: \"polly\" })\n // Add state to context\n .decorate(\"pollyState\", {\n client: config.state?.client || {},\n server: config.state?.server || {},\n })\n .decorate(\"pollyClock\", clock)\n .decorate(\"pollyBroadcast\", broadcaster)\n\n // WebSocket endpoint for real-time updates\n .ws(config.websocketPath || \"/polly/ws\", {\n // @ts-expect-error - Elysia WebSocket types from optional peer dependency\n open(ws) {\n const clientId = ws.data.headers?.[\"x-client-id\"] || crypto.randomUUID();\n broadcaster.register(clientId, ws.raw);\n\n // Send initial state sync\n ws.send(\n JSON.stringify({\n type: \"state-sync\",\n state: config.state?.client || {},\n clock: clock.now(),\n })\n );\n },\n // @ts-expect-error - Elysia WebSocket types from optional peer dependency\n close(ws) {\n const clientId = ws.data.headers?.[\"x-client-id\"];\n if (clientId) {\n broadcaster.unregister(clientId);\n clientStateByConnection.delete(clientId);\n }\n },\n // @ts-expect-error - Elysia WebSocket types from optional peer dependency\n message(ws, message) {\n // Handle client state updates\n const data = JSON.parse(message as unknown as string);\n\n if (data.type === \"state-update\") {\n const clientId = ws.data.headers?.[\"x-client-id\"];\n if (clientId) {\n clientStateByConnection.set(clientId, data.state);\n }\n }\n },\n })\n\n // Authorization hook (runs before handler)\n // @ts-expect-error - Elysia context types from optional peer dependency\n .onBeforeHandle(async ({ request, pollyState, body, params }) => {\n const method = request.method;\n const path = new URL(request.url).pathname;\n\n const authHandler = findMatchingConfig(config.authorization, method, path);\n\n if (authHandler) {\n const allowed = await authHandler({\n state: pollyState,\n body,\n params,\n headers: Object.fromEntries(request.headers.entries()),\n });\n\n if (!allowed) {\n return new Response(\"Unauthorized\", { status: 403 });\n }\n }\n })\n\n // Response hook (runs after handler)\n // @ts-expect-error - Elysia context types from optional peer dependency\n .onAfterHandle(\n async ({ request, response, _pollyState, pollyClock, pollyBroadcast, _body, _params }) => {\n const method = request.method;\n const path = new URL(request.url).pathname;\n\n // Find matching effect config\n const effectConfig = findMatchingConfig(config.effects, method, path);\n\n // Tick clock\n pollyClock.tick();\n\n // If broadcast enabled, send to all connected clients\n // This works in both dev and prod for real-time updates\n if (effectConfig?.broadcast) {\n const broadcastMessage = {\n type: \"effect\",\n path,\n method,\n result: response,\n clock: pollyClock.now(),\n };\n\n if (effectConfig.broadcastFilter) {\n pollyBroadcast.broadcast(broadcastMessage, (clientId) => {\n const clientState = clientStateByConnection.get(clientId) || {};\n return effectConfig.broadcastFilter?.(clientState) ?? false;\n });\n } else {\n pollyBroadcast.broadcast(broadcastMessage);\n }\n }\n\n // In production, skip expensive metadata operations\n if (!isDev) {\n return response;\n }\n\n // DEV ONLY: Add Polly metadata to response for debugging/hot-reload\n const offlineConfig = findMatchingConfig(config.offline, method, path);\n const metadata: PollyResponseMetadata = {\n clientEffect: effectConfig\n ? {\n handler: serializeFunction(effectConfig.client),\n broadcast: effectConfig.broadcast || false,\n }\n : undefined,\n offline: offlineConfig,\n clock: pollyClock.now(),\n };\n\n // Attach metadata as header\n return new Response(JSON.stringify(response), {\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Polly-Metadata\": JSON.stringify(metadata),\n },\n });\n }\n );\n\n return app;\n}\n",
8
8
  "/**\n * Lamport Clock Implementation\n *\n * Provides logical timestamps for distributed systems to establish\n * causal ordering of events across different contexts (client/server).\n *\n * Key properties:\n * - Each event increments the local clock\n * - When receiving a message, clock = max(local, received) + 1\n * - If A happens before B, then timestamp(A) < timestamp(B)\n *\n * References:\n * - Lamport, L. (1978). \"Time, Clocks, and the Ordering of Events in a Distributed System\"\n * - https://lamport.azurewebsites.net/pubs/time-clocks.pdf\n */\n\n/**\n * Lamport clock state\n */\nexport interface LamportClock {\n tick: number;\n contextId: string;\n}\n\n/**\n * Lamport clock with operations\n */\nexport interface LamportClockOps {\n /**\n * Get current clock value\n */\n now(): LamportClock;\n\n /**\n * Increment the clock (before sending a message or performing an action)\n */\n tick(): number;\n\n /**\n * Update clock when receiving a message\n * Sets clock to max(local, received) + 1\n */\n update(receivedClock: LamportClock): void;\n}\n\n/**\n * Create a Lamport clock for a specific context\n *\n * @param contextId - Unique identifier for this context (e.g., \"client\", \"server\", \"worker-1\")\n * @returns Clock operations\n *\n * @example\n * ```typescript\n * const serverClock = createLamportClock(\"server\");\n *\n * // Before sending a message\n * serverClock.tick();\n * const timestamp = serverClock.now();\n * send({ data: \"...\", clock: timestamp });\n *\n * // When receiving a message\n * onReceive((message) => {\n * serverClock.update(message.clock);\n * // Process message with updated clock\n * });\n * ```\n */\nexport function createLamportClock(contextId: string): LamportClockOps {\n let tick = 0;\n\n return {\n now(): LamportClock {\n return { tick, contextId };\n },\n\n tick(): number {\n tick += 1;\n return tick;\n },\n\n update(receivedClock: LamportClock): void {\n tick = Math.max(tick, receivedClock.tick) + 1;\n },\n };\n}\n",
9
9
  "import serialize from \"serialize-javascript\";\n\n/**\n * Check if we're in development mode\n */\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/**\n * Serialize a function to send to client\n *\n * DEV ONLY: Used for hot reloading and debugging\n * PROD: No-op - client effects are baked into bundle at build time\n */\n// biome-ignore lint/complexity/noBannedTypes: Generic function serialization requires Function type\nexport function serializeFunction(fn: Function): string {\n if (!isDev) {\n // In production, return empty string - this won't be used\n return \"\";\n }\n\n return serialize(fn, { space: 0 });\n}\n\n/**\n * Deserialize a function received from server\n *\n * DEV ONLY: Eval serialized function source\n * PROD: Should never be called - effects come from bundle\n */\n// biome-ignore lint/complexity/noBannedTypes: Generic function deserialization requires Function type\nexport function deserializeFunction(serialized: string): Function {\n if (!isDev) {\n throw new Error(\n \"[Polly] deserializeFunction should not be called in production. \" +\n \"Client effects should be imported from your bundle.\"\n );\n }\n\n if (!serialized) {\n throw new Error(\"[Polly] Cannot deserialize empty function\");\n }\n\n // biome-ignore lint/security/noGlobalEval: Required for dev-mode function deserialization\n return eval(`(${serialized})`);\n}\n",
10
- "// @ts-nocheck - Optional peer dependencies (elysia, @elysiajs/eden)\n/**\n * signalingServer — Phase 2 Elysia plugin that exposes a stateless\n * WebSocket route for SDP/ICE relay between $meshState peers.\n *\n * The mesh transport is a star-of-data-channels: peers establish direct\n * WebRTC connections to each other and exchange document operations\n * peer-to-peer once those channels are open. WebRTC connection setup\n * needs an out-of-band channel for SDP offer/answer and ICE candidate\n * exchange, and that channel is what this plugin provides. The plugin\n * does not own any document state, does not hold any encryption keys,\n * and never inspects the contents of the messages it relays. It is a\n * pure pub-sub by peer id.\n *\n * Once two peers have completed the SDP exchange and opened a direct\n * data channel, the signalling server is no longer on the critical\n * path — the peers talk directly. The signalling server's role is\n * therefore intermittent: peers connect to it only during the brief\n * windows when they are establishing or re-establishing connections.\n *\n * Wire protocol:\n *\n * Client → server (join):\n * { type: \"join\", peerId: \"peer-alice\" }\n *\n * Client → server (signal to another peer):\n * { type: \"signal\", peerId: \"peer-alice\", targetPeerId: \"peer-bob\",\n * payload: { ... } }\n *\n * Server → client (delivered signal):\n * { type: \"signal\", peerId: \"peer-alice\", targetPeerId: \"peer-bob\",\n * payload: { ... } }\n *\n * Server → client (notification of unknown target):\n * { type: \"error\", reason: \"unknown-target\", targetPeerId: \"...\" }\n *\n * The `payload` is opaque to the signalling server — typically it\n * carries an SDP offer, SDP answer, or ICE candidate. Applications can\n * also use the relay for any other peer-to-peer message that needs an\n * intermediary, such as the initial handshake of a pairing flow.\n *\n * @example\n * ```ts\n * import { Elysia } from \"elysia\";\n * import { signalingServer } from \"@fairfox/polly/elysia\";\n *\n * const app = new Elysia()\n * .use(signalingServer({ path: \"/polly/signaling\" }))\n * .listen(8080);\n * ```\n */\n\nimport { Elysia } from \"elysia\";\n\n/** A signalling message. The `type` discriminates between join (peer\n * registration), signal (relayed message), and error (server response). */\nexport type SignalingMessage =\n | {\n type: \"join\";\n /** The peer registering itself with the signalling server. */\n peerId: string;\n }\n | {\n type: \"signal\";\n /** The peer sending the signal. */\n peerId: string;\n /** The peer the signal is being relayed to. */\n targetPeerId: string;\n /** Opaque payload, typically SDP or ICE. */\n payload: unknown;\n }\n | {\n type: \"error\";\n reason: \"unknown-target\" | \"not-joined\" | \"malformed\";\n targetPeerId?: string;\n };\n\nexport interface SignalingServerOptions {\n /** WebSocket route path. Defaults to \"/polly/signaling\". */\n path?: string;\n}\n\n/**\n * Construct the signalling-server Elysia plugin. The plugin keeps a\n * per-instance map of peer id → WebSocket connection so that incoming\n * \"signal\" messages can be routed to the right target socket. The map\n * is local to the plugin instance, not shared across processes; for\n * multi-instance deployments behind a load balancer, applications need\n * sticky connection routing or a shared backplane (Redis pub-sub or\n * similar). That is a follow-up.\n */\nexport function signalingServer(options: SignalingServerOptions = {}) {\n const path = options.path ?? \"/polly/signaling\";\n // Per-peer-id map of joined sockets. The inverse mapping is stored\n // directly on ws.data (a mutable property bag that Elysia preserves\n // across message callbacks for a given connection); the webrtc-p2p-chat\n // example in examples/ confirms this pattern is stable under Bun.\n const peerSockets = new Map<string, { send: (msg: unknown) => void }>();\n\n // Intentionally unnamed — Elysia deduplicates plugins by name, and each\n // signalingServer() call needs its own closure-captured peer map.\n const parseMessage = (raw: unknown): SignalingMessage | undefined => {\n try {\n return typeof raw === \"string\" ? JSON.parse(raw) : (raw as unknown as SignalingMessage);\n } catch {\n return undefined;\n }\n };\n\n const handleJoin = (ws: unknown, peerId: string): void => {\n peerSockets.set(peerId, ws as unknown as { send: (m: unknown) => void });\n const wsWithData = ws as unknown as { data: Record<string, unknown> };\n wsWithData.data.peerId = peerId;\n };\n\n const sendUnknownTarget = (ws: unknown, targetPeerId: string): void => {\n (ws as unknown as { send: (m: unknown) => void }).send({\n type: \"error\",\n reason: \"unknown-target\",\n targetPeerId,\n } as unknown as SignalingMessage);\n };\n\n /** Look up a target socket and confirm it is still open. */\n const findOpenTarget = (targetPeerId: string): { send: (msg: unknown) => void } | undefined => {\n const target = peerSockets.get(targetPeerId);\n if (!target) return undefined;\n const readyState = (target as unknown as { readyState?: number }).readyState;\n const OPEN = 1;\n if (readyState !== undefined && readyState !== OPEN) {\n peerSockets.delete(targetPeerId);\n return undefined;\n }\n return target;\n };\n\n const handleSignal = (ws: unknown, msg: Extract<SignalingMessage, { type: \"signal\" }>): void => {\n const wsWithData = ws as unknown as {\n data: Record<string, unknown>;\n send: (m: unknown) => void;\n };\n const senderId = wsWithData.data.peerId as unknown as string | undefined;\n if (!senderId) {\n wsWithData.send({ type: \"error\", reason: \"not-joined\" } as unknown as SignalingMessage);\n return;\n }\n const target = findOpenTarget(msg.targetPeerId);\n if (!target) {\n sendUnknownTarget(ws, msg.targetPeerId);\n return;\n }\n const relayed: SignalingMessage = {\n type: \"signal\",\n peerId: senderId,\n targetPeerId: msg.targetPeerId,\n payload: msg.payload,\n };\n try {\n target.send(relayed);\n } catch {\n peerSockets.delete(msg.targetPeerId);\n sendUnknownTarget(ws, msg.targetPeerId);\n }\n };\n\n return new Elysia().ws(path, {\n message(ws, raw) {\n const msg = parseMessage(raw);\n if (!msg) {\n ws.send({ type: \"error\", reason: \"malformed\" } as unknown as SignalingMessage);\n return;\n }\n if (msg.type === \"join\") {\n handleJoin(ws, msg.peerId);\n return;\n }\n if (msg.type === \"signal\") {\n handleSignal(ws, msg);\n return;\n }\n ws.send({ type: \"error\", reason: \"malformed\" } as unknown as SignalingMessage);\n },\n\n close(ws) {\n const peerId = (ws.data as unknown as Record<string, unknown>).peerId as unknown as\n | string\n | undefined;\n if (peerId) {\n // Only delete the entry if it still points at *this* socket, so a\n // stale close after a reconnect does not evict the new socket.\n if (peerSockets.get(peerId) === (ws as unknown as { send: (m: unknown) => void })) {\n peerSockets.delete(peerId);\n }\n }\n },\n });\n}\n"
10
+ "// @ts-nocheck - Optional peer dependencies (elysia, @elysiajs/eden)\n/**\n * signalingServer — Phase 2 Elysia plugin that exposes a stateless\n * WebSocket route for SDP/ICE relay between $meshState peers.\n *\n * The mesh transport is a star-of-data-channels: peers establish direct\n * WebRTC connections to each other and exchange document operations\n * peer-to-peer once those channels are open. WebRTC connection setup\n * needs an out-of-band channel for SDP offer/answer and ICE candidate\n * exchange, and that channel is what this plugin provides. The plugin\n * does not own any document state, does not hold any encryption keys,\n * and never inspects the contents of the messages it relays. It is a\n * pure pub-sub by peer id.\n *\n * Once two peers have completed the SDP exchange and opened a direct\n * data channel, the signalling server is no longer on the critical\n * path — the peers talk directly. The signalling server's role is\n * therefore intermittent: peers connect to it only during the brief\n * windows when they are establishing or re-establishing connections.\n *\n * Wire protocol:\n *\n * Client → server (join):\n * { type: \"join\", peerId: \"peer-alice\" }\n *\n * Client → server (signal to another peer):\n * { type: \"signal\", peerId: \"peer-alice\", targetPeerId: \"peer-bob\",\n * payload: { ... } }\n *\n * Server → client (delivered signal):\n * { type: \"signal\", peerId: \"peer-alice\", targetPeerId: \"peer-bob\",\n * payload: { ... } }\n *\n * Server → client (notification of unknown target):\n * { type: \"error\", reason: \"unknown-target\", targetPeerId: \"...\" }\n *\n * The `payload` is opaque to the signalling server — typically it\n * carries an SDP offer, SDP answer, or ICE candidate. Applications can\n * also use the relay for any other peer-to-peer message that needs an\n * intermediary, such as the initial handshake of a pairing flow.\n *\n * @example\n * ```ts\n * import { Elysia } from \"elysia\";\n * import { signalingServer } from \"@fairfox/polly/elysia\";\n *\n * const app = new Elysia()\n * .use(signalingServer({ path: \"/polly/signaling\" }))\n * .listen(8080);\n * ```\n */\n\nimport { Elysia } from \"elysia\";\n\n/** A signalling message. The `type` discriminates between client-to-server\n * requests (join, signal), the error envelope, and the server-to-client\n * discovery frames (peers-present, peer-joined, peer-left) that let\n * peers learn about each other without polling. */\nexport type SignalingMessage =\n | {\n type: \"join\";\n /** The peer registering itself with the signalling server. */\n peerId: string;\n }\n | {\n type: \"signal\";\n /** The peer sending the signal. */\n peerId: string;\n /** The peer the signal is being relayed to. */\n targetPeerId: string;\n /** Opaque payload, typically SDP or ICE. */\n payload: unknown;\n }\n | {\n type: \"error\";\n reason: \"unknown-target\" | \"not-joined\" | \"malformed\";\n targetPeerId?: string;\n }\n | {\n /** Sent to a newcomer immediately after it joins, listing every\n * peer that was already joined at that moment. Empty for a lone\n * newcomer. */\n type: \"peers-present\";\n peerIds: string[];\n }\n | {\n /** Broadcast to every incumbent when a new peer joins. */\n type: \"peer-joined\";\n peerId: string;\n }\n | {\n /** Broadcast to every remaining incumbent when a joined peer\n * closes its socket. Never emitted for a connection that never\n * sent a join frame. */\n type: \"peer-left\";\n peerId: string;\n };\n\nexport interface SignalingServerOptions {\n /** WebSocket route path. Defaults to \"/polly/signaling\". */\n path?: string;\n}\n\n/**\n * Construct the signalling-server Elysia plugin. The plugin keeps a\n * per-instance map of peer id → WebSocket connection so that incoming\n * \"signal\" messages can be routed to the right target socket. The map\n * is local to the plugin instance, not shared across processes; for\n * multi-instance deployments behind a load balancer, applications need\n * sticky connection routing or a shared backplane (Redis pub-sub or\n * similar). That is a follow-up.\n */\nexport function signalingServer(options: SignalingServerOptions = {}) {\n const path = options.path ?? \"/polly/signaling\";\n // Per-peer-id map of joined sockets. The inverse mapping is stored\n // directly on ws.data (a mutable property bag that Elysia preserves\n // across message callbacks for a given connection); the webrtc-p2p-chat\n // example in examples/ confirms this pattern is stable under Bun.\n const peerSockets = new Map<string, { send: (msg: unknown) => void }>();\n\n // Intentionally unnamed — Elysia deduplicates plugins by name, and each\n // signalingServer() call needs its own closure-captured peer map.\n const parseMessage = (raw: unknown): SignalingMessage | undefined => {\n try {\n return typeof raw === \"string\" ? JSON.parse(raw) : (raw as unknown as SignalingMessage);\n } catch {\n return undefined;\n }\n };\n\n const handleJoin = (ws: unknown, peerId: string): void => {\n const newcomer = ws as unknown as { send: (m: unknown) => void };\n // Collect the peers that were already joined so we can (a) tell the\n // newcomer who is present and (b) tell each of them about the\n // newcomer. A rejoin with the same peerId replaces the prior entry\n // but is otherwise treated as a fresh arrival.\n const incumbents: Array<{ peerId: string; socket: { send: (m: unknown) => void } }> = [];\n for (const [existingPeerId, existingSocket] of peerSockets) {\n if (existingPeerId === peerId) continue;\n incumbents.push({ peerId: existingPeerId, socket: existingSocket });\n }\n peerSockets.set(peerId, newcomer);\n const wsWithData = ws as unknown as { data: Record<string, unknown> };\n wsWithData.data.peerId = peerId;\n\n newcomer.send({\n type: \"peers-present\",\n peerIds: incumbents.map((i) => i.peerId),\n } as unknown as SignalingMessage);\n\n for (const incumbent of incumbents) {\n try {\n incumbent.socket.send({ type: \"peer-joined\", peerId } as unknown as SignalingMessage);\n } catch {\n // If a send fails we leave the stale socket to its own close\n // handler to evict. Dropping here would open a window where\n // the next signal to this peer still thinks it's alive.\n }\n }\n };\n\n const sendUnknownTarget = (ws: unknown, targetPeerId: string): void => {\n (ws as unknown as { send: (m: unknown) => void }).send({\n type: \"error\",\n reason: \"unknown-target\",\n targetPeerId,\n } as unknown as SignalingMessage);\n };\n\n /** Look up a target socket and confirm it is still open. */\n const findOpenTarget = (targetPeerId: string): { send: (msg: unknown) => void } | undefined => {\n const target = peerSockets.get(targetPeerId);\n if (!target) return undefined;\n const readyState = (target as unknown as { readyState?: number }).readyState;\n const OPEN = 1;\n if (readyState !== undefined && readyState !== OPEN) {\n peerSockets.delete(targetPeerId);\n return undefined;\n }\n return target;\n };\n\n const handleSignal = (ws: unknown, msg: Extract<SignalingMessage, { type: \"signal\" }>): void => {\n const wsWithData = ws as unknown as {\n data: Record<string, unknown>;\n send: (m: unknown) => void;\n };\n const senderId = wsWithData.data.peerId as unknown as string | undefined;\n if (!senderId) {\n wsWithData.send({ type: \"error\", reason: \"not-joined\" } as unknown as SignalingMessage);\n return;\n }\n const target = findOpenTarget(msg.targetPeerId);\n if (!target) {\n sendUnknownTarget(ws, msg.targetPeerId);\n return;\n }\n const relayed: SignalingMessage = {\n type: \"signal\",\n peerId: senderId,\n targetPeerId: msg.targetPeerId,\n payload: msg.payload,\n };\n try {\n target.send(relayed);\n } catch {\n peerSockets.delete(msg.targetPeerId);\n sendUnknownTarget(ws, msg.targetPeerId);\n }\n };\n\n return new Elysia().ws(path, {\n message(ws, raw) {\n const msg = parseMessage(raw);\n if (!msg) {\n ws.send({ type: \"error\", reason: \"malformed\" } as unknown as SignalingMessage);\n return;\n }\n if (msg.type === \"join\") {\n handleJoin(ws, msg.peerId);\n return;\n }\n if (msg.type === \"signal\") {\n handleSignal(ws, msg);\n return;\n }\n ws.send({ type: \"error\", reason: \"malformed\" } as unknown as SignalingMessage);\n },\n\n close(ws) {\n const peerId = (ws.data as unknown as Record<string, unknown>).peerId as unknown as\n | string\n | undefined;\n if (!peerId) {\n // Connection that never sent a join — nothing to broadcast and\n // nothing to evict. A bystander coming and going leaves no trace.\n return;\n }\n // Only evict if the map still points at *this* socket. A stale\n // close after the same peerId rejoined on a new socket should not\n // take the fresh entry with it. The comparison uses the `data` bag\n // Elysia attaches to each connection because it is preserved across\n // message and close callbacks, unlike the `ws` wrapper object which\n // Elysia may or may not reuse.\n const mapped = peerSockets.get(peerId);\n const wsData = (ws as unknown as { data: Record<string, unknown> }).data;\n const mappedData = (mapped as unknown as { data?: Record<string, unknown> } | undefined)\n ?.data;\n if (mapped === undefined || mappedData !== wsData) {\n return;\n }\n peerSockets.delete(peerId);\n for (const [_incumbentId, incumbentSocket] of peerSockets) {\n try {\n incumbentSocket.send({ type: \"peer-left\", peerId } as unknown as SignalingMessage);\n } catch {\n // Incumbent socket is gone; its own close handler will tidy.\n }\n }\n },\n });\n}\n"
11
11
  ],
12
- "mappings": "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",
13
- "debugId": "ABCACD6B736E00DA64756E2164756E21",
12
+ "mappings": "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",
13
+ "debugId": "3F789E611CD3A45864756E2164756E21",
14
14
  "names": []
15
15
  }
@@ -49,8 +49,10 @@
49
49
  * ```
50
50
  */
51
51
  import { Elysia } from "elysia";
52
- /** A signalling message. The `type` discriminates between join (peer
53
- * registration), signal (relayed message), and error (server response). */
52
+ /** A signalling message. The `type` discriminates between client-to-server
53
+ * requests (join, signal), the error envelope, and the server-to-client
54
+ * discovery frames (peers-present, peer-joined, peer-left) that let
55
+ * peers learn about each other without polling. */
54
56
  export type SignalingMessage = {
55
57
  type: "join";
56
58
  /** The peer registering itself with the signalling server. */
@@ -67,6 +69,22 @@ export type SignalingMessage = {
67
69
  type: "error";
68
70
  reason: "unknown-target" | "not-joined" | "malformed";
69
71
  targetPeerId?: string;
72
+ } | {
73
+ /** Sent to a newcomer immediately after it joins, listing every
74
+ * peer that was already joined at that moment. Empty for a lone
75
+ * newcomer. */
76
+ type: "peers-present";
77
+ peerIds: string[];
78
+ } | {
79
+ /** Broadcast to every incumbent when a new peer joins. */
80
+ type: "peer-joined";
81
+ peerId: string;
82
+ } | {
83
+ /** Broadcast to every remaining incumbent when a joined peer
84
+ * closes its socket. Never emitted for a connection that never
85
+ * sent a join frame. */
86
+ type: "peer-left";
87
+ peerId: string;
70
88
  };
71
89
  export interface SignalingServerOptions {
72
90
  /** WebSocket route path. Defaults to "/polly/signaling". */
package/dist/src/mesh.js CHANGED
@@ -1168,6 +1168,9 @@ class MeshSignalingClient {
1168
1168
  onError;
1169
1169
  onOpen;
1170
1170
  onClose;
1171
+ onPeersPresent;
1172
+ onPeerJoined;
1173
+ onPeerLeft;
1171
1174
  socket;
1172
1175
  joined = false;
1173
1176
  WebSocketCtor;
@@ -1181,6 +1184,12 @@ class MeshSignalingClient {
1181
1184
  this.onOpen = options.onOpen;
1182
1185
  if (options.onClose !== undefined)
1183
1186
  this.onClose = options.onClose;
1187
+ if (options.onPeersPresent !== undefined)
1188
+ this.onPeersPresent = options.onPeersPresent;
1189
+ if (options.onPeerJoined !== undefined)
1190
+ this.onPeerJoined = options.onPeerJoined;
1191
+ if (options.onPeerLeft !== undefined)
1192
+ this.onPeerLeft = options.onPeerLeft;
1184
1193
  const WS = options.WebSocket ?? globalThis.WebSocket;
1185
1194
  if (typeof WS !== "function") {
1186
1195
  throw new Error("MeshSignalingClient: no WebSocket implementation found. Pass one via options.WebSocket, or run in an environment where `globalThis.WebSocket` exists (Node 21+, Bun, browsers).");
@@ -1198,19 +1207,7 @@ class MeshSignalingClient {
1198
1207
  resolve();
1199
1208
  });
1200
1209
  ws.addEventListener("message", (event) => {
1201
- let msg;
1202
- try {
1203
- msg = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
1204
- } catch {
1205
- return;
1206
- }
1207
- if (msg.type === "signal" && typeof msg.peerId === "string") {
1208
- this.onSignal(msg.peerId, msg.payload);
1209
- return;
1210
- }
1211
- if (msg.type === "error" && msg.reason) {
1212
- this.onError?.(msg.reason, msg.targetPeerId);
1213
- }
1210
+ this.dispatchFrame(event.data);
1214
1211
  });
1215
1212
  ws.addEventListener("error", (err) => {
1216
1213
  reject(err);
@@ -1221,6 +1218,38 @@ class MeshSignalingClient {
1221
1218
  });
1222
1219
  });
1223
1220
  }
1221
+ dispatchFrame(raw) {
1222
+ let msg;
1223
+ try {
1224
+ msg = typeof raw === "string" ? JSON.parse(raw) : raw;
1225
+ } catch {
1226
+ return;
1227
+ }
1228
+ switch (msg.type) {
1229
+ case "signal":
1230
+ if (typeof msg.peerId === "string")
1231
+ this.onSignal(msg.peerId, msg.payload);
1232
+ return;
1233
+ case "peers-present":
1234
+ if (Array.isArray(msg.peerIds))
1235
+ this.onPeersPresent?.(msg.peerIds);
1236
+ return;
1237
+ case "peer-joined":
1238
+ if (typeof msg.peerId === "string")
1239
+ this.onPeerJoined?.(msg.peerId);
1240
+ return;
1241
+ case "peer-left":
1242
+ if (typeof msg.peerId === "string")
1243
+ this.onPeerLeft?.(msg.peerId);
1244
+ return;
1245
+ case "error":
1246
+ if (msg.reason)
1247
+ this.onError?.(msg.reason, msg.targetPeerId);
1248
+ return;
1249
+ default:
1250
+ return;
1251
+ }
1252
+ }
1224
1253
  sendSignal(targetPeerId, payload) {
1225
1254
  if (!this.socket || this.socket.readyState !== this.WebSocketCtor.OPEN || !this.joined) {
1226
1255
  return false;
@@ -1244,6 +1273,13 @@ class MeshSignalingClient {
1244
1273
  }
1245
1274
  }
1246
1275
 
1276
+ // src/shared/lib/mesh-state.ts
1277
+ import {
1278
+ Automerge,
1279
+ interpretAsDocumentId
1280
+ } from "@automerge/automerge-repo/slim";
1281
+ import nacl3 from "tweetnacl";
1282
+
1247
1283
  // src/shared/lib/crdt-specialised.ts
1248
1284
  import { Counter, updateText } from "@automerge/automerge-repo/slim";
1249
1285
  import { effect, signal } from "@preact/signals";
@@ -1620,11 +1656,9 @@ function applyTopLevel(doc, value) {
1620
1656
  }
1621
1657
 
1622
1658
  // src/shared/lib/mesh-state.ts
1623
- var keyMapsByRepo = new WeakMap;
1624
1659
  var defaultRepo;
1625
1660
  function configureMeshState(repo) {
1626
1661
  defaultRepo = repo;
1627
- keyMapsByRepo.set(repo, new Map);
1628
1662
  }
1629
1663
  function resetMeshState() {
1630
1664
  defaultRepo = undefined;
@@ -1636,23 +1670,30 @@ function resolveRepo(option) {
1636
1670
  }
1637
1671
  return repo;
1638
1672
  }
1639
- function getKeyMap(repo) {
1640
- let map = keyMapsByRepo.get(repo);
1641
- if (!map) {
1642
- map = new Map;
1643
- keyMapsByRepo.set(repo, map);
1644
- }
1645
- return map;
1673
+ var DOC_ID_DOMAIN = "polly/meshState/v1";
1674
+ var keyEncoder = new TextEncoder;
1675
+ function deriveDocumentId(key) {
1676
+ const digest = nacl3.hash(keyEncoder.encode(`${DOC_ID_DOMAIN}:${key}`));
1677
+ const bytes = digest.slice(0, 16);
1678
+ return interpretAsDocumentId(bytes);
1646
1679
  }
1647
1680
  function buildHandleFactory(repo, key, initialDoc) {
1681
+ const documentId = deriveDocumentId(key);
1648
1682
  return async () => {
1649
- const map = getKeyMap(repo);
1650
- const existingId = map.get(key);
1651
- if (existingId !== undefined) {
1652
- return repo.find(existingId);
1683
+ const cached = repo.handles[documentId];
1684
+ if (cached) {
1685
+ await cached.whenReady(["ready", "unavailable"]);
1686
+ if (cached.state === "ready") {
1687
+ return cached;
1688
+ }
1653
1689
  }
1654
- const handle = repo.create(initialDoc);
1655
- map.set(key, handle.documentId);
1690
+ const stored = await repo.storageSubsystem?.loadDoc(documentId);
1691
+ if (stored) {
1692
+ return repo.find(documentId, { allowableStates: ["ready"] });
1693
+ }
1694
+ const seeded = Automerge.save(Automerge.from(initialDoc));
1695
+ const handle = repo.import(seeded, { docId: documentId });
1696
+ handle.doneLoading();
1656
1697
  return handle;
1657
1698
  };
1658
1699
  }
@@ -1713,6 +1754,7 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
1713
1754
  iceServers;
1714
1755
  dataChannelLabel;
1715
1756
  knownPeerIds;
1757
+ localPeerId;
1716
1758
  RTCPeerConnectionCtor;
1717
1759
  slots = new Map;
1718
1760
  ready = false;
@@ -1724,6 +1766,7 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
1724
1766
  this.iceServers = options.iceServers ?? DEFAULT_ICE_SERVERS;
1725
1767
  this.dataChannelLabel = options.dataChannelLabel ?? "polly-mesh";
1726
1768
  this.knownPeerIds = options.knownPeerIds ?? [];
1769
+ this.localPeerId = options.peerId;
1727
1770
  const PC = options.RTCPeerConnection ?? globalThis.RTCPeerConnection;
1728
1771
  if (typeof PC !== "function") {
1729
1772
  throw new Error("MeshWebRTCAdapter: no RTCPeerConnection implementation found. Pass one via options.RTCPeerConnection (e.g. from `werift` or `@roamhq/wrtc`), or run in a browser where `globalThis.RTCPeerConnection` exists.");
@@ -1733,6 +1776,40 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
1733
1776
  isReady() {
1734
1777
  return this.ready;
1735
1778
  }
1779
+ peerSlotCount() {
1780
+ return this.slots.size;
1781
+ }
1782
+ handlePeerJoined(remotePeerId) {
1783
+ if (!this.shouldInitiateTo(remotePeerId))
1784
+ return;
1785
+ this.createInitiatingSlot(remotePeerId);
1786
+ }
1787
+ handlePeersPresent(peerIds) {
1788
+ for (const remotePeerId of peerIds) {
1789
+ if (!this.shouldInitiateTo(remotePeerId))
1790
+ continue;
1791
+ this.createInitiatingSlot(remotePeerId);
1792
+ }
1793
+ }
1794
+ handlePeerLeft(remotePeerId) {
1795
+ const slot = this.slots.get(remotePeerId);
1796
+ if (!slot)
1797
+ return;
1798
+ slot.channel?.close();
1799
+ slot.connection.close();
1800
+ this.slots.delete(remotePeerId);
1801
+ }
1802
+ shouldInitiateTo(remotePeerId) {
1803
+ if (remotePeerId === this.localPeerId)
1804
+ return false;
1805
+ if (!this.knownPeerIds.includes(remotePeerId))
1806
+ return false;
1807
+ if (this.slots.has(remotePeerId))
1808
+ return false;
1809
+ if (this.localPeerId <= remotePeerId)
1810
+ return false;
1811
+ return true;
1812
+ }
1736
1813
  whenReady() {
1737
1814
  if (this.ready)
1738
1815
  return Promise.resolve();
@@ -1747,11 +1824,6 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
1747
1824
  }
1748
1825
  this.ready = true;
1749
1826
  this.readyResolver?.();
1750
- for (const remotePeerId of this.knownPeerIds) {
1751
- if (remotePeerId !== peerId && !this.slots.has(remotePeerId)) {
1752
- this.createInitiatingSlot(remotePeerId);
1753
- }
1754
- }
1755
1827
  }
1756
1828
  disconnect() {
1757
1829
  for (const slot of this.slots.values()) {
@@ -1994,6 +2066,15 @@ async function createMeshClient(options) {
1994
2066
  ...options.signaling.onError !== undefined && { onError: options.signaling.onError },
1995
2067
  onSignal: (fromPeerId, payload) => {
1996
2068
  webrtcAdapter?.handleSignal(fromPeerId, payload);
2069
+ },
2070
+ onPeersPresent: (peerIds) => {
2071
+ webrtcAdapter?.handlePeersPresent(peerIds);
2072
+ },
2073
+ onPeerJoined: (peerId) => {
2074
+ webrtcAdapter?.handlePeerJoined(peerId);
2075
+ },
2076
+ onPeerLeft: (peerId) => {
2077
+ webrtcAdapter?.handlePeerLeft(peerId);
1997
2078
  }
1998
2079
  });
1999
2080
  webrtcAdapterOptions.signaling = signaling;
@@ -2426,4 +2507,4 @@ export {
2426
2507
  $meshCounter
2427
2508
  };
2428
2509
 
2429
- //# debugId=6CA75FA83D35A2A964756E2164756E21
2510
+ //# debugId=60DD954C8C33F57F64756E2164756E21