@adobe/ecs 0.2.3 → 0.2.4
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/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 +4 -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/world/activate.js +14 -2
- 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 +40 -5
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +53 -2
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +132 -83
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +2 -0
- package/dist/implementation/world/finalizers.js +24 -3
- 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/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 +13 -0
- package/dist/implementation/world/state.js +13 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +72 -22
- 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 +3 -2
- package/dist/implementation/world/tables.js +83 -37
- 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 +18 -3
- package/dist/interface/finalizer/create.d.ts +7 -5
- package/dist/interface/finalizer/create.js +8 -5
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +4 -2
- 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/schedule.d.ts +28 -1
- package/dist/interface/registry/schedule.js +182 -21
- 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 +17 -3
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +26 -5
- package/dist/interface/system/create-resolved.js +1 -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/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 +6 -0
- package/dist/interface/system/render.js +1 -0
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +5 -1
- package/dist/interface/system/resolve.js +19 -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/03-systems.out +1 -1
- package/src/guide/06-sync-points.md +12 -8
- package/src/guide/06-sync-points.out +1 -2
- package/src/guide/06-sync-points.ts +9 -5
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +3 -150
- 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 +4 -2
- package/src/implementation/compiler/transform.test.ts +41 -21
- package/src/implementation/compiler/transform.ts +5 -3
- package/src/implementation/world/activate.ts +11 -2
- 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 +36 -5
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +57 -2
- package/src/implementation/world/despawn-where.test.ts +287 -0
- package/src/implementation/world/execute.ts +125 -78
- package/src/implementation/world/finalizers.ts +27 -4
- 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/names.test.ts +1 -2
- package/src/implementation/world/perf.md +3 -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/runner.ts +17 -13
- package/src/implementation/world/shared.test.ts +2 -2
- package/src/implementation/world/state.ts +19 -0
- package/src/implementation/world/sync-points.test.ts +135 -0
- package/src/implementation/world/synchronize.ts +65 -18
- package/src/implementation/world/tables-read.ts +16 -5
- package/src/implementation/world/tables.test.ts +106 -4
- package/src/implementation/world/tables.ts +71 -33
- 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/where.ts +38 -10
- package/src/interface/finalizer/body.ts +13 -3
- package/src/interface/finalizer/create.test.ts +3 -1
- package/src/interface/finalizer/create.ts +12 -7
- package/src/interface/finalizer/declaration.ts +4 -2
- 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/schedule.ts +196 -23
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/check.ts +15 -2
- package/src/interface/system/commands.ts +23 -4
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +1 -1
- package/src/interface/system/create.test.ts +8 -0
- package/src/interface/system/create.ts +9 -5
- package/src/interface/system/describe.test.ts +79 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +6 -0
- package/src/interface/system/render.ts +1 -0
- package/src/interface/system/resolve.ts +25 -2
- package/src/interface/system/shared-only.ts +5 -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 +18 -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();
|
|
@@ -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);
|
|
@@ -7,6 +7,8 @@ import type { LookupFunction } from '../system/values.js';
|
|
|
7
7
|
import type { WriteResources } from '../system/body.js';
|
|
8
8
|
import type { ChangesDeclaration, Declaration } from './declaration.js';
|
|
9
9
|
import type { Essential, ReadonlyType } from '../component/value.js';
|
|
10
|
+
import type { Resource } from '../resource/resource.js';
|
|
11
|
+
import type { ReadonlyType as ResourceType } from '../resource/value.js';
|
|
10
12
|
import type { Elsewhere, ThreadsOf } from '../system/threads.js';
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -29,7 +31,10 @@ export type Context<D extends Declaration | ChangesDeclaration> =
|
|
|
29
31
|
& (true extends Elsewhere<ThreadsOf<D>> ? {} : { readonly commands: Commands });
|
|
30
32
|
|
|
31
33
|
/** The components a changes finalizer sees. */
|
|
32
|
-
export type Watched<D extends ChangesDeclaration> = Property<D, "watch"> extends readonly
|
|
34
|
+
export type Watched<D extends ChangesDeclaration> = Property<D, "watch"> extends readonly unknown[] ? Listed<D, "watch"> & Essential : Essential;
|
|
35
|
+
|
|
36
|
+
/** The resources a changes finalizer sees: those it watches. */
|
|
37
|
+
export type WatchedResources<D extends ChangesDeclaration> = Listed<D, "watch"> & Resource.Key;
|
|
33
38
|
|
|
34
39
|
/** Entities whose components changed the same way: `from` and `to` are null for those spawned and despawned. */
|
|
35
40
|
export type Group<C extends Component.Key> = {
|
|
@@ -42,9 +47,14 @@ export type Group<C extends Component.Key> = {
|
|
|
42
47
|
readonly entities: readonly Entity[];
|
|
43
48
|
};
|
|
44
49
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
50
|
+
/**
|
|
51
|
+
* A sync point's structural changes to the watched components, and the values adds replace, before they apply; and each
|
|
52
|
+
* watched resource written since the last sync point's finalizers ran, to a value that isn't equal to its last one,
|
|
53
|
+
* leaf by leaf for data and by identity for objects, with its value before and after.
|
|
54
|
+
*/
|
|
55
|
+
export type ChangeSet<C extends Component.Key, R extends Resource.Key = never> = {
|
|
47
56
|
readonly groups: readonly Group<C>[];
|
|
57
|
+
readonly resources: readonly { readonly [K in R]: { readonly key: K; readonly previous: ResourceType<K>; readonly next: ResourceType<K> } }[R][];
|
|
48
58
|
/** The value before the sync point, or null if the entity didn't have it. */
|
|
49
59
|
previous<K extends C>(entity: Entity, component: K): ReadonlyType<K> | null;
|
|
50
60
|
/** The value after it, or null if the entity won't have it. */
|
|
@@ -75,7 +75,7 @@ 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 or resource");
|
|
79
79
|
expect(() => untyped(Finalizer.create, { components: { with: [time] } })).toThrow(`uses component "typesFinalizerTest/time", which isn't a registered component`);
|
|
80
80
|
expect(() => untyped(Finalizer.create, { components: { with: [label] }, lookup: { others: { not: [Symbol("typesFinalizerTest/unregistered")] } } }, { read: [label] })).toThrow("which isn't a registered component");
|
|
81
81
|
expect(programFinalizers()).toHaveLength(7);
|
|
@@ -93,6 +93,8 @@ describe("Finalizer.create and Finalizer.changes", () => {
|
|
|
93
93
|
.toThrow('uses "typesFinalizerTest/tags" on "world" and "typesFinalizerTest/page" on "main"');
|
|
94
94
|
expect(() => untyped(Finalizer.changes, {}, { resources: { read: [page] } }))
|
|
95
95
|
.toThrow(`watches or reads values on thread "main"; a changes finalizer runs on the world's thread, in the sync point.`);
|
|
96
|
+
expect(() => untyped(Finalizer.changes, { name: "savePage", watch: [page] }))
|
|
97
|
+
.toThrow(`watches or reads values on thread "main"; a changes finalizer runs on the world's thread, in the sync point.`);
|
|
96
98
|
// Reading another thread's values, without writing or looking up, it's created.
|
|
97
99
|
const detach = Finalizer.create({ components: { with: [node] } }, row => { void row[node]; });
|
|
98
100
|
expect(programFinalizers()).toHaveLength(8);
|
|
@@ -8,9 +8,11 @@ import { checkWorldWritten } from '../registry/written.js';
|
|
|
8
8
|
import { checkLookup } from '../registry/indexes.js';
|
|
9
9
|
import type { Listed, 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 } 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<Listed<D, "watch"> & Component.Key>>
|
|
31
|
+
type OnWorld<D> = true extends Elsewhere<ThreadsOf<D> | ThreadOfComponent<Listed<D, "watch"> & Component.Key> | ThreadOfResource<Listed<D, "watch"> & Resource.Key>>
|
|
30
32
|
? { readonly "error: a changes finalizer watches and reads only the world's thread's objects": true }
|
|
31
33
|
: {};
|
|
32
34
|
|
|
@@ -127,12 +129,12 @@ export const create = <const D extends Declaration>(
|
|
|
127
129
|
|
|
128
130
|
/**
|
|
129
131
|
* 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.
|
|
132
|
+
* on the world's thread. Throws as `create` does, for a watched component that isn't registered or is derived, a
|
|
133
|
+
* watched key that's neither a component nor a resource, and for one watching or reading another thread's values.
|
|
132
134
|
*/
|
|
133
135
|
export const changes = <const D extends ChangesDeclaration>(
|
|
134
136
|
declared: D & Inferred & OnWorld<D>,
|
|
135
|
-
body: (changes: ChangeSet<Watched<D>>, context: Context<D>) => void,
|
|
137
|
+
body: (changes: ChangeSet<Watched<D>, WatchedResources<D>>, context: Context<D>) => void,
|
|
136
138
|
): Finalizer => {
|
|
137
139
|
const id = takeIdOfDeclaration(declared);
|
|
138
140
|
const name = nameOf(declared, id, "finalizer");
|
|
@@ -148,10 +150,13 @@ export const changes = <const D extends ChangesDeclaration>(
|
|
|
148
150
|
for (const keys of [lookup.read, "has" in lookup ? lookup.has : undefined, "not" in lookup ? lookup.not : undefined]) requireRegistered("Finalizer", name, "component", keys);
|
|
149
151
|
}
|
|
150
152
|
for (const key of declaration.watch ?? []) {
|
|
151
|
-
|
|
153
|
+
const kind = kindOf(key);
|
|
154
|
+
if (kind !== "component" && kind !== "resource") throw new Error(`Finalizer "${name}" watches "${labelOf(key)}", which isn't a registered component or resource: ${whyUnregistered(key)}.`);
|
|
152
155
|
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.`);
|
|
153
156
|
}
|
|
154
|
-
const
|
|
157
|
+
const watched = (kind: "component" | "resource"): readonly symbol[] => (declaration.watch ?? []).filter(key => kindOf(key) === kind);
|
|
158
|
+
const read = [...declaration.resources?.read ?? [], ...watched("resource")];
|
|
159
|
+
const thread = threadOf({ components: { read: watched("component") }, resources: { ...declaration.resources, read }, lookup: declaration.lookup }, "Finalizer", name);
|
|
155
160
|
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
161
|
checkOrder(name, "world", declaration);
|
|
157
162
|
return addFinalizer(finalizer, id);
|
|
@@ -4,6 +4,7 @@ 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';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* What every finalizer declares. What its body reads of its row and lookups, and reads and writes of resources, the
|
|
@@ -26,9 +27,10 @@ export type Declaration = Shared & {
|
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* A finalizer that sees each sync point's structural changes to the components it watches: by default, every one that
|
|
29
|
-
* isn't derived or an object a joined thread fills
|
|
30
|
+
* isn't derived or an object a joined thread fills; and the resources it watches, none by default, written since the
|
|
31
|
+
* last sync point's finalizers ran.
|
|
30
32
|
*/
|
|
31
|
-
export type ChangesDeclaration = Shared & { readonly watch?: readonly Essential[] };
|
|
33
|
+
export type ChangesDeclaration = Shared & { readonly watch?: readonly (Essential | Resource.Key)[] };
|
|
32
34
|
|
|
33
35
|
type Key = symbol;
|
|
34
36
|
|
|
@@ -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
|
+
};
|