@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.
- package/README.md +17 -0
- package/package.json +33 -0
- package/src/Document.ts +256 -0
- package/src/FractionalIndex.ts +1249 -0
- package/src/Operation.ts +59 -0
- package/src/OperationDefinition.ts +23 -0
- package/src/OperationPath.ts +197 -0
- package/src/Presence.ts +142 -0
- package/src/Primitive.ts +32 -0
- package/src/Proxy.ts +8 -0
- package/src/ProxyEnvironment.ts +52 -0
- package/src/Transaction.ts +72 -0
- package/src/Transform.ts +13 -0
- package/src/client/ClientDocument.ts +1163 -0
- package/src/client/Rebase.ts +309 -0
- package/src/client/StateMonitor.ts +307 -0
- package/src/client/Transport.ts +318 -0
- package/src/client/WebSocketTransport.ts +572 -0
- package/src/client/errors.ts +145 -0
- package/src/client/index.ts +61 -0
- package/src/index.ts +12 -0
- package/src/primitives/Array.ts +457 -0
- package/src/primitives/Boolean.ts +128 -0
- package/src/primitives/Lazy.ts +89 -0
- package/src/primitives/Literal.ts +128 -0
- package/src/primitives/Number.ts +169 -0
- package/src/primitives/String.ts +189 -0
- package/src/primitives/Struct.ts +348 -0
- package/src/primitives/Tree.ts +1120 -0
- package/src/primitives/TreeNode.ts +113 -0
- package/src/primitives/Union.ts +329 -0
- package/src/primitives/shared.ts +122 -0
- package/src/server/ServerDocument.ts +267 -0
- package/src/server/errors.ts +90 -0
- package/src/server/index.ts +40 -0
- package/tests/Document.test.ts +556 -0
- package/tests/FractionalIndex.test.ts +377 -0
- package/tests/OperationPath.test.ts +151 -0
- package/tests/Presence.test.ts +321 -0
- package/tests/Primitive.test.ts +381 -0
- package/tests/client/ClientDocument.test.ts +1398 -0
- package/tests/client/WebSocketTransport.test.ts +992 -0
- package/tests/primitives/Array.test.ts +418 -0
- package/tests/primitives/Boolean.test.ts +126 -0
- package/tests/primitives/Lazy.test.ts +143 -0
- package/tests/primitives/Literal.test.ts +122 -0
- package/tests/primitives/Number.test.ts +133 -0
- package/tests/primitives/String.test.ts +128 -0
- package/tests/primitives/Struct.test.ts +311 -0
- package/tests/primitives/Tree.test.ts +467 -0
- package/tests/primitives/TreeNode.test.ts +50 -0
- package/tests/primitives/Union.test.ts +210 -0
- package/tests/server/ServerDocument.test.ts +528 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +8 -0
- package/tsdown.config.ts +18 -0
- 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
|
+
});
|