@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,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