@spearwolf/shadow-objects 0.33.0 → 0.35.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 (312) hide show
  1. package/CHANGELOG.md +350 -61
  2. package/README.md +66 -13
  3. package/bundle.js +6 -53
  4. package/package.json +15 -3
  5. package/src/ChangeTrailRefusedError.d.ts +27 -0
  6. package/src/ChangeTrailRefusedError.d.ts.map +1 -0
  7. package/src/ChangeTrailRefusedError.js +12 -0
  8. package/src/ChangeTrailRefusedError.js.map +7 -0
  9. package/src/EntityUuidInUseError.d.ts +19 -0
  10. package/src/EntityUuidInUseError.d.ts.map +1 -0
  11. package/src/EntityUuidInUseError.js +11 -0
  12. package/src/EntityUuidInUseError.js.map +7 -0
  13. package/src/WorkerTimeoutError.d.ts +26 -0
  14. package/src/WorkerTimeoutError.d.ts.map +1 -0
  15. package/src/WorkerTimeoutError.js +12 -0
  16. package/src/WorkerTimeoutError.js.map +7 -0
  17. package/src/bundle.d.ts +1 -1
  18. package/src/bundle.d.ts.map +1 -1
  19. package/src/bundle.js +1 -1
  20. package/src/bundle.js.map +2 -2
  21. package/src/constants.d.ts +13 -2
  22. package/src/constants.d.ts.map +1 -1
  23. package/src/constants.js +7 -1
  24. package/src/constants.js.map +2 -2
  25. package/src/create-worker.bundle.d.ts +1 -1
  26. package/src/create-worker.bundle.d.ts.map +1 -1
  27. package/src/create-worker.bundle.js.map +1 -1
  28. package/src/create-worker.d.ts +1 -1
  29. package/src/create-worker.d.ts.map +1 -1
  30. package/src/create-worker.js.map +1 -1
  31. package/src/elements/ShaeElement.d.ts +70 -2
  32. package/src/elements/ShaeElement.d.ts.map +1 -1
  33. package/src/elements/ShaeElement.js +129 -25
  34. package/src/elements/ShaeElement.js.map +3 -3
  35. package/src/elements/ShaeEntElement.d.ts +39 -1
  36. package/src/elements/ShaeEntElement.d.ts.map +1 -1
  37. package/src/elements/ShaeEntElement.js +397 -167
  38. package/src/elements/ShaeEntElement.js.map +2 -2
  39. package/src/elements/ShaeLifecycleElement.d.ts +71 -0
  40. package/src/elements/ShaeLifecycleElement.d.ts.map +1 -0
  41. package/src/elements/ShaeLifecycleElement.js +83 -0
  42. package/src/elements/ShaeLifecycleElement.js.map +7 -0
  43. package/src/elements/ShaePropElement.d.ts +61 -2
  44. package/src/elements/ShaePropElement.d.ts.map +1 -1
  45. package/src/elements/ShaePropElement.js +264 -219
  46. package/src/elements/ShaePropElement.js.map +2 -2
  47. package/src/elements/ShaeWorkerElement.d.ts +59 -5
  48. package/src/elements/ShaeWorkerElement.d.ts.map +1 -1
  49. package/src/elements/ShaeWorkerElement.js +271 -73
  50. package/src/elements/ShaeWorkerElement.js.map +2 -2
  51. package/src/elements/constants.d.ts +9 -0
  52. package/src/elements/constants.d.ts.map +1 -1
  53. package/src/elements/constants.js +18 -0
  54. package/src/elements/constants.js.map +2 -2
  55. package/src/elements/deferredTeardown.d.ts +24 -0
  56. package/src/elements/deferredTeardown.d.ts.map +1 -0
  57. package/src/elements/deferredTeardown.js +30 -0
  58. package/src/elements/deferredTeardown.js.map +7 -0
  59. package/src/elements/displayContentsRule.d.ts +25 -0
  60. package/src/elements/displayContentsRule.d.ts.map +1 -0
  61. package/src/elements/displayContentsRule.js +37 -0
  62. package/src/elements/displayContentsRule.js.map +7 -0
  63. package/src/elements/events.d.ts +10 -7
  64. package/src/elements/events.d.ts.map +1 -1
  65. package/src/elements/forwardCustomEvents.d.ts +19 -0
  66. package/src/elements/forwardCustomEvents.d.ts.map +1 -0
  67. package/src/elements/forwardCustomEvents.js +47 -0
  68. package/src/elements/forwardCustomEvents.js.map +7 -0
  69. package/src/elements/hostedSlots.d.ts +36 -0
  70. package/src/elements/hostedSlots.d.ts.map +1 -0
  71. package/src/elements/hostedSlots.js +118 -0
  72. package/src/elements/hostedSlots.js.map +7 -0
  73. package/src/elements/parentRemoval.d.ts +18 -0
  74. package/src/elements/parentRemoval.d.ts.map +1 -0
  75. package/src/elements/parentRemoval.js +60 -0
  76. package/src/elements/parentRemoval.js.map +7 -0
  77. package/src/elements/propValueConverters.d.ts +3 -0
  78. package/src/elements/propValueConverters.d.ts.map +1 -0
  79. package/src/elements/propValueConverters.js +54 -0
  80. package/src/elements/propValueConverters.js.map +7 -0
  81. package/src/elements/requestEntAncestor.d.ts +32 -0
  82. package/src/elements/requestEntAncestor.d.ts.map +1 -0
  83. package/src/elements/requestEntAncestor.js +16 -0
  84. package/src/elements/requestEntAncestor.js.map +7 -0
  85. package/src/elements.js.map +1 -1
  86. package/src/in-the-dark/Entity.d.ts +143 -3
  87. package/src/in-the-dark/Entity.d.ts.map +1 -1
  88. package/src/in-the-dark/Entity.js +330 -63
  89. package/src/in-the-dark/Entity.js.map +3 -3
  90. package/src/in-the-dark/Kernel.d.ts +98 -16
  91. package/src/in-the-dark/Kernel.d.ts.map +1 -1
  92. package/src/in-the-dark/Kernel.js +466 -379
  93. package/src/in-the-dark/Kernel.js.map +3 -3
  94. package/src/in-the-dark/Registry.d.ts +7 -1
  95. package/src/in-the-dark/Registry.d.ts.map +1 -1
  96. package/src/in-the-dark/Registry.js +112 -14
  97. package/src/in-the-dark/Registry.js.map +2 -2
  98. package/src/in-the-dark/ShadowObject.d.ts.map +1 -1
  99. package/src/in-the-dark/ShadowObject.js +1 -0
  100. package/src/in-the-dark/ShadowObject.js.map +2 -2
  101. package/src/in-the-dark/ShadowObjectCreationScope.d.ts +133 -0
  102. package/src/in-the-dark/ShadowObjectCreationScope.d.ts.map +1 -0
  103. package/src/in-the-dark/ShadowObjectCreationScope.js +603 -0
  104. package/src/in-the-dark/ShadowObjectCreationScope.js.map +7 -0
  105. package/src/in-the-dark/SignalsPath.d.ts +2 -0
  106. package/src/in-the-dark/SignalsPath.d.ts.map +1 -1
  107. package/src/in-the-dark/SignalsPath.js +6 -2
  108. package/src/in-the-dark/SignalsPath.js.map +2 -2
  109. package/src/in-the-dark/displayName.d.ts +4 -0
  110. package/src/in-the-dark/displayName.d.ts.map +1 -0
  111. package/src/in-the-dark/displayName.js +5 -0
  112. package/src/in-the-dark/displayName.js.map +7 -0
  113. package/src/in-the-dark/events.js.map +1 -1
  114. package/src/in-the-dark/importModule.d.ts +7 -0
  115. package/src/in-the-dark/importModule.d.ts.map +1 -1
  116. package/src/in-the-dark/importModule.js +5 -2
  117. package/src/in-the-dark/importModule.js.map +2 -2
  118. package/src/index.d.ts +9 -1
  119. package/src/index.d.ts.map +1 -1
  120. package/src/index.js +9 -1
  121. package/src/index.js.map +2 -2
  122. package/src/inspect/createKernelSnapshot.d.ts +17 -0
  123. package/src/inspect/createKernelSnapshot.d.ts.map +1 -0
  124. package/src/inspect/createKernelSnapshot.js +247 -0
  125. package/src/inspect/createKernelSnapshot.js.map +7 -0
  126. package/src/inspect/createViewSnapshot.d.ts +10 -0
  127. package/src/inspect/createViewSnapshot.d.ts.map +1 -0
  128. package/src/inspect/createViewSnapshot.js +143 -0
  129. package/src/inspect/createViewSnapshot.js.map +7 -0
  130. package/src/inspect/normalizeInspectRequest.d.ts +32 -0
  131. package/src/inspect/normalizeInspectRequest.d.ts.map +1 -0
  132. package/src/inspect/normalizeInspectRequest.js +55 -0
  133. package/src/inspect/normalizeInspectRequest.js.map +7 -0
  134. package/src/inspect/serializeValue.d.ts +13 -0
  135. package/src/inspect/serializeValue.d.ts.map +1 -0
  136. package/src/inspect/serializeValue.js +149 -0
  137. package/src/inspect/serializeValue.js.map +7 -0
  138. package/src/inspect/types.d.ts +260 -0
  139. package/src/inspect/types.d.ts.map +1 -0
  140. package/src/inspect/types.js +1 -0
  141. package/src/inspect/types.js.map +7 -0
  142. package/src/model-context/ModelContextLike.d.ts +59 -0
  143. package/src/model-context/ModelContextLike.d.ts.map +1 -0
  144. package/src/model-context/ModelContextLike.js +15 -0
  145. package/src/model-context/ModelContextLike.js.map +7 -0
  146. package/src/model-context/exposeShadowEnvsToModelContext.d.ts +48 -0
  147. package/src/model-context/exposeShadowEnvsToModelContext.d.ts.map +1 -0
  148. package/src/model-context/exposeShadowEnvsToModelContext.js +39 -0
  149. package/src/model-context/exposeShadowEnvsToModelContext.js.map +7 -0
  150. package/src/model-context/redactProps.d.ts +14 -0
  151. package/src/model-context/redactProps.d.ts.map +1 -0
  152. package/src/model-context/redactProps.js +22 -0
  153. package/src/model-context/redactProps.js.map +7 -0
  154. package/src/model-context/sharedExposure.d.ts +32 -0
  155. package/src/model-context/sharedExposure.d.ts.map +1 -0
  156. package/src/model-context/sharedExposure.js +108 -0
  157. package/src/model-context/sharedExposure.js.map +7 -0
  158. package/src/model-context/toolSupport.d.ts +72 -0
  159. package/src/model-context/toolSupport.d.ts.map +1 -0
  160. package/src/model-context/toolSupport.js +145 -0
  161. package/src/model-context/toolSupport.js.map +7 -0
  162. package/src/model-context/tools/findEntities.d.ts +15 -0
  163. package/src/model-context/tools/findEntities.d.ts.map +1 -0
  164. package/src/model-context/tools/findEntities.js +71 -0
  165. package/src/model-context/tools/findEntities.js.map +7 -0
  166. package/src/model-context/tools/getEntity.d.ts +16 -0
  167. package/src/model-context/tools/getEntity.d.ts.map +1 -0
  168. package/src/model-context/tools/getEntity.js +65 -0
  169. package/src/model-context/tools/getEntity.js.map +7 -0
  170. package/src/model-context/tools/getEntityTree.d.ts +4 -0
  171. package/src/model-context/tools/getEntityTree.d.ts.map +1 -0
  172. package/src/model-context/tools/getEntityTree.js +58 -0
  173. package/src/model-context/tools/getEntityTree.js.map +7 -0
  174. package/src/model-context/tools/getRegistry.d.ts +14 -0
  175. package/src/model-context/tools/getRegistry.d.ts.map +1 -0
  176. package/src/model-context/tools/getRegistry.js +39 -0
  177. package/src/model-context/tools/getRegistry.js.map +7 -0
  178. package/src/model-context/tools/index.d.ts +9 -0
  179. package/src/model-context/tools/index.d.ts.map +1 -0
  180. package/src/model-context/tools/index.js +16 -0
  181. package/src/model-context/tools/index.js.map +7 -0
  182. package/src/model-context/tools/listEnvs.d.ts +33 -0
  183. package/src/model-context/tools/listEnvs.d.ts.map +1 -0
  184. package/src/model-context/tools/listEnvs.js +37 -0
  185. package/src/model-context/tools/listEnvs.js.map +7 -0
  186. package/src/model-context.d.ts +13 -0
  187. package/src/model-context.d.ts.map +1 -0
  188. package/src/model-context.js +15 -0
  189. package/src/model-context.js.map +7 -0
  190. package/src/shadow-objects.d.ts +4 -0
  191. package/src/shadow-objects.d.ts.map +1 -1
  192. package/src/shadow-objects.js +6 -0
  193. package/src/shadow-objects.js.map +2 -2
  194. package/src/shadow-objects.worker.js +0 -1
  195. package/src/shadow-objects.worker.js.map +2 -2
  196. package/src/shae-ent.js.map +1 -1
  197. package/src/shae-prop.js +2 -2
  198. package/src/shae-prop.js.map +2 -2
  199. package/src/shae-worker.js.map +1 -1
  200. package/src/testing/TestEntity.d.ts +79 -0
  201. package/src/testing/TestEntity.d.ts.map +1 -0
  202. package/src/testing/TestEntity.js +135 -0
  203. package/src/testing/TestEntity.js.map +7 -0
  204. package/src/testing/createTestKernel.d.ts +11 -0
  205. package/src/testing/createTestKernel.d.ts.map +1 -0
  206. package/src/testing/createTestKernel.js +132 -0
  207. package/src/testing/createTestKernel.js.map +7 -0
  208. package/src/testing/mountShadowObject.d.ts +21 -0
  209. package/src/testing/mountShadowObject.d.ts.map +1 -0
  210. package/src/testing/mountShadowObject.js +72 -0
  211. package/src/testing/mountShadowObject.js.map +7 -0
  212. package/src/testing/recordKernelErrors.d.ts +21 -0
  213. package/src/testing/recordKernelErrors.d.ts.map +1 -0
  214. package/src/testing/recordKernelErrors.js +39 -0
  215. package/src/testing/recordKernelErrors.js.map +7 -0
  216. package/src/testing/settle.d.ts +15 -0
  217. package/src/testing/settle.d.ts.map +1 -0
  218. package/src/testing/settle.js +19 -0
  219. package/src/testing/settle.js.map +7 -0
  220. package/src/testing/types.d.ts +119 -0
  221. package/src/testing/types.d.ts.map +1 -0
  222. package/src/testing/types.js +1 -0
  223. package/src/testing/types.js.map +7 -0
  224. package/src/testing.d.ts +14 -0
  225. package/src/testing.d.ts.map +1 -0
  226. package/src/testing.js +11 -0
  227. package/src/testing.js.map +7 -0
  228. package/src/types.d.ts +185 -14
  229. package/src/types.d.ts.map +1 -1
  230. package/src/utils/ConsoleLogger.d.ts +45 -10
  231. package/src/utils/ConsoleLogger.d.ts.map +1 -1
  232. package/src/utils/ConsoleLogger.js +51 -26
  233. package/src/utils/ConsoleLogger.js.map +2 -2
  234. package/src/utils/FrameLoop.d.ts +42 -3
  235. package/src/utils/FrameLoop.d.ts.map +1 -1
  236. package/src/utils/FrameLoop.js +71 -10
  237. package/src/utils/FrameLoop.js.map +2 -2
  238. package/src/utils/MicrotaskCollector.d.ts +45 -0
  239. package/src/utils/MicrotaskCollector.d.ts.map +1 -0
  240. package/src/utils/MicrotaskCollector.js +33 -0
  241. package/src/utils/MicrotaskCollector.js.map +7 -0
  242. package/src/utils/MicrotaskGate.d.ts +17 -0
  243. package/src/utils/MicrotaskGate.d.ts.map +1 -0
  244. package/src/utils/MicrotaskGate.js +20 -0
  245. package/src/utils/MicrotaskGate.js.map +7 -0
  246. package/src/utils/array-utils.js.map +1 -1
  247. package/src/utils/attr-utils.d.ts +11 -0
  248. package/src/utils/attr-utils.d.ts.map +1 -1
  249. package/src/utils/attr-utils.js +5 -1
  250. package/src/utils/attr-utils.js.map +2 -2
  251. package/src/utils/constants.js.map +1 -1
  252. package/src/utils/generateUUID.d.ts +11 -0
  253. package/src/utils/generateUUID.d.ts.map +1 -1
  254. package/src/utils/generateUUID.js +31 -266
  255. package/src/utils/generateUUID.js.map +2 -2
  256. package/src/utils/props-utils.d.ts +3 -2
  257. package/src/utils/props-utils.d.ts.map +1 -1
  258. package/src/utils/props-utils.js +13 -24
  259. package/src/utils/props-utils.js.map +2 -2
  260. package/src/utils/runGuarded.d.ts +27 -0
  261. package/src/utils/runGuarded.d.ts.map +1 -0
  262. package/src/utils/runGuarded.js +11 -0
  263. package/src/utils/runGuarded.js.map +7 -0
  264. package/src/utils/toMaybe.d.ts.map +1 -1
  265. package/src/utils/toMaybe.js.map +1 -1
  266. package/src/utils/toNamespace.d.ts.map +1 -1
  267. package/src/utils/toNamespace.js.map +1 -1
  268. package/src/utils/toUrlString.d.ts +12 -0
  269. package/src/utils/toUrlString.d.ts.map +1 -1
  270. package/src/utils/toUrlString.js.map +2 -2
  271. package/src/utils/waitForMessageOfType.d.ts +12 -1
  272. package/src/utils/waitForMessageOfType.d.ts.map +1 -1
  273. package/src/utils/waitForMessageOfType.js +16 -1
  274. package/src/utils/waitForMessageOfType.js.map +2 -2
  275. package/src/view/ComponentChanges.d.ts +88 -1
  276. package/src/view/ComponentChanges.d.ts.map +1 -1
  277. package/src/view/ComponentChanges.js +243 -73
  278. package/src/view/ComponentChanges.js.map +2 -2
  279. package/src/view/ComponentContext.d.ts +206 -16
  280. package/src/view/ComponentContext.d.ts.map +1 -1
  281. package/src/view/ComponentContext.js +548 -91
  282. package/src/view/ComponentContext.js.map +2 -2
  283. package/src/view/ComponentMemory.d.ts +3 -3
  284. package/src/view/ComponentMemory.d.ts.map +1 -1
  285. package/src/view/ComponentMemory.js.map +1 -1
  286. package/src/view/IShadowObjectEnvProxy.d.ts +38 -3
  287. package/src/view/IShadowObjectEnvProxy.d.ts.map +1 -1
  288. package/src/view/LocalShadowObjectEnv.d.ts +8 -1
  289. package/src/view/LocalShadowObjectEnv.d.ts.map +1 -1
  290. package/src/view/LocalShadowObjectEnv.js +23 -7
  291. package/src/view/LocalShadowObjectEnv.js.map +2 -2
  292. package/src/view/RemoteWorkerEnv.d.ts +108 -5
  293. package/src/view/RemoteWorkerEnv.d.ts.map +1 -1
  294. package/src/view/RemoteWorkerEnv.js +244 -77
  295. package/src/view/RemoteWorkerEnv.js.map +2 -2
  296. package/src/view/ShadowEnv.d.ts +79 -2
  297. package/src/view/ShadowEnv.d.ts.map +1 -1
  298. package/src/view/ShadowEnv.js +323 -59
  299. package/src/view/ShadowEnv.js.map +3 -3
  300. package/src/view/ViewComponent.d.ts +78 -14
  301. package/src/view/ViewComponent.d.ts.map +1 -1
  302. package/src/view/ViewComponent.js +90 -14
  303. package/src/view/ViewComponent.js.map +2 -2
  304. package/src/view/cloneChangeTrail.js.map +2 -2
  305. package/src/worker/MessageRouter.d.ts +14 -0
  306. package/src/worker/MessageRouter.d.ts.map +1 -1
  307. package/src/worker/MessageRouter.js +98 -15
  308. package/src/worker/MessageRouter.js.map +2 -2
  309. package/src/worker/WorkerRuntime.d.ts +22 -1
  310. package/src/worker/WorkerRuntime.d.ts.map +1 -1
  311. package/src/worker/WorkerRuntime.js +60 -8
  312. package/src/worker/WorkerRuntime.js.map +2 -2
@@ -1,4 +1,4 @@
1
- import { emit, off, on, once, Priority } from "@spearwolf/eventize";
1
+ import { emitStrict, off, on, once, Priority } from "@spearwolf/eventize";
2
2
  import {
3
3
  batch,
4
4
  createSignal,
@@ -7,24 +7,31 @@ import {
7
7
  SignalAutoMap,
8
8
  value
9
9
  } from "@spearwolf/signalize";
10
+ import { MicrotaskCollector } from "../utils/MicrotaskCollector.js";
11
+ import { runGuarded } from "../utils/runGuarded.js";
10
12
  import { onDestroy, onViewEvent } from "./events.js";
11
13
  import { Kernel } from "./Kernel.js";
12
14
  import { SignalsPath } from "./SignalsPath.js";
13
- const updateContextValues = /* @__PURE__ */ new Map();
14
- let requestedContextValueBatchUpdate = false;
15
- const deferContextValueUpdate = (sig, val) => {
16
- updateContextValues.set(sig, val);
17
- if (!requestedContextValueBatchUpdate) {
18
- requestedContextValueBatchUpdate = true;
19
- queueMicrotask(() => {
20
- requestedContextValueBatchUpdate = false;
21
- const contextValues = Array.from(updateContextValues.entries());
22
- updateContextValues.clear();
23
- for (const [sig2, val2] of contextValues) {
24
- sig2.set(val2);
15
+ const contextValueCollectors = /* @__PURE__ */ new WeakMap();
16
+ const collectorOf = (kernel) => {
17
+ let collector = contextValueCollectors.get(kernel);
18
+ if (collector == null) {
19
+ collector = new MicrotaskCollector((contextValues) => {
20
+ for (const [contextSignal, entry] of contextValues) {
21
+ runGuarded(
22
+ kernel.logger,
23
+ () => contextSignal.set(entry.value),
24
+ `an effect of a context value failed (${String(entry.name)}):`,
25
+ entry.uuid
26
+ );
25
27
  }
26
28
  });
29
+ contextValueCollectors.set(kernel, collector);
27
30
  }
31
+ return collector;
32
+ };
33
+ const deferContextValueUpdate = (kernel, signal, val, name, uuid) => {
34
+ collectorOf(kernel).add(signal, { value: val, name, uuid });
28
35
  };
29
36
  class Entity {
30
37
  #kernel;
@@ -32,11 +39,14 @@ class Entity {
32
39
  #props = new SignalAutoMap();
33
40
  #context = /* @__PURE__ */ new Map();
34
41
  #rootContexts = /* @__PURE__ */ new Map();
35
- #parentUuid;
42
+ // There is one place the parent is kept: `parentUuid` answers from `#parent.uuid`, so a half-set
43
+ // link -- a uuid with no matching entity, or an entity with no uuid to show for it -- has no field
44
+ // left to hold it in.
36
45
  #parent;
37
46
  #childrenUuids = /* @__PURE__ */ new Set();
38
47
  #children = [];
39
48
  #order = 0;
49
+ #isReleased = false;
40
50
  get kernel() {
41
51
  return this.#kernel;
42
52
  }
@@ -49,37 +59,40 @@ class Entity {
49
59
  set order(value2) {
50
60
  if (this.#order !== value2) {
51
61
  this.#order = value2;
52
- if (this.#parentUuid) {
53
- this.parent.resortChildren();
62
+ if (this.#parent) {
63
+ this.#parent.resortChildren();
54
64
  }
65
+ this.#kernel.noteEntityTreeChange(this.#uuid);
55
66
  }
56
67
  }
57
68
  get parentUuid() {
58
- return this.#parentUuid || void 0;
69
+ return this.#parent?.uuid;
59
70
  }
60
71
  set parentUuid(parentUuid) {
61
- if (this.#parentUuid !== parentUuid) {
72
+ if (this.#parent?.uuid !== parentUuid) {
62
73
  const nextParent = parentUuid ? this.#kernel.getEntity(parentUuid) : void 0;
63
- this.removeFromParent();
64
- this.#parentUuid = parentUuid || void 0;
74
+ if (nextParent != null) {
75
+ this.assertAttachableTo(nextParent);
76
+ }
77
+ this.#detachFromParent(nextParent == null);
65
78
  this.#parent = nextParent;
66
79
  if (this.#parent) {
67
80
  this.#parent.addChild(this);
68
81
  }
69
82
  this.#updateAutoDestructionSubscription();
83
+ this.#kernel.noteEntityTreeChange(this.#uuid);
70
84
  }
71
85
  }
72
86
  get parent() {
73
- if (!this.#parent && this.#parentUuid) {
74
- this.#parent = this.#kernel.getEntity(this.#parentUuid);
75
- }
76
87
  return this.#parent;
77
88
  }
78
89
  set parent(parent) {
79
90
  this.parentUuid = parent?.uuid;
80
91
  }
92
+ // Reads the same field `parent` and `parentUuid` do, so `Kernel.noteEntityTreeChange()` -- which
93
+ // reads `hasParent` to decide whether the uuid belongs in the root set -- sees what they see.
81
94
  get hasParent() {
82
- return !!this.#parentUuid;
95
+ return !!this.#parent;
83
96
  }
84
97
  get children() {
85
98
  return this.#children;
@@ -87,74 +100,166 @@ class Entity {
87
100
  constructor(kernel, uuid) {
88
101
  this.#kernel = kernel;
89
102
  this.#uuid = uuid;
90
- once(this, onDestroy, Priority.Min, this);
91
103
  }
92
104
  traverse(callback) {
105
+ this.#traverse(callback, /* @__PURE__ */ new Set());
106
+ }
107
+ // The visited set guards the one traversal an outside shadow object drives, over a tree it did not
108
+ // build. `addChild()` writes a children list without touching the parent link, so no ancestor check
109
+ // can cover it.
110
+ #traverse(callback, visited) {
111
+ if (visited.has(this)) return;
112
+ visited.add(this);
93
113
  callback(this);
94
114
  for (const child of this.#children) {
95
- child.traverse(callback);
115
+ child.#traverse(callback, visited);
96
116
  }
97
117
  }
118
+ /**
119
+ * Releases everything the entity holds: its properties, its subscriptions, its contexts and its place
120
+ * in the entity tree.
121
+ *
122
+ * It runs once, whichever way it is reached. The kernel reaches it directly, right behind the
123
+ * destruction notification it sends on the entity, so that every listener on that notification has
124
+ * had its turn first -- down to `Priority.Min`, which is nobody's but the caller's. The entity is
125
+ * not a listener on its own notification: registering it in the constructor would put it ahead of
126
+ * anything registered at the same priority, because eventize breaks a tie by order of registration,
127
+ * and the bulk `off(this)` below would then take the listeners still waiting in that delivery with
128
+ * it. The flag makes a second call cost nothing and keeps every step below written for a
129
+ * single run: `kernel.getEntity()` hands the entity to anyone, and this method is as reachable as
130
+ * the rest of it.
131
+ *
132
+ * The flag is raised before the first step rather than after the last, because that is what a release
133
+ * running twice would cost: the steps below are written for one pass, not for a repeat.
134
+ *
135
+ * Every step below runs behind a guard of its own, the way `ShadowObjectCreationScope.tearDown()`
136
+ * gives each of its steps one: a step that throws is reported through the kernel's logger, named by
137
+ * the step and by the uuid of this entity, and costs only itself -- the steps behind it still run.
138
+ * The method therefore never throws to its own caller. The plain resets at the end -- clearing a
139
+ * collection, overwriting a field -- cannot throw and run unguarded, exactly as `tearDown()` empties
140
+ * its own sets at the end.
141
+ */
98
142
  [onDestroy]() {
99
- this.#props.clear();
100
- off(this);
101
- this.#autoDestructionSubscription?.();
143
+ if (this.#isReleased) return;
144
+ this.#isReleased = true;
145
+ this.#runGuarded("properties", () => this.#props.clear());
146
+ this.#runGuarded("listeners", () => off(this));
147
+ this.#runGuarded("auto-destruction subscription", () => this.#autoDestructionSubscription?.());
102
148
  for (const rootCtx of this.#rootContexts.values()) {
103
- rootCtx.cleanup();
104
- rootCtx.signal.destroy();
149
+ this.#runGuarded("global context cleanup", () => rootCtx.cleanup());
150
+ this.#runGuarded("global context signal", () => rootCtx.signal.destroy());
105
151
  }
106
- this.#rootContexts.clear();
107
152
  for (const ctx of this.#context.values()) {
108
- ctx.context.set(void 0);
109
- ctx.unsubscribePathValue();
110
- ctx.unsubscribeFromParent?.();
111
- ctx.valuePath.dispose();
112
- ctx.inherited.destroy();
113
- ctx.provide.destroy();
114
- ctx.context.destroy();
153
+ this.#runGuarded("context value reset", () => ctx.context.set(void 0));
154
+ this.#runGuarded("context value subscription", () => ctx.unsubscribePathValue());
155
+ this.#runGuarded("context parent subscription", () => ctx.unsubscribeFromParent?.());
156
+ this.#runGuarded("context value path", () => ctx.valuePath.dispose());
157
+ this.#runGuarded("inherited context signal", () => ctx.inherited.destroy());
158
+ this.#runGuarded("provided context signal", () => ctx.provide.destroy());
159
+ this.#runGuarded("context signal", () => ctx.context.destroy());
115
160
  }
116
- this.#parentUuid = void 0;
161
+ this.#rootContexts.clear();
162
+ this.#context.clear();
117
163
  this.#parent = void 0;
118
164
  this.#childrenUuids.clear();
119
165
  this.#children.length = 0;
120
166
  }
121
- addChild(child) {
122
- if (this.#children.length === 0) {
123
- this.#childrenUuids.add(child.uuid);
124
- this.#children.push(child);
125
- return;
167
+ /**
168
+ * Isolates one release step from the ones around it: a step that throws is reported through the
169
+ * kernel's logger and does not stop `[onDestroy]()` from reaching the steps that follow. The
170
+ * report goes out through the shared `runGuarded()`; what this wrapper adds is the label of the
171
+ * step and the uuid of the entity it belongs to.
172
+ */
173
+ #runGuarded(step, run) {
174
+ runGuarded(this.#kernel.logger, run, `entity teardown step failed (${step}):`, this.#uuid);
175
+ }
176
+ /**
177
+ * Refuses a parent that would close the entity tree into a ring: the entity itself, or an entity that
178
+ * already sits below it. The walk follows the parent chain upwards, which is a chain exactly because
179
+ * this guard keeps it one.
180
+ *
181
+ * `Kernel.setParent()` calls it before it detaches, and the `parentUuid` setter before it resolves the
182
+ * link, so a refused attachment leaves the entity where it was instead of orphaned halfway through.
183
+ * `ViewComponent.addChild()` guards the same thing on the view side.
184
+ */
185
+ assertAttachableTo(nextParent) {
186
+ for (let e = nextParent; e != null; e = e.parent) {
187
+ if (e === this) {
188
+ throw new Error(
189
+ `entity "${nextParent.uuid}" cannot become the parent of "${this.uuid}": it is the entity itself or one of its descendants`
190
+ );
191
+ }
126
192
  }
193
+ }
194
+ /**
195
+ * Puts `child` into the children list, at its place among the siblings.
196
+ *
197
+ * Nothing here notifies. Neither this method nor {@link Entity.removeChild} nor a detachment
198
+ * sends anything to a listener on the entity -- the one notification over a moved entity is
199
+ * `onParentChanged`, and `Kernel.setParent()` is the call that sends it.
200
+ */
201
+ addChild(child) {
127
202
  if (this.#childrenUuids.has(child.uuid)) {
128
203
  throw new Error(`child with uuid: ${child.uuid} already exists! parentUuid: ${this.uuid}`);
129
204
  }
130
205
  this.#childrenUuids.add(child.uuid);
131
- this.#children.push(child);
132
- this.resortChildren();
206
+ this.#insertChildInOrder(child);
133
207
  for (const [, ctx] of child.#context) {
134
208
  child.#subscribeToParent(ctx);
135
209
  }
136
210
  }
211
+ /**
212
+ * Puts a child at its place among the siblings, so building a subtree costs one insertion per
213
+ * child. The scan runs from the end because children usually arrive with an equal or rising
214
+ * `order`, which makes the common case a plain append. A child lands behind every sibling that
215
+ * shares its `order` -- the same place a stable sort would give it.
216
+ *
217
+ * The result only matches a full sort while `#children` is already sorted when the insertion
218
+ * starts, so name what keeps it that way: the insertion here preserves the order, `removeChild()`
219
+ * cuts an element out with `splice`, the destroy handler empties the list, and the `order` setter
220
+ * re-sorts through the parent. That last one is the condition worth stating, because it needs the
221
+ * child to know its parent -- the parent link has to be established. Every way an entity is
222
+ * attached inside this class goes through the `parentUuid` setter, which sets the parent before it
223
+ * calls `addChild()`. A caller that reaches for `addChild()` on its own leaves the child without a
224
+ * parent link, and a later `order` on such a child never reaches this list.
225
+ */
226
+ #insertChildInOrder(child) {
227
+ let i = this.#children.length;
228
+ while (i > 0 && child.order < this.#children[i - 1].order) i--;
229
+ this.#children.splice(i, 0, child);
230
+ }
137
231
  resortChildren() {
138
232
  this.#children.sort((a, b) => a.order - b.order);
139
233
  }
140
234
  removeChild(child) {
141
- if (this.#childrenUuids.has(child.uuid)) {
235
+ const idx = this.#children.indexOf(child);
236
+ if (idx !== -1) {
142
237
  this.#childrenUuids.delete(child.uuid);
143
- this.#children.splice(this.#children.indexOf(child), 1);
238
+ this.#children.splice(idx, 1);
144
239
  }
145
240
  }
146
- removeFromParent() {
241
+ /**
242
+ * @param rebindContexts bind every context back to the root of the entity context tree. An entity
243
+ * that carries a context out of a subtree keeps following the value it can still see, rather
244
+ * than freezing on the last one the former parent held. Only a caller that attaches the entity
245
+ * to the next parent in the same breath passes `false` -- that parent binds the contexts itself,
246
+ * and the detour over the root would be visible to `useParentContext()`, which hands out the
247
+ * inherited value without the microtask collector `useContext()` has in front of it.
248
+ */
249
+ removeFromParent(rebindContexts = true) {
250
+ this.#detachFromParent(rebindContexts);
251
+ }
252
+ #detachFromParent(rebindContexts) {
147
253
  if (this.#parent) {
148
254
  this.#parent.removeChild(this);
149
255
  this.#parent = void 0;
150
- this.#parentUuid = void 0;
151
- for (const [, ctx] of this.#context) {
152
- if (ctx.unsubscribeFromParent) {
153
- ctx.unsubscribeFromParent();
154
- ctx.unsubscribeFromParent = void 0;
256
+ if (rebindContexts) {
257
+ for (const [, ctx] of this.#context) {
258
+ this.#subscribeToParent(ctx);
155
259
  }
156
260
  }
157
261
  this.#unsubscribeAutoDestruction();
262
+ this.#kernel.noteEntityTreeChange(this.#uuid);
158
263
  }
159
264
  }
160
265
  #autoDestructionEnabled = false;
@@ -182,6 +287,15 @@ class Entity {
182
287
  });
183
288
  }
184
289
  }
290
+ /**
291
+ * Binds every context of this entity to its current position in the entity tree -- to the parent
292
+ * if there is one, to the root otherwise. `addChild()` and `removeFromParent()` each do this for
293
+ * the entity they move, so a caller needs this only when it changed the position by other means
294
+ * or wants the binding re-established without knowing which of the two ran.
295
+ *
296
+ * Moving an entity through the kernel is not such a case: both directions bring their own binding,
297
+ * and a third one would reach every `useParentContext()` reader as another change.
298
+ */
185
299
  reSubscribeToParentContexts() {
186
300
  for (const [, ctx] of this.#context) {
187
301
  this.#subscribeToParent(ctx);
@@ -190,9 +304,28 @@ class Entity {
190
304
  dispatchMessageToView(type, data, transferables, traverseChildren = false) {
191
305
  this.#kernel.dispatchMessageToView({ uuid: this.#uuid, type, data, transferables, traverseChildren });
192
306
  }
307
+ /**
308
+ * Hands the events the view sent to whoever listens on this entity, one delivery per event.
309
+ *
310
+ * Each delivery is guarded, and the reason is where this call sits: the kernel makes it from
311
+ * inside a change trail, on a path that builds. Unguarded, a single shadow-object that cannot cope
312
+ * with a single view event would leave through `Kernel.run()` and refuse the whole trail -- the
313
+ * same argument that puts a guard around the `onParentChanged` notification, which is a delivery
314
+ * on a building path as well. The guard is per event, so one that fails costs neither the events
315
+ * behind it nor the caller anything; `emitStrict()` inside it makes the same promise one level
316
+ * down, to the listeners of one event.
317
+ */
193
318
  dispatchViewEvents(events) {
194
319
  for (const { type, data } of events) {
195
- emit(this, onViewEvent, type, data);
320
+ runGuarded(
321
+ this.#kernel.logger,
322
+ // the concrete class is needed here: eventize cannot reduce its emitter conditional
323
+ // (`NonTypedEmitter<this>`) over the polymorphic `this` type, and no overload then matches
324
+ () => emitStrict(this, onViewEvent, type, data),
325
+ "entity view event delivery failed:",
326
+ type,
327
+ this.#uuid
328
+ );
196
329
  }
197
330
  }
198
331
  dispatchViewEvent(type, data) {
@@ -204,11 +337,30 @@ class Entity {
204
337
  getPropertyReader(key) {
205
338
  return this.#getPropSignal(key).get;
206
339
  }
340
+ /**
341
+ * A write head for one property. Every write through it drops the cache behind
342
+ * `truthyProps()`: that cache answers `Kernel.#updateShadowObjects()`, which picks the shadow
343
+ * objects of this entity along the property routes of the registry, and a write the cache does
344
+ * not see routes the entity to a state it no longer has.
345
+ *
346
+ * What comes back is a function of the entity, not the `set` of the signal behind the property.
347
+ * The signalize helpers that take a signal-like -- `isSignal()`, `destroySignal()`, `touch()` --
348
+ * do not recognise it; `getPropertyReader()` hands out the signal's own reader for that.
349
+ *
350
+ * A fresh closure is handed out on every call rather than one cached per key: `SignalAutoMap`
351
+ * hands out "a fresh, live" signal for a key whose signal was destroyed from the outside, so a
352
+ * writer cached against the signal captured on an earlier call could end up closing over a corpse.
353
+ * The call sites this feeds -- `setProperty()` from a change-trail application, at most once per
354
+ * property that actually changed -- do not turn this into a hot path.
355
+ */
207
356
  getPropertyWriter(key) {
208
- return this.#getPropSignal(key).set;
357
+ const signal = this.#getPropSignal(key);
358
+ return (value2, params) => {
359
+ this.clearTruthyPropsCache();
360
+ signal.set(value2, params);
361
+ };
209
362
  }
210
363
  setProperties(properties) {
211
- this.clearTruthyPropsCache();
212
364
  batch(() => {
213
365
  for (const [key, val] of properties) {
214
366
  this.setProperty(key, val);
@@ -250,6 +402,44 @@ class Entity {
250
402
  hasContext(name) {
251
403
  return this.#context.has(name);
252
404
  }
405
+ /** The names of the Entity Contexts this entity holds, used or provided. Reads only, creates none. */
406
+ contextNames() {
407
+ return Array.from(this.#context.keys());
408
+ }
409
+ /**
410
+ * The three values behind one Entity Context of this entity: what its own providers wrote, what it
411
+ * inherits from the parent (or from the global chain at a root), and what `useContext(name)` reads.
412
+ * `hasProviders` says whether a provider feed is attached -- a value written straight into the
413
+ * signal from `provideContext()` counts as provided but not as a provider.
414
+ *
415
+ * `undefined` when the entity holds no context of that name; asking does not create one. The
416
+ * reads are plain `.value` reads and track nothing, so a call from inside an effect does not
417
+ * subscribe that effect to the context.
418
+ */
419
+ describeContext(name) {
420
+ const ctx = this.#context.get(name);
421
+ if (ctx === void 0) return void 0;
422
+ return {
423
+ provided: ctx.provide.value,
424
+ inherited: ctx.inherited.value,
425
+ effective: ctx.context.value,
426
+ hasProviders: ctx.providerFeeds.size > 0
427
+ };
428
+ }
429
+ /** The names of the global Entity Contexts this entity contributes to. Reads only, creates none. */
430
+ globalContextNames() {
431
+ return Array.from(this.#rootContexts.keys());
432
+ }
433
+ /**
434
+ * What this entity contributes to the kernel-wide chain of one global Entity Context: the value
435
+ * and the very signal that stands in the chain, so a caller holding the chain can find this entity
436
+ * in it by identity. `undefined` when the entity contributes nothing under that name.
437
+ */
438
+ describeGlobalContext(name) {
439
+ const rootCtx = this.#rootContexts.get(name);
440
+ if (rootCtx === void 0) return void 0;
441
+ return { value: rootCtx.signal.value, signal: rootCtx.signal, hasProviders: rootCtx.providerFeeds.size > 0 };
442
+ }
253
443
  useContext(name) {
254
444
  return this.#findOrCreateContext(name).context.get;
255
445
  }
@@ -260,14 +450,91 @@ class Entity {
260
450
  return this.#findOrCreateContext(name).provide;
261
451
  }
262
452
  provideGlobalContext(name) {
453
+ return this.#findOrCreateGlobalContext(name).signal;
454
+ }
455
+ /**
456
+ * Attaches a provider signal to the context of `name` on this entity and answers with the release
457
+ * that detaches it again.
458
+ *
459
+ * Every provider of one name writes into the single signal that `provideContext()` hands out, so
460
+ * what a consumer reads is what was written last. The release therefore does two things: it cuts
461
+ * this feed, and it then lets a provider that is still attached write its value once more. Without
462
+ * that second step the context would keep whatever the departure left standing -- the `undefined`
463
+ * of a clearing provider, or the last value of one that opted out of it -- even though this entity
464
+ * still has a provider of the name that says otherwise.
465
+ *
466
+ * A value written straight into the signal from `provideContext()` is not a feed and is therefore
467
+ * overwritten by the next release. Feeding a value in through this method is what makes it survive
468
+ * one.
469
+ */
470
+ attachContextProvider(name, provider) {
471
+ const ctx = this.#findOrCreateContext(name);
472
+ return Entity.#attachProviderFeed(provider, ctx.provide, ctx.providerFeeds);
473
+ }
474
+ /**
475
+ * The counterpart of {@link Entity.attachContextProvider} for the global context of `name`.
476
+ *
477
+ * Within one entity the situation is the same one: every provider feeds the single signal this
478
+ * entity contributes to the kernel-wide chain of that name, and a release hands the name back to a
479
+ * provider that stays. Across entities the chain decides on its own -- it resolves to the first
480
+ * entry that holds something, so an entity whose signal falls empty lets the next one through.
481
+ */
482
+ attachGlobalContextProvider(name, provider) {
483
+ const rootCtx = this.#findOrCreateGlobalContext(name);
484
+ return Entity.#attachProviderFeed(provider, rootCtx.signal, rootCtx.providerFeeds);
485
+ }
486
+ static #attachProviderFeed(provider, target, feeds) {
487
+ const feed = link(provider, target);
488
+ feeds.add(feed);
489
+ return () => {
490
+ if (!feeds.delete(feed)) return;
491
+ feed.destroy();
492
+ Entity.#handOverToRemainingProvider(feeds);
493
+ };
494
+ }
495
+ /**
496
+ * Lets the provider that stays write its value into the context signal again.
497
+ *
498
+ * The set iterates in the order the providers were attached and the loop keeps the last hit, so the
499
+ * winner is the one attached last that still holds a value. Attachment order is the order in which the
500
+ * providers took the name: attaching feeds the value straight through, so a later attachment writes
501
+ * over an earlier one, and the hand-over falls back on that order instead of inventing one of its own.
502
+ * It is not the order of the writes -- a provider that writes to its signal after attaching carries
503
+ * the name until the next departure, and the hand-over does not restore that write, because what this
504
+ * entity keeps on file is its providers and not the sequence in which they wrote. Providers holding
505
+ * nothing are passed over -- by the same `!= null` rule with which `SignalsPath` resolves a context
506
+ * chain, and for the same reason: a provider without a value has nothing to say about the name, and
507
+ * electing it would clear a name that is still being provided.
508
+ *
509
+ * A feed whose link is destroyed is passed over as well. A shadow-object owns the signal it was
510
+ * handed and may end it early, which destroys the link without the release ever having run: such a
511
+ * feed still reports a value through its source while its writes go nowhere, so electing it would
512
+ * leave the write of the departure standing, silently and over a provider that is alive.
513
+ *
514
+ * `touch()` is what makes the winner write, because `SignalLink.write()` is protected: touching is
515
+ * the only public way to push the current value of a source through its link again, and the source
516
+ * of a provider that merely stays has no reason to change.
517
+ *
518
+ * Where no remaining feed qualifies, nothing is written: then what the departure left stands,
519
+ * which is the `undefined` of a clearing provider or the last value of one that opted out.
520
+ */
521
+ static #handOverToRemainingProvider(feeds) {
522
+ let winner;
523
+ for (const feed of feeds) {
524
+ if (!feed.isDestroyed && feed.source.value != null) winner = feed;
525
+ }
526
+ winner?.touch();
527
+ }
528
+ #findOrCreateGlobalContext(name) {
263
529
  if (this.#rootContexts.has(name)) {
264
- return this.#rootContexts.get(name).signal;
530
+ return this.#rootContexts.get(name);
265
531
  }
266
532
  const rootCtx = this.#kernel.findOrCreateRootContext(name);
267
533
  const signal = createSignal();
268
534
  const cleanup = rootCtx.add(signal);
269
- this.#rootContexts.set(name, { cleanup, signal });
270
- return signal;
535
+ const ctx = { cleanup, signal, providerFeeds: /* @__PURE__ */ new Set() };
536
+ this.#rootContexts.set(name, ctx);
537
+ return ctx;
271
538
  }
272
539
  #findOrCreateContext(name) {
273
540
  if (this.#context.has(name)) {
@@ -278,9 +545,9 @@ class Entity {
278
545
  const context = createSignal();
279
546
  const valuePath = new SignalsPath([provide, inherited]);
280
547
  const unsubscribePathValue = on(valuePath, SignalsPath.Value, (val) => {
281
- deferContextValueUpdate(context, val);
548
+ deferContextValueUpdate(this.#kernel, context, val, name, this.#uuid);
282
549
  });
283
- const ctx = { name, inherited, provide, context, valuePath, unsubscribePathValue };
550
+ const ctx = { name, inherited, provide, context, valuePath, providerFeeds: /* @__PURE__ */ new Set(), unsubscribePathValue };
284
551
  this.#context.set(name, ctx);
285
552
  this.#subscribeToParent(ctx);
286
553
  return ctx;