@adobe/data-sync 0.9.90
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/LICENSE +21 -0
- package/README.md +646 -0
- package/dist/create-sync-server.d.ts +29 -0
- package/dist/create-sync-server.js +156 -0
- package/dist/create-sync-server.js.map +1 -0
- package/dist/create-sync-service.d.ts +120 -0
- package/dist/create-sync-service.js +192 -0
- package/dist/create-sync-service.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/keepalive.test.d.ts +1 -0
- package/dist/keepalive.test.js +161 -0
- package/dist/keepalive.test.js.map +1 -0
- package/dist/loopback-transport.d.ts +20 -0
- package/dist/loopback-transport.js +98 -0
- package/dist/loopback-transport.js.map +1 -0
- package/dist/sync.reconnect.test.d.ts +1 -0
- package/dist/sync.reconnect.test.js +248 -0
- package/dist/sync.reconnect.test.js.map +1 -0
- package/dist/sync.test.d.ts +1 -0
- package/dist/sync.test.js +233 -0
- package/dist/sync.test.js.map +1 -0
- package/dist/transport-onclose.test.d.ts +1 -0
- package/dist/transport-onclose.test.js +74 -0
- package/dist/transport-onclose.test.js.map +1 -0
- package/dist/transport.d.ts +94 -0
- package/dist/transport.js +3 -0
- package/dist/transport.js.map +1 -0
- package/dist/websocket-transport.d.ts +71 -0
- package/dist/websocket-transport.js +148 -0
- package/dist/websocket-transport.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
/**
|
|
3
|
+
* Creates a sync server that accepts client proposals, assigns canonical
|
|
4
|
+
* commit timestamps, and broadcasts committed envelopes to all connected
|
|
5
|
+
* clients.
|
|
6
|
+
*
|
|
7
|
+
* The server is intentionally minimal:
|
|
8
|
+
* - It does **not** maintain a database — it is a pure ordering service.
|
|
9
|
+
* - Proposals are accepted in FIFO order (first-come-first-served).
|
|
10
|
+
* - No authentication or validation is performed here; layer those on top via
|
|
11
|
+
* a middleware pattern (intercept `propose` messages before passing to
|
|
12
|
+
* `SyncServer`).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const server = createSyncServer();
|
|
17
|
+
* // When a WebSocket opens:
|
|
18
|
+
* const disconnect = server.connect(webSocketServerTransport);
|
|
19
|
+
* // When the WebSocket closes:
|
|
20
|
+
* disconnect();
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
const generateId = () => typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
|
24
|
+
? crypto.randomUUID()
|
|
25
|
+
: Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
26
|
+
export const createSyncServer = (options = {}) => {
|
|
27
|
+
const { livenessTimeoutMs = 25_000, logger } = options;
|
|
28
|
+
const log = logger ?? (() => undefined);
|
|
29
|
+
const sessionId = generateId();
|
|
30
|
+
log(`session ${sessionId} starting`);
|
|
31
|
+
const clients = new Set();
|
|
32
|
+
const unsubscribers = new Map();
|
|
33
|
+
const lastInboundAt = new Map();
|
|
34
|
+
// Ordered log of every committed envelope. Replayed to new clients on
|
|
35
|
+
// connect so they can reconstruct the current committed state without a
|
|
36
|
+
// separate snapshot mechanism.
|
|
37
|
+
const committedLog = [];
|
|
38
|
+
let nextTime = 1;
|
|
39
|
+
const handleMessage = (origin, msg) => {
|
|
40
|
+
// Record liveness for any inbound message — pings are explicit, but a
|
|
41
|
+
// chatty client that's actively sending proposals also counts as alive.
|
|
42
|
+
lastInboundAt.set(origin, Date.now());
|
|
43
|
+
if (msg.kind === "ping") {
|
|
44
|
+
origin.send({ kind: "pong" });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (msg.kind === "hello") {
|
|
48
|
+
const clientSessionId = msg.sessionId;
|
|
49
|
+
const lastAppliedTime = msg.lastAppliedTime;
|
|
50
|
+
const maxServerTime = committedLog.length > 0 ? committedLog[committedLog.length - 1].time : 0;
|
|
51
|
+
// A fresh client (no prior session id) always gets the full log with
|
|
52
|
+
// resetRequired:false — its database is empty so no reset is needed.
|
|
53
|
+
// A returning client with a mismatched session or a watermark ahead
|
|
54
|
+
// of the server must reset and replay from scratch.
|
|
55
|
+
const sessionMismatch = clientSessionId !== undefined && clientSessionId !== sessionId;
|
|
56
|
+
const clientAhead = lastAppliedTime > maxServerTime;
|
|
57
|
+
const resetRequired = sessionMismatch || clientAhead;
|
|
58
|
+
const replayCount = committedLog.filter(e => e.time > (resetRequired ? 0 : lastAppliedTime)).length;
|
|
59
|
+
log(`hello in (priorSession=${clientSessionId ?? "<fresh>"}, watermark=${lastAppliedTime}) → welcome (resetRequired=${resetRequired}, replaying=${replayCount})`);
|
|
60
|
+
origin.send({ kind: "welcome", sessionId, resetRequired });
|
|
61
|
+
// Replay the tail the client is missing (or the full log if reset needed).
|
|
62
|
+
const fromTime = resetRequired ? 0 : lastAppliedTime;
|
|
63
|
+
for (const envelope of committedLog) {
|
|
64
|
+
if (envelope.time > fromTime) {
|
|
65
|
+
origin.send({ kind: "committed", envelope });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (msg.kind === "propose") {
|
|
70
|
+
const committed = {
|
|
71
|
+
...msg.envelope,
|
|
72
|
+
time: nextTime++,
|
|
73
|
+
};
|
|
74
|
+
committedLog.push(committed);
|
|
75
|
+
log(`propose in (id=${msg.envelope.id}, name=${msg.envelope.name}) → committed at time=${committed.time}, broadcast to ${clients.size}`);
|
|
76
|
+
// Echo to ALL clients (including proposer, which promotes its transient).
|
|
77
|
+
for (const c of clients) {
|
|
78
|
+
c.send({ kind: "committed", envelope: committed });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (msg.kind === "cancel") {
|
|
82
|
+
log(`cancel in (id=${msg.id}) → broadcast to ${clients.size}`);
|
|
83
|
+
for (const c of clients) {
|
|
84
|
+
c.send({ kind: "cancelled", id: msg.id });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (msg.kind === "intermediate") {
|
|
88
|
+
// Speculative intermediate — relay to peers, never logged.
|
|
89
|
+
for (const c of clients) {
|
|
90
|
+
if (c !== origin)
|
|
91
|
+
c.send({ kind: "committed", envelope: { ...msg.envelope } });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const connect = (transport) => {
|
|
96
|
+
// Add to the live set immediately so live commits during the replay
|
|
97
|
+
// window are forwarded to this client too.
|
|
98
|
+
clients.add(transport);
|
|
99
|
+
lastInboundAt.set(transport, Date.now());
|
|
100
|
+
log(`client connected (now ${clients.size} client${clients.size === 1 ? "" : "s"})`);
|
|
101
|
+
// Do NOT replay history here — wait for the client's `hello` which
|
|
102
|
+
// tells us what tail (if any) it already has.
|
|
103
|
+
const unsub = transport.onMessage(msg => handleMessage(transport, msg));
|
|
104
|
+
unsubscribers.set(transport, [unsub]);
|
|
105
|
+
return () => {
|
|
106
|
+
for (const fn of unsubscribers.get(transport) ?? [])
|
|
107
|
+
fn();
|
|
108
|
+
unsubscribers.delete(transport);
|
|
109
|
+
clients.delete(transport);
|
|
110
|
+
lastInboundAt.delete(transport);
|
|
111
|
+
transport.close();
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
// Per-client liveness sweep. We close transports that haven't sent any
|
|
115
|
+
// inbound traffic for longer than livenessTimeoutMs; closing fires the
|
|
116
|
+
// host-side onClose listeners so the negotiation layer can drive its
|
|
117
|
+
// disconnect/reconnect flow.
|
|
118
|
+
let livenessTimer;
|
|
119
|
+
if (livenessTimeoutMs > 0) {
|
|
120
|
+
const checkInterval = Math.max(50, Math.floor(livenessTimeoutMs / 4));
|
|
121
|
+
livenessTimer = setInterval(() => {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
for (const transport of clients) {
|
|
124
|
+
const last = lastInboundAt.get(transport) ?? now;
|
|
125
|
+
if (now - last > livenessTimeoutMs) {
|
|
126
|
+
log(`client liveness timeout — disconnecting (${now - last}ms silent)`);
|
|
127
|
+
// close() triggers our own onClose paths via the transport
|
|
128
|
+
// adapter, but we also clean the local maps eagerly.
|
|
129
|
+
clients.delete(transport);
|
|
130
|
+
lastInboundAt.delete(transport);
|
|
131
|
+
for (const fn of unsubscribers.get(transport) ?? [])
|
|
132
|
+
fn();
|
|
133
|
+
unsubscribers.delete(transport);
|
|
134
|
+
transport.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}, checkInterval);
|
|
138
|
+
}
|
|
139
|
+
const dispose = () => {
|
|
140
|
+
log(`dispose (${clients.size} client${clients.size === 1 ? "" : "s"})`);
|
|
141
|
+
if (livenessTimer !== undefined) {
|
|
142
|
+
clearInterval(livenessTimer);
|
|
143
|
+
livenessTimer = undefined;
|
|
144
|
+
}
|
|
145
|
+
for (const transport of clients) {
|
|
146
|
+
for (const fn of unsubscribers.get(transport) ?? [])
|
|
147
|
+
fn();
|
|
148
|
+
transport.close();
|
|
149
|
+
}
|
|
150
|
+
clients.clear();
|
|
151
|
+
unsubscribers.clear();
|
|
152
|
+
lastInboundAt.clear();
|
|
153
|
+
};
|
|
154
|
+
return { sessionId, connect, dispose };
|
|
155
|
+
};
|
|
156
|
+
//# sourceMappingURL=create-sync-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-sync-server.js","sourceRoot":"","sources":["../src/create-sync-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { type Database } from "@adobe/data/ecs";
|
|
2
|
+
import type { ClientTransport } from "./transport.js";
|
|
3
|
+
/**
|
|
4
|
+
* Options for {@link createSyncService}.
|
|
5
|
+
*/
|
|
6
|
+
export interface SyncServiceOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The database to wire into the sync layer. Must have been created with
|
|
9
|
+
* `createRebaseReplayConcurrency(userId)` as its concurrency strategy,
|
|
10
|
+
* where `userId` is unique per peer — the reconciler's transient queue
|
|
11
|
+
* uses `(userId, id)` as its compound key to keep concurrent peers from
|
|
12
|
+
* colliding, and the deferred-commit semantics are required for cross-peer
|
|
13
|
+
* entity-id determinism.
|
|
14
|
+
*/
|
|
15
|
+
readonly database: Database<any, any, any, any>;
|
|
16
|
+
/** Transport connecting this peer to the sync server. */
|
|
17
|
+
readonly transport: ClientTransport;
|
|
18
|
+
/**
|
|
19
|
+
* Maximum `"intermediate"` messages forwarded per second. Intermediate
|
|
20
|
+
* samples beyond this rate are silently dropped. Commits and cancels
|
|
21
|
+
* are always forwarded immediately.
|
|
22
|
+
*
|
|
23
|
+
* Defaults to 20 (one per ~50 ms).
|
|
24
|
+
*/
|
|
25
|
+
readonly maxIntermediatesPerSecond?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Session id received from a previous connection's `welcome` message.
|
|
28
|
+
* Omit (or pass `undefined`) for a fresh client. If the server echoes back
|
|
29
|
+
* the same id in `welcome`, it replays only the tail; otherwise it signals
|
|
30
|
+
* a full reset and replay.
|
|
31
|
+
*/
|
|
32
|
+
readonly priorSessionId?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Highest committed `time` already applied to `database` from a previous
|
|
35
|
+
* connection. The server uses this as the replay start point when the
|
|
36
|
+
* session matches. Defaults to `0` (full replay).
|
|
37
|
+
*/
|
|
38
|
+
readonly initialWatermark?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Called once when the server's `welcome` message arrives, before any
|
|
41
|
+
* replay envelopes are applied. Use this to call `database.reset()` when
|
|
42
|
+
* `resetRequired` is true. Because it runs before the replay buffer is
|
|
43
|
+
* flushed, it is safe to reset the database inside this callback.
|
|
44
|
+
*/
|
|
45
|
+
readonly onWelcome?: (msg: {
|
|
46
|
+
sessionId: string;
|
|
47
|
+
resetRequired: boolean;
|
|
48
|
+
}) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Interval between outbound `ping` keep-alives, in milliseconds. Pings
|
|
51
|
+
* keep intermediate NAT mappings warm and let the peer detect a silent
|
|
52
|
+
* drop faster than the underlying transport's own heartbeat. Pass `0` to
|
|
53
|
+
* disable ping sending (the service will still observe inbound traffic
|
|
54
|
+
* for liveness if `livenessTimeoutMs > 0`). Defaults to `10_000`.
|
|
55
|
+
*/
|
|
56
|
+
readonly pingIntervalMs?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Maximum gap between inbound messages before the transport is considered
|
|
59
|
+
* dead. When exceeded the service calls `transport.close()`, which fires
|
|
60
|
+
* the transport's `onClose` listeners — application code typically wires
|
|
61
|
+
* those to a reconnect flow. Pass `0` to disable liveness checking.
|
|
62
|
+
* Defaults to `25_000`.
|
|
63
|
+
*/
|
|
64
|
+
readonly livenessTimeoutMs?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Optional sink for human-readable lifecycle messages — handshake,
|
|
67
|
+
* commits, cancels, liveness events, dispose. Pings/pongs are
|
|
68
|
+
* intentionally not logged (too noisy). The library itself never imports
|
|
69
|
+
* `console`, so consumers that want visibility wire one up explicitly:
|
|
70
|
+
*
|
|
71
|
+
* logger: (m) => console.log(`[sync] ${m}`)
|
|
72
|
+
*/
|
|
73
|
+
readonly logger?: (msg: string) => void;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Background service returned by {@link createSyncService}. It owns no public
|
|
77
|
+
* mutation surface — all mutations flow through `db.transactions.X(args)`.
|
|
78
|
+
*/
|
|
79
|
+
export interface SyncService {
|
|
80
|
+
/** Stop forwarding, unsubscribe from the transport, and close it. */
|
|
81
|
+
readonly dispose: () => void;
|
|
82
|
+
/** The highest committed `time` applied to the database so far. */
|
|
83
|
+
readonly lastAppliedTime: () => number;
|
|
84
|
+
/** The session id received from the server's `welcome` message, or `undefined` if not yet connected. */
|
|
85
|
+
readonly sessionId: () => string | undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Wires a {@link Database} to a {@link ClientTransport} so that every
|
|
89
|
+
* locally-initiated transaction is automatically replicated to peers, and
|
|
90
|
+
* every server-broadcast envelope is automatically applied to the local DB.
|
|
91
|
+
*
|
|
92
|
+
* The service is a pure subscriber — application code only ever calls
|
|
93
|
+
* `db.transactions.X(args)`. It mirrors the shape of
|
|
94
|
+
* `createStoragePersistenceService`: side-effect lifecycle attached to a DB.
|
|
95
|
+
*
|
|
96
|
+
* Forwarding rules:
|
|
97
|
+
* - `db.observe.envelopes` fires once per locally-initiated envelope (the
|
|
98
|
+
* wrapper notifies it after `applyEnvelope` returns). Replays inside the
|
|
99
|
+
* reconciler and inbound `db.apply()` calls do NOT fire it, so there is
|
|
100
|
+
* no bounce-back to filter.
|
|
101
|
+
* - Envelopes whose `TransactionResult.persistent === false` (i.e. they only
|
|
102
|
+
* touched non-persistent resources/entities) are skipped. UI-only state lives
|
|
103
|
+
* inside the same database without ever reaching the wire.
|
|
104
|
+
* - `time > 0` → `kind: "propose"` (reliable).
|
|
105
|
+
* - `time < 0` → `kind: "intermediate"` (rate-limited, lossy).
|
|
106
|
+
* - `time === 0` → `kind: "cancel"` (reliable).
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* const db = Database.create(myPlugin, {
|
|
111
|
+
* concurrency: createRebaseReplayConcurrency(crypto.randomUUID()),
|
|
112
|
+
* });
|
|
113
|
+
* const sync = createSyncService({ database: db, transport: ws });
|
|
114
|
+
*
|
|
115
|
+
* db.transactions.spawnUnit({ x: 0, y: 0, hp: 100 });
|
|
116
|
+
*
|
|
117
|
+
* sync.dispose();
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export declare const createSyncService: (options: SyncServiceOptions) => SyncService;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { Entity } from "@adobe/data/ecs";
|
|
3
|
+
/**
|
|
4
|
+
* Wires a {@link Database} to a {@link ClientTransport} so that every
|
|
5
|
+
* locally-initiated transaction is automatically replicated to peers, and
|
|
6
|
+
* every server-broadcast envelope is automatically applied to the local DB.
|
|
7
|
+
*
|
|
8
|
+
* The service is a pure subscriber — application code only ever calls
|
|
9
|
+
* `db.transactions.X(args)`. It mirrors the shape of
|
|
10
|
+
* `createStoragePersistenceService`: side-effect lifecycle attached to a DB.
|
|
11
|
+
*
|
|
12
|
+
* Forwarding rules:
|
|
13
|
+
* - `db.observe.envelopes` fires once per locally-initiated envelope (the
|
|
14
|
+
* wrapper notifies it after `applyEnvelope` returns). Replays inside the
|
|
15
|
+
* reconciler and inbound `db.apply()` calls do NOT fire it, so there is
|
|
16
|
+
* no bounce-back to filter.
|
|
17
|
+
* - Envelopes whose `TransactionResult.persistent === false` (i.e. they only
|
|
18
|
+
* touched non-persistent resources/entities) are skipped. UI-only state lives
|
|
19
|
+
* inside the same database without ever reaching the wire.
|
|
20
|
+
* - `time > 0` → `kind: "propose"` (reliable).
|
|
21
|
+
* - `time < 0` → `kind: "intermediate"` (rate-limited, lossy).
|
|
22
|
+
* - `time === 0` → `kind: "cancel"` (reliable).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const db = Database.create(myPlugin, {
|
|
27
|
+
* concurrency: createRebaseReplayConcurrency(crypto.randomUUID()),
|
|
28
|
+
* });
|
|
29
|
+
* const sync = createSyncService({ database: db, transport: ws });
|
|
30
|
+
*
|
|
31
|
+
* db.transactions.spawnUnit({ x: 0, y: 0, hp: 100 });
|
|
32
|
+
*
|
|
33
|
+
* sync.dispose();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const createSyncService = (options) => {
|
|
37
|
+
const { database, transport, maxIntermediatesPerSecond = 20, priorSessionId, initialWatermark = 0, onWelcome, pingIntervalMs = 10_000, livenessTimeoutMs = 25_000, logger, } = options;
|
|
38
|
+
const log = logger ?? (() => undefined);
|
|
39
|
+
if (!database.concurrency.deferredCommit || database.concurrency.userId === undefined) {
|
|
40
|
+
throw new Error("createSyncService: database must use createRebaseReplayConcurrency(userId) as its concurrency strategy.");
|
|
41
|
+
}
|
|
42
|
+
const minIntervalMs = 1000 / maxIntermediatesPerSecond;
|
|
43
|
+
let lastIntermediateSentAt = 0;
|
|
44
|
+
let lastAppliedTime = initialWatermark;
|
|
45
|
+
let serverSessionId;
|
|
46
|
+
let lastInboundAt = Date.now();
|
|
47
|
+
let pingTimer;
|
|
48
|
+
let livenessTimer;
|
|
49
|
+
const stopHeartbeat = () => {
|
|
50
|
+
if (pingTimer !== undefined) {
|
|
51
|
+
clearInterval(pingTimer);
|
|
52
|
+
pingTimer = undefined;
|
|
53
|
+
}
|
|
54
|
+
if (livenessTimer !== undefined) {
|
|
55
|
+
clearInterval(livenessTimer);
|
|
56
|
+
livenessTimer = undefined;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
// Buffer inbound committed/cancelled envelopes that arrive before the
|
|
60
|
+
// welcome handler runs. This lets onWelcome call database.reset()
|
|
61
|
+
// synchronously before any replayed envelopes are applied.
|
|
62
|
+
let bufferedInbound = [];
|
|
63
|
+
const flushBuffered = () => {
|
|
64
|
+
if (bufferedInbound === null)
|
|
65
|
+
return;
|
|
66
|
+
const msgs = bufferedInbound;
|
|
67
|
+
bufferedInbound = null;
|
|
68
|
+
for (const msg of msgs)
|
|
69
|
+
applyInbound(msg);
|
|
70
|
+
};
|
|
71
|
+
const applyInbound = (msg) => {
|
|
72
|
+
if (msg.kind === "committed") {
|
|
73
|
+
log(`committed in (time=${msg.envelope.time}, name=${msg.envelope.name})`);
|
|
74
|
+
database.apply(msg.envelope);
|
|
75
|
+
if (msg.envelope.time > lastAppliedTime) {
|
|
76
|
+
lastAppliedTime = msg.envelope.time;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (msg.kind === "cancelled") {
|
|
80
|
+
log(`cancelled in (id=${msg.id})`);
|
|
81
|
+
database.cancel(msg.id);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const unsubscribeInbound = transport.onMessage((msg) => {
|
|
85
|
+
// Any inbound traffic — committed envelope, welcome, pong, anything —
|
|
86
|
+
// proves the peer is still alive. Refresh the liveness deadline first
|
|
87
|
+
// so we don't trip over slow callbacks below.
|
|
88
|
+
lastInboundAt = Date.now();
|
|
89
|
+
if (msg.kind === "pong")
|
|
90
|
+
return;
|
|
91
|
+
if (msg.kind === "welcome") {
|
|
92
|
+
log(`welcome in (sessionId=${msg.sessionId}, resetRequired=${msg.resetRequired})`);
|
|
93
|
+
serverSessionId = msg.sessionId;
|
|
94
|
+
// Invoke the welcome handler synchronously (it may call database.reset()).
|
|
95
|
+
onWelcome?.({ sessionId: msg.sessionId, resetRequired: msg.resetRequired });
|
|
96
|
+
// After the handler returns, flush buffered replay envelopes.
|
|
97
|
+
flushBuffered();
|
|
98
|
+
}
|
|
99
|
+
else if (bufferedInbound !== null) {
|
|
100
|
+
bufferedInbound.push(msg);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
applyInbound(msg);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
// A component whose SCHEMA is marked nonShared is local to this client.
|
|
107
|
+
const isNonSharedComponent = (component) => database.componentSchemas[component]?.nonShared === true;
|
|
108
|
+
// Whether a transaction has any effect that must reach peers: it changed a
|
|
109
|
+
// shared entity through a shared (not nonShared) component. A transaction
|
|
110
|
+
// whose effects are entirely non-shared — only non-shared entities, or only
|
|
111
|
+
// nonShared components — stays local. NOTE: peers replay the WHOLE
|
|
112
|
+
// transaction, so a mixed shared/non-shared transaction cannot be partially
|
|
113
|
+
// stripped; keep non-shared mutations in their own transactions to keep them
|
|
114
|
+
// off the wire.
|
|
115
|
+
const hasSharedEffect = (result) => {
|
|
116
|
+
let sharedEntity = false;
|
|
117
|
+
for (const entity of result.changedEntities.keys()) {
|
|
118
|
+
if (Entity.isShared(entity)) {
|
|
119
|
+
sharedEntity = true;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!sharedEntity)
|
|
124
|
+
return false;
|
|
125
|
+
for (const component of result.changedComponents) {
|
|
126
|
+
if (!isNonSharedComponent(component))
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
};
|
|
131
|
+
const unsubscribeOutbound = database.observe.envelopes(({ envelope, result, intent }) => {
|
|
132
|
+
// Skip envelopes whose only effect was on non-persistent entities/
|
|
133
|
+
// resources, or entirely on non-shared (client-local) entities/components.
|
|
134
|
+
if (result !== undefined && (!result.persistent || !hasSharedEffect(result)))
|
|
135
|
+
return;
|
|
136
|
+
if (intent === "commit") {
|
|
137
|
+
log(`propose out (id=${envelope.id}, name=${envelope.name})`);
|
|
138
|
+
transport.send({ kind: "propose", envelope });
|
|
139
|
+
}
|
|
140
|
+
else if (intent === "intermediate") {
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
if (now - lastIntermediateSentAt < minIntervalMs)
|
|
143
|
+
return;
|
|
144
|
+
lastIntermediateSentAt = now;
|
|
145
|
+
transport.send({ kind: "intermediate", envelope });
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
log(`cancel out (id=${envelope.id})`);
|
|
149
|
+
transport.send({ kind: "cancel", id: envelope.id });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// Send hello immediately — the onWelcome option is already registered, so
|
|
153
|
+
// the welcome response can be processed synchronously even on loopback.
|
|
154
|
+
log(`hello out (priorSessionId=${priorSessionId ?? "<fresh>"}, watermark=${initialWatermark})`);
|
|
155
|
+
transport.send({ kind: "hello", sessionId: priorSessionId, lastAppliedTime: initialWatermark });
|
|
156
|
+
// Keep-alive timers. The transport's own onClose listener clears them as
|
|
157
|
+
// a safety net so a leaked service doesn't keep the event loop alive.
|
|
158
|
+
if (pingIntervalMs > 0) {
|
|
159
|
+
pingTimer = setInterval(() => {
|
|
160
|
+
transport.send({ kind: "ping" });
|
|
161
|
+
}, pingIntervalMs);
|
|
162
|
+
}
|
|
163
|
+
if (livenessTimeoutMs > 0) {
|
|
164
|
+
// Check at a fraction of the timeout so we detect a drop within
|
|
165
|
+
// ~timeout + checkInterval and tests can use small timeouts.
|
|
166
|
+
const checkInterval = Math.max(50, Math.floor(livenessTimeoutMs / 4));
|
|
167
|
+
livenessTimer = setInterval(() => {
|
|
168
|
+
if (Date.now() - lastInboundAt > livenessTimeoutMs) {
|
|
169
|
+
log(`liveness timeout — closing transport`);
|
|
170
|
+
stopHeartbeat();
|
|
171
|
+
transport.close();
|
|
172
|
+
}
|
|
173
|
+
}, checkInterval);
|
|
174
|
+
}
|
|
175
|
+
const unsubscribeClose = transport.onClose(() => {
|
|
176
|
+
log(`transport closed`);
|
|
177
|
+
stopHeartbeat();
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
dispose: () => {
|
|
181
|
+
log(`dispose`);
|
|
182
|
+
stopHeartbeat();
|
|
183
|
+
unsubscribeClose();
|
|
184
|
+
unsubscribeOutbound();
|
|
185
|
+
unsubscribeInbound();
|
|
186
|
+
transport.close();
|
|
187
|
+
},
|
|
188
|
+
lastAppliedTime: () => lastAppliedTime,
|
|
189
|
+
sessionId: () => serverSessionId,
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
//# sourceMappingURL=create-sync-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-sync-service.js","sourceRoot":"","sources":["../src/create-sync-service.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { SyncTransport, ClientTransport, ServerTransport, ServerMessage, ClientMessage, } from "./transport.js";
|
|
2
|
+
export { createLoopbackTransport } from "./loopback-transport.js";
|
|
3
|
+
export type { SyncServer, SyncServerOptions } from "./create-sync-server.js";
|
|
4
|
+
export { createSyncServer } from "./create-sync-server.js";
|
|
5
|
+
export type { SyncService, SyncServiceOptions } from "./create-sync-service.js";
|
|
6
|
+
export { createSyncService } from "./create-sync-service.js";
|
|
7
|
+
export type { WebSocketClientTransportOptions, WebSocketLike, } from "./websocket-transport.js";
|
|
8
|
+
export { createWebSocketClientTransport, createWebSocketServerTransport, } from "./websocket-transport.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
export { createLoopbackTransport } from "./loopback-transport.js";
|
|
3
|
+
export { createSyncServer } from "./create-sync-server.js";
|
|
4
|
+
export { createSyncService } from "./create-sync-service.js";
|
|
5
|
+
export { createWebSocketClientTransport, createWebSocketServerTransport, } from "./websocket-transport.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAUvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAGlE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAM7D,OAAO,EACH,8BAA8B,EAC9B,8BAA8B,GACjC,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|