@adobe/ecs 0.2.1 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. package/README.md +2 -2
  2. package/dist/implementation/compiler/body-access.d.ts +18 -1
  3. package/dist/implementation/compiler/body-access.js +104 -15
  4. package/dist/implementation/compiler/body-access.js.map +1 -1
  5. package/dist/implementation/compiler/describe.d.ts +1 -1
  6. package/dist/implementation/compiler/describe.js +3 -3
  7. package/dist/implementation/compiler/describe.js.map +1 -1
  8. package/dist/implementation/compiler/estree.d.ts +5 -2
  9. package/dist/implementation/compiler/estree.js +8 -5
  10. package/dist/implementation/compiler/estree.js.map +1 -1
  11. package/dist/implementation/compiler/inferred.js +4 -2
  12. package/dist/implementation/compiler/inferred.js.map +1 -1
  13. package/dist/implementation/compiler/signature.js +1 -1
  14. package/dist/implementation/compiler/transform.js +6 -5
  15. package/dist/implementation/compiler/transform.js.map +1 -1
  16. package/dist/implementation/world/activate.js +14 -2
  17. package/dist/implementation/world/activate.js.map +1 -1
  18. package/dist/implementation/world/buffer.d.ts +7 -0
  19. package/dist/implementation/world/buffer.js +7 -0
  20. package/dist/implementation/world/buffer.js.map +1 -1
  21. package/dist/implementation/world/cascade.d.ts +10 -3
  22. package/dist/implementation/world/cascade.js +48 -23
  23. package/dist/implementation/world/cascade.js.map +1 -1
  24. package/dist/implementation/world/commands.js +86 -2
  25. package/dist/implementation/world/commands.js.map +1 -1
  26. package/dist/implementation/world/create.js +53 -2
  27. package/dist/implementation/world/create.js.map +1 -1
  28. package/dist/implementation/world/entities.d.ts +2 -0
  29. package/dist/implementation/world/entities.js +6 -0
  30. package/dist/implementation/world/entities.js.map +1 -1
  31. package/dist/implementation/world/execute.d.ts +4 -4
  32. package/dist/implementation/world/execute.js +132 -83
  33. package/dist/implementation/world/execute.js.map +1 -1
  34. package/dist/implementation/world/finalizers.d.ts +2 -0
  35. package/dist/implementation/world/finalizers.js +24 -3
  36. package/dist/implementation/world/finalizers.js.map +1 -1
  37. package/dist/implementation/world/join.js +22 -4
  38. package/dist/implementation/world/join.js.map +1 -1
  39. package/dist/implementation/world/register.js +2 -2
  40. package/dist/implementation/world/resource-changes.d.ts +18 -0
  41. package/dist/implementation/world/resource-changes.js +66 -0
  42. package/dist/implementation/world/resource-changes.js.map +1 -0
  43. package/dist/implementation/world/rows.d.ts +1 -1
  44. package/dist/implementation/world/rows.js +23 -13
  45. package/dist/implementation/world/rows.js.map +1 -1
  46. package/dist/implementation/world/run-changes.d.ts +38 -0
  47. package/dist/implementation/world/run-changes.js +138 -0
  48. package/dist/implementation/world/run-changes.js.map +1 -0
  49. package/dist/implementation/world/runner.js +19 -15
  50. package/dist/implementation/world/runner.js.map +1 -1
  51. package/dist/implementation/world/sharing.js +1 -1
  52. package/dist/implementation/world/staging.d.ts +4 -0
  53. package/dist/implementation/world/staging.js +15 -3
  54. package/dist/implementation/world/staging.js.map +1 -1
  55. package/dist/implementation/world/state.d.ts +13 -0
  56. package/dist/implementation/world/state.js +13 -1
  57. package/dist/implementation/world/state.js.map +1 -1
  58. package/dist/implementation/world/synchronize.js +72 -22
  59. package/dist/implementation/world/synchronize.js.map +1 -1
  60. package/dist/implementation/world/tables-read.d.ts +9 -0
  61. package/dist/implementation/world/tables-read.js +72 -0
  62. package/dist/implementation/world/tables-read.js.map +1 -0
  63. package/dist/implementation/world/tables.d.ts +10 -0
  64. package/dist/implementation/world/tables.js +401 -0
  65. package/dist/implementation/world/tables.js.map +1 -0
  66. package/dist/implementation/world/thread-observer.js +1 -1
  67. package/dist/implementation/world/thread-observer.js.map +1 -1
  68. package/dist/implementation/world/thread-systems.js +11 -6
  69. package/dist/implementation/world/thread-systems.js.map +1 -1
  70. package/dist/implementation/world/up.js +7 -1
  71. package/dist/implementation/world/up.js.map +1 -1
  72. package/dist/implementation/world/where.d.ts +8 -0
  73. package/dist/implementation/world/where.js +40 -11
  74. package/dist/implementation/world/where.js.map +1 -1
  75. package/dist/index.d.ts +8 -28
  76. package/dist/index.js +4 -9
  77. package/dist/index.js.map +1 -1
  78. package/dist/interface/component/declarations.d.ts +4 -0
  79. package/dist/interface/component/declarations.js +8 -0
  80. package/dist/interface/component/declarations.js.map +1 -0
  81. package/dist/interface/component/declared.d.ts +12 -0
  82. package/dist/interface/component/declared.js +2 -0
  83. package/dist/interface/component/declared.js.map +1 -0
  84. package/dist/interface/component/index.d.ts +2 -0
  85. package/dist/interface/component/index.js +1 -1
  86. package/dist/interface/component/index.js.map +1 -1
  87. package/dist/interface/finalizer/body.d.ts +18 -3
  88. package/dist/interface/finalizer/create.d.ts +7 -5
  89. package/dist/interface/finalizer/create.js +8 -5
  90. package/dist/interface/finalizer/create.js.map +1 -1
  91. package/dist/interface/finalizer/declaration.d.ts +4 -2
  92. package/dist/interface/indexes/declaration-of.d.ts +17 -0
  93. package/dist/interface/indexes/declaration-of.js +20 -0
  94. package/dist/interface/indexes/declaration-of.js.map +1 -0
  95. package/dist/interface/indexes/index.d.ts +1 -0
  96. package/dist/interface/indexes/index.js +1 -0
  97. package/dist/interface/indexes/index.js.map +1 -1
  98. package/dist/interface/registry/names.d.ts +2 -0
  99. package/dist/interface/registry/names.js +9 -0
  100. package/dist/interface/registry/names.js.map +1 -1
  101. package/dist/interface/registry/schedule.d.ts +31 -4
  102. package/dist/interface/registry/schedule.js +185 -24
  103. package/dist/interface/registry/schedule.js.map +1 -1
  104. package/dist/interface/resource/declarations.d.ts +4 -0
  105. package/dist/interface/resource/declarations.js +8 -0
  106. package/dist/interface/resource/declarations.js.map +1 -0
  107. package/dist/interface/resource/index.d.ts +2 -0
  108. package/dist/interface/resource/index.js +1 -1
  109. package/dist/interface/resource/index.js.map +1 -1
  110. package/dist/interface/system/access.d.ts +15 -0
  111. package/dist/interface/system/body.d.ts +11 -6
  112. package/dist/interface/system/check.d.ts +8 -1
  113. package/dist/interface/system/check.js +42 -12
  114. package/dist/interface/system/check.js.map +1 -1
  115. package/dist/interface/system/commands.d.ts +37 -0
  116. package/dist/interface/system/create-resolved.js +1 -1
  117. package/dist/interface/system/create-resolved.js.map +1 -1
  118. package/dist/interface/system/create.d.ts +31 -15
  119. package/dist/interface/system/create.js +8 -7
  120. package/dist/interface/system/create.js.map +1 -1
  121. package/dist/interface/system/declaration.d.ts +9 -2
  122. package/dist/interface/system/describe.d.ts +120 -0
  123. package/dist/interface/system/describe.js +116 -0
  124. package/dist/interface/system/describe.js.map +1 -0
  125. package/dist/interface/system/index.d.ts +1 -0
  126. package/dist/interface/system/index.js +1 -0
  127. package/dist/interface/system/index.js.map +1 -1
  128. package/dist/interface/system/leaf.d.ts +6 -0
  129. package/dist/interface/system/render.js +1 -0
  130. package/dist/interface/system/render.js.map +1 -1
  131. package/dist/interface/system/resolve.d.ts +5 -1
  132. package/dist/interface/system/resolve.js +19 -1
  133. package/dist/interface/system/resolve.js.map +1 -1
  134. package/dist/interface/system/shared-only.d.ts +7 -0
  135. package/dist/interface/system/shared-only.js +2 -0
  136. package/dist/interface/system/shared-only.js.map +1 -0
  137. package/dist/interface/system/tables.d.ts +28 -0
  138. package/dist/interface/system/tables.js +2 -0
  139. package/dist/interface/system/tables.js.map +1 -0
  140. package/dist/interface/world/index.d.ts +1 -1
  141. package/dist/interface/world/table/index.d.ts +27 -0
  142. package/dist/interface/world/table/index.js +3 -0
  143. package/dist/interface/world/table/index.js.map +1 -0
  144. package/dist/interface/world/table/table.d.ts +11 -0
  145. package/dist/interface/world/table/table.js +2 -0
  146. package/dist/interface/world/table/table.js.map +1 -0
  147. package/dist/interface/world/world.d.ts +25 -10
  148. package/dist/world.d.ts +27 -0
  149. package/dist/world.js +27 -0
  150. package/dist/world.js.map +1 -0
  151. package/package.json +11 -2
  152. package/src/exports.test.ts +1 -1
  153. package/src/guide/00-keys.ts +1 -1
  154. package/src/guide/01-resources.md +1 -1
  155. package/src/guide/01-resources.ts +1 -1
  156. package/src/guide/02-entities.ts +2 -2
  157. package/src/guide/03-systems.md +2 -2
  158. package/src/guide/03-systems.out +1 -1
  159. package/src/guide/03-systems.ts +3 -3
  160. package/src/guide/04-ticks.md +1 -1
  161. package/src/guide/04-ticks.ts +3 -3
  162. package/src/guide/05-scheduling.ts +1 -1
  163. package/src/guide/06-sync-points.md +18 -7
  164. package/src/guide/06-sync-points.out +1 -2
  165. package/src/guide/06-sync-points.ts +10 -6
  166. package/src/guide/07-change-filters.ts +2 -2
  167. package/src/guide/08-input.ts +1 -1
  168. package/src/guide/09-rules.ts +1 -1
  169. package/src/guide/11-traits.ts +3 -3
  170. package/src/guide/12-lookups.ts +2 -2
  171. package/src/guide/13-indexes.ts +4 -4
  172. package/src/guide/14-kernels.ts +1 -1
  173. package/src/guide/15-reductions.ts +1 -1
  174. package/src/guide/16-gpu.ts +1 -1
  175. package/src/guide/17-hierarchies.ts +1 -1
  176. package/src/guide/18-identity.out +1 -1
  177. package/src/guide/19-threads.md +4 -2
  178. package/src/guide/19-threads.ts +2 -2
  179. package/src/guide/20-shared.ts +3 -3
  180. package/src/guide/21-dom.md +2 -2
  181. package/src/guide/21-dom.ts +1 -1
  182. package/src/guide/22-canvas.md +1 -1
  183. package/src/guide/22-canvas.ts +1 -1
  184. package/src/guide/CLAUDE.md +1 -1
  185. package/src/guide/guide.test.ts +9 -154
  186. package/src/implementation/compiler/body-access.ts +102 -12
  187. package/src/implementation/compiler/describe.test-helper.ts +1 -1
  188. package/src/implementation/compiler/describe.test.ts +1 -1
  189. package/src/implementation/compiler/describe.ts +3 -3
  190. package/src/implementation/compiler/estree.ts +9 -6
  191. package/src/implementation/compiler/hygiene.test.ts +1 -1
  192. package/src/implementation/compiler/inferred.test.ts +1 -1
  193. package/src/implementation/compiler/inferred.ts +4 -2
  194. package/src/implementation/compiler/plugin.test.ts +2 -2
  195. package/src/implementation/compiler/signature.ts +1 -1
  196. package/src/implementation/compiler/transform.test.ts +43 -23
  197. package/src/implementation/compiler/transform.ts +6 -4
  198. package/src/implementation/world/activate.ts +11 -2
  199. package/src/implementation/world/batches.test.ts +3 -3
  200. package/src/implementation/world/buffer.ts +7 -0
  201. package/src/implementation/world/cascade.ts +46 -22
  202. package/src/implementation/world/change-blocks.test.ts +1 -1
  203. package/src/implementation/world/commands.ts +76 -2
  204. package/src/implementation/world/create.test.ts +8 -8
  205. package/src/implementation/world/create.ts +57 -2
  206. package/src/implementation/world/defaults.test.ts +1 -1
  207. package/src/implementation/world/despawn-where.test.ts +287 -0
  208. package/src/implementation/world/entities.ts +6 -0
  209. package/src/implementation/world/execute.ts +125 -78
  210. package/src/implementation/world/finalizers.test.ts +1 -1
  211. package/src/implementation/world/finalizers.ts +27 -4
  212. package/src/implementation/world/ids.test.ts +1 -1
  213. package/src/implementation/world/index-changed.test.ts +2 -2
  214. package/src/implementation/world/index-joined.test.ts +5 -5
  215. package/src/implementation/world/index-order.test.ts +1 -1
  216. package/src/implementation/world/indexes.test.ts +2 -1
  217. package/src/implementation/world/join.test.ts +23 -4
  218. package/src/implementation/world/join.ts +26 -4
  219. package/src/implementation/world/joined-observer.test.ts +1 -1
  220. package/src/implementation/world/joining-worker.fixture.ts +1 -1
  221. package/src/implementation/world/joining.fixture.ts +1 -1
  222. package/src/implementation/world/joining.test-helper.ts +1 -1
  223. package/src/implementation/world/kernel-spawns.test.ts +1 -1
  224. package/src/implementation/world/names.test.ts +1 -2
  225. package/src/implementation/world/observe-factories.test.ts +2 -2
  226. package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
  227. package/src/implementation/world/observe.test-helper.ts +1 -1
  228. package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
  229. package/src/implementation/world/observer-contained.test-helper.ts +1 -1
  230. package/src/implementation/world/observer-contained.test.ts +2 -2
  231. package/src/implementation/world/observer-joined.test-helper.ts +4 -4
  232. package/src/implementation/world/observer-joined.test.ts +23 -15
  233. package/src/implementation/world/observer.browser.test.ts +2 -2
  234. package/src/implementation/world/observer.test.ts +8 -8
  235. package/src/implementation/world/optional-workers.fixture.ts +1 -1
  236. package/src/implementation/world/optional.test.ts +7 -7
  237. package/src/implementation/world/perf.md +3 -1
  238. package/src/implementation/world/reclaim.test.ts +3 -3
  239. package/src/implementation/world/register.ts +2 -2
  240. package/src/implementation/world/remote-objects.test-helper.ts +1 -1
  241. package/src/implementation/world/resource-changes.test.ts +61 -0
  242. package/src/implementation/world/resource-changes.ts +67 -0
  243. package/src/implementation/world/rows.ts +24 -13
  244. package/src/implementation/world/rules.test.ts +37 -0
  245. package/src/implementation/world/run-changes.test.ts +88 -0
  246. package/src/implementation/world/run-changes.ts +160 -0
  247. package/src/implementation/world/run-results.test.ts +10 -10
  248. package/src/implementation/world/runner.ts +20 -16
  249. package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
  250. package/src/implementation/world/shared-kernels.test.ts +2 -2
  251. package/src/implementation/world/shared-workers.fixture.ts +2 -2
  252. package/src/implementation/world/shared.test.ts +10 -10
  253. package/src/implementation/world/sharing.ts +1 -1
  254. package/src/implementation/world/staging.ts +16 -3
  255. package/src/implementation/world/state.ts +19 -0
  256. package/src/implementation/world/sync-points.test.ts +135 -0
  257. package/src/implementation/world/synchronize.ts +65 -18
  258. package/src/implementation/world/tables-read.ts +70 -0
  259. package/src/implementation/world/tables.test.ts +369 -0
  260. package/src/implementation/world/tables.ts +368 -0
  261. package/src/implementation/world/tails.test.ts +1 -1
  262. package/src/implementation/world/target.test.ts +2 -2
  263. package/src/implementation/world/thread-observer.ts +1 -1
  264. package/src/implementation/world/thread-systems.test-helper.ts +3 -3
  265. package/src/implementation/world/thread-systems.ts +11 -6
  266. package/src/implementation/world/up.test.ts +13 -1
  267. package/src/implementation/world/up.ts +6 -1
  268. package/src/implementation/world/where.test.ts +4 -4
  269. package/src/implementation/world/where.ts +38 -10
  270. package/src/implementation/world/workers.fixture.ts +2 -2
  271. package/src/index.ts +4 -29
  272. package/src/interface/component/declarations.ts +11 -0
  273. package/src/interface/component/declared.ts +9 -0
  274. package/src/interface/component/index.ts +2 -0
  275. package/src/interface/finalizer/body.ts +13 -3
  276. package/src/interface/finalizer/create.test.ts +3 -1
  277. package/src/interface/finalizer/create.ts +12 -7
  278. package/src/interface/finalizer/declaration.ts +4 -2
  279. package/src/interface/index.type-test.ts +2 -2
  280. package/src/interface/indexes/create.test.ts +10 -0
  281. package/src/interface/indexes/declaration-of.ts +34 -0
  282. package/src/interface/indexes/index.ts +1 -0
  283. package/src/interface/indexes/index.type-test.ts +1 -1
  284. package/src/interface/registry/names.ts +8 -0
  285. package/src/interface/registry/schedule.ts +200 -27
  286. package/src/interface/resource/declarations.ts +11 -0
  287. package/src/interface/resource/index.ts +2 -0
  288. package/src/interface/system/access.ts +16 -0
  289. package/src/interface/system/body.ts +7 -5
  290. package/src/interface/system/check.ts +37 -10
  291. package/src/interface/system/commands.ts +33 -0
  292. package/src/interface/system/commands.type-test.ts +31 -0
  293. package/src/interface/system/create-resolved.ts +1 -1
  294. package/src/interface/system/create.test.ts +21 -13
  295. package/src/interface/system/create.ts +24 -15
  296. package/src/interface/system/declaration.ts +9 -2
  297. package/src/interface/system/describe.test.ts +79 -0
  298. package/src/interface/system/describe.ts +122 -0
  299. package/src/interface/system/index.ts +1 -0
  300. package/src/interface/system/leaf.ts +6 -0
  301. package/src/interface/system/lists.type-test.ts +1 -1
  302. package/src/interface/system/optional.type-test.ts +1 -1
  303. package/src/interface/system/render.ts +1 -0
  304. package/src/interface/system/resolve.ts +25 -2
  305. package/src/interface/system/shared-only.ts +5 -0
  306. package/src/interface/system/shared.type-test.ts +1 -1
  307. package/src/interface/system/system.type-test.ts +26 -20
  308. package/src/interface/system/tables.ts +27 -0
  309. package/src/interface/system/up.type-test.ts +3 -3
  310. package/src/interface/world/index.ts +2 -2
  311. package/src/interface/world/run.type-test.ts +14 -4
  312. package/src/interface/world/table/index.ts +20 -0
  313. package/src/interface/world/table/table.ts +12 -0
  314. package/src/interface/world/world.ts +23 -10
  315. package/src/observe/entity.ts +2 -2
  316. package/src/observe/resource.ts +2 -2
  317. package/src/observe/select.ts +4 -4
  318. package/src/observe/world-state.ts +3 -3
  319. package/src/perftest/benchmarks.ts +19 -1
  320. package/src/perftest/indexes.ts +3 -3
  321. package/src/perftest/loop.ts +1 -1
  322. package/src/perftest/observer.ts +4 -4
  323. package/src/perftest/reads.ts +2 -2
  324. package/src/perftest/remote.ts +5 -5
  325. package/src/perftest/strings.ts +1 -1
  326. package/src/perftest/structural.ts +20 -0
  327. package/src/perftest/suite.ts +1 -1
  328. package/src/perftest/tables.ts +107 -0
  329. package/src/world.test.ts +2 -2
  330. package/src/world.ts +32 -0
@@ -0,0 +1,368 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import type { Layout, Path } from '../../interface/registry/layout.js';
3
+ import { declarationOf, labelOf } from '../../interface/registry/names.js';
4
+ import { isEntity } from '../../interface/registry/schemas.js';
5
+ import type { Schema } from '../../interface/schema/schema.js';
6
+ import type { Table } from '../../interface/world/table/table.js';
7
+ import { batchOf } from './batches.js';
8
+ import { indexOf, locate, reserveRun } from './entities.js';
9
+ import { internShared } from './shared.js';
10
+ import { claimRows, openRows, writerOf, type Column as BatchColumn } from './staging.js';
11
+ import { commandIdOf, type Key, type State } from './state.js';
12
+
13
+ /** Whether a value of `schema` may hold an entity anywhere in it. */
14
+ const holdsEntity = (schema: Schema): boolean => {
15
+ if (isEntity(schema)) return true;
16
+ if ("anyOf" in schema) return schema.anyOf.some(holdsEntity);
17
+ if ("allOf" in schema) return schema.allOf.some(holdsEntity);
18
+ if (!("type" in schema)) return false;
19
+ switch (schema.type) {
20
+ case "array": case "set": return holdsEntity(schema.items);
21
+ case "map": return holdsEntity(schema.keys) || holdsEntity(schema.values);
22
+ case "object": return Object.values(schema.properties).some(holdsEntity);
23
+ default: return false;
24
+ }
25
+ };
26
+
27
+ /** The schema at `path` inside `schema`, through struct fields and tuple items. */
28
+ const schemaAt = (schema: Schema, path: Path): Schema | undefined => {
29
+ let at: Schema | undefined = schema;
30
+ for (const step of path) {
31
+ if (at === undefined || !("type" in at)) return undefined;
32
+ if (at.type === "object" && typeof step === "string") at = at.properties[step];
33
+ else if (at.type === "array" && typeof step === "number") at = at.items;
34
+ else return undefined;
35
+ }
36
+ return at;
37
+ };
38
+
39
+ /** The f64 leaves of a linear layout holding entities: where, in f64 elements, each row's is. */
40
+ const entityLeavesOf = (layout: Layout, schema: Schema | undefined): readonly { readonly unitStride: number; readonly unitOffset: number }[] => {
41
+ if (layout.kind !== "linear" || schema === undefined) return [];
42
+ return layout.leaves.filter(leaf => {
43
+ const leafSchema = schemaAt(schema, leaf.path);
44
+ return leaf.view === "f64" && leafSchema !== undefined && isEntity(leafSchema);
45
+ });
46
+ };
47
+
48
+ /** The entity each entity of the tables given is spawned as, and what a value naming none of them becomes. */
49
+ type Remap = (entity: number) => number;
50
+
51
+ /** `value` of `schema` with each entity in it replaced by `remap`'s. */
52
+ const remapValue = (schema: Schema, value: unknown, remap: Remap): unknown => {
53
+ if (isEntity(schema)) return typeof value === "number" ? remap(value) : value;
54
+ if (!holdsEntity(schema)) return value;
55
+ if ("anyOf" in schema) {
56
+ for (const member of schema.anyOf) {
57
+ if (isEntity(member) && typeof value === "number") return remap(value);
58
+ if (!("type" in member) || !holdsEntity(member)) continue;
59
+ if ((member.type === "array" && Array.isArray(value)) || (member.type === "set" && value instanceof Set) || (member.type === "map" && value instanceof Map)
60
+ || (member.type === "object" && typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Set) && !(value instanceof Map))) return remapValue(member, value, remap);
61
+ }
62
+ return value;
63
+ }
64
+ if ("allOf" in schema) return schema.allOf.reduce<unknown>((next, member) => remapValue(member, next, remap), value);
65
+ if (!("type" in schema)) return value;
66
+ switch (schema.type) {
67
+ case "array": return Array.isArray(value) ? value.map(item => remapValue(schema.items, item, remap)) : value;
68
+ case "set": return value instanceof Set ? new Set(Array.from(value, item => remapValue(schema.items, item, remap))) : value;
69
+ case "map": return value instanceof Map ? new Map(Array.from(value, ([key, item]) => [remapValue(schema.keys, key, remap), remapValue(schema.values, item, remap)])) : value;
70
+ case "object": {
71
+ if (typeof value !== "object" || value === null) return value;
72
+ const copy: { [name: string]: unknown } = {};
73
+ for (const name of Object.keys(value)) {
74
+ const field = schema.properties[name];
75
+ const item: unknown = Reflect.get(value, name);
76
+ copy[name] = field === undefined ? item : remapValue(field, item, remap);
77
+ }
78
+ return copy;
79
+ }
80
+ default: return value;
81
+ }
82
+ };
83
+
84
+ /** A table's components with their ids, the columns given for them, and their layouts. */
85
+ type Planned = {
86
+ readonly table: Table;
87
+ readonly keys: readonly Key[];
88
+ readonly ids: readonly number[];
89
+ readonly shared: (number | undefined)[];
90
+ };
91
+
92
+ /** Throws, naming the component, for a column that doesn't fit its component's layout or its table's count. */
93
+ const checkColumn = (state: State, key: Key, id: number, column: Table.Column, count: number): void => {
94
+ const layout = state.layouts[id];
95
+ const fail = (message: string): never => { throw new Error(`commands.spawnTables: "${labelOf(key)}" ${message}.`); };
96
+ if (state.shared[id] !== undefined) {
97
+ if (column.kind !== "value") fail("is a shared component, so its column is one value");
98
+ return;
99
+ }
100
+ if (layout.kind === "constant" || column.kind === "value") return;
101
+ if (layout.kind === "linear") {
102
+ if (column.kind !== "bytes") fail("is stored as bytes, so its column gives bytes");
103
+ else if (column.stride !== layout.stride) fail(`has a stride of ${layout.stride} bytes in this world, not ${column.stride}`);
104
+ else if (column.bytes.length !== count * layout.stride) fail(`gives ${column.bytes.length} bytes for ${count} rows of ${layout.stride}`);
105
+ return;
106
+ }
107
+ if (column.kind !== "values") fail("is stored as values, so its column gives values");
108
+ else if (column.values.length !== count) fail(`gives ${column.values.length} values for ${count} rows`);
109
+ };
110
+
111
+ /** The plan of each table: its keys, their command ids, and the numbers of its shared values. Throws for one that won't spawn. */
112
+ const planOf = (state: State, table: Table): Planned => {
113
+ const { count, entities, columns } = table;
114
+ if (!Number.isInteger(count) || count < 0 || entities.length !== count) throw new Error(`commands.spawnTables: a table of ${count} rows gives ${entities.length} entities.`);
115
+ const keys = Object.getOwnPropertySymbols(columns).filter(key => Reflect.get(columns, key) !== undefined);
116
+ const ids = keys.map(key => commandIdOf(state, key));
117
+ const shared: (number | undefined)[] = [];
118
+ keys.forEach((key, k) => {
119
+ const column: Table.Column = Reflect.get(columns, key);
120
+ checkColumn(state, key, ids[k], column, count);
121
+ const values = state.shared[ids[k]];
122
+ if (values !== undefined && column.kind === "value") {
123
+ const number = internShared(values, column.value);
124
+ if (number < 0) throw new Error(`commands.spawnTables: "${labelOf(key)}", a shared component, isn't given one of its type's values: ${labelOf(column.value)}.`);
125
+ shared[ids[k]] = number;
126
+ }
127
+ });
128
+ return { table, keys, ids, shared };
129
+ };
130
+
131
+ /**
132
+ * Where each entity of the tables is, by its index: the entity itself, to tell it from another of that index, and its
133
+ * position among all the tables' rows. Throws for an entity two rows name.
134
+ */
135
+ const placesOf = (plans: readonly Planned[]): { readonly entities: Float64Array; readonly positions: Int32Array } => {
136
+ let largest = 0;
137
+ for (const { table } of plans) for (let row = 0; row < table.count; row++) largest = Math.max(largest, indexOf(table.entities[row]));
138
+ const entities = new Float64Array(largest + 1).fill(-1);
139
+ const positions = new Int32Array(largest + 1);
140
+ let position = 0;
141
+ for (const { table } of plans) {
142
+ for (let row = 0; row < table.count; row++) {
143
+ const entity = table.entities[row];
144
+ const index = indexOf(entity);
145
+ if (entities[index] === entity) throw new Error(`commands.spawnTables: entity ${entity} is in two rows.`);
146
+ entities[index] = entity;
147
+ positions[index] = position++;
148
+ }
149
+ }
150
+ return { entities, positions };
151
+ };
152
+
153
+ /** A reference column's entity value in each row: the f64 at the row's start, a reference's whole value. */
154
+ const referencesOf = (column: Table.Column): ((row: number) => number) => {
155
+ if (column.kind !== "bytes") return () => 0;
156
+ const { bytes } = column;
157
+ if (bytes.byteOffset % 8 === 0 && column.stride % 8 === 0) {
158
+ const view = new Float64Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 3);
159
+ const words = column.stride >> 3;
160
+ return row => view[row * words];
161
+ }
162
+ const view = new DataView(column.bytes.buffer, column.bytes.byteOffset, column.bytes.byteLength);
163
+ const { stride } = column;
164
+ return row => view.getFloat64(row * stride, true);
165
+ };
166
+
167
+ /** Copies a table's `rows`, or all of them, of one column into rows `start` on of a batch's column, remapping entities. */
168
+ const copyColumn = (target: BatchColumn, column: Table.Column, schema: Schema | undefined, rows: readonly number[] | null, count: number, start: number, remap: Remap | null): void => {
169
+ const rowAt = (i: number): number => rows === null ? i : rows[i];
170
+ const remapped = remap !== null && schema !== undefined && holdsEntity(schema);
171
+ if (column.kind === "value") {
172
+ const write = writerOf(target, column.value);
173
+ const value = remapped ? remapValue(schema, column.value, remap) : column.value;
174
+ for (let i = 0; i < count; i++) write(start + i, value);
175
+ return;
176
+ }
177
+ if (column.kind === "bytes") {
178
+ const { stride } = target;
179
+ if (rows === null) target.views.u8.set(column.bytes, start * stride);
180
+ else for (let i = 0; i < count; i++) target.views.u8.set(column.bytes.subarray(rows[i] * stride, (rows[i] + 1) * stride), (start + i) * stride);
181
+ if (!remapped) return;
182
+ const { f64 } = target.views;
183
+ for (const leaf of entityLeavesOf(target.layout, schema)) {
184
+ for (let row = start, end = start + count; row < end; row++) {
185
+ const index = row * leaf.unitStride + leaf.unitOffset;
186
+ f64[index] = remap(f64[index]);
187
+ }
188
+ }
189
+ return;
190
+ }
191
+ if (target.remote !== null && column.values.some(value => value !== undefined && value !== null)) throw new Error(target.remote);
192
+ for (let i = 0; i < count; i++) {
193
+ const value = column.values[rowAt(i)];
194
+ target.values[start + i] = remapped ? remapValue(schema, value, remap) : value;
195
+ }
196
+ };
197
+
198
+ /** Spawns `rows` of a plan's table, or all of them, with the components it holds but `removed`, as `entities`. */
199
+ const spawnRows = (state: State, { table, keys, ids, shared }: Planned, rows: readonly number[] | null, removed: ReadonlySet<Key>, entities: Float64Array, remap: Remap | null): void => {
200
+ const kept = keys.flatMap((key, k) => removed.has(key) ? [] : [k]);
201
+ const batch = batchOf(state, kept.map(k => ids[k]), shared.length === 0 ? undefined : shared);
202
+ const start = openRows(batch, entities.length);
203
+ for (const k of kept) {
204
+ const target = batch.columns.find(column => column.id === ids[k]);
205
+ if (target === undefined || target.layout.kind === "constant") continue;
206
+ const column: Table.Column = Reflect.get(table.columns, keys[k]);
207
+ copyColumn(target, column, declarationOf(keys[k])?.type, rows, entities.length, start, remap);
208
+ }
209
+ claimRows(batch, entities);
210
+ };
211
+
212
+ /** Throws for an entity `existing` maps to that isn't alive in this world. */
213
+ const checkExisting = (state: State, existing: ReadonlyMap<number, number>): void => {
214
+ for (const [source, target] of existing) {
215
+ if (locate(state.entities, target) < 0) throw new Error(`commands.spawnTables: entity ${source} is given as existing as entity ${target}, which isn't alive.`);
216
+ }
217
+ };
218
+
219
+ /**
220
+ * Spawns an entity for each row of `tables` into the batches of their components, as references would leave them had
221
+ * the entities outside the tables despawned: a row owned, directly or through others, by an entity outside them isn't
222
+ * spawned; a weak reference to one is removed; and any other entity value naming one is `Entity.none`. Every entity
223
+ * value naming an entity spawned names the one spawned for it. A row whose entity `existing` maps isn't spawned: the
224
+ * entity it maps to stands for it, as it does for an entity outside the tables that `existing` maps.
225
+ */
226
+ export const spawnTables = (state: State, tables: readonly Table[], existing: ReadonlyMap<number, number> = new Map()): Table.Spawned => {
227
+ const plans = tables.map(table => planOf(state, table));
228
+ checkExisting(state, existing);
229
+ // Entities are mapped only where a value of the tables may hold one, or a row is one that exists.
230
+ const remapping = existing.size > 0 || plans.some(({ keys }) => keys.some(key => {
231
+ const schema = declarationOf(key)?.type;
232
+ return schema !== undefined && holdsEntity(schema);
233
+ }));
234
+ const none: ReadonlySet<Key> = new Set();
235
+ if (!remapping) {
236
+ const entities = plans.map(({ table }) => {
237
+ const spawned = new Float64Array(table.count);
238
+ reserveRun(state.entities, spawned);
239
+ return spawned;
240
+ });
241
+ plans.forEach((plan, t) => spawnRows(state, plan, null, none, entities[t], null));
242
+ return { entities, dropped: 0 };
243
+ }
244
+ const places = placesOf(plans);
245
+ const positionOf = (entity: number): number => {
246
+ const index = indexOf(entity);
247
+ return entity !== 0 && index < places.entities.length && places.entities[index] === entity ? places.positions[index] : -1;
248
+ };
249
+ const firsts: number[] = [];
250
+ let total = 0;
251
+ for (const { table } of plans) {
252
+ firsts.push(total);
253
+ total += table.count;
254
+ }
255
+ // The entity standing for each row `existing` maps, 0 for a row spawned.
256
+ const existingAt = new Float64Array(total);
257
+ if (existing.size > 0) plans.forEach(({ table }, t) => {
258
+ for (let row = 0; row < table.count; row++) existingAt[firsts[t] + row] = existing.get(table.entities[row]) ?? 0;
259
+ });
260
+ // A row owned by an entity outside the tables isn't spawned, nor anything it owns, as despawning its owner would.
261
+ const dropped = new Uint8Array(total);
262
+ const waiting: number[] = [];
263
+ /**
264
+ * Calls `each` with each owner reference of a row that doesn't exist: the owned row's position, its owner's among
265
+ * the tables' rows or -1 for an owner outside them, and the owner.
266
+ */
267
+ const forEachOwner = (each: (position: number, at: number, owner: number) => void): void => plans.forEach(({ table, keys }, t) => {
268
+ for (const key of keys) {
269
+ if (declarationOf(key)?.reference !== "owner") continue;
270
+ const ownerAt = referencesOf(Reflect.get(table.columns, key));
271
+ for (let row = 0; row < table.count; row++) {
272
+ const owner = ownerAt(row);
273
+ const position = firsts[t] + row;
274
+ // A row that exists stands, whatever owns it.
275
+ if (owner !== 0 && existingAt[position] === 0) each(position, positionOf(owner), owner);
276
+ }
277
+ }
278
+ });
279
+ forEachOwner((position, at, owner) => {
280
+ if (at >= 0 || dropped[position] !== 0 || existing.has(owner)) return;
281
+ dropped[position] = 1;
282
+ waiting.push(position);
283
+ });
284
+ if (waiting.length > 0) {
285
+ const owned = new Map<number, number[]>();
286
+ forEachOwner((position, at) => {
287
+ if (at < 0) return;
288
+ const list = owned.get(at);
289
+ if (list === undefined) owned.set(at, [position]);
290
+ else list.push(position);
291
+ });
292
+ for (let next = waiting.pop(); next !== undefined; next = waiting.pop()) {
293
+ for (const child of owned.get(next) ?? []) {
294
+ if (dropped[child] !== 0 || existingAt[child] !== 0) continue;
295
+ dropped[child] = 1;
296
+ waiting.push(child);
297
+ }
298
+ }
299
+ }
300
+ // Each row's entity in this world: the one spawned for it, the one existing for it, or 0 for one dropped.
301
+ const spawnedAt = new Float64Array(total);
302
+ /** By table, whether every row of it is spawned: none exists or is dropped. */
303
+ const wholly: boolean[] = [];
304
+ const entities = plans.map(({ table }, t) => {
305
+ const first = firsts[t];
306
+ const spawned = new Float64Array(table.count);
307
+ let standing = 0;
308
+ for (let row = 0; row < table.count; row++) if (existingAt[first + row] !== 0 || dropped[first + row] !== 0) standing++;
309
+ wholly.push(standing === 0);
310
+ if (standing === 0) {
311
+ reserveRun(state.entities, spawned);
312
+ spawnedAt.set(spawned, first);
313
+ return spawned;
314
+ }
315
+ const alive = [];
316
+ for (let row = 0; row < table.count; row++) {
317
+ const position = first + row;
318
+ if (existingAt[position] !== 0) spawnedAt[position] = spawned[row] = existingAt[position];
319
+ else if (dropped[position] === 0) alive.push(row);
320
+ }
321
+ const reserved = new Float64Array(alive.length);
322
+ reserveRun(state.entities, reserved);
323
+ alive.forEach((row, i) => {
324
+ spawned[row] = reserved[i];
325
+ spawnedAt[first + row] = reserved[i];
326
+ });
327
+ return spawned;
328
+ });
329
+ /** The entity in this world for one of the tables' or one `existing` maps, or 0 for one that isn't there. */
330
+ const entityFor = (entity: number): number => {
331
+ const at = positionOf(entity);
332
+ return at >= 0 ? spawnedAt[at] : existing.get(entity) ?? 0;
333
+ };
334
+ const isSpawned = (entity: number): boolean => entityFor(entity) !== 0;
335
+ const remap: Remap = entityFor;
336
+ let droppedCount = 0;
337
+ plans.forEach((plan, t) => {
338
+ const { table, keys } = plan;
339
+ const first = firsts[t];
340
+ const weak = keys.filter(key => declarationOf(key)?.reference === "weak").map(key => ({ key, targetAt: referencesOf(Reflect.get(table.columns, key)) }));
341
+ const loses = (row: number): boolean => weak.some(({ targetAt }) => {
342
+ const target = targetAt(row);
343
+ return target !== 0 && !isSpawned(target);
344
+ });
345
+ let whole = wholly[t];
346
+ if (weak.length > 0) for (let row = 0; row < table.count && whole; row++) if (loses(row)) whole = false;
347
+ if (whole) return spawnRows(state, plan, null, none, entities[t], remap);
348
+ // Rows by the weak references they lose, whose targets aren't spawned: each set of components a batch of its own.
349
+ const groups = new Map<string, { readonly removed: Set<Key>; readonly rows: number[] }>();
350
+ for (let row = 0; row < table.count; row++) {
351
+ if (existingAt[first + row] !== 0) continue;
352
+ if (dropped[first + row] !== 0) {
353
+ droppedCount++;
354
+ continue;
355
+ }
356
+ const removed = weak.filter(({ targetAt }) => {
357
+ const target = targetAt(row);
358
+ return target !== 0 && !isSpawned(target);
359
+ }).map(({ key }) => key);
360
+ const signature = removed.map(key => weak.findIndex(entry => entry.key === key)).join(",");
361
+ let group = groups.get(signature);
362
+ if (group === undefined) groups.set(signature, group = { removed: new Set(removed), rows: [] });
363
+ group.rows.push(row);
364
+ }
365
+ for (const { removed, rows } of groups.values()) spawnRows(state, plan, rows, removed, Float64Array.from(rows, row => entities[t][row]), remap);
366
+ });
367
+ return { entities, dropped: droppedCount };
368
+ };
@@ -75,7 +75,7 @@ describe("tails", () => {
75
75
  const { world, remote } = await joined({ loop: driver }, { [always]: true });
76
76
  await world.run(seed, { count: 10 });
77
77
  const counters: number[] = [];
78
- const values = System.create({ observer: true }, ({ resources }) => { counters.push(resources[counter]); });
78
+ const values = System.create({ readonly: true }, ({ resources }) => { counters.push(resources[counter]); });
79
79
  world.add(ticked);
80
80
  world.add(values);
81
81
  await until(Observe.resource(world, counter), value => value >= 5);
@@ -24,8 +24,8 @@ const remove = System.create({ target: "entity", components: { with: [tagged] }
24
24
  });
25
25
  let amounts: number[] = [];
26
26
  let changed: number[] = [];
27
- const readAmounts = System.create({ observer: true, components: { with: [amount] } }, row => { amounts.push(row[amount]); });
28
- const watchChanged = System.create({ observer: true, components: { with: [amount], changed: [amount] } }, row => { changed.push(row[amount]); });
27
+ const readAmounts = System.create({ readonly: true, components: { with: [amount] } }, row => { amounts.push(row[amount]); });
28
+ const watchChanged = System.create({ readonly: true, components: { with: [amount], changed: [amount] } }, row => { changed.push(row[amount]); });
29
29
 
30
30
  const inline = () => createWorld({ [workers]: 0 });
31
31
  const amountsOf = async (world: ReturnType<typeof inline>): Promise<number[]> => {
@@ -69,7 +69,7 @@ const errorOf = (error: unknown): WorkerError =>
69
69
 
70
70
  const refuse = (): never => { throw new Error("An observer system issues no commands."); };
71
71
  /** An observer system's context has no commands, so these are never called. */
72
- const noCommands: Commands = { spawn: refuse, despawn: refuse, add: refuse, remove: refuse };
72
+ const noCommands: Commands = { spawn: refuse, despawn: refuse, despawnWhere: refuse, add: refuse, remove: refuse, spawnTables: refuse };
73
73
  const noStamps: number[] = [];
74
74
 
75
75
  export const createThreadObserver = (host: ThreadObserverHost) => {
@@ -172,7 +172,7 @@ const reading: { entities: readonly Entity[]; objects: readonly { readonly [name
172
172
 
173
173
  /** Each entity's objects of the thread, as data, a note by its text: none for an entity that's dead or has no uid. */
174
174
  const readObjects = System.create({
175
- observer: true,
175
+ readonly: true,
176
176
  lookup: { ids: { with: [uid] }, captions: { with: [caption] }, notes: { with: [note] } },
177
177
  }, ({ lookup }) => {
178
178
  reading.objects = reading.entities.map(entity => {
@@ -322,9 +322,9 @@ export const paintReader = System.create(({ resources }) => () => resources[pain
322
322
  /** A composite of systems on the thread returning values, which no run resolves to. */
323
323
  export const paintsTogether = System.create([paintReader, paintLengthThere]);
324
324
  /** An observer system on the thread, as it reads the thread's paint: what a run of it resolves to. */
325
- export const paintObserved = System.create({ observer: true }, ({ resources }) => resources[paint].text);
325
+ export const paintObserved = System.create({ readonly: true }, ({ resources }) => resources[paint].text);
326
326
  /** An observer system on the thread, throwing there. */
327
- export const paintObservedFails = System.create({ observer: true }, ({ resources }) => {
327
+ export const paintObservedFails = System.create({ readonly: true }, ({ resources }) => {
328
328
  if (resources[paint].text !== "never") throw new Error("threadSystemsTest observer failure there");
329
329
  return 0;
330
330
  });
@@ -279,6 +279,8 @@ export const createThreadSystems = (host: ThreadSystemsHost) => {
279
279
  return typeof preset === "object" && preset !== null ? presetSpawnerOf(components, preset) : spawnerOf(components);
280
280
  },
281
281
  despawn: entity => beginCommand(writer, despawnCommand, entity),
282
+ spawnTables: () => { throw new Error("commands.spawnTables runs on the world's thread: run the system there."); },
283
+ despawnWhere: () => { throw new Error("commands.despawnWhere runs on the world's thread: run the system there."); },
282
284
  add: (entity: number, components: symbol | readonly symbol[], values?: unknown) => {
283
285
  if (typeof components === "symbol") {
284
286
  const key = givenKeyOf(components);
@@ -989,18 +991,19 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
989
991
  const row = bindRow(archetype);
990
992
  const subject = row.value;
991
993
  const input = runInput;
994
+ const { body } = system;
992
995
  const filters = filtersOf(archetype);
993
996
  if (since < 0 || filters.length === 0) {
994
997
  for (let index = 0; index < count; index++) {
995
998
  row.index = index;
996
- system.body(subject, value, input);
999
+ body(subject, value, input);
997
1000
  }
998
1001
  return;
999
1002
  }
1000
1003
  forEachPassingRange(filters, since, 0, count, (start, end) => {
1001
1004
  for (let index = start; index < end; index++) {
1002
1005
  row.index = index;
1003
- system.body(subject, value, input);
1006
+ body(subject, value, input);
1004
1007
  }
1005
1008
  });
1006
1009
  };
@@ -1102,18 +1105,19 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
1102
1105
  const row = bindRow(archetype);
1103
1106
  const subject = row.value;
1104
1107
  const input = runInput;
1108
+ const { body } = system;
1105
1109
  const filters = filtersOf(archetype);
1106
1110
  if (since < 0 || filters.length === 0) {
1107
1111
  for (let index = start; index < end && !halted; index++) {
1108
1112
  row.index = index;
1109
- system.body(subject, tailValue, input);
1113
+ body(subject, tailValue, input);
1110
1114
  }
1111
1115
  return;
1112
1116
  }
1113
1117
  forEachPassingRange(filters, since, start, end, (first, last) => {
1114
1118
  for (let index = first; index < last && !halted; index++) {
1115
1119
  row.index = index;
1116
- system.body(subject, tailValue, input);
1120
+ body(subject, tailValue, input);
1117
1121
  }
1118
1122
  });
1119
1123
  };
@@ -1232,6 +1236,7 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
1232
1236
  const copied = (storages: readonly (ThreadStorage | undefined)[]): ThreadArchetype =>
1233
1237
  ({ id: -1, entities, storageById: storages, record: -1, version: -1, mirrored: null, components: [], resource: false, reclaimed: false, generation: 0 });
1234
1238
  const ownedInput = runInput;
1239
+ const { body } = system;
1235
1240
  try {
1236
1241
  if (optionalNumbers.length === 0 && sharedReads.length === 0) {
1237
1242
  const row = bindRow(copied(storageById));
@@ -1239,7 +1244,7 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
1239
1244
  for (let index = first; index < rows && !halted; index++) {
1240
1245
  if (skipped !== null && skipped.has(archetypeIds[index])) continue;
1241
1246
  row.index = index;
1242
- system.body(subject, tailValue, ownedInput);
1247
+ body(subject, tailValue, ownedInput);
1243
1248
  }
1244
1249
  }
1245
1250
  else {
@@ -1274,7 +1279,7 @@ export const createThreadRunner = (host: RunnerHost, leaf: Leaf, records: Readon
1274
1279
  const subject = row.value;
1275
1280
  for (; index < end && !halted; index++) {
1276
1281
  row.index = index;
1277
- system.body(subject, tailValue, ownedInput);
1282
+ body(subject, tailValue, ownedInput);
1278
1283
  }
1279
1284
  }
1280
1285
  }
@@ -190,7 +190,7 @@ describe("up", () => {
190
190
  expect(await read(ecs, entities.grandchild, world)).toBe(100);
191
191
  });
192
192
 
193
- it("runs every row after a spawn outruns the journal, with change filters, then only the rows that changed", async () => {
193
+ it("runs every row after spawning more than a patch takes, with change filters, then only the rows that changed", async () => {
194
194
  await using ecs = createWorld({ [workers]: 0 });
195
195
  await ecs.run(spawn);
196
196
  await ecs.run(nest);
@@ -211,6 +211,18 @@ describe("up", () => {
211
211
  expect(ran).toEqual([]);
212
212
  });
213
213
 
214
+ it("runs only the rows spawned in bulk, which the journal holds once each, with change filters", async () => {
215
+ await using ecs = createWorld({ [workers]: 0 });
216
+ await ecs.run(spawn);
217
+ await ecs.run(nest);
218
+ await ecs.run(propagateChanged);
219
+ await ecs.run(bulkUnder, { count: 3000 });
220
+ ran.length = 0;
221
+ await ecs.run(propagateChanged);
222
+ expect(ran.length).toBe(3000);
223
+ expect(await readEach(ecs, bulk.slice(0, 3), world)).toEqual([112, 112, 112]);
224
+ });
225
+
214
226
  it("finds a cycle a reparent closes after the tree was built, and runs its rows again once it opens", async () => {
215
227
  await using ecs = createWorld({ [workers]: 0 });
216
228
  await ecs.run(spawn);
@@ -22,6 +22,9 @@ const cyclic = -1;
22
22
  const denseShare = 1 / 8;
23
23
  /** Rows below this many are sorted rather than scanned. */
24
24
  const denseMinimum = 4096;
25
+ /** Past this many journal entries, and past this share of the matched rows, the tree is built again rather than patched. */
26
+ const patchMinimum = 4096;
27
+ const patchShare = 1 / 8;
25
28
  /** Sort keys pack a row with its parent's archetype: `(parent + 1) * rowLimit + row`. */
26
29
  const rowLimit = 2 ** 27;
27
30
 
@@ -375,7 +378,9 @@ const createShared = (state: State, through: number, read: readonly number[], ma
375
378
  const { journal } = state;
376
379
  const end = journalEnd(journal);
377
380
  if (position === end) return;
378
- if (position < journal.first) {
381
+ let matched = 0;
382
+ for (let k = 0; k < matches.length; k++) matched += matches[k].count;
383
+ if (position < journal.first || end - position > Math.max(patchMinimum, matched * patchShare)) {
379
384
  build(matches);
380
385
  position = end;
381
386
  rebuiltAll();
@@ -62,10 +62,10 @@ export const noteInView = System.create({ components: { with: [position, note],
62
62
  row[note] = { text: `${row[position]}` };
63
63
  });
64
64
  const heard: number[] = [];
65
- export const hearInView = System.create({ observer: true, components: { with: [position], changed: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } } }, row => { heard.push(row[position]); });
65
+ export const hearInView = System.create({ readonly: true, components: { with: [position], changed: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } } }, row => { heard.push(row[position]); });
66
66
  // Filtered by a resource the predicate doesn't read: a culled archetype coming into view catches up on its change.
67
67
  const signalled: number[] = [];
68
- export const signalInView = System.create({ observer: true, components: { with: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } }, resources: { changed: [other] } }, row => { signalled.push(row[position]); });
68
+ export const signalInView = System.create({ readonly: true, components: { with: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } }, resources: { changed: [other] } }, row => { signalled.push(row[position]); });
69
69
  // A system moving the camera its own predicate reads: its next run sees the new view.
70
70
  const followed: number[] = [];
71
71
  export const follow = System.create({ components: { with: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } } }, (row, { resources }) => {
@@ -79,7 +79,7 @@ export const noteChanged = System.create({ components: { with: [position, note],
79
79
  row[note] = { text: `${row[position]}` };
80
80
  });
81
81
  const notes: string[] = [];
82
- export const readNotes = System.create({ observer: true, components: { with: [note] } }, row => { if (row[note] !== null) notes.push(row[note].text); });
82
+ export const readNotes = System.create({ readonly: true, components: { with: [note] } }, row => { if (row[note] !== null) notes.push(row[note].text); });
83
83
  export const nudgeNoted = System.create({ components: { with: [position, note] } }, row => { row[position] += 0.5; });
84
84
 
85
85
  /** Waits a turn at a time until `done`, for two seconds at most. */
@@ -91,7 +91,7 @@ const until = async (done: () => boolean): Promise<void> => {
91
91
  /** Every row's position and how often it was seen, by position. */
92
92
  const seenOf = async (world: ReturnType<typeof createWorld>): Promise<readonly string[]> => {
93
93
  const read: [number, string][] = [];
94
- await world.run(System.create({ observer: true, components: { with: [position, seen] } }, row => { read.push([row[position], `${row[position]}:${row[seen]}`]); }));
94
+ await world.run(System.create({ readonly: true, components: { with: [position, seen] } }, row => { read.push([row[position], `${row[position]}:${row[seen]}`]); }));
95
95
  return read.sort((a, b) => a[0] - b[0]).map(([, text]) => text).filter(text => !text.endsWith(":0"));
96
96
  };
97
97
 
@@ -115,22 +115,50 @@ const predicateOf = (given: unknown): ((value: unknown, context: object) => unkn
115
115
  typeof given === "function" ? (value, context) => Reflect.apply(given, undefined, [value, context]) : null;
116
116
 
117
117
  /**
118
- * A system's `where` in a world: its values interned there, by number, its predicates' context reading `reads`,
119
- * evaluated again once one of them changed since they last were.
118
+ * `where`'s conditions in a world, each value interned there, by number, and with `pinned`, pinned, so its number stays
119
+ * while no archetype holds it and one that comes to hold it again matches; or the key of a component that isn't shared.
120
+ * A value interned unpinned keeps its number to the end of the sync point.
120
121
  */
121
- export const createWhere = <A extends Valued>(state: State, where: { readonly [key: symbol]: unknown }, reads: readonly symbol[], leaf: Leaf): Where<A> => {
122
- const conditions: Condition[] = Object.getOwnPropertySymbols(where).map(key => {
122
+ const conditionsOf = (state: State, where: { readonly [key: symbol]: unknown }, pinned: boolean): Condition[] | symbol => {
123
+ const conditions: Condition[] = [];
124
+ for (const key of Object.getOwnPropertySymbols(where)) {
123
125
  const id = idOf(state, key);
124
126
  const given: unknown = Reflect.get(where, key);
125
127
  const predicate = predicateOf(given);
126
- if (predicate !== null) return { id, number: -1, predicate };
128
+ if (predicate !== null) {
129
+ conditions.push({ id, number: -1, predicate });
130
+ continue;
131
+ }
127
132
  const values = state.shared[id];
128
- if (values === undefined) throw new Error(`System "${leaf.name}": where names "${labelOf(key)}", which isn't a shared component.`);
133
+ if (values === undefined) return key;
129
134
  const number = internShared(values, given);
130
- // Pinned, so its number stays while no archetype holds it: one that comes to hold it again matches.
131
- if (number >= 0) holdShared(values, number);
132
- return { id, number, predicate: null };
133
- });
135
+ if (pinned && number >= 0) holdShared(values, number);
136
+ conditions.push({ id, number, predicate: null });
137
+ }
138
+ return conditions;
139
+ };
140
+
141
+ /**
142
+ * Whether an archetype holds each of `where`'s values, as a system's `where` of values passes it, for a `where` naming
143
+ * only shared components and giving no predicate; or the key of one that isn't shared. Its values are pinned with
144
+ * `pinned`, and otherwise only interned, for a `where` used before the sync point ends.
145
+ */
146
+ export const holdsValuesOf = (state: State, where: { readonly [key: symbol]: unknown }, pinned: boolean): ((archetype: Valued) => boolean) | symbol => {
147
+ const conditions = conditionsOf(state, where, pinned);
148
+ if (typeof conditions === "symbol") return conditions;
149
+ return archetype => {
150
+ for (let c = 0; c < conditions.length; c++) if (archetype.shared[conditions[c].id] !== conditions[c].number) return false;
151
+ return true;
152
+ };
153
+ };
154
+
155
+ /**
156
+ * A system's `where` in a world: its values interned there, by number, its predicates' context reading `reads`,
157
+ * evaluated again once one of them changed since they last were.
158
+ */
159
+ export const createWhere = <A extends Valued>(state: State, where: { readonly [key: symbol]: unknown }, reads: readonly symbol[], leaf: Leaf): Where<A> => {
160
+ const conditions = conditionsOf(state, where, true);
161
+ if (typeof conditions === "symbol") throw new Error(`System "${leaf.name}": where names "${labelOf(conditions)}", which isn't a shared component.`);
134
162
  const readIds = reads.map(key => idOf(state, key));
135
163
  // Its predicates' context: each resource read once per evaluation, since a struct's read copies it out.
136
164
  const cached: unknown[] = reads.map(() => undefined);