@adobe/ecs-persistence 0.0.0-stage → 0.1.0

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -2
  3. package/dist/index.js +754 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/types/decode.d.ts +11 -0
  6. package/dist/types/encode.d.ts +6 -0
  7. package/dist/types/failure.d.ts +4 -0
  8. package/dist/types/format/format.d.ts +10 -0
  9. package/dist/types/guide/01-saving.d.ts +40 -0
  10. package/dist/types/guide/02-copying.d.ts +23 -0
  11. package/dist/types/guide/03-chunks.d.ts +23 -0
  12. package/dist/types/guide/04-settings.d.ts +45 -0
  13. package/dist/types/index.d.ts +10 -0
  14. package/dist/types/match/keys.d.ts +36 -0
  15. package/dist/types/match/match.d.ts +13 -0
  16. package/dist/types/persist.d.ts +14 -0
  17. package/dist/types/program/contents.d.ts +19 -0
  18. package/dist/types/program/program.d.ts +21 -0
  19. package/dist/types/selection/selection.d.ts +24 -0
  20. package/dist/types/snapshot.d.ts +28 -0
  21. package/dist/types/spawn.d.ts +42 -0
  22. package/dist/types/values/values.d.ts +5 -0
  23. package/package.json +46 -3
  24. package/src/decode.ts +16 -0
  25. package/src/encode.ts +10 -0
  26. package/src/failure.ts +4 -0
  27. package/src/format/format.ts +185 -0
  28. package/src/guide/01-saving.md +18 -0
  29. package/src/guide/01-saving.out +4 -0
  30. package/src/guide/01-saving.ts +55 -0
  31. package/src/guide/02-copying.md +15 -0
  32. package/src/guide/02-copying.out +4 -0
  33. package/src/guide/02-copying.ts +70 -0
  34. package/src/guide/03-chunks.md +15 -0
  35. package/src/guide/03-chunks.out +3 -0
  36. package/src/guide/03-chunks.ts +75 -0
  37. package/src/guide/04-settings.md +15 -0
  38. package/src/guide/04-settings.out +4 -0
  39. package/src/guide/04-settings.ts +75 -0
  40. package/src/guide/README.md +10 -0
  41. package/src/index.ts +11 -0
  42. package/src/match/keys.ts +105 -0
  43. package/src/match/match.ts +165 -0
  44. package/src/program/contents.ts +19 -0
  45. package/src/program/program.ts +45 -0
  46. package/src/selection/selection.ts +110 -0
  47. package/src/snapshot.ts +30 -0
  48. package/src/spawn.ts +79 -0
  49. package/src/values/values.ts +97 -0
package/dist/index.js ADDED
@@ -0,0 +1,754 @@
1
+ import { Component, Index, Resource, System } from "@adobe/ecs";
2
+ //#region src/program/program.ts
3
+ /**
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.
6
+ */
7
+ const namedIn = (declarations, kind, by) => {
8
+ const named = /* @__PURE__ */ new Map();
9
+ for (const [key, declaration] of declarations) {
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}".`;
16
+ named.set(name, {
17
+ name,
18
+ key,
19
+ schema: declaration.type
20
+ });
21
+ }
22
+ return named;
23
+ };
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);
28
+ if (typeof components === "string") return components;
29
+ const resources = namedIn(Resource.declarations(), "resource", by);
30
+ if (typeof resources === "string") return resources;
31
+ return {
32
+ by,
33
+ components,
34
+ resources
35
+ };
36
+ };
37
+ //#endregion
38
+ //#region src/values/values.ts
39
+ /** A number JSON can't hold, as text: `NaN`, `Infinity`, `-Infinity` or `-0`. */
40
+ const numberToJson = (value) => Number.isNaN(value) ? "NaN" : value === Infinity ? "Infinity" : value === -Infinity ? "-Infinity" : Object.is(value, -0) ? "-0" : value;
41
+ const numberFromJson = (json) => {
42
+ if (typeof json !== "string") return json;
43
+ switch (json) {
44
+ case "NaN": return NaN;
45
+ case "Infinity": return Infinity;
46
+ case "-Infinity": return -Infinity;
47
+ case "-0": return -0;
48
+ default: return json;
49
+ }
50
+ };
51
+ /** Whether a value is of the JSON kind a schema's values are, to pick a union's member. */
52
+ const fits = (schema, value) => {
53
+ if ("const" in schema) return Object.is(schema.const, value) || JSON.stringify(schema.const) === JSON.stringify(value);
54
+ if ("anyOf" in schema) return schema.anyOf.some((member) => fits(member, value));
55
+ if ("allOf" in schema) return schema.allOf.every((member) => fits(member, value));
56
+ if (!("type" in schema)) return true;
57
+ switch (schema.type) {
58
+ case "number":
59
+ case "integer": return typeof value === "number";
60
+ case "string": return typeof value === "string";
61
+ case "boolean": return typeof value === "boolean";
62
+ case "null": return value === null;
63
+ case "array": return Array.isArray(value);
64
+ case "set": return value instanceof Set;
65
+ case "map": return value instanceof Map;
66
+ case "object": return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Set) && !(value instanceof Map);
67
+ default: return true;
68
+ }
69
+ };
70
+ /** A value of `schema` as JSON. */
71
+ const toJson = (schema, value) => {
72
+ if ("anyOf" in schema) {
73
+ const index = schema.anyOf.findIndex((candidate) => fits(candidate, value));
74
+ const member = schema.anyOf[index];
75
+ return member === void 0 ? [-1, value] : [index, toJson(member, value)];
76
+ }
77
+ if (typeof value === "number") return numberToJson(value);
78
+ if (!("type" in schema)) return value;
79
+ switch (schema.type) {
80
+ case "array": return Array.isArray(value) ? value.map((item) => toJson(schema.items, item)) : value;
81
+ case "set": return value instanceof Set ? Array.from(value, (item) => toJson(schema.items, item)) : value;
82
+ case "map": return value instanceof Map ? Array.from(value, ([key, item]) => [toJson(schema.keys, key), toJson(schema.values, item)]) : value;
83
+ case "object": {
84
+ if (typeof value !== "object" || value === null) return value;
85
+ const json = {};
86
+ for (const name of Object.keys(value)) {
87
+ const field = schema.properties[name];
88
+ const item = Reflect.get(value, name);
89
+ if (item !== void 0) json[name] = field === void 0 ? item : toJson(field, item);
90
+ }
91
+ return json;
92
+ }
93
+ default: return value;
94
+ }
95
+ };
96
+ /** A value of `schema` read back from its JSON. */
97
+ const fromJson = (schema, json) => {
98
+ if ("anyOf" in schema) {
99
+ if (!Array.isArray(json) || typeof json[0] !== "number") return json;
100
+ const member = schema.anyOf[json[0]];
101
+ return member === void 0 ? json[1] : fromJson(member, json[1]);
102
+ }
103
+ if ("const" in schema) return schema.const;
104
+ if (!("type" in schema)) return json;
105
+ switch (schema.type) {
106
+ case "number":
107
+ case "integer": return numberFromJson(json);
108
+ case "array": return Array.isArray(json) ? json.map((item) => fromJson(schema.items, item)) : json;
109
+ case "set": return Array.isArray(json) ? new Set(json.map((item) => fromJson(schema.items, item))) : json;
110
+ case "map": return Array.isArray(json) ? new Map(json.map((entry) => Array.isArray(entry) ? [fromJson(schema.keys, entry[0]), fromJson(schema.values, entry[1])] : [entry, void 0])) : json;
111
+ case "object": {
112
+ if (typeof json !== "object" || json === null || Array.isArray(json)) return json;
113
+ const value = {};
114
+ for (const name of Object.keys(json)) {
115
+ const field = schema.properties[name];
116
+ const item = Reflect.get(json, name);
117
+ value[name] = field === void 0 ? item : fromJson(field, item);
118
+ }
119
+ return value;
120
+ }
121
+ default: return json;
122
+ }
123
+ };
124
+ //#endregion
125
+ //#region src/format/format.ts
126
+ /**
127
+ * The binary form of a snapshot, little-endian:
128
+ * - 16 bytes: `ECSP`, the format's version as a u32, the header's length in bytes as a u32, and 4 bytes of zeros;
129
+ * - the header, JSON text padded with spaces to a multiple of 8 bytes: each component and resource by name with its
130
+ * schema, and each table's count and where its entities and columns are;
131
+ * - blocks, each starting at a multiple of 8 bytes: a table's entities as f64s, a column's bytes as its components'
132
+ * layout holds them, and a column of values as JSON text.
133
+ * A loader views the blocks of bytes where they are, in the buffer it's given.
134
+ */
135
+ const magic = [
136
+ 69,
137
+ 67,
138
+ 83,
139
+ 80
140
+ ];
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();
146
+ /** A snapshot's contents as bytes. */
147
+ const encodeContents = (snapshot) => {
148
+ const blocks = [];
149
+ let offset = 0;
150
+ /** Places a block, returning its offset from the first block. */
151
+ const place = (bytes) => {
152
+ const at = offset;
153
+ blocks.push(bytes);
154
+ offset = aligned(offset + bytes.byteLength);
155
+ return at;
156
+ };
157
+ const indexOf = new Map(snapshot.components.map((persisted, index) => [persisted.key, index]));
158
+ const tables = snapshot.tables.map((table) => {
159
+ const entities = place(new Uint8Array(table.entities.buffer, table.entities.byteOffset, table.entities.byteLength));
160
+ const columns = snapshot.components.flatMap((persisted) => {
161
+ const key = persisted.key;
162
+ const column = Reflect.get(table.columns, key);
163
+ const component = indexOf.get(persisted.key) ?? -1;
164
+ if (column === void 0) return [];
165
+ if (column.kind === "bytes") return [{
166
+ component,
167
+ kind: "bytes",
168
+ offset: place(column.bytes),
169
+ length: column.bytes.byteLength,
170
+ stride: column.stride
171
+ }];
172
+ if (column.kind === "value") return [{
173
+ component,
174
+ kind: "value",
175
+ value: toJson(persisted.schema, column.value)
176
+ }];
177
+ const text = encoder.encode(JSON.stringify(column.values.map((value) => toJson(persisted.schema, value))));
178
+ return [{
179
+ component,
180
+ kind: "values",
181
+ offset: place(text),
182
+ length: text.byteLength
183
+ }];
184
+ });
185
+ return {
186
+ count: table.count,
187
+ entities,
188
+ columns
189
+ };
190
+ });
191
+ const header = {
192
+ components: snapshot.components.map(({ name, schema }) => ({
193
+ name,
194
+ schema
195
+ })),
196
+ resources: snapshot.resources.map(({ persisted, value }) => ({
197
+ name: persisted.name,
198
+ schema: persisted.schema,
199
+ value: toJson(persisted.schema, value)
200
+ })),
201
+ tables
202
+ };
203
+ const text = encoder.encode(JSON.stringify(header));
204
+ const headerLength = aligned(text.byteLength);
205
+ const bytes = new Uint8Array(prefix + headerLength + offset);
206
+ bytes.set(magic, 0);
207
+ const view = new DataView(bytes.buffer);
208
+ view.setUint32(4, version, true);
209
+ view.setUint32(8, headerLength, true);
210
+ bytes.set(text, prefix);
211
+ bytes.fill(32, prefix + text.byteLength, prefix + headerLength);
212
+ let at = prefix + headerLength;
213
+ for (const block of blocks) {
214
+ bytes.set(block, at);
215
+ at = aligned(at + block.byteLength);
216
+ }
217
+ return bytes;
218
+ };
219
+ const isObject = (value) => typeof value === "object" && value !== null;
220
+ /** The header's JSON, checked enough to read it without casts, or null. */
221
+ const headerOf = (json) => {
222
+ if (!isObject(json)) return null;
223
+ const components = Reflect.get(json, "components");
224
+ const resources = Reflect.get(json, "resources");
225
+ const tables = Reflect.get(json, "tables");
226
+ if (!Array.isArray(components) || !Array.isArray(resources) || !Array.isArray(tables)) return null;
227
+ const named = (item) => isObject(item) && typeof Reflect.get(item, "name") === "string" && isObject(Reflect.get(item, "schema"));
228
+ if (!components.every(named) || !resources.every(named)) return null;
229
+ const isColumn = (item) => {
230
+ if (!isObject(item) || typeof Reflect.get(item, "component") !== "number") return false;
231
+ const kind = Reflect.get(item, "kind");
232
+ if (kind === "value") return true;
233
+ const numbers = kind === "bytes" ? [
234
+ "offset",
235
+ "length",
236
+ "stride"
237
+ ] : kind === "values" ? ["offset", "length"] : null;
238
+ return numbers !== null && numbers.every((name) => typeof Reflect.get(item, name) === "number");
239
+ };
240
+ const isTable = (item) => {
241
+ if (!isObject(item) || typeof Reflect.get(item, "count") !== "number" || typeof Reflect.get(item, "entities") !== "number") return false;
242
+ const columns = Reflect.get(item, "columns");
243
+ return Array.isArray(columns) && columns.every(isColumn);
244
+ };
245
+ if (!tables.every(isTable)) return null;
246
+ return json;
247
+ };
248
+ /** Whether two schemas are the same, by their JSON in their properties' order, which places a struct's fields. */
249
+ const sameSchema = (a, b) => JSON.stringify(a) === JSON.stringify(b);
250
+ /**
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.
254
+ */
255
+ const decodeContents = (bytes, by) => {
256
+ if (bytes.byteLength < prefix || magic.some((byte, at) => bytes[at] !== byte)) return { failure: "These bytes aren't a snapshot." };
257
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
258
+ const found = view.getUint32(4, true);
259
+ if (found !== version) return { failure: `This snapshot's format is version ${found}; this package reads version ${version}.` };
260
+ const headerLength = view.getUint32(8, true);
261
+ let json;
262
+ try {
263
+ json = JSON.parse(decoder.decode(bytes.subarray(prefix, prefix + headerLength)));
264
+ } catch {
265
+ return { failure: "This snapshot's header isn't JSON." };
266
+ }
267
+ const header = headerOf(json);
268
+ if (header === null) return { failure: "This snapshot's header isn't one this package writes." };
269
+ const program = programOf(by);
270
+ if (typeof program === "string") return { failure: program };
271
+ const components = [];
272
+ for (const { name, schema } of header.components) {
273
+ const persisted = program.components.get(name);
274
+ if (persisted === void 0) return { failure: `This snapshot holds component "${name}", which the trait doesn't cover.` };
275
+ if (!sameSchema(persisted.schema, schema)) return { failure: `Component "${name}" has another schema here than in this snapshot.` };
276
+ components.push(persisted);
277
+ }
278
+ const resources = [];
279
+ for (const { name, schema, value } of header.resources) {
280
+ const persisted = program.resources.get(name);
281
+ if (persisted === void 0) return { failure: `This snapshot holds resource "${name}", which the trait doesn't cover.` };
282
+ if (!sameSchema(persisted.schema, schema)) return { failure: `Resource "${name}" has another schema here than in this snapshot.` };
283
+ resources.push({
284
+ persisted,
285
+ value: fromJson(schema, value)
286
+ });
287
+ }
288
+ const blocks = (bytes.byteOffset + prefix + headerLength) % 8 === 0 ? bytes : bytes.slice();
289
+ const base = blocks.byteOffset + prefix + headerLength;
290
+ const end = blocks.byteOffset + blocks.byteLength;
291
+ const inBounds = (offset, length) => offset >= 0 && length >= 0 && base + offset + length <= end;
292
+ const tables = [];
293
+ for (const table of header.tables) {
294
+ if (!inBounds(table.entities, table.count * 8)) return { failure: "This snapshot's blocks are cut short." };
295
+ const columns = {};
296
+ for (const column of table.columns) {
297
+ const persisted = components[column.component];
298
+ if (persisted === void 0) return { failure: "This snapshot names a component it doesn't hold." };
299
+ if (column.kind === "value") columns[persisted.key] = {
300
+ kind: "value",
301
+ value: fromJson(persisted.schema, column.value)
302
+ };
303
+ else if (!inBounds(column.offset, column.length)) return { failure: "This snapshot's blocks are cut short." };
304
+ else if (column.kind === "bytes") columns[persisted.key] = {
305
+ kind: "bytes",
306
+ bytes: new Uint8Array(blocks.buffer, base + column.offset, column.length),
307
+ stride: column.stride
308
+ };
309
+ else {
310
+ let values;
311
+ try {
312
+ values = JSON.parse(decoder.decode(new Uint8Array(blocks.buffer, base + column.offset, column.length)));
313
+ } catch {
314
+ return { failure: `Component "${persisted.name}"'s values in this snapshot aren't JSON.` };
315
+ }
316
+ if (!Array.isArray(values)) return { failure: `Component "${persisted.name}"'s values in this snapshot aren't a list.` };
317
+ columns[persisted.key] = {
318
+ kind: "values",
319
+ values: values.map((value) => fromJson(persisted.schema, value))
320
+ };
321
+ }
322
+ }
323
+ tables.push({
324
+ count: table.count,
325
+ entities: new Float64Array(blocks.buffer, base + table.entities, table.count),
326
+ columns
327
+ });
328
+ }
329
+ return {
330
+ by: program.by,
331
+ components,
332
+ tables,
333
+ resources
334
+ };
335
+ };
336
+ //#endregion
337
+ //#region src/program/contents.ts
338
+ /** Where a snapshot holds what it holds, a key only this package uses. */
339
+ const contents = Symbol("snapshot contents");
340
+ //#endregion
341
+ //#region src/decode.ts
342
+ /**
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.
346
+ */
347
+ const decode = (bytes, options) => {
348
+ const decoded = decodeContents(bytes, options.by);
349
+ return "failure" in decoded ? decoded : { [contents]: decoded };
350
+ };
351
+ //#endregion
352
+ //#region src/encode.ts
353
+ /**
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.
356
+ */
357
+ const encode = (snapshot) => encodeContents(snapshot[contents]);
358
+ //#endregion
359
+ //#region src/selection/selection.ts
360
+ /** A readonly system reading the tables `declaration` names. */
361
+ const tablesSystemOf = (declaration) => System.create({
362
+ readonly: true,
363
+ tables: declaration
364
+ }, ({ tables }) => tables);
365
+ /** The rows of `table` whose entities `keep` takes, as a table of their own. */
366
+ const rowsOf = (table, keep) => {
367
+ const rows = [];
368
+ for (let row = 0; row < table.count; row++) if (keep(table.entities[row])) rows.push(row);
369
+ if (rows.length === table.count) return table;
370
+ if (rows.length === 0) return null;
371
+ const columns = {};
372
+ for (const key of Object.getOwnPropertySymbols(table.columns)) {
373
+ const column = Reflect.get(table.columns, key);
374
+ if (column === void 0) continue;
375
+ if (column.kind === "value") columns[key] = column;
376
+ else if (column.kind === "values") columns[key] = {
377
+ kind: "values",
378
+ values: rows.map((row) => column.values[row])
379
+ };
380
+ else {
381
+ const { stride } = column;
382
+ const bytes = new Uint8Array(rows.length * stride);
383
+ rows.forEach((row, at) => bytes.set(column.bytes.subarray(row * stride, (row + 1) * stride), at * stride));
384
+ columns[key] = {
385
+ kind: "bytes",
386
+ bytes,
387
+ stride
388
+ };
389
+ }
390
+ }
391
+ return {
392
+ count: rows.length,
393
+ entities: Float64Array.from(rows, (row) => table.entities[row]),
394
+ columns
395
+ };
396
+ };
397
+ /** The entities `root` owns, itself included, through the owner references among `owners` in the tables. */
398
+ const subtreeOf = (tables, owners, root) => {
399
+ const children = /* @__PURE__ */ new Map();
400
+ for (const table of tables) for (const reference of owners) {
401
+ const key = reference;
402
+ const column = Reflect.get(table.columns, key);
403
+ if (column?.kind !== "bytes") continue;
404
+ const view = new DataView(column.bytes.buffer, column.bytes.byteOffset, column.bytes.byteLength);
405
+ for (let row = 0; row < table.count; row++) {
406
+ const owner = view.getFloat64(row * column.stride, true);
407
+ const list = children.get(owner);
408
+ if (list === void 0) children.set(owner, [table.entities[row]]);
409
+ else list.push(table.entities[row]);
410
+ }
411
+ }
412
+ const taken = /* @__PURE__ */ new Set([root]);
413
+ const waiting = [root];
414
+ for (let next = waiting.pop(); next !== void 0; next = waiting.pop()) for (const child of children.get(next) ?? []) {
415
+ if (taken.has(child)) continue;
416
+ taken.add(child);
417
+ waiting.push(child);
418
+ }
419
+ return taken;
420
+ };
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) => {
423
+ const components = [...program.components.values()];
424
+ const keys = components.map((persisted) => persisted.key);
425
+ const subtree = "root" in selection;
426
+ const resources = subtree || selection.resources === false ? [] : [...program.resources.values()];
427
+ const declaration = subtree ? { components: keys } : {
428
+ components: keys,
429
+ with: selection.with ?? [],
430
+ not: selection.not ?? [],
431
+ where: selection.where ?? {},
432
+ resources: resources.map((persisted) => persisted.key)
433
+ };
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);
442
+ if ("failure" in read) return read;
443
+ let tables = read.archetypes;
444
+ if (subtree) {
445
+ const owners = keys.filter((key) => Component.declarations().get(key)?.reference === "owner");
446
+ const taken = subtreeOf(tables, owners, selection.root);
447
+ tables = tables.flatMap((table) => rowsOf(table, (entity) => taken.has(entity)) ?? []);
448
+ }
449
+ return {
450
+ by: program.by,
451
+ components,
452
+ tables,
453
+ resources: resources.map((persisted) => ({
454
+ persisted,
455
+ value: read.resources[persisted.key]
456
+ }))
457
+ };
458
+ };
459
+ //#endregion
460
+ //#region src/snapshot.ts
461
+ /**
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.
471
+ */
472
+ const snapshot = async (input) => {
473
+ const program = programOf(input.by);
474
+ if (typeof program === "string") return { failure: program };
475
+ const taken = await select(input.world, program, input);
476
+ return "failure" in taken ? taken : { [contents]: taken };
477
+ };
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
696
+ //#region src/spawn.ts
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
+ /** Spawns its input's tables and sets its resources, a snapshot's, rewriting their entities, those `existing` maps standing for theirs. */
706
+ const spawnTables = System.create({}, ({ commands }, input) => {
707
+ if (!isTables(input.tables) || !isKeyed(input.resources) || !isEntities(input.existing)) throw new Error("A snapshot holds tables and resources.");
708
+ return commands.spawnTables(input.tables, {
709
+ resources: input.resources,
710
+ existing: input.existing
711
+ });
712
+ }, {
713
+ "type": "object",
714
+ "properties": {
715
+ "tables": { "allOf": [] },
716
+ "resources": { "allOf": [] },
717
+ "existing": { "allOf": [] }
718
+ }
719
+ });
720
+ async function spawn(input) {
721
+ const from = input.snapshot[contents];
722
+ const { tables, resources } = from;
723
+ const values = {};
724
+ for (const { persisted, value } of resources) values[persisted.key] = value;
725
+ const existing = input.match === void 0 || input.match.length === 0 ? /* @__PURE__ */ new Map() : await existingFor(input.world, from, input.match);
726
+ if ("failure" in existing) return existing;
727
+ const spawned = await input.world.run(spawnTables, {
728
+ tables,
729
+ resources: values,
730
+ existing
731
+ }).catch((error) => ({ failure: error instanceof Error ? error.message : String(error) }));
732
+ if ("failure" in spawned) return spawned;
733
+ if (input.entities === false) return { dropped: spawned.dropped };
734
+ return {
735
+ dropped: spawned.dropped,
736
+ entities: entitiesOf(tables, spawned.entities)
737
+ };
738
+ }
739
+ /** Each of the tables' entities to the one spawned for it, the one existing for it, or null for one that wasn't. */
740
+ const entitiesOf = (tables, spawned) => {
741
+ const entities = /* @__PURE__ */ new Map();
742
+ tables.forEach((table, t) => {
743
+ const made = spawned[t];
744
+ for (let row = 0; row < table.count; row++) {
745
+ const entity = made[row];
746
+ entities.set(table.entities[row], entity === 0 ? null : entity);
747
+ }
748
+ });
749
+ return entities;
750
+ };
751
+ //#endregion
752
+ export { decode, encode, snapshot, spawn };
753
+
754
+ //# sourceMappingURL=index.js.map