@adobe/ecs-persistence 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Adobe, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,72 @@
1
- # Temporary Holding Version
1
+ # @adobe/ecs-persistence
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Saving, loading and copying the entities of `@adobe/ecs` worlds. A snapshot holds whole archetypes' columns: a struct's
4
+ or a number's bytes as the world stores them, which saving and loading copy as blocks, and the values of strings and
5
+ other data. Its entity values are rewritten as they're loaded, so references between the entities it holds still name
6
+ each other.
7
+
8
+ ## Marking what persists
9
+
10
+ A component or resource persists when its declaration names it with the `persist` trait, by a stable name: saved data
11
+ names it so, whatever its module's path or version.
12
+
13
+ ```ts
14
+ import type { Entity, Vec3 } from "@adobe/ecs";
15
+ import { persist } from "@adobe/ecs-persistence";
16
+
17
+ export const position = Symbol();
18
+ export const parent = Symbol();
19
+ export const score = Symbol();
20
+ declare module "@adobe/ecs" {
21
+ interface Components {
22
+ [position]: { type: Vec3; [persist]: "position" };
23
+ [parent]: { type: Entity; reference: "owner"; [persist]: "parent" };
24
+ }
25
+ interface Resources {
26
+ [score]: { type: number; default: 0; [persist]: "score" };
27
+ }
28
+ }
29
+ ```
30
+
31
+ Only data persists: a value with a schema. A derived component doesn't: its rule derives it again once its entity is
32
+ loaded.
33
+
34
+ ## Saving and loading
35
+
36
+ ```ts
37
+ import { decode, encode, snapshot, spawn } from "@adobe/ecs-persistence";
38
+
39
+ const taken = await snapshot({ world });
40
+ if (taken.failure !== undefined) throw new Error(taken.failure);
41
+ const bytes = encode(taken);
42
+
43
+ const loaded = decode(bytes);
44
+ if (loaded.failure !== undefined) throw new Error(loaded.failure);
45
+ const spawned = await spawn({ world: other, snapshot: loaded });
46
+ ```
47
+
48
+ - `snapshot({ world, ... })` takes every entity holding a persisted component, those matching `with` and `not`, or
49
+ `root` and everything it owns through persisted owner references. Except with `root` or `resources: false`, it
50
+ takes the persisted resources too.
51
+ - `encode` writes it as bytes, and `decode` reads them back, viewing each column's bytes where they are in the buffer.
52
+ It refuses bytes naming a component or resource this program doesn't persist, or persists with another schema.
53
+ - `spawn({ world, snapshot, entities? })` spawns an entity for each of the snapshot's and sets its resources, its entity values
54
+ naming the entities spawned for theirs. An entity value naming an entity outside the snapshot follows what
55
+ despawning that entity would do: an entity it owns isn't spawned, nor what that owns, and the result counts them in
56
+ `dropped`; a weak reference to it is left out; any other value naming it is `Entity.none`. The result's `entities`
57
+ maps each of the snapshot's entities to the one spawned for it, or null; `entities: false` leaves the map out, which
58
+ saves about 85 ms and 50 MB at a million entities.
59
+ - Copying entities from one world into another is a snapshot spawned there: `spawn({ world: to, snapshot: await snapshot({ world: from }) })`, without
60
+ bytes.
61
+
62
+ Each reports why it didn't happen as `{ failure }`, rather than throwing, and `result.failure !== undefined` tells
63
+ which. A snapshot is opaque: only these functions
64
+ read it.
65
+
66
+ ## Speed
67
+
68
+ `pnpm test:perf` compares, at 100K and 1M entities with a `Vec3`, a number, an owner reference and a struct holding an
69
+ entity, this package with reading rows and spawning entity by entity. At 1M entities, a snapshot takes about 6 ms
70
+ against 110 ms, encoding 5 ms against 150 ms for `JSON.stringify` of the rows, and decoding views the bytes where they
71
+ are. Spawning takes about 310 ms against 1,450 ms, most of it the world's bookkeeping of owner references at its sync
72
+ point; without references, 1M entities spawn in about 26 ms.
package/dist/index.js ADDED
@@ -0,0 +1,516 @@
1
+ import { Component, Resource, System } from "@adobe/ecs";
2
+ //#region src/persist.ts
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.
7
+ */
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) => {
20
+ const named = /* @__PURE__ */ new Map();
21
+ 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}".`;
26
+ named.set(name, {
27
+ name,
28
+ key,
29
+ schema: declaration.type
30
+ });
31
+ }
32
+ return named;
33
+ };
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");
37
+ if (typeof components === "string") return components;
38
+ const resources = namedIn(Resource.declarations(), "resource");
39
+ if (typeof resources === "string") return resources;
40
+ return {
41
+ components,
42
+ resources
43
+ };
44
+ };
45
+ //#endregion
46
+ //#region src/values/values.ts
47
+ /** 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) => {
50
+ if (typeof json !== "string") return json;
51
+ switch (json) {
52
+ case "NaN": return NaN;
53
+ case "Infinity": return Infinity;
54
+ case "-Infinity": return -Infinity;
55
+ case "-0": return -0;
56
+ default: return json;
57
+ }
58
+ };
59
+ /** Whether a value is of the JSON kind a schema's values are, to pick a union's member. */
60
+ var fits = (schema, value) => {
61
+ if ("const" in schema) return Object.is(schema.const, value) || JSON.stringify(schema.const) === JSON.stringify(value);
62
+ if ("anyOf" in schema) return schema.anyOf.some((member) => fits(member, value));
63
+ if ("allOf" in schema) return schema.allOf.every((member) => fits(member, value));
64
+ if (!("type" in schema)) return true;
65
+ switch (schema.type) {
66
+ case "number":
67
+ case "integer": return typeof value === "number";
68
+ case "string": return typeof value === "string";
69
+ case "boolean": return typeof value === "boolean";
70
+ case "null": return value === null;
71
+ case "array": return Array.isArray(value);
72
+ case "set": return value instanceof Set;
73
+ case "map": return value instanceof Map;
74
+ case "object": return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Set) && !(value instanceof Map);
75
+ default: return true;
76
+ }
77
+ };
78
+ /** A value of `schema` as JSON. */
79
+ var toJson = (schema, value) => {
80
+ if ("anyOf" in schema) {
81
+ const index = schema.anyOf.findIndex((candidate) => fits(candidate, value));
82
+ const member = schema.anyOf[index];
83
+ return member === void 0 ? [-1, value] : [index, toJson(member, value)];
84
+ }
85
+ if (typeof value === "number") return numberToJson(value);
86
+ if (!("type" in schema)) return value;
87
+ switch (schema.type) {
88
+ case "array": return Array.isArray(value) ? value.map((item) => toJson(schema.items, item)) : value;
89
+ case "set": return value instanceof Set ? Array.from(value, (item) => toJson(schema.items, item)) : value;
90
+ case "map": return value instanceof Map ? Array.from(value, ([key, item]) => [toJson(schema.keys, key), toJson(schema.values, item)]) : value;
91
+ case "object": {
92
+ if (typeof value !== "object" || value === null) return value;
93
+ const json = {};
94
+ for (const name of Object.keys(value)) {
95
+ const field = schema.properties[name];
96
+ const item = Reflect.get(value, name);
97
+ if (item !== void 0) json[name] = field === void 0 ? item : toJson(field, item);
98
+ }
99
+ return json;
100
+ }
101
+ default: return value;
102
+ }
103
+ };
104
+ /** A value of `schema` read back from its JSON. */
105
+ var fromJson = (schema, json) => {
106
+ if ("anyOf" in schema) {
107
+ if (!Array.isArray(json) || typeof json[0] !== "number") return json;
108
+ const member = schema.anyOf[json[0]];
109
+ return member === void 0 ? json[1] : fromJson(member, json[1]);
110
+ }
111
+ if ("const" in schema) return schema.const;
112
+ if (!("type" in schema)) return json;
113
+ switch (schema.type) {
114
+ case "number":
115
+ case "integer": return numberFromJson(json);
116
+ case "array": return Array.isArray(json) ? json.map((item) => fromJson(schema.items, item)) : json;
117
+ case "set": return Array.isArray(json) ? new Set(json.map((item) => fromJson(schema.items, item))) : json;
118
+ 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;
119
+ case "object": {
120
+ if (typeof json !== "object" || json === null || Array.isArray(json)) return json;
121
+ const value = {};
122
+ for (const name of Object.keys(json)) {
123
+ const field = schema.properties[name];
124
+ const item = Reflect.get(json, name);
125
+ value[name] = field === void 0 ? item : fromJson(field, item);
126
+ }
127
+ return value;
128
+ }
129
+ default: return json;
130
+ }
131
+ };
132
+ //#endregion
133
+ //#region src/format/format.ts
134
+ /**
135
+ * The binary form of a snapshot, little-endian:
136
+ * - 16 bytes: `ECSP`, the format's version as a u32, the header's length in bytes as a u32, and 4 bytes of zeros;
137
+ * - the header, JSON text padded with spaces to a multiple of 8 bytes: each component and resource by name with its
138
+ * schema, and each table's count and where its entities and columns are;
139
+ * - blocks, each starting at a multiple of 8 bytes: a table's entities as f64s, a column's bytes as its components'
140
+ * layout holds them, and a column of values as JSON text.
141
+ * A loader views the blocks of bytes where they are, in the buffer it's given.
142
+ */
143
+ var magic = [
144
+ 69,
145
+ 67,
146
+ 83,
147
+ 80
148
+ ];
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();
154
+ /** A snapshot's contents as bytes. */
155
+ var encodeContents = (snapshot) => {
156
+ const blocks = [];
157
+ let offset = 0;
158
+ /** Places a block, returning its offset from the first block. */
159
+ const place = (bytes) => {
160
+ const at = offset;
161
+ blocks.push(bytes);
162
+ offset = aligned(offset + bytes.byteLength);
163
+ return at;
164
+ };
165
+ const indexOf = new Map(snapshot.components.map((persisted, index) => [persisted.key, index]));
166
+ const tables = snapshot.tables.map((table) => {
167
+ const entities = place(new Uint8Array(table.entities.buffer, table.entities.byteOffset, table.entities.byteLength));
168
+ const columns = snapshot.components.flatMap((persisted) => {
169
+ const key = persisted.key;
170
+ const column = Reflect.get(table.columns, key);
171
+ const component = indexOf.get(persisted.key) ?? -1;
172
+ if (column === void 0) return [];
173
+ if (column.kind === "bytes") return [{
174
+ component,
175
+ kind: "bytes",
176
+ offset: place(column.bytes),
177
+ length: column.bytes.byteLength,
178
+ stride: column.stride
179
+ }];
180
+ if (column.kind === "value") return [{
181
+ component,
182
+ kind: "value",
183
+ value: toJson(persisted.schema, column.value)
184
+ }];
185
+ const text = encoder.encode(JSON.stringify(column.values.map((value) => toJson(persisted.schema, value))));
186
+ return [{
187
+ component,
188
+ kind: "values",
189
+ offset: place(text),
190
+ length: text.byteLength
191
+ }];
192
+ });
193
+ return {
194
+ count: table.count,
195
+ entities,
196
+ columns
197
+ };
198
+ });
199
+ const header = {
200
+ components: snapshot.components.map(({ name, schema }) => ({
201
+ name,
202
+ schema
203
+ })),
204
+ resources: snapshot.resources.map(({ persisted, value }) => ({
205
+ name: persisted.name,
206
+ schema: persisted.schema,
207
+ value: toJson(persisted.schema, value)
208
+ })),
209
+ tables
210
+ };
211
+ const text = encoder.encode(JSON.stringify(header));
212
+ const headerLength = aligned(text.byteLength);
213
+ const bytes = new Uint8Array(prefix + headerLength + offset);
214
+ bytes.set(magic, 0);
215
+ const view = new DataView(bytes.buffer);
216
+ view.setUint32(4, version, true);
217
+ view.setUint32(8, headerLength, true);
218
+ bytes.set(text, prefix);
219
+ bytes.fill(32, prefix + text.byteLength, prefix + headerLength);
220
+ let at = prefix + headerLength;
221
+ for (const block of blocks) {
222
+ bytes.set(block, at);
223
+ at = aligned(at + block.byteLength);
224
+ }
225
+ return bytes;
226
+ };
227
+ var isObject = (value) => typeof value === "object" && value !== null;
228
+ /** The header's JSON, checked enough to read it without casts, or null. */
229
+ var headerOf = (json) => {
230
+ if (!isObject(json)) return null;
231
+ const components = Reflect.get(json, "components");
232
+ const resources = Reflect.get(json, "resources");
233
+ const tables = Reflect.get(json, "tables");
234
+ if (!Array.isArray(components) || !Array.isArray(resources) || !Array.isArray(tables)) return null;
235
+ const named = (item) => isObject(item) && typeof Reflect.get(item, "name") === "string" && isObject(Reflect.get(item, "schema"));
236
+ if (!components.every(named) || !resources.every(named)) return null;
237
+ const isColumn = (item) => {
238
+ if (!isObject(item) || typeof Reflect.get(item, "component") !== "number") return false;
239
+ const kind = Reflect.get(item, "kind");
240
+ if (kind === "value") return true;
241
+ const numbers = kind === "bytes" ? [
242
+ "offset",
243
+ "length",
244
+ "stride"
245
+ ] : kind === "values" ? ["offset", "length"] : null;
246
+ return numbers !== null && numbers.every((name) => typeof Reflect.get(item, name) === "number");
247
+ };
248
+ const isTable = (item) => {
249
+ if (!isObject(item) || typeof Reflect.get(item, "count") !== "number" || typeof Reflect.get(item, "entities") !== "number") return false;
250
+ const columns = Reflect.get(item, "columns");
251
+ return Array.isArray(columns) && columns.every(isColumn);
252
+ };
253
+ if (!tables.every(isTable)) return null;
254
+ return json;
255
+ };
256
+ /** 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);
258
+ /**
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.
262
+ */
263
+ var decodeContents = (bytes) => {
264
+ if (bytes.byteLength < prefix || magic.some((byte, at) => bytes[at] !== byte)) return { failure: "These bytes aren't a snapshot." };
265
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
266
+ const found = view.getUint32(4, true);
267
+ if (found !== version) return { failure: `This snapshot's format is version ${found}; this package reads version ${version}.` };
268
+ const headerLength = view.getUint32(8, true);
269
+ let json;
270
+ try {
271
+ json = JSON.parse(decoder.decode(bytes.subarray(prefix, prefix + headerLength)));
272
+ } catch {
273
+ return { failure: "This snapshot's header isn't JSON." };
274
+ }
275
+ const header = headerOf(json);
276
+ if (header === null) return { failure: "This snapshot's header isn't one this package writes." };
277
+ const program = programOf();
278
+ if (typeof program === "string") return { failure: program };
279
+ const components = [];
280
+ for (const { name, schema } of header.components) {
281
+ const persisted = program.components.get(name);
282
+ if (persisted === void 0) return { failure: `This snapshot holds component "${name}", which this program doesn't persist.` };
283
+ if (!sameSchema(persisted.schema, schema)) return { failure: `Component "${name}" has another schema here than in this snapshot.` };
284
+ components.push(persisted);
285
+ }
286
+ const resources = [];
287
+ for (const { name, schema, value } of header.resources) {
288
+ const persisted = program.resources.get(name);
289
+ if (persisted === void 0) return { failure: `This snapshot holds resource "${name}", which this program doesn't persist.` };
290
+ if (!sameSchema(persisted.schema, schema)) return { failure: `Resource "${name}" has another schema here than in this snapshot.` };
291
+ resources.push({
292
+ persisted,
293
+ value: fromJson(schema, value)
294
+ });
295
+ }
296
+ const blocks = (bytes.byteOffset + prefix + headerLength) % 8 === 0 ? bytes : bytes.slice();
297
+ const base = blocks.byteOffset + prefix + headerLength;
298
+ const end = blocks.byteOffset + blocks.byteLength;
299
+ const inBounds = (offset, length) => offset >= 0 && length >= 0 && base + offset + length <= end;
300
+ const tables = [];
301
+ for (const table of header.tables) {
302
+ if (!inBounds(table.entities, table.count * 8)) return { failure: "This snapshot's blocks are cut short." };
303
+ const columns = {};
304
+ for (const column of table.columns) {
305
+ const persisted = components[column.component];
306
+ if (persisted === void 0) return { failure: "This snapshot names a component it doesn't hold." };
307
+ if (column.kind === "value") columns[persisted.key] = {
308
+ kind: "value",
309
+ value: fromJson(persisted.schema, column.value)
310
+ };
311
+ else if (!inBounds(column.offset, column.length)) return { failure: "This snapshot's blocks are cut short." };
312
+ else if (column.kind === "bytes") columns[persisted.key] = {
313
+ kind: "bytes",
314
+ bytes: new Uint8Array(blocks.buffer, base + column.offset, column.length),
315
+ stride: column.stride
316
+ };
317
+ else {
318
+ let values;
319
+ try {
320
+ values = JSON.parse(decoder.decode(new Uint8Array(blocks.buffer, base + column.offset, column.length)));
321
+ } catch {
322
+ return { failure: `Component "${persisted.name}"'s values in this snapshot aren't JSON.` };
323
+ }
324
+ if (!Array.isArray(values)) return { failure: `Component "${persisted.name}"'s values in this snapshot aren't a list.` };
325
+ columns[persisted.key] = {
326
+ kind: "values",
327
+ values: values.map((value) => fromJson(persisted.schema, value))
328
+ };
329
+ }
330
+ }
331
+ tables.push({
332
+ count: table.count,
333
+ entities: new Float64Array(blocks.buffer, base + table.entities, table.count),
334
+ columns
335
+ });
336
+ }
337
+ return {
338
+ components,
339
+ tables,
340
+ resources
341
+ };
342
+ };
343
+ //#endregion
344
+ //#region src/program/contents.ts
345
+ /** Where a snapshot holds what it holds, a key only this package uses. */
346
+ var contents = Symbol("snapshot contents");
347
+ //#endregion
348
+ //#region src/decode.ts
349
+ /**
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.
353
+ */
354
+ var decode = (bytes) => {
355
+ const decoded = decodeContents(bytes);
356
+ return "failure" in decoded ? decoded : { [contents]: decoded };
357
+ };
358
+ //#endregion
359
+ //#region src/encode.ts
360
+ /**
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.
363
+ */
364
+ var encode = (snapshot) => encodeContents(snapshot[contents]);
365
+ //#endregion
366
+ //#region src/selection/selection.ts
367
+ /** A readonly system reading the tables `declaration` names. */
368
+ var tablesSystemOf = (declaration) => System.create({
369
+ readonly: true,
370
+ tables: declaration
371
+ }, ({ tables }) => tables);
372
+ /** The rows of `table` whose entities `keep` takes, as a table of their own. */
373
+ var rowsOf = (table, keep) => {
374
+ const rows = [];
375
+ for (let row = 0; row < table.count; row++) if (keep(table.entities[row])) rows.push(row);
376
+ if (rows.length === table.count) return table;
377
+ if (rows.length === 0) return null;
378
+ const columns = {};
379
+ for (const key of Object.getOwnPropertySymbols(table.columns)) {
380
+ const column = Reflect.get(table.columns, key);
381
+ if (column === void 0) continue;
382
+ if (column.kind === "value") columns[key] = column;
383
+ else if (column.kind === "values") columns[key] = {
384
+ kind: "values",
385
+ values: rows.map((row) => column.values[row])
386
+ };
387
+ else {
388
+ const { stride } = column;
389
+ const bytes = new Uint8Array(rows.length * stride);
390
+ rows.forEach((row, at) => bytes.set(column.bytes.subarray(row * stride, (row + 1) * stride), at * stride));
391
+ columns[key] = {
392
+ kind: "bytes",
393
+ bytes,
394
+ stride
395
+ };
396
+ }
397
+ }
398
+ return {
399
+ count: rows.length,
400
+ entities: Float64Array.from(rows, (row) => table.entities[row]),
401
+ columns
402
+ };
403
+ };
404
+ /** The entities `root` owns, itself included, through the owner references among `owners` in the tables. */
405
+ var subtreeOf = (tables, owners, root) => {
406
+ const children = /* @__PURE__ */ new Map();
407
+ for (const table of tables) for (const reference of owners) {
408
+ const key = reference;
409
+ const column = Reflect.get(table.columns, key);
410
+ if (column?.kind !== "bytes") continue;
411
+ const view = new DataView(column.bytes.buffer, column.bytes.byteOffset, column.bytes.byteLength);
412
+ for (let row = 0; row < table.count; row++) {
413
+ const owner = view.getFloat64(row * column.stride, true);
414
+ const list = children.get(owner);
415
+ if (list === void 0) children.set(owner, [table.entities[row]]);
416
+ else list.push(table.entities[row]);
417
+ }
418
+ }
419
+ const taken = /* @__PURE__ */ new Set([root]);
420
+ const waiting = [root];
421
+ for (let next = waiting.pop(); next !== void 0; next = waiting.pop()) for (const child of children.get(next) ?? []) {
422
+ if (taken.has(child)) continue;
423
+ taken.add(child);
424
+ waiting.push(child);
425
+ }
426
+ return taken;
427
+ };
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) => {
430
+ const components = [...program.components.values()];
431
+ const keys = components.map((persisted) => persisted.key);
432
+ const subtree = "root" in selection;
433
+ const resources = subtree || selection.resources === false ? [] : [...program.resources.values()];
434
+ const declaration = subtree ? { components: keys } : {
435
+ components: keys,
436
+ with: selection.with ?? [],
437
+ not: selection.not ?? [],
438
+ resources: resources.map((persisted) => persisted.key)
439
+ };
440
+ const read = await world.run(tablesSystemOf(declaration)).catch((error) => ({ failure: error instanceof Error ? error.message : String(error) }));
441
+ if ("failure" in read) return read;
442
+ let tables = read.archetypes;
443
+ if (subtree) {
444
+ const owners = keys.filter((key) => Component.declarations().get(key)?.reference === "owner");
445
+ const taken = subtreeOf(tables, owners, selection.root);
446
+ tables = tables.flatMap((table) => rowsOf(table, (entity) => taken.has(entity)) ?? []);
447
+ }
448
+ return {
449
+ components,
450
+ tables,
451
+ resources: resources.map((persisted) => ({
452
+ persisted,
453
+ value: read.resources[persisted.key]
454
+ }))
455
+ };
456
+ };
457
+ //#endregion
458
+ //#region src/snapshot.ts
459
+ /**
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.
464
+ */
465
+ var snapshot = async (input) => {
466
+ const program = programOf();
467
+ if (typeof program === "string") return { failure: program };
468
+ const taken = await select(input.world, program, input);
469
+ return "failure" in taken ? taken : { [contents]: taken };
470
+ };
471
+ //#endregion
472
+ //#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);
479
+ }, {
480
+ "type": "object",
481
+ "properties": {
482
+ "tables": { "allOf": [] },
483
+ "resources": { "allOf": [] }
484
+ }
485
+ });
486
+ async function spawn(input) {
487
+ const { tables, resources } = input.snapshot[contents];
488
+ const values = {};
489
+ for (const { persisted, value } of resources) values[persisted.key] = value;
490
+ const spawned = await input.world.run(spawnTables, {
491
+ tables,
492
+ resources: values
493
+ }).catch((error) => ({ failure: error instanceof Error ? error.message : String(error) }));
494
+ if ("failure" in spawned) return spawned;
495
+ if (input.entities === false) return { dropped: spawned.dropped };
496
+ return {
497
+ dropped: spawned.dropped,
498
+ entities: entitiesOf(tables, spawned.entities)
499
+ };
500
+ }
501
+ /** Each of the tables' entities to the one spawned for it, or null for one that wasn't. */
502
+ var entitiesOf = (tables, spawned) => {
503
+ const entities = /* @__PURE__ */ new Map();
504
+ tables.forEach((table, t) => {
505
+ const made = spawned[t];
506
+ for (let row = 0; row < table.count; row++) {
507
+ const entity = made[row];
508
+ entities.set(table.entities[row], entity === 0 ? null : entity);
509
+ }
510
+ });
511
+ return entities;
512
+ };
513
+ //#endregion
514
+ export { decode, encode, persist, snapshot, spawn };
515
+
516
+ //# sourceMappingURL=index.js.map