@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,17 +1,82 @@
1
1
  import { ChangeTrailPhase, ContextLost, GlobalNS } from "../constants.js";
2
- import { removeFrom } from "../utils/array-utils.js";
2
+ import { ConsoleLogger } from "../utils/ConsoleLogger.js";
3
+ import { MicrotaskCollector } from "../utils/MicrotaskCollector.js";
4
+ import { runGuarded } from "../utils/runGuarded.js";
3
5
  import { toNamespace } from "../utils/toNamespace.js";
4
- import { ComponentChanges } from "./ComponentChanges.js";
6
+ import { ComponentChanges, PropertyWithoutValue } from "./ComponentChanges.js";
5
7
  import { ComponentMemory } from "./ComponentMemory.js";
8
+ class OrderedUuids {
9
+ #uuids = [];
10
+ #members = /* @__PURE__ */ new Set();
11
+ /** The uuids in their order. Read-only: every write goes through this class. */
12
+ get uuids() {
13
+ return this.#uuids;
14
+ }
15
+ get size() {
16
+ return this.#uuids.length;
17
+ }
18
+ has(uuid) {
19
+ return this.#members.has(uuid);
20
+ }
21
+ push(uuid) {
22
+ this.#uuids.push(uuid);
23
+ this.#members.add(uuid);
24
+ }
25
+ insertAt(index, uuid) {
26
+ this.#uuids.splice(index, 0, uuid);
27
+ this.#members.add(uuid);
28
+ }
29
+ /** @returns whether the uuid was in the list */
30
+ delete(uuid) {
31
+ if (!this.#members.delete(uuid)) return false;
32
+ const idx = this.#uuids.indexOf(uuid);
33
+ if (idx !== -1) {
34
+ this.#uuids.splice(idx, 1);
35
+ }
36
+ return true;
37
+ }
38
+ clear() {
39
+ this.#uuids.length = 0;
40
+ this.#members.clear();
41
+ }
42
+ }
43
+ class ComponentContextDisposedError extends Error {
44
+ constructor(message = "the component context has been disposed") {
45
+ super(message);
46
+ this.name = "ComponentContextDisposedError";
47
+ }
48
+ }
49
+ class ComponentUuidInUseError extends Error {
50
+ constructor(uuid) {
51
+ super(`the view component cannot join the component context because the uuid ${uuid} is already held by another component`);
52
+ this.name = "ComponentUuidInUseError";
53
+ this.uuid = uuid;
54
+ }
55
+ }
6
56
  class ComponentContext {
7
57
  static {
8
58
  this.ReRequestParentRoots = "re-request-parent-roots";
9
59
  }
60
+ static {
61
+ this.ReRequestParent = "re-request-parent";
62
+ }
63
+ static {
64
+ /**
65
+ * Ask the element behind a component to let the properties hanging on it look for their host
66
+ * again.
67
+ *
68
+ * A context knows nothing about `<shae-prop>` — a property has no view component and cannot be
69
+ * addressed here. The signal still belongs next to the two above: it travels the same broadcast
70
+ * channel to the same receivers, and the element on the other end is the one that can reach the
71
+ * properties below it.
72
+ */
73
+ this.ReRequestEntHost = "re-request-ent-host";
74
+ }
10
75
  static getContextsMap() {
11
- if (globalThis.__shadowEntsContexts == null) {
12
- globalThis.__shadowEntsContexts = /* @__PURE__ */ new Map();
76
+ if (globalThis.__shadowObjectsContexts == null) {
77
+ globalThis.__shadowObjectsContexts = /* @__PURE__ */ new Map();
13
78
  }
14
- return globalThis.__shadowEntsContexts;
79
+ return globalThis.__shadowObjectsContexts;
15
80
  }
16
81
  static get(namespace) {
17
82
  const ns = toNamespace(namespace);
@@ -22,9 +87,63 @@ class ComponentContext {
22
87
  return new ComponentContext(ns);
23
88
  }
24
89
  #components = /* @__PURE__ */ new Map();
25
- #rootComponents = [];
26
- // we use an Array here and not a Set, because we want to keep the insertion order
90
+ // the entity tree is walked in this order, so the roots are a list and not a set
91
+ #rootComponents = new OrderedUuids();
92
+ #isDisposed = false;
93
+ // which instances name this ComponentContext, and the one place that can say so. #components holds one
94
+ // entry per uuid, and an entry outlives the departure of its component until the next change
95
+ // trail — so a uuid standing in that map is no statement about anyone still being a member
96
+ #componentInstances = /* @__PURE__ */ new Set();
97
+ /**
98
+ * The entry `component` owns, and nothing else.
99
+ *
100
+ * An entry outlives the departure of its component until the next change trail, so a later
101
+ * component can take the uuid over while the entry is still standing. A lookup by uuid alone
102
+ * would then hand back the entry of a namesake, and every write that followed would land on a
103
+ * live component that was never named. Each method that is given an instance asks through here,
104
+ * so that it acts on that instance or on nothing.
105
+ */
106
+ #entryOf(component) {
107
+ const entry = this.#components.get(component.uuid);
108
+ return entry !== void 0 && entry.component === component ? entry : void 0;
109
+ }
27
110
  #componentMemory = new ComponentMemory();
111
+ #logger = new ConsoleLogger("ComponentContext");
112
+ /**
113
+ * The change trail this context built last, as long as nobody has settled it yet.
114
+ * `owners[i]` is the bookkeeping that produced `entries[i]`; `retiring` are the components the
115
+ * build read as spent, which is a verdict of the build and not of the commit — a component
116
+ * destroyed while the trail travels owes its destruction to the *next* trail, and dropping its
117
+ * entry here would leave the entity standing with nothing left to take it down.
118
+ */
119
+ #uncommittedTrail;
120
+ /** The components a build reads as spent: destroyed, or created and dropped without ever going out. */
121
+ #retiringComponents(participants) {
122
+ return participants.filter((changes) => changes.isDestroyed || changes.isNew && !changes.isCreated);
123
+ }
124
+ /**
125
+ * Carry out the verdict the build took, for the components whose every entry is settled.
126
+ *
127
+ * A component that has been claimed again since the build stays: a uuid its holder has left is
128
+ * free, and the entry now belongs to whoever took it over.
129
+ */
130
+ #retireComponents(retiring, stillPending) {
131
+ for (const changes of retiring) {
132
+ if (stillPending?.has(changes)) continue;
133
+ if (changes.isCreated) continue;
134
+ this.#deleteComponent(changes.uuid, changes);
135
+ }
136
+ }
137
+ /**
138
+ * Release the record of the built trail and drop the components it retired, without folding
139
+ * anything into their bookkeeping.
140
+ */
141
+ #retireBuiltTrail() {
142
+ const uncommitted = this.#uncommittedTrail;
143
+ if (uncommitted == null) return;
144
+ this.#uncommittedTrail = void 0;
145
+ this.#retireComponents(uncommitted.retiring);
146
+ }
28
147
  constructor(namespace = GlobalNS) {
29
148
  const ns = toNamespace(namespace);
30
149
  const ctxMap = ComponentContext.getContextsMap();
@@ -34,16 +153,48 @@ class ComponentContext {
34
153
  this.ns = ns;
35
154
  ctxMap.set(ns, this);
36
155
  }
156
+ /**
157
+ * Whether this context has been torn down by {@link ComponentContext.dispose}.
158
+ *
159
+ * A disposed context holds no components, produces empty change trails and no longer
160
+ * occupies its namespace. It cannot be revived; use {@link ComponentContext.get} to
161
+ * obtain a fresh context for the same namespace.
162
+ */
163
+ get isDisposed() {
164
+ return this.#isDisposed;
165
+ }
166
+ /**
167
+ * Take a component in: it becomes a member of this {@link ComponentContext} and a
168
+ * `CreateEntities` change is written for its uuid.
169
+ *
170
+ * Called by the {@link ViewComponent#context} setter, and by nothing else.
171
+ *
172
+ * @throws {ComponentContextDisposedError} if this {@link ComponentContext} has been disposed
173
+ * @throws {ComponentUuidInUseError} if another member of this {@link ComponentContext} holds
174
+ * `component.uuid`
175
+ */
37
176
  addComponent(component) {
38
- let viewInstance;
39
- if (this.#components.has(component.uuid)) {
40
- viewInstance = this.#components.get(component.uuid);
177
+ if (this.#isDisposed) {
178
+ throw new ComponentContextDisposedError(
179
+ `the view component ${component.uuid} cannot join the component context because it has been disposed`
180
+ );
181
+ }
182
+ let viewInstance = this.#components.get(component.uuid);
183
+ if (viewInstance) {
184
+ if (viewInstance.component !== component) {
185
+ if (this.#componentInstances.has(viewInstance.component)) {
186
+ throw new ComponentUuidInUseError(component.uuid);
187
+ }
188
+ for (const childUuid of [...viewInstance.children.uuids]) {
189
+ this.#components.get(childUuid)?.component.removeFromParent();
190
+ }
191
+ }
41
192
  viewInstance.component = component;
42
- viewInstance.children = [];
193
+ viewInstance.children.clear();
43
194
  } else {
44
195
  viewInstance = {
45
196
  component,
46
- children: [],
197
+ children: new OrderedUuids(),
47
198
  changes: new ComponentChanges(component.uuid),
48
199
  propIsEqual: void 0
49
200
  };
@@ -61,80 +212,127 @@ class ComponentContext {
61
212
  } else {
62
213
  this.#appendToOrdered(component, this.#rootComponents);
63
214
  }
215
+ this.#componentInstances.add(component);
64
216
  this.#viewInstances = void 0;
65
217
  }
218
+ /**
219
+ * Whether `component` is a member of this {@link ComponentContext} right now.
220
+ *
221
+ * This is the question about the instance, and the only one this class answers about
222
+ * membership. A component that has left gets `false`, even while its entry is still standing —
223
+ * {@link ComponentContext.hasComponents} is what counts entries.
224
+ */
66
225
  hasComponent(component) {
67
- return this.#components.has(component.uuid);
226
+ return this.#componentInstances.has(component);
68
227
  }
69
228
  hasComponents() {
70
229
  return this.#components.size > 0;
71
230
  }
72
231
  isRootComponent(component) {
73
- return this.#rootComponents.includes(component.uuid);
232
+ return this.#entryOf(component) !== void 0 && this.#rootComponents.has(component.uuid);
74
233
  }
75
234
  destroyComponent(component) {
76
- if (this.hasComponent(component)) {
77
- const entry = this.#components.get(component.uuid);
78
- entry.children.slice(0).forEach((childUuid) => this.#components.get(childUuid)?.component.removeFromParent());
235
+ this.#componentInstances.delete(component);
236
+ const entry = this.#entryOf(component);
237
+ if (entry !== void 0 && !entry.changes.isDestroyed) {
238
+ for (const childUuid of [...entry.children.uuids]) {
239
+ this.#components.get(childUuid)?.component.removeFromParent();
240
+ }
79
241
  entry.changes.destroy();
80
242
  this.#viewInstances = void 0;
81
243
  }
244
+ if (component.context === this) {
245
+ component.destroy();
246
+ }
82
247
  }
248
+ /**
249
+ * The children of `component`, in sort order — a fresh array each call, and an empty one for
250
+ * an instance that does not own its entry.
251
+ *
252
+ * Uuids without a matching view instance are skipped instead of dereferenced: `addToChildren()`
253
+ * writes a uuid into a children list without the parent link of the child following along, so
254
+ * `#deleteComponent()` has no list to take that uuid out of and the entry behind it goes while
255
+ * the list keeps naming it. `#appendToOrdered()` and `#traverseLevelOrderBFS()` read a children
256
+ * list the same way.
257
+ */
83
258
  getChildren(component) {
84
- return this.#components.get(component.uuid)?.children.map((uuid) => this.#components.get(uuid).component) ?? [];
85
- }
86
- removeFromParent(childUuid, parent) {
87
- if (this.hasComponent(parent)) {
88
- const childEntry = this.#components.get(childUuid);
89
- const parentEntry = this.#components.get(parent.uuid);
90
- const childIdx = parentEntry.children.indexOf(childUuid);
91
- if (childIdx !== -1) {
92
- parentEntry.children.splice(childIdx, 1);
93
- childEntry.changes.setParent(void 0);
259
+ const children = this.#entryOf(component)?.children;
260
+ if (children === void 0) return [];
261
+ const result = [];
262
+ for (const uuid of children.uuids) {
263
+ const entry = this.#components.get(uuid);
264
+ if (entry !== void 0) {
265
+ result.push(entry.component);
94
266
  }
95
- this.#appendToOrdered(childEntry.component, this.#rootComponents);
96
- this.#viewInstances = void 0;
97
267
  }
268
+ return result;
98
269
  }
99
- moveToRoot(childUuid) {
100
- const childEntry = this.#components.get(childUuid);
101
- if (childEntry) {
102
- childEntry.changes?.setParent(void 0);
270
+ removeFromParent(component, parent) {
271
+ const parentEntry = this.#entryOf(parent);
272
+ if (parentEntry === void 0) return;
273
+ const childEntry = this.#entryOf(component);
274
+ if (childEntry === void 0) return;
275
+ if (parentEntry.children.delete(component.uuid)) {
276
+ childEntry.changes.setParent(void 0);
103
277
  this.#appendToOrdered(childEntry.component, this.#rootComponents);
278
+ this.#viewInstances = void 0;
104
279
  }
280
+ }
281
+ moveToRoot(component) {
282
+ const childEntry = this.#entryOf(component);
283
+ if (childEntry === void 0) return;
284
+ const parentEntry = component.parent ? this.#entryOf(component.parent) : void 0;
285
+ parentEntry?.children.delete(component.uuid);
286
+ childEntry.changes.setParent(void 0);
287
+ this.#appendToOrdered(childEntry.component, this.#rootComponents);
105
288
  this.#viewInstances = void 0;
106
289
  }
107
290
  changeToken(component, token) {
108
- this.#components.get(component.uuid)?.changes.changeToken(token);
291
+ this.#entryOf(component)?.changes.changeToken(token);
109
292
  }
110
293
  isChildOf(child, parent) {
111
- if (this.hasComponent(parent)) {
112
- const entry = this.#components.get(parent.uuid);
113
- return entry.children.includes(child.uuid);
114
- }
115
- return false;
294
+ const parentEntry = this.#entryOf(parent);
295
+ return parentEntry !== void 0 && this.#entryOf(child) !== void 0 && parentEntry.children.has(child.uuid);
116
296
  }
117
297
  addToChildren(parent, child) {
118
- const entry = this.#components.get(parent.uuid);
298
+ const entry = this.#entryOf(parent);
119
299
  if (entry) {
300
+ const childEntry = this.#entryOf(child);
301
+ if (childEntry === void 0) return;
120
302
  this.#appendToOrdered(child, entry.children);
121
- this.#components.get(child.uuid)?.changes.setParent(parent.uuid);
122
- removeFrom(this.#rootComponents, child.uuid);
303
+ childEntry.changes.setParent(parent.uuid);
304
+ this.#rootComponents.delete(child.uuid);
123
305
  this.#viewInstances = void 0;
124
306
  } else {
125
- throw new Error(`the view component ${parent.uuid} cannot have a child added to it because the component do not exist!`);
307
+ throw new Error(`the view component ${parent.uuid} cannot have a child added to it because this context does not hold it`);
126
308
  }
127
309
  }
310
+ /**
311
+ * Destroy a component and all its descendants without writing anything to a change trail.
312
+ * Each of them is detached from this context and reports {@link ViewComponent#isDestroyed}.
313
+ *
314
+ * @see {@link ComponentContext.clear}
315
+ */
128
316
  removeSubTree(uuid) {
317
+ this.#removeSubTree(uuid, /* @__PURE__ */ new Set());
318
+ }
319
+ #removeSubTree(uuid, visited) {
320
+ if (visited.has(uuid)) return;
321
+ visited.add(uuid);
129
322
  const entry = this.#components.get(uuid);
130
323
  if (entry) {
131
- entry.children.slice(0).forEach((childUuid) => this.removeSubTree(childUuid));
132
- this.destroyComponent(entry.component);
324
+ for (const childUuid of [...entry.children.uuids]) {
325
+ this.#removeSubTree(childUuid, visited);
326
+ }
133
327
  this.#deleteComponent(uuid);
328
+ this.destroyComponent(entry.component);
134
329
  }
135
330
  }
331
+ /**
332
+ * @returns `true` if the value differs from the last value written to a change trail
333
+ */
136
334
  setProperty(component, propKey, value, isEqual) {
137
- const vi = this.#components.get(component.uuid);
335
+ const vi = this.#entryOf(component);
138
336
  if (vi != null) {
139
337
  if (isEqual != null) {
140
338
  vi.propIsEqual ??= /* @__PURE__ */ new Map();
@@ -146,19 +344,89 @@ class ComponentContext {
146
344
  }
147
345
  return false;
148
346
  }
347
+ /**
348
+ * Mark `propKey` on `component` as set without giving it a value: the change trail carries
349
+ * an entry that names only the key, and the entity behind it reads the property as
350
+ * `undefined` with the key in place. {@link ComponentContext.setProperty} with `undefined`
351
+ * is the other thing — a removal.
352
+ *
353
+ * An equality rule registered for the key is forgotten, the way it is for a
354
+ * {@link ComponentContext.setProperty} that comes without one: there is no value here for a
355
+ * rule to compare.
356
+ *
357
+ * @returns `true` when this differs from the last value written to a change trail, and
358
+ * `false` for an instance that does not own its entry
359
+ */
360
+ setPropertyWithoutValue(component, propKey) {
361
+ const vi = this.#entryOf(component);
362
+ if (vi != null) {
363
+ vi.propIsEqual?.delete(propKey);
364
+ return vi.changes.setPropertyWithoutValue(propKey);
365
+ }
366
+ return false;
367
+ }
149
368
  removeProperty(component, propKey) {
150
- this.#components.get(component.uuid)?.changes.removeProperty(propKey);
369
+ this.#entryOf(component)?.changes.removeProperty(propKey);
370
+ }
371
+ /**
372
+ * Hand the properties of `component` over to the context it has just joined.
373
+ *
374
+ * Called by the {@link ViewComponent#context} setter, and by nothing else. It reads the state
375
+ * this context still holds for the component: leaving a context destroys it, but the entry
376
+ * survives until the next {@link ComponentContext.buildChangeTrails}, so the properties are
377
+ * still there to be read once the join has happened.
378
+ *
379
+ * The equality function travels with each property, because it is a rule about the key and not
380
+ * about the value — the same key means the same notion of "unchanged" wherever it lives. It does
381
+ * not get to decide about the arrival, though: the target holds nothing for the key yet, so it
382
+ * would be asked whether the value equals `undefined`, and a function that says yes would drop
383
+ * the property instead of carrying it over. The value is therefore written first and the rule
384
+ * registered after it — the other way round, the write would delete the entry it just made,
385
+ * because {@link ComponentContext.setProperty} keeps only what its caller passes.
386
+ *
387
+ * What the registered rule is for: {@link ComponentContext.reCreateChanges} is its one reader,
388
+ * and it compares with it when it rebuilds a component from the memory. A later
389
+ * {@link ComponentContext.setProperty} does not consult it — it uses its own argument, or none.
390
+ *
391
+ * A property that is set without a value goes over as one: handing it on as the value
392
+ * `undefined` would have the target read it as a removal, and the key would not arrive.
393
+ *
394
+ * Built like {@link ComponentChanges.transferEventsTo}, with one difference: the properties are
395
+ * copied, not moved. What stays behind goes down with the entity in this context.
396
+ */
397
+ transferPropertiesTo(component, target) {
398
+ const vi = this.#entryOf(component);
399
+ if (vi === void 0) return;
400
+ for (const [key, value] of vi.changes.getProperties()) {
401
+ if (value === PropertyWithoutValue) {
402
+ target.setPropertyWithoutValue(component, key);
403
+ } else {
404
+ target.setProperty(component, key, value);
405
+ }
406
+ const isEqual = vi.propIsEqual?.get(key);
407
+ if (isEqual != null) {
408
+ target.#registerPropIsEqual(component, key, isEqual);
409
+ }
410
+ }
411
+ }
412
+ #registerPropIsEqual(component, propKey, isEqual) {
413
+ const vi = this.#entryOf(component);
414
+ if (vi === void 0) return;
415
+ vi.propIsEqual ??= /* @__PURE__ */ new Map();
416
+ vi.propIsEqual.set(propKey, isEqual);
151
417
  }
152
418
  changeOrder(component) {
153
- if (component.parent) {
154
- const parentEntry = this.#components.get(component.parent.uuid);
155
- removeFrom(parentEntry.children, component.uuid);
419
+ const entry = this.#entryOf(component);
420
+ if (entry === void 0) return;
421
+ const parentEntry = component.parent ? this.#entryOf(component.parent) : void 0;
422
+ if (parentEntry !== void 0) {
423
+ parentEntry.children.delete(component.uuid);
156
424
  this.#appendToOrdered(component, parentEntry.children);
157
- } else {
158
- removeFrom(this.#rootComponents, component.uuid);
425
+ } else if (component.parent == null) {
426
+ this.#rootComponents.delete(component.uuid);
159
427
  this.#appendToOrdered(component, this.#rootComponents);
160
428
  }
161
- this.#components.get(component.uuid)?.changes.changeOrder(component.order);
429
+ entry.changes.changeOrder(component.order);
162
430
  this.#viewInstances = void 0;
163
431
  }
164
432
  /**
@@ -171,7 +439,7 @@ class ComponentContext {
171
439
  * Dispatch an event to the shadow objects linked to the view component
172
440
  */
173
441
  dispatchShadowObjectsEvent(component, type, data, transferables) {
174
- this.#components.get(component.uuid)?.changes.createEvent(type, data, transferables);
442
+ this.#entryOf(component)?.changes.createEvent(type, data, transferables);
175
443
  }
176
444
  /**
177
445
  * Dispatch an event to all view components
@@ -188,56 +456,258 @@ class ComponentContext {
188
456
  this.#components.get(uuid)?.component.dispatchEvent(type, data, traverseChildren);
189
457
  }
190
458
  /**
191
- * Inform all root components that they should re-request their parents
459
+ * Inform all root components that they should re-request their parents.
460
+ *
461
+ * Every root is asked, because there is nothing here to ask about: the message carries no
462
+ * sender, so this side cannot narrow the set down at all. The receiver is the one that can:
463
+ * it re-asks the element tree, and whoever answers first wins.
464
+ *
465
+ * Delivery is immediate: the round is over by the time the call returns. Two paths inside the
466
+ * library end here — {@link ComponentContext.dispatchReRequestParentSiblings} for a component
467
+ * without a parent, whose candidate set is the roots, and the delivery of a collected round for
468
+ * that same set. What does not come this way is an entity that has just arrived: it hands its
469
+ * round to the collector below. The method is public because running a round at a moment of
470
+ * one's own choosing is what it is for.
192
471
  */
193
472
  dispatchReRequestParentRoots() {
194
- for (const uuid of this.#rootComponents) {
473
+ for (const uuid of [...this.#rootComponents.uuids]) {
195
474
  this.dispatchMessage(uuid, ComponentContext.ReRequestParentRoots);
196
475
  }
197
476
  }
477
+ /**
478
+ * Inform the children of `component` that they should re-request their parents.
479
+ *
480
+ * The receivers are the components hanging on `component` right now, and every one of them has
481
+ * to let go: the message carries no sender and needs no filter, because there is nothing here to
482
+ * decide. {@link ComponentContext.ReRequestParentRoots} is the signal for it — it means "drop
483
+ * your parent and ask again", which is what the roots happen to be asked for most often, not
484
+ * what it says.
485
+ */
486
+ dispatchReRequestParentChildren(component) {
487
+ for (const child of this.getChildren(component)) {
488
+ this.dispatchMessage(child.uuid, ComponentContext.ReRequestParentRoots);
489
+ }
490
+ }
491
+ /**
492
+ * Inform the siblings of `component` that they should re-request their parents.
493
+ *
494
+ * This narrows the candidate set for one specific question: which components could have bound
495
+ * to an ancestor further away than they should have, because `component` was not yet answering
496
+ * when they asked? Such a component sits below `component` in the element tree and shares its
497
+ * parent — any entity in between would have answered first. As long as `component` has no
498
+ * parent of its own, the roots are that same set.
499
+ *
500
+ * The signal alone does not decide anything: the receiver re-asks, and whoever answers first
501
+ * wins. Which components exist as candidates is what this method knows; whether one of them
502
+ * really sits below `component` is settled on the receiving side, which is where the element
503
+ * tree is visible — `data` is what carries the sender's identity there.
504
+ *
505
+ * The two signals differ in the order they work in, and that decides more than it looks like:
506
+ * {@link ComponentContext.ReRequestParentRoots} releases the parent before asking, so a
507
+ * component that gets the same answer back re-joins its parent's children at the end;
508
+ * {@link ComponentContext.ReRequestParent} asks first and releases only where nobody answers,
509
+ * which leaves every component that is already bound where it stands, order included.
510
+ *
511
+ * Delivery is immediate, and nothing inside the library calls it: like
512
+ * {@link ComponentContext.dispatchReRequestParentRoots} it is public so that a round can be run
513
+ * at a moment of one's own choosing.
514
+ */
515
+ dispatchReRequestParentSiblings(component, data = void 0) {
516
+ const parent = component.parent;
517
+ if (parent == null) {
518
+ this.dispatchReRequestParentRoots();
519
+ return;
520
+ }
521
+ for (const child of this.getChildren(parent)) {
522
+ if (child !== component) {
523
+ this.dispatchMessage(child.uuid, ComponentContext.ReRequestParent, data);
524
+ }
525
+ }
526
+ }
527
+ /**
528
+ * The peers waiting for a round: sender by sender in arrival order, each with the data the
529
+ * round it asks for would carry.
530
+ */
531
+ #peerReRequests = new MicrotaskCollector((pending) => this.#deliverPeerReRequests(pending));
532
+ /**
533
+ * Take a peer re-request from `sender` — an entity that has just become one and may be the new
534
+ * closest ancestor for entities that bound while it was not yet answering.
535
+ *
536
+ * Everything that arrives in the same task shares one round. Alone, each arrival is a broadcast
537
+ * over the whole candidate set: the root channel carries no sender and has to ask every root,
538
+ * the sibling channel asks every child of one parent. n entities coming up together then cost
539
+ * n(n+1)/2 messages, and each message is a full ancestor request through the DOM — for 600 roots
540
+ * in one namespace that is 180 300 messages and some 257 ms on top of a build that takes 42 ms
541
+ * with the channel switched off. One round per task costs 600 messages and 2 ms on top. The
542
+ * point where a round per arrival costs more than a frame lies at about 145 roots in one
543
+ * namespace; one round per task stays clear of it over the whole measured range, up to 600.
544
+ *
545
+ * Numbers measured 2026-08-22 in Chromium via Playwright 1.62.1 — a snapshot, not a guarantee;
546
+ * the size series is in `packages/shadow-objects/docs/guides.md`, under "How Many Entities Fit
547
+ * in One Namespace".
548
+ *
549
+ * @param data travels with the round as long as `sender` is the only one in it — see
550
+ * {@link ComponentContext.dispatchReRequestParentSiblings} for what a receiver does with it.
551
+ *
552
+ * @internal
553
+ */
554
+ collectPeerReRequest(sender, data = void 0) {
555
+ this.#peerReRequests.add(sender, data);
556
+ }
557
+ /** Run the collected rounds, one per candidate set. */
558
+ #deliverPeerReRequests(pending) {
559
+ const rounds = /* @__PURE__ */ new Map();
560
+ for (const [sender, data] of pending) {
561
+ if (!this.#componentInstances.has(sender)) continue;
562
+ const key = sender.parent ?? null;
563
+ const round = rounds.get(key);
564
+ if (round === void 0) {
565
+ rounds.set(key, { data });
566
+ } else {
567
+ round.data = void 0;
568
+ }
569
+ }
570
+ for (const [parent, round] of rounds) {
571
+ runGuarded(
572
+ this.#logger,
573
+ () => {
574
+ if (parent == null) {
575
+ this.dispatchReRequestParentRoots();
576
+ } else {
577
+ for (const child of this.getChildren(parent)) {
578
+ this.dispatchMessage(child.uuid, ComponentContext.ReRequestParent, round.data);
579
+ }
580
+ }
581
+ },
582
+ "a peer re-request round could not be delivered:",
583
+ parent?.uuid ?? "roots"
584
+ );
585
+ }
586
+ }
198
587
  /**
199
588
  * Create the component change trails at this point in time.
200
589
  * The next call will only return the differences from the previous call.
201
590
  *
591
+ * @param commit whether the trail counts as applied the moment it is built. Pass `false` when
592
+ * the trail still has to travel to a Shadow Environment that may refuse it, and settle it
593
+ * afterwards with {@link ComponentContext.commitChangeTrail}.
594
+ *
595
+ * @see {@link ComponentContext.commitChangeTrail}
202
596
  * @see {@link ComponentContext.reCreateChanges}
203
597
  */
204
- buildChangeTrails(clearChanges = true) {
205
- const trails = [];
206
- if (!this.hasComponents()) return trails;
207
- const pathOfChanges = this.#buildPathOfChanges();
208
- for (const changes of pathOfChanges) {
209
- changes.buildChangeTrail(trails, ChangeTrailPhase.StructuralChanges);
210
- }
211
- for (const changes of pathOfChanges) {
212
- changes.buildChangeTrail(trails, ChangeTrailPhase.ContentUpdates);
213
- }
214
- for (const changes of pathOfChanges) {
215
- changes.buildChangeTrail(trails, ChangeTrailPhase.Removal);
216
- if (changes.isDestroyed || changes.isNew && !changes.isCreated) {
217
- this.#deleteComponent(changes.uuid);
598
+ buildChangeTrails(commit = true) {
599
+ this.#peerReRequests.flush();
600
+ if (this.#uncommittedTrail != null) {
601
+ this.#logger.debug("committing an open change trail because a second one is being built", this.#uncommittedTrail.entries);
602
+ this.commitChangeTrail(this.#uncommittedTrail.entries.length);
603
+ }
604
+ const entries = [];
605
+ const owners = [];
606
+ const participants = this.hasComponents() ? this.#buildPathOfChanges() : [];
607
+ const build = (changes, phase) => {
608
+ const before = entries.length;
609
+ changes.buildChangeTrail(entries, phase);
610
+ for (let i = before; i < entries.length; i++) {
611
+ owners[i] = changes;
218
612
  }
219
- if (clearChanges) changes.clear();
613
+ };
614
+ for (const changes of participants) build(changes, ChangeTrailPhase.StructuralChanges);
615
+ for (const changes of participants) build(changes, ChangeTrailPhase.ContentUpdates);
616
+ for (const changes of participants) build(changes, ChangeTrailPhase.Removal);
617
+ this.#uncommittedTrail = { entries, owners, retiring: this.#retiringComponents(participants) };
618
+ if (commit) this.commitChangeTrail(entries.length);
619
+ return entries;
620
+ }
621
+ /**
622
+ * Fold the first `appliedCount` entries of the change trail this context built last into the
623
+ * state the next trail is diffed against, and write them to the Component Memory. Every entry
624
+ * behind that line stays pending and goes out again with the next trail.
625
+ *
626
+ * A component is retired -- its entry dropped -- only where the build read it as spent and every
627
+ * entry it contributed is settled. One component can hold a creation and an event in the same
628
+ * trail, and a creation sent twice is refused by the Shadow Environment, which already holds an
629
+ * entity behind that uuid.
630
+ *
631
+ * @param appliedCount how many entries the Shadow Environment applied, counted from the front.
632
+ * Clamped to the length of the trail.
633
+ * @param changeTrail the trail this call settles. Given, the call is ignored unless it is the
634
+ * very trail this context built last -- a cycle that lost its trail to a later build must not
635
+ * draw a line through that later trail.
636
+ */
637
+ commitChangeTrail(appliedCount, changeTrail) {
638
+ const uncommitted = this.#uncommittedTrail;
639
+ if (uncommitted == null) return;
640
+ if (changeTrail != null && changeTrail !== uncommitted.entries) return;
641
+ const { entries, owners, retiring } = uncommitted;
642
+ const count = Math.min(Math.max(appliedCount, 0), entries.length);
643
+ this.#uncommittedTrail = void 0;
644
+ const stillPending = new Set(owners.slice(count));
645
+ for (let i = 0; i < count; i++) {
646
+ owners[i].commitChange(entries[i]);
220
647
  }
221
- this.#componentMemory.write(trails);
222
- return trails;
648
+ this.#componentMemory.write(count === entries.length ? entries : entries.slice(0, count));
649
+ this.#retireComponents(retiring, stillPending);
650
+ }
651
+ /**
652
+ * Whether the Component Memory holds a state for `uuid` — the state
653
+ * {@link ComponentContext.reCreateChanges} would rebuild that component from.
654
+ */
655
+ hasComponentState(uuid) {
656
+ return this.#componentMemory.hasComponentState(uuid);
657
+ }
658
+ /**
659
+ * The state the Component Memory holds for `uuid`, or `undefined` where it holds none.
660
+ *
661
+ * A snapshot, not the record: the property list of the record is rewritten in place as
662
+ * trails come in, so a caller holding on to it would be reading a moving value — or
663
+ * writing one. What it is a snapshot of is the memory at the moment of the call, and the
664
+ * memory moves under several hands: {@link ComponentContext.commitChangeTrail} folds a
665
+ * settled trail into it, {@link ComponentContext.buildChangeTrails} does the same for the
666
+ * trail it builds unless it is told not to commit, {@link ComponentContext.reCreateChanges}
667
+ * writes the trail it builds and empties the memory afterwards, and
668
+ * {@link ComponentContext.clear} empties it outright. A snapshot taken before any of those
669
+ * describes a state that is over; take it again to see the current one.
670
+ *
671
+ * This is the window a test or a diagnosis needs, and the one that leaves the context as it
672
+ * found it — reading the same thing out of {@link ComponentContext.reCreateChanges} costs a
673
+ * rebuild of every component in the namespace.
674
+ */
675
+ getComponentState(uuid) {
676
+ const state = this.#componentMemory.getComponentState(uuid);
677
+ if (state === void 0) return void 0;
678
+ return {
679
+ ...state,
680
+ properties: state.properties?.map((entry) => entry.length === 1 ? [entry[0]] : [entry[0], entry[1]])
681
+ };
223
682
  }
224
683
  /**
225
684
  * Resets the internal component change states so that all view components are regenerated with the next change trail.
226
685
  * The outstanding events are taken over.
227
686
  *
687
+ * The trail this produces belongs to a Shadow Environment that holds none of these uuids -- a
688
+ * fresh proxy. One whose Kernel still holds them refuses the first re-created creation it reads,
689
+ * and every cycle that follows, because a uuid names one entity at a time.
690
+ *
228
691
  * @see {@link ComponentContext.buildChangeTrails}
229
692
  */
230
693
  reCreateChanges() {
231
694
  if (this.#componentMemory.isEmpty()) return;
232
- this.buildChangeTrails(false);
695
+ const trails = this.buildChangeTrails(false);
696
+ this.#componentMemory.write(trails);
697
+ this.#retireBuiltTrail();
233
698
  for (const [uuid, cMem] of this.#componentMemory) {
234
699
  const c = this.#components.get(uuid);
235
700
  if (c) {
236
701
  const changes = new ComponentChanges(uuid);
237
702
  changes.create(cMem.token, cMem.parentUuid, cMem.order, cMem.autoDestructionOnParentRemoval);
238
703
  if (cMem.properties) {
239
- for (const [key, value] of cMem.properties) {
240
- changes.changeProperty(key, value, c.propIsEqual?.get(key));
704
+ for (const entry of cMem.properties) {
705
+ const key = entry[0];
706
+ if (entry.length === 1) {
707
+ changes.setPropertyWithoutValue(key);
708
+ } else {
709
+ changes.changeProperty(key, entry[1], c.propIsEqual?.get(key));
710
+ }
241
711
  }
242
712
  }
243
713
  c.changes.transferEventsTo(changes);
@@ -248,86 +718,149 @@ class ComponentContext {
248
718
  this.#componentMemory.clear();
249
719
  this.broadcastEvent(ContextLost);
250
720
  }
721
+ /**
722
+ * Remove all components without writing anything to a change trail. Every
723
+ * {@link ViewComponent} this context holds is destroyed, so each of them reports
724
+ * {@link ViewComponent#isDestroyed} and holds no context afterwards. The context itself stays
725
+ * registered under its namespace and can be used again — assigning it to a component takes
726
+ * that component back in under the same uuid.
727
+ *
728
+ * @see {@link ComponentContext.dispose} for the final teardown
729
+ */
251
730
  clear() {
252
731
  this.#viewInstances = void 0;
253
732
  this.#componentMemory.clear();
254
- this.#rootComponents.slice(0).forEach((uuid) => this.removeSubTree(uuid));
255
- if (this.#rootComponents.length !== 0) {
733
+ this.#uncommittedTrail = void 0;
734
+ for (const component of Array.from(this.#componentInstances)) {
735
+ component.destroy();
736
+ }
737
+ this.#componentInstances.clear();
738
+ for (const uuid of [...this.#rootComponents.uuids]) {
739
+ this.removeSubTree(uuid);
740
+ }
741
+ if (this.#rootComponents.size !== 0) {
256
742
  throw new Error("component-context panic: #rootComponents is not empty!");
257
743
  }
258
744
  if (this.#components.size !== 0) {
259
745
  throw new Error("component-context panic: #components is not empty!");
260
746
  }
261
747
  }
262
- #deleteComponent(uuid) {
263
- if (this.#components.has(uuid)) {
264
- this.#components.delete(uuid);
265
- removeFrom(this.#rootComponents, uuid);
266
- this.#viewInstances = void 0;
748
+ /**
749
+ * Tear the context down for good: every {@link ViewComponent} it holds is destroyed, the
750
+ * component memory is dropped, and the namespace is released so that
751
+ * {@link ComponentContext.get} creates a fresh context for it.
752
+ *
753
+ * Unlike {@link ComponentContext.clear} this is final. The context holds no components,
754
+ * produces empty change trails, and rejects any component that tries to join it. Calling
755
+ * it more than once is a no-op.
756
+ *
757
+ * A `ShadowEnv` bound to this context keeps its reference; destroy the environment
758
+ * first if you want the namespace released on both sides.
759
+ */
760
+ dispose() {
761
+ if (this.#isDisposed) return;
762
+ this.clear();
763
+ this.#isDisposed = true;
764
+ const ctxMap = ComponentContext.getContextsMap();
765
+ if (this.ns != null && ctxMap.get(this.ns) === this) {
766
+ ctxMap.delete(this.ns);
267
767
  }
268
768
  }
769
+ /**
770
+ * Drop the component entry behind `uuid`, and with it the uuid's place in every children list.
771
+ *
772
+ * @param expectedChanges the bookkeeping that asked for the deletion. A round trip lies between
773
+ * the build of a change trail and its commit, and an entry that a different bookkeeping stands
774
+ * behind by then belongs to a component that joined in the meantime.
775
+ */
776
+ #deleteComponent(uuid, expectedChanges) {
777
+ const entry = this.#components.get(uuid);
778
+ if (entry === void 0) return;
779
+ if (expectedChanges !== void 0 && entry.changes !== expectedChanges) return;
780
+ const parentUuid = entry.component.parent?.uuid;
781
+ if (parentUuid !== void 0) {
782
+ const parentEntry = this.#components.get(parentUuid);
783
+ if (parentEntry !== void 0) {
784
+ parentEntry.children.delete(uuid);
785
+ }
786
+ }
787
+ this.#components.delete(uuid);
788
+ this.#rootComponents.delete(uuid);
789
+ this.#viewInstances = void 0;
790
+ }
269
791
  #buildPathOfChanges() {
270
792
  return this.#traverseLevelOrderBFS().filter((vi) => vi.changes.hasChanges()).map((vi) => vi.changes);
271
793
  }
794
+ /**
795
+ * Insert a component into an ordered list of uuids.
796
+ *
797
+ * The list is kept sorted by ascending {@link ViewComponent#order}. Components sharing
798
+ * the same order value keep their insertion order, so the new component is placed after
799
+ * all components with an equal order.
800
+ *
801
+ * Uuids without a matching view instance are skipped instead of dereferenced, so a
802
+ * partially torn down list can never turn a reordering into an exception.
803
+ *
804
+ * Two questions decide what one insertion costs, and a list that carries nothing but its uuids
805
+ * has to be walked for both: once to see whether the uuid already stands in it, once to find the
806
+ * place it sorts into. Filling such a list with n components costs n(n+1)/2 steps. `OrderedUuids`
807
+ * answers the membership in one step, and the shortcut below answers the place in one more for
808
+ * every component that sorts at or after the last member; a component that sorts ahead of it
809
+ * still walks the list. Building n roots one after another, all on the default order, that is
810
+ * 1.01 ms for 600 and 2.03 ms for 1200, against 4.44 ms and 16.04 ms for the walked list.
811
+ * Numbers measured 2026-08-31 on node v25.9.0, on the context alone with no DOM around
812
+ * it -- a snapshot, not a guarantee. The parent-resolution series in
813
+ * `packages/shadow-objects/docs/guides.md` was measured in a browser and covers a different question.
814
+ */
272
815
  #appendToOrdered(component, childUuids) {
273
- if (childUuids.length === 0) {
274
- childUuids.push(component.uuid);
275
- return;
276
- }
277
- if (childUuids.includes(component.uuid)) {
816
+ if (childUuids.has(component.uuid)) {
278
817
  return;
279
818
  }
280
- const len = childUuids.length;
281
- const childComponents = new Array(len);
282
- childComponents[0] = this.#components.get(childUuids[0]).component;
283
- if (component.order < childComponents[0].order) {
284
- childUuids.unshift(component.uuid);
285
- return;
286
- }
287
- if (len === 1) {
819
+ const { order } = component;
820
+ const last = childUuids.size > 0 ? this.#components.get(childUuids.uuids[childUuids.size - 1])?.component : void 0;
821
+ if (last !== void 0 && order >= last.order) {
288
822
  childUuids.push(component.uuid);
289
823
  return;
290
824
  }
291
- const lastIdx = len - 1;
292
- childComponents[lastIdx] = this.#components.get(childUuids[lastIdx]).component;
293
- if (component.order >= childComponents[lastIdx].order) {
294
- childUuids.push(component.uuid);
295
- return;
296
- }
297
- if (len === 2) {
298
- childUuids.splice(1, 0, component.uuid);
299
- return;
300
- }
301
- for (let i = lastIdx - 1; i >= 1; i--) {
302
- childComponents[i] = this.#components.get(childUuids[i]).component;
303
- if (component.order >= childComponents[i].order) {
304
- childUuids.splice(i + 1, 0, component.uuid);
825
+ for (let i = 0; i < childUuids.size; i++) {
826
+ const other = this.#components.get(childUuids.uuids[i])?.component;
827
+ if (other !== void 0 && order < other.order) {
828
+ childUuids.insertAt(i, component.uuid);
305
829
  return;
306
830
  }
307
831
  }
832
+ childUuids.push(component.uuid);
308
833
  }
309
834
  #viewInstances;
310
835
  #traverseLevelOrderBFS() {
311
836
  if (this.#viewInstances) return this.#viewInstances;
312
837
  const lvl = /* @__PURE__ */ new Map();
838
+ const visited = /* @__PURE__ */ new Set();
313
839
  const traverse = (uuid, depth) => {
840
+ if (visited.has(uuid)) return;
841
+ visited.add(uuid);
314
842
  const viewInstance = this.#components.get(uuid);
315
843
  if (viewInstance == null) return;
316
- if (lvl.has(depth)) {
317
- lvl.get(depth).push(viewInstance);
844
+ const atDepth = lvl.get(depth);
845
+ if (atDepth) {
846
+ atDepth.push(viewInstance);
318
847
  } else {
319
848
  lvl.set(depth, [viewInstance]);
320
849
  }
321
- for (const childUuid of viewInstance.children) {
850
+ for (const childUuid of viewInstance.children.uuids) {
322
851
  traverse(childUuid, depth + 1);
323
852
  }
324
853
  };
325
- this.#rootComponents.forEach((uuid) => traverse(uuid, 0));
854
+ for (const uuid of this.#rootComponents.uuids) {
855
+ traverse(uuid, 0);
856
+ }
326
857
  this.#viewInstances = Array.from(lvl.entries()).sort((a, b) => a[0] - b[0]).flatMap(([, vi]) => vi);
327
858
  return this.#viewInstances;
328
859
  }
329
860
  }
330
861
  export {
331
- ComponentContext
862
+ ComponentContext,
863
+ ComponentContextDisposedError,
864
+ ComponentUuidInUseError
332
865
  };
333
866
  //# sourceMappingURL=ComponentContext.js.map