@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,528 @@
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, { initial: 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
+ });
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "Preserve",
4
+ "lib": ["es2022", "dom", "dom.iterable"],
5
+ "target": "es2022",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "declarationDir": "dist",
9
+ "outDir": "./dist",
10
+ "strict": true,
11
+ "strictNullChecks": true,
12
+ "noUnusedLocals": false,
13
+ "noUnusedParameters": true,
14
+ "noImplicitReturns": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "noUncheckedIndexedAccess": true,
17
+ "esModuleInterop": true,
18
+ "skipLibCheck": true,
19
+ "noPropertyAccessFromIndexSignature": true,
20
+ "noImplicitOverride": true
21
+ },
22
+ "include": ["src"],
23
+ "exclude": ["test", "**/*.test.ts", "**/*.test.tsx", "__tests__"]
24
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.build.json",
3
+ "include": ["src", "test"],
4
+ "compilerOptions": {
5
+ "allowJs": false,
6
+ "strictNullChecks": true
7
+ }
8
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export const input = ["./src/index.ts"];
4
+
5
+ export default defineConfig({
6
+ target: ["es2017"],
7
+ entry: input,
8
+ dts: {
9
+ sourcemap: true,
10
+ tsconfig: "./tsconfig.build.json",
11
+ },
12
+ // unbundle: true,
13
+ format: ["cjs", "esm"],
14
+ outExtensions: (ctx) => ({
15
+ dts: ctx.format === "cjs" ? ".d.cts" : ".d.mts",
16
+ js: ctx.format === "cjs" ? ".cjs" : ".mjs",
17
+ }),
18
+ });
package/vitest.mts ADDED
@@ -0,0 +1,11 @@
1
+ import tsconfigPaths from "vite-tsconfig-paths";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ export default defineConfig({
5
+ plugins: [tsconfigPaths()],
6
+ test: {
7
+ include: ["./**/*.test.ts"],
8
+ exclude: ["./node_modules/**"],
9
+ reporters: ["verbose"],
10
+ },
11
+ });