@fieldnotes/sync-server 0.10.1 → 0.11.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/README.md +29 -1
- package/dist/index.cjs +23 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +23 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -44,6 +44,32 @@ const { close } = createSyncServer({ port: 8080 });
|
|
|
44
44
|
// ws://localhost:8080?room=my-room
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
## Server-originated presence
|
|
48
|
+
|
|
49
|
+
Trusted server integrations can broadcast ephemeral data without accessing the hub's connection
|
|
50
|
+
registries:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const { hub } = createSyncServer({ port: 8080, fanout });
|
|
54
|
+
|
|
55
|
+
const localDeliveries = hub.broadcastPresence('my-room', {
|
|
56
|
+
kind: 'poke',
|
|
57
|
+
feature: 'initiative',
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Recipients receive the existing presence envelope with the server-owned identity
|
|
62
|
+
`{ from: 'hub', op: { kind: 'presence', data } }`. The event is delivered to every local room
|
|
63
|
+
member and published to the configured `HubFanout`, so other relay instances deliver it to their
|
|
64
|
+
local members too. The return value counts successful sends on the originating hub only; it is not a
|
|
65
|
+
cluster-wide acknowledgement.
|
|
66
|
+
|
|
67
|
+
Server presence is best-effort, is not persisted, does not enter a room's durable operation queue,
|
|
68
|
+
and is not filtered by `authorize` or `canRead`. Validate and authorize application requests before
|
|
69
|
+
calling this trusted-host API. Data must be JSON-serializable; invalid data throws before delivery.
|
|
70
|
+
The method constructs the presence operation itself and does not accept a raw sync operation or
|
|
71
|
+
caller-controlled sender identity.
|
|
72
|
+
|
|
47
73
|
## Heartbeat
|
|
48
74
|
|
|
49
75
|
The server pings every client on an interval and **terminates** any that miss a pong,
|
|
@@ -207,7 +233,9 @@ createSyncServer({
|
|
|
207
233
|
optimistic local edit self-corrects immediately — no client change, no waiting for reconnect:
|
|
208
234
|
a rejected new element is **removed**, a rejected edit/remove is re-**upserted** from the canonical
|
|
209
235
|
stored element, and a rejected `clear` gets a fresh **snapshot**. The correction reuses existing op
|
|
210
|
-
kinds and is sent only to the offending connection.
|
|
236
|
+
kinds and is sent only to the offending connection. When `canRead` is configured, corrections use
|
|
237
|
+
the same viewer filter: unreadable canonical upserts become byte-free removes and corrective
|
|
238
|
+
snapshots omit unreadable elements.
|
|
211
239
|
|
|
212
240
|
## Read filtering
|
|
213
241
|
|
package/dist/index.cjs
CHANGED
|
@@ -202,6 +202,17 @@ var SyncHub = class {
|
|
|
202
202
|
roomCount() {
|
|
203
203
|
return this.rooms.size;
|
|
204
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Broadcasts ephemeral server-owned presence data to every connection in a room.
|
|
207
|
+
* The returned count covers successful delivery on this hub instance only; configured fan-out
|
|
208
|
+
* forwards the same event to other instances on a best-effort basis.
|
|
209
|
+
*/
|
|
210
|
+
broadcastPresence(room, data) {
|
|
211
|
+
const op = { kind: "presence", data };
|
|
212
|
+
const sent = this.relayToRoom(room, void 0, JSON.stringify({ from: HUB_FROM, op }));
|
|
213
|
+
this.safePublish(JSON.stringify({ o: this.instanceId, room, from: HUB_FROM, op }));
|
|
214
|
+
return sent;
|
|
215
|
+
}
|
|
205
216
|
handleMessage(connId, message) {
|
|
206
217
|
const conn = this.conns.get(connId);
|
|
207
218
|
if (!conn) return Promise.resolve();
|
|
@@ -284,18 +295,21 @@ var SyncHub = class {
|
|
|
284
295
|
}
|
|
285
296
|
relayToRoom(room, excludeId, message) {
|
|
286
297
|
const members = this.rooms.get(room);
|
|
287
|
-
if (!members) return;
|
|
298
|
+
if (!members) return 0;
|
|
299
|
+
let sent = 0;
|
|
288
300
|
for (const cid of members) {
|
|
289
301
|
if (cid === excludeId) continue;
|
|
290
302
|
const conn = this.conns.get(cid);
|
|
291
303
|
if (!conn) continue;
|
|
292
304
|
try {
|
|
293
305
|
conn.send(message);
|
|
306
|
+
sent += 1;
|
|
294
307
|
} catch {
|
|
295
308
|
}
|
|
296
309
|
}
|
|
310
|
+
return sent;
|
|
297
311
|
}
|
|
298
|
-
|
|
312
|
+
broadcastClientPresence(conn, data) {
|
|
299
313
|
this.presenceConnections.add(conn.id);
|
|
300
314
|
const message = JSON.stringify({ from: conn.id, op: { kind: "presence", data } });
|
|
301
315
|
this.relayToRoom(conn.room, conn.id, message);
|
|
@@ -310,14 +324,14 @@ var SyncHub = class {
|
|
|
310
324
|
}
|
|
311
325
|
schedulePresence(conn, data) {
|
|
312
326
|
if (this.presenceThrottleMs <= 0) {
|
|
313
|
-
this.
|
|
327
|
+
this.broadcastClientPresence(conn, data);
|
|
314
328
|
return;
|
|
315
329
|
}
|
|
316
330
|
const now = Date.now();
|
|
317
331
|
const lastSentAt = this.lastPresenceAt.get(conn.id);
|
|
318
332
|
if (lastSentAt === void 0 || now - lastSentAt >= this.presenceThrottleMs) {
|
|
319
333
|
this.lastPresenceAt.set(conn.id, now);
|
|
320
|
-
this.
|
|
334
|
+
this.broadcastClientPresence(conn, data);
|
|
321
335
|
return;
|
|
322
336
|
}
|
|
323
337
|
const existing = this.pendingPresence.get(conn.id);
|
|
@@ -331,7 +345,7 @@ var SyncHub = class {
|
|
|
331
345
|
this.pendingPresence.delete(conn.id);
|
|
332
346
|
if (!pending || !this.conns.has(conn.id)) return;
|
|
333
347
|
this.lastPresenceAt.set(conn.id, Date.now());
|
|
334
|
-
this.
|
|
348
|
+
this.broadcastClientPresence(conn, pending.data);
|
|
335
349
|
},
|
|
336
350
|
this.presenceThrottleMs - (now - lastSentAt)
|
|
337
351
|
);
|
|
@@ -388,11 +402,12 @@ var SyncHub = class {
|
|
|
388
402
|
async sendCorrection(conn, from, op, current) {
|
|
389
403
|
let correction;
|
|
390
404
|
if (op.kind === "upsert") {
|
|
391
|
-
correction = current ? { kind: "upsert", element: current } : { kind: "remove", id: op.element.id };
|
|
405
|
+
correction = current ? this.mayRead(conn, current.audience) ? { kind: "upsert", element: current } : { kind: "remove", id: current.id } : { kind: "remove", id: op.element.id };
|
|
392
406
|
} else if (op.kind === "remove") {
|
|
393
|
-
correction = current ? { kind: "upsert", element: current } : void 0;
|
|
407
|
+
correction = current ? this.mayRead(conn, current.audience) ? { kind: "upsert", element: current } : { kind: "remove", id: current.id } : void 0;
|
|
394
408
|
} else if (op.kind === "clear") {
|
|
395
|
-
const
|
|
409
|
+
const all = await this.backend.snapshot(conn.room);
|
|
410
|
+
const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;
|
|
396
411
|
correction = { kind: "snapshot", to: from, elements };
|
|
397
412
|
}
|
|
398
413
|
if (correction) conn.send(JSON.stringify({ from: HUB_FROM, op: correction }));
|
package/dist/index.cjs.map
CHANGED
|
@@ -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';\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 { Authorize, AuthorizeContext, OwnedElement, ReadContext, CanRead } from './authorize';\r\nexport { startHeartbeat } from './heartbeat';\r\nexport type { Heartbeat, HeartbeatSocket, HeartbeatServer } from './heartbeat';\r\n","import { parseEnvelope, isValidElement, type SyncOp, type SyncEnvelope } 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, 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 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\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 canRead?: CanRead;\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.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 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 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({ from: HUB_FROM, op: { kind: 'snapshot', to: env.from, elements } }),\n );\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 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): void {\n const members = this.rooms.get(room);\n if (!members) return;\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 } catch {\n /* a throwing socket must not break the relay loop */\n }\n }\n }\n\n private broadcastPresence(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.broadcastPresence(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.broadcastPresence(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.broadcastPresence(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 ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: op.element.id };\n } else if (op.kind === 'remove') {\n correction = current ? { kind: 'upsert', element: current } : undefined;\n } else if (op.kind === 'clear') {\n const elements = await this.backend.snapshot(conn.room);\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 (!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 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 } 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\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 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 this.rooms.delete(room);\r\n return;\r\n }\r\n applyOpToMap(this.room(room), op);\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;\r\nexport const DEFAULT_MAX_JSON_DEPTH = 32;\r\nexport const DEFAULT_MAX_PENDING_AUTH_MESSAGES = 100;\r\nexport const DEFAULT_MAX_PENDING_AUTH_BYTES = 2 * 1024 * 1024;\r\nexport const DEFAULT_MESSAGES_PER_SECOND = 120;\r\nexport const DEFAULT_MESSAGE_BURST = 240;\r\nexport const DEFAULT_PRESENCE_THROTTLE_MS = 50;\r\n\r\nexport function hasJsonDepthAtMost(message: string, maxDepth: number): boolean {\r\n let depth = 0;\r\n let inString = false;\r\n let escaped = false;\r\n\r\n for (const char of message) {\r\n if (inString) {\r\n if (escaped) escaped = false;\r\n else if (char === '\\\\') escaped = true;\r\n else if (char === '\"') inString = false;\r\n continue;\r\n }\r\n if (char === '\"') inString = true;\r\n else if (char === '{' || char === '[') {\r\n depth += 1;\r\n if (depth > maxDepth) return false;\r\n } else if (char === '}' || char === ']') {\r\n depth -= 1;\r\n if (depth < 0) return false;\r\n }\r\n }\r\n\r\n return depth === 0 && !inString;\r\n}\r\n\r\nexport class MessageRateLimiter {\r\n private tokens: number;\r\n private updatedAt: number;\r\n\r\n constructor(\r\n private readonly ratePerSecond: number,\r\n private readonly burst: number,\r\n now = Date.now(),\r\n ) {\r\n this.tokens = burst;\r\n this.updatedAt = now;\r\n }\r\n\r\n take(now = Date.now()): boolean {\r\n const elapsedSeconds = Math.max(0, now - this.updatedAt) / 1000;\r\n this.tokens = Math.min(this.burst, this.tokens + elapsedSeconds * this.ratePerSecond);\r\n this.updatedAt = now;\r\n if (this.tokens < 1) return false;\r\n this.tokens -= 1;\r\n return true;\r\n }\r\n}\r\n","import { WebSocketServer, type RawData } from 'ws';\r\nimport type { Server } from 'http';\r\nimport { SyncHub } from './sync-hub';\r\nimport type { HubBackend } from './hub-backend';\r\nimport type { HubFanout } from './hub-fanout';\r\nimport type { Authenticate } from './authenticate';\r\nimport type { Authorize, CanRead } from './authorize';\r\nimport { startHeartbeat } from './heartbeat';\r\nimport {\r\n DEFAULT_MAX_JSON_DEPTH,\r\n DEFAULT_MAX_MESSAGE_BYTES,\r\n DEFAULT_MAX_PENDING_AUTH_BYTES,\r\n DEFAULT_MAX_PENDING_AUTH_MESSAGES,\r\n DEFAULT_MESSAGE_BURST,\r\n DEFAULT_MESSAGES_PER_SECOND,\r\n DEFAULT_PRESENCE_THROTTLE_MS,\r\n MessageRateLimiter,\r\n} from './resource-limits';\r\nimport { DEFAULT_SHUTDOWN_GRACE_MS, drainWebSocketServer } from './shutdown';\r\n\r\nexport interface CreateSyncServerOptions {\r\n port?: number;\r\n server?: Server;\r\n backend?: HubBackend;\r\n fanout?: HubFanout;\r\n instanceId?: string;\r\n authenticate?: Authenticate;\r\n authorize?: Authorize;\r\n canRead?: CanRead;\r\n heartbeatIntervalMs?: number;\r\n maxMessageBytes?: number;\r\n maxJsonDepth?: number;\r\n maxPendingAuthMessages?: number;\r\n maxPendingAuthBytes?: number;\r\n messagesPerSecond?: number;\r\n messageBurst?: number;\r\n presenceThrottleMs?: number;\r\n shutdownGraceMs?: number;\r\n}\r\n\r\nfunction rawDataByteLength(data: RawData): number {\r\n if (Array.isArray(data)) return data.reduce((total, chunk) => total + chunk.byteLength, 0);\r\n return data.byteLength;\r\n}\r\n\r\nexport function createSyncServer(options: CreateSyncServerOptions = {}): {\r\n hub: SyncHub;\r\n wss: WebSocketServer;\r\n close: () => Promise<void>;\r\n} {\r\n const shutdownGraceMs = options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;\r\n if (!Number.isFinite(shutdownGraceMs) || shutdownGraceMs < 0) {\r\n throw new RangeError('shutdownGraceMs must be a non-negative finite number');\r\n }\r\n const hub = new SyncHub({\r\n backend: options.backend,\r\n fanout: options.fanout,\r\n instanceId: options.instanceId,\r\n authorize: options.authorize,\r\n canRead: options.canRead,\r\n maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,\r\n presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,\r\n });\r\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\r\n const wss = options.server\r\n ? new WebSocketServer({ server: options.server, maxPayload: maxMessageBytes })\r\n : new WebSocketServer({ port: options.port ?? 0, maxPayload: maxMessageBytes });\r\n const heartbeat = startHeartbeat(wss, options.heartbeatIntervalMs ?? 30000);\r\n let shuttingDown = false;\r\n let closePromise: Promise<void> | undefined;\r\n let counter = 0;\r\n wss.on('connection', (ws, req) => {\r\n if (shuttingDown) {\r\n ws.close(1001, 'server shutting down');\r\n return;\r\n }\r\n heartbeat.track(ws);\r\n // Protocol/parser failures (including maxPayload) close the peer. Consuming the socket-level\r\n // error keeps hostile input from becoming an uncaught process error.\r\n ws.on('error', () => undefined);\r\n const url = new URL(req.url ?? '', 'http://localhost');\r\n const room = url.searchParams.get('room');\r\n if (!room) {\r\n ws.close(4400, 'room required');\r\n return;\r\n }\r\n const connId = `c${++counter}-${Math.random().toString(36).slice(2, 8)}`;\r\n\r\n let state: 'pending' | 'ready' | 'rejected' = 'pending';\r\n let closed = false;\r\n let admitted = false;\r\n const queue: string[] = [];\r\n let queuedBytes = 0;\r\n const maxPendingAuthMessages =\r\n options.maxPendingAuthMessages ?? DEFAULT_MAX_PENDING_AUTH_MESSAGES;\r\n const maxPendingAuthBytes = options.maxPendingAuthBytes ?? DEFAULT_MAX_PENDING_AUTH_BYTES;\r\n const limiter = new MessageRateLimiter(\r\n options.messagesPerSecond ?? DEFAULT_MESSAGES_PER_SECOND,\r\n options.messageBurst ?? DEFAULT_MESSAGE_BURST,\r\n );\r\n\r\n const send = (m: string) => {\r\n try {\r\n ws.send(m);\r\n } catch {\r\n /* socket closed mid-send */\r\n }\r\n };\r\n\r\n ws.on('message', (data) => {\r\n if (state === 'rejected') return;\r\n const messageBytes = rawDataByteLength(data);\r\n if (messageBytes > maxMessageBytes) {\r\n state = 'rejected';\r\n ws.close(1009, 'message too large');\r\n return;\r\n }\r\n if (!limiter.take()) {\r\n state = 'rejected';\r\n ws.close(4408, 'rate limit exceeded');\r\n return;\r\n }\r\n const msg = String(data);\r\n if (state === 'pending') {\r\n if (\r\n queue.length >= maxPendingAuthMessages ||\r\n queuedBytes + messageBytes > maxPendingAuthBytes\r\n ) {\r\n state = 'rejected';\r\n ws.close(4408, 'authentication queue limit exceeded');\r\n return;\r\n }\r\n queue.push(msg);\r\n queuedBytes += messageBytes;\r\n return;\r\n }\r\n void hub.handleMessage(connId, msg).catch((err) => console.error('[sync-server]', err));\r\n });\r\n ws.on('close', () => {\r\n closed = true;\r\n if (admitted) hub.removeConnection(connId);\r\n });\r\n\r\n Promise.resolve(options.authenticate ? options.authenticate({ req, room }) : { userId: connId })\r\n .then((result) => {\r\n if (closed || state === 'rejected' || shuttingDown) return;\r\n if (!result) {\r\n state = 'rejected';\r\n ws.close(4401, 'unauthorized');\r\n return;\r\n }\r\n state = 'ready';\r\n admitted = true;\r\n hub.addConnection({ id: connId, room, userId: result.userId, role: result.role, send });\r\n for (const m of queue) {\r\n void hub.handleMessage(connId, m).catch((err) => console.error('[sync-server]', err));\r\n }\r\n queue.length = 0;\r\n queuedBytes = 0;\r\n })\r\n .catch(() => {\r\n if (!closed && !shuttingDown) {\r\n state = 'rejected';\r\n ws.close(4401, 'unauthorized');\r\n }\r\n });\r\n });\r\n const close = (): Promise<void> => {\r\n if (closePromise) return closePromise;\r\n shuttingDown = true;\r\n closePromise = drainWebSocketServer(wss, shutdownGraceMs).then(() => {\r\n heartbeat.stop();\r\n hub.close();\r\n });\r\n return closePromise;\r\n };\r\n\r\n return {\r\n hub,\r\n wss,\r\n close,\r\n };\r\n}\r\n","export interface HeartbeatSocket {\r\n ping(): void;\r\n terminate(): void;\r\n on(event: 'pong', listener: () => void): void;\r\n}\r\nexport interface HeartbeatServer {\r\n clients: Set<HeartbeatSocket>;\r\n}\r\nexport interface Heartbeat {\r\n track(ws: HeartbeatSocket): void;\r\n stop(): void;\r\n}\r\n\r\nexport function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat {\r\n if (intervalMs <= 0) {\r\n return {\r\n track: () => {\r\n /* disabled */\r\n },\r\n stop: () => {\r\n /* disabled */\r\n },\r\n };\r\n }\r\n const alive = new WeakMap<HeartbeatSocket, boolean>();\r\n const track = (ws: HeartbeatSocket): void => {\r\n alive.set(ws, true);\r\n ws.on('pong', () => alive.set(ws, true));\r\n };\r\n const interval = setInterval(() => {\r\n for (const ws of wss.clients) {\r\n try {\r\n if (alive.get(ws) === false) {\r\n ws.terminate();\r\n continue;\r\n }\r\n alive.set(ws, false);\r\n ws.ping();\r\n } catch {\r\n /* socket dying mid-tick — skip it */\r\n }\r\n }\r\n }, intervalMs);\r\n return { track, stop: () => clearInterval(interval) };\r\n}\r\n","export const DEFAULT_SHUTDOWN_GRACE_MS = 5000;\r\n\r\nexport interface ShutdownSocket {\r\n close(code?: number, reason?: string): void;\r\n terminate(): void;\r\n}\r\n\r\nexport interface ShutdownServer {\r\n clients: Set<ShutdownSocket>;\r\n close(callback: (error?: Error) => void): void;\r\n}\r\n\r\n/** Stop admission, ask connected peers to leave, then forcefully reap stragglers. */\r\nexport function drainWebSocketServer(wss: ShutdownServer, graceMs: number): Promise<void> {\r\n return new Promise((resolve) => {\r\n let settled = false;\r\n const finish = (): void => {\r\n if (settled) return;\r\n settled = true;\r\n clearTimeout(forceTimer);\r\n resolve();\r\n };\r\n const forceTimer = setTimeout(() => {\r\n for (const ws of wss.clients) {\r\n try {\r\n ws.terminate();\r\n } catch {\r\n /* socket already failed while shutdown was in progress */\r\n }\r\n }\r\n finish();\r\n }, graceMs);\r\n\r\n // Calling close first prevents new connections. Its callback runs once all clients have left.\r\n wss.close(() => finish());\r\n for (const ws of wss.clients) {\r\n try {\r\n ws.close(1001, 'server shutting down');\r\n } catch {\r\n try {\r\n ws.terminate();\r\n } catch {\r\n /* socket already closed */\r\n }\r\n }\r\n }\r\n });\r\n}\r\n"],"mappings":"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","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 { Authorize, AuthorizeContext, OwnedElement, ReadContext, CanRead } from './authorize';\r\nexport { startHeartbeat } from './heartbeat';\r\nexport type { Heartbeat, HeartbeatSocket, HeartbeatServer } from './heartbeat';\r\n","import { parseEnvelope, isValidElement, type SyncOp, type SyncEnvelope } 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, 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 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\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 canRead?: CanRead;\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.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 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({ from: HUB_FROM, op: { kind: 'snapshot', to: env.from, elements } }),\n );\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 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 (!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 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 } 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\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 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 this.rooms.delete(room);\r\n return;\r\n }\r\n applyOpToMap(this.room(room), op);\r\n }\r\n}\r\n","export interface HubFanout {\r\n publish(payload: string): void | Promise<void>;\r\n subscribe(handler: (payload: string) => void): () => void;\r\n close?(): void;\r\n}\r\n\r\nexport class InMemoryHubFanout implements HubFanout {\r\n private readonly handlers = new Set<(payload: string) => void>();\r\n\r\n publish(payload: string): void {\r\n for (const h of this.handlers) {\r\n try {\r\n h(payload);\r\n } catch {\r\n /* one throwing subscriber must not break the publish loop */\r\n }\r\n }\r\n }\r\n\r\n subscribe(handler: (payload: string) => void): () => void {\r\n this.handlers.add(handler);\r\n return () => this.handlers.delete(handler);\r\n }\r\n}\r\n","export const DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024;\r\nexport const DEFAULT_MAX_JSON_DEPTH = 32;\r\nexport const DEFAULT_MAX_PENDING_AUTH_MESSAGES = 100;\r\nexport const DEFAULT_MAX_PENDING_AUTH_BYTES = 2 * 1024 * 1024;\r\nexport const DEFAULT_MESSAGES_PER_SECOND = 120;\r\nexport const DEFAULT_MESSAGE_BURST = 240;\r\nexport const DEFAULT_PRESENCE_THROTTLE_MS = 50;\r\n\r\nexport function hasJsonDepthAtMost(message: string, maxDepth: number): boolean {\r\n let depth = 0;\r\n let inString = false;\r\n let escaped = false;\r\n\r\n for (const char of message) {\r\n if (inString) {\r\n if (escaped) escaped = false;\r\n else if (char === '\\\\') escaped = true;\r\n else if (char === '\"') inString = false;\r\n continue;\r\n }\r\n if (char === '\"') inString = true;\r\n else if (char === '{' || char === '[') {\r\n depth += 1;\r\n if (depth > maxDepth) return false;\r\n } else if (char === '}' || char === ']') {\r\n depth -= 1;\r\n if (depth < 0) return false;\r\n }\r\n }\r\n\r\n return depth === 0 && !inString;\r\n}\r\n\r\nexport class MessageRateLimiter {\r\n private tokens: number;\r\n private updatedAt: number;\r\n\r\n constructor(\r\n private readonly ratePerSecond: number,\r\n private readonly burst: number,\r\n now = Date.now(),\r\n ) {\r\n this.tokens = burst;\r\n this.updatedAt = now;\r\n }\r\n\r\n take(now = Date.now()): boolean {\r\n const elapsedSeconds = Math.max(0, now - this.updatedAt) / 1000;\r\n this.tokens = Math.min(this.burst, this.tokens + elapsedSeconds * this.ratePerSecond);\r\n this.updatedAt = now;\r\n if (this.tokens < 1) return false;\r\n this.tokens -= 1;\r\n return true;\r\n }\r\n}\r\n","import { WebSocketServer, type RawData } from 'ws';\r\nimport type { Server } from 'http';\r\nimport { SyncHub } from './sync-hub';\r\nimport type { HubBackend } from './hub-backend';\r\nimport type { HubFanout } from './hub-fanout';\r\nimport type { Authenticate } from './authenticate';\r\nimport type { Authorize, CanRead } from './authorize';\r\nimport { startHeartbeat } from './heartbeat';\r\nimport {\r\n DEFAULT_MAX_JSON_DEPTH,\r\n DEFAULT_MAX_MESSAGE_BYTES,\r\n DEFAULT_MAX_PENDING_AUTH_BYTES,\r\n DEFAULT_MAX_PENDING_AUTH_MESSAGES,\r\n DEFAULT_MESSAGE_BURST,\r\n DEFAULT_MESSAGES_PER_SECOND,\r\n DEFAULT_PRESENCE_THROTTLE_MS,\r\n MessageRateLimiter,\r\n} from './resource-limits';\r\nimport { DEFAULT_SHUTDOWN_GRACE_MS, drainWebSocketServer } from './shutdown';\r\n\r\nexport interface CreateSyncServerOptions {\r\n port?: number;\r\n server?: Server;\r\n backend?: HubBackend;\r\n fanout?: HubFanout;\r\n instanceId?: string;\r\n authenticate?: Authenticate;\r\n authorize?: Authorize;\r\n canRead?: CanRead;\r\n heartbeatIntervalMs?: number;\r\n maxMessageBytes?: number;\r\n maxJsonDepth?: number;\r\n maxPendingAuthMessages?: number;\r\n maxPendingAuthBytes?: number;\r\n messagesPerSecond?: number;\r\n messageBurst?: number;\r\n presenceThrottleMs?: number;\r\n shutdownGraceMs?: number;\r\n}\r\n\r\nfunction rawDataByteLength(data: RawData): number {\r\n if (Array.isArray(data)) return data.reduce((total, chunk) => total + chunk.byteLength, 0);\r\n return data.byteLength;\r\n}\r\n\r\nexport function createSyncServer(options: CreateSyncServerOptions = {}): {\r\n hub: SyncHub;\r\n wss: WebSocketServer;\r\n close: () => Promise<void>;\r\n} {\r\n const shutdownGraceMs = options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;\r\n if (!Number.isFinite(shutdownGraceMs) || shutdownGraceMs < 0) {\r\n throw new RangeError('shutdownGraceMs must be a non-negative finite number');\r\n }\r\n const hub = new SyncHub({\r\n backend: options.backend,\r\n fanout: options.fanout,\r\n instanceId: options.instanceId,\r\n authorize: options.authorize,\r\n canRead: options.canRead,\r\n maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,\r\n presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,\r\n });\r\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\r\n const wss = options.server\r\n ? new WebSocketServer({ server: options.server, maxPayload: maxMessageBytes })\r\n : new WebSocketServer({ port: options.port ?? 0, maxPayload: maxMessageBytes });\r\n const heartbeat = startHeartbeat(wss, options.heartbeatIntervalMs ?? 30000);\r\n let shuttingDown = false;\r\n let closePromise: Promise<void> | undefined;\r\n let counter = 0;\r\n wss.on('connection', (ws, req) => {\r\n if (shuttingDown) {\r\n ws.close(1001, 'server shutting down');\r\n return;\r\n }\r\n heartbeat.track(ws);\r\n // Protocol/parser failures (including maxPayload) close the peer. Consuming the socket-level\r\n // error keeps hostile input from becoming an uncaught process error.\r\n ws.on('error', () => undefined);\r\n const url = new URL(req.url ?? '', 'http://localhost');\r\n const room = url.searchParams.get('room');\r\n if (!room) {\r\n ws.close(4400, 'room required');\r\n return;\r\n }\r\n const connId = `c${++counter}-${Math.random().toString(36).slice(2, 8)}`;\r\n\r\n let state: 'pending' | 'ready' | 'rejected' = 'pending';\r\n let closed = false;\r\n let admitted = false;\r\n const queue: string[] = [];\r\n let queuedBytes = 0;\r\n const maxPendingAuthMessages =\r\n options.maxPendingAuthMessages ?? DEFAULT_MAX_PENDING_AUTH_MESSAGES;\r\n const maxPendingAuthBytes = options.maxPendingAuthBytes ?? DEFAULT_MAX_PENDING_AUTH_BYTES;\r\n const limiter = new MessageRateLimiter(\r\n options.messagesPerSecond ?? DEFAULT_MESSAGES_PER_SECOND,\r\n options.messageBurst ?? DEFAULT_MESSAGE_BURST,\r\n );\r\n\r\n const send = (m: string) => {\r\n try {\r\n ws.send(m);\r\n } catch {\r\n /* socket closed mid-send */\r\n }\r\n };\r\n\r\n ws.on('message', (data) => {\r\n if (state === 'rejected') return;\r\n const messageBytes = rawDataByteLength(data);\r\n if (messageBytes > maxMessageBytes) {\r\n state = 'rejected';\r\n ws.close(1009, 'message too large');\r\n return;\r\n }\r\n if (!limiter.take()) {\r\n state = 'rejected';\r\n ws.close(4408, 'rate limit exceeded');\r\n return;\r\n }\r\n const msg = String(data);\r\n if (state === 'pending') {\r\n if (\r\n queue.length >= maxPendingAuthMessages ||\r\n queuedBytes + messageBytes > maxPendingAuthBytes\r\n ) {\r\n state = 'rejected';\r\n ws.close(4408, 'authentication queue limit exceeded');\r\n return;\r\n }\r\n queue.push(msg);\r\n queuedBytes += messageBytes;\r\n return;\r\n }\r\n void hub.handleMessage(connId, msg).catch((err) => console.error('[sync-server]', err));\r\n });\r\n ws.on('close', () => {\r\n closed = true;\r\n if (admitted) hub.removeConnection(connId);\r\n });\r\n\r\n Promise.resolve(options.authenticate ? options.authenticate({ req, room }) : { userId: connId })\r\n .then((result) => {\r\n if (closed || state === 'rejected' || shuttingDown) return;\r\n if (!result) {\r\n state = 'rejected';\r\n ws.close(4401, 'unauthorized');\r\n return;\r\n }\r\n state = 'ready';\r\n admitted = true;\r\n hub.addConnection({ id: connId, room, userId: result.userId, role: result.role, send });\r\n for (const m of queue) {\r\n void hub.handleMessage(connId, m).catch((err) => console.error('[sync-server]', err));\r\n }\r\n queue.length = 0;\r\n queuedBytes = 0;\r\n })\r\n .catch(() => {\r\n if (!closed && !shuttingDown) {\r\n state = 'rejected';\r\n ws.close(4401, 'unauthorized');\r\n }\r\n });\r\n });\r\n const close = (): Promise<void> => {\r\n if (closePromise) return closePromise;\r\n shuttingDown = true;\r\n closePromise = drainWebSocketServer(wss, shutdownGraceMs).then(() => {\r\n heartbeat.stop();\r\n hub.close();\r\n });\r\n return closePromise;\r\n };\r\n\r\n return {\r\n hub,\r\n wss,\r\n close,\r\n };\r\n}\r\n","export interface HeartbeatSocket {\r\n ping(): void;\r\n terminate(): void;\r\n on(event: 'pong', listener: () => void): void;\r\n}\r\nexport interface HeartbeatServer {\r\n clients: Set<HeartbeatSocket>;\r\n}\r\nexport interface Heartbeat {\r\n track(ws: HeartbeatSocket): void;\r\n stop(): void;\r\n}\r\n\r\nexport function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat {\r\n if (intervalMs <= 0) {\r\n return {\r\n track: () => {\r\n /* disabled */\r\n },\r\n stop: () => {\r\n /* disabled */\r\n },\r\n };\r\n }\r\n const alive = new WeakMap<HeartbeatSocket, boolean>();\r\n const track = (ws: HeartbeatSocket): void => {\r\n alive.set(ws, true);\r\n ws.on('pong', () => alive.set(ws, true));\r\n };\r\n const interval = setInterval(() => {\r\n for (const ws of wss.clients) {\r\n try {\r\n if (alive.get(ws) === false) {\r\n ws.terminate();\r\n continue;\r\n }\r\n alive.set(ws, false);\r\n ws.ping();\r\n } catch {\r\n /* socket dying mid-tick — skip it */\r\n }\r\n }\r\n }, intervalMs);\r\n return { track, stop: () => clearInterval(interval) };\r\n}\r\n","export const DEFAULT_SHUTDOWN_GRACE_MS = 5000;\r\n\r\nexport interface ShutdownSocket {\r\n close(code?: number, reason?: string): void;\r\n terminate(): void;\r\n}\r\n\r\nexport interface ShutdownServer {\r\n clients: Set<ShutdownSocket>;\r\n close(callback: (error?: Error) => void): void;\r\n}\r\n\r\n/** Stop admission, ask connected peers to leave, then forcefully reap stragglers. */\r\nexport function drainWebSocketServer(wss: ShutdownServer, graceMs: number): Promise<void> {\r\n return new Promise((resolve) => {\r\n let settled = false;\r\n const finish = (): void => {\r\n if (settled) return;\r\n settled = true;\r\n clearTimeout(forceTimer);\r\n resolve();\r\n };\r\n const forceTimer = setTimeout(() => {\r\n for (const ws of wss.clients) {\r\n try {\r\n ws.terminate();\r\n } catch {\r\n /* socket already failed while shutdown was in progress */\r\n }\r\n }\r\n finish();\r\n }, graceMs);\r\n\r\n // Calling close first prevents new connections. Its callback runs once all clients have left.\r\n wss.close(() => finish());\r\n for (const ws of wss.clients) {\r\n try {\r\n ws.close(1001, 'server shutting down');\r\n } catch {\r\n try {\r\n ws.terminate();\r\n } catch {\r\n /* socket already closed */\r\n }\r\n }\r\n }\r\n });\r\n}\r\n"],"mappings":"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","names":["import_sync"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -75,12 +75,18 @@ declare class SyncHub {
|
|
|
75
75
|
addConnection(conn: Connection): void;
|
|
76
76
|
removeConnection(connId: string): void;
|
|
77
77
|
roomCount(): number;
|
|
78
|
+
/**
|
|
79
|
+
* Broadcasts ephemeral server-owned presence data to every connection in a room.
|
|
80
|
+
* The returned count covers successful delivery on this hub instance only; configured fan-out
|
|
81
|
+
* forwards the same event to other instances on a best-effort basis.
|
|
82
|
+
*/
|
|
83
|
+
broadcastPresence<T>(room: string, data: T): number;
|
|
78
84
|
handleMessage(connId: string, message: string): Promise<void>;
|
|
79
85
|
private process;
|
|
80
86
|
private mayRead;
|
|
81
87
|
private safePublish;
|
|
82
88
|
private relayToRoom;
|
|
83
|
-
private
|
|
89
|
+
private broadcastClientPresence;
|
|
84
90
|
private schedulePresence;
|
|
85
91
|
private broadcastLeave;
|
|
86
92
|
private deliverToRoom;
|
package/dist/index.d.ts
CHANGED
|
@@ -75,12 +75,18 @@ declare class SyncHub {
|
|
|
75
75
|
addConnection(conn: Connection): void;
|
|
76
76
|
removeConnection(connId: string): void;
|
|
77
77
|
roomCount(): number;
|
|
78
|
+
/**
|
|
79
|
+
* Broadcasts ephemeral server-owned presence data to every connection in a room.
|
|
80
|
+
* The returned count covers successful delivery on this hub instance only; configured fan-out
|
|
81
|
+
* forwards the same event to other instances on a best-effort basis.
|
|
82
|
+
*/
|
|
83
|
+
broadcastPresence<T>(room: string, data: T): number;
|
|
78
84
|
handleMessage(connId: string, message: string): Promise<void>;
|
|
79
85
|
private process;
|
|
80
86
|
private mayRead;
|
|
81
87
|
private safePublish;
|
|
82
88
|
private relayToRoom;
|
|
83
|
-
private
|
|
89
|
+
private broadcastClientPresence;
|
|
84
90
|
private schedulePresence;
|
|
85
91
|
private broadcastLeave;
|
|
86
92
|
private deliverToRoom;
|
package/dist/index.js
CHANGED
|
@@ -172,6 +172,17 @@ var SyncHub = class {
|
|
|
172
172
|
roomCount() {
|
|
173
173
|
return this.rooms.size;
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Broadcasts ephemeral server-owned presence data to every connection in a room.
|
|
177
|
+
* The returned count covers successful delivery on this hub instance only; configured fan-out
|
|
178
|
+
* forwards the same event to other instances on a best-effort basis.
|
|
179
|
+
*/
|
|
180
|
+
broadcastPresence(room, data) {
|
|
181
|
+
const op = { kind: "presence", data };
|
|
182
|
+
const sent = this.relayToRoom(room, void 0, JSON.stringify({ from: HUB_FROM, op }));
|
|
183
|
+
this.safePublish(JSON.stringify({ o: this.instanceId, room, from: HUB_FROM, op }));
|
|
184
|
+
return sent;
|
|
185
|
+
}
|
|
175
186
|
handleMessage(connId, message) {
|
|
176
187
|
const conn = this.conns.get(connId);
|
|
177
188
|
if (!conn) return Promise.resolve();
|
|
@@ -254,18 +265,21 @@ var SyncHub = class {
|
|
|
254
265
|
}
|
|
255
266
|
relayToRoom(room, excludeId, message) {
|
|
256
267
|
const members = this.rooms.get(room);
|
|
257
|
-
if (!members) return;
|
|
268
|
+
if (!members) return 0;
|
|
269
|
+
let sent = 0;
|
|
258
270
|
for (const cid of members) {
|
|
259
271
|
if (cid === excludeId) continue;
|
|
260
272
|
const conn = this.conns.get(cid);
|
|
261
273
|
if (!conn) continue;
|
|
262
274
|
try {
|
|
263
275
|
conn.send(message);
|
|
276
|
+
sent += 1;
|
|
264
277
|
} catch {
|
|
265
278
|
}
|
|
266
279
|
}
|
|
280
|
+
return sent;
|
|
267
281
|
}
|
|
268
|
-
|
|
282
|
+
broadcastClientPresence(conn, data) {
|
|
269
283
|
this.presenceConnections.add(conn.id);
|
|
270
284
|
const message = JSON.stringify({ from: conn.id, op: { kind: "presence", data } });
|
|
271
285
|
this.relayToRoom(conn.room, conn.id, message);
|
|
@@ -280,14 +294,14 @@ var SyncHub = class {
|
|
|
280
294
|
}
|
|
281
295
|
schedulePresence(conn, data) {
|
|
282
296
|
if (this.presenceThrottleMs <= 0) {
|
|
283
|
-
this.
|
|
297
|
+
this.broadcastClientPresence(conn, data);
|
|
284
298
|
return;
|
|
285
299
|
}
|
|
286
300
|
const now = Date.now();
|
|
287
301
|
const lastSentAt = this.lastPresenceAt.get(conn.id);
|
|
288
302
|
if (lastSentAt === void 0 || now - lastSentAt >= this.presenceThrottleMs) {
|
|
289
303
|
this.lastPresenceAt.set(conn.id, now);
|
|
290
|
-
this.
|
|
304
|
+
this.broadcastClientPresence(conn, data);
|
|
291
305
|
return;
|
|
292
306
|
}
|
|
293
307
|
const existing = this.pendingPresence.get(conn.id);
|
|
@@ -301,7 +315,7 @@ var SyncHub = class {
|
|
|
301
315
|
this.pendingPresence.delete(conn.id);
|
|
302
316
|
if (!pending || !this.conns.has(conn.id)) return;
|
|
303
317
|
this.lastPresenceAt.set(conn.id, Date.now());
|
|
304
|
-
this.
|
|
318
|
+
this.broadcastClientPresence(conn, pending.data);
|
|
305
319
|
},
|
|
306
320
|
this.presenceThrottleMs - (now - lastSentAt)
|
|
307
321
|
);
|
|
@@ -358,11 +372,12 @@ var SyncHub = class {
|
|
|
358
372
|
async sendCorrection(conn, from, op, current) {
|
|
359
373
|
let correction;
|
|
360
374
|
if (op.kind === "upsert") {
|
|
361
|
-
correction = current ? { kind: "upsert", element: current } : { kind: "remove", id: op.element.id };
|
|
375
|
+
correction = current ? this.mayRead(conn, current.audience) ? { kind: "upsert", element: current } : { kind: "remove", id: current.id } : { kind: "remove", id: op.element.id };
|
|
362
376
|
} else if (op.kind === "remove") {
|
|
363
|
-
correction = current ? { kind: "upsert", element: current } : void 0;
|
|
377
|
+
correction = current ? this.mayRead(conn, current.audience) ? { kind: "upsert", element: current } : { kind: "remove", id: current.id } : void 0;
|
|
364
378
|
} else if (op.kind === "clear") {
|
|
365
|
-
const
|
|
379
|
+
const all = await this.backend.snapshot(conn.room);
|
|
380
|
+
const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;
|
|
366
381
|
correction = { kind: "snapshot", to: from, elements };
|
|
367
382
|
}
|
|
368
383
|
if (correction) conn.send(JSON.stringify({ from: HUB_FROM, op: correction }));
|
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 { parseEnvelope, isValidElement, type SyncOp, type SyncEnvelope } 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, 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 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\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 canRead?: CanRead;\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.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 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 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({ from: HUB_FROM, op: { kind: 'snapshot', to: env.from, elements } }),\n );\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 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): void {\n const members = this.rooms.get(room);\n if (!members) return;\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 } catch {\n /* a throwing socket must not break the relay loop */\n }\n }\n }\n\n private broadcastPresence(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.broadcastPresence(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.broadcastPresence(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.broadcastPresence(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 ? { kind: 'upsert', element: current }\n : { kind: 'remove', id: op.element.id };\n } else if (op.kind === 'remove') {\n correction = current ? { kind: 'upsert', element: current } : undefined;\n } else if (op.kind === 'clear') {\n const elements = await this.backend.snapshot(conn.room);\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 (!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 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 } 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\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 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 this.rooms.delete(room);\r\n return;\r\n }\r\n applyOpToMap(this.room(room), op);\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;\r\nexport const DEFAULT_MAX_JSON_DEPTH = 32;\r\nexport const DEFAULT_MAX_PENDING_AUTH_MESSAGES = 100;\r\nexport const DEFAULT_MAX_PENDING_AUTH_BYTES = 2 * 1024 * 1024;\r\nexport const DEFAULT_MESSAGES_PER_SECOND = 120;\r\nexport const DEFAULT_MESSAGE_BURST = 240;\r\nexport const DEFAULT_PRESENCE_THROTTLE_MS = 50;\r\n\r\nexport function hasJsonDepthAtMost(message: string, maxDepth: number): boolean {\r\n let depth = 0;\r\n let inString = false;\r\n let escaped = false;\r\n\r\n for (const char of message) {\r\n if (inString) {\r\n if (escaped) escaped = false;\r\n else if (char === '\\\\') escaped = true;\r\n else if (char === '\"') inString = false;\r\n continue;\r\n }\r\n if (char === '\"') inString = true;\r\n else if (char === '{' || char === '[') {\r\n depth += 1;\r\n if (depth > maxDepth) return false;\r\n } else if (char === '}' || char === ']') {\r\n depth -= 1;\r\n if (depth < 0) return false;\r\n }\r\n }\r\n\r\n return depth === 0 && !inString;\r\n}\r\n\r\nexport class MessageRateLimiter {\r\n private tokens: number;\r\n private updatedAt: number;\r\n\r\n constructor(\r\n private readonly ratePerSecond: number,\r\n private readonly burst: number,\r\n now = Date.now(),\r\n ) {\r\n this.tokens = burst;\r\n this.updatedAt = now;\r\n }\r\n\r\n take(now = Date.now()): boolean {\r\n const elapsedSeconds = Math.max(0, now - this.updatedAt) / 1000;\r\n this.tokens = Math.min(this.burst, this.tokens + elapsedSeconds * this.ratePerSecond);\r\n this.updatedAt = now;\r\n if (this.tokens < 1) return false;\r\n this.tokens -= 1;\r\n return true;\r\n }\r\n}\r\n","import { WebSocketServer, type RawData } from 'ws';\r\nimport type { Server } from 'http';\r\nimport { SyncHub } from './sync-hub';\r\nimport type { HubBackend } from './hub-backend';\r\nimport type { HubFanout } from './hub-fanout';\r\nimport type { Authenticate } from './authenticate';\r\nimport type { Authorize, CanRead } from './authorize';\r\nimport { startHeartbeat } from './heartbeat';\r\nimport {\r\n DEFAULT_MAX_JSON_DEPTH,\r\n DEFAULT_MAX_MESSAGE_BYTES,\r\n DEFAULT_MAX_PENDING_AUTH_BYTES,\r\n DEFAULT_MAX_PENDING_AUTH_MESSAGES,\r\n DEFAULT_MESSAGE_BURST,\r\n DEFAULT_MESSAGES_PER_SECOND,\r\n DEFAULT_PRESENCE_THROTTLE_MS,\r\n MessageRateLimiter,\r\n} from './resource-limits';\r\nimport { DEFAULT_SHUTDOWN_GRACE_MS, drainWebSocketServer } from './shutdown';\r\n\r\nexport interface CreateSyncServerOptions {\r\n port?: number;\r\n server?: Server;\r\n backend?: HubBackend;\r\n fanout?: HubFanout;\r\n instanceId?: string;\r\n authenticate?: Authenticate;\r\n authorize?: Authorize;\r\n canRead?: CanRead;\r\n heartbeatIntervalMs?: number;\r\n maxMessageBytes?: number;\r\n maxJsonDepth?: number;\r\n maxPendingAuthMessages?: number;\r\n maxPendingAuthBytes?: number;\r\n messagesPerSecond?: number;\r\n messageBurst?: number;\r\n presenceThrottleMs?: number;\r\n shutdownGraceMs?: number;\r\n}\r\n\r\nfunction rawDataByteLength(data: RawData): number {\r\n if (Array.isArray(data)) return data.reduce((total, chunk) => total + chunk.byteLength, 0);\r\n return data.byteLength;\r\n}\r\n\r\nexport function createSyncServer(options: CreateSyncServerOptions = {}): {\r\n hub: SyncHub;\r\n wss: WebSocketServer;\r\n close: () => Promise<void>;\r\n} {\r\n const shutdownGraceMs = options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;\r\n if (!Number.isFinite(shutdownGraceMs) || shutdownGraceMs < 0) {\r\n throw new RangeError('shutdownGraceMs must be a non-negative finite number');\r\n }\r\n const hub = new SyncHub({\r\n backend: options.backend,\r\n fanout: options.fanout,\r\n instanceId: options.instanceId,\r\n authorize: options.authorize,\r\n canRead: options.canRead,\r\n maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,\r\n presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,\r\n });\r\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\r\n const wss = options.server\r\n ? new WebSocketServer({ server: options.server, maxPayload: maxMessageBytes })\r\n : new WebSocketServer({ port: options.port ?? 0, maxPayload: maxMessageBytes });\r\n const heartbeat = startHeartbeat(wss, options.heartbeatIntervalMs ?? 30000);\r\n let shuttingDown = false;\r\n let closePromise: Promise<void> | undefined;\r\n let counter = 0;\r\n wss.on('connection', (ws, req) => {\r\n if (shuttingDown) {\r\n ws.close(1001, 'server shutting down');\r\n return;\r\n }\r\n heartbeat.track(ws);\r\n // Protocol/parser failures (including maxPayload) close the peer. Consuming the socket-level\r\n // error keeps hostile input from becoming an uncaught process error.\r\n ws.on('error', () => undefined);\r\n const url = new URL(req.url ?? '', 'http://localhost');\r\n const room = url.searchParams.get('room');\r\n if (!room) {\r\n ws.close(4400, 'room required');\r\n return;\r\n }\r\n const connId = `c${++counter}-${Math.random().toString(36).slice(2, 8)}`;\r\n\r\n let state: 'pending' | 'ready' | 'rejected' = 'pending';\r\n let closed = false;\r\n let admitted = false;\r\n const queue: string[] = [];\r\n let queuedBytes = 0;\r\n const maxPendingAuthMessages =\r\n options.maxPendingAuthMessages ?? DEFAULT_MAX_PENDING_AUTH_MESSAGES;\r\n const maxPendingAuthBytes = options.maxPendingAuthBytes ?? DEFAULT_MAX_PENDING_AUTH_BYTES;\r\n const limiter = new MessageRateLimiter(\r\n options.messagesPerSecond ?? DEFAULT_MESSAGES_PER_SECOND,\r\n options.messageBurst ?? DEFAULT_MESSAGE_BURST,\r\n );\r\n\r\n const send = (m: string) => {\r\n try {\r\n ws.send(m);\r\n } catch {\r\n /* socket closed mid-send */\r\n }\r\n };\r\n\r\n ws.on('message', (data) => {\r\n if (state === 'rejected') return;\r\n const messageBytes = rawDataByteLength(data);\r\n if (messageBytes > maxMessageBytes) {\r\n state = 'rejected';\r\n ws.close(1009, 'message too large');\r\n return;\r\n }\r\n if (!limiter.take()) {\r\n state = 'rejected';\r\n ws.close(4408, 'rate limit exceeded');\r\n return;\r\n }\r\n const msg = String(data);\r\n if (state === 'pending') {\r\n if (\r\n queue.length >= maxPendingAuthMessages ||\r\n queuedBytes + messageBytes > maxPendingAuthBytes\r\n ) {\r\n state = 'rejected';\r\n ws.close(4408, 'authentication queue limit exceeded');\r\n return;\r\n }\r\n queue.push(msg);\r\n queuedBytes += messageBytes;\r\n return;\r\n }\r\n void hub.handleMessage(connId, msg).catch((err) => console.error('[sync-server]', err));\r\n });\r\n ws.on('close', () => {\r\n closed = true;\r\n if (admitted) hub.removeConnection(connId);\r\n });\r\n\r\n Promise.resolve(options.authenticate ? options.authenticate({ req, room }) : { userId: connId })\r\n .then((result) => {\r\n if (closed || state === 'rejected' || shuttingDown) return;\r\n if (!result) {\r\n state = 'rejected';\r\n ws.close(4401, 'unauthorized');\r\n return;\r\n }\r\n state = 'ready';\r\n admitted = true;\r\n hub.addConnection({ id: connId, room, userId: result.userId, role: result.role, send });\r\n for (const m of queue) {\r\n void hub.handleMessage(connId, m).catch((err) => console.error('[sync-server]', err));\r\n }\r\n queue.length = 0;\r\n queuedBytes = 0;\r\n })\r\n .catch(() => {\r\n if (!closed && !shuttingDown) {\r\n state = 'rejected';\r\n ws.close(4401, 'unauthorized');\r\n }\r\n });\r\n });\r\n const close = (): Promise<void> => {\r\n if (closePromise) return closePromise;\r\n shuttingDown = true;\r\n closePromise = drainWebSocketServer(wss, shutdownGraceMs).then(() => {\r\n heartbeat.stop();\r\n hub.close();\r\n });\r\n return closePromise;\r\n };\r\n\r\n return {\r\n hub,\r\n wss,\r\n close,\r\n };\r\n}\r\n","export interface HeartbeatSocket {\r\n ping(): void;\r\n terminate(): void;\r\n on(event: 'pong', listener: () => void): void;\r\n}\r\nexport interface HeartbeatServer {\r\n clients: Set<HeartbeatSocket>;\r\n}\r\nexport interface Heartbeat {\r\n track(ws: HeartbeatSocket): void;\r\n stop(): void;\r\n}\r\n\r\nexport function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat {\r\n if (intervalMs <= 0) {\r\n return {\r\n track: () => {\r\n /* disabled */\r\n },\r\n stop: () => {\r\n /* disabled */\r\n },\r\n };\r\n }\r\n const alive = new WeakMap<HeartbeatSocket, boolean>();\r\n const track = (ws: HeartbeatSocket): void => {\r\n alive.set(ws, true);\r\n ws.on('pong', () => alive.set(ws, true));\r\n };\r\n const interval = setInterval(() => {\r\n for (const ws of wss.clients) {\r\n try {\r\n if (alive.get(ws) === false) {\r\n ws.terminate();\r\n continue;\r\n }\r\n alive.set(ws, false);\r\n ws.ping();\r\n } catch {\r\n /* socket dying mid-tick — skip it */\r\n }\r\n }\r\n }, intervalMs);\r\n return { track, stop: () => clearInterval(interval) };\r\n}\r\n","export const DEFAULT_SHUTDOWN_GRACE_MS = 5000;\r\n\r\nexport interface ShutdownSocket {\r\n close(code?: number, reason?: string): void;\r\n terminate(): void;\r\n}\r\n\r\nexport interface ShutdownServer {\r\n clients: Set<ShutdownSocket>;\r\n close(callback: (error?: Error) => void): void;\r\n}\r\n\r\n/** Stop admission, ask connected peers to leave, then forcefully reap stragglers. */\r\nexport function drainWebSocketServer(wss: ShutdownServer, graceMs: number): Promise<void> {\r\n return new Promise((resolve) => {\r\n let settled = false;\r\n const finish = (): void => {\r\n if (settled) return;\r\n settled = true;\r\n clearTimeout(forceTimer);\r\n resolve();\r\n };\r\n const forceTimer = setTimeout(() => {\r\n for (const ws of wss.clients) {\r\n try {\r\n ws.terminate();\r\n } catch {\r\n /* socket already failed while shutdown was in progress */\r\n }\r\n }\r\n finish();\r\n }, graceMs);\r\n\r\n // Calling close first prevents new connections. Its callback runs once all clients have left.\r\n wss.close(() => finish());\r\n for (const ws of wss.clients) {\r\n try {\r\n ws.close(1001, 'server shutting down');\r\n } catch {\r\n try {\r\n ws.terminate();\r\n } catch {\r\n /* socket already closed */\r\n }\r\n }\r\n }\r\n });\r\n}\r\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 { parseEnvelope, isValidElement, type SyncOp, type SyncEnvelope } 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, 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 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\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 canRead?: CanRead;\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.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 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({ from: HUB_FROM, op: { kind: 'snapshot', to: env.from, elements } }),\n );\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 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 (!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 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 } 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\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 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 this.rooms.delete(room);\r\n return;\r\n }\r\n applyOpToMap(this.room(room), op);\r\n }\r\n}\r\n","export interface HubFanout {\r\n publish(payload: string): void | Promise<void>;\r\n subscribe(handler: (payload: string) => void): () => void;\r\n close?(): void;\r\n}\r\n\r\nexport class InMemoryHubFanout implements HubFanout {\r\n private readonly handlers = new Set<(payload: string) => void>();\r\n\r\n publish(payload: string): void {\r\n for (const h of this.handlers) {\r\n try {\r\n h(payload);\r\n } catch {\r\n /* one throwing subscriber must not break the publish loop */\r\n }\r\n }\r\n }\r\n\r\n subscribe(handler: (payload: string) => void): () => void {\r\n this.handlers.add(handler);\r\n return () => this.handlers.delete(handler);\r\n }\r\n}\r\n","export const DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024;\r\nexport const DEFAULT_MAX_JSON_DEPTH = 32;\r\nexport const DEFAULT_MAX_PENDING_AUTH_MESSAGES = 100;\r\nexport const DEFAULT_MAX_PENDING_AUTH_BYTES = 2 * 1024 * 1024;\r\nexport const DEFAULT_MESSAGES_PER_SECOND = 120;\r\nexport const DEFAULT_MESSAGE_BURST = 240;\r\nexport const DEFAULT_PRESENCE_THROTTLE_MS = 50;\r\n\r\nexport function hasJsonDepthAtMost(message: string, maxDepth: number): boolean {\r\n let depth = 0;\r\n let inString = false;\r\n let escaped = false;\r\n\r\n for (const char of message) {\r\n if (inString) {\r\n if (escaped) escaped = false;\r\n else if (char === '\\\\') escaped = true;\r\n else if (char === '\"') inString = false;\r\n continue;\r\n }\r\n if (char === '\"') inString = true;\r\n else if (char === '{' || char === '[') {\r\n depth += 1;\r\n if (depth > maxDepth) return false;\r\n } else if (char === '}' || char === ']') {\r\n depth -= 1;\r\n if (depth < 0) return false;\r\n }\r\n }\r\n\r\n return depth === 0 && !inString;\r\n}\r\n\r\nexport class MessageRateLimiter {\r\n private tokens: number;\r\n private updatedAt: number;\r\n\r\n constructor(\r\n private readonly ratePerSecond: number,\r\n private readonly burst: number,\r\n now = Date.now(),\r\n ) {\r\n this.tokens = burst;\r\n this.updatedAt = now;\r\n }\r\n\r\n take(now = Date.now()): boolean {\r\n const elapsedSeconds = Math.max(0, now - this.updatedAt) / 1000;\r\n this.tokens = Math.min(this.burst, this.tokens + elapsedSeconds * this.ratePerSecond);\r\n this.updatedAt = now;\r\n if (this.tokens < 1) return false;\r\n this.tokens -= 1;\r\n return true;\r\n }\r\n}\r\n","import { WebSocketServer, type RawData } from 'ws';\r\nimport type { Server } from 'http';\r\nimport { SyncHub } from './sync-hub';\r\nimport type { HubBackend } from './hub-backend';\r\nimport type { HubFanout } from './hub-fanout';\r\nimport type { Authenticate } from './authenticate';\r\nimport type { Authorize, CanRead } from './authorize';\r\nimport { startHeartbeat } from './heartbeat';\r\nimport {\r\n DEFAULT_MAX_JSON_DEPTH,\r\n DEFAULT_MAX_MESSAGE_BYTES,\r\n DEFAULT_MAX_PENDING_AUTH_BYTES,\r\n DEFAULT_MAX_PENDING_AUTH_MESSAGES,\r\n DEFAULT_MESSAGE_BURST,\r\n DEFAULT_MESSAGES_PER_SECOND,\r\n DEFAULT_PRESENCE_THROTTLE_MS,\r\n MessageRateLimiter,\r\n} from './resource-limits';\r\nimport { DEFAULT_SHUTDOWN_GRACE_MS, drainWebSocketServer } from './shutdown';\r\n\r\nexport interface CreateSyncServerOptions {\r\n port?: number;\r\n server?: Server;\r\n backend?: HubBackend;\r\n fanout?: HubFanout;\r\n instanceId?: string;\r\n authenticate?: Authenticate;\r\n authorize?: Authorize;\r\n canRead?: CanRead;\r\n heartbeatIntervalMs?: number;\r\n maxMessageBytes?: number;\r\n maxJsonDepth?: number;\r\n maxPendingAuthMessages?: number;\r\n maxPendingAuthBytes?: number;\r\n messagesPerSecond?: number;\r\n messageBurst?: number;\r\n presenceThrottleMs?: number;\r\n shutdownGraceMs?: number;\r\n}\r\n\r\nfunction rawDataByteLength(data: RawData): number {\r\n if (Array.isArray(data)) return data.reduce((total, chunk) => total + chunk.byteLength, 0);\r\n return data.byteLength;\r\n}\r\n\r\nexport function createSyncServer(options: CreateSyncServerOptions = {}): {\r\n hub: SyncHub;\r\n wss: WebSocketServer;\r\n close: () => Promise<void>;\r\n} {\r\n const shutdownGraceMs = options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;\r\n if (!Number.isFinite(shutdownGraceMs) || shutdownGraceMs < 0) {\r\n throw new RangeError('shutdownGraceMs must be a non-negative finite number');\r\n }\r\n const hub = new SyncHub({\r\n backend: options.backend,\r\n fanout: options.fanout,\r\n instanceId: options.instanceId,\r\n authorize: options.authorize,\r\n canRead: options.canRead,\r\n maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,\r\n presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,\r\n });\r\n const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;\r\n const wss = options.server\r\n ? new WebSocketServer({ server: options.server, maxPayload: maxMessageBytes })\r\n : new WebSocketServer({ port: options.port ?? 0, maxPayload: maxMessageBytes });\r\n const heartbeat = startHeartbeat(wss, options.heartbeatIntervalMs ?? 30000);\r\n let shuttingDown = false;\r\n let closePromise: Promise<void> | undefined;\r\n let counter = 0;\r\n wss.on('connection', (ws, req) => {\r\n if (shuttingDown) {\r\n ws.close(1001, 'server shutting down');\r\n return;\r\n }\r\n heartbeat.track(ws);\r\n // Protocol/parser failures (including maxPayload) close the peer. Consuming the socket-level\r\n // error keeps hostile input from becoming an uncaught process error.\r\n ws.on('error', () => undefined);\r\n const url = new URL(req.url ?? '', 'http://localhost');\r\n const room = url.searchParams.get('room');\r\n if (!room) {\r\n ws.close(4400, 'room required');\r\n return;\r\n }\r\n const connId = `c${++counter}-${Math.random().toString(36).slice(2, 8)}`;\r\n\r\n let state: 'pending' | 'ready' | 'rejected' = 'pending';\r\n let closed = false;\r\n let admitted = false;\r\n const queue: string[] = [];\r\n let queuedBytes = 0;\r\n const maxPendingAuthMessages =\r\n options.maxPendingAuthMessages ?? DEFAULT_MAX_PENDING_AUTH_MESSAGES;\r\n const maxPendingAuthBytes = options.maxPendingAuthBytes ?? DEFAULT_MAX_PENDING_AUTH_BYTES;\r\n const limiter = new MessageRateLimiter(\r\n options.messagesPerSecond ?? DEFAULT_MESSAGES_PER_SECOND,\r\n options.messageBurst ?? DEFAULT_MESSAGE_BURST,\r\n );\r\n\r\n const send = (m: string) => {\r\n try {\r\n ws.send(m);\r\n } catch {\r\n /* socket closed mid-send */\r\n }\r\n };\r\n\r\n ws.on('message', (data) => {\r\n if (state === 'rejected') return;\r\n const messageBytes = rawDataByteLength(data);\r\n if (messageBytes > maxMessageBytes) {\r\n state = 'rejected';\r\n ws.close(1009, 'message too large');\r\n return;\r\n }\r\n if (!limiter.take()) {\r\n state = 'rejected';\r\n ws.close(4408, 'rate limit exceeded');\r\n return;\r\n }\r\n const msg = String(data);\r\n if (state === 'pending') {\r\n if (\r\n queue.length >= maxPendingAuthMessages ||\r\n queuedBytes + messageBytes > maxPendingAuthBytes\r\n ) {\r\n state = 'rejected';\r\n ws.close(4408, 'authentication queue limit exceeded');\r\n return;\r\n }\r\n queue.push(msg);\r\n queuedBytes += messageBytes;\r\n return;\r\n }\r\n void hub.handleMessage(connId, msg).catch((err) => console.error('[sync-server]', err));\r\n });\r\n ws.on('close', () => {\r\n closed = true;\r\n if (admitted) hub.removeConnection(connId);\r\n });\r\n\r\n Promise.resolve(options.authenticate ? options.authenticate({ req, room }) : { userId: connId })\r\n .then((result) => {\r\n if (closed || state === 'rejected' || shuttingDown) return;\r\n if (!result) {\r\n state = 'rejected';\r\n ws.close(4401, 'unauthorized');\r\n return;\r\n }\r\n state = 'ready';\r\n admitted = true;\r\n hub.addConnection({ id: connId, room, userId: result.userId, role: result.role, send });\r\n for (const m of queue) {\r\n void hub.handleMessage(connId, m).catch((err) => console.error('[sync-server]', err));\r\n }\r\n queue.length = 0;\r\n queuedBytes = 0;\r\n })\r\n .catch(() => {\r\n if (!closed && !shuttingDown) {\r\n state = 'rejected';\r\n ws.close(4401, 'unauthorized');\r\n }\r\n });\r\n });\r\n const close = (): Promise<void> => {\r\n if (closePromise) return closePromise;\r\n shuttingDown = true;\r\n closePromise = drainWebSocketServer(wss, shutdownGraceMs).then(() => {\r\n heartbeat.stop();\r\n hub.close();\r\n });\r\n return closePromise;\r\n };\r\n\r\n return {\r\n hub,\r\n wss,\r\n close,\r\n };\r\n}\r\n","export interface HeartbeatSocket {\r\n ping(): void;\r\n terminate(): void;\r\n on(event: 'pong', listener: () => void): void;\r\n}\r\nexport interface HeartbeatServer {\r\n clients: Set<HeartbeatSocket>;\r\n}\r\nexport interface Heartbeat {\r\n track(ws: HeartbeatSocket): void;\r\n stop(): void;\r\n}\r\n\r\nexport function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat {\r\n if (intervalMs <= 0) {\r\n return {\r\n track: () => {\r\n /* disabled */\r\n },\r\n stop: () => {\r\n /* disabled */\r\n },\r\n };\r\n }\r\n const alive = new WeakMap<HeartbeatSocket, boolean>();\r\n const track = (ws: HeartbeatSocket): void => {\r\n alive.set(ws, true);\r\n ws.on('pong', () => alive.set(ws, true));\r\n };\r\n const interval = setInterval(() => {\r\n for (const ws of wss.clients) {\r\n try {\r\n if (alive.get(ws) === false) {\r\n ws.terminate();\r\n continue;\r\n }\r\n alive.set(ws, false);\r\n ws.ping();\r\n } catch {\r\n /* socket dying mid-tick — skip it */\r\n }\r\n }\r\n }, intervalMs);\r\n return { track, stop: () => clearInterval(interval) };\r\n}\r\n","export const DEFAULT_SHUTDOWN_GRACE_MS = 5000;\r\n\r\nexport interface ShutdownSocket {\r\n close(code?: number, reason?: string): void;\r\n terminate(): void;\r\n}\r\n\r\nexport interface ShutdownServer {\r\n clients: Set<ShutdownSocket>;\r\n close(callback: (error?: Error) => void): void;\r\n}\r\n\r\n/** Stop admission, ask connected peers to leave, then forcefully reap stragglers. */\r\nexport function drainWebSocketServer(wss: ShutdownServer, graceMs: number): Promise<void> {\r\n return new Promise((resolve) => {\r\n let settled = false;\r\n const finish = (): void => {\r\n if (settled) return;\r\n settled = true;\r\n clearTimeout(forceTimer);\r\n resolve();\r\n };\r\n const forceTimer = setTimeout(() => {\r\n for (const ws of wss.clients) {\r\n try {\r\n ws.terminate();\r\n } catch {\r\n /* socket already failed while shutdown was in progress */\r\n }\r\n }\r\n finish();\r\n }, graceMs);\r\n\r\n // Calling close first prevents new connections. Its callback runs once all clients have left.\r\n wss.close(() => finish());\r\n for (const ws of wss.clients) {\r\n try {\r\n ws.close(1001, 'server shutting down');\r\n } catch {\r\n try {\r\n ws.terminate();\r\n } catch {\r\n /* socket already closed */\r\n }\r\n }\r\n }\r\n });\r\n}\r\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.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Authoritative WebSocket relay server for Field Notes real-time sync",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@vitest/coverage-v8": "^4.1.0",
|
|
46
46
|
"tsup": "^8.5.1",
|
|
47
47
|
"vitest": "^4.1.0",
|
|
48
|
-
"@fieldnotes/core": "0.
|
|
48
|
+
"@fieldnotes/core": "0.53.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "tsup",
|