@voidhash/mimic 0.0.1-alpha.6 → 0.0.1-alpha.8

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