@spearwolf/shadow-objects 0.33.0 → 0.34.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 (210) hide show
  1. package/CHANGELOG.md +320 -61
  2. package/README.md +57 -13
  3. package/bundle.js +5 -54
  4. package/package.json +7 -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 +25 -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 +9 -2
  22. package/src/constants.d.ts.map +1 -1
  23. package/src/constants.js +1 -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 +52 -2
  44. package/src/elements/ShaePropElement.d.ts.map +1 -1
  45. package/src/elements/ShaePropElement.js +240 -219
  46. package/src/elements/ShaePropElement.js.map +2 -2
  47. package/src/elements/ShaeWorkerElement.d.ts +49 -5
  48. package/src/elements/ShaeWorkerElement.d.ts.map +1 -1
  49. package/src/elements/ShaeWorkerElement.js +204 -73
  50. package/src/elements/ShaeWorkerElement.js.map +2 -2
  51. package/src/elements/constants.d.ts +6 -0
  52. package/src/elements/constants.d.ts.map +1 -1
  53. package/src/elements/constants.js +12 -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 +113 -3
  87. package/src/in-the-dark/Entity.d.ts.map +1 -1
  88. package/src/in-the-dark/Entity.js +292 -63
  89. package/src/in-the-dark/Entity.js.map +3 -3
  90. package/src/in-the-dark/Kernel.d.ts +76 -17
  91. package/src/in-the-dark/Kernel.d.ts.map +1 -1
  92. package/src/in-the-dark/Kernel.js +410 -378
  93. package/src/in-the-dark/Kernel.js.map +3 -3
  94. package/src/in-the-dark/Registry.d.ts.map +1 -1
  95. package/src/in-the-dark/Registry.js +83 -14
  96. package/src/in-the-dark/Registry.js.map +2 -2
  97. package/src/in-the-dark/ShadowObject.d.ts.map +1 -1
  98. package/src/in-the-dark/ShadowObject.js +1 -0
  99. package/src/in-the-dark/ShadowObject.js.map +2 -2
  100. package/src/in-the-dark/ShadowObjectCreationScope.d.ts +126 -0
  101. package/src/in-the-dark/ShadowObjectCreationScope.d.ts.map +1 -0
  102. package/src/in-the-dark/ShadowObjectCreationScope.js +596 -0
  103. package/src/in-the-dark/ShadowObjectCreationScope.js.map +7 -0
  104. package/src/in-the-dark/SignalsPath.d.ts.map +1 -1
  105. package/src/in-the-dark/SignalsPath.js +2 -2
  106. package/src/in-the-dark/SignalsPath.js.map +2 -2
  107. package/src/in-the-dark/events.js.map +1 -1
  108. package/src/in-the-dark/importModule.d.ts +7 -0
  109. package/src/in-the-dark/importModule.d.ts.map +1 -1
  110. package/src/in-the-dark/importModule.js +5 -2
  111. package/src/in-the-dark/importModule.js.map +2 -2
  112. package/src/index.d.ts +7 -1
  113. package/src/index.d.ts.map +1 -1
  114. package/src/index.js +5 -1
  115. package/src/index.js.map +2 -2
  116. package/src/shadow-objects.d.ts +2 -0
  117. package/src/shadow-objects.d.ts.map +1 -1
  118. package/src/shadow-objects.js +2 -0
  119. package/src/shadow-objects.js.map +2 -2
  120. package/src/shadow-objects.worker.js +0 -1
  121. package/src/shadow-objects.worker.js.map +2 -2
  122. package/src/shae-ent.js.map +1 -1
  123. package/src/shae-prop.js +2 -2
  124. package/src/shae-prop.js.map +2 -2
  125. package/src/shae-worker.js.map +1 -1
  126. package/src/types.d.ts +35 -4
  127. package/src/types.d.ts.map +1 -1
  128. package/src/utils/ConsoleLogger.d.ts +45 -10
  129. package/src/utils/ConsoleLogger.d.ts.map +1 -1
  130. package/src/utils/ConsoleLogger.js +51 -26
  131. package/src/utils/ConsoleLogger.js.map +2 -2
  132. package/src/utils/FrameLoop.d.ts +42 -3
  133. package/src/utils/FrameLoop.d.ts.map +1 -1
  134. package/src/utils/FrameLoop.js +71 -10
  135. package/src/utils/FrameLoop.js.map +2 -2
  136. package/src/utils/MicrotaskCollector.d.ts +45 -0
  137. package/src/utils/MicrotaskCollector.d.ts.map +1 -0
  138. package/src/utils/MicrotaskCollector.js +33 -0
  139. package/src/utils/MicrotaskCollector.js.map +7 -0
  140. package/src/utils/MicrotaskGate.d.ts +17 -0
  141. package/src/utils/MicrotaskGate.d.ts.map +1 -0
  142. package/src/utils/MicrotaskGate.js +20 -0
  143. package/src/utils/MicrotaskGate.js.map +7 -0
  144. package/src/utils/array-utils.js.map +1 -1
  145. package/src/utils/attr-utils.d.ts +6 -0
  146. package/src/utils/attr-utils.d.ts.map +1 -1
  147. package/src/utils/attr-utils.js +3 -1
  148. package/src/utils/attr-utils.js.map +2 -2
  149. package/src/utils/constants.js.map +1 -1
  150. package/src/utils/generateUUID.d.ts +11 -0
  151. package/src/utils/generateUUID.d.ts.map +1 -1
  152. package/src/utils/generateUUID.js +31 -266
  153. package/src/utils/generateUUID.js.map +2 -2
  154. package/src/utils/props-utils.d.ts +3 -2
  155. package/src/utils/props-utils.d.ts.map +1 -1
  156. package/src/utils/props-utils.js +13 -24
  157. package/src/utils/props-utils.js.map +2 -2
  158. package/src/utils/runGuarded.d.ts +27 -0
  159. package/src/utils/runGuarded.d.ts.map +1 -0
  160. package/src/utils/runGuarded.js +11 -0
  161. package/src/utils/runGuarded.js.map +7 -0
  162. package/src/utils/toMaybe.d.ts.map +1 -1
  163. package/src/utils/toMaybe.js.map +1 -1
  164. package/src/utils/toNamespace.d.ts.map +1 -1
  165. package/src/utils/toNamespace.js.map +1 -1
  166. package/src/utils/toUrlString.d.ts +12 -0
  167. package/src/utils/toUrlString.d.ts.map +1 -1
  168. package/src/utils/toUrlString.js.map +2 -2
  169. package/src/utils/waitForMessageOfType.d.ts +12 -1
  170. package/src/utils/waitForMessageOfType.d.ts.map +1 -1
  171. package/src/utils/waitForMessageOfType.js +16 -1
  172. package/src/utils/waitForMessageOfType.js.map +2 -2
  173. package/src/view/ComponentChanges.d.ts +88 -1
  174. package/src/view/ComponentChanges.d.ts.map +1 -1
  175. package/src/view/ComponentChanges.js +243 -73
  176. package/src/view/ComponentChanges.js.map +2 -2
  177. package/src/view/ComponentContext.d.ts +206 -16
  178. package/src/view/ComponentContext.d.ts.map +1 -1
  179. package/src/view/ComponentContext.js +548 -91
  180. package/src/view/ComponentContext.js.map +2 -2
  181. package/src/view/ComponentMemory.d.ts +3 -3
  182. package/src/view/ComponentMemory.d.ts.map +1 -1
  183. package/src/view/ComponentMemory.js.map +1 -1
  184. package/src/view/IShadowObjectEnvProxy.d.ts +29 -3
  185. package/src/view/IShadowObjectEnvProxy.d.ts.map +1 -1
  186. package/src/view/LocalShadowObjectEnv.d.ts +1 -1
  187. package/src/view/LocalShadowObjectEnv.d.ts.map +1 -1
  188. package/src/view/LocalShadowObjectEnv.js +8 -6
  189. package/src/view/LocalShadowObjectEnv.js.map +2 -2
  190. package/src/view/RemoteWorkerEnv.d.ts +89 -5
  191. package/src/view/RemoteWorkerEnv.d.ts.map +1 -1
  192. package/src/view/RemoteWorkerEnv.js +195 -77
  193. package/src/view/RemoteWorkerEnv.js.map +2 -2
  194. package/src/view/ShadowEnv.d.ts +49 -2
  195. package/src/view/ShadowEnv.d.ts.map +1 -1
  196. package/src/view/ShadowEnv.js +223 -60
  197. package/src/view/ShadowEnv.js.map +2 -2
  198. package/src/view/ViewComponent.d.ts +78 -14
  199. package/src/view/ViewComponent.d.ts.map +1 -1
  200. package/src/view/ViewComponent.js +90 -14
  201. package/src/view/ViewComponent.js.map +2 -2
  202. package/src/view/cloneChangeTrail.js.map +2 -2
  203. package/src/worker/MessageRouter.d.ts +14 -0
  204. package/src/worker/MessageRouter.d.ts.map +1 -1
  205. package/src/worker/MessageRouter.js +75 -15
  206. package/src/worker/MessageRouter.js.map +2 -2
  207. package/src/worker/WorkerRuntime.d.ts +22 -1
  208. package/src/worker/WorkerRuntime.d.ts.map +1 -1
  209. package/src/worker/WorkerRuntime.js +60 -8
  210. package/src/worker/WorkerRuntime.js.map +2 -2
@@ -1,19 +1,14 @@
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";
14
8
  import { Entity } from "./Entity.js";
15
9
  import { onCreate, onDestroy, onParentChanged, onViewEvent } from "./events.js";
16
10
  import { Registry } from "./Registry.js";
11
+ import { ShadowObjectCreationScope } from "./ShadowObjectCreationScope.js";
17
12
  import { SignalsPath } from "./SignalsPath.js";
18
13
  var ShadowObjectAction = /* @__PURE__ */ ((ShadowObjectAction2) => {
19
14
  ShadowObjectAction2[ShadowObjectAction2["CreateAndDestroy"] = 0] = "CreateAndDestroy";
@@ -22,37 +17,69 @@ var ShadowObjectAction = /* @__PURE__ */ ((ShadowObjectAction2) => {
22
17
  return ShadowObjectAction2;
23
18
  })(ShadowObjectAction || {});
24
19
  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,42 @@ class Kernel {
65
92
  hasEntity(uuid) {
66
93
  return this.#entities.has(uuid);
67
94
  }
95
+ /**
96
+ * How many entities the kernel is holding, one count per store, read without waiting on a garbage
97
+ * collector: a test can destroy a kernel and see that it let go of every one of them, rather than
98
+ * inferring it from what a collector happened to do. The two traversal counts are the cached
99
+ * breadth-first order and its reverse.
100
+ *
101
+ * @internal
102
+ */
103
+ get debugEntityCounts() {
104
+ return {
105
+ entities: this.#entities.size,
106
+ rootEntities: this.#rootEntities.size,
107
+ traversal: this.#allEntities.length,
108
+ traversalReversed: this.#allEntitiesReversed.length
109
+ };
110
+ }
68
111
  /**
69
112
  * @returns all entities in breadth-first order
113
+ *
114
+ * The list belongs to the caller: sorting or reversing it changes nothing about what the next call
115
+ * hands out. The walk behind it visits every entity once, which also carries it over a children list
116
+ * that points back at an ancestor -- `Entity.addChild()` writes such a list without touching the parent
117
+ * link, so no check along the parent chain can cover it.
118
+ *
119
+ * A uuid the kernel no longer holds drops out, and what hangs below it with it -- `getEntityGraph()`
120
+ * walks the same tree by the same rule.
70
121
  */
71
122
  traverseLevelOrderBFS(reverse = false) {
72
123
  if (this.#allEntitiesNeedUpdate) {
73
124
  const lvl = /* @__PURE__ */ new Map();
125
+ const visited = /* @__PURE__ */ new Set();
74
126
  const traverse = (uuid, depth) => {
75
- const e = this.getEntity(uuid);
127
+ if (visited.has(uuid)) return;
128
+ visited.add(uuid);
129
+ const e = this.findEntity(uuid);
130
+ if (e === void 0) return;
76
131
  const entities = lvl.get(depth);
77
132
  if (entities) {
78
133
  entities.push(e);
@@ -90,46 +145,84 @@ class Kernel {
90
145
  this.#allEntitiesReversed = this.#allEntities.slice().reverse();
91
146
  this.#allEntitiesNeedUpdate = false;
92
147
  }
93
- return reverse ? this.#allEntitiesReversed : this.#allEntities;
148
+ return reverse ? this.#allEntitiesReversed.slice() : this.#allEntities.slice();
94
149
  }
150
+ /**
151
+ * The entity tree as nested nodes, one node per entity. The visited set is what makes that count
152
+ * hold: a children list that points back at an ancestor would otherwise be walked forever, and this
153
+ * is the one traversal a caller reaches for while debugging a tree that is already broken. A child
154
+ * the walk leaves out is named at its parent under `omittedChildren`, with the reason -- a uuid the
155
+ * kernel does not hold, or one the walk has already placed elsewhere in the graph. A root the walk
156
+ * has already reached through another root's children falls off the top level without a note; it
157
+ * still stands in the graph, just at the other position.
158
+ */
95
159
  getEntityGraph() {
96
- return Array.from(this.#rootEntities).map((uuid) => this.getEntityGraphNode(uuid)).filter((node) => node !== void 0);
160
+ const visited = /* @__PURE__ */ new Set();
161
+ return Array.from(this.#rootEntities).map((uuid) => this.#getEntityGraphNode(uuid, visited)).filter((node) => node !== void 0);
97
162
  }
98
- getEntityGraphNode(uuid) {
163
+ #getEntityGraphNode(uuid, visited, omitted) {
99
164
  const entry = this.#entities.get(uuid);
100
- if (entry === void 0) return void 0;
165
+ if (entry === void 0) {
166
+ omitted?.push({ uuid, reason: "not-in-kernel" });
167
+ return void 0;
168
+ }
169
+ if (visited.has(uuid)) {
170
+ omitted?.push({ uuid, reason: "already-in-graph" });
171
+ return void 0;
172
+ }
173
+ visited.add(uuid);
101
174
  const { token, entity } = entry;
175
+ const omittedChildren = [];
176
+ const children = entity.children.map((child) => this.#getEntityGraphNode(child.uuid, visited, omittedChildren)).filter((node) => node !== void 0);
102
177
  return {
103
178
  token,
104
179
  entity,
105
180
  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)
181
+ children,
182
+ ...omittedChildren.length > 0 ? { omittedChildren } : {}
108
183
  };
109
184
  }
110
185
  upgradeEntities() {
111
186
  const entityConstructors = /* @__PURE__ */ new Map();
112
187
  for (const entity of this.traverseLevelOrderBFS(true)) {
113
188
  if (!this.hasEntity(entity.uuid)) continue;
114
- entityConstructors.set(entity.uuid, this.updateShadowObjects(entity.uuid, 2 /* DestroyOnly */));
189
+ entityConstructors.set(entity.uuid, this.#updateShadowObjects(entity.uuid, 2 /* DestroyOnly */));
115
190
  }
116
191
  for (const entity of this.traverseLevelOrderBFS(false)) {
117
192
  if (!this.hasEntity(entity.uuid)) continue;
118
- this.updateShadowObjects(entity.uuid, 1 /* JustCreate */, entityConstructors.get(entity.uuid));
193
+ this.#updateShadowObjects(entity.uuid, 1 /* JustCreate */, entityConstructors.get(entity.uuid));
119
194
  }
120
195
  entityConstructors.clear();
121
196
  }
197
+ /**
198
+ * Apply a change trail, entry by entry, and say how far it got if it cannot be applied in full.
199
+ *
200
+ * The loop ends at the entry that throws, so what the kernel holds afterwards is a prefix of the
201
+ * trail; the {@link ChangeTrailRefusedError} names its length. The counter sits behind
202
+ * `#parse` and therefore counts only entries that returned normally.
203
+ *
204
+ * An effect the batch defers belongs to no single entry: the batch releases it once the loop is
205
+ * through, and a throw from there arrives with every entry already counted. `appliedCount` then
206
+ * equals `entryCount` — everything was applied, something failed nevertheless, and both
207
+ * statements are true at once.
208
+ *
209
+ * @throws {ChangeTrailRefusedError} if any entry, or any effect the batch deferred, threw
210
+ */
122
211
  run(event) {
123
- if (this.logger.isDebug) {
124
- this.logger.debug("sync", event);
212
+ this.logger.debug("sync", event);
213
+ let appliedCount = 0;
214
+ try {
215
+ batch(() => {
216
+ for (const entry of event.changeTrail) {
217
+ this.#parse(entry);
218
+ appliedCount++;
219
+ }
220
+ });
221
+ } catch (error) {
222
+ throw new ChangeTrailRefusedError(appliedCount, event.changeTrail.length, { cause: error });
125
223
  }
126
- batch(() => {
127
- for (const entry of event.changeTrail) {
128
- this.parse(entry);
129
- }
130
- });
131
224
  }
132
- parse(entry) {
225
+ #parse(entry) {
133
226
  switch (entry.type) {
134
227
  case ComponentChangeType.CreateEntities:
135
228
  this.createEntity(
@@ -140,19 +233,15 @@ class Kernel {
140
233
  entry.properties,
141
234
  entry.autoDestructionOnParentRemoval
142
235
  );
143
- this.#allEntitiesNeedUpdate = true;
144
236
  break;
145
237
  case ComponentChangeType.DestroyEntities:
146
238
  this.destroyEntity(entry.uuid);
147
- this.#allEntitiesNeedUpdate = true;
148
239
  break;
149
240
  case ComponentChangeType.SetParent:
150
241
  this.setParent(entry.uuid, entry.parentUuid, entry.order);
151
- this.#allEntitiesNeedUpdate = true;
152
242
  break;
153
243
  case ComponentChangeType.UpdateOrder:
154
244
  this.updateOrder(entry.uuid, entry.order);
155
- this.#allEntitiesNeedUpdate = true;
156
245
  break;
157
246
  case ComponentChangeType.ChangeProperties:
158
247
  this.changeProperties(entry.uuid, entry.properties);
@@ -166,41 +255,94 @@ class Kernel {
166
255
  }
167
256
  }
168
257
  createEntity(uuid, token, parentUuid, order = 0, properties, autoDestructionOnParentRemoval = false) {
258
+ if (this.#entities.has(uuid)) {
259
+ throw new EntityUuidInUseError(uuid);
260
+ }
169
261
  const e = new Entity(this, uuid);
170
262
  e.order = order;
171
263
  const entry = { token, entity: e, usedConstructors: /* @__PURE__ */ new Map() };
172
264
  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);
265
+ this.noteEntityTreeChange(uuid);
266
+ try {
267
+ if (parentUuid) {
268
+ e.parentUuid = parentUuid;
269
+ }
270
+ e.autoDestructionOnParentRemoval = autoDestructionOnParentRemoval;
271
+ if (properties) {
272
+ e.setProperties(properties);
273
+ }
274
+ this.#createShadowObjects(entry);
275
+ } catch (error) {
276
+ this.#rollbackFailedCreation(uuid);
277
+ throw error;
182
278
  }
183
- this.createShadowObjects(entry);
279
+ }
280
+ /**
281
+ * Takes back what a creation managed before it failed. The teardown is the regular one, so a
282
+ * shadow-object that already stands hears its `onDestroy` and its creation scope ends the way
283
+ * it would on any other destruction; the entry, the root registration and the link to the
284
+ * parent go with it. `destroyEntity()` clears the bookkeeping of the entity whatever a callback
285
+ * along the way does, so there is nothing left here to take back by hand; only a failure of the
286
+ * teardown itself is reported, because the creation error is the one the caller is waiting for.
287
+ *
288
+ * The rollback reaches this one entity, not the kernel around it. What a constructor did to other
289
+ * entities before it threw stands, and the teardown even adds to it, because `destroyEntity()` walks
290
+ * the children list: an entity the constructor hung under this one survives as a root, and one it
291
+ * moved under this one is taken off the parent it came from -- or destroyed outright, where it
292
+ * carries `autoDestructionOnParentRemoval`. Both leave an entity in a state that is neither the one
293
+ * before the call nor the one the constructor built. A rollback that covers those cases needs a
294
+ * snapshot of the kernel, which this path does not take.
295
+ */
296
+ #rollbackFailedCreation(uuid) {
297
+ runGuarded(
298
+ this.logger,
299
+ () => this.destroyEntity(uuid),
300
+ "rollback of a failed entity creation could not destroy the entity:",
301
+ uuid
302
+ );
184
303
  }
185
304
  destroyEntity(uuid) {
186
305
  const entry = this.#entities.get(uuid);
187
306
  if (entry === void 0) return;
188
307
  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);
308
+ try {
309
+ const childrenSnapshot = [...entity.children];
310
+ for (const child of childrenSnapshot) {
311
+ runGuarded(
312
+ this.logger,
313
+ () => {
314
+ if (child.autoDestructionOnParentRemoval) {
315
+ this.destroyEntity(child.uuid);
316
+ } else {
317
+ child.removeFromParent();
318
+ }
319
+ },
320
+ "child of a destroyed entity could not be handed on:",
321
+ child.uuid
322
+ );
323
+ }
324
+ runGuarded(this.logger, () => entity.removeFromParent(), "entity could not be detached from its parent:", entity.uuid);
325
+ const shadowObjects = this.findShadowObjects(entity.uuid);
326
+ for (const shadowObject of shadowObjects) {
327
+ off(entity, shadowObject);
328
+ this.#notifyShadowObjectDestroy(shadowObject, entity);
196
329
  }
330
+ runGuarded(this.logger, () => emitStrict(entity, onDestroy, entity), "entity onDestroy notification failed:", entity.uuid);
331
+ for (const shadowObject of shadowObjects) {
332
+ runGuarded(
333
+ this.logger,
334
+ () => this.#shadowObjectScopes.get(shadowObject)?.tearDown(),
335
+ "creation scope teardown of a destroyed entity failed:",
336
+ entity.uuid
337
+ );
338
+ }
339
+ runGuarded(this.logger, () => entity[onDestroy](), "entity release failed:", entity.uuid);
340
+ } finally {
341
+ usedConstructors.clear();
342
+ this.#entities.delete(entity.uuid);
343
+ this.#rootEntities.delete(entity.uuid);
344
+ this.#allEntitiesNeedUpdate = true;
197
345
  }
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
346
  }
205
347
  /**
206
348
  * @param order the new order, or `undefined` to keep the current one. A set-parent change
@@ -214,59 +356,93 @@ class Kernel {
214
356
  if (parentUuid && !this.#entities.has(parentUuid)) {
215
357
  throw new Error(`entity with uuid "${parentUuid}" not found!`);
216
358
  }
217
- e.removeFromParent();
359
+ if (parentUuid) {
360
+ e.assertAttachableTo(this.getEntity(parentUuid));
361
+ }
362
+ e.removeFromParent(parentUuid == null);
218
363
  e.order = nextOrder;
219
364
  e.parentUuid = parentUuid;
220
- if (e.hasParent) {
365
+ this.logger.debug("entity.onParentChanged", { uuid, parentUuid, order: nextOrder, entity: e });
366
+ runGuarded(this.logger, () => emitStrict(e, onParentChanged, e), "entity onParentChanged notification failed:", uuid);
367
+ }
368
+ updateOrder(uuid, order) {
369
+ this.getEntity(uuid).order = order;
370
+ }
371
+ /**
372
+ * Records that an entity has changed its place in the entity tree: it took a parent, lost one,
373
+ * or moved among its siblings. Two pieces of bookkeeping follow from that -- the cached
374
+ * traversal is dropped, and an entity without a parent joins the set of root entities while one
375
+ * with a parent leaves it.
376
+ *
377
+ * `Entity` reports here from every write that moves it, because those writes are reachable from
378
+ * a shadow-object with no kernel call in between: `entity.parent`, `entity.parentUuid`,
379
+ * `entity.order` and `entity.removeFromParent()`. The state is read off the entity rather than
380
+ * taken as an argument, so a caller cannot describe a move the entity did not make.
381
+ *
382
+ * A uuid the kernel does not hold reaches the cache and stops there: an entity joins the root
383
+ * set when the kernel takes it in and leaves it when the kernel lets go.
384
+ *
385
+ * That such a uuid passes in silence, where `setParent()` and `updateOrder()` throw at one, is the
386
+ * difference between a request and a report. Those two are asked to move an entity and cannot do it
387
+ * without one, so an unknown uuid is a disagreement the caller has to hear about. This is an entity
388
+ * saying where it stands, and it says so while the kernel is still taking it in: `createEntity()`
389
+ * writes the order on the entity before the entry is registered, so an entity created with an order
390
+ * of its own reports once before the kernel holds it. A throw there would cost the creation its own
391
+ * bookkeeping, and there is nothing to disagree about -- dropping the cache is right either way.
392
+ */
393
+ noteEntityTreeChange(uuid) {
394
+ this.#allEntitiesNeedUpdate = true;
395
+ const entry = this.#entities.get(uuid);
396
+ if (entry === void 0) return;
397
+ if (entry.entity.hasParent) {
221
398
  this.#rootEntities.delete(uuid);
222
399
  } else {
223
400
  this.#rootEntities.add(uuid);
224
401
  }
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
402
  }
236
403
  dispatchEventsToEntity(uuid, events) {
237
- this.getEntity(uuid)?.dispatchViewEvents(events);
404
+ this.findEntity(uuid)?.dispatchViewEvents(events);
238
405
  }
239
406
  changeProperties(uuid, properties) {
240
407
  this.getEntity(uuid).setProperties(properties);
241
- this.updateShadowObjects(uuid);
408
+ this.#updateShadowObjects(uuid);
242
409
  }
243
410
  changeToken(uuid, token) {
244
411
  const entry = this.#entities.get(uuid);
245
412
  if (entry === void 0) return;
246
413
  if (entry.token === token) return;
414
+ const previousToken = entry.token;
247
415
  entry.token = token;
248
- this.updateShadowObjects(uuid);
416
+ this.#updateShadowObjects(uuid, 0 /* CreateAndDestroy */, void 0, previousToken);
249
417
  }
250
418
  dispatchMessageToView(message) {
251
419
  queueMicrotask(() => {
252
- emit(this, MessageToView, message);
420
+ emitSafe(this, MessageToView, message);
253
421
  });
254
422
  }
255
423
  /**
256
424
  * Create or destroy the shadow-objects of an entity using the registered constructors.
257
425
  * After a token change or registry changes, an entity may be given different shadow-objects.
426
+ *
427
+ * @param previousToken the token the entry carried before the caller wrote the new one, or
428
+ * `undefined` where the caller left the token alone. A rebuild that does not get through puts it
429
+ * back before it restores the shadow-objects, so an entity is never built against a token it does
430
+ * not carry.
258
431
  */
259
- updateShadowObjects(uuid, action = 0 /* CreateAndDestroy */, nextConstructors) {
432
+ #updateShadowObjects(uuid, action = 0 /* CreateAndDestroy */, nextConstructors, previousToken) {
260
433
  const entry = this.#requireEntry(uuid);
261
434
  nextConstructors ??= new Set(this.registry.findConstructors(entry.token, entry.entity.truthyProps()));
262
435
  const shouldDestroy = action === 0 /* CreateAndDestroy */ || action === 2 /* DestroyOnly */;
263
436
  const shouldCreate = action === 0 /* CreateAndDestroy */ || action === 1 /* JustCreate */;
437
+ const removed = [];
438
+ const created = [];
264
439
  if (shouldDestroy) {
265
440
  for (const [construct, shadowObjects] of entry.usedConstructors) {
266
441
  if (!nextConstructors.has(construct)) {
267
442
  entry.usedConstructors.delete(construct);
443
+ removed.push({ construct, count: shadowObjects.size });
268
444
  for (const obj of shadowObjects) {
269
- this.destroyShadowObject(obj, entry.entity);
445
+ this.#destroyShadowObject(obj, entry.entity);
270
446
  }
271
447
  }
272
448
  }
@@ -274,304 +450,110 @@ class Kernel {
274
450
  if (shouldCreate) {
275
451
  for (const construct of nextConstructors) {
276
452
  if (!entry.usedConstructors.has(construct)) {
277
- this.constructShadowObject(construct, entry);
453
+ try {
454
+ created.push(this.#constructShadowObject(construct, entry));
455
+ } catch (error) {
456
+ this.#rollbackFailedShadowObjectUpdate(entry, created, removed, previousToken);
457
+ throw error;
458
+ }
278
459
  }
279
460
  }
280
461
  }
281
462
  return nextConstructors;
282
463
  }
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.`
464
+ /**
465
+ * Takes a rebuild of the shadow-objects of one entity back to where the call found it.
466
+ *
467
+ * The identity check comes first, because a constructor may destroy the entity it is coming to life
468
+ * on. Where the kernel no longer holds this entry -- including where another entry stands under the
469
+ * same uuid by now -- `destroyEntity()` has already notified every shadow-object of the entry and
470
+ * cleared `usedConstructors`; both halves of the rollback would find nothing to work with.
471
+ *
472
+ * The token before the shadow-objects: what is rebuilt is built against the token the entity carries
473
+ * afterwards.
474
+ *
475
+ * Backwards down, forwards up: the new shadow-objects leave before the previous ones come back, so
476
+ * the two sets never stand on the entity at the same time.
477
+ *
478
+ * Every step behind a guard of its own and nothing re-thrown -- the caller is waiting for the error
479
+ * of the construction, not for one from the way back.
480
+ *
481
+ * What the rollback does not reach is named rather than passed over. `changeProperties()` has written its
482
+ * properties before it gets here, and they stay written -- a constructor restored below can therefore
483
+ * stand on an entity whose properties no longer route to it, until the next re-resolution of the set
484
+ * leaves it out and takes it down. And `upgradeEntities()` takes its shadow-objects down in one pass over
485
+ * the entity tree and builds in the next, so a rollback in the second pass knows nothing of what the first
486
+ * one took -- at this entity as much as at any other; the entity keeps the token it came in with, an
487
+ * upgrade having written none, and is left with whatever the first pass spared. Both would need a snapshot
488
+ * of the kernel, which this path does not take.
489
+ */
490
+ #rollbackFailedShadowObjectUpdate(entry, created, removed, previousToken) {
491
+ if (this.#entities.get(entry.entity.uuid) !== entry) return;
492
+ if (previousToken !== void 0) {
493
+ entry.token = previousToken;
494
+ }
495
+ for (let i = created.length - 1; i >= 0; i--) {
496
+ runGuarded(
497
+ this.logger,
498
+ () => this.#destroyShadowObject(created[i], entry.entity),
499
+ "rollback of a failed shadow-object update could not remove a new shadow-object:",
500
+ entry.entity.uuid
501
+ );
502
+ }
503
+ for (const { construct, count } of removed) {
504
+ for (let i = 0; i < count; i++) {
505
+ runGuarded(
506
+ this.logger,
507
+ () => this.#constructShadowObject(construct, entry),
508
+ "rollback of a failed shadow-object update could not restore a shadow-object:",
509
+ getDisplayName(construct),
510
+ entry.entity.uuid
312
511
  );
313
512
  }
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
513
  }
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);
514
+ }
515
+ #constructShadowObject(construct, entry) {
516
+ const scope = new ShadowObjectCreationScope(entry.entity, this.logger, getDisplayName(construct), this.#shownDeprecations);
517
+ let shadowObject;
518
+ try {
519
+ shadowObject = eventize(new construct(scope.createAPI()));
520
+ } catch (error) {
521
+ scope.tearDown();
522
+ throw error;
523
+ }
524
+ this.#shadowObjectScopes.set(shadowObject, scope);
525
+ scope.bindTo(
526
+ shadowObject,
527
+ () => {
528
+ this.#shadowObjectScopes.delete(shadowObject);
529
+ },
530
+ () => {
531
+ const otherShadowObjects = entry.usedConstructors.get(construct);
532
+ if (otherShadowObjects) {
533
+ otherShadowObjects.delete(shadowObject);
534
+ if (otherShadowObjects.size === 0) {
535
+ entry.usedConstructors.delete(construct);
536
+ }
560
537
  }
561
538
  }
562
- });
539
+ );
563
540
  const createdBy = entry.usedConstructors.get(construct);
564
541
  if (createdBy) {
565
542
  createdBy.add(shadowObject);
566
543
  } else {
567
544
  entry.usedConstructors.set(construct, /* @__PURE__ */ new Set([shadowObject]));
568
545
  }
569
- this.attachShadowObject(shadowObject, entry.entity);
546
+ try {
547
+ this.#attachShadowObject(shadowObject, entry.entity);
548
+ } catch (error) {
549
+ this.#destroyShadowObject(shadowObject, entry.entity);
550
+ throw error;
551
+ }
570
552
  return shadowObject;
571
553
  }
572
- createShadowObjects(entry) {
554
+ #createShadowObjects(entry) {
573
555
  this.registry.findConstructors(entry.token, entry.entity.truthyProps())?.forEach((construct) => {
574
- this.constructShadowObject(construct, entry);
556
+ this.#constructShadowObject(construct, entry);
575
557
  });
576
558
  }
577
559
  findShadowObjects(uuid) {
@@ -580,17 +562,54 @@ class Kernel {
580
562
  const { usedConstructors } = entry;
581
563
  return Array.from(new Set(Array.from(usedConstructors.values()).flatMap((objs) => Array.from(objs))));
582
564
  }
583
- attachShadowObject(shadowObject, entity) {
565
+ #attachShadowObject(shadowObject, entity) {
566
+ for (const [name, symbol] of LIFECYCLE_HOOKS) {
567
+ if (typeof shadowObject[name] === "function" && typeof shadowObject[symbol] !== "function") {
568
+ const displayName = this.#shadowObjectScopes.get(shadowObject)?.displayName;
569
+ this.logger.error(
570
+ `the "${name}" lifecycle hook is a plain method and is never called; use the [${name}] symbol from "@spearwolf/shadow-objects/shadow-objects.js":`,
571
+ displayName
572
+ );
573
+ }
574
+ }
584
575
  on(entity, shadowObject);
585
576
  if (typeof shadowObject[onCreate] === "function") {
586
577
  shadowObject[onCreate](entity);
587
578
  }
588
579
  }
589
- destroyShadowObject(shadowObject, entity) {
580
+ /**
581
+ * Tells one shadow-object that it is about to end, in both halves the notification has: the
582
+ * class-side `[onDestroy]` hook and the event other objects can listen for.
583
+ *
584
+ * Each half stands behind a guard of its own rather than a shared one, because neither is allowed to
585
+ * cost the other: a hook that throws still lets the event go out, and a listener that throws leaves
586
+ * the hook it came after untouched. Within the event half the same rule holds one level down, through
587
+ * `emitStrict()`: a listener that throws costs no other listener its turn. Nothing is re-thrown -- the
588
+ * shadow-objects of one entity reach their end in one sweep, and a failure at one of them may not take
589
+ * the sweep with it. Reports are
590
+ * keyed by the name the scope carries rather than the one the instance would give -- see
591
+ * `ShadowObjectCreationScope.displayName` for where the two part ways.
592
+ */
593
+ #notifyShadowObjectDestroy(shadowObject, entity) {
594
+ const displayName = this.#shadowObjectScopes.get(shadowObject)?.displayName;
590
595
  if (typeof shadowObject[onDestroy] === "function") {
591
- shadowObject[onDestroy](entity);
596
+ runGuarded(
597
+ this.logger,
598
+ () => shadowObject[onDestroy](entity),
599
+ "shadow-object onDestroy hook failed:",
600
+ displayName
601
+ );
592
602
  }
593
- emit(shadowObject, onDestroy, entity);
603
+ runGuarded(
604
+ this.logger,
605
+ () => emitStrict(shadowObject, onDestroy, entity),
606
+ "shadow-object onDestroy notification failed:",
607
+ displayName
608
+ );
609
+ }
610
+ #destroyShadowObject(shadowObject, entity) {
611
+ this.#notifyShadowObjectDestroy(shadowObject, entity);
612
+ this.#shadowObjectScopes.get(shadowObject)?.tearDown();
594
613
  off(entity, shadowObject);
595
614
  }
596
615
  findOrCreateRootContext(name) {
@@ -602,13 +621,26 @@ class Kernel {
602
621
  return ctx;
603
622
  }
604
623
  destroy() {
624
+ for (const entity of this.traverseLevelOrderBFS(true)) {
625
+ runGuarded(
626
+ this.logger,
627
+ () => this.destroyEntity(entity.uuid),
628
+ "entity teardown during kernel destroy failed:",
629
+ entity.uuid
630
+ );
631
+ }
632
+ this.#entities.clear();
633
+ this.#rootEntities.clear();
634
+ this.#allEntities.length = 0;
635
+ this.#allEntitiesReversed.length = 0;
636
+ this.#allEntitiesNeedUpdate = true;
605
637
  for (const ctx of this.#rootContexts.values()) {
606
638
  ctx.dispose();
607
639
  }
608
640
  this.#rootContexts.clear();
609
- for (const entity of this.traverseLevelOrderBFS().reverse()) {
610
- this.destroyEntity(entity.uuid);
611
- }
641
+ queueMicrotask(() => {
642
+ off(this);
643
+ });
612
644
  }
613
645
  }
614
646
  export {