@voidhash/mimic 0.0.1-alpha.1 → 0.0.1-alpha.10
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/.turbo/turbo-build.log +51 -0
- package/LICENSE.md +663 -0
- package/dist/Document-ChuFrTk1.cjs +571 -0
- package/dist/Document-CwiAFTIq.mjs +438 -0
- package/dist/Document-CwiAFTIq.mjs.map +1 -0
- package/dist/Presence-DKKP4v5X.d.cts +91 -0
- package/dist/Presence-DKKP4v5X.d.cts.map +1 -0
- package/dist/Presence-DdMVKcOv.mjs +110 -0
- package/dist/Presence-DdMVKcOv.mjs.map +1 -0
- package/dist/Presence-N8u7Eppr.d.mts +91 -0
- package/dist/Presence-N8u7Eppr.d.mts.map +1 -0
- package/dist/Presence-gWrmGBeu.cjs +126 -0
- package/dist/Primitive-CvFVxR8_.d.cts +1175 -0
- package/dist/Primitive-CvFVxR8_.d.cts.map +1 -0
- package/dist/Primitive-lEhQyGVL.d.mts +1175 -0
- package/dist/Primitive-lEhQyGVL.d.mts.map +1 -0
- package/dist/chunk-CLMFDpHK.mjs +18 -0
- package/dist/client/index.cjs +1456 -0
- package/dist/client/index.d.cts +692 -0
- package/dist/client/index.d.cts.map +1 -0
- package/dist/client/index.d.mts +692 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +1413 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/index.cjs +2577 -0
- package/dist/index.d.cts +143 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +143 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2526 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server/index.cjs +191 -0
- package/dist/server/index.d.cts +148 -0
- package/dist/server/index.d.cts.map +1 -0
- package/dist/server/index.d.mts +148 -0
- package/dist/server/index.d.mts.map +1 -0
- package/dist/server/index.mjs +182 -0
- package/dist/server/index.mjs.map +1 -0
- package/package.json +25 -13
- package/src/EffectSchema.ts +374 -0
- package/src/Primitive.ts +3 -0
- package/src/client/ClientDocument.ts +1 -1
- package/src/client/errors.ts +10 -10
- package/src/index.ts +1 -0
- package/src/primitives/Array.ts +57 -22
- package/src/primitives/Boolean.ts +33 -19
- package/src/primitives/Either.ts +379 -0
- package/src/primitives/Lazy.ts +16 -2
- package/src/primitives/Literal.ts +33 -20
- package/src/primitives/Number.ts +39 -26
- package/src/primitives/String.ts +40 -25
- package/src/primitives/Struct.ts +126 -29
- package/src/primitives/Tree.ts +119 -32
- package/src/primitives/TreeNode.ts +77 -30
- package/src/primitives/Union.ts +56 -29
- package/src/primitives/shared.ts +111 -9
- package/src/server/errors.ts +6 -6
- package/tests/EffectSchema.test.ts +546 -0
- package/tests/primitives/Array.test.ts +108 -0
- package/tests/primitives/Either.test.ts +707 -0
- package/tests/primitives/Struct.test.ts +250 -0
- package/tests/primitives/Tree.test.ts +250 -0
- package/tsdown.config.ts +1 -1
|
@@ -0,0 +1,1413 @@
|
|
|
1
|
+
import { t as __export } from "../chunk-CLMFDpHK.mjs";
|
|
2
|
+
import { a as encode, d as isPrefix, f as pathsEqual, i as decode, m as _defineProperty, n as make$3, o as isEmpty, p as pathsOverlap } from "../Document-CwiAFTIq.mjs";
|
|
3
|
+
import { n as validate, r as _objectSpread2, t as Presence_exports } from "../Presence-DdMVKcOv.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/client/Rebase.ts
|
|
6
|
+
var Rebase_exports = /* @__PURE__ */ __export({
|
|
7
|
+
rebaseAfterRejection: () => rebaseAfterRejection,
|
|
8
|
+
rebaseAfterRejectionWithPrimitive: () => rebaseAfterRejectionWithPrimitive,
|
|
9
|
+
rebasePendingTransactions: () => rebasePendingTransactions,
|
|
10
|
+
rebasePendingTransactionsWithPrimitive: () => rebasePendingTransactionsWithPrimitive,
|
|
11
|
+
transformOperation: () => transformOperation,
|
|
12
|
+
transformOperationWithPrimitive: () => transformOperationWithPrimitive,
|
|
13
|
+
transformTransaction: () => transformTransaction,
|
|
14
|
+
transformTransactionWithPrimitive: () => transformTransactionWithPrimitive
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Transforms a client operation against a server operation using a primitive.
|
|
18
|
+
*
|
|
19
|
+
* This delegates to the primitive's transformOperation method, which handles
|
|
20
|
+
* type-specific conflict resolution.
|
|
21
|
+
*
|
|
22
|
+
* @param clientOp - The client's operation to transform
|
|
23
|
+
* @param serverOp - The server's operation that has already been applied
|
|
24
|
+
* @param primitive - The root primitive to use for transformation
|
|
25
|
+
* @returns TransformResult indicating how the client operation should be handled
|
|
26
|
+
*/
|
|
27
|
+
const transformOperationWithPrimitive = (clientOp, serverOp, primitive) => {
|
|
28
|
+
return primitive._internal.transformOperation(clientOp, serverOp);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Transforms a client operation against a server operation.
|
|
32
|
+
*
|
|
33
|
+
* This is a standalone implementation for cases where the primitive is not available.
|
|
34
|
+
* For schema-aware transformation, use transformOperationWithPrimitive instead.
|
|
35
|
+
*
|
|
36
|
+
* The key principle: client ops "shadow" server ops for the same path,
|
|
37
|
+
* meaning if both touch the same field, the client's intention wins
|
|
38
|
+
* (since it was made with knowledge of the server state at that time).
|
|
39
|
+
*/
|
|
40
|
+
const transformOperation = (clientOp, serverOp) => {
|
|
41
|
+
const clientPath = clientOp.path;
|
|
42
|
+
const serverPath = serverOp.path;
|
|
43
|
+
if (!pathsOverlap(clientPath, serverPath)) return {
|
|
44
|
+
type: "transformed",
|
|
45
|
+
operation: clientOp
|
|
46
|
+
};
|
|
47
|
+
if (serverOp.kind === "array.remove") {
|
|
48
|
+
const removedId = serverOp.payload.id;
|
|
49
|
+
const clientTokens = clientPath.toTokens().filter((t) => t !== "");
|
|
50
|
+
const serverTokens = serverPath.toTokens().filter((t) => t !== "");
|
|
51
|
+
if (clientTokens.length > serverTokens.length) {
|
|
52
|
+
if (clientTokens[serverTokens.length] === removedId) return { type: "noop" };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (serverOp.kind === "array.insert" && clientOp.kind === "array.insert") return {
|
|
56
|
+
type: "transformed",
|
|
57
|
+
operation: clientOp
|
|
58
|
+
};
|
|
59
|
+
if (serverOp.kind === "array.move" && clientOp.kind === "array.move") {
|
|
60
|
+
if (serverOp.payload.id === clientOp.payload.id) return {
|
|
61
|
+
type: "transformed",
|
|
62
|
+
operation: clientOp
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
type: "transformed",
|
|
66
|
+
operation: clientOp
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (pathsEqual(clientPath, serverPath)) return {
|
|
70
|
+
type: "transformed",
|
|
71
|
+
operation: clientOp
|
|
72
|
+
};
|
|
73
|
+
if (isPrefix(serverPath, clientPath)) {
|
|
74
|
+
const serverKind = serverOp.kind;
|
|
75
|
+
if (serverKind === "struct.set" || serverKind === "array.set" || serverKind === "union.set") return {
|
|
76
|
+
type: "transformed",
|
|
77
|
+
operation: clientOp
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
type: "transformed",
|
|
82
|
+
operation: clientOp
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Transforms all operations in a client transaction against a server transaction.
|
|
87
|
+
* Uses the primitive's transformOperation for schema-aware transformation.
|
|
88
|
+
*/
|
|
89
|
+
const transformTransactionWithPrimitive = (clientTx, serverTx, primitive) => {
|
|
90
|
+
const transformedOps = [];
|
|
91
|
+
for (const clientOp of clientTx.ops) {
|
|
92
|
+
let currentOp = clientOp;
|
|
93
|
+
for (const serverOp of serverTx.ops) {
|
|
94
|
+
if (currentOp === null) break;
|
|
95
|
+
const result = transformOperationWithPrimitive(currentOp, serverOp, primitive);
|
|
96
|
+
switch (result.type) {
|
|
97
|
+
case "transformed":
|
|
98
|
+
currentOp = result.operation;
|
|
99
|
+
break;
|
|
100
|
+
case "noop":
|
|
101
|
+
currentOp = null;
|
|
102
|
+
break;
|
|
103
|
+
case "conflict": break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (currentOp !== null) transformedOps.push(currentOp);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
id: clientTx.id,
|
|
110
|
+
ops: transformedOps,
|
|
111
|
+
timestamp: clientTx.timestamp
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Transforms all operations in a client transaction against a server transaction.
|
|
116
|
+
* This is a standalone version that doesn't require a primitive.
|
|
117
|
+
*/
|
|
118
|
+
const transformTransaction = (clientTx, serverTx) => {
|
|
119
|
+
const transformedOps = [];
|
|
120
|
+
for (const clientOp of clientTx.ops) {
|
|
121
|
+
let currentOp = clientOp;
|
|
122
|
+
for (const serverOp of serverTx.ops) {
|
|
123
|
+
if (currentOp === null) break;
|
|
124
|
+
const result = transformOperation(currentOp, serverOp);
|
|
125
|
+
switch (result.type) {
|
|
126
|
+
case "transformed":
|
|
127
|
+
currentOp = result.operation;
|
|
128
|
+
break;
|
|
129
|
+
case "noop":
|
|
130
|
+
currentOp = null;
|
|
131
|
+
break;
|
|
132
|
+
case "conflict": break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (currentOp !== null) transformedOps.push(currentOp);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
id: clientTx.id,
|
|
139
|
+
ops: transformedOps,
|
|
140
|
+
timestamp: clientTx.timestamp
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Rebases a list of pending transactions against a server transaction using a primitive.
|
|
145
|
+
*
|
|
146
|
+
* This is called when a server transaction arrives that is NOT one of our pending
|
|
147
|
+
* transactions. We need to transform all pending transactions to work correctly
|
|
148
|
+
* on top of the new server state.
|
|
149
|
+
*/
|
|
150
|
+
const rebasePendingTransactionsWithPrimitive = (pendingTxs, serverTx, primitive) => {
|
|
151
|
+
return pendingTxs.map((pendingTx) => transformTransactionWithPrimitive(pendingTx, serverTx, primitive));
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Rebases a list of pending transactions against a server transaction.
|
|
155
|
+
*
|
|
156
|
+
* This is called when a server transaction arrives that is NOT one of our pending
|
|
157
|
+
* transactions. We need to transform all pending transactions to work correctly
|
|
158
|
+
* on top of the new server state.
|
|
159
|
+
*/
|
|
160
|
+
const rebasePendingTransactions = (pendingTxs, serverTx) => {
|
|
161
|
+
return pendingTxs.map((pendingTx) => transformTransaction(pendingTx, serverTx));
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Rebases pending transactions after a rejection using a primitive.
|
|
165
|
+
*
|
|
166
|
+
* When a transaction is rejected, we need to re-transform remaining pending
|
|
167
|
+
* transactions as if the rejected transaction never happened. This is done by
|
|
168
|
+
* rebuilding from the original operations against the current server state.
|
|
169
|
+
*
|
|
170
|
+
* @param originalPendingTxs - The original pending transactions before any rebasing
|
|
171
|
+
* @param rejectedTxId - ID of the rejected transaction
|
|
172
|
+
* @param serverTxsSinceOriginal - Server transactions that have arrived since original
|
|
173
|
+
* @param primitive - The root primitive to use for transformation
|
|
174
|
+
*/
|
|
175
|
+
const rebaseAfterRejectionWithPrimitive = (originalPendingTxs, rejectedTxId, serverTxsSinceOriginal, primitive) => {
|
|
176
|
+
let result = [...originalPendingTxs.filter((tx) => tx.id !== rejectedTxId)];
|
|
177
|
+
for (const serverTx of serverTxsSinceOriginal) result = rebasePendingTransactionsWithPrimitive(result, serverTx, primitive);
|
|
178
|
+
return result;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Rebases pending transactions after a rejection.
|
|
182
|
+
*
|
|
183
|
+
* When a transaction is rejected, we need to re-transform remaining pending
|
|
184
|
+
* transactions as if the rejected transaction never happened. This is done by
|
|
185
|
+
* rebuilding from the original operations against the current server state.
|
|
186
|
+
*
|
|
187
|
+
* @param originalPendingTxs - The original pending transactions before any rebasing
|
|
188
|
+
* @param rejectedTxId - ID of the rejected transaction
|
|
189
|
+
* @param serverTxsSinceOriginal - Server transactions that have arrived since original
|
|
190
|
+
*/
|
|
191
|
+
const rebaseAfterRejection = (originalPendingTxs, rejectedTxId, serverTxsSinceOriginal) => {
|
|
192
|
+
let result = [...originalPendingTxs.filter((tx) => tx.id !== rejectedTxId)];
|
|
193
|
+
for (const serverTx of serverTxsSinceOriginal) result = rebasePendingTransactions(result, serverTx);
|
|
194
|
+
return result;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/client/errors.ts
|
|
199
|
+
/**
|
|
200
|
+
* Base error class for all mimic-client errors.
|
|
201
|
+
*/
|
|
202
|
+
var MimicClientError = class extends Error {
|
|
203
|
+
constructor(message) {
|
|
204
|
+
super(message);
|
|
205
|
+
_defineProperty(this, "_tag", "MimicClientError");
|
|
206
|
+
this.name = "MimicClientError";
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Error thrown when a transaction is rejected by the server.
|
|
211
|
+
*/
|
|
212
|
+
var TransactionRejectedError = class extends MimicClientError {
|
|
213
|
+
constructor(transaction, reason) {
|
|
214
|
+
super(`Transaction ${transaction.id} rejected: ${reason}`);
|
|
215
|
+
_defineProperty(this, "_tag", "TransactionRejectedError");
|
|
216
|
+
_defineProperty(this, "transaction", void 0);
|
|
217
|
+
_defineProperty(this, "reason", void 0);
|
|
218
|
+
this.name = "TransactionRejectedError";
|
|
219
|
+
this.transaction = transaction;
|
|
220
|
+
this.reason = reason;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Error thrown when the transport is not connected.
|
|
225
|
+
*/
|
|
226
|
+
var NotConnectedError = class extends MimicClientError {
|
|
227
|
+
constructor() {
|
|
228
|
+
super("Transport is not connected");
|
|
229
|
+
_defineProperty(this, "_tag", "NotConnectedError");
|
|
230
|
+
this.name = "NotConnectedError";
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* Error thrown when connection to the server fails.
|
|
235
|
+
*/
|
|
236
|
+
var ConnectionError = class extends MimicClientError {
|
|
237
|
+
constructor(message, cause) {
|
|
238
|
+
super(message);
|
|
239
|
+
_defineProperty(this, "_tag", "ConnectionError");
|
|
240
|
+
_defineProperty(this, "cause", void 0);
|
|
241
|
+
this.name = "ConnectionError";
|
|
242
|
+
this.cause = cause;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* Error thrown when state drift is detected and cannot be recovered.
|
|
247
|
+
*/
|
|
248
|
+
var StateDriftError = class extends MimicClientError {
|
|
249
|
+
constructor(expectedVersion, receivedVersion) {
|
|
250
|
+
super(`State drift detected: expected version ${expectedVersion}, received ${receivedVersion}`);
|
|
251
|
+
_defineProperty(this, "_tag", "StateDriftError");
|
|
252
|
+
_defineProperty(this, "expectedVersion", void 0);
|
|
253
|
+
_defineProperty(this, "receivedVersion", void 0);
|
|
254
|
+
this.name = "StateDriftError";
|
|
255
|
+
this.expectedVersion = expectedVersion;
|
|
256
|
+
this.receivedVersion = receivedVersion;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Error thrown when a pending transaction times out waiting for confirmation.
|
|
261
|
+
*/
|
|
262
|
+
var TransactionTimeoutError = class extends MimicClientError {
|
|
263
|
+
constructor(transaction, timeoutMs) {
|
|
264
|
+
super(`Transaction ${transaction.id} timed out after ${timeoutMs}ms waiting for confirmation`);
|
|
265
|
+
_defineProperty(this, "_tag", "TransactionTimeoutError");
|
|
266
|
+
_defineProperty(this, "transaction", void 0);
|
|
267
|
+
_defineProperty(this, "timeoutMs", void 0);
|
|
268
|
+
this.name = "TransactionTimeoutError";
|
|
269
|
+
this.transaction = transaction;
|
|
270
|
+
this.timeoutMs = timeoutMs;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* Error thrown when rebasing operations fails.
|
|
275
|
+
*/
|
|
276
|
+
var RebaseError = class extends MimicClientError {
|
|
277
|
+
constructor(transactionId, message) {
|
|
278
|
+
super(`Failed to rebase transaction ${transactionId}: ${message}`);
|
|
279
|
+
_defineProperty(this, "_tag", "RebaseError");
|
|
280
|
+
_defineProperty(this, "transactionId", void 0);
|
|
281
|
+
this.name = "RebaseError";
|
|
282
|
+
this.transactionId = transactionId;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* Error thrown when the client document is in an invalid state.
|
|
287
|
+
*/
|
|
288
|
+
var InvalidStateError = class extends MimicClientError {
|
|
289
|
+
constructor(message) {
|
|
290
|
+
super(message);
|
|
291
|
+
_defineProperty(this, "_tag", "InvalidStateError");
|
|
292
|
+
this.name = "InvalidStateError";
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* Error thrown when WebSocket connection or communication fails.
|
|
297
|
+
*/
|
|
298
|
+
var WebSocketError = class extends MimicClientError {
|
|
299
|
+
constructor(message, code, reason) {
|
|
300
|
+
super(message);
|
|
301
|
+
_defineProperty(this, "_tag", "WebSocketError");
|
|
302
|
+
_defineProperty(this, "code", void 0);
|
|
303
|
+
_defineProperty(this, "reason", void 0);
|
|
304
|
+
this.name = "WebSocketError";
|
|
305
|
+
this.code = code;
|
|
306
|
+
this.reason = reason;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* Error thrown when authentication fails.
|
|
311
|
+
*/
|
|
312
|
+
var AuthenticationError = class extends MimicClientError {
|
|
313
|
+
constructor(message) {
|
|
314
|
+
super(message);
|
|
315
|
+
_defineProperty(this, "_tag", "AuthenticationError");
|
|
316
|
+
this.name = "AuthenticationError";
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/client/ClientDocument.ts
|
|
322
|
+
var ClientDocument_exports = /* @__PURE__ */ __export({ make: () => make$2 });
|
|
323
|
+
/**
|
|
324
|
+
* Creates a new ClientDocument for the given schema.
|
|
325
|
+
*/
|
|
326
|
+
const make$2 = (options) => {
|
|
327
|
+
const { schema, transport, initialState, initialVersion = 0, onRejection, onStateChange, onConnectionChange, onReady, transactionTimeout = 3e4, initTimeout = 1e4, debug = false, presence: presenceSchema, initialPresence } = options;
|
|
328
|
+
let _serverState = initialState;
|
|
329
|
+
let _serverVersion = initialVersion;
|
|
330
|
+
let _pending = [];
|
|
331
|
+
let _serverTransactionHistory = [];
|
|
332
|
+
const MAX_HISTORY_SIZE = 100;
|
|
333
|
+
let _optimisticDoc = make$3(schema, { initial: _serverState });
|
|
334
|
+
let _unsubscribe = null;
|
|
335
|
+
const _timeoutHandles = /* @__PURE__ */ new Map();
|
|
336
|
+
let _initState = initialState !== void 0 ? { type: "ready" } : { type: "uninitialized" };
|
|
337
|
+
let _initTimeoutHandle = null;
|
|
338
|
+
let _initResolver = null;
|
|
339
|
+
let _initRejecter = null;
|
|
340
|
+
const _subscribers = /* @__PURE__ */ new Set();
|
|
341
|
+
let _presenceSelfId = void 0;
|
|
342
|
+
let _presenceSelfData = void 0;
|
|
343
|
+
const _presenceOthers = /* @__PURE__ */ new Map();
|
|
344
|
+
const _presenceSubscribers = /* @__PURE__ */ new Set();
|
|
345
|
+
/**
|
|
346
|
+
* Debug logging helper that only logs when debug is enabled.
|
|
347
|
+
*/
|
|
348
|
+
const debugLog = (...args) => {
|
|
349
|
+
if (debug) console.log("[ClientDocument]", ...args);
|
|
350
|
+
};
|
|
351
|
+
/**
|
|
352
|
+
* Notifies all listeners of a state change.
|
|
353
|
+
*/
|
|
354
|
+
const notifyStateChange = (state) => {
|
|
355
|
+
debugLog("notifyStateChange", {
|
|
356
|
+
state,
|
|
357
|
+
subscriberCount: _subscribers.size,
|
|
358
|
+
hasOnStateChange: !!onStateChange
|
|
359
|
+
});
|
|
360
|
+
onStateChange === null || onStateChange === void 0 || onStateChange(state);
|
|
361
|
+
for (const listener of _subscribers) {
|
|
362
|
+
var _listener$onStateChan;
|
|
363
|
+
(_listener$onStateChan = listener.onStateChange) === null || _listener$onStateChan === void 0 || _listener$onStateChan.call(listener, state);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
367
|
+
* Notifies all listeners of a connection change.
|
|
368
|
+
*/
|
|
369
|
+
const notifyConnectionChange = (connected) => {
|
|
370
|
+
debugLog("notifyConnectionChange", {
|
|
371
|
+
connected,
|
|
372
|
+
subscriberCount: _subscribers.size,
|
|
373
|
+
hasOnConnectionChange: !!onConnectionChange
|
|
374
|
+
});
|
|
375
|
+
onConnectionChange === null || onConnectionChange === void 0 || onConnectionChange(connected);
|
|
376
|
+
for (const listener of _subscribers) {
|
|
377
|
+
var _listener$onConnectio;
|
|
378
|
+
(_listener$onConnectio = listener.onConnectionChange) === null || _listener$onConnectio === void 0 || _listener$onConnectio.call(listener, connected);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
/**
|
|
382
|
+
* Notifies all listeners when ready.
|
|
383
|
+
*/
|
|
384
|
+
const notifyReady = () => {
|
|
385
|
+
debugLog("notifyReady", {
|
|
386
|
+
subscriberCount: _subscribers.size,
|
|
387
|
+
hasOnReady: !!onReady
|
|
388
|
+
});
|
|
389
|
+
onReady === null || onReady === void 0 || onReady();
|
|
390
|
+
for (const listener of _subscribers) {
|
|
391
|
+
var _listener$onReady;
|
|
392
|
+
(_listener$onReady = listener.onReady) === null || _listener$onReady === void 0 || _listener$onReady.call(listener);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
/**
|
|
396
|
+
* Notifies all presence listeners of a change.
|
|
397
|
+
*/
|
|
398
|
+
const notifyPresenceChange = () => {
|
|
399
|
+
debugLog("notifyPresenceChange", { subscriberCount: _presenceSubscribers.size });
|
|
400
|
+
for (const listener of _presenceSubscribers) try {
|
|
401
|
+
var _listener$onPresenceC;
|
|
402
|
+
(_listener$onPresenceC = listener.onPresenceChange) === null || _listener$onPresenceC === void 0 || _listener$onPresenceC.call(listener);
|
|
403
|
+
} catch (_unused) {}
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* Handles incoming presence snapshot from server.
|
|
407
|
+
*/
|
|
408
|
+
const handlePresenceSnapshot = (message) => {
|
|
409
|
+
if (!presenceSchema) return;
|
|
410
|
+
debugLog("handlePresenceSnapshot", {
|
|
411
|
+
selfId: message.selfId,
|
|
412
|
+
presenceCount: Object.keys(message.presences).length
|
|
413
|
+
});
|
|
414
|
+
_presenceSelfId = message.selfId;
|
|
415
|
+
_presenceOthers.clear();
|
|
416
|
+
for (const [id, entry] of Object.entries(message.presences)) if (id !== message.selfId) _presenceOthers.set(id, entry);
|
|
417
|
+
notifyPresenceChange();
|
|
418
|
+
};
|
|
419
|
+
/**
|
|
420
|
+
* Handles incoming presence update from server (another user).
|
|
421
|
+
*/
|
|
422
|
+
const handlePresenceUpdate = (message) => {
|
|
423
|
+
if (!presenceSchema) return;
|
|
424
|
+
debugLog("handlePresenceUpdate", {
|
|
425
|
+
id: message.id,
|
|
426
|
+
userId: message.userId
|
|
427
|
+
});
|
|
428
|
+
_presenceOthers.set(message.id, {
|
|
429
|
+
data: message.data,
|
|
430
|
+
userId: message.userId
|
|
431
|
+
});
|
|
432
|
+
notifyPresenceChange();
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* Handles incoming presence remove from server (user disconnected).
|
|
436
|
+
*/
|
|
437
|
+
const handlePresenceRemove = (message) => {
|
|
438
|
+
if (!presenceSchema) return;
|
|
439
|
+
debugLog("handlePresenceRemove", { id: message.id });
|
|
440
|
+
_presenceOthers.delete(message.id);
|
|
441
|
+
notifyPresenceChange();
|
|
442
|
+
};
|
|
443
|
+
/**
|
|
444
|
+
* Clears all presence state (on disconnect).
|
|
445
|
+
*/
|
|
446
|
+
const clearPresenceState = () => {
|
|
447
|
+
_presenceSelfId = void 0;
|
|
448
|
+
_presenceSelfData = void 0;
|
|
449
|
+
_presenceOthers.clear();
|
|
450
|
+
notifyPresenceChange();
|
|
451
|
+
};
|
|
452
|
+
/**
|
|
453
|
+
* Recomputes the optimistic document from server state + pending transactions.
|
|
454
|
+
*/
|
|
455
|
+
const recomputeOptimisticState = () => {
|
|
456
|
+
debugLog("recomputeOptimisticState", {
|
|
457
|
+
serverVersion: _serverVersion,
|
|
458
|
+
pendingCount: _pending.length,
|
|
459
|
+
serverState: _serverState
|
|
460
|
+
});
|
|
461
|
+
_optimisticDoc = make$3(schema, { initial: _serverState });
|
|
462
|
+
for (const pending of _pending) _optimisticDoc.apply(pending.transaction.ops);
|
|
463
|
+
const newState = _optimisticDoc.get();
|
|
464
|
+
debugLog("recomputeOptimisticState: new optimistic state", newState);
|
|
465
|
+
notifyStateChange(newState);
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* Adds a transaction to pending queue and sends to server.
|
|
469
|
+
*/
|
|
470
|
+
const submitTransaction = (tx) => {
|
|
471
|
+
if (!transport.isConnected()) throw new NotConnectedError();
|
|
472
|
+
debugLog("submitTransaction", {
|
|
473
|
+
txId: tx.id,
|
|
474
|
+
ops: tx.ops,
|
|
475
|
+
pendingCount: _pending.length + 1
|
|
476
|
+
});
|
|
477
|
+
const pending = {
|
|
478
|
+
transaction: tx,
|
|
479
|
+
original: tx,
|
|
480
|
+
sentAt: Date.now()
|
|
481
|
+
};
|
|
482
|
+
_pending.push(pending);
|
|
483
|
+
const timeoutHandle = setTimeout(() => {
|
|
484
|
+
handleTransactionTimeout(tx.id);
|
|
485
|
+
}, transactionTimeout);
|
|
486
|
+
_timeoutHandles.set(tx.id, timeoutHandle);
|
|
487
|
+
transport.send(tx);
|
|
488
|
+
debugLog("submitTransaction: sent to server", { txId: tx.id });
|
|
489
|
+
};
|
|
490
|
+
/**
|
|
491
|
+
* Handles a transaction timeout.
|
|
492
|
+
*/
|
|
493
|
+
const handleTransactionTimeout = (txId) => {
|
|
494
|
+
debugLog("handleTransactionTimeout", { txId });
|
|
495
|
+
const index = _pending.findIndex((p) => p.transaction.id === txId);
|
|
496
|
+
if (index === -1) {
|
|
497
|
+
debugLog("handleTransactionTimeout: transaction not found (already confirmed/rejected)", { txId });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const [removed] = _pending.splice(index, 1);
|
|
501
|
+
_timeoutHandles.delete(txId);
|
|
502
|
+
debugLog("handleTransactionTimeout: removed from pending", {
|
|
503
|
+
txId,
|
|
504
|
+
remainingPending: _pending.length
|
|
505
|
+
});
|
|
506
|
+
recomputeOptimisticState();
|
|
507
|
+
onRejection === null || onRejection === void 0 || onRejection(removed.transaction, "Transaction timed out");
|
|
508
|
+
};
|
|
509
|
+
/**
|
|
510
|
+
* Handles an incoming server transaction.
|
|
511
|
+
*/
|
|
512
|
+
const handleServerTransaction = (serverTx, version) => {
|
|
513
|
+
debugLog("handleServerTransaction", {
|
|
514
|
+
txId: serverTx.id,
|
|
515
|
+
version,
|
|
516
|
+
ops: serverTx.ops,
|
|
517
|
+
currentServerVersion: _serverVersion,
|
|
518
|
+
pendingCount: _pending.length
|
|
519
|
+
});
|
|
520
|
+
_serverVersion = version;
|
|
521
|
+
const pendingIndex = _pending.findIndex((p) => p.transaction.id === serverTx.id);
|
|
522
|
+
if (pendingIndex !== -1) {
|
|
523
|
+
debugLog("handleServerTransaction: transaction confirmed (ACK)", {
|
|
524
|
+
txId: serverTx.id,
|
|
525
|
+
pendingIndex
|
|
526
|
+
});
|
|
527
|
+
_pending[pendingIndex];
|
|
528
|
+
const timeoutHandle = _timeoutHandles.get(serverTx.id);
|
|
529
|
+
if (timeoutHandle) {
|
|
530
|
+
clearTimeout(timeoutHandle);
|
|
531
|
+
_timeoutHandles.delete(serverTx.id);
|
|
532
|
+
}
|
|
533
|
+
_pending.splice(pendingIndex, 1);
|
|
534
|
+
const tempDoc = make$3(schema, { initial: _serverState });
|
|
535
|
+
tempDoc.apply(serverTx.ops);
|
|
536
|
+
_serverState = tempDoc.get();
|
|
537
|
+
debugLog("handleServerTransaction: updated server state", {
|
|
538
|
+
txId: serverTx.id,
|
|
539
|
+
newServerState: _serverState,
|
|
540
|
+
remainingPending: _pending.length
|
|
541
|
+
});
|
|
542
|
+
recomputeOptimisticState();
|
|
543
|
+
} else {
|
|
544
|
+
debugLog("handleServerTransaction: remote transaction, rebasing pending", {
|
|
545
|
+
txId: serverTx.id,
|
|
546
|
+
pendingCount: _pending.length
|
|
547
|
+
});
|
|
548
|
+
const tempDoc = make$3(schema, { initial: _serverState });
|
|
549
|
+
tempDoc.apply(serverTx.ops);
|
|
550
|
+
_serverState = tempDoc.get();
|
|
551
|
+
_serverTransactionHistory.push(serverTx);
|
|
552
|
+
if (_serverTransactionHistory.length > MAX_HISTORY_SIZE) _serverTransactionHistory.shift();
|
|
553
|
+
const rebasedPending = _pending.map((p) => _objectSpread2(_objectSpread2({}, p), {}, { transaction: transformTransactionWithPrimitive(p.transaction, serverTx, schema) }));
|
|
554
|
+
debugLog("handleServerTransaction: rebased pending transactions", {
|
|
555
|
+
txId: serverTx.id,
|
|
556
|
+
rebasedCount: rebasedPending.length,
|
|
557
|
+
originalPendingIds: _pending.map((p) => p.transaction.id),
|
|
558
|
+
rebasedPendingIds: rebasedPending.map((p) => p.transaction.id)
|
|
559
|
+
});
|
|
560
|
+
_pending = rebasedPending;
|
|
561
|
+
recomputeOptimisticState();
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
/**
|
|
565
|
+
* Handles a transaction rejection from the server.
|
|
566
|
+
*/
|
|
567
|
+
const handleRejection = (txId, reason) => {
|
|
568
|
+
debugLog("handleRejection", {
|
|
569
|
+
txId,
|
|
570
|
+
reason,
|
|
571
|
+
pendingCount: _pending.length
|
|
572
|
+
});
|
|
573
|
+
const index = _pending.findIndex((p) => p.transaction.id === txId);
|
|
574
|
+
if (index === -1) {
|
|
575
|
+
debugLog("handleRejection: transaction not found (already removed)", { txId });
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const rejected = _pending[index];
|
|
579
|
+
const timeoutHandle = _timeoutHandles.get(txId);
|
|
580
|
+
if (timeoutHandle) {
|
|
581
|
+
clearTimeout(timeoutHandle);
|
|
582
|
+
_timeoutHandles.delete(txId);
|
|
583
|
+
}
|
|
584
|
+
_pending.splice(index, 1);
|
|
585
|
+
debugLog("handleRejection: removed rejected transaction, rebasing remaining", {
|
|
586
|
+
txId,
|
|
587
|
+
remainingPending: _pending.length,
|
|
588
|
+
serverHistorySize: _serverTransactionHistory.length
|
|
589
|
+
});
|
|
590
|
+
const remainingOriginals = _pending.map((p) => p.original);
|
|
591
|
+
const retransformed = rebaseAfterRejectionWithPrimitive([...remainingOriginals, rejected.original], txId, _serverTransactionHistory, schema);
|
|
592
|
+
_pending = _pending.map((p, i) => {
|
|
593
|
+
var _retransformed$i;
|
|
594
|
+
return _objectSpread2(_objectSpread2({}, p), {}, { transaction: (_retransformed$i = retransformed[i]) !== null && _retransformed$i !== void 0 ? _retransformed$i : p.transaction });
|
|
595
|
+
});
|
|
596
|
+
debugLog("handleRejection: rebased remaining transactions", {
|
|
597
|
+
txId,
|
|
598
|
+
rebasedCount: _pending.length
|
|
599
|
+
});
|
|
600
|
+
recomputeOptimisticState();
|
|
601
|
+
onRejection === null || onRejection === void 0 || onRejection(rejected.original, reason);
|
|
602
|
+
};
|
|
603
|
+
/**
|
|
604
|
+
* Handles a snapshot from the server.
|
|
605
|
+
* @param isInitialSnapshot - If true, this is the initial sync snapshot
|
|
606
|
+
*/
|
|
607
|
+
const handleSnapshot = (state, version, isInitialSnapshot = false) => {
|
|
608
|
+
debugLog("handleSnapshot", {
|
|
609
|
+
isInitialSnapshot,
|
|
610
|
+
version,
|
|
611
|
+
currentServerVersion: _serverVersion,
|
|
612
|
+
pendingCount: _pending.length,
|
|
613
|
+
state
|
|
614
|
+
});
|
|
615
|
+
if (!isInitialSnapshot) {
|
|
616
|
+
debugLog("handleSnapshot: non-initial snapshot, clearing pending transactions", { clearedPendingCount: _pending.length });
|
|
617
|
+
for (const handle of _timeoutHandles.values()) clearTimeout(handle);
|
|
618
|
+
_timeoutHandles.clear();
|
|
619
|
+
for (const pending of _pending) onRejection === null || onRejection === void 0 || onRejection(pending.original, "State reset due to resync");
|
|
620
|
+
_pending = [];
|
|
621
|
+
}
|
|
622
|
+
_serverTransactionHistory = [];
|
|
623
|
+
_serverState = state;
|
|
624
|
+
_serverVersion = version;
|
|
625
|
+
debugLog("handleSnapshot: updated server state", {
|
|
626
|
+
newVersion: _serverVersion,
|
|
627
|
+
newState: _serverState
|
|
628
|
+
});
|
|
629
|
+
recomputeOptimisticState();
|
|
630
|
+
};
|
|
631
|
+
/**
|
|
632
|
+
* Processes buffered messages after receiving the initial snapshot.
|
|
633
|
+
* Filters out transactions already included in the snapshot (version <= snapshotVersion)
|
|
634
|
+
* and applies newer transactions in order.
|
|
635
|
+
*/
|
|
636
|
+
const processBufferedMessages = (bufferedMessages, snapshotVersion) => {
|
|
637
|
+
debugLog("processBufferedMessages", {
|
|
638
|
+
bufferedCount: bufferedMessages.length,
|
|
639
|
+
snapshotVersion
|
|
640
|
+
});
|
|
641
|
+
const sortedMessages = [...bufferedMessages].sort((a, b) => {
|
|
642
|
+
if (a.type === "transaction" && b.type === "transaction") return a.version - b.version;
|
|
643
|
+
return 0;
|
|
644
|
+
});
|
|
645
|
+
for (const message of sortedMessages) switch (message.type) {
|
|
646
|
+
case "transaction":
|
|
647
|
+
if (message.version > snapshotVersion) {
|
|
648
|
+
debugLog("processBufferedMessages: applying buffered transaction", {
|
|
649
|
+
txId: message.transaction.id,
|
|
650
|
+
version: message.version,
|
|
651
|
+
snapshotVersion
|
|
652
|
+
});
|
|
653
|
+
handleServerTransaction(message.transaction, message.version);
|
|
654
|
+
} else debugLog("processBufferedMessages: skipping buffered transaction (already in snapshot)", {
|
|
655
|
+
txId: message.transaction.id,
|
|
656
|
+
version: message.version,
|
|
657
|
+
snapshotVersion
|
|
658
|
+
});
|
|
659
|
+
break;
|
|
660
|
+
case "error":
|
|
661
|
+
debugLog("processBufferedMessages: processing buffered error", {
|
|
662
|
+
txId: message.transactionId,
|
|
663
|
+
reason: message.reason
|
|
664
|
+
});
|
|
665
|
+
handleRejection(message.transactionId, message.reason);
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
/**
|
|
670
|
+
* Completes initialization and transitions to ready state.
|
|
671
|
+
*/
|
|
672
|
+
const completeInitialization = () => {
|
|
673
|
+
debugLog("completeInitialization");
|
|
674
|
+
if (_initTimeoutHandle !== null) {
|
|
675
|
+
clearTimeout(_initTimeoutHandle);
|
|
676
|
+
_initTimeoutHandle = null;
|
|
677
|
+
}
|
|
678
|
+
_initState = { type: "ready" };
|
|
679
|
+
if (_initResolver) {
|
|
680
|
+
_initResolver();
|
|
681
|
+
_initResolver = null;
|
|
682
|
+
_initRejecter = null;
|
|
683
|
+
}
|
|
684
|
+
debugLog("completeInitialization: ready", {
|
|
685
|
+
serverVersion: _serverVersion,
|
|
686
|
+
serverState: _serverState
|
|
687
|
+
});
|
|
688
|
+
notifyReady();
|
|
689
|
+
};
|
|
690
|
+
/**
|
|
691
|
+
* Handles initialization timeout.
|
|
692
|
+
*/
|
|
693
|
+
const handleInitTimeout = () => {
|
|
694
|
+
debugLog("handleInitTimeout: initialization timed out");
|
|
695
|
+
_initTimeoutHandle = null;
|
|
696
|
+
if (_initRejecter) {
|
|
697
|
+
_initRejecter(/* @__PURE__ */ new Error("Initialization timed out waiting for snapshot"));
|
|
698
|
+
_initResolver = null;
|
|
699
|
+
_initRejecter = null;
|
|
700
|
+
}
|
|
701
|
+
_initState = { type: "uninitialized" };
|
|
702
|
+
};
|
|
703
|
+
/**
|
|
704
|
+
* Handles incoming server messages.
|
|
705
|
+
* During initialization, messages are buffered until the snapshot arrives.
|
|
706
|
+
* Presence messages are always processed immediately (not buffered).
|
|
707
|
+
*/
|
|
708
|
+
const handleServerMessage = (message) => {
|
|
709
|
+
debugLog("handleServerMessage", {
|
|
710
|
+
messageType: message.type,
|
|
711
|
+
initState: _initState.type
|
|
712
|
+
});
|
|
713
|
+
if (message.type === "presence_snapshot") {
|
|
714
|
+
handlePresenceSnapshot(message);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (message.type === "presence_update") {
|
|
718
|
+
handlePresenceUpdate(message);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (message.type === "presence_remove") {
|
|
722
|
+
handlePresenceRemove(message);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (_initState.type === "initializing") {
|
|
726
|
+
if (message.type === "snapshot") {
|
|
727
|
+
debugLog("handleServerMessage: received snapshot during initialization", {
|
|
728
|
+
version: message.version,
|
|
729
|
+
bufferedCount: _initState.bufferedMessages.length
|
|
730
|
+
});
|
|
731
|
+
const buffered = _initState.bufferedMessages;
|
|
732
|
+
handleSnapshot(message.state, message.version, true);
|
|
733
|
+
processBufferedMessages(buffered, message.version);
|
|
734
|
+
completeInitialization();
|
|
735
|
+
} else {
|
|
736
|
+
debugLog("handleServerMessage: buffering message during initialization", {
|
|
737
|
+
messageType: message.type,
|
|
738
|
+
bufferedCount: _initState.bufferedMessages.length + 1
|
|
739
|
+
});
|
|
740
|
+
_initState.bufferedMessages.push(message);
|
|
741
|
+
}
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
switch (message.type) {
|
|
745
|
+
case "transaction":
|
|
746
|
+
handleServerTransaction(message.transaction, message.version);
|
|
747
|
+
break;
|
|
748
|
+
case "snapshot":
|
|
749
|
+
handleSnapshot(message.state, message.version, false);
|
|
750
|
+
break;
|
|
751
|
+
case "error":
|
|
752
|
+
handleRejection(message.transactionId, message.reason);
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
return {
|
|
757
|
+
schema,
|
|
758
|
+
get root() {
|
|
759
|
+
return _optimisticDoc.root;
|
|
760
|
+
},
|
|
761
|
+
get: () => _optimisticDoc.get(),
|
|
762
|
+
getServerState: () => _serverState,
|
|
763
|
+
getServerVersion: () => _serverVersion,
|
|
764
|
+
getPendingCount: () => _pending.length,
|
|
765
|
+
hasPendingChanges: () => _pending.length > 0,
|
|
766
|
+
transaction: (fn) => {
|
|
767
|
+
debugLog("transaction: starting", {
|
|
768
|
+
isConnected: transport.isConnected(),
|
|
769
|
+
isReady: _initState.type === "ready",
|
|
770
|
+
pendingCount: _pending.length
|
|
771
|
+
});
|
|
772
|
+
if (!transport.isConnected()) throw new NotConnectedError();
|
|
773
|
+
if (_initState.type !== "ready") throw new InvalidStateError("Client is not ready. Wait for initialization to complete.");
|
|
774
|
+
const result = _optimisticDoc.transaction(fn);
|
|
775
|
+
const tx = _optimisticDoc.flush();
|
|
776
|
+
if (!isEmpty(tx)) {
|
|
777
|
+
debugLog("transaction: flushed, submitting", {
|
|
778
|
+
txId: tx.id,
|
|
779
|
+
opsCount: tx.ops.length
|
|
780
|
+
});
|
|
781
|
+
submitTransaction(tx);
|
|
782
|
+
} else debugLog("transaction: flushed, empty transaction (no ops)");
|
|
783
|
+
notifyStateChange(_optimisticDoc.get());
|
|
784
|
+
return result;
|
|
785
|
+
},
|
|
786
|
+
connect: async () => {
|
|
787
|
+
debugLog("connect: starting");
|
|
788
|
+
_unsubscribe = transport.subscribe(handleServerMessage);
|
|
789
|
+
await transport.connect();
|
|
790
|
+
debugLog("connect: transport connected");
|
|
791
|
+
notifyConnectionChange(true);
|
|
792
|
+
if (presenceSchema && initialPresence !== void 0) {
|
|
793
|
+
debugLog("connect: setting initial presence", { initialPresence });
|
|
794
|
+
const validated = validate(presenceSchema, initialPresence);
|
|
795
|
+
_presenceSelfData = validated;
|
|
796
|
+
transport.sendPresenceSet(validated);
|
|
797
|
+
notifyPresenceChange();
|
|
798
|
+
}
|
|
799
|
+
if (_initState.type === "ready") {
|
|
800
|
+
debugLog("connect: already ready (has initial state)");
|
|
801
|
+
notifyReady();
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
_initState = {
|
|
805
|
+
type: "initializing",
|
|
806
|
+
bufferedMessages: []
|
|
807
|
+
};
|
|
808
|
+
debugLog("connect: entering initializing state", { initTimeout });
|
|
809
|
+
_initTimeoutHandle = setTimeout(handleInitTimeout, initTimeout);
|
|
810
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
811
|
+
_initResolver = resolve;
|
|
812
|
+
_initRejecter = reject;
|
|
813
|
+
});
|
|
814
|
+
debugLog("connect: requesting initial snapshot");
|
|
815
|
+
transport.requestSnapshot();
|
|
816
|
+
await readyPromise;
|
|
817
|
+
debugLog("connect: completed");
|
|
818
|
+
},
|
|
819
|
+
disconnect: () => {
|
|
820
|
+
debugLog("disconnect: starting", {
|
|
821
|
+
pendingCount: _pending.length,
|
|
822
|
+
initState: _initState.type
|
|
823
|
+
});
|
|
824
|
+
for (const handle of _timeoutHandles.values()) clearTimeout(handle);
|
|
825
|
+
_timeoutHandles.clear();
|
|
826
|
+
if (_initTimeoutHandle !== null) {
|
|
827
|
+
clearTimeout(_initTimeoutHandle);
|
|
828
|
+
_initTimeoutHandle = null;
|
|
829
|
+
}
|
|
830
|
+
if (_initRejecter) {
|
|
831
|
+
_initRejecter(/* @__PURE__ */ new Error("Disconnected during initialization"));
|
|
832
|
+
_initResolver = null;
|
|
833
|
+
_initRejecter = null;
|
|
834
|
+
}
|
|
835
|
+
if (_initState.type === "initializing") _initState = { type: "uninitialized" };
|
|
836
|
+
clearPresenceState();
|
|
837
|
+
if (_unsubscribe) {
|
|
838
|
+
_unsubscribe();
|
|
839
|
+
_unsubscribe = null;
|
|
840
|
+
}
|
|
841
|
+
transport.disconnect();
|
|
842
|
+
notifyConnectionChange(false);
|
|
843
|
+
debugLog("disconnect: completed");
|
|
844
|
+
},
|
|
845
|
+
isConnected: () => transport.isConnected(),
|
|
846
|
+
isReady: () => _initState.type === "ready",
|
|
847
|
+
resync: () => {
|
|
848
|
+
debugLog("resync: requesting snapshot", {
|
|
849
|
+
currentVersion: _serverVersion,
|
|
850
|
+
pendingCount: _pending.length
|
|
851
|
+
});
|
|
852
|
+
if (!transport.isConnected()) throw new NotConnectedError();
|
|
853
|
+
transport.requestSnapshot();
|
|
854
|
+
},
|
|
855
|
+
subscribe: (listener) => {
|
|
856
|
+
_subscribers.add(listener);
|
|
857
|
+
return () => {
|
|
858
|
+
_subscribers.delete(listener);
|
|
859
|
+
};
|
|
860
|
+
},
|
|
861
|
+
presence: presenceSchema ? {
|
|
862
|
+
selfId: () => _presenceSelfId,
|
|
863
|
+
self: () => _presenceSelfData,
|
|
864
|
+
others: () => _presenceOthers,
|
|
865
|
+
all: () => {
|
|
866
|
+
const all = /* @__PURE__ */ new Map();
|
|
867
|
+
for (const [id, entry] of _presenceOthers) all.set(id, entry);
|
|
868
|
+
if (_presenceSelfId !== void 0 && _presenceSelfData !== void 0) all.set(_presenceSelfId, { data: _presenceSelfData });
|
|
869
|
+
return all;
|
|
870
|
+
},
|
|
871
|
+
set: (data) => {
|
|
872
|
+
if (!presenceSchema) return;
|
|
873
|
+
const validated = validate(presenceSchema, data);
|
|
874
|
+
debugLog("presence.set", { data: validated });
|
|
875
|
+
_presenceSelfData = validated;
|
|
876
|
+
transport.sendPresenceSet(validated);
|
|
877
|
+
notifyPresenceChange();
|
|
878
|
+
},
|
|
879
|
+
clear: () => {
|
|
880
|
+
if (!presenceSchema) return;
|
|
881
|
+
debugLog("presence.clear");
|
|
882
|
+
_presenceSelfData = void 0;
|
|
883
|
+
transport.sendPresenceClear();
|
|
884
|
+
notifyPresenceChange();
|
|
885
|
+
},
|
|
886
|
+
subscribe: (listener) => {
|
|
887
|
+
_presenceSubscribers.add(listener);
|
|
888
|
+
return () => {
|
|
889
|
+
_presenceSubscribers.delete(listener);
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
} : void 0
|
|
893
|
+
};
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/client/Transport.ts
|
|
898
|
+
var Transport_exports = {};
|
|
899
|
+
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region src/client/WebSocketTransport.ts
|
|
902
|
+
var WebSocketTransport_exports = /* @__PURE__ */ __export({ make: () => make$1 });
|
|
903
|
+
/**
|
|
904
|
+
* Creates a WebSocket-based transport for real-time server communication.
|
|
905
|
+
*/
|
|
906
|
+
/**
|
|
907
|
+
* Build the WebSocket URL with optional document ID path.
|
|
908
|
+
*/
|
|
909
|
+
const buildWebSocketUrl = (baseUrl, documentId) => {
|
|
910
|
+
if (!documentId) return baseUrl;
|
|
911
|
+
return `${baseUrl.replace(/\/+$/, "")}/doc/${encodeURIComponent(documentId)}`;
|
|
912
|
+
};
|
|
913
|
+
const make$1 = (options) => {
|
|
914
|
+
const { url: baseUrl, documentId, protocols, authToken, onEvent, connectionTimeout = 1e4, autoReconnect = true, maxReconnectAttempts = 10, reconnectDelay = 1e3, maxReconnectDelay = 3e4, heartbeatInterval = 3e4, heartbeatTimeout = 1e4 } = options;
|
|
915
|
+
const url = buildWebSocketUrl(baseUrl, documentId);
|
|
916
|
+
let _state = { type: "disconnected" };
|
|
917
|
+
let _ws = null;
|
|
918
|
+
let _messageHandlers = /* @__PURE__ */ new Set();
|
|
919
|
+
let _connectionTimeoutHandle = null;
|
|
920
|
+
let _heartbeatIntervalHandle = null;
|
|
921
|
+
let _heartbeatTimeoutHandle = null;
|
|
922
|
+
let _reconnectTimeoutHandle = null;
|
|
923
|
+
let _messageQueue = [];
|
|
924
|
+
let _connectResolver = null;
|
|
925
|
+
let _connectRejecter = null;
|
|
926
|
+
let _reconnectAttempt = 0;
|
|
927
|
+
const emit = (handler, event) => {
|
|
928
|
+
handler === null || handler === void 0 || handler(event);
|
|
929
|
+
};
|
|
930
|
+
/**
|
|
931
|
+
* Encodes a client message for network transport.
|
|
932
|
+
*/
|
|
933
|
+
const encodeClientMessage = (message) => {
|
|
934
|
+
if (message.type === "submit") return {
|
|
935
|
+
type: "submit",
|
|
936
|
+
transaction: encode(message.transaction)
|
|
937
|
+
};
|
|
938
|
+
return message;
|
|
939
|
+
};
|
|
940
|
+
/**
|
|
941
|
+
* Decodes a server message from network transport.
|
|
942
|
+
*/
|
|
943
|
+
const decodeServerMessage = (encoded) => {
|
|
944
|
+
if (encoded.type === "transaction") return {
|
|
945
|
+
type: "transaction",
|
|
946
|
+
transaction: decode(encoded.transaction),
|
|
947
|
+
version: encoded.version
|
|
948
|
+
};
|
|
949
|
+
return encoded;
|
|
950
|
+
};
|
|
951
|
+
/**
|
|
952
|
+
* Sends a raw message over the WebSocket.
|
|
953
|
+
*/
|
|
954
|
+
const sendRaw = (message) => {
|
|
955
|
+
if (_ws && _ws.readyState === WebSocket.OPEN) _ws.send(JSON.stringify(encodeClientMessage(message)));
|
|
956
|
+
};
|
|
957
|
+
/**
|
|
958
|
+
* Clears all active timers.
|
|
959
|
+
*/
|
|
960
|
+
const clearTimers = () => {
|
|
961
|
+
if (_connectionTimeoutHandle) {
|
|
962
|
+
clearTimeout(_connectionTimeoutHandle);
|
|
963
|
+
_connectionTimeoutHandle = null;
|
|
964
|
+
}
|
|
965
|
+
if (_heartbeatIntervalHandle) {
|
|
966
|
+
clearInterval(_heartbeatIntervalHandle);
|
|
967
|
+
_heartbeatIntervalHandle = null;
|
|
968
|
+
}
|
|
969
|
+
if (_heartbeatTimeoutHandle) {
|
|
970
|
+
clearTimeout(_heartbeatTimeoutHandle);
|
|
971
|
+
_heartbeatTimeoutHandle = null;
|
|
972
|
+
}
|
|
973
|
+
if (_reconnectTimeoutHandle) {
|
|
974
|
+
clearTimeout(_reconnectTimeoutHandle);
|
|
975
|
+
_reconnectTimeoutHandle = null;
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
/**
|
|
979
|
+
* Starts the heartbeat mechanism.
|
|
980
|
+
*/
|
|
981
|
+
const startHeartbeat = () => {
|
|
982
|
+
stopHeartbeat();
|
|
983
|
+
_heartbeatIntervalHandle = setInterval(() => {
|
|
984
|
+
if (_state.type !== "connected") return;
|
|
985
|
+
sendRaw({ type: "ping" });
|
|
986
|
+
_heartbeatTimeoutHandle = setTimeout(() => {
|
|
987
|
+
handleConnectionLost("Heartbeat timeout");
|
|
988
|
+
}, heartbeatTimeout);
|
|
989
|
+
}, heartbeatInterval);
|
|
990
|
+
};
|
|
991
|
+
/**
|
|
992
|
+
* Stops the heartbeat mechanism.
|
|
993
|
+
*/
|
|
994
|
+
const stopHeartbeat = () => {
|
|
995
|
+
if (_heartbeatIntervalHandle) {
|
|
996
|
+
clearInterval(_heartbeatIntervalHandle);
|
|
997
|
+
_heartbeatIntervalHandle = null;
|
|
998
|
+
}
|
|
999
|
+
if (_heartbeatTimeoutHandle) {
|
|
1000
|
+
clearTimeout(_heartbeatTimeoutHandle);
|
|
1001
|
+
_heartbeatTimeoutHandle = null;
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
/**
|
|
1005
|
+
* Handles pong response - clears the heartbeat timeout.
|
|
1006
|
+
*/
|
|
1007
|
+
const handlePong = () => {
|
|
1008
|
+
if (_heartbeatTimeoutHandle) {
|
|
1009
|
+
clearTimeout(_heartbeatTimeoutHandle);
|
|
1010
|
+
_heartbeatTimeoutHandle = null;
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
/**
|
|
1014
|
+
* Flushes the message queue after reconnection.
|
|
1015
|
+
*/
|
|
1016
|
+
const flushMessageQueue = () => {
|
|
1017
|
+
const queue = _messageQueue;
|
|
1018
|
+
_messageQueue = [];
|
|
1019
|
+
for (const message of queue) sendRaw(message);
|
|
1020
|
+
};
|
|
1021
|
+
/**
|
|
1022
|
+
* Calculates reconnection delay with exponential backoff.
|
|
1023
|
+
*/
|
|
1024
|
+
const getReconnectDelay = (attempt) => {
|
|
1025
|
+
const delay = reconnectDelay * Math.pow(2, attempt);
|
|
1026
|
+
return Math.min(delay, maxReconnectDelay);
|
|
1027
|
+
};
|
|
1028
|
+
/**
|
|
1029
|
+
* Resolves the auth token (handles both string and function).
|
|
1030
|
+
* Returns empty string if no token is configured.
|
|
1031
|
+
*/
|
|
1032
|
+
const resolveAuthToken = async () => {
|
|
1033
|
+
if (!authToken) return "";
|
|
1034
|
+
if (typeof authToken === "string") return authToken;
|
|
1035
|
+
return authToken();
|
|
1036
|
+
};
|
|
1037
|
+
/**
|
|
1038
|
+
* Performs authentication after connection.
|
|
1039
|
+
* Always sends an auth message (even with empty token) to trigger server auth flow.
|
|
1040
|
+
*/
|
|
1041
|
+
const authenticate = async () => {
|
|
1042
|
+
const token = await resolveAuthToken();
|
|
1043
|
+
_state = { type: "authenticating" };
|
|
1044
|
+
sendRaw({
|
|
1045
|
+
type: "auth",
|
|
1046
|
+
token
|
|
1047
|
+
});
|
|
1048
|
+
};
|
|
1049
|
+
/**
|
|
1050
|
+
* Handles authentication result from server.
|
|
1051
|
+
*/
|
|
1052
|
+
const handleAuthResult = (success, error) => {
|
|
1053
|
+
if (!success) {
|
|
1054
|
+
const authError = new AuthenticationError(error || "Authentication failed");
|
|
1055
|
+
cleanup();
|
|
1056
|
+
_connectRejecter === null || _connectRejecter === void 0 || _connectRejecter(authError);
|
|
1057
|
+
_connectResolver = null;
|
|
1058
|
+
_connectRejecter = null;
|
|
1059
|
+
emit(onEvent, {
|
|
1060
|
+
type: "error",
|
|
1061
|
+
error: authError
|
|
1062
|
+
});
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
completeConnection();
|
|
1066
|
+
};
|
|
1067
|
+
/**
|
|
1068
|
+
* Completes the connection process.
|
|
1069
|
+
*/
|
|
1070
|
+
const completeConnection = () => {
|
|
1071
|
+
_state = { type: "connected" };
|
|
1072
|
+
_reconnectAttempt = 0;
|
|
1073
|
+
if (_connectionTimeoutHandle) {
|
|
1074
|
+
clearTimeout(_connectionTimeoutHandle);
|
|
1075
|
+
_connectionTimeoutHandle = null;
|
|
1076
|
+
}
|
|
1077
|
+
startHeartbeat();
|
|
1078
|
+
flushMessageQueue();
|
|
1079
|
+
_connectResolver === null || _connectResolver === void 0 || _connectResolver();
|
|
1080
|
+
_connectResolver = null;
|
|
1081
|
+
_connectRejecter = null;
|
|
1082
|
+
emit(onEvent, { type: "connected" });
|
|
1083
|
+
};
|
|
1084
|
+
/**
|
|
1085
|
+
* Cleans up WebSocket and related state.
|
|
1086
|
+
*/
|
|
1087
|
+
const cleanup = () => {
|
|
1088
|
+
clearTimers();
|
|
1089
|
+
if (_ws) {
|
|
1090
|
+
_ws.onopen = null;
|
|
1091
|
+
_ws.onclose = null;
|
|
1092
|
+
_ws.onerror = null;
|
|
1093
|
+
_ws.onmessage = null;
|
|
1094
|
+
if (_ws.readyState === WebSocket.OPEN || _ws.readyState === WebSocket.CONNECTING) _ws.close();
|
|
1095
|
+
_ws = null;
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
/**
|
|
1099
|
+
* Handles connection lost - initiates reconnection if enabled.
|
|
1100
|
+
*/
|
|
1101
|
+
const handleConnectionLost = (reason) => {
|
|
1102
|
+
cleanup();
|
|
1103
|
+
if (_state.type === "disconnected") return;
|
|
1104
|
+
if (_connectRejecter !== null) {
|
|
1105
|
+
_state = { type: "disconnected" };
|
|
1106
|
+
_reconnectAttempt = 0;
|
|
1107
|
+
_connectRejecter(new WebSocketError("Connection failed", void 0, reason));
|
|
1108
|
+
_connectResolver = null;
|
|
1109
|
+
_connectRejecter = null;
|
|
1110
|
+
emit(onEvent, {
|
|
1111
|
+
type: "disconnected",
|
|
1112
|
+
reason
|
|
1113
|
+
});
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
if (!autoReconnect) {
|
|
1117
|
+
_state = { type: "disconnected" };
|
|
1118
|
+
_reconnectAttempt = 0;
|
|
1119
|
+
emit(onEvent, {
|
|
1120
|
+
type: "disconnected",
|
|
1121
|
+
reason
|
|
1122
|
+
});
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
_reconnectAttempt++;
|
|
1126
|
+
if (_reconnectAttempt > maxReconnectAttempts) {
|
|
1127
|
+
_state = { type: "disconnected" };
|
|
1128
|
+
_reconnectAttempt = 0;
|
|
1129
|
+
emit(onEvent, {
|
|
1130
|
+
type: "disconnected",
|
|
1131
|
+
reason: "Max reconnection attempts reached"
|
|
1132
|
+
});
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
_state = {
|
|
1136
|
+
type: "reconnecting",
|
|
1137
|
+
attempt: _reconnectAttempt
|
|
1138
|
+
};
|
|
1139
|
+
emit(onEvent, {
|
|
1140
|
+
type: "reconnecting",
|
|
1141
|
+
attempt: _reconnectAttempt
|
|
1142
|
+
});
|
|
1143
|
+
const delay = getReconnectDelay(_reconnectAttempt - 1);
|
|
1144
|
+
_reconnectTimeoutHandle = setTimeout(() => {
|
|
1145
|
+
_reconnectTimeoutHandle = null;
|
|
1146
|
+
attemptConnection();
|
|
1147
|
+
}, delay);
|
|
1148
|
+
};
|
|
1149
|
+
/**
|
|
1150
|
+
* Attempts to establish WebSocket connection.
|
|
1151
|
+
*/
|
|
1152
|
+
const attemptConnection = () => {
|
|
1153
|
+
if (_state.type === "connected") return;
|
|
1154
|
+
_state = { type: "connecting" };
|
|
1155
|
+
try {
|
|
1156
|
+
_ws = new WebSocket(url, protocols);
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
handleConnectionLost(error.message);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
_connectionTimeoutHandle = setTimeout(() => {
|
|
1162
|
+
_connectionTimeoutHandle = null;
|
|
1163
|
+
handleConnectionLost("Connection timeout");
|
|
1164
|
+
}, connectionTimeout);
|
|
1165
|
+
_ws.onopen = async () => {
|
|
1166
|
+
if (_connectionTimeoutHandle) {
|
|
1167
|
+
clearTimeout(_connectionTimeoutHandle);
|
|
1168
|
+
_connectionTimeoutHandle = null;
|
|
1169
|
+
}
|
|
1170
|
+
try {
|
|
1171
|
+
await authenticate();
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
handleConnectionLost(error.message);
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
_ws.onclose = (event) => {
|
|
1177
|
+
handleConnectionLost(event.reason || `Connection closed (code: ${event.code})`);
|
|
1178
|
+
};
|
|
1179
|
+
_ws.onerror = () => {};
|
|
1180
|
+
_ws.onmessage = (event) => {
|
|
1181
|
+
try {
|
|
1182
|
+
handleMessage(decodeServerMessage(JSON.parse(event.data)));
|
|
1183
|
+
} catch (_unused) {}
|
|
1184
|
+
};
|
|
1185
|
+
};
|
|
1186
|
+
/**
|
|
1187
|
+
* Handles incoming server messages.
|
|
1188
|
+
*/
|
|
1189
|
+
const handleMessage = (message) => {
|
|
1190
|
+
if (message.type === "pong") {
|
|
1191
|
+
handlePong();
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
if (message.type === "auth_result") {
|
|
1195
|
+
handleAuthResult(message.success, message.error);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
for (const handler of _messageHandlers) try {
|
|
1199
|
+
handler(message);
|
|
1200
|
+
} catch (_unused2) {}
|
|
1201
|
+
};
|
|
1202
|
+
return {
|
|
1203
|
+
send: (transaction) => {
|
|
1204
|
+
const message = {
|
|
1205
|
+
type: "submit",
|
|
1206
|
+
transaction
|
|
1207
|
+
};
|
|
1208
|
+
if (_state.type === "connected") sendRaw(message);
|
|
1209
|
+
else if (_state.type === "reconnecting") _messageQueue.push(message);
|
|
1210
|
+
},
|
|
1211
|
+
requestSnapshot: () => {
|
|
1212
|
+
const message = { type: "request_snapshot" };
|
|
1213
|
+
if (_state.type === "connected") sendRaw(message);
|
|
1214
|
+
else if (_state.type === "reconnecting") _messageQueue.push(message);
|
|
1215
|
+
},
|
|
1216
|
+
subscribe: (handler) => {
|
|
1217
|
+
_messageHandlers.add(handler);
|
|
1218
|
+
return () => {
|
|
1219
|
+
_messageHandlers.delete(handler);
|
|
1220
|
+
};
|
|
1221
|
+
},
|
|
1222
|
+
connect: async () => {
|
|
1223
|
+
if (_state.type === "connected") return;
|
|
1224
|
+
if (_state.type === "connecting" || _state.type === "authenticating") return new Promise((resolve, reject) => {
|
|
1225
|
+
const existingResolver = _connectResolver;
|
|
1226
|
+
const existingRejecter = _connectRejecter;
|
|
1227
|
+
_connectResolver = () => {
|
|
1228
|
+
existingResolver === null || existingResolver === void 0 || existingResolver();
|
|
1229
|
+
resolve();
|
|
1230
|
+
};
|
|
1231
|
+
_connectRejecter = (error) => {
|
|
1232
|
+
existingRejecter === null || existingRejecter === void 0 || existingRejecter(error);
|
|
1233
|
+
reject(error);
|
|
1234
|
+
};
|
|
1235
|
+
});
|
|
1236
|
+
return new Promise((resolve, reject) => {
|
|
1237
|
+
_connectResolver = resolve;
|
|
1238
|
+
_connectRejecter = reject;
|
|
1239
|
+
attemptConnection();
|
|
1240
|
+
});
|
|
1241
|
+
},
|
|
1242
|
+
disconnect: () => {
|
|
1243
|
+
if (_reconnectTimeoutHandle) {
|
|
1244
|
+
clearTimeout(_reconnectTimeoutHandle);
|
|
1245
|
+
_reconnectTimeoutHandle = null;
|
|
1246
|
+
}
|
|
1247
|
+
if (_connectRejecter) {
|
|
1248
|
+
_connectRejecter(new WebSocketError("Disconnected by user"));
|
|
1249
|
+
_connectResolver = null;
|
|
1250
|
+
_connectRejecter = null;
|
|
1251
|
+
}
|
|
1252
|
+
cleanup();
|
|
1253
|
+
_state = { type: "disconnected" };
|
|
1254
|
+
_reconnectAttempt = 0;
|
|
1255
|
+
_messageQueue = [];
|
|
1256
|
+
emit(onEvent, {
|
|
1257
|
+
type: "disconnected",
|
|
1258
|
+
reason: "User disconnected"
|
|
1259
|
+
});
|
|
1260
|
+
},
|
|
1261
|
+
isConnected: () => {
|
|
1262
|
+
return _state.type === "connected";
|
|
1263
|
+
},
|
|
1264
|
+
sendPresenceSet: (data) => {
|
|
1265
|
+
const message = {
|
|
1266
|
+
type: "presence_set",
|
|
1267
|
+
data
|
|
1268
|
+
};
|
|
1269
|
+
if (_state.type === "connected") sendRaw(message);
|
|
1270
|
+
else if (_state.type === "reconnecting") {
|
|
1271
|
+
_messageQueue = _messageQueue.filter((message$1) => message$1.type !== "presence_set");
|
|
1272
|
+
_messageQueue.push(message);
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
sendPresenceClear: () => {
|
|
1276
|
+
const message = { type: "presence_clear" };
|
|
1277
|
+
if (_state.type === "connected") sendRaw(message);
|
|
1278
|
+
else if (_state.type === "reconnecting") {
|
|
1279
|
+
_messageQueue = _messageQueue.filter((message$1) => message$1.type !== "presence_clear");
|
|
1280
|
+
_messageQueue.push(message);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
//#endregion
|
|
1287
|
+
//#region src/client/StateMonitor.ts
|
|
1288
|
+
var StateMonitor_exports = /* @__PURE__ */ __export({
|
|
1289
|
+
determineRecoveryAction: () => determineRecoveryAction,
|
|
1290
|
+
make: () => make
|
|
1291
|
+
});
|
|
1292
|
+
/**
|
|
1293
|
+
* Creates a new StateMonitor.
|
|
1294
|
+
*/
|
|
1295
|
+
const make = (options = {}) => {
|
|
1296
|
+
const { onEvent, healthCheckInterval = 5e3, stalePendingThreshold = 1e4, maxVersionGap = 10 } = options;
|
|
1297
|
+
let _expectedVersion = 0;
|
|
1298
|
+
let _pendingMap = /* @__PURE__ */ new Map();
|
|
1299
|
+
let _isRecovering = false;
|
|
1300
|
+
let _healthCheckHandle = null;
|
|
1301
|
+
/**
|
|
1302
|
+
* Emits an event if handler is provided.
|
|
1303
|
+
*/
|
|
1304
|
+
const emit = (event) => {
|
|
1305
|
+
onEvent === null || onEvent === void 0 || onEvent(event);
|
|
1306
|
+
};
|
|
1307
|
+
/**
|
|
1308
|
+
* Checks if there's a version gap indicating drift.
|
|
1309
|
+
*/
|
|
1310
|
+
const checkVersionGap = (receivedVersion) => {
|
|
1311
|
+
const expectedNext = _expectedVersion + 1;
|
|
1312
|
+
if (receivedVersion < expectedNext) return true;
|
|
1313
|
+
if (receivedVersion > expectedNext + maxVersionGap) {
|
|
1314
|
+
emit({
|
|
1315
|
+
type: "drift_detected",
|
|
1316
|
+
expectedVersion: expectedNext,
|
|
1317
|
+
receivedVersion
|
|
1318
|
+
});
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
return true;
|
|
1322
|
+
};
|
|
1323
|
+
/**
|
|
1324
|
+
* Runs a health check.
|
|
1325
|
+
*/
|
|
1326
|
+
const runHealthCheck = () => {
|
|
1327
|
+
const now = Date.now();
|
|
1328
|
+
let oldestPendingMs = null;
|
|
1329
|
+
for (const [id, info] of _pendingMap) {
|
|
1330
|
+
const elapsed = now - info.sentAt;
|
|
1331
|
+
if (oldestPendingMs === null || elapsed > oldestPendingMs) oldestPendingMs = elapsed;
|
|
1332
|
+
if (elapsed > stalePendingThreshold) emit({
|
|
1333
|
+
type: "pending_timeout",
|
|
1334
|
+
transactionId: id,
|
|
1335
|
+
elapsedMs: elapsed
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
emit({
|
|
1339
|
+
type: "health_check",
|
|
1340
|
+
pendingCount: _pendingMap.size,
|
|
1341
|
+
oldestPendingMs
|
|
1342
|
+
});
|
|
1343
|
+
};
|
|
1344
|
+
return {
|
|
1345
|
+
onServerVersion: (version) => {
|
|
1346
|
+
const isValid = checkVersionGap(version);
|
|
1347
|
+
if (isValid) _expectedVersion = Math.max(_expectedVersion, version);
|
|
1348
|
+
return isValid;
|
|
1349
|
+
},
|
|
1350
|
+
trackPending: (info) => {
|
|
1351
|
+
_pendingMap.set(info.id, info);
|
|
1352
|
+
},
|
|
1353
|
+
untrackPending: (id) => {
|
|
1354
|
+
_pendingMap.delete(id);
|
|
1355
|
+
},
|
|
1356
|
+
getStalePending: () => {
|
|
1357
|
+
const now = Date.now();
|
|
1358
|
+
const stale = [];
|
|
1359
|
+
for (const info of _pendingMap.values()) if (now - info.sentAt > stalePendingThreshold) stale.push(info);
|
|
1360
|
+
return stale;
|
|
1361
|
+
},
|
|
1362
|
+
getStatus: () => {
|
|
1363
|
+
const now = Date.now();
|
|
1364
|
+
let oldestPendingMs = null;
|
|
1365
|
+
for (const info of _pendingMap.values()) {
|
|
1366
|
+
const elapsed = now - info.sentAt;
|
|
1367
|
+
if (oldestPendingMs === null || elapsed > oldestPendingMs) oldestPendingMs = elapsed;
|
|
1368
|
+
}
|
|
1369
|
+
const isHealthy = !_isRecovering && (oldestPendingMs === null || oldestPendingMs < stalePendingThreshold * 2);
|
|
1370
|
+
return {
|
|
1371
|
+
expectedVersion: _expectedVersion,
|
|
1372
|
+
pendingCount: _pendingMap.size,
|
|
1373
|
+
oldestPendingMs,
|
|
1374
|
+
isHealthy,
|
|
1375
|
+
isRecovering: _isRecovering
|
|
1376
|
+
};
|
|
1377
|
+
},
|
|
1378
|
+
start: () => {
|
|
1379
|
+
if (_healthCheckHandle !== null) return;
|
|
1380
|
+
_healthCheckHandle = setInterval(runHealthCheck, healthCheckInterval);
|
|
1381
|
+
},
|
|
1382
|
+
stop: () => {
|
|
1383
|
+
if (_healthCheckHandle !== null) {
|
|
1384
|
+
clearInterval(_healthCheckHandle);
|
|
1385
|
+
_healthCheckHandle = null;
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
reset: (newVersion) => {
|
|
1389
|
+
_expectedVersion = newVersion;
|
|
1390
|
+
_pendingMap.clear();
|
|
1391
|
+
_isRecovering = false;
|
|
1392
|
+
emit({
|
|
1393
|
+
type: "recovery_completed",
|
|
1394
|
+
version: newVersion
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
};
|
|
1399
|
+
/**
|
|
1400
|
+
* Determines the appropriate recovery action based on current state.
|
|
1401
|
+
*/
|
|
1402
|
+
const determineRecoveryAction = (status, stalePending) => {
|
|
1403
|
+
if (!status.isHealthy || stalePending.length > 3) return { type: "request_snapshot" };
|
|
1404
|
+
if (stalePending.length > 0) return {
|
|
1405
|
+
type: "drop_pending",
|
|
1406
|
+
transactionIds: stalePending.map((p) => p.id)
|
|
1407
|
+
};
|
|
1408
|
+
return { type: "request_snapshot" };
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
//#endregion
|
|
1412
|
+
export { AuthenticationError, ClientDocument_exports as ClientDocument, ConnectionError, InvalidStateError, MimicClientError, NotConnectedError, Presence_exports as Presence, Rebase_exports as Rebase, RebaseError, StateDriftError, StateMonitor_exports as StateMonitor, TransactionRejectedError, TransactionTimeoutError, Transport_exports as Transport, WebSocketError, WebSocketTransport_exports as WebSocketTransport };
|
|
1413
|
+
//# sourceMappingURL=index.mjs.map
|