@adobe/ecs-persistence 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +49 -27
  2. package/dist/index.js +324 -80
  3. package/dist/index.js.map +1 -1
  4. package/dist/types/decode.d.ts +7 -4
  5. package/dist/types/encode.d.ts +2 -2
  6. package/dist/types/format/format.d.ts +4 -4
  7. package/dist/types/guide/01-saving.d.ts +40 -0
  8. package/dist/types/guide/02-copying.d.ts +23 -0
  9. package/dist/types/guide/03-chunks.d.ts +23 -0
  10. package/dist/types/guide/04-settings.d.ts +55 -0
  11. package/dist/types/index.d.ts +2 -3
  12. package/dist/types/match/keys.d.ts +36 -0
  13. package/dist/types/match/match.d.ts +13 -0
  14. package/dist/types/program/contents.d.ts +5 -3
  15. package/dist/types/program/program.d.ts +12 -5
  16. package/dist/types/selection/selection.d.ts +10 -3
  17. package/dist/types/snapshot.d.ts +15 -5
  18. package/dist/types/spawn.d.ts +30 -13
  19. package/package.json +10 -3
  20. package/src/decode.ts +6 -5
  21. package/src/encode.ts +2 -2
  22. package/src/format/format.ts +8 -8
  23. package/src/guide/01-saving.md +18 -0
  24. package/src/guide/01-saving.out +4 -0
  25. package/src/guide/01-saving.ts +55 -0
  26. package/src/guide/02-copying.md +15 -0
  27. package/src/guide/02-copying.out +4 -0
  28. package/src/guide/02-copying.ts +70 -0
  29. package/src/guide/03-chunks.md +15 -0
  30. package/src/guide/03-chunks.out +3 -0
  31. package/src/guide/03-chunks.ts +75 -0
  32. package/src/guide/04-settings.md +20 -0
  33. package/src/guide/04-settings.out +4 -0
  34. package/src/guide/04-settings.ts +90 -0
  35. package/src/guide/README.md +10 -0
  36. package/src/index.ts +2 -3
  37. package/src/match/keys.ts +105 -0
  38. package/src/match/match.ts +165 -0
  39. package/src/program/contents.ts +5 -3
  40. package/src/program/program.ts +26 -15
  41. package/src/selection/selection.ts +22 -8
  42. package/src/snapshot.ts +16 -8
  43. package/src/spawn.ts +46 -17
  44. package/src/persist.ts +0 -15
package/dist/index.js CHANGED
@@ -1,28 +1,18 @@
1
- import { Component, Resource, System } from "@adobe/ecs";
2
- //#region src/persist.ts
1
+ import { Component, Index, Resource, System } from "@adobe/ecs";
2
+ //#region src/program/program.ts
3
3
  /**
4
- * The trait marking a component or resource to persist, its value the stable name saved data names it by: unlike its
5
- * key's id, the name stays the same across versions and moves of its module. Not on a derived component, which its
6
- * rule derives again once its entity is loaded.
4
+ * The keys of `declarations` with the trait `by`, by the name it gives each, or why one can't be saved: the trait's
5
+ * value isn't a string, or the key isn't data, is derived, or has a name another has.
7
6
  */
8
- var persist = Symbol();
9
- declare$(persist, "trait");
10
- function declare$(key, kind, declaration) {
11
- (globalThis[Symbol.for("@adobe/ecs/declarations@1")] ??= /* @__PURE__ */ new Map()).set(key, {
12
- kind,
13
- declaration
14
- });
15
- }
16
- //#endregion
17
- //#region src/program/program.ts
18
- /** The keys of `declarations` with a `persist` trait by name, or why one can't persist: it isn't data, or a name is taken. */
19
- var namedIn = (declarations, kind) => {
7
+ const namedIn = (declarations, kind, by) => {
20
8
  const named = /* @__PURE__ */ new Map();
21
9
  for (const [key, declaration] of declarations) {
22
- const name = declaration[persist];
23
- if (typeof name !== "string") continue;
24
- if (declaration.type === void 0) return `The ${kind} persisted as "${name}" isn't data: it has no schema.`;
25
- if (named.has(name)) return `Two ${kind}s are persisted as "${name}".`;
10
+ const name = Reflect.get(declaration, by);
11
+ if (name === void 0) continue;
12
+ if (typeof name !== "string") return `A ${kind}'s value of the trait isn't a name: ${String(name)}.`;
13
+ if (declaration.type === void 0) return `The ${kind} the trait names "${name}" isn't data: it has no schema.`;
14
+ if (declaration.derived === true) return `The ${kind} the trait names "${name}" is derived, which its rule derives again once its entity is loaded.`;
15
+ if (named.has(name)) return `The trait names two ${kind}s "${name}".`;
26
16
  named.set(name, {
27
17
  name,
28
18
  key,
@@ -31,13 +21,15 @@ var namedIn = (declarations, kind) => {
31
21
  }
32
22
  return named;
33
23
  };
34
- /** What the program persists, as its modules registered so far declare it, or why it can't. */
35
- var programOf = () => {
36
- const components = namedIn(Component.declarations(), "component");
24
+ /** What the trait `by` covers, as the modules registered so far declare it, or why it can't be saved. */
25
+ const programOf = (by) => {
26
+ if (typeof by !== "symbol") return "`by` is the key of the trait to save by, a symbol.";
27
+ const components = namedIn(Component.declarations(), "component", by);
37
28
  if (typeof components === "string") return components;
38
- const resources = namedIn(Resource.declarations(), "resource");
29
+ const resources = namedIn(Resource.declarations(), "resource", by);
39
30
  if (typeof resources === "string") return resources;
40
31
  return {
32
+ by,
41
33
  components,
42
34
  resources
43
35
  };
@@ -45,8 +37,8 @@ var programOf = () => {
45
37
  //#endregion
46
38
  //#region src/values/values.ts
47
39
  /** A number JSON can't hold, as text: `NaN`, `Infinity`, `-Infinity` or `-0`. */
48
- var numberToJson = (value) => Number.isNaN(value) ? "NaN" : value === Infinity ? "Infinity" : value === -Infinity ? "-Infinity" : Object.is(value, -0) ? "-0" : value;
49
- var numberFromJson = (json) => {
40
+ const numberToJson = (value) => Number.isNaN(value) ? "NaN" : value === Infinity ? "Infinity" : value === -Infinity ? "-Infinity" : Object.is(value, -0) ? "-0" : value;
41
+ const numberFromJson = (json) => {
50
42
  if (typeof json !== "string") return json;
51
43
  switch (json) {
52
44
  case "NaN": return NaN;
@@ -57,7 +49,7 @@ var numberFromJson = (json) => {
57
49
  }
58
50
  };
59
51
  /** Whether a value is of the JSON kind a schema's values are, to pick a union's member. */
60
- var fits = (schema, value) => {
52
+ const fits = (schema, value) => {
61
53
  if ("const" in schema) return Object.is(schema.const, value) || JSON.stringify(schema.const) === JSON.stringify(value);
62
54
  if ("anyOf" in schema) return schema.anyOf.some((member) => fits(member, value));
63
55
  if ("allOf" in schema) return schema.allOf.every((member) => fits(member, value));
@@ -76,7 +68,7 @@ var fits = (schema, value) => {
76
68
  }
77
69
  };
78
70
  /** A value of `schema` as JSON. */
79
- var toJson = (schema, value) => {
71
+ const toJson = (schema, value) => {
80
72
  if ("anyOf" in schema) {
81
73
  const index = schema.anyOf.findIndex((candidate) => fits(candidate, value));
82
74
  const member = schema.anyOf[index];
@@ -102,7 +94,7 @@ var toJson = (schema, value) => {
102
94
  }
103
95
  };
104
96
  /** A value of `schema` read back from its JSON. */
105
- var fromJson = (schema, json) => {
97
+ const fromJson = (schema, json) => {
106
98
  if ("anyOf" in schema) {
107
99
  if (!Array.isArray(json) || typeof json[0] !== "number") return json;
108
100
  const member = schema.anyOf[json[0]];
@@ -140,19 +132,19 @@ var fromJson = (schema, json) => {
140
132
  * layout holds them, and a column of values as JSON text.
141
133
  * A loader views the blocks of bytes where they are, in the buffer it's given.
142
134
  */
143
- var magic = [
135
+ const magic = [
144
136
  69,
145
137
  67,
146
138
  83,
147
139
  80
148
140
  ];
149
- var version = 1;
150
- var prefix = 16;
151
- var aligned = (offset) => Math.ceil(offset / 8) * 8;
152
- var encoder = new TextEncoder();
153
- var decoder = new TextDecoder();
141
+ const version = 1;
142
+ const prefix = 16;
143
+ const aligned = (offset) => Math.ceil(offset / 8) * 8;
144
+ const encoder = new TextEncoder();
145
+ const decoder = new TextDecoder();
154
146
  /** A snapshot's contents as bytes. */
155
- var encodeContents = (snapshot) => {
147
+ const encodeContents = (snapshot) => {
156
148
  const blocks = [];
157
149
  let offset = 0;
158
150
  /** Places a block, returning its offset from the first block. */
@@ -224,9 +216,9 @@ var encodeContents = (snapshot) => {
224
216
  }
225
217
  return bytes;
226
218
  };
227
- var isObject = (value) => typeof value === "object" && value !== null;
219
+ const isObject = (value) => typeof value === "object" && value !== null;
228
220
  /** The header's JSON, checked enough to read it without casts, or null. */
229
- var headerOf = (json) => {
221
+ const headerOf = (json) => {
230
222
  if (!isObject(json)) return null;
231
223
  const components = Reflect.get(json, "components");
232
224
  const resources = Reflect.get(json, "resources");
@@ -254,13 +246,13 @@ var headerOf = (json) => {
254
246
  return json;
255
247
  };
256
248
  /** Whether two schemas are the same, by their JSON in their properties' order, which places a struct's fields. */
257
- var sameSchema = (a, b) => JSON.stringify(a) === JSON.stringify(b);
249
+ const sameSchema = (a, b) => JSON.stringify(a) === JSON.stringify(b);
258
250
  /**
259
- * The snapshot contents `bytes` holds, its components and resources by name as this program persists them, its blocks of
260
- * bytes viewed where they are; or why it can't be read: it isn't one, or names what this program doesn't persist, or
261
- * persists with another schema.
251
+ * The snapshot contents `bytes` holds, its components and resources by the names the trait `by` gives them, its blocks
252
+ * of bytes viewed where they are; or why it can't be read: it isn't one, or names what the trait doesn't cover, or
253
+ * covers with another schema.
262
254
  */
263
- var decodeContents = (bytes) => {
255
+ const decodeContents = (bytes, by) => {
264
256
  if (bytes.byteLength < prefix || magic.some((byte, at) => bytes[at] !== byte)) return { failure: "These bytes aren't a snapshot." };
265
257
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
266
258
  const found = view.getUint32(4, true);
@@ -274,19 +266,19 @@ var decodeContents = (bytes) => {
274
266
  }
275
267
  const header = headerOf(json);
276
268
  if (header === null) return { failure: "This snapshot's header isn't one this package writes." };
277
- const program = programOf();
269
+ const program = programOf(by);
278
270
  if (typeof program === "string") return { failure: program };
279
271
  const components = [];
280
272
  for (const { name, schema } of header.components) {
281
273
  const persisted = program.components.get(name);
282
- if (persisted === void 0) return { failure: `This snapshot holds component "${name}", which this program doesn't persist.` };
274
+ if (persisted === void 0) return { failure: `This snapshot holds component "${name}", which the trait doesn't cover.` };
283
275
  if (!sameSchema(persisted.schema, schema)) return { failure: `Component "${name}" has another schema here than in this snapshot.` };
284
276
  components.push(persisted);
285
277
  }
286
278
  const resources = [];
287
279
  for (const { name, schema, value } of header.resources) {
288
280
  const persisted = program.resources.get(name);
289
- if (persisted === void 0) return { failure: `This snapshot holds resource "${name}", which this program doesn't persist.` };
281
+ if (persisted === void 0) return { failure: `This snapshot holds resource "${name}", which the trait doesn't cover.` };
290
282
  if (!sameSchema(persisted.schema, schema)) return { failure: `Resource "${name}" has another schema here than in this snapshot.` };
291
283
  resources.push({
292
284
  persisted,
@@ -335,6 +327,7 @@ var decodeContents = (bytes) => {
335
327
  });
336
328
  }
337
329
  return {
330
+ by: program.by,
338
331
  components,
339
332
  tables,
340
333
  resources
@@ -343,34 +336,34 @@ var decodeContents = (bytes) => {
343
336
  //#endregion
344
337
  //#region src/program/contents.ts
345
338
  /** Where a snapshot holds what it holds, a key only this package uses. */
346
- var contents = Symbol("snapshot contents");
339
+ const contents = Symbol("snapshot contents");
347
340
  //#endregion
348
341
  //#region src/decode.ts
349
342
  /**
350
- * The snapshot `bytes` holds, its columns of bytes viewed where they are in `bytes`, copied once only when `bytes`
351
- * doesn't start at a multiple of 8; or why it can't be read: it isn't one, or names a component or resource this
352
- * program doesn't persist, or persists with another schema.
343
+ * The snapshot `bytes` holds, read by the trait `by`, its columns of bytes viewed where they are in `bytes`, copied once
344
+ * only when `bytes` doesn't start at a multiple of 8; or why it can't be read: it isn't one, `by` isn't a symbol, or it
345
+ * names a component or resource the trait doesn't cover, or covers with another schema.
353
346
  */
354
- var decode = (bytes) => {
355
- const decoded = decodeContents(bytes);
347
+ const decode = (bytes, options) => {
348
+ const decoded = decodeContents(bytes, options.by);
356
349
  return "failure" in decoded ? decoded : { [contents]: decoded };
357
350
  };
358
351
  //#endregion
359
352
  //#region src/encode.ts
360
353
  /**
361
- * A snapshot as bytes, little-endian: a header naming each persisted component and resource with its schema, then
362
- * 8-byte-aligned blocks of each table's entities and columns, its bytes columns copied whole.
354
+ * A snapshot as bytes, little-endian: a header naming each component and resource it holds by its trait's name for it,
355
+ * with its schema, then 8-byte-aligned blocks of each table's entities and columns, its bytes columns copied whole.
363
356
  */
364
- var encode = (snapshot) => encodeContents(snapshot[contents]);
357
+ const encode = (snapshot) => encodeContents(snapshot[contents]);
365
358
  //#endregion
366
359
  //#region src/selection/selection.ts
367
360
  /** A readonly system reading the tables `declaration` names. */
368
- var tablesSystemOf = (declaration) => System.create({
361
+ const tablesSystemOf = (declaration) => System.create({
369
362
  readonly: true,
370
363
  tables: declaration
371
364
  }, ({ tables }) => tables);
372
365
  /** The rows of `table` whose entities `keep` takes, as a table of their own. */
373
- var rowsOf = (table, keep) => {
366
+ const rowsOf = (table, keep) => {
374
367
  const rows = [];
375
368
  for (let row = 0; row < table.count; row++) if (keep(table.entities[row])) rows.push(row);
376
369
  if (rows.length === table.count) return table;
@@ -402,7 +395,7 @@ var rowsOf = (table, keep) => {
402
395
  };
403
396
  };
404
397
  /** The entities `root` owns, itself included, through the owner references among `owners` in the tables. */
405
- var subtreeOf = (tables, owners, root) => {
398
+ const subtreeOf = (tables, owners, root) => {
406
399
  const children = /* @__PURE__ */ new Map();
407
400
  for (const table of tables) for (const reference of owners) {
408
401
  const key = reference;
@@ -425,8 +418,8 @@ var subtreeOf = (tables, owners, root) => {
425
418
  }
426
419
  return taken;
427
420
  };
428
- /** The persisted part of the entities `selection` takes from `world`, on its thread, or why it can't be read. */
429
- var select = async (world, program, selection) => {
421
+ /** What `program`'s trait covers of the entities `selection` takes from `world`, on its thread, or why it can't be read. */
422
+ const select = async (world, program, selection) => {
430
423
  const components = [...program.components.values()];
431
424
  const keys = components.map((persisted) => persisted.key);
432
425
  const subtree = "root" in selection;
@@ -435,9 +428,17 @@ var select = async (world, program, selection) => {
435
428
  components: keys,
436
429
  with: selection.with ?? [],
437
430
  not: selection.not ?? [],
431
+ where: selection.where ?? {},
438
432
  resources: resources.map((persisted) => persisted.key)
439
433
  };
440
- const read = await world.run(tablesSystemOf(declaration)).catch((error) => ({ failure: error instanceof Error ? error.message : String(error) }));
434
+ const failed = (error) => ({ failure: error instanceof Error ? error.message : String(error) });
435
+ let system;
436
+ try {
437
+ system = tablesSystemOf(declaration);
438
+ } catch (error) {
439
+ return failed(error);
440
+ }
441
+ const read = await world.run(system).catch(failed);
441
442
  if ("failure" in read) return read;
442
443
  let tables = read.archetypes;
443
444
  if (subtree) {
@@ -446,6 +447,7 @@ var select = async (world, program, selection) => {
446
447
  tables = tables.flatMap((table) => rowsOf(table, (entity) => taken.has(entity)) ?? []);
447
448
  }
448
449
  return {
450
+ by: program.by,
449
451
  components,
450
452
  tables,
451
453
  resources: resources.map((persisted) => ({
@@ -457,39 +459,281 @@ var select = async (world, program, selection) => {
457
459
  //#endregion
458
460
  //#region src/snapshot.ts
459
461
  /**
460
- * A snapshot of some of `world`'s entities, read on its thread: by default every entity holding a persisted
461
- * component, and the persisted resources; with `with` and `not`, those a query matches, and the resources unless
462
- * `resources` is false; with `root`, it and every entity it owns, through persisted owner references, without
463
- * resources. Fails, as data, when the program's persisted keys can't be saved or the world can't be read.
462
+ * A snapshot of some of `world`'s entities, read on its thread, by the trait `by`: a trait of the application's own,
463
+ * whose value on a component's or resource's declaration is the name it's saved under. Of each entity it takes, it
464
+ * takes the components the trait covers. By default it takes every entity holding one of them, and the resources the
465
+ * trait covers; with `with` and `not`, those a query matches, and the resources unless `resources` is false; with
466
+ * `where`, those of the archetypes holding its values of shared components, as a system's `where` of values keeps it to
467
+ * them, read whole, so a snapshot of one value's entities costs what they hold, not the world; with `root`, it and every
468
+ * entity it owns, through owner references the trait covers, without resources. Fails, as data, when `by` isn't a
469
+ * symbol, what the trait covers can't be saved, `where` names what isn't a shared component or gives one a value not
470
+ * of its type, or the world can't be read.
464
471
  */
465
- var snapshot = async (input) => {
466
- const program = programOf();
472
+ const snapshot = async (input) => {
473
+ const program = programOf(input.by);
467
474
  if (typeof program === "string") return { failure: program };
468
475
  const taken = await select(input.world, program, input);
469
476
  return "failure" in taken ? taken : { [contents]: taken };
470
477
  };
471
478
  //#endregion
479
+ //#region src/match/keys.ts
480
+ /** A table for up to `capacity` keys. */
481
+ const createKeyTable = (capacity) => {
482
+ let size = 16;
483
+ while (size < 2 * capacity) size *= 2;
484
+ return {
485
+ mask: size - 1,
486
+ slots: new Int32Array(2 * size),
487
+ tables: new Int32Array(capacity),
488
+ rows: new Int32Array(capacity),
489
+ values: new Float64Array(capacity),
490
+ columns: [],
491
+ count: 0
492
+ };
493
+ };
494
+ /** FNV-1a over a row's bytes in each column, then mixed so its low bits, which pick a slot, depend on all of them. */
495
+ const hashOf = (columns, row) => {
496
+ let hash = 2166136261;
497
+ for (let c = 0; c < columns.length; c++) {
498
+ const { bytes, stride } = columns[c];
499
+ for (let at = row * stride, end = at + stride; at < end; at++) hash = Math.imul(hash ^ bytes[at], 16777619);
500
+ }
501
+ hash ^= hash >>> 16;
502
+ hash = Math.imul(hash, 2246822507);
503
+ hash ^= hash >>> 13;
504
+ hash = Math.imul(hash, 3266489909);
505
+ return hash ^ hash >>> 16;
506
+ };
507
+ const equalAt = (a, rowA, b, rowB) => {
508
+ if (a.length !== b.length) return false;
509
+ for (let c = 0; c < a.length; c++) {
510
+ const { bytes: left, stride } = a[c];
511
+ const { bytes: right, stride: other } = b[c];
512
+ if (stride !== other) return false;
513
+ for (let k = 0, i = rowA * stride, j = rowB * stride; k < stride; k++) if (left[i + k] !== right[j + k]) return false;
514
+ }
515
+ return true;
516
+ };
517
+ /** The slot holding `row`'s key, or the empty one where it would go. */
518
+ const slotOf = (table, columns, row, hash) => {
519
+ const { mask, slots, tables, rows } = table;
520
+ for (let slot = hash & mask;; slot = slot + 1 & mask) {
521
+ const entry = slots[2 * slot + 1] - 1;
522
+ if (entry < 0) return slot;
523
+ if (slots[2 * slot] === hash && equalAt(table.columns[tables[entry]], rows[entry], columns, row)) return slot;
524
+ }
525
+ };
526
+ /**
527
+ * Begins adding the rows of a table of `columns`; `addKey` then takes rows of it. Each table's columns are kept, since
528
+ * its entries' keys are their rows' bytes.
529
+ */
530
+ const addKeyColumns = (table, columns) => {
531
+ table.columns.push(columns);
532
+ };
533
+ /**
534
+ * Adds row `row` of the table `addKeyColumns` began last, with `value`, unless a key equal to its is there: whether
535
+ * it was added. Takes at most the capacity the table was created with.
536
+ */
537
+ const addKey = (table, row, value) => {
538
+ const at = table.columns.length - 1;
539
+ const columns = table.columns[at];
540
+ const hash = hashOf(columns, row);
541
+ const slot = slotOf(table, columns, row, hash);
542
+ if (table.slots[2 * slot + 1] !== 0) return false;
543
+ const entry = table.count++;
544
+ table.slots[2 * slot] = hash;
545
+ table.slots[2 * slot + 1] = entry + 1;
546
+ table.tables[entry] = at;
547
+ table.rows[entry] = row;
548
+ table.values[entry] = value;
549
+ return true;
550
+ };
551
+ /**
552
+ * The entry whose key is equal to row `row`'s of `columns`, or -1 for none. `guess` is an entry to compare first, such
553
+ * as the one after the entry found for the row before: rows in the order they were added are found without hashing.
554
+ */
555
+ const findKey = (table, columns, row, guess) => {
556
+ if (guess >= 0 && guess < table.count && equalAt(table.columns[table.tables[guess]], table.rows[guess], columns, row)) return guess;
557
+ return table.slots[2 * slotOf(table, columns, row, hashOf(columns, row)) + 1] - 1;
558
+ };
559
+ //#endregion
560
+ //#region src/match/match.ts
561
+ /** What a match reads of a world: the tables of the entities an index holds, with its key's columns. */
562
+ const keysSystemOf = (declaration) => System.create({
563
+ readonly: true,
564
+ tables: {
565
+ components: declaration.key,
566
+ with: [...declaration.key, ...declaration.components.with ?? []],
567
+ not: declaration.components.not ?? []
568
+ }
569
+ }, ({ tables }) => tables.archetypes);
570
+ /** Bytes as text, a character each, so equal bytes give equal text and a row's are a slice of its column's. */
571
+ const bytesDecoder = new TextDecoder("latin1");
572
+ /**
573
+ * Each row's key in a table, as text, or null for a table without a column of each part: each part's column, its bytes
574
+ * in the world's layout, which two worlds of one program share, or its value as JSON by its schema. Equal keys give
575
+ * equal text.
576
+ */
577
+ const keysOf = (table, parts) => {
578
+ const columns = [];
579
+ for (const { key, schema } of parts) {
580
+ const column = table.columns[key];
581
+ if (column === void 0) return null;
582
+ if (column.kind === "bytes") {
583
+ const text = bytesDecoder.decode(column.bytes);
584
+ const { stride } = column;
585
+ columns.push((row) => text.slice(row * stride, (row + 1) * stride));
586
+ } else if (column.kind === "values") columns.push((row) => JSON.stringify(toJson(schema, column.values[row])));
587
+ else {
588
+ const text = JSON.stringify(toJson(schema, column.value));
589
+ columns.push(() => text);
590
+ }
591
+ }
592
+ const [only] = columns;
593
+ return columns.length === 1 && only !== void 0 ? only : (row) => columns.map((part) => part(row)).join("\0");
594
+ };
595
+ /** Whether a table holds the entities an index holds: every key part and `with` component, and none in `not`. */
596
+ const covers = (table, declaration) => [...declaration.key, ...declaration.components.with ?? []].every((key) => table.columns[key] !== void 0) && (declaration.components.not ?? []).every((key) => table.columns[key] === void 0);
597
+ const twoWithOneKey = (name) => ({ failure: `Index "${name}" can't match: two of the snapshot's entities have one key.` });
598
+ const readWorld = (world, declaration) => world.run(keysSystemOf(declaration)).catch((error) => ({ failure: error instanceof Error ? error.message : String(error) }));
599
+ /** Two of the world's entities with one key: the one with the lower entity index, as its unique index keeps. */
600
+ const keep = (existing, entity, candidate) => {
601
+ const kept = existing.get(entity);
602
+ if (kept === void 0 || candidate >>> 0 < kept >>> 0) existing.set(entity, candidate);
603
+ };
604
+ /** A table's columns of each part, or null for one without a column of bytes for each. */
605
+ const bytesOf = (table, parts) => {
606
+ const columns = [];
607
+ for (const { key } of parts) {
608
+ const column = table.columns[key];
609
+ if (column === void 0 || column.kind !== "bytes") return null;
610
+ columns.push(column);
611
+ }
612
+ return columns;
613
+ };
614
+ /**
615
+ * Matches by the bytes of keys' columns, without making text of them, when every table of the snapshot holding the
616
+ * index's entities has columns of bytes for its key; null otherwise. The world's tables, of the same program, then do too.
617
+ */
618
+ const matchBytes = (world, from, declaration, parts) => {
619
+ const tables = from.tables.filter((table) => covers(table, declaration));
620
+ const columns = tables.map((table) => bytesOf(table, parts));
621
+ if (columns.some((item) => item === null)) return null;
622
+ return async (name, existing) => {
623
+ const saved = createKeyTable(tables.reduce((sum, table) => sum + table.count, 0));
624
+ for (let t = 0; t < tables.length; t++) {
625
+ const keys = columns[t];
626
+ if (keys === null) continue;
627
+ addKeyColumns(saved, keys);
628
+ const { entities, count } = tables[t];
629
+ for (let row = 0; row < count; row++) if (!addKey(saved, row, entities[row])) return twoWithOneKey(name);
630
+ }
631
+ if (saved.count === 0) return void 0;
632
+ const read = await readWorld(world, declaration);
633
+ if ("failure" in read) return read;
634
+ for (const table of read) {
635
+ const keys = bytesOf(table, parts);
636
+ if (keys === null) continue;
637
+ for (let row = 0, entry = -1; row < table.count; row++) {
638
+ entry = findKey(saved, keys, row, entry + 1);
639
+ if (entry >= 0) keep(existing, saved.values[entry], table.entities[row]);
640
+ }
641
+ }
642
+ };
643
+ };
644
+ /** Matches by keys as text, which any column gives. */
645
+ const matchText = (world, from, declaration, parts) => async (name, existing) => {
646
+ const saved = /* @__PURE__ */ new Map();
647
+ for (const table of from.tables) {
648
+ if (!covers(table, declaration)) continue;
649
+ const keyAt = keysOf(table, parts);
650
+ if (keyAt === null) continue;
651
+ for (let row = 0; row < table.count; row++) {
652
+ const key = keyAt(row);
653
+ if (saved.has(key)) return twoWithOneKey(name);
654
+ saved.set(key, table.entities[row]);
655
+ }
656
+ }
657
+ if (saved.size === 0) return void 0;
658
+ const read = await readWorld(world, declaration);
659
+ if ("failure" in read) return read;
660
+ for (const table of read) {
661
+ const keyAt = keysOf(table, parts);
662
+ if (keyAt === null) continue;
663
+ for (let row = 0; row < table.count; row++) {
664
+ const entity = saved.get(keyAt(row));
665
+ if (entity !== void 0) keep(existing, entity, table.entities[row]);
666
+ }
667
+ }
668
+ };
669
+ /**
670
+ * Each of the snapshot's entities that `indexes` say is the same as one of `world`'s, to that one: an index holds both,
671
+ * with equal keys. Fails, as data, for an index whose key, `with` or `not` names a component the snapshot's trait doesn't
672
+ * cover, or two of the snapshot's entities one index holds with one key.
673
+ */
674
+ const existingFor = async (world, from, indexes) => {
675
+ const existing = /* @__PURE__ */ new Map();
676
+ const persisted = new Map(from.components.map((item) => [item.key, item]));
677
+ for (const index of indexes) {
678
+ const declaration = Index.declarationOf(index);
679
+ if (declaration === void 0) return { failure: `${index.name} isn't an index.` };
680
+ const parts = [];
681
+ for (const key of declaration.key) {
682
+ const found = persisted.get(key);
683
+ if (found === void 0) return { failure: `Index "${index.name}" can't match: a part of its key isn't one the snapshot's trait covers, so the snapshot holds no key for it.` };
684
+ parts.push({
685
+ key,
686
+ schema: found.schema
687
+ });
688
+ }
689
+ if ([...declaration.components.with ?? [], ...declaration.components.not ?? []].some((key) => !persisted.has(key))) return { failure: `Index "${index.name}" can't match: its with or not names a component the snapshot's trait doesn't cover, so the snapshot can't tell which of its entities the index holds.` };
690
+ const found = await (matchBytes(world, from, declaration, parts) ?? matchText(world, from, declaration, parts))(index.name, existing);
691
+ if (found !== void 0) return found;
692
+ }
693
+ return existing;
694
+ };
695
+ //#endregion
472
696
  //#region src/spawn.ts
473
- var isTables = (value) => Array.isArray(value) && value.every((table) => typeof table === "object" && table !== null && typeof Reflect.get(table, "count") === "number" && Reflect.get(table, "entities") instanceof Float64Array && typeof Reflect.get(table, "columns") === "object");
474
- var isKeyed = (value) => typeof value === "object" && value !== null;
475
- /** Spawns its input's tables and sets its resources, a snapshot's, rewriting their entities. */
476
- var spawnTables = System.create({}, ({ commands }, input) => {
477
- if (!isTables(input.tables) || !isKeyed(input.resources)) throw new Error("A snapshot holds tables and resources.");
478
- return commands.spawnTables(input.tables, input.resources);
697
+ const isTables = (value) => Array.isArray(value) && value.every((table) => typeof table === "object" && table !== null && typeof Reflect.get(table, "count") === "number" && Reflect.get(table, "entities") instanceof Float64Array && typeof Reflect.get(table, "columns") === "object");
698
+ const isKeyed = (value) => typeof value === "object" && value !== null;
699
+ /** `spawnTables` takes entities by their numbers: a map of the snapshot's to the world's holds only numbers. */
700
+ const isEntities = (value) => {
701
+ if (!(value instanceof Map)) return false;
702
+ for (const [saved, found] of value) if (typeof saved !== "number" || typeof found !== "number") return false;
703
+ return true;
704
+ };
705
+ /**
706
+ * Spawns its input's tables and sets its resources, a snapshot's, rewriting their entities, those `existing` maps
707
+ * standing for theirs, onto which `update` writes their rows.
708
+ */
709
+ const spawnTables = System.create({}, ({ commands }, input) => {
710
+ if (!isTables(input.tables) || !isKeyed(input.resources) || !isEntities(input.existing)) throw new Error("A snapshot holds tables and resources.");
711
+ return commands.spawnTables(input.tables, {
712
+ resources: input.resources,
713
+ existing: input.existing,
714
+ update: input.update
715
+ });
479
716
  }, {
480
717
  "type": "object",
481
718
  "properties": {
482
719
  "tables": { "allOf": [] },
483
- "resources": { "allOf": [] }
720
+ "resources": { "allOf": [] },
721
+ "existing": { "allOf": [] },
722
+ "update": { "type": "boolean" }
484
723
  }
485
724
  });
486
725
  async function spawn(input) {
487
- const { tables, resources } = input.snapshot[contents];
726
+ const from = input.snapshot[contents];
727
+ const { tables, resources } = from;
488
728
  const values = {};
489
729
  for (const { persisted, value } of resources) values[persisted.key] = value;
730
+ const existing = input.match === void 0 || input.match.length === 0 ? /* @__PURE__ */ new Map() : await existingFor(input.world, from, input.match);
731
+ if ("failure" in existing) return existing;
490
732
  const spawned = await input.world.run(spawnTables, {
491
733
  tables,
492
- resources: values
734
+ resources: values,
735
+ existing,
736
+ update: input.update === true
493
737
  }).catch((error) => ({ failure: error instanceof Error ? error.message : String(error) }));
494
738
  if ("failure" in spawned) return spawned;
495
739
  if (input.entities === false) return { dropped: spawned.dropped };
@@ -498,8 +742,8 @@ async function spawn(input) {
498
742
  entities: entitiesOf(tables, spawned.entities)
499
743
  };
500
744
  }
501
- /** Each of the tables' entities to the one spawned for it, or null for one that wasn't. */
502
- var entitiesOf = (tables, spawned) => {
745
+ /** Each of the tables' entities to the one spawned for it, the one existing for it, or null for one that wasn't. */
746
+ const entitiesOf = (tables, spawned) => {
503
747
  const entities = /* @__PURE__ */ new Map();
504
748
  tables.forEach((table, t) => {
505
749
  const made = spawned[t];
@@ -511,6 +755,6 @@ var entitiesOf = (tables, spawned) => {
511
755
  return entities;
512
756
  };
513
757
  //#endregion
514
- export { decode, encode, persist, snapshot, spawn };
758
+ export { decode, encode, snapshot, spawn };
515
759
 
516
760
  //# sourceMappingURL=index.js.map