@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.
- package/README.md +49 -27
- package/dist/index.js +324 -80
- package/dist/index.js.map +1 -1
- package/dist/types/decode.d.ts +7 -4
- package/dist/types/encode.d.ts +2 -2
- package/dist/types/format/format.d.ts +4 -4
- package/dist/types/guide/01-saving.d.ts +40 -0
- package/dist/types/guide/02-copying.d.ts +23 -0
- package/dist/types/guide/03-chunks.d.ts +23 -0
- package/dist/types/guide/04-settings.d.ts +55 -0
- package/dist/types/index.d.ts +2 -3
- package/dist/types/match/keys.d.ts +36 -0
- package/dist/types/match/match.d.ts +13 -0
- package/dist/types/program/contents.d.ts +5 -3
- package/dist/types/program/program.d.ts +12 -5
- package/dist/types/selection/selection.d.ts +10 -3
- package/dist/types/snapshot.d.ts +15 -5
- package/dist/types/spawn.d.ts +30 -13
- package/package.json +10 -3
- package/src/decode.ts +6 -5
- package/src/encode.ts +2 -2
- package/src/format/format.ts +8 -8
- package/src/guide/01-saving.md +18 -0
- package/src/guide/01-saving.out +4 -0
- package/src/guide/01-saving.ts +55 -0
- package/src/guide/02-copying.md +15 -0
- package/src/guide/02-copying.out +4 -0
- package/src/guide/02-copying.ts +70 -0
- package/src/guide/03-chunks.md +15 -0
- package/src/guide/03-chunks.out +3 -0
- package/src/guide/03-chunks.ts +75 -0
- package/src/guide/04-settings.md +20 -0
- package/src/guide/04-settings.out +4 -0
- package/src/guide/04-settings.ts +90 -0
- package/src/guide/README.md +10 -0
- package/src/index.ts +2 -3
- package/src/match/keys.ts +105 -0
- package/src/match/match.ts +165 -0
- package/src/program/contents.ts +5 -3
- package/src/program/program.ts +26 -15
- package/src/selection/selection.ts +22 -8
- package/src/snapshot.ts +16 -8
- package/src/spawn.ts +46 -17
- 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/
|
|
1
|
+
import { Component, Index, Resource, System } from "@adobe/ecs";
|
|
2
|
+
//#region src/program/program.ts
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
|
23
|
-
if (
|
|
24
|
-
if (
|
|
25
|
-
if (
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
+
const magic = [
|
|
144
136
|
69,
|
|
145
137
|
67,
|
|
146
138
|
83,
|
|
147
139
|
80
|
|
148
140
|
];
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
+
const sameSchema = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
258
250
|
/**
|
|
259
|
-
* The snapshot contents `bytes` holds, its components and resources by
|
|
260
|
-
* bytes viewed where they are; or why it can't be read: it isn't one, or names what
|
|
261
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
351
|
-
* doesn't start at a multiple of 8; or why it can't be read: it isn't one,
|
|
352
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
429
|
-
|
|
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
|
|
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
|
|
461
|
-
* component
|
|
462
|
-
*
|
|
463
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
|
|
477
|
-
if (!
|
|
478
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
758
|
+
export { decode, encode, snapshot, spawn };
|
|
515
759
|
|
|
516
760
|
//# sourceMappingURL=index.js.map
|