@adobe/ecs 0.2.3 → 0.3.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.
- package/dist/implementation/compiler/augmentations.js +8 -1
- package/dist/implementation/compiler/augmentations.js.map +1 -1
- package/dist/implementation/compiler/body-access.d.ts +18 -1
- package/dist/implementation/compiler/body-access.js +104 -15
- package/dist/implementation/compiler/body-access.js.map +1 -1
- package/dist/implementation/compiler/estree.d.ts +5 -2
- package/dist/implementation/compiler/estree.js +8 -5
- package/dist/implementation/compiler/estree.js.map +1 -1
- package/dist/implementation/compiler/inferred.js +5 -2
- package/dist/implementation/compiler/inferred.js.map +1 -1
- package/dist/implementation/compiler/transform.js +5 -4
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/compiler/type-schemas.d.ts +1 -0
- package/dist/implementation/compiler/type-schemas.js +35 -2
- package/dist/implementation/compiler/type-schemas.js.map +1 -1
- package/dist/implementation/world/activate.d.ts +14 -3
- package/dist/implementation/world/activate.js +28 -10
- package/dist/implementation/world/activate.js.map +1 -1
- package/dist/implementation/world/buffer.d.ts +7 -0
- package/dist/implementation/world/buffer.js +7 -0
- package/dist/implementation/world/buffer.js.map +1 -1
- package/dist/implementation/world/cascade.d.ts +10 -3
- package/dist/implementation/world/cascade.js +48 -23
- package/dist/implementation/world/cascade.js.map +1 -1
- package/dist/implementation/world/commands.js +41 -5
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +57 -6
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +228 -93
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +10 -0
- package/dist/implementation/world/finalizers.js +60 -10
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/join.js +12 -2
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/program.js +5 -1
- package/dist/implementation/world/program.js.map +1 -1
- package/dist/implementation/world/resource-changes.d.ts +18 -0
- package/dist/implementation/world/resource-changes.js +66 -0
- package/dist/implementation/world/resource-changes.js.map +1 -0
- package/dist/implementation/world/rows.js +21 -16
- package/dist/implementation/world/rows.js.map +1 -1
- package/dist/implementation/world/run-changes.d.ts +38 -0
- package/dist/implementation/world/run-changes.js +138 -0
- package/dist/implementation/world/run-changes.js.map +1 -0
- package/dist/implementation/world/runner.js +16 -12
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/state.d.ts +15 -0
- package/dist/implementation/world/state.js +22 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +81 -24
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables-read.d.ts +5 -3
- package/dist/implementation/world/tables-read.js +16 -4
- package/dist/implementation/world/tables-read.js.map +1 -1
- package/dist/implementation/world/tables.d.ts +4 -2
- package/dist/implementation/world/tables.js +137 -38
- package/dist/implementation/world/tables.js.map +1 -1
- package/dist/implementation/world/thread-observer.js +1 -1
- package/dist/implementation/world/thread-observer.js.map +1 -1
- package/dist/implementation/world/thread-systems.js +10 -6
- package/dist/implementation/world/thread-systems.js.map +1 -1
- package/dist/implementation/world/up.js +7 -1
- package/dist/implementation/world/up.js.map +1 -1
- package/dist/implementation/world/where.d.ts +8 -0
- package/dist/implementation/world/where.js +40 -11
- package/dist/implementation/world/where.js.map +1 -1
- package/dist/interface/finalizer/body.d.ts +40 -4
- package/dist/interface/finalizer/create.d.ts +8 -6
- package/dist/interface/finalizer/create.js +45 -10
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +26 -4
- package/dist/interface/indexes/declaration-of.d.ts +17 -0
- package/dist/interface/indexes/declaration-of.js +20 -0
- package/dist/interface/indexes/declaration-of.js.map +1 -0
- package/dist/interface/indexes/index.d.ts +1 -0
- package/dist/interface/indexes/index.js +1 -0
- package/dist/interface/indexes/index.js.map +1 -1
- package/dist/interface/registry/index.d.ts +1 -1
- package/dist/interface/registry/index.js +1 -1
- package/dist/interface/registry/index.js.map +1 -1
- package/dist/interface/registry/names.d.ts +4 -0
- package/dist/interface/registry/names.js +20 -0
- package/dist/interface/registry/names.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +29 -1
- package/dist/interface/registry/schedule.js +261 -25
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/system/access.d.ts +15 -0
- package/dist/interface/system/check.d.ts +7 -0
- package/dist/interface/system/check.js +21 -3
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +31 -5
- package/dist/interface/system/create-resolved.js +2 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.d.ts +10 -6
- package/dist/interface/system/create.js +5 -4
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +11 -4
- package/dist/interface/system/describe.d.ts +120 -0
- package/dist/interface/system/describe.js +116 -0
- package/dist/interface/system/describe.js.map +1 -0
- package/dist/interface/system/index.d.ts +1 -0
- package/dist/interface/system/index.js +1 -0
- package/dist/interface/system/index.js.map +1 -1
- package/dist/interface/system/leaf.d.ts +7 -0
- package/dist/interface/system/render.js +2 -1
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +14 -1
- package/dist/interface/system/resolve.js +50 -1
- package/dist/interface/system/resolve.js.map +1 -1
- package/dist/interface/system/shared-only.d.ts +7 -0
- package/dist/interface/system/shared-only.js +2 -0
- package/dist/interface/system/shared-only.js.map +1 -0
- package/dist/interface/system/tables.d.ts +9 -1
- package/dist/interface/world/table/index.d.ts +3 -2
- package/dist/interface/world/world.d.ts +15 -0
- package/package.json +11 -2
- package/src/guide/01-resources.md +4 -3
- package/src/guide/03-systems.out +1 -1
- package/src/guide/05-scheduling.md +12 -6
- package/src/guide/05-scheduling.out +1 -0
- package/src/guide/05-scheduling.ts +17 -0
- package/src/guide/06-sync-points.md +20 -8
- package/src/guide/06-sync-points.out +3 -2
- package/src/guide/06-sync-points.ts +24 -5
- package/src/guide/10-finalizers.md +5 -0
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +6 -153
- package/src/implementation/compiler/augmentations.test.ts +28 -1
- package/src/implementation/compiler/augmentations.ts +5 -1
- package/src/implementation/compiler/body-access.ts +102 -12
- package/src/implementation/compiler/estree.ts +9 -6
- package/src/implementation/compiler/inferred.test.ts +1 -1
- package/src/implementation/compiler/inferred.ts +5 -2
- package/src/implementation/compiler/transform.test.ts +41 -21
- package/src/implementation/compiler/transform.ts +5 -3
- package/src/implementation/compiler/type-schemas.ts +28 -2
- package/src/implementation/world/activate.ts +32 -10
- package/src/implementation/world/batches.test.ts +3 -3
- package/src/implementation/world/buffer.ts +7 -0
- package/src/implementation/world/cascade.ts +46 -22
- package/src/implementation/world/commands.ts +37 -5
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +61 -6
- package/src/implementation/world/defaults.test.ts +24 -1
- package/src/implementation/world/despawn-where.test.ts +310 -0
- package/src/implementation/world/execute.ts +222 -86
- package/src/implementation/world/finalizers.ts +63 -11
- package/src/implementation/world/ids.test.ts +1 -1
- package/src/implementation/world/indexes.test.ts +2 -1
- package/src/implementation/world/join.test.ts +19 -0
- package/src/implementation/world/join.ts +16 -2
- package/src/implementation/world/lifetime.test-helper.ts +1 -1
- package/src/implementation/world/names.test.ts +1 -2
- package/src/implementation/world/passes.test.ts +61 -0
- package/src/implementation/world/perf.md +12 -1
- package/src/implementation/world/program.ts +4 -1
- package/src/implementation/world/resource-changes.test.ts +61 -0
- package/src/implementation/world/resource-changes.ts +67 -0
- package/src/implementation/world/rows.ts +21 -14
- package/src/implementation/world/rules.test.ts +37 -0
- package/src/implementation/world/run-changes.test.ts +88 -0
- package/src/implementation/world/run-changes.ts +160 -0
- package/src/implementation/world/run-results.test.ts +17 -0
- package/src/implementation/world/runner.ts +17 -13
- package/src/implementation/world/schedule.test.ts +42 -1
- package/src/implementation/world/shared.test.ts +2 -2
- package/src/implementation/world/state.ts +27 -0
- package/src/implementation/world/sync-points.test.ts +147 -0
- package/src/implementation/world/synchronize.ts +73 -20
- package/src/implementation/world/tables-read.ts +16 -5
- package/src/implementation/world/tables.test.ts +139 -4
- package/src/implementation/world/tables.ts +118 -35
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.ts +10 -6
- package/src/implementation/world/up.test.ts +13 -1
- package/src/implementation/world/up.ts +6 -1
- package/src/implementation/world/watch-traits-late.test-helper.ts +14 -0
- package/src/implementation/world/watch-traits.test-helper.ts +10 -0
- package/src/implementation/world/watch-traits.test.ts +62 -0
- package/src/implementation/world/where.ts +38 -10
- package/src/interface/finalizer/body.ts +33 -4
- package/src/interface/finalizer/create.test.ts +6 -1
- package/src/interface/finalizer/create.ts +45 -12
- package/src/interface/finalizer/declaration.ts +31 -4
- package/src/interface/indexes/create.test.ts +10 -0
- package/src/interface/indexes/declaration-of.ts +34 -0
- package/src/interface/indexes/index.ts +1 -0
- package/src/interface/registry/index.ts +1 -1
- package/src/interface/registry/names.ts +19 -0
- package/src/interface/registry/schedule.ts +275 -29
- package/src/interface/rule/rule.type-test.ts +23 -1
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/check.ts +17 -2
- package/src/interface/system/commands.ts +28 -4
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +2 -1
- package/src/interface/system/create.test.ts +10 -2
- package/src/interface/system/create.ts +9 -5
- package/src/interface/system/declaration.ts +10 -4
- package/src/interface/system/describe.test.ts +98 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +7 -0
- package/src/interface/system/render.ts +2 -1
- package/src/interface/system/resolve.test.ts +17 -1
- package/src/interface/system/resolve.ts +60 -2
- package/src/interface/system/shared-only.ts +5 -0
- package/src/interface/system/system.type-test.ts +6 -0
- package/src/interface/system/tables.ts +7 -1
- package/src/interface/world/run.type-test.ts +11 -1
- package/src/interface/world/table/index.ts +3 -2
- package/src/interface/world/world.ts +13 -0
- package/src/perftest/benchmarks.ts +20 -0
- package/src/perftest/chained.ts +108 -0
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/tables.ts +107 -0
|
@@ -280,6 +280,7 @@ export const createThreadSystems = (host: ThreadSystemsHost) => {
|
|
|
280
280
|
},
|
|
281
281
|
despawn: entity => beginCommand(writer, despawnCommand, entity),
|
|
282
282
|
spawnTables: () => { throw new Error("commands.spawnTables runs on the world's thread: run the system there."); },
|
|
283
|
+
despawnWhere: () => { throw new Error("commands.despawnWhere runs on the world's thread: run the system there."); },
|
|
283
284
|
add: (entity: number, components: symbol | readonly symbol[], values?: unknown) => {
|
|
284
285
|
if (typeof components === "symbol") {
|
|
285
286
|
const key = givenKeyOf(components);
|
|
@@ -990,18 +991,19 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
|
|
|
990
991
|
const row = bindRow(archetype);
|
|
991
992
|
const subject = row.value;
|
|
992
993
|
const input = runInput;
|
|
994
|
+
const { body } = system;
|
|
993
995
|
const filters = filtersOf(archetype);
|
|
994
996
|
if (since < 0 || filters.length === 0) {
|
|
995
997
|
for (let index = 0; index < count; index++) {
|
|
996
998
|
row.index = index;
|
|
997
|
-
|
|
999
|
+
body(subject, value, input);
|
|
998
1000
|
}
|
|
999
1001
|
return;
|
|
1000
1002
|
}
|
|
1001
1003
|
forEachPassingRange(filters, since, 0, count, (start, end) => {
|
|
1002
1004
|
for (let index = start; index < end; index++) {
|
|
1003
1005
|
row.index = index;
|
|
1004
|
-
|
|
1006
|
+
body(subject, value, input);
|
|
1005
1007
|
}
|
|
1006
1008
|
});
|
|
1007
1009
|
};
|
|
@@ -1103,18 +1105,19 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
|
|
|
1103
1105
|
const row = bindRow(archetype);
|
|
1104
1106
|
const subject = row.value;
|
|
1105
1107
|
const input = runInput;
|
|
1108
|
+
const { body } = system;
|
|
1106
1109
|
const filters = filtersOf(archetype);
|
|
1107
1110
|
if (since < 0 || filters.length === 0) {
|
|
1108
1111
|
for (let index = start; index < end && !halted; index++) {
|
|
1109
1112
|
row.index = index;
|
|
1110
|
-
|
|
1113
|
+
body(subject, tailValue, input);
|
|
1111
1114
|
}
|
|
1112
1115
|
return;
|
|
1113
1116
|
}
|
|
1114
1117
|
forEachPassingRange(filters, since, start, end, (first, last) => {
|
|
1115
1118
|
for (let index = first; index < last && !halted; index++) {
|
|
1116
1119
|
row.index = index;
|
|
1117
|
-
|
|
1120
|
+
body(subject, tailValue, input);
|
|
1118
1121
|
}
|
|
1119
1122
|
});
|
|
1120
1123
|
};
|
|
@@ -1233,6 +1236,7 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
|
|
|
1233
1236
|
const copied = (storages: readonly (ThreadStorage | undefined)[]): ThreadArchetype =>
|
|
1234
1237
|
({ id: -1, entities, storageById: storages, record: -1, version: -1, mirrored: null, components: [], resource: false, reclaimed: false, generation: 0 });
|
|
1235
1238
|
const ownedInput = runInput;
|
|
1239
|
+
const { body } = system;
|
|
1236
1240
|
try {
|
|
1237
1241
|
if (optionalNumbers.length === 0 && sharedReads.length === 0) {
|
|
1238
1242
|
const row = bindRow(copied(storageById));
|
|
@@ -1240,7 +1244,7 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
|
|
|
1240
1244
|
for (let index = first; index < rows && !halted; index++) {
|
|
1241
1245
|
if (skipped !== null && skipped.has(archetypeIds[index])) continue;
|
|
1242
1246
|
row.index = index;
|
|
1243
|
-
|
|
1247
|
+
body(subject, tailValue, ownedInput);
|
|
1244
1248
|
}
|
|
1245
1249
|
}
|
|
1246
1250
|
else {
|
|
@@ -1275,7 +1279,7 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
|
|
|
1275
1279
|
const subject = row.value;
|
|
1276
1280
|
for (; index < end && !halted; index++) {
|
|
1277
1281
|
row.index = index;
|
|
1278
|
-
|
|
1282
|
+
body(subject, tailValue, ownedInput);
|
|
1279
1283
|
}
|
|
1280
1284
|
}
|
|
1281
1285
|
}
|
|
@@ -190,7 +190,7 @@ describe("up", () => {
|
|
|
190
190
|
expect(await read(ecs, entities.grandchild, world)).toBe(100);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
-
it("runs every row after
|
|
193
|
+
it("runs every row after spawning more than a patch takes, with change filters, then only the rows that changed", async () => {
|
|
194
194
|
await using ecs = createWorld({ [workers]: 0 });
|
|
195
195
|
await ecs.run(spawn);
|
|
196
196
|
await ecs.run(nest);
|
|
@@ -211,6 +211,18 @@ describe("up", () => {
|
|
|
211
211
|
expect(ran).toEqual([]);
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
it("runs only the rows spawned in bulk, which the journal holds once each, with change filters", async () => {
|
|
215
|
+
await using ecs = createWorld({ [workers]: 0 });
|
|
216
|
+
await ecs.run(spawn);
|
|
217
|
+
await ecs.run(nest);
|
|
218
|
+
await ecs.run(propagateChanged);
|
|
219
|
+
await ecs.run(bulkUnder, { count: 3000 });
|
|
220
|
+
ran.length = 0;
|
|
221
|
+
await ecs.run(propagateChanged);
|
|
222
|
+
expect(ran.length).toBe(3000);
|
|
223
|
+
expect(await readEach(ecs, bulk.slice(0, 3), world)).toEqual([112, 112, 112]);
|
|
224
|
+
});
|
|
225
|
+
|
|
214
226
|
it("finds a cycle a reparent closes after the tree was built, and runs its rows again once it opens", async () => {
|
|
215
227
|
await using ecs = createWorld({ [workers]: 0 });
|
|
216
228
|
await ecs.run(spawn);
|
|
@@ -22,6 +22,9 @@ const cyclic = -1;
|
|
|
22
22
|
const denseShare = 1 / 8;
|
|
23
23
|
/** Rows below this many are sorted rather than scanned. */
|
|
24
24
|
const denseMinimum = 4096;
|
|
25
|
+
/** Past this many journal entries, and past this share of the matched rows, the tree is built again rather than patched. */
|
|
26
|
+
const patchMinimum = 4096;
|
|
27
|
+
const patchShare = 1 / 8;
|
|
25
28
|
/** Sort keys pack a row with its parent's archetype: `(parent + 1) * rowLimit + row`. */
|
|
26
29
|
const rowLimit = 2 ** 27;
|
|
27
30
|
|
|
@@ -375,7 +378,9 @@ const createShared = (state: State, through: number, read: readonly number[], ma
|
|
|
375
378
|
const { journal } = state;
|
|
376
379
|
const end = journalEnd(journal);
|
|
377
380
|
if (position === end) return;
|
|
378
|
-
|
|
381
|
+
let matched = 0;
|
|
382
|
+
for (let k = 0; k < matches.length; k++) matched += matches[k].count;
|
|
383
|
+
if (position < journal.first || end - position > Math.max(patchMinimum, matched * patchShare)) {
|
|
379
384
|
build(matches);
|
|
380
385
|
position = end;
|
|
381
386
|
rebuiltAll();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { System } from "../../interface/system/system.js";
|
|
3
|
+
import { inDocument } from "./watch-traits.test-helper.js";
|
|
4
|
+
|
|
5
|
+
/** A resource of the document's, registered once this module is imported. */
|
|
6
|
+
export const margin = Symbol("watchTraits/margin");
|
|
7
|
+
|
|
8
|
+
declare module "@adobe/ecs" {
|
|
9
|
+
export interface Resources {
|
|
10
|
+
[margin]: { type: number; [inDocument]: true };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const widen = System.create(({ resources }) => { resources[margin] = 5; });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
|
|
3
|
+
/** A trait of the document's: what changes finalizers watching it see. */
|
|
4
|
+
export const inDocument = Symbol("watchTraits/inDocument");
|
|
5
|
+
|
|
6
|
+
declare module "@adobe/ecs" {
|
|
7
|
+
export interface Traits<N> {
|
|
8
|
+
readonly [inDocument]: N extends unknown ? true : never;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import { Finalizer } from "../../interface/finalizer/finalizer.js";
|
|
4
|
+
import { System } from "../../interface/system/system.js";
|
|
5
|
+
import { createWorld } from "../index.js";
|
|
6
|
+
import { readResource } from "./reads.test-helper.js";
|
|
7
|
+
import { inDocument } from "./watch-traits.test-helper.js";
|
|
8
|
+
|
|
9
|
+
const title = Symbol("watchTraits/title");
|
|
10
|
+
const cursor = Symbol("watchTraits/cursor");
|
|
11
|
+
const zoom = Symbol("watchTraits/zoom");
|
|
12
|
+
const theme = Symbol("watchTraits/theme");
|
|
13
|
+
const saved = Symbol("watchTraits/saved");
|
|
14
|
+
const recorded = Symbol("watchTraits/recorded");
|
|
15
|
+
|
|
16
|
+
declare module "@adobe/ecs" {
|
|
17
|
+
interface Components {
|
|
18
|
+
[title]: { type: string; [inDocument]: true };
|
|
19
|
+
[cursor]: { type: number };
|
|
20
|
+
}
|
|
21
|
+
interface Resources {
|
|
22
|
+
[zoom]: { type: number; default: 1; [inDocument]: true };
|
|
23
|
+
[theme]: { type: string };
|
|
24
|
+
[saved]: { type: readonly string[]; default: [] };
|
|
25
|
+
[recorded]: { type: readonly string[]; default: [] };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What a change set holds, as lines: each group's components added and removed, and each resource written. */
|
|
30
|
+
const linesOf = (changes: { readonly groups: readonly { readonly added: readonly symbol[]; readonly removed: readonly symbol[] }[]; readonly resources: readonly { readonly key: symbol; readonly previous: unknown; readonly next: unknown }[] }): string[] => [
|
|
31
|
+
...changes.groups.map(group => [...group.added.map(key => `+${key.description}`), ...group.removed.map(key => `-${key.description}`)].join(" ")),
|
|
32
|
+
...changes.resources.map(change => `${change.key.description} ${String(change.previous)} → ${String(change.next)}`),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
Finalizer.changes({ watch: [inDocument] }, (changes, { resources }) => {
|
|
36
|
+
resources[saved] = [...resources[saved], ...linesOf(changes)];
|
|
37
|
+
});
|
|
38
|
+
Finalizer.changes({ watch: { components: true, resources: [theme] } }, (changes, { resources }) => {
|
|
39
|
+
resources[recorded] = [...resources[recorded], ...linesOf(changes)];
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const spawn = System.create(({ commands }) => { commands.spawn([title, cursor])({ [title]: "notes", [cursor]: 0 }); });
|
|
43
|
+
const edit = System.create(({ resources }) => {
|
|
44
|
+
resources[zoom] = 2;
|
|
45
|
+
resources[theme] = "dark";
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("watches each component and resource declaring a trait, and every component with the resources chosen", async () => {
|
|
49
|
+
await using world = createWorld();
|
|
50
|
+
await world.run(spawn);
|
|
51
|
+
await world.run(edit);
|
|
52
|
+
expect(await readResource(world, saved)).toEqual(["+watchTraits/title", "watchTraits/zoom 1 → 2"]);
|
|
53
|
+
expect(await readResource(world, recorded)).toEqual(["+watchTraits/title +watchTraits/cursor", "watchTraits/theme → dark"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("watches a resource declaring a trait it watches that registers after the world started", async () => {
|
|
57
|
+
await using world = createWorld();
|
|
58
|
+
await world.run(spawn);
|
|
59
|
+
const { widen } = await import("./watch-traits-late.test-helper.js");
|
|
60
|
+
await world.run(widen);
|
|
61
|
+
expect(await readResource(world, saved)).toEqual(["+watchTraits/title", "watchTraits/margin 0 → 5"]);
|
|
62
|
+
});
|
|
@@ -115,22 +115,50 @@ const predicateOf = (given: unknown): ((value: unknown, context: object) => unkn
|
|
|
115
115
|
typeof given === "function" ? (value, context) => Reflect.apply(given, undefined, [value, context]) : null;
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
118
|
+
* `where`'s conditions in a world, each value interned there, by number, and with `pinned`, pinned, so its number stays
|
|
119
|
+
* while no archetype holds it and one that comes to hold it again matches; or the key of a component that isn't shared.
|
|
120
|
+
* A value interned unpinned keeps its number to the end of the sync point.
|
|
120
121
|
*/
|
|
121
|
-
|
|
122
|
-
const conditions: Condition[] =
|
|
122
|
+
const conditionsOf = (state: State, where: { readonly [key: symbol]: unknown }, pinned: boolean): Condition[] | symbol => {
|
|
123
|
+
const conditions: Condition[] = [];
|
|
124
|
+
for (const key of Object.getOwnPropertySymbols(where)) {
|
|
123
125
|
const id = idOf(state, key);
|
|
124
126
|
const given: unknown = Reflect.get(where, key);
|
|
125
127
|
const predicate = predicateOf(given);
|
|
126
|
-
if (predicate !== null)
|
|
128
|
+
if (predicate !== null) {
|
|
129
|
+
conditions.push({ id, number: -1, predicate });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
127
132
|
const values = state.shared[id];
|
|
128
|
-
if (values === undefined)
|
|
133
|
+
if (values === undefined) return key;
|
|
129
134
|
const number = internShared(values, given);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
if (pinned && number >= 0) holdShared(values, number);
|
|
136
|
+
conditions.push({ id, number, predicate: null });
|
|
137
|
+
}
|
|
138
|
+
return conditions;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Whether an archetype holds each of `where`'s values, as a system's `where` of values passes it, for a `where` naming
|
|
143
|
+
* only shared components and giving no predicate; or the key of one that isn't shared. Its values are pinned with
|
|
144
|
+
* `pinned`, and otherwise only interned, for a `where` used before the sync point ends.
|
|
145
|
+
*/
|
|
146
|
+
export const holdsValuesOf = (state: State, where: { readonly [key: symbol]: unknown }, pinned: boolean): ((archetype: Valued) => boolean) | symbol => {
|
|
147
|
+
const conditions = conditionsOf(state, where, pinned);
|
|
148
|
+
if (typeof conditions === "symbol") return conditions;
|
|
149
|
+
return archetype => {
|
|
150
|
+
for (let c = 0; c < conditions.length; c++) if (archetype.shared[conditions[c].id] !== conditions[c].number) return false;
|
|
151
|
+
return true;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A system's `where` in a world: its values interned there, by number, its predicates' context reading `reads`,
|
|
157
|
+
* evaluated again once one of them changed since they last were.
|
|
158
|
+
*/
|
|
159
|
+
export const createWhere = <A extends Valued>(state: State, where: { readonly [key: symbol]: unknown }, reads: readonly symbol[], leaf: Leaf): Where<A> => {
|
|
160
|
+
const conditions = conditionsOf(state, where, true);
|
|
161
|
+
if (typeof conditions === "symbol") throw new Error(`System "${leaf.name}": where names "${labelOf(conditions)}", which isn't a shared component.`);
|
|
134
162
|
const readIds = reads.map(key => idOf(state, key));
|
|
135
163
|
// Its predicates' context: each resource read once per evaluation, since a struct's read copies it out.
|
|
136
164
|
const cached: unknown[] = reads.map(() => undefined);
|
|
@@ -5,8 +5,12 @@ import type { Commands } from '../system/commands.js';
|
|
|
5
5
|
import type { Listed, Property } from '../system/property.js';
|
|
6
6
|
import type { LookupFunction } from '../system/values.js';
|
|
7
7
|
import type { WriteResources } from '../system/body.js';
|
|
8
|
-
import type { ChangesDeclaration, Declaration } from './declaration.js';
|
|
8
|
+
import type { ChangesDeclaration, Declaration, OwnTrait } from './declaration.js';
|
|
9
9
|
import type { Essential, ReadonlyType } from '../component/value.js';
|
|
10
|
+
import type { Resource } from '../resource/resource.js';
|
|
11
|
+
import type { Resources } from '../resource/types.js';
|
|
12
|
+
import type { Components } from '../component/types.js';
|
|
13
|
+
import type { ReadonlyType as ResourceType } from '../resource/value.js';
|
|
10
14
|
import type { Elsewhere, ThreadsOf } from '../system/threads.js';
|
|
11
15
|
|
|
12
16
|
/**
|
|
@@ -28,8 +32,28 @@ export type Context<D extends Declaration | ChangesDeclaration> =
|
|
|
28
32
|
})
|
|
29
33
|
& (true extends Elsewhere<ThreadsOf<D>> ? {} : { readonly commands: Commands });
|
|
30
34
|
|
|
35
|
+
/** The components declaring any of the traits `T`. */
|
|
36
|
+
type ComponentsDeclaring<T> = { [K in Component.Key]: [Extract<keyof Components[K], T>] extends [never] ? never : K }[Component.Key];
|
|
37
|
+
|
|
38
|
+
/** The resources declaring any of the traits `T`. */
|
|
39
|
+
type ResourcesDeclaring<T> = { [K in Resource.Key]: [Extract<keyof Resources[K], T>] extends [never] ? never : K }[Resource.Key];
|
|
40
|
+
|
|
41
|
+
/** The components keys `L` name: each, and each declaring a trait among them. */
|
|
42
|
+
type ComponentsOf<L> = Extract<L, Essential> | Extract<ComponentsDeclaring<Extract<L, OwnTrait>>, Essential>;
|
|
43
|
+
|
|
44
|
+
/** The resources keys `L` name: each, and each declaring a trait among them. */
|
|
45
|
+
type ResourcesOf<L> = Extract<L, Resource.Key> | ResourcesDeclaring<Extract<L, OwnTrait>>;
|
|
46
|
+
|
|
47
|
+
/** The keys a changes finalizer's `watch` lists, by kind: those of its list, or of its object's lists. */
|
|
48
|
+
export type WatchListed<D extends ChangesDeclaration, Of extends "components" | "resources"> = Property<D, "watch"> extends readonly (infer E)[] ? E
|
|
49
|
+
: Property<D, "watch"> extends { readonly [P in Of]: readonly (infer E)[] } ? E : never;
|
|
50
|
+
|
|
31
51
|
/** The components a changes finalizer sees. */
|
|
32
|
-
export type Watched<D extends ChangesDeclaration> = Property<D, "watch"> extends readonly
|
|
52
|
+
export type Watched<D extends ChangesDeclaration> = Property<D, "watch"> extends readonly unknown[] | { readonly components: readonly unknown[] }
|
|
53
|
+
? ComponentsOf<WatchListed<D, "components">> : Essential;
|
|
54
|
+
|
|
55
|
+
/** The resources a changes finalizer sees: those it watches. */
|
|
56
|
+
export type WatchedResources<D extends ChangesDeclaration> = ResourcesOf<WatchListed<D, "resources">>;
|
|
33
57
|
|
|
34
58
|
/** Entities whose components changed the same way: `from` and `to` are null for those spawned and despawned. */
|
|
35
59
|
export type Group<C extends Component.Key> = {
|
|
@@ -42,9 +66,14 @@ export type Group<C extends Component.Key> = {
|
|
|
42
66
|
readonly entities: readonly Entity[];
|
|
43
67
|
};
|
|
44
68
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
69
|
+
/**
|
|
70
|
+
* A sync point's structural changes to the watched components, and the values adds replace, before they apply; and each
|
|
71
|
+
* watched resource written since the last sync point's finalizers ran, to a value that isn't equal to its last one,
|
|
72
|
+
* leaf by leaf for data and by identity for objects, with its value before and after.
|
|
73
|
+
*/
|
|
74
|
+
export type ChangeSet<C extends Component.Key, R extends Resource.Key = never> = {
|
|
47
75
|
readonly groups: readonly Group<C>[];
|
|
76
|
+
readonly resources: readonly { readonly [K in R]: { readonly key: K; readonly previous: ResourceType<K>; readonly next: ResourceType<K> } }[R][];
|
|
48
77
|
/** The value before the sync point, or null if the entity didn't have it. */
|
|
49
78
|
previous<K extends C>(entity: Entity, component: K): ReadonlyType<K> | null;
|
|
50
79
|
/** The value after it, or null if the entity won't have it. */
|
|
@@ -75,7 +75,10 @@ describe("Finalizer.create and Finalizer.changes", () => {
|
|
|
75
75
|
expect(() => untyped(Finalizer.create, { components: {} }, { resources: { reduce: [total] } })).toThrow("only systems contribute to a reduction");
|
|
76
76
|
expect(() => untyped(Finalizer.create, { components: { with: [node, tags] } }, { read: [node, tags] })).toThrow('uses "typesFinalizerTest/node" on "main" and "typesFinalizerTest/tags" on "world"');
|
|
77
77
|
expect(() => untyped(Finalizer.changes, { name: "derived", watch: [slot] })).toThrow("which is derived");
|
|
78
|
-
expect(() => untyped(Finalizer.changes, { name: "
|
|
78
|
+
expect(() => untyped(Finalizer.changes, { name: "unregistered", watch: [Symbol("typesFinalizerTest/unregistered")] })).toThrow("which isn't a registered component, resource or trait");
|
|
79
|
+
expect(() => untyped(Finalizer.changes, { watch: { components: [time], resources: [] } })).toThrow(`watches "typesFinalizerTest/time" among its components, which isn't a registered component or trait`);
|
|
80
|
+
expect(() => untyped(Finalizer.changes, { watch: { components: true, resources: [label] } })).toThrow(`watches "typesFinalizerTest/label" among its resources, which isn't a registered resource or trait`);
|
|
81
|
+
expect(() => untyped(Finalizer.changes, { watch: { components: [slot], resources: [] } })).toThrow("which is derived");
|
|
79
82
|
expect(() => untyped(Finalizer.create, { components: { with: [time] } })).toThrow(`uses component "typesFinalizerTest/time", which isn't a registered component`);
|
|
80
83
|
expect(() => untyped(Finalizer.create, { components: { with: [label] }, lookup: { others: { not: [Symbol("typesFinalizerTest/unregistered")] } } }, { read: [label] })).toThrow("which isn't a registered component");
|
|
81
84
|
expect(programFinalizers()).toHaveLength(7);
|
|
@@ -93,6 +96,8 @@ describe("Finalizer.create and Finalizer.changes", () => {
|
|
|
93
96
|
.toThrow('uses "typesFinalizerTest/tags" on "world" and "typesFinalizerTest/page" on "main"');
|
|
94
97
|
expect(() => untyped(Finalizer.changes, {}, { resources: { read: [page] } }))
|
|
95
98
|
.toThrow(`watches or reads values on thread "main"; a changes finalizer runs on the world's thread, in the sync point.`);
|
|
99
|
+
expect(() => untyped(Finalizer.changes, { name: "savePage", watch: [page] }))
|
|
100
|
+
.toThrow(`watches or reads values on thread "main"; a changes finalizer runs on the world's thread, in the sync point.`);
|
|
96
101
|
// Reading another thread's values, without writing or looking up, it's created.
|
|
97
102
|
const detach = Finalizer.create({ components: { with: [node] } }, row => { void row[node]; });
|
|
98
103
|
expect(programFinalizers()).toHaveLength(8);
|
|
@@ -6,11 +6,13 @@ import { addFinalizer, programFinalizers } from '../registry/program.js';
|
|
|
6
6
|
import { requireRegistered, threadOf } from '../registry/threads.js';
|
|
7
7
|
import { checkWorldWritten } from '../registry/written.js';
|
|
8
8
|
import { checkLookup } from '../registry/indexes.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type { Property } from '../system/property.js';
|
|
10
10
|
import type { ThreadOfComponent } from '../component/value.js';
|
|
11
|
+
import type { ThreadOfResource } from '../resource/value.js';
|
|
12
|
+
import type { Resource } from '../resource/resource.js';
|
|
11
13
|
import type { Component } from '../component/component.js';
|
|
12
14
|
import type { Elsewhere, OneThread, ThreadedBy, ThreadsOf } from '../system/threads.js';
|
|
13
|
-
import type { ChangeSet, Context, Row, Watched } from './body.js';
|
|
15
|
+
import type { ChangeSet, Context, Row, Watched, WatchedResources, WatchListed } from './body.js';
|
|
14
16
|
import type { Changes, Leaf } from './created.js';
|
|
15
17
|
import type { ChangesDeclaration, Declaration, Resolved, ResolvedChanges } from './declaration.js';
|
|
16
18
|
import { accessOf, compact, lookupsLists, resourceUses, type Access } from '../system/resolve.js';
|
|
@@ -26,7 +28,7 @@ type Remote<D> = true extends Elsewhere<ThreadsOf<D>>
|
|
|
26
28
|
: {};
|
|
27
29
|
|
|
28
30
|
/** A changes finalizer runs on the world's thread, in the sync point: it watches and reads no other thread's objects. */
|
|
29
|
-
type OnWorld<D> = true extends Elsewhere<ThreadsOf<D> | ThreadOfComponent<
|
|
31
|
+
type OnWorld<D extends ChangesDeclaration> = true extends Elsewhere<ThreadsOf<D> | ThreadOfComponent<Extract<WatchListed<D, "components">, Component.Key>> | ThreadOfResource<Extract<WatchListed<D, "resources">, Resource.Key>>>
|
|
30
32
|
? { readonly "error: a changes finalizer watches and reads only the world's thread's objects": true }
|
|
31
33
|
: {};
|
|
32
34
|
|
|
@@ -125,19 +127,52 @@ export const create = <const D extends Declaration>(
|
|
|
125
127
|
return addFinalizer(finalizer, id);
|
|
126
128
|
};
|
|
127
129
|
|
|
130
|
+
const isList = (watch: NonNullable<ChangesDeclaration["watch"]>): watch is Extract<ChangesDeclaration["watch"], readonly unknown[]> => Array.isArray(watch);
|
|
131
|
+
|
|
132
|
+
/** What a changes finalizer watches, sorted by kind. Throws for a key that's neither of the kinds it's listed as, or derived. */
|
|
133
|
+
const watchOf = (name: string, watch: ChangesDeclaration["watch"]): ResolvedChanges["watch"] => {
|
|
134
|
+
const components: symbol[] = [];
|
|
135
|
+
const componentTraits: symbol[] = [];
|
|
136
|
+
const resources: symbol[] = [];
|
|
137
|
+
const resourceTraits: symbol[] = [];
|
|
138
|
+
const sort = (key: unknown, kinds: readonly ("component" | "resource")[]): void => {
|
|
139
|
+
const kind = kindOf(key);
|
|
140
|
+
if (typeof key !== "symbol" || kind === undefined || (kind !== "trait" && !kinds.includes(kind))) {
|
|
141
|
+
const among = kinds.length === 2 ? "" : ` among its ${kinds[0]}s`;
|
|
142
|
+
throw new Error(`Finalizer "${name}" watches "${labelOf(key)}"${among}, which isn't a registered ${kinds.length === 2 ? "component, resource" : kinds[0]} or trait: ${whyUnregistered(key)}.`);
|
|
143
|
+
}
|
|
144
|
+
if (kind === "trait") {
|
|
145
|
+
if (kinds.includes("component")) componentTraits.push(key);
|
|
146
|
+
if (kinds.includes("resource")) resourceTraits.push(key);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (declarationOf(key)?.derived === true) throw new Error(`Finalizer "${name}" watches "${labelOf(key)}", which is derived; it sees only components user code adds and removes.`);
|
|
150
|
+
(kind === "component" ? components : resources).push(key);
|
|
151
|
+
};
|
|
152
|
+
if (watch === undefined) return { components: true, componentTraits, resources, resourceTraits };
|
|
153
|
+
if (isList(watch)) {
|
|
154
|
+
for (const key of watch) sort(key, ["component", "resource"]);
|
|
155
|
+
return { components, componentTraits, resources, resourceTraits };
|
|
156
|
+
}
|
|
157
|
+
const every = watch.components === true;
|
|
158
|
+
if (!every) for (const key of watch.components) sort(key, ["component"]);
|
|
159
|
+
for (const key of watch.resources) sort(key, ["resource"]);
|
|
160
|
+
return { components: every ? true : components, componentTraits: every ? [] : componentTraits, resources, resourceTraits };
|
|
161
|
+
};
|
|
162
|
+
|
|
128
163
|
/**
|
|
129
164
|
* Declares a finalizer of each sync point's structural changes, which applies in every world, and returns it. It runs
|
|
130
|
-
* on the world's thread. Throws as `create` does, for a watched component that isn't registered or is derived,
|
|
131
|
-
* one watching or reading another thread's values.
|
|
165
|
+
* on the world's thread. Throws as `create` does, for a watched component that isn't registered or is derived, a
|
|
166
|
+
* watched key that's neither a component nor a resource, and for one watching or reading another thread's values.
|
|
132
167
|
*/
|
|
133
168
|
export const changes = <const D extends ChangesDeclaration>(
|
|
134
169
|
declared: D & Inferred & OnWorld<D>,
|
|
135
|
-
body: (changes: ChangeSet<Watched<D>>, context: Context<D>) => void,
|
|
170
|
+
body: (changes: ChangeSet<Watched<D>, WatchedResources<D>>, context: Context<D>) => void,
|
|
136
171
|
): Finalizer => {
|
|
137
172
|
const id = takeIdOfDeclaration(declared);
|
|
138
173
|
const name = nameOf(declared, id, "finalizer");
|
|
139
174
|
const what = `Finalizer "${name}"`;
|
|
140
|
-
const declaration: ResolvedChanges = { ...resolveShared(what, declared, accessOf(what, declared)),
|
|
175
|
+
const declaration: ResolvedChanges = { ...resolveShared(what, declared, accessOf(what, declared)), watch: watchOf(name, declared.watch) };
|
|
141
176
|
const finalizer: Changes = { kind: "changes", name, declaration, body };
|
|
142
177
|
checkResources(name, declaration.resources);
|
|
143
178
|
requireRegistered("Finalizer", name, "resource", declaration.resources?.read);
|
|
@@ -147,11 +182,9 @@ export const changes = <const D extends ChangesDeclaration>(
|
|
|
147
182
|
checkLookup("Finalizer", name, lookupName, lookup);
|
|
148
183
|
for (const keys of [lookup.read, "has" in lookup ? lookup.has : undefined, "not" in lookup ? lookup.not : undefined]) requireRegistered("Finalizer", name, "component", keys);
|
|
149
184
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
const thread = threadOf({ components: { read: declaration.watch }, resources: declaration.resources, lookup: declaration.lookup }, "Finalizer", name);
|
|
185
|
+
const { watch } = declaration;
|
|
186
|
+
const read = [...declaration.resources?.read ?? [], ...watch.resources];
|
|
187
|
+
const thread = threadOf({ components: { read: watch.components === true ? [] : watch.components }, resources: { ...declaration.resources, read }, lookup: declaration.lookup }, "Finalizer", name);
|
|
155
188
|
if (thread !== null && thread !== "world") throw new Error(`Finalizer "${name}" watches or reads values on thread "${thread}"; a changes finalizer runs on the world's thread, in the sync point.`);
|
|
156
189
|
checkOrder(name, "world", declaration);
|
|
157
190
|
return addFinalizer(finalizer, id);
|
|
@@ -4,6 +4,9 @@ import type { SystemLookupLists, ResourceLists } from '../system/access.js';
|
|
|
4
4
|
import type { Lookup } from '../system/access.js';
|
|
5
5
|
import type { Finalizer } from './finalizer.js';
|
|
6
6
|
import type { Essential } from '../component/value.js';
|
|
7
|
+
import type { Resource } from '../resource/resource.js';
|
|
8
|
+
import type { Traits } from '../trait/types.js';
|
|
9
|
+
import type { BuiltIn } from '../trait/key.js';
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* What every finalizer declares. What its body reads of its row and lookups, and reads and writes of resources, the
|
|
@@ -24,11 +27,23 @@ export type Declaration = Shared & {
|
|
|
24
27
|
readonly components: { readonly with?: readonly Component.Key[]; readonly optional?: readonly Component.Key[]; readonly not?: readonly Component.Key[] };
|
|
25
28
|
};
|
|
26
29
|
|
|
30
|
+
/** A trait of your own, which watches every component and resource declaring it. */
|
|
31
|
+
export type OwnTrait = Exclude<keyof Traits<never>, BuiltIn>;
|
|
32
|
+
|
|
27
33
|
/**
|
|
28
|
-
* A finalizer that sees each sync point's structural changes to the components it watches
|
|
29
|
-
*
|
|
34
|
+
* A finalizer that sees each sync point's structural changes to the components it watches, and the resources it
|
|
35
|
+
* watches written since the last sync point's finalizers ran. By default it watches every component that isn't derived
|
|
36
|
+
* or an object a joined thread fills, and no resource. `watch` lists components, resources and traits of your own, or,
|
|
37
|
+
* as an object, every component (`components: true`) or those listed, beside the resources listed. A trait watches each
|
|
38
|
+
* component and resource declaring it, as each registers, but those the default leaves out and resources on another
|
|
39
|
+
* thread.
|
|
30
40
|
*/
|
|
31
|
-
export type ChangesDeclaration = Shared & {
|
|
41
|
+
export type ChangesDeclaration = Shared & {
|
|
42
|
+
readonly watch?: readonly (Essential | Resource.Key | OwnTrait)[] | {
|
|
43
|
+
readonly components: true | readonly (Essential | OwnTrait)[];
|
|
44
|
+
readonly resources: readonly (Resource.Key | OwnTrait)[];
|
|
45
|
+
};
|
|
46
|
+
};
|
|
32
47
|
|
|
33
48
|
type Key = symbol;
|
|
34
49
|
|
|
@@ -45,4 +60,16 @@ export type Resolved = ResolvedShared & {
|
|
|
45
60
|
readonly components: { readonly read?: readonly Key[]; readonly has?: readonly Key[]; readonly optional?: readonly Key[]; readonly not?: readonly Key[] };
|
|
46
61
|
};
|
|
47
62
|
|
|
48
|
-
|
|
63
|
+
/**
|
|
64
|
+
* A changes finalizer's declaration as the world takes it. It watches the components listed, or every one (`true`),
|
|
65
|
+
* and those declaring a trait of `componentTraits`; the resources listed, and those declaring a trait of
|
|
66
|
+
* `resourceTraits`.
|
|
67
|
+
*/
|
|
68
|
+
export type ResolvedChanges = ResolvedShared & {
|
|
69
|
+
readonly watch: {
|
|
70
|
+
readonly components: true | readonly Key[];
|
|
71
|
+
readonly componentTraits: readonly Key[];
|
|
72
|
+
readonly resources: readonly Key[];
|
|
73
|
+
readonly resourceTraits: readonly Key[];
|
|
74
|
+
};
|
|
75
|
+
};
|
|
@@ -144,3 +144,13 @@ describe("systems ordering their rows by an index", () => {
|
|
|
144
144
|
expect(() => untypedRender(Index.create({ key: [amount] }))).toThrow(/orders its rows by index "index #\d+", which isn't sorted/);
|
|
145
145
|
});
|
|
146
146
|
});
|
|
147
|
+
|
|
148
|
+
describe("Index.declarationOf", () => {
|
|
149
|
+
it("gives an index's declaration as registered, its with and not as declared, and nothing for a value Index.create didn't return", () => {
|
|
150
|
+
const byName = Index.create({ key: [name], unique: true, components: { with: [shown], not: [parent] } });
|
|
151
|
+
expect(Index.declarationOf(byName)).toEqual({ key: [name], components: { with: [shown], not: [parent] }, unique: true });
|
|
152
|
+
const byCell = Index.create({ key: [cell, amount], sorted: true });
|
|
153
|
+
expect(Index.declarationOf(byCell)).toEqual({ key: [cell, amount], components: {}, sorted: true });
|
|
154
|
+
expect(Index.declarationOf({ name: "not an index" })).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Component } from '../component/component.js';
|
|
3
|
+
import { indexDefinitionOf } from '../registry/indexes.js';
|
|
4
|
+
import { kindOf } from '../registry/names.js';
|
|
5
|
+
import type { Index } from './index-type.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An index's declaration as `Index.create` registered it: its key, the components it holds by besides its key and those
|
|
9
|
+
* it excludes, and whether it's sorted or unique.
|
|
10
|
+
*/
|
|
11
|
+
export type Declared = {
|
|
12
|
+
readonly key: readonly Component.Key[];
|
|
13
|
+
readonly components: { readonly with?: readonly Component.Key[]; readonly not?: readonly Component.Key[] };
|
|
14
|
+
readonly sorted?: boolean;
|
|
15
|
+
readonly unique?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Registration takes only components for a key part, `with` and `not`. */
|
|
19
|
+
const isComponent = (key: symbol): key is Component.Key => kindOf(key) === "component";
|
|
20
|
+
|
|
21
|
+
/** The declaration `index` was created from, as registered, or undefined for a value `Index.create` didn't return. */
|
|
22
|
+
export const declarationOf = <D extends object>(index: Index<D>): Declared | undefined => {
|
|
23
|
+
const created = indexDefinitionOf(index);
|
|
24
|
+
if (created === undefined) return undefined;
|
|
25
|
+
const { key, components, sorted, unique } = created.declaration;
|
|
26
|
+
const held = components?.has?.filter(isComponent);
|
|
27
|
+
const excluded = components?.not?.filter(isComponent);
|
|
28
|
+
return {
|
|
29
|
+
key: key.filter(isComponent),
|
|
30
|
+
components: { ...held === undefined ? {} : { with: held }, ...excluded === undefined ? {} : { not: excluded } },
|
|
31
|
+
...sorted === undefined ? {} : { sorted },
|
|
32
|
+
...unique === undefined ? {} : { unique },
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export type { AnyDeclaration } from './declarations.js';
|
|
4
4
|
export { schemaOf, threadOfDeclaration } from './declarations.js';
|
|
5
5
|
export type { Kind } from './names.js';
|
|
6
|
-
export { declarationOf, firstKeyOf, keyOfId, kindOf, labelOf, newestKeyOf, whyUnregistered } from './names.js';
|
|
6
|
+
export { declarationOf, declaresTrait, declaredOf, firstKeyOf, keyOfId, keysVersion, kindOf, labelOf, newestKeyOf, whyUnregistered } from './names.js';
|
|
7
7
|
export type { Item } from './program.js';
|
|
8
8
|
export { definitionOf, programFinalizers, programRules, programVersion, requireDefinition } from './program.js';
|
|
9
9
|
export type { Schedule } from './schedule.js';
|
|
@@ -22,6 +22,9 @@ type Entry = {
|
|
|
22
22
|
/** Every component, resource and user trait, by symbol identity: symbols of one id share an entry. */
|
|
23
23
|
const entries = new Map<symbol, Entry>();
|
|
24
24
|
|
|
25
|
+
/** How many keys were registered, counting a symbol registered under another's id. */
|
|
26
|
+
let registered = 0;
|
|
27
|
+
|
|
25
28
|
/** Entries by the ids the plugin gave their keys. */
|
|
26
29
|
const identified = new Map<string, Entry>();
|
|
27
30
|
|
|
@@ -155,12 +158,28 @@ export const claimKey = (key: unknown, kind: Kind, declaration?: AnyDeclaration)
|
|
|
155
158
|
}
|
|
156
159
|
named.newest = symbol;
|
|
157
160
|
entries.set(symbol, named);
|
|
161
|
+
registered++;
|
|
158
162
|
return;
|
|
159
163
|
}
|
|
160
164
|
const description = descriptionOf(symbol);
|
|
161
165
|
const entry: Entry = { kind, declaration, description, id, first: symbol, newest: symbol };
|
|
162
166
|
if (id !== undefined) identified.set(id, entry);
|
|
163
167
|
entries.set(symbol, entry);
|
|
168
|
+
registered++;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/** How many keys are registered so far, so a world can tell when one registered since it last looked. */
|
|
172
|
+
export const keysVersion = (): number => {
|
|
173
|
+
catchUp();
|
|
174
|
+
return registered;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** Whether a component's or resource's declaration declares one of `traits`, keys of traits of your own. */
|
|
178
|
+
export const declaresTrait = (key: unknown, traits: readonly symbol[]): boolean => {
|
|
179
|
+
const declaration = declarationOf(key);
|
|
180
|
+
if (declaration === undefined) return false;
|
|
181
|
+
for (const declared of Object.getOwnPropertySymbols(declaration)) for (const trait of traits) if (sameKey(declared, trait)) return true;
|
|
182
|
+
return false;
|
|
164
183
|
};
|
|
165
184
|
|
|
166
185
|
/** The symbol last registered under an id, by which a world gives values, or undefined where none was. */
|