@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.
Files changed (120) hide show
  1. package/dist/implementation/compiler/augmentations.js +8 -1
  2. package/dist/implementation/compiler/augmentations.js.map +1 -1
  3. package/dist/implementation/compiler/inferred.js +3 -2
  4. package/dist/implementation/compiler/inferred.js.map +1 -1
  5. package/dist/implementation/compiler/type-schemas.d.ts +1 -0
  6. package/dist/implementation/compiler/type-schemas.js +35 -2
  7. package/dist/implementation/compiler/type-schemas.js.map +1 -1
  8. package/dist/implementation/world/activate.d.ts +14 -3
  9. package/dist/implementation/world/activate.js +19 -13
  10. package/dist/implementation/world/activate.js.map +1 -1
  11. package/dist/implementation/world/commands.js +3 -2
  12. package/dist/implementation/world/commands.js.map +1 -1
  13. package/dist/implementation/world/create.js +4 -4
  14. package/dist/implementation/world/create.js.map +1 -1
  15. package/dist/implementation/world/execute.js +97 -11
  16. package/dist/implementation/world/execute.js.map +1 -1
  17. package/dist/implementation/world/finalizers.d.ts +10 -2
  18. package/dist/implementation/world/finalizers.js +42 -13
  19. package/dist/implementation/world/finalizers.js.map +1 -1
  20. package/dist/implementation/world/program.js +5 -1
  21. package/dist/implementation/world/program.js.map +1 -1
  22. package/dist/implementation/world/state.d.ts +2 -0
  23. package/dist/implementation/world/state.js +9 -0
  24. package/dist/implementation/world/state.js.map +1 -1
  25. package/dist/implementation/world/synchronize.js +16 -9
  26. package/dist/implementation/world/synchronize.js.map +1 -1
  27. package/dist/implementation/world/tables.d.ts +2 -1
  28. package/dist/implementation/world/tables.js +55 -2
  29. package/dist/implementation/world/tables.js.map +1 -1
  30. package/dist/interface/finalizer/body.d.ts +24 -3
  31. package/dist/interface/finalizer/create.d.ts +3 -3
  32. package/dist/interface/finalizer/create.js +43 -11
  33. package/dist/interface/finalizer/create.js.map +1 -1
  34. package/dist/interface/finalizer/declaration.d.ts +25 -5
  35. package/dist/interface/registry/index.d.ts +1 -1
  36. package/dist/interface/registry/index.js +1 -1
  37. package/dist/interface/registry/index.js.map +1 -1
  38. package/dist/interface/registry/names.d.ts +4 -0
  39. package/dist/interface/registry/names.js +20 -0
  40. package/dist/interface/registry/names.js.map +1 -1
  41. package/dist/interface/registry/schedule.d.ts +1 -0
  42. package/dist/interface/registry/schedule.js +86 -11
  43. package/dist/interface/registry/schedule.js.map +1 -1
  44. package/dist/interface/system/check.js +4 -0
  45. package/dist/interface/system/check.js.map +1 -1
  46. package/dist/interface/system/commands.d.ts +8 -3
  47. package/dist/interface/system/create-resolved.js +2 -1
  48. package/dist/interface/system/create-resolved.js.map +1 -1
  49. package/dist/interface/system/create.js +2 -2
  50. package/dist/interface/system/create.js.map +1 -1
  51. package/dist/interface/system/declaration.d.ts +11 -4
  52. package/dist/interface/system/describe.js +1 -1
  53. package/dist/interface/system/describe.js.map +1 -1
  54. package/dist/interface/system/leaf.d.ts +4 -3
  55. package/dist/interface/system/render.js +2 -2
  56. package/dist/interface/system/render.js.map +1 -1
  57. package/dist/interface/system/resolve.d.ts +12 -3
  58. package/dist/interface/system/resolve.js +35 -4
  59. package/dist/interface/system/resolve.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/guide/01-resources.md +4 -3
  62. package/src/guide/03-systems.out +1 -1
  63. package/src/guide/05-scheduling.md +12 -6
  64. package/src/guide/05-scheduling.out +1 -0
  65. package/src/guide/05-scheduling.ts +17 -0
  66. package/src/guide/06-sync-points.md +14 -6
  67. package/src/guide/06-sync-points.out +2 -0
  68. package/src/guide/06-sync-points.ts +15 -0
  69. package/src/guide/10-finalizers.md +5 -0
  70. package/src/guide/18-identity.out +1 -1
  71. package/src/guide/guide.test.ts +4 -4
  72. package/src/implementation/compiler/augmentations.test.ts +28 -1
  73. package/src/implementation/compiler/augmentations.ts +5 -1
  74. package/src/implementation/compiler/inferred.ts +3 -2
  75. package/src/implementation/compiler/transform.test.ts +4 -4
  76. package/src/implementation/compiler/type-schemas.ts +28 -2
  77. package/src/implementation/world/activate.ts +24 -11
  78. package/src/implementation/world/commands.ts +3 -2
  79. package/src/implementation/world/create.ts +4 -4
  80. package/src/implementation/world/defaults.test.ts +24 -1
  81. package/src/implementation/world/despawn-where.test.ts +31 -8
  82. package/src/implementation/world/execute.ts +98 -9
  83. package/src/implementation/world/finalizers.ts +43 -14
  84. package/src/implementation/world/lifetime.test-helper.ts +1 -1
  85. package/src/implementation/world/passes.test.ts +61 -0
  86. package/src/implementation/world/perf.md +9 -0
  87. package/src/implementation/world/program.ts +4 -1
  88. package/src/implementation/world/run-results.test.ts +17 -0
  89. package/src/implementation/world/schedule.test.ts +42 -1
  90. package/src/implementation/world/state.ts +8 -0
  91. package/src/implementation/world/sync-points.test.ts +12 -0
  92. package/src/implementation/world/synchronize.ts +15 -9
  93. package/src/implementation/world/tables.test.ts +36 -3
  94. package/src/implementation/world/tables.ts +48 -3
  95. package/src/implementation/world/watch-traits-late.test-helper.ts +14 -0
  96. package/src/implementation/world/watch-traits.test-helper.ts +10 -0
  97. package/src/implementation/world/watch-traits.test.ts +62 -0
  98. package/src/interface/finalizer/body.ts +22 -3
  99. package/src/interface/finalizer/create.test.ts +4 -1
  100. package/src/interface/finalizer/create.ts +40 -12
  101. package/src/interface/finalizer/declaration.ts +30 -5
  102. package/src/interface/registry/index.ts +1 -1
  103. package/src/interface/registry/names.ts +19 -0
  104. package/src/interface/registry/schedule.ts +89 -16
  105. package/src/interface/rule/rule.type-test.ts +23 -1
  106. package/src/interface/system/check.ts +2 -0
  107. package/src/interface/system/commands.ts +8 -3
  108. package/src/interface/system/create-resolved.ts +2 -1
  109. package/src/interface/system/create.test.ts +3 -3
  110. package/src/interface/system/create.ts +2 -2
  111. package/src/interface/system/declaration.ts +10 -4
  112. package/src/interface/system/describe.test.ts +19 -0
  113. package/src/interface/system/describe.ts +1 -1
  114. package/src/interface/system/leaf.ts +4 -3
  115. package/src/interface/system/render.ts +2 -2
  116. package/src/interface/system/resolve.test.ts +17 -1
  117. package/src/interface/system/resolve.ts +40 -5
  118. package/src/interface/system/system.type-test.ts +6 -0
  119. package/src/perftest/benchmarks.ts +2 -0
  120. package/src/perftest/chained.ts +108 -0
@@ -6,13 +6,13 @@ import { addFinalizer, programFinalizers } from '../registry/program.js';
6
6
  import { requireRegistered, threadOf } from '../registry/threads.js';
7
7
  import { checkWorldWritten } from '../registry/written.js';
8
8
  import { checkLookup } from '../registry/indexes.js';
9
- import type { Listed, Property } from '../system/property.js';
9
+ import type { Property } from '../system/property.js';
10
10
  import type { ThreadOfComponent } from '../component/value.js';
11
11
  import type { ThreadOfResource } from '../resource/value.js';
12
12
  import type { Resource } from '../resource/resource.js';
13
13
  import type { Component } from '../component/component.js';
14
14
  import type { Elsewhere, OneThread, ThreadedBy, ThreadsOf } from '../system/threads.js';
15
- import type { ChangeSet, Context, Row, Watched, WatchedResources } from './body.js';
15
+ import type { ChangeSet, Context, Row, Watched, WatchedResources, WatchListed } from './body.js';
16
16
  import type { Changes, Leaf } from './created.js';
17
17
  import type { ChangesDeclaration, Declaration, Resolved, ResolvedChanges } from './declaration.js';
18
18
  import { accessOf, compact, lookupsLists, resourceUses, type Access } from '../system/resolve.js';
@@ -28,7 +28,7 @@ type Remote<D> = true extends Elsewhere<ThreadsOf<D>>
28
28
  : {};
29
29
 
30
30
  /** A changes finalizer runs on the world's thread, in the sync point: it watches and reads no other thread's objects. */
31
- type OnWorld<D> = true extends Elsewhere<ThreadsOf<D> | ThreadOfComponent<Listed<D, "watch"> & Component.Key> | ThreadOfResource<Listed<D, "watch"> & Resource.Key>>
31
+ type OnWorld<D extends ChangesDeclaration> = true extends Elsewhere<ThreadsOf<D> | ThreadOfComponent<Extract<WatchListed<D, "components">, Component.Key>> | ThreadOfResource<Extract<WatchListed<D, "resources">, Resource.Key>>>
32
32
  ? { readonly "error: a changes finalizer watches and reads only the world's thread's objects": true }
33
33
  : {};
34
34
 
@@ -127,6 +127,39 @@ export const create = <const D extends Declaration>(
127
127
  return addFinalizer(finalizer, id);
128
128
  };
129
129
 
130
+ const isList = (watch: NonNullable<ChangesDeclaration["watch"]>): watch is Extract<ChangesDeclaration["watch"], readonly unknown[]> => Array.isArray(watch);
131
+
132
+ /** What a changes finalizer watches, sorted by kind. Throws for a key that's neither of the kinds it's listed as, or derived. */
133
+ const watchOf = (name: string, watch: ChangesDeclaration["watch"]): ResolvedChanges["watch"] => {
134
+ const components: symbol[] = [];
135
+ const componentTraits: symbol[] = [];
136
+ const resources: symbol[] = [];
137
+ const resourceTraits: symbol[] = [];
138
+ const sort = (key: unknown, kinds: readonly ("component" | "resource")[]): void => {
139
+ const kind = kindOf(key);
140
+ if (typeof key !== "symbol" || kind === undefined || (kind !== "trait" && !kinds.includes(kind))) {
141
+ const among = kinds.length === 2 ? "" : ` among its ${kinds[0]}s`;
142
+ throw new Error(`Finalizer "${name}" watches "${labelOf(key)}"${among}, which isn't a registered ${kinds.length === 2 ? "component, resource" : kinds[0]} or trait: ${whyUnregistered(key)}.`);
143
+ }
144
+ if (kind === "trait") {
145
+ if (kinds.includes("component")) componentTraits.push(key);
146
+ if (kinds.includes("resource")) resourceTraits.push(key);
147
+ return;
148
+ }
149
+ if (declarationOf(key)?.derived === true) throw new Error(`Finalizer "${name}" watches "${labelOf(key)}", which is derived; it sees only components user code adds and removes.`);
150
+ (kind === "component" ? components : resources).push(key);
151
+ };
152
+ if (watch === undefined) return { components: true, componentTraits, resources, resourceTraits };
153
+ if (isList(watch)) {
154
+ for (const key of watch) sort(key, ["component", "resource"]);
155
+ return { components, componentTraits, resources, resourceTraits };
156
+ }
157
+ const every = watch.components === true;
158
+ if (!every) for (const key of watch.components) sort(key, ["component"]);
159
+ for (const key of watch.resources) sort(key, ["resource"]);
160
+ return { components: every ? true : components, componentTraits: every ? [] : componentTraits, resources, resourceTraits };
161
+ };
162
+
130
163
  /**
131
164
  * Declares a finalizer of each sync point's structural changes, which applies in every world, and returns it. It runs
132
165
  * on the world's thread. Throws as `create` does, for a watched component that isn't registered or is derived, a
@@ -139,7 +172,7 @@ export const changes = <const D extends ChangesDeclaration>(
139
172
  const id = takeIdOfDeclaration(declared);
140
173
  const name = nameOf(declared, id, "finalizer");
141
174
  const what = `Finalizer "${name}"`;
142
- const declaration: ResolvedChanges = { ...resolveShared(what, declared, accessOf(what, declared)), ...declared.watch === undefined ? {} : { watch: declared.watch } };
175
+ const declaration: ResolvedChanges = { ...resolveShared(what, declared, accessOf(what, declared)), watch: watchOf(name, declared.watch) };
143
176
  const finalizer: Changes = { kind: "changes", name, declaration, body };
144
177
  checkResources(name, declaration.resources);
145
178
  requireRegistered("Finalizer", name, "resource", declaration.resources?.read);
@@ -149,14 +182,9 @@ export const changes = <const D extends ChangesDeclaration>(
149
182
  checkLookup("Finalizer", name, lookupName, lookup);
150
183
  for (const keys of [lookup.read, "has" in lookup ? lookup.has : undefined, "not" in lookup ? lookup.not : undefined]) requireRegistered("Finalizer", name, "component", keys);
151
184
  }
152
- for (const key of declaration.watch ?? []) {
153
- const kind = kindOf(key);
154
- if (kind !== "component" && kind !== "resource") throw new Error(`Finalizer "${name}" watches "${labelOf(key)}", which isn't a registered component or resource: ${whyUnregistered(key)}.`);
155
- if (declarationOf(key)?.derived === true) throw new Error(`Finalizer "${name}" watches "${labelOf(key)}", which is derived; it sees only components user code adds and removes.`);
156
- }
157
- const watched = (kind: "component" | "resource"): readonly symbol[] => (declaration.watch ?? []).filter(key => kindOf(key) === kind);
158
- const read = [...declaration.resources?.read ?? [], ...watched("resource")];
159
- const thread = threadOf({ components: { read: watched("component") }, resources: { ...declaration.resources, read }, lookup: declaration.lookup }, "Finalizer", name);
185
+ const { watch } = declaration;
186
+ const read = [...declaration.resources?.read ?? [], ...watch.resources];
187
+ const thread = threadOf({ components: { read: watch.components === true ? [] : watch.components }, resources: { ...declaration.resources, read }, lookup: declaration.lookup }, "Finalizer", name);
160
188
  if (thread !== null && thread !== "world") throw new Error(`Finalizer "${name}" watches or reads values on thread "${thread}"; a changes finalizer runs on the world's thread, in the sync point.`);
161
189
  checkOrder(name, "world", declaration);
162
190
  return addFinalizer(finalizer, id);
@@ -5,6 +5,8 @@ import type { Lookup } from '../system/access.js';
5
5
  import type { Finalizer } from './finalizer.js';
6
6
  import type { Essential } from '../component/value.js';
7
7
  import type { Resource } from '../resource/resource.js';
8
+ import type { Traits } from '../trait/types.js';
9
+ import type { BuiltIn } from '../trait/key.js';
8
10
 
9
11
  /**
10
12
  * What every finalizer declares. What its body reads of its row and lookups, and reads and writes of resources, the
@@ -25,12 +27,23 @@ export type Declaration = Shared & {
25
27
  readonly components: { readonly with?: readonly Component.Key[]; readonly optional?: readonly Component.Key[]; readonly not?: readonly Component.Key[] };
26
28
  };
27
29
 
30
+ /** A trait of your own, which watches every component and resource declaring it. */
31
+ export type OwnTrait = Exclude<keyof Traits<never>, BuiltIn>;
32
+
28
33
  /**
29
- * A finalizer that sees each sync point's structural changes to the components it watches: by default, every one that
30
- * isn't derived or an object a joined thread fills; and the resources it watches, none by default, written since the
31
- * last sync point's finalizers ran.
34
+ * A finalizer that sees each sync point's structural changes to the components it watches, and the resources it
35
+ * watches written since the last sync point's finalizers ran. By default it watches every component that isn't derived
36
+ * or an object a joined thread fills, and no resource. `watch` lists components, resources and traits of your own, or,
37
+ * as an object, every component (`components: true`) or those listed, beside the resources listed. A trait watches each
38
+ * component and resource declaring it, as each registers, but those the default leaves out and resources on another
39
+ * thread.
32
40
  */
33
- export type ChangesDeclaration = Shared & { readonly watch?: readonly (Essential | Resource.Key)[] };
41
+ export type ChangesDeclaration = Shared & {
42
+ readonly watch?: readonly (Essential | Resource.Key | OwnTrait)[] | {
43
+ readonly components: true | readonly (Essential | OwnTrait)[];
44
+ readonly resources: readonly (Resource.Key | OwnTrait)[];
45
+ };
46
+ };
34
47
 
35
48
  type Key = symbol;
36
49
 
@@ -47,4 +60,16 @@ export type Resolved = ResolvedShared & {
47
60
  readonly components: { readonly read?: readonly Key[]; readonly has?: readonly Key[]; readonly optional?: readonly Key[]; readonly not?: readonly Key[] };
48
61
  };
49
62
 
50
- export type ResolvedChanges = ResolvedShared & { readonly watch?: readonly Key[] };
63
+ /**
64
+ * A changes finalizer's declaration as the world takes it. It watches the components listed, or every one (`true`),
65
+ * and those declaring a trait of `componentTraits`; the resources listed, and those declaring a trait of
66
+ * `resourceTraits`.
67
+ */
68
+ export type ResolvedChanges = ResolvedShared & {
69
+ readonly watch: {
70
+ readonly components: true | readonly Key[];
71
+ readonly componentTraits: readonly Key[];
72
+ readonly resources: readonly Key[];
73
+ readonly resourceTraits: readonly Key[];
74
+ };
75
+ };
@@ -3,7 +3,7 @@
3
3
  export type { AnyDeclaration } from './declarations.js';
4
4
  export { schemaOf, threadOfDeclaration } from './declarations.js';
5
5
  export type { Kind } from './names.js';
6
- export { declarationOf, firstKeyOf, keyOfId, kindOf, labelOf, newestKeyOf, whyUnregistered } from './names.js';
6
+ export { declarationOf, declaresTrait, declaredOf, firstKeyOf, keyOfId, keysVersion, kindOf, labelOf, newestKeyOf, whyUnregistered } from './names.js';
7
7
  export type { Item } from './program.js';
8
8
  export { definitionOf, programFinalizers, programRules, programVersion, requireDefinition } from './program.js';
9
9
  export type { Schedule } from './schedule.js';
@@ -22,6 +22,9 @@ type Entry = {
22
22
  /** Every component, resource and user trait, by symbol identity: symbols of one id share an entry. */
23
23
  const entries = new Map<symbol, Entry>();
24
24
 
25
+ /** How many keys were registered, counting a symbol registered under another's id. */
26
+ let registered = 0;
27
+
25
28
  /** Entries by the ids the plugin gave their keys. */
26
29
  const identified = new Map<string, Entry>();
27
30
 
@@ -155,12 +158,28 @@ export const claimKey = (key: unknown, kind: Kind, declaration?: AnyDeclaration)
155
158
  }
156
159
  named.newest = symbol;
157
160
  entries.set(symbol, named);
161
+ registered++;
158
162
  return;
159
163
  }
160
164
  const description = descriptionOf(symbol);
161
165
  const entry: Entry = { kind, declaration, description, id, first: symbol, newest: symbol };
162
166
  if (id !== undefined) identified.set(id, entry);
163
167
  entries.set(symbol, entry);
168
+ registered++;
169
+ };
170
+
171
+ /** How many keys are registered so far, so a world can tell when one registered since it last looked. */
172
+ export const keysVersion = (): number => {
173
+ catchUp();
174
+ return registered;
175
+ };
176
+
177
+ /** Whether a component's or resource's declaration declares one of `traits`, keys of traits of your own. */
178
+ export const declaresTrait = (key: unknown, traits: readonly symbol[]): boolean => {
179
+ const declaration = declarationOf(key);
180
+ if (declaration === undefined) return false;
181
+ for (const declared of Object.getOwnPropertySymbols(declaration)) for (const trait of traits) if (sameKey(declared, trait)) return true;
182
+ return false;
164
183
  };
165
184
 
166
185
  /** The symbol last registered under an id, by which a world gives values, or undefined where none was. */
@@ -53,8 +53,8 @@ type Graph = {
53
53
  readonly successors: readonly Set<number>[];
54
54
  /** Each unit's members, by unit: none for a leaf. */
55
55
  readonly members: readonly (readonly number[])[];
56
- /** The units a list of systems names, readonly systems left out. */
57
- readonly resolve: (systems: readonly object[] | undefined) => readonly number[];
56
+ /** The units a list of systems names, readonly systems and resources left out. */
57
+ readonly resolve: (systems: readonly unknown[] | undefined) => readonly number[];
58
58
  };
59
59
 
60
60
  /**
@@ -79,8 +79,9 @@ const graphOf = (root: Definition): Graph => {
79
79
  visit(root);
80
80
  const successors = units.flatMap(() => [new Set<number>(), new Set<number>()]);
81
81
  // A readonly system is ordered with nothing, so constraints naming one are ignored.
82
- const resolve = (systems: readonly object[] | undefined): readonly number[] =>
82
+ const resolve = (systems: readonly unknown[] | undefined): readonly number[] =>
83
83
  (systems ?? []).flatMap(system => {
84
+ if (typeof system !== "object" || system === null) return [];
84
85
  const unit = indexOf.get(firstItemOf(system));
85
86
  return unit === undefined || isObserver(units[unit]) ? [] : [unit];
86
87
  });
@@ -149,7 +150,7 @@ const depthsOf = ({ units, successors }: Graph, order: readonly number[]): numbe
149
150
  return depth;
150
151
  };
151
152
 
152
- type Access = { readonly reads: ReadonlySet<Key>; readonly writes: ReadonlySet<Key>; readonly reduces: ReadonlySet<Key> };
153
+ type Access = { readonly reads: ReadonlySet<Key>; readonly writes: ReadonlySet<Key>; readonly reduces: ReadonlySet<Key>; readonly leaf: Leaf };
153
154
 
154
155
  /** The resources a leaf reduces into. */
155
156
  export const reducedBy = (leaf: Leaf): readonly Key[] => {
@@ -168,21 +169,53 @@ export const accessOf = (leaf: Leaf): Access => {
168
169
  for (const key of orderKeysOf(leaf.declaration)) reads.add(firstKeyOf(key));
169
170
  if (up !== undefined) for (const key of [up.through, ...up.read ?? []]) reads.add(firstKeyOf(key));
170
171
  const writes = new Set<Key>([...components?.write ?? [], ...resources?.write ?? []].map(firstKeyOf));
171
- return { reads, writes, reduces: new Set(reducedBy(leaf).map(firstKeyOf)) };
172
+ return { reads, writes, reduces: new Set(reducedBy(leaf).map(firstKeyOf)), leaf };
172
173
  };
173
174
 
174
- /** Contributors to one resource don't conflict with each other: their fold doesn't depend on which runs first. */
175
+ const names = (list: readonly Key[] | undefined, key: Key): boolean => list !== undefined && list.some(item => firstKeyOf(item) === key);
176
+
177
+ /** Whether a leaf reaches a key only through its rows: neither looked up, read through `up`, indexed nor ordered by. */
178
+ const onlyThroughRows = (leaf: Leaf, key: Key): boolean => {
179
+ const { components, lookup, up } = leaf.declaration;
180
+ if (!names(components?.read, key) && !names(components?.write, key) && !names(components?.added, key) && !names(components?.changed, key)) return false;
181
+ for (const name in lookup) if (names(lookup[name].read, key) || names(lookupKeysOf(lookup[name]), key)) return false;
182
+ if (names(orderKeysOf(leaf.declaration), key)) return false;
183
+ return up === undefined || (firstKeyOf(up.through) !== key && !names(up.read, key));
184
+ };
185
+
186
+ /** Whether no entity can match both leaves' rows: one requires a component the other's `not` lists. */
187
+ const exclusive = (a: Leaf, b: Leaf): boolean => {
188
+ const requiresExcluded = (one: Leaf, other: Leaf): boolean => {
189
+ const { components } = one.declaration;
190
+ const not = other.declaration.components?.not;
191
+ if (components === undefined || not === undefined || not.length === 0) return false;
192
+ for (const list of [components.read, components.write, components.has, components.added, components.changed]) {
193
+ for (const key of list ?? []) if (!names(components.optional, firstKeyOf(key)) && names(not, firstKeyOf(key))) return true;
194
+ }
195
+ return false;
196
+ };
197
+ return requiresExcluded(a, b) || requiresExcluded(b, a);
198
+ };
199
+
200
+ /** Whether a conflict on `key` is waived: both reach it only through rows that never match one entity. */
201
+ const waived = (a: Access, b: Access, key: Key): boolean => onlyThroughRows(a.leaf, key) && onlyThroughRows(b.leaf, key) && exclusive(a.leaf, b.leaf);
202
+
203
+ /**
204
+ * The first key two accesses conflict on, one writing it, or undefined. Contributors to one resource don't conflict
205
+ * with each other: their fold doesn't depend on which runs first. Nor do two whose rows never match one entity on a key
206
+ * both reach only through their rows.
207
+ */
175
208
  const conflictOf = (a: Access, b: Access): Key | undefined => {
176
- for (const key of a.writes) if (b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)) return key;
177
- for (const key of b.writes) if (a.reads.has(key) || a.reduces.has(key)) return key;
209
+ for (const key of a.writes) if ((b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)) && !waived(a, b, key)) return key;
210
+ for (const key of b.writes) if ((a.reads.has(key) || a.reduces.has(key)) && !waived(a, b, key)) return key;
178
211
  return undefined;
179
212
  };
180
213
 
181
- /** Every key two accesses conflict on. */
214
+ /** Every key two accesses conflict on, as `conflictOf` finds the first. */
182
215
  const conflictsOf = (a: Access, b: Access): readonly Key[] => [...new Set([
183
216
  ...[...a.writes].filter(key => b.writes.has(key) || b.reads.has(key) || b.reduces.has(key)),
184
217
  ...[...b.writes].filter(key => a.reads.has(key) || a.reduces.has(key)),
185
- ])];
218
+ ])].filter(key => !waived(a, b, key));
186
219
 
187
220
  /** A system's leaves in its tiers, with their units and accesses, and its graph with readers of folds after their contributors. */
188
221
  const orderedOf = (system: Definition) => {
@@ -199,7 +232,46 @@ const orderedOf = (system: Definition) => {
199
232
  });
200
233
  }
201
234
  });
202
- return { graph, leaves, accesses, reductions };
235
+ return { graph, leaves, accesses, reductions, keyed: orderByResources(graph, leaves, accesses) };
236
+ };
237
+
238
+ const noneKeyed: ReadonlyMap<string, "before" | "after"> = new Map();
239
+
240
+ const listsResource = (list: readonly unknown[] | undefined): boolean => list !== undefined && list.some(item => typeof item === "symbol");
241
+
242
+ /** The resources a list of `before` or `after` names, each by its first symbol. */
243
+ const resourcesIn = (list: readonly unknown[] | undefined): readonly Key[] =>
244
+ (list ?? []).flatMap(item => typeof item === "symbol" ? [firstKeyOf(item)] : []);
245
+
246
+ /**
247
+ * Orders each leaf naming a resource in `before` or `after` before or after every other leaf using it, adding the edges
248
+ * to `graph`: but one its constraints already order the other way, and one naming the resource in the same list. By
249
+ * unit, as `"<earlier> <later>"`, the list that ordered each pair.
250
+ */
251
+ const orderByResources = (graph: Graph, leaves: readonly { readonly leaf: Leaf; readonly unit: number }[], accesses: readonly Access[]): ReadonlyMap<string, "before" | "after"> => {
252
+ if (!leaves.some(({ leaf }) => listsResource(leaf.declaration.before) || listsResource(leaf.declaration.after))) return noneKeyed;
253
+ const keyed = new Map<string, "before" | "after">();
254
+ const lists = leaves.map(({ leaf }) => ({ before: resourcesIn(leaf.declaration.before), after: resourcesIn(leaf.declaration.after) }));
255
+ const uses = leaves.map(({ leaf }, index) => new Set([...accesses[index].reads, ...accesses[index].writes, ...accesses[index].reduces,
256
+ ...[...leaf.declaration.resources?.is ?? [], ...leaf.declaration.resources?.not ?? []].map(firstKeyOf)]));
257
+ const reach = reachability(topologicalOrder(graph), graph.successors);
258
+ const edges: (readonly [number, number, "before" | "after"])[] = [];
259
+ leaves.forEach(({ unit }, index) => {
260
+ for (const list of ["before", "after"] as const) {
261
+ for (const key of lists[index][list]) {
262
+ leaves.forEach((other, o) => {
263
+ if (o === index || !uses[o].has(key) || lists[o][list].includes(key)) return;
264
+ const [earlier, later] = list === "before" ? [unit, other.unit] : [other.unit, unit];
265
+ if (!reaches(reach, end(later), start(earlier))) edges.push([earlier, later, list]);
266
+ });
267
+ }
268
+ }
269
+ });
270
+ for (const [earlier, later, list] of edges) {
271
+ graph.successors[end(earlier)].add(start(later));
272
+ keyed.set(`${earlier} ${later}`, list);
273
+ }
274
+ return keyed;
203
275
  };
204
276
 
205
277
  /**
@@ -214,7 +286,7 @@ export type Placement = { readonly leaf: Leaf; readonly because: "after" | "befo
214
286
  * leaf of its tiers.
215
287
  */
216
288
  export const placementsOf = (system: Definition): ReadonlyMap<Leaf, readonly Placement[]> => {
217
- const { graph, leaves, accesses } = orderedOf(system);
289
+ const { graph, leaves, accesses, keyed } = orderedOf(system);
218
290
  const reach = reachability(topologicalOrder(graph), graph.successors);
219
291
  const ancestors = graph.units.map(() => [] as number[]);
220
292
  graph.members.forEach((members, unit) => {
@@ -225,7 +297,7 @@ export const placementsOf = (system: Definition): ReadonlyMap<Leaf, readonly Pla
225
297
  for (const member of members) visit(member);
226
298
  });
227
299
  /** Whether `systems` names `unit` or a composite holding it. */
228
- const names = (systems: readonly object[] | undefined, unit: number): boolean => {
300
+ const names = (systems: readonly unknown[] | undefined, unit: number): boolean => {
229
301
  const named = graph.resolve(systems);
230
302
  return named.includes(unit) || ancestors[unit].some(holder => named.includes(holder));
231
303
  };
@@ -239,7 +311,7 @@ export const placementsOf = (system: Definition): ReadonlyMap<Leaf, readonly Pla
239
311
  const other = leaves[a];
240
312
  const because = constrains(unit, "after", other.unit) ? "after"
241
313
  : constrains(other.unit, "before", unit) ? "before"
242
- : [...accesses[a].reduces].some(key => accesses[b].reads.has(key)) ? "reduction" : "during";
314
+ : keyed.get(`${other.unit} ${unit}`) ?? ([...accesses[a].reduces].some(key => accesses[b].reads.has(key)) ? "reduction" : "during");
243
315
  return { leaf: other.leaf, because, conflicts: conflictsOf(accesses[a], accesses[b]) };
244
316
  }));
245
317
  });
@@ -271,7 +343,8 @@ const createSchedule = (system: Definition): Schedule => {
271
343
  if (key !== undefined && !ordered(reach, leaves[a].unit, leaves[b].unit)) {
272
344
  const whole = [leaves[a].leaf, leaves[b].leaf].filter(leaf => leaf.declaration.components?.whole === true)
273
345
  .map(leaf => ` "${leaf.name}" takes its row whole (components.whole), so it writes every member.`).join("");
274
- throw new Error(`Systems "${leaves[a].leaf.name}" and "${leaves[b].leaf.name}" both access "${labelOf(key)}" and one writes it; order them with before or after.${whole}`);
346
+ const rows = onlyThroughRows(leaves[a].leaf, key) && onlyThroughRows(leaves[b].leaf, key) ? ", or, if they never match one entity, add a component of one to the other's not" : "";
347
+ throw new Error(`Systems "${leaves[a].leaf.name}" and "${leaves[b].leaf.name}" both access "${labelOf(key)}" and one writes it; order them with before or after${rows}.${whole}`);
275
348
  }
276
349
  }
277
350
  }
@@ -305,7 +378,7 @@ export type SyncReason = { readonly from: Leaf; readonly to: Leaf; readonly keys
305
378
 
306
379
  /** The components a leaf's commands may add or remove, by spawn, add or remove; `"any"` once it despawns or isn't followed. */
307
380
  const structuralOf = (leaf: Leaf): ReadonlySet<Key> | "any" | null => {
308
- const { commands } = leaf;
381
+ const commands = leaf.commands();
309
382
  if (commands === undefined) return null;
310
383
  if (commands === null || commands.despawn === true) return "any";
311
384
  const keys = new Set([...commands.spawn ?? [], ...commands.add ?? [], ...commands.remove ?? []].map(firstKeyOf));
@@ -7,7 +7,7 @@ import { System } from '../system/system.js';
7
7
  import type { Create } from '../world/create.js';
8
8
  import type { World } from '../world/world.js';
9
9
  import { Rule } from './rule.js';
10
- import type { Writable } from '../component/value.js';
10
+ import type { Essential, Writable } from '../component/value.js';
11
11
  import type { ChangeSet } from '../finalizer/body.js';
12
12
 
13
13
  type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
@@ -18,6 +18,9 @@ const hidden = Symbol("ruleTypeTest/hidden");
18
18
  const slot = Symbol("ruleTypeTest/slot");
19
19
  const parent = Symbol("ruleTypeTest/parent");
20
20
  const time = Symbol("ruleTypeTest/time");
21
+ const inDocument = Symbol("ruleTypeTest/inDocument");
22
+ const kept = Symbol("ruleTypeTest/kept");
23
+ const saved = Symbol("ruleTypeTest/saved");
21
24
 
22
25
  declare module '@adobe/ecs' {
23
26
  export interface Components {
@@ -25,9 +28,14 @@ declare module '@adobe/ecs' {
25
28
  [hidden]: { type: true };
26
29
  [slot]: { type: number; derived: true };
27
30
  [parent]: { type: Entity; reference: "owner" };
31
+ [kept]: { type: number; [inDocument]: true };
28
32
  }
29
33
  export interface Resources {
30
34
  [time]: { type: number };
35
+ [saved]: { type: number; [inDocument]: true };
36
+ }
37
+ export interface Traits<N> {
38
+ readonly [inDocument]: N extends unknown ? true : never;
31
39
  }
32
40
  }
33
41
 
@@ -81,6 +89,20 @@ Finalizer.changes({ watch: [source, parent] }, (changes, { commands }) => {
81
89
  // @ts-expect-error changes finalizers don't watch derived components
82
90
  Finalizer.changes({ watch: [slot] }, () => {});
83
91
 
92
+ // A trait of your own watches every component and resource declaring it.
93
+ Finalizer.changes({ watch: [inDocument, source] }, changes => {
94
+ check<Equal<typeof changes, ChangeSet<typeof kept | typeof source, typeof saved>>>();
95
+ });
96
+ // Every component, and the resources chosen, by key or trait.
97
+ Finalizer.changes({ watch: { components: true, resources: [time, inDocument] } }, changes => {
98
+ check<Equal<typeof changes, ChangeSet<Essential, typeof time | typeof saved>>>();
99
+ });
100
+ Finalizer.changes({ watch: { components: [inDocument], resources: [] } }, changes => {
101
+ check<Equal<typeof changes, ChangeSet<typeof kept>>>();
102
+ });
103
+ // @ts-expect-error components lists components and traits, resources resources and traits
104
+ Finalizer.changes({ watch: { components: [time], resources: [source] } }, () => {});
105
+
84
106
  // References change only through commands, so systems can't write them.
85
107
  System.create({ components: { with: [parent, source] } }, row => { row[source] = row[parent]; });
86
108
  System.create({ components: { with: [parent] } }, row => {
@@ -220,6 +220,8 @@ export const checkLeaf = (leaf: Leaf): void => {
220
220
  threadOf(declaration, "System", name);
221
221
  requireRegistered("System", name, "component", components?.has);
222
222
  requireRegistered("System", name, "component", components?.not);
223
+ if (declaration.before !== undefined) requireRegistered("System", name, "resource", declaration.before.filter(item => typeof item === "symbol"));
224
+ if (declaration.after !== undefined) requireRegistered("System", name, "resource", declaration.after.filter(item => typeof item === "symbol"));
223
225
  for (const lookupName in lookup) {
224
226
  const found = lookup[lookupName];
225
227
  requireRegistered("System", name, "component", "has" in found ? found.has : undefined);
@@ -65,9 +65,9 @@ export type Commands = {
65
65
  ): (...values: SpawnValues<C, P>) => Entity;
66
66
  despawn(entity: Entity): void;
67
67
  /**
68
- * Despawns, as a `despawn` of each would, every entity that has each component of `with` and none of `not` once the
69
- * commands issued before it apply, and every entity spawned at the same sync point that does, whenever spawned.
70
- * Derived components may be named. `where` keeps it to the archetypes holding these values of shared components,
68
+ * Despawns, as a `despawn` of each would, every entity alive before its sync point that has each component of
69
+ * `with` and none of `not` once the commands issued before it apply, as a row system over the same query despawning
70
+ * each of its rows would; never one the sync point spawns, whenever spawned. Derived components may be named. `where` keeps it to the archetypes holding these values of shared components,
71
71
  * each one required as `with` requires it, as a system's `where` of values does. Never the resource entity. Only on
72
72
  * the world's thread: a joined thread's throws.
73
73
  */
@@ -94,11 +94,16 @@ export type Commands = {
94
94
  * - `existing` maps entities to ones already alive in this world, which stand for them: a row whose entity it maps
95
95
  * isn't spawned, its entry in the result is the entity it maps to, and every value naming one it maps names that
96
96
  * entity, whether or not it's in the tables. Fails for one that isn't alive.
97
+ * - `update` writes each row `existing` maps onto the entity standing for it, as `commands.add` of its table's
98
+ * components would: adding those the entity lacks, replacing and marking changed those it has, and leaving its
99
+ * other components as they are. Its entity values are rewritten as a spawned row's are, but a reference naming an
100
+ * entity that isn't there, weak or owner, is left out, so the entity's own stands.
97
101
  * - `resources` sets each resource it names, by key, at the sync point, as commands apply, each value checked
98
102
  * against its resource's schema; not one only the world writes, nor one living on another thread.
99
103
  */
100
104
  spawnTables(tables: readonly Table[], options?: {
101
105
  readonly resources?: { readonly [key: symbol]: unknown };
102
106
  readonly existing?: ReadonlyMap<Entity, Entity>;
107
+ readonly update?: boolean;
103
108
  }): Table.Spawned;
104
109
  };
@@ -3,6 +3,7 @@ import { nameOf } from '../registry/items.js';
3
3
  import { claimSystem } from '../registry/program.js';
4
4
  import { checkLeaf, inputSchemaOf } from './check.js';
5
5
  import type { Resolved } from './declaration.js';
6
+ import { noCommands } from './resolve.js';
6
7
  import type { Leaf } from './leaf.js';
7
8
  import type { System } from './system.js';
8
9
 
@@ -14,7 +15,7 @@ import type { System } from './system.js';
14
15
  export const createResolved = (declaration: Resolved, body: Leaf["body"], input?: unknown): System<unknown> => {
15
16
  const name = nameOf(declaration, undefined, "system");
16
17
  const schema = inputSchemaOf(name, body, declaration.components === undefined ? 1 : 2, input);
17
- const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands: undefined };
18
+ const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands: noCommands };
18
19
  checkLeaf(system);
19
20
  claimSystem(system, undefined);
20
21
  return system;
@@ -49,7 +49,7 @@ describe("System.create", () => {
49
49
  const body = (): void => {};
50
50
  // A body the build doesn't see is given what it uses.
51
51
  const move = System.create(giveAccess({ components: { with: [position] } }, { write: [position], resources: { read: [time] } }), body);
52
- expect(definitionOf(move)).toEqual({ kind: "system", name: "move", declaration: { components: { write: [position] }, resources: { read: [time] } }, body });
52
+ expect(definitionOf(move)).toEqual({ kind: "system", name: "move", declaration: { components: { write: [position] }, resources: { read: [time] } }, body, commands: expect.any(Function) });
53
53
  expect(move.name).toBe("move");
54
54
  expect(definitionOf({ name: "move" })).toBe(undefined);
55
55
  });
@@ -71,7 +71,7 @@ describe("System.create", () => {
71
71
  it("labels a system made with neither an id nor a binding by its kind numbered", () => {
72
72
  const body = ({ commands }: { readonly commands: System.Commands }): void => { commands.spawn([position])({ [position]: 0 }); };
73
73
  const [alone] = [System.create(giveAccess(identify(body, "setup.ts#spawnOnce")))];
74
- expect(definitionOf(alone)).toEqual({ kind: "system", name: "spawnOnce", declaration: {}, body });
74
+ expect(definitionOf(alone)).toEqual({ kind: "system", name: "spawnOnce", declaration: {}, body, commands: expect.any(Function) });
75
75
  const [first, second] = [System.create(() => {}), System.create({ systems: [alone] })].map(system => system.name);
76
76
  const number = Number(first?.slice("system #".length));
77
77
  expect([first, second]).toEqual([`system #${number}`, `system #${number + 1}`]);
@@ -177,7 +177,7 @@ describe("System.create of a composite", () => {
177
177
  const reader = System.create({ components: { with: [position] } }, row => { void row[position]; });
178
178
  const definition = definitionOf(whole);
179
179
  expect(definition?.kind === "system" ? definition.declaration.components : null).toEqual({ read: [parent], write: [position], whole: true });
180
- expect(() => System.create([whole, reader])).toThrow('both access "typesSystemTest/position" and one writes it; order them with before or after. "whole" takes its row whole (components.whole), so it writes every member.');
180
+ expect(() => System.create([whole, reader])).toThrow(`both access "typesSystemTest/position" and one writes it; order them with before or after, or, if they never match one entity, add a component of one to the other's not. "whole" takes its row whole (components.whole), so it writes every member.`);
181
181
  });
182
182
  it("runs readonly members after its sync point, in no tier, never conflicting and ignoring constraints naming them", () => {
183
183
  const write = System.create({ components: { with: [position] } }, row => { row[position] += 1; });
@@ -18,7 +18,7 @@ import type { OneThread, ThreadedBy, ThreadsOf } from './threads.js';
18
18
  import type { HeldBy } from './values.js';
19
19
  import type { SharedOnly } from './shared-only.js';
20
20
  import type { Shared } from '../component/value.js';
21
- import { accessOf, commandLists, compact, componentLists, lookupsLists, resourceUses, upReads, whereReadsOf } from './resolve.js';
21
+ import { accessOf, commandListsOf, compact, componentLists, lookupsLists, resourceUses, upReads, whereReadsOf } from './resolve.js';
22
22
 
23
23
  type Lookups<D> = Property<D, "lookup">;
24
24
 
@@ -181,7 +181,7 @@ export function create(first: Declaration | Leaf["body"] | readonly System<unkno
181
181
  const access = accessOf(`System "${name}"`, first);
182
182
  const declaration = resolve(`System "${name}"`, declared, access);
183
183
  const schema = inputSchemaOf(name, body, declaration.components === undefined ? 1 : 2, input);
184
- const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands: commandLists(access) };
184
+ const system: Leaf & System<unknown> = { kind: "system", name, declaration, input: schema, body, commands: commandListsOf(`System "${name}"`, access) };
185
185
  checkLeaf(system);
186
186
  claimSystem(system, id);
187
187
  return system;
@@ -52,10 +52,16 @@ export type Declaration = {
52
52
  readonly target?: string;
53
53
  /** Makes this a render system (see `render`): its body records GPU work over the GPU copy of the components it names. */
54
54
  readonly shader?: Shader;
55
- /** Hard constraint. */
56
- readonly before?: readonly System<unknown>[];
57
- /** Hard constraint. */
58
- readonly after?: readonly System<unknown>[];
55
+ /**
56
+ * Hard constraint: runs before each system listed, and, for a resource listed, before every other system of the
57
+ * composite that uses it, but those its constraints already order before it and those listing it in `before` too.
58
+ */
59
+ readonly before?: readonly (System<unknown> | Resource.Key)[];
60
+ /**
61
+ * Hard constraint: runs after each system listed, and, for a resource listed, after every other system of the
62
+ * composite that uses it, but those its constraints already order after it and those listing it in `after` too.
63
+ */
64
+ readonly after?: readonly (System<unknown> | Resource.Key)[];
59
65
  /** Runs inside the span of the systems listed: after what they follow and before what follows them. */
60
66
  readonly during?: readonly System<unknown>[];
61
67
  /**
@@ -56,6 +56,25 @@ describe("System.describe", () => {
56
56
  const tally = System.create({ after: [count] }, ({ resources }) => { resources[hover] = resources[speed]; });
57
57
  expect(System.describe(System.create([clear, count, tally])).tiers.map(tier => tier.sync)).toEqual([[{ from: clear, to: count, keys: [] }], null, null]);
58
58
  });
59
+ it("lists no conflict on a key two systems reach through rows that never match one entity", () => {
60
+ const placeCells = System.create({ components: { with: [position, cell] } }, row => { row[cell] = row[position]; });
61
+ const clearCells = System.create({ components: { with: [cell], not: [position] }, after: [placeCells] }, row => { row[cell] = 0; });
62
+ expect(System.describe(System.create([placeCells, clearCells])).tiers[1]?.systems[0]?.after).toEqual([{ system: placeCells, name: "placeCells", because: "after", conflicts: [] }]);
63
+ });
64
+ it("says a system follows another by a resource one lists in before or after", () => {
65
+ const setSpeed = System.create({ before: [speed] }, ({ resources }) => { resources[speed] = 2; });
66
+ const readSpeed = System.create({ components: { with: [position] } }, (row, { resources }) => { row[position] = resources[speed]; });
67
+ const readHover = System.create({ after: [hover] }, ({ resources }) => resources[hover] + resources[speed]);
68
+ const setHover = System.create({ after: [setSpeed] }, ({ resources }) => { resources[hover] = 1; });
69
+ const described = System.describe(System.create([readHover, readSpeed, setHover, setSpeed]));
70
+ expect(described.tiers.map(tier => tier.systems.map(system => [system.name, system.after.map(placement => [placement.name, placement.because])]))).toEqual([
71
+ [["setSpeed", []]],
72
+ [["readSpeed", [["setSpeed", "before"]]], ["setHover", [["setSpeed", "after"]]]],
73
+ [["readHover", [["setHover", "after"]]]],
74
+ ]);
75
+ // @ts-expect-error ordering takes resources, not components
76
+ expect(() => System.create({ after: [position] }, () => {})).toThrow(`uses resource "describeTest/position", which isn't a registered resource`);
77
+ });
59
78
  it("gives a text form to read", () => {
60
79
  expect(System.describe(frame).text).toBe([
61
80
  "tier 0 spawnCells commands spawn describeTest/position",
@@ -16,7 +16,7 @@ const asSystem = (leaf: Leaf): System<unknown, unknown> => leaf;
16
16
  /** What the build inferred one system uses, and what placed it, as plain data. */
17
17
  const describeLeaf = (leaf: Leaf, placements: readonly Placement[]) => {
18
18
  const { components, resources, lookup, up, order } = leaf.declaration;
19
- const { commands } = leaf;
19
+ const commands = leaf.commands();
20
20
  return {
21
21
  /** The system, the value its constructor returned. */
22
22
  system: asSystem(leaf),
@@ -19,8 +19,9 @@ export type Leaf = {
19
19
  readonly input: InputSchema | undefined;
20
20
  readonly body: (...parameters: never[]) => void;
21
21
  /**
22
- * What its body's commands name, as the build inferred them: null where it couldn't follow them, undefined for
23
- * none. Apart from its declaration, so a module evaluated again with other commands declares the same system.
22
+ * What its body's commands name, as the build inferred them, read at first need: null where it couldn't follow
23
+ * them, undefined for none. Apart from its declaration, so a module evaluated again with other commands declares the
24
+ * same system.
24
25
  */
25
- readonly commands?: CommandLists | null;
26
+ readonly commands: () => CommandLists | null | undefined;
26
27
  };