@adobe/data 0.6.3 → 0.7.2

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 (83) hide show
  1. package/dist/ecs/database/applied/applied-database.d.ts +47 -0
  2. package/dist/ecs/database/applied/applied-database.js +22 -0
  3. package/dist/ecs/database/applied/applied-database.js.map +1 -0
  4. package/dist/ecs/database/applied/applied-entry.d.ts +30 -0
  5. package/dist/ecs/database/applied/applied-entry.js +84 -0
  6. package/dist/ecs/database/applied/applied-entry.js.map +1 -0
  7. package/dist/ecs/database/applied/create-applied-database.d.ts +8 -0
  8. package/dist/ecs/database/applied/create-applied-database.js +266 -0
  9. package/dist/ecs/database/applied/create-applied-database.js.map +1 -0
  10. package/dist/ecs/database/applied/create-applied-database.test.d.ts +1 -0
  11. package/dist/ecs/database/applied/create-applied-database.test.js +94 -0
  12. package/dist/ecs/database/applied/create-applied-database.test.js.map +1 -0
  13. package/dist/ecs/database/create-applied-database.d.ts +49 -0
  14. package/dist/ecs/database/create-applied-database.js +275 -0
  15. package/dist/ecs/database/create-applied-database.js.map +1 -0
  16. package/dist/ecs/database/create-database.d.ts +1 -1
  17. package/dist/ecs/database/create-database.js +123 -198
  18. package/dist/ecs/database/create-database.js.map +1 -1
  19. package/dist/ecs/database/create-database.test.js +155 -684
  20. package/dist/ecs/database/create-database.test.js.map +1 -1
  21. package/dist/ecs/database/database.d.ts +1 -0
  22. package/dist/ecs/database/index.d.ts +6 -0
  23. package/dist/ecs/database/index.js +6 -0
  24. package/dist/ecs/database/index.js.map +1 -1
  25. package/dist/ecs/database/observe-select-entities.performance.test.js +6 -5
  26. package/dist/ecs/database/observe-select-entities.performance.test.js.map +1 -1
  27. package/dist/ecs/database/observed/create-observed-database.d.ts +7 -0
  28. package/dist/ecs/database/observed/create-observed-database.js +154 -0
  29. package/dist/ecs/database/observed/create-observed-database.js.map +1 -0
  30. package/dist/ecs/database/observed/create-observed-database.test.d.ts +1 -0
  31. package/dist/ecs/database/observed/create-observed-database.test.js +557 -0
  32. package/dist/ecs/database/observed/create-observed-database.test.js.map +1 -0
  33. package/dist/ecs/database/observed/observed-database.d.ts +36 -0
  34. package/dist/ecs/database/observed/observed-database.js +19 -0
  35. package/dist/ecs/database/observed/observed-database.js.map +1 -0
  36. package/dist/ecs/database/reconciling/applied-database.d.ts +47 -0
  37. package/dist/ecs/database/reconciling/applied-database.js +22 -0
  38. package/dist/ecs/database/reconciling/applied-database.js.map +1 -0
  39. package/dist/ecs/database/reconciling/applied-entry.d.ts +30 -0
  40. package/dist/ecs/database/reconciling/applied-entry.js +84 -0
  41. package/dist/ecs/database/reconciling/applied-entry.js.map +1 -0
  42. package/dist/ecs/database/reconciling/create-applied-database.d.ts +8 -0
  43. package/dist/ecs/database/reconciling/create-applied-database.js +266 -0
  44. package/dist/ecs/database/reconciling/create-applied-database.js.map +1 -0
  45. package/dist/ecs/database/reconciling/create-applied-database.test.d.ts +1 -0
  46. package/dist/ecs/database/reconciling/create-applied-database.test.js +94 -0
  47. package/dist/ecs/database/reconciling/create-applied-database.test.js.map +1 -0
  48. package/dist/ecs/database/reconciling/create-reconciling-database.d.ts +8 -0
  49. package/dist/ecs/database/reconciling/create-reconciling-database.js +146 -0
  50. package/dist/ecs/database/reconciling/create-reconciling-database.js.map +1 -0
  51. package/dist/ecs/database/reconciling/create-reconciling-database.test.d.ts +1 -0
  52. package/dist/ecs/database/reconciling/create-reconciling-database.test.js +94 -0
  53. package/dist/ecs/database/reconciling/create-reconciling-database.test.js.map +1 -0
  54. package/dist/ecs/database/reconciling/reconciling-database.d.ts +21 -0
  55. package/dist/ecs/database/reconciling/reconciling-database.js +22 -0
  56. package/dist/ecs/database/reconciling/reconciling-database.js.map +1 -0
  57. package/dist/ecs/database/reconciling/reconciling-entry.d.ts +30 -0
  58. package/dist/ecs/database/reconciling/reconciling-entry.js +88 -0
  59. package/dist/ecs/database/reconciling/reconciling-entry.js.map +1 -0
  60. package/dist/ecs/database/replicate.d.ts +9 -0
  61. package/dist/ecs/database/replicate.js +66 -0
  62. package/dist/ecs/database/replicate.js.map +1 -0
  63. package/dist/ecs/database/replicate.test.d.ts +1 -0
  64. package/dist/ecs/database/replicate.test.js +235 -0
  65. package/dist/ecs/database/replicate.test.js.map +1 -0
  66. package/dist/ecs/database/transactional-store/create-transactional-store.test.js +20 -0
  67. package/dist/ecs/database/transactional-store/create-transactional-store.test.js.map +1 -1
  68. package/dist/ecs/database/transactional-store/patch-entity-values.js +4 -1
  69. package/dist/ecs/database/transactional-store/patch-entity-values.js.map +1 -1
  70. package/dist/ecs/store/core/core.d.ts +2 -2
  71. package/dist/ecs/store/core/create-core.js +5 -2
  72. package/dist/ecs/store/core/create-core.js.map +1 -1
  73. package/dist/functions/serialization/index.d.ts +1 -0
  74. package/dist/functions/serialization/index.js +1 -0
  75. package/dist/functions/serialization/index.js.map +1 -1
  76. package/dist/functions/serialization/serialize-to-json.d.ts +9 -0
  77. package/dist/functions/serialization/serialize-to-json.js +161 -0
  78. package/dist/functions/serialization/serialize-to-json.js.map +1 -0
  79. package/dist/functions/serialization/serialize-to-json.test.d.ts +1 -0
  80. package/dist/functions/serialization/serialize-to-json.test.js +244 -0
  81. package/dist/functions/serialization/serialize-to-json.test.js.map +1 -0
  82. package/dist/tsconfig.tsbuildinfo +1 -1
  83. package/package.json +1 -1
@@ -21,6 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.*/
22
22
  import { describe, it, expect, vi } from "vitest";
23
23
  import { createDatabase } from "./create-database.js";
24
+ import { createReconcilingDatabase } from "./reconciling/create-reconciling-database.js";
24
25
  import { createStore } from "../store/create-store.js";
25
26
  import { F32Schema } from "../../schema/f32.js";
26
27
  import { toPromise } from "../../observe/to-promise.js";
@@ -48,7 +49,7 @@ const nameSchema = {
48
49
  type: "string",
49
50
  maxLength: 50,
50
51
  };
51
- function createTestDatabase() {
52
+ const createStoreConfig = () => {
52
53
  const baseStore = createStore({ position: positionSchema, health: healthSchema, name: nameSchema }, {
53
54
  time: { default: { delta: 0.016, elapsed: 0 } },
54
55
  generating: { type: "boolean", default: false }
@@ -59,7 +60,7 @@ function createTestDatabase() {
59
60
  PositionName: ["position", "name"],
60
61
  Full: ["position", "health", "name"],
61
62
  });
62
- return createDatabase(baseStore, {
63
+ const transactions = {
63
64
  createPositionEntity(t, args) {
64
65
  return t.archetypes.Position.insert(args);
65
66
  },
@@ -84,6 +85,10 @@ function createTestDatabase() {
84
85
  updateTime(t, args) {
85
86
  t.resources.time = args;
86
87
  },
88
+ createFailingPositionEntity(t, args) {
89
+ const entity = t.archetypes.Position.insert(args);
90
+ throw new Error("Simulated failure");
91
+ },
87
92
  startGenerating(t, args) {
88
93
  if (args.progress < 1.0) {
89
94
  t.resources.generating = true;
@@ -95,260 +100,62 @@ function createTestDatabase() {
95
100
  t.delete(entity);
96
101
  }
97
102
  }
98
- });
103
+ };
104
+ return { baseStore, transactions };
105
+ };
106
+ const createSequentialClock = (sequence, startFallback = 1) => {
107
+ let index = 0;
108
+ let current = Math.max(startFallback, 1);
109
+ return () => {
110
+ if (index < sequence.length) {
111
+ const value = sequence[index++];
112
+ current = Math.max(value, current);
113
+ return value;
114
+ }
115
+ current += 1;
116
+ return current;
117
+ };
118
+ };
119
+ function createTestDatabase(now = Date.now) {
120
+ const { baseStore, transactions } = createStoreConfig();
121
+ return createDatabase(baseStore, transactions, now);
99
122
  }
100
123
  describe("createDatabase", () => {
124
+ it("should replay out-of-order commits by absolute time in reconciling database", () => {
125
+ const { baseStore, transactions } = createStoreConfig();
126
+ const reconciling = createReconcilingDatabase(baseStore, transactions);
127
+ reconciling.apply({
128
+ id: 1,
129
+ name: "createPositionNameEntity",
130
+ args: { position: { x: 10, y: 20, z: 30 }, name: "LateCommit" },
131
+ time: 5,
132
+ });
133
+ reconciling.apply({
134
+ id: 2,
135
+ name: "createPositionNameEntity",
136
+ args: { position: { x: 40, y: 50, z: 60 }, name: "EarlyCommit" },
137
+ time: 1,
138
+ });
139
+ const entities = reconciling.select(["name"]);
140
+ const namesById = entities
141
+ .map(entity => reconciling.read(entity)?.name)
142
+ .filter((name) => !!name);
143
+ expect(namesById).toHaveLength(2);
144
+ expect(namesById).toEqual(["EarlyCommit", "LateCommit"]);
145
+ });
101
146
  it("should support deleting entities", () => {
102
147
  const store = createTestDatabase();
103
148
  const entity = store.transactions.createPositionEntity({ position: { x: 1, y: 2, z: 3 } });
104
149
  store.transactions.deletePositionEntities();
105
150
  expect(store.locate(entity)).toBeNull();
106
151
  });
107
- it("should notify component observers when components change", () => {
108
- const store = createTestDatabase();
109
- const positionObserver = vi.fn();
110
- const nameObserver = vi.fn();
111
- // Subscribe to component changes
112
- const unsubscribePosition = store.observe.components.position(positionObserver);
113
- const unsubscribeName = store.observe.components.name(nameObserver);
114
- // Create an entity that affects both components
115
- const testEntity = store.transactions.createFullEntity({
116
- position: { x: 1, y: 2, z: 3 },
117
- name: "Test",
118
- health: { current: 100, max: 100 }
119
- });
120
- // Both observers should be notified
121
- expect(positionObserver).toHaveBeenCalledTimes(1);
122
- expect(nameObserver).toHaveBeenCalledTimes(1);
123
- // Update only position
124
- store.transactions.updateEntity({
125
- entity: testEntity,
126
- values: { position: { x: 4, y: 5, z: 6 } }
127
- });
128
- // Only position observer should be notified
129
- expect(positionObserver).toHaveBeenCalledTimes(2);
130
- expect(nameObserver).toHaveBeenCalledTimes(1);
131
- // Unsubscribe and verify no more notifications
132
- unsubscribePosition();
133
- unsubscribeName();
134
- store.transactions.updateEntity({
135
- entity: testEntity,
136
- values: { position: { x: 7, y: 8, z: 9 }, name: "Updated" }
137
- });
138
- expect(positionObserver).toHaveBeenCalledTimes(2);
139
- expect(nameObserver).toHaveBeenCalledTimes(1);
140
- });
141
- it("should notify entity observers with correct values", () => {
142
- const store = createTestDatabase();
143
- // Create initial entity
144
- const testEntity = store.transactions.createFullEntity({
145
- position: { x: 1, y: 2, z: 3 },
146
- name: "Test",
147
- health: { current: 100, max: 100 }
148
- });
149
- // Subscribe to entity changes
150
- const observer = vi.fn();
151
- const unsubscribe = store.observe.entity(testEntity)(observer);
152
- // Initial notification should have current values
153
- expect(observer).toHaveBeenCalledWith(expect.objectContaining({
154
- position: { x: 1, y: 2, z: 3 },
155
- name: "Test",
156
- health: { current: 100, max: 100 }
157
- }));
158
- // Update entity
159
- store.transactions.updateEntity({
160
- entity: testEntity,
161
- values: { name: "Updated", health: { current: 50, max: 100 } }
162
- });
163
- // Observer should be notified with new values
164
- expect(observer).toHaveBeenCalledWith(expect.objectContaining({
165
- position: { x: 1, y: 2, z: 3 }, // unchanged
166
- name: "Updated",
167
- health: { current: 50, max: 100 }
168
- }));
169
- // Delete entity
170
- store.transactions.deleteEntity({ entity: testEntity });
171
- // Observer should be notified with null
172
- expect(observer).toHaveBeenCalledWith(null);
173
- unsubscribe();
174
- });
175
- it("should notify transaction observers with full transaction results", () => {
152
+ it("should roll back state when a transaction throws synchronously", () => {
176
153
  const store = createTestDatabase();
177
- const transactionObserver = vi.fn();
178
- const unsubscribe = store.observe.transactions(transactionObserver);
179
- // Execute a transaction with multiple operations
180
- store.transactions.createFullEntity({
154
+ expect(() => store.transactions.createFailingPositionEntity({
181
155
  position: { x: 1, y: 2, z: 3 },
182
- name: "Test",
183
- health: { current: 100, max: 100 }
184
- });
185
- // Transaction observer should be called with the full result
186
- expect(transactionObserver).toHaveBeenCalledWith(expect.objectContaining({
187
- changedEntities: expect.any(Map),
188
- changedComponents: expect.any(Set),
189
- changedArchetypes: expect.any(Set),
190
- redo: expect.any(Array),
191
- undo: expect.any(Array)
192
- }));
193
- const result = transactionObserver.mock.calls[0][0];
194
- expect(result.changedEntities.size).toBe(1);
195
- expect(result.changedComponents.has("position")).toBe(true);
196
- expect(result.changedComponents.has("name")).toBe(true);
197
- unsubscribe();
198
- });
199
- it("should notify archetype observers when entities change archetypes", () => {
200
- const store = createTestDatabase();
201
- // Create initial entity
202
- const entity = store.transactions.createPositionEntity({
203
- position: { x: 1, y: 2, z: 3 }
204
- });
205
- const archetype = store.locate(entity)?.archetype;
206
- expect(archetype).toBeDefined();
207
- const archetypeObserver = vi.fn();
208
- const unsubscribe = store.observe.archetype(archetype.id)(archetypeObserver);
209
- // No initial notification for archetype observers
210
- expect(archetypeObserver).toHaveBeenCalledTimes(0);
211
- // Update entity to add name component, potentially changing archetype
212
- store.transactions.updateEntity({
213
- entity,
214
- values: { name: "Test" }
215
- });
216
- // Archetype observer should be notified of the change
217
- expect(archetypeObserver).toHaveBeenCalledTimes(1);
218
- unsubscribe();
219
- });
220
- it("should notify resource observers with immediate and update notifications", () => {
221
- const store = createTestDatabase();
222
- const timeObserver = vi.fn();
223
- // Subscribe to resource changes
224
- const unsubscribeTime = store.observe.resources.time(timeObserver);
225
- // Observer should be notified immediately with initial value
226
- expect(timeObserver).toHaveBeenCalledWith({ delta: 0.016, elapsed: 0 });
227
- // Update time resource
228
- store.transactions.updateTime({ delta: 0.032, elapsed: 1 });
229
- // Observer should be notified with new value
230
- expect(timeObserver).toHaveBeenCalledWith({ delta: 0.032, elapsed: 1 });
231
- });
232
- it("should support multiple observers for the same target", () => {
233
- const store = createTestDatabase();
234
- const observer1 = vi.fn();
235
- const observer2 = vi.fn();
236
- const observer3 = vi.fn();
237
- // Subscribe multiple observers to the same component
238
- const unsubscribe1 = store.observe.components.position(observer1);
239
- const unsubscribe2 = store.observe.components.position(observer2);
240
- const unsubscribe3 = store.observe.components.position(observer3);
241
- // Create entity with position
242
- const entity = store.transactions.createPositionEntity({
243
- position: { x: 1, y: 2, z: 3 }
244
- });
245
- // All observers should be notified
246
- expect(observer1).toHaveBeenCalledTimes(1);
247
- expect(observer2).toHaveBeenCalledTimes(1);
248
- expect(observer3).toHaveBeenCalledTimes(1);
249
- // Unsubscribe one observer
250
- unsubscribe2();
251
- // Update position
252
- store.transactions.updateEntity({
253
- entity,
254
- values: { position: { x: 4, y: 5, z: 6 } }
255
- });
256
- // Only remaining observers should be notified
257
- expect(observer1).toHaveBeenCalledTimes(2);
258
- expect(observer2).toHaveBeenCalledTimes(1); // No more calls
259
- expect(observer3).toHaveBeenCalledTimes(2);
260
- unsubscribe1();
261
- unsubscribe3();
262
- });
263
- it("should handle observer cleanup correctly", () => {
264
- const store = createTestDatabase();
265
- const observer = vi.fn();
266
- const unsubscribe = store.observe.components.position(observer);
267
- // Create entity
268
- const entity = store.transactions.createPositionEntity({
269
- position: { x: 1, y: 2, z: 3 }
270
- });
271
- expect(observer).toHaveBeenCalledTimes(1);
272
- // Unsubscribe
273
- unsubscribe();
274
- // Update entity
275
- store.transactions.updateEntity({
276
- entity,
277
- values: { position: { x: 4, y: 5, z: 6 } }
278
- });
279
- // Observer should not be called after unsubscribe
280
- expect(observer).toHaveBeenCalledTimes(1);
281
- });
282
- it("should handle observing non-existent entities", () => {
283
- const store = createTestDatabase();
284
- const observer = vi.fn();
285
- const unsubscribe = store.observe.entity(999)(observer);
286
- // Should be notified with null for non-existent entity
287
- expect(observer).toHaveBeenCalledWith(null);
288
- unsubscribe();
289
- });
290
- it("should handle complex transaction scenarios with multiple observers", () => {
291
- const store = createTestDatabase();
292
- const positionObserver = vi.fn();
293
- const healthObserver = vi.fn();
294
- const transactionObserver = vi.fn();
295
- const entityObserver = vi.fn();
296
- // Subscribe to various observers
297
- const unsubscribePosition = store.observe.components.position(positionObserver);
298
- const unsubscribeHealth = store.observe.components.health(healthObserver);
299
- const unsubscribeTransaction = store.observe.transactions(transactionObserver);
300
- // Create entity
301
- const entity = store.transactions.createPositionHealthEntity({
302
- position: { x: 1, y: 2, z: 3 },
303
- health: { current: 100, max: 100 }
304
- });
305
- const unsubscribeEntity = store.observe.entity(entity)(entityObserver);
306
- // All observers should be notified
307
- expect(positionObserver).toHaveBeenCalledTimes(1);
308
- expect(healthObserver).toHaveBeenCalledTimes(1);
309
- expect(transactionObserver).toHaveBeenCalledTimes(1);
310
- expect(entityObserver).toHaveBeenCalledTimes(1);
311
- // Update multiple components
312
- store.transactions.updateEntity({
313
- entity,
314
- values: {
315
- position: { x: 4, y: 5, z: 6 },
316
- health: { current: 50, max: 100 }
317
- }
318
- });
319
- // All observers should be notified again
320
- expect(positionObserver).toHaveBeenCalledTimes(2);
321
- expect(healthObserver).toHaveBeenCalledTimes(2);
322
- expect(transactionObserver).toHaveBeenCalledTimes(2);
323
- expect(entityObserver).toHaveBeenCalledTimes(2);
324
- // Verify entity observer received correct values
325
- expect(entityObserver).toHaveBeenCalledWith(expect.objectContaining({
326
- position: { x: 4, y: 5, z: 6 },
327
- health: { current: 50, max: 100 }
328
- }));
329
- unsubscribePosition();
330
- unsubscribeHealth();
331
- unsubscribeTransaction();
332
- unsubscribeEntity();
333
- });
334
- it("should handle rapid successive changes efficiently", () => {
335
- const store = createTestDatabase();
336
- const observer = vi.fn();
337
- const unsubscribe = store.observe.components.position(observer);
338
- // Create entity
339
- const entity = store.transactions.createPositionEntity({
340
- position: { x: 1, y: 2, z: 3 }
341
- });
342
- // Make rapid successive updates
343
- for (let i = 0; i < 5; i++) {
344
- store.transactions.updateEntity({
345
- entity,
346
- values: { position: { x: i, y: i, z: i } }
347
- });
348
- }
349
- // Observer should be called for each change
350
- expect(observer).toHaveBeenCalledTimes(6); // 1 for create + 5 for updates
351
- unsubscribe();
156
+ })).toThrow("Simulated failure");
157
+ const entities = store.select(["position"]);
158
+ expect(entities).toHaveLength(0);
352
159
  });
353
160
  it("should support transaction functions that return an Entity", () => {
354
161
  const store = createTestDatabase();
@@ -542,14 +349,14 @@ describe("createDatabase", () => {
542
349
  expect(error.message).toBe("Test error");
543
350
  // Wait for processing to complete
544
351
  await new Promise(resolve => setTimeout(resolve, 10));
545
- // Verify only the first entity was created before the error
352
+ // Verify the transient entity was rolled back after the error
546
353
  const entities = store.select(["position", "name"]);
547
- const beforeErrorEntity = entities.find(entityId => {
354
+ const beforeErrorEntities = entities.filter(entityId => {
548
355
  const values = store.read(entityId);
549
356
  return values?.name === "BeforeError";
550
357
  });
551
- expect(beforeErrorEntity).toBeDefined();
552
- expect(observer).toHaveBeenCalledTimes(1);
358
+ expect(beforeErrorEntities).toHaveLength(0);
359
+ expect(observer).toHaveBeenCalled();
553
360
  unsubscribe();
554
361
  });
555
362
  it("should handle complex AsyncGenerator with conditional yielding", async () => {
@@ -796,463 +603,127 @@ describe("createDatabase", () => {
796
603
  }
797
604
  });
798
605
  });
799
- describe("entity observation with minArchetype filtering", () => {
800
- it("should observe entity when it matches minArchetype exactly", () => {
606
+ describe("toData/fromData functionality", () => {
607
+ it("should serialize and deserialize database state correctly", () => {
801
608
  const store = createTestDatabase();
802
- // Create entity with position only
803
- const entity = store.transactions.createPositionEntity({
609
+ // Create some entities and update resources
610
+ const entity1 = store.transactions.createPositionEntity({
804
611
  position: { x: 1, y: 2, z: 3 }
805
612
  });
806
- const observer = vi.fn();
807
- const unsubscribe = store.observe.entity(entity, store.archetypes.Position)(observer);
808
- // Should receive the entity data since it matches exactly
809
- expect(observer).toHaveBeenCalledWith(expect.objectContaining({
810
- id: entity,
811
- position: { x: 1, y: 2, z: 3 }
812
- }));
813
- unsubscribe();
814
- });
815
- it("should observe entity when it has more components than minArchetype", () => {
816
- const store = createTestDatabase();
817
- // Create entity with position and health
818
- const entity = store.transactions.createPositionHealthEntity({
819
- position: { x: 1, y: 2, z: 3 },
820
- health: { current: 100, max: 100 }
613
+ const entity2 = store.transactions.createFullEntity({
614
+ position: { x: 4, y: 5, z: 6 },
615
+ health: { current: 100, max: 100 },
616
+ name: "TestEntity"
821
617
  });
822
- const observer = vi.fn();
823
- const unsubscribe = store.observe.entity(entity, store.archetypes.Position)(observer);
824
- // Should receive the entity data since it has all required components
825
- expect(observer).toHaveBeenCalledWith(expect.objectContaining({
826
- id: entity,
827
- position: { x: 1, y: 2, z: 3 }
828
- }));
829
- unsubscribe();
830
- });
831
- it("should return null when entity has fewer components than minArchetype", () => {
832
- const store = createTestDatabase();
833
- // Create entity with position only
834
- const entity = store.transactions.createPositionEntity({
618
+ store.transactions.updateTime({ delta: 0.033, elapsed: 1.5 });
619
+ // Serialize the database
620
+ const serializedData = store.toData();
621
+ // Create a new database and restore from serialized data
622
+ const newStore = createTestDatabase();
623
+ newStore.fromData(serializedData);
624
+ // Verify entities are restored
625
+ const restoredEntities = newStore.select(["position"]);
626
+ expect(restoredEntities).toHaveLength(2);
627
+ // Verify entity data is correct
628
+ const restoredData1 = newStore.read(restoredEntities[0]);
629
+ const restoredData2 = newStore.read(restoredEntities[1]);
630
+ expect(restoredData1).toEqual({
631
+ id: restoredEntities[0],
835
632
  position: { x: 1, y: 2, z: 3 }
836
633
  });
837
- const observer = vi.fn();
838
- const unsubscribe = store.observe.entity(entity, store.archetypes.PositionHealth)(observer);
839
- // Should return null since entity doesn't have health component
840
- expect(observer).toHaveBeenCalledWith(null);
841
- unsubscribe();
842
- });
843
- it("should return null when entity has different components than minArchetype", () => {
844
- const store = createTestDatabase();
845
- // Create entity with position and name
846
- const entity = store.transactions.createPositionNameEntity({
847
- position: { x: 1, y: 2, z: 3 },
848
- name: "Test"
634
+ expect(restoredData2).toEqual({
635
+ id: restoredEntities[1],
636
+ position: { x: 4, y: 5, z: 6 },
637
+ health: { current: 100, max: 100 },
638
+ name: "TestEntity"
849
639
  });
850
- const observer = vi.fn();
851
- const unsubscribe = store.observe.entity(entity, store.archetypes.PositionHealth)(observer);
852
- // Should return null since entity doesn't have health component
853
- expect(observer).toHaveBeenCalledWith(null);
854
- unsubscribe();
640
+ // Verify resources are restored
641
+ expect(newStore.resources.time).toEqual({ delta: 0.033, elapsed: 1.5 });
855
642
  });
856
- it("should update observation when entity gains required components", () => {
857
- const store = createTestDatabase();
858
- // Create entity with position only
859
- const entity = store.transactions.createPositionEntity({
860
- position: { x: 1, y: 2, z: 3 }
861
- });
862
- const observer = vi.fn();
863
- const unsubscribe = store.observe.entity(entity, store.archetypes.PositionHealth)(observer);
864
- // Initially should be null
865
- expect(observer).toHaveBeenCalledWith(null);
866
- // Add health component
867
- store.transactions.updateEntity({
868
- entity,
869
- values: { health: { current: 100, max: 100 } }
870
- });
871
- // Should now receive the entity data
872
- expect(observer).toHaveBeenCalledWith(expect.objectContaining({
873
- id: entity,
874
- position: { x: 1, y: 2, z: 3 },
875
- health: { current: 100, max: 100 }
876
- }));
877
- unsubscribe();
878
- });
879
- it("should update observation when entity loses required components", () => {
880
- const store = createTestDatabase();
881
- // Create entity with position and health
882
- const entity = store.transactions.createPositionHealthEntity({
883
- position: { x: 1, y: 2, z: 3 },
884
- health: { current: 100, max: 100 }
643
+ it("should restore applied entry ordering after serialization", () => {
644
+ const store = createTestDatabase(createSequentialClock([2, 1], 2));
645
+ store.transactions.createPositionNameEntity({
646
+ position: { x: 10, y: 20, z: 30 },
647
+ name: "LateCommit",
885
648
  });
886
- const observer = vi.fn();
887
- const unsubscribe = store.observe.entity(entity, store.archetypes.PositionHealth)(observer);
888
- // Initially should receive data
889
- expect(observer).toHaveBeenCalledWith(expect.objectContaining({
890
- id: entity,
891
- position: { x: 1, y: 2, z: 3 },
892
- health: { current: 100, max: 100 }
893
- }));
894
- // Remove health component
895
- store.transactions.updateEntity({
896
- entity,
897
- values: { health: undefined }
649
+ store.transactions.createPositionNameEntity({
650
+ position: { x: 40, y: 50, z: 60 },
651
+ name: "EarlyCommit",
898
652
  });
899
- // Should now return null
900
- expect(observer).toHaveBeenCalledWith(null);
901
- unsubscribe();
902
- });
903
- it("should handle entity deletion correctly with minArchetype", () => {
904
- const store = createTestDatabase();
905
- // Create entity with position and health
906
- const entity = store.transactions.createPositionHealthEntity({
907
- position: { x: 1, y: 2, z: 3 },
908
- health: { current: 100, max: 100 }
653
+ const serializedData = store.toData();
654
+ expect(Array.isArray(serializedData.appliedEntries)).toBe(true);
655
+ const newStore = createTestDatabase(createSequentialClock([0.5], 1));
656
+ newStore.fromData(serializedData);
657
+ newStore.transactions.createPositionNameEntity({
658
+ position: { x: 70, y: 80, z: 90 },
659
+ name: "EarliestCommit",
909
660
  });
910
- const observer = vi.fn();
911
- const unsubscribe = store.observe.entity(entity, store.archetypes.PositionHealth)(observer);
912
- // Initially should receive data
913
- expect(observer).toHaveBeenCalledWith(expect.objectContaining({
914
- id: entity,
915
- position: { x: 1, y: 2, z: 3 }
916
- }));
917
- // Delete entity
918
- store.transactions.deleteEntity({ entity });
919
- // Should return null for deleted entity
920
- expect(observer).toHaveBeenCalledWith(null);
921
- unsubscribe();
922
- });
923
- it("should handle non-existent entity with minArchetype", () => {
924
- const store = createTestDatabase();
925
- const observer = vi.fn();
926
- const unsubscribe = store.observe.entity(999, store.archetypes.Position)(observer);
927
- // Should return null for non-existent entity
928
- expect(observer).toHaveBeenCalledWith(null);
929
- unsubscribe();
661
+ const entities = newStore.select(["name"]);
662
+ const names = entities
663
+ .map(entityId => newStore.read(entityId)?.name)
664
+ .filter((name) => Boolean(name));
665
+ expect(new Set(names)).toEqual(new Set(["EarliestCommit", "LateCommit", "EarlyCommit"]));
930
666
  });
931
- it("should handle invalid entity ID with minArchetype", () => {
667
+ it("should remove cancelled applied entries", async () => {
932
668
  const store = createTestDatabase();
933
- const observer = vi.fn();
934
- const unsubscribe = store.observe.entity(-1, store.archetypes.Position)(observer);
935
- // Should return null for invalid entity ID
936
- expect(observer).toHaveBeenCalledWith(null);
937
- unsubscribe();
669
+ let rejectGenerator = () => { };
670
+ const generator = async function* () {
671
+ yield { position: { x: 1, y: 1, z: 1 }, name: "Transient" };
672
+ await new Promise((_, reject) => {
673
+ rejectGenerator = reject;
674
+ });
675
+ };
676
+ const promise = store.transactions.createPositionNameEntity(generator);
677
+ // Allow the first yield to be processed
678
+ await new Promise(resolve => setTimeout(resolve, 0));
679
+ const serializedBefore = store.toData();
680
+ expect(serializedBefore.appliedEntries).toHaveLength(1);
681
+ const transientId = serializedBefore.appliedEntries[0].id;
682
+ store.cancelTransaction(transientId);
683
+ rejectGenerator(new Error("cancelled"));
684
+ await expect(promise).rejects.toThrow("cancelled");
685
+ const serializedAfter = store.toData();
686
+ expect(serializedAfter.appliedEntries ?? []).toHaveLength(0);
687
+ const entities = store.select(["position"]);
688
+ expect(entities).toHaveLength(0);
938
689
  });
939
- it("should maintain separate observations for different minArchetypes", () => {
690
+ it("should preserve transaction functionality after restoration", () => {
940
691
  const store = createTestDatabase();
941
- // Create entity with position and health
942
- const entity = store.transactions.createPositionHealthEntity({
943
- position: { x: 1, y: 2, z: 3 },
944
- health: { current: 100, max: 100 }
945
- });
946
- const positionObserver = vi.fn();
947
- const healthObserver = vi.fn();
948
- const fullObserver = vi.fn();
949
- const unsubscribePosition = store.observe.entity(entity, store.archetypes.Position)(positionObserver);
950
- const unsubscribeHealth = store.observe.entity(entity, store.archetypes.Health)(healthObserver);
951
- const unsubscribeFull = store.observe.entity(entity, store.archetypes.PositionHealth)(fullObserver);
952
- // All should receive data since entity has all components
953
- expect(positionObserver).toHaveBeenCalledWith(expect.objectContaining({
954
- id: entity,
692
+ // Create initial state
693
+ store.transactions.updateTime({ delta: 0.016, elapsed: 0 });
694
+ // Serialize the database
695
+ const serializedData = store.toData();
696
+ // Create a new database and restore
697
+ const newStore = createTestDatabase();
698
+ newStore.fromData(serializedData);
699
+ // Verify transactions still work
700
+ const entity = newStore.transactions.createPositionEntity({
955
701
  position: { x: 1, y: 2, z: 3 }
956
- }));
957
- expect(healthObserver).toHaveBeenCalledWith(expect.objectContaining({
958
- id: entity,
959
- health: { current: 100, max: 100 }
960
- }));
961
- expect(fullObserver).toHaveBeenCalledWith(expect.objectContaining({
962
- id: entity,
963
- position: { x: 1, y: 2, z: 3 },
964
- health: { current: 100, max: 100 }
965
- }));
966
- // Remove health component
967
- store.transactions.updateEntity({
968
- entity,
969
- values: { health: undefined }
970
702
  });
971
- // Position observer should still receive data
972
- expect(positionObserver).toHaveBeenCalledWith(expect.objectContaining({
703
+ expect(entity).toBeDefined();
704
+ expect(typeof entity).toBe("number");
705
+ const entityData = newStore.read(entity);
706
+ expect(entityData).toEqual({
973
707
  id: entity,
974
708
  position: { x: 1, y: 2, z: 3 }
975
- }));
976
- // Health and full observers should return null
977
- expect(healthObserver).toHaveBeenCalledWith(null);
978
- expect(fullObserver).toHaveBeenCalledWith(null);
979
- unsubscribePosition();
980
- unsubscribeHealth();
981
- unsubscribeFull();
709
+ });
710
+ // Verify resource transactions work
711
+ newStore.transactions.updateTime({ delta: 0.033, elapsed: 1.5 });
712
+ expect(newStore.resources.time).toEqual({ delta: 0.033, elapsed: 1.5 });
982
713
  });
983
- it("should handle component updates that don't affect minArchetype requirements", () => {
714
+ it("all transient operations should be rolled back", async () => {
984
715
  const store = createTestDatabase();
985
- // Create entity with position and health
986
- const entity = store.transactions.createPositionHealthEntity({
987
- position: { x: 1, y: 2, z: 3 },
988
- health: { current: 100, max: 100 }
989
- });
990
- const observer = vi.fn();
991
- const unsubscribe = store.observe.entity(entity, store.archetypes.PositionHealth)(observer);
992
- // Initially should receive data
993
- expect(observer).toHaveBeenCalledWith(expect.objectContaining({
994
- id: entity,
995
- position: { x: 1, y: 2, z: 3 }
996
- }));
997
- // Update position (should trigger notification)
998
- store.transactions.updateEntity({
999
- entity,
1000
- values: { position: { x: 10, y: 20, z: 30 } }
1001
- });
1002
- // Should receive updated data
1003
- expect(observer).toHaveBeenCalledWith(expect.objectContaining({
1004
- id: entity,
1005
- position: { x: 10, y: 20, z: 30 }
1006
- }));
1007
- // Update health (should not affect position observation)
1008
- store.transactions.updateEntity({
1009
- entity,
1010
- values: { health: { current: 50, max: 100 } }
716
+ const promise = store.transactions.startGenerating(async function* () {
717
+ yield { progress: 0 };
718
+ yield { progress: 1 };
1011
719
  });
1012
- // Should still receive position data (health update shouldn't trigger position observer)
1013
- expect(observer).toHaveBeenCalledWith(expect.objectContaining({
1014
- id: entity,
1015
- position: { x: 10, y: 20, z: 30 }
1016
- }));
1017
- unsubscribe();
1018
- });
1019
- });
1020
- });
1021
- describe("toData/fromData functionality", () => {
1022
- it("should serialize and deserialize database state correctly", () => {
1023
- const store = createTestDatabase();
1024
- // Create some entities and update resources
1025
- const entity1 = store.transactions.createPositionEntity({
1026
- position: { x: 1, y: 2, z: 3 }
1027
- });
1028
- const entity2 = store.transactions.createFullEntity({
1029
- position: { x: 4, y: 5, z: 6 },
1030
- health: { current: 100, max: 100 },
1031
- name: "TestEntity"
1032
- });
1033
- store.transactions.updateTime({ delta: 0.033, elapsed: 1.5 });
1034
- // Serialize the database
1035
- const serializedData = store.toData();
1036
- // Create a new database and restore from serialized data
1037
- const newStore = createTestDatabase();
1038
- newStore.fromData(serializedData);
1039
- // Verify entities are restored
1040
- const restoredEntities = newStore.select(["position"]);
1041
- expect(restoredEntities).toHaveLength(2);
1042
- // Verify entity data is correct
1043
- const restoredData1 = newStore.read(restoredEntities[0]);
1044
- const restoredData2 = newStore.read(restoredEntities[1]);
1045
- expect(restoredData1).toEqual({
1046
- id: restoredEntities[0],
1047
- position: { x: 1, y: 2, z: 3 }
1048
- });
1049
- expect(restoredData2).toEqual({
1050
- id: restoredEntities[1],
1051
- position: { x: 4, y: 5, z: 6 },
1052
- health: { current: 100, max: 100 },
1053
- name: "TestEntity"
1054
- });
1055
- // Verify resources are restored
1056
- expect(newStore.resources.time).toEqual({ delta: 0.033, elapsed: 1.5 });
1057
- });
1058
- it("should notify all observers when database is restored from serialized data", () => {
1059
- const store = createTestDatabase();
1060
- // Create initial state
1061
- const entity = store.transactions.createPositionEntity({
1062
- position: { x: 1, y: 2, z: 3 }
1063
- });
1064
- store.transactions.updateTime({ delta: 0.016, elapsed: 0 });
1065
- // Set up observers
1066
- const positionObserver = vi.fn();
1067
- const timeObserver = vi.fn();
1068
- const entityObserver = vi.fn();
1069
- const transactionObserver = vi.fn();
1070
- const unsubscribePosition = store.observe.components.position(positionObserver);
1071
- const unsubscribeTime = store.observe.resources.time(timeObserver);
1072
- const unsubscribeEntity = store.observe.entity(entity)(entityObserver);
1073
- const unsubscribeTransaction = store.observe.transactions(transactionObserver);
1074
- // Clear initial notifications
1075
- positionObserver.mockClear();
1076
- timeObserver.mockClear();
1077
- entityObserver.mockClear();
1078
- transactionObserver.mockClear();
1079
- // Serialize the database
1080
- const serializedData = store.toData();
1081
- // Create a new database with different state
1082
- const newStore = createTestDatabase();
1083
- const newEntity = newStore.transactions.createFullEntity({
1084
- position: { x: 10, y: 20, z: 30 },
1085
- health: { current: 50, max: 100 },
1086
- name: "NewEntity"
1087
- });
1088
- newStore.transactions.updateTime({ delta: 0.025, elapsed: 2.0 });
1089
- // Set up observers on the new store
1090
- const newPositionObserver = vi.fn();
1091
- const newTimeObserver = vi.fn();
1092
- const newEntityObserver = vi.fn();
1093
- const newTransactionObserver = vi.fn();
1094
- const newUnsubscribePosition = newStore.observe.components.position(newPositionObserver);
1095
- const newUnsubscribeTime = newStore.observe.resources.time(newTimeObserver);
1096
- const newUnsubscribeEntity = newStore.observe.entity(newEntity)(newEntityObserver);
1097
- const newUnsubscribeTransaction = newStore.observe.transactions(newTransactionObserver);
1098
- // Clear initial notifications
1099
- newPositionObserver.mockClear();
1100
- newTimeObserver.mockClear();
1101
- newEntityObserver.mockClear();
1102
- newTransactionObserver.mockClear();
1103
- // Restore from serialized data
1104
- newStore.fromData(serializedData);
1105
- // All observers should be notified because the entire state changed
1106
- expect(newPositionObserver).toHaveBeenCalledTimes(1);
1107
- expect(newTimeObserver).toHaveBeenCalledTimes(1);
1108
- expect(newEntityObserver).toHaveBeenCalledTimes(1);
1109
- expect(newTransactionObserver).toHaveBeenCalledTimes(1);
1110
- // Verify the transaction observer received the correct notification
1111
- const transactionResult = newTransactionObserver.mock.calls[0][0];
1112
- expect(transactionResult.changedComponents.has("position")).toBe(true);
1113
- expect(transactionResult.transient).toBe(false);
1114
- // Verify the entity observer received the correct data
1115
- const entityData = newEntityObserver.mock.calls[0][0];
1116
- expect(entityData).toEqual({
1117
- id: newEntity,
1118
- position: { x: 1, y: 2, z: 3 }
1119
- });
1120
- // Verify the time observer received the correct data
1121
- const timeData = newTimeObserver.mock.calls[0][0];
1122
- expect(timeData).toEqual({ delta: 0.016, elapsed: 0 });
1123
- // Clean up
1124
- unsubscribePosition();
1125
- unsubscribeTime();
1126
- unsubscribeEntity();
1127
- unsubscribeTransaction();
1128
- newUnsubscribePosition();
1129
- newUnsubscribeTime();
1130
- newUnsubscribeEntity();
1131
- newUnsubscribeTransaction();
1132
- });
1133
- it("should notify observers even when no entities exist in restored data", () => {
1134
- const store = createTestDatabase();
1135
- // Set up observers on empty store
1136
- const positionObserver = vi.fn();
1137
- const timeObserver = vi.fn();
1138
- const transactionObserver = vi.fn();
1139
- const unsubscribePosition = store.observe.components.position(positionObserver);
1140
- const unsubscribeTime = store.observe.resources.time(timeObserver);
1141
- const unsubscribeTransaction = store.observe.transactions(transactionObserver);
1142
- // Clear initial notifications
1143
- positionObserver.mockClear();
1144
- timeObserver.mockClear();
1145
- transactionObserver.mockClear();
1146
- // Serialize empty database
1147
- const serializedData = store.toData();
1148
- // Create a new database with some data
1149
- const newStore = createTestDatabase();
1150
- newStore.transactions.createPositionEntity({
1151
- position: { x: 1, y: 2, z: 3 }
1152
- });
1153
- newStore.transactions.updateTime({ delta: 0.033, elapsed: 1.5 });
1154
- // Set up observers on the new store
1155
- const newPositionObserver = vi.fn();
1156
- const newTimeObserver = vi.fn();
1157
- const newTransactionObserver = vi.fn();
1158
- const newUnsubscribePosition = newStore.observe.components.position(newPositionObserver);
1159
- const newUnsubscribeTime = newStore.observe.resources.time(newTimeObserver);
1160
- const newUnsubscribeTransaction = newStore.observe.transactions(newTransactionObserver);
1161
- // Clear initial notifications
1162
- newPositionObserver.mockClear();
1163
- newTimeObserver.mockClear();
1164
- newTransactionObserver.mockClear();
1165
- // Restore from empty serialized data
1166
- newStore.fromData(serializedData);
1167
- // All observers should still be notified
1168
- expect(newPositionObserver).toHaveBeenCalledTimes(1);
1169
- expect(newTimeObserver).toHaveBeenCalledTimes(1);
1170
- expect(newTransactionObserver).toHaveBeenCalledTimes(1);
1171
- // Verify the store is now empty
1172
- const entities = newStore.select(["position"]);
1173
- expect(entities).toHaveLength(0);
1174
- expect(newStore.resources.time).toEqual({ delta: 0.016, elapsed: 0 });
1175
- // Clean up
1176
- unsubscribePosition();
1177
- unsubscribeTime();
1178
- unsubscribeTransaction();
1179
- newUnsubscribePosition();
1180
- newUnsubscribeTime();
1181
- newUnsubscribeTransaction();
1182
- });
1183
- it("should handle entity observers correctly during restoration", () => {
1184
- const store = createTestDatabase();
1185
- // Create entity and set up observer
1186
- const entity = store.transactions.createPositionEntity({
1187
- position: { x: 1, y: 2, z: 3 }
1188
- });
1189
- const entityObserver = vi.fn();
1190
- const unsubscribe = store.observe.entity(entity)(entityObserver);
1191
- // Clear initial notification
1192
- entityObserver.mockClear();
1193
- // Serialize the database
1194
- const serializedData = store.toData();
1195
- // Create a new database
1196
- const newStore = createTestDatabase();
1197
- // Set up observer on the new store for a different entity
1198
- const newEntity = newStore.transactions.createFullEntity({
1199
- position: { x: 10, y: 20, z: 30 },
1200
- health: { current: 100, max: 100 },
1201
- name: "NewEntity"
1202
- });
1203
- const newEntityObserver = vi.fn();
1204
- const newUnsubscribe = newStore.observe.entity(newEntity)(newEntityObserver);
1205
- // Clear initial notification
1206
- newEntityObserver.mockClear();
1207
- // Restore from serialized data
1208
- newStore.fromData(serializedData);
1209
- // The entity observer should be notified with the restored entity data
1210
- expect(newEntityObserver).toHaveBeenCalledTimes(1);
1211
- const restoredEntityData = newEntityObserver.mock.calls[0][0];
1212
- expect(restoredEntityData).toEqual({
1213
- id: newEntity,
1214
- position: { x: 1, y: 2, z: 3 }
1215
- });
1216
- // Clean up
1217
- unsubscribe();
1218
- newUnsubscribe();
1219
- });
1220
- it("should preserve transaction functionality after restoration", () => {
1221
- const store = createTestDatabase();
1222
- // Create initial state
1223
- store.transactions.updateTime({ delta: 0.016, elapsed: 0 });
1224
- // Serialize the database
1225
- const serializedData = store.toData();
1226
- // Create a new database and restore
1227
- const newStore = createTestDatabase();
1228
- newStore.fromData(serializedData);
1229
- // Verify transactions still work
1230
- const entity = newStore.transactions.createPositionEntity({
1231
- position: { x: 1, y: 2, z: 3 }
1232
- });
1233
- expect(entity).toBeDefined();
1234
- expect(typeof entity).toBe("number");
1235
- const entityData = newStore.read(entity);
1236
- expect(entityData).toEqual({
1237
- id: entity,
1238
- position: { x: 1, y: 2, z: 3 }
1239
- });
1240
- // Verify resource transactions work
1241
- newStore.transactions.updateTime({ delta: 0.033, elapsed: 1.5 });
1242
- expect(newStore.resources.time).toEqual({ delta: 0.033, elapsed: 1.5 });
1243
- });
1244
- it("all transient operations should be rolled back", async () => {
1245
- const store = createTestDatabase();
1246
- const promise = store.transactions.startGenerating(async function* () {
1247
- yield { progress: 0 };
1248
- yield { progress: 1 };
720
+ // Check that the result is a promise
721
+ expect(promise).toBeInstanceOf(Promise);
722
+ const result = await promise;
723
+ expect(result).toBe(-1);
724
+ const generating = await toPromise(store.observe.resources.generating);
725
+ expect(generating).toBe(false);
1249
726
  });
1250
- // Check that the result is a promise
1251
- expect(promise).toBeInstanceOf(Promise);
1252
- const result = await promise;
1253
- expect(result).toBe(-1);
1254
- const generating = await toPromise(store.observe.resources.generating);
1255
- expect(generating).toBe(false);
1256
727
  });
1257
728
  });
1258
729
  //# sourceMappingURL=create-database.test.js.map