@voidhash/mimic 0.0.1-alpha.1

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 (57) hide show
  1. package/README.md +17 -0
  2. package/package.json +33 -0
  3. package/src/Document.ts +256 -0
  4. package/src/FractionalIndex.ts +1249 -0
  5. package/src/Operation.ts +59 -0
  6. package/src/OperationDefinition.ts +23 -0
  7. package/src/OperationPath.ts +197 -0
  8. package/src/Presence.ts +142 -0
  9. package/src/Primitive.ts +32 -0
  10. package/src/Proxy.ts +8 -0
  11. package/src/ProxyEnvironment.ts +52 -0
  12. package/src/Transaction.ts +72 -0
  13. package/src/Transform.ts +13 -0
  14. package/src/client/ClientDocument.ts +1163 -0
  15. package/src/client/Rebase.ts +309 -0
  16. package/src/client/StateMonitor.ts +307 -0
  17. package/src/client/Transport.ts +318 -0
  18. package/src/client/WebSocketTransport.ts +572 -0
  19. package/src/client/errors.ts +145 -0
  20. package/src/client/index.ts +61 -0
  21. package/src/index.ts +12 -0
  22. package/src/primitives/Array.ts +457 -0
  23. package/src/primitives/Boolean.ts +128 -0
  24. package/src/primitives/Lazy.ts +89 -0
  25. package/src/primitives/Literal.ts +128 -0
  26. package/src/primitives/Number.ts +169 -0
  27. package/src/primitives/String.ts +189 -0
  28. package/src/primitives/Struct.ts +348 -0
  29. package/src/primitives/Tree.ts +1120 -0
  30. package/src/primitives/TreeNode.ts +113 -0
  31. package/src/primitives/Union.ts +329 -0
  32. package/src/primitives/shared.ts +122 -0
  33. package/src/server/ServerDocument.ts +267 -0
  34. package/src/server/errors.ts +90 -0
  35. package/src/server/index.ts +40 -0
  36. package/tests/Document.test.ts +556 -0
  37. package/tests/FractionalIndex.test.ts +377 -0
  38. package/tests/OperationPath.test.ts +151 -0
  39. package/tests/Presence.test.ts +321 -0
  40. package/tests/Primitive.test.ts +381 -0
  41. package/tests/client/ClientDocument.test.ts +1398 -0
  42. package/tests/client/WebSocketTransport.test.ts +992 -0
  43. package/tests/primitives/Array.test.ts +418 -0
  44. package/tests/primitives/Boolean.test.ts +126 -0
  45. package/tests/primitives/Lazy.test.ts +143 -0
  46. package/tests/primitives/Literal.test.ts +122 -0
  47. package/tests/primitives/Number.test.ts +133 -0
  48. package/tests/primitives/String.test.ts +128 -0
  49. package/tests/primitives/Struct.test.ts +311 -0
  50. package/tests/primitives/Tree.test.ts +467 -0
  51. package/tests/primitives/TreeNode.test.ts +50 -0
  52. package/tests/primitives/Union.test.ts +210 -0
  53. package/tests/server/ServerDocument.test.ts +528 -0
  54. package/tsconfig.build.json +24 -0
  55. package/tsconfig.json +8 -0
  56. package/tsdown.config.ts +18 -0
  57. package/vitest.mts +11 -0
@@ -0,0 +1,556 @@
1
+ import { describe, expect, it } from "@effect/vitest";
2
+ import * as Document from "../src/Document";
3
+ import * as Primitive from "../src/Primitive";
4
+ import * as Transaction from "../src/Transaction";
5
+ import * as OperationPath from "../src/OperationPath";
6
+
7
+ describe("Document", () => {
8
+ describe("make", () => {
9
+ it("creates a document with a schema", () => {
10
+ const schema = Primitive.Struct({
11
+ name: Primitive.String(),
12
+ age: Primitive.Number(),
13
+ });
14
+
15
+ const doc = Document.make(schema);
16
+
17
+ expect(doc.schema).toBe(schema);
18
+ expect(doc.root).toBeDefined();
19
+ });
20
+
21
+ it("initializes with default values from schema", () => {
22
+ const schema = Primitive.Struct({
23
+ name: Primitive.String().default("John"),
24
+ age: Primitive.Number().default(25),
25
+ });
26
+
27
+ const doc = Document.make(schema);
28
+
29
+ expect(doc.get()).toEqual({ name: "John", age: 25 });
30
+ });
31
+
32
+ it("initializes with provided initial state", () => {
33
+ const schema = Primitive.Struct({
34
+ name: Primitive.String(),
35
+ age: Primitive.Number(),
36
+ });
37
+
38
+ const doc = Document.make(schema, {
39
+ initial: { name: "Jane", age: 30 },
40
+ });
41
+
42
+ expect(doc.get()).toEqual({ name: "Jane", age: 30 });
43
+ });
44
+
45
+ it("returns undefined state when no defaults or initial value", () => {
46
+ const schema = Primitive.Struct({
47
+ name: Primitive.String(),
48
+ age: Primitive.Number(),
49
+ });
50
+
51
+ const doc = Document.make(schema);
52
+
53
+ expect(doc.get()).toBeUndefined();
54
+ });
55
+ });
56
+
57
+ describe("root proxy", () => {
58
+ it("get() returns current field value", () => {
59
+ const schema = Primitive.Struct({
60
+ name: Primitive.String(),
61
+ });
62
+
63
+ const doc = Document.make(schema, {
64
+ initial: { name: "Alice" },
65
+ });
66
+
67
+ expect(doc.root.name.get()).toBe("Alice");
68
+ });
69
+
70
+ it("set() updates state and generates operation", () => {
71
+ const schema = Primitive.Struct({
72
+ name: Primitive.String(),
73
+ });
74
+
75
+ const doc = Document.make(schema, {
76
+ initial: { name: "Alice" },
77
+ });
78
+
79
+ doc.root.name.set("Bob");
80
+
81
+ expect(doc.root.name.get()).toBe("Bob");
82
+ expect(doc.get()).toEqual({ name: "Bob" });
83
+ });
84
+
85
+ it("nested field access works correctly", () => {
86
+ const schema = Primitive.Struct({
87
+ user: Primitive.Struct({
88
+ profile: Primitive.Struct({
89
+ email: Primitive.String(),
90
+ }),
91
+ }),
92
+ });
93
+
94
+ const doc = Document.make(schema, {
95
+ initial: {
96
+ user: {
97
+ profile: {
98
+ email: "old@example.com",
99
+ },
100
+ },
101
+ },
102
+ });
103
+
104
+ expect(doc.root.user.profile.email.get()).toBe("old@example.com");
105
+
106
+ doc.root.user.profile.email.set("new@example.com");
107
+
108
+ expect(doc.root.user.profile.email.get()).toBe("new@example.com");
109
+ });
110
+ });
111
+
112
+ describe("transaction", () => {
113
+ it("commits multiple operations atomically", () => {
114
+ const schema = Primitive.Struct({
115
+ name: Primitive.String(),
116
+ age: Primitive.Number(),
117
+ });
118
+
119
+ const doc = Document.make(schema, {
120
+ initial: { name: "Alice", age: 20 },
121
+ });
122
+
123
+ doc.transaction((root) => {
124
+ root.name.set("Bob");
125
+ root.age.set(30);
126
+ });
127
+
128
+ expect(doc.get()).toEqual({ name: "Bob", age: 30 });
129
+ });
130
+
131
+ it("returns the result of the transaction function", () => {
132
+ const schema = Primitive.Struct({
133
+ name: Primitive.String(),
134
+ });
135
+
136
+ const doc = Document.make(schema, {
137
+ initial: { name: "Alice" },
138
+ });
139
+
140
+ const result = doc.transaction((root) => {
141
+ root.name.set("Bob");
142
+ return "success";
143
+ });
144
+
145
+ expect(result).toBe("success");
146
+ });
147
+
148
+ it("rolls back on error", () => {
149
+ const schema = Primitive.Struct({
150
+ name: Primitive.String(),
151
+ age: Primitive.Number(),
152
+ });
153
+
154
+ const doc = Document.make(schema, {
155
+ initial: { name: "Alice", age: 20 },
156
+ });
157
+
158
+ expect(() => {
159
+ doc.transaction((root) => {
160
+ root.name.set("Bob");
161
+ throw new Error("Intentional error");
162
+ });
163
+ }).toThrow("Intentional error");
164
+
165
+ // State should be rolled back
166
+ expect(doc.get()).toEqual({ name: "Alice", age: 20 });
167
+ });
168
+
169
+ it("reads updated values within transaction", () => {
170
+ const schema = Primitive.Struct({
171
+ count: Primitive.Number(),
172
+ });
173
+
174
+ const doc = Document.make(schema, {
175
+ initial: { count: 0 },
176
+ });
177
+
178
+ doc.transaction((root) => {
179
+ root.count.set(1);
180
+ expect(root.count.get()).toBe(1);
181
+
182
+ root.count.set(2);
183
+ expect(root.count.get()).toBe(2);
184
+ });
185
+
186
+ expect(doc.root.count.get()).toBe(2);
187
+ });
188
+
189
+ it("throws NestedTransactionError for nested transactions", () => {
190
+ const schema = Primitive.Struct({
191
+ name: Primitive.String(),
192
+ });
193
+
194
+ const doc = Document.make(schema);
195
+
196
+ expect(() => {
197
+ doc.transaction((root) => {
198
+ doc.transaction((innerRoot) => {
199
+ innerRoot.name.set("nested");
200
+ });
201
+ });
202
+ }).toThrow(Document.NestedTransactionError);
203
+ });
204
+
205
+ it("operations outside transaction are auto-wrapped", () => {
206
+ const schema = Primitive.Struct({
207
+ name: Primitive.String(),
208
+ });
209
+
210
+ const doc = Document.make(schema, {
211
+ initial: { name: "Alice" },
212
+ });
213
+
214
+ // Direct set outside transaction
215
+ doc.root.name.set("Bob");
216
+
217
+ expect(doc.get()).toEqual({ name: "Bob" });
218
+
219
+ // Should have pending operations
220
+ const tx = doc.flush();
221
+ expect(tx.ops).toHaveLength(1);
222
+ expect(tx.ops[0]!.kind).toBe("string.set");
223
+ });
224
+ });
225
+
226
+ describe("flush", () => {
227
+ it("returns pending operations as a transaction", () => {
228
+ const schema = Primitive.Struct({
229
+ name: Primitive.String(),
230
+ age: Primitive.Number(),
231
+ });
232
+
233
+ const doc = Document.make(schema, {
234
+ initial: { name: "Alice", age: 20 },
235
+ });
236
+
237
+ doc.transaction((root) => {
238
+ root.name.set("Bob");
239
+ root.age.set(30);
240
+ });
241
+
242
+ const tx = doc.flush();
243
+
244
+ expect(tx.ops).toHaveLength(2);
245
+ expect(tx.id).toBeDefined();
246
+ expect(tx.timestamp).toBeDefined();
247
+ });
248
+
249
+ it("clears pending operations after flush", () => {
250
+ const schema = Primitive.Struct({
251
+ name: Primitive.String(),
252
+ });
253
+
254
+ const doc = Document.make(schema, {
255
+ initial: { name: "Alice" },
256
+ });
257
+
258
+ doc.root.name.set("Bob");
259
+
260
+ const tx1 = doc.flush();
261
+ expect(tx1.ops).toHaveLength(1);
262
+
263
+ const tx2 = doc.flush();
264
+ expect(tx2.ops).toHaveLength(0);
265
+ });
266
+
267
+ it("returns empty transaction when no pending operations", () => {
268
+ const schema = Primitive.Struct({
269
+ name: Primitive.String(),
270
+ });
271
+
272
+ const doc = Document.make(schema);
273
+
274
+ const tx = doc.flush();
275
+
276
+ expect(tx.ops).toHaveLength(0);
277
+ expect(Transaction.isEmpty(tx)).toBe(true);
278
+ });
279
+ });
280
+
281
+ describe("apply", () => {
282
+ it("applies external operations to state", () => {
283
+ const schema = Primitive.Struct({
284
+ name: Primitive.String(),
285
+ });
286
+
287
+ const doc = Document.make(schema, {
288
+ initial: { name: "Alice" },
289
+ });
290
+
291
+ doc.apply([
292
+ {
293
+ kind: "string.set",
294
+ path: OperationPath.make("name"),
295
+ payload: "Bob",
296
+ },
297
+ ]);
298
+
299
+ expect(doc.get()).toEqual({ name: "Bob" });
300
+ });
301
+
302
+ it("applied operations are not added to pending", () => {
303
+ const schema = Primitive.Struct({
304
+ name: Primitive.String(),
305
+ });
306
+
307
+ const doc = Document.make(schema, {
308
+ initial: { name: "Alice" },
309
+ });
310
+
311
+ doc.apply([
312
+ {
313
+ kind: "string.set",
314
+ path: OperationPath.make("name"),
315
+ payload: "Bob",
316
+ },
317
+ ]);
318
+
319
+ const tx = doc.flush();
320
+ expect(tx.ops).toHaveLength(0);
321
+ });
322
+
323
+ it("applies multiple operations in sequence", () => {
324
+ const schema = Primitive.Struct({
325
+ name: Primitive.String(),
326
+ age: Primitive.Number(),
327
+ });
328
+
329
+ const doc = Document.make(schema, {
330
+ initial: { name: "Alice", age: 20 },
331
+ });
332
+
333
+ doc.apply([
334
+ {
335
+ kind: "string.set",
336
+ path: OperationPath.make("name"),
337
+ payload: "Bob",
338
+ },
339
+ {
340
+ kind: "number.set",
341
+ path: OperationPath.make("age"),
342
+ payload: 30,
343
+ },
344
+ ]);
345
+
346
+ expect(doc.get()).toEqual({ name: "Bob", age: 30 });
347
+ });
348
+
349
+ it("throws OperationError for invalid operations", () => {
350
+ const schema = Primitive.Struct({
351
+ name: Primitive.String(),
352
+ });
353
+
354
+ const doc = Document.make(schema, {
355
+ initial: { name: "Alice" },
356
+ });
357
+
358
+ expect(() => {
359
+ doc.apply([
360
+ {
361
+ kind: "string.set",
362
+ path: OperationPath.make("name"),
363
+ payload: 123, // Invalid: number instead of string
364
+ },
365
+ ]);
366
+ }).toThrow(Document.OperationError);
367
+ });
368
+ });
369
+
370
+ describe("arrays", () => {
371
+ it("works with array primitives", () => {
372
+ const schema = Primitive.Struct({
373
+ items: Primitive.Array(Primitive.String()),
374
+ });
375
+
376
+ const doc = Document.make(schema, {
377
+ initial: { items: [] },
378
+ });
379
+
380
+ doc.root.items.push("first");
381
+
382
+ const items = doc.root.items.get();
383
+ expect(items).toHaveLength(1);
384
+ expect(items[0]!.value).toBe("first");
385
+ });
386
+
387
+ it("array operations generate correct pending ops", () => {
388
+ const schema = Primitive.Struct({
389
+ items: Primitive.Array(Primitive.String()),
390
+ });
391
+
392
+ const doc = Document.make(schema, {
393
+ initial: { items: [] },
394
+ });
395
+
396
+ doc.transaction((root) => {
397
+ root.items.push("first");
398
+ root.items.push("second");
399
+ });
400
+
401
+ const tx = doc.flush();
402
+ expect(tx.ops).toHaveLength(2);
403
+ expect(tx.ops[0]!.kind).toBe("array.insert");
404
+ expect(tx.ops[1]!.kind).toBe("array.insert");
405
+ });
406
+
407
+ it("modifying array elements works", () => {
408
+ const schema = Primitive.Struct({
409
+ users: Primitive.Array(
410
+ Primitive.Struct({
411
+ name: Primitive.String(),
412
+ })
413
+ ),
414
+ });
415
+
416
+ const entryId = "test-entry-id";
417
+ const doc = Document.make(schema, {
418
+ initial: {
419
+ users: [{ id: entryId, pos: "a0", value: { name: "Alice" } }],
420
+ },
421
+ });
422
+
423
+ doc.root.users.at(entryId).name.set("Bob");
424
+
425
+ const users = doc.root.users.get();
426
+ expect(users[0]!.value.name).toBe("Bob");
427
+ });
428
+ });
429
+
430
+ describe("complex scenarios", () => {
431
+ it("handles interleaved local and remote operations", () => {
432
+ const schema = Primitive.Struct({
433
+ counter: Primitive.Number(),
434
+ });
435
+
436
+ const doc = Document.make(schema, {
437
+ initial: { counter: 0 },
438
+ });
439
+
440
+ // Local operation
441
+ doc.root.counter.set(1);
442
+
443
+ // Remote operation
444
+ doc.apply([
445
+ {
446
+ kind: "number.set",
447
+ path: OperationPath.make("counter"),
448
+ payload: 10,
449
+ },
450
+ ]);
451
+
452
+ // Another local operation
453
+ doc.root.counter.set(11);
454
+
455
+ expect(doc.get()).toEqual({ counter: 11 });
456
+
457
+ // Only local ops should be pending
458
+ const tx = doc.flush();
459
+ expect(tx.ops).toHaveLength(2);
460
+ });
461
+
462
+ it("handles struct with all primitive types", () => {
463
+ const schema = Primitive.Struct({
464
+ str: Primitive.String(),
465
+ num: Primitive.Number(),
466
+ bool: Primitive.Boolean(),
467
+ literal: Primitive.Literal("status" as const),
468
+ });
469
+
470
+ const doc = Document.make(schema, {
471
+ initial: {
472
+ str: "hello",
473
+ num: 42,
474
+ bool: true,
475
+ literal: "status",
476
+ },
477
+ });
478
+
479
+ doc.transaction((root) => {
480
+ root.str.set("world");
481
+ root.num.set(100);
482
+ root.bool.set(false);
483
+ });
484
+
485
+ expect(doc.get()).toEqual({
486
+ str: "world",
487
+ num: 100,
488
+ bool: false,
489
+ literal: "status",
490
+ });
491
+ });
492
+ });
493
+
494
+ describe("toSnapshot", () => {
495
+ it("returns snapshot of document state", () => {
496
+ const schema = Primitive.Struct({
497
+ name: Primitive.String(),
498
+ age: Primitive.Number(),
499
+ });
500
+
501
+ const doc = Document.make(schema, {
502
+ initial: { name: "Alice", age: 30 },
503
+ });
504
+
505
+ const snapshot = doc.toSnapshot();
506
+
507
+ expect(snapshot).toEqual({ name: "Alice", age: 30 });
508
+ });
509
+
510
+ it("respects field defaults in snapshot", () => {
511
+ const schema = Primitive.Struct({
512
+ name: Primitive.String().default("Unknown"),
513
+ count: Primitive.Number().default(0),
514
+ });
515
+
516
+ const doc = Document.make(schema);
517
+
518
+ const snapshot = doc.toSnapshot();
519
+
520
+ expect(snapshot).toEqual({ name: "Unknown", count: 0 });
521
+ });
522
+
523
+ it("reflects state changes in snapshot", () => {
524
+ const schema = Primitive.Struct({
525
+ name: Primitive.String(),
526
+ });
527
+
528
+ const doc = Document.make(schema, {
529
+ initial: { name: "Alice" },
530
+ });
531
+
532
+ expect(doc.toSnapshot()).toEqual({ name: "Alice" });
533
+
534
+ doc.root.name.set("Bob");
535
+
536
+ expect(doc.toSnapshot()).toEqual({ name: "Bob" });
537
+ });
538
+
539
+ it("handles arrays in snapshot", () => {
540
+ const schema = Primitive.Struct({
541
+ items: Primitive.Array(Primitive.String()),
542
+ });
543
+
544
+ const doc = Document.make(schema);
545
+
546
+ doc.root.items.push("first");
547
+ doc.root.items.push("second");
548
+
549
+ const snapshot = doc.toSnapshot();
550
+
551
+ expect(snapshot?.items).toHaveLength(2);
552
+ expect(snapshot?.items[0]?.value).toBe("first");
553
+ expect(snapshot?.items[1]?.value).toBe("second");
554
+ });
555
+ });
556
+ });