@spearwolf/shadow-objects 0.32.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 (213) hide show
  1. package/CHANGELOG.md +333 -10
  2. package/README.md +100 -20
  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 +43 -5
  36. package/src/elements/ShaeEntElement.d.ts.map +1 -1
  37. package/src/elements/ShaeEntElement.js +399 -170
  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 +57 -7
  44. package/src/elements/ShaePropElement.d.ts.map +1 -1
  45. package/src/elements/ShaePropElement.js +241 -220
  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 +230 -74
  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 +81 -17
  91. package/src/in-the-dark/Kernel.d.ts.map +1 -1
  92. package/src/in-the-dark/Kernel.js +449 -392
  93. package/src/in-the-dark/Kernel.js.map +3 -3
  94. package/src/in-the-dark/Registry.d.ts +1 -1
  95. package/src/in-the-dark/Registry.d.ts.map +1 -1
  96. package/src/in-the-dark/Registry.js +99 -26
  97. package/src/in-the-dark/Registry.js.map +2 -2
  98. package/src/in-the-dark/ShadowObject.d.ts +1 -1
  99. package/src/in-the-dark/ShadowObject.d.ts.map +1 -1
  100. package/src/in-the-dark/ShadowObject.js +3 -2
  101. package/src/in-the-dark/ShadowObject.js.map +2 -2
  102. package/src/in-the-dark/ShadowObjectCreationScope.d.ts +126 -0
  103. package/src/in-the-dark/ShadowObjectCreationScope.d.ts.map +1 -0
  104. package/src/in-the-dark/ShadowObjectCreationScope.js +596 -0
  105. package/src/in-the-dark/ShadowObjectCreationScope.js.map +7 -0
  106. package/src/in-the-dark/SignalsPath.d.ts.map +1 -1
  107. package/src/in-the-dark/SignalsPath.js +2 -2
  108. package/src/in-the-dark/SignalsPath.js.map +2 -2
  109. package/src/in-the-dark/events.js.map +1 -1
  110. package/src/in-the-dark/importModule.d.ts +7 -0
  111. package/src/in-the-dark/importModule.d.ts.map +1 -1
  112. package/src/in-the-dark/importModule.js +8 -5
  113. package/src/in-the-dark/importModule.js.map +2 -2
  114. package/src/index.d.ts +7 -1
  115. package/src/index.d.ts.map +1 -1
  116. package/src/index.js +5 -1
  117. package/src/index.js.map +2 -2
  118. package/src/shadow-objects.d.ts +2 -0
  119. package/src/shadow-objects.d.ts.map +1 -1
  120. package/src/shadow-objects.js +2 -0
  121. package/src/shadow-objects.js.map +2 -2
  122. package/src/shadow-objects.worker.js +0 -1
  123. package/src/shadow-objects.worker.js.map +2 -2
  124. package/src/shae-ent.js.map +1 -1
  125. package/src/shae-prop.js +2 -2
  126. package/src/shae-prop.js.map +2 -2
  127. package/src/shae-worker.js.map +1 -1
  128. package/src/types.d.ts +39 -8
  129. package/src/types.d.ts.map +1 -1
  130. package/src/utils/ConsoleLogger.d.ts +45 -10
  131. package/src/utils/ConsoleLogger.d.ts.map +1 -1
  132. package/src/utils/ConsoleLogger.js +54 -28
  133. package/src/utils/ConsoleLogger.js.map +2 -2
  134. package/src/utils/FrameLoop.d.ts +42 -3
  135. package/src/utils/FrameLoop.d.ts.map +1 -1
  136. package/src/utils/FrameLoop.js +71 -12
  137. package/src/utils/FrameLoop.js.map +2 -2
  138. package/src/utils/MicrotaskCollector.d.ts +45 -0
  139. package/src/utils/MicrotaskCollector.d.ts.map +1 -0
  140. package/src/utils/MicrotaskCollector.js +33 -0
  141. package/src/utils/MicrotaskCollector.js.map +7 -0
  142. package/src/utils/MicrotaskGate.d.ts +17 -0
  143. package/src/utils/MicrotaskGate.d.ts.map +1 -0
  144. package/src/utils/MicrotaskGate.js +20 -0
  145. package/src/utils/MicrotaskGate.js.map +7 -0
  146. package/src/utils/array-utils.js.map +1 -1
  147. package/src/utils/attr-utils.d.ts +6 -0
  148. package/src/utils/attr-utils.d.ts.map +1 -1
  149. package/src/utils/attr-utils.js +4 -2
  150. package/src/utils/attr-utils.js.map +2 -2
  151. package/src/utils/constants.js.map +1 -1
  152. package/src/utils/generateUUID.d.ts +12 -1
  153. package/src/utils/generateUUID.d.ts.map +1 -1
  154. package/src/utils/generateUUID.js +31 -266
  155. package/src/utils/generateUUID.js.map +2 -2
  156. package/src/utils/props-utils.d.ts +3 -2
  157. package/src/utils/props-utils.d.ts.map +1 -1
  158. package/src/utils/props-utils.js +13 -24
  159. package/src/utils/props-utils.js.map +2 -2
  160. package/src/utils/runGuarded.d.ts +27 -0
  161. package/src/utils/runGuarded.d.ts.map +1 -0
  162. package/src/utils/runGuarded.js +11 -0
  163. package/src/utils/runGuarded.js.map +7 -0
  164. package/src/utils/toMaybe.d.ts.map +1 -1
  165. package/src/utils/toMaybe.js.map +1 -1
  166. package/src/utils/toNamespace.d.ts.map +1 -1
  167. package/src/utils/toNamespace.js.map +1 -1
  168. package/src/utils/toUrlString.d.ts +12 -0
  169. package/src/utils/toUrlString.d.ts.map +1 -1
  170. package/src/utils/toUrlString.js.map +2 -2
  171. package/src/utils/waitForMessageOfType.d.ts +16 -1
  172. package/src/utils/waitForMessageOfType.d.ts.map +1 -1
  173. package/src/utils/waitForMessageOfType.js +28 -3
  174. package/src/utils/waitForMessageOfType.js.map +2 -2
  175. package/src/view/ComponentChanges.d.ts +91 -1
  176. package/src/view/ComponentChanges.d.ts.map +1 -1
  177. package/src/view/ComponentChanges.js +253 -69
  178. package/src/view/ComponentChanges.js.map +2 -2
  179. package/src/view/ComponentContext.d.ts +239 -7
  180. package/src/view/ComponentContext.d.ts.map +1 -1
  181. package/src/view/ComponentContext.js +648 -115
  182. package/src/view/ComponentContext.js.map +2 -2
  183. package/src/view/ComponentMemory.d.ts +3 -3
  184. package/src/view/ComponentMemory.d.ts.map +1 -1
  185. package/src/view/ComponentMemory.js +3 -1
  186. package/src/view/ComponentMemory.js.map +2 -2
  187. package/src/view/IShadowObjectEnvProxy.d.ts +33 -1
  188. package/src/view/IShadowObjectEnvProxy.d.ts.map +1 -1
  189. package/src/view/LocalShadowObjectEnv.d.ts +2 -2
  190. package/src/view/LocalShadowObjectEnv.d.ts.map +1 -1
  191. package/src/view/LocalShadowObjectEnv.js +18 -11
  192. package/src/view/LocalShadowObjectEnv.js.map +2 -2
  193. package/src/view/RemoteWorkerEnv.d.ts +129 -3
  194. package/src/view/RemoteWorkerEnv.d.ts.map +1 -1
  195. package/src/view/RemoteWorkerEnv.js +290 -60
  196. package/src/view/RemoteWorkerEnv.js.map +2 -2
  197. package/src/view/ShadowEnv.d.ts +73 -2
  198. package/src/view/ShadowEnv.d.ts.map +1 -1
  199. package/src/view/ShadowEnv.js +301 -64
  200. package/src/view/ShadowEnv.js.map +2 -2
  201. package/src/view/ViewComponent.d.ts +93 -8
  202. package/src/view/ViewComponent.d.ts.map +1 -1
  203. package/src/view/ViewComponent.js +148 -28
  204. package/src/view/ViewComponent.js.map +2 -2
  205. package/src/view/cloneChangeTrail.js.map +2 -2
  206. package/src/worker/MessageRouter.d.ts +14 -0
  207. package/src/worker/MessageRouter.d.ts.map +1 -1
  208. package/src/worker/MessageRouter.js +80 -18
  209. package/src/worker/MessageRouter.js.map +2 -2
  210. package/src/worker/WorkerRuntime.d.ts +22 -1
  211. package/src/worker/WorkerRuntime.d.ts.map +1 -1
  212. package/src/worker/WorkerRuntime.js +60 -8
  213. 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,47 +17,120 @@ 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.#allEntitiesNeedUpdate = true;
36
- this.#rootContexts = /* @__PURE__ */ new Map();
37
60
  eventize(this);
38
61
  this.registry = Registry.get(registry);
39
62
  }
40
- #entities;
41
- #rootEntities;
42
- #allEntities;
43
- #allEntitiesReversed;
44
- #allEntitiesNeedUpdate;
45
- #rootContexts;
46
63
  getEntity(uuid) {
47
- const entity = this.#entities.get(uuid)?.entity;
48
- if (!entity) {
64
+ return this.#requireEntry(uuid).entity;
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
+ }
78
+ /**
79
+ * The entry for an entity that is expected to exist.
80
+ *
81
+ * The uuid is the caller's responsibility: `getEntity()` hands the throw on to its own
82
+ * callers, and `#updateShadowObjects()` is reached only for an entity the caller has just
83
+ * confirmed. Failing here names the uuid instead of failing later on an undefined field.
84
+ */
85
+ #requireEntry(uuid) {
86
+ const entry = this.#entities.get(uuid);
87
+ if (entry === void 0) {
49
88
  throw new Error(`entity with uuid "${uuid}" not found!`);
50
89
  }
51
- return entity;
90
+ return entry;
52
91
  }
53
92
  hasEntity(uuid) {
54
93
  return this.#entities.has(uuid);
55
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
+ }
56
111
  /**
57
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.
58
121
  */
59
122
  traverseLevelOrderBFS(reverse = false) {
60
123
  if (this.#allEntitiesNeedUpdate) {
61
124
  const lvl = /* @__PURE__ */ new Map();
125
+ const visited = /* @__PURE__ */ new Set();
62
126
  const traverse = (uuid, depth) => {
63
- const e = this.getEntity(uuid);
64
- if (lvl.has(depth)) {
65
- lvl.get(depth).push(e);
127
+ if (visited.has(uuid)) return;
128
+ visited.add(uuid);
129
+ const e = this.findEntity(uuid);
130
+ if (e === void 0) return;
131
+ const entities = lvl.get(depth);
132
+ if (entities) {
133
+ entities.push(e);
66
134
  } else {
67
135
  lvl.set(depth, [e]);
68
136
  }
@@ -77,42 +145,84 @@ class Kernel {
77
145
  this.#allEntitiesReversed = this.#allEntities.slice().reverse();
78
146
  this.#allEntitiesNeedUpdate = false;
79
147
  }
80
- return reverse ? this.#allEntitiesReversed : this.#allEntities;
148
+ return reverse ? this.#allEntitiesReversed.slice() : this.#allEntities.slice();
81
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
+ */
82
159
  getEntityGraph() {
83
- return Array.from(this.#rootEntities).map((uuid) => this.getEntityGraphNode(uuid));
160
+ const visited = /* @__PURE__ */ new Set();
161
+ return Array.from(this.#rootEntities).map((uuid) => this.#getEntityGraphNode(uuid, visited)).filter((node) => node !== void 0);
84
162
  }
85
- getEntityGraphNode(uuid) {
86
- if (!this.#entities.has(uuid)) return void 0;
87
- const { token, entity } = this.#entities.get(uuid);
163
+ #getEntityGraphNode(uuid, visited, omitted) {
164
+ const entry = this.#entities.get(uuid);
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);
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);
88
177
  return {
89
178
  token,
90
179
  entity,
91
180
  props: Object.fromEntries(entity.propEntries()),
92
- children: entity.children.map((child) => this.getEntityGraphNode(child.uuid))
181
+ children,
182
+ ...omittedChildren.length > 0 ? { omittedChildren } : {}
93
183
  };
94
184
  }
95
185
  upgradeEntities() {
96
186
  const entityConstructors = /* @__PURE__ */ new Map();
97
187
  for (const entity of this.traverseLevelOrderBFS(true)) {
98
- entityConstructors.set(entity.uuid, this.updateShadowObjects(entity.uuid, 2 /* DestroyOnly */));
188
+ if (!this.hasEntity(entity.uuid)) continue;
189
+ entityConstructors.set(entity.uuid, this.#updateShadowObjects(entity.uuid, 2 /* DestroyOnly */));
99
190
  }
100
191
  for (const entity of this.traverseLevelOrderBFS(false)) {
101
- this.updateShadowObjects(entity.uuid, 1 /* JustCreate */, entityConstructors.get(entity.uuid));
192
+ if (!this.hasEntity(entity.uuid)) continue;
193
+ this.#updateShadowObjects(entity.uuid, 1 /* JustCreate */, entityConstructors.get(entity.uuid));
102
194
  }
103
195
  entityConstructors.clear();
104
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
+ */
105
211
  run(event) {
106
- if (this.logger.isDebug) {
107
- 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 });
108
223
  }
109
- batch(() => {
110
- for (const entry of event.changeTrail) {
111
- this.parse(entry);
112
- }
113
- });
114
224
  }
115
- parse(entry) {
225
+ #parse(entry) {
116
226
  switch (entry.type) {
117
227
  case ComponentChangeType.CreateEntities:
118
228
  this.createEntity(
@@ -123,19 +233,15 @@ class Kernel {
123
233
  entry.properties,
124
234
  entry.autoDestructionOnParentRemoval
125
235
  );
126
- this.#allEntitiesNeedUpdate = true;
127
236
  break;
128
237
  case ComponentChangeType.DestroyEntities:
129
238
  this.destroyEntity(entry.uuid);
130
- this.#allEntitiesNeedUpdate = true;
131
239
  break;
132
240
  case ComponentChangeType.SetParent:
133
241
  this.setParent(entry.uuid, entry.parentUuid, entry.order);
134
- this.#allEntitiesNeedUpdate = true;
135
242
  break;
136
243
  case ComponentChangeType.UpdateOrder:
137
244
  this.updateOrder(entry.uuid, entry.order);
138
- this.#allEntitiesNeedUpdate = true;
139
245
  break;
140
246
  case ComponentChangeType.ChangeProperties:
141
247
  this.changeProperties(entry.uuid, entry.properties);
@@ -149,100 +255,194 @@ class Kernel {
149
255
  }
150
256
  }
151
257
  createEntity(uuid, token, parentUuid, order = 0, properties, autoDestructionOnParentRemoval = false) {
258
+ if (this.#entities.has(uuid)) {
259
+ throw new EntityUuidInUseError(uuid);
260
+ }
152
261
  const e = new Entity(this, uuid);
153
262
  e.order = order;
154
263
  const entry = { token, entity: e, usedConstructors: /* @__PURE__ */ new Map() };
155
264
  this.#entities.set(uuid, entry);
156
- if (parentUuid) {
157
- e.parentUuid = parentUuid;
158
- }
159
- e.autoDestructionOnParentRemoval = autoDestructionOnParentRemoval;
160
- if (!e.hasParent) {
161
- this.#rootEntities.add(uuid);
162
- }
163
- if (properties) {
164
- 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;
165
278
  }
166
- this.createShadowObjects(uuid);
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
+ );
167
303
  }
168
304
  destroyEntity(uuid) {
169
- if (!this.#entities.has(uuid)) return;
170
- const { entity, usedConstructors } = this.#entities.get(uuid);
171
- const childrenSnapshot = [...entity.children];
172
- for (const child of childrenSnapshot) {
173
- if (child.autoDestructionOnParentRemoval) {
174
- this.destroyEntity(child.uuid);
175
- } else {
176
- child.removeFromParent();
177
- this.#rootEntities.add(child.uuid);
305
+ const entry = this.#entities.get(uuid);
306
+ if (entry === void 0) return;
307
+ const { entity, usedConstructors } = entry;
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);
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
+ );
178
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;
179
345
  }
180
- entity.removeFromParent();
181
- emit(entity, onDestroy, entity);
182
- usedConstructors.clear();
183
- this.#entities.delete(entity.uuid);
184
- this.#rootEntities.delete(entity.uuid);
185
- this.#allEntitiesNeedUpdate = true;
186
346
  }
187
- setParent(uuid, parentUuid, order = 0) {
347
+ /**
348
+ * @param order the new order, or `undefined` to keep the current one. A set-parent change
349
+ * only carries an order when it actually changed, so an absent order must not be read as
350
+ * a reset to `0` — that would silently drop the order the view side still holds.
351
+ */
352
+ setParent(uuid, parentUuid, order) {
188
353
  const e = this.getEntity(uuid);
189
- if (e.parentUuid === parentUuid && e.order === order) return;
354
+ const nextOrder = order ?? e.order;
355
+ if (e.parentUuid === parentUuid && e.order === nextOrder) return;
190
356
  if (parentUuid && !this.#entities.has(parentUuid)) {
191
357
  throw new Error(`entity with uuid "${parentUuid}" not found!`);
192
358
  }
193
- e.removeFromParent();
194
- e.order = order;
359
+ if (parentUuid) {
360
+ e.assertAttachableTo(this.getEntity(parentUuid));
361
+ }
362
+ e.removeFromParent(parentUuid == null);
363
+ e.order = nextOrder;
195
364
  e.parentUuid = parentUuid;
196
- 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) {
197
398
  this.#rootEntities.delete(uuid);
198
399
  } else {
199
400
  this.#rootEntities.add(uuid);
200
401
  }
201
- e.reSubscribeToParentContexts();
202
- queueMicrotask(() => {
203
- if (this.logger.isDebug) {
204
- this.logger.debug("entity.onParentChanged", { uuid, parentUuid, order, entity: e });
205
- }
206
- emit(e, onParentChanged, e);
207
- });
208
- }
209
- updateOrder(uuid, order) {
210
- this.getEntity(uuid).order = order;
211
402
  }
212
403
  dispatchEventsToEntity(uuid, events) {
213
- this.getEntity(uuid)?.dispatchViewEvents(events);
404
+ this.findEntity(uuid)?.dispatchViewEvents(events);
214
405
  }
215
406
  changeProperties(uuid, properties) {
216
407
  this.getEntity(uuid).setProperties(properties);
217
- this.updateShadowObjects(uuid);
408
+ this.#updateShadowObjects(uuid);
218
409
  }
219
410
  changeToken(uuid, token) {
220
- if (!this.#entities.has(uuid)) return;
221
411
  const entry = this.#entities.get(uuid);
412
+ if (entry === void 0) return;
222
413
  if (entry.token === token) return;
414
+ const previousToken = entry.token;
223
415
  entry.token = token;
224
- this.updateShadowObjects(uuid);
416
+ this.#updateShadowObjects(uuid, 0 /* CreateAndDestroy */, void 0, previousToken);
225
417
  }
226
418
  dispatchMessageToView(message) {
227
419
  queueMicrotask(() => {
228
- emit(this, MessageToView, message);
420
+ emitSafe(this, MessageToView, message);
229
421
  });
230
422
  }
231
423
  /**
232
424
  * Create or destroy the shadow-objects of an entity using the registered constructors.
233
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.
234
431
  */
235
- updateShadowObjects(uuid, action = 0 /* CreateAndDestroy */, nextConstructors) {
236
- const entry = this.#entities.get(uuid);
432
+ #updateShadowObjects(uuid, action = 0 /* CreateAndDestroy */, nextConstructors, previousToken) {
433
+ const entry = this.#requireEntry(uuid);
237
434
  nextConstructors ??= new Set(this.registry.findConstructors(entry.token, entry.entity.truthyProps()));
238
435
  const shouldDestroy = action === 0 /* CreateAndDestroy */ || action === 2 /* DestroyOnly */;
239
436
  const shouldCreate = action === 0 /* CreateAndDestroy */ || action === 1 /* JustCreate */;
437
+ const removed = [];
438
+ const created = [];
240
439
  if (shouldDestroy) {
241
440
  for (const [construct, shadowObjects] of entry.usedConstructors) {
242
441
  if (!nextConstructors.has(construct)) {
243
442
  entry.usedConstructors.delete(construct);
443
+ removed.push({ construct, count: shadowObjects.size });
244
444
  for (const obj of shadowObjects) {
245
- this.destroyShadowObject(obj, entry.entity);
445
+ this.#destroyShadowObject(obj, entry.entity);
246
446
  }
247
447
  }
248
448
  }
@@ -250,322 +450,166 @@ class Kernel {
250
450
  if (shouldCreate) {
251
451
  for (const construct of nextConstructors) {
252
452
  if (!entry.usedConstructors.has(construct)) {
253
- 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
+ }
254
459
  }
255
460
  }
256
461
  }
257
462
  return nextConstructors;
258
463
  }
259
- constructShadowObject(construct, entry) {
260
- const unsubscribePrimary = /* @__PURE__ */ new Set();
261
- const unsubscribeSecondary = /* @__PURE__ */ new Set();
262
- const contextReaders = /* @__PURE__ */ new Map();
263
- const contextReaderCompares = /* @__PURE__ */ new Map();
264
- const contextParentReaders = /* @__PURE__ */ new Map();
265
- const contextParentReaderCompares = /* @__PURE__ */ new Map();
266
- const contextProviders = /* @__PURE__ */ new Map();
267
- const contextRootProviders = /* @__PURE__ */ new Map();
268
- const propertyReaders = /* @__PURE__ */ new Map();
269
- const propertyCompares = /* @__PURE__ */ new Map();
270
- const getUseProperty = (name, options) => {
271
- if (!usePropertyOptionsDeprecatedShown && options != null && typeof options === "function") {
272
- console.warn(
273
- '[shadow-objects] Deprecation Warning: The "isEqual" option of "useProperty()" is now passed as {compare} argument. Please update your code accordingly.'
274
- );
275
- usePropertyOptionsDeprecatedShown = true;
276
- }
277
- const opts = typeof options === "function" ? { compare: options } : options;
278
- let propReader = propertyReaders.get(name);
279
- if (propReader === void 0) {
280
- propReader = createSignal(void 0, opts).get;
281
- propertyReaders.set(name, propReader);
282
- propertyCompares.set(name, opts?.compare);
283
- const con = link(entry.entity.getPropertyReader(name), propReader);
284
- unsubscribeSecondary.add(con.destroy.bind(con));
285
- } else if (opts?.compare != null && propertyCompares.get(name) !== opts.compare) {
286
- console.warn(
287
- `[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
288
511
  );
289
512
  }
290
- return propReader;
291
- };
292
- const shadowObject = eventize(
293
- new construct({
294
- entity: entry.entity,
295
- provideContext(name, sourceOrInitialValue, options) {
296
- if (!provideContextOptionsDeprecatedShown && options != null && typeof options === "function") {
297
- console.warn(
298
- '[shadow-objects] Deprecation Warning: The "isEqual" option of "provideContext()" is now passed as {compare} argument. Please update your code accordingly.'
299
- );
300
- provideContextOptionsDeprecatedShown = true;
301
- }
302
- const opts = typeof options === "function" ? { compare: options } : options;
303
- let ctxProvider = contextProviders.get(name);
304
- if (ctxProvider == null) {
305
- const isSig = isSignal(sourceOrInitialValue);
306
- const initialValue = isSig ? void 0 : toMaybe(sourceOrInitialValue);
307
- ctxProvider = createSignal(initialValue, opts?.compare ? { compare: opts.compare } : void 0);
308
- if (isSig) {
309
- const ln2 = link(sourceOrInitialValue, ctxProvider);
310
- unsubscribeSecondary.add(ln2.destroy.bind(ln2));
311
- }
312
- const ln = link(ctxProvider, entry.entity.provideContext(name));
313
- unsubscribeSecondary.add(ln.destroy.bind(ln));
314
- contextProviders.set(name, ctxProvider);
315
- }
316
- if (ctxProvider != null && (opts?.clearOnDestroy ?? true)) {
317
- unsubscribeSecondary.add(() => {
318
- ctxProvider.set(void 0);
319
- });
320
- }
321
- return ctxProvider;
322
- },
323
- provideGlobalContext(name, sourceOrInitialValue, options) {
324
- if (!provideGlobalContextOptionsDeprecatedShown && options != null && typeof options === "function") {
325
- console.warn(
326
- '[shadow-objects] Deprecation Warning: The "isEqual" option of "provideGlobalContext()" is now passed as {compare} argument. Please update your code accordingly.'
327
- );
328
- provideGlobalContextOptionsDeprecatedShown = true;
329
- }
330
- const opts = typeof options === "function" ? { compare: options } : options;
331
- let ctxProvider = contextRootProviders.get(name);
332
- if (ctxProvider == null) {
333
- const isSig = isSignal(sourceOrInitialValue);
334
- const initialValue = isSig ? void 0 : toMaybe(sourceOrInitialValue);
335
- ctxProvider = createSignal(initialValue, opts?.compare ? { compare: opts.compare } : void 0);
336
- if (isSig) {
337
- const ln2 = link(sourceOrInitialValue, ctxProvider);
338
- unsubscribeSecondary.add(ln2.destroy.bind(ln2));
339
- }
340
- const ln = link(ctxProvider, entry.entity.provideGlobalContext(name));
341
- unsubscribeSecondary.add(ln.destroy.bind(ln));
342
- contextRootProviders.set(name, ctxProvider);
343
- }
344
- if (ctxProvider != null && (opts?.clearOnDestroy ?? true)) {
345
- unsubscribeSecondary.add(() => {
346
- ctxProvider.set(void 0);
347
- });
348
- }
349
- return ctxProvider;
350
- },
351
- useContext(name, options) {
352
- if (!useContextOptionsDeprecatedShown && options != null && typeof options === "function") {
353
- console.warn(
354
- '[shadow-objects] Deprecation Warning: The "isEqual" option of "useContext()" is now passed as {compare} argument. Please update your code accordingly.'
355
- );
356
- useContextOptionsDeprecatedShown = true;
357
- }
358
- const opts = typeof options === "function" ? { compare: options } : options;
359
- let ctxReader = contextReaders.get(name);
360
- if (ctxReader === void 0) {
361
- ctxReader = createSignal(void 0, opts).get;
362
- contextReaders.set(name, ctxReader);
363
- contextReaderCompares.set(name, opts?.compare);
364
- const ln = link(entry.entity.useContext(name), ctxReader);
365
- unsubscribeSecondary.add(ln.destroy.bind(ln));
366
- } else if (opts?.compare != null && contextReaderCompares.get(name) !== opts.compare) {
367
- console.warn(
368
- `[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.`
369
- );
370
- }
371
- return ctxReader;
372
- },
373
- useParentContext(name, options) {
374
- if (!useParentContextOptionsDeprecatedShown && options != null && typeof options === "function") {
375
- console.warn(
376
- '[shadow-objects] Deprecation Warning: The "isEqual" option of "useParentContext()" is now passed as {compare} argument. Please update your code accordingly.'
377
- );
378
- useParentContextOptionsDeprecatedShown = true;
379
- }
380
- const opts = typeof options === "function" ? { compare: options } : options;
381
- let ctxReader = contextParentReaders.get(name);
382
- if (ctxReader === void 0) {
383
- ctxReader = createSignal(void 0, opts).get;
384
- contextParentReaders.set(name, ctxReader);
385
- contextParentReaderCompares.set(name, opts?.compare);
386
- const ln = link(entry.entity.useParentContext(name), ctxReader);
387
- unsubscribeSecondary.add(ln.destroy.bind(ln));
388
- } else if (opts?.compare != null && contextParentReaderCompares.get(name) !== opts.compare) {
389
- console.warn(
390
- `[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.`
391
- );
392
- }
393
- return ctxReader;
394
- },
395
- dispatchMessageToView(type, data, transferables, traverseChildren = false) {
396
- entry.entity.dispatchMessageToView(type, data, transferables, traverseChildren);
397
- },
398
- useProperty: getUseProperty,
399
- useProperties(props) {
400
- const result = {};
401
- for (const key in props) {
402
- if (Object.hasOwn(props, key)) {
403
- result[key] = getUseProperty(props[key]);
404
- }
405
- }
406
- return result;
407
- },
408
- createResource(factory, cleanup) {
409
- const resourceSignal = createSignal();
410
- const effect = createEffect(() => {
411
- const resource = toMaybe(factory());
412
- resourceSignal.set(resource);
413
- if (resource !== void 0 && cleanup) {
414
- return () => {
415
- cleanup(resource);
416
- resourceSignal.set(void 0);
417
- };
418
- }
419
- return () => {
420
- resourceSignal.set(void 0);
421
- };
422
- });
423
- unsubscribeSecondary.add(() => {
424
- effect.destroy();
425
- resourceSignal.set(void 0);
426
- destroySignal(resourceSignal);
427
- });
428
- return resourceSignal;
429
- },
430
- createEffect(...args) {
431
- const effect = createEffect(...args);
432
- unsubscribeSecondary.add(effect.destroy);
433
- return effect;
434
- },
435
- createSignal(...args) {
436
- const sig = createSignal(...args);
437
- unsubscribeSecondary.add(() => {
438
- destroySignal(sig);
439
- });
440
- return sig;
441
- },
442
- createMemo(...args) {
443
- const sig = createMemo(...args);
444
- unsubscribeSecondary.add(() => {
445
- destroySignal(sig);
446
- });
447
- return sig;
448
- },
449
- on(...args) {
450
- const [firstArg] = args;
451
- if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
452
- const unsub2 = on(entry.entity, ...args);
453
- unsubscribeSecondary.add(unsub2);
454
- return unsub2;
455
- }
456
- const unsub = on(...args);
457
- unsubscribeSecondary.add(unsub);
458
- return Object.assign(() => {
459
- unsubscribeSecondary.delete(unsub);
460
- unsub();
461
- }, unsub);
462
- },
463
- once(...args) {
464
- const [firstArg] = args;
465
- if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
466
- const unsub2 = once(entry.entity, ...args);
467
- unsubscribeSecondary.add(unsub2);
468
- return unsub2;
469
- }
470
- const unsub = once(...args);
471
- unsubscribeSecondary.add(unsub);
472
- return Object.assign(() => {
473
- unsubscribeSecondary.delete(unsub);
474
- unsub();
475
- }, unsub);
476
- },
477
- emit(...args) {
478
- const [firstArg] = args;
479
- if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
480
- emit(entry.entity, ...args);
481
- } else {
482
- emit(...args);
483
- }
484
- },
485
- onViewEvent(callback) {
486
- const unsub = on(entry.entity, onViewEvent, (type, data) => {
487
- callback(type, data);
488
- });
489
- unsubscribeSecondary.add(unsub);
490
- },
491
- onDestroy(callback) {
492
- unsubscribePrimary.add(callback);
493
- }
494
- })
495
- );
496
- if (this.logger.isInfo) {
497
- this.logger.info("create shadow-object", getDisplayName(construct), { shadowObject, entity: entry.entity });
498
513
  }
499
- once(entry.entity, onDestroy, Priority.Low, () => {
500
- if (this.logger.isInfo) {
501
- this.logger.info("destroy shadow-object", getDisplayName(construct), { shadowObject, entity: entry.entity });
502
- }
503
- for (const callback of unsubscribePrimary) {
504
- callback();
505
- }
506
- for (const callback of unsubscribeSecondary) {
507
- callback();
508
- }
509
- for (const sig of contextReaders.values()) {
510
- destroySignal(sig);
511
- }
512
- for (const sig of contextParentReaders.values()) {
513
- destroySignal(sig);
514
- }
515
- for (const sig of propertyReaders.values()) {
516
- destroySignal(sig);
517
- }
518
- for (const sig of contextProviders.values()) {
519
- destroySignal(sig);
520
- }
521
- for (const sig of contextRootProviders.values()) {
522
- destroySignal(sig);
523
- }
524
- unsubscribePrimary.clear();
525
- unsubscribeSecondary.clear();
526
- contextReaders.clear();
527
- contextParentReaders.clear();
528
- propertyReaders.clear();
529
- contextProviders.clear();
530
- contextRootProviders.clear();
531
- const otherShadowObjects = entry.usedConstructors.get(construct);
532
- if (otherShadowObjects) {
533
- otherShadowObjects.delete(shadowObject);
534
- if (otherShadowObjects.size === 0) {
535
- 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
+ }
536
537
  }
537
538
  }
538
- });
539
- if (entry.usedConstructors.has(construct)) {
540
- entry.usedConstructors.get(construct).add(shadowObject);
539
+ );
540
+ const createdBy = entry.usedConstructors.get(construct);
541
+ if (createdBy) {
542
+ createdBy.add(shadowObject);
541
543
  } else {
542
544
  entry.usedConstructors.set(construct, /* @__PURE__ */ new Set([shadowObject]));
543
545
  }
544
- 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
+ }
545
552
  return shadowObject;
546
553
  }
547
- createShadowObjects(uuid) {
548
- const entry = this.#entities.get(uuid);
554
+ #createShadowObjects(entry) {
549
555
  this.registry.findConstructors(entry.token, entry.entity.truthyProps())?.forEach((construct) => {
550
- this.constructShadowObject(construct, entry);
556
+ this.#constructShadowObject(construct, entry);
551
557
  });
552
558
  }
553
559
  findShadowObjects(uuid) {
554
- if (!this.#entities.has(uuid)) return [];
555
- const { usedConstructors } = this.#entities.get(uuid);
560
+ const entry = this.#entities.get(uuid);
561
+ if (entry === void 0) return [];
562
+ const { usedConstructors } = entry;
556
563
  return Array.from(new Set(Array.from(usedConstructors.values()).flatMap((objs) => Array.from(objs))));
557
564
  }
558
- 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
+ }
559
575
  on(entity, shadowObject);
560
576
  if (typeof shadowObject[onCreate] === "function") {
561
577
  shadowObject[onCreate](entity);
562
578
  }
563
579
  }
564
- 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;
565
595
  if (typeof shadowObject[onDestroy] === "function") {
566
- shadowObject[onDestroy](entity);
596
+ runGuarded(
597
+ this.logger,
598
+ () => shadowObject[onDestroy](entity),
599
+ "shadow-object onDestroy hook failed:",
600
+ displayName
601
+ );
567
602
  }
568
- 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();
569
613
  off(entity, shadowObject);
570
614
  }
571
615
  findOrCreateRootContext(name) {
@@ -577,13 +621,26 @@ class Kernel {
577
621
  return ctx;
578
622
  }
579
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;
580
637
  for (const ctx of this.#rootContexts.values()) {
581
638
  ctx.dispose();
582
639
  }
583
640
  this.#rootContexts.clear();
584
- for (const entity of this.traverseLevelOrderBFS().reverse()) {
585
- this.destroyEntity(entity.uuid);
586
- }
641
+ queueMicrotask(() => {
642
+ off(this);
643
+ });
587
644
  }
588
645
  }
589
646
  export {