@adobe/ecs 0.2.1 → 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/README.md +2 -2
- 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/describe.d.ts +1 -1
- package/dist/implementation/compiler/describe.js +3 -3
- package/dist/implementation/compiler/describe.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/signature.js +1 -1
- package/dist/implementation/compiler/transform.js +6 -5
- 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 +86 -2
- 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/entities.d.ts +2 -0
- package/dist/implementation/world/entities.js +6 -0
- package/dist/implementation/world/entities.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 +22 -4
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/register.js +2 -2
- 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.d.ts +1 -1
- package/dist/implementation/world/rows.js +23 -13
- 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 +19 -15
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/sharing.js +1 -1
- package/dist/implementation/world/staging.d.ts +4 -0
- package/dist/implementation/world/staging.js +15 -3
- package/dist/implementation/world/staging.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 +9 -0
- package/dist/implementation/world/tables-read.js +72 -0
- package/dist/implementation/world/tables-read.js.map +1 -0
- package/dist/implementation/world/tables.d.ts +10 -0
- package/dist/implementation/world/tables.js +401 -0
- package/dist/implementation/world/tables.js.map +1 -0
- 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 +11 -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/index.d.ts +8 -28
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/dist/interface/component/declarations.d.ts +4 -0
- package/dist/interface/component/declarations.js +8 -0
- package/dist/interface/component/declarations.js.map +1 -0
- package/dist/interface/component/declared.d.ts +12 -0
- package/dist/interface/component/declared.js +2 -0
- package/dist/interface/component/declared.js.map +1 -0
- package/dist/interface/component/index.d.ts +2 -0
- package/dist/interface/component/index.js +1 -1
- package/dist/interface/component/index.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/names.d.ts +2 -0
- package/dist/interface/registry/names.js +9 -0
- package/dist/interface/registry/names.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +31 -4
- package/dist/interface/registry/schedule.js +185 -24
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/resource/declarations.d.ts +4 -0
- package/dist/interface/resource/declarations.js +8 -0
- package/dist/interface/resource/declarations.js.map +1 -0
- package/dist/interface/resource/index.d.ts +2 -0
- package/dist/interface/resource/index.js +1 -1
- package/dist/interface/resource/index.js.map +1 -1
- package/dist/interface/system/access.d.ts +15 -0
- package/dist/interface/system/body.d.ts +11 -6
- package/dist/interface/system/check.d.ts +8 -1
- package/dist/interface/system/check.js +42 -12
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +37 -0
- 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 +31 -15
- package/dist/interface/system/create.js +8 -7
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +9 -2
- 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 +28 -0
- package/dist/interface/system/tables.js +2 -0
- package/dist/interface/system/tables.js.map +1 -0
- package/dist/interface/world/index.d.ts +1 -1
- package/dist/interface/world/table/index.d.ts +27 -0
- package/dist/interface/world/table/index.js +3 -0
- package/dist/interface/world/table/index.js.map +1 -0
- package/dist/interface/world/table/table.d.ts +11 -0
- package/dist/interface/world/table/table.js +2 -0
- package/dist/interface/world/table/table.js.map +1 -0
- package/dist/interface/world/world.d.ts +25 -10
- package/dist/world.d.ts +27 -0
- package/dist/world.js +27 -0
- package/dist/world.js.map +1 -0
- package/package.json +11 -2
- package/src/exports.test.ts +1 -1
- package/src/guide/00-keys.ts +1 -1
- package/src/guide/01-resources.md +1 -1
- package/src/guide/01-resources.ts +1 -1
- package/src/guide/02-entities.ts +2 -2
- package/src/guide/03-systems.md +2 -2
- package/src/guide/03-systems.out +1 -1
- package/src/guide/03-systems.ts +3 -3
- package/src/guide/04-ticks.md +1 -1
- package/src/guide/04-ticks.ts +3 -3
- package/src/guide/05-scheduling.ts +1 -1
- package/src/guide/06-sync-points.md +18 -7
- package/src/guide/06-sync-points.out +1 -2
- package/src/guide/06-sync-points.ts +10 -6
- package/src/guide/07-change-filters.ts +2 -2
- package/src/guide/08-input.ts +1 -1
- package/src/guide/09-rules.ts +1 -1
- package/src/guide/11-traits.ts +3 -3
- package/src/guide/12-lookups.ts +2 -2
- package/src/guide/13-indexes.ts +4 -4
- package/src/guide/14-kernels.ts +1 -1
- package/src/guide/15-reductions.ts +1 -1
- package/src/guide/16-gpu.ts +1 -1
- package/src/guide/17-hierarchies.ts +1 -1
- package/src/guide/18-identity.out +1 -1
- package/src/guide/19-threads.md +4 -2
- package/src/guide/19-threads.ts +2 -2
- package/src/guide/20-shared.ts +3 -3
- package/src/guide/21-dom.md +2 -2
- package/src/guide/21-dom.ts +1 -1
- package/src/guide/22-canvas.md +1 -1
- package/src/guide/22-canvas.ts +1 -1
- package/src/guide/CLAUDE.md +1 -1
- package/src/guide/guide.test.ts +9 -154
- package/src/implementation/compiler/body-access.ts +102 -12
- package/src/implementation/compiler/describe.test-helper.ts +1 -1
- package/src/implementation/compiler/describe.test.ts +1 -1
- package/src/implementation/compiler/describe.ts +3 -3
- package/src/implementation/compiler/estree.ts +9 -6
- package/src/implementation/compiler/hygiene.test.ts +1 -1
- package/src/implementation/compiler/inferred.test.ts +1 -1
- package/src/implementation/compiler/inferred.ts +4 -2
- package/src/implementation/compiler/plugin.test.ts +2 -2
- package/src/implementation/compiler/signature.ts +1 -1
- package/src/implementation/compiler/transform.test.ts +43 -23
- package/src/implementation/compiler/transform.ts +6 -4
- 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/change-blocks.test.ts +1 -1
- package/src/implementation/world/commands.ts +76 -2
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +57 -2
- package/src/implementation/world/defaults.test.ts +1 -1
- package/src/implementation/world/despawn-where.test.ts +287 -0
- package/src/implementation/world/entities.ts +6 -0
- package/src/implementation/world/execute.ts +125 -78
- package/src/implementation/world/finalizers.test.ts +1 -1
- package/src/implementation/world/finalizers.ts +27 -4
- package/src/implementation/world/ids.test.ts +1 -1
- package/src/implementation/world/index-changed.test.ts +2 -2
- package/src/implementation/world/index-joined.test.ts +5 -5
- package/src/implementation/world/index-order.test.ts +1 -1
- package/src/implementation/world/indexes.test.ts +2 -1
- package/src/implementation/world/join.test.ts +23 -4
- package/src/implementation/world/join.ts +26 -4
- package/src/implementation/world/joined-observer.test.ts +1 -1
- package/src/implementation/world/joining-worker.fixture.ts +1 -1
- package/src/implementation/world/joining.fixture.ts +1 -1
- package/src/implementation/world/joining.test-helper.ts +1 -1
- package/src/implementation/world/kernel-spawns.test.ts +1 -1
- package/src/implementation/world/names.test.ts +1 -2
- package/src/implementation/world/observe-factories.test.ts +2 -2
- package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
- package/src/implementation/world/observe.test-helper.ts +1 -1
- package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
- package/src/implementation/world/observer-contained.test-helper.ts +1 -1
- package/src/implementation/world/observer-contained.test.ts +2 -2
- package/src/implementation/world/observer-joined.test-helper.ts +4 -4
- package/src/implementation/world/observer-joined.test.ts +23 -15
- package/src/implementation/world/observer.browser.test.ts +2 -2
- package/src/implementation/world/observer.test.ts +8 -8
- package/src/implementation/world/optional-workers.fixture.ts +1 -1
- package/src/implementation/world/optional.test.ts +7 -7
- package/src/implementation/world/perf.md +3 -1
- package/src/implementation/world/reclaim.test.ts +3 -3
- package/src/implementation/world/register.ts +2 -2
- package/src/implementation/world/remote-objects.test-helper.ts +1 -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 +24 -13
- 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 +10 -10
- package/src/implementation/world/runner.ts +20 -16
- package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
- package/src/implementation/world/shared-kernels.test.ts +2 -2
- package/src/implementation/world/shared-workers.fixture.ts +2 -2
- package/src/implementation/world/shared.test.ts +10 -10
- package/src/implementation/world/sharing.ts +1 -1
- package/src/implementation/world/staging.ts +16 -3
- 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 +70 -0
- package/src/implementation/world/tables.test.ts +369 -0
- package/src/implementation/world/tables.ts +368 -0
- package/src/implementation/world/tails.test.ts +1 -1
- package/src/implementation/world/target.test.ts +2 -2
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.test-helper.ts +3 -3
- package/src/implementation/world/thread-systems.ts +11 -6
- package/src/implementation/world/up.test.ts +13 -1
- package/src/implementation/world/up.ts +6 -1
- package/src/implementation/world/where.test.ts +4 -4
- package/src/implementation/world/where.ts +38 -10
- package/src/implementation/world/workers.fixture.ts +2 -2
- package/src/index.ts +4 -29
- package/src/interface/component/declarations.ts +11 -0
- package/src/interface/component/declared.ts +9 -0
- package/src/interface/component/index.ts +2 -0
- 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/index.type-test.ts +2 -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/indexes/index.type-test.ts +1 -1
- package/src/interface/registry/names.ts +8 -0
- package/src/interface/registry/schedule.ts +200 -27
- package/src/interface/resource/declarations.ts +11 -0
- package/src/interface/resource/index.ts +2 -0
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/body.ts +7 -5
- package/src/interface/system/check.ts +37 -10
- package/src/interface/system/commands.ts +33 -0
- 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 +21 -13
- package/src/interface/system/create.ts +24 -15
- package/src/interface/system/declaration.ts +9 -2
- 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/lists.type-test.ts +1 -1
- package/src/interface/system/optional.type-test.ts +1 -1
- 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/shared.type-test.ts +1 -1
- package/src/interface/system/system.type-test.ts +26 -20
- package/src/interface/system/tables.ts +27 -0
- package/src/interface/system/up.type-test.ts +3 -3
- package/src/interface/world/index.ts +2 -2
- package/src/interface/world/run.type-test.ts +14 -4
- package/src/interface/world/table/index.ts +20 -0
- package/src/interface/world/table/table.ts +12 -0
- package/src/interface/world/world.ts +23 -10
- package/src/observe/entity.ts +2 -2
- package/src/observe/resource.ts +2 -2
- package/src/observe/select.ts +4 -4
- package/src/observe/world-state.ts +3 -3
- package/src/perftest/benchmarks.ts +19 -1
- package/src/perftest/indexes.ts +3 -3
- package/src/perftest/loop.ts +1 -1
- package/src/perftest/observer.ts +4 -4
- package/src/perftest/reads.ts +2 -2
- package/src/perftest/remote.ts +5 -5
- package/src/perftest/strings.ts +1 -1
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/suite.ts +1 -1
- package/src/perftest/tables.ts +107 -0
- package/src/world.test.ts +2 -2
- package/src/world.ts +32 -0
|
@@ -39,10 +39,10 @@ export type ExecuteContext = {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Runs a schedule tier by tier,
|
|
43
|
-
* first so workers run while main-thread systems do; commands merge in
|
|
44
|
-
* ran them. A failing system stops the run after its tier finishes:
|
|
45
|
-
* apply. `system` is the one the run was given, which the GPU's errors are reported under.
|
|
42
|
+
* Runs a schedule tier by tier, applying a sync point after each tier its schedule says and after the last, and reports
|
|
43
|
+
* what went wrong. Within a tier, kernels start first so workers run while main-thread systems do; commands merge in
|
|
44
|
+
* tier order, then chunk order, whichever thread ran them. A failing system stops the run after its tier finishes:
|
|
45
|
+
* later tiers are skipped, and commands so far still apply. `system` is the one the run was given, which the GPU's errors are reported under.
|
|
46
46
|
*/
|
|
47
47
|
export const execute = (context: ExecuteContext, schedule: Schedule, args: unknown, system: { readonly name: string }): RunReport | Promise<RunReport> => {
|
|
48
48
|
const { gpu } = context.state;
|
|
@@ -128,6 +128,8 @@ type Prepared = {
|
|
|
128
128
|
chunk: number;
|
|
129
129
|
error: unknown;
|
|
130
130
|
tierFailed: boolean;
|
|
131
|
+
/** How many commands the run's sync points dropped so far. */
|
|
132
|
+
dropped: number;
|
|
131
133
|
};
|
|
132
134
|
|
|
133
135
|
/** A schedule's tails: the schedule without them, and their runners, leaves and tiers in schedule order. */
|
|
@@ -139,6 +141,7 @@ const tailingOf = (context: ExecuteContext, schedule: Schedule): Tailing | null
|
|
|
139
141
|
const leaves: Leaf[] = [];
|
|
140
142
|
const tiers: number[] = [];
|
|
141
143
|
const rest: Leaf[][] = [];
|
|
144
|
+
const syncs: (Schedule["syncs"][number])[] = [];
|
|
142
145
|
schedule.tiers.forEach((tier, t) => {
|
|
143
146
|
const kept = tier.filter((leaf, i) => {
|
|
144
147
|
if (!flags[t][i]) return true;
|
|
@@ -147,9 +150,13 @@ const tailingOf = (context: ExecuteContext, schedule: Schedule): Tailing | null
|
|
|
147
150
|
return false;
|
|
148
151
|
});
|
|
149
152
|
if (kept.length > 0) rest.push(kept);
|
|
153
|
+
// A sync point after a tier left empty follows the tier kept before it.
|
|
154
|
+
const reasons = schedule.syncs[t];
|
|
155
|
+
if (reasons !== null && rest.length > 0) syncs[rest.length - 1] = [...syncs[rest.length - 1] ?? [], ...reasons];
|
|
150
156
|
});
|
|
151
157
|
if (leaves.length === 0) return null;
|
|
152
|
-
|
|
158
|
+
const kept = rest.map((_, t) => t < rest.length - 1 ? syncs[t] ?? null : null);
|
|
159
|
+
return { schedule: { tiers: rest, reductions: schedule.reductions, observer: schedule.observer, syncs: kept }, runners: leaves.map(context.runnerOf), leaves, tiers };
|
|
153
160
|
};
|
|
154
161
|
|
|
155
162
|
type Outcome = { failures: Failure[] | null; skipped: Leaf[] | null };
|
|
@@ -164,7 +171,7 @@ const preparedFor = (context: ExecuteContext, schedule: Schedule): Prepared => {
|
|
|
164
171
|
runners: schedule.tiers.map(tier => tier.map(context.runnerOf)), remotes: schedule.tiers.map(() => 0), uses: schedule.tiers.map(() => []), version: -1,
|
|
165
172
|
started: schedule.tiers.map(tier => new Array<readonly Chunk[]>(tier.length)),
|
|
166
173
|
systems: [], tiers: [], tasks: new Map(), warnings: [], warningCount: 0, warned: null, outcome: { failures: null, skipped: null },
|
|
167
|
-
placement: -1, tailing: null, settling: 0, chunk: 0, error: undefined, tierFailed: false,
|
|
174
|
+
placement: -1, tailing: null, settling: 0, chunk: 0, error: undefined, tierFailed: false, dropped: 0,
|
|
168
175
|
};
|
|
169
176
|
bySchedule.set(schedule, prepared);
|
|
170
177
|
}
|
|
@@ -315,8 +322,8 @@ const runFused = async (context: ExecuteContext, schedule: Schedule, prepared: P
|
|
|
315
322
|
const { gpu } = state;
|
|
316
323
|
let last = first;
|
|
317
324
|
if (prepared.version !== state.runnersVersion) refresh(context, schedule, prepared);
|
|
318
|
-
// A tier using an index starts a task of its own, after the index is refreshed for it.
|
|
319
|
-
while (last + 1 < schedule.tiers.length && allOn(prepared.runners[last + 1], channel) && prepared.uses[last + 1].length === 0) last++;
|
|
325
|
+
// A tier using an index starts a task of its own, after the index is refreshed for it, as does one after a sync point.
|
|
326
|
+
while (last + 1 < schedule.tiers.length && schedule.syncs[last] === null && allOn(prepared.runners[last + 1], channel) && prepared.uses[last + 1].length === 0) last++;
|
|
320
327
|
if (channel.busy()) await channel.idle();
|
|
321
328
|
for (let v = first; v <= last; v++) {
|
|
322
329
|
if (state.tails.length !== 0) {
|
|
@@ -431,7 +438,7 @@ const endTier = (state: State, gpu: GpuEngine | null, prepared: Prepared, schedu
|
|
|
431
438
|
};
|
|
432
439
|
|
|
433
440
|
/**
|
|
434
|
-
* Runs a schedule tier by tier
|
|
441
|
+
* Runs a schedule tier by tier with its sync points, and reports what went wrong (`execute`). Runs with no GPU,
|
|
435
442
|
* tails or observer posts awaiting nothing, so a run that waits for nothing allocates no async frame: at the first
|
|
436
443
|
* tier on a joined thread, index build across the pool or chunk on workers, `executeFrom` goes on from there.
|
|
437
444
|
*/
|
|
@@ -454,11 +461,11 @@ const executeTiers = (context: ExecuteContext, whole: Schedule, args: unknown):
|
|
|
454
461
|
if (schedule.reductions.size !== 0) for (const [key, contributors] of schedule.reductions) (folds ??= new Map()).set(key, { target: null, values: [], remaining: contributors, ran: false, failed: false });
|
|
455
462
|
const { outcome } = prepared;
|
|
456
463
|
prepared.warningCount = 0;
|
|
464
|
+
prepared.dropped = 0;
|
|
457
465
|
outcome.failures = null;
|
|
458
466
|
outcome.skipped = null;
|
|
459
467
|
if (state.gpu !== null || tailing !== null || state.tails.length !== 0 || state.observer.posts.length !== 0) return executeFrom(context, schedule, prepared, folds, args, tailing, 0, null, null);
|
|
460
468
|
let handed: Promise<RunReport> | null = null;
|
|
461
|
-
let dropped = 0;
|
|
462
469
|
try {
|
|
463
470
|
for (let t = 0; t < schedule.tiers.length; t++) {
|
|
464
471
|
// A tick in progress takes up runners replaced since it started from its next tier on.
|
|
@@ -474,37 +481,35 @@ const executeTiers = (context: ExecuteContext, whole: Schedule, args: unknown):
|
|
|
474
481
|
const waiting = settleTier(state, folds, prepared, schedule.tiers[t], t, nothing);
|
|
475
482
|
if (waiting !== null) return handed = executeFrom(context, schedule, prepared, folds, args, null, t, null, waiting);
|
|
476
483
|
if (endTier(state, null, prepared, schedule, t)) break;
|
|
484
|
+
if (schedule.syncs[t] !== null) syncNow(context, prepared, false);
|
|
477
485
|
}
|
|
478
486
|
}
|
|
479
487
|
finally {
|
|
480
|
-
if (handed === null)
|
|
488
|
+
if (handed === null) syncNow(context, prepared, true);
|
|
481
489
|
}
|
|
482
|
-
return reportOf(state, prepared
|
|
490
|
+
return reportOf(state, prepared);
|
|
483
491
|
};
|
|
484
492
|
|
|
485
493
|
/**
|
|
486
|
-
* A
|
|
487
|
-
*
|
|
494
|
+
* A sync point with nothing to wait for: no GPU, tails or observer posts, which `executeTiers` checked before each
|
|
495
|
+
* tier and bodies can't add. The run's `last` lets go of strings no reader holds.
|
|
488
496
|
*/
|
|
489
|
-
const
|
|
497
|
+
const syncNow = (context: ExecuteContext, prepared: Prepared, last: boolean): void => {
|
|
490
498
|
const { state, workers, publish } = context;
|
|
491
|
-
|
|
492
|
-
let dropped = 0;
|
|
493
|
-
// This run's tasks on joined threads are done: they return to their event loops while its sync point applies.
|
|
499
|
+
// The run's tasks on joined threads so far are done: they return to their event loops while its sync point applies.
|
|
494
500
|
for (let k = 0; k < state.logs.length; k++) markRun(state.logs[k]);
|
|
495
501
|
try {
|
|
496
502
|
if (prepareSync(state)) {
|
|
497
|
-
dropped
|
|
498
|
-
for (const { finalizer, error, kind } of applySync(state)) (outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
|
|
503
|
+
prepared.dropped += state.plan.dropped;
|
|
504
|
+
for (const { finalizer, error, kind } of applySync(state)) (prepared.outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
|
|
499
505
|
}
|
|
500
506
|
}
|
|
501
507
|
finally {
|
|
502
508
|
// Strings let go before the last run's sync point have no reader left: this run's waited for every one.
|
|
503
|
-
state.strings?.settle();
|
|
509
|
+
if (last) state.strings?.settle();
|
|
504
510
|
rewind(state, workers);
|
|
505
511
|
publish();
|
|
506
512
|
}
|
|
507
|
-
return dropped;
|
|
508
513
|
};
|
|
509
514
|
|
|
510
515
|
/**
|
|
@@ -512,12 +517,11 @@ const finishSync = (context: ExecuteContext, prepared: Prepared): number => {
|
|
|
512
517
|
* indexes' build, is done, or once `settling`, what settling it waits for, settles.
|
|
513
518
|
*/
|
|
514
519
|
const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared: Prepared, folds: Map<unknown, Fold> | null, args: unknown, tailing: Tailing | null, first: number, building: Promise<void> | null, settling: Promise<Settled> | null): Promise<RunReport> => {
|
|
515
|
-
const { state, workers
|
|
520
|
+
const { state, workers } = context;
|
|
516
521
|
const { gpu } = state;
|
|
517
522
|
const { outcome } = prepared;
|
|
518
523
|
let built = building;
|
|
519
524
|
let waited = settling;
|
|
520
|
-
let dropped = 0;
|
|
521
525
|
try {
|
|
522
526
|
for (let t = first; t < schedule.tiers.length; t++) {
|
|
523
527
|
const tier = schedule.tiers[t];
|
|
@@ -553,6 +557,10 @@ const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared
|
|
|
553
557
|
const last = await runFused(context, schedule, prepared, folds, args, t, only);
|
|
554
558
|
if (last < 0) break;
|
|
555
559
|
t = last;
|
|
560
|
+
if (schedule.syncs[t] !== null) {
|
|
561
|
+
const pending = syncPoint(context, prepared, false);
|
|
562
|
+
if (pending !== null) await pending;
|
|
563
|
+
}
|
|
556
564
|
continue;
|
|
557
565
|
}
|
|
558
566
|
if (gpu !== null) await readAhead(state, gpu, runners);
|
|
@@ -578,60 +586,15 @@ const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared
|
|
|
578
586
|
let waiting = settleTier(state, folds, prepared, tier, t, value);
|
|
579
587
|
while (waiting !== null) waiting = settleTier(state, folds, prepared, tier, t, await waiting);
|
|
580
588
|
if (endTier(state, gpu, prepared, schedule, t)) break;
|
|
589
|
+
if (schedule.syncs[t] !== null) {
|
|
590
|
+
const pending = syncPoint(context, prepared, false);
|
|
591
|
+
if (pending !== null) await pending;
|
|
592
|
+
}
|
|
581
593
|
}
|
|
582
594
|
}
|
|
583
595
|
finally {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
try {
|
|
587
|
-
if (gpu !== null) {
|
|
588
|
-
gpu.submit();
|
|
589
|
-
gpu.beforeSync();
|
|
590
|
-
}
|
|
591
|
-
try {
|
|
592
|
-
// A sync point counts as a write to everything a tail still reads.
|
|
593
|
-
if (state.tails.length !== 0) {
|
|
594
|
-
activate(state);
|
|
595
|
-
const waiting = syncPending(state) ? settleAll(state) : null;
|
|
596
|
-
if (waiting !== null) await waiting;
|
|
597
|
-
}
|
|
598
|
-
// Each sync point waits for observer systems on joined threads in progress, and cancels the rest.
|
|
599
|
-
if (state.observer.posts.length !== 0) {
|
|
600
|
-
const waiting = settlePosts(state);
|
|
601
|
-
if (waiting !== null) await waiting;
|
|
602
|
-
}
|
|
603
|
-
if (prepareSync(state)) {
|
|
604
|
-
try {
|
|
605
|
-
if (gpu !== null) {
|
|
606
|
-
// The sync point moves and writes existing rows of these archetypes on the CPU, and
|
|
607
|
-
// finalizers read them, so it needs the GPU's writes there first.
|
|
608
|
-
const disturbed = disturbedBy(state);
|
|
609
|
-
gpu.disturb(disturbed.archetypes);
|
|
610
|
-
if (gpu.anyAhead()) await gpu.readback(disturbed.storages);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
finally {
|
|
614
|
-
// Applied even when the readback fails, which only a lost device makes it do: the CPU state
|
|
615
|
-
// stays whole.
|
|
616
|
-
dropped = state.plan.dropped;
|
|
617
|
-
for (const { finalizer, error, kind } of applySync(state)) (outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
finally {
|
|
622
|
-
// Even a sync point that failed partway moved rows.
|
|
623
|
-
if (gpu !== null && state.touched !== null) {
|
|
624
|
-
gpu.touched(state.touched);
|
|
625
|
-
state.touched.clear();
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
finally {
|
|
630
|
-
// Strings let go before the last run's sync point have no reader left: this run's waited for every one.
|
|
631
|
-
state.strings?.settle();
|
|
632
|
-
rewind(state, workers);
|
|
633
|
-
publish();
|
|
634
|
-
}
|
|
596
|
+
const pending = syncPoint(context, prepared, true);
|
|
597
|
+
if (pending !== null) await pending;
|
|
635
598
|
}
|
|
636
599
|
if (tailing !== null) {
|
|
637
600
|
// Placed again since, as when a joined thread is gone: they're found again next run.
|
|
@@ -644,12 +607,96 @@ const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared
|
|
|
644
607
|
countRan(state, tailing.runners);
|
|
645
608
|
}
|
|
646
609
|
}
|
|
647
|
-
return reportOf(state, prepared
|
|
610
|
+
return reportOf(state, prepared);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Applies the commands so far as a sync point, once what it changes is read: by tails and observer systems on joined
|
|
615
|
+
* threads still reading it, and where it moves rows, by the GPU. One within a run lets the tiers after it see them;
|
|
616
|
+
* the run's `last` lets go of strings no reader holds. Returns what to wait for, or null once it applied, so a sync
|
|
617
|
+
* point with nothing to wait for takes no turn of the event loop before what follows it.
|
|
618
|
+
*/
|
|
619
|
+
const syncPoint = (context: ExecuteContext, prepared: Prepared, last: boolean): Promise<void> | null => {
|
|
620
|
+
const { state } = context;
|
|
621
|
+
const { gpu } = state;
|
|
622
|
+
// The run's tasks on joined threads so far are done: they return to their event loops while its sync point applies.
|
|
623
|
+
for (let k = 0; k < state.logs.length; k++) markRun(state.logs[k]);
|
|
624
|
+
let pending: Promise<void> | null = null;
|
|
625
|
+
try {
|
|
626
|
+
if (gpu !== null) {
|
|
627
|
+
gpu.submit();
|
|
628
|
+
gpu.beforeSync();
|
|
629
|
+
}
|
|
630
|
+
const reading = readersDone(state);
|
|
631
|
+
pending = reading === null ? applyOnDevice(state, prepared) : reading.then(() => applyOnDevice(state, prepared) ?? undefined);
|
|
632
|
+
}
|
|
633
|
+
finally {
|
|
634
|
+
if (pending === null) afterSync(context, last);
|
|
635
|
+
}
|
|
636
|
+
return pending === null ? null : pending.finally(() => afterSync(context, last));
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Waits for the readers a sync point changes what they read for: tails, as it counts as a write to everything they
|
|
641
|
+
* read, then observer systems on joined threads in progress, cancelling the rest; null when none needs waiting for.
|
|
642
|
+
*/
|
|
643
|
+
const readersDone = (state: State): Promise<void> | null => {
|
|
644
|
+
if (state.tails.length !== 0) {
|
|
645
|
+
activate(state);
|
|
646
|
+
const waiting = syncPending(state) ? settleAll(state) : null;
|
|
647
|
+
if (waiting !== null) return waiting.then(() => (state.observer.posts.length === 0 ? null : settlePosts(state)) ?? undefined);
|
|
648
|
+
}
|
|
649
|
+
return state.observer.posts.length === 0 ? null : settlePosts(state);
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Prepares and applies a sync point, reading back first what the GPU wrote where it moves and writes existing rows,
|
|
654
|
+
* which finalizers read too; applied even when the readback fails, which only a lost device makes it do, so the CPU
|
|
655
|
+
* state stays whole.
|
|
656
|
+
*/
|
|
657
|
+
const applyOnDevice = (state: State, prepared: Prepared): Promise<void> | null => {
|
|
658
|
+
if (!prepareSync(state)) return null;
|
|
659
|
+
const { gpu } = state;
|
|
660
|
+
let reading: Promise<void> | null = null;
|
|
661
|
+
try {
|
|
662
|
+
if (gpu !== null) {
|
|
663
|
+
const disturbed = disturbedBy(state);
|
|
664
|
+
gpu.disturb(disturbed.archetypes);
|
|
665
|
+
if (gpu.anyAhead()) reading = gpu.readback(disturbed.storages);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
finally {
|
|
669
|
+
if (reading === null) apply(state, prepared);
|
|
670
|
+
}
|
|
671
|
+
return reading === null ? null : reading.finally(() => apply(state, prepared));
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const apply = (state: State, prepared: Prepared): void => {
|
|
675
|
+
prepared.dropped += state.plan.dropped;
|
|
676
|
+
for (const { finalizer, error, kind } of applySync(state)) (prepared.outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
/** Once a sync point applied or failed: the rows it moved, even partway, the strings it let go, and its buffers. */
|
|
680
|
+
const afterSync = (context: ExecuteContext, last: boolean): void => {
|
|
681
|
+
const { state, workers, publish } = context;
|
|
682
|
+
const { gpu } = state;
|
|
683
|
+
try {
|
|
684
|
+
if (gpu !== null && state.touched !== null) {
|
|
685
|
+
gpu.touched(state.touched);
|
|
686
|
+
state.touched.clear();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
finally {
|
|
690
|
+
// Strings let go before the last run's sync point have no reader left: this run's waited for every one.
|
|
691
|
+
if (last) state.strings?.settle();
|
|
692
|
+
rewind(state, workers);
|
|
693
|
+
publish();
|
|
694
|
+
}
|
|
648
695
|
};
|
|
649
696
|
|
|
650
697
|
/** A run's report once its sync point applied, its index conflicts included: a shared one when nothing went wrong. */
|
|
651
|
-
const reportOf = (state: State, prepared: Prepared
|
|
652
|
-
const { outcome } = prepared;
|
|
698
|
+
const reportOf = (state: State, prepared: Prepared): RunReport => {
|
|
699
|
+
const { outcome, dropped } = prepared;
|
|
653
700
|
// Conflicts unique indexes found while refreshing for finalizers.
|
|
654
701
|
if (state.indexFailures.length !== 0) (outcome.failures ??= []).push(...state.indexFailures.splice(0));
|
|
655
702
|
const { failures, skipped } = outcome;
|
|
@@ -137,7 +137,7 @@ describe("finalizers", () => {
|
|
|
137
137
|
const reads = System.create({}, ({ resources }) => { void resources[level]; });
|
|
138
138
|
// Read after every run, as a read through a run of its own would apply the commands left for it.
|
|
139
139
|
let seen: string | null = null;
|
|
140
|
-
const watch = System.create({
|
|
140
|
+
const watch = System.create({ readonly: true, lookup: { labels: { with: [label] } } }, ({ lookup }) => {
|
|
141
141
|
seen = lookup.labels(other)?.[label] ?? null;
|
|
142
142
|
});
|
|
143
143
|
const world = createWorld({ [workers]: 0 });
|
|
@@ -20,6 +20,8 @@ import type { TaskChannel } from './remote-systems.js';
|
|
|
20
20
|
import { resourceOffsetsOf, writeStops, type StopsOf } from './stop-lists.js';
|
|
21
21
|
import { acquireLookups, refreshUses, type IndexLookups } from './indexes.js';
|
|
22
22
|
import { indexWatchesOf } from './lookup-keys.js';
|
|
23
|
+
import { takeResourceChanges, watchResource, type ResourceChange } from './resource-changes.js';
|
|
24
|
+
import type { Resource } from '../../interface/resource/resource.js';
|
|
23
25
|
|
|
24
26
|
/** A registered finalizer, resolved for a world: what it matches or watches, its context, and the buffer its commands go to. */
|
|
25
27
|
export type FinalizerRunner = {
|
|
@@ -28,6 +30,8 @@ export type FinalizerRunner = {
|
|
|
28
30
|
readonly excluded: readonly number[];
|
|
29
31
|
/** For a changes finalizer: whether it watches a component, by id. */
|
|
30
32
|
readonly watches: (id: number) => boolean;
|
|
33
|
+
/** For a changes finalizer: the resources it watches, by id. */
|
|
34
|
+
readonly resourceWatches: readonly number[];
|
|
31
35
|
/** By archetype id: 1 if it matches, 2 if not, 0 until known. */
|
|
32
36
|
readonly matches: number[];
|
|
33
37
|
readonly context: Context;
|
|
@@ -61,9 +65,16 @@ export const createFinalizerRunner = (state: State, finalizer: Finalizer): Final
|
|
|
61
65
|
// the objects a joined thread fills, whose storage here holds only tags.
|
|
62
66
|
let watches = (id: number): boolean => !state.derived[id] && !state.resources.has(id) && state.remote[id] === undefined;
|
|
63
67
|
const watch = finalizer.kind === "changes" ? finalizer.declaration.watch : undefined;
|
|
68
|
+
const resourceWatches: number[] = [];
|
|
64
69
|
if (watch !== undefined) {
|
|
65
70
|
// `Finalizer.changes` rejected derived components.
|
|
66
71
|
const watched = new Set(watch.map(key => idOf(state, key)));
|
|
72
|
+
for (const id of watched) {
|
|
73
|
+
if (!state.resources.has(id)) continue;
|
|
74
|
+
watched.delete(id);
|
|
75
|
+
resourceWatches.push(id);
|
|
76
|
+
watchResource(state, id);
|
|
77
|
+
}
|
|
67
78
|
watches = id => watched.has(id);
|
|
68
79
|
}
|
|
69
80
|
const target = { buffer: createBuffer() };
|
|
@@ -79,6 +90,7 @@ export const createFinalizerRunner = (state: State, finalizer: Finalizer): Final
|
|
|
79
90
|
required: [...new Set([...reads, ...query?.has ?? []])].filter(key => !(query?.optional ?? []).includes(key)).map(key => idOf(state, key)),
|
|
80
91
|
excluded: (query?.not ?? []).map(key => idOf(state, key)),
|
|
81
92
|
watches,
|
|
93
|
+
resourceWatches,
|
|
82
94
|
matches: [],
|
|
83
95
|
context: createContext(state, declaration, commanding ? { commands, indexes: indexes.byName } : { indexes: indexes.byName }),
|
|
84
96
|
target,
|
|
@@ -248,9 +260,14 @@ type Grouped = { from: Essential[] | null; to: Essential[] | null; added: Essent
|
|
|
248
260
|
|
|
249
261
|
/** No components, shared by every group that replaces none. */
|
|
250
262
|
const none: readonly number[] = [];
|
|
263
|
+
const noResources: readonly never[] = [];
|
|
264
|
+
const noResourceChanges: readonly ResourceChange[] = [];
|
|
251
265
|
|
|
252
|
-
/**
|
|
253
|
-
|
|
266
|
+
/**
|
|
267
|
+
* The sync point's structural changes to a changes finalizer's components, grouped by how they changed, and of
|
|
268
|
+
* `written`, the resources it watches.
|
|
269
|
+
*/
|
|
270
|
+
const changesOf = (state: State, runner: FinalizerRunner, queued: readonly Queued[], written: readonly ResourceChange[]): ChangeSet<Essential, Resource.Key> => {
|
|
254
271
|
const { plan, archetypes, entities } = state;
|
|
255
272
|
const { watches } = runner;
|
|
256
273
|
const names = (archetype: Archetype | null): Essential[] | null =>
|
|
@@ -366,8 +383,12 @@ const changesOf = (state: State, runner: FinalizerRunner, queued: readonly Queue
|
|
|
366
383
|
const value = commandValue(state, queued, entry, id);
|
|
367
384
|
return isMissing(value) ? live(entity, id) : value;
|
|
368
385
|
};
|
|
386
|
+
const resources = written.length === 0 || runner.resourceWatches.length === 0 ? noResources
|
|
387
|
+
: written.filter(change => runner.resourceWatches.includes(change.id)).map(({ id, previous, next }) => ({ key: newestKeyOf(state.keys[id]), previous, next }));
|
|
369
388
|
return {
|
|
370
389
|
groups: [...groups.values()],
|
|
390
|
+
// Stored values match each resource's registered schema, so they have its type.
|
|
391
|
+
resources: resources as ChangeSet<Essential, Resource.Key>["resources"],
|
|
371
392
|
// Stored and commanded values match each component's registered schema, so they have its type.
|
|
372
393
|
previous: <K extends Essential>(entity: Entity, key: K) => (bornHere(entity) ? null : live(entity, idOf(state, key))) as ReadonlyType<K> | null,
|
|
373
394
|
next: <K extends Essential>(entity: Entity, key: K) => next(entity, idOf(state, key)) as ReadonlyType<K> | null,
|
|
@@ -388,6 +409,8 @@ const staged = (state: State): boolean => {
|
|
|
388
409
|
export const runFinalizers = (state: State, queued: readonly Queued[], born: Map<number, number>, tick: number): Thrown[] => {
|
|
389
410
|
const errors: Thrown[] = [];
|
|
390
411
|
state.born = born;
|
|
412
|
+
// Taken before any finalizer runs, so what finalizers write is seen at the next sync point.
|
|
413
|
+
const written = state.resourcesSeen.size === 0 ? noResourceChanges : takeResourceChanges(state);
|
|
391
414
|
try {
|
|
392
415
|
for (const runner of state.finalizers) {
|
|
393
416
|
const { finalizer, remote } = runner;
|
|
@@ -399,8 +422,8 @@ export const runFinalizers = (state: State, queued: readonly Queued[], born: Map
|
|
|
399
422
|
if (finalizer.kind === "finalizer") finalizeStops(state, runner, finalizer, errors);
|
|
400
423
|
else {
|
|
401
424
|
// The context is built to the finalizer's declaration, so its body accepts it.
|
|
402
|
-
const changes: { body(changes: ChangeSet<Essential>, context: object): void } = finalizer;
|
|
403
|
-
guarded(state, errors, finalizer, () => changes.body(changesOf(state, runner, queued), runner.context.value));
|
|
425
|
+
const changes: { body(changes: ChangeSet<Essential, Resource.Key>, context: object): void } = finalizer;
|
|
426
|
+
guarded(state, errors, finalizer, () => changes.body(changesOf(state, runner, queued, written), runner.context.value));
|
|
404
427
|
}
|
|
405
428
|
if (runner.target.buffer.count > 0) {
|
|
406
429
|
state.queued.push({ buffer: runner.target.buffer, guarded: true });
|
|
@@ -19,7 +19,7 @@ describe("a world given keys with ids", () => {
|
|
|
19
19
|
const again: typeof import("./ids-keys.test-helper.js") = await import(/* @vite-ignore */ `./ids-keys.test-helper.js?again=${Date.now()}`);
|
|
20
20
|
expect(again.score).not.toBe(score);
|
|
21
21
|
const spawnAgain = System.create({}, ({ commands }) => { spawned.push(commands.spawn([again.score])({ [again.score]: 2 })); });
|
|
22
|
-
const add = System.create({ components: { with: [again.score] } }, row => { row[again.score] += 10; });
|
|
22
|
+
const add = System.create({ components: { with: [again.score], whole: true } }, row => { row[again.score] += 10; });
|
|
23
23
|
await world.run(spawnAgain);
|
|
24
24
|
await world.run(add);
|
|
25
25
|
const [first, second] = spawned;
|
|
@@ -109,7 +109,7 @@ describe("an index lookup's changed: true", () => {
|
|
|
109
109
|
await using world = inline();
|
|
110
110
|
spawned = [];
|
|
111
111
|
const orders: string[] = [];
|
|
112
|
-
const ordering = System.create({
|
|
112
|
+
const ordering = System.create({ readonly: true, lookup: { inOrder: { index: inOrder, changed: true } } }, ({ lookup }) => {
|
|
113
113
|
const all = lookup.inOrder();
|
|
114
114
|
const groups: number[] = [];
|
|
115
115
|
for (let i = 0; i < all.length; i++) groups.push(all.at(i).entity === spawned[0] ? -1 : 0);
|
|
@@ -205,7 +205,7 @@ describe("an index lookup's changed: true", () => {
|
|
|
205
205
|
await using world = inline();
|
|
206
206
|
spawned = [];
|
|
207
207
|
const totals: number[] = [];
|
|
208
|
-
const readTotals = System.create({
|
|
208
|
+
const readTotals = System.create({ readonly: true, components: { with: [total] } }, row => {
|
|
209
209
|
totals.push(row[total]);
|
|
210
210
|
});
|
|
211
211
|
const totalsOf = async (): Promise<number[]> => {
|
|
@@ -87,7 +87,7 @@ const stirBands = System.create({ components: { with: [serial, band] } }, (row,
|
|
|
87
87
|
/** An observer system of the joined thread's labels: the serials of each reader's band, in entity order, by reader. */
|
|
88
88
|
let observedBands: string[] = [];
|
|
89
89
|
const observeBands = System.create({
|
|
90
|
-
|
|
90
|
+
readonly: true,
|
|
91
91
|
components: { with: [target, bandReader, label] },
|
|
92
92
|
lookup: { inBand: { index: bands } },
|
|
93
93
|
}, (row, { lookup }) => {
|
|
@@ -219,14 +219,14 @@ const inOrder = System.create({ components: { with: [rank, label] }, order: rank
|
|
|
219
219
|
const countWhenMoved = System.create({ components: { with: [cell, label] }, lookup: { near: { index: grid, changed: true } } }, (row, { lookup }) => {
|
|
220
220
|
row[label] = { text: String(lookup.near(row[cell]).length) };
|
|
221
221
|
});
|
|
222
|
-
const readWhenMoved = System.create({
|
|
222
|
+
const readWhenMoved = System.create({ readonly: true, components: { with: [label] }, lookup: { near: { index: grid, changed: true } } }, row => {
|
|
223
223
|
void row[label];
|
|
224
224
|
});
|
|
225
|
-
const ownWhenMoved = System.create({
|
|
225
|
+
const ownWhenMoved = System.create({ readonly: true, lookup: { near: { index: grid, changed: true } } }, () => {});
|
|
226
226
|
|
|
227
227
|
/** The entities `readLabels` reads, and their labels. */
|
|
228
228
|
const reading: { entities: readonly Entity[]; labels: readonly string[] } = { entities: [], labels: [] };
|
|
229
|
-
const readLabels = System.create({
|
|
229
|
+
const readLabels = System.create({ readonly: true, lookup: { labeled: { with: [label] } } }, ({ lookup }) => {
|
|
230
230
|
reading.labels = reading.entities.map(entity => lookup.labeled(entity)?.[label].text ?? "missing");
|
|
231
231
|
});
|
|
232
232
|
|
|
@@ -279,7 +279,7 @@ const heldSerials = Index.create({ key: [serial], unique: true, components: { wi
|
|
|
279
279
|
let observedHeld = "";
|
|
280
280
|
/** An observer system reading every held row through the keyless index, and serial 3's through the unique one. */
|
|
281
281
|
const observeHeld = System.create({
|
|
282
|
-
|
|
282
|
+
readonly: true,
|
|
283
283
|
lookup: { all: { index: held }, bySerial: { index: heldSerials } },
|
|
284
284
|
}, ({ lookup }) => {
|
|
285
285
|
const all = lookup.all();
|
|
@@ -72,7 +72,7 @@ const expected = async (world: CreatedWorld, only?: (weighed: number) => boolean
|
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
const markerValues: number[] = [];
|
|
75
|
-
const readMarkers = System.create({
|
|
75
|
+
const readMarkers = System.create({ readonly: true, components: { with: [marker] } }, row => { markerValues.push(row[marker]); });
|
|
76
76
|
|
|
77
77
|
/** Markers' values in the order they were spawned: their archetype's rows', in storage order. */
|
|
78
78
|
const markers = async (world: CreatedWorld): Promise<number[]> => {
|
|
@@ -253,7 +253,8 @@ const keepTicking = System.create({}, () => {});
|
|
|
253
253
|
const watchGrid = System.create({ lookup: { near: { index: grid } } }, ({ lookup }) => { void lookup.near([0, 0]).length; });
|
|
254
254
|
|
|
255
255
|
describe("index lifetime", () => {
|
|
256
|
-
|
|
256
|
+
// Flaky on CI's runners, which runs no flaky test: run locally until it's hardened (`followups.md`, Correctness).
|
|
257
|
+
it.skipIf(process.env["CI"] !== undefined)("builds an index the first time a system uses it and releases it, arena blocks included, once none does", async () => {
|
|
257
258
|
const driver = backToBack();
|
|
258
259
|
await using world = createWorld({ [workers]: 0, [always]: true }, { loop: driver });
|
|
259
260
|
await world.run(spawnCells, { cells: [[0, 0], [1, 1]] });
|
|
@@ -57,16 +57,21 @@ const idle = System.create({}, () => {});
|
|
|
57
57
|
const totalPlus = System.create({}, ({ resources }, input: { readonly plus: number }) => resources[total] + input.plus);
|
|
58
58
|
const described = System.create(({ resources }) => ({ total: resources[total], names: resources[names] }));
|
|
59
59
|
const unpostable = System.create(() => () => "a function");
|
|
60
|
-
const readTotal = System.create({
|
|
60
|
+
const readTotal = System.create({ readonly: true }, ({ resources }) => resources[total]);
|
|
61
61
|
/** Writes levels, an object of joinTestAudio, so it runs on the thread claiming it. */
|
|
62
62
|
const mix = System.create({}, ({ resources }, input: { readonly level: number }) => {
|
|
63
63
|
resources[levels] = [...resources[levels], input.level];
|
|
64
64
|
});
|
|
65
|
+
/** Runs on the thread claiming joinTestAudio, as `mix` does, and despawns by a query, which only the world's thread does. */
|
|
66
|
+
const mixAndClear = System.create({}, ({ resources, commands }) => {
|
|
67
|
+
resources[levels] = [...resources[levels], 0];
|
|
68
|
+
commands.despawnWhere({ with: [marked] });
|
|
69
|
+
});
|
|
65
70
|
/** An observer system recording each total it reads into `into`. */
|
|
66
|
-
const totalsInto = (into: number[]) => System.create({
|
|
71
|
+
const totalsInto = (into: number[]) => System.create({ readonly: true }, ({ resources }) => { into.push(resources[total]); });
|
|
67
72
|
/** An observer system recording how many levels it reads into `into`. */
|
|
68
|
-
const levelsInto = (into: number[]) => System.create({
|
|
69
|
-
const failingRead = System.create({
|
|
73
|
+
const levelsInto = (into: number[]) => System.create({ readonly: true }, ({ resources }) => { into.push(resources[levels].length); });
|
|
74
|
+
const failingRead = System.create({ readonly: true }, () => { throw new Error("joinTest read failure"); });
|
|
70
75
|
|
|
71
76
|
/** A world shared with a thread joined through a channel, and the joined world, once both say it joined. */
|
|
72
77
|
const joined = async (threads: readonly ("main")[] = []): Promise<{ readonly world: CreatedWorld; readonly remote: World; readonly channel: MessageChannel }> => {
|
|
@@ -155,6 +160,12 @@ describe("joining", () => {
|
|
|
155
160
|
await remote.dispose();
|
|
156
161
|
await world.dispose();
|
|
157
162
|
});
|
|
163
|
+
it("rejects a run asking for its changes, which only the world's thread records", async () => {
|
|
164
|
+
const { world, remote } = await joined();
|
|
165
|
+
await expect(remote.run(idle, { changes: true })).rejects.toThrow("a run's changes are recorded only on the world's thread");
|
|
166
|
+
await remote.dispose();
|
|
167
|
+
await world.dispose();
|
|
168
|
+
});
|
|
158
169
|
it("rejects a run that failed, naming its system, and a run of a system without an id", async () => {
|
|
159
170
|
const { world, remote } = await joined();
|
|
160
171
|
await expect(remote.run(fail)).rejects.toThrow(/^System "fail" \(.*join\.test\.ts#fail\) failed: joinTest failure$/);
|
|
@@ -216,6 +227,14 @@ describe("the join signal", () => {
|
|
|
216
227
|
await world.run(mix, { level: 1 });
|
|
217
228
|
expect(await eventually(() => heard.includes(1))).toBe(true);
|
|
218
229
|
});
|
|
230
|
+
it("fails a system on a joined thread that despawns by a query, which runs on the world's thread", async () => {
|
|
231
|
+
await using world = createWorld({ [workers]: 0 });
|
|
232
|
+
const channel = new MessageChannel();
|
|
233
|
+
const shared = world.share(channel.port1);
|
|
234
|
+
await using _remote = await join(channel.port2, { threads: ["joinTestAudio"] });
|
|
235
|
+
await shared;
|
|
236
|
+
await expect(world.run(mixAndClear)).rejects.toThrow("commands.despawnWhere runs on the world's thread");
|
|
237
|
+
});
|
|
219
238
|
it("resolves several joins at once, each with its own claims, and each of their observer systems sees the world's next run", async () => {
|
|
220
239
|
await using world = createWorld({ [workers]: 0 });
|
|
221
240
|
const claims: readonly (readonly ("main" | "joinTestAudio")[])[] = [[], ["main"], ["joinTestAudio"]];
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { asksForChanges, takesInput } from './run-changes.js';
|
|
3
|
+
import type { ChangeSet } from '../../interface/finalizer/body.js';
|
|
4
|
+
import type { Essential } from '../../interface/component/value.js';
|
|
5
|
+
import type { Resource } from '../../interface/resource/resource.js';
|
|
2
6
|
import type { Thread } from '../../interface/thread/thread.js';
|
|
3
7
|
import type { World } from '../../interface/world/world.js';
|
|
4
8
|
import type { Input } from '../../interface/system/input.js';
|
|
5
9
|
import type { System } from '../../interface/system/system.js';
|
|
6
|
-
import { declarationOf, identifiedItems, idOfItem, idOfKey, indexOfLookup, keyOfId, labelOf, requireDefinition, threadOf, threadOfDeclaration, watchedBy, type Definition } from '../../interface/registry/index.js';
|
|
10
|
+
import { declarationOf, definitionOf, identifiedItems, idOfItem, idOfKey, indexOfLookup, keyOfId, labelOf, requireDefinition, threadOf, threadOfDeclaration, watchedBy, type Definition } from '../../interface/registry/index.js';
|
|
7
11
|
import { unsharedKeyOf } from './index-keys.js';
|
|
8
12
|
import { layoutOf, type Views } from '../../interface/registry/layout.js';
|
|
9
13
|
import {
|
|
@@ -33,6 +37,12 @@ const observerLeavesOf = (definition: Definition): readonly Leaf[] | null => {
|
|
|
33
37
|
return schedule.tiers.length === 0 && schedule.observer.length > 0 ? schedule.observer : null;
|
|
34
38
|
};
|
|
35
39
|
|
|
40
|
+
/** Throws for a readonly system reading tables, which only the world's thread gives. */
|
|
41
|
+
const refuseTables = (leaves: readonly Leaf[]): void => {
|
|
42
|
+
const reading = leaves.find(leaf => leaf.declaration.tables !== undefined);
|
|
43
|
+
if (reading !== undefined) throw new Error(`System "${reading.name}" reads tables, which only the world's thread gives: run or add it there.`);
|
|
44
|
+
};
|
|
45
|
+
|
|
36
46
|
/** A run not yet settled: how to settle it, and its rejection once posted. */
|
|
37
47
|
type Run = { readonly resolve: (value: unknown) => void; readonly reject: (error: unknown) => void; message: ToJoined | null; error: unknown; readonly local: Local | null };
|
|
38
48
|
|
|
@@ -659,6 +669,7 @@ export const joinWorld = (port: Parameters<World["share"]>[0], options: { readon
|
|
|
659
669
|
// This thread's own observer systems run here, after the sync point of an empty run of the world's.
|
|
660
670
|
const leaves = observerLeavesOf(definition);
|
|
661
671
|
if (leaves !== null) {
|
|
672
|
+
refuseTables(leaves);
|
|
662
673
|
if (failure !== null) return Promise.reject(failure);
|
|
663
674
|
const locals = leaves.map(localOf);
|
|
664
675
|
if (locals.length === 1 && definition.kind !== "composite") return runNumbered(locals[0].number, locals[0].leaf, undefined, locals[0]);
|
|
@@ -674,13 +685,24 @@ export const joinWorld = (port: Parameters<World["share"]>[0], options: { readon
|
|
|
674
685
|
return runNumbered(numberOf(id, true), definition, input, null);
|
|
675
686
|
};
|
|
676
687
|
|
|
688
|
+
function run<I, R>(system: System<I, R>, ...input: Input<NoInfer<I>>): Promise<R>;
|
|
689
|
+
function run<I, R>(system: System<I, R>, ...input: [...Input<NoInfer<I>>, options: { readonly changes: true }]): Promise<ChangeSet<Essential, Resource.Key>>;
|
|
690
|
+
function run(system: System<unknown, unknown>, ...args: readonly unknown[]): Promise<unknown> {
|
|
691
|
+
const definition = definitionOf(system);
|
|
692
|
+
// A system taking input whose input is all it was given runs with it.
|
|
693
|
+
const asked = args.length > 0 && asksForChanges(args[args.length - 1]) && definition !== undefined && !(args.length < 2 && takesInput(definition));
|
|
694
|
+
if (asked) return Promise.reject(new Error(`System "${system.name}": a run's changes are recorded only on the world's thread.`));
|
|
695
|
+
return runOn(system, args);
|
|
696
|
+
}
|
|
697
|
+
|
|
677
698
|
const world: World = {
|
|
678
699
|
// A run gives what its system's body returned, `R` by the system's type.
|
|
679
|
-
run
|
|
700
|
+
run,
|
|
680
701
|
add: system => {
|
|
681
702
|
const definition = requireDefinition(system);
|
|
682
703
|
const leaves = observerLeavesOf(definition);
|
|
683
|
-
if (leaves === null) throw new Error("
|
|
704
|
+
if (leaves === null) throw new Error(`System "${definition.name}" isn't readonly, and a joined thread adds only readonly systems: declare it readonly: true to read the world from this thread, or add it on the world's thread.`);
|
|
705
|
+
refuseTables(leaves);
|
|
684
706
|
if (added.has(definition) || failure !== null) return;
|
|
685
707
|
const locals = leaves.map(localOf);
|
|
686
708
|
added.set(definition, locals);
|
|
@@ -692,7 +714,7 @@ export const joinWorld = (port: Parameters<World["share"]>[0], options: { readon
|
|
|
692
714
|
},
|
|
693
715
|
remove: system => {
|
|
694
716
|
const definition = requireDefinition(system);
|
|
695
|
-
if (observerLeavesOf(definition) === null) throw new Error("
|
|
717
|
+
if (observerLeavesOf(definition) === null) throw new Error(`System "${definition.name}" isn't readonly, and a joined thread removes only readonly systems: others are removed on the world's thread.`);
|
|
696
718
|
const locals = added.get(definition);
|
|
697
719
|
if (locals === undefined) return;
|
|
698
720
|
added.delete(definition);
|