@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.
- package/dist/EffectSchema.cjs +3 -3
- package/dist/EffectSchema.d.cts +5 -5
- package/dist/EffectSchema.d.cts.map +1 -1
- package/dist/EffectSchema.d.mts +5 -5
- package/dist/EffectSchema.d.mts.map +1 -1
- package/dist/EffectSchema.mjs +3 -3
- package/dist/EffectSchema.mjs.map +1 -1
- package/dist/FractionalIndex.mjs.map +1 -1
- package/dist/Operation.d.cts +4 -4
- package/dist/Operation.d.cts.map +1 -1
- package/dist/Operation.d.mts +4 -4
- package/dist/Operation.d.mts.map +1 -1
- package/dist/Operation.mjs.map +1 -1
- package/dist/OperationDefinition.d.cts +2 -2
- package/dist/OperationDefinition.d.cts.map +1 -1
- package/dist/OperationDefinition.d.mts +2 -2
- package/dist/OperationDefinition.d.mts.map +1 -1
- package/dist/OperationDefinition.mjs.map +1 -1
- package/dist/Presence.mjs.map +1 -1
- package/dist/SchemaJSON.cjs +305 -0
- package/dist/SchemaJSON.d.cts +11 -0
- package/dist/SchemaJSON.d.cts.map +1 -0
- package/dist/SchemaJSON.d.mts +11 -0
- package/dist/SchemaJSON.d.mts.map +1 -0
- package/dist/SchemaJSON.mjs +301 -0
- package/dist/SchemaJSON.mjs.map +1 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/primitives/Array.cjs +12 -2
- package/dist/primitives/Array.d.cts.map +1 -1
- package/dist/primitives/Array.d.mts.map +1 -1
- package/dist/primitives/Array.mjs +12 -2
- package/dist/primitives/Array.mjs.map +1 -1
- package/dist/primitives/Boolean.mjs.map +1 -1
- package/dist/primitives/Either.mjs.map +1 -1
- package/dist/primitives/Literal.mjs.map +1 -1
- package/dist/primitives/Number.cjs +27 -5
- package/dist/primitives/Number.d.cts.map +1 -1
- package/dist/primitives/Number.d.mts.map +1 -1
- package/dist/primitives/Number.mjs +27 -5
- package/dist/primitives/Number.mjs.map +1 -1
- package/dist/primitives/String.cjs +44 -13
- package/dist/primitives/String.d.cts.map +1 -1
- package/dist/primitives/String.d.mts.map +1 -1
- package/dist/primitives/String.mjs +44 -13
- package/dist/primitives/String.mjs.map +1 -1
- package/dist/primitives/Union.mjs.map +1 -1
- package/dist/primitives/shared.d.cts +2 -0
- package/dist/primitives/shared.d.cts.map +1 -1
- package/dist/primitives/shared.d.mts +2 -0
- package/dist/primitives/shared.d.mts.map +1 -1
- package/dist/primitives/shared.mjs.map +1 -1
- package/package.json +15 -8
- package/src/EffectSchema.ts +3 -3
- package/src/FractionalIndex.ts +18 -18
- package/src/Operation.ts +5 -5
- package/src/OperationDefinition.ts +2 -2
- package/src/Presence.ts +3 -3
- package/src/SchemaJSON.ts +396 -0
- package/src/index.ts +1 -0
- package/src/primitives/Array.ts +18 -8
- package/src/primitives/Boolean.ts +2 -2
- package/src/primitives/Either.ts +2 -2
- package/src/primitives/Literal.ts +2 -2
- package/src/primitives/Number.ts +44 -22
- package/src/primitives/String.ts +61 -34
- package/src/primitives/Union.ts +1 -1
- package/src/primitives/shared.ts +2 -0
- package/.turbo/turbo-build.log +0 -270
- package/tests/Document.test.ts +0 -557
- package/tests/EffectSchema.test.ts +0 -546
- package/tests/FractionalIndex.test.ts +0 -377
- package/tests/OperationPath.test.ts +0 -151
- package/tests/Presence.test.ts +0 -321
- package/tests/Primitive.test.ts +0 -381
- package/tests/client/ClientDocument.test.ts +0 -1981
- package/tests/client/WebSocketTransport.test.ts +0 -1217
- package/tests/primitives/Array.test.ts +0 -526
- package/tests/primitives/Boolean.test.ts +0 -126
- package/tests/primitives/Either.test.ts +0 -707
- package/tests/primitives/Lazy.test.ts +0 -143
- package/tests/primitives/Literal.test.ts +0 -122
- package/tests/primitives/Number.test.ts +0 -133
- package/tests/primitives/String.test.ts +0 -128
- package/tests/primitives/Struct.test.ts +0 -1154
- package/tests/primitives/Tree.test.ts +0 -1139
- package/tests/primitives/TreeNode.test.ts +0 -50
- package/tests/primitives/Union.test.ts +0 -554
- package/tests/server/ServerDocument.test.ts +0 -903
- package/tsconfig.build.json +0 -24
- package/tsconfig.json +0 -8
- package/tsdown.config.ts +0 -18
- 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
|
-
});
|