@adobe/ecs 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/implementation/compiler/augmentations.js +8 -1
- package/dist/implementation/compiler/augmentations.js.map +1 -1
- package/dist/implementation/compiler/inferred.js +3 -2
- package/dist/implementation/compiler/inferred.js.map +1 -1
- package/dist/implementation/compiler/type-schemas.d.ts +1 -0
- package/dist/implementation/compiler/type-schemas.js +35 -2
- package/dist/implementation/compiler/type-schemas.js.map +1 -1
- package/dist/implementation/world/activate.d.ts +14 -3
- package/dist/implementation/world/activate.js +19 -13
- package/dist/implementation/world/activate.js.map +1 -1
- package/dist/implementation/world/commands.js +3 -2
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +4 -4
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.js +97 -11
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +10 -2
- package/dist/implementation/world/finalizers.js +42 -13
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/program.js +5 -1
- package/dist/implementation/world/program.js.map +1 -1
- package/dist/implementation/world/state.d.ts +2 -0
- package/dist/implementation/world/state.js +9 -0
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +16 -9
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables.d.ts +2 -1
- package/dist/implementation/world/tables.js +55 -2
- package/dist/implementation/world/tables.js.map +1 -1
- package/dist/interface/finalizer/body.d.ts +24 -3
- package/dist/interface/finalizer/create.d.ts +3 -3
- package/dist/interface/finalizer/create.js +43 -11
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +25 -5
- package/dist/interface/registry/index.d.ts +1 -1
- package/dist/interface/registry/index.js +1 -1
- package/dist/interface/registry/index.js.map +1 -1
- package/dist/interface/registry/names.d.ts +4 -0
- package/dist/interface/registry/names.js +20 -0
- package/dist/interface/registry/names.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +1 -0
- package/dist/interface/registry/schedule.js +86 -11
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/system/check.js +4 -0
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +8 -3
- package/dist/interface/system/create-resolved.js +2 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.js +2 -2
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +11 -4
- package/dist/interface/system/describe.js +1 -1
- package/dist/interface/system/describe.js.map +1 -1
- package/dist/interface/system/leaf.d.ts +4 -3
- package/dist/interface/system/render.js +2 -2
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +12 -3
- package/dist/interface/system/resolve.js +35 -4
- package/dist/interface/system/resolve.js.map +1 -1
- package/package.json +1 -1
- package/src/guide/01-resources.md +4 -3
- package/src/guide/03-systems.out +1 -1
- package/src/guide/05-scheduling.md +12 -6
- package/src/guide/05-scheduling.out +1 -0
- package/src/guide/05-scheduling.ts +17 -0
- package/src/guide/06-sync-points.md +14 -6
- package/src/guide/06-sync-points.out +2 -0
- package/src/guide/06-sync-points.ts +15 -0
- package/src/guide/10-finalizers.md +5 -0
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +4 -4
- package/src/implementation/compiler/augmentations.test.ts +28 -1
- package/src/implementation/compiler/augmentations.ts +5 -1
- package/src/implementation/compiler/inferred.ts +3 -2
- package/src/implementation/compiler/transform.test.ts +4 -4
- package/src/implementation/compiler/type-schemas.ts +28 -2
- package/src/implementation/world/activate.ts +24 -11
- package/src/implementation/world/commands.ts +3 -2
- package/src/implementation/world/create.ts +4 -4
- package/src/implementation/world/defaults.test.ts +24 -1
- package/src/implementation/world/despawn-where.test.ts +31 -8
- package/src/implementation/world/execute.ts +98 -9
- package/src/implementation/world/finalizers.ts +43 -14
- package/src/implementation/world/lifetime.test-helper.ts +1 -1
- package/src/implementation/world/passes.test.ts +61 -0
- package/src/implementation/world/perf.md +9 -0
- package/src/implementation/world/program.ts +4 -1
- package/src/implementation/world/run-results.test.ts +17 -0
- package/src/implementation/world/schedule.test.ts +42 -1
- package/src/implementation/world/state.ts +8 -0
- package/src/implementation/world/sync-points.test.ts +12 -0
- package/src/implementation/world/synchronize.ts +15 -9
- package/src/implementation/world/tables.test.ts +36 -3
- package/src/implementation/world/tables.ts +48 -3
- package/src/implementation/world/watch-traits-late.test-helper.ts +14 -0
- package/src/implementation/world/watch-traits.test-helper.ts +10 -0
- package/src/implementation/world/watch-traits.test.ts +62 -0
- package/src/interface/finalizer/body.ts +22 -3
- package/src/interface/finalizer/create.test.ts +4 -1
- package/src/interface/finalizer/create.ts +40 -12
- package/src/interface/finalizer/declaration.ts +30 -5
- package/src/interface/registry/index.ts +1 -1
- package/src/interface/registry/names.ts +19 -0
- package/src/interface/registry/schedule.ts +89 -16
- package/src/interface/rule/rule.type-test.ts +23 -1
- package/src/interface/system/check.ts +2 -0
- package/src/interface/system/commands.ts +8 -3
- package/src/interface/system/create-resolved.ts +2 -1
- package/src/interface/system/create.test.ts +3 -3
- package/src/interface/system/create.ts +2 -2
- package/src/interface/system/declaration.ts +10 -4
- package/src/interface/system/describe.test.ts +19 -0
- package/src/interface/system/describe.ts +1 -1
- package/src/interface/system/leaf.ts +4 -3
- package/src/interface/system/render.ts +2 -2
- package/src/interface/system/resolve.test.ts +17 -1
- package/src/interface/system/resolve.ts +40 -5
- package/src/interface/system/system.type-test.ts +6 -0
- package/src/perftest/benchmarks.ts +2 -0
- package/src/perftest/chained.ts +108 -0
|
@@ -819,6 +819,9 @@ export const augmentationInsertions = async (module: Module, host: AugmentationH
|
|
|
819
819
|
if (error.notData.node && !thread) fail(module, member, `${key.name} is a DOM Node, which lives on "main": declare \`thread: "main"\``);
|
|
820
820
|
}
|
|
821
821
|
await checkBuiltIns(key.name, kind, typeNode, schema, builtIns);
|
|
822
|
+
const freshType = schema !== undefined ? null : await converter.freshOf(typeNode, module);
|
|
823
|
+
const emptied = schema !== undefined && "type" in schema && (schema.type === "map" || schema.type === "set") ? (schema.type === "map" ? "Map" : "Set") : freshType;
|
|
824
|
+
if (defaultNode !== undefined && emptied !== null) fail(module, defaultNode, `${key.name} is a Readonly${emptied}, which starts empty in each world: leave its default out`);
|
|
822
825
|
const given = defaultNode === undefined ? undefined : await literalDefault(defaultNode, key.name);
|
|
823
826
|
if (schema !== undefined && given !== undefined && !matchesSchema(schema, given)) fail(module, defaultNode ?? member, `${key.name}'s default isn't of its declared type`);
|
|
824
827
|
let start = given === undefined ? undefined : codeOf(given);
|
|
@@ -838,7 +841,8 @@ export const augmentationInsertions = async (module: Module, host: AugmentationH
|
|
|
838
841
|
const namespaced = await namespacedDefault(typeNode, key.name, undefined, oneObject);
|
|
839
842
|
if (namespaced !== null) read({ text: namespaced.text, locals: namespaced.local === undefined ? [] : [namespaced.local], given: true });
|
|
840
843
|
else if (needed && !await allowsNullType(typeNode, module, new Set())) {
|
|
841
|
-
|
|
844
|
+
if (freshType !== null) fresh = freshType;
|
|
845
|
+
else fail(module, member, `${kind} ${key.name} holds a value that isn't data, so nothing gives it a start: let its type allow null, give its type a namespace with a deeply readonly \`default\`, or declare it a ReadonlyMap or ReadonlySet, which starts empty in each world`);
|
|
842
846
|
}
|
|
843
847
|
}
|
|
844
848
|
else {
|
|
@@ -85,8 +85,9 @@ export const accessText = (used: Used, at: string, where?: ReadonlySet<string> |
|
|
|
85
85
|
const lookupText = lookups === null ? "null" : `{ ${[...lookups].map(([name, read]) => `${JSON.stringify(name)}: ${read === null ? "null" : list(read)}`).join(", ")} }`;
|
|
86
86
|
// The resources `where`'s predicates read, only with predicates.
|
|
87
87
|
const whereText = where === undefined ? "" : `, where: ${where === null ? "null" : list(where)}`;
|
|
88
|
-
// Lists of keys stay as their bindings name them, so a list imported from another module is read where it's made
|
|
89
|
-
|
|
88
|
+
// Lists of keys stay as their bindings name them, so a list imported from another module is read where it's made,
|
|
89
|
+
// and in a function, read at first need, so a list declared below the system is read once its module has loaded.
|
|
90
|
+
const commandText = commands === null ? "null" : `() => ({ spawn: ${list(commands.spawn)}, add: ${list(commands.add)}, remove: ${list(commands.remove)}, despawn: ${commands.despawn} })`;
|
|
90
91
|
return `{ components: ${components}, resources: ${resourceText}, lookup: ${lookupText}, up: ${up === null ? "null" : list(up)}${whereText}, commands: ${commandText}, at: ${JSON.stringify(at)} }`;
|
|
91
92
|
};
|
|
92
93
|
|
|
@@ -1436,13 +1436,13 @@ describe("inferred access", () => {
|
|
|
1436
1436
|
|
|
1437
1437
|
test("commands by kind, keys and lists as their bindings name them, however the context reaches them", async () => {
|
|
1438
1438
|
const commanded = async (body: string, declaration = "components: { with: [position] }"): Promise<string> =>
|
|
1439
|
-
/commands: (null|\{[^}]*\})/.exec(await inferred(`const Shapes = [mass];\nconst Listed = [velocity];\n${system(declaration, body)}`))?.[1] ?? "none";
|
|
1439
|
+
/commands: (null|\(\) => \(\{[^}]*\}\))/.exec(await inferred(`const Shapes = [mass];\nconst Listed = [velocity];\n${system(declaration, body)}`))?.[1] ?? "none";
|
|
1440
1440
|
expect(await commanded("(row, { commands }) => { commands.spawn([mass, ...Shapes])({}); commands.add(row.entity, velocity, 1); commands.remove(row.entity, [mass]); }"))
|
|
1441
|
-
.toBe("{ spawn: [mass, Shapes], add: [velocity], remove: [mass], despawn: false }");
|
|
1441
|
+
.toBe("() => ({ spawn: [mass, Shapes], add: [velocity], remove: [mass], despawn: false })");
|
|
1442
1442
|
expect(await commanded("(row, context) => { const c = context.commands; c.despawn(row.entity); context.commands.add(row.entity, Listed, {}); }"))
|
|
1443
|
-
.toBe("{ spawn: [], add: [Listed], remove: [], despawn: true }");
|
|
1443
|
+
.toBe("() => ({ spawn: [], add: [Listed], remove: [], despawn: true })");
|
|
1444
1444
|
expect(await commanded("(row, context) => { const { commands } = context; commands.despawnWhere({ with: [mass] }); }")).toContain("despawn: true");
|
|
1445
|
-
expect(await commanded("(row) => { row[position] = 1; }")).toBe("{ spawn: [], add: [], remove: [], despawn: false }");
|
|
1445
|
+
expect(await commanded("(row) => { row[position] = 1; }")).toBe("() => ({ spawn: [], add: [], remove: [], despawn: false })");
|
|
1446
1446
|
for (const body of ["(row, { commands }) => { helper(commands); }", "(row, { commands }) => { commands.add(row.entity, keyOf(1), 1); }",
|
|
1447
1447
|
"(row, { commands: { spawn } }) => { spawn([mass]); }", "(row, context) => { helper(context); }",
|
|
1448
1448
|
"(row, { commands }) => { commands.spawnTables(tables); }"]) {
|
|
@@ -424,11 +424,15 @@ export const createConverter = (host: TypeHost, root: string) => {
|
|
|
424
424
|
case "TSBooleanKeyword": return { type: "boolean" };
|
|
425
425
|
case "TSNullKeyword": return { type: "null" };
|
|
426
426
|
case "TSUnknownKeyword": return notData(use, false);
|
|
427
|
+
// A symbol can't be cloned to another thread or saved.
|
|
428
|
+
case "TSSymbolKeyword": return use === "declaration" ? notData(use, false) : fail(module, type, `${a} is ${supported}`);
|
|
427
429
|
case "TSFunctionType": case "TSConstructorType":
|
|
428
430
|
return use === "declaration" ? notData(use, false) : fail(module, type, `${a} is ${supported}`);
|
|
429
431
|
case "TSParenthesizedType": return await convert(type.typeAnnotation, module, active, use);
|
|
430
432
|
case "TSTypeOperator":
|
|
431
|
-
|
|
433
|
+
if (type.operator === "readonly") return await convert(type.typeAnnotation, module, active, use);
|
|
434
|
+
// Keys, as `Resource.Key` and `unique symbol` are, may be symbols.
|
|
435
|
+
return use === "declaration" ? notData(use, false) : fail(module, type, `\`${type.operator}\` isn't ${a}: ${a} is ${supported}`);
|
|
432
436
|
case "TSLiteralType": {
|
|
433
437
|
const { literal } = type;
|
|
434
438
|
if (literal.type === "Literal" && (typeof literal.value === "string" || typeof literal.value === "number" || typeof literal.value === "boolean")) return { const: literal.value };
|
|
@@ -677,7 +681,29 @@ export const createConverter = (host: TypeHost, root: string) => {
|
|
|
677
681
|
return { anyOf: values.map(value => ({ const: value })) };
|
|
678
682
|
};
|
|
679
683
|
|
|
680
|
-
|
|
684
|
+
/**
|
|
685
|
+
* `"Map"` or `"Set"` for a declared type that is a `ReadonlyMap` or `ReadonlySet`, read through aliases, which starts
|
|
686
|
+
* empty in each world whatever it holds; null for any other.
|
|
687
|
+
*/
|
|
688
|
+
const freshOf = async (type: TypeNode, module: Module, seen: Set<Declared> = new Set()): Promise<"Map" | "Set" | null> => {
|
|
689
|
+
if (type.type === "TSParenthesizedType") return await freshOf(type.typeAnnotation, module, seen);
|
|
690
|
+
if (type.type === "TSTypeOperator" && type.operator === "readonly") return await freshOf(type.typeAnnotation, module, seen);
|
|
691
|
+
if (type.type !== "TSTypeReference") return null;
|
|
692
|
+
const path = nameParts(type.typeName);
|
|
693
|
+
const [only] = path;
|
|
694
|
+
const count = type.typeArguments?.params.length ?? 0;
|
|
695
|
+
if (path.length === 1 && only !== undefined && !module.types.has(only) && !module.imports.has(only)) {
|
|
696
|
+
if (only === "ReadonlyMap" && count === 2) return "Map";
|
|
697
|
+
if (only === "ReadonlySet" && count === 1) return "Set";
|
|
698
|
+
}
|
|
699
|
+
if (count !== 0) return null;
|
|
700
|
+
const found = await typeNamed(path, module);
|
|
701
|
+
if (found === null || typeof found === "string" || found.declaration.type !== "TSTypeAliasDeclaration" || seen.has(found.declaration)) return null;
|
|
702
|
+
seen.add(found.declaration);
|
|
703
|
+
return await freshOf(found.declaration.typeAnnotation, found.module, seen);
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
return { convert, freshOf, read, site, fail, typeNamed, constantNamed, returnTypeNamed, valueOfType };
|
|
681
707
|
};
|
|
682
708
|
|
|
683
709
|
/** A name and the properties read from it, `Entity.none` as `["Entity", "none"]`; null for any other expression. */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { Finalizer } from '../../interface/finalizer/created.js';
|
|
3
3
|
import type { Rule } from '../../interface/rule/created.js';
|
|
4
4
|
import type { RuleFacts } from '../../interface/registry/strata.js';
|
|
5
|
-
import { createFinalizerRunner, placeFinalizer, type FinalizerRunner } from './finalizers.js';
|
|
5
|
+
import { createFinalizerRunner, followResources, placeFinalizer, watchedResourcesOf, type FinalizerRunner } from './finalizers.js';
|
|
6
6
|
import { watchResource } from './resource-changes.js';
|
|
7
7
|
import { finalizerKeysOf, type TaskChannel } from './remote-systems.js';
|
|
8
8
|
import { programFinalizers, programRules, programVersion } from '../../interface/registry/program.js';
|
|
@@ -10,7 +10,7 @@ import { triggersOf } from './program.js';
|
|
|
10
10
|
import { ensureResources, findResource } from './resources.js';
|
|
11
11
|
import { idOf, metIdOf, type Key, type State } from './state.js';
|
|
12
12
|
import { restratify } from './structure.js';
|
|
13
|
-
import { idOfItem, idOfKey,
|
|
13
|
+
import { idOfItem, idOfKey, keysVersion, labelOf, threadOf } from '../../interface/registry/index.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Why a joined thread that imported the items of ids `imported` can't claim `threads`: a program finalizer on one that
|
|
@@ -71,19 +71,31 @@ export const placeFinalizers = (state: State): void => {
|
|
|
71
71
|
/** The resources a finalizer reads, writes or, for a changes finalizer, watches. */
|
|
72
72
|
const resourcesOf = (finalizer: Finalizer): readonly Key[] => [
|
|
73
73
|
...finalizer.declaration.resources?.read ?? [], ...finalizer.declaration.resources?.write ?? [],
|
|
74
|
-
...finalizer.kind === "changes" ? (finalizer.declaration.watch
|
|
74
|
+
...finalizer.kind === "changes" ? watchedResourcesOf(finalizer.declaration.watch) : [],
|
|
75
75
|
];
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* What a world ensured of the program's resources: the program's version then, and the keys' while a changes finalizer
|
|
79
|
+
* watches resources by a trait, which a resource registering later may declare.
|
|
80
|
+
*/
|
|
81
|
+
export type Ensured = { version: number; keys: number; traits: boolean };
|
|
82
|
+
|
|
83
|
+
/** Whether the program's finalizers may use a resource the world hasn't ensured. */
|
|
84
|
+
export const programResourcesChanged = (ensured: Ensured): boolean => ensured.version !== programVersion() || (ensured.traits && ensured.keys !== keysVersion());
|
|
85
|
+
|
|
77
86
|
/** Between runs: adds the resources of every finalizer in the program, so one can activate at any sync point. */
|
|
78
|
-
export const ensureProgramResources = (state: State, ensured:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
ensured.
|
|
87
|
+
export const ensureProgramResources = (state: State, ensured: Ensured): void => {
|
|
88
|
+
if (!programResourcesChanged(ensured)) return;
|
|
89
|
+
ensured.version = programVersion();
|
|
90
|
+
ensured.keys = keysVersion();
|
|
91
|
+
ensured.traits = programFinalizers().some(finalizer => finalizer.kind === "changes" && finalizer.declaration.watch.resourceTraits.length > 0);
|
|
82
92
|
ensureResources(state, new Set(programFinalizers().flatMap(resourcesOf)));
|
|
83
93
|
// A watched resource is followed from now, before its finalizer activates, so writes before then are seen.
|
|
84
94
|
for (const finalizer of programFinalizers()) {
|
|
85
|
-
if (finalizer.kind === "changes") for (const key of finalizer.declaration.watch
|
|
95
|
+
if (finalizer.kind === "changes") for (const key of watchedResourcesOf(finalizer.declaration.watch)) watchResource(state, idOf(state, key));
|
|
86
96
|
}
|
|
97
|
+
// Active finalizers watching resources by a trait follow those registered since.
|
|
98
|
+
for (const runner of state.finalizers) if (runner.finalizer.kind === "changes") followResources(state, runner.finalizer.declaration.watch, runner.resourceWatches);
|
|
87
99
|
// Finalizers waiting for these can activate at the next sync point.
|
|
88
100
|
state.activation.met = true;
|
|
89
101
|
};
|
|
@@ -99,11 +111,12 @@ const ready = (state: State, item: Rule | Finalizer): boolean => {
|
|
|
99
111
|
* resolves its components' ids, which may make more ready.
|
|
100
112
|
*/
|
|
101
113
|
export const activate = (state: State): void => {
|
|
102
|
-
|
|
103
|
-
if (!activation.met && activation.version === programVersion()) return;
|
|
104
|
-
activateNow(state);
|
|
114
|
+
if (activationPending(state)) activateNow(state);
|
|
105
115
|
};
|
|
106
116
|
|
|
117
|
+
/** Whether a rule or finalizer of the program may be ready to activate: a component met, or the program changed. */
|
|
118
|
+
export const activationPending = (state: State): boolean => state.activation.met || state.activation.version !== programVersion();
|
|
119
|
+
|
|
107
120
|
/** `activate` once something may be ready: apart, so a sync point with nothing to activate allocates no closure context. */
|
|
108
121
|
const activateNow = (state: State): void => {
|
|
109
122
|
const { activation, activated, structure } = state;
|
|
@@ -151,9 +151,10 @@ export const createCommands = (state: State, target: { readonly buffer: CommandB
|
|
|
151
151
|
},
|
|
152
152
|
spawnTables: (tables, options) => {
|
|
153
153
|
const resources = options?.resources;
|
|
154
|
-
|
|
154
|
+
const updates = options?.update === true ? target.buffer : null;
|
|
155
|
+
if (resources === undefined) return spawnTables(state, tables, options?.existing, updates);
|
|
155
156
|
checkResources(resources);
|
|
156
|
-
const spawned = spawnTables(state, tables, options?.existing);
|
|
157
|
+
const spawned = spawnTables(state, tables, options?.existing, updates);
|
|
157
158
|
queueResources(state, resources);
|
|
158
159
|
return spawned;
|
|
159
160
|
},
|
|
@@ -33,8 +33,8 @@ import { refreshDirectories } from './directories.js';
|
|
|
33
33
|
import { createRegistrar, type Registration } from './register.js';
|
|
34
34
|
import type { Schedule } from '../../interface/registry/schedule.js';
|
|
35
35
|
import { ensureResources, findResource, initialOf, setResource } from './resources.js';
|
|
36
|
-
import { ensureProgramResources, activate, finalizerCrossing, placeFinalizers } from './activate.js';
|
|
37
|
-
import { isObserver, isWorldWritten, kindOf,
|
|
36
|
+
import { ensureProgramResources, activate, finalizerCrossing, placeFinalizers, programResourcesChanged, type Ensured } from './activate.js';
|
|
37
|
+
import { isObserver, isWorldWritten, kindOf, scheduleOf, type Composite } from '../../interface/registry/index.js';
|
|
38
38
|
import { always } from '../../interface/resources/always.js';
|
|
39
39
|
import { cadence, type Cadence } from '../../interface/resources/cadence.js';
|
|
40
40
|
import { clock } from '../../interface/resources/clock.js';
|
|
@@ -284,7 +284,7 @@ export const createWorld = (resources: InitialResources = {}, options: WorldOpti
|
|
|
284
284
|
if (state.observer.posts.length !== 0) await settlePosts(state, true);
|
|
285
285
|
};
|
|
286
286
|
let disposed = false;
|
|
287
|
-
const ensured = { version: -1 };
|
|
287
|
+
const ensured: Ensured = { version: -1, keys: -1, traits: false };
|
|
288
288
|
|
|
289
289
|
const minimumChunk = options.minimumChunk ?? defaultMinimumChunk;
|
|
290
290
|
|
|
@@ -545,7 +545,7 @@ export const createWorld = (resources: InitialResources = {}, options: WorldOpti
|
|
|
545
545
|
/** Follows what changed since the last run: program resources, change watches, workers and the device; null when nothing did. */
|
|
546
546
|
const prepareRun = (): Promise<void> | null => {
|
|
547
547
|
// Resources added between runs move the resource entity, whose structs a tail may still read.
|
|
548
|
-
if (ensured
|
|
548
|
+
if (programResourcesChanged(ensured) && (state.channels.length > 0 || state.observer.posts.length > 0)) return drainThenPrepare();
|
|
549
549
|
ensureProgramResources(state, ensured);
|
|
550
550
|
if (startWatches(state)) return followChanges(true, false);
|
|
551
551
|
if (workersChanged()) return followChanges(false, true);
|
|
@@ -4,7 +4,7 @@ import { Rule } from "../../interface/rule/rule.js";
|
|
|
4
4
|
import { System } from "../../interface/system/system.js";
|
|
5
5
|
import { createWorld } from "../index.js";
|
|
6
6
|
import { readResource } from "./reads.test-helper.js";
|
|
7
|
-
import { Entity, F32, I32, Mat4x4, Quat, U32, Vec2, Vec3, Vec4 } from "../../index.js";
|
|
7
|
+
import { Entity, F32, I32, Mat4x4, Quat, U32, Vec2, Vec3, Vec4, type Resource } from "../../index.js";
|
|
8
8
|
|
|
9
9
|
const pool = Symbol();
|
|
10
10
|
const names = Symbol();
|
|
@@ -14,6 +14,8 @@ const index = Symbol();
|
|
|
14
14
|
const turn = Symbol();
|
|
15
15
|
const least = Symbol();
|
|
16
16
|
const all = Symbol();
|
|
17
|
+
const written = Symbol();
|
|
18
|
+
const keys = Symbol();
|
|
17
19
|
|
|
18
20
|
/** A pool declared through an alias of ReadonlyMap. */
|
|
19
21
|
type Pool = ReadonlyMap<string, number>;
|
|
@@ -30,6 +32,8 @@ declare module "@adobe/ecs" {
|
|
|
30
32
|
[turn]: { type: Quat };
|
|
31
33
|
[least]: { type: F32; reduce: "min" };
|
|
32
34
|
[all]: { type: boolean; reduce: "&" };
|
|
35
|
+
[written]: { type: ReadonlyMap<Resource.Key, unknown> };
|
|
36
|
+
[keys]: { type: ReadonlySet<symbol> };
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
|
@@ -49,6 +53,25 @@ describe("a resource declared without a default", () => {
|
|
|
49
53
|
});
|
|
50
54
|
});
|
|
51
55
|
|
|
56
|
+
describe("a ReadonlyMap or ReadonlySet of what isn't data", () => {
|
|
57
|
+
it("starts at an empty one of each world's own, which a system replaces whole, keys and values kept as they are", async () => {
|
|
58
|
+
await using first = createWorld();
|
|
59
|
+
await using second = createWorld();
|
|
60
|
+
const [firstWritten, secondWritten] = [await readResource(first, written), await readResource(second, written)];
|
|
61
|
+
expect([firstWritten, secondWritten, await readResource(first, keys)]).toEqual([new Map(), new Map(), new Set()]);
|
|
62
|
+
expect(firstWritten).not.toBe(secondWritten);
|
|
63
|
+
const value = { at: 1 };
|
|
64
|
+
await first.run(System.create(({ resources }) => {
|
|
65
|
+
resources[written] = new Map([[count, value]]);
|
|
66
|
+
resources[keys] = new Set([count]);
|
|
67
|
+
}));
|
|
68
|
+
const read = await readResource(first, written);
|
|
69
|
+
expect([read.get(count), await readResource(first, keys)]).toEqual([value, new Set([count])]);
|
|
70
|
+
expect(read.get(count)).toBe(value);
|
|
71
|
+
expect(await readResource(second, written)).toBe(secondWritten);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
52
75
|
describe("a derived component declared without a default", () => {
|
|
53
76
|
it("starts every row of a world at one empty Map of its own, since values are replaced whole", async () => {
|
|
54
77
|
const spawn = System.create(({ commands }) => {
|
|
@@ -119,16 +119,36 @@ describe("despawnWhere", () => {
|
|
|
119
119
|
await world.dispose();
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
-
it("
|
|
122
|
+
it("keeps what the same sync point spawns, whenever spawned and whatever earlier commands give it", async () => {
|
|
123
123
|
const { world, run } = withCommands();
|
|
124
|
+
let alive = Entity.none;
|
|
125
|
+
await run(commands => { alive = commands.spawn([mark, value])({ [value]: 0 }); });
|
|
124
126
|
const spawned: Entity[] = [];
|
|
125
127
|
await run(commands => {
|
|
126
128
|
spawned.push(commands.spawn([mark, value])({ [value]: 1 }));
|
|
129
|
+
const tagged = commands.spawn([value])({ [value]: 2 });
|
|
130
|
+
commands.add(tagged, mark);
|
|
131
|
+
spawned.push(tagged);
|
|
127
132
|
commands.despawnWhere({ with: [mark] });
|
|
128
|
-
spawned.push(commands.spawn([mark, value])({ [value]:
|
|
129
|
-
spawned.push(commands.spawn([value])({ [value]: 3 }));
|
|
133
|
+
spawned.push(commands.spawn([mark, value])({ [value]: 3 }));
|
|
130
134
|
});
|
|
131
|
-
expect(await aliveEach(world, spawned)).toEqual([false,
|
|
135
|
+
expect([await aliveEach(world, [alive]), await aliveEach(world, spawned)]).toEqual([[false], [true, true, true]]);
|
|
136
|
+
await world.dispose();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("clears the events a frame made before, keeping those it makes, in one system", async () => {
|
|
140
|
+
const { world, run } = withCommands();
|
|
141
|
+
let first = Entity.none, second = Entity.none;
|
|
142
|
+
await run(commands => {
|
|
143
|
+
commands.despawnWhere({ with: [mark] });
|
|
144
|
+
first = commands.spawn([mark, value])({ [value]: 1 });
|
|
145
|
+
});
|
|
146
|
+
expect(await aliveEach(world, [first])).toEqual([true]);
|
|
147
|
+
await run(commands => {
|
|
148
|
+
commands.despawnWhere({ with: [mark] });
|
|
149
|
+
second = commands.spawn([mark, value])({ [value]: 2 });
|
|
150
|
+
});
|
|
151
|
+
expect(await aliveEach(world, [first, second])).toEqual([false, true]);
|
|
132
152
|
await world.dispose();
|
|
133
153
|
});
|
|
134
154
|
|
|
@@ -256,18 +276,21 @@ describe("despawnWhere", () => {
|
|
|
256
276
|
await world.dispose();
|
|
257
277
|
});
|
|
258
278
|
|
|
259
|
-
it("despawns by where
|
|
279
|
+
it("despawns by where what was alive before the sync point, keeping what it spawns, and none for a value no archetype holds", async () => {
|
|
260
280
|
const { world, run } = withCommands();
|
|
261
281
|
const spawned: Entity[] = [];
|
|
262
282
|
await run(commands => {
|
|
263
283
|
spawned.push(commands.spawn([mark, chunk], { [chunk]: 1 })());
|
|
264
|
-
commands.despawnWhere({ with: [mark], where: { [chunk]: 1 } });
|
|
265
284
|
spawned.push(commands.spawn([mark, chunk], { [chunk]: 2 })());
|
|
285
|
+
});
|
|
286
|
+
await run(commands => {
|
|
287
|
+
spawned.push(commands.spawn([mark, chunk], { [chunk]: 1 })());
|
|
288
|
+
commands.despawnWhere({ with: [mark], where: { [chunk]: 1 } });
|
|
266
289
|
commands.despawnWhere({ with: [mark], where: { [chunk]: 3 } });
|
|
267
290
|
});
|
|
268
|
-
expect(await aliveEach(world, spawned)).toEqual([false, true]);
|
|
291
|
+
expect(await aliveEach(world, spawned)).toEqual([false, true, true]);
|
|
269
292
|
await run(commands => commands.despawnWhere({ with: [mark], where: {} }));
|
|
270
|
-
expect(await aliveEach(world, spawned)).toEqual([false, false]);
|
|
293
|
+
expect(await aliveEach(world, spawned)).toEqual([false, false, false]);
|
|
271
294
|
await world.dispose();
|
|
272
295
|
});
|
|
273
296
|
|
|
@@ -3,14 +3,15 @@ import type { Leaf } from '../../interface/system/leaf.js';
|
|
|
3
3
|
import type { Chunk, ChunkResult, Runner, Workers } from './runner.js';
|
|
4
4
|
import type { Schedule } from '../../interface/registry/schedule.js';
|
|
5
5
|
import type { Archetype, Storage } from './archetype.js';
|
|
6
|
-
import { countRan, idOf, startRan, type State } from './state.js';
|
|
6
|
+
import { countRan, countRanHere, idOf, startRan, type State } from './state.js';
|
|
7
7
|
import type { GpuEngine } from './gpu.js';
|
|
8
8
|
import { applySync, prepareSync, syncPending } from './synchronize.js';
|
|
9
|
-
import {
|
|
9
|
+
import { resourcesWritten } from './resource-changes.js';
|
|
10
|
+
import { activate, activationPending } from './activate.js';
|
|
10
11
|
import { markRun } from './move-log.js';
|
|
11
12
|
import { postTails, settleAll, settleKeys, settleRunners, tailsOf, type TailOutcome } from './tails.js';
|
|
12
13
|
import { settlePosts, settleObserverKeys, settleObserverRunners } from './joined-observer.js';
|
|
13
|
-
import { locate } from './entities.js';
|
|
14
|
+
import { locate, reserved } from './entities.js';
|
|
14
15
|
import { clearBuffer, createBuffer } from './buffer.js';
|
|
15
16
|
import { drainSegment, type Segment } from './command-ring.js';
|
|
16
17
|
import { combine, identityOf, writeReduction, type ReductionTarget } from './reductions.js';
|
|
@@ -101,6 +102,18 @@ type Prepared = {
|
|
|
101
102
|
readonly remotes: number[];
|
|
102
103
|
/** The indexes each tier's runners look up through, refreshed before it. */
|
|
103
104
|
readonly uses: (readonly IndexUse[])[];
|
|
105
|
+
/**
|
|
106
|
+
* By tier, the last tier of the pass starting there: tiers whose systems all run on the world's thread as written,
|
|
107
|
+
* which look up through no index from the second on, run in one pass while their sync points apply nothing. Its own
|
|
108
|
+
* tier for any other.
|
|
109
|
+
*/
|
|
110
|
+
readonly passes: number[];
|
|
111
|
+
/**
|
|
112
|
+
* By tier within a pass of several, how many runners it and the pass's later tiers hold, and whether any runs on the
|
|
113
|
+
* GPU: what a pass entered there counts once it ran to its end.
|
|
114
|
+
*/
|
|
115
|
+
readonly passSystems: number[];
|
|
116
|
+
readonly passGpu: boolean[];
|
|
104
117
|
/** The world's `runnersVersion` its runners were read at. */
|
|
105
118
|
version: number;
|
|
106
119
|
readonly started: (readonly Chunk[])[][];
|
|
@@ -168,7 +181,8 @@ const preparedFor = (context: ExecuteContext, schedule: Schedule): Prepared => {
|
|
|
168
181
|
let prepared = bySchedule.get(schedule);
|
|
169
182
|
if (prepared === undefined) {
|
|
170
183
|
prepared = {
|
|
171
|
-
runners: schedule.tiers.map(tier => tier.map(context.runnerOf)), remotes: schedule.tiers.map(() => 0), uses: schedule.tiers.map(() => []),
|
|
184
|
+
runners: schedule.tiers.map(tier => tier.map(context.runnerOf)), remotes: schedule.tiers.map(() => 0), uses: schedule.tiers.map(() => []),
|
|
185
|
+
passes: schedule.tiers.map((_, t) => t), passSystems: schedule.tiers.map(() => 0), passGpu: schedule.tiers.map(() => false), version: -1,
|
|
172
186
|
started: schedule.tiers.map(tier => new Array<readonly Chunk[]>(tier.length)),
|
|
173
187
|
systems: [], tiers: [], tasks: new Map(), warnings: [], warningCount: 0, warned: null, outcome: { failures: null, skipped: null },
|
|
174
188
|
placement: -1, tailing: null, settling: 0, chunk: 0, error: undefined, tierFailed: false, dropped: 0,
|
|
@@ -189,9 +203,38 @@ const refresh = (context: ExecuteContext, schedule: Schedule, prepared: Prepared
|
|
|
189
203
|
prepared.remotes[t] = remote;
|
|
190
204
|
prepared.uses[t] = mergeUses(runners);
|
|
191
205
|
}
|
|
206
|
+
passesOf(schedule, prepared);
|
|
192
207
|
prepared.version = context.state.runnersVersion;
|
|
193
208
|
};
|
|
194
209
|
|
|
210
|
+
/** Whether every runner of a tier runs on the world's thread as written: none a kernel or on a joined thread. */
|
|
211
|
+
const asWritten = (runners: readonly Runner[]): boolean => {
|
|
212
|
+
for (let i = 0; i < runners.length; i++) if (runners[i].kernel !== null || runners[i].remote !== null) return false;
|
|
213
|
+
return true;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/** Finds a schedule's passes (`Prepared["passes"]`) from its runners as read now. */
|
|
217
|
+
const passesOf = (schedule: Schedule, prepared: Prepared): void => {
|
|
218
|
+
const { runners, uses, passes, passSystems, passGpu } = prepared;
|
|
219
|
+
let last = -1;
|
|
220
|
+
for (let t = 0; t < schedule.tiers.length; t++) {
|
|
221
|
+
if (!asWritten(runners[t])) last = t;
|
|
222
|
+
else if (last < t) {
|
|
223
|
+
last = t;
|
|
224
|
+
while (last + 1 < schedule.tiers.length && uses[last + 1].length === 0 && asWritten(runners[last + 1])) last++;
|
|
225
|
+
}
|
|
226
|
+
passes[t] = last;
|
|
227
|
+
}
|
|
228
|
+
for (let t = schedule.tiers.length - 1; t >= 0; t--) {
|
|
229
|
+
const tier = runners[t];
|
|
230
|
+
const continues = passes[t] > t;
|
|
231
|
+
let gpu = continues && passGpu[t + 1];
|
|
232
|
+
for (let i = 0; i < tier.length; i++) if (tier[i].placement !== "cpu") gpu = true;
|
|
233
|
+
passSystems[t] = tier.length + (continues ? passSystems[t + 1] : 0);
|
|
234
|
+
passGpu[t] = gpu;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
195
238
|
/**
|
|
196
239
|
* Drains a worker's segment into a buffer of its own, queued in its place: commands, spawn blocks staged to commit in
|
|
197
240
|
* its place, and the reservations it left unused. Fails if it doesn't start with its task's merge tag.
|
|
@@ -477,11 +520,20 @@ const executeTiers = (context: ExecuteContext, whole: Schedule, args: unknown):
|
|
|
477
520
|
if (building !== null) return handed = executeFrom(context, schedule, prepared, folds, args, null, t, building, null);
|
|
478
521
|
if (state.indexFailures.length !== 0) (outcome.failures ??= []).push(...state.indexFailures.splice(0));
|
|
479
522
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
523
|
+
const last = prepared.passes[t];
|
|
524
|
+
if (last > t) {
|
|
525
|
+
const reached = runPass(context, prepared, schedule, folds, args, t, last);
|
|
526
|
+
if (typeof reached !== "number") return handed = reached;
|
|
527
|
+
if (reached < 0) break;
|
|
528
|
+
t = reached;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
startTier(context, prepared, t, args);
|
|
532
|
+
const waiting = settleTier(state, folds, prepared, schedule.tiers[t], t, nothing);
|
|
533
|
+
if (waiting !== null) return handed = executeFrom(context, schedule, prepared, folds, args, null, t, null, waiting);
|
|
534
|
+
if (endTier(state, null, prepared, schedule, t)) break;
|
|
535
|
+
}
|
|
536
|
+
if (schedule.syncs[t] !== null && !idleSync(state)) syncNow(context, prepared, false);
|
|
485
537
|
}
|
|
486
538
|
}
|
|
487
539
|
finally {
|
|
@@ -490,6 +542,43 @@ const executeTiers = (context: ExecuteContext, whole: Schedule, args: unknown):
|
|
|
490
542
|
return reportOf(state, prepared);
|
|
491
543
|
};
|
|
492
544
|
|
|
545
|
+
/**
|
|
546
|
+
* Runs tiers `first` to `last`, a pass, one after another with none of the checks between tiers that only a tier on
|
|
547
|
+
* another thread, using an index or after a sync point that applies something needs. Returns the last tier it ran,
|
|
548
|
+
* after which the caller applies its sync point: `last`, or one before it whose sync point applies something or whose
|
|
549
|
+
* bodies replaced runners; -1 when one failed, the tiers after it skipped; or what `executeFrom` returns once one waits
|
|
550
|
+
* for something.
|
|
551
|
+
*/
|
|
552
|
+
const runPass = (context: ExecuteContext, prepared: Prepared, schedule: Schedule, folds: Map<unknown, Fold> | null, args: unknown, first: number, last: number): number | Promise<RunReport> => {
|
|
553
|
+
const { state } = context;
|
|
554
|
+
for (let v = first; v <= last; v++) {
|
|
555
|
+
startTier(context, prepared, v, args);
|
|
556
|
+
const waiting = settleTier(state, folds, prepared, schedule.tiers[v], v, nothing);
|
|
557
|
+
if (waiting !== null) {
|
|
558
|
+
for (let ran = first; ran < v; ran++) countRan(state, prepared.runners[ran]);
|
|
559
|
+
return executeFrom(context, schedule, prepared, folds, args, null, v, null, waiting);
|
|
560
|
+
}
|
|
561
|
+
if (prepared.tierFailed || (v < last && (prepared.version !== state.runnersVersion || (schedule.syncs[v] !== null && !idleSync(state))))) {
|
|
562
|
+
for (let ran = first; ran <= v; ran++) countRan(state, prepared.runners[ran]);
|
|
563
|
+
if (!prepared.tierFailed) return v;
|
|
564
|
+
const { outcome } = prepared;
|
|
565
|
+
for (let later = v + 1; later < schedule.tiers.length; later++) for (const leaf of schedule.tiers[later]) (outcome.skipped ??= []).push(leaf);
|
|
566
|
+
return -1;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
countRanHere(state, prepared.passSystems[first], prepared.passGpu[first]);
|
|
570
|
+
return last;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Whether a sync point within a run, with no joined thread, would apply nothing and leave nothing to do after:
|
|
575
|
+
* nothing queued, drained, unreserved or pending (`syncPending`), no rule or finalizer to activate, and no layout for
|
|
576
|
+
* workers to see.
|
|
577
|
+
*/
|
|
578
|
+
const idleSync = (state: State): boolean => state.queued.length === 0 && state.drained === 0 && state.unused.length === 0
|
|
579
|
+
&& state.channels.length === 0 && state.logs.length === 0 && state.bumped.length === 0 && state.counted.length === 0 && state.carried === null && !state.structure.rederive && state.unplaced.length === 0
|
|
580
|
+
&& (state.resourcesSeen.size === 0 || !resourcesWritten(state)) && !activationPending(state) && !reserved(state.entities);
|
|
581
|
+
|
|
493
582
|
/**
|
|
494
583
|
* A sync point with nothing to wait for: no GPU, tails or observer posts, which `executeTiers` checked before each
|
|
495
584
|
* tier and bodies can't add. The run's `last` lets go of strings no reader holds.
|
|
@@ -14,7 +14,8 @@ import { idOf, type Key, type State } from './state.js';
|
|
|
14
14
|
import type { Queued } from './synchronize.js';
|
|
15
15
|
import type { Essential, ReadonlyType } from '../../interface/component/value.js';
|
|
16
16
|
import type { Leaf } from '../../interface/finalizer/created.js';
|
|
17
|
-
import { newestKeyOf, threadOf } from '../../interface/registry/index.js';
|
|
17
|
+
import { declaredOf, declaresTrait, newestKeyOf, threadOf, threadOfDeclaration } from '../../interface/registry/index.js';
|
|
18
|
+
import type { ResolvedChanges } from '../../interface/finalizer/declaration.js';
|
|
18
19
|
import type { Layout } from '../../interface/registry/layout.js';
|
|
19
20
|
import type { TaskChannel } from './remote-systems.js';
|
|
20
21
|
import { resourceOffsetsOf, writeStops, type StopsOf } from './stop-lists.js';
|
|
@@ -30,8 +31,8 @@ export type FinalizerRunner = {
|
|
|
30
31
|
readonly excluded: readonly number[];
|
|
31
32
|
/** For a changes finalizer: whether it watches a component, by id. */
|
|
32
33
|
readonly watches: (id: number) => boolean;
|
|
33
|
-
/** For a changes finalizer: the resources it watches, by id. */
|
|
34
|
-
readonly resourceWatches:
|
|
34
|
+
/** For a changes finalizer: the resources it watches, by id, which grow as resources declaring a trait it watches register. */
|
|
35
|
+
readonly resourceWatches: number[];
|
|
35
36
|
/** By archetype id: 1 if it matches, 2 if not, 0 until known. */
|
|
36
37
|
readonly matches: number[];
|
|
37
38
|
readonly context: Context;
|
|
@@ -63,19 +64,23 @@ export const createFinalizerRunner = (state: State, finalizer: Finalizer): Final
|
|
|
63
64
|
const reads = query?.read ?? [];
|
|
64
65
|
// By default every component that isn't derived, including ones a world first meets later; never resources, nor
|
|
65
66
|
// the objects a joined thread fills, whose storage here holds only tags.
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
const everyComponent = (id: number): boolean => !state.derived[id] && !state.resources.has(id) && state.remote[id] === undefined;
|
|
68
|
+
let watches = everyComponent;
|
|
68
69
|
const resourceWatches: number[] = [];
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
if (finalizer.kind === "changes") {
|
|
71
|
+
const { watch } = finalizer.declaration;
|
|
72
|
+
followResources(state, watch, resourceWatches);
|
|
73
|
+
if (watch.components !== true) {
|
|
74
|
+
// `Finalizer.changes` rejected derived components.
|
|
75
|
+
const listed = new Set(watch.components.map(key => idOf(state, key)));
|
|
76
|
+
const traits = watch.componentTraits;
|
|
77
|
+
if (traits.length === 0) watches = id => listed.has(id);
|
|
78
|
+
else {
|
|
79
|
+
/** By id: 1 where its key declares one of the traits, 2 where it doesn't. */
|
|
80
|
+
const declaring: number[] = [];
|
|
81
|
+
watches = id => listed.has(id) || (everyComponent(id) && (declaring[id] ??= declaresTrait(state.keys[id], traits) ? 1 : 2) === 1);
|
|
82
|
+
}
|
|
77
83
|
}
|
|
78
|
-
watches = id => watched.has(id);
|
|
79
84
|
}
|
|
80
85
|
const target = { buffer: createBuffer() };
|
|
81
86
|
const commands = createCommands(state, target);
|
|
@@ -102,6 +107,30 @@ export const createFinalizerRunner = (state: State, finalizer: Finalizer): Final
|
|
|
102
107
|
};
|
|
103
108
|
};
|
|
104
109
|
|
|
110
|
+
/**
|
|
111
|
+
* The resources a changes finalizer watches: those it lists, and those registered so far that declare a trait it
|
|
112
|
+
* watches, but those on another thread than the world's.
|
|
113
|
+
*/
|
|
114
|
+
export const watchedResourcesOf = (watch: ResolvedChanges["watch"]): readonly Key[] => {
|
|
115
|
+
if (watch.resourceTraits.length === 0) return watch.resources;
|
|
116
|
+
const found: Key[] = [...watch.resources];
|
|
117
|
+
for (const [key, declared] of declaredOf("resource")) {
|
|
118
|
+
const thread = threadOfDeclaration(declared);
|
|
119
|
+
if ((thread === null || thread === "world") && declaresTrait(key, watch.resourceTraits)) found.push(key);
|
|
120
|
+
}
|
|
121
|
+
return found;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Adds to `ids` each resource a changes finalizer watches that it doesn't hold, following its changes from now. */
|
|
125
|
+
export const followResources = (state: State, watch: ResolvedChanges["watch"], ids: number[]): void => {
|
|
126
|
+
for (const key of watchedResourcesOf(watch)) {
|
|
127
|
+
const id = idOf(state, key);
|
|
128
|
+
if (ids.includes(id)) continue;
|
|
129
|
+
ids.push(id);
|
|
130
|
+
watchResource(state, id);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
105
134
|
/**
|
|
106
135
|
* Places a finalizer on a joined thread's channel, posting it there, or back on the world's thread with null. Its reads
|
|
107
136
|
* are listed as the thread reads them: objects by their tags, linear structs by their bytes, strings by handles of
|
|
@@ -73,7 +73,7 @@ export const freshKernel = (system: System): System => {
|
|
|
73
73
|
const description = kernelOf(system);
|
|
74
74
|
if (leaf === undefined || leaf.kind === "composite" || description === undefined) throw new Error("Not a kernel system.");
|
|
75
75
|
// What `System.create` makes of the same declaration and body, without the id a top-level binding takes.
|
|
76
|
-
const copy: Leaf & System = { kind: leaf.kind, name: leaf.name, declaration: leaf.declaration, input: leaf.input, body: leaf.body };
|
|
76
|
+
const copy: Leaf & System = { kind: leaf.kind, name: leaf.name, declaration: leaf.declaration, input: leaf.input, body: leaf.body, commands: leaf.commands };
|
|
77
77
|
claimSystem(copy, undefined);
|
|
78
78
|
return attachKernel(copy, description);
|
|
79
79
|
};
|