@fieldnotes/sync-server 0.12.0 → 0.13.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/dist/index.cjs CHANGED
@@ -101,6 +101,7 @@ var DEFAULT_MAX_PENDING_AUTH_BYTES = 2 * 1024 * 1024;
101
101
  var DEFAULT_MESSAGES_PER_SECOND = 120;
102
102
  var DEFAULT_MESSAGE_BURST = 240;
103
103
  var DEFAULT_PRESENCE_THROTTLE_MS = 50;
104
+ var DEFAULT_MAX_PRESENCE_LANES = 16;
104
105
  function hasJsonDepthAtMost(message, maxDepth) {
105
106
  let depth = 0;
106
107
  let inString = false;
@@ -156,6 +157,16 @@ function isFanoutOp(op) {
156
157
  if (o.kind === "remove") return typeof o.id === "string";
157
158
  return o.kind === "clear";
158
159
  }
160
+ var FALLBACK_PRESENCE_LANE = "";
161
+ var MAX_PRESENCE_LANE_LENGTH = 64;
162
+ function presenceLaneOf(data) {
163
+ if (typeof data !== "object" || data === null) return FALLBACK_PRESENCE_LANE;
164
+ const kind = data.kind;
165
+ if (typeof kind !== "string" || kind.length === 0 || kind.length > MAX_PRESENCE_LANE_LENGTH) {
166
+ return FALLBACK_PRESENCE_LANE;
167
+ }
168
+ return kind;
169
+ }
159
170
  function isLayerOp(op) {
160
171
  if (typeof op !== "object" || op === null) return false;
161
172
  const k = op.kind;
@@ -195,8 +206,17 @@ var SyncHub = class {
195
206
  memoryLayers = /* @__PURE__ */ new Map();
196
207
  maxJsonDepth;
197
208
  presenceThrottleMs;
198
- lastPresenceAt = /* @__PURE__ */ new Map();
199
- pendingPresence = /* @__PURE__ */ new Map();
209
+ maxPresenceLanes;
210
+ /**
211
+ * Presence throttle state keyed by connection, then by lane. A lane is the
212
+ * payload's `kind` (a non-empty string of at most 64 chars) or the reserved
213
+ * fallback lane `''`, so a rapid stream of one kind (awareness cursors) can
214
+ * never replace a pending frame of another kind (a ping, a path `cleared`).
215
+ * Within a lane the newest payload wins. The lane count per connection is
216
+ * capped by `maxPresenceLanes`, counting the fallback lane, so a client
217
+ * cannot mint timers by varying `kind`.
218
+ */
219
+ presenceLanes = /* @__PURE__ */ new Map();
200
220
  constructor(options = {}) {
201
221
  this.backend = options.backend ?? new MemoryHubBackend();
202
222
  this.instanceId = options.instanceId ?? generateInstanceId();
@@ -206,6 +226,11 @@ var SyncHub = class {
206
226
  this.canRead = options.canRead;
207
227
  this.maxJsonDepth = options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH;
208
228
  this.presenceThrottleMs = options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS;
229
+ const maxPresenceLanes = options.maxPresenceLanes ?? DEFAULT_MAX_PRESENCE_LANES;
230
+ if (!Number.isFinite(maxPresenceLanes) || maxPresenceLanes < 1) {
231
+ throw new RangeError("maxPresenceLanes must be a finite number of at least 1");
232
+ }
233
+ this.maxPresenceLanes = Math.floor(maxPresenceLanes);
209
234
  this.fanoutUnsub = this.fanout.subscribe((payload) => this.onFanout(payload));
210
235
  }
211
236
  addConnection(conn) {
@@ -223,10 +248,7 @@ var SyncHub = class {
223
248
  this.conns.delete(connId);
224
249
  const room = conn.room;
225
250
  const hadPresence = this.presenceConnections.delete(connId);
226
- this.lastPresenceAt.delete(connId);
227
- const pendingPresence = this.pendingPresence.get(connId);
228
- if (pendingPresence) clearTimeout(pendingPresence.timer);
229
- this.pendingPresence.delete(connId);
251
+ this.clearPresenceLanes(connId);
230
252
  const members = this.rooms.get(room);
231
253
  if (members) {
232
254
  members.delete(connId);
@@ -432,34 +454,63 @@ var SyncHub = class {
432
454
  })
433
455
  );
434
456
  }
457
+ clearPresenceLanes(connId) {
458
+ const lanes = this.presenceLanes.get(connId);
459
+ if (!lanes) return;
460
+ for (const lane of lanes.values()) {
461
+ if (lane.pending) clearTimeout(lane.pending.timer);
462
+ }
463
+ this.presenceLanes.delete(connId);
464
+ }
435
465
  schedulePresence(conn, data) {
436
466
  if (this.presenceThrottleMs <= 0) {
437
467
  this.broadcastClientPresence(conn, data);
438
468
  return;
439
469
  }
470
+ const lane = this.presenceLaneFor(conn.id, presenceLaneOf(data));
440
471
  const now = Date.now();
441
- const lastSentAt = this.lastPresenceAt.get(conn.id);
442
- if (lastSentAt === void 0 || now - lastSentAt >= this.presenceThrottleMs) {
443
- this.lastPresenceAt.set(conn.id, now);
472
+ if (lane.lastSentAt === void 0 || now - lane.lastSentAt >= this.presenceThrottleMs) {
473
+ if (lane.pending) {
474
+ clearTimeout(lane.pending.timer);
475
+ lane.pending = null;
476
+ }
477
+ lane.lastSentAt = now;
444
478
  this.broadcastClientPresence(conn, data);
445
479
  return;
446
480
  }
447
- const existing = this.pendingPresence.get(conn.id);
448
- if (existing) {
449
- existing.data = data;
481
+ if (lane.pending) {
482
+ lane.pending.data = data;
450
483
  return;
451
484
  }
452
485
  const timer = setTimeout(
453
486
  () => {
454
- const pending = this.pendingPresence.get(conn.id);
455
- this.pendingPresence.delete(conn.id);
487
+ const pending = lane.pending;
488
+ lane.pending = null;
456
489
  if (!pending || !this.conns.has(conn.id)) return;
457
- this.lastPresenceAt.set(conn.id, Date.now());
490
+ lane.lastSentAt = Date.now();
458
491
  this.broadcastClientPresence(conn, pending.data);
459
492
  },
460
- this.presenceThrottleMs - (now - lastSentAt)
493
+ this.presenceThrottleMs - (now - lane.lastSentAt)
461
494
  );
462
- this.pendingPresence.set(conn.id, { data, timer });
495
+ lane.pending = { data, timer };
496
+ }
497
+ presenceLaneFor(connId, requested) {
498
+ let lanes = this.presenceLanes.get(connId);
499
+ if (!lanes) {
500
+ lanes = /* @__PURE__ */ new Map();
501
+ this.presenceLanes.set(connId, lanes);
502
+ }
503
+ let key = requested;
504
+ if (key !== FALLBACK_PRESENCE_LANE && !lanes.has(key)) {
505
+ const named = lanes.size - (lanes.has(FALLBACK_PRESENCE_LANE) ? 1 : 0);
506
+ if (named >= this.maxPresenceLanes - 1) key = FALLBACK_PRESENCE_LANE;
507
+ }
508
+ let lane = lanes.get(key);
509
+ if (!lane) {
510
+ lane = { lastSentAt: void 0, pending: null };
511
+ lanes.set(key, lane);
512
+ }
513
+ return lane;
463
514
  }
464
515
  broadcastLeave(room, from) {
465
516
  const message = JSON.stringify({ from, op: { kind: "presence-leave" } });
@@ -555,8 +606,7 @@ var SyncHub = class {
555
606
  await this.applyLayerRecord(room, record);
556
607
  }
557
608
  close() {
558
- for (const pending of this.pendingPresence.values()) clearTimeout(pending.timer);
559
- this.pendingPresence.clear();
609
+ for (const connId of [...this.presenceLanes.keys()]) this.clearPresenceLanes(connId);
560
610
  this.fanoutUnsub();
561
611
  }
562
612
  };
@@ -647,7 +697,8 @@ function createSyncServer(options = {}) {
647
697
  authorizeLayer: options.authorizeLayer,
648
698
  canRead: options.canRead,
649
699
  maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,
650
- presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS
700
+ presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,
701
+ maxPresenceLanes: options.maxPresenceLanes
651
702
  });
652
703
  const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
653
704
  const wss = options.server ? new import_ws.WebSocketServer({ server: options.server, maxPayload: maxMessageBytes }) : new import_ws.WebSocketServer({ port: options.port ?? 0, maxPayload: maxMessageBytes });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/sync-hub.ts","../src/memory-hub-backend.ts","../src/hub-fanout.ts","../src/resource-limits.ts","../src/create-sync-server.ts","../src/heartbeat.ts","../src/shutdown.ts"],"sourcesContent":["export { SyncHub } from './sync-hub';\nexport type { SyncHubOptions, Connection } from './sync-hub';\nexport { MemoryHubBackend } from './memory-hub-backend';\nexport type { HubBackend } from './hub-backend';\nexport { createSyncServer } from './create-sync-server';\nexport type { CreateSyncServerOptions } from './create-sync-server';\nexport { InMemoryHubFanout } from './hub-fanout';\nexport type { HubFanout } from './hub-fanout';\nexport type { AuthInfo, AuthResult, Authenticate } from './authenticate';\nexport type {\n Authorize,\n AuthorizeContext,\n AuthorizeLayer,\n AuthorizeLayerContext,\n OwnedElement,\n ReadContext,\n CanRead,\n} from './authorize';\nexport { startHeartbeat } from './heartbeat';\nexport type { Heartbeat, HeartbeatSocket, HeartbeatServer } from './heartbeat';\n","import {\n parseEnvelope,\n isValidElement,\n isNewerLayerRecord,\n type LayerRecord,\n type SyncOp,\n type SyncEnvelope,\n} from '@fieldnotes/sync';\nimport { MemoryHubBackend } from './memory-hub-backend';\nimport { InMemoryHubFanout, type HubFanout } from './hub-fanout';\nimport type { HubBackend } from './hub-backend';\nimport type { Authorize, AuthorizeLayer, CanRead, OwnedElement } from './authorize';\nimport {\n DEFAULT_MAX_JSON_DEPTH,\n DEFAULT_PRESENCE_THROTTLE_MS,\n hasJsonDepthAtMost,\n} from './resource-limits';\n\nexport interface Connection {\n id: string;\n room: string;\n userId?: string;\n role?: string;\n send(message: string): void;\n}\n\nexport interface SyncHubOptions {\n backend?: HubBackend;\n fanout?: HubFanout;\n instanceId?: string;\n authorize?: Authorize;\n authorizeLayer?: AuthorizeLayer;\n canRead?: CanRead;\n maxJsonDepth?: number;\n presenceThrottleMs?: number;\n}\n\nconst HUB_FROM = 'hub';\n\nfunction generateInstanceId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')\n return crypto.randomUUID();\n return `i-${Math.random().toString(36).slice(2)}`;\n}\n\nfunction isFanoutOp(op: unknown): op is Extract<SyncOp, { kind: 'upsert' | 'remove' | 'clear' }> {\n if (typeof op !== 'object' || op === null) return false;\n const o = op as { kind?: unknown; element?: unknown; id?: unknown };\n if (o.kind === 'upsert') return isValidElement(o.element);\n if (o.kind === 'remove') return typeof o.id === 'string';\n return o.kind === 'clear';\n}\n\ntype LayerOp = Extract<SyncOp, { kind: 'layer-upsert' | 'layer-remove' }>;\n\nfunction isLayerOp(op: unknown): op is LayerOp {\n if (typeof op !== 'object' || op === null) return false;\n const k = (op as { kind?: unknown }).kind;\n // Shape is re-validated by parseEnvelope on the serial path; fanout payloads\n // come from a sibling hub that already validated them.\n return k === 'layer-upsert' || k === 'layer-remove';\n}\n\nfunction layerOpToRecord(op: LayerOp): LayerRecord {\n return op.kind === 'layer-upsert'\n ? { id: op.layer.id, version: op.version, editor: op.editor, definition: op.layer }\n : { id: op.id, version: op.version, editor: op.editor };\n}\n\nfunction layerRecordToOp(record: LayerRecord): LayerOp {\n return record.definition\n ? {\n kind: 'layer-upsert',\n layer: record.definition,\n version: record.version,\n editor: record.editor,\n }\n : { kind: 'layer-remove', id: record.id, version: record.version, editor: record.editor };\n}\n\nfunction isPresenceOp(\n op: unknown,\n): op is { kind: 'presence'; data: unknown } | { kind: 'presence-leave' } {\n if (typeof op !== 'object' || op === null) return false;\n const k = (op as { kind?: unknown }).kind;\n return k === 'presence' || k === 'presence-leave';\n}\n\nexport class SyncHub {\n private readonly backend: HubBackend;\n private readonly conns = new Map<string, Connection>();\n private readonly rooms = new Map<string, Set<string>>(); // room → connIds\n private readonly roomQueues = new Map<string, Promise<void>>(); // room → serial tail\n private readonly presenceConnections = new Set<string>();\n private readonly instanceId: string;\n private readonly fanout: HubFanout;\n private readonly fanoutUnsub: () => void;\n private readonly authorize?: Authorize;\n private readonly authorizeLayer?: AuthorizeLayer;\n private readonly canRead?: CanRead;\n /** Fallback layer-record store for backends without layer persistence. */\n private readonly memoryLayers = new Map<string, Map<string, LayerRecord>>();\n private readonly maxJsonDepth: number;\n private readonly presenceThrottleMs: number;\n private readonly lastPresenceAt = new Map<string, number>();\n private readonly pendingPresence = new Map<\n string,\n { data: unknown; timer: ReturnType<typeof setTimeout> }\n >();\n\n constructor(options: SyncHubOptions = {}) {\n this.backend = options.backend ?? new MemoryHubBackend();\n this.instanceId = options.instanceId ?? generateInstanceId();\n this.fanout = options.fanout ?? new InMemoryHubFanout();\n this.authorize = options.authorize;\n this.authorizeLayer = options.authorizeLayer;\n this.canRead = options.canRead;\n this.maxJsonDepth = options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH;\n this.presenceThrottleMs = options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS;\n this.fanoutUnsub = this.fanout.subscribe((payload) => this.onFanout(payload));\n }\n\n addConnection(conn: Connection): void {\n this.conns.set(conn.id, conn);\n let set = this.rooms.get(conn.room);\n if (!set) {\n set = new Set();\n this.rooms.set(conn.room, set);\n }\n set.add(conn.id);\n }\n\n removeConnection(connId: string): void {\n const conn = this.conns.get(connId);\n if (!conn) return;\n this.conns.delete(connId);\n const room = conn.room;\n const hadPresence = this.presenceConnections.delete(connId);\n this.lastPresenceAt.delete(connId);\n const pendingPresence = this.pendingPresence.get(connId);\n if (pendingPresence) clearTimeout(pendingPresence.timer);\n this.pendingPresence.delete(connId);\n const members = this.rooms.get(room);\n if (members) {\n members.delete(connId);\n if (members.size === 0) {\n this.rooms.delete(room);\n this.roomQueues.delete(room);\n }\n }\n if (hadPresence) this.broadcastLeave(room, conn.id);\n }\n\n roomCount(): number {\n return this.rooms.size;\n }\n\n /**\n * Broadcasts ephemeral server-owned presence data to every connection in a room.\n * The returned count covers successful delivery on this hub instance only; configured fan-out\n * forwards the same event to other instances on a best-effort basis.\n */\n broadcastPresence<T>(room: string, data: T): number {\n const op = { kind: 'presence' as const, data };\n const sent = this.relayToRoom(room, undefined, JSON.stringify({ from: HUB_FROM, op }));\n this.safePublish(JSON.stringify({ o: this.instanceId, room, from: HUB_FROM, op }));\n return sent;\n }\n\n handleMessage(connId: string, message: string): Promise<void> {\n const conn = this.conns.get(connId);\n if (!conn) return Promise.resolve();\n if (!hasJsonDepthAtMost(message, this.maxJsonDepth)) return Promise.resolve();\n const env = parseEnvelope(message);\n if (!env) return Promise.resolve();\n if (env.op.kind === 'presence') {\n this.schedulePresence(conn, env.op.data); // off-queue, throttled independently\n return Promise.resolve();\n }\n const room = conn.room;\n // The per-room serial queue is the single total-order authority: ops apply in arrival order\n // (arrival-order LWW — no per-element seq; see D3 / TD-12). Different rooms run independently.\n const prev = this.roomQueues.get(room) ?? Promise.resolve();\n const operation = prev.then(() => this.process(conn, env));\n this.roomQueues.set(\n room,\n operation.catch(() => {\n // Recover only the internal tail so one failed message never wedges the room queue.\n // The caller still receives the operation rejection for observability.\n }),\n );\n return operation;\n }\n\n private async process(conn: Connection, env: SyncEnvelope): Promise<void> {\n const op = env.op;\n if (op.kind === 'request-snapshot') {\n const all = (await this.backend.snapshot(conn.room)) as OwnedElement[];\n const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;\n // Layer records are presentation-only and carry no element bytes, so no\n // audience filter applies; the field is omitted while a room has never\n // used layer sync, keeping snapshot frames byte-identical to before.\n const layers = await this.getLayerRecords(conn.room);\n conn.send(\n // `to` is a private correlation address for the requesting SyncClient. It is never\n // broadcast; every public sender identity below comes from the server-owned connection.\n JSON.stringify({\n from: HUB_FROM,\n op:\n layers.length > 0\n ? { kind: 'snapshot', to: env.from, elements, layers }\n : { kind: 'snapshot', to: env.from, elements },\n }),\n );\n } else if (op.kind === 'layer-upsert' || op.kind === 'layer-remove') {\n await this.processLayerOp(conn, op);\n } else if (op.kind === 'upsert' || op.kind === 'remove' || op.kind === 'clear') {\n const id = op.kind === 'upsert' ? op.element.id : op.kind === 'remove' ? op.id : undefined;\n const needCurrent = (this.authorize || this.canRead) && id !== undefined;\n const current: OwnedElement | undefined = needCurrent\n ? await this.backend.get(conn.room, id)\n : undefined;\n\n let outboundOp: SyncOp = op;\n if (this.authorize) {\n const allowed = await this.authorize({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n op,\n currentElement: current,\n });\n if (!allowed) {\n await this.sendCorrection(conn, env.from, op, current);\n return;\n }\n if (op.kind === 'upsert') {\n const ownerId = current?.ownerId ?? conn.userId;\n const stampedElement: OwnedElement = { ...op.element, ownerId };\n outboundOp = { kind: 'upsert', element: stampedElement };\n }\n }\n\n await this.backend.apply(conn.room, outboundOp);\n\n const prevExisted = current !== undefined;\n const prevAudience = current?.audience;\n\n await this.fanout.publish(\n JSON.stringify({\n o: this.instanceId,\n room: conn.room,\n from: conn.id,\n op: outboundOp,\n prev: prevAudience,\n existed: prevExisted,\n }),\n );\n\n this.deliverToRoom(conn.room, conn.id, conn.id, outboundOp, prevAudience, prevExisted);\n }\n // 'snapshot' from a client → ignored\n }\n\n /**\n * Applies a layer-definition edit on the room's serial queue. Convergence is\n * last-writer-wins under the deterministic (version, editor) ordering — the\n * same rule every client applies — so arrival order never decides a race. A\n * stale or denied edit is answered with an authoritative correction to the\n * sender only.\n */\n private async processLayerOp(conn: Connection, op: LayerOp): Promise<void> {\n const record = layerOpToRecord(op);\n const current = await this.getLayerRecord(conn.room, record.id);\n if (this.authorizeLayer) {\n const allowed = await this.authorizeLayer({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n op,\n currentRecord: current,\n });\n if (!allowed) {\n // Revert the sender to the room's record; a tombstone when there is\n // none, so the denied local edit disappears everywhere consistently.\n const correction = current ?? { id: record.id, version: record.version, editor: HUB_FROM };\n conn.send(JSON.stringify({ from: HUB_FROM, op: layerRecordToOp(correction) }));\n return;\n }\n }\n if (current && !isNewerLayerRecord(record, current)) {\n // Stale under (version, editor): converge the sender, do not broadcast.\n conn.send(JSON.stringify({ from: HUB_FROM, op: layerRecordToOp(current) }));\n return;\n }\n await this.applyLayerRecord(conn.room, record);\n await this.fanout.publish(\n JSON.stringify({ o: this.instanceId, room: conn.room, from: conn.id, op }),\n );\n this.relayToRoom(conn.room, conn.id, JSON.stringify({ from: conn.id, op }));\n }\n\n private layerBackend(): Required<\n Pick<HubBackend, 'layerRecords' | 'getLayerRecord' | 'applyLayerRecord'>\n > | null {\n const { layerRecords, getLayerRecord, applyLayerRecord } = this.backend;\n if (!layerRecords || !getLayerRecord || !applyLayerRecord) return null;\n return {\n layerRecords: layerRecords.bind(this.backend),\n getLayerRecord: getLayerRecord.bind(this.backend),\n applyLayerRecord: applyLayerRecord.bind(this.backend),\n };\n }\n\n private async getLayerRecords(room: string): Promise<LayerRecord[]> {\n const backend = this.layerBackend();\n if (backend) return backend.layerRecords(room);\n return [...(this.memoryLayers.get(room)?.values() ?? [])];\n }\n\n private async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n const backend = this.layerBackend();\n if (backend) return backend.getLayerRecord(room, id);\n return this.memoryLayers.get(room)?.get(id);\n }\n\n private async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n const backend = this.layerBackend();\n if (backend) {\n await backend.applyLayerRecord(room, record);\n return;\n }\n let map = this.memoryLayers.get(room);\n if (!map) {\n map = new Map();\n this.memoryLayers.set(room, map);\n }\n map.set(record.id, record);\n }\n\n private mayRead(conn: Connection, audience: string | undefined): boolean {\n if (!this.canRead) return true;\n return this.canRead({ userId: conn.userId, role: conn.role, room: conn.room, audience });\n }\n\n private safePublish(payload: string): void {\n try {\n void Promise.resolve(this.fanout.publish(payload)).catch(() => {\n /* presence is ephemeral; a broken publisher must not create an unhandled rejection */\n });\n } catch {\n /* a broken fanout publisher must not break the un-queued presence relay */\n }\n }\n\n private relayToRoom(room: string, excludeId: string | undefined, message: string): number {\n const members = this.rooms.get(room);\n if (!members) return 0;\n let sent = 0;\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n try {\n conn.send(message);\n sent += 1;\n } catch {\n /* a throwing socket must not break the relay loop */\n }\n }\n return sent;\n }\n\n private broadcastClientPresence(conn: Connection, data: unknown): void {\n this.presenceConnections.add(conn.id);\n const message = JSON.stringify({ from: conn.id, op: { kind: 'presence', data } });\n this.relayToRoom(conn.room, conn.id, message);\n this.safePublish(\n JSON.stringify({\n o: this.instanceId,\n room: conn.room,\n from: conn.id,\n op: { kind: 'presence', data },\n }),\n );\n }\n\n private schedulePresence(conn: Connection, data: unknown): void {\n if (this.presenceThrottleMs <= 0) {\n this.broadcastClientPresence(conn, data);\n return;\n }\n const now = Date.now();\n const lastSentAt = this.lastPresenceAt.get(conn.id);\n if (lastSentAt === undefined || now - lastSentAt >= this.presenceThrottleMs) {\n this.lastPresenceAt.set(conn.id, now);\n this.broadcastClientPresence(conn, data);\n return;\n }\n\n const existing = this.pendingPresence.get(conn.id);\n if (existing) {\n existing.data = data;\n return;\n }\n const timer = setTimeout(\n () => {\n const pending = this.pendingPresence.get(conn.id);\n this.pendingPresence.delete(conn.id);\n if (!pending || !this.conns.has(conn.id)) return;\n this.lastPresenceAt.set(conn.id, Date.now());\n this.broadcastClientPresence(conn, pending.data);\n },\n this.presenceThrottleMs - (now - lastSentAt),\n );\n this.pendingPresence.set(conn.id, { data, timer });\n }\n\n private broadcastLeave(room: string, from: string): void {\n const message = JSON.stringify({ from, op: { kind: 'presence-leave' } });\n this.relayToRoom(room, undefined, message);\n this.safePublish(\n JSON.stringify({ o: this.instanceId, room, from, op: { kind: 'presence-leave' } }),\n );\n }\n\n private deliverToRoom(\n room: string,\n excludeId: string | undefined,\n from: string,\n op: SyncOp,\n prevAudience: string | undefined,\n prevExisted: boolean,\n ): void {\n const members = this.rooms.get(room);\n if (!members) return;\n const send = (conn: Connection, msg: string): void => {\n try {\n conn.send(msg);\n } catch {\n /* a throwing socket must not break the delivery loop */\n }\n };\n if (op.kind === 'upsert') {\n const audience = (op.element as OwnedElement).audience;\n const upsertMsg = JSON.stringify({ from, op });\n const removeMsg = JSON.stringify({\n from: HUB_FROM,\n op: { kind: 'remove', id: op.element.id },\n });\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n if (this.mayRead(conn, audience)) send(conn, upsertMsg);\n else if (prevExisted && this.mayRead(conn, prevAudience)) send(conn, removeMsg);\n }\n } else if (op.kind === 'remove') {\n const removeMsg = JSON.stringify({ from, op });\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n // No read filter → forward to all (today's behavior; current/prevExisted aren't fetched without\n // a hook). With canRead, only recipients who could see the removed element get it.\n const wasVisible = !this.canRead || (prevExisted && this.mayRead(conn, prevAudience));\n if (wasVisible) send(conn, removeMsg);\n }\n } else if (op.kind === 'clear') {\n const clearMsg = JSON.stringify({ from, op });\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (conn) send(conn, clearMsg);\n }\n }\n }\n\n private async sendCorrection(\n conn: Connection,\n from: string,\n op: SyncOp,\n current: OwnedElement | undefined,\n ): Promise<void> {\n let correction: SyncOp | undefined;\n if (op.kind === 'upsert') {\n correction = current\n ? this.mayRead(conn, current.audience)\n ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: current.id }\n : { kind: 'remove', id: op.element.id };\n } else if (op.kind === 'remove') {\n correction = current\n ? this.mayRead(conn, current.audience)\n ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: current.id }\n : undefined;\n } else if (op.kind === 'clear') {\n const all = (await this.backend.snapshot(conn.room)) as OwnedElement[];\n const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;\n correction = { kind: 'snapshot', to: from, elements };\n }\n if (correction) conn.send(JSON.stringify({ from: HUB_FROM, op: correction }));\n }\n\n private onFanout(payload: string): void {\n // Off the serial queue on purpose: forward-only (the origin already applied to the SHARED backend),\n // and delivery is already ordered. Re-filter per local member (canRead runs on EVERY instance).\n let env: {\n o?: unknown;\n room?: unknown;\n from?: unknown;\n op?: unknown;\n prev?: unknown;\n existed?: unknown;\n };\n try {\n env = JSON.parse(payload);\n } catch {\n return;\n }\n if (typeof env.o !== 'string' || typeof env.room !== 'string' || typeof env.from !== 'string')\n return;\n if (env.o === this.instanceId) return; // our own publish — already delivered locally\n const op = env.op;\n if (isPresenceOp(op)) {\n // presence/leave: raw forward to all local members (the sender lives on the origin instance),\n // no backend, no canRead filter.\n this.relayToRoom(env.room, undefined, JSON.stringify({ from: env.from, op }));\n return;\n }\n if (isLayerOp(op)) {\n // LWW-guarded local apply keeps memory-fallback instances converged; a\n // shared backend already holds the record (the guarded re-apply is a\n // no-op). Relay is unconditional — receiving clients LWW-drop stale ops.\n void this.applyFanoutLayerOp(env.room, op).catch(() => {\n /* a broken backend must not break the fanout relay */\n });\n this.relayToRoom(env.room, undefined, JSON.stringify({ from: env.from, op }));\n return;\n }\n if (!isFanoutOp(op)) return;\n const prevAudience = typeof env.prev === 'string' ? env.prev : undefined;\n const prevExisted = env.existed === true;\n this.deliverToRoom(env.room, undefined, env.from, op, prevAudience, prevExisted);\n }\n\n private async applyFanoutLayerOp(room: string, op: LayerOp): Promise<void> {\n const record = layerOpToRecord(op);\n const current = await this.getLayerRecord(room, record.id);\n if (current && !isNewerLayerRecord(record, current)) return;\n await this.applyLayerRecord(room, record);\n }\n\n close(): void {\n for (const pending of this.pendingPresence.values()) clearTimeout(pending.timer);\n this.pendingPresence.clear();\n this.fanoutUnsub();\n }\n}\n","import { applyOpToMap, type SyncOp, type LayerRecord } from '@fieldnotes/sync';\nimport type { CanvasElement } from '@fieldnotes/core';\nimport type { HubBackend } from './hub-backend';\n\nexport class MemoryHubBackend implements HubBackend {\n private rooms = new Map<string, Map<string, CanvasElement>>();\n private roomLayers = new Map<string, Map<string, LayerRecord>>();\n\n private room(id: string): Map<string, CanvasElement> {\n let r = this.rooms.get(id);\n if (!r) {\n r = new Map();\n this.rooms.set(id, r);\n }\n return r;\n }\n\n private layers(room: string): Map<string, LayerRecord> {\n let r = this.roomLayers.get(room);\n if (!r) {\n r = new Map();\n this.roomLayers.set(room, r);\n }\n return r;\n }\n\n async snapshot(room: string): Promise<CanvasElement[]> {\n return [...this.room(room).values()];\n }\n\n async get(room: string, id: string): Promise<CanvasElement | undefined> {\n return this.room(room).get(id);\n }\n\n async apply(room: string, op: SyncOp): Promise<void> {\n if (op.kind === 'clear') {\n // Clears elements only; layer records are a separate, longer-lived ledger.\n this.rooms.delete(room);\n return;\n }\n applyOpToMap(this.room(room), op);\n }\n\n async layerRecords(room: string): Promise<LayerRecord[]> {\n return [...this.layers(room).values()];\n }\n\n async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n return this.layers(room).get(id);\n }\n\n async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n this.layers(room).set(record.id, record);\n }\n}\n","export interface HubFanout {\n publish(payload: string): void | Promise<void>;\n subscribe(handler: (payload: string) => void): () => void;\n close?(): void;\n}\n\nexport class InMemoryHubFanout implements HubFanout {\n private readonly handlers = new Set<(payload: string) => void>();\n\n publish(payload: string): void {\n for (const h of this.handlers) {\n try {\n h(payload);\n } catch {\n /* one throwing subscriber must not break the publish loop */\n }\n }\n }\n\n subscribe(handler: (payload: string) => void): () => void {\n this.handlers.add(handler);\n return () => this.handlers.delete(handler);\n }\n}\n","export const DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024;\nexport const DEFAULT_MAX_JSON_DEPTH = 32;\nexport const DEFAULT_MAX_PENDING_AUTH_MESSAGES = 100;\nexport const DEFAULT_MAX_PENDING_AUTH_BYTES = 2 * 1024 * 1024;\nexport const DEFAULT_MESSAGES_PER_SECOND = 120;\nexport const DEFAULT_MESSAGE_BURST = 240;\nexport const DEFAULT_PRESENCE_THROTTLE_MS = 50;\n\nexport function hasJsonDepthAtMost(message: string, maxDepth: number): boolean {\n let depth = 0;\n let inString = false;\n let escaped = false;\n\n for (const char of message) {\n if (inString) {\n if (escaped) escaped = false;\n else if (char === '\\\\') escaped = true;\n else if (char === '\"') inString = false;\n continue;\n }\n if (char === '\"') inString = true;\n else if (char === '{' || char === '[') {\n depth += 1;\n if (depth > maxDepth) return false;\n } else if (char === '}' || char === ']') {\n depth -= 1;\n if (depth < 0) return false;\n }\n }\n\n return depth === 0 && !inString;\n}\n\nexport class MessageRateLimiter {\n private tokens: number;\n private updatedAt: number;\n\n constructor(\n private readonly ratePerSecond: number,\n private readonly burst: number,\n now = Date.now(),\n ) {\n this.tokens = burst;\n this.updatedAt = now;\n }\n\n take(now = Date.now()): boolean {\n const elapsedSeconds = Math.max(0, now - this.updatedAt) / 1000;\n this.tokens = Math.min(this.burst, this.tokens + elapsedSeconds * this.ratePerSecond);\n this.updatedAt = now;\n if (this.tokens < 1) return false;\n this.tokens -= 1;\n return true;\n }\n}\n","import { WebSocketServer, type RawData } from 'ws';\nimport type { Server } from 'http';\nimport { SyncHub } from './sync-hub';\nimport type { HubBackend } from './hub-backend';\nimport type { HubFanout } from './hub-fanout';\nimport type { Authenticate } from './authenticate';\nimport type { Authorize, AuthorizeLayer, CanRead } from './authorize';\nimport { startHeartbeat } from './heartbeat';\nimport {\n DEFAULT_MAX_JSON_DEPTH,\n DEFAULT_MAX_MESSAGE_BYTES,\n DEFAULT_MAX_PENDING_AUTH_BYTES,\n DEFAULT_MAX_PENDING_AUTH_MESSAGES,\n DEFAULT_MESSAGE_BURST,\n DEFAULT_MESSAGES_PER_SECOND,\n DEFAULT_PRESENCE_THROTTLE_MS,\n MessageRateLimiter,\n} from './resource-limits';\nimport { DEFAULT_SHUTDOWN_GRACE_MS, drainWebSocketServer } from './shutdown';\n\nexport interface CreateSyncServerOptions {\n port?: number;\n server?: Server;\n backend?: HubBackend;\n fanout?: HubFanout;\n instanceId?: string;\n authenticate?: Authenticate;\n authorize?: Authorize;\n authorizeLayer?: AuthorizeLayer;\n canRead?: CanRead;\n heartbeatIntervalMs?: number;\n maxMessageBytes?: number;\n maxJsonDepth?: number;\n maxPendingAuthMessages?: number;\n maxPendingAuthBytes?: number;\n messagesPerSecond?: number;\n messageBurst?: number;\n presenceThrottleMs?: number;\n shutdownGraceMs?: number;\n}\n\nfunction rawDataByteLength(data: RawData): number {\n if (Array.isArray(data)) return data.reduce((total, chunk) => total + chunk.byteLength, 0);\n return data.byteLength;\n}\n\nexport function createSyncServer(options: CreateSyncServerOptions = {}): {\n hub: SyncHub;\n wss: WebSocketServer;\n close: () => Promise<void>;\n} {\n const shutdownGraceMs = options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;\n if (!Number.isFinite(shutdownGraceMs) || shutdownGraceMs < 0) {\n throw new RangeError('shutdownGraceMs must be a non-negative finite number');\n }\n const hub = new SyncHub({\n backend: options.backend,\n fanout: options.fanout,\n instanceId: options.instanceId,\n authorize: options.authorize,\n authorizeLayer: options.authorizeLayer,\n canRead: options.canRead,\n maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,\n presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,\n });\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const wss = options.server\n ? new WebSocketServer({ server: options.server, maxPayload: maxMessageBytes })\n : new WebSocketServer({ port: options.port ?? 0, maxPayload: maxMessageBytes });\n const heartbeat = startHeartbeat(wss, options.heartbeatIntervalMs ?? 30000);\n let shuttingDown = false;\n let closePromise: Promise<void> | undefined;\n let counter = 0;\n wss.on('connection', (ws, req) => {\n if (shuttingDown) {\n ws.close(1001, 'server shutting down');\n return;\n }\n heartbeat.track(ws);\n // Protocol/parser failures (including maxPayload) close the peer. Consuming the socket-level\n // error keeps hostile input from becoming an uncaught process error.\n ws.on('error', () => undefined);\n const url = new URL(req.url ?? '', 'http://localhost');\n const room = url.searchParams.get('room');\n if (!room) {\n ws.close(4400, 'room required');\n return;\n }\n const connId = `c${++counter}-${Math.random().toString(36).slice(2, 8)}`;\n\n let state: 'pending' | 'ready' | 'rejected' = 'pending';\n let closed = false;\n let admitted = false;\n const queue: string[] = [];\n let queuedBytes = 0;\n const maxPendingAuthMessages =\n options.maxPendingAuthMessages ?? DEFAULT_MAX_PENDING_AUTH_MESSAGES;\n const maxPendingAuthBytes = options.maxPendingAuthBytes ?? DEFAULT_MAX_PENDING_AUTH_BYTES;\n const limiter = new MessageRateLimiter(\n options.messagesPerSecond ?? DEFAULT_MESSAGES_PER_SECOND,\n options.messageBurst ?? DEFAULT_MESSAGE_BURST,\n );\n\n const send = (m: string) => {\n try {\n ws.send(m);\n } catch {\n /* socket closed mid-send */\n }\n };\n\n ws.on('message', (data) => {\n if (state === 'rejected') return;\n const messageBytes = rawDataByteLength(data);\n if (messageBytes > maxMessageBytes) {\n state = 'rejected';\n ws.close(1009, 'message too large');\n return;\n }\n if (!limiter.take()) {\n state = 'rejected';\n ws.close(4408, 'rate limit exceeded');\n return;\n }\n const msg = String(data);\n if (state === 'pending') {\n if (\n queue.length >= maxPendingAuthMessages ||\n queuedBytes + messageBytes > maxPendingAuthBytes\n ) {\n state = 'rejected';\n ws.close(4408, 'authentication queue limit exceeded');\n return;\n }\n queue.push(msg);\n queuedBytes += messageBytes;\n return;\n }\n void hub.handleMessage(connId, msg).catch((err) => console.error('[sync-server]', err));\n });\n ws.on('close', () => {\n closed = true;\n if (admitted) hub.removeConnection(connId);\n });\n\n Promise.resolve(options.authenticate ? options.authenticate({ req, room }) : { userId: connId })\n .then((result) => {\n if (closed || state === 'rejected' || shuttingDown) return;\n if (!result) {\n state = 'rejected';\n ws.close(4401, 'unauthorized');\n return;\n }\n state = 'ready';\n admitted = true;\n hub.addConnection({ id: connId, room, userId: result.userId, role: result.role, send });\n for (const m of queue) {\n void hub.handleMessage(connId, m).catch((err) => console.error('[sync-server]', err));\n }\n queue.length = 0;\n queuedBytes = 0;\n })\n .catch(() => {\n if (!closed && !shuttingDown) {\n state = 'rejected';\n ws.close(4401, 'unauthorized');\n }\n });\n });\n const close = (): Promise<void> => {\n if (closePromise) return closePromise;\n shuttingDown = true;\n closePromise = drainWebSocketServer(wss, shutdownGraceMs).then(() => {\n heartbeat.stop();\n hub.close();\n });\n return closePromise;\n };\n\n return {\n hub,\n wss,\n close,\n };\n}\n","export interface HeartbeatSocket {\n ping(): void;\n terminate(): void;\n on(event: 'pong', listener: () => void): void;\n}\nexport interface HeartbeatServer {\n clients: Set<HeartbeatSocket>;\n}\nexport interface Heartbeat {\n track(ws: HeartbeatSocket): void;\n stop(): void;\n}\n\nexport function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat {\n if (intervalMs <= 0) {\n return {\n track: () => {\n /* disabled */\n },\n stop: () => {\n /* disabled */\n },\n };\n }\n const alive = new WeakMap<HeartbeatSocket, boolean>();\n const track = (ws: HeartbeatSocket): void => {\n alive.set(ws, true);\n ws.on('pong', () => alive.set(ws, true));\n };\n const interval = setInterval(() => {\n for (const ws of wss.clients) {\n try {\n if (alive.get(ws) === false) {\n ws.terminate();\n continue;\n }\n alive.set(ws, false);\n ws.ping();\n } catch {\n /* socket dying mid-tick — skip it */\n }\n }\n }, intervalMs);\n return { track, stop: () => clearInterval(interval) };\n}\n","export const DEFAULT_SHUTDOWN_GRACE_MS = 5000;\n\nexport interface ShutdownSocket {\n close(code?: number, reason?: string): void;\n terminate(): void;\n}\n\nexport interface ShutdownServer {\n clients: Set<ShutdownSocket>;\n close(callback: (error?: Error) => void): void;\n}\n\n/** Stop admission, ask connected peers to leave, then forcefully reap stragglers. */\nexport function drainWebSocketServer(wss: ShutdownServer, graceMs: number): Promise<void> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = (): void => {\n if (settled) return;\n settled = true;\n clearTimeout(forceTimer);\n resolve();\n };\n const forceTimer = setTimeout(() => {\n for (const ws of wss.clients) {\n try {\n ws.terminate();\n } catch {\n /* socket already failed while shutdown was in progress */\n }\n }\n finish();\n }, graceMs);\n\n // Calling close first prevents new connections. Its callback runs once all clients have left.\n wss.close(() => finish());\n for (const ws of wss.clients) {\n try {\n ws.close(1001, 'server shutting down');\n } catch {\n try {\n ws.terminate();\n } catch {\n /* socket already closed */\n }\n }\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,eAOO;;;ACPP,kBAA4D;AAIrD,IAAM,mBAAN,MAA6C;AAAA,EAC1C,QAAQ,oBAAI,IAAwC;AAAA,EACpD,aAAa,oBAAI,IAAsC;AAAA,EAEvD,KAAK,IAAwC;AACnD,QAAI,IAAI,KAAK,MAAM,IAAI,EAAE;AACzB,QAAI,CAAC,GAAG;AACN,UAAI,oBAAI,IAAI;AACZ,WAAK,MAAM,IAAI,IAAI,CAAC;AAAA,IACtB;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,OAAO,MAAwC;AACrD,QAAI,IAAI,KAAK,WAAW,IAAI,IAAI;AAChC,QAAI,CAAC,GAAG;AACN,UAAI,oBAAI,IAAI;AACZ,WAAK,WAAW,IAAI,MAAM,CAAC;AAAA,IAC7B;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAS,MAAwC;AACrD,WAAO,CAAC,GAAG,KAAK,KAAK,IAAI,EAAE,OAAO,CAAC;AAAA,EACrC;AAAA,EAEA,MAAM,IAAI,MAAc,IAAgD;AACtE,WAAO,KAAK,KAAK,IAAI,EAAE,IAAI,EAAE;AAAA,EAC/B;AAAA,EAEA,MAAM,MAAM,MAAc,IAA2B;AACnD,QAAI,GAAG,SAAS,SAAS;AAEvB,WAAK,MAAM,OAAO,IAAI;AACtB;AAAA,IACF;AACA,kCAAa,KAAK,KAAK,IAAI,GAAG,EAAE;AAAA,EAClC;AAAA,EAEA,MAAM,aAAa,MAAsC;AACvD,WAAO,CAAC,GAAG,KAAK,OAAO,IAAI,EAAE,OAAO,CAAC;AAAA,EACvC;AAAA,EAEA,MAAM,eAAe,MAAc,IAA8C;AAC/E,WAAO,KAAK,OAAO,IAAI,EAAE,IAAI,EAAE;AAAA,EACjC;AAAA,EAEA,MAAM,iBAAiB,MAAc,QAAoC;AACvE,SAAK,OAAO,IAAI,EAAE,IAAI,OAAO,IAAI,MAAM;AAAA,EACzC;AACF;;;AChDO,IAAM,oBAAN,MAA6C;AAAA,EACjC,WAAW,oBAAI,IAA+B;AAAA,EAE/D,QAAQ,SAAuB;AAC7B,eAAW,KAAK,KAAK,UAAU;AAC7B,UAAI;AACF,UAAE,OAAO;AAAA,MACX,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAAA,EAEA,UAAU,SAAgD;AACxD,SAAK,SAAS,IAAI,OAAO;AACzB,WAAO,MAAM,KAAK,SAAS,OAAO,OAAO;AAAA,EAC3C;AACF;;;ACvBO,IAAM,4BAA4B,OAAO;AACzC,IAAM,yBAAyB;AAC/B,IAAM,oCAAoC;AAC1C,IAAM,iCAAiC,IAAI,OAAO;AAClD,IAAM,8BAA8B;AACpC,IAAM,wBAAwB;AAC9B,IAAM,+BAA+B;AAErC,SAAS,mBAAmB,SAAiB,UAA2B;AAC7E,MAAI,QAAQ;AACZ,MAAI,WAAW;AACf,MAAI,UAAU;AAEd,aAAW,QAAQ,SAAS;AAC1B,QAAI,UAAU;AACZ,UAAI,QAAS,WAAU;AAAA,eACd,SAAS,KAAM,WAAU;AAAA,eACzB,SAAS,IAAK,YAAW;AAClC;AAAA,IACF;AACA,QAAI,SAAS,IAAK,YAAW;AAAA,aACpB,SAAS,OAAO,SAAS,KAAK;AACrC,eAAS;AACT,UAAI,QAAQ,SAAU,QAAO;AAAA,IAC/B,WAAW,SAAS,OAAO,SAAS,KAAK;AACvC,eAAS;AACT,UAAI,QAAQ,EAAG,QAAO;AAAA,IACxB;AAAA,EACF;AAEA,SAAO,UAAU,KAAK,CAAC;AACzB;AAEO,IAAM,qBAAN,MAAyB;AAAA,EAI9B,YACmB,eACA,OACjB,MAAM,KAAK,IAAI,GACf;AAHiB;AACA;AAGjB,SAAK,SAAS;AACd,SAAK,YAAY;AAAA,EACnB;AAAA,EAVQ;AAAA,EACA;AAAA,EAWR,KAAK,MAAM,KAAK,IAAI,GAAY;AAC9B,UAAM,iBAAiB,KAAK,IAAI,GAAG,MAAM,KAAK,SAAS,IAAI;AAC3D,SAAK,SAAS,KAAK,IAAI,KAAK,OAAO,KAAK,SAAS,iBAAiB,KAAK,aAAa;AACpF,SAAK,YAAY;AACjB,QAAI,KAAK,SAAS,EAAG,QAAO;AAC5B,SAAK,UAAU;AACf,WAAO;AAAA,EACT;AACF;;;AHjBA,IAAM,WAAW;AAEjB,SAAS,qBAA6B;AACpC,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe;AAChE,WAAO,OAAO,WAAW;AAC3B,SAAO,KAAK,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AACjD;AAEA,SAAS,WAAW,IAA6E;AAC/F,MAAI,OAAO,OAAO,YAAY,OAAO,KAAM,QAAO;AAClD,QAAM,IAAI;AACV,MAAI,EAAE,SAAS,SAAU,YAAO,6BAAe,EAAE,OAAO;AACxD,MAAI,EAAE,SAAS,SAAU,QAAO,OAAO,EAAE,OAAO;AAChD,SAAO,EAAE,SAAS;AACpB;AAIA,SAAS,UAAU,IAA4B;AAC7C,MAAI,OAAO,OAAO,YAAY,OAAO,KAAM,QAAO;AAClD,QAAM,IAAK,GAA0B;AAGrC,SAAO,MAAM,kBAAkB,MAAM;AACvC;AAEA,SAAS,gBAAgB,IAA0B;AACjD,SAAO,GAAG,SAAS,iBACf,EAAE,IAAI,GAAG,MAAM,IAAI,SAAS,GAAG,SAAS,QAAQ,GAAG,QAAQ,YAAY,GAAG,MAAM,IAChF,EAAE,IAAI,GAAG,IAAI,SAAS,GAAG,SAAS,QAAQ,GAAG,OAAO;AAC1D;AAEA,SAAS,gBAAgB,QAA8B;AACrD,SAAO,OAAO,aACV;AAAA,IACE,MAAM;AAAA,IACN,OAAO,OAAO;AAAA,IACd,SAAS,OAAO;AAAA,IAChB,QAAQ,OAAO;AAAA,EACjB,IACA,EAAE,MAAM,gBAAgB,IAAI,OAAO,IAAI,SAAS,OAAO,SAAS,QAAQ,OAAO,OAAO;AAC5F;AAEA,SAAS,aACP,IACwE;AACxE,MAAI,OAAO,OAAO,YAAY,OAAO,KAAM,QAAO;AAClD,QAAM,IAAK,GAA0B;AACrC,SAAO,MAAM,cAAc,MAAM;AACnC;AAEO,IAAM,UAAN,MAAc;AAAA,EACF;AAAA,EACA,QAAQ,oBAAI,IAAwB;AAAA,EACpC,QAAQ,oBAAI,IAAyB;AAAA;AAAA,EACrC,aAAa,oBAAI,IAA2B;AAAA;AAAA,EAC5C,sBAAsB,oBAAI,IAAY;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA,eAAe,oBAAI,IAAsC;AAAA,EACzD;AAAA,EACA;AAAA,EACA,iBAAiB,oBAAI,IAAoB;AAAA,EACzC,kBAAkB,oBAAI,IAGrC;AAAA,EAEF,YAAY,UAA0B,CAAC,GAAG;AACxC,SAAK,UAAU,QAAQ,WAAW,IAAI,iBAAiB;AACvD,SAAK,aAAa,QAAQ,cAAc,mBAAmB;AAC3D,SAAK,SAAS,QAAQ,UAAU,IAAI,kBAAkB;AACtD,SAAK,YAAY,QAAQ;AACzB,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,UAAU,QAAQ;AACvB,SAAK,eAAe,QAAQ,gBAAgB;AAC5C,SAAK,qBAAqB,QAAQ,sBAAsB;AACxD,SAAK,cAAc,KAAK,OAAO,UAAU,CAAC,YAAY,KAAK,SAAS,OAAO,CAAC;AAAA,EAC9E;AAAA,EAEA,cAAc,MAAwB;AACpC,SAAK,MAAM,IAAI,KAAK,IAAI,IAAI;AAC5B,QAAI,MAAM,KAAK,MAAM,IAAI,KAAK,IAAI;AAClC,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAI;AACd,WAAK,MAAM,IAAI,KAAK,MAAM,GAAG;AAAA,IAC/B;AACA,QAAI,IAAI,KAAK,EAAE;AAAA,EACjB;AAAA,EAEA,iBAAiB,QAAsB;AACrC,UAAM,OAAO,KAAK,MAAM,IAAI,MAAM;AAClC,QAAI,CAAC,KAAM;AACX,SAAK,MAAM,OAAO,MAAM;AACxB,UAAM,OAAO,KAAK;AAClB,UAAM,cAAc,KAAK,oBAAoB,OAAO,MAAM;AAC1D,SAAK,eAAe,OAAO,MAAM;AACjC,UAAM,kBAAkB,KAAK,gBAAgB,IAAI,MAAM;AACvD,QAAI,gBAAiB,cAAa,gBAAgB,KAAK;AACvD,SAAK,gBAAgB,OAAO,MAAM;AAClC,UAAM,UAAU,KAAK,MAAM,IAAI,IAAI;AACnC,QAAI,SAAS;AACX,cAAQ,OAAO,MAAM;AACrB,UAAI,QAAQ,SAAS,GAAG;AACtB,aAAK,MAAM,OAAO,IAAI;AACtB,aAAK,WAAW,OAAO,IAAI;AAAA,MAC7B;AAAA,IACF;AACA,QAAI,YAAa,MAAK,eAAe,MAAM,KAAK,EAAE;AAAA,EACpD;AAAA,EAEA,YAAoB;AAClB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,kBAAqB,MAAc,MAAiB;AAClD,UAAM,KAAK,EAAE,MAAM,YAAqB,KAAK;AAC7C,UAAM,OAAO,KAAK,YAAY,MAAM,QAAW,KAAK,UAAU,EAAE,MAAM,UAAU,GAAG,CAAC,CAAC;AACrF,SAAK,YAAY,KAAK,UAAU,EAAE,GAAG,KAAK,YAAY,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC;AACjF,WAAO;AAAA,EACT;AAAA,EAEA,cAAc,QAAgB,SAAgC;AAC5D,UAAM,OAAO,KAAK,MAAM,IAAI,MAAM;AAClC,QAAI,CAAC,KAAM,QAAO,QAAQ,QAAQ;AAClC,QAAI,CAAC,mBAAmB,SAAS,KAAK,YAAY,EAAG,QAAO,QAAQ,QAAQ;AAC5E,UAAM,UAAM,4BAAc,OAAO;AACjC,QAAI,CAAC,IAAK,QAAO,QAAQ,QAAQ;AACjC,QAAI,IAAI,GAAG,SAAS,YAAY;AAC9B,WAAK,iBAAiB,MAAM,IAAI,GAAG,IAAI;AACvC,aAAO,QAAQ,QAAQ;AAAA,IACzB;AACA,UAAM,OAAO,KAAK;AAGlB,UAAM,OAAO,KAAK,WAAW,IAAI,IAAI,KAAK,QAAQ,QAAQ;AAC1D,UAAM,YAAY,KAAK,KAAK,MAAM,KAAK,QAAQ,MAAM,GAAG,CAAC;AACzD,SAAK,WAAW;AAAA,MACd;AAAA,MACA,UAAU,MAAM,MAAM;AAAA,MAGtB,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,QAAQ,MAAkB,KAAkC;AACxE,UAAM,KAAK,IAAI;AACf,QAAI,GAAG,SAAS,oBAAoB;AAClC,YAAM,MAAO,MAAM,KAAK,QAAQ,SAAS,KAAK,IAAI;AAClD,YAAM,WAAW,KAAK,UAAU,IAAI,OAAO,CAAC,OAAO,KAAK,QAAQ,MAAM,GAAG,QAAQ,CAAC,IAAI;AAItF,YAAM,SAAS,MAAM,KAAK,gBAAgB,KAAK,IAAI;AACnD,WAAK;AAAA;AAAA;AAAA,QAGH,KAAK,UAAU;AAAA,UACb,MAAM;AAAA,UACN,IACE,OAAO,SAAS,IACZ,EAAE,MAAM,YAAY,IAAI,IAAI,MAAM,UAAU,OAAO,IACnD,EAAE,MAAM,YAAY,IAAI,IAAI,MAAM,SAAS;AAAA,QACnD,CAAC;AAAA,MACH;AAAA,IACF,WAAW,GAAG,SAAS,kBAAkB,GAAG,SAAS,gBAAgB;AACnE,YAAM,KAAK,eAAe,MAAM,EAAE;AAAA,IACpC,WAAW,GAAG,SAAS,YAAY,GAAG,SAAS,YAAY,GAAG,SAAS,SAAS;AAC9E,YAAM,KAAK,GAAG,SAAS,WAAW,GAAG,QAAQ,KAAK,GAAG,SAAS,WAAW,GAAG,KAAK;AACjF,YAAM,eAAe,KAAK,aAAa,KAAK,YAAY,OAAO;AAC/D,YAAM,UAAoC,cACtC,MAAM,KAAK,QAAQ,IAAI,KAAK,MAAM,EAAE,IACpC;AAEJ,UAAI,aAAqB;AACzB,UAAI,KAAK,WAAW;AAClB,cAAM,UAAU,MAAM,KAAK,UAAU;AAAA,UACnC,QAAQ,KAAK;AAAA,UACb,MAAM,KAAK;AAAA,UACX,MAAM,KAAK;AAAA,UACX;AAAA,UACA,gBAAgB;AAAA,QAClB,CAAC;AACD,YAAI,CAAC,SAAS;AACZ,gBAAM,KAAK,eAAe,MAAM,IAAI,MAAM,IAAI,OAAO;AACrD;AAAA,QACF;AACA,YAAI,GAAG,SAAS,UAAU;AACxB,gBAAM,UAAU,SAAS,WAAW,KAAK;AACzC,gBAAM,iBAA+B,EAAE,GAAG,GAAG,SAAS,QAAQ;AAC9D,uBAAa,EAAE,MAAM,UAAU,SAAS,eAAe;AAAA,QACzD;AAAA,MACF;AAEA,YAAM,KAAK,QAAQ,MAAM,KAAK,MAAM,UAAU;AAE9C,YAAM,cAAc,YAAY;AAChC,YAAM,eAAe,SAAS;AAE9B,YAAM,KAAK,OAAO;AAAA,QAChB,KAAK,UAAU;AAAA,UACb,GAAG,KAAK;AAAA,UACR,MAAM,KAAK;AAAA,UACX,MAAM,KAAK;AAAA,UACX,IAAI;AAAA,UACJ,MAAM;AAAA,UACN,SAAS;AAAA,QACX,CAAC;AAAA,MACH;AAEA,WAAK,cAAc,KAAK,MAAM,KAAK,IAAI,KAAK,IAAI,YAAY,cAAc,WAAW;AAAA,IACvF;AAAA,EAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,eAAe,MAAkB,IAA4B;AACzE,UAAM,SAAS,gBAAgB,EAAE;AACjC,UAAM,UAAU,MAAM,KAAK,eAAe,KAAK,MAAM,OAAO,EAAE;AAC9D,QAAI,KAAK,gBAAgB;AACvB,YAAM,UAAU,MAAM,KAAK,eAAe;AAAA,QACxC,QAAQ,KAAK;AAAA,QACb,MAAM,KAAK;AAAA,QACX,MAAM,KAAK;AAAA,QACX;AAAA,QACA,eAAe;AAAA,MACjB,CAAC;AACD,UAAI,CAAC,SAAS;AAGZ,cAAM,aAAa,WAAW,EAAE,IAAI,OAAO,IAAI,SAAS,OAAO,SAAS,QAAQ,SAAS;AACzF,aAAK,KAAK,KAAK,UAAU,EAAE,MAAM,UAAU,IAAI,gBAAgB,UAAU,EAAE,CAAC,CAAC;AAC7E;AAAA,MACF;AAAA,IACF;AACA,QAAI,WAAW,KAAC,iCAAmB,QAAQ,OAAO,GAAG;AAEnD,WAAK,KAAK,KAAK,UAAU,EAAE,MAAM,UAAU,IAAI,gBAAgB,OAAO,EAAE,CAAC,CAAC;AAC1E;AAAA,IACF;AACA,UAAM,KAAK,iBAAiB,KAAK,MAAM,MAAM;AAC7C,UAAM,KAAK,OAAO;AAAA,MAChB,KAAK,UAAU,EAAE,GAAG,KAAK,YAAY,MAAM,KAAK,MAAM,MAAM,KAAK,IAAI,GAAG,CAAC;AAAA,IAC3E;AACA,SAAK,YAAY,KAAK,MAAM,KAAK,IAAI,KAAK,UAAU,EAAE,MAAM,KAAK,IAAI,GAAG,CAAC,CAAC;AAAA,EAC5E;AAAA,EAEQ,eAEC;AACP,UAAM,EAAE,cAAc,gBAAgB,iBAAiB,IAAI,KAAK;AAChE,QAAI,CAAC,gBAAgB,CAAC,kBAAkB,CAAC,iBAAkB,QAAO;AAClE,WAAO;AAAA,MACL,cAAc,aAAa,KAAK,KAAK,OAAO;AAAA,MAC5C,gBAAgB,eAAe,KAAK,KAAK,OAAO;AAAA,MAChD,kBAAkB,iBAAiB,KAAK,KAAK,OAAO;AAAA,IACtD;AAAA,EACF;AAAA,EAEA,MAAc,gBAAgB,MAAsC;AAClE,UAAM,UAAU,KAAK,aAAa;AAClC,QAAI,QAAS,QAAO,QAAQ,aAAa,IAAI;AAC7C,WAAO,CAAC,GAAI,KAAK,aAAa,IAAI,IAAI,GAAG,OAAO,KAAK,CAAC,CAAE;AAAA,EAC1D;AAAA,EAEA,MAAc,eAAe,MAAc,IAA8C;AACvF,UAAM,UAAU,KAAK,aAAa;AAClC,QAAI,QAAS,QAAO,QAAQ,eAAe,MAAM,EAAE;AACnD,WAAO,KAAK,aAAa,IAAI,IAAI,GAAG,IAAI,EAAE;AAAA,EAC5C;AAAA,EAEA,MAAc,iBAAiB,MAAc,QAAoC;AAC/E,UAAM,UAAU,KAAK,aAAa;AAClC,QAAI,SAAS;AACX,YAAM,QAAQ,iBAAiB,MAAM,MAAM;AAC3C;AAAA,IACF;AACA,QAAI,MAAM,KAAK,aAAa,IAAI,IAAI;AACpC,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAI;AACd,WAAK,aAAa,IAAI,MAAM,GAAG;AAAA,IACjC;AACA,QAAI,IAAI,OAAO,IAAI,MAAM;AAAA,EAC3B;AAAA,EAEQ,QAAQ,MAAkB,UAAuC;AACvE,QAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,WAAO,KAAK,QAAQ,EAAE,QAAQ,KAAK,QAAQ,MAAM,KAAK,MAAM,MAAM,KAAK,MAAM,SAAS,CAAC;AAAA,EACzF;AAAA,EAEQ,YAAY,SAAuB;AACzC,QAAI;AACF,WAAK,QAAQ,QAAQ,KAAK,OAAO,QAAQ,OAAO,CAAC,EAAE,MAAM,MAAM;AAAA,MAE/D,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEQ,YAAY,MAAc,WAA+B,SAAyB;AACxF,UAAM,UAAU,KAAK,MAAM,IAAI,IAAI;AACnC,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,OAAO;AACX,eAAW,OAAO,SAAS;AACzB,UAAI,QAAQ,UAAW;AACvB,YAAM,OAAO,KAAK,MAAM,IAAI,GAAG;AAC/B,UAAI,CAAC,KAAM;AACX,UAAI;AACF,aAAK,KAAK,OAAO;AACjB,gBAAQ;AAAA,MACV,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,wBAAwB,MAAkB,MAAqB;AACrE,SAAK,oBAAoB,IAAI,KAAK,EAAE;AACpC,UAAM,UAAU,KAAK,UAAU,EAAE,MAAM,KAAK,IAAI,IAAI,EAAE,MAAM,YAAY,KAAK,EAAE,CAAC;AAChF,SAAK,YAAY,KAAK,MAAM,KAAK,IAAI,OAAO;AAC5C,SAAK;AAAA,MACH,KAAK,UAAU;AAAA,QACb,GAAG,KAAK;AAAA,QACR,MAAM,KAAK;AAAA,QACX,MAAM,KAAK;AAAA,QACX,IAAI,EAAE,MAAM,YAAY,KAAK;AAAA,MAC/B,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEQ,iBAAiB,MAAkB,MAAqB;AAC9D,QAAI,KAAK,sBAAsB,GAAG;AAChC,WAAK,wBAAwB,MAAM,IAAI;AACvC;AAAA,IACF;AACA,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,aAAa,KAAK,eAAe,IAAI,KAAK,EAAE;AAClD,QAAI,eAAe,UAAa,MAAM,cAAc,KAAK,oBAAoB;AAC3E,WAAK,eAAe,IAAI,KAAK,IAAI,GAAG;AACpC,WAAK,wBAAwB,MAAM,IAAI;AACvC;AAAA,IACF;AAEA,UAAM,WAAW,KAAK,gBAAgB,IAAI,KAAK,EAAE;AACjD,QAAI,UAAU;AACZ,eAAS,OAAO;AAChB;AAAA,IACF;AACA,UAAM,QAAQ;AAAA,MACZ,MAAM;AACJ,cAAM,UAAU,KAAK,gBAAgB,IAAI,KAAK,EAAE;AAChD,aAAK,gBAAgB,OAAO,KAAK,EAAE;AACnC,YAAI,CAAC,WAAW,CAAC,KAAK,MAAM,IAAI,KAAK,EAAE,EAAG;AAC1C,aAAK,eAAe,IAAI,KAAK,IAAI,KAAK,IAAI,CAAC;AAC3C,aAAK,wBAAwB,MAAM,QAAQ,IAAI;AAAA,MACjD;AAAA,MACA,KAAK,sBAAsB,MAAM;AAAA,IACnC;AACA,SAAK,gBAAgB,IAAI,KAAK,IAAI,EAAE,MAAM,MAAM,CAAC;AAAA,EACnD;AAAA,EAEQ,eAAe,MAAc,MAAoB;AACvD,UAAM,UAAU,KAAK,UAAU,EAAE,MAAM,IAAI,EAAE,MAAM,iBAAiB,EAAE,CAAC;AACvE,SAAK,YAAY,MAAM,QAAW,OAAO;AACzC,SAAK;AAAA,MACH,KAAK,UAAU,EAAE,GAAG,KAAK,YAAY,MAAM,MAAM,IAAI,EAAE,MAAM,iBAAiB,EAAE,CAAC;AAAA,IACnF;AAAA,EACF;AAAA,EAEQ,cACN,MACA,WACA,MACA,IACA,cACA,aACM;AACN,UAAM,UAAU,KAAK,MAAM,IAAI,IAAI;AACnC,QAAI,CAAC,QAAS;AACd,UAAM,OAAO,CAAC,MAAkB,QAAsB;AACpD,UAAI;AACF,aAAK,KAAK,GAAG;AAAA,MACf,QAAQ;AAAA,MAER;AAAA,IACF;AACA,QAAI,GAAG,SAAS,UAAU;AACxB,YAAM,WAAY,GAAG,QAAyB;AAC9C,YAAM,YAAY,KAAK,UAAU,EAAE,MAAM,GAAG,CAAC;AAC7C,YAAM,YAAY,KAAK,UAAU;AAAA,QAC/B,MAAM;AAAA,QACN,IAAI,EAAE,MAAM,UAAU,IAAI,GAAG,QAAQ,GAAG;AAAA,MAC1C,CAAC;AACD,iBAAW,OAAO,SAAS;AACzB,YAAI,QAAQ,UAAW;AACvB,cAAM,OAAO,KAAK,MAAM,IAAI,GAAG;AAC/B,YAAI,CAAC,KAAM;AACX,YAAI,KAAK,QAAQ,MAAM,QAAQ,EAAG,MAAK,MAAM,SAAS;AAAA,iBAC7C,eAAe,KAAK,QAAQ,MAAM,YAAY,EAAG,MAAK,MAAM,SAAS;AAAA,MAChF;AAAA,IACF,WAAW,GAAG,SAAS,UAAU;AAC/B,YAAM,YAAY,KAAK,UAAU,EAAE,MAAM,GAAG,CAAC;AAC7C,iBAAW,OAAO,SAAS;AACzB,YAAI,QAAQ,UAAW;AACvB,cAAM,OAAO,KAAK,MAAM,IAAI,GAAG;AAC/B,YAAI,CAAC,KAAM;AAGX,cAAM,aAAa,CAAC,KAAK,WAAY,eAAe,KAAK,QAAQ,MAAM,YAAY;AACnF,YAAI,WAAY,MAAK,MAAM,SAAS;AAAA,MACtC;AAAA,IACF,WAAW,GAAG,SAAS,SAAS;AAC9B,YAAM,WAAW,KAAK,UAAU,EAAE,MAAM,GAAG,CAAC;AAC5C,iBAAW,OAAO,SAAS;AACzB,YAAI,QAAQ,UAAW;AACvB,cAAM,OAAO,KAAK,MAAM,IAAI,GAAG;AAC/B,YAAI,KAAM,MAAK,MAAM,QAAQ;AAAA,MAC/B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,eACZ,MACA,MACA,IACA,SACe;AACf,QAAI;AACJ,QAAI,GAAG,SAAS,UAAU;AACxB,mBAAa,UACT,KAAK,QAAQ,MAAM,QAAQ,QAAQ,IACjC,EAAE,MAAM,UAAU,SAAS,QAAQ,IACnC,EAAE,MAAM,UAAU,IAAI,QAAQ,GAAG,IACnC,EAAE,MAAM,UAAU,IAAI,GAAG,QAAQ,GAAG;AAAA,IAC1C,WAAW,GAAG,SAAS,UAAU;AAC/B,mBAAa,UACT,KAAK,QAAQ,MAAM,QAAQ,QAAQ,IACjC,EAAE,MAAM,UAAU,SAAS,QAAQ,IACnC,EAAE,MAAM,UAAU,IAAI,QAAQ,GAAG,IACnC;AAAA,IACN,WAAW,GAAG,SAAS,SAAS;AAC9B,YAAM,MAAO,MAAM,KAAK,QAAQ,SAAS,KAAK,IAAI;AAClD,YAAM,WAAW,KAAK,UAAU,IAAI,OAAO,CAAC,OAAO,KAAK,QAAQ,MAAM,GAAG,QAAQ,CAAC,IAAI;AACtF,mBAAa,EAAE,MAAM,YAAY,IAAI,MAAM,SAAS;AAAA,IACtD;AACA,QAAI,WAAY,MAAK,KAAK,KAAK,UAAU,EAAE,MAAM,UAAU,IAAI,WAAW,CAAC,CAAC;AAAA,EAC9E;AAAA,EAEQ,SAAS,SAAuB;AAGtC,QAAI;AAQJ,QAAI;AACF,YAAM,KAAK,MAAM,OAAO;AAAA,IAC1B,QAAQ;AACN;AAAA,IACF;AACA,QAAI,OAAO,IAAI,MAAM,YAAY,OAAO,IAAI,SAAS,YAAY,OAAO,IAAI,SAAS;AACnF;AACF,QAAI,IAAI,MAAM,KAAK,WAAY;AAC/B,UAAM,KAAK,IAAI;AACf,QAAI,aAAa,EAAE,GAAG;AAGpB,WAAK,YAAY,IAAI,MAAM,QAAW,KAAK,UAAU,EAAE,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC;AAC5E;AAAA,IACF;AACA,QAAI,UAAU,EAAE,GAAG;AAIjB,WAAK,KAAK,mBAAmB,IAAI,MAAM,EAAE,EAAE,MAAM,MAAM;AAAA,MAEvD,CAAC;AACD,WAAK,YAAY,IAAI,MAAM,QAAW,KAAK,UAAU,EAAE,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC;AAC5E;AAAA,IACF;AACA,QAAI,CAAC,WAAW,EAAE,EAAG;AACrB,UAAM,eAAe,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AAC/D,UAAM,cAAc,IAAI,YAAY;AACpC,SAAK,cAAc,IAAI,MAAM,QAAW,IAAI,MAAM,IAAI,cAAc,WAAW;AAAA,EACjF;AAAA,EAEA,MAAc,mBAAmB,MAAc,IAA4B;AACzE,UAAM,SAAS,gBAAgB,EAAE;AACjC,UAAM,UAAU,MAAM,KAAK,eAAe,MAAM,OAAO,EAAE;AACzD,QAAI,WAAW,KAAC,iCAAmB,QAAQ,OAAO,EAAG;AACrD,UAAM,KAAK,iBAAiB,MAAM,MAAM;AAAA,EAC1C;AAAA,EAEA,QAAc;AACZ,eAAW,WAAW,KAAK,gBAAgB,OAAO,EAAG,cAAa,QAAQ,KAAK;AAC/E,SAAK,gBAAgB,MAAM;AAC3B,SAAK,YAAY;AAAA,EACnB;AACF;;;AI/iBA,gBAA8C;;;ACavC,SAAS,eAAe,KAAsB,YAA+B;AAClF,MAAI,cAAc,GAAG;AACnB,WAAO;AAAA,MACL,OAAO,MAAM;AAAA,MAEb;AAAA,MACA,MAAM,MAAM;AAAA,MAEZ;AAAA,IACF;AAAA,EACF;AACA,QAAM,QAAQ,oBAAI,QAAkC;AACpD,QAAM,QAAQ,CAAC,OAA8B;AAC3C,UAAM,IAAI,IAAI,IAAI;AAClB,OAAG,GAAG,QAAQ,MAAM,MAAM,IAAI,IAAI,IAAI,CAAC;AAAA,EACzC;AACA,QAAM,WAAW,YAAY,MAAM;AACjC,eAAW,MAAM,IAAI,SAAS;AAC5B,UAAI;AACF,YAAI,MAAM,IAAI,EAAE,MAAM,OAAO;AAC3B,aAAG,UAAU;AACb;AAAA,QACF;AACA,cAAM,IAAI,IAAI,KAAK;AACnB,WAAG,KAAK;AAAA,MACV,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF,GAAG,UAAU;AACb,SAAO,EAAE,OAAO,MAAM,MAAM,cAAc,QAAQ,EAAE;AACtD;;;AC5CO,IAAM,4BAA4B;AAalC,SAAS,qBAAqB,KAAqB,SAAgC;AACxF,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,QAAI,UAAU;AACd,UAAM,SAAS,MAAY;AACzB,UAAI,QAAS;AACb,gBAAU;AACV,mBAAa,UAAU;AACvB,cAAQ;AAAA,IACV;AACA,UAAM,aAAa,WAAW,MAAM;AAClC,iBAAW,MAAM,IAAI,SAAS;AAC5B,YAAI;AACF,aAAG,UAAU;AAAA,QACf,QAAQ;AAAA,QAER;AAAA,MACF;AACA,aAAO;AAAA,IACT,GAAG,OAAO;AAGV,QAAI,MAAM,MAAM,OAAO,CAAC;AACxB,eAAW,MAAM,IAAI,SAAS;AAC5B,UAAI;AACF,WAAG,MAAM,MAAM,sBAAsB;AAAA,MACvC,QAAQ;AACN,YAAI;AACF,aAAG,UAAU;AAAA,QACf,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;AFNA,SAAS,kBAAkB,MAAuB;AAChD,MAAI,MAAM,QAAQ,IAAI,EAAG,QAAO,KAAK,OAAO,CAAC,OAAO,UAAU,QAAQ,MAAM,YAAY,CAAC;AACzF,SAAO,KAAK;AACd;AAEO,SAAS,iBAAiB,UAAmC,CAAC,GAInE;AACA,QAAM,kBAAkB,QAAQ,mBAAmB;AACnD,MAAI,CAAC,OAAO,SAAS,eAAe,KAAK,kBAAkB,GAAG;AAC5D,UAAM,IAAI,WAAW,sDAAsD;AAAA,EAC7E;AACA,QAAM,MAAM,IAAI,QAAQ;AAAA,IACtB,SAAS,QAAQ;AAAA,IACjB,QAAQ,QAAQ;AAAA,IAChB,YAAY,QAAQ;AAAA,IACpB,WAAW,QAAQ;AAAA,IACnB,gBAAgB,QAAQ;AAAA,IACxB,SAAS,QAAQ;AAAA,IACjB,cAAc,QAAQ,gBAAgB;AAAA,IACtC,oBAAoB,QAAQ,sBAAsB;AAAA,EACpD,CAAC;AACD,QAAM,kBAAkB,QAAQ,mBAAmB;AACnD,QAAM,MAAM,QAAQ,SAChB,IAAI,0BAAgB,EAAE,QAAQ,QAAQ,QAAQ,YAAY,gBAAgB,CAAC,IAC3E,IAAI,0BAAgB,EAAE,MAAM,QAAQ,QAAQ,GAAG,YAAY,gBAAgB,CAAC;AAChF,QAAM,YAAY,eAAe,KAAK,QAAQ,uBAAuB,GAAK;AAC1E,MAAI,eAAe;AACnB,MAAI;AACJ,MAAI,UAAU;AACd,MAAI,GAAG,cAAc,CAAC,IAAI,QAAQ;AAChC,QAAI,cAAc;AAChB,SAAG,MAAM,MAAM,sBAAsB;AACrC;AAAA,IACF;AACA,cAAU,MAAM,EAAE;AAGlB,OAAG,GAAG,SAAS,MAAM,MAAS;AAC9B,UAAM,MAAM,IAAI,IAAI,IAAI,OAAO,IAAI,kBAAkB;AACrD,UAAM,OAAO,IAAI,aAAa,IAAI,MAAM;AACxC,QAAI,CAAC,MAAM;AACT,SAAG,MAAM,MAAM,eAAe;AAC9B;AAAA,IACF;AACA,UAAM,SAAS,IAAI,EAAE,OAAO,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAEtE,QAAI,QAA0C;AAC9C,QAAI,SAAS;AACb,QAAI,WAAW;AACf,UAAM,QAAkB,CAAC;AACzB,QAAI,cAAc;AAClB,UAAM,yBACJ,QAAQ,0BAA0B;AACpC,UAAM,sBAAsB,QAAQ,uBAAuB;AAC3D,UAAM,UAAU,IAAI;AAAA,MAClB,QAAQ,qBAAqB;AAAA,MAC7B,QAAQ,gBAAgB;AAAA,IAC1B;AAEA,UAAM,OAAO,CAAC,MAAc;AAC1B,UAAI;AACF,WAAG,KAAK,CAAC;AAAA,MACX,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,OAAG,GAAG,WAAW,CAAC,SAAS;AACzB,UAAI,UAAU,WAAY;AAC1B,YAAM,eAAe,kBAAkB,IAAI;AAC3C,UAAI,eAAe,iBAAiB;AAClC,gBAAQ;AACR,WAAG,MAAM,MAAM,mBAAmB;AAClC;AAAA,MACF;AACA,UAAI,CAAC,QAAQ,KAAK,GAAG;AACnB,gBAAQ;AACR,WAAG,MAAM,MAAM,qBAAqB;AACpC;AAAA,MACF;AACA,YAAM,MAAM,OAAO,IAAI;AACvB,UAAI,UAAU,WAAW;AACvB,YACE,MAAM,UAAU,0BAChB,cAAc,eAAe,qBAC7B;AACA,kBAAQ;AACR,aAAG,MAAM,MAAM,qCAAqC;AACpD;AAAA,QACF;AACA,cAAM,KAAK,GAAG;AACd,uBAAe;AACf;AAAA,MACF;AACA,WAAK,IAAI,cAAc,QAAQ,GAAG,EAAE,MAAM,CAAC,QAAQ,QAAQ,MAAM,iBAAiB,GAAG,CAAC;AAAA,IACxF,CAAC;AACD,OAAG,GAAG,SAAS,MAAM;AACnB,eAAS;AACT,UAAI,SAAU,KAAI,iBAAiB,MAAM;AAAA,IAC3C,CAAC;AAED,YAAQ,QAAQ,QAAQ,eAAe,QAAQ,aAAa,EAAE,KAAK,KAAK,CAAC,IAAI,EAAE,QAAQ,OAAO,CAAC,EAC5F,KAAK,CAAC,WAAW;AAChB,UAAI,UAAU,UAAU,cAAc,aAAc;AACpD,UAAI,CAAC,QAAQ;AACX,gBAAQ;AACR,WAAG,MAAM,MAAM,cAAc;AAC7B;AAAA,MACF;AACA,cAAQ;AACR,iBAAW;AACX,UAAI,cAAc,EAAE,IAAI,QAAQ,MAAM,QAAQ,OAAO,QAAQ,MAAM,OAAO,MAAM,KAAK,CAAC;AACtF,iBAAW,KAAK,OAAO;AACrB,aAAK,IAAI,cAAc,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,QAAQ,MAAM,iBAAiB,GAAG,CAAC;AAAA,MACtF;AACA,YAAM,SAAS;AACf,oBAAc;AAAA,IAChB,CAAC,EACA,MAAM,MAAM;AACX,UAAI,CAAC,UAAU,CAAC,cAAc;AAC5B,gBAAQ;AACR,WAAG,MAAM,MAAM,cAAc;AAAA,MAC/B;AAAA,IACF,CAAC;AAAA,EACL,CAAC;AACD,QAAM,QAAQ,MAAqB;AACjC,QAAI,aAAc,QAAO;AACzB,mBAAe;AACf,mBAAe,qBAAqB,KAAK,eAAe,EAAE,KAAK,MAAM;AACnE,gBAAU,KAAK;AACf,UAAI,MAAM;AAAA,IACZ,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":["import_sync"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/sync-hub.ts","../src/memory-hub-backend.ts","../src/hub-fanout.ts","../src/resource-limits.ts","../src/create-sync-server.ts","../src/heartbeat.ts","../src/shutdown.ts"],"sourcesContent":["export { SyncHub } from './sync-hub';\r\nexport type { SyncHubOptions, Connection } from './sync-hub';\r\nexport { MemoryHubBackend } from './memory-hub-backend';\r\nexport type { HubBackend } from './hub-backend';\r\nexport { createSyncServer } from './create-sync-server';\r\nexport type { CreateSyncServerOptions } from './create-sync-server';\r\nexport { InMemoryHubFanout } from './hub-fanout';\r\nexport type { HubFanout } from './hub-fanout';\r\nexport type { AuthInfo, AuthResult, Authenticate } from './authenticate';\r\nexport type {\r\n Authorize,\r\n AuthorizeContext,\r\n AuthorizeLayer,\r\n AuthorizeLayerContext,\r\n OwnedElement,\r\n ReadContext,\r\n CanRead,\r\n} from './authorize';\r\nexport { startHeartbeat } from './heartbeat';\r\nexport type { Heartbeat, HeartbeatSocket, HeartbeatServer } from './heartbeat';\r\n","import {\n parseEnvelope,\n isValidElement,\n isNewerLayerRecord,\n type LayerRecord,\n type SyncOp,\n type SyncEnvelope,\n} from '@fieldnotes/sync';\nimport { MemoryHubBackend } from './memory-hub-backend';\nimport { InMemoryHubFanout, type HubFanout } from './hub-fanout';\nimport type { HubBackend } from './hub-backend';\nimport type { Authorize, AuthorizeLayer, CanRead, OwnedElement } from './authorize';\nimport {\n DEFAULT_MAX_JSON_DEPTH,\n DEFAULT_MAX_PRESENCE_LANES,\n DEFAULT_PRESENCE_THROTTLE_MS,\n hasJsonDepthAtMost,\n} from './resource-limits';\n\nexport interface Connection {\n id: string;\n room: string;\n userId?: string;\n role?: string;\n send(message: string): void;\n}\n\nexport interface SyncHubOptions {\n backend?: HubBackend;\n fanout?: HubFanout;\n instanceId?: string;\n authorize?: Authorize;\n authorizeLayer?: AuthorizeLayer;\n canRead?: CanRead;\n maxJsonDepth?: number;\n presenceThrottleMs?: number;\n maxPresenceLanes?: number;\n}\n\nconst HUB_FROM = 'hub';\n\nfunction generateInstanceId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')\n return crypto.randomUUID();\n return `i-${Math.random().toString(36).slice(2)}`;\n}\n\nfunction isFanoutOp(op: unknown): op is Extract<SyncOp, { kind: 'upsert' | 'remove' | 'clear' }> {\n if (typeof op !== 'object' || op === null) return false;\n const o = op as { kind?: unknown; element?: unknown; id?: unknown };\n if (o.kind === 'upsert') return isValidElement(o.element);\n if (o.kind === 'remove') return typeof o.id === 'string';\n return o.kind === 'clear';\n}\n\ntype LayerOp = Extract<SyncOp, { kind: 'layer-upsert' | 'layer-remove' }>;\n\ninterface PresenceLane {\n lastSentAt: number | undefined;\n pending: { data: unknown; timer: ReturnType<typeof setTimeout> } | null;\n}\n\nconst FALLBACK_PRESENCE_LANE = '';\nconst MAX_PRESENCE_LANE_LENGTH = 64;\n\nfunction presenceLaneOf(data: unknown): string {\n if (typeof data !== 'object' || data === null) return FALLBACK_PRESENCE_LANE;\n const kind = (data as { kind?: unknown }).kind;\n if (typeof kind !== 'string' || kind.length === 0 || kind.length > MAX_PRESENCE_LANE_LENGTH) {\n return FALLBACK_PRESENCE_LANE;\n }\n return kind;\n}\n\nfunction isLayerOp(op: unknown): op is LayerOp {\n if (typeof op !== 'object' || op === null) return false;\n const k = (op as { kind?: unknown }).kind;\n // Shape is re-validated by parseEnvelope on the serial path; fanout payloads\n // come from a sibling hub that already validated them.\n return k === 'layer-upsert' || k === 'layer-remove';\n}\n\nfunction layerOpToRecord(op: LayerOp): LayerRecord {\n return op.kind === 'layer-upsert'\n ? { id: op.layer.id, version: op.version, editor: op.editor, definition: op.layer }\n : { id: op.id, version: op.version, editor: op.editor };\n}\n\nfunction layerRecordToOp(record: LayerRecord): LayerOp {\n return record.definition\n ? {\n kind: 'layer-upsert',\n layer: record.definition,\n version: record.version,\n editor: record.editor,\n }\n : { kind: 'layer-remove', id: record.id, version: record.version, editor: record.editor };\n}\n\nfunction isPresenceOp(\n op: unknown,\n): op is { kind: 'presence'; data: unknown } | { kind: 'presence-leave' } {\n if (typeof op !== 'object' || op === null) return false;\n const k = (op as { kind?: unknown }).kind;\n return k === 'presence' || k === 'presence-leave';\n}\n\nexport class SyncHub {\n private readonly backend: HubBackend;\n private readonly conns = new Map<string, Connection>();\n private readonly rooms = new Map<string, Set<string>>(); // room → connIds\n private readonly roomQueues = new Map<string, Promise<void>>(); // room → serial tail\n private readonly presenceConnections = new Set<string>();\n private readonly instanceId: string;\n private readonly fanout: HubFanout;\n private readonly fanoutUnsub: () => void;\n private readonly authorize?: Authorize;\n private readonly authorizeLayer?: AuthorizeLayer;\n private readonly canRead?: CanRead;\n /** Fallback layer-record store for backends without layer persistence. */\n private readonly memoryLayers = new Map<string, Map<string, LayerRecord>>();\n private readonly maxJsonDepth: number;\n private readonly presenceThrottleMs: number;\n private readonly maxPresenceLanes: number;\n /**\n * Presence throttle state keyed by connection, then by lane. A lane is the\n * payload's `kind` (a non-empty string of at most 64 chars) or the reserved\n * fallback lane `''`, so a rapid stream of one kind (awareness cursors) can\n * never replace a pending frame of another kind (a ping, a path `cleared`).\n * Within a lane the newest payload wins. The lane count per connection is\n * capped by `maxPresenceLanes`, counting the fallback lane, so a client\n * cannot mint timers by varying `kind`.\n */\n private readonly presenceLanes = new Map<string, Map<string, PresenceLane>>();\n\n constructor(options: SyncHubOptions = {}) {\n this.backend = options.backend ?? new MemoryHubBackend();\n this.instanceId = options.instanceId ?? generateInstanceId();\n this.fanout = options.fanout ?? new InMemoryHubFanout();\n this.authorize = options.authorize;\n this.authorizeLayer = options.authorizeLayer;\n this.canRead = options.canRead;\n this.maxJsonDepth = options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH;\n this.presenceThrottleMs = options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS;\n const maxPresenceLanes = options.maxPresenceLanes ?? DEFAULT_MAX_PRESENCE_LANES;\n if (!Number.isFinite(maxPresenceLanes) || maxPresenceLanes < 1) {\n throw new RangeError('maxPresenceLanes must be a finite number of at least 1');\n }\n this.maxPresenceLanes = Math.floor(maxPresenceLanes);\n this.fanoutUnsub = this.fanout.subscribe((payload) => this.onFanout(payload));\n }\n\n addConnection(conn: Connection): void {\n this.conns.set(conn.id, conn);\n let set = this.rooms.get(conn.room);\n if (!set) {\n set = new Set();\n this.rooms.set(conn.room, set);\n }\n set.add(conn.id);\n }\n\n removeConnection(connId: string): void {\n const conn = this.conns.get(connId);\n if (!conn) return;\n this.conns.delete(connId);\n const room = conn.room;\n const hadPresence = this.presenceConnections.delete(connId);\n this.clearPresenceLanes(connId);\n const members = this.rooms.get(room);\n if (members) {\n members.delete(connId);\n if (members.size === 0) {\n this.rooms.delete(room);\n this.roomQueues.delete(room);\n }\n }\n if (hadPresence) this.broadcastLeave(room, conn.id);\n }\n\n roomCount(): number {\n return this.rooms.size;\n }\n\n /**\n * Broadcasts ephemeral server-owned presence data to every connection in a room.\n * The returned count covers successful delivery on this hub instance only; configured fan-out\n * forwards the same event to other instances on a best-effort basis.\n */\n broadcastPresence<T>(room: string, data: T): number {\n const op = { kind: 'presence' as const, data };\n const sent = this.relayToRoom(room, undefined, JSON.stringify({ from: HUB_FROM, op }));\n this.safePublish(JSON.stringify({ o: this.instanceId, room, from: HUB_FROM, op }));\n return sent;\n }\n\n handleMessage(connId: string, message: string): Promise<void> {\n const conn = this.conns.get(connId);\n if (!conn) return Promise.resolve();\n if (!hasJsonDepthAtMost(message, this.maxJsonDepth)) return Promise.resolve();\n const env = parseEnvelope(message);\n if (!env) return Promise.resolve();\n if (env.op.kind === 'presence') {\n this.schedulePresence(conn, env.op.data); // off-queue, throttled independently\n return Promise.resolve();\n }\n const room = conn.room;\n // The per-room serial queue is the single total-order authority: ops apply in arrival order\n // (arrival-order LWW — no per-element seq; see D3 / TD-12). Different rooms run independently.\n const prev = this.roomQueues.get(room) ?? Promise.resolve();\n const operation = prev.then(() => this.process(conn, env));\n this.roomQueues.set(\n room,\n operation.catch(() => {\n // Recover only the internal tail so one failed message never wedges the room queue.\n // The caller still receives the operation rejection for observability.\n }),\n );\n return operation;\n }\n\n private async process(conn: Connection, env: SyncEnvelope): Promise<void> {\n const op = env.op;\n if (op.kind === 'request-snapshot') {\n const all = (await this.backend.snapshot(conn.room)) as OwnedElement[];\n const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;\n // Layer records are presentation-only and carry no element bytes, so no\n // audience filter applies; the field is omitted while a room has never\n // used layer sync, keeping snapshot frames byte-identical to before.\n const layers = await this.getLayerRecords(conn.room);\n conn.send(\n // `to` is a private correlation address for the requesting SyncClient. It is never\n // broadcast; every public sender identity below comes from the server-owned connection.\n JSON.stringify({\n from: HUB_FROM,\n op:\n layers.length > 0\n ? { kind: 'snapshot', to: env.from, elements, layers }\n : { kind: 'snapshot', to: env.from, elements },\n }),\n );\n } else if (op.kind === 'layer-upsert' || op.kind === 'layer-remove') {\n await this.processLayerOp(conn, op);\n } else if (op.kind === 'upsert' || op.kind === 'remove' || op.kind === 'clear') {\n const id = op.kind === 'upsert' ? op.element.id : op.kind === 'remove' ? op.id : undefined;\n const needCurrent = (this.authorize || this.canRead) && id !== undefined;\n const current: OwnedElement | undefined = needCurrent\n ? await this.backend.get(conn.room, id)\n : undefined;\n\n let outboundOp: SyncOp = op;\n if (this.authorize) {\n const allowed = await this.authorize({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n op,\n currentElement: current,\n });\n if (!allowed) {\n await this.sendCorrection(conn, env.from, op, current);\n return;\n }\n if (op.kind === 'upsert') {\n const ownerId = current?.ownerId ?? conn.userId;\n const stampedElement: OwnedElement = { ...op.element, ownerId };\n outboundOp = { kind: 'upsert', element: stampedElement };\n }\n }\n\n await this.backend.apply(conn.room, outboundOp);\n\n const prevExisted = current !== undefined;\n const prevAudience = current?.audience;\n\n await this.fanout.publish(\n JSON.stringify({\n o: this.instanceId,\n room: conn.room,\n from: conn.id,\n op: outboundOp,\n prev: prevAudience,\n existed: prevExisted,\n }),\n );\n\n this.deliverToRoom(conn.room, conn.id, conn.id, outboundOp, prevAudience, prevExisted);\n }\n // 'snapshot' from a client → ignored\n }\n\n /**\n * Applies a layer-definition edit on the room's serial queue. Convergence is\n * last-writer-wins under the deterministic (version, editor) ordering — the\n * same rule every client applies — so arrival order never decides a race. A\n * stale or denied edit is answered with an authoritative correction to the\n * sender only.\n */\n private async processLayerOp(conn: Connection, op: LayerOp): Promise<void> {\n const record = layerOpToRecord(op);\n const current = await this.getLayerRecord(conn.room, record.id);\n if (this.authorizeLayer) {\n const allowed = await this.authorizeLayer({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n op,\n currentRecord: current,\n });\n if (!allowed) {\n // Revert the sender to the room's record; a tombstone when there is\n // none, so the denied local edit disappears everywhere consistently.\n const correction = current ?? { id: record.id, version: record.version, editor: HUB_FROM };\n conn.send(JSON.stringify({ from: HUB_FROM, op: layerRecordToOp(correction) }));\n return;\n }\n }\n if (current && !isNewerLayerRecord(record, current)) {\n // Stale under (version, editor): converge the sender, do not broadcast.\n conn.send(JSON.stringify({ from: HUB_FROM, op: layerRecordToOp(current) }));\n return;\n }\n await this.applyLayerRecord(conn.room, record);\n await this.fanout.publish(\n JSON.stringify({ o: this.instanceId, room: conn.room, from: conn.id, op }),\n );\n this.relayToRoom(conn.room, conn.id, JSON.stringify({ from: conn.id, op }));\n }\n\n private layerBackend(): Required<\n Pick<HubBackend, 'layerRecords' | 'getLayerRecord' | 'applyLayerRecord'>\n > | null {\n const { layerRecords, getLayerRecord, applyLayerRecord } = this.backend;\n if (!layerRecords || !getLayerRecord || !applyLayerRecord) return null;\n return {\n layerRecords: layerRecords.bind(this.backend),\n getLayerRecord: getLayerRecord.bind(this.backend),\n applyLayerRecord: applyLayerRecord.bind(this.backend),\n };\n }\n\n private async getLayerRecords(room: string): Promise<LayerRecord[]> {\n const backend = this.layerBackend();\n if (backend) return backend.layerRecords(room);\n return [...(this.memoryLayers.get(room)?.values() ?? [])];\n }\n\n private async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n const backend = this.layerBackend();\n if (backend) return backend.getLayerRecord(room, id);\n return this.memoryLayers.get(room)?.get(id);\n }\n\n private async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n const backend = this.layerBackend();\n if (backend) {\n await backend.applyLayerRecord(room, record);\n return;\n }\n let map = this.memoryLayers.get(room);\n if (!map) {\n map = new Map();\n this.memoryLayers.set(room, map);\n }\n map.set(record.id, record);\n }\n\n private mayRead(conn: Connection, audience: string | undefined): boolean {\n if (!this.canRead) return true;\n return this.canRead({ userId: conn.userId, role: conn.role, room: conn.room, audience });\n }\n\n private safePublish(payload: string): void {\n try {\n void Promise.resolve(this.fanout.publish(payload)).catch(() => {\n /* presence is ephemeral; a broken publisher must not create an unhandled rejection */\n });\n } catch {\n /* a broken fanout publisher must not break the un-queued presence relay */\n }\n }\n\n private relayToRoom(room: string, excludeId: string | undefined, message: string): number {\n const members = this.rooms.get(room);\n if (!members) return 0;\n let sent = 0;\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n try {\n conn.send(message);\n sent += 1;\n } catch {\n /* a throwing socket must not break the relay loop */\n }\n }\n return sent;\n }\n\n private broadcastClientPresence(conn: Connection, data: unknown): void {\n this.presenceConnections.add(conn.id);\n const message = JSON.stringify({ from: conn.id, op: { kind: 'presence', data } });\n this.relayToRoom(conn.room, conn.id, message);\n this.safePublish(\n JSON.stringify({\n o: this.instanceId,\n room: conn.room,\n from: conn.id,\n op: { kind: 'presence', data },\n }),\n );\n }\n\n private clearPresenceLanes(connId: string): void {\n const lanes = this.presenceLanes.get(connId);\n if (!lanes) return;\n for (const lane of lanes.values()) {\n if (lane.pending) clearTimeout(lane.pending.timer);\n }\n this.presenceLanes.delete(connId);\n }\n\n private schedulePresence(conn: Connection, data: unknown): void {\n if (this.presenceThrottleMs <= 0) {\n this.broadcastClientPresence(conn, data);\n return;\n }\n const lane = this.presenceLaneFor(conn.id, presenceLaneOf(data));\n const now = Date.now();\n if (lane.lastSentAt === undefined || now - lane.lastSentAt >= this.presenceThrottleMs) {\n if (lane.pending) {\n clearTimeout(lane.pending.timer);\n lane.pending = null;\n }\n lane.lastSentAt = now;\n this.broadcastClientPresence(conn, data);\n return;\n }\n if (lane.pending) {\n lane.pending.data = data;\n return;\n }\n const timer = setTimeout(\n () => {\n const pending = lane.pending;\n lane.pending = null;\n if (!pending || !this.conns.has(conn.id)) return;\n lane.lastSentAt = Date.now();\n this.broadcastClientPresence(conn, pending.data);\n },\n this.presenceThrottleMs - (now - lane.lastSentAt),\n );\n lane.pending = { data, timer };\n }\n\n private presenceLaneFor(connId: string, requested: string): PresenceLane {\n let lanes = this.presenceLanes.get(connId);\n if (!lanes) {\n lanes = new Map();\n this.presenceLanes.set(connId, lanes);\n }\n let key = requested;\n if (key !== FALLBACK_PRESENCE_LANE && !lanes.has(key)) {\n const named = lanes.size - (lanes.has(FALLBACK_PRESENCE_LANE) ? 1 : 0);\n if (named >= this.maxPresenceLanes - 1) key = FALLBACK_PRESENCE_LANE;\n }\n let lane = lanes.get(key);\n if (!lane) {\n lane = { lastSentAt: undefined, pending: null };\n lanes.set(key, lane);\n }\n return lane;\n }\n\n private broadcastLeave(room: string, from: string): void {\n const message = JSON.stringify({ from, op: { kind: 'presence-leave' } });\n this.relayToRoom(room, undefined, message);\n this.safePublish(\n JSON.stringify({ o: this.instanceId, room, from, op: { kind: 'presence-leave' } }),\n );\n }\n\n private deliverToRoom(\n room: string,\n excludeId: string | undefined,\n from: string,\n op: SyncOp,\n prevAudience: string | undefined,\n prevExisted: boolean,\n ): void {\n const members = this.rooms.get(room);\n if (!members) return;\n const send = (conn: Connection, msg: string): void => {\n try {\n conn.send(msg);\n } catch {\n /* a throwing socket must not break the delivery loop */\n }\n };\n if (op.kind === 'upsert') {\n const audience = (op.element as OwnedElement).audience;\n const upsertMsg = JSON.stringify({ from, op });\n const removeMsg = JSON.stringify({\n from: HUB_FROM,\n op: { kind: 'remove', id: op.element.id },\n });\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n if (this.mayRead(conn, audience)) send(conn, upsertMsg);\n else if (prevExisted && this.mayRead(conn, prevAudience)) send(conn, removeMsg);\n }\n } else if (op.kind === 'remove') {\n const removeMsg = JSON.stringify({ from, op });\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n // No read filter → forward to all (today's behavior; current/prevExisted aren't fetched without\n // a hook). With canRead, only recipients who could see the removed element get it.\n const wasVisible = !this.canRead || (prevExisted && this.mayRead(conn, prevAudience));\n if (wasVisible) send(conn, removeMsg);\n }\n } else if (op.kind === 'clear') {\n const clearMsg = JSON.stringify({ from, op });\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (conn) send(conn, clearMsg);\n }\n }\n }\n\n private async sendCorrection(\n conn: Connection,\n from: string,\n op: SyncOp,\n current: OwnedElement | undefined,\n ): Promise<void> {\n let correction: SyncOp | undefined;\n if (op.kind === 'upsert') {\n correction = current\n ? this.mayRead(conn, current.audience)\n ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: current.id }\n : { kind: 'remove', id: op.element.id };\n } else if (op.kind === 'remove') {\n correction = current\n ? this.mayRead(conn, current.audience)\n ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: current.id }\n : undefined;\n } else if (op.kind === 'clear') {\n const all = (await this.backend.snapshot(conn.room)) as OwnedElement[];\n const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;\n correction = { kind: 'snapshot', to: from, elements };\n }\n if (correction) conn.send(JSON.stringify({ from: HUB_FROM, op: correction }));\n }\n\n private onFanout(payload: string): void {\n // Off the serial queue on purpose: forward-only (the origin already applied to the SHARED backend),\n // and delivery is already ordered. Re-filter per local member (canRead runs on EVERY instance).\n let env: {\n o?: unknown;\n room?: unknown;\n from?: unknown;\n op?: unknown;\n prev?: unknown;\n existed?: unknown;\n };\n try {\n env = JSON.parse(payload);\n } catch {\n return;\n }\n if (typeof env.o !== 'string' || typeof env.room !== 'string' || typeof env.from !== 'string')\n return;\n if (env.o === this.instanceId) return; // our own publish — already delivered locally\n const op = env.op;\n if (isPresenceOp(op)) {\n // presence/leave: raw forward to all local members (the sender lives on the origin instance),\n // no backend, no canRead filter.\n this.relayToRoom(env.room, undefined, JSON.stringify({ from: env.from, op }));\n return;\n }\n if (isLayerOp(op)) {\n // LWW-guarded local apply keeps memory-fallback instances converged; a\n // shared backend already holds the record (the guarded re-apply is a\n // no-op). Relay is unconditional — receiving clients LWW-drop stale ops.\n void this.applyFanoutLayerOp(env.room, op).catch(() => {\n /* a broken backend must not break the fanout relay */\n });\n this.relayToRoom(env.room, undefined, JSON.stringify({ from: env.from, op }));\n return;\n }\n if (!isFanoutOp(op)) return;\n const prevAudience = typeof env.prev === 'string' ? env.prev : undefined;\n const prevExisted = env.existed === true;\n this.deliverToRoom(env.room, undefined, env.from, op, prevAudience, prevExisted);\n }\n\n private async applyFanoutLayerOp(room: string, op: LayerOp): Promise<void> {\n const record = layerOpToRecord(op);\n const current = await this.getLayerRecord(room, record.id);\n if (current && !isNewerLayerRecord(record, current)) return;\n await this.applyLayerRecord(room, record);\n }\n\n close(): void {\n for (const connId of [...this.presenceLanes.keys()]) this.clearPresenceLanes(connId);\n this.fanoutUnsub();\n }\n}\n","import { applyOpToMap, type SyncOp, type LayerRecord } from '@fieldnotes/sync';\r\nimport type { CanvasElement } from '@fieldnotes/core';\r\nimport type { HubBackend } from './hub-backend';\r\n\r\nexport class MemoryHubBackend implements HubBackend {\r\n private rooms = new Map<string, Map<string, CanvasElement>>();\r\n private roomLayers = new Map<string, Map<string, LayerRecord>>();\r\n\r\n private room(id: string): Map<string, CanvasElement> {\r\n let r = this.rooms.get(id);\r\n if (!r) {\r\n r = new Map();\r\n this.rooms.set(id, r);\r\n }\r\n return r;\r\n }\r\n\r\n private layers(room: string): Map<string, LayerRecord> {\r\n let r = this.roomLayers.get(room);\r\n if (!r) {\r\n r = new Map();\r\n this.roomLayers.set(room, r);\r\n }\r\n return r;\r\n }\r\n\r\n async snapshot(room: string): Promise<CanvasElement[]> {\r\n return [...this.room(room).values()];\r\n }\r\n\r\n async get(room: string, id: string): Promise<CanvasElement | undefined> {\r\n return this.room(room).get(id);\r\n }\r\n\r\n async apply(room: string, op: SyncOp): Promise<void> {\r\n if (op.kind === 'clear') {\r\n // Clears elements only; layer records are a separate, longer-lived ledger.\r\n this.rooms.delete(room);\r\n return;\r\n }\r\n applyOpToMap(this.room(room), op);\r\n }\r\n\r\n async layerRecords(room: string): Promise<LayerRecord[]> {\r\n return [...this.layers(room).values()];\r\n }\r\n\r\n async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\r\n return this.layers(room).get(id);\r\n }\r\n\r\n async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\r\n this.layers(room).set(record.id, record);\r\n }\r\n}\r\n","export interface HubFanout {\n publish(payload: string): void | Promise<void>;\n subscribe(handler: (payload: string) => void): () => void;\n close?(): void;\n}\n\nexport class InMemoryHubFanout implements HubFanout {\n private readonly handlers = new Set<(payload: string) => void>();\n\n publish(payload: string): void {\n for (const h of this.handlers) {\n try {\n h(payload);\n } catch {\n /* one throwing subscriber must not break the publish loop */\n }\n }\n }\n\n subscribe(handler: (payload: string) => void): () => void {\n this.handlers.add(handler);\n return () => this.handlers.delete(handler);\n }\n}\n","export const DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024;\nexport const DEFAULT_MAX_JSON_DEPTH = 32;\nexport const DEFAULT_MAX_PENDING_AUTH_MESSAGES = 100;\nexport const DEFAULT_MAX_PENDING_AUTH_BYTES = 2 * 1024 * 1024;\nexport const DEFAULT_MESSAGES_PER_SECOND = 120;\nexport const DEFAULT_MESSAGE_BURST = 240;\nexport const DEFAULT_PRESENCE_THROTTLE_MS = 50;\n/** Per-connection presence throttle lanes, INCLUDING the reserved fallback lane. */\nexport const DEFAULT_MAX_PRESENCE_LANES = 16;\n\nexport function hasJsonDepthAtMost(message: string, maxDepth: number): boolean {\n let depth = 0;\n let inString = false;\n let escaped = false;\n\n for (const char of message) {\n if (inString) {\n if (escaped) escaped = false;\n else if (char === '\\\\') escaped = true;\n else if (char === '\"') inString = false;\n continue;\n }\n if (char === '\"') inString = true;\n else if (char === '{' || char === '[') {\n depth += 1;\n if (depth > maxDepth) return false;\n } else if (char === '}' || char === ']') {\n depth -= 1;\n if (depth < 0) return false;\n }\n }\n\n return depth === 0 && !inString;\n}\n\nexport class MessageRateLimiter {\n private tokens: number;\n private updatedAt: number;\n\n constructor(\n private readonly ratePerSecond: number,\n private readonly burst: number,\n now = Date.now(),\n ) {\n this.tokens = burst;\n this.updatedAt = now;\n }\n\n take(now = Date.now()): boolean {\n const elapsedSeconds = Math.max(0, now - this.updatedAt) / 1000;\n this.tokens = Math.min(this.burst, this.tokens + elapsedSeconds * this.ratePerSecond);\n this.updatedAt = now;\n if (this.tokens < 1) return false;\n this.tokens -= 1;\n return true;\n }\n}\n","import { WebSocketServer, type RawData } from 'ws';\nimport type { Server } from 'http';\nimport { SyncHub } from './sync-hub';\nimport type { HubBackend } from './hub-backend';\nimport type { HubFanout } from './hub-fanout';\nimport type { Authenticate } from './authenticate';\nimport type { Authorize, AuthorizeLayer, CanRead } from './authorize';\nimport { startHeartbeat } from './heartbeat';\nimport {\n DEFAULT_MAX_JSON_DEPTH,\n DEFAULT_MAX_MESSAGE_BYTES,\n DEFAULT_MAX_PENDING_AUTH_BYTES,\n DEFAULT_MAX_PENDING_AUTH_MESSAGES,\n DEFAULT_MESSAGE_BURST,\n DEFAULT_MESSAGES_PER_SECOND,\n DEFAULT_PRESENCE_THROTTLE_MS,\n MessageRateLimiter,\n} from './resource-limits';\nimport { DEFAULT_SHUTDOWN_GRACE_MS, drainWebSocketServer } from './shutdown';\n\nexport interface CreateSyncServerOptions {\n port?: number;\n server?: Server;\n backend?: HubBackend;\n fanout?: HubFanout;\n instanceId?: string;\n authenticate?: Authenticate;\n authorize?: Authorize;\n authorizeLayer?: AuthorizeLayer;\n canRead?: CanRead;\n heartbeatIntervalMs?: number;\n maxMessageBytes?: number;\n maxJsonDepth?: number;\n maxPendingAuthMessages?: number;\n maxPendingAuthBytes?: number;\n messagesPerSecond?: number;\n messageBurst?: number;\n presenceThrottleMs?: number;\n maxPresenceLanes?: number;\n shutdownGraceMs?: number;\n}\n\nfunction rawDataByteLength(data: RawData): number {\n if (Array.isArray(data)) return data.reduce((total, chunk) => total + chunk.byteLength, 0);\n return data.byteLength;\n}\n\nexport function createSyncServer(options: CreateSyncServerOptions = {}): {\n hub: SyncHub;\n wss: WebSocketServer;\n close: () => Promise<void>;\n} {\n const shutdownGraceMs = options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;\n if (!Number.isFinite(shutdownGraceMs) || shutdownGraceMs < 0) {\n throw new RangeError('shutdownGraceMs must be a non-negative finite number');\n }\n const hub = new SyncHub({\n backend: options.backend,\n fanout: options.fanout,\n instanceId: options.instanceId,\n authorize: options.authorize,\n authorizeLayer: options.authorizeLayer,\n canRead: options.canRead,\n maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,\n presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,\n maxPresenceLanes: options.maxPresenceLanes,\n });\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const wss = options.server\n ? new WebSocketServer({ server: options.server, maxPayload: maxMessageBytes })\n : new WebSocketServer({ port: options.port ?? 0, maxPayload: maxMessageBytes });\n const heartbeat = startHeartbeat(wss, options.heartbeatIntervalMs ?? 30000);\n let shuttingDown = false;\n let closePromise: Promise<void> | undefined;\n let counter = 0;\n wss.on('connection', (ws, req) => {\n if (shuttingDown) {\n ws.close(1001, 'server shutting down');\n return;\n }\n heartbeat.track(ws);\n // Protocol/parser failures (including maxPayload) close the peer. Consuming the socket-level\n // error keeps hostile input from becoming an uncaught process error.\n ws.on('error', () => undefined);\n const url = new URL(req.url ?? '', 'http://localhost');\n const room = url.searchParams.get('room');\n if (!room) {\n ws.close(4400, 'room required');\n return;\n }\n const connId = `c${++counter}-${Math.random().toString(36).slice(2, 8)}`;\n\n let state: 'pending' | 'ready' | 'rejected' = 'pending';\n let closed = false;\n let admitted = false;\n const queue: string[] = [];\n let queuedBytes = 0;\n const maxPendingAuthMessages =\n options.maxPendingAuthMessages ?? DEFAULT_MAX_PENDING_AUTH_MESSAGES;\n const maxPendingAuthBytes = options.maxPendingAuthBytes ?? DEFAULT_MAX_PENDING_AUTH_BYTES;\n const limiter = new MessageRateLimiter(\n options.messagesPerSecond ?? DEFAULT_MESSAGES_PER_SECOND,\n options.messageBurst ?? DEFAULT_MESSAGE_BURST,\n );\n\n const send = (m: string) => {\n try {\n ws.send(m);\n } catch {\n /* socket closed mid-send */\n }\n };\n\n ws.on('message', (data) => {\n if (state === 'rejected') return;\n const messageBytes = rawDataByteLength(data);\n if (messageBytes > maxMessageBytes) {\n state = 'rejected';\n ws.close(1009, 'message too large');\n return;\n }\n if (!limiter.take()) {\n state = 'rejected';\n ws.close(4408, 'rate limit exceeded');\n return;\n }\n const msg = String(data);\n if (state === 'pending') {\n if (\n queue.length >= maxPendingAuthMessages ||\n queuedBytes + messageBytes > maxPendingAuthBytes\n ) {\n state = 'rejected';\n ws.close(4408, 'authentication queue limit exceeded');\n return;\n }\n queue.push(msg);\n queuedBytes += messageBytes;\n return;\n }\n void hub.handleMessage(connId, msg).catch((err) => console.error('[sync-server]', err));\n });\n ws.on('close', () => {\n closed = true;\n if (admitted) hub.removeConnection(connId);\n });\n\n Promise.resolve(options.authenticate ? options.authenticate({ req, room }) : { userId: connId })\n .then((result) => {\n if (closed || state === 'rejected' || shuttingDown) return;\n if (!result) {\n state = 'rejected';\n ws.close(4401, 'unauthorized');\n return;\n }\n state = 'ready';\n admitted = true;\n hub.addConnection({ id: connId, room, userId: result.userId, role: result.role, send });\n for (const m of queue) {\n void hub.handleMessage(connId, m).catch((err) => console.error('[sync-server]', err));\n }\n queue.length = 0;\n queuedBytes = 0;\n })\n .catch(() => {\n if (!closed && !shuttingDown) {\n state = 'rejected';\n ws.close(4401, 'unauthorized');\n }\n });\n });\n const close = (): Promise<void> => {\n if (closePromise) return closePromise;\n shuttingDown = true;\n closePromise = drainWebSocketServer(wss, shutdownGraceMs).then(() => {\n heartbeat.stop();\n hub.close();\n });\n return closePromise;\n };\n\n return {\n hub,\n wss,\n close,\n };\n}\n","export interface HeartbeatSocket {\n ping(): void;\n terminate(): void;\n on(event: 'pong', listener: () => void): void;\n}\nexport interface HeartbeatServer {\n clients: Set<HeartbeatSocket>;\n}\nexport interface Heartbeat {\n track(ws: HeartbeatSocket): void;\n stop(): void;\n}\n\nexport function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat {\n if (intervalMs <= 0) {\n return {\n track: () => {\n /* disabled */\n },\n stop: () => {\n /* disabled */\n },\n };\n }\n const alive = new WeakMap<HeartbeatSocket, boolean>();\n const track = (ws: HeartbeatSocket): void => {\n alive.set(ws, true);\n ws.on('pong', () => alive.set(ws, true));\n };\n const interval = setInterval(() => {\n for (const ws of wss.clients) {\n try {\n if (alive.get(ws) === false) {\n ws.terminate();\n continue;\n }\n alive.set(ws, false);\n ws.ping();\n } catch {\n /* socket dying mid-tick — skip it */\n }\n }\n }, intervalMs);\n return { track, stop: () => clearInterval(interval) };\n}\n","export const DEFAULT_SHUTDOWN_GRACE_MS = 5000;\n\nexport interface ShutdownSocket {\n close(code?: number, reason?: string): void;\n terminate(): void;\n}\n\nexport interface ShutdownServer {\n clients: Set<ShutdownSocket>;\n close(callback: (error?: Error) => void): void;\n}\n\n/** Stop admission, ask connected peers to leave, then forcefully reap stragglers. */\nexport function drainWebSocketServer(wss: ShutdownServer, graceMs: number): Promise<void> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = (): void => {\n if (settled) return;\n settled = true;\n clearTimeout(forceTimer);\n resolve();\n };\n const forceTimer = setTimeout(() => {\n for (const ws of wss.clients) {\n try {\n ws.terminate();\n } catch {\n /* socket already failed while shutdown was in progress */\n }\n }\n finish();\n }, graceMs);\n\n // Calling close first prevents new connections. Its callback runs once all clients have left.\n wss.close(() => finish());\n for (const ws of wss.clients) {\n try {\n ws.close(1001, 'server shutting down');\n } catch {\n try {\n ws.terminate();\n } catch {\n /* socket already closed */\n }\n }\n }\n });\n}\n"],"mappings":"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","names":["import_sync"]}
package/dist/index.d.cts CHANGED
@@ -84,6 +84,7 @@ interface SyncHubOptions {
84
84
  canRead?: CanRead;
85
85
  maxJsonDepth?: number;
86
86
  presenceThrottleMs?: number;
87
+ maxPresenceLanes?: number;
87
88
  }
88
89
  declare class SyncHub {
89
90
  private readonly backend;
@@ -101,8 +102,17 @@ declare class SyncHub {
101
102
  private readonly memoryLayers;
102
103
  private readonly maxJsonDepth;
103
104
  private readonly presenceThrottleMs;
104
- private readonly lastPresenceAt;
105
- private readonly pendingPresence;
105
+ private readonly maxPresenceLanes;
106
+ /**
107
+ * Presence throttle state keyed by connection, then by lane. A lane is the
108
+ * payload's `kind` (a non-empty string of at most 64 chars) or the reserved
109
+ * fallback lane `''`, so a rapid stream of one kind (awareness cursors) can
110
+ * never replace a pending frame of another kind (a ping, a path `cleared`).
111
+ * Within a lane the newest payload wins. The lane count per connection is
112
+ * capped by `maxPresenceLanes`, counting the fallback lane, so a client
113
+ * cannot mint timers by varying `kind`.
114
+ */
115
+ private readonly presenceLanes;
106
116
  constructor(options?: SyncHubOptions);
107
117
  addConnection(conn: Connection): void;
108
118
  removeConnection(connId: string): void;
@@ -131,7 +141,9 @@ declare class SyncHub {
131
141
  private safePublish;
132
142
  private relayToRoom;
133
143
  private broadcastClientPresence;
144
+ private clearPresenceLanes;
134
145
  private schedulePresence;
146
+ private presenceLaneFor;
135
147
  private broadcastLeave;
136
148
  private deliverToRoom;
137
149
  private sendCorrection;
@@ -181,6 +193,7 @@ interface CreateSyncServerOptions {
181
193
  messagesPerSecond?: number;
182
194
  messageBurst?: number;
183
195
  presenceThrottleMs?: number;
196
+ maxPresenceLanes?: number;
184
197
  shutdownGraceMs?: number;
185
198
  }
186
199
  declare function createSyncServer(options?: CreateSyncServerOptions): {
package/dist/index.d.ts CHANGED
@@ -84,6 +84,7 @@ interface SyncHubOptions {
84
84
  canRead?: CanRead;
85
85
  maxJsonDepth?: number;
86
86
  presenceThrottleMs?: number;
87
+ maxPresenceLanes?: number;
87
88
  }
88
89
  declare class SyncHub {
89
90
  private readonly backend;
@@ -101,8 +102,17 @@ declare class SyncHub {
101
102
  private readonly memoryLayers;
102
103
  private readonly maxJsonDepth;
103
104
  private readonly presenceThrottleMs;
104
- private readonly lastPresenceAt;
105
- private readonly pendingPresence;
105
+ private readonly maxPresenceLanes;
106
+ /**
107
+ * Presence throttle state keyed by connection, then by lane. A lane is the
108
+ * payload's `kind` (a non-empty string of at most 64 chars) or the reserved
109
+ * fallback lane `''`, so a rapid stream of one kind (awareness cursors) can
110
+ * never replace a pending frame of another kind (a ping, a path `cleared`).
111
+ * Within a lane the newest payload wins. The lane count per connection is
112
+ * capped by `maxPresenceLanes`, counting the fallback lane, so a client
113
+ * cannot mint timers by varying `kind`.
114
+ */
115
+ private readonly presenceLanes;
106
116
  constructor(options?: SyncHubOptions);
107
117
  addConnection(conn: Connection): void;
108
118
  removeConnection(connId: string): void;
@@ -131,7 +141,9 @@ declare class SyncHub {
131
141
  private safePublish;
132
142
  private relayToRoom;
133
143
  private broadcastClientPresence;
144
+ private clearPresenceLanes;
134
145
  private schedulePresence;
146
+ private presenceLaneFor;
135
147
  private broadcastLeave;
136
148
  private deliverToRoom;
137
149
  private sendCorrection;
@@ -181,6 +193,7 @@ interface CreateSyncServerOptions {
181
193
  messagesPerSecond?: number;
182
194
  messageBurst?: number;
183
195
  presenceThrottleMs?: number;
196
+ maxPresenceLanes?: number;
184
197
  shutdownGraceMs?: number;
185
198
  }
186
199
  declare function createSyncServer(options?: CreateSyncServerOptions): {
package/dist/index.js CHANGED
@@ -75,6 +75,7 @@ var DEFAULT_MAX_PENDING_AUTH_BYTES = 2 * 1024 * 1024;
75
75
  var DEFAULT_MESSAGES_PER_SECOND = 120;
76
76
  var DEFAULT_MESSAGE_BURST = 240;
77
77
  var DEFAULT_PRESENCE_THROTTLE_MS = 50;
78
+ var DEFAULT_MAX_PRESENCE_LANES = 16;
78
79
  function hasJsonDepthAtMost(message, maxDepth) {
79
80
  let depth = 0;
80
81
  let inString = false;
@@ -130,6 +131,16 @@ function isFanoutOp(op) {
130
131
  if (o.kind === "remove") return typeof o.id === "string";
131
132
  return o.kind === "clear";
132
133
  }
134
+ var FALLBACK_PRESENCE_LANE = "";
135
+ var MAX_PRESENCE_LANE_LENGTH = 64;
136
+ function presenceLaneOf(data) {
137
+ if (typeof data !== "object" || data === null) return FALLBACK_PRESENCE_LANE;
138
+ const kind = data.kind;
139
+ if (typeof kind !== "string" || kind.length === 0 || kind.length > MAX_PRESENCE_LANE_LENGTH) {
140
+ return FALLBACK_PRESENCE_LANE;
141
+ }
142
+ return kind;
143
+ }
133
144
  function isLayerOp(op) {
134
145
  if (typeof op !== "object" || op === null) return false;
135
146
  const k = op.kind;
@@ -169,8 +180,17 @@ var SyncHub = class {
169
180
  memoryLayers = /* @__PURE__ */ new Map();
170
181
  maxJsonDepth;
171
182
  presenceThrottleMs;
172
- lastPresenceAt = /* @__PURE__ */ new Map();
173
- pendingPresence = /* @__PURE__ */ new Map();
183
+ maxPresenceLanes;
184
+ /**
185
+ * Presence throttle state keyed by connection, then by lane. A lane is the
186
+ * payload's `kind` (a non-empty string of at most 64 chars) or the reserved
187
+ * fallback lane `''`, so a rapid stream of one kind (awareness cursors) can
188
+ * never replace a pending frame of another kind (a ping, a path `cleared`).
189
+ * Within a lane the newest payload wins. The lane count per connection is
190
+ * capped by `maxPresenceLanes`, counting the fallback lane, so a client
191
+ * cannot mint timers by varying `kind`.
192
+ */
193
+ presenceLanes = /* @__PURE__ */ new Map();
174
194
  constructor(options = {}) {
175
195
  this.backend = options.backend ?? new MemoryHubBackend();
176
196
  this.instanceId = options.instanceId ?? generateInstanceId();
@@ -180,6 +200,11 @@ var SyncHub = class {
180
200
  this.canRead = options.canRead;
181
201
  this.maxJsonDepth = options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH;
182
202
  this.presenceThrottleMs = options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS;
203
+ const maxPresenceLanes = options.maxPresenceLanes ?? DEFAULT_MAX_PRESENCE_LANES;
204
+ if (!Number.isFinite(maxPresenceLanes) || maxPresenceLanes < 1) {
205
+ throw new RangeError("maxPresenceLanes must be a finite number of at least 1");
206
+ }
207
+ this.maxPresenceLanes = Math.floor(maxPresenceLanes);
183
208
  this.fanoutUnsub = this.fanout.subscribe((payload) => this.onFanout(payload));
184
209
  }
185
210
  addConnection(conn) {
@@ -197,10 +222,7 @@ var SyncHub = class {
197
222
  this.conns.delete(connId);
198
223
  const room = conn.room;
199
224
  const hadPresence = this.presenceConnections.delete(connId);
200
- this.lastPresenceAt.delete(connId);
201
- const pendingPresence = this.pendingPresence.get(connId);
202
- if (pendingPresence) clearTimeout(pendingPresence.timer);
203
- this.pendingPresence.delete(connId);
225
+ this.clearPresenceLanes(connId);
204
226
  const members = this.rooms.get(room);
205
227
  if (members) {
206
228
  members.delete(connId);
@@ -406,34 +428,63 @@ var SyncHub = class {
406
428
  })
407
429
  );
408
430
  }
431
+ clearPresenceLanes(connId) {
432
+ const lanes = this.presenceLanes.get(connId);
433
+ if (!lanes) return;
434
+ for (const lane of lanes.values()) {
435
+ if (lane.pending) clearTimeout(lane.pending.timer);
436
+ }
437
+ this.presenceLanes.delete(connId);
438
+ }
409
439
  schedulePresence(conn, data) {
410
440
  if (this.presenceThrottleMs <= 0) {
411
441
  this.broadcastClientPresence(conn, data);
412
442
  return;
413
443
  }
444
+ const lane = this.presenceLaneFor(conn.id, presenceLaneOf(data));
414
445
  const now = Date.now();
415
- const lastSentAt = this.lastPresenceAt.get(conn.id);
416
- if (lastSentAt === void 0 || now - lastSentAt >= this.presenceThrottleMs) {
417
- this.lastPresenceAt.set(conn.id, now);
446
+ if (lane.lastSentAt === void 0 || now - lane.lastSentAt >= this.presenceThrottleMs) {
447
+ if (lane.pending) {
448
+ clearTimeout(lane.pending.timer);
449
+ lane.pending = null;
450
+ }
451
+ lane.lastSentAt = now;
418
452
  this.broadcastClientPresence(conn, data);
419
453
  return;
420
454
  }
421
- const existing = this.pendingPresence.get(conn.id);
422
- if (existing) {
423
- existing.data = data;
455
+ if (lane.pending) {
456
+ lane.pending.data = data;
424
457
  return;
425
458
  }
426
459
  const timer = setTimeout(
427
460
  () => {
428
- const pending = this.pendingPresence.get(conn.id);
429
- this.pendingPresence.delete(conn.id);
461
+ const pending = lane.pending;
462
+ lane.pending = null;
430
463
  if (!pending || !this.conns.has(conn.id)) return;
431
- this.lastPresenceAt.set(conn.id, Date.now());
464
+ lane.lastSentAt = Date.now();
432
465
  this.broadcastClientPresence(conn, pending.data);
433
466
  },
434
- this.presenceThrottleMs - (now - lastSentAt)
467
+ this.presenceThrottleMs - (now - lane.lastSentAt)
435
468
  );
436
- this.pendingPresence.set(conn.id, { data, timer });
469
+ lane.pending = { data, timer };
470
+ }
471
+ presenceLaneFor(connId, requested) {
472
+ let lanes = this.presenceLanes.get(connId);
473
+ if (!lanes) {
474
+ lanes = /* @__PURE__ */ new Map();
475
+ this.presenceLanes.set(connId, lanes);
476
+ }
477
+ let key = requested;
478
+ if (key !== FALLBACK_PRESENCE_LANE && !lanes.has(key)) {
479
+ const named = lanes.size - (lanes.has(FALLBACK_PRESENCE_LANE) ? 1 : 0);
480
+ if (named >= this.maxPresenceLanes - 1) key = FALLBACK_PRESENCE_LANE;
481
+ }
482
+ let lane = lanes.get(key);
483
+ if (!lane) {
484
+ lane = { lastSentAt: void 0, pending: null };
485
+ lanes.set(key, lane);
486
+ }
487
+ return lane;
437
488
  }
438
489
  broadcastLeave(room, from) {
439
490
  const message = JSON.stringify({ from, op: { kind: "presence-leave" } });
@@ -529,8 +580,7 @@ var SyncHub = class {
529
580
  await this.applyLayerRecord(room, record);
530
581
  }
531
582
  close() {
532
- for (const pending of this.pendingPresence.values()) clearTimeout(pending.timer);
533
- this.pendingPresence.clear();
583
+ for (const connId of [...this.presenceLanes.keys()]) this.clearPresenceLanes(connId);
534
584
  this.fanoutUnsub();
535
585
  }
536
586
  };
@@ -621,7 +671,8 @@ function createSyncServer(options = {}) {
621
671
  authorizeLayer: options.authorizeLayer,
622
672
  canRead: options.canRead,
623
673
  maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,
624
- presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS
674
+ presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,
675
+ maxPresenceLanes: options.maxPresenceLanes
625
676
  });
626
677
  const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
627
678
  const wss = options.server ? new WebSocketServer({ server: options.server, maxPayload: maxMessageBytes }) : new WebSocketServer({ port: options.port ?? 0, maxPayload: maxMessageBytes });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sync-hub.ts","../src/memory-hub-backend.ts","../src/hub-fanout.ts","../src/resource-limits.ts","../src/create-sync-server.ts","../src/heartbeat.ts","../src/shutdown.ts"],"sourcesContent":["import {\n parseEnvelope,\n isValidElement,\n isNewerLayerRecord,\n type LayerRecord,\n type SyncOp,\n type SyncEnvelope,\n} from '@fieldnotes/sync';\nimport { MemoryHubBackend } from './memory-hub-backend';\nimport { InMemoryHubFanout, type HubFanout } from './hub-fanout';\nimport type { HubBackend } from './hub-backend';\nimport type { Authorize, AuthorizeLayer, CanRead, OwnedElement } from './authorize';\nimport {\n DEFAULT_MAX_JSON_DEPTH,\n DEFAULT_PRESENCE_THROTTLE_MS,\n hasJsonDepthAtMost,\n} from './resource-limits';\n\nexport interface Connection {\n id: string;\n room: string;\n userId?: string;\n role?: string;\n send(message: string): void;\n}\n\nexport interface SyncHubOptions {\n backend?: HubBackend;\n fanout?: HubFanout;\n instanceId?: string;\n authorize?: Authorize;\n authorizeLayer?: AuthorizeLayer;\n canRead?: CanRead;\n maxJsonDepth?: number;\n presenceThrottleMs?: number;\n}\n\nconst HUB_FROM = 'hub';\n\nfunction generateInstanceId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')\n return crypto.randomUUID();\n return `i-${Math.random().toString(36).slice(2)}`;\n}\n\nfunction isFanoutOp(op: unknown): op is Extract<SyncOp, { kind: 'upsert' | 'remove' | 'clear' }> {\n if (typeof op !== 'object' || op === null) return false;\n const o = op as { kind?: unknown; element?: unknown; id?: unknown };\n if (o.kind === 'upsert') return isValidElement(o.element);\n if (o.kind === 'remove') return typeof o.id === 'string';\n return o.kind === 'clear';\n}\n\ntype LayerOp = Extract<SyncOp, { kind: 'layer-upsert' | 'layer-remove' }>;\n\nfunction isLayerOp(op: unknown): op is LayerOp {\n if (typeof op !== 'object' || op === null) return false;\n const k = (op as { kind?: unknown }).kind;\n // Shape is re-validated by parseEnvelope on the serial path; fanout payloads\n // come from a sibling hub that already validated them.\n return k === 'layer-upsert' || k === 'layer-remove';\n}\n\nfunction layerOpToRecord(op: LayerOp): LayerRecord {\n return op.kind === 'layer-upsert'\n ? { id: op.layer.id, version: op.version, editor: op.editor, definition: op.layer }\n : { id: op.id, version: op.version, editor: op.editor };\n}\n\nfunction layerRecordToOp(record: LayerRecord): LayerOp {\n return record.definition\n ? {\n kind: 'layer-upsert',\n layer: record.definition,\n version: record.version,\n editor: record.editor,\n }\n : { kind: 'layer-remove', id: record.id, version: record.version, editor: record.editor };\n}\n\nfunction isPresenceOp(\n op: unknown,\n): op is { kind: 'presence'; data: unknown } | { kind: 'presence-leave' } {\n if (typeof op !== 'object' || op === null) return false;\n const k = (op as { kind?: unknown }).kind;\n return k === 'presence' || k === 'presence-leave';\n}\n\nexport class SyncHub {\n private readonly backend: HubBackend;\n private readonly conns = new Map<string, Connection>();\n private readonly rooms = new Map<string, Set<string>>(); // room → connIds\n private readonly roomQueues = new Map<string, Promise<void>>(); // room → serial tail\n private readonly presenceConnections = new Set<string>();\n private readonly instanceId: string;\n private readonly fanout: HubFanout;\n private readonly fanoutUnsub: () => void;\n private readonly authorize?: Authorize;\n private readonly authorizeLayer?: AuthorizeLayer;\n private readonly canRead?: CanRead;\n /** Fallback layer-record store for backends without layer persistence. */\n private readonly memoryLayers = new Map<string, Map<string, LayerRecord>>();\n private readonly maxJsonDepth: number;\n private readonly presenceThrottleMs: number;\n private readonly lastPresenceAt = new Map<string, number>();\n private readonly pendingPresence = new Map<\n string,\n { data: unknown; timer: ReturnType<typeof setTimeout> }\n >();\n\n constructor(options: SyncHubOptions = {}) {\n this.backend = options.backend ?? new MemoryHubBackend();\n this.instanceId = options.instanceId ?? generateInstanceId();\n this.fanout = options.fanout ?? new InMemoryHubFanout();\n this.authorize = options.authorize;\n this.authorizeLayer = options.authorizeLayer;\n this.canRead = options.canRead;\n this.maxJsonDepth = options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH;\n this.presenceThrottleMs = options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS;\n this.fanoutUnsub = this.fanout.subscribe((payload) => this.onFanout(payload));\n }\n\n addConnection(conn: Connection): void {\n this.conns.set(conn.id, conn);\n let set = this.rooms.get(conn.room);\n if (!set) {\n set = new Set();\n this.rooms.set(conn.room, set);\n }\n set.add(conn.id);\n }\n\n removeConnection(connId: string): void {\n const conn = this.conns.get(connId);\n if (!conn) return;\n this.conns.delete(connId);\n const room = conn.room;\n const hadPresence = this.presenceConnections.delete(connId);\n this.lastPresenceAt.delete(connId);\n const pendingPresence = this.pendingPresence.get(connId);\n if (pendingPresence) clearTimeout(pendingPresence.timer);\n this.pendingPresence.delete(connId);\n const members = this.rooms.get(room);\n if (members) {\n members.delete(connId);\n if (members.size === 0) {\n this.rooms.delete(room);\n this.roomQueues.delete(room);\n }\n }\n if (hadPresence) this.broadcastLeave(room, conn.id);\n }\n\n roomCount(): number {\n return this.rooms.size;\n }\n\n /**\n * Broadcasts ephemeral server-owned presence data to every connection in a room.\n * The returned count covers successful delivery on this hub instance only; configured fan-out\n * forwards the same event to other instances on a best-effort basis.\n */\n broadcastPresence<T>(room: string, data: T): number {\n const op = { kind: 'presence' as const, data };\n const sent = this.relayToRoom(room, undefined, JSON.stringify({ from: HUB_FROM, op }));\n this.safePublish(JSON.stringify({ o: this.instanceId, room, from: HUB_FROM, op }));\n return sent;\n }\n\n handleMessage(connId: string, message: string): Promise<void> {\n const conn = this.conns.get(connId);\n if (!conn) return Promise.resolve();\n if (!hasJsonDepthAtMost(message, this.maxJsonDepth)) return Promise.resolve();\n const env = parseEnvelope(message);\n if (!env) return Promise.resolve();\n if (env.op.kind === 'presence') {\n this.schedulePresence(conn, env.op.data); // off-queue, throttled independently\n return Promise.resolve();\n }\n const room = conn.room;\n // The per-room serial queue is the single total-order authority: ops apply in arrival order\n // (arrival-order LWW — no per-element seq; see D3 / TD-12). Different rooms run independently.\n const prev = this.roomQueues.get(room) ?? Promise.resolve();\n const operation = prev.then(() => this.process(conn, env));\n this.roomQueues.set(\n room,\n operation.catch(() => {\n // Recover only the internal tail so one failed message never wedges the room queue.\n // The caller still receives the operation rejection for observability.\n }),\n );\n return operation;\n }\n\n private async process(conn: Connection, env: SyncEnvelope): Promise<void> {\n const op = env.op;\n if (op.kind === 'request-snapshot') {\n const all = (await this.backend.snapshot(conn.room)) as OwnedElement[];\n const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;\n // Layer records are presentation-only and carry no element bytes, so no\n // audience filter applies; the field is omitted while a room has never\n // used layer sync, keeping snapshot frames byte-identical to before.\n const layers = await this.getLayerRecords(conn.room);\n conn.send(\n // `to` is a private correlation address for the requesting SyncClient. It is never\n // broadcast; every public sender identity below comes from the server-owned connection.\n JSON.stringify({\n from: HUB_FROM,\n op:\n layers.length > 0\n ? { kind: 'snapshot', to: env.from, elements, layers }\n : { kind: 'snapshot', to: env.from, elements },\n }),\n );\n } else if (op.kind === 'layer-upsert' || op.kind === 'layer-remove') {\n await this.processLayerOp(conn, op);\n } else if (op.kind === 'upsert' || op.kind === 'remove' || op.kind === 'clear') {\n const id = op.kind === 'upsert' ? op.element.id : op.kind === 'remove' ? op.id : undefined;\n const needCurrent = (this.authorize || this.canRead) && id !== undefined;\n const current: OwnedElement | undefined = needCurrent\n ? await this.backend.get(conn.room, id)\n : undefined;\n\n let outboundOp: SyncOp = op;\n if (this.authorize) {\n const allowed = await this.authorize({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n op,\n currentElement: current,\n });\n if (!allowed) {\n await this.sendCorrection(conn, env.from, op, current);\n return;\n }\n if (op.kind === 'upsert') {\n const ownerId = current?.ownerId ?? conn.userId;\n const stampedElement: OwnedElement = { ...op.element, ownerId };\n outboundOp = { kind: 'upsert', element: stampedElement };\n }\n }\n\n await this.backend.apply(conn.room, outboundOp);\n\n const prevExisted = current !== undefined;\n const prevAudience = current?.audience;\n\n await this.fanout.publish(\n JSON.stringify({\n o: this.instanceId,\n room: conn.room,\n from: conn.id,\n op: outboundOp,\n prev: prevAudience,\n existed: prevExisted,\n }),\n );\n\n this.deliverToRoom(conn.room, conn.id, conn.id, outboundOp, prevAudience, prevExisted);\n }\n // 'snapshot' from a client → ignored\n }\n\n /**\n * Applies a layer-definition edit on the room's serial queue. Convergence is\n * last-writer-wins under the deterministic (version, editor) ordering — the\n * same rule every client applies — so arrival order never decides a race. A\n * stale or denied edit is answered with an authoritative correction to the\n * sender only.\n */\n private async processLayerOp(conn: Connection, op: LayerOp): Promise<void> {\n const record = layerOpToRecord(op);\n const current = await this.getLayerRecord(conn.room, record.id);\n if (this.authorizeLayer) {\n const allowed = await this.authorizeLayer({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n op,\n currentRecord: current,\n });\n if (!allowed) {\n // Revert the sender to the room's record; a tombstone when there is\n // none, so the denied local edit disappears everywhere consistently.\n const correction = current ?? { id: record.id, version: record.version, editor: HUB_FROM };\n conn.send(JSON.stringify({ from: HUB_FROM, op: layerRecordToOp(correction) }));\n return;\n }\n }\n if (current && !isNewerLayerRecord(record, current)) {\n // Stale under (version, editor): converge the sender, do not broadcast.\n conn.send(JSON.stringify({ from: HUB_FROM, op: layerRecordToOp(current) }));\n return;\n }\n await this.applyLayerRecord(conn.room, record);\n await this.fanout.publish(\n JSON.stringify({ o: this.instanceId, room: conn.room, from: conn.id, op }),\n );\n this.relayToRoom(conn.room, conn.id, JSON.stringify({ from: conn.id, op }));\n }\n\n private layerBackend(): Required<\n Pick<HubBackend, 'layerRecords' | 'getLayerRecord' | 'applyLayerRecord'>\n > | null {\n const { layerRecords, getLayerRecord, applyLayerRecord } = this.backend;\n if (!layerRecords || !getLayerRecord || !applyLayerRecord) return null;\n return {\n layerRecords: layerRecords.bind(this.backend),\n getLayerRecord: getLayerRecord.bind(this.backend),\n applyLayerRecord: applyLayerRecord.bind(this.backend),\n };\n }\n\n private async getLayerRecords(room: string): Promise<LayerRecord[]> {\n const backend = this.layerBackend();\n if (backend) return backend.layerRecords(room);\n return [...(this.memoryLayers.get(room)?.values() ?? [])];\n }\n\n private async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n const backend = this.layerBackend();\n if (backend) return backend.getLayerRecord(room, id);\n return this.memoryLayers.get(room)?.get(id);\n }\n\n private async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n const backend = this.layerBackend();\n if (backend) {\n await backend.applyLayerRecord(room, record);\n return;\n }\n let map = this.memoryLayers.get(room);\n if (!map) {\n map = new Map();\n this.memoryLayers.set(room, map);\n }\n map.set(record.id, record);\n }\n\n private mayRead(conn: Connection, audience: string | undefined): boolean {\n if (!this.canRead) return true;\n return this.canRead({ userId: conn.userId, role: conn.role, room: conn.room, audience });\n }\n\n private safePublish(payload: string): void {\n try {\n void Promise.resolve(this.fanout.publish(payload)).catch(() => {\n /* presence is ephemeral; a broken publisher must not create an unhandled rejection */\n });\n } catch {\n /* a broken fanout publisher must not break the un-queued presence relay */\n }\n }\n\n private relayToRoom(room: string, excludeId: string | undefined, message: string): number {\n const members = this.rooms.get(room);\n if (!members) return 0;\n let sent = 0;\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n try {\n conn.send(message);\n sent += 1;\n } catch {\n /* a throwing socket must not break the relay loop */\n }\n }\n return sent;\n }\n\n private broadcastClientPresence(conn: Connection, data: unknown): void {\n this.presenceConnections.add(conn.id);\n const message = JSON.stringify({ from: conn.id, op: { kind: 'presence', data } });\n this.relayToRoom(conn.room, conn.id, message);\n this.safePublish(\n JSON.stringify({\n o: this.instanceId,\n room: conn.room,\n from: conn.id,\n op: { kind: 'presence', data },\n }),\n );\n }\n\n private schedulePresence(conn: Connection, data: unknown): void {\n if (this.presenceThrottleMs <= 0) {\n this.broadcastClientPresence(conn, data);\n return;\n }\n const now = Date.now();\n const lastSentAt = this.lastPresenceAt.get(conn.id);\n if (lastSentAt === undefined || now - lastSentAt >= this.presenceThrottleMs) {\n this.lastPresenceAt.set(conn.id, now);\n this.broadcastClientPresence(conn, data);\n return;\n }\n\n const existing = this.pendingPresence.get(conn.id);\n if (existing) {\n existing.data = data;\n return;\n }\n const timer = setTimeout(\n () => {\n const pending = this.pendingPresence.get(conn.id);\n this.pendingPresence.delete(conn.id);\n if (!pending || !this.conns.has(conn.id)) return;\n this.lastPresenceAt.set(conn.id, Date.now());\n this.broadcastClientPresence(conn, pending.data);\n },\n this.presenceThrottleMs - (now - lastSentAt),\n );\n this.pendingPresence.set(conn.id, { data, timer });\n }\n\n private broadcastLeave(room: string, from: string): void {\n const message = JSON.stringify({ from, op: { kind: 'presence-leave' } });\n this.relayToRoom(room, undefined, message);\n this.safePublish(\n JSON.stringify({ o: this.instanceId, room, from, op: { kind: 'presence-leave' } }),\n );\n }\n\n private deliverToRoom(\n room: string,\n excludeId: string | undefined,\n from: string,\n op: SyncOp,\n prevAudience: string | undefined,\n prevExisted: boolean,\n ): void {\n const members = this.rooms.get(room);\n if (!members) return;\n const send = (conn: Connection, msg: string): void => {\n try {\n conn.send(msg);\n } catch {\n /* a throwing socket must not break the delivery loop */\n }\n };\n if (op.kind === 'upsert') {\n const audience = (op.element as OwnedElement).audience;\n const upsertMsg = JSON.stringify({ from, op });\n const removeMsg = JSON.stringify({\n from: HUB_FROM,\n op: { kind: 'remove', id: op.element.id },\n });\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n if (this.mayRead(conn, audience)) send(conn, upsertMsg);\n else if (prevExisted && this.mayRead(conn, prevAudience)) send(conn, removeMsg);\n }\n } else if (op.kind === 'remove') {\n const removeMsg = JSON.stringify({ from, op });\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n // No read filter → forward to all (today's behavior; current/prevExisted aren't fetched without\n // a hook). With canRead, only recipients who could see the removed element get it.\n const wasVisible = !this.canRead || (prevExisted && this.mayRead(conn, prevAudience));\n if (wasVisible) send(conn, removeMsg);\n }\n } else if (op.kind === 'clear') {\n const clearMsg = JSON.stringify({ from, op });\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (conn) send(conn, clearMsg);\n }\n }\n }\n\n private async sendCorrection(\n conn: Connection,\n from: string,\n op: SyncOp,\n current: OwnedElement | undefined,\n ): Promise<void> {\n let correction: SyncOp | undefined;\n if (op.kind === 'upsert') {\n correction = current\n ? this.mayRead(conn, current.audience)\n ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: current.id }\n : { kind: 'remove', id: op.element.id };\n } else if (op.kind === 'remove') {\n correction = current\n ? this.mayRead(conn, current.audience)\n ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: current.id }\n : undefined;\n } else if (op.kind === 'clear') {\n const all = (await this.backend.snapshot(conn.room)) as OwnedElement[];\n const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;\n correction = { kind: 'snapshot', to: from, elements };\n }\n if (correction) conn.send(JSON.stringify({ from: HUB_FROM, op: correction }));\n }\n\n private onFanout(payload: string): void {\n // Off the serial queue on purpose: forward-only (the origin already applied to the SHARED backend),\n // and delivery is already ordered. Re-filter per local member (canRead runs on EVERY instance).\n let env: {\n o?: unknown;\n room?: unknown;\n from?: unknown;\n op?: unknown;\n prev?: unknown;\n existed?: unknown;\n };\n try {\n env = JSON.parse(payload);\n } catch {\n return;\n }\n if (typeof env.o !== 'string' || typeof env.room !== 'string' || typeof env.from !== 'string')\n return;\n if (env.o === this.instanceId) return; // our own publish — already delivered locally\n const op = env.op;\n if (isPresenceOp(op)) {\n // presence/leave: raw forward to all local members (the sender lives on the origin instance),\n // no backend, no canRead filter.\n this.relayToRoom(env.room, undefined, JSON.stringify({ from: env.from, op }));\n return;\n }\n if (isLayerOp(op)) {\n // LWW-guarded local apply keeps memory-fallback instances converged; a\n // shared backend already holds the record (the guarded re-apply is a\n // no-op). Relay is unconditional — receiving clients LWW-drop stale ops.\n void this.applyFanoutLayerOp(env.room, op).catch(() => {\n /* a broken backend must not break the fanout relay */\n });\n this.relayToRoom(env.room, undefined, JSON.stringify({ from: env.from, op }));\n return;\n }\n if (!isFanoutOp(op)) return;\n const prevAudience = typeof env.prev === 'string' ? env.prev : undefined;\n const prevExisted = env.existed === true;\n this.deliverToRoom(env.room, undefined, env.from, op, prevAudience, prevExisted);\n }\n\n private async applyFanoutLayerOp(room: string, op: LayerOp): Promise<void> {\n const record = layerOpToRecord(op);\n const current = await this.getLayerRecord(room, record.id);\n if (current && !isNewerLayerRecord(record, current)) return;\n await this.applyLayerRecord(room, record);\n }\n\n close(): void {\n for (const pending of this.pendingPresence.values()) clearTimeout(pending.timer);\n this.pendingPresence.clear();\n this.fanoutUnsub();\n }\n}\n","import { applyOpToMap, type SyncOp, type LayerRecord } from '@fieldnotes/sync';\nimport type { CanvasElement } from '@fieldnotes/core';\nimport type { HubBackend } from './hub-backend';\n\nexport class MemoryHubBackend implements HubBackend {\n private rooms = new Map<string, Map<string, CanvasElement>>();\n private roomLayers = new Map<string, Map<string, LayerRecord>>();\n\n private room(id: string): Map<string, CanvasElement> {\n let r = this.rooms.get(id);\n if (!r) {\n r = new Map();\n this.rooms.set(id, r);\n }\n return r;\n }\n\n private layers(room: string): Map<string, LayerRecord> {\n let r = this.roomLayers.get(room);\n if (!r) {\n r = new Map();\n this.roomLayers.set(room, r);\n }\n return r;\n }\n\n async snapshot(room: string): Promise<CanvasElement[]> {\n return [...this.room(room).values()];\n }\n\n async get(room: string, id: string): Promise<CanvasElement | undefined> {\n return this.room(room).get(id);\n }\n\n async apply(room: string, op: SyncOp): Promise<void> {\n if (op.kind === 'clear') {\n // Clears elements only; layer records are a separate, longer-lived ledger.\n this.rooms.delete(room);\n return;\n }\n applyOpToMap(this.room(room), op);\n }\n\n async layerRecords(room: string): Promise<LayerRecord[]> {\n return [...this.layers(room).values()];\n }\n\n async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n return this.layers(room).get(id);\n }\n\n async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n this.layers(room).set(record.id, record);\n }\n}\n","export interface HubFanout {\n publish(payload: string): void | Promise<void>;\n subscribe(handler: (payload: string) => void): () => void;\n close?(): void;\n}\n\nexport class InMemoryHubFanout implements HubFanout {\n private readonly handlers = new Set<(payload: string) => void>();\n\n publish(payload: string): void {\n for (const h of this.handlers) {\n try {\n h(payload);\n } catch {\n /* one throwing subscriber must not break the publish loop */\n }\n }\n }\n\n subscribe(handler: (payload: string) => void): () => void {\n this.handlers.add(handler);\n return () => this.handlers.delete(handler);\n }\n}\n","export const DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024;\nexport const DEFAULT_MAX_JSON_DEPTH = 32;\nexport const DEFAULT_MAX_PENDING_AUTH_MESSAGES = 100;\nexport const DEFAULT_MAX_PENDING_AUTH_BYTES = 2 * 1024 * 1024;\nexport const DEFAULT_MESSAGES_PER_SECOND = 120;\nexport const DEFAULT_MESSAGE_BURST = 240;\nexport const DEFAULT_PRESENCE_THROTTLE_MS = 50;\n\nexport function hasJsonDepthAtMost(message: string, maxDepth: number): boolean {\n let depth = 0;\n let inString = false;\n let escaped = false;\n\n for (const char of message) {\n if (inString) {\n if (escaped) escaped = false;\n else if (char === '\\\\') escaped = true;\n else if (char === '\"') inString = false;\n continue;\n }\n if (char === '\"') inString = true;\n else if (char === '{' || char === '[') {\n depth += 1;\n if (depth > maxDepth) return false;\n } else if (char === '}' || char === ']') {\n depth -= 1;\n if (depth < 0) return false;\n }\n }\n\n return depth === 0 && !inString;\n}\n\nexport class MessageRateLimiter {\n private tokens: number;\n private updatedAt: number;\n\n constructor(\n private readonly ratePerSecond: number,\n private readonly burst: number,\n now = Date.now(),\n ) {\n this.tokens = burst;\n this.updatedAt = now;\n }\n\n take(now = Date.now()): boolean {\n const elapsedSeconds = Math.max(0, now - this.updatedAt) / 1000;\n this.tokens = Math.min(this.burst, this.tokens + elapsedSeconds * this.ratePerSecond);\n this.updatedAt = now;\n if (this.tokens < 1) return false;\n this.tokens -= 1;\n return true;\n }\n}\n","import { WebSocketServer, type RawData } from 'ws';\nimport type { Server } from 'http';\nimport { SyncHub } from './sync-hub';\nimport type { HubBackend } from './hub-backend';\nimport type { HubFanout } from './hub-fanout';\nimport type { Authenticate } from './authenticate';\nimport type { Authorize, AuthorizeLayer, CanRead } from './authorize';\nimport { startHeartbeat } from './heartbeat';\nimport {\n DEFAULT_MAX_JSON_DEPTH,\n DEFAULT_MAX_MESSAGE_BYTES,\n DEFAULT_MAX_PENDING_AUTH_BYTES,\n DEFAULT_MAX_PENDING_AUTH_MESSAGES,\n DEFAULT_MESSAGE_BURST,\n DEFAULT_MESSAGES_PER_SECOND,\n DEFAULT_PRESENCE_THROTTLE_MS,\n MessageRateLimiter,\n} from './resource-limits';\nimport { DEFAULT_SHUTDOWN_GRACE_MS, drainWebSocketServer } from './shutdown';\n\nexport interface CreateSyncServerOptions {\n port?: number;\n server?: Server;\n backend?: HubBackend;\n fanout?: HubFanout;\n instanceId?: string;\n authenticate?: Authenticate;\n authorize?: Authorize;\n authorizeLayer?: AuthorizeLayer;\n canRead?: CanRead;\n heartbeatIntervalMs?: number;\n maxMessageBytes?: number;\n maxJsonDepth?: number;\n maxPendingAuthMessages?: number;\n maxPendingAuthBytes?: number;\n messagesPerSecond?: number;\n messageBurst?: number;\n presenceThrottleMs?: number;\n shutdownGraceMs?: number;\n}\n\nfunction rawDataByteLength(data: RawData): number {\n if (Array.isArray(data)) return data.reduce((total, chunk) => total + chunk.byteLength, 0);\n return data.byteLength;\n}\n\nexport function createSyncServer(options: CreateSyncServerOptions = {}): {\n hub: SyncHub;\n wss: WebSocketServer;\n close: () => Promise<void>;\n} {\n const shutdownGraceMs = options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;\n if (!Number.isFinite(shutdownGraceMs) || shutdownGraceMs < 0) {\n throw new RangeError('shutdownGraceMs must be a non-negative finite number');\n }\n const hub = new SyncHub({\n backend: options.backend,\n fanout: options.fanout,\n instanceId: options.instanceId,\n authorize: options.authorize,\n authorizeLayer: options.authorizeLayer,\n canRead: options.canRead,\n maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,\n presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,\n });\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const wss = options.server\n ? new WebSocketServer({ server: options.server, maxPayload: maxMessageBytes })\n : new WebSocketServer({ port: options.port ?? 0, maxPayload: maxMessageBytes });\n const heartbeat = startHeartbeat(wss, options.heartbeatIntervalMs ?? 30000);\n let shuttingDown = false;\n let closePromise: Promise<void> | undefined;\n let counter = 0;\n wss.on('connection', (ws, req) => {\n if (shuttingDown) {\n ws.close(1001, 'server shutting down');\n return;\n }\n heartbeat.track(ws);\n // Protocol/parser failures (including maxPayload) close the peer. Consuming the socket-level\n // error keeps hostile input from becoming an uncaught process error.\n ws.on('error', () => undefined);\n const url = new URL(req.url ?? '', 'http://localhost');\n const room = url.searchParams.get('room');\n if (!room) {\n ws.close(4400, 'room required');\n return;\n }\n const connId = `c${++counter}-${Math.random().toString(36).slice(2, 8)}`;\n\n let state: 'pending' | 'ready' | 'rejected' = 'pending';\n let closed = false;\n let admitted = false;\n const queue: string[] = [];\n let queuedBytes = 0;\n const maxPendingAuthMessages =\n options.maxPendingAuthMessages ?? DEFAULT_MAX_PENDING_AUTH_MESSAGES;\n const maxPendingAuthBytes = options.maxPendingAuthBytes ?? DEFAULT_MAX_PENDING_AUTH_BYTES;\n const limiter = new MessageRateLimiter(\n options.messagesPerSecond ?? DEFAULT_MESSAGES_PER_SECOND,\n options.messageBurst ?? DEFAULT_MESSAGE_BURST,\n );\n\n const send = (m: string) => {\n try {\n ws.send(m);\n } catch {\n /* socket closed mid-send */\n }\n };\n\n ws.on('message', (data) => {\n if (state === 'rejected') return;\n const messageBytes = rawDataByteLength(data);\n if (messageBytes > maxMessageBytes) {\n state = 'rejected';\n ws.close(1009, 'message too large');\n return;\n }\n if (!limiter.take()) {\n state = 'rejected';\n ws.close(4408, 'rate limit exceeded');\n return;\n }\n const msg = String(data);\n if (state === 'pending') {\n if (\n queue.length >= maxPendingAuthMessages ||\n queuedBytes + messageBytes > maxPendingAuthBytes\n ) {\n state = 'rejected';\n ws.close(4408, 'authentication queue limit exceeded');\n return;\n }\n queue.push(msg);\n queuedBytes += messageBytes;\n return;\n }\n void hub.handleMessage(connId, msg).catch((err) => console.error('[sync-server]', err));\n });\n ws.on('close', () => {\n closed = true;\n if (admitted) hub.removeConnection(connId);\n });\n\n Promise.resolve(options.authenticate ? options.authenticate({ req, room }) : { userId: connId })\n .then((result) => {\n if (closed || state === 'rejected' || shuttingDown) return;\n if (!result) {\n state = 'rejected';\n ws.close(4401, 'unauthorized');\n return;\n }\n state = 'ready';\n admitted = true;\n hub.addConnection({ id: connId, room, userId: result.userId, role: result.role, send });\n for (const m of queue) {\n void hub.handleMessage(connId, m).catch((err) => console.error('[sync-server]', err));\n }\n queue.length = 0;\n queuedBytes = 0;\n })\n .catch(() => {\n if (!closed && !shuttingDown) {\n state = 'rejected';\n ws.close(4401, 'unauthorized');\n }\n });\n });\n const close = (): Promise<void> => {\n if (closePromise) return closePromise;\n shuttingDown = true;\n closePromise = drainWebSocketServer(wss, shutdownGraceMs).then(() => {\n heartbeat.stop();\n hub.close();\n });\n return closePromise;\n };\n\n return {\n hub,\n wss,\n close,\n };\n}\n","export interface HeartbeatSocket {\n ping(): void;\n terminate(): void;\n on(event: 'pong', listener: () => void): void;\n}\nexport interface HeartbeatServer {\n clients: Set<HeartbeatSocket>;\n}\nexport interface Heartbeat {\n track(ws: HeartbeatSocket): void;\n stop(): void;\n}\n\nexport function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat {\n if (intervalMs <= 0) {\n return {\n track: () => {\n /* disabled */\n },\n stop: () => {\n /* disabled */\n },\n };\n }\n const alive = new WeakMap<HeartbeatSocket, boolean>();\n const track = (ws: HeartbeatSocket): void => {\n alive.set(ws, true);\n ws.on('pong', () => alive.set(ws, true));\n };\n const interval = setInterval(() => {\n for (const ws of wss.clients) {\n try {\n if (alive.get(ws) === false) {\n ws.terminate();\n continue;\n }\n alive.set(ws, false);\n ws.ping();\n } catch {\n /* socket dying mid-tick — skip it */\n }\n }\n }, intervalMs);\n return { track, stop: () => clearInterval(interval) };\n}\n","export const DEFAULT_SHUTDOWN_GRACE_MS = 5000;\n\nexport interface ShutdownSocket {\n close(code?: number, reason?: string): void;\n terminate(): void;\n}\n\nexport interface ShutdownServer {\n clients: Set<ShutdownSocket>;\n close(callback: (error?: Error) => void): void;\n}\n\n/** Stop admission, ask connected peers to leave, then forcefully reap stragglers. */\nexport function drainWebSocketServer(wss: ShutdownServer, graceMs: number): Promise<void> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = (): void => {\n if (settled) return;\n settled = true;\n clearTimeout(forceTimer);\n resolve();\n };\n const forceTimer = setTimeout(() => {\n for (const ws of wss.clients) {\n try {\n ws.terminate();\n } catch {\n /* socket already failed while shutdown was in progress */\n }\n }\n finish();\n }, graceMs);\n\n // Calling close first prevents new connections. Its callback runs once all clients have left.\n wss.close(() => finish());\n for (const ws of wss.clients) {\n try {\n ws.close(1001, 'server shutting down');\n } catch {\n try {\n ws.terminate();\n } catch {\n /* socket already closed */\n }\n }\n }\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/sync-hub.ts","../src/memory-hub-backend.ts","../src/hub-fanout.ts","../src/resource-limits.ts","../src/create-sync-server.ts","../src/heartbeat.ts","../src/shutdown.ts"],"sourcesContent":["import {\n parseEnvelope,\n isValidElement,\n isNewerLayerRecord,\n type LayerRecord,\n type SyncOp,\n type SyncEnvelope,\n} from '@fieldnotes/sync';\nimport { MemoryHubBackend } from './memory-hub-backend';\nimport { InMemoryHubFanout, type HubFanout } from './hub-fanout';\nimport type { HubBackend } from './hub-backend';\nimport type { Authorize, AuthorizeLayer, CanRead, OwnedElement } from './authorize';\nimport {\n DEFAULT_MAX_JSON_DEPTH,\n DEFAULT_MAX_PRESENCE_LANES,\n DEFAULT_PRESENCE_THROTTLE_MS,\n hasJsonDepthAtMost,\n} from './resource-limits';\n\nexport interface Connection {\n id: string;\n room: string;\n userId?: string;\n role?: string;\n send(message: string): void;\n}\n\nexport interface SyncHubOptions {\n backend?: HubBackend;\n fanout?: HubFanout;\n instanceId?: string;\n authorize?: Authorize;\n authorizeLayer?: AuthorizeLayer;\n canRead?: CanRead;\n maxJsonDepth?: number;\n presenceThrottleMs?: number;\n maxPresenceLanes?: number;\n}\n\nconst HUB_FROM = 'hub';\n\nfunction generateInstanceId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')\n return crypto.randomUUID();\n return `i-${Math.random().toString(36).slice(2)}`;\n}\n\nfunction isFanoutOp(op: unknown): op is Extract<SyncOp, { kind: 'upsert' | 'remove' | 'clear' }> {\n if (typeof op !== 'object' || op === null) return false;\n const o = op as { kind?: unknown; element?: unknown; id?: unknown };\n if (o.kind === 'upsert') return isValidElement(o.element);\n if (o.kind === 'remove') return typeof o.id === 'string';\n return o.kind === 'clear';\n}\n\ntype LayerOp = Extract<SyncOp, { kind: 'layer-upsert' | 'layer-remove' }>;\n\ninterface PresenceLane {\n lastSentAt: number | undefined;\n pending: { data: unknown; timer: ReturnType<typeof setTimeout> } | null;\n}\n\nconst FALLBACK_PRESENCE_LANE = '';\nconst MAX_PRESENCE_LANE_LENGTH = 64;\n\nfunction presenceLaneOf(data: unknown): string {\n if (typeof data !== 'object' || data === null) return FALLBACK_PRESENCE_LANE;\n const kind = (data as { kind?: unknown }).kind;\n if (typeof kind !== 'string' || kind.length === 0 || kind.length > MAX_PRESENCE_LANE_LENGTH) {\n return FALLBACK_PRESENCE_LANE;\n }\n return kind;\n}\n\nfunction isLayerOp(op: unknown): op is LayerOp {\n if (typeof op !== 'object' || op === null) return false;\n const k = (op as { kind?: unknown }).kind;\n // Shape is re-validated by parseEnvelope on the serial path; fanout payloads\n // come from a sibling hub that already validated them.\n return k === 'layer-upsert' || k === 'layer-remove';\n}\n\nfunction layerOpToRecord(op: LayerOp): LayerRecord {\n return op.kind === 'layer-upsert'\n ? { id: op.layer.id, version: op.version, editor: op.editor, definition: op.layer }\n : { id: op.id, version: op.version, editor: op.editor };\n}\n\nfunction layerRecordToOp(record: LayerRecord): LayerOp {\n return record.definition\n ? {\n kind: 'layer-upsert',\n layer: record.definition,\n version: record.version,\n editor: record.editor,\n }\n : { kind: 'layer-remove', id: record.id, version: record.version, editor: record.editor };\n}\n\nfunction isPresenceOp(\n op: unknown,\n): op is { kind: 'presence'; data: unknown } | { kind: 'presence-leave' } {\n if (typeof op !== 'object' || op === null) return false;\n const k = (op as { kind?: unknown }).kind;\n return k === 'presence' || k === 'presence-leave';\n}\n\nexport class SyncHub {\n private readonly backend: HubBackend;\n private readonly conns = new Map<string, Connection>();\n private readonly rooms = new Map<string, Set<string>>(); // room → connIds\n private readonly roomQueues = new Map<string, Promise<void>>(); // room → serial tail\n private readonly presenceConnections = new Set<string>();\n private readonly instanceId: string;\n private readonly fanout: HubFanout;\n private readonly fanoutUnsub: () => void;\n private readonly authorize?: Authorize;\n private readonly authorizeLayer?: AuthorizeLayer;\n private readonly canRead?: CanRead;\n /** Fallback layer-record store for backends without layer persistence. */\n private readonly memoryLayers = new Map<string, Map<string, LayerRecord>>();\n private readonly maxJsonDepth: number;\n private readonly presenceThrottleMs: number;\n private readonly maxPresenceLanes: number;\n /**\n * Presence throttle state keyed by connection, then by lane. A lane is the\n * payload's `kind` (a non-empty string of at most 64 chars) or the reserved\n * fallback lane `''`, so a rapid stream of one kind (awareness cursors) can\n * never replace a pending frame of another kind (a ping, a path `cleared`).\n * Within a lane the newest payload wins. The lane count per connection is\n * capped by `maxPresenceLanes`, counting the fallback lane, so a client\n * cannot mint timers by varying `kind`.\n */\n private readonly presenceLanes = new Map<string, Map<string, PresenceLane>>();\n\n constructor(options: SyncHubOptions = {}) {\n this.backend = options.backend ?? new MemoryHubBackend();\n this.instanceId = options.instanceId ?? generateInstanceId();\n this.fanout = options.fanout ?? new InMemoryHubFanout();\n this.authorize = options.authorize;\n this.authorizeLayer = options.authorizeLayer;\n this.canRead = options.canRead;\n this.maxJsonDepth = options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH;\n this.presenceThrottleMs = options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS;\n const maxPresenceLanes = options.maxPresenceLanes ?? DEFAULT_MAX_PRESENCE_LANES;\n if (!Number.isFinite(maxPresenceLanes) || maxPresenceLanes < 1) {\n throw new RangeError('maxPresenceLanes must be a finite number of at least 1');\n }\n this.maxPresenceLanes = Math.floor(maxPresenceLanes);\n this.fanoutUnsub = this.fanout.subscribe((payload) => this.onFanout(payload));\n }\n\n addConnection(conn: Connection): void {\n this.conns.set(conn.id, conn);\n let set = this.rooms.get(conn.room);\n if (!set) {\n set = new Set();\n this.rooms.set(conn.room, set);\n }\n set.add(conn.id);\n }\n\n removeConnection(connId: string): void {\n const conn = this.conns.get(connId);\n if (!conn) return;\n this.conns.delete(connId);\n const room = conn.room;\n const hadPresence = this.presenceConnections.delete(connId);\n this.clearPresenceLanes(connId);\n const members = this.rooms.get(room);\n if (members) {\n members.delete(connId);\n if (members.size === 0) {\n this.rooms.delete(room);\n this.roomQueues.delete(room);\n }\n }\n if (hadPresence) this.broadcastLeave(room, conn.id);\n }\n\n roomCount(): number {\n return this.rooms.size;\n }\n\n /**\n * Broadcasts ephemeral server-owned presence data to every connection in a room.\n * The returned count covers successful delivery on this hub instance only; configured fan-out\n * forwards the same event to other instances on a best-effort basis.\n */\n broadcastPresence<T>(room: string, data: T): number {\n const op = { kind: 'presence' as const, data };\n const sent = this.relayToRoom(room, undefined, JSON.stringify({ from: HUB_FROM, op }));\n this.safePublish(JSON.stringify({ o: this.instanceId, room, from: HUB_FROM, op }));\n return sent;\n }\n\n handleMessage(connId: string, message: string): Promise<void> {\n const conn = this.conns.get(connId);\n if (!conn) return Promise.resolve();\n if (!hasJsonDepthAtMost(message, this.maxJsonDepth)) return Promise.resolve();\n const env = parseEnvelope(message);\n if (!env) return Promise.resolve();\n if (env.op.kind === 'presence') {\n this.schedulePresence(conn, env.op.data); // off-queue, throttled independently\n return Promise.resolve();\n }\n const room = conn.room;\n // The per-room serial queue is the single total-order authority: ops apply in arrival order\n // (arrival-order LWW — no per-element seq; see D3 / TD-12). Different rooms run independently.\n const prev = this.roomQueues.get(room) ?? Promise.resolve();\n const operation = prev.then(() => this.process(conn, env));\n this.roomQueues.set(\n room,\n operation.catch(() => {\n // Recover only the internal tail so one failed message never wedges the room queue.\n // The caller still receives the operation rejection for observability.\n }),\n );\n return operation;\n }\n\n private async process(conn: Connection, env: SyncEnvelope): Promise<void> {\n const op = env.op;\n if (op.kind === 'request-snapshot') {\n const all = (await this.backend.snapshot(conn.room)) as OwnedElement[];\n const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;\n // Layer records are presentation-only and carry no element bytes, so no\n // audience filter applies; the field is omitted while a room has never\n // used layer sync, keeping snapshot frames byte-identical to before.\n const layers = await this.getLayerRecords(conn.room);\n conn.send(\n // `to` is a private correlation address for the requesting SyncClient. It is never\n // broadcast; every public sender identity below comes from the server-owned connection.\n JSON.stringify({\n from: HUB_FROM,\n op:\n layers.length > 0\n ? { kind: 'snapshot', to: env.from, elements, layers }\n : { kind: 'snapshot', to: env.from, elements },\n }),\n );\n } else if (op.kind === 'layer-upsert' || op.kind === 'layer-remove') {\n await this.processLayerOp(conn, op);\n } else if (op.kind === 'upsert' || op.kind === 'remove' || op.kind === 'clear') {\n const id = op.kind === 'upsert' ? op.element.id : op.kind === 'remove' ? op.id : undefined;\n const needCurrent = (this.authorize || this.canRead) && id !== undefined;\n const current: OwnedElement | undefined = needCurrent\n ? await this.backend.get(conn.room, id)\n : undefined;\n\n let outboundOp: SyncOp = op;\n if (this.authorize) {\n const allowed = await this.authorize({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n op,\n currentElement: current,\n });\n if (!allowed) {\n await this.sendCorrection(conn, env.from, op, current);\n return;\n }\n if (op.kind === 'upsert') {\n const ownerId = current?.ownerId ?? conn.userId;\n const stampedElement: OwnedElement = { ...op.element, ownerId };\n outboundOp = { kind: 'upsert', element: stampedElement };\n }\n }\n\n await this.backend.apply(conn.room, outboundOp);\n\n const prevExisted = current !== undefined;\n const prevAudience = current?.audience;\n\n await this.fanout.publish(\n JSON.stringify({\n o: this.instanceId,\n room: conn.room,\n from: conn.id,\n op: outboundOp,\n prev: prevAudience,\n existed: prevExisted,\n }),\n );\n\n this.deliverToRoom(conn.room, conn.id, conn.id, outboundOp, prevAudience, prevExisted);\n }\n // 'snapshot' from a client → ignored\n }\n\n /**\n * Applies a layer-definition edit on the room's serial queue. Convergence is\n * last-writer-wins under the deterministic (version, editor) ordering — the\n * same rule every client applies — so arrival order never decides a race. A\n * stale or denied edit is answered with an authoritative correction to the\n * sender only.\n */\n private async processLayerOp(conn: Connection, op: LayerOp): Promise<void> {\n const record = layerOpToRecord(op);\n const current = await this.getLayerRecord(conn.room, record.id);\n if (this.authorizeLayer) {\n const allowed = await this.authorizeLayer({\n userId: conn.userId,\n role: conn.role,\n room: conn.room,\n op,\n currentRecord: current,\n });\n if (!allowed) {\n // Revert the sender to the room's record; a tombstone when there is\n // none, so the denied local edit disappears everywhere consistently.\n const correction = current ?? { id: record.id, version: record.version, editor: HUB_FROM };\n conn.send(JSON.stringify({ from: HUB_FROM, op: layerRecordToOp(correction) }));\n return;\n }\n }\n if (current && !isNewerLayerRecord(record, current)) {\n // Stale under (version, editor): converge the sender, do not broadcast.\n conn.send(JSON.stringify({ from: HUB_FROM, op: layerRecordToOp(current) }));\n return;\n }\n await this.applyLayerRecord(conn.room, record);\n await this.fanout.publish(\n JSON.stringify({ o: this.instanceId, room: conn.room, from: conn.id, op }),\n );\n this.relayToRoom(conn.room, conn.id, JSON.stringify({ from: conn.id, op }));\n }\n\n private layerBackend(): Required<\n Pick<HubBackend, 'layerRecords' | 'getLayerRecord' | 'applyLayerRecord'>\n > | null {\n const { layerRecords, getLayerRecord, applyLayerRecord } = this.backend;\n if (!layerRecords || !getLayerRecord || !applyLayerRecord) return null;\n return {\n layerRecords: layerRecords.bind(this.backend),\n getLayerRecord: getLayerRecord.bind(this.backend),\n applyLayerRecord: applyLayerRecord.bind(this.backend),\n };\n }\n\n private async getLayerRecords(room: string): Promise<LayerRecord[]> {\n const backend = this.layerBackend();\n if (backend) return backend.layerRecords(room);\n return [...(this.memoryLayers.get(room)?.values() ?? [])];\n }\n\n private async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\n const backend = this.layerBackend();\n if (backend) return backend.getLayerRecord(room, id);\n return this.memoryLayers.get(room)?.get(id);\n }\n\n private async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\n const backend = this.layerBackend();\n if (backend) {\n await backend.applyLayerRecord(room, record);\n return;\n }\n let map = this.memoryLayers.get(room);\n if (!map) {\n map = new Map();\n this.memoryLayers.set(room, map);\n }\n map.set(record.id, record);\n }\n\n private mayRead(conn: Connection, audience: string | undefined): boolean {\n if (!this.canRead) return true;\n return this.canRead({ userId: conn.userId, role: conn.role, room: conn.room, audience });\n }\n\n private safePublish(payload: string): void {\n try {\n void Promise.resolve(this.fanout.publish(payload)).catch(() => {\n /* presence is ephemeral; a broken publisher must not create an unhandled rejection */\n });\n } catch {\n /* a broken fanout publisher must not break the un-queued presence relay */\n }\n }\n\n private relayToRoom(room: string, excludeId: string | undefined, message: string): number {\n const members = this.rooms.get(room);\n if (!members) return 0;\n let sent = 0;\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n try {\n conn.send(message);\n sent += 1;\n } catch {\n /* a throwing socket must not break the relay loop */\n }\n }\n return sent;\n }\n\n private broadcastClientPresence(conn: Connection, data: unknown): void {\n this.presenceConnections.add(conn.id);\n const message = JSON.stringify({ from: conn.id, op: { kind: 'presence', data } });\n this.relayToRoom(conn.room, conn.id, message);\n this.safePublish(\n JSON.stringify({\n o: this.instanceId,\n room: conn.room,\n from: conn.id,\n op: { kind: 'presence', data },\n }),\n );\n }\n\n private clearPresenceLanes(connId: string): void {\n const lanes = this.presenceLanes.get(connId);\n if (!lanes) return;\n for (const lane of lanes.values()) {\n if (lane.pending) clearTimeout(lane.pending.timer);\n }\n this.presenceLanes.delete(connId);\n }\n\n private schedulePresence(conn: Connection, data: unknown): void {\n if (this.presenceThrottleMs <= 0) {\n this.broadcastClientPresence(conn, data);\n return;\n }\n const lane = this.presenceLaneFor(conn.id, presenceLaneOf(data));\n const now = Date.now();\n if (lane.lastSentAt === undefined || now - lane.lastSentAt >= this.presenceThrottleMs) {\n if (lane.pending) {\n clearTimeout(lane.pending.timer);\n lane.pending = null;\n }\n lane.lastSentAt = now;\n this.broadcastClientPresence(conn, data);\n return;\n }\n if (lane.pending) {\n lane.pending.data = data;\n return;\n }\n const timer = setTimeout(\n () => {\n const pending = lane.pending;\n lane.pending = null;\n if (!pending || !this.conns.has(conn.id)) return;\n lane.lastSentAt = Date.now();\n this.broadcastClientPresence(conn, pending.data);\n },\n this.presenceThrottleMs - (now - lane.lastSentAt),\n );\n lane.pending = { data, timer };\n }\n\n private presenceLaneFor(connId: string, requested: string): PresenceLane {\n let lanes = this.presenceLanes.get(connId);\n if (!lanes) {\n lanes = new Map();\n this.presenceLanes.set(connId, lanes);\n }\n let key = requested;\n if (key !== FALLBACK_PRESENCE_LANE && !lanes.has(key)) {\n const named = lanes.size - (lanes.has(FALLBACK_PRESENCE_LANE) ? 1 : 0);\n if (named >= this.maxPresenceLanes - 1) key = FALLBACK_PRESENCE_LANE;\n }\n let lane = lanes.get(key);\n if (!lane) {\n lane = { lastSentAt: undefined, pending: null };\n lanes.set(key, lane);\n }\n return lane;\n }\n\n private broadcastLeave(room: string, from: string): void {\n const message = JSON.stringify({ from, op: { kind: 'presence-leave' } });\n this.relayToRoom(room, undefined, message);\n this.safePublish(\n JSON.stringify({ o: this.instanceId, room, from, op: { kind: 'presence-leave' } }),\n );\n }\n\n private deliverToRoom(\n room: string,\n excludeId: string | undefined,\n from: string,\n op: SyncOp,\n prevAudience: string | undefined,\n prevExisted: boolean,\n ): void {\n const members = this.rooms.get(room);\n if (!members) return;\n const send = (conn: Connection, msg: string): void => {\n try {\n conn.send(msg);\n } catch {\n /* a throwing socket must not break the delivery loop */\n }\n };\n if (op.kind === 'upsert') {\n const audience = (op.element as OwnedElement).audience;\n const upsertMsg = JSON.stringify({ from, op });\n const removeMsg = JSON.stringify({\n from: HUB_FROM,\n op: { kind: 'remove', id: op.element.id },\n });\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n if (this.mayRead(conn, audience)) send(conn, upsertMsg);\n else if (prevExisted && this.mayRead(conn, prevAudience)) send(conn, removeMsg);\n }\n } else if (op.kind === 'remove') {\n const removeMsg = JSON.stringify({ from, op });\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (!conn) continue;\n // No read filter → forward to all (today's behavior; current/prevExisted aren't fetched without\n // a hook). With canRead, only recipients who could see the removed element get it.\n const wasVisible = !this.canRead || (prevExisted && this.mayRead(conn, prevAudience));\n if (wasVisible) send(conn, removeMsg);\n }\n } else if (op.kind === 'clear') {\n const clearMsg = JSON.stringify({ from, op });\n for (const cid of members) {\n if (cid === excludeId) continue;\n const conn = this.conns.get(cid);\n if (conn) send(conn, clearMsg);\n }\n }\n }\n\n private async sendCorrection(\n conn: Connection,\n from: string,\n op: SyncOp,\n current: OwnedElement | undefined,\n ): Promise<void> {\n let correction: SyncOp | undefined;\n if (op.kind === 'upsert') {\n correction = current\n ? this.mayRead(conn, current.audience)\n ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: current.id }\n : { kind: 'remove', id: op.element.id };\n } else if (op.kind === 'remove') {\n correction = current\n ? this.mayRead(conn, current.audience)\n ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: current.id }\n : undefined;\n } else if (op.kind === 'clear') {\n const all = (await this.backend.snapshot(conn.room)) as OwnedElement[];\n const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;\n correction = { kind: 'snapshot', to: from, elements };\n }\n if (correction) conn.send(JSON.stringify({ from: HUB_FROM, op: correction }));\n }\n\n private onFanout(payload: string): void {\n // Off the serial queue on purpose: forward-only (the origin already applied to the SHARED backend),\n // and delivery is already ordered. Re-filter per local member (canRead runs on EVERY instance).\n let env: {\n o?: unknown;\n room?: unknown;\n from?: unknown;\n op?: unknown;\n prev?: unknown;\n existed?: unknown;\n };\n try {\n env = JSON.parse(payload);\n } catch {\n return;\n }\n if (typeof env.o !== 'string' || typeof env.room !== 'string' || typeof env.from !== 'string')\n return;\n if (env.o === this.instanceId) return; // our own publish — already delivered locally\n const op = env.op;\n if (isPresenceOp(op)) {\n // presence/leave: raw forward to all local members (the sender lives on the origin instance),\n // no backend, no canRead filter.\n this.relayToRoom(env.room, undefined, JSON.stringify({ from: env.from, op }));\n return;\n }\n if (isLayerOp(op)) {\n // LWW-guarded local apply keeps memory-fallback instances converged; a\n // shared backend already holds the record (the guarded re-apply is a\n // no-op). Relay is unconditional — receiving clients LWW-drop stale ops.\n void this.applyFanoutLayerOp(env.room, op).catch(() => {\n /* a broken backend must not break the fanout relay */\n });\n this.relayToRoom(env.room, undefined, JSON.stringify({ from: env.from, op }));\n return;\n }\n if (!isFanoutOp(op)) return;\n const prevAudience = typeof env.prev === 'string' ? env.prev : undefined;\n const prevExisted = env.existed === true;\n this.deliverToRoom(env.room, undefined, env.from, op, prevAudience, prevExisted);\n }\n\n private async applyFanoutLayerOp(room: string, op: LayerOp): Promise<void> {\n const record = layerOpToRecord(op);\n const current = await this.getLayerRecord(room, record.id);\n if (current && !isNewerLayerRecord(record, current)) return;\n await this.applyLayerRecord(room, record);\n }\n\n close(): void {\n for (const connId of [...this.presenceLanes.keys()]) this.clearPresenceLanes(connId);\n this.fanoutUnsub();\n }\n}\n","import { applyOpToMap, type SyncOp, type LayerRecord } from '@fieldnotes/sync';\r\nimport type { CanvasElement } from '@fieldnotes/core';\r\nimport type { HubBackend } from './hub-backend';\r\n\r\nexport class MemoryHubBackend implements HubBackend {\r\n private rooms = new Map<string, Map<string, CanvasElement>>();\r\n private roomLayers = new Map<string, Map<string, LayerRecord>>();\r\n\r\n private room(id: string): Map<string, CanvasElement> {\r\n let r = this.rooms.get(id);\r\n if (!r) {\r\n r = new Map();\r\n this.rooms.set(id, r);\r\n }\r\n return r;\r\n }\r\n\r\n private layers(room: string): Map<string, LayerRecord> {\r\n let r = this.roomLayers.get(room);\r\n if (!r) {\r\n r = new Map();\r\n this.roomLayers.set(room, r);\r\n }\r\n return r;\r\n }\r\n\r\n async snapshot(room: string): Promise<CanvasElement[]> {\r\n return [...this.room(room).values()];\r\n }\r\n\r\n async get(room: string, id: string): Promise<CanvasElement | undefined> {\r\n return this.room(room).get(id);\r\n }\r\n\r\n async apply(room: string, op: SyncOp): Promise<void> {\r\n if (op.kind === 'clear') {\r\n // Clears elements only; layer records are a separate, longer-lived ledger.\r\n this.rooms.delete(room);\r\n return;\r\n }\r\n applyOpToMap(this.room(room), op);\r\n }\r\n\r\n async layerRecords(room: string): Promise<LayerRecord[]> {\r\n return [...this.layers(room).values()];\r\n }\r\n\r\n async getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined> {\r\n return this.layers(room).get(id);\r\n }\r\n\r\n async applyLayerRecord(room: string, record: LayerRecord): Promise<void> {\r\n this.layers(room).set(record.id, record);\r\n }\r\n}\r\n","export interface HubFanout {\n publish(payload: string): void | Promise<void>;\n subscribe(handler: (payload: string) => void): () => void;\n close?(): void;\n}\n\nexport class InMemoryHubFanout implements HubFanout {\n private readonly handlers = new Set<(payload: string) => void>();\n\n publish(payload: string): void {\n for (const h of this.handlers) {\n try {\n h(payload);\n } catch {\n /* one throwing subscriber must not break the publish loop */\n }\n }\n }\n\n subscribe(handler: (payload: string) => void): () => void {\n this.handlers.add(handler);\n return () => this.handlers.delete(handler);\n }\n}\n","export const DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024;\nexport const DEFAULT_MAX_JSON_DEPTH = 32;\nexport const DEFAULT_MAX_PENDING_AUTH_MESSAGES = 100;\nexport const DEFAULT_MAX_PENDING_AUTH_BYTES = 2 * 1024 * 1024;\nexport const DEFAULT_MESSAGES_PER_SECOND = 120;\nexport const DEFAULT_MESSAGE_BURST = 240;\nexport const DEFAULT_PRESENCE_THROTTLE_MS = 50;\n/** Per-connection presence throttle lanes, INCLUDING the reserved fallback lane. */\nexport const DEFAULT_MAX_PRESENCE_LANES = 16;\n\nexport function hasJsonDepthAtMost(message: string, maxDepth: number): boolean {\n let depth = 0;\n let inString = false;\n let escaped = false;\n\n for (const char of message) {\n if (inString) {\n if (escaped) escaped = false;\n else if (char === '\\\\') escaped = true;\n else if (char === '\"') inString = false;\n continue;\n }\n if (char === '\"') inString = true;\n else if (char === '{' || char === '[') {\n depth += 1;\n if (depth > maxDepth) return false;\n } else if (char === '}' || char === ']') {\n depth -= 1;\n if (depth < 0) return false;\n }\n }\n\n return depth === 0 && !inString;\n}\n\nexport class MessageRateLimiter {\n private tokens: number;\n private updatedAt: number;\n\n constructor(\n private readonly ratePerSecond: number,\n private readonly burst: number,\n now = Date.now(),\n ) {\n this.tokens = burst;\n this.updatedAt = now;\n }\n\n take(now = Date.now()): boolean {\n const elapsedSeconds = Math.max(0, now - this.updatedAt) / 1000;\n this.tokens = Math.min(this.burst, this.tokens + elapsedSeconds * this.ratePerSecond);\n this.updatedAt = now;\n if (this.tokens < 1) return false;\n this.tokens -= 1;\n return true;\n }\n}\n","import { WebSocketServer, type RawData } from 'ws';\nimport type { Server } from 'http';\nimport { SyncHub } from './sync-hub';\nimport type { HubBackend } from './hub-backend';\nimport type { HubFanout } from './hub-fanout';\nimport type { Authenticate } from './authenticate';\nimport type { Authorize, AuthorizeLayer, CanRead } from './authorize';\nimport { startHeartbeat } from './heartbeat';\nimport {\n DEFAULT_MAX_JSON_DEPTH,\n DEFAULT_MAX_MESSAGE_BYTES,\n DEFAULT_MAX_PENDING_AUTH_BYTES,\n DEFAULT_MAX_PENDING_AUTH_MESSAGES,\n DEFAULT_MESSAGE_BURST,\n DEFAULT_MESSAGES_PER_SECOND,\n DEFAULT_PRESENCE_THROTTLE_MS,\n MessageRateLimiter,\n} from './resource-limits';\nimport { DEFAULT_SHUTDOWN_GRACE_MS, drainWebSocketServer } from './shutdown';\n\nexport interface CreateSyncServerOptions {\n port?: number;\n server?: Server;\n backend?: HubBackend;\n fanout?: HubFanout;\n instanceId?: string;\n authenticate?: Authenticate;\n authorize?: Authorize;\n authorizeLayer?: AuthorizeLayer;\n canRead?: CanRead;\n heartbeatIntervalMs?: number;\n maxMessageBytes?: number;\n maxJsonDepth?: number;\n maxPendingAuthMessages?: number;\n maxPendingAuthBytes?: number;\n messagesPerSecond?: number;\n messageBurst?: number;\n presenceThrottleMs?: number;\n maxPresenceLanes?: number;\n shutdownGraceMs?: number;\n}\n\nfunction rawDataByteLength(data: RawData): number {\n if (Array.isArray(data)) return data.reduce((total, chunk) => total + chunk.byteLength, 0);\n return data.byteLength;\n}\n\nexport function createSyncServer(options: CreateSyncServerOptions = {}): {\n hub: SyncHub;\n wss: WebSocketServer;\n close: () => Promise<void>;\n} {\n const shutdownGraceMs = options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;\n if (!Number.isFinite(shutdownGraceMs) || shutdownGraceMs < 0) {\n throw new RangeError('shutdownGraceMs must be a non-negative finite number');\n }\n const hub = new SyncHub({\n backend: options.backend,\n fanout: options.fanout,\n instanceId: options.instanceId,\n authorize: options.authorize,\n authorizeLayer: options.authorizeLayer,\n canRead: options.canRead,\n maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,\n presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,\n maxPresenceLanes: options.maxPresenceLanes,\n });\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\n const wss = options.server\n ? new WebSocketServer({ server: options.server, maxPayload: maxMessageBytes })\n : new WebSocketServer({ port: options.port ?? 0, maxPayload: maxMessageBytes });\n const heartbeat = startHeartbeat(wss, options.heartbeatIntervalMs ?? 30000);\n let shuttingDown = false;\n let closePromise: Promise<void> | undefined;\n let counter = 0;\n wss.on('connection', (ws, req) => {\n if (shuttingDown) {\n ws.close(1001, 'server shutting down');\n return;\n }\n heartbeat.track(ws);\n // Protocol/parser failures (including maxPayload) close the peer. Consuming the socket-level\n // error keeps hostile input from becoming an uncaught process error.\n ws.on('error', () => undefined);\n const url = new URL(req.url ?? '', 'http://localhost');\n const room = url.searchParams.get('room');\n if (!room) {\n ws.close(4400, 'room required');\n return;\n }\n const connId = `c${++counter}-${Math.random().toString(36).slice(2, 8)}`;\n\n let state: 'pending' | 'ready' | 'rejected' = 'pending';\n let closed = false;\n let admitted = false;\n const queue: string[] = [];\n let queuedBytes = 0;\n const maxPendingAuthMessages =\n options.maxPendingAuthMessages ?? DEFAULT_MAX_PENDING_AUTH_MESSAGES;\n const maxPendingAuthBytes = options.maxPendingAuthBytes ?? DEFAULT_MAX_PENDING_AUTH_BYTES;\n const limiter = new MessageRateLimiter(\n options.messagesPerSecond ?? DEFAULT_MESSAGES_PER_SECOND,\n options.messageBurst ?? DEFAULT_MESSAGE_BURST,\n );\n\n const send = (m: string) => {\n try {\n ws.send(m);\n } catch {\n /* socket closed mid-send */\n }\n };\n\n ws.on('message', (data) => {\n if (state === 'rejected') return;\n const messageBytes = rawDataByteLength(data);\n if (messageBytes > maxMessageBytes) {\n state = 'rejected';\n ws.close(1009, 'message too large');\n return;\n }\n if (!limiter.take()) {\n state = 'rejected';\n ws.close(4408, 'rate limit exceeded');\n return;\n }\n const msg = String(data);\n if (state === 'pending') {\n if (\n queue.length >= maxPendingAuthMessages ||\n queuedBytes + messageBytes > maxPendingAuthBytes\n ) {\n state = 'rejected';\n ws.close(4408, 'authentication queue limit exceeded');\n return;\n }\n queue.push(msg);\n queuedBytes += messageBytes;\n return;\n }\n void hub.handleMessage(connId, msg).catch((err) => console.error('[sync-server]', err));\n });\n ws.on('close', () => {\n closed = true;\n if (admitted) hub.removeConnection(connId);\n });\n\n Promise.resolve(options.authenticate ? options.authenticate({ req, room }) : { userId: connId })\n .then((result) => {\n if (closed || state === 'rejected' || shuttingDown) return;\n if (!result) {\n state = 'rejected';\n ws.close(4401, 'unauthorized');\n return;\n }\n state = 'ready';\n admitted = true;\n hub.addConnection({ id: connId, room, userId: result.userId, role: result.role, send });\n for (const m of queue) {\n void hub.handleMessage(connId, m).catch((err) => console.error('[sync-server]', err));\n }\n queue.length = 0;\n queuedBytes = 0;\n })\n .catch(() => {\n if (!closed && !shuttingDown) {\n state = 'rejected';\n ws.close(4401, 'unauthorized');\n }\n });\n });\n const close = (): Promise<void> => {\n if (closePromise) return closePromise;\n shuttingDown = true;\n closePromise = drainWebSocketServer(wss, shutdownGraceMs).then(() => {\n heartbeat.stop();\n hub.close();\n });\n return closePromise;\n };\n\n return {\n hub,\n wss,\n close,\n };\n}\n","export interface HeartbeatSocket {\n ping(): void;\n terminate(): void;\n on(event: 'pong', listener: () => void): void;\n}\nexport interface HeartbeatServer {\n clients: Set<HeartbeatSocket>;\n}\nexport interface Heartbeat {\n track(ws: HeartbeatSocket): void;\n stop(): void;\n}\n\nexport function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat {\n if (intervalMs <= 0) {\n return {\n track: () => {\n /* disabled */\n },\n stop: () => {\n /* disabled */\n },\n };\n }\n const alive = new WeakMap<HeartbeatSocket, boolean>();\n const track = (ws: HeartbeatSocket): void => {\n alive.set(ws, true);\n ws.on('pong', () => alive.set(ws, true));\n };\n const interval = setInterval(() => {\n for (const ws of wss.clients) {\n try {\n if (alive.get(ws) === false) {\n ws.terminate();\n continue;\n }\n alive.set(ws, false);\n ws.ping();\n } catch {\n /* socket dying mid-tick — skip it */\n }\n }\n }, intervalMs);\n return { track, stop: () => clearInterval(interval) };\n}\n","export const DEFAULT_SHUTDOWN_GRACE_MS = 5000;\n\nexport interface ShutdownSocket {\n close(code?: number, reason?: string): void;\n terminate(): void;\n}\n\nexport interface ShutdownServer {\n clients: Set<ShutdownSocket>;\n close(callback: (error?: Error) => void): void;\n}\n\n/** Stop admission, ask connected peers to leave, then forcefully reap stragglers. */\nexport function drainWebSocketServer(wss: ShutdownServer, graceMs: number): Promise<void> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = (): void => {\n if (settled) return;\n settled = true;\n clearTimeout(forceTimer);\n resolve();\n };\n const forceTimer = setTimeout(() => {\n for (const ws of wss.clients) {\n try {\n ws.terminate();\n } catch {\n /* socket already failed while shutdown was in progress */\n }\n }\n finish();\n }, graceMs);\n\n // Calling close first prevents new connections. Its callback runs once all clients have left.\n wss.close(() => finish());\n for (const ws of wss.clients) {\n try {\n ws.close(1001, 'server shutting down');\n } catch {\n try {\n ws.terminate();\n } catch {\n /* socket already closed */\n }\n }\n }\n });\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldnotes/sync-server",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Authoritative WebSocket relay server for Field Notes real-time sync",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -38,14 +38,14 @@
38
38
  ],
39
39
  "dependencies": {
40
40
  "ws": "^8.18.0",
41
- "@fieldnotes/sync": "0.10.0"
41
+ "@fieldnotes/sync": "0.11.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/ws": "^8.5.12",
45
45
  "@vitest/coverage-v8": "^4.1.0",
46
46
  "tsup": "^8.5.1",
47
47
  "vitest": "^4.1.0",
48
- "@fieldnotes/core": "0.53.0"
48
+ "@fieldnotes/core": "0.65.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "tsup",