@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,19 +1,15 @@
1
- import { emit, eventize, off, on, once, Priority } from "@spearwolf/eventize";
2
- import {
3
- batch,
4
- createEffect,
5
- createMemo,
6
- createSignal,
7
- destroySignal,
8
- isSignal,
9
- link
10
- } from "@spearwolf/signalize";
1
+ import { emitSafe, emitStrict, eventize, off, on } from "@spearwolf/eventize";
2
+ import { batch } from "@spearwolf/signalize";
3
+ import { ChangeTrailRefusedError } from "../ChangeTrailRefusedError.js";
11
4
  import { ComponentChangeType, MessageToView } from "../constants.js";
5
+ import { EntityUuidInUseError } from "../EntityUuidInUseError.js";
12
6
  import { ConsoleLogger } from "../utils/ConsoleLogger.js";
13
- import { toMaybe } from "../utils/toMaybe.js";
7
+ import { runGuarded } from "../utils/runGuarded.js";
8
+ import { getDisplayName } from "./displayName.js";
14
9
  import { Entity } from "./Entity.js";
15
10
  import { onCreate, onDestroy, onParentChanged, onViewEvent } from "./events.js";
16
11
  import { Registry } from "./Registry.js";
12
+ import { ShadowObjectCreationScope } from "./ShadowObjectCreationScope.js";
17
13
  import { SignalsPath } from "./SignalsPath.js";
18
14
  var ShadowObjectAction = /* @__PURE__ */ ((ShadowObjectAction2) => {
19
15
  ShadowObjectAction2[ShadowObjectAction2["CreateAndDestroy"] = 0] = "CreateAndDestroy";
@@ -21,38 +17,69 @@ var ShadowObjectAction = /* @__PURE__ */ ((ShadowObjectAction2) => {
21
17
  ShadowObjectAction2[ShadowObjectAction2["DestroyOnly"] = 2] = "DestroyOnly";
22
18
  return ShadowObjectAction2;
23
19
  })(ShadowObjectAction || {});
24
- const getDisplayName = (construct) => construct.displayName || construct.name;
25
- let provideContextOptionsDeprecatedShown = false;
26
- let provideGlobalContextOptionsDeprecatedShown = false;
27
- let useContextOptionsDeprecatedShown = false;
28
- let useParentContextOptionsDeprecatedShown = false;
29
- let usePropertyOptionsDeprecatedShown = false;
20
+ const LIFECYCLE_HOOKS = [
21
+ ["onCreate", onCreate],
22
+ ["onDestroy", onDestroy],
23
+ ["onParentChanged", onParentChanged],
24
+ ["onViewEvent", onViewEvent]
25
+ ];
30
26
  class Kernel {
27
+ #logger = new ConsoleLogger("Kernel");
28
+ /** The logger this Kernel reports through. */
29
+ get logger() {
30
+ return this.#logger;
31
+ }
32
+ #entities = /* @__PURE__ */ new Map();
33
+ #rootEntities = /* @__PURE__ */ new Set();
34
+ #allEntities = [];
35
+ #allEntitiesReversed = [];
36
+ #allEntitiesNeedUpdate = true;
37
+ #rootContexts = /* @__PURE__ */ new Map();
38
+ /**
39
+ * The creation scope of a shadow-object, keyed by the shadow-object itself.
40
+ *
41
+ * `#destroyShadowObject()` needs to reach the scope from the outside when a shadow-object leaves the
42
+ * constructor set of an entity that stays alive. The scope removes its own entry when it tears down,
43
+ * so nothing here outlives the shadow-object it belongs to.
44
+ *
45
+ * The key is the shadow-object instance, which assumes one instance per construction. A
46
+ * constructor handing out the same instance twice — to a second entity, or to the same one
47
+ * under another token — leaves only the later scope reachable from here; the earlier one
48
+ * still tears down when its entity is destroyed.
49
+ */
50
+ #shadowObjectScopes = /* @__PURE__ */ new WeakMap();
51
+ // The member names whose deprecation report this kernel has already made, one entry each. Handed
52
+ // to every creation scope this kernel builds, which is what gives the report the lifetime of the
53
+ // kernel rather than that of this module: an application running two shadow environments would
54
+ // otherwise report the deprecated call form to whichever of them got there first and to no other.
55
+ // One entry per name rather than a single flag, because a flag would swallow the reports of the
56
+ // four members that come after the first. `destroy()` leaves the set alone -- a kernel that has
57
+ // said it once has said it.
58
+ #shownDeprecations = /* @__PURE__ */ new Set();
31
59
  constructor(registry) {
32
- this.logger = new ConsoleLogger("Kernel");
33
- this.#entities = /* @__PURE__ */ new Map();
34
- this.#rootEntities = /* @__PURE__ */ new Set();
35
- this.#allEntities = [];
36
- this.#allEntitiesReversed = [];
37
- this.#allEntitiesNeedUpdate = true;
38
- this.#rootContexts = /* @__PURE__ */ new Map();
39
60
  eventize(this);
40
61
  this.registry = Registry.get(registry);
41
62
  }
42
- #entities;
43
- #rootEntities;
44
- #allEntities;
45
- #allEntitiesReversed;
46
- #allEntitiesNeedUpdate;
47
- #rootContexts;
48
63
  getEntity(uuid) {
49
64
  return this.#requireEntry(uuid).entity;
50
65
  }
66
+ /**
67
+ * The entity behind a uuid, or `undefined` when the kernel does not hold one.
68
+ *
69
+ * The counterpart to `getEntity()`, which throws. Which of the two a caller wants follows from what an
70
+ * absent entity means to it: a change that describes the entity tree -- a new parent, a new order, new
71
+ * properties -- names an entity the view believes to be there, and a uuid the kernel does not know is a
72
+ * disagreement the caller has to hear about. An event is the other case: it carries no structure, and the
73
+ * entity it was meant for may have been torn down between the two sides.
74
+ */
75
+ findEntity(uuid) {
76
+ return this.#entities.get(uuid)?.entity;
77
+ }
51
78
  /**
52
79
  * The entry for an entity that is expected to exist.
53
80
  *
54
81
  * The uuid is the caller's responsibility: `getEntity()` hands the throw on to its own
55
- * callers, and `updateShadowObjects()` is reached only for an entity the caller has just
82
+ * callers, and `#updateShadowObjects()` is reached only for an entity the caller has just
56
83
  * confirmed. Failing here names the uuid instead of failing later on an undefined field.
57
84
  */
58
85
  #requireEntry(uuid) {
@@ -65,14 +92,46 @@ class Kernel {
65
92
  hasEntity(uuid) {
66
93
  return this.#entities.has(uuid);
67
94
  }
95
+ /** The token of the entity behind `uuid`, or `undefined` when the kernel does not hold one. */
96
+ tokenOf(uuid) {
97
+ return this.#entities.get(uuid)?.token;
98
+ }
99
+ /**
100
+ * How many entities the kernel is holding, one count per store, read without waiting on a garbage
101
+ * collector: a test can destroy a kernel and see that it let go of every one of them, rather than
102
+ * inferring it from what a collector happened to do. The two traversal counts are the cached
103
+ * breadth-first order and its reverse.
104
+ *
105
+ * @internal
106
+ */
107
+ get debugEntityCounts() {
108
+ return {
109
+ entities: this.#entities.size,
110
+ rootEntities: this.#rootEntities.size,
111
+ traversal: this.#allEntities.length,
112
+ traversalReversed: this.#allEntitiesReversed.length
113
+ };
114
+ }
68
115
  /**
69
116
  * @returns all entities in breadth-first order
117
+ *
118
+ * The list belongs to the caller: sorting or reversing it changes nothing about what the next call
119
+ * hands out. The walk behind it visits every entity once, which also carries it over a children list
120
+ * that points back at an ancestor -- `Entity.addChild()` writes such a list without touching the parent
121
+ * link, so no check along the parent chain can cover it.
122
+ *
123
+ * A uuid the kernel no longer holds drops out, and what hangs below it with it -- `getEntityGraph()`
124
+ * walks the same tree by the same rule.
70
125
  */
71
126
  traverseLevelOrderBFS(reverse = false) {
72
127
  if (this.#allEntitiesNeedUpdate) {
73
128
  const lvl = /* @__PURE__ */ new Map();
129
+ const visited = /* @__PURE__ */ new Set();
74
130
  const traverse = (uuid, depth) => {
75
- const e = this.getEntity(uuid);
131
+ if (visited.has(uuid)) return;
132
+ visited.add(uuid);
133
+ const e = this.findEntity(uuid);
134
+ if (e === void 0) return;
76
135
  const entities = lvl.get(depth);
77
136
  if (entities) {
78
137
  entities.push(e);
@@ -90,46 +149,84 @@ class Kernel {
90
149
  this.#allEntitiesReversed = this.#allEntities.slice().reverse();
91
150
  this.#allEntitiesNeedUpdate = false;
92
151
  }
93
- return reverse ? this.#allEntitiesReversed : this.#allEntities;
152
+ return reverse ? this.#allEntitiesReversed.slice() : this.#allEntities.slice();
94
153
  }
154
+ /**
155
+ * The entity tree as nested nodes, one node per entity. The visited set is what makes that count
156
+ * hold: a children list that points back at an ancestor would otherwise be walked forever, and this
157
+ * is the one traversal a caller reaches for while debugging a tree that is already broken. A child
158
+ * the walk leaves out is named at its parent under `omittedChildren`, with the reason -- a uuid the
159
+ * kernel does not hold, or one the walk has already placed elsewhere in the graph. A root the walk
160
+ * has already reached through another root's children falls off the top level without a note; it
161
+ * still stands in the graph, just at the other position.
162
+ */
95
163
  getEntityGraph() {
96
- return Array.from(this.#rootEntities).map((uuid) => this.getEntityGraphNode(uuid)).filter((node) => node !== void 0);
164
+ const visited = /* @__PURE__ */ new Set();
165
+ return Array.from(this.#rootEntities).map((uuid) => this.#getEntityGraphNode(uuid, visited)).filter((node) => node !== void 0);
97
166
  }
98
- getEntityGraphNode(uuid) {
167
+ #getEntityGraphNode(uuid, visited, omitted) {
99
168
  const entry = this.#entities.get(uuid);
100
- if (entry === void 0) return void 0;
169
+ if (entry === void 0) {
170
+ omitted?.push({ uuid, reason: "not-in-kernel" });
171
+ return void 0;
172
+ }
173
+ if (visited.has(uuid)) {
174
+ omitted?.push({ uuid, reason: "already-in-graph" });
175
+ return void 0;
176
+ }
177
+ visited.add(uuid);
101
178
  const { token, entity } = entry;
179
+ const omittedChildren = [];
180
+ const children = entity.children.map((child) => this.#getEntityGraphNode(child.uuid, visited, omittedChildren)).filter((node) => node !== void 0);
102
181
  return {
103
182
  token,
104
183
  entity,
105
184
  props: Object.fromEntries(entity.propEntries()),
106
- // A node the kernel no longer holds drops out of the graph.
107
- children: entity.children.map((child) => this.getEntityGraphNode(child.uuid)).filter((node) => node !== void 0)
185
+ children,
186
+ ...omittedChildren.length > 0 ? { omittedChildren } : {}
108
187
  };
109
188
  }
110
189
  upgradeEntities() {
111
190
  const entityConstructors = /* @__PURE__ */ new Map();
112
191
  for (const entity of this.traverseLevelOrderBFS(true)) {
113
192
  if (!this.hasEntity(entity.uuid)) continue;
114
- entityConstructors.set(entity.uuid, this.updateShadowObjects(entity.uuid, 2 /* DestroyOnly */));
193
+ entityConstructors.set(entity.uuid, this.#updateShadowObjects(entity.uuid, 2 /* DestroyOnly */));
115
194
  }
116
195
  for (const entity of this.traverseLevelOrderBFS(false)) {
117
196
  if (!this.hasEntity(entity.uuid)) continue;
118
- this.updateShadowObjects(entity.uuid, 1 /* JustCreate */, entityConstructors.get(entity.uuid));
197
+ this.#updateShadowObjects(entity.uuid, 1 /* JustCreate */, entityConstructors.get(entity.uuid));
119
198
  }
120
199
  entityConstructors.clear();
121
200
  }
201
+ /**
202
+ * Apply a change trail, entry by entry, and say how far it got if it cannot be applied in full.
203
+ *
204
+ * The loop ends at the entry that throws, so what the kernel holds afterwards is a prefix of the
205
+ * trail; the {@link ChangeTrailRefusedError} names its length. The counter sits behind
206
+ * `#parse` and therefore counts only entries that returned normally.
207
+ *
208
+ * An effect the batch defers belongs to no single entry: the batch releases it once the loop is
209
+ * through, and a throw from there arrives with every entry already counted. `appliedCount` then
210
+ * equals `entryCount` — everything was applied, something failed nevertheless, and both
211
+ * statements are true at once.
212
+ *
213
+ * @throws {ChangeTrailRefusedError} if any entry, or any effect the batch deferred, threw
214
+ */
122
215
  run(event) {
123
- if (this.logger.isDebug) {
124
- this.logger.debug("sync", event);
216
+ this.logger.debug("sync", event);
217
+ let appliedCount = 0;
218
+ try {
219
+ batch(() => {
220
+ for (const entry of event.changeTrail) {
221
+ this.#parse(entry);
222
+ appliedCount++;
223
+ }
224
+ });
225
+ } catch (error) {
226
+ throw new ChangeTrailRefusedError(appliedCount, event.changeTrail.length, { cause: error });
125
227
  }
126
- batch(() => {
127
- for (const entry of event.changeTrail) {
128
- this.parse(entry);
129
- }
130
- });
131
228
  }
132
- parse(entry) {
229
+ #parse(entry) {
133
230
  switch (entry.type) {
134
231
  case ComponentChangeType.CreateEntities:
135
232
  this.createEntity(
@@ -140,19 +237,15 @@ class Kernel {
140
237
  entry.properties,
141
238
  entry.autoDestructionOnParentRemoval
142
239
  );
143
- this.#allEntitiesNeedUpdate = true;
144
240
  break;
145
241
  case ComponentChangeType.DestroyEntities:
146
242
  this.destroyEntity(entry.uuid);
147
- this.#allEntitiesNeedUpdate = true;
148
243
  break;
149
244
  case ComponentChangeType.SetParent:
150
245
  this.setParent(entry.uuid, entry.parentUuid, entry.order);
151
- this.#allEntitiesNeedUpdate = true;
152
246
  break;
153
247
  case ComponentChangeType.UpdateOrder:
154
248
  this.updateOrder(entry.uuid, entry.order);
155
- this.#allEntitiesNeedUpdate = true;
156
249
  break;
157
250
  case ComponentChangeType.ChangeProperties:
158
251
  this.changeProperties(entry.uuid, entry.properties);
@@ -166,41 +259,94 @@ class Kernel {
166
259
  }
167
260
  }
168
261
  createEntity(uuid, token, parentUuid, order = 0, properties, autoDestructionOnParentRemoval = false) {
262
+ if (this.#entities.has(uuid)) {
263
+ throw new EntityUuidInUseError(uuid);
264
+ }
169
265
  const e = new Entity(this, uuid);
170
266
  e.order = order;
171
267
  const entry = { token, entity: e, usedConstructors: /* @__PURE__ */ new Map() };
172
268
  this.#entities.set(uuid, entry);
173
- if (parentUuid) {
174
- e.parentUuid = parentUuid;
175
- }
176
- e.autoDestructionOnParentRemoval = autoDestructionOnParentRemoval;
177
- if (!e.hasParent) {
178
- this.#rootEntities.add(uuid);
179
- }
180
- if (properties) {
181
- e.setProperties(properties);
269
+ this.noteEntityTreeChange(uuid);
270
+ try {
271
+ if (parentUuid) {
272
+ e.parentUuid = parentUuid;
273
+ }
274
+ e.autoDestructionOnParentRemoval = autoDestructionOnParentRemoval;
275
+ if (properties) {
276
+ e.setProperties(properties);
277
+ }
278
+ this.#createShadowObjects(entry);
279
+ } catch (error) {
280
+ this.#rollbackFailedCreation(uuid);
281
+ throw error;
182
282
  }
183
- this.createShadowObjects(entry);
283
+ }
284
+ /**
285
+ * Takes back what a creation managed before it failed. The teardown is the regular one, so a
286
+ * shadow-object that already stands hears its `onDestroy` and its creation scope ends the way
287
+ * it would on any other destruction; the entry, the root registration and the link to the
288
+ * parent go with it. `destroyEntity()` clears the bookkeeping of the entity whatever a callback
289
+ * along the way does, so there is nothing left here to take back by hand; only a failure of the
290
+ * teardown itself is reported, because the creation error is the one the caller is waiting for.
291
+ *
292
+ * The rollback reaches this one entity, not the kernel around it. What a constructor did to other
293
+ * entities before it threw stands, and the teardown even adds to it, because `destroyEntity()` walks
294
+ * the children list: an entity the constructor hung under this one survives as a root, and one it
295
+ * moved under this one is taken off the parent it came from -- or destroyed outright, where it
296
+ * carries `autoDestructionOnParentRemoval`. Both leave an entity in a state that is neither the one
297
+ * before the call nor the one the constructor built. A rollback that covers those cases needs a
298
+ * snapshot of the kernel, which this path does not take.
299
+ */
300
+ #rollbackFailedCreation(uuid) {
301
+ runGuarded(
302
+ this.logger,
303
+ () => this.destroyEntity(uuid),
304
+ "rollback of a failed entity creation could not destroy the entity:",
305
+ uuid
306
+ );
184
307
  }
185
308
  destroyEntity(uuid) {
186
309
  const entry = this.#entities.get(uuid);
187
310
  if (entry === void 0) return;
188
311
  const { entity, usedConstructors } = entry;
189
- const childrenSnapshot = [...entity.children];
190
- for (const child of childrenSnapshot) {
191
- if (child.autoDestructionOnParentRemoval) {
192
- this.destroyEntity(child.uuid);
193
- } else {
194
- child.removeFromParent();
195
- this.#rootEntities.add(child.uuid);
312
+ try {
313
+ const childrenSnapshot = [...entity.children];
314
+ for (const child of childrenSnapshot) {
315
+ runGuarded(
316
+ this.logger,
317
+ () => {
318
+ if (child.autoDestructionOnParentRemoval) {
319
+ this.destroyEntity(child.uuid);
320
+ } else {
321
+ child.removeFromParent();
322
+ }
323
+ },
324
+ "child of a destroyed entity could not be handed on:",
325
+ child.uuid
326
+ );
327
+ }
328
+ runGuarded(this.logger, () => entity.removeFromParent(), "entity could not be detached from its parent:", entity.uuid);
329
+ const shadowObjects = this.findShadowObjects(entity.uuid);
330
+ for (const shadowObject of shadowObjects) {
331
+ off(entity, shadowObject);
332
+ this.#notifyShadowObjectDestroy(shadowObject, entity);
196
333
  }
334
+ runGuarded(this.logger, () => emitStrict(entity, onDestroy, entity), "entity onDestroy notification failed:", entity.uuid);
335
+ for (const shadowObject of shadowObjects) {
336
+ runGuarded(
337
+ this.logger,
338
+ () => this.#shadowObjectScopes.get(shadowObject)?.tearDown(),
339
+ "creation scope teardown of a destroyed entity failed:",
340
+ entity.uuid
341
+ );
342
+ }
343
+ runGuarded(this.logger, () => entity[onDestroy](), "entity release failed:", entity.uuid);
344
+ } finally {
345
+ usedConstructors.clear();
346
+ this.#entities.delete(entity.uuid);
347
+ this.#rootEntities.delete(entity.uuid);
348
+ this.#allEntitiesNeedUpdate = true;
197
349
  }
198
- entity.removeFromParent();
199
- emit(entity, onDestroy, entity);
200
- usedConstructors.clear();
201
- this.#entities.delete(entity.uuid);
202
- this.#rootEntities.delete(entity.uuid);
203
- this.#allEntitiesNeedUpdate = true;
204
350
  }
205
351
  /**
206
352
  * @param order the new order, or `undefined` to keep the current one. A set-parent change
@@ -214,59 +360,93 @@ class Kernel {
214
360
  if (parentUuid && !this.#entities.has(parentUuid)) {
215
361
  throw new Error(`entity with uuid "${parentUuid}" not found!`);
216
362
  }
217
- e.removeFromParent();
363
+ if (parentUuid) {
364
+ e.assertAttachableTo(this.getEntity(parentUuid));
365
+ }
366
+ e.removeFromParent(parentUuid == null);
218
367
  e.order = nextOrder;
219
368
  e.parentUuid = parentUuid;
220
- if (e.hasParent) {
369
+ this.logger.debug("entity.onParentChanged", { uuid, parentUuid, order: nextOrder, entity: e });
370
+ runGuarded(this.logger, () => emitStrict(e, onParentChanged, e), "entity onParentChanged notification failed:", uuid);
371
+ }
372
+ updateOrder(uuid, order) {
373
+ this.getEntity(uuid).order = order;
374
+ }
375
+ /**
376
+ * Records that an entity has changed its place in the entity tree: it took a parent, lost one,
377
+ * or moved among its siblings. Two pieces of bookkeeping follow from that -- the cached
378
+ * traversal is dropped, and an entity without a parent joins the set of root entities while one
379
+ * with a parent leaves it.
380
+ *
381
+ * `Entity` reports here from every write that moves it, because those writes are reachable from
382
+ * a shadow-object with no kernel call in between: `entity.parent`, `entity.parentUuid`,
383
+ * `entity.order` and `entity.removeFromParent()`. The state is read off the entity rather than
384
+ * taken as an argument, so a caller cannot describe a move the entity did not make.
385
+ *
386
+ * A uuid the kernel does not hold reaches the cache and stops there: an entity joins the root
387
+ * set when the kernel takes it in and leaves it when the kernel lets go.
388
+ *
389
+ * That such a uuid passes in silence, where `setParent()` and `updateOrder()` throw at one, is the
390
+ * difference between a request and a report. Those two are asked to move an entity and cannot do it
391
+ * without one, so an unknown uuid is a disagreement the caller has to hear about. This is an entity
392
+ * saying where it stands, and it says so while the kernel is still taking it in: `createEntity()`
393
+ * writes the order on the entity before the entry is registered, so an entity created with an order
394
+ * of its own reports once before the kernel holds it. A throw there would cost the creation its own
395
+ * bookkeeping, and there is nothing to disagree about -- dropping the cache is right either way.
396
+ */
397
+ noteEntityTreeChange(uuid) {
398
+ this.#allEntitiesNeedUpdate = true;
399
+ const entry = this.#entities.get(uuid);
400
+ if (entry === void 0) return;
401
+ if (entry.entity.hasParent) {
221
402
  this.#rootEntities.delete(uuid);
222
403
  } else {
223
404
  this.#rootEntities.add(uuid);
224
405
  }
225
- e.reSubscribeToParentContexts();
226
- queueMicrotask(() => {
227
- if (this.logger.isDebug) {
228
- this.logger.debug("entity.onParentChanged", { uuid, parentUuid, order: nextOrder, entity: e });
229
- }
230
- emit(e, onParentChanged, e);
231
- });
232
- }
233
- updateOrder(uuid, order) {
234
- this.getEntity(uuid).order = order;
235
406
  }
236
407
  dispatchEventsToEntity(uuid, events) {
237
- this.getEntity(uuid)?.dispatchViewEvents(events);
408
+ this.findEntity(uuid)?.dispatchViewEvents(events);
238
409
  }
239
410
  changeProperties(uuid, properties) {
240
411
  this.getEntity(uuid).setProperties(properties);
241
- this.updateShadowObjects(uuid);
412
+ this.#updateShadowObjects(uuid);
242
413
  }
243
414
  changeToken(uuid, token) {
244
415
  const entry = this.#entities.get(uuid);
245
416
  if (entry === void 0) return;
246
417
  if (entry.token === token) return;
418
+ const previousToken = entry.token;
247
419
  entry.token = token;
248
- this.updateShadowObjects(uuid);
420
+ this.#updateShadowObjects(uuid, 0 /* CreateAndDestroy */, void 0, previousToken);
249
421
  }
250
422
  dispatchMessageToView(message) {
251
423
  queueMicrotask(() => {
252
- emit(this, MessageToView, message);
424
+ emitSafe(this, MessageToView, message);
253
425
  });
254
426
  }
255
427
  /**
256
428
  * Create or destroy the shadow-objects of an entity using the registered constructors.
257
429
  * After a token change or registry changes, an entity may be given different shadow-objects.
430
+ *
431
+ * @param previousToken the token the entry carried before the caller wrote the new one, or
432
+ * `undefined` where the caller left the token alone. A rebuild that does not get through puts it
433
+ * back before it restores the shadow-objects, so an entity is never built against a token it does
434
+ * not carry.
258
435
  */
259
- updateShadowObjects(uuid, action = 0 /* CreateAndDestroy */, nextConstructors) {
436
+ #updateShadowObjects(uuid, action = 0 /* CreateAndDestroy */, nextConstructors, previousToken) {
260
437
  const entry = this.#requireEntry(uuid);
261
438
  nextConstructors ??= new Set(this.registry.findConstructors(entry.token, entry.entity.truthyProps()));
262
439
  const shouldDestroy = action === 0 /* CreateAndDestroy */ || action === 2 /* DestroyOnly */;
263
440
  const shouldCreate = action === 0 /* CreateAndDestroy */ || action === 1 /* JustCreate */;
441
+ const removed = [];
442
+ const created = [];
264
443
  if (shouldDestroy) {
265
444
  for (const [construct, shadowObjects] of entry.usedConstructors) {
266
445
  if (!nextConstructors.has(construct)) {
267
446
  entry.usedConstructors.delete(construct);
447
+ removed.push({ construct, count: shadowObjects.size });
268
448
  for (const obj of shadowObjects) {
269
- this.destroyShadowObject(obj, entry.entity);
449
+ this.#destroyShadowObject(obj, entry.entity);
270
450
  }
271
451
  }
272
452
  }
@@ -274,304 +454,110 @@ class Kernel {
274
454
  if (shouldCreate) {
275
455
  for (const construct of nextConstructors) {
276
456
  if (!entry.usedConstructors.has(construct)) {
277
- this.constructShadowObject(construct, entry);
457
+ try {
458
+ created.push(this.#constructShadowObject(construct, entry));
459
+ } catch (error) {
460
+ this.#rollbackFailedShadowObjectUpdate(entry, created, removed, previousToken);
461
+ throw error;
462
+ }
278
463
  }
279
464
  }
280
465
  }
281
466
  return nextConstructors;
282
467
  }
283
- constructShadowObject(construct, entry) {
284
- const unsubscribePrimary = /* @__PURE__ */ new Set();
285
- const unsubscribeSecondary = /* @__PURE__ */ new Set();
286
- const contextReaders = /* @__PURE__ */ new Map();
287
- const contextReaderCompares = /* @__PURE__ */ new Map();
288
- const contextParentReaders = /* @__PURE__ */ new Map();
289
- const contextParentReaderCompares = /* @__PURE__ */ new Map();
290
- const contextProviders = /* @__PURE__ */ new Map();
291
- const contextRootProviders = /* @__PURE__ */ new Map();
292
- const propertyReaders = /* @__PURE__ */ new Map();
293
- const propertyCompares = /* @__PURE__ */ new Map();
294
- const getUseProperty = (name, options) => {
295
- if (!usePropertyOptionsDeprecatedShown && options != null && typeof options === "function") {
296
- console.warn(
297
- '[shadow-objects] Deprecation Warning: The "isEqual" option of "useProperty()" is now passed as {compare} argument. Please update your code accordingly.'
298
- );
299
- usePropertyOptionsDeprecatedShown = true;
300
- }
301
- const opts = typeof options === "function" ? { compare: options } : options;
302
- let propReader = propertyReaders.get(name);
303
- if (propReader === void 0) {
304
- propReader = createSignal(void 0, opts).get;
305
- propertyReaders.set(name, propReader);
306
- propertyCompares.set(name, opts?.compare);
307
- const con = link(entry.entity.getPropertyReader(name), propReader);
308
- unsubscribeSecondary.add(con.destroy.bind(con));
309
- } else if (opts?.compare != null && propertyCompares.get(name) !== opts.compare) {
310
- console.warn(
311
- `[shadow-objects] useProperty("${name}"): the cached signal already exists with a different (or no) {compare} function \u2014 the new options are ignored. Pass options only on the first call per property.`
468
+ /**
469
+ * Takes a rebuild of the shadow-objects of one entity back to where the call found it.
470
+ *
471
+ * The identity check comes first, because a constructor may destroy the entity it is coming to life
472
+ * on. Where the kernel no longer holds this entry -- including where another entry stands under the
473
+ * same uuid by now -- `destroyEntity()` has already notified every shadow-object of the entry and
474
+ * cleared `usedConstructors`; both halves of the rollback would find nothing to work with.
475
+ *
476
+ * The token before the shadow-objects: what is rebuilt is built against the token the entity carries
477
+ * afterwards.
478
+ *
479
+ * Backwards down, forwards up: the new shadow-objects leave before the previous ones come back, so
480
+ * the two sets never stand on the entity at the same time.
481
+ *
482
+ * Every step behind a guard of its own and nothing re-thrown -- the caller is waiting for the error
483
+ * of the construction, not for one from the way back.
484
+ *
485
+ * What the rollback does not reach is named rather than passed over. `changeProperties()` has written its
486
+ * properties before it gets here, and they stay written -- a constructor restored below can therefore
487
+ * stand on an entity whose properties no longer route to it, until the next re-resolution of the set
488
+ * leaves it out and takes it down. And `upgradeEntities()` takes its shadow-objects down in one pass over
489
+ * the entity tree and builds in the next, so a rollback in the second pass knows nothing of what the first
490
+ * one took -- at this entity as much as at any other; the entity keeps the token it came in with, an
491
+ * upgrade having written none, and is left with whatever the first pass spared. Both would need a snapshot
492
+ * of the kernel, which this path does not take.
493
+ */
494
+ #rollbackFailedShadowObjectUpdate(entry, created, removed, previousToken) {
495
+ if (this.#entities.get(entry.entity.uuid) !== entry) return;
496
+ if (previousToken !== void 0) {
497
+ entry.token = previousToken;
498
+ }
499
+ for (let i = created.length - 1; i >= 0; i--) {
500
+ runGuarded(
501
+ this.logger,
502
+ () => this.#destroyShadowObject(created[i], entry.entity),
503
+ "rollback of a failed shadow-object update could not remove a new shadow-object:",
504
+ entry.entity.uuid
505
+ );
506
+ }
507
+ for (const { construct, count } of removed) {
508
+ for (let i = 0; i < count; i++) {
509
+ runGuarded(
510
+ this.logger,
511
+ () => this.#constructShadowObject(construct, entry),
512
+ "rollback of a failed shadow-object update could not restore a shadow-object:",
513
+ getDisplayName(construct),
514
+ entry.entity.uuid
312
515
  );
313
516
  }
314
- return propReader;
315
- };
316
- const shadowObject = eventize(
317
- new construct({
318
- entity: entry.entity,
319
- provideContext(name, sourceOrInitialValue, options) {
320
- if (!provideContextOptionsDeprecatedShown && options != null && typeof options === "function") {
321
- console.warn(
322
- '[shadow-objects] Deprecation Warning: The "isEqual" option of "provideContext()" is now passed as {compare} argument. Please update your code accordingly.'
323
- );
324
- provideContextOptionsDeprecatedShown = true;
325
- }
326
- const opts = typeof options === "function" ? { compare: options } : options;
327
- let ctxProvider = contextProviders.get(name);
328
- if (ctxProvider == null) {
329
- const isSig = isSignal(sourceOrInitialValue);
330
- const initialValue = isSig ? void 0 : toMaybe(sourceOrInitialValue);
331
- ctxProvider = createSignal(initialValue, opts?.compare ? { compare: opts.compare } : void 0);
332
- if (isSig) {
333
- const ln2 = link(sourceOrInitialValue, ctxProvider);
334
- unsubscribeSecondary.add(ln2.destroy.bind(ln2));
335
- }
336
- const ln = link(ctxProvider, entry.entity.provideContext(name));
337
- unsubscribeSecondary.add(ln.destroy.bind(ln));
338
- contextProviders.set(name, ctxProvider);
339
- }
340
- if (ctxProvider != null && (opts?.clearOnDestroy ?? true)) {
341
- unsubscribeSecondary.add(() => {
342
- ctxProvider.set(void 0);
343
- });
344
- }
345
- return ctxProvider;
346
- },
347
- provideGlobalContext(name, sourceOrInitialValue, options) {
348
- if (!provideGlobalContextOptionsDeprecatedShown && options != null && typeof options === "function") {
349
- console.warn(
350
- '[shadow-objects] Deprecation Warning: The "isEqual" option of "provideGlobalContext()" is now passed as {compare} argument. Please update your code accordingly.'
351
- );
352
- provideGlobalContextOptionsDeprecatedShown = true;
353
- }
354
- const opts = typeof options === "function" ? { compare: options } : options;
355
- let ctxProvider = contextRootProviders.get(name);
356
- if (ctxProvider == null) {
357
- const isSig = isSignal(sourceOrInitialValue);
358
- const initialValue = isSig ? void 0 : toMaybe(sourceOrInitialValue);
359
- ctxProvider = createSignal(initialValue, opts?.compare ? { compare: opts.compare } : void 0);
360
- if (isSig) {
361
- const ln2 = link(sourceOrInitialValue, ctxProvider);
362
- unsubscribeSecondary.add(ln2.destroy.bind(ln2));
363
- }
364
- const ln = link(ctxProvider, entry.entity.provideGlobalContext(name));
365
- unsubscribeSecondary.add(ln.destroy.bind(ln));
366
- contextRootProviders.set(name, ctxProvider);
367
- }
368
- if (ctxProvider != null && (opts?.clearOnDestroy ?? true)) {
369
- unsubscribeSecondary.add(() => {
370
- ctxProvider.set(void 0);
371
- });
372
- }
373
- return ctxProvider;
374
- },
375
- useContext(name, options) {
376
- if (!useContextOptionsDeprecatedShown && options != null && typeof options === "function") {
377
- console.warn(
378
- '[shadow-objects] Deprecation Warning: The "isEqual" option of "useContext()" is now passed as {compare} argument. Please update your code accordingly.'
379
- );
380
- useContextOptionsDeprecatedShown = true;
381
- }
382
- const opts = typeof options === "function" ? { compare: options } : options;
383
- let ctxReader = contextReaders.get(name);
384
- if (ctxReader === void 0) {
385
- ctxReader = createSignal(void 0, opts).get;
386
- contextReaders.set(name, ctxReader);
387
- contextReaderCompares.set(name, opts?.compare);
388
- const ln = link(entry.entity.useContext(name), ctxReader);
389
- unsubscribeSecondary.add(ln.destroy.bind(ln));
390
- } else if (opts?.compare != null && contextReaderCompares.get(name) !== opts.compare) {
391
- console.warn(
392
- `[shadow-objects] useContext("${String(name)}"): the cached signal already exists with a different (or no) {compare} function \u2014 the new options are ignored. Pass options only on the first call per context.`
393
- );
394
- }
395
- return ctxReader;
396
- },
397
- useParentContext(name, options) {
398
- if (!useParentContextOptionsDeprecatedShown && options != null && typeof options === "function") {
399
- console.warn(
400
- '[shadow-objects] Deprecation Warning: The "isEqual" option of "useParentContext()" is now passed as {compare} argument. Please update your code accordingly.'
401
- );
402
- useParentContextOptionsDeprecatedShown = true;
403
- }
404
- const opts = typeof options === "function" ? { compare: options } : options;
405
- let ctxReader = contextParentReaders.get(name);
406
- if (ctxReader === void 0) {
407
- ctxReader = createSignal(void 0, opts).get;
408
- contextParentReaders.set(name, ctxReader);
409
- contextParentReaderCompares.set(name, opts?.compare);
410
- const ln = link(entry.entity.useParentContext(name), ctxReader);
411
- unsubscribeSecondary.add(ln.destroy.bind(ln));
412
- } else if (opts?.compare != null && contextParentReaderCompares.get(name) !== opts.compare) {
413
- console.warn(
414
- `[shadow-objects] useParentContext("${String(name)}"): the cached signal already exists with a different (or no) {compare} function \u2014 the new options are ignored. Pass options only on the first call per parent context.`
415
- );
416
- }
417
- return ctxReader;
418
- },
419
- dispatchMessageToView(type, data, transferables, traverseChildren = false) {
420
- entry.entity.dispatchMessageToView(type, data, transferables, traverseChildren);
421
- },
422
- useProperty: getUseProperty,
423
- useProperties(props) {
424
- const result = {};
425
- for (const key in props) {
426
- if (Object.hasOwn(props, key)) {
427
- result[key] = getUseProperty(props[key]);
428
- }
429
- }
430
- return result;
431
- },
432
- createResource(factory, cleanup) {
433
- const resourceSignal = createSignal();
434
- const effect = createEffect(() => {
435
- const resource = toMaybe(factory());
436
- resourceSignal.set(resource);
437
- if (resource !== void 0 && cleanup) {
438
- return () => {
439
- cleanup(resource);
440
- resourceSignal.set(void 0);
441
- };
442
- }
443
- return () => {
444
- resourceSignal.set(void 0);
445
- };
446
- });
447
- unsubscribeSecondary.add(() => {
448
- effect.destroy();
449
- resourceSignal.set(void 0);
450
- destroySignal(resourceSignal);
451
- });
452
- return resourceSignal;
453
- },
454
- createEffect(...args) {
455
- const effect = createEffect(...args);
456
- unsubscribeSecondary.add(effect.destroy);
457
- return effect;
458
- },
459
- createSignal(...args) {
460
- const sig = createSignal(...args);
461
- unsubscribeSecondary.add(() => {
462
- destroySignal(sig);
463
- });
464
- return sig;
465
- },
466
- createMemo(...args) {
467
- const sig = createMemo(...args);
468
- unsubscribeSecondary.add(() => {
469
- destroySignal(sig);
470
- });
471
- return sig;
472
- },
473
- on(...args) {
474
- const [firstArg] = args;
475
- if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
476
- const unsub2 = on(entry.entity, ...args);
477
- unsubscribeSecondary.add(unsub2);
478
- return unsub2;
479
- }
480
- const unsub = on(...args);
481
- unsubscribeSecondary.add(unsub);
482
- return Object.assign(() => {
483
- unsubscribeSecondary.delete(unsub);
484
- unsub();
485
- }, unsub);
486
- },
487
- once(...args) {
488
- const [firstArg] = args;
489
- if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
490
- const unsub2 = once(entry.entity, ...args);
491
- unsubscribeSecondary.add(unsub2);
492
- return unsub2;
493
- }
494
- const unsub = once(...args);
495
- unsubscribeSecondary.add(unsub);
496
- return Object.assign(() => {
497
- unsubscribeSecondary.delete(unsub);
498
- unsub();
499
- }, unsub);
500
- },
501
- emit(...args) {
502
- const [firstArg] = args;
503
- if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
504
- emit(entry.entity, ...args);
505
- } else {
506
- emit(...args);
507
- }
508
- },
509
- onViewEvent(callback) {
510
- const unsub = on(entry.entity, onViewEvent, (type, data) => {
511
- callback(type, data);
512
- });
513
- unsubscribeSecondary.add(unsub);
514
- },
515
- onDestroy(callback) {
516
- unsubscribePrimary.add(callback);
517
- }
518
- })
519
- );
520
- if (this.logger.isInfo) {
521
- this.logger.info("create shadow-object", getDisplayName(construct), { shadowObject, entity: entry.entity });
522
517
  }
523
- once(entry.entity, onDestroy, Priority.Low, () => {
524
- if (this.logger.isInfo) {
525
- this.logger.info("destroy shadow-object", getDisplayName(construct), { shadowObject, entity: entry.entity });
526
- }
527
- for (const callback of unsubscribePrimary) {
528
- callback();
529
- }
530
- for (const callback of unsubscribeSecondary) {
531
- callback();
532
- }
533
- for (const sig of contextReaders.values()) {
534
- destroySignal(sig);
535
- }
536
- for (const sig of contextParentReaders.values()) {
537
- destroySignal(sig);
538
- }
539
- for (const sig of propertyReaders.values()) {
540
- destroySignal(sig);
541
- }
542
- for (const sig of contextProviders.values()) {
543
- destroySignal(sig);
544
- }
545
- for (const sig of contextRootProviders.values()) {
546
- destroySignal(sig);
547
- }
548
- unsubscribePrimary.clear();
549
- unsubscribeSecondary.clear();
550
- contextReaders.clear();
551
- contextParentReaders.clear();
552
- propertyReaders.clear();
553
- contextProviders.clear();
554
- contextRootProviders.clear();
555
- const otherShadowObjects = entry.usedConstructors.get(construct);
556
- if (otherShadowObjects) {
557
- otherShadowObjects.delete(shadowObject);
558
- if (otherShadowObjects.size === 0) {
559
- entry.usedConstructors.delete(construct);
518
+ }
519
+ #constructShadowObject(construct, entry) {
520
+ const scope = new ShadowObjectCreationScope(entry.entity, this.logger, getDisplayName(construct), this.#shownDeprecations);
521
+ let shadowObject;
522
+ try {
523
+ shadowObject = eventize(new construct(scope.createAPI()));
524
+ } catch (error) {
525
+ scope.tearDown();
526
+ throw error;
527
+ }
528
+ this.#shadowObjectScopes.set(shadowObject, scope);
529
+ scope.bindTo(
530
+ shadowObject,
531
+ () => {
532
+ this.#shadowObjectScopes.delete(shadowObject);
533
+ },
534
+ () => {
535
+ const otherShadowObjects = entry.usedConstructors.get(construct);
536
+ if (otherShadowObjects) {
537
+ otherShadowObjects.delete(shadowObject);
538
+ if (otherShadowObjects.size === 0) {
539
+ entry.usedConstructors.delete(construct);
540
+ }
560
541
  }
561
542
  }
562
- });
543
+ );
563
544
  const createdBy = entry.usedConstructors.get(construct);
564
545
  if (createdBy) {
565
546
  createdBy.add(shadowObject);
566
547
  } else {
567
548
  entry.usedConstructors.set(construct, /* @__PURE__ */ new Set([shadowObject]));
568
549
  }
569
- this.attachShadowObject(shadowObject, entry.entity);
550
+ try {
551
+ this.#attachShadowObject(shadowObject, entry.entity);
552
+ } catch (error) {
553
+ this.#destroyShadowObject(shadowObject, entry.entity);
554
+ throw error;
555
+ }
570
556
  return shadowObject;
571
557
  }
572
- createShadowObjects(entry) {
558
+ #createShadowObjects(entry) {
573
559
  this.registry.findConstructors(entry.token, entry.entity.truthyProps())?.forEach((construct) => {
574
- this.constructShadowObject(construct, entry);
560
+ this.#constructShadowObject(construct, entry);
575
561
  });
576
562
  }
577
563
  findShadowObjects(uuid) {
@@ -580,17 +566,90 @@ class Kernel {
580
566
  const { usedConstructors } = entry;
581
567
  return Array.from(new Set(Array.from(usedConstructors.values()).flatMap((objs) => Array.from(objs))));
582
568
  }
583
- attachShadowObject(shadowObject, entity) {
569
+ /**
570
+ * One description per Shadow Object of the entity, in the order `findShadowObjects()` lists them:
571
+ * what the creation scope knows (display name, the five name lists), the tokens the constructor is
572
+ * defined under in this kernel's registry, and which of the four lifecycle hooks the instance
573
+ * implements. `[]` for a uuid the kernel does not hold. Reads only; a scope that is already gone
574
+ * contributes its display name and empty lists.
575
+ */
576
+ describeShadowObjects(uuid) {
577
+ const entry = this.#entities.get(uuid);
578
+ if (entry === void 0) return [];
579
+ const seen = /* @__PURE__ */ new Set();
580
+ const descriptions = [];
581
+ for (const [construct, shadowObjects] of entry.usedConstructors) {
582
+ for (const shadowObject of shadowObjects) {
583
+ if (seen.has(shadowObject)) continue;
584
+ seen.add(shadowObject);
585
+ const scope = this.#shadowObjectScopes.get(shadowObject);
586
+ const hooks = LIFECYCLE_HOOKS.filter(([, symbol]) => typeof shadowObject[symbol] === "function").map(
587
+ ([name]) => name
588
+ );
589
+ descriptions.push({
590
+ ...scope?.describe() ?? {
591
+ displayName: getDisplayName(construct),
592
+ usesProperties: [],
593
+ usesContexts: [],
594
+ usesParentContexts: [],
595
+ providesContexts: [],
596
+ providesGlobalContexts: []
597
+ },
598
+ definedUnder: this.registry.tokensOf(construct),
599
+ hooks
600
+ });
601
+ }
602
+ }
603
+ return descriptions;
604
+ }
605
+ #attachShadowObject(shadowObject, entity) {
606
+ for (const [name, symbol] of LIFECYCLE_HOOKS) {
607
+ if (typeof shadowObject[name] === "function" && typeof shadowObject[symbol] !== "function") {
608
+ const displayName = this.#shadowObjectScopes.get(shadowObject)?.displayName;
609
+ this.logger.error(
610
+ `the "${name}" lifecycle hook is a plain method and is never called; use the [${name}] symbol from "@spearwolf/shadow-objects/shadow-objects.js":`,
611
+ displayName
612
+ );
613
+ }
614
+ }
584
615
  on(entity, shadowObject);
585
616
  if (typeof shadowObject[onCreate] === "function") {
586
617
  shadowObject[onCreate](entity);
587
618
  }
588
619
  }
589
- destroyShadowObject(shadowObject, entity) {
620
+ /**
621
+ * Tells one shadow-object that it is about to end, in both halves the notification has: the
622
+ * class-side `[onDestroy]` hook and the event other objects can listen for.
623
+ *
624
+ * Each half stands behind a guard of its own rather than a shared one, because neither is allowed to
625
+ * cost the other: a hook that throws still lets the event go out, and a listener that throws leaves
626
+ * the hook it came after untouched. Within the event half the same rule holds one level down, through
627
+ * `emitStrict()`: a listener that throws costs no other listener its turn. Nothing is re-thrown -- the
628
+ * shadow-objects of one entity reach their end in one sweep, and a failure at one of them may not take
629
+ * the sweep with it. Reports are
630
+ * keyed by the name the scope carries rather than the one the instance would give -- see
631
+ * `ShadowObjectCreationScope.displayName` for where the two part ways.
632
+ */
633
+ #notifyShadowObjectDestroy(shadowObject, entity) {
634
+ const displayName = this.#shadowObjectScopes.get(shadowObject)?.displayName;
590
635
  if (typeof shadowObject[onDestroy] === "function") {
591
- shadowObject[onDestroy](entity);
636
+ runGuarded(
637
+ this.logger,
638
+ () => shadowObject[onDestroy](entity),
639
+ "shadow-object onDestroy hook failed:",
640
+ displayName
641
+ );
592
642
  }
593
- emit(shadowObject, onDestroy, entity);
643
+ runGuarded(
644
+ this.logger,
645
+ () => emitStrict(shadowObject, onDestroy, entity),
646
+ "shadow-object onDestroy notification failed:",
647
+ displayName
648
+ );
649
+ }
650
+ #destroyShadowObject(shadowObject, entity) {
651
+ this.#notifyShadowObjectDestroy(shadowObject, entity);
652
+ this.#shadowObjectScopes.get(shadowObject)?.tearDown();
594
653
  off(entity, shadowObject);
595
654
  }
596
655
  findOrCreateRootContext(name) {
@@ -601,14 +660,42 @@ class Kernel {
601
660
  }
602
661
  return ctx;
603
662
  }
663
+ /** The names of the kernel-wide context chains -- every name a root entity uses or any entity provides globally. */
664
+ rootContextNames() {
665
+ return Array.from(this.#rootContexts.keys());
666
+ }
667
+ /**
668
+ * One kernel-wide context chain: what it resolves to, and its members in chain order. The members
669
+ * are the signals the entities contributed -- `Entity.describeGlobalContext()` hands out the same
670
+ * objects, so a caller can match them by identity. `undefined` for a name the kernel holds no
671
+ * chain for; asking does not create one.
672
+ */
673
+ describeRootContext(name) {
674
+ const path = this.#rootContexts.get(name);
675
+ if (path === void 0) return void 0;
676
+ return { value: path.value, signals: path.signals };
677
+ }
604
678
  destroy() {
679
+ for (const entity of this.traverseLevelOrderBFS(true)) {
680
+ runGuarded(
681
+ this.logger,
682
+ () => this.destroyEntity(entity.uuid),
683
+ "entity teardown during kernel destroy failed:",
684
+ entity.uuid
685
+ );
686
+ }
687
+ this.#entities.clear();
688
+ this.#rootEntities.clear();
689
+ this.#allEntities.length = 0;
690
+ this.#allEntitiesReversed.length = 0;
691
+ this.#allEntitiesNeedUpdate = true;
605
692
  for (const ctx of this.#rootContexts.values()) {
606
693
  ctx.dispose();
607
694
  }
608
695
  this.#rootContexts.clear();
609
- for (const entity of this.traverseLevelOrderBFS().reverse()) {
610
- this.destroyEntity(entity.uuid);
611
- }
696
+ queueMicrotask(() => {
697
+ off(this);
698
+ });
612
699
  }
613
700
  }
614
701
  export {