@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,418 @@
1
+ import { describe, expect, it } from "@effect/vitest";
2
+ import * as Primitive from "../../src/Primitive";
3
+ import * as ProxyEnvironment from "../../src/ProxyEnvironment";
4
+ import * as OperationPath from "../../src/OperationPath";
5
+ import * as Operation from "../../src/Operation";
6
+
7
+ describe("ArrayPrimitive", () => {
8
+ // Helper to create a mock environment with state access
9
+ const createEnvWithState = (
10
+ state: Primitive.ArrayEntry<any>[] = []
11
+ ): { env: ReturnType<typeof ProxyEnvironment.make>; operations: Operation.Operation<any, any, any>[] } => {
12
+ const operations: Operation.Operation<any, any, any>[] = [];
13
+ let currentState = [...state];
14
+
15
+ const env = ProxyEnvironment.make({
16
+ onOperation: (op) => {
17
+ operations.push(op);
18
+ // Apply operation to keep state in sync for subsequent operations
19
+ if (op.kind === "array.insert") {
20
+ currentState.push(op.payload);
21
+ } else if (op.kind === "array.remove") {
22
+ currentState = currentState.filter(e => e.id !== op.payload.id);
23
+ } else if (op.kind === "array.move") {
24
+ currentState = currentState.map(e =>
25
+ e.id === op.payload.id ? { ...e, pos: op.payload.pos } : e
26
+ );
27
+ } else if (op.kind === "array.set") {
28
+ currentState = op.payload;
29
+ }
30
+ },
31
+ getState: () => currentState,
32
+ generateId: () => crypto.randomUUID(),
33
+ });
34
+
35
+ return { env, operations };
36
+ };
37
+
38
+ describe("proxy", () => {
39
+ it("set() generates array.set operation with entries", () => {
40
+ const { env, operations } = createEnvWithState();
41
+ const arrayPrimitive = Primitive.Array(Primitive.String());
42
+ const proxy = arrayPrimitive._internal.createProxy(env, OperationPath.make(""));
43
+
44
+ proxy.set(["a", "b", "c"]);
45
+
46
+ expect(operations).toHaveLength(1);
47
+ expect(operations[0]!.kind).toBe("array.set");
48
+
49
+ const entries = operations[0]!.payload as Primitive.ArrayEntry<string>[];
50
+ expect(entries).toHaveLength(3);
51
+ expect(entries[0]!.value).toBe("a");
52
+ expect(entries[1]!.value).toBe("b");
53
+ expect(entries[2]!.value).toBe("c");
54
+
55
+ // Each entry should have an ID and position
56
+ entries.forEach((entry: Primitive.ArrayEntry<string>) => {
57
+ expect(typeof entry.id).toBe("string");
58
+ expect(typeof entry.pos).toBe("string");
59
+ });
60
+
61
+ // Positions should be in order
62
+ expect(entries[0]!.pos < entries[1]!.pos).toBe(true);
63
+ expect(entries[1]!.pos < entries[2]!.pos).toBe(true);
64
+ });
65
+
66
+ it("push() generates array.insert operation with ID and position", () => {
67
+ const { env, operations } = createEnvWithState();
68
+ const arrayPrimitive = Primitive.Array(Primitive.String());
69
+ const proxy = arrayPrimitive._internal.createProxy(env, OperationPath.make(""));
70
+
71
+ proxy.push("newItem");
72
+
73
+ expect(operations).toHaveLength(1);
74
+ expect(operations[0]!.kind).toBe("array.insert");
75
+
76
+ const payload = operations[0]!.payload as { id: string; pos: string; value: string };
77
+ expect(payload.value).toBe("newItem");
78
+ expect(typeof payload.id).toBe("string");
79
+ expect(typeof payload.pos).toBe("string");
80
+ });
81
+
82
+ it("push() generates position after last element", () => {
83
+ const existingEntry = { id: "existing-id", pos: "a0", value: "existing" };
84
+ const { env, operations } = createEnvWithState([existingEntry]);
85
+ const arrayPrimitive = Primitive.Array(Primitive.String());
86
+ const proxy = arrayPrimitive._internal.createProxy(env, OperationPath.make(""));
87
+
88
+ proxy.push("newItem");
89
+
90
+ const payload = operations[0]!.payload as { id: string; pos: string; value: string };
91
+ expect(payload.pos > existingEntry.pos).toBe(true);
92
+ });
93
+
94
+ it("insertAt() generates array.insert with position between neighbors", () => {
95
+ const entries = [
96
+ { id: "id1", pos: "a0", value: "first" },
97
+ { id: "id2", pos: "a2", value: "third" },
98
+ ];
99
+ const { env, operations } = createEnvWithState(entries);
100
+ const arrayPrimitive = Primitive.Array(Primitive.String());
101
+ const proxy = arrayPrimitive._internal.createProxy(env, OperationPath.make(""));
102
+
103
+ proxy.insertAt(1, "second");
104
+
105
+ expect(operations).toHaveLength(1);
106
+ expect(operations[0]!.kind).toBe("array.insert");
107
+
108
+ const payload = operations[0]!.payload as { id: string; pos: string; value: string };
109
+ expect(payload.value).toBe("second");
110
+ // Position should be between a0 and a2
111
+ expect(payload.pos > "a0").toBe(true);
112
+ expect(payload.pos < "a2").toBe(true);
113
+ });
114
+
115
+ it("insertAt(0) generates position before first element", () => {
116
+ const entries = [{ id: "id1", pos: "a0", value: "existing" }];
117
+ const { env, operations } = createEnvWithState(entries);
118
+ const arrayPrimitive = Primitive.Array(Primitive.String());
119
+ const proxy = arrayPrimitive._internal.createProxy(env, OperationPath.make(""));
120
+
121
+ proxy.insertAt(0, "first");
122
+
123
+ const payload = operations[0]!.payload as { id: string; pos: string; value: string };
124
+ expect(payload.pos < "a0").toBe(true);
125
+ });
126
+
127
+ it("remove() generates array.remove operation with ID", () => {
128
+ const { env, operations } = createEnvWithState();
129
+ const arrayPrimitive = Primitive.Array(Primitive.String());
130
+ const proxy = arrayPrimitive._internal.createProxy(env, OperationPath.make(""));
131
+
132
+ proxy.remove("some-id");
133
+
134
+ expect(operations).toHaveLength(1);
135
+ expect(operations[0]!.kind).toBe("array.remove");
136
+ expect(operations[0]!.payload).toEqual({ id: "some-id" });
137
+ });
138
+
139
+ it("move() generates array.move operation with new position", () => {
140
+ const entries = [
141
+ { id: "id1", pos: "a0", value: "first" },
142
+ { id: "id2", pos: "a1", value: "second" },
143
+ { id: "id3", pos: "a2", value: "third" },
144
+ ];
145
+ const { env, operations } = createEnvWithState(entries);
146
+ const arrayPrimitive = Primitive.Array(Primitive.String());
147
+ const proxy = arrayPrimitive._internal.createProxy(env, OperationPath.make(""));
148
+
149
+ // Move first element to end
150
+ proxy.move("id1", 3);
151
+
152
+ expect(operations).toHaveLength(1);
153
+ expect(operations[0]!.kind).toBe("array.move");
154
+
155
+ const payload = operations[0]!.payload as { id: string; pos: string };
156
+ expect(payload.id).toBe("id1");
157
+ // New position should be after a2
158
+ expect(payload.pos > "a2").toBe(true);
159
+ });
160
+
161
+ it("at() returns element proxy with ID in path", () => {
162
+ const { env, operations } = createEnvWithState();
163
+ const arrayPrimitive = Primitive.Array(Primitive.String());
164
+ const proxy = arrayPrimitive._internal.createProxy(env, OperationPath.make("items"));
165
+
166
+ const elementProxy = proxy.at("some-uuid");
167
+ elementProxy.set("element value");
168
+
169
+ expect(operations).toHaveLength(1);
170
+ expect(operations[0]!.kind).toBe("string.set");
171
+ expect(operations[0]!.path.toTokens()).toEqual(["items", "some-uuid"]);
172
+ });
173
+
174
+ it("at() with nested struct returns nested proxy", () => {
175
+ const { env, operations } = createEnvWithState();
176
+ const arrayPrimitive = Primitive.Array(
177
+ Primitive.Struct({
178
+ name: Primitive.String(),
179
+ age: Primitive.Number(),
180
+ })
181
+ );
182
+ const proxy = arrayPrimitive._internal.createProxy(env, OperationPath.make("users"));
183
+
184
+ proxy.at("user-id-123").name.set("John");
185
+
186
+ expect(operations).toHaveLength(1);
187
+ expect(operations[0]!.kind).toBe("string.set");
188
+ expect(operations[0]!.path.toTokens()).toEqual(["users", "user-id-123", "name"]);
189
+ });
190
+
191
+ it("find() returns proxy for matching element", () => {
192
+ const entries = [
193
+ { id: "id1", pos: "a0", value: "alice" },
194
+ { id: "id2", pos: "a1", value: "bob" },
195
+ ];
196
+ const { env, operations } = createEnvWithState(entries);
197
+ const arrayPrimitive = Primitive.Array(Primitive.String());
198
+ const proxy = arrayPrimitive._internal.createProxy(env, OperationPath.make(""));
199
+
200
+ const found = proxy.find((value) => value === "bob");
201
+ expect(found).toBeDefined();
202
+
203
+ found!.set("robert");
204
+
205
+ expect(operations[0]!.path.toTokens()).toEqual(["id2"]);
206
+ });
207
+ });
208
+
209
+ describe("applyOperation", () => {
210
+ it("array.set replaces entire array with entries", () => {
211
+ const arrayPrimitive = Primitive.Array(Primitive.String());
212
+ const newEntries: Primitive.ArrayEntry<string>[] = [
213
+ { id: "id1", pos: "a0", value: "x" },
214
+ { id: "id2", pos: "a1", value: "y" },
215
+ ];
216
+ const operation: Operation.Operation<any, any, any> = {
217
+ kind: "array.set",
218
+ path: OperationPath.make(""),
219
+ payload: newEntries,
220
+ };
221
+
222
+ const result = arrayPrimitive._internal.applyOperation([], operation);
223
+ expect(result).toEqual(newEntries);
224
+ });
225
+
226
+ it("array.insert adds new entry to array", () => {
227
+ const arrayPrimitive = Primitive.Array(Primitive.String());
228
+ const existingEntries: Primitive.ArrayEntry<string>[] = [
229
+ { id: "id1", pos: "a0", value: "existing" },
230
+ ];
231
+ const operation: Operation.Operation<any, any, any> = {
232
+ kind: "array.insert",
233
+ path: OperationPath.make(""),
234
+ payload: { id: "id2", pos: "a1", value: "new" },
235
+ };
236
+
237
+ const result = arrayPrimitive._internal.applyOperation(existingEntries, operation);
238
+ expect(result).toHaveLength(2);
239
+ expect(result[1]).toEqual({ id: "id2", pos: "a1", value: "new" });
240
+ });
241
+
242
+ it("array.insert works with undefined state", () => {
243
+ const arrayPrimitive = Primitive.Array(Primitive.String());
244
+ const operation: Operation.Operation<any, any, any> = {
245
+ kind: "array.insert",
246
+ path: OperationPath.make(""),
247
+ payload: { id: "id1", pos: "a0", value: "first" },
248
+ };
249
+
250
+ const result = arrayPrimitive._internal.applyOperation(undefined, operation);
251
+ expect(result).toHaveLength(1);
252
+ expect(result[0]).toEqual({ id: "id1", pos: "a0", value: "first" });
253
+ });
254
+
255
+ it("array.remove removes entry by ID", () => {
256
+ const arrayPrimitive = Primitive.Array(Primitive.String());
257
+ const entries: Primitive.ArrayEntry<string>[] = [
258
+ { id: "id1", pos: "a0", value: "a" },
259
+ { id: "id2", pos: "a1", value: "b" },
260
+ { id: "id3", pos: "a2", value: "c" },
261
+ ];
262
+ const operation: Operation.Operation<any, any, any> = {
263
+ kind: "array.remove",
264
+ path: OperationPath.make(""),
265
+ payload: { id: "id2" },
266
+ };
267
+
268
+ const result = arrayPrimitive._internal.applyOperation(entries, operation);
269
+ expect(result).toHaveLength(2);
270
+ expect(result.map(e => e.id)).toEqual(["id1", "id3"]);
271
+ });
272
+
273
+ it("array.move updates entry position", () => {
274
+ const arrayPrimitive = Primitive.Array(Primitive.String());
275
+ const entries: Primitive.ArrayEntry<string>[] = [
276
+ { id: "id1", pos: "a0", value: "first" },
277
+ { id: "id2", pos: "a1", value: "second" },
278
+ ];
279
+ const operation: Operation.Operation<any, any, any> = {
280
+ kind: "array.move",
281
+ path: OperationPath.make(""),
282
+ payload: { id: "id1", pos: "a2" },
283
+ };
284
+
285
+ const result = arrayPrimitive._internal.applyOperation(entries, operation);
286
+ const movedEntry = result.find(e => e.id === "id1");
287
+ expect(movedEntry!.pos).toBe("a2");
288
+ expect(movedEntry!.value).toBe("first");
289
+ });
290
+
291
+ it("delegates element operations by ID", () => {
292
+ const arrayPrimitive = Primitive.Array(Primitive.String());
293
+ const entries: Primitive.ArrayEntry<string>[] = [
294
+ { id: "id1", pos: "a0", value: "a" },
295
+ { id: "id2", pos: "a1", value: "b" },
296
+ ];
297
+ const operation: Operation.Operation<any, any, any> = {
298
+ kind: "string.set",
299
+ path: OperationPath.make("id2"),
300
+ payload: "updated",
301
+ };
302
+
303
+ const result = arrayPrimitive._internal.applyOperation(entries, operation);
304
+ expect(result[1]!.value).toBe("updated");
305
+ expect(result[1]!.id).toBe("id2");
306
+ expect(result[1]!.pos).toBe("a1");
307
+ });
308
+
309
+ it("delegates nested struct operations by ID", () => {
310
+ const arrayPrimitive = Primitive.Array(
311
+ Primitive.Struct({
312
+ name: Primitive.String(),
313
+ })
314
+ );
315
+ const entries: Primitive.ArrayEntry<{ name: string }>[] = [
316
+ { id: "user-1", pos: "a0", value: { name: "Original" } },
317
+ ];
318
+ const operation: Operation.Operation<any, any, any> = {
319
+ kind: "string.set",
320
+ path: OperationPath.make("user-1/name"),
321
+ payload: "Updated Name",
322
+ };
323
+
324
+ const result = arrayPrimitive._internal.applyOperation(entries, operation);
325
+ expect(result[0]!.value.name).toBe("Updated Name");
326
+ });
327
+
328
+ it("throws ValidationError for non-array payload on set", () => {
329
+ const arrayPrimitive = Primitive.Array(Primitive.String());
330
+ const operation: Operation.Operation<any, any, any> = {
331
+ kind: "array.set",
332
+ path: OperationPath.make(""),
333
+ payload: "not an array",
334
+ };
335
+
336
+ expect(() => arrayPrimitive._internal.applyOperation(undefined, operation)).toThrow(
337
+ Primitive.ValidationError
338
+ );
339
+ });
340
+
341
+ it("throws ValidationError for unknown element ID", () => {
342
+ const arrayPrimitive = Primitive.Array(Primitive.String());
343
+ const entries: Primitive.ArrayEntry<string>[] = [
344
+ { id: "id1", pos: "a0", value: "a" },
345
+ ];
346
+ const operation: Operation.Operation<any, any, any> = {
347
+ kind: "string.set",
348
+ path: OperationPath.make("unknown-id"),
349
+ payload: "value",
350
+ };
351
+
352
+ expect(() => arrayPrimitive._internal.applyOperation(entries, operation)).toThrow(
353
+ Primitive.ValidationError
354
+ );
355
+ });
356
+ });
357
+
358
+ describe("getInitialState", () => {
359
+ it("returns undefined when no default is set", () => {
360
+ const arrayPrimitive = Primitive.Array(Primitive.String());
361
+ expect(arrayPrimitive._internal.getInitialState()).toBeUndefined();
362
+ });
363
+
364
+ it("returns the default value when set", () => {
365
+ const defaultEntries: Primitive.ArrayEntry<string>[] = [
366
+ { id: "id1", pos: "a0", value: "a" },
367
+ { id: "id2", pos: "a1", value: "b" },
368
+ ];
369
+ const arrayPrimitive = Primitive.Array(Primitive.String()).default(defaultEntries);
370
+ expect(arrayPrimitive._internal.getInitialState()).toEqual(defaultEntries);
371
+ });
372
+ });
373
+
374
+ describe("fractional index ordering", () => {
375
+ it("entries are sorted by position when accessed", () => {
376
+ const arrayPrimitive = Primitive.Array(Primitive.String());
377
+ // Entries stored out of order
378
+ const entries: Primitive.ArrayEntry<string>[] = [
379
+ { id: "id3", pos: "a2", value: "third" },
380
+ { id: "id1", pos: "a0", value: "first" },
381
+ { id: "id2", pos: "a1", value: "second" },
382
+ ];
383
+
384
+ // Apply a move operation - it should work regardless of storage order
385
+ const operation: Operation.Operation<any, any, any> = {
386
+ kind: "array.move",
387
+ path: OperationPath.make(""),
388
+ payload: { id: "id1", pos: "a3" },
389
+ };
390
+
391
+ const result = arrayPrimitive._internal.applyOperation(entries, operation);
392
+ const movedEntry = result.find(e => e.id === "id1");
393
+ expect(movedEntry!.pos).toBe("a3");
394
+ });
395
+
396
+ it("multiple inserts generate valid ordering", () => {
397
+ const { env, operations } = createEnvWithState();
398
+ const arrayPrimitive = Primitive.Array(Primitive.String());
399
+ const proxy = arrayPrimitive._internal.createProxy(env, OperationPath.make(""));
400
+
401
+ // Push multiple items
402
+ proxy.push("first");
403
+ proxy.push("second");
404
+ proxy.push("third");
405
+
406
+ expect(operations).toHaveLength(3);
407
+
408
+ const positions = operations.map(op => (op.payload as { pos: string }).pos);
409
+ // All positions should be different and in order
410
+ expect(positions[0]! < positions[1]!).toBe(true);
411
+ expect(positions[1]! < positions[2]!).toBe(true);
412
+ });
413
+ });
414
+ });
415
+
416
+ // =============================================================================
417
+ // Lazy Primitive Tests
418
+ // =============================================================================
@@ -0,0 +1,126 @@
1
+ import { describe, expect, it } from "@effect/vitest";
2
+ import * as Primitive from "../../src/Primitive";
3
+ import * as ProxyEnvironment from "../../src/ProxyEnvironment";
4
+ import * as OperationPath from "../../src/OperationPath";
5
+ import * as Operation from "../../src/Operation";
6
+
7
+ describe("BooleanPrimitive", () => {
8
+ describe("proxy", () => {
9
+ it("set() generates correct operation with path and payload", () => {
10
+ const operations: Operation.Operation<any, any, any>[] = [];
11
+ const env = ProxyEnvironment.make((op) => operations.push(op));
12
+
13
+ const booleanPrimitive = Primitive.Boolean().required();
14
+ const proxy = booleanPrimitive._internal.createProxy(env, OperationPath.make(""));
15
+
16
+ proxy.set(true);
17
+
18
+ expect(operations).toHaveLength(1);
19
+ expect(operations[0]!.kind).toBe("boolean.set");
20
+ expect(operations[0]!.payload).toBe(true);
21
+ });
22
+
23
+ it("set() includes the correct path in operation", () => {
24
+ const operations: Operation.Operation<any, any, any>[] = [];
25
+ const env = ProxyEnvironment.make((op) => operations.push(op));
26
+
27
+ const booleanPrimitive = Primitive.Boolean();
28
+ const proxy = booleanPrimitive._internal.createProxy(env, OperationPath.make("visible"));
29
+
30
+ proxy.set(false);
31
+
32
+ expect(operations[0]!.path.toTokens()).toEqual(["visible"]);
33
+ });
34
+ });
35
+
36
+ describe("applyOperation", () => {
37
+ it("returns the new boolean value from boolean.set operation", () => {
38
+ const booleanPrimitive = Primitive.Boolean();
39
+ const operation: Operation.Operation<any, any, any> = {
40
+ kind: "boolean.set",
41
+ path: OperationPath.make(""),
42
+ payload: true,
43
+ };
44
+
45
+ const result = booleanPrimitive._internal.applyOperation(undefined, operation);
46
+ expect(result).toBe(true);
47
+ });
48
+
49
+ it("replaces existing state with new value", () => {
50
+ const booleanPrimitive = Primitive.Boolean();
51
+ const operation: Operation.Operation<any, any, any> = {
52
+ kind: "boolean.set",
53
+ path: OperationPath.make(""),
54
+ payload: false,
55
+ };
56
+
57
+ const result = booleanPrimitive._internal.applyOperation(true, operation);
58
+ expect(result).toBe(false);
59
+ });
60
+
61
+ it("throws ValidationError for non-boolean payload", () => {
62
+ const booleanPrimitive = Primitive.Boolean();
63
+ const operation: Operation.Operation<any, any, any> = {
64
+ kind: "boolean.set",
65
+ path: OperationPath.make(""),
66
+ payload: "true",
67
+ };
68
+
69
+ expect(() => booleanPrimitive._internal.applyOperation(undefined, operation)).toThrow(
70
+ Primitive.ValidationError
71
+ );
72
+ });
73
+
74
+ it("throws ValidationError for wrong operation kind", () => {
75
+ const booleanPrimitive = Primitive.Boolean();
76
+ const operation: Operation.Operation<any, any, any> = {
77
+ kind: "string.set",
78
+ path: OperationPath.make(""),
79
+ payload: true,
80
+ };
81
+
82
+ expect(() => booleanPrimitive._internal.applyOperation(undefined, operation)).toThrow(
83
+ Primitive.ValidationError
84
+ );
85
+ });
86
+ });
87
+
88
+ describe("getInitialState", () => {
89
+ it("returns undefined when no default is set", () => {
90
+ const booleanPrimitive = Primitive.Boolean();
91
+ expect(booleanPrimitive._internal.getInitialState()).toBeUndefined();
92
+ });
93
+
94
+ it("returns the default value when set", () => {
95
+ const booleanPrimitive = Primitive.Boolean().default(true);
96
+ expect(booleanPrimitive._internal.getInitialState()).toBe(true);
97
+ });
98
+
99
+ it("returns false as default when explicitly set", () => {
100
+ const booleanPrimitive = Primitive.Boolean().default(false);
101
+ expect(booleanPrimitive._internal.getInitialState()).toBe(false);
102
+ });
103
+ });
104
+
105
+ describe("schema modifiers", () => {
106
+ it("required() returns a new BooleanPrimitive", () => {
107
+ const original = Primitive.Boolean();
108
+ const required = original.required();
109
+
110
+ expect(required).toBeInstanceOf(Primitive.BooleanPrimitive);
111
+ expect(required).not.toBe(original);
112
+ });
113
+
114
+ it("default() returns a new BooleanPrimitive with default value", () => {
115
+ const original = Primitive.Boolean();
116
+ const withDefault = original.default(true);
117
+
118
+ expect(withDefault).toBeInstanceOf(Primitive.BooleanPrimitive);
119
+ expect(withDefault._internal.getInitialState()).toBe(true);
120
+ });
121
+ });
122
+ });
123
+
124
+ // =============================================================================
125
+ // Number Primitive Tests
126
+ // =============================================================================
@@ -0,0 +1,143 @@
1
+ import { describe, expect, it } from "@effect/vitest";
2
+ import * as Primitive from "../../src/Primitive";
3
+ import * as ProxyEnvironment from "../../src/ProxyEnvironment";
4
+ import * as OperationPath from "../../src/OperationPath";
5
+ import * as Operation from "../../src/Operation";
6
+
7
+ describe("LazyPrimitive", () => {
8
+ describe("proxy", () => {
9
+ it("delegates proxy creation to resolved primitive", () => {
10
+ const operations: Operation.Operation<any, any, any>[] = [];
11
+ const env = ProxyEnvironment.make((op) => operations.push(op));
12
+
13
+ const lazyPrimitive = Primitive.Lazy(() => Primitive.String());
14
+ const proxy = lazyPrimitive._internal.createProxy(env, OperationPath.make(""));
15
+
16
+ proxy.set("lazy value");
17
+
18
+ expect(operations).toHaveLength(1);
19
+ expect(operations[0]!.kind).toBe("string.set");
20
+ expect(operations[0]!.payload).toBe("lazy value");
21
+ });
22
+
23
+ it("works with lazy struct", () => {
24
+ const operations: Operation.Operation<any, any, any>[] = [];
25
+ const env = ProxyEnvironment.make((op) => operations.push(op));
26
+
27
+ const lazyPrimitive = Primitive.Lazy(() =>
28
+ Primitive.Struct({
29
+ name: Primitive.String(),
30
+ })
31
+ );
32
+ const proxy = lazyPrimitive._internal.createProxy(env, OperationPath.make(""));
33
+
34
+ proxy.name.set("lazy struct field");
35
+
36
+ expect(operations).toHaveLength(1);
37
+ expect(operations[0]!.kind).toBe("string.set");
38
+ expect(operations[0]!.path.toTokens()).toEqual(["name"]);
39
+ });
40
+ });
41
+
42
+ describe("applyOperation", () => {
43
+ it("delegates operation application to resolved primitive", () => {
44
+ const lazyPrimitive = Primitive.Lazy(() => Primitive.String());
45
+ const operation: Operation.Operation<any, any, any> = {
46
+ kind: "string.set",
47
+ path: OperationPath.make(""),
48
+ payload: "lazy applied",
49
+ };
50
+
51
+ const result = lazyPrimitive._internal.applyOperation(undefined, operation);
52
+ expect(result).toBe("lazy applied");
53
+ });
54
+
55
+ it("works with lazy struct operations", () => {
56
+ const lazyPrimitive = Primitive.Lazy(() =>
57
+ Primitive.Struct({
58
+ name: Primitive.String(),
59
+ })
60
+ );
61
+ const operation: Operation.Operation<any, any, any> = {
62
+ kind: "string.set",
63
+ path: OperationPath.make("name"),
64
+ payload: "Updated",
65
+ };
66
+
67
+ const result = lazyPrimitive._internal.applyOperation({ name: "Original" }, operation);
68
+ expect(result).toEqual({ name: "Updated" });
69
+ });
70
+ });
71
+
72
+ describe("getInitialState", () => {
73
+ it("delegates to resolved primitive", () => {
74
+ const lazyPrimitive = Primitive.Lazy(() => Primitive.String().default("lazy default"));
75
+ expect(lazyPrimitive._internal.getInitialState()).toBe("lazy default");
76
+ });
77
+
78
+ it("returns undefined when resolved primitive has no default", () => {
79
+ const lazyPrimitive = Primitive.Lazy(() => Primitive.String());
80
+ expect(lazyPrimitive._internal.getInitialState()).toBeUndefined();
81
+ });
82
+ });
83
+
84
+ describe("recursive structures", () => {
85
+ it("supports self-referential structures", () => {
86
+ const operations: Operation.Operation<any, any, any>[] = [];
87
+ const env = ProxyEnvironment.make((op) => operations.push(op));
88
+
89
+ // Define a recursive node structure
90
+ const Node: Primitive.LazyPrimitive<any> = Primitive.Lazy(() =>
91
+ Primitive.Struct({
92
+ name: Primitive.String(),
93
+ children: Primitive.Array(Node),
94
+ })
95
+ );
96
+
97
+ const proxy = Node._internal.createProxy(env, OperationPath.make("")) as any;
98
+
99
+ // Access nested child
100
+ proxy.children.at(0).name.set("Child Name");
101
+
102
+ expect(operations).toHaveLength(1);
103
+ expect(operations[0]!.kind).toBe("string.set");
104
+ expect(operations[0]!.path.toTokens()).toEqual(["children", "0", "name"]);
105
+ });
106
+
107
+ it("applies operations to recursive structures", () => {
108
+ const Node: Primitive.LazyPrimitive<any> = Primitive.Lazy(() =>
109
+ Primitive.Struct({
110
+ name: Primitive.String(),
111
+ children: Primitive.Array(Node),
112
+ })
113
+ );
114
+
115
+ // Use entry ID in path (arrays now use ID-based addressing)
116
+ const operation: Operation.Operation<any, any, any> = {
117
+ kind: "string.set",
118
+ path: OperationPath.make("children/child-entry-1/name"),
119
+ payload: "Updated Child",
120
+ };
121
+
122
+ // State with array entries format { id, pos, value }
123
+ const state = {
124
+ name: "Root",
125
+ children: [
126
+ { id: "child-entry-1", pos: "a0", value: { name: "Child", children: [] } }
127
+ ],
128
+ };
129
+
130
+ const result = Node._internal.applyOperation(state, operation);
131
+ expect(result).toEqual({
132
+ name: "Root",
133
+ children: [
134
+ { id: "child-entry-1", pos: "a0", value: { name: "Updated Child", children: [] } }
135
+ ],
136
+ });
137
+ });
138
+ });
139
+ });
140
+
141
+ // =============================================================================
142
+ // Union Primitive Tests
143
+ // =============================================================================