@spearwolf/shadow-objects 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/CHANGELOG.md +320 -61
  2. package/README.md +57 -13
  3. package/bundle.js +5 -54
  4. package/package.json +7 -3
  5. package/src/ChangeTrailRefusedError.d.ts +27 -0
  6. package/src/ChangeTrailRefusedError.d.ts.map +1 -0
  7. package/src/ChangeTrailRefusedError.js +12 -0
  8. package/src/ChangeTrailRefusedError.js.map +7 -0
  9. package/src/EntityUuidInUseError.d.ts +19 -0
  10. package/src/EntityUuidInUseError.d.ts.map +1 -0
  11. package/src/EntityUuidInUseError.js +11 -0
  12. package/src/EntityUuidInUseError.js.map +7 -0
  13. package/src/WorkerTimeoutError.d.ts +25 -0
  14. package/src/WorkerTimeoutError.d.ts.map +1 -0
  15. package/src/WorkerTimeoutError.js +12 -0
  16. package/src/WorkerTimeoutError.js.map +7 -0
  17. package/src/bundle.d.ts +1 -1
  18. package/src/bundle.d.ts.map +1 -1
  19. package/src/bundle.js +1 -1
  20. package/src/bundle.js.map +2 -2
  21. package/src/constants.d.ts +9 -2
  22. package/src/constants.d.ts.map +1 -1
  23. package/src/constants.js +1 -1
  24. package/src/constants.js.map +2 -2
  25. package/src/create-worker.bundle.d.ts +1 -1
  26. package/src/create-worker.bundle.d.ts.map +1 -1
  27. package/src/create-worker.bundle.js.map +1 -1
  28. package/src/create-worker.d.ts +1 -1
  29. package/src/create-worker.d.ts.map +1 -1
  30. package/src/create-worker.js.map +1 -1
  31. package/src/elements/ShaeElement.d.ts +70 -2
  32. package/src/elements/ShaeElement.d.ts.map +1 -1
  33. package/src/elements/ShaeElement.js +129 -25
  34. package/src/elements/ShaeElement.js.map +3 -3
  35. package/src/elements/ShaeEntElement.d.ts +39 -1
  36. package/src/elements/ShaeEntElement.d.ts.map +1 -1
  37. package/src/elements/ShaeEntElement.js +397 -167
  38. package/src/elements/ShaeEntElement.js.map +2 -2
  39. package/src/elements/ShaeLifecycleElement.d.ts +71 -0
  40. package/src/elements/ShaeLifecycleElement.d.ts.map +1 -0
  41. package/src/elements/ShaeLifecycleElement.js +83 -0
  42. package/src/elements/ShaeLifecycleElement.js.map +7 -0
  43. package/src/elements/ShaePropElement.d.ts +52 -2
  44. package/src/elements/ShaePropElement.d.ts.map +1 -1
  45. package/src/elements/ShaePropElement.js +240 -219
  46. package/src/elements/ShaePropElement.js.map +2 -2
  47. package/src/elements/ShaeWorkerElement.d.ts +49 -5
  48. package/src/elements/ShaeWorkerElement.d.ts.map +1 -1
  49. package/src/elements/ShaeWorkerElement.js +204 -73
  50. package/src/elements/ShaeWorkerElement.js.map +2 -2
  51. package/src/elements/constants.d.ts +6 -0
  52. package/src/elements/constants.d.ts.map +1 -1
  53. package/src/elements/constants.js +12 -0
  54. package/src/elements/constants.js.map +2 -2
  55. package/src/elements/deferredTeardown.d.ts +24 -0
  56. package/src/elements/deferredTeardown.d.ts.map +1 -0
  57. package/src/elements/deferredTeardown.js +30 -0
  58. package/src/elements/deferredTeardown.js.map +7 -0
  59. package/src/elements/displayContentsRule.d.ts +25 -0
  60. package/src/elements/displayContentsRule.d.ts.map +1 -0
  61. package/src/elements/displayContentsRule.js +37 -0
  62. package/src/elements/displayContentsRule.js.map +7 -0
  63. package/src/elements/events.d.ts +10 -7
  64. package/src/elements/events.d.ts.map +1 -1
  65. package/src/elements/forwardCustomEvents.d.ts +19 -0
  66. package/src/elements/forwardCustomEvents.d.ts.map +1 -0
  67. package/src/elements/forwardCustomEvents.js +47 -0
  68. package/src/elements/forwardCustomEvents.js.map +7 -0
  69. package/src/elements/hostedSlots.d.ts +36 -0
  70. package/src/elements/hostedSlots.d.ts.map +1 -0
  71. package/src/elements/hostedSlots.js +118 -0
  72. package/src/elements/hostedSlots.js.map +7 -0
  73. package/src/elements/parentRemoval.d.ts +18 -0
  74. package/src/elements/parentRemoval.d.ts.map +1 -0
  75. package/src/elements/parentRemoval.js +60 -0
  76. package/src/elements/parentRemoval.js.map +7 -0
  77. package/src/elements/propValueConverters.d.ts +3 -0
  78. package/src/elements/propValueConverters.d.ts.map +1 -0
  79. package/src/elements/propValueConverters.js +54 -0
  80. package/src/elements/propValueConverters.js.map +7 -0
  81. package/src/elements/requestEntAncestor.d.ts +32 -0
  82. package/src/elements/requestEntAncestor.d.ts.map +1 -0
  83. package/src/elements/requestEntAncestor.js +16 -0
  84. package/src/elements/requestEntAncestor.js.map +7 -0
  85. package/src/elements.js.map +1 -1
  86. package/src/in-the-dark/Entity.d.ts +113 -3
  87. package/src/in-the-dark/Entity.d.ts.map +1 -1
  88. package/src/in-the-dark/Entity.js +292 -63
  89. package/src/in-the-dark/Entity.js.map +3 -3
  90. package/src/in-the-dark/Kernel.d.ts +76 -17
  91. package/src/in-the-dark/Kernel.d.ts.map +1 -1
  92. package/src/in-the-dark/Kernel.js +410 -378
  93. package/src/in-the-dark/Kernel.js.map +3 -3
  94. package/src/in-the-dark/Registry.d.ts.map +1 -1
  95. package/src/in-the-dark/Registry.js +83 -14
  96. package/src/in-the-dark/Registry.js.map +2 -2
  97. package/src/in-the-dark/ShadowObject.d.ts.map +1 -1
  98. package/src/in-the-dark/ShadowObject.js +1 -0
  99. package/src/in-the-dark/ShadowObject.js.map +2 -2
  100. package/src/in-the-dark/ShadowObjectCreationScope.d.ts +126 -0
  101. package/src/in-the-dark/ShadowObjectCreationScope.d.ts.map +1 -0
  102. package/src/in-the-dark/ShadowObjectCreationScope.js +596 -0
  103. package/src/in-the-dark/ShadowObjectCreationScope.js.map +7 -0
  104. package/src/in-the-dark/SignalsPath.d.ts.map +1 -1
  105. package/src/in-the-dark/SignalsPath.js +2 -2
  106. package/src/in-the-dark/SignalsPath.js.map +2 -2
  107. package/src/in-the-dark/events.js.map +1 -1
  108. package/src/in-the-dark/importModule.d.ts +7 -0
  109. package/src/in-the-dark/importModule.d.ts.map +1 -1
  110. package/src/in-the-dark/importModule.js +5 -2
  111. package/src/in-the-dark/importModule.js.map +2 -2
  112. package/src/index.d.ts +7 -1
  113. package/src/index.d.ts.map +1 -1
  114. package/src/index.js +5 -1
  115. package/src/index.js.map +2 -2
  116. package/src/shadow-objects.d.ts +2 -0
  117. package/src/shadow-objects.d.ts.map +1 -1
  118. package/src/shadow-objects.js +2 -0
  119. package/src/shadow-objects.js.map +2 -2
  120. package/src/shadow-objects.worker.js +0 -1
  121. package/src/shadow-objects.worker.js.map +2 -2
  122. package/src/shae-ent.js.map +1 -1
  123. package/src/shae-prop.js +2 -2
  124. package/src/shae-prop.js.map +2 -2
  125. package/src/shae-worker.js.map +1 -1
  126. package/src/types.d.ts +35 -4
  127. package/src/types.d.ts.map +1 -1
  128. package/src/utils/ConsoleLogger.d.ts +45 -10
  129. package/src/utils/ConsoleLogger.d.ts.map +1 -1
  130. package/src/utils/ConsoleLogger.js +51 -26
  131. package/src/utils/ConsoleLogger.js.map +2 -2
  132. package/src/utils/FrameLoop.d.ts +42 -3
  133. package/src/utils/FrameLoop.d.ts.map +1 -1
  134. package/src/utils/FrameLoop.js +71 -10
  135. package/src/utils/FrameLoop.js.map +2 -2
  136. package/src/utils/MicrotaskCollector.d.ts +45 -0
  137. package/src/utils/MicrotaskCollector.d.ts.map +1 -0
  138. package/src/utils/MicrotaskCollector.js +33 -0
  139. package/src/utils/MicrotaskCollector.js.map +7 -0
  140. package/src/utils/MicrotaskGate.d.ts +17 -0
  141. package/src/utils/MicrotaskGate.d.ts.map +1 -0
  142. package/src/utils/MicrotaskGate.js +20 -0
  143. package/src/utils/MicrotaskGate.js.map +7 -0
  144. package/src/utils/array-utils.js.map +1 -1
  145. package/src/utils/attr-utils.d.ts +6 -0
  146. package/src/utils/attr-utils.d.ts.map +1 -1
  147. package/src/utils/attr-utils.js +3 -1
  148. package/src/utils/attr-utils.js.map +2 -2
  149. package/src/utils/constants.js.map +1 -1
  150. package/src/utils/generateUUID.d.ts +11 -0
  151. package/src/utils/generateUUID.d.ts.map +1 -1
  152. package/src/utils/generateUUID.js +31 -266
  153. package/src/utils/generateUUID.js.map +2 -2
  154. package/src/utils/props-utils.d.ts +3 -2
  155. package/src/utils/props-utils.d.ts.map +1 -1
  156. package/src/utils/props-utils.js +13 -24
  157. package/src/utils/props-utils.js.map +2 -2
  158. package/src/utils/runGuarded.d.ts +27 -0
  159. package/src/utils/runGuarded.d.ts.map +1 -0
  160. package/src/utils/runGuarded.js +11 -0
  161. package/src/utils/runGuarded.js.map +7 -0
  162. package/src/utils/toMaybe.d.ts.map +1 -1
  163. package/src/utils/toMaybe.js.map +1 -1
  164. package/src/utils/toNamespace.d.ts.map +1 -1
  165. package/src/utils/toNamespace.js.map +1 -1
  166. package/src/utils/toUrlString.d.ts +12 -0
  167. package/src/utils/toUrlString.d.ts.map +1 -1
  168. package/src/utils/toUrlString.js.map +2 -2
  169. package/src/utils/waitForMessageOfType.d.ts +12 -1
  170. package/src/utils/waitForMessageOfType.d.ts.map +1 -1
  171. package/src/utils/waitForMessageOfType.js +16 -1
  172. package/src/utils/waitForMessageOfType.js.map +2 -2
  173. package/src/view/ComponentChanges.d.ts +88 -1
  174. package/src/view/ComponentChanges.d.ts.map +1 -1
  175. package/src/view/ComponentChanges.js +243 -73
  176. package/src/view/ComponentChanges.js.map +2 -2
  177. package/src/view/ComponentContext.d.ts +206 -16
  178. package/src/view/ComponentContext.d.ts.map +1 -1
  179. package/src/view/ComponentContext.js +548 -91
  180. package/src/view/ComponentContext.js.map +2 -2
  181. package/src/view/ComponentMemory.d.ts +3 -3
  182. package/src/view/ComponentMemory.d.ts.map +1 -1
  183. package/src/view/ComponentMemory.js.map +1 -1
  184. package/src/view/IShadowObjectEnvProxy.d.ts +29 -3
  185. package/src/view/IShadowObjectEnvProxy.d.ts.map +1 -1
  186. package/src/view/LocalShadowObjectEnv.d.ts +1 -1
  187. package/src/view/LocalShadowObjectEnv.d.ts.map +1 -1
  188. package/src/view/LocalShadowObjectEnv.js +8 -6
  189. package/src/view/LocalShadowObjectEnv.js.map +2 -2
  190. package/src/view/RemoteWorkerEnv.d.ts +89 -5
  191. package/src/view/RemoteWorkerEnv.d.ts.map +1 -1
  192. package/src/view/RemoteWorkerEnv.js +195 -77
  193. package/src/view/RemoteWorkerEnv.js.map +2 -2
  194. package/src/view/ShadowEnv.d.ts +49 -2
  195. package/src/view/ShadowEnv.d.ts.map +1 -1
  196. package/src/view/ShadowEnv.js +223 -60
  197. package/src/view/ShadowEnv.js.map +2 -2
  198. package/src/view/ViewComponent.d.ts +78 -14
  199. package/src/view/ViewComponent.d.ts.map +1 -1
  200. package/src/view/ViewComponent.js +90 -14
  201. package/src/view/ViewComponent.js.map +2 -2
  202. package/src/view/cloneChangeTrail.js.map +2 -2
  203. package/src/worker/MessageRouter.d.ts +14 -0
  204. package/src/worker/MessageRouter.d.ts.map +1 -1
  205. package/src/worker/MessageRouter.js +75 -15
  206. package/src/worker/MessageRouter.js.map +2 -2
  207. package/src/worker/WorkerRuntime.d.ts +22 -1
  208. package/src/worker/WorkerRuntime.d.ts.map +1 -1
  209. package/src/worker/WorkerRuntime.js +60 -8
  210. package/src/worker/WorkerRuntime.js.map +2 -2
@@ -0,0 +1,596 @@
1
+ import { emit, on, once, Priority } from "@spearwolf/eventize";
2
+ import {
3
+ createEffect,
4
+ createMemo,
5
+ createSignal,
6
+ destroySignal,
7
+ isSignal,
8
+ link
9
+ } from "@spearwolf/signalize";
10
+ import { runGuarded } from "../utils/runGuarded.js";
11
+ import { toMaybe } from "../utils/toMaybe.js";
12
+ import { onDestroy, onViewEvent } from "./events.js";
13
+ const inertSignal = () => {
14
+ const sig = createSignal(void 0);
15
+ destroySignal(sig);
16
+ return sig;
17
+ };
18
+ const inertEffect = () => {
19
+ const effect = createEffect(() => {
20
+ }, { autorun: false });
21
+ effect.destroy();
22
+ return effect;
23
+ };
24
+ const noSubscription = () => {
25
+ };
26
+ class ShadowObjectCreationScope {
27
+ #entity;
28
+ #logger;
29
+ #displayName;
30
+ // The kernel's list of member names whose deprecation report has already been made. Shared by
31
+ // every scope of one kernel, which is what makes the report fall once per kernel and name.
32
+ #shownDeprecations;
33
+ // The cleanup callbacks of the shadow-object itself, registered through `onDestroy()`.
34
+ #unsubscribePrimary = /* @__PURE__ */ new Set();
35
+ // The cleanup callbacks the creation API registered on its own behalf. A `Set` iterating in
36
+ // insertion order: which cleanup runs before which is observable behaviour.
37
+ #unsubscribeSecondary = /* @__PURE__ */ new Set();
38
+ // The releases of the provider feeds, one per context name this shadow-object provides. The
39
+ // entity hands each of them out when the provider is attached to it, and they fall after every
40
+ // other cleanup callback, so a write the teardown still makes -- the `undefined` of a
41
+ // `clearOnDestroy`, or one from an `onDestroy` callback -- reaches the entity-side signal instead
42
+ // of stopping at the provider. Where the entity lives on, that is what makes the write count: the
43
+ // entity hands it on to its own context signal one microtask later, and from there to its readers
44
+ // and to the entities below it. The order matters in the other direction as well, because a
45
+ // release also lets a provider that stays on the entity write its value once more, and that
46
+ // hand-over may only happen once the write of the leaving provider is through.
47
+ //
48
+ // Where the entity is destroyed, the two kinds of target part ways. The signal behind
49
+ // `provideContext()` takes the write and keeps it: the microtask that would hand it on falls
50
+ // after the entity has cleared and destroyed its context signals, and the children that outlive
51
+ // it were re-bound before the teardown even began. The signal behind `provideGlobalContext()`
52
+ // stays a member of the kernel-wide chain of its name until the entity's own destruction runs at
53
+ // `Priority.Min`, so the write does travel on to the inherited signals of other entities -- with
54
+ // no observable difference, because the entity takes its signal out of that chain in the same run,
55
+ // and what the chain resolves to without it is what those readers were going to see anyway: the
56
+ // contribution of the next entity that holds the name, or nothing where there is none. The
57
+ // ordering is kept for the paths on which it is observable.
58
+ #unsubscribeContextFeeds = /* @__PURE__ */ new Set();
59
+ #contextReaders = /* @__PURE__ */ new Map();
60
+ #contextReaderCompares = /* @__PURE__ */ new Map();
61
+ #contextParentReaders = /* @__PURE__ */ new Map();
62
+ #contextParentReaderCompares = /* @__PURE__ */ new Map();
63
+ #contextProviders = /* @__PURE__ */ new Map();
64
+ #contextRootProviders = /* @__PURE__ */ new Map();
65
+ // The provider signals that already have their `clearOnDestroy` write registered in
66
+ // `#unsubscribeSecondary`. Keyed by the signal itself rather than by name, because a
67
+ // `provideContext(name)` and a `provideGlobalContext(name)` call can share one name while landing
68
+ // in two different maps above -- a name-keyed set would conflate the two providers.
69
+ #clearOnDestroyRegistered = /* @__PURE__ */ new Set();
70
+ #propertyReaders = /* @__PURE__ */ new Map();
71
+ #propertyCompares = /* @__PURE__ */ new Map();
72
+ #shadowObject;
73
+ #releaseScope;
74
+ #forgetShadowObject;
75
+ #unsubscribeFromEntityDestroy;
76
+ #isTornDown = false;
77
+ // Armed at the *end* of `tearDown()`, not at its start, and the difference is the whole point:
78
+ // the teardown runs the shadow-object's own cleanup callbacks, and those are allowed the creation
79
+ // API -- a goodbye to the view, a last write. What they register there is still swept up by the
80
+ // steps behind them. What arrives after the teardown has returned is not, which is what this
81
+ // closes.
82
+ #isCreationApiClosed = false;
83
+ // The member names whose late call has already been reported, at most as many as the API has
84
+ // members. Lazy, because a scope that is used correctly never allocates it.
85
+ #lateCallsReported;
86
+ /**
87
+ * The name the kernel gave this scope, taken from the constructor it belongs to rather than from
88
+ * the shadow-object that came out of it. The two agree for most forms -- a class and a function
89
+ * that returns nothing both leave their own name on the instance's `constructor` -- but not for a
90
+ * function that returns an object literal: `new` hands that literal back, and it reports `Object`.
91
+ * Reports about the shadow-object are keyed by this name, so the kernel reads it here instead of
92
+ * asking the instance.
93
+ */
94
+ get displayName() {
95
+ return this.#displayName;
96
+ }
97
+ /**
98
+ * The four teardown handles, read without waiting on a garbage collector: a test can check that
99
+ * `tearDown()` let go of them by comparing this before and after, rather than by proving the
100
+ * absence of a reference through a `WeakRef`.
101
+ *
102
+ * @internal
103
+ */
104
+ get debugHandles() {
105
+ return {
106
+ shadowObject: this.#shadowObject,
107
+ releaseScope: this.#releaseScope,
108
+ forgetShadowObject: this.#forgetShadowObject,
109
+ unsubscribeFromEntityDestroy: this.#unsubscribeFromEntityDestroy
110
+ };
111
+ }
112
+ /**
113
+ * How many cleanup callbacks the scope is holding, one count per set, read without waiting on a
114
+ * garbage collector: a test can subscribe and unsubscribe n times and see that the scope let go
115
+ * of each handle as it went, rather than inferring it from what a collector happened to do.
116
+ *
117
+ * @internal
118
+ */
119
+ get debugCleanupCounts() {
120
+ return {
121
+ primary: this.#unsubscribePrimary.size,
122
+ secondary: this.#unsubscribeSecondary.size,
123
+ contextFeeds: this.#unsubscribeContextFeeds.size
124
+ };
125
+ }
126
+ constructor(entity, logger, displayName, shownDeprecations) {
127
+ this.#entity = entity;
128
+ this.#logger = logger;
129
+ this.#displayName = displayName;
130
+ this.#shownDeprecations = shownDeprecations;
131
+ }
132
+ /**
133
+ * The object that goes into `new construct(…)`.
134
+ *
135
+ * Every member is a bound function, because a shadow-object constructor is expected to destructure the
136
+ * API in its parameter list — a member that took its receiver from the call site would lose it there.
137
+ */
138
+ createAPI() {
139
+ return {
140
+ entity: this.#entity,
141
+ provideContext: this.provideContext.bind(this),
142
+ provideGlobalContext: this.provideGlobalContext.bind(this),
143
+ useContext: this.useContext.bind(this),
144
+ useParentContext: this.useParentContext.bind(this),
145
+ dispatchMessageToView: this.dispatchMessageToView.bind(this),
146
+ useProperty: this.useProperty.bind(this),
147
+ useProperties: this.useProperties.bind(this),
148
+ createResource: this.createResource.bind(this),
149
+ createEffect: this.createEffect.bind(this),
150
+ createSignal: this.createSignal.bind(this),
151
+ createMemo: this.createMemo.bind(this),
152
+ on: this.on.bind(this),
153
+ once: this.once.bind(this),
154
+ emit: this.emit.bind(this),
155
+ onViewEvent: this.onViewEvent.bind(this),
156
+ onDestroy: this.onDestroy.bind(this)
157
+ };
158
+ }
159
+ /**
160
+ * Binds the scope to the shadow-object it made the API for and subscribes to the destruction of the entity.
161
+ *
162
+ * A shadow-object reaches its end on two independent paths: the entity is destroyed, or the shadow-object
163
+ * leaves the constructor set of a still living entity (token or route change). Both run the same teardown,
164
+ * and each path reaches it through a handle of its own.
165
+ *
166
+ * The kernel keeps two releases rather than one, because they belong to opposite ends of the teardown:
167
+ * `releaseScope` runs at its start, `forgetShadowObject` at its end.
168
+ *
169
+ * One scope serves one shadow-object, once: the call is part of the contract, and both ways of breaking
170
+ * it are refused below.
171
+ */
172
+ bindTo(shadowObject, releaseScope, forgetShadowObject) {
173
+ if (this.#isTornDown) {
174
+ throw new Error(`the creation scope of "${this.#displayName}" has torn down and cannot be bound`);
175
+ }
176
+ if (this.#shadowObject !== void 0) {
177
+ throw new Error(`the creation scope of "${this.#displayName}" is already bound to a shadow-object`);
178
+ }
179
+ this.#shadowObject = shadowObject;
180
+ this.#releaseScope = releaseScope;
181
+ this.#forgetShadowObject = forgetShadowObject;
182
+ this.#logger.info("create shadow-object", this.#displayName, { shadowObject, entity: this.#entity });
183
+ this.#unsubscribeFromEntityDestroy = once(this.#entity, onDestroy, Priority.Low, () => {
184
+ this.tearDown();
185
+ });
186
+ }
187
+ /**
188
+ * Runs once, whichever path reaches it first.
189
+ *
190
+ * Three lead here. Two belong to a shadow-object that lived: its entity is destroyed, or it leaves the
191
+ * constructor set of an entity that stays. On the third, the kernel calls this directly on a scope that
192
+ * `bindTo()` never saw, because the constructor threw -- there is no shadow-object then, and three of the
193
+ * handles below are still unset (`#releaseScope` and `#unsubscribeFromEntityDestroy` at the start,
194
+ * `#forgetShadowObject` at the end), which is why all three are called optionally.
195
+ *
196
+ * The provider feeds are released after everything else, so that a write made on the way out reaches the
197
+ * entity-side signal rather than stopping at the provider, and so that the entity hands the context over to
198
+ * a provider that stays only once that write is through.
199
+ *
200
+ * The flag makes the teardown a one-time act: a destroy callback reaching back into the kernel finds no
201
+ * way to start it a second time. Releasing both handles right after ends the retention in both directions
202
+ * -- the kernel's map points from the shadow-object to this scope, the subscription points from the entity
203
+ * to the same scope.
204
+ *
205
+ * The kernel is told to forget the shadow-object at the very end, so a destroy callback that reaches back
206
+ * into the kernel still finds its shadow-object listed among the ones the constructor created. Once that
207
+ * call is through, the four handles above are let go of as well -- the shadow-object, both kernel releases
208
+ * and the entity subscription -- so a scope past this point holds none of the four through these fields.
209
+ *
210
+ * Every step below runs behind its own guard, so a callback or signal teardown that throws does not stop
211
+ * the ones after it: the remaining callbacks, the remaining signal releases, and the provider feed
212
+ * hand-over all still happen. A caught error goes to the logger by displayName and the label of the step
213
+ * that failed, and is not re-thrown -- neither `changeToken()` nor `destroyEntity()` deliver it to their
214
+ * caller, nor does it stop the destroy notification the entity is still delivering to whatever comes after
215
+ * this shadow-object in the same run.
216
+ *
217
+ * The creation API is closed as the very last step, after everything above has run. Until then it
218
+ * is open, because the cleanup callbacks of the shadow-object run inside this method and are
219
+ * entitled to it -- a last message to the view, a last write to a context. What such a callback
220
+ * registers is still reached by the steps behind it. The two sets of cleanup callbacks are run
221
+ * in rounds until neither has anything left, so a callback that another one registers is reached
222
+ * whichever of the two it lands in. The context feed releases follow all of them. What arrives
223
+ * once this method has returned is not, and is turned away by `#refuseAfterTearDown()`.
224
+ */
225
+ tearDown() {
226
+ if (this.#isTornDown) return;
227
+ this.#isTornDown = true;
228
+ this.#runGuarded("scope release", () => this.#releaseScope?.());
229
+ this.#runGuarded("entity destroy subscription", () => this.#unsubscribeFromEntityDestroy?.());
230
+ if (this.#shadowObject !== void 0) {
231
+ this.#logger.info("destroy shadow-object", this.#displayName, { shadowObject: this.#shadowObject, entity: this.#entity });
232
+ }
233
+ const alreadyRun = /* @__PURE__ */ new Set();
234
+ const runCleanups = (callbacks, step) => {
235
+ let ranAny = false;
236
+ for (const callback of callbacks) {
237
+ if (alreadyRun.has(callback)) continue;
238
+ alreadyRun.add(callback);
239
+ ranAny = true;
240
+ this.#runGuarded(step, callback);
241
+ }
242
+ return ranAny;
243
+ };
244
+ let ranAnything = true;
245
+ while (ranAnything) {
246
+ const ranPrimary = runCleanups(this.#unsubscribePrimary, "onDestroy callback");
247
+ const ranSecondary = runCleanups(this.#unsubscribeSecondary, "creation-api cleanup");
248
+ ranAnything = ranPrimary || ranSecondary;
249
+ }
250
+ for (const callback of this.#unsubscribeContextFeeds) {
251
+ this.#runGuarded("context feed release", callback);
252
+ }
253
+ for (const sig of this.#contextReaders.values()) {
254
+ this.#runGuarded("context reader", () => destroySignal(sig));
255
+ }
256
+ for (const sig of this.#contextParentReaders.values()) {
257
+ this.#runGuarded("parent context reader", () => destroySignal(sig));
258
+ }
259
+ for (const sig of this.#propertyReaders.values()) {
260
+ this.#runGuarded("property reader", () => destroySignal(sig));
261
+ }
262
+ for (const sig of this.#contextProviders.values()) {
263
+ this.#runGuarded("context provider", () => destroySignal(sig));
264
+ }
265
+ for (const sig of this.#contextRootProviders.values()) {
266
+ this.#runGuarded("global context provider", () => destroySignal(sig));
267
+ }
268
+ this.#unsubscribePrimary.clear();
269
+ this.#unsubscribeSecondary.clear();
270
+ this.#unsubscribeContextFeeds.clear();
271
+ this.#contextReaders.clear();
272
+ this.#contextReaderCompares.clear();
273
+ this.#contextParentReaders.clear();
274
+ this.#contextParentReaderCompares.clear();
275
+ this.#propertyReaders.clear();
276
+ this.#propertyCompares.clear();
277
+ this.#contextProviders.clear();
278
+ this.#contextRootProviders.clear();
279
+ this.#clearOnDestroyRegistered.clear();
280
+ this.#runGuarded("forget shadow-object", () => this.#forgetShadowObject?.());
281
+ this.#shadowObject = void 0;
282
+ this.#releaseScope = void 0;
283
+ this.#forgetShadowObject = void 0;
284
+ this.#unsubscribeFromEntityDestroy = void 0;
285
+ this.#isCreationApiClosed = true;
286
+ }
287
+ /**
288
+ * Isolates one teardown step from the ones around it: a step that throws is reported through the
289
+ * logger -- ungated, so the report stays visible outside localhost -- and does not stop `tearDown()`
290
+ * from reaching the steps that follow. The report goes out through the shared `runGuarded()`;
291
+ * what this wrapper adds is the label of the step and the display name it belongs to.
292
+ */
293
+ #runGuarded(step, run) {
294
+ runGuarded(this.#logger, run, `shadow-object teardown failed (${step}):`, this.#displayName);
295
+ }
296
+ /**
297
+ * Books an `on()` or `once()` subscription into the cleanup set and hands back a handle that takes
298
+ * it out again. Unsubscribing releases the callback there and then rather than at the teardown, so
299
+ * a shadow-object that subscribes and unsubscribes over the whole life of its entity holds one
300
+ * handle at a time instead of one per call. The target makes no difference to that: a subscription
301
+ * on the entity ends the same way one on any other object does.
302
+ */
303
+ #trackSubscription(unsubscribe) {
304
+ this.#unsubscribeSecondary.add(unsubscribe);
305
+ return Object.assign(() => {
306
+ this.#unsubscribeSecondary.delete(unsubscribe);
307
+ unsubscribe();
308
+ }, unsubscribe);
309
+ }
310
+ /**
311
+ * The body the three cached readers share: one reader per name, created on the first call and fed
312
+ * from the entity-side source, plus a warning when a later call brings a different {compare}
313
+ * function than the one the reader was created with.
314
+ *
315
+ * `linkSource` is a thunk because the source may only be read when a reader is actually created.
316
+ */
317
+ #cachedReader(name, readers, compares, linkSource, opts, apiName, subject) {
318
+ let reader = readers.get(name);
319
+ if (reader === void 0) {
320
+ reader = createSignal(void 0, opts).get;
321
+ readers.set(name, reader);
322
+ compares.set(name, opts?.compare);
323
+ const ln = link(linkSource(), reader);
324
+ this.#unsubscribeSecondary.add(ln.destroy.bind(ln));
325
+ } else if (opts?.compare != null && compares.get(name) !== opts.compare) {
326
+ this.#logger.error(
327
+ `[shadow-objects] ${apiName}("${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 ${subject}.`
328
+ );
329
+ }
330
+ return reader;
331
+ }
332
+ /**
333
+ * Reports the deprecated call form in which a bare compare function stands where the options
334
+ * object belongs, at most once per kernel and member name.
335
+ *
336
+ * Through `error` rather than `warn`: this names a mistake in the calling code, and its author
337
+ * has to see it wherever the application runs. `logger.warn` is asked `isWarn` first, and the
338
+ * shared `enable` behind that getter is off anywhere but `localhost` -- a deprecation notice
339
+ * that goes silent everywhere the code actually ships is no notice at all. See the table of
340
+ * call against getter under "Console Logger" in `docs/api-reference.md`.
341
+ *
342
+ * One line per kernel and member name: a shadow-object calling a deprecated member inside a
343
+ * loop would otherwise fill the console.
344
+ */
345
+ #reportDeprecatedIsEqualOption(options, apiName) {
346
+ if (typeof options !== "function" || this.#shownDeprecations.has(apiName)) return;
347
+ this.#logger.error(
348
+ `[shadow-objects] Deprecation Warning: The "isEqual" option of "${apiName}()" is now passed as {compare} argument. Please update your code accordingly.`
349
+ );
350
+ this.#shownDeprecations.add(apiName);
351
+ }
352
+ /**
353
+ * Answers whether the creation API still has anything to give, and reports it where it has not.
354
+ *
355
+ * Past the teardown the scope has released what it held and cleared what it tracked. A call
356
+ * arriving now would fill those maps again with entries no second teardown ever reaches -- the
357
+ * teardown runs once, and it has run. Such a call is therefore turned away rather than served.
358
+ *
359
+ * Turned away quietly: the callers that arrive late are the ones with nobody to catch a throw --
360
+ * a timer, a continuation behind an `await`, a callback of some other object that outlived this
361
+ * one. It is reported instead, through `error` for the same reason the deprecation report above
362
+ * uses it: this names a mistake in the calling code, and its author has to see it wherever the
363
+ * application runs. Once per member name and scope, because a stale timer comes back on its next
364
+ * tick and a line per tick would bury the first one.
365
+ */
366
+ #refuseAfterTearDown(apiName) {
367
+ if (!this.#isCreationApiClosed) return false;
368
+ if (!this.#lateCallsReported?.has(apiName)) {
369
+ (this.#lateCallsReported ??= /* @__PURE__ */ new Set()).add(apiName);
370
+ this.#logger.error(
371
+ `[shadow-objects] ${apiName}(): the creation scope of "${this.#displayName}" has torn down \u2014 the call does nothing. Something is still holding the creation API past the end of its shadow-object.`
372
+ );
373
+ }
374
+ return true;
375
+ }
376
+ useProperty(name, options) {
377
+ if (this.#refuseAfterTearDown("useProperty")) return inertSignal().get;
378
+ this.#reportDeprecatedIsEqualOption(options, "useProperty");
379
+ const opts = typeof options === "function" ? { compare: options } : options;
380
+ return this.#cachedReader(
381
+ name,
382
+ this.#propertyReaders,
383
+ this.#propertyCompares,
384
+ () => this.#entity.getPropertyReader(name),
385
+ opts,
386
+ "useProperty",
387
+ "property"
388
+ );
389
+ }
390
+ useProperties(props) {
391
+ const result = {};
392
+ for (const key in props) {
393
+ if (Object.hasOwn(props, key)) {
394
+ result[key] = this.useProperty(props[key]);
395
+ }
396
+ }
397
+ return result;
398
+ }
399
+ /**
400
+ * The body the two context providers share: one provider signal per name, created on the first
401
+ * call, attached to the entity-side context signal, and cleared on teardown unless the caller
402
+ * opted out -- a clearing that finds the way to the entity side still open. A source signal handed
403
+ * in instead of an initial value feeds the provider through a link of its own.
404
+ *
405
+ * The entity keeps every provider of a name together and answers the attachment with the release
406
+ * that detaches this one again. Releasing it does more than cut the feed: the entity then lets a
407
+ * provider that stays write its value once more, so the name ends up with a value that is actually
408
+ * still being provided rather than with what this shadow-object left behind on its way out.
409
+ *
410
+ * `attachToEntity` is called inside the creation branch only, so the entity-side signal is
411
+ * requested when a provider is actually created. The `clearOnDestroy` check sits outside that
412
+ * branch: every call is allowed to ask for the clearing, not only the one that created the signal.
413
+ * The request is booked once per provider, though -- a repeated ask does not queue a second write,
414
+ * and once a provider has one on file, an opt-out on a later call does not take it back.
415
+ */
416
+ #provideContextSignal(name, providers, attachToEntity, sourceOrInitialValue, opts) {
417
+ let ctxProvider = providers.get(name);
418
+ if (ctxProvider == null) {
419
+ const isSig = isSignal(sourceOrInitialValue);
420
+ const initialValue = isSig ? void 0 : toMaybe(sourceOrInitialValue);
421
+ ctxProvider = createSignal(initialValue, opts?.compare ? { compare: opts.compare } : void 0);
422
+ if (isSig) {
423
+ const ln = link(sourceOrInitialValue, ctxProvider);
424
+ this.#unsubscribeSecondary.add(ln.destroy.bind(ln));
425
+ }
426
+ this.#unsubscribeContextFeeds.add(attachToEntity(ctxProvider));
427
+ providers.set(name, ctxProvider);
428
+ }
429
+ if ((opts?.clearOnDestroy ?? true) && !this.#clearOnDestroyRegistered.has(ctxProvider)) {
430
+ this.#clearOnDestroyRegistered.add(ctxProvider);
431
+ this.#unsubscribeSecondary.add(() => {
432
+ ctxProvider.set(void 0);
433
+ });
434
+ }
435
+ return ctxProvider;
436
+ }
437
+ provideContext(name, sourceOrInitialValue, options) {
438
+ if (this.#refuseAfterTearDown("provideContext")) return inertSignal();
439
+ this.#reportDeprecatedIsEqualOption(options, "provideContext");
440
+ const opts = typeof options === "function" ? { compare: options } : options;
441
+ return this.#provideContextSignal(
442
+ name,
443
+ this.#contextProviders,
444
+ (provider) => this.#entity.attachContextProvider(name, provider),
445
+ sourceOrInitialValue,
446
+ opts
447
+ );
448
+ }
449
+ provideGlobalContext(name, sourceOrInitialValue, options) {
450
+ if (this.#refuseAfterTearDown("provideGlobalContext")) return inertSignal();
451
+ this.#reportDeprecatedIsEqualOption(options, "provideGlobalContext");
452
+ const opts = typeof options === "function" ? { compare: options } : options;
453
+ return this.#provideContextSignal(
454
+ name,
455
+ this.#contextRootProviders,
456
+ (provider) => this.#entity.attachGlobalContextProvider(name, provider),
457
+ sourceOrInitialValue,
458
+ opts
459
+ );
460
+ }
461
+ useContext(name, options) {
462
+ if (this.#refuseAfterTearDown("useContext")) return inertSignal().get;
463
+ this.#reportDeprecatedIsEqualOption(options, "useContext");
464
+ const opts = typeof options === "function" ? { compare: options } : options;
465
+ return this.#cachedReader(
466
+ name,
467
+ this.#contextReaders,
468
+ this.#contextReaderCompares,
469
+ () => this.#entity.useContext(name),
470
+ opts,
471
+ "useContext",
472
+ "context"
473
+ );
474
+ }
475
+ useParentContext(name, options) {
476
+ if (this.#refuseAfterTearDown("useParentContext")) return inertSignal().get;
477
+ this.#reportDeprecatedIsEqualOption(options, "useParentContext");
478
+ const opts = typeof options === "function" ? { compare: options } : options;
479
+ return this.#cachedReader(
480
+ name,
481
+ this.#contextParentReaders,
482
+ this.#contextParentReaderCompares,
483
+ () => this.#entity.useParentContext(name),
484
+ opts,
485
+ "useParentContext",
486
+ "parent context"
487
+ );
488
+ }
489
+ createSignal(...args) {
490
+ if (this.#refuseAfterTearDown("createSignal")) return inertSignal();
491
+ const sig = createSignal(...args);
492
+ this.#unsubscribeSecondary.add(() => {
493
+ destroySignal(sig);
494
+ });
495
+ return sig;
496
+ }
497
+ createEffect(...args) {
498
+ if (this.#refuseAfterTearDown("createEffect")) return inertEffect();
499
+ const effect = createEffect(...args);
500
+ this.#unsubscribeSecondary.add(effect.destroy);
501
+ return effect;
502
+ }
503
+ createMemo(...args) {
504
+ if (this.#refuseAfterTearDown("createMemo")) return inertSignal().get;
505
+ const sig = createMemo(...args);
506
+ this.#unsubscribeSecondary.add(() => {
507
+ destroySignal(sig);
508
+ });
509
+ return sig;
510
+ }
511
+ createResource(factory, cleanup) {
512
+ if (this.#refuseAfterTearDown("createResource")) return inertSignal();
513
+ const resourceSignal = createSignal();
514
+ const effect = createEffect(() => {
515
+ const resource = toMaybe(factory());
516
+ resourceSignal.set(resource);
517
+ if (resource !== void 0 && cleanup) {
518
+ return () => {
519
+ cleanup(resource);
520
+ resourceSignal.set(void 0);
521
+ };
522
+ }
523
+ return () => {
524
+ resourceSignal.set(void 0);
525
+ };
526
+ });
527
+ this.#unsubscribeSecondary.add(() => {
528
+ try {
529
+ effect.destroy();
530
+ } finally {
531
+ resourceSignal.set(void 0);
532
+ destroySignal(resourceSignal);
533
+ }
534
+ });
535
+ return resourceSignal;
536
+ }
537
+ on(...args) {
538
+ if (this.#refuseAfterTearDown("on")) return noSubscription;
539
+ const [firstArg] = args;
540
+ const unsubscribe = typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg) ? (
541
+ // @ts-ignore
542
+ on(this.#entity, ...args)
543
+ ) : (
544
+ // @ts-ignore
545
+ on(...args)
546
+ );
547
+ return this.#trackSubscription(unsubscribe);
548
+ }
549
+ once(...args) {
550
+ if (this.#refuseAfterTearDown("once")) return noSubscription;
551
+ const [firstArg] = args;
552
+ const unsubscribe = typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg) ? (
553
+ // @ts-ignore
554
+ once(this.#entity, ...args)
555
+ ) : (
556
+ // @ts-ignore
557
+ once(...args)
558
+ );
559
+ return this.#trackSubscription(unsubscribe);
560
+ }
561
+ /**
562
+ * The plain `emit()`, deliberately, where every notification the framework sends out of its own
563
+ * accord runs on the guarded dispatch. This one is outbound and belongs to the Shadow Object: it
564
+ * owns the listeners, a throw from one of them is its own bug, and letting it surface where it
565
+ * happened is the fastest way to find it. A listener that needs a policy of its own carries a
566
+ * `try`/`catch` in its own body, which is where that decision belongs.
567
+ */
568
+ emit(...args) {
569
+ if (this.#refuseAfterTearDown("emit")) return;
570
+ const [firstArg] = args;
571
+ if (typeof firstArg === "string" || typeof firstArg === "symbol" || Array.isArray(firstArg)) {
572
+ emit(this.#entity, ...args);
573
+ } else {
574
+ emit(...args);
575
+ }
576
+ }
577
+ onViewEvent(callback) {
578
+ if (this.#refuseAfterTearDown("onViewEvent")) return;
579
+ const unsub = on(this.#entity, onViewEvent, (type, data) => {
580
+ callback(type, data);
581
+ });
582
+ this.#unsubscribeSecondary.add(unsub);
583
+ }
584
+ onDestroy(callback) {
585
+ if (this.#refuseAfterTearDown("onDestroy")) return;
586
+ this.#unsubscribePrimary.add(callback);
587
+ }
588
+ dispatchMessageToView(type, data, transferables, traverseChildren = false) {
589
+ if (this.#refuseAfterTearDown("dispatchMessageToView")) return;
590
+ this.#entity.dispatchMessageToView(type, data, transferables, traverseChildren);
591
+ }
592
+ }
593
+ export {
594
+ ShadowObjectCreationScope
595
+ };
596
+ //# sourceMappingURL=ShadowObjectCreationScope.js.map