@voidhash/mimic 1.0.0-beta.16 → 1.0.0-beta.18

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 (95) hide show
  1. package/dist/EffectSchema.cjs +3 -3
  2. package/dist/EffectSchema.d.cts +5 -5
  3. package/dist/EffectSchema.d.cts.map +1 -1
  4. package/dist/EffectSchema.d.mts +5 -5
  5. package/dist/EffectSchema.d.mts.map +1 -1
  6. package/dist/EffectSchema.mjs +3 -3
  7. package/dist/EffectSchema.mjs.map +1 -1
  8. package/dist/FractionalIndex.mjs.map +1 -1
  9. package/dist/Operation.d.cts +4 -4
  10. package/dist/Operation.d.cts.map +1 -1
  11. package/dist/Operation.d.mts +4 -4
  12. package/dist/Operation.d.mts.map +1 -1
  13. package/dist/Operation.mjs.map +1 -1
  14. package/dist/OperationDefinition.d.cts +2 -2
  15. package/dist/OperationDefinition.d.cts.map +1 -1
  16. package/dist/OperationDefinition.d.mts +2 -2
  17. package/dist/OperationDefinition.d.mts.map +1 -1
  18. package/dist/OperationDefinition.mjs.map +1 -1
  19. package/dist/Presence.mjs.map +1 -1
  20. package/dist/SchemaJSON.cjs +305 -0
  21. package/dist/SchemaJSON.d.cts +11 -0
  22. package/dist/SchemaJSON.d.cts.map +1 -0
  23. package/dist/SchemaJSON.d.mts +11 -0
  24. package/dist/SchemaJSON.d.mts.map +1 -0
  25. package/dist/SchemaJSON.mjs +301 -0
  26. package/dist/SchemaJSON.mjs.map +1 -0
  27. package/dist/index.cjs +7 -0
  28. package/dist/index.d.cts +2 -1
  29. package/dist/index.d.mts +2 -1
  30. package/dist/index.mjs +2 -1
  31. package/dist/primitives/Array.cjs +12 -2
  32. package/dist/primitives/Array.d.cts.map +1 -1
  33. package/dist/primitives/Array.d.mts.map +1 -1
  34. package/dist/primitives/Array.mjs +12 -2
  35. package/dist/primitives/Array.mjs.map +1 -1
  36. package/dist/primitives/Boolean.mjs.map +1 -1
  37. package/dist/primitives/Either.mjs.map +1 -1
  38. package/dist/primitives/Literal.mjs.map +1 -1
  39. package/dist/primitives/Number.cjs +27 -5
  40. package/dist/primitives/Number.d.cts.map +1 -1
  41. package/dist/primitives/Number.d.mts.map +1 -1
  42. package/dist/primitives/Number.mjs +27 -5
  43. package/dist/primitives/Number.mjs.map +1 -1
  44. package/dist/primitives/String.cjs +44 -13
  45. package/dist/primitives/String.d.cts.map +1 -1
  46. package/dist/primitives/String.d.mts.map +1 -1
  47. package/dist/primitives/String.mjs +44 -13
  48. package/dist/primitives/String.mjs.map +1 -1
  49. package/dist/primitives/Union.mjs.map +1 -1
  50. package/dist/primitives/shared.d.cts +2 -0
  51. package/dist/primitives/shared.d.cts.map +1 -1
  52. package/dist/primitives/shared.d.mts +2 -0
  53. package/dist/primitives/shared.d.mts.map +1 -1
  54. package/dist/primitives/shared.mjs.map +1 -1
  55. package/package.json +15 -8
  56. package/src/EffectSchema.ts +3 -3
  57. package/src/FractionalIndex.ts +18 -18
  58. package/src/Operation.ts +5 -5
  59. package/src/OperationDefinition.ts +2 -2
  60. package/src/Presence.ts +3 -3
  61. package/src/SchemaJSON.ts +396 -0
  62. package/src/index.ts +1 -0
  63. package/src/primitives/Array.ts +18 -8
  64. package/src/primitives/Boolean.ts +2 -2
  65. package/src/primitives/Either.ts +2 -2
  66. package/src/primitives/Literal.ts +2 -2
  67. package/src/primitives/Number.ts +44 -22
  68. package/src/primitives/String.ts +61 -34
  69. package/src/primitives/Union.ts +1 -1
  70. package/src/primitives/shared.ts +2 -0
  71. package/.turbo/turbo-build.log +0 -270
  72. package/tests/Document.test.ts +0 -557
  73. package/tests/EffectSchema.test.ts +0 -546
  74. package/tests/FractionalIndex.test.ts +0 -377
  75. package/tests/OperationPath.test.ts +0 -151
  76. package/tests/Presence.test.ts +0 -321
  77. package/tests/Primitive.test.ts +0 -381
  78. package/tests/client/ClientDocument.test.ts +0 -1981
  79. package/tests/client/WebSocketTransport.test.ts +0 -1217
  80. package/tests/primitives/Array.test.ts +0 -526
  81. package/tests/primitives/Boolean.test.ts +0 -126
  82. package/tests/primitives/Either.test.ts +0 -707
  83. package/tests/primitives/Lazy.test.ts +0 -143
  84. package/tests/primitives/Literal.test.ts +0 -122
  85. package/tests/primitives/Number.test.ts +0 -133
  86. package/tests/primitives/String.test.ts +0 -128
  87. package/tests/primitives/Struct.test.ts +0 -1154
  88. package/tests/primitives/Tree.test.ts +0 -1139
  89. package/tests/primitives/TreeNode.test.ts +0 -50
  90. package/tests/primitives/Union.test.ts +0 -554
  91. package/tests/server/ServerDocument.test.ts +0 -903
  92. package/tsconfig.build.json +0 -24
  93. package/tsconfig.json +0 -8
  94. package/tsdown.config.ts +0 -18
  95. package/vitest.mts +0 -11
@@ -1,903 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
- import * as Primitive from "../../src/Primitive";
3
- import * as Transaction from "../../src/Transaction";
4
- import * as Document from "../../src/Document";
5
- import * as ServerDocument from "../../src/server/ServerDocument";
6
-
7
- // =============================================================================
8
- // Test Schema
9
- // =============================================================================
10
-
11
- const TestSchema = Primitive.Struct({
12
- title: Primitive.String().default(""),
13
- count: Primitive.Number().default(0),
14
- items: Primitive.Array(
15
- Primitive.Struct({
16
- name: Primitive.String(),
17
- done: Primitive.Boolean().default(false),
18
- })
19
- ),
20
- });
21
-
22
- type TestState = Primitive.InferState<typeof TestSchema>;
23
-
24
- // Default initial state matching schema defaults
25
- const defaultInitialState: TestState = {
26
- title: "",
27
- count: 0,
28
- items: [],
29
- };
30
-
31
- // =============================================================================
32
- // Helper Functions
33
- // =============================================================================
34
-
35
- /**
36
- * Creates a transaction using a Document to generate valid ops.
37
- * Note: The proxy uses .set() method calls, not direct assignment.
38
- */
39
- const createTransactionFromDoc = <TSchema extends Primitive.AnyPrimitive>(
40
- schema: TSchema,
41
- initialState: Primitive.InferState<TSchema> | undefined,
42
- fn: (root: Primitive.InferProxy<TSchema>) => void
43
- ): Transaction.Transaction => {
44
- const doc = Document.make(schema, { initialState: initialState });
45
- doc.transaction(fn);
46
- return doc.flush();
47
- };
48
-
49
- // =============================================================================
50
- // ServerDocument Tests
51
- // =============================================================================
52
-
53
- describe("ServerDocument", () => {
54
- let broadcastMessages: ServerDocument.TransactionMessage[];
55
- let rejections: Array<{ transactionId: string; reason: string }>;
56
- let onBroadcast: (message: ServerDocument.TransactionMessage) => void;
57
- let onRejection: (transactionId: string, reason: string) => void;
58
-
59
- beforeEach(() => {
60
- broadcastMessages = [];
61
- rejections = [];
62
- onBroadcast = (message) => broadcastMessages.push(message);
63
- onRejection = (transactionId, reason) =>
64
- rejections.push({ transactionId, reason });
65
- });
66
-
67
- describe("make", () => {
68
- it("should create a server document with default state from schema", () => {
69
- const server = ServerDocument.make({
70
- schema: TestSchema,
71
- onBroadcast,
72
- });
73
-
74
- // Schema defaults may not include items array, check what we get
75
- const state = server.get();
76
- expect(state?.title).toBe("");
77
- expect(state?.count).toBe(0);
78
- expect(server.getVersion()).toBe(0);
79
- });
80
-
81
- it("should create a server document with initial state", () => {
82
- const initialState: TestState = {
83
- title: "Initial",
84
- count: 42,
85
- items: [],
86
- };
87
-
88
- const server = ServerDocument.make({
89
- schema: TestSchema,
90
- initialState,
91
- onBroadcast,
92
- });
93
-
94
- expect(server.get()).toEqual(initialState);
95
- });
96
-
97
- it("should create a server document with initial version", () => {
98
- const server = ServerDocument.make({
99
- schema: TestSchema,
100
- initialVersion: 100,
101
- onBroadcast,
102
- });
103
-
104
- expect(server.getVersion()).toBe(100);
105
- });
106
- });
107
-
108
- describe("submit", () => {
109
- it("should accept valid transactions and increment version", () => {
110
- const server = ServerDocument.make({
111
- schema: TestSchema,
112
- initialState: defaultInitialState,
113
- onBroadcast,
114
- });
115
-
116
- // Create a valid transaction using .set() method
117
- const tx = createTransactionFromDoc(
118
- TestSchema,
119
- defaultInitialState,
120
- (root) => {
121
- root.title.set("Updated Title");
122
- }
123
- );
124
-
125
- const result = server.submit(tx);
126
-
127
- expect(result.success).toBe(true);
128
- if (result.success) {
129
- expect(result.version).toBe(1);
130
- }
131
- expect(server.get()?.title).toBe("Updated Title");
132
- expect(server.getVersion()).toBe(1);
133
- });
134
-
135
- it("should broadcast confirmed transactions", () => {
136
- const server = ServerDocument.make({
137
- schema: TestSchema,
138
- initialState: defaultInitialState,
139
- onBroadcast,
140
- });
141
-
142
- const tx = createTransactionFromDoc(
143
- TestSchema,
144
- defaultInitialState,
145
- (root) => {
146
- root.count.set(10);
147
- }
148
- );
149
-
150
- server.submit(tx);
151
-
152
- expect(broadcastMessages).toHaveLength(1);
153
- expect(broadcastMessages[0]).toEqual({
154
- type: "transaction",
155
- transaction: tx,
156
- version: 1,
157
- });
158
- });
159
-
160
- it("should reject empty transactions", () => {
161
- const server = ServerDocument.make({
162
- schema: TestSchema,
163
- initialState: defaultInitialState,
164
- onBroadcast,
165
- onRejection,
166
- });
167
-
168
- const emptyTx: Transaction.Transaction = {
169
- id: crypto.randomUUID(),
170
- ops: [],
171
- timestamp: Date.now(),
172
- };
173
-
174
- const result = server.submit(emptyTx);
175
-
176
- expect(result.success).toBe(false);
177
- if (!result.success) {
178
- expect(result.reason).toBe("Transaction is empty");
179
- }
180
- expect(server.getVersion()).toBe(0);
181
- expect(broadcastMessages).toHaveLength(0);
182
- });
183
-
184
- it("should reject duplicate transactions", () => {
185
- const server = ServerDocument.make({
186
- schema: TestSchema,
187
- initialState: defaultInitialState,
188
- onBroadcast,
189
- onRejection,
190
- });
191
-
192
- const tx = createTransactionFromDoc(
193
- TestSchema,
194
- defaultInitialState,
195
- (root) => {
196
- root.title.set("First");
197
- }
198
- );
199
-
200
- // Submit once - should succeed
201
- const result1 = server.submit(tx);
202
- expect(result1.success).toBe(true);
203
-
204
- // Submit again - should be rejected as duplicate
205
- const result2 = server.submit(tx);
206
- expect(result2.success).toBe(false);
207
- if (!result2.success) {
208
- expect(result2.reason).toBe("Transaction has already been processed");
209
- }
210
-
211
- // Version should not have incremented for duplicate
212
- expect(server.getVersion()).toBe(1);
213
- expect(broadcastMessages).toHaveLength(1);
214
- });
215
-
216
- it("should call onRejection callback for rejected transactions", () => {
217
- const server = ServerDocument.make({
218
- schema: TestSchema,
219
- initialState: defaultInitialState,
220
- onBroadcast,
221
- onRejection,
222
- });
223
-
224
- const emptyTx: Transaction.Transaction = {
225
- id: "test-tx-id",
226
- ops: [],
227
- timestamp: Date.now(),
228
- };
229
-
230
- server.submit(emptyTx);
231
-
232
- expect(rejections).toHaveLength(1);
233
- expect(rejections[0]).toEqual({
234
- transactionId: "test-tx-id",
235
- reason: "Transaction is empty",
236
- });
237
- });
238
-
239
- it("should apply multiple transactions in sequence", () => {
240
- const server = ServerDocument.make({
241
- schema: TestSchema,
242
- initialState: defaultInitialState,
243
- onBroadcast,
244
- });
245
-
246
- // First transaction
247
- const tx1 = createTransactionFromDoc(
248
- TestSchema,
249
- server.get(),
250
- (root) => {
251
- root.title.set("First");
252
- }
253
- );
254
- server.submit(tx1);
255
-
256
- // Second transaction
257
- const tx2 = createTransactionFromDoc(
258
- TestSchema,
259
- server.get(),
260
- (root) => {
261
- root.count.set(5);
262
- }
263
- );
264
- server.submit(tx2);
265
-
266
- // Third transaction
267
- const tx3 = createTransactionFromDoc(
268
- TestSchema,
269
- server.get(),
270
- (root) => {
271
- root.title.set("Third");
272
- }
273
- );
274
- server.submit(tx3);
275
-
276
- expect(server.getVersion()).toBe(3);
277
- expect(server.get()?.title).toBe("Third");
278
- expect(server.get()?.count).toBe(5);
279
- expect(broadcastMessages).toHaveLength(3);
280
- });
281
- });
282
-
283
- describe("getSnapshot", () => {
284
- it("should return current state and version as snapshot", () => {
285
- const initialState: TestState = {
286
- title: "Snapshot Test",
287
- count: 99,
288
- items: [],
289
- };
290
-
291
- const server = ServerDocument.make({
292
- schema: TestSchema,
293
- initialState,
294
- initialVersion: 50,
295
- onBroadcast,
296
- });
297
-
298
- const snapshot = server.getSnapshot();
299
-
300
- expect(snapshot).toEqual({
301
- type: "snapshot",
302
- state: initialState,
303
- version: 50,
304
- });
305
- });
306
-
307
- it("should return updated snapshot after transactions", () => {
308
- const server = ServerDocument.make({
309
- schema: TestSchema,
310
- initialState: defaultInitialState,
311
- onBroadcast,
312
- });
313
-
314
- const tx = createTransactionFromDoc(
315
- TestSchema,
316
- defaultInitialState,
317
- (root) => {
318
- root.title.set("After Transaction");
319
- root.count.set(42);
320
- }
321
- );
322
- server.submit(tx);
323
-
324
- const snapshot = server.getSnapshot();
325
-
326
- expect(snapshot.type).toBe("snapshot");
327
- expect(snapshot.version).toBe(1);
328
- expect((snapshot.state as TestState)?.title).toBe("After Transaction");
329
- expect((snapshot.state as TestState)?.count).toBe(42);
330
- });
331
- });
332
-
333
- describe("hasProcessed", () => {
334
- it("should return false for unprocessed transactions", () => {
335
- const server = ServerDocument.make({
336
- schema: TestSchema,
337
- initialState: defaultInitialState,
338
- onBroadcast,
339
- });
340
-
341
- expect(server.hasProcessed("unknown-tx-id")).toBe(false);
342
- });
343
-
344
- it("should return true for processed transactions", () => {
345
- const server = ServerDocument.make({
346
- schema: TestSchema,
347
- initialState: defaultInitialState,
348
- onBroadcast,
349
- });
350
-
351
- const tx = createTransactionFromDoc(
352
- TestSchema,
353
- defaultInitialState,
354
- (root) => {
355
- root.title.set("Test");
356
- }
357
- );
358
- server.submit(tx);
359
-
360
- expect(server.hasProcessed(tx.id)).toBe(true);
361
- });
362
-
363
- it("should evict old transaction IDs when over limit", () => {
364
- const server = ServerDocument.make({
365
- schema: TestSchema,
366
- initialState: defaultInitialState,
367
- onBroadcast,
368
- maxTransactionHistory: 3,
369
- });
370
-
371
- const txIds: string[] = [];
372
-
373
- // Submit 5 transactions (limit is 3)
374
- for (let i = 0; i < 5; i++) {
375
- const tx = createTransactionFromDoc(
376
- TestSchema,
377
- server.get(),
378
- (root) => {
379
- root.count.set(i);
380
- }
381
- );
382
- txIds.push(tx.id);
383
- server.submit(tx);
384
- }
385
-
386
- // First 2 should have been evicted
387
- expect(server.hasProcessed(txIds[0]!)).toBe(false);
388
- expect(server.hasProcessed(txIds[1]!)).toBe(false);
389
-
390
- // Last 3 should still be tracked
391
- expect(server.hasProcessed(txIds[2]!)).toBe(true);
392
- expect(server.hasProcessed(txIds[3]!)).toBe(true);
393
- expect(server.hasProcessed(txIds[4]!)).toBe(true);
394
- });
395
- });
396
-
397
- describe("array operations", () => {
398
- it("should handle array insert operations", () => {
399
- const server = ServerDocument.make({
400
- schema: TestSchema,
401
- initialState: defaultInitialState,
402
- onBroadcast,
403
- });
404
-
405
- const tx = createTransactionFromDoc(
406
- TestSchema,
407
- defaultInitialState,
408
- (root) => {
409
- root.items.push({ name: "Item 1", done: false });
410
- }
411
- );
412
-
413
- const result = server.submit(tx);
414
-
415
- expect(result.success).toBe(true);
416
- expect(server.get()?.items).toHaveLength(1);
417
- expect(server.get()?.items[0]?.value.name).toBe("Item 1");
418
- });
419
-
420
- it("should handle multiple array operations", () => {
421
- const server = ServerDocument.make({
422
- schema: TestSchema,
423
- initialState: defaultInitialState,
424
- onBroadcast,
425
- });
426
-
427
- // Insert first item
428
- const tx1 = createTransactionFromDoc(
429
- TestSchema,
430
- server.get(),
431
- (root) => {
432
- root.items.push({ name: "Item 1", done: false });
433
- }
434
- );
435
- server.submit(tx1);
436
-
437
- // Insert second item
438
- const tx2 = createTransactionFromDoc(
439
- TestSchema,
440
- server.get(),
441
- (root) => {
442
- root.items.push({ name: "Item 2", done: true });
443
- }
444
- );
445
- server.submit(tx2);
446
-
447
- expect(server.get()?.items).toHaveLength(2);
448
- expect(server.getVersion()).toBe(2);
449
- });
450
- });
451
-
452
- describe("state isolation", () => {
453
- it("should not affect state on rejected transactions", () => {
454
- const initialState: TestState = {
455
- title: "Original",
456
- count: 0,
457
- items: [],
458
- };
459
-
460
- const server = ServerDocument.make({
461
- schema: TestSchema,
462
- initialState,
463
- onBroadcast,
464
- });
465
-
466
- // Submit empty transaction (will be rejected)
467
- const emptyTx: Transaction.Transaction = {
468
- id: crypto.randomUUID(),
469
- ops: [],
470
- timestamp: Date.now(),
471
- };
472
- server.submit(emptyTx);
473
-
474
- // State should be unchanged
475
- expect(server.get()).toEqual(initialState);
476
- });
477
- });
478
-
479
- describe("concurrent simulation", () => {
480
- it("should handle interleaved transactions from different clients", () => {
481
- const server = ServerDocument.make({
482
- schema: TestSchema,
483
- initialState: defaultInitialState,
484
- onBroadcast,
485
- });
486
-
487
- // Simulate Client A and Client B submitting interleaved transactions
488
- // Client A sets title
489
- const txA1 = createTransactionFromDoc(
490
- TestSchema,
491
- server.get(),
492
- (root) => {
493
- root.title.set("Client A Title");
494
- }
495
- );
496
- server.submit(txA1);
497
-
498
- // Client B sets count
499
- const txB1 = createTransactionFromDoc(
500
- TestSchema,
501
- server.get(),
502
- (root) => {
503
- root.count.set(100);
504
- }
505
- );
506
- server.submit(txB1);
507
-
508
- // Client A updates title again
509
- const txA2 = createTransactionFromDoc(
510
- TestSchema,
511
- server.get(),
512
- (root) => {
513
- root.title.set("Client A Final");
514
- }
515
- );
516
- server.submit(txA2);
517
-
518
- expect(server.getVersion()).toBe(3);
519
- expect(server.get()?.title).toBe("Client A Final");
520
- expect(server.get()?.count).toBe(100);
521
-
522
- // All broadcasts should have incremental versions
523
- expect(broadcastMessages[0]?.version).toBe(1);
524
- expect(broadcastMessages[1]?.version).toBe(2);
525
- expect(broadcastMessages[2]?.version).toBe(3);
526
- });
527
- });
528
-
529
- describe("validate (two-phase commit)", () => {
530
- it("should return valid=true with nextVersion for valid transaction", () => {
531
- const server = ServerDocument.make({
532
- schema: TestSchema,
533
- initialState: defaultInitialState,
534
- onBroadcast,
535
- });
536
-
537
- const tx = createTransactionFromDoc(
538
- TestSchema,
539
- defaultInitialState,
540
- (root) => {
541
- root.title.set("Test");
542
- }
543
- );
544
-
545
- const result = server.validate(tx);
546
-
547
- expect(result.valid).toBe(true);
548
- if (result.valid) {
549
- expect(result.nextVersion).toBe(1);
550
- }
551
- });
552
-
553
- it("should return valid=false for empty transaction", () => {
554
- const server = ServerDocument.make({
555
- schema: TestSchema,
556
- initialState: defaultInitialState,
557
- onBroadcast,
558
- });
559
-
560
- const emptyTx: Transaction.Transaction = {
561
- id: crypto.randomUUID(),
562
- ops: [],
563
- timestamp: Date.now(),
564
- };
565
-
566
- const result = server.validate(emptyTx);
567
-
568
- expect(result.valid).toBe(false);
569
- if (!result.valid) {
570
- expect(result.reason).toBe("Transaction is empty");
571
- }
572
- });
573
-
574
- it("should return valid=false for duplicate transaction", () => {
575
- const server = ServerDocument.make({
576
- schema: TestSchema,
577
- initialState: defaultInitialState,
578
- onBroadcast,
579
- });
580
-
581
- const tx = createTransactionFromDoc(
582
- TestSchema,
583
- defaultInitialState,
584
- (root) => {
585
- root.title.set("First");
586
- }
587
- );
588
-
589
- // Submit the transaction
590
- server.submit(tx);
591
-
592
- // Validate the same transaction again
593
- const result = server.validate(tx);
594
-
595
- expect(result.valid).toBe(false);
596
- if (!result.valid) {
597
- expect(result.reason).toBe("Transaction has already been processed");
598
- }
599
- });
600
-
601
- it("should NOT modify state during validation", () => {
602
- const server = ServerDocument.make({
603
- schema: TestSchema,
604
- initialState: defaultInitialState,
605
- onBroadcast,
606
- });
607
-
608
- const tx = createTransactionFromDoc(
609
- TestSchema,
610
- defaultInitialState,
611
- (root) => {
612
- root.title.set("Should Not Apply");
613
- }
614
- );
615
-
616
- const stateBefore = server.get();
617
- const versionBefore = server.getVersion();
618
-
619
- server.validate(tx);
620
-
621
- // State and version should be unchanged
622
- expect(server.get()).toEqual(stateBefore);
623
- expect(server.getVersion()).toBe(versionBefore);
624
- expect(broadcastMessages).toHaveLength(0);
625
- });
626
-
627
- it("should NOT add transaction to processed list during validation", () => {
628
- const server = ServerDocument.make({
629
- schema: TestSchema,
630
- initialState: defaultInitialState,
631
- onBroadcast,
632
- });
633
-
634
- const tx = createTransactionFromDoc(
635
- TestSchema,
636
- defaultInitialState,
637
- (root) => {
638
- root.title.set("Test");
639
- }
640
- );
641
-
642
- server.validate(tx);
643
-
644
- // Transaction should not be marked as processed
645
- expect(server.hasProcessed(tx.id)).toBe(false);
646
- });
647
-
648
- it("should return incrementing nextVersion for multiple validations", () => {
649
- const server = ServerDocument.make({
650
- schema: TestSchema,
651
- initialState: defaultInitialState,
652
- initialVersion: 5,
653
- onBroadcast,
654
- });
655
-
656
- const tx1 = createTransactionFromDoc(
657
- TestSchema,
658
- defaultInitialState,
659
- (root) => {
660
- root.title.set("First");
661
- }
662
- );
663
-
664
- const result1 = server.validate(tx1);
665
- expect(result1.valid).toBe(true);
666
- if (result1.valid) {
667
- expect(result1.nextVersion).toBe(6);
668
- }
669
-
670
- // Apply the first transaction
671
- server.apply(tx1);
672
-
673
- // Validate another transaction
674
- const tx2 = createTransactionFromDoc(
675
- TestSchema,
676
- server.get(),
677
- (root) => {
678
- root.count.set(10);
679
- }
680
- );
681
-
682
- const result2 = server.validate(tx2);
683
- expect(result2.valid).toBe(true);
684
- if (result2.valid) {
685
- expect(result2.nextVersion).toBe(7);
686
- }
687
- });
688
- });
689
-
690
- describe("apply (two-phase commit)", () => {
691
- it("should mutate state when applying transaction", () => {
692
- const server = ServerDocument.make({
693
- schema: TestSchema,
694
- initialState: defaultInitialState,
695
- onBroadcast,
696
- });
697
-
698
- const tx = createTransactionFromDoc(
699
- TestSchema,
700
- defaultInitialState,
701
- (root) => {
702
- root.title.set("Applied Title");
703
- root.count.set(42);
704
- }
705
- );
706
-
707
- server.apply(tx);
708
-
709
- expect(server.get()?.title).toBe("Applied Title");
710
- expect(server.get()?.count).toBe(42);
711
- });
712
-
713
- it("should increment version when applying transaction", () => {
714
- const server = ServerDocument.make({
715
- schema: TestSchema,
716
- initialState: defaultInitialState,
717
- initialVersion: 10,
718
- onBroadcast,
719
- });
720
-
721
- const tx = createTransactionFromDoc(
722
- TestSchema,
723
- defaultInitialState,
724
- (root) => {
725
- root.title.set("Test");
726
- }
727
- );
728
-
729
- server.apply(tx);
730
-
731
- expect(server.getVersion()).toBe(11);
732
- });
733
-
734
- it("should broadcast when applying transaction", () => {
735
- const server = ServerDocument.make({
736
- schema: TestSchema,
737
- initialState: defaultInitialState,
738
- onBroadcast,
739
- });
740
-
741
- const tx = createTransactionFromDoc(
742
- TestSchema,
743
- defaultInitialState,
744
- (root) => {
745
- root.title.set("Broadcast Test");
746
- }
747
- );
748
-
749
- server.apply(tx);
750
-
751
- expect(broadcastMessages).toHaveLength(1);
752
- expect(broadcastMessages[0]).toEqual({
753
- type: "transaction",
754
- transaction: tx,
755
- version: 1,
756
- });
757
- });
758
-
759
- it("should mark transaction as processed when applying", () => {
760
- const server = ServerDocument.make({
761
- schema: TestSchema,
762
- initialState: defaultInitialState,
763
- onBroadcast,
764
- });
765
-
766
- const tx = createTransactionFromDoc(
767
- TestSchema,
768
- defaultInitialState,
769
- (root) => {
770
- root.title.set("Test");
771
- }
772
- );
773
-
774
- expect(server.hasProcessed(tx.id)).toBe(false);
775
-
776
- server.apply(tx);
777
-
778
- expect(server.hasProcessed(tx.id)).toBe(true);
779
- });
780
- });
781
-
782
- describe("validate + apply (two-phase commit flow)", () => {
783
- it("should work correctly when used together", () => {
784
- const server = ServerDocument.make({
785
- schema: TestSchema,
786
- initialState: defaultInitialState,
787
- onBroadcast,
788
- });
789
-
790
- const tx = createTransactionFromDoc(
791
- TestSchema,
792
- defaultInitialState,
793
- (root) => {
794
- root.title.set("Two Phase");
795
- root.count.set(100);
796
- }
797
- );
798
-
799
- // Phase 1: Validate
800
- const validation = server.validate(tx);
801
- expect(validation.valid).toBe(true);
802
- if (validation.valid) {
803
- expect(validation.nextVersion).toBe(1);
804
- }
805
-
806
- // At this point, state should be unchanged
807
- expect(server.get()?.title).toBe("");
808
- expect(server.get()?.count).toBe(0);
809
- expect(server.getVersion()).toBe(0);
810
-
811
- // Phase 2: Apply (after WAL success would happen in real code)
812
- server.apply(tx);
813
-
814
- // Now state should be updated
815
- expect(server.get()?.title).toBe("Two Phase");
816
- expect(server.get()?.count).toBe(100);
817
- expect(server.getVersion()).toBe(1);
818
- expect(broadcastMessages).toHaveLength(1);
819
- });
820
-
821
- it("should not apply if validation fails", () => {
822
- const server = ServerDocument.make({
823
- schema: TestSchema,
824
- initialState: defaultInitialState,
825
- onBroadcast,
826
- });
827
-
828
- const emptyTx: Transaction.Transaction = {
829
- id: crypto.randomUUID(),
830
- ops: [],
831
- timestamp: Date.now(),
832
- };
833
-
834
- // Phase 1: Validate (should fail)
835
- const validation = server.validate(emptyTx);
836
- expect(validation.valid).toBe(false);
837
-
838
- // Since validation failed, we should NOT call apply
839
- // State should remain unchanged
840
- expect(server.get()?.title).toBe("");
841
- expect(server.getVersion()).toBe(0);
842
- expect(broadcastMessages).toHaveLength(0);
843
- });
844
-
845
- it("should handle multiple two-phase commit cycles", () => {
846
- const server = ServerDocument.make({
847
- schema: TestSchema,
848
- initialState: defaultInitialState,
849
- onBroadcast,
850
- });
851
-
852
- // First cycle
853
- const tx1 = createTransactionFromDoc(
854
- TestSchema,
855
- server.get(),
856
- (root) => {
857
- root.title.set("First");
858
- }
859
- );
860
-
861
- const v1 = server.validate(tx1);
862
- expect(v1.valid).toBe(true);
863
- server.apply(tx1);
864
-
865
- // Second cycle
866
- const tx2 = createTransactionFromDoc(
867
- TestSchema,
868
- server.get(),
869
- (root) => {
870
- root.count.set(50);
871
- }
872
- );
873
-
874
- const v2 = server.validate(tx2);
875
- expect(v2.valid).toBe(true);
876
- if (v2.valid) {
877
- expect(v2.nextVersion).toBe(2);
878
- }
879
- server.apply(tx2);
880
-
881
- // Third cycle
882
- const tx3 = createTransactionFromDoc(
883
- TestSchema,
884
- server.get(),
885
- (root) => {
886
- root.title.set("Third");
887
- }
888
- );
889
-
890
- const v3 = server.validate(tx3);
891
- expect(v3.valid).toBe(true);
892
- if (v3.valid) {
893
- expect(v3.nextVersion).toBe(3);
894
- }
895
- server.apply(tx3);
896
-
897
- expect(server.getVersion()).toBe(3);
898
- expect(server.get()?.title).toBe("Third");
899
- expect(server.get()?.count).toBe(50);
900
- expect(broadcastMessages).toHaveLength(3);
901
- });
902
- });
903
- });