@fieldnotes/sync-server 0.17.0 → 0.18.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 +2 -2
- package/dist/index.cjs +142 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -20
- package/dist/index.d.ts +45 -20
- package/dist/index.js +150 -40
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ServiceKey, ElementRegistry } from '@fieldnotes/core';
|
|
2
|
+
import { WireSyncElement, WireSyncOp, LayerRecord, ExtensionKind, TypedExtensionOp, PluginSnapshot } from '@fieldnotes/sync';
|
|
3
3
|
export { PluginSnapshot } from '@fieldnotes/sync';
|
|
4
4
|
import { WebSocketServer } from 'ws';
|
|
5
5
|
import { IncomingMessage, Server } from 'http';
|
|
@@ -18,24 +18,21 @@ declare class InMemoryHubFanout implements HubFanout {
|
|
|
18
18
|
interface HubBackend {
|
|
19
19
|
/** True when every hub instance addresses the same atomic backing state (for example Redis). */
|
|
20
20
|
readonly sharedAcrossInstances?: boolean;
|
|
21
|
-
snapshot(room: string): Promise<
|
|
22
|
-
get(room: string, id: string): Promise<
|
|
23
|
-
apply(room: string, op:
|
|
21
|
+
snapshot(room: string): Promise<WireSyncElement[]>;
|
|
22
|
+
get(room: string, id: string): Promise<WireSyncElement | undefined>;
|
|
23
|
+
apply(room: string, op: WireSyncOp): Promise<void>;
|
|
24
24
|
layerRecords?(room: string): Promise<LayerRecord[]>;
|
|
25
25
|
getLayerRecord?(room: string, id: string): Promise<LayerRecord | undefined>;
|
|
26
26
|
applyLayerRecord?(room: string, record: LayerRecord): Promise<void>;
|
|
27
27
|
getService?<T>(key: ServiceKey<T>): T | undefined;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
type OwnedElement =
|
|
31
|
-
ownerId?: string;
|
|
32
|
-
audience?: string;
|
|
33
|
-
};
|
|
30
|
+
type OwnedElement = WireSyncElement;
|
|
34
31
|
interface AuthorizeContext {
|
|
35
32
|
userId?: string;
|
|
36
33
|
role?: string;
|
|
37
34
|
room: string;
|
|
38
|
-
op:
|
|
35
|
+
op: WireSyncOp;
|
|
39
36
|
currentElement?: OwnedElement;
|
|
40
37
|
}
|
|
41
38
|
type Authorize = (ctx: AuthorizeContext) => boolean | Promise<boolean>;
|
|
@@ -43,7 +40,7 @@ interface AuthorizeLayerContext {
|
|
|
43
40
|
userId?: string;
|
|
44
41
|
role?: string;
|
|
45
42
|
room: string;
|
|
46
|
-
op: Extract<
|
|
43
|
+
op: Extract<WireSyncOp, {
|
|
47
44
|
kind: 'layer-upsert' | 'layer-remove';
|
|
48
45
|
}>;
|
|
49
46
|
/** The hub's current record for the target layer, tombstones included. */
|
|
@@ -65,9 +62,9 @@ interface ReadContext {
|
|
|
65
62
|
type CanRead = (ctx: ReadContext) => boolean;
|
|
66
63
|
|
|
67
64
|
interface ApplyResult {
|
|
68
|
-
readonly accepted:
|
|
69
|
-
readonly corrections:
|
|
70
|
-
readonly broadcast?:
|
|
65
|
+
readonly accepted: WireSyncOp | null;
|
|
66
|
+
readonly corrections: WireSyncOp[];
|
|
67
|
+
readonly broadcast?: WireSyncOp[];
|
|
71
68
|
readonly locality?: 'shared' | 'local';
|
|
72
69
|
}
|
|
73
70
|
interface ServerOpContext {
|
|
@@ -78,7 +75,7 @@ interface ServerOpContext {
|
|
|
78
75
|
readonly backend: HubBackend;
|
|
79
76
|
backendPlugin<T>(key: ServiceKey<T>): T | undefined;
|
|
80
77
|
}
|
|
81
|
-
type ServerNext = (op:
|
|
78
|
+
type ServerNext = (op: WireSyncOp, context: ServerOpContext) => Promise<ApplyResult>;
|
|
82
79
|
interface ServerExtensionRegistry {
|
|
83
80
|
register<TPayload>(kind: ExtensionKind<TPayload>, handler: (op: TypedExtensionOp<TPayload>, context: ServerOpContext) => Promise<ApplyResult>): void;
|
|
84
81
|
}
|
|
@@ -86,8 +83,8 @@ interface ServerSyncPlugin {
|
|
|
86
83
|
readonly name: string;
|
|
87
84
|
readonly ownedLegacyKinds?: readonly string[];
|
|
88
85
|
readonly legacySnapshotKey?: string;
|
|
89
|
-
process?(op:
|
|
90
|
-
applyFanout?(op:
|
|
86
|
+
process?(op: WireSyncOp, context: ServerOpContext, next: ServerNext): Promise<ApplyResult>;
|
|
87
|
+
applyFanout?(op: WireSyncOp, context: ServerOpContext): Promise<WireSyncOp | null>;
|
|
91
88
|
registerExtensionKinds?(registry: ServerExtensionRegistry): void;
|
|
92
89
|
snapshot?(room: string, backend: HubBackend): Promise<PluginSnapshot | undefined>;
|
|
93
90
|
filterSnapshot?(snapshot: PluginSnapshot, viewer: {
|
|
@@ -114,6 +111,8 @@ interface SyncHubOptions {
|
|
|
114
111
|
maxJsonDepth?: number;
|
|
115
112
|
presenceThrottleMs?: number;
|
|
116
113
|
maxPresenceLanes?: number;
|
|
114
|
+
/** Registry used to translate extension elements for legacy peers. */
|
|
115
|
+
elementRegistry?: ElementRegistry;
|
|
117
116
|
}
|
|
118
117
|
declare class SyncHub {
|
|
119
118
|
private readonly backend;
|
|
@@ -127,6 +126,8 @@ declare class SyncHub {
|
|
|
127
126
|
private readonly authorize?;
|
|
128
127
|
private readonly authorizeLayer?;
|
|
129
128
|
private readonly pluginRegistry;
|
|
129
|
+
private readonly elementRegistry;
|
|
130
|
+
private readonly peerCapabilities;
|
|
130
131
|
private readonly canRead?;
|
|
131
132
|
private readonly memoryLayers;
|
|
132
133
|
private readonly maxJsonDepth;
|
|
@@ -173,6 +174,24 @@ declare class SyncHub {
|
|
|
173
174
|
private mayRead;
|
|
174
175
|
private safePublish;
|
|
175
176
|
private relayToRoom;
|
|
177
|
+
/**
|
|
178
|
+
* Translates `op` for the peer and sends it. Returns false when the op is
|
|
179
|
+
* lossy for this peer (no legacy encoding) or the socket throws; neither
|
|
180
|
+
* may reject the room operation that produced it.
|
|
181
|
+
*/
|
|
182
|
+
private sendToConnection;
|
|
183
|
+
/** Returns the wire frame for `op` translated for `capabilities`, or null when lossy. */
|
|
184
|
+
private encodeForPeer;
|
|
185
|
+
/**
|
|
186
|
+
* Normalize registered legacy wire elements before authorization, storage,
|
|
187
|
+
* and relay. A legacy type this hub has no adapter for is forwarded and
|
|
188
|
+
* stored verbatim — the hub is a relay, and the peers decide whether they
|
|
189
|
+
* understand it — so a hub deployed without domain adapters never erases
|
|
190
|
+
* the room's existing elements. Only a malformed registered element is dropped.
|
|
191
|
+
*/
|
|
192
|
+
private normalizeElement;
|
|
193
|
+
private normalizeElements;
|
|
194
|
+
private relayOpToRoom;
|
|
176
195
|
private broadcastClientPresence;
|
|
177
196
|
private clearPresenceLanes;
|
|
178
197
|
private schedulePresence;
|
|
@@ -190,9 +209,9 @@ declare class MemoryHubBackend implements HubBackend {
|
|
|
190
209
|
private roomLayers;
|
|
191
210
|
private room;
|
|
192
211
|
private layers;
|
|
193
|
-
snapshot(room: string): Promise<
|
|
194
|
-
get(room: string, id: string): Promise<
|
|
195
|
-
apply(room: string, op:
|
|
212
|
+
snapshot(room: string): Promise<WireSyncElement[]>;
|
|
213
|
+
get(room: string, id: string): Promise<WireSyncElement | undefined>;
|
|
214
|
+
apply(room: string, op: WireSyncOp): Promise<void>;
|
|
196
215
|
layerRecords(room: string): Promise<LayerRecord[]>;
|
|
197
216
|
getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined>;
|
|
198
217
|
applyLayerRecord(room: string, record: LayerRecord): Promise<void>;
|
|
@@ -229,6 +248,12 @@ interface CreateSyncServerOptions {
|
|
|
229
248
|
presenceThrottleMs?: number;
|
|
230
249
|
maxPresenceLanes?: number;
|
|
231
250
|
shutdownGraceMs?: number;
|
|
251
|
+
/**
|
|
252
|
+
* Registry used to translate extension envelopes for legacy peers. Without
|
|
253
|
+
* it the hub relays unknown legacy element types verbatim and cannot encode
|
|
254
|
+
* envelopes for pre-envelope clients.
|
|
255
|
+
*/
|
|
256
|
+
elementRegistry?: ElementRegistry;
|
|
232
257
|
}
|
|
233
258
|
declare function createSyncServer(options?: CreateSyncServerOptions): {
|
|
234
259
|
hub: SyncHub;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
// src/sync-hub.ts
|
|
2
2
|
import {
|
|
3
|
+
createCurrentCapabilities,
|
|
4
|
+
createLegacyCapabilities,
|
|
3
5
|
parseEnvelope,
|
|
4
6
|
isValidElement,
|
|
5
|
-
|
|
7
|
+
isValidWireElement,
|
|
8
|
+
isNewerLayerRecord,
|
|
9
|
+
translateOpForPeer
|
|
6
10
|
} from "@fieldnotes/sync";
|
|
11
|
+
import { getDefaultElementRegistry } from "@fieldnotes/core";
|
|
7
12
|
|
|
8
13
|
// src/memory-hub-backend.ts
|
|
9
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
applyOpToMap
|
|
16
|
+
} from "@fieldnotes/sync";
|
|
10
17
|
var MemoryHubBackend = class {
|
|
11
18
|
rooms = /* @__PURE__ */ new Map();
|
|
12
19
|
roomLayers = /* @__PURE__ */ new Map();
|
|
@@ -72,8 +79,10 @@ var ServerPluginRegistry = class {
|
|
|
72
79
|
byName = /* @__PURE__ */ new Map();
|
|
73
80
|
legacyOwners = /* @__PURE__ */ new Map();
|
|
74
81
|
extensions = /* @__PURE__ */ new Map();
|
|
82
|
+
definitions;
|
|
75
83
|
constructor(plugins) {
|
|
76
84
|
for (const plugin of plugins) this.register(plugin);
|
|
85
|
+
this.definitions = new Map([...this.extensions].map(([name, entry]) => [name, entry.kind]));
|
|
77
86
|
}
|
|
78
87
|
register(plugin) {
|
|
79
88
|
if (this.byName.has(plugin.name))
|
|
@@ -105,6 +114,12 @@ var ServerPluginRegistry = class {
|
|
|
105
114
|
extension(extensionKind) {
|
|
106
115
|
return this.extensions.get(extensionKind);
|
|
107
116
|
}
|
|
117
|
+
get extensionKinds() {
|
|
118
|
+
return [...this.extensions.keys()];
|
|
119
|
+
}
|
|
120
|
+
get extensionDefinitions() {
|
|
121
|
+
return this.definitions;
|
|
122
|
+
}
|
|
108
123
|
};
|
|
109
124
|
|
|
110
125
|
// src/resource-limits.ts
|
|
@@ -167,7 +182,7 @@ function generateInstanceId() {
|
|
|
167
182
|
function isFanoutOp(op) {
|
|
168
183
|
if (typeof op !== "object" || op === null) return false;
|
|
169
184
|
const o = op;
|
|
170
|
-
if (o.kind === "upsert") return
|
|
185
|
+
if (o.kind === "upsert") return isValidWireElement(o.element);
|
|
171
186
|
if (o.kind === "remove") return typeof o.id === "string";
|
|
172
187
|
return o.kind === "clear";
|
|
173
188
|
}
|
|
@@ -197,6 +212,9 @@ function layerRecordToOp(record) {
|
|
|
197
212
|
editor: record.editor
|
|
198
213
|
} : { kind: "layer-remove", id: record.id, version: record.version, editor: record.editor };
|
|
199
214
|
}
|
|
215
|
+
function capabilityProfile(capabilities) {
|
|
216
|
+
return JSON.stringify([capabilities.elementEnvelope, capabilities.extensionKinds]);
|
|
217
|
+
}
|
|
200
218
|
function isPresenceOp(op) {
|
|
201
219
|
if (typeof op !== "object" || op === null) return false;
|
|
202
220
|
const k = op.kind;
|
|
@@ -216,6 +234,8 @@ var SyncHub = class {
|
|
|
216
234
|
authorize;
|
|
217
235
|
authorizeLayer;
|
|
218
236
|
pluginRegistry;
|
|
237
|
+
elementRegistry;
|
|
238
|
+
peerCapabilities = /* @__PURE__ */ new Map();
|
|
219
239
|
canRead;
|
|
220
240
|
memoryLayers = /* @__PURE__ */ new Map();
|
|
221
241
|
maxJsonDepth;
|
|
@@ -234,6 +254,7 @@ var SyncHub = class {
|
|
|
234
254
|
constructor(options = {}) {
|
|
235
255
|
this.backend = options.backend ?? new MemoryHubBackend();
|
|
236
256
|
this.pluginRegistry = new ServerPluginRegistry(options.plugins ?? []);
|
|
257
|
+
this.elementRegistry = options.elementRegistry ?? getDefaultElementRegistry();
|
|
237
258
|
this.instanceId = options.instanceId ?? generateInstanceId();
|
|
238
259
|
this.fanout = options.fanout ?? new InMemoryHubFanout();
|
|
239
260
|
this.authorize = options.authorize;
|
|
@@ -261,6 +282,7 @@ var SyncHub = class {
|
|
|
261
282
|
const conn = this.conns.get(connId);
|
|
262
283
|
if (!conn) return;
|
|
263
284
|
this.conns.delete(connId);
|
|
285
|
+
this.peerCapabilities.delete(connId);
|
|
264
286
|
const room = conn.room;
|
|
265
287
|
const hadPresence = this.presenceConnections.delete(connId);
|
|
266
288
|
this.clearPresenceLanes(connId);
|
|
@@ -294,6 +316,14 @@ var SyncHub = class {
|
|
|
294
316
|
if (!hasJsonDepthAtMost(message, this.maxJsonDepth)) return Promise.resolve();
|
|
295
317
|
const env = parseEnvelope(message);
|
|
296
318
|
if (!env) return Promise.resolve();
|
|
319
|
+
if (env.op.kind === "capabilities") {
|
|
320
|
+
this.peerCapabilities.set(conn.id, env.op.capabilities);
|
|
321
|
+
this.sendToConnection(conn, HUB_FROM, {
|
|
322
|
+
kind: "capabilities",
|
|
323
|
+
capabilities: createCurrentCapabilities(this.pluginRegistry.extensionKinds)
|
|
324
|
+
});
|
|
325
|
+
return Promise.resolve();
|
|
326
|
+
}
|
|
297
327
|
if (env.op.kind === "presence") {
|
|
298
328
|
this.schedulePresence(conn, env.op.data);
|
|
299
329
|
return Promise.resolve();
|
|
@@ -309,9 +339,14 @@ var SyncHub = class {
|
|
|
309
339
|
return operation;
|
|
310
340
|
}
|
|
311
341
|
async process(conn, env) {
|
|
312
|
-
|
|
342
|
+
let op = env.op;
|
|
343
|
+
if (op.kind === "upsert") {
|
|
344
|
+
const element = this.normalizeElement(op.element);
|
|
345
|
+
if (!element) return;
|
|
346
|
+
op = { ...op, element };
|
|
347
|
+
}
|
|
313
348
|
if (op.kind === "request-snapshot") {
|
|
314
|
-
const all = await this.backend.snapshot(conn.room);
|
|
349
|
+
const all = this.normalizeElements(await this.backend.snapshot(conn.room));
|
|
315
350
|
const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;
|
|
316
351
|
const layers = await this.getLayerRecords(conn.room);
|
|
317
352
|
const snapshotOp = {
|
|
@@ -332,7 +367,7 @@ var SyncHub = class {
|
|
|
332
367
|
else extensions[plugin.name] = snapshot;
|
|
333
368
|
}
|
|
334
369
|
if (Object.keys(extensions).length > 0) snapshotOp["extensions"] = extensions;
|
|
335
|
-
|
|
370
|
+
this.sendToConnection(conn, HUB_FROM, snapshotOp);
|
|
336
371
|
} else if (op.kind === "layer-upsert" || op.kind === "layer-remove") {
|
|
337
372
|
await this.processLayerOp(conn, op);
|
|
338
373
|
} else if (op.kind === "extension") {
|
|
@@ -350,7 +385,8 @@ var SyncHub = class {
|
|
|
350
385
|
} else if (op.kind === "upsert" || op.kind === "remove" || op.kind === "clear") {
|
|
351
386
|
const id = op.kind === "upsert" ? op.element.id : op.kind === "remove" ? op.id : void 0;
|
|
352
387
|
const needCurrent = (this.authorize || this.canRead) && id !== void 0;
|
|
353
|
-
const
|
|
388
|
+
const storedCurrent = needCurrent ? await this.backend.get(conn.room, id) : void 0;
|
|
389
|
+
const current = storedCurrent ? this.normalizeElement(storedCurrent) ?? void 0 : void 0;
|
|
354
390
|
let outboundOp = op;
|
|
355
391
|
if (this.authorize) {
|
|
356
392
|
const allowed = await this.authorize({
|
|
@@ -374,7 +410,7 @@ var SyncHub = class {
|
|
|
374
410
|
const prevAudience = current?.audience;
|
|
375
411
|
const result = await this.runCorePlugins(conn, outboundOp);
|
|
376
412
|
for (const correction of result.corrections) {
|
|
377
|
-
|
|
413
|
+
this.sendToConnection(conn, HUB_FROM, correction);
|
|
378
414
|
}
|
|
379
415
|
const accepted = result.accepted;
|
|
380
416
|
if (accepted && (accepted.kind === "upsert" || accepted.kind === "remove" || accepted.kind === "clear")) {
|
|
@@ -430,7 +466,7 @@ var SyncHub = class {
|
|
|
430
466
|
}
|
|
431
467
|
async deliverPluginResult(conn, result) {
|
|
432
468
|
for (const correction of result.corrections) {
|
|
433
|
-
|
|
469
|
+
this.sendToConnection(conn, HUB_FROM, correction);
|
|
434
470
|
}
|
|
435
471
|
if (result.accepted) await this.publishPluginOp(conn, result.accepted, result.locality);
|
|
436
472
|
for (const broadcast of result.broadcast ?? []) {
|
|
@@ -443,7 +479,7 @@ var SyncHub = class {
|
|
|
443
479
|
JSON.stringify({ o: this.instanceId, room: conn.room, from: conn.id, op })
|
|
444
480
|
);
|
|
445
481
|
}
|
|
446
|
-
this.
|
|
482
|
+
this.relayOpToRoom(conn.room, conn.id, conn.id, op);
|
|
447
483
|
}
|
|
448
484
|
/**
|
|
449
485
|
* Applies a layer-definition edit on the room's serial queue. Convergence is
|
|
@@ -465,19 +501,19 @@ var SyncHub = class {
|
|
|
465
501
|
});
|
|
466
502
|
if (!allowed) {
|
|
467
503
|
const correction = current ?? { id: record.id, version: record.version, editor: HUB_FROM };
|
|
468
|
-
|
|
504
|
+
this.sendToConnection(conn, HUB_FROM, layerRecordToOp(correction));
|
|
469
505
|
return;
|
|
470
506
|
}
|
|
471
507
|
}
|
|
472
508
|
if (current && !isNewerLayerRecord(record, current)) {
|
|
473
|
-
|
|
509
|
+
this.sendToConnection(conn, HUB_FROM, layerRecordToOp(current));
|
|
474
510
|
return;
|
|
475
511
|
}
|
|
476
512
|
await this.applyLayerRecord(conn.room, record);
|
|
477
513
|
await this.fanout.publish(
|
|
478
514
|
JSON.stringify({ o: this.instanceId, room: conn.room, from: conn.id, op })
|
|
479
515
|
);
|
|
480
|
-
this.
|
|
516
|
+
this.relayOpToRoom(conn.room, conn.id, conn.id, op);
|
|
481
517
|
}
|
|
482
518
|
layerBackend() {
|
|
483
519
|
const { layerRecords, getLayerRecord, applyLayerRecord } = this.backend;
|
|
@@ -538,6 +574,86 @@ var SyncHub = class {
|
|
|
538
574
|
}
|
|
539
575
|
return sent;
|
|
540
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* Translates `op` for the peer and sends it. Returns false when the op is
|
|
579
|
+
* lossy for this peer (no legacy encoding) or the socket throws; neither
|
|
580
|
+
* may reject the room operation that produced it.
|
|
581
|
+
*/
|
|
582
|
+
sendToConnection(conn, from, op, encoded) {
|
|
583
|
+
const capabilities = this.peerCapabilities.get(conn.id) ?? createLegacyCapabilities();
|
|
584
|
+
const profile = encoded ? capabilityProfile(capabilities) : void 0;
|
|
585
|
+
let message = profile === void 0 ? void 0 : encoded?.get(profile);
|
|
586
|
+
if (message === void 0) {
|
|
587
|
+
message = this.encodeForPeer(from, op, capabilities);
|
|
588
|
+
if (profile !== void 0) encoded?.set(profile, message);
|
|
589
|
+
}
|
|
590
|
+
if (message === null) return false;
|
|
591
|
+
try {
|
|
592
|
+
conn.send(message);
|
|
593
|
+
return true;
|
|
594
|
+
} catch {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/** Returns the wire frame for `op` translated for `capabilities`, or null when lossy. */
|
|
599
|
+
encodeForPeer(from, op, capabilities) {
|
|
600
|
+
try {
|
|
601
|
+
const translated = translateOpForPeer(
|
|
602
|
+
op,
|
|
603
|
+
capabilities,
|
|
604
|
+
this.elementRegistry,
|
|
605
|
+
this.pluginRegistry.extensionDefinitions
|
|
606
|
+
);
|
|
607
|
+
return JSON.stringify({ from, op: translated });
|
|
608
|
+
} catch {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Normalize registered legacy wire elements before authorization, storage,
|
|
614
|
+
* and relay. A legacy type this hub has no adapter for is forwarded and
|
|
615
|
+
* stored verbatim — the hub is a relay, and the peers decide whether they
|
|
616
|
+
* understand it — so a hub deployed without domain adapters never erases
|
|
617
|
+
* the room's existing elements. Only a malformed registered element is dropped.
|
|
618
|
+
*/
|
|
619
|
+
normalizeElement(element) {
|
|
620
|
+
if (isValidElement(element)) return element;
|
|
621
|
+
const adapter = this.elementRegistry.getAdapterByLegacyType(element.type);
|
|
622
|
+
if (!adapter) return element;
|
|
623
|
+
try {
|
|
624
|
+
const raw = Object.fromEntries(Object.entries(element));
|
|
625
|
+
const envelope = adapter.decodeLegacy(raw);
|
|
626
|
+
if (!adapter.validateEnvelope(envelope)) return null;
|
|
627
|
+
return {
|
|
628
|
+
...envelope,
|
|
629
|
+
...typeof raw["audience"] === "string" ? { audience: raw["audience"] } : {},
|
|
630
|
+
...typeof raw["ownerId"] === "string" ? { ownerId: raw["ownerId"] } : {}
|
|
631
|
+
};
|
|
632
|
+
} catch {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
normalizeElements(elements) {
|
|
637
|
+
const normalized = [];
|
|
638
|
+
for (const element of elements) {
|
|
639
|
+
const runtime = this.normalizeElement(element);
|
|
640
|
+
if (runtime) normalized.push(runtime);
|
|
641
|
+
}
|
|
642
|
+
return normalized;
|
|
643
|
+
}
|
|
644
|
+
relayOpToRoom(room, excludeId, from, op) {
|
|
645
|
+
const members = this.rooms.get(room);
|
|
646
|
+
if (!members) return 0;
|
|
647
|
+
let sent = 0;
|
|
648
|
+
const encoded = /* @__PURE__ */ new Map();
|
|
649
|
+
for (const connectionId of members) {
|
|
650
|
+
if (connectionId === excludeId) continue;
|
|
651
|
+
const conn = this.conns.get(connectionId);
|
|
652
|
+
if (!conn) continue;
|
|
653
|
+
if (this.sendToConnection(conn, from, op, encoded)) sent += 1;
|
|
654
|
+
}
|
|
655
|
+
return sent;
|
|
656
|
+
}
|
|
541
657
|
broadcastClientPresence(conn, data) {
|
|
542
658
|
this.presenceConnections.add(conn.id);
|
|
543
659
|
const message = JSON.stringify({ from: conn.id, op: { kind: "presence", data } });
|
|
@@ -619,41 +735,33 @@ var SyncHub = class {
|
|
|
619
735
|
deliverToRoom(room, excludeId, from, op, prevAudience, prevExisted) {
|
|
620
736
|
const members = this.rooms.get(room);
|
|
621
737
|
if (!members) return;
|
|
622
|
-
const
|
|
623
|
-
try {
|
|
624
|
-
conn.send(msg);
|
|
625
|
-
} catch {
|
|
626
|
-
}
|
|
627
|
-
};
|
|
738
|
+
const encoded = /* @__PURE__ */ new Map();
|
|
628
739
|
if (op.kind === "upsert") {
|
|
629
740
|
const audience = op.element.audience;
|
|
630
|
-
const
|
|
631
|
-
const
|
|
632
|
-
from: HUB_FROM,
|
|
633
|
-
op: { kind: "remove", id: op.element.id }
|
|
634
|
-
});
|
|
741
|
+
const removeOp = { kind: "remove", id: op.element.id };
|
|
742
|
+
const encodedRemove = /* @__PURE__ */ new Map();
|
|
635
743
|
for (const cid of members) {
|
|
636
744
|
if (cid === excludeId) continue;
|
|
637
745
|
const conn = this.conns.get(cid);
|
|
638
746
|
if (!conn) continue;
|
|
639
|
-
if (this.mayRead(conn, audience))
|
|
640
|
-
else if (prevExisted && this.mayRead(conn, prevAudience))
|
|
747
|
+
if (this.mayRead(conn, audience)) this.sendToConnection(conn, from, op, encoded);
|
|
748
|
+
else if (prevExisted && this.mayRead(conn, prevAudience)) {
|
|
749
|
+
this.sendToConnection(conn, HUB_FROM, removeOp, encodedRemove);
|
|
750
|
+
}
|
|
641
751
|
}
|
|
642
752
|
} else if (op.kind === "remove") {
|
|
643
|
-
const removeMsg = JSON.stringify({ from, op });
|
|
644
753
|
for (const cid of members) {
|
|
645
754
|
if (cid === excludeId) continue;
|
|
646
755
|
const conn = this.conns.get(cid);
|
|
647
756
|
if (!conn) continue;
|
|
648
757
|
const wasVisible = !this.canRead || prevExisted && this.mayRead(conn, prevAudience);
|
|
649
|
-
if (wasVisible)
|
|
758
|
+
if (wasVisible) this.sendToConnection(conn, from, op, encoded);
|
|
650
759
|
}
|
|
651
760
|
} else if (op.kind === "clear") {
|
|
652
|
-
const clearMsg = JSON.stringify({ from, op });
|
|
653
761
|
for (const cid of members) {
|
|
654
762
|
if (cid === excludeId) continue;
|
|
655
763
|
const conn = this.conns.get(cid);
|
|
656
|
-
if (conn)
|
|
764
|
+
if (conn) this.sendToConnection(conn, from, op, encoded);
|
|
657
765
|
}
|
|
658
766
|
}
|
|
659
767
|
}
|
|
@@ -664,11 +772,11 @@ var SyncHub = class {
|
|
|
664
772
|
} else if (op.kind === "remove") {
|
|
665
773
|
correction = current ? this.mayRead(conn, current.audience) ? { kind: "upsert", element: current } : { kind: "remove", id: current.id } : void 0;
|
|
666
774
|
} else if (op.kind === "clear") {
|
|
667
|
-
const all = await this.backend.snapshot(conn.room);
|
|
775
|
+
const all = this.normalizeElements(await this.backend.snapshot(conn.room));
|
|
668
776
|
const elements = this.canRead ? all.filter((el) => this.mayRead(conn, el.audience)) : all;
|
|
669
777
|
correction = { kind: "snapshot", to: from, elements };
|
|
670
778
|
}
|
|
671
|
-
if (correction)
|
|
779
|
+
if (correction) this.sendToConnection(conn, HUB_FROM, correction);
|
|
672
780
|
}
|
|
673
781
|
onFanout(payload) {
|
|
674
782
|
let env;
|
|
@@ -688,7 +796,7 @@ var SyncHub = class {
|
|
|
688
796
|
if (isLayerOp(op)) {
|
|
689
797
|
void this.applyFanoutLayerOp(env.room, op).catch(() => {
|
|
690
798
|
});
|
|
691
|
-
this.
|
|
799
|
+
this.relayOpToRoom(env.room, void 0, env.from, op);
|
|
692
800
|
return;
|
|
693
801
|
}
|
|
694
802
|
const plugin = typeof op === "object" && op !== null && op.kind === "extension" ? this.pluginRegistry.extension(op.extensionKind ?? "")?.plugin : typeof op === "object" && op !== null && typeof op.kind === "string" ? this.pluginRegistry.ownerOf(op.kind) : void 0;
|
|
@@ -703,11 +811,7 @@ var SyncHub = class {
|
|
|
703
811
|
};
|
|
704
812
|
const accepted = plugin.applyFanout ? await plugin.applyFanout(op, context) : op;
|
|
705
813
|
if (accepted) {
|
|
706
|
-
this.
|
|
707
|
-
env.room,
|
|
708
|
-
void 0,
|
|
709
|
-
JSON.stringify({ from: env.from, op: accepted })
|
|
710
|
-
);
|
|
814
|
+
this.relayOpToRoom(env.room, void 0, env.from, accepted);
|
|
711
815
|
}
|
|
712
816
|
});
|
|
713
817
|
this.roomQueues.set(
|
|
@@ -718,9 +822,14 @@ var SyncHub = class {
|
|
|
718
822
|
return;
|
|
719
823
|
}
|
|
720
824
|
if (!isFanoutOp(op)) return;
|
|
825
|
+
const runtimeOp = op.kind === "upsert" ? (() => {
|
|
826
|
+
const element = this.normalizeElement(op.element);
|
|
827
|
+
return element ? { ...op, element } : null;
|
|
828
|
+
})() : op;
|
|
829
|
+
if (!runtimeOp) return;
|
|
721
830
|
const prevAudience = typeof env.prev === "string" ? env.prev : void 0;
|
|
722
831
|
const prevExisted = env.existed === true;
|
|
723
|
-
this.deliverToRoom(env.room, void 0, env.from,
|
|
832
|
+
this.deliverToRoom(env.room, void 0, env.from, runtimeOp, prevAudience, prevExisted);
|
|
724
833
|
}
|
|
725
834
|
async applyFanoutLayerOp(room, op) {
|
|
726
835
|
const record = layerOpToRecord(op);
|
|
@@ -822,7 +931,8 @@ function createSyncServer(options = {}) {
|
|
|
822
931
|
canRead: options.canRead,
|
|
823
932
|
maxJsonDepth: options.maxJsonDepth ?? DEFAULT_MAX_JSON_DEPTH,
|
|
824
933
|
presenceThrottleMs: options.presenceThrottleMs ?? DEFAULT_PRESENCE_THROTTLE_MS,
|
|
825
|
-
maxPresenceLanes: options.maxPresenceLanes
|
|
934
|
+
maxPresenceLanes: options.maxPresenceLanes,
|
|
935
|
+
elementRegistry: options.elementRegistry
|
|
826
936
|
});
|
|
827
937
|
const maxMessageBytes = options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
|
|
828
938
|
const wss = options.server ? new WebSocketServer({ server: options.server, maxPayload: maxMessageBytes }) : new WebSocketServer({ port: options.port ?? 0, maxPayload: maxMessageBytes });
|