@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
|
@@ -22,7 +22,7 @@ declare module "@adobe/ecs" {
|
|
|
22
22
|
|
|
23
23
|
/** What the observer system read, were it run: this test stands for the thread, so it never runs. */
|
|
24
24
|
const seen: number[] = [];
|
|
25
|
-
const watch = System.create({
|
|
25
|
+
const watch = System.create({ readonly: true, components: { with: [amount] } }, row => { seen.push(row[amount]); });
|
|
26
26
|
|
|
27
27
|
describe("an observer system on a joined thread", () => {
|
|
28
28
|
it("is waited for, not cancelled, by a sync point after a writer copied the rest of a post the thread claimed partway", async () => {
|
|
@@ -42,7 +42,7 @@ const channel = scopeChannel<FromJoiningWorker, ToJoiningWorker>();
|
|
|
42
42
|
let hosted: ReturnType<typeof createWorld> | null = null;
|
|
43
43
|
/** What the observer system `listen` adds heard. */
|
|
44
44
|
const heard: number[] = [];
|
|
45
|
-
const hear = System.create({
|
|
45
|
+
const hear = System.create({ readonly: true }, ({ resources }) => { heard.push(resources[total]); });
|
|
46
46
|
const messageOf = (error: unknown): string => error instanceof Error ? error.message : String(error);
|
|
47
47
|
/** Ends this thread at once: node's worker exits, and a browser worker closes, dropping the tasks it queued. */
|
|
48
48
|
const exit = (): void => {
|
|
@@ -16,7 +16,7 @@ import { add, drive, idle, names, shelf, total, until } from "./joining.test-hel
|
|
|
16
16
|
/** A system only this thread imports, which a world on a worker never did. */
|
|
17
17
|
const unimportedByWorker = System.create({}, () => {});
|
|
18
18
|
/** An observer system only this thread imports, reading an object of `main`, so it's placed on a thread claiming it. */
|
|
19
|
-
const shelvedHere = System.create({
|
|
19
|
+
const shelvedHere = System.create({ readonly: true }, ({ resources }) => resources[shelf].length);
|
|
20
20
|
|
|
21
21
|
type Worker = {
|
|
22
22
|
readonly ask: (message: ToJoiningWorker, transfer?: readonly unknown[]) => Promise<FromJoiningWorker>;
|
|
@@ -36,7 +36,7 @@ export const append = System.create({}, ({ resources }, input: { readonly name:
|
|
|
36
36
|
return resources[names].length;
|
|
37
37
|
});
|
|
38
38
|
/** Reads `total` from an observer system, which a joined thread runs itself. */
|
|
39
|
-
export const readTotal = System.create({
|
|
39
|
+
export const readTotal = System.create({ readonly: true }, ({ resources }) => resources[total]);
|
|
40
40
|
export const spawn = System.create({}, ({ commands }, input: { readonly x: number }) => {
|
|
41
41
|
commands.spawn([point, label, marked])({ [point]: [input.x, -input.x], [label]: `p${input.x}` });
|
|
42
42
|
});
|
|
@@ -63,7 +63,7 @@ const countAdded = System.create({ components: { added: [value], with: [made] }
|
|
|
63
63
|
|
|
64
64
|
/** Each made entity's value, in storage order. */
|
|
65
65
|
const madeValues: number[] = [];
|
|
66
|
-
const readMade = System.create({
|
|
66
|
+
const readMade = System.create({ readonly: true, components: { with: [value, made] } }, row => { madeValues.push(row[value]); });
|
|
67
67
|
const valuesOf = async (world: ReturnType<typeof createWorld>): Promise<number[]> => {
|
|
68
68
|
madeValues.length = 0;
|
|
69
69
|
await world.run(readMade);
|
|
@@ -43,8 +43,7 @@ const failOnce = System.create(() => { throw new Error("unnamed"); });
|
|
|
43
43
|
|
|
44
44
|
describe("systems, rules and finalizers of one label", () => {
|
|
45
45
|
it("are distinct in schedules, both run, and are told apart in reports by their ids", async () => {
|
|
46
|
-
|
|
47
|
-
const linked = (after: readonly System[], body: (row: { [value]: number }) => void): System => System.create({ components: { with: [value] }, after }, row => body(row));
|
|
46
|
+
const linked = (after: readonly System[], body: (row: { [value]: number }) => void): System => System.create({ components: { with: [value], whole: true }, after }, row => body(row));
|
|
48
47
|
const a = linked([], row => { ran.push("a"); row[value] += 1; });
|
|
49
48
|
const b = linked([a], row => { ran.push("b"); row[value] += 1; throw new Error("b failed"); });
|
|
50
49
|
const c = linked([b], row => { ran.push(`c${row[value]}`); });
|
|
@@ -37,7 +37,7 @@ const nothing = System.create({}, () => {});
|
|
|
37
37
|
|
|
38
38
|
/** Counts passes: the observer passes after each run's sync point. */
|
|
39
39
|
const passes = { count: 0 };
|
|
40
|
-
const counter = System.create({
|
|
40
|
+
const counter = System.create({ readonly: true }, () => { passes.count++; });
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* A world of `count` entities, each amount its index and one more, at level 1: the runs below registered, so runners
|
|
@@ -184,7 +184,7 @@ describe("observer factories", () => {
|
|
|
184
184
|
it("rethrow only their own systems' failures from their runs, not those of observer systems the world added", async () => {
|
|
185
185
|
using reported = catchOutOfBand();
|
|
186
186
|
await using world = await seeded(3);
|
|
187
|
-
const failing = System.create({
|
|
187
|
+
const failing = System.create({ readonly: true }, () => { throw new Error("added failed"); });
|
|
188
188
|
world.add(failing);
|
|
189
189
|
const caught: unknown[] = [];
|
|
190
190
|
const original = globalThis.queueMicrotask;
|
|
@@ -29,7 +29,7 @@ const count = System.create({}, ({ resources }) => { resources[frames] += 1; });
|
|
|
29
29
|
const watch = System.create({}, ({ resources }) => { resources[seen] = resources[clock].tick; });
|
|
30
30
|
const fail = System.create({}, () => { throw new Error("tick failed"); });
|
|
31
31
|
const nothing = System.create({}, () => {});
|
|
32
|
-
const idle = System.create({
|
|
32
|
+
const idle = System.create({ readonly: true }, () => {});
|
|
33
33
|
const fast = System.create({}, ({ resources }) => {
|
|
34
34
|
resources[cadence] = { kind: "fixed", step: 1 };
|
|
35
35
|
resources[always] = true;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { System } from "../../interface/system/system.js";
|
|
3
3
|
import type { World } from "../../interface/world/world.js";
|
|
4
4
|
|
|
5
|
-
const settled = System.create({
|
|
5
|
+
const settled = System.create({ readonly: true }, () => {});
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Resolves once every observer factory subscribed before it had its first value: after the factories' flush, which the
|
|
@@ -24,7 +24,7 @@ export type FromContainedWorker =
|
|
|
24
24
|
|
|
25
25
|
const channel = scopeChannel<FromContainedWorker, ToContainedWorker>();
|
|
26
26
|
let remote: World | null = null;
|
|
27
|
-
const placed = System.create({
|
|
27
|
+
const placed = System.create({ readonly: true }, () => {});
|
|
28
28
|
|
|
29
29
|
channel.onMessage(async message => {
|
|
30
30
|
if ("protocol" in message) return;
|
|
@@ -32,7 +32,7 @@ export const setHolding = System.create({}, ({ resources }, input: { readonly ho
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
/** After each sync point that changed the tally: notes it, and, while `holding` says so, waits there to be released. */
|
|
35
|
-
export const watchTally = System.create({
|
|
35
|
+
export const watchTally = System.create({ readonly: true, resources: { changed: [tally] } }, ({ resources }) => {
|
|
36
36
|
const value = resources[tally];
|
|
37
37
|
tallies.push(value);
|
|
38
38
|
const { words } = gate;
|
|
@@ -29,10 +29,10 @@ const pause = System.create({}, ({ resources }, input: { readonly paused: boolea
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
const seen: { wide: number[][]; steps: number[] } = { wide: [], steps: [] };
|
|
32
|
-
const watchWide = System.create({
|
|
32
|
+
const watchWide = System.create({ readonly: true, resources: { changed: [wide] } }, ({ resources }) => {
|
|
33
33
|
seen.wide.push([...resources[wide]]);
|
|
34
34
|
});
|
|
35
|
-
const watchSteps = System.create({
|
|
35
|
+
const watchSteps = System.create({ readonly: true, resources: { not: [paused], changed: [steps] } }, ({ resources }) => {
|
|
36
36
|
seen.steps.push(resources[steps]);
|
|
37
37
|
});
|
|
38
38
|
|
|
@@ -103,7 +103,7 @@ export type Seen = { counter: number; rows: number; wrong: number };
|
|
|
103
103
|
export const seen: Seen[] = [];
|
|
104
104
|
|
|
105
105
|
/** Reads every row, which must all be of one tick, pausing at the end of its first chunk while `stall` says so. */
|
|
106
|
-
export const watchRows = System.create({
|
|
106
|
+
export const watchRows = System.create({ readonly: true, components: { with: [stamp, spot, word] } }, (row, { resources }) => {
|
|
107
107
|
const value = resources[counter];
|
|
108
108
|
let last = seen.at(-1);
|
|
109
109
|
if (last === undefined || last.counter !== value || last.rows === 1000) seen.push(last = { counter: value, rows: 0, wrong: 0 });
|
|
@@ -117,7 +117,7 @@ export const watchRows = System.create({ observer: true, components: { with: [st
|
|
|
117
117
|
export const looked: Entity[] = [];
|
|
118
118
|
|
|
119
119
|
/** Looks up every row by entity, which must all be of one tick, spinning halfway while `stall` says so. */
|
|
120
|
-
export const lookRows = System.create({
|
|
120
|
+
export const lookRows = System.create({ readonly: true, lookup: { found: { with: [stamp, spot] } } }, ({ resources, lookup }) => {
|
|
121
121
|
const value = resources[counter];
|
|
122
122
|
const run: Seen = { counter: value, rows: 0, wrong: 0 };
|
|
123
123
|
seen.push(run);
|
|
@@ -146,7 +146,7 @@ export const nextPhase = System.create({}, ({ resources }) => {
|
|
|
146
146
|
export const hot: number[] = [];
|
|
147
147
|
export const byHeat = Index.create({ key: [heat], sorted: true });
|
|
148
148
|
/** Counts the rows at least 150 hot, through the sorted index on what the GPU kernel writes. */
|
|
149
|
-
export const countHot = System.create({
|
|
149
|
+
export const countHot = System.create({ readonly: true, lookup: { hot: { index: byHeat } } }, ({ lookup }) => {
|
|
150
150
|
hot.push(lookup.hot({ ">=": 150 }).length);
|
|
151
151
|
});
|
|
152
152
|
|
|
@@ -164,7 +164,7 @@ export const mottos: { readonly round: number; readonly motto: string }[] = [];
|
|
|
164
164
|
* Reads only resources, a string among them: waits on the gate first, while `stall` says so, its round plus one, then
|
|
165
165
|
* reads the motto, which must be its round's.
|
|
166
166
|
*/
|
|
167
|
-
export const watchMotto = System.create({
|
|
167
|
+
export const watchMotto = System.create({ readonly: true }, ({ resources }) => {
|
|
168
168
|
const round = resources[mottoRound];
|
|
169
169
|
pauseAt(resources[stall], round + 1);
|
|
170
170
|
mottos.push({ round, motto: resources[motto] });
|
|
@@ -59,43 +59,43 @@ const reset = (): void => {
|
|
|
59
59
|
/** The entities the tests look up, by the joined thread's systems. */
|
|
60
60
|
const watched: Entity[] = [];
|
|
61
61
|
|
|
62
|
-
const watchLevel = System.create({
|
|
62
|
+
const watchLevel = System.create({ readonly: true, resources: { changed: [level] } }, ({ resources }) => {
|
|
63
63
|
seen.calls++;
|
|
64
64
|
seen.levels.push(resources[level]);
|
|
65
65
|
});
|
|
66
|
-
const watchAmounts = System.create({
|
|
66
|
+
const watchAmounts = System.create({ readonly: true, components: { with: [amount], changed: [amount] } }, row => {
|
|
67
67
|
(seen.amounts[seen.amounts.length - 1] ??= []).push(row[amount]);
|
|
68
68
|
});
|
|
69
|
-
const startAmounts = System.create({
|
|
69
|
+
const startAmounts = System.create({ readonly: true }, () => { seen.amounts.push([]); });
|
|
70
70
|
const amountsPass = System.create([startAmounts, watchAmounts]);
|
|
71
|
-
const lookChanged = System.create({
|
|
71
|
+
const lookChanged = System.create({ readonly: true, lookup: { found: { with: [amount], changed: [amount] } } }, ({ lookup }) => {
|
|
72
72
|
seen.looked.push(watched.flatMap(entity => {
|
|
73
73
|
const row = lookup.found(entity);
|
|
74
74
|
return row === null ? [] : [row[amount], row.changed];
|
|
75
75
|
}));
|
|
76
76
|
});
|
|
77
|
-
const countTagged = System.create({
|
|
77
|
+
const countTagged = System.create({ readonly: true, lookup: { all: { index: allTagged } } }, ({ lookup }) => {
|
|
78
78
|
seen.tagged.push(lookup.all().length);
|
|
79
79
|
});
|
|
80
|
-
const inCell = System.create({
|
|
80
|
+
const inCell = System.create({ readonly: true, lookup: { found: { index: byCell } } }, ({ lookup }) => {
|
|
81
81
|
const found = lookup.found(11);
|
|
82
82
|
seen.tagged.push(found.length === 0 ? 0 : found.length * 100 + (found.at(0)?.[amount] ?? -1));
|
|
83
83
|
});
|
|
84
|
-
const failing = System.create({
|
|
84
|
+
const failing = System.create({ readonly: true }, ({ resources }) => {
|
|
85
85
|
if (resources[level] > 1) throw new Error("observer joined failure");
|
|
86
86
|
});
|
|
87
|
-
const readLabels = System.create({
|
|
87
|
+
const readLabels = System.create({ readonly: true, components: { with: [label, amount] } }, row => {
|
|
88
88
|
seen.labels.push(`${row[label].text}${row[amount]}`);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
/** On the joined thread, as it reads labels: each watched entity's label and amount, which the world's run resolves to. */
|
|
92
|
-
const labelsThere = System.create({
|
|
92
|
+
const labelsThere = System.create({ readonly: true, lookup: { labelled: { with: [label, amount] } } }, ({ lookup }) =>
|
|
93
93
|
watched.map(entity => {
|
|
94
94
|
const row = lookup.labelled(entity);
|
|
95
95
|
return row === null ? "none" : `${row[label].text}${row[amount]}`;
|
|
96
96
|
}));
|
|
97
97
|
/** On the joined thread, as it reads labels: throws there. */
|
|
98
|
-
const failsThere = System.create({
|
|
98
|
+
const failsThere = System.create({ readonly: true, lookup: { labelled: { with: [label] } } }, ({ lookup }) => {
|
|
99
99
|
if (lookup.labelled(Entity.none) === null) throw new Error("observerJoinedTest failure there");
|
|
100
100
|
return 0;
|
|
101
101
|
});
|
|
@@ -218,7 +218,7 @@ describe("observer systems on a joined thread", () => {
|
|
|
218
218
|
Index.create({ key: [cell], sorted: true, components: { with: [amount] } }),
|
|
219
219
|
];
|
|
220
220
|
const [madeTagged, madeByCell] = made();
|
|
221
|
-
const sizes = System.create({
|
|
221
|
+
const sizes = System.create({ readonly: true, lookup: { all: { index: madeTagged, changed: [cell] }, cells: { index: madeByCell } } }, ({ lookup }) => {
|
|
222
222
|
const all = lookup.all();
|
|
223
223
|
seen.looked.push([all.changed]);
|
|
224
224
|
const high = lookup.cells({ ">=": 5 });
|
|
@@ -284,10 +284,18 @@ describe("observer systems on a joined thread", () => {
|
|
|
284
284
|
await close();
|
|
285
285
|
});
|
|
286
286
|
|
|
287
|
-
it("refuse only systems that aren't
|
|
287
|
+
it("refuse only systems that aren't readonly, saying to declare them so", async () => {
|
|
288
288
|
const { remote, close } = await joined();
|
|
289
|
-
expect(() => remote.add(raise)).toThrow("
|
|
290
|
-
expect(() => remote.remove(raise)).toThrow("
|
|
289
|
+
expect(() => remote.add(raise)).toThrow(`System "raise" 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.`);
|
|
290
|
+
expect(() => remote.remove(raise)).toThrow(`System "raise" isn't readonly, and a joined thread removes only readonly systems: others are removed on the world's thread.`);
|
|
291
|
+
await close();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("refuse a readonly system reading tables, which only the world's thread gives", async () => {
|
|
295
|
+
const { remote, close } = await joined();
|
|
296
|
+
const readTables = System.create({ readonly: true, tables: { components: [label] } }, ({ tables }) => tables.archetypes.length);
|
|
297
|
+
expect(() => remote.add(readTables)).toThrow(`System "readTables" reads tables, which only the world's thread gives: run or add it there.`);
|
|
298
|
+
await expect(remote.run(readTables)).rejects.toThrow(`System "readTables" reads tables, which only the world's thread gives: run or add it there.`);
|
|
291
299
|
await close();
|
|
292
300
|
});
|
|
293
301
|
|
|
@@ -309,7 +317,7 @@ describe("observer systems on a joined thread", () => {
|
|
|
309
317
|
const { world, close } = await joined(["observerJoinedTest"]);
|
|
310
318
|
await world.run(spawnLabelled);
|
|
311
319
|
// Made in a function, so the build gives it no id.
|
|
312
|
-
const made = () => System.create({
|
|
320
|
+
const made = () => System.create({ readonly: true, components: { with: [label, amount] } }, row => {
|
|
313
321
|
seen.labels.push(`${row[label].text}${row[amount]}`);
|
|
314
322
|
});
|
|
315
323
|
const madeLabels = made();
|
|
@@ -27,11 +27,11 @@ const warm = System.create({ kernel: "gpu", components: { with: [rate, heat] } }
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
let total = 0;
|
|
30
|
-
const sum = System.create({
|
|
30
|
+
const sum = System.create({ readonly: true, components: { with: [heat] } }, row => { total += row[heat]; });
|
|
31
31
|
|
|
32
32
|
const byHeat = Index.create({ key: [heat], sorted: true });
|
|
33
33
|
let hot = 0;
|
|
34
|
-
const countHot = System.create({
|
|
34
|
+
const countHot = System.create({ readonly: true, lookup: { hot: { index: byHeat } } }, ({ lookup }) => {
|
|
35
35
|
hot = lookup.hot({ ">=": 150 }).length;
|
|
36
36
|
});
|
|
37
37
|
|
|
@@ -37,33 +37,33 @@ const grow = System.create({ components: { with: [amount] } }, row => { row[amou
|
|
|
37
37
|
const nothing = System.create({}, () => {});
|
|
38
38
|
|
|
39
39
|
// It keeps its context, which the build can't follow, so it's given what it reads.
|
|
40
|
-
const watchLevel = System.create(giveAccess({
|
|
40
|
+
const watchLevel = System.create(giveAccess({ readonly: true }, { resources: { read: [level] } }), context => {
|
|
41
41
|
seen.levels.push(context.resources[level]);
|
|
42
42
|
seen.contexts.push(context);
|
|
43
43
|
});
|
|
44
|
-
const watchAmounts = System.create({
|
|
45
|
-
const count = System.create({
|
|
46
|
-
const inCell = System.create({
|
|
44
|
+
const watchAmounts = System.create({ readonly: true, components: { with: [amount], changed: [amount] } }, row => { seen.amounts.push(row[amount]); });
|
|
45
|
+
const count = System.create({ readonly: true, components: { with: [amount] } }, () => { seen.counted.push(1); });
|
|
46
|
+
const inCell = System.create({ readonly: true, lookup: { found: { index: byCell } } }, ({ lookup }) => {
|
|
47
47
|
seen.cells.push(lookup.found(7).length);
|
|
48
48
|
});
|
|
49
|
-
const failing = System.create({
|
|
49
|
+
const failing = System.create({ readonly: true }, ({ resources }) => {
|
|
50
50
|
if (resources[level] > 0) throw new Error("observer failed");
|
|
51
51
|
});
|
|
52
52
|
const raiseThenWatch = System.create([raise, watchLevel]);
|
|
53
|
-
const watchLevelChanges = System.create({
|
|
53
|
+
const watchLevelChanges = System.create({ readonly: true, resources: { changed: [level] } }, ({ resources }) => {
|
|
54
54
|
seen.levels.push(resources[level]);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
/** The world a removing body acts on. */
|
|
58
58
|
let current: World | null = null;
|
|
59
|
-
const removeLevel = System.create({
|
|
59
|
+
const removeLevel = System.create({ readonly: true }, () => {
|
|
60
60
|
seen.removals.push(1);
|
|
61
61
|
current?.remove(watchLevel);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
/** Adds `watchLevel` to the world the test runs, during a run. */
|
|
65
65
|
const addWatch = System.create({}, () => { current?.add(watchLevel); });
|
|
66
|
-
const addLevel = System.create({
|
|
66
|
+
const addLevel = System.create({ readonly: true }, () => {
|
|
67
67
|
current?.add(watchLevel);
|
|
68
68
|
});
|
|
69
69
|
|
|
@@ -30,7 +30,7 @@ const assign = System.create({ kernel: true, components: { with: [position], opt
|
|
|
30
30
|
row[velocity] = row[position];
|
|
31
31
|
});
|
|
32
32
|
const totals = { moved: 0, still: 0 };
|
|
33
|
-
const sum = System.create({
|
|
33
|
+
const sum = System.create({ readonly: true, components: { with: [position], optional: [velocity] } }, row => {
|
|
34
34
|
if (row[velocity] === undefined) totals.still += row[position];
|
|
35
35
|
else totals.moved += row[position];
|
|
36
36
|
});
|
|
@@ -73,7 +73,7 @@ const copySpeedKernel = System.create({
|
|
|
73
73
|
if (found !== null) row[order] = found[velocity] === undefined ? -found[position] : found[velocity];
|
|
74
74
|
});
|
|
75
75
|
const speeds: number[] = [];
|
|
76
|
-
const readSpeeds = System.create({
|
|
76
|
+
const readSpeeds = System.create({ readonly: true, components: { with: [other, order] } }, row => { speeds.push(row[order]); });
|
|
77
77
|
|
|
78
78
|
/** What finalizers read of rows that stopped, on the world's thread and on the markers'. */
|
|
79
79
|
const stopped: string[] = [];
|
|
@@ -87,7 +87,7 @@ export const finalizeThere = Finalizer.create({ components: { with: [position, m
|
|
|
87
87
|
const despawnAll = System.create({ components: { with: [position] } }, (row, { commands }) => { commands.despawn(row.entity); });
|
|
88
88
|
|
|
89
89
|
let seen: (readonly [number, number | undefined, string | undefined])[] = [];
|
|
90
|
-
const read = System.create({
|
|
90
|
+
const read = System.create({ readonly: true, components: { with: [position], optional: [velocity, label] } }, row => {
|
|
91
91
|
seen.push([row[position], row[velocity], row[label]]);
|
|
92
92
|
});
|
|
93
93
|
const sorted = () => [...seen].sort((a, b) => a[0] - b[0]);
|
|
@@ -109,7 +109,7 @@ const lookedThere = System.create({ components: { with: [position, marker] }, lo
|
|
|
109
109
|
});
|
|
110
110
|
// An observer system there reads copies of rows across archetypes, which tell which have the optional component.
|
|
111
111
|
let watched: (readonly [number, number | undefined])[] = [];
|
|
112
|
-
const watchThere = System.create({
|
|
112
|
+
const watchThere = System.create({ readonly: true, components: { with: [position], optional: [velocity] } }, row => {
|
|
113
113
|
watched.push([row[position], row[velocity]]);
|
|
114
114
|
});
|
|
115
115
|
// A tail there: it writes only its thread's objects, so it runs after the sync point, reading copies of rows.
|
|
@@ -165,7 +165,7 @@ const markEither = System.create({ kernel: true, components: { with: [mark], opt
|
|
|
165
165
|
if (row[far] !== undefined) row[mark] = 2;
|
|
166
166
|
});
|
|
167
167
|
const marked: number[] = [];
|
|
168
|
-
const watchStamped = System.create({
|
|
168
|
+
const watchStamped = System.create({ readonly: true, components: { with: [mark], changed: [mark] } }, row => { marked.push(row[mark]); });
|
|
169
169
|
const clearNear = System.create({ kernel: false, components: { with: [mark], optional: [near] } }, row => {
|
|
170
170
|
if (row[near] !== undefined) row[near] = undefined;
|
|
171
171
|
});
|
|
@@ -195,7 +195,7 @@ describe("optional components", () => {
|
|
|
195
195
|
|
|
196
196
|
it("pass change filters on other components, and are stamped changed when written", async () => {
|
|
197
197
|
const changed: number[] = [];
|
|
198
|
-
const watch = System.create({
|
|
198
|
+
const watch = System.create({ readonly: true, components: { with: [velocity], changed: [velocity] } }, row => { changed.push(row[velocity]); });
|
|
199
199
|
const speedUp = System.create({ components: { with: [position], optional: [velocity] } }, row => {
|
|
200
200
|
const speed = row[velocity];
|
|
201
201
|
if (speed !== undefined && row[position] > 50) row[velocity] = speed + 1;
|
|
@@ -212,7 +212,7 @@ describe("optional components", () => {
|
|
|
212
212
|
it("run in an index's order across archetypes that have them and don't", async () => {
|
|
213
213
|
const byOrder = Index.create({ key: [order], sorted: true });
|
|
214
214
|
const ordered: (number | undefined)[] = [];
|
|
215
|
-
const list = System.create({
|
|
215
|
+
const list = System.create({ readonly: true, components: { with: [position], optional: [velocity] }, order: byOrder }, row => {
|
|
216
216
|
ordered.push(row[velocity]);
|
|
217
217
|
});
|
|
218
218
|
await using world = inline();
|
|
@@ -297,7 +297,7 @@ describe("optional components", () => {
|
|
|
297
297
|
await world.run(spawnMarked);
|
|
298
298
|
stoppedThere.length = 0;
|
|
299
299
|
await world.run(despawnAll);
|
|
300
|
-
await remote.run(System.create({
|
|
300
|
+
await remote.run(System.create({ readonly: true }, () => {}));
|
|
301
301
|
expect(stoppedThere.sort()).toEqual(["10:5:null", "1:none:null"]);
|
|
302
302
|
await remote.dispose();
|
|
303
303
|
await world.dispose();
|
|
@@ -220,7 +220,7 @@ Shared components and `where`, against `12e366d`, node: the whole suite three ti
|
|
|
220
220
|
- **A shared value read in a row costs what a column's does:** `shared read 100K` 4.04–4.10 ns a row against 4.24–4.27 for `column read 100K`, the value bound with the row once per archetype. In a kernel it's a parameter of each call: `shared read 100K kernel` 0.40–0.41 against 0.50 for `column read 100K kernel`, one column fewer to stream.
|
|
221
221
|
- **Spawning with a shared constant:** `spawn bulk shared`, one spawner reused, 17.3–18.1 ns a spawn against 33.3–34.3 for `spawn bulk plain`, whose cell is a column; `spawn singles shared`, its spawner found by its constants each call, 313–323 ns against 229 for `spawn singles plain`: about 90 ns a call to find the spawner, a tuple's leaves walked through a tree of its stored leaves (`shared.ts`) and each constant in turn through the shape's tree of spawners, with no text built.
|
|
222
222
|
- **`where` over 1K cells,** 3 × 3 of them in view: `where 1K cells still` 0.49 ns a cell, 0.5 µs a run with nothing evaluated; `where 1K cells moving`, every predicate evaluated again as the camera moves each run, 18.7–19.0 ns a cell, each predicate's resources read once per evaluation.
|
|
223
|
-
- **Many small archetypes:** `small archetypes 1K`, 10K rows over 1K archetypes of one cell each,
|
|
223
|
+
- **Many small archetypes:** `small archetypes 1K`, 10K rows over 1K archetypes of one cell each, 6.8–7.0 ns a row against 3.04 for `one archetype 10K`: about 38 ns an archetype, binding its row, which a shared component's values multiply.
|
|
224
224
|
|
|
225
225
|
Review fixes to shared components and `where`, node, alternating against the commit before each, medians of four to six rounds:
|
|
226
226
|
- **Arrivals precise** (`arrivals.ts`): a system with `where` and change filters reads a ring of the moves since its last run, each archetype moved into owing the point of the one its rows came from, where every arrival made the archetype count from the oldest point of all matches, found by a scan of them. New `where changed 1K cells drifting in view`, every cell in view, 1% of 10K rows moving to the next cell before each run: 123.6 → 69.2 ns a cell; `where changed 1K cells drifting`, 3 × 3 cells in view, 2.0 → 2.8 ns a cell, the ring's 102 entries a run read, about 7 ns each, and cells moved into from culled ones scanned since that one's point; `changed 1K cells drifting`, without `where`, 52.3 → 52.0. `where 1K cells still` and `moving` within noise.
|
|
@@ -241,6 +241,8 @@ Step figures marked (vitest) are from the node vitest lane: read them as relativ
|
|
|
241
241
|
kernel, the second call stayed a call and boxed each entity, a double past 2^32. `despawn bulk kernel` 4.60 → 4.12 ms;
|
|
242
242
|
`despawn bulk` compiled 4.55 ms against 4.32 as written before, 4.10 after.
|
|
243
243
|
- Inline value writes instead of closures, an 8-shape spawn cache, edges indexed by component id, archetypes grown once per sync point: create 38 → 27.6 ms.
|
|
244
|
+
- Holders of references spawned in a sync point are linked a block at a time before the cascade, from their columns, rather than listed as triples for the cascade and for the holder index each; and `commands.spawnTables` lists owners between its rows only when one is dropped, and reserves a table's entities at once when every row spawns: `spawn tables 1M owner` 160 → 53 ms, `spawn owned 1M` 133 → 64 ms, against 10 ms for `spawn tables 1M`. Each holder is journaled once, so an `up` tree builds again past 4096 journal entries and an eighth of its rows, as indexes do: `up load` 183 → 120 ms.
|
|
245
|
+
- A row loop reads the body into a local once, which V8 otherwise loads again each row, since the call could change it; rows bind without allocating, and filters are found by archetype id: `with 100K` 4.24 → 3.97 ns a row, `one archetype 10K` 3.47 → 3.04, `small archetypes 1K` 7.74 → 6.90, `shared read 100K` 4.07 → 3.78.
|
|
244
246
|
|
|
245
247
|
**Change filters.**
|
|
246
248
|
- Chunk ticks, the newest tick per 1024 rows, so filters skip unchanged chunks: clustered sparse to ~0.09 ms.
|
|
@@ -42,7 +42,7 @@ const caught: number[] = [];
|
|
|
42
42
|
export const catchUp = System.create({ kernel: false, components: { with: [position], changed: [position], where: { [cell]: (at, { resources }) => at[0] === resources[camera] } } }, row => { caught.push(row[position]); });
|
|
43
43
|
// On a joined thread.
|
|
44
44
|
const heard: number[] = [];
|
|
45
|
-
export const hear = System.create({
|
|
45
|
+
export const hear = System.create({ readonly: true, components: { with: [cell, position], where: { [cell]: at => at[0] === 50 } } }, row => { heard.push(row[cell][0] * 1000 + row[position]); });
|
|
46
46
|
|
|
47
47
|
/** Moves an entity to cell `[x, 0]`. */
|
|
48
48
|
const step = System.create({}, ({ commands }, input: { readonly entity: Entity; readonly x: number }) => { commands.add(input.entity, cell, [input.x, 0]); });
|
|
@@ -59,7 +59,7 @@ const spawnAt = (world: ReturnType<typeof inline>, x: number, components: readon
|
|
|
59
59
|
/** Every row's cell and position, by position. */
|
|
60
60
|
const cellsOf = async (world: ReturnType<typeof inline>): Promise<readonly string[]> => {
|
|
61
61
|
const found: [number, string][] = [];
|
|
62
|
-
await world.run(System.create({
|
|
62
|
+
await world.run(System.create({ readonly: true, components: { with: [cell, position] } }, row => { found.push([row[position], `${row[cell][0]}:${row[position]}`]); }));
|
|
63
63
|
return found.sort((a, b) => a[0] - b[0]).map(([, text]) => text);
|
|
64
64
|
};
|
|
65
65
|
|
|
@@ -132,7 +132,7 @@ describe("reclaiming archetypes of shared values", () => {
|
|
|
132
132
|
found.length = 0;
|
|
133
133
|
await world.run(System.create({ kernel: false, components: { with: [cell, position] } }, row => { found.push(`body ${row[cell][0]}`); }));
|
|
134
134
|
await world.run(copyCell);
|
|
135
|
-
await world.run(System.create({
|
|
135
|
+
await world.run(System.create({ readonly: true, components: { with: [cell, seen] } }, row => { found.push(`kernel ${row[seen]}`); }));
|
|
136
136
|
await world.run(atFifty);
|
|
137
137
|
await world.run(atLast);
|
|
138
138
|
expect(found.sort()).toEqual(["body 50", `body ${emptyFor + 2}`, "kernel 50", `kernel ${emptyFor + 2}`, "value 0", "where 100"].sort());
|
|
@@ -132,7 +132,7 @@ const observerTargetOf = (state: State, leaf: Leaf): ObserverTarget | undefined
|
|
|
132
132
|
|
|
133
133
|
/** A runner for a leaf where it runs now: on the joined thread filling its thread, or that added it, or here. */
|
|
134
134
|
const placedRunner = (state: State, leaf: Leaf, since = 0): Runner => {
|
|
135
|
-
if (leaf.declaration.
|
|
135
|
+
if (leaf.declaration.readonly === true) {
|
|
136
136
|
const target = observerTargetOf(state, leaf);
|
|
137
137
|
return target === undefined ? createRunner(state, leaf, since) : createJoinedObserverRunner(state, leaf, target, since);
|
|
138
138
|
}
|
|
@@ -394,7 +394,7 @@ export const createRegistrar = (state: State): Registrar => {
|
|
|
394
394
|
|
|
395
395
|
const place = (): void => {
|
|
396
396
|
eachRunner((leaf, old, entry) => {
|
|
397
|
-
if (leaf.declaration.
|
|
397
|
+
if (leaf.declaration.readonly === true) {
|
|
398
398
|
if (observerTargetOf(state, leaf) === old.joined?.target) return;
|
|
399
399
|
replace(leaf, entry, old, placedRunner(state, leaf, old.lastRun()));
|
|
400
400
|
return;
|
|
@@ -72,7 +72,7 @@ export type Observers = "factory" | "observer";
|
|
|
72
72
|
const observer: { entity: Entity | null; seen: ((odd: boolean, held: boolean, text: string | null | undefined) => void) | null } = { entity: null, seen: null };
|
|
73
73
|
|
|
74
74
|
/** After each sync point, on the joined thread: the watched entity's phase and caption, and whether it's flagged. */
|
|
75
|
-
export const observerWatch = System.create({
|
|
75
|
+
export const observerWatch = System.create({ readonly: true, lookup: { values: { with: [phase, caption] }, flagged: { with: [flag] } } }, ({ lookup }) => {
|
|
76
76
|
if (observer.entity === null) return;
|
|
77
77
|
const row = lookup.values(observer.entity);
|
|
78
78
|
if (row === null) return;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import { Finalizer, System } from "../../index.js";
|
|
4
|
+
import { Observe } from "../../observe/index.js";
|
|
5
|
+
import { createWorld } from "../index.js";
|
|
6
|
+
import { sameValue } from "./resource-changes.js";
|
|
7
|
+
|
|
8
|
+
const size = Symbol("resourceChanges/size");
|
|
9
|
+
const corner = Symbol("resourceChanges/corner");
|
|
10
|
+
const unwatched = Symbol("resourceChanges/unwatched");
|
|
11
|
+
const log = Symbol("resourceChanges/log");
|
|
12
|
+
const label = Symbol("resourceChanges/label");
|
|
13
|
+
|
|
14
|
+
declare module "@adobe/ecs" {
|
|
15
|
+
interface Components {
|
|
16
|
+
[label]: { type: string };
|
|
17
|
+
}
|
|
18
|
+
interface Resources {
|
|
19
|
+
[size]: { type: number; default: 1 };
|
|
20
|
+
[corner]: { type: readonly [number, number] };
|
|
21
|
+
[unwatched]: { type: number };
|
|
22
|
+
[log]: { type: readonly string[]; default: [] };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Watching only resources, it sees no component's changes.
|
|
27
|
+
Finalizer.changes({ watch: [size, corner] }, (changes, { resources }) => {
|
|
28
|
+
const lines = changes.resources.map(change => change.key === size ? `size ${change.previous} → ${change.next}` : `corner ${change.previous.join(",")} → ${change.next.join(",")}`);
|
|
29
|
+
resources[log] = [...resources[log], ...changes.groups.map(() => "group"), ...lines];
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const resize = System.create({}, ({ resources }, input: { readonly to: number }) => {
|
|
33
|
+
resources[size] = input.to;
|
|
34
|
+
resources[size] = input.to + 1;
|
|
35
|
+
});
|
|
36
|
+
const move = System.create({}, ({ resources }, input: { readonly x: number }) => { resources[corner] = [input.x, 0]; });
|
|
37
|
+
const touch = System.create({}, ({ resources }) => { resources[unwatched] += 1; });
|
|
38
|
+
const spawn = System.create({}, ({ commands }) => { commands.spawn([label])({ [label]: "a" }); });
|
|
39
|
+
const read = System.create({ readonly: true }, ({ resources }) => resources[log]);
|
|
40
|
+
|
|
41
|
+
it("reports each watched resource written since its finalizers last ran, with its first and last value, once", async () => {
|
|
42
|
+
await using world = createWorld();
|
|
43
|
+
// A run writing nothing else still has a sync point, so its finalizers see the write.
|
|
44
|
+
await world.run(resize, { to: 4 });
|
|
45
|
+
expect(await world.run(read)).toEqual(["size 1 → 5"]);
|
|
46
|
+
await world.run(spawn);
|
|
47
|
+
await world.run(touch);
|
|
48
|
+
expect(await world.run(read)).toEqual(["size 1 → 5"]);
|
|
49
|
+
await world.run(move, { x: 2 });
|
|
50
|
+
// Written back to an equal value, leaf by leaf: not reported.
|
|
51
|
+
await world.run(move, { x: 2 });
|
|
52
|
+
await world.run(resize, { to: 4 });
|
|
53
|
+
expect(await world.run(read)).toEqual(["size 1 → 5", "corner 0,0 → 2,0"]);
|
|
54
|
+
expect(await Observe.toPromise(Observe.resource(world, size))).toBe(5);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("compares data leaf by leaf and other objects by identity", () => {
|
|
58
|
+
const map = new Map();
|
|
59
|
+
expect([sameValue([1, { a: -0 }], [1, { a: -0 }]), sameValue({ a: 1 }, { a: 1, b: 2 }), sameValue(0, -0), sameValue(NaN, NaN), sameValue(map, new Map()), sameValue(map, map)])
|
|
60
|
+
.toEqual([true, false, false, true, false, true]);
|
|
61
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { readCell } from './cells.js';
|
|
3
|
+
import { indexOf } from './entities.js';
|
|
4
|
+
import type { Storage } from './archetype.js';
|
|
5
|
+
import type { State } from './state.js';
|
|
6
|
+
|
|
7
|
+
/** A watched resource written to a value that isn't equal to the one its finalizers last saw. */
|
|
8
|
+
export type ResourceChange = { readonly id: number; readonly previous: unknown; readonly next: unknown };
|
|
9
|
+
|
|
10
|
+
const noChanges: readonly ResourceChange[] = [];
|
|
11
|
+
|
|
12
|
+
/** Where resource `id` lives now, or null before the world has it. */
|
|
13
|
+
const cellOf = (state: State, id: number): { readonly storage: Storage; readonly row: number } | null => {
|
|
14
|
+
if (state.resourceEntity < 0) return null;
|
|
15
|
+
const index = indexOf(state.resourceEntity);
|
|
16
|
+
const storage = state.archetypes[state.entities.archetype[index]]?.storageById[id];
|
|
17
|
+
return storage === undefined ? null : { storage, row: state.entities.row[index] };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Starts following resource `id` for a changes finalizer watching it, from its value now. */
|
|
21
|
+
export const watchResource = (state: State, id: number): void => {
|
|
22
|
+
if (state.resourcesSeen.has(id)) return;
|
|
23
|
+
const cell = cellOf(state, id);
|
|
24
|
+
state.resourcesSeen.set(id, cell === null ? { value: state.starts[id], tick: 0 } : { value: readCell(cell.storage, cell.row), tick: cell.storage.changed[cell.row] });
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Whether a watched resource was written since its finalizers last ran, so a sync point is due for them to see it. */
|
|
28
|
+
export const resourcesWritten = (state: State): boolean => {
|
|
29
|
+
for (const [id, seen] of state.resourcesSeen) {
|
|
30
|
+
const cell = cellOf(state, id);
|
|
31
|
+
if (cell !== null && cell.storage.changed[cell.row] > seen.tick) return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Each watched resource written since its finalizers last ran, to a value that isn't equal to the one they saw then,
|
|
38
|
+
* with that value and its value now, which they see from now on.
|
|
39
|
+
*/
|
|
40
|
+
export const takeResourceChanges = (state: State): readonly ResourceChange[] => {
|
|
41
|
+
let changes: ResourceChange[] | null = null;
|
|
42
|
+
for (const [id, seen] of state.resourcesSeen) {
|
|
43
|
+
const cell = cellOf(state, id);
|
|
44
|
+
if (cell === null) continue;
|
|
45
|
+
const tick = cell.storage.changed[cell.row];
|
|
46
|
+
if (tick <= seen.tick) continue;
|
|
47
|
+
const next = readCell(cell.storage, cell.row);
|
|
48
|
+
if (!sameValue(seen.value, next)) (changes ??= []).push({ id, previous: seen.value, next });
|
|
49
|
+
state.resourcesSeen.set(id, { value: next, tick });
|
|
50
|
+
}
|
|
51
|
+
return changes ?? noChanges;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const isPlain = (value: object): boolean => {
|
|
55
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
56
|
+
return prototype === Object.prototype || prototype === null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Whether two values are equal: data leaf by leaf, as `Object.is` compares leaves, and any other object by identity. */
|
|
60
|
+
export const sameValue = (a: unknown, b: unknown): boolean => {
|
|
61
|
+
if (Object.is(a, b)) return true;
|
|
62
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
63
|
+
if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((item, i) => sameValue(item, b[i]));
|
|
64
|
+
if (Array.isArray(b) || !isPlain(a) || !isPlain(b)) return false;
|
|
65
|
+
const keys = Object.keys(a);
|
|
66
|
+
return keys.length === Object.keys(b).length && keys.every(key => Object.hasOwn(b, key) && sameValue(Reflect.get(a, key), Reflect.get(b, key)));
|
|
67
|
+
};
|