@adobe/ecs 0.2.1 → 0.2.3
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/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/signature.js +1 -1
- package/dist/implementation/compiler/transform.js +3 -3
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/world/commands.js +50 -1
- package/dist/implementation/world/commands.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/join.js +10 -2
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/register.js +2 -2
- package/dist/implementation/world/rows.d.ts +1 -1
- package/dist/implementation/world/rows.js +5 -0
- package/dist/implementation/world/rows.js.map +1 -1
- package/dist/implementation/world/runner.js +3 -3
- 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/tables-read.d.ts +7 -0
- package/dist/implementation/world/tables-read.js +60 -0
- package/dist/implementation/world/tables-read.js.map +1 -0
- package/dist/implementation/world/tables.d.ts +9 -0
- package/dist/implementation/world/tables.js +355 -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 +1 -0
- package/dist/implementation/world/thread-systems.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/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 +3 -3
- package/dist/interface/registry/schedule.js +3 -3
- 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/body.d.ts +11 -6
- package/dist/interface/system/check.d.ts +1 -1
- package/dist/interface/system/check.js +25 -9
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +16 -0
- package/dist/interface/system/create.d.ts +21 -9
- package/dist/interface/system/create.js +3 -3
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +9 -2
- package/dist/interface/system/tables.d.ts +20 -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 +26 -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 +10 -10
- package/dist/world.d.ts +27 -0
- package/dist/world.js +27 -0
- package/dist/world.js.map +1 -0
- package/package.json +1 -1
- 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 +9 -2
- package/src/guide/06-sync-points.ts +2 -2
- 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 +7 -5
- 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/hygiene.test.ts +1 -1
- package/src/implementation/compiler/plugin.test.ts +2 -2
- package/src/implementation/compiler/signature.ts +1 -1
- package/src/implementation/compiler/transform.test.ts +3 -3
- package/src/implementation/compiler/transform.ts +3 -3
- package/src/implementation/world/change-blocks.test.ts +1 -1
- package/src/implementation/world/commands.ts +44 -1
- package/src/implementation/world/defaults.test.ts +1 -1
- package/src/implementation/world/entities.ts +6 -0
- package/src/implementation/world/finalizers.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/join.test.ts +4 -4
- package/src/implementation/world/join.ts +10 -2
- 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/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/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/rows.ts +5 -1
- package/src/implementation/world/run-results.test.ts +10 -10
- package/src/implementation/world/runner.ts +3 -3
- 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 +8 -8
- package/src/implementation/world/sharing.ts +1 -1
- package/src/implementation/world/staging.ts +16 -3
- package/src/implementation/world/tables-read.ts +59 -0
- package/src/implementation/world/tables.test.ts +267 -0
- package/src/implementation/world/tables.ts +330 -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 +1 -0
- package/src/implementation/world/where.test.ts +4 -4
- 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/index.type-test.ts +2 -2
- package/src/interface/indexes/index.type-test.ts +1 -1
- package/src/interface/registry/names.ts +8 -0
- package/src/interface/registry/schedule.ts +4 -4
- package/src/interface/resource/declarations.ts +11 -0
- package/src/interface/resource/index.ts +2 -0
- package/src/interface/system/body.ts +7 -5
- package/src/interface/system/check.ts +22 -8
- package/src/interface/system/commands.ts +14 -0
- package/src/interface/system/create.test.ts +13 -13
- package/src/interface/system/create.ts +15 -10
- package/src/interface/system/declaration.ts +9 -2
- package/src/interface/system/lists.type-test.ts +1 -1
- package/src/interface/system/optional.type-test.ts +1 -1
- 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 +21 -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 +3 -3
- package/src/interface/world/table/index.ts +19 -0
- package/src/interface/world/table/table.ts +12 -0
- package/src/interface/world/world.ts +10 -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 +1 -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/suite.ts +1 -1
- package/src/world.test.ts +2 -2
- package/src/world.ts +32 -0
|
@@ -645,7 +645,7 @@ export { World as OtherWorld };
|
|
|
645
645
|
export const runOther = async (key) => {
|
|
646
646
|
const spawn = System.create(({ commands }) => { commands.spawn([key].slice())({ [key]: 10 }); });
|
|
647
647
|
let total = 0;
|
|
648
|
-
const count = System.create({
|
|
648
|
+
const count = System.create({ readonly: true, components: { with: [key] } }, (row) => { total += row[key]; });
|
|
649
649
|
const world = World.create();
|
|
650
650
|
await world.run(spawn);
|
|
651
651
|
await world.run(spawn);
|
|
@@ -662,7 +662,7 @@ declare$(score, "component", { type: { type: "number" } });
|
|
|
662
662
|
${recorder}
|
|
663
663
|
const spawn = System.create(({ commands }) => { commands.spawn([score].slice())({ [score]: 1 }); });
|
|
664
664
|
let counted = 0;
|
|
665
|
-
const count = System.create({
|
|
665
|
+
const count = System.create({ readonly: true, components: { with: [score] } }, (row) => { counted += row[score]; });
|
|
666
666
|
export const copies = World === OtherWorld ? 1 : 2;
|
|
667
667
|
export const run = async () => {
|
|
668
668
|
const world = World.create();
|
|
@@ -120,7 +120,7 @@ export const kernelOf = (declaration: ObjectExpression): KernelRequest | null =>
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
if (declaration.properties.some(property => property.type === "SpreadElement")) return null;
|
|
123
|
-
if (!declares(declaration, "components") || declares(declaration, "
|
|
123
|
+
if (!declares(declaration, "components") || declares(declaration, "readonly") || declares(declaration, "target")) return null;
|
|
124
124
|
return { target: "cpu", required: false };
|
|
125
125
|
};
|
|
126
126
|
|
|
@@ -335,7 +335,7 @@ describe("kernels by default", () => {
|
|
|
335
335
|
test("runs as written, without failing, a body that leaves the subset or a declaration a kernel can't have", async () => {
|
|
336
336
|
expect(await attached(`const log = []; const kernel = System.create({ components: { with: [mass] } }, row => { log.push(row[mass]); });`)).toBe(false);
|
|
337
337
|
expect(await attached(`const kernel = System.create({ components: { with: [mass] } }, (row, { resources }) => { resources[delta] = row[mass]; });`)).toBe(false);
|
|
338
|
-
expect(await attached(`const kernel = System.create({
|
|
338
|
+
expect(await attached(`const kernel = System.create({ readonly: true, components: { with: [mass] } }, row => { void row[mass]; });`)).toBe(false);
|
|
339
339
|
expect(await attached(`const kernel = System.create({ target: "entity", components: { with: [score] } }, row => { row[score] = 1; });`)).toBe(false);
|
|
340
340
|
expect(await attached(`const make = () => System.create({ components: { with: [score] } }, row => { row[score] = 1; });`)).toBe(false);
|
|
341
341
|
});
|
|
@@ -1418,11 +1418,11 @@ describe("inferred access", () => {
|
|
|
1418
1418
|
});
|
|
1419
1419
|
|
|
1420
1420
|
test("an observer's row it can't follow as unknown without a warning, since the world takes an observer's as read", async () => {
|
|
1421
|
-
const result = await transform(system("
|
|
1421
|
+
const result = await transform(system("readonly: true, components: { with: [position] }", "(row) => { helper(row); }"));
|
|
1422
1422
|
expect(result !== null && "code" in result ? [result.code.includes("components: null"), result.warnings] : null).toEqual([true, []]);
|
|
1423
1423
|
});
|
|
1424
1424
|
|
|
1425
|
-
test("a row it can't follow with a warning unless the declaration says
|
|
1425
|
+
test("a row it can't follow with a warning unless the declaration says readonly: true, as the world reads it", async () => {
|
|
1426
1426
|
for (const observer of ["observer: false, ", "observer: watching, "]) {
|
|
1427
1427
|
const result = await transform(`const watching = true;\n${system(`${observer}components: { with: [position] }`, "(row) => { helper(row); }")}`);
|
|
1428
1428
|
expect(result !== null && "code" in result ? result.warnings.length : null).toBe(1);
|
|
@@ -141,7 +141,7 @@ export const functionOf = (binding: ModuleBinding | undefined): Node | undefined
|
|
|
141
141
|
const empty: ObjectExpression = { type: "ObjectExpression", properties: [], start: 0, end: 0 };
|
|
142
142
|
|
|
143
143
|
/** Whether a declaration has property `name`, written out. */
|
|
144
|
-
/** Whether a declaration gives `name` the literal `true`, as the world takes `
|
|
144
|
+
/** Whether a declaration gives `name` the literal `true`, as the world takes `readonly`. */
|
|
145
145
|
const declaresTrue = (declaration: ObjectExpression, name: string): boolean => declaration.properties.some(property =>
|
|
146
146
|
property.type === "Property" && !property.computed && property.key.type === "Identifier" && property.key.name === name
|
|
147
147
|
&& property.value.type === "Literal" && property.value.value === true);
|
|
@@ -313,9 +313,9 @@ export const transformKernels = async (
|
|
|
313
313
|
const used = kernel?.used ?? inOrder(usedBy(declared, body, module.bindings, positions), declared);
|
|
314
314
|
requireRequired(used, declared, body);
|
|
315
315
|
const where = at(body);
|
|
316
|
-
//
|
|
316
|
+
// A readonly system's row, and a lookup's rows, it can't follow are taken as read whole, which is what their
|
|
317
317
|
// declarations already say; only a row taken as written whole costs what the author should know of.
|
|
318
|
-
if (used.query === null && !declaresTrue(declared, "
|
|
318
|
+
if (used.query === null && !declaresTrue(declared, "readonly")) warnings.push(`${where}: the build can't follow how this body uses its row, so it takes every component in with, added and changed as written: index the row by key directly, or destructure it by key, to let it tell what the body reads and writes.`);
|
|
319
319
|
accessing = true;
|
|
320
320
|
const access = accessText(used, where, whereReads.has(node) ? whereReads.get(node) ?? null : undefined);
|
|
321
321
|
if (kernel !== undefined) {
|
|
@@ -89,7 +89,7 @@ const setGroup = System.create({ target: "entity", components: { with: [group] }
|
|
|
89
89
|
// Readers.
|
|
90
90
|
let found: number[] = [];
|
|
91
91
|
const onMain = System.create({ components: { with: [at], changed: [value] } }, row => { found.push(row[at]); });
|
|
92
|
-
const observed = System.create({
|
|
92
|
+
const observed = System.create({ readonly: true, components: { with: [at], changed: [value] } }, row => { found.push(row[at]); });
|
|
93
93
|
const ofTwo = System.create({ components: { with: [at], changed: [group, value] } }, row => { found.push(row[at]); });
|
|
94
94
|
const inKernel = System.create({ kernel: true, components: { with: [seen], changed: [value] } }, row => { row[seen] += 1; });
|
|
95
95
|
const inUp = System.create({ components: { with: [at], changed: [value] }, up: { through: parent } }, row => { found.push(row[at]); });
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import type { Commands } from '../../interface/system/commands.js';
|
|
3
|
-
import { addCommand, despawnCommand, pushCommand, pushComponent, removeCommand, type CommandBuffer } from './buffer.js';
|
|
3
|
+
import { addCommand, createBuffer, despawnCommand, pushCommand, pushComponent, removeCommand, spawnCommand, type CommandBuffer } from './buffer.js';
|
|
4
|
+
import { reserve } from './entities.js';
|
|
4
5
|
import { listShapeOf, presetSpawnerOf, spawnerOf, type ListShape } from './lists.js';
|
|
5
6
|
import { commandIdOf, idOf, storedShared, type Key, type State } from './state.js';
|
|
7
|
+
import { spawnTables } from './tables.js';
|
|
8
|
+
import { declarationOf, isWorldWritten, kindOf, labelOf, threadOfDeclaration, whyUnregistered } from '../../interface/registry/index.js';
|
|
9
|
+
import { matchesSchema } from '../../interface/registry/schemas.js';
|
|
6
10
|
|
|
7
11
|
const collect = (state: State, buffer: CommandBuffer, key: Key, value: unknown): void => {
|
|
8
12
|
if (value !== undefined) pushComponent(buffer, idOf(state, key), value);
|
|
@@ -44,6 +48,38 @@ const addShared = (state: State, buffer: CommandBuffer, entity: number, shape: L
|
|
|
44
48
|
for (let k = 0; k < keys.length; k++) pushComponent(buffer, ids[k], values[k]);
|
|
45
49
|
};
|
|
46
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Throws for a key of `resources` that isn't a resource, is one only the world writes or one living on another thread,
|
|
53
|
+
* or is given a value its schema doesn't take.
|
|
54
|
+
*/
|
|
55
|
+
const checkResources = (resources: object): void => {
|
|
56
|
+
for (const key of Object.getOwnPropertySymbols(resources)) {
|
|
57
|
+
const fail = (message: string): never => { throw new Error(`commands.spawnTables: "${labelOf(key)}" ${message}.`); };
|
|
58
|
+
if (kindOf(key) !== "resource") fail(`isn't a registered resource: ${whyUnregistered(key)}`);
|
|
59
|
+
if (isWorldWritten(key)) fail("is written only by the world");
|
|
60
|
+
const declaration = declarationOf(key);
|
|
61
|
+
if (declaration === undefined) continue;
|
|
62
|
+
const thread = threadOfDeclaration(declaration);
|
|
63
|
+
if (thread !== null && thread !== "world") fail(`lives on thread "${thread}", not the world's`);
|
|
64
|
+
if (declaration.type !== undefined && !matchesSchema(declaration.type, Reflect.get(resources, key))) fail("is given a value its type doesn't take");
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Queues `resources`' values for the sync point as the world's own change to its resource entity, which it reserves
|
|
70
|
+
* when the world has none yet: a buffer of their own, since the sync point skips systems' commands to that entity.
|
|
71
|
+
*/
|
|
72
|
+
const queueResources = (state: State, resources: object): void => {
|
|
73
|
+
if (Object.getOwnPropertySymbols(resources).length === 0) return;
|
|
74
|
+
const buffer = createBuffer();
|
|
75
|
+
if (state.resourceEntity < 0) {
|
|
76
|
+
state.resourceEntity = reserve(state.entities);
|
|
77
|
+
pushValues(state, buffer, spawnCommand, state.resourceEntity, resources);
|
|
78
|
+
}
|
|
79
|
+
else pushValues(state, buffer, addCommand, state.resourceEntity, resources);
|
|
80
|
+
state.queued.push({ buffer, guarded: false });
|
|
81
|
+
};
|
|
82
|
+
|
|
47
83
|
/** Values of a list add given none, which only an untyped caller does. */
|
|
48
84
|
const empty = {};
|
|
49
85
|
|
|
@@ -83,6 +119,13 @@ export const createCommands = (state: State, target: { readonly buffer: CommandB
|
|
|
83
119
|
pushComponent(buffer, ids[k], value === undefined ? starts[k] : value);
|
|
84
120
|
}
|
|
85
121
|
},
|
|
122
|
+
spawnTables: (tables, resources) => {
|
|
123
|
+
if (resources === undefined) return spawnTables(state, tables);
|
|
124
|
+
checkResources(resources);
|
|
125
|
+
const spawned = spawnTables(state, tables);
|
|
126
|
+
queueResources(state, resources);
|
|
127
|
+
return spawned;
|
|
128
|
+
},
|
|
86
129
|
remove: (entity: number, components: symbol | readonly symbol[]) => {
|
|
87
130
|
const { buffer } = target;
|
|
88
131
|
if (typeof components === "symbol") {
|
|
@@ -56,7 +56,7 @@ describe("a derived component declared without a default", () => {
|
|
|
56
56
|
commands.spawn([tagged])({});
|
|
57
57
|
});
|
|
58
58
|
const seen: unknown[] = [];
|
|
59
|
-
const read = System.create({
|
|
59
|
+
const read = System.create({ readonly: true, components: { with: [index] } }, row => { seen.push(row[index]); });
|
|
60
60
|
await using first = createWorld();
|
|
61
61
|
await using second = createWorld();
|
|
62
62
|
for (const world of [first, second]) {
|
|
@@ -70,6 +70,12 @@ export const createEntities = (memory: Memory): Entities => {
|
|
|
70
70
|
/** Reserves an entity. Safe to call from any thread between sync points. */
|
|
71
71
|
export const reserve = ({ cursor, free, generation }: Reservations): Entity => entityAt(cursor, free, generation, Atomics.add(cursor, taken, 1));
|
|
72
72
|
|
|
73
|
+
/** Reserves an entity for each of `into`'s elements, with one atomic add. Safe to call from any thread between sync points. */
|
|
74
|
+
export const reserveRun = ({ cursor, free, generation }: Reservations, into: Float64Array): void => {
|
|
75
|
+
const first = Atomics.add(cursor, taken, into.length);
|
|
76
|
+
for (let i = 0; i < into.length; i++) into[i] = entityAt(cursor, free, generation, first + i);
|
|
77
|
+
};
|
|
78
|
+
|
|
73
79
|
/**
|
|
74
80
|
* Reservations a thread takes in blocks of positions, so threads spawning together don't contend for the cursor.
|
|
75
81
|
* Positions a block leaves unused are reserved entities that never spawn, which the sync point releases.
|
|
@@ -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 });
|
|
@@ -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[]> => {
|
|
@@ -57,16 +57,16 @@ 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
65
|
/** An observer system recording each total it reads into `into`. */
|
|
66
|
-
const totalsInto = (into: number[]) => System.create({
|
|
66
|
+
const totalsInto = (into: number[]) => System.create({ readonly: true }, ({ resources }) => { into.push(resources[total]); });
|
|
67
67
|
/** An observer system recording how many levels it reads into `into`. */
|
|
68
|
-
const levelsInto = (into: number[]) => System.create({
|
|
69
|
-
const failingRead = System.create({
|
|
68
|
+
const levelsInto = (into: number[]) => System.create({ readonly: true }, ({ resources }) => { into.push(resources[levels].length); });
|
|
69
|
+
const failingRead = System.create({ readonly: true }, () => { throw new Error("joinTest read failure"); });
|
|
70
70
|
|
|
71
71
|
/** A world shared with a thread joined through a channel, and the joined world, once both say it joined. */
|
|
72
72
|
const joined = async (threads: readonly ("main")[] = []): Promise<{ readonly world: CreatedWorld; readonly remote: World; readonly channel: MessageChannel }> => {
|
|
@@ -33,6 +33,12 @@ const observerLeavesOf = (definition: Definition): readonly Leaf[] | null => {
|
|
|
33
33
|
return schedule.tiers.length === 0 && schedule.observer.length > 0 ? schedule.observer : null;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
/** Throws for a readonly system reading tables, which only the world's thread gives. */
|
|
37
|
+
const refuseTables = (leaves: readonly Leaf[]): void => {
|
|
38
|
+
const reading = leaves.find(leaf => leaf.declaration.tables !== undefined);
|
|
39
|
+
if (reading !== undefined) throw new Error(`System "${reading.name}" reads tables, which only the world's thread gives: run or add it there.`);
|
|
40
|
+
};
|
|
41
|
+
|
|
36
42
|
/** A run not yet settled: how to settle it, and its rejection once posted. */
|
|
37
43
|
type Run = { readonly resolve: (value: unknown) => void; readonly reject: (error: unknown) => void; message: ToJoined | null; error: unknown; readonly local: Local | null };
|
|
38
44
|
|
|
@@ -659,6 +665,7 @@ export const joinWorld = (port: Parameters<World["share"]>[0], options: { readon
|
|
|
659
665
|
// This thread's own observer systems run here, after the sync point of an empty run of the world's.
|
|
660
666
|
const leaves = observerLeavesOf(definition);
|
|
661
667
|
if (leaves !== null) {
|
|
668
|
+
refuseTables(leaves);
|
|
662
669
|
if (failure !== null) return Promise.reject(failure);
|
|
663
670
|
const locals = leaves.map(localOf);
|
|
664
671
|
if (locals.length === 1 && definition.kind !== "composite") return runNumbered(locals[0].number, locals[0].leaf, undefined, locals[0]);
|
|
@@ -680,7 +687,8 @@ export const joinWorld = (port: Parameters<World["share"]>[0], options: { readon
|
|
|
680
687
|
add: system => {
|
|
681
688
|
const definition = requireDefinition(system);
|
|
682
689
|
const leaves = observerLeavesOf(definition);
|
|
683
|
-
if (leaves === null) throw new Error("
|
|
690
|
+
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.`);
|
|
691
|
+
refuseTables(leaves);
|
|
684
692
|
if (added.has(definition) || failure !== null) return;
|
|
685
693
|
const locals = leaves.map(localOf);
|
|
686
694
|
added.set(definition, locals);
|
|
@@ -692,7 +700,7 @@ export const joinWorld = (port: Parameters<World["share"]>[0], options: { readon
|
|
|
692
700
|
},
|
|
693
701
|
remove: system => {
|
|
694
702
|
const definition = requireDefinition(system);
|
|
695
|
-
if (observerLeavesOf(definition) === null) throw new Error("
|
|
703
|
+
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
704
|
const locals = added.get(definition);
|
|
697
705
|
if (locals === undefined) return;
|
|
698
706
|
added.delete(definition);
|
|
@@ -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);
|
|
@@ -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
|
|