@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,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
package/tsdown.config.ts
ADDED
|
@@ -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
|
+
});
|