@spearwolf/shadow-objects 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/CHANGELOG.md +320 -61
  2. package/README.md +57 -13
  3. package/bundle.js +5 -54
  4. package/package.json +7 -3
  5. package/src/ChangeTrailRefusedError.d.ts +27 -0
  6. package/src/ChangeTrailRefusedError.d.ts.map +1 -0
  7. package/src/ChangeTrailRefusedError.js +12 -0
  8. package/src/ChangeTrailRefusedError.js.map +7 -0
  9. package/src/EntityUuidInUseError.d.ts +19 -0
  10. package/src/EntityUuidInUseError.d.ts.map +1 -0
  11. package/src/EntityUuidInUseError.js +11 -0
  12. package/src/EntityUuidInUseError.js.map +7 -0
  13. package/src/WorkerTimeoutError.d.ts +25 -0
  14. package/src/WorkerTimeoutError.d.ts.map +1 -0
  15. package/src/WorkerTimeoutError.js +12 -0
  16. package/src/WorkerTimeoutError.js.map +7 -0
  17. package/src/bundle.d.ts +1 -1
  18. package/src/bundle.d.ts.map +1 -1
  19. package/src/bundle.js +1 -1
  20. package/src/bundle.js.map +2 -2
  21. package/src/constants.d.ts +9 -2
  22. package/src/constants.d.ts.map +1 -1
  23. package/src/constants.js +1 -1
  24. package/src/constants.js.map +2 -2
  25. package/src/create-worker.bundle.d.ts +1 -1
  26. package/src/create-worker.bundle.d.ts.map +1 -1
  27. package/src/create-worker.bundle.js.map +1 -1
  28. package/src/create-worker.d.ts +1 -1
  29. package/src/create-worker.d.ts.map +1 -1
  30. package/src/create-worker.js.map +1 -1
  31. package/src/elements/ShaeElement.d.ts +70 -2
  32. package/src/elements/ShaeElement.d.ts.map +1 -1
  33. package/src/elements/ShaeElement.js +129 -25
  34. package/src/elements/ShaeElement.js.map +3 -3
  35. package/src/elements/ShaeEntElement.d.ts +39 -1
  36. package/src/elements/ShaeEntElement.d.ts.map +1 -1
  37. package/src/elements/ShaeEntElement.js +397 -167
  38. package/src/elements/ShaeEntElement.js.map +2 -2
  39. package/src/elements/ShaeLifecycleElement.d.ts +71 -0
  40. package/src/elements/ShaeLifecycleElement.d.ts.map +1 -0
  41. package/src/elements/ShaeLifecycleElement.js +83 -0
  42. package/src/elements/ShaeLifecycleElement.js.map +7 -0
  43. package/src/elements/ShaePropElement.d.ts +52 -2
  44. package/src/elements/ShaePropElement.d.ts.map +1 -1
  45. package/src/elements/ShaePropElement.js +240 -219
  46. package/src/elements/ShaePropElement.js.map +2 -2
  47. package/src/elements/ShaeWorkerElement.d.ts +49 -5
  48. package/src/elements/ShaeWorkerElement.d.ts.map +1 -1
  49. package/src/elements/ShaeWorkerElement.js +204 -73
  50. package/src/elements/ShaeWorkerElement.js.map +2 -2
  51. package/src/elements/constants.d.ts +6 -0
  52. package/src/elements/constants.d.ts.map +1 -1
  53. package/src/elements/constants.js +12 -0
  54. package/src/elements/constants.js.map +2 -2
  55. package/src/elements/deferredTeardown.d.ts +24 -0
  56. package/src/elements/deferredTeardown.d.ts.map +1 -0
  57. package/src/elements/deferredTeardown.js +30 -0
  58. package/src/elements/deferredTeardown.js.map +7 -0
  59. package/src/elements/displayContentsRule.d.ts +25 -0
  60. package/src/elements/displayContentsRule.d.ts.map +1 -0
  61. package/src/elements/displayContentsRule.js +37 -0
  62. package/src/elements/displayContentsRule.js.map +7 -0
  63. package/src/elements/events.d.ts +10 -7
  64. package/src/elements/events.d.ts.map +1 -1
  65. package/src/elements/forwardCustomEvents.d.ts +19 -0
  66. package/src/elements/forwardCustomEvents.d.ts.map +1 -0
  67. package/src/elements/forwardCustomEvents.js +47 -0
  68. package/src/elements/forwardCustomEvents.js.map +7 -0
  69. package/src/elements/hostedSlots.d.ts +36 -0
  70. package/src/elements/hostedSlots.d.ts.map +1 -0
  71. package/src/elements/hostedSlots.js +118 -0
  72. package/src/elements/hostedSlots.js.map +7 -0
  73. package/src/elements/parentRemoval.d.ts +18 -0
  74. package/src/elements/parentRemoval.d.ts.map +1 -0
  75. package/src/elements/parentRemoval.js +60 -0
  76. package/src/elements/parentRemoval.js.map +7 -0
  77. package/src/elements/propValueConverters.d.ts +3 -0
  78. package/src/elements/propValueConverters.d.ts.map +1 -0
  79. package/src/elements/propValueConverters.js +54 -0
  80. package/src/elements/propValueConverters.js.map +7 -0
  81. package/src/elements/requestEntAncestor.d.ts +32 -0
  82. package/src/elements/requestEntAncestor.d.ts.map +1 -0
  83. package/src/elements/requestEntAncestor.js +16 -0
  84. package/src/elements/requestEntAncestor.js.map +7 -0
  85. package/src/elements.js.map +1 -1
  86. package/src/in-the-dark/Entity.d.ts +113 -3
  87. package/src/in-the-dark/Entity.d.ts.map +1 -1
  88. package/src/in-the-dark/Entity.js +292 -63
  89. package/src/in-the-dark/Entity.js.map +3 -3
  90. package/src/in-the-dark/Kernel.d.ts +76 -17
  91. package/src/in-the-dark/Kernel.d.ts.map +1 -1
  92. package/src/in-the-dark/Kernel.js +410 -378
  93. package/src/in-the-dark/Kernel.js.map +3 -3
  94. package/src/in-the-dark/Registry.d.ts.map +1 -1
  95. package/src/in-the-dark/Registry.js +83 -14
  96. package/src/in-the-dark/Registry.js.map +2 -2
  97. package/src/in-the-dark/ShadowObject.d.ts.map +1 -1
  98. package/src/in-the-dark/ShadowObject.js +1 -0
  99. package/src/in-the-dark/ShadowObject.js.map +2 -2
  100. package/src/in-the-dark/ShadowObjectCreationScope.d.ts +126 -0
  101. package/src/in-the-dark/ShadowObjectCreationScope.d.ts.map +1 -0
  102. package/src/in-the-dark/ShadowObjectCreationScope.js +596 -0
  103. package/src/in-the-dark/ShadowObjectCreationScope.js.map +7 -0
  104. package/src/in-the-dark/SignalsPath.d.ts.map +1 -1
  105. package/src/in-the-dark/SignalsPath.js +2 -2
  106. package/src/in-the-dark/SignalsPath.js.map +2 -2
  107. package/src/in-the-dark/events.js.map +1 -1
  108. package/src/in-the-dark/importModule.d.ts +7 -0
  109. package/src/in-the-dark/importModule.d.ts.map +1 -1
  110. package/src/in-the-dark/importModule.js +5 -2
  111. package/src/in-the-dark/importModule.js.map +2 -2
  112. package/src/index.d.ts +7 -1
  113. package/src/index.d.ts.map +1 -1
  114. package/src/index.js +5 -1
  115. package/src/index.js.map +2 -2
  116. package/src/shadow-objects.d.ts +2 -0
  117. package/src/shadow-objects.d.ts.map +1 -1
  118. package/src/shadow-objects.js +2 -0
  119. package/src/shadow-objects.js.map +2 -2
  120. package/src/shadow-objects.worker.js +0 -1
  121. package/src/shadow-objects.worker.js.map +2 -2
  122. package/src/shae-ent.js.map +1 -1
  123. package/src/shae-prop.js +2 -2
  124. package/src/shae-prop.js.map +2 -2
  125. package/src/shae-worker.js.map +1 -1
  126. package/src/types.d.ts +35 -4
  127. package/src/types.d.ts.map +1 -1
  128. package/src/utils/ConsoleLogger.d.ts +45 -10
  129. package/src/utils/ConsoleLogger.d.ts.map +1 -1
  130. package/src/utils/ConsoleLogger.js +51 -26
  131. package/src/utils/ConsoleLogger.js.map +2 -2
  132. package/src/utils/FrameLoop.d.ts +42 -3
  133. package/src/utils/FrameLoop.d.ts.map +1 -1
  134. package/src/utils/FrameLoop.js +71 -10
  135. package/src/utils/FrameLoop.js.map +2 -2
  136. package/src/utils/MicrotaskCollector.d.ts +45 -0
  137. package/src/utils/MicrotaskCollector.d.ts.map +1 -0
  138. package/src/utils/MicrotaskCollector.js +33 -0
  139. package/src/utils/MicrotaskCollector.js.map +7 -0
  140. package/src/utils/MicrotaskGate.d.ts +17 -0
  141. package/src/utils/MicrotaskGate.d.ts.map +1 -0
  142. package/src/utils/MicrotaskGate.js +20 -0
  143. package/src/utils/MicrotaskGate.js.map +7 -0
  144. package/src/utils/array-utils.js.map +1 -1
  145. package/src/utils/attr-utils.d.ts +6 -0
  146. package/src/utils/attr-utils.d.ts.map +1 -1
  147. package/src/utils/attr-utils.js +3 -1
  148. package/src/utils/attr-utils.js.map +2 -2
  149. package/src/utils/constants.js.map +1 -1
  150. package/src/utils/generateUUID.d.ts +11 -0
  151. package/src/utils/generateUUID.d.ts.map +1 -1
  152. package/src/utils/generateUUID.js +31 -266
  153. package/src/utils/generateUUID.js.map +2 -2
  154. package/src/utils/props-utils.d.ts +3 -2
  155. package/src/utils/props-utils.d.ts.map +1 -1
  156. package/src/utils/props-utils.js +13 -24
  157. package/src/utils/props-utils.js.map +2 -2
  158. package/src/utils/runGuarded.d.ts +27 -0
  159. package/src/utils/runGuarded.d.ts.map +1 -0
  160. package/src/utils/runGuarded.js +11 -0
  161. package/src/utils/runGuarded.js.map +7 -0
  162. package/src/utils/toMaybe.d.ts.map +1 -1
  163. package/src/utils/toMaybe.js.map +1 -1
  164. package/src/utils/toNamespace.d.ts.map +1 -1
  165. package/src/utils/toNamespace.js.map +1 -1
  166. package/src/utils/toUrlString.d.ts +12 -0
  167. package/src/utils/toUrlString.d.ts.map +1 -1
  168. package/src/utils/toUrlString.js.map +2 -2
  169. package/src/utils/waitForMessageOfType.d.ts +12 -1
  170. package/src/utils/waitForMessageOfType.d.ts.map +1 -1
  171. package/src/utils/waitForMessageOfType.js +16 -1
  172. package/src/utils/waitForMessageOfType.js.map +2 -2
  173. package/src/view/ComponentChanges.d.ts +88 -1
  174. package/src/view/ComponentChanges.d.ts.map +1 -1
  175. package/src/view/ComponentChanges.js +243 -73
  176. package/src/view/ComponentChanges.js.map +2 -2
  177. package/src/view/ComponentContext.d.ts +206 -16
  178. package/src/view/ComponentContext.d.ts.map +1 -1
  179. package/src/view/ComponentContext.js +548 -91
  180. package/src/view/ComponentContext.js.map +2 -2
  181. package/src/view/ComponentMemory.d.ts +3 -3
  182. package/src/view/ComponentMemory.d.ts.map +1 -1
  183. package/src/view/ComponentMemory.js.map +1 -1
  184. package/src/view/IShadowObjectEnvProxy.d.ts +29 -3
  185. package/src/view/IShadowObjectEnvProxy.d.ts.map +1 -1
  186. package/src/view/LocalShadowObjectEnv.d.ts +1 -1
  187. package/src/view/LocalShadowObjectEnv.d.ts.map +1 -1
  188. package/src/view/LocalShadowObjectEnv.js +8 -6
  189. package/src/view/LocalShadowObjectEnv.js.map +2 -2
  190. package/src/view/RemoteWorkerEnv.d.ts +89 -5
  191. package/src/view/RemoteWorkerEnv.d.ts.map +1 -1
  192. package/src/view/RemoteWorkerEnv.js +195 -77
  193. package/src/view/RemoteWorkerEnv.js.map +2 -2
  194. package/src/view/ShadowEnv.d.ts +49 -2
  195. package/src/view/ShadowEnv.d.ts.map +1 -1
  196. package/src/view/ShadowEnv.js +223 -60
  197. package/src/view/ShadowEnv.js.map +2 -2
  198. package/src/view/ViewComponent.d.ts +78 -14
  199. package/src/view/ViewComponent.d.ts.map +1 -1
  200. package/src/view/ViewComponent.js +90 -14
  201. package/src/view/ViewComponent.js.map +2 -2
  202. package/src/view/cloneChangeTrail.js.map +2 -2
  203. package/src/worker/MessageRouter.d.ts +14 -0
  204. package/src/worker/MessageRouter.d.ts.map +1 -1
  205. package/src/worker/MessageRouter.js +75 -15
  206. package/src/worker/MessageRouter.js.map +2 -2
  207. package/src/worker/WorkerRuntime.d.ts +22 -1
  208. package/src/worker/WorkerRuntime.d.ts.map +1 -1
  209. package/src/worker/WorkerRuntime.js +60 -8
  210. package/src/worker/WorkerRuntime.js.map +2 -2
@@ -1,6 +1,31 @@
1
1
  import { ComponentContext } from './ComponentContext.js';
2
+ interface ViewComponentOptions {
3
+ parent?: ViewComponent | undefined;
4
+ order?: number | undefined;
5
+ context?: ComponentContext | undefined;
6
+ uuid?: string | undefined;
7
+ autoDestructionOnParentRemoval?: boolean | undefined;
8
+ }
2
9
  export declare class ViewComponent {
3
10
  #private;
11
+ /**
12
+ * The last thing a component says before {@link ViewComponent#destroy} takes every subscription
13
+ * off it. An integration that put something on the instance — event subscriptions, an own
14
+ * `dispatchEvent` — hears it here and can set that up again on the same component; a context
15
+ * that takes the component back in (`vc.context = ctx`) revives it under the same uuid.
16
+ *
17
+ * It goes out directly on the component rather than through {@link ViewComponent#dispatchEvent},
18
+ * so an own `dispatchEvent` an integration installed does not see it and cannot carry it further.
19
+ * Leaving a context says nothing: `vc.context = null` detaches the component and keeps everything
20
+ * on it.
21
+ *
22
+ * A listener that throws is reported through `console.warn` and costs neither the listeners
23
+ * behind it their turn nor the component the rest of its teardown.
24
+ *
25
+ * Not to be confused with the `Destroyed` exported at the top level of the package: that one is
26
+ * `'destroyed'` and belongs to the worker channel, this one is `'view-component-destroyed'`.
27
+ */
28
+ static readonly Destroyed = "view-component-destroyed";
4
29
  get uuid(): string;
5
30
  get token(): string | undefined;
6
31
  set token(token: string | undefined);
@@ -8,19 +33,31 @@ export declare class ViewComponent {
8
33
  set parent(parent: ViewComponent | null | undefined);
9
34
  get context(): ComponentContext | undefined;
10
35
  /**
11
- * @throws {ComponentContextDisposedError} if the new context has been disposed. The component
12
- * keeps its current context in that case: leaving the old one is only worth it if the new
13
- * one can actually be joined.
36
+ * @throws {ComponentContextDisposedError} if the new {@link ComponentContext} has been disposed.
37
+ * The component keeps its current one in that case: leaving the old one is only worth it if
38
+ * the new one can actually be joined.
39
+ * @throws {ComponentUuidInUseError} if another component of the new {@link ComponentContext}
40
+ * holds this component's uuid. A uuid names one component of a {@link ComponentContext} at a
41
+ * time, and it is free again once its holder has left. This one is thrown after the old
42
+ * {@link ComponentContext} has been left, so the component holds none afterwards — assign one
43
+ * again to take it back in.
14
44
  */
15
45
  set context(context: ComponentContext | null | undefined);
16
46
  /**
17
47
  * A destroyed component is detached from its {@link ComponentContext}: it no longer appears
18
48
  * in any change trail and no longer has a corresponding entity.
19
49
  *
20
- * Every mutation that only concerns the component itself (`token`, `order`, properties, events,
21
- * `removeFromParent`, `destroy`) is silently ignored while destroyed. Operations that would tie
22
- * a second, live component to it (`addChild`, the `parent` setter) throw instead, because
23
- * ignoring them would leave the caller with a wrong picture of the entity tree.
50
+ * Every mutation that only concerns the component itself (`token`, `order`, properties,
51
+ * `removeFromParent`) is silently ignored while destroyed. Operations that would tie a second,
52
+ * live component to it (`addChild`, the `parent` setter) throw instead, because ignoring them
53
+ * would leave the caller with a wrong picture of the entity tree.
54
+ * {@link ViewComponent#destroy} finds nothing left to detach and still takes off whatever lies
55
+ * on the component at that moment.
56
+ *
57
+ * Assigning `null` or `undefined` to {@link ViewComponent#context} reports the same state: it
58
+ * detaches the component without silencing it, so {@link ViewComponent#dispatchEvent} reaches
59
+ * every listener on it. After a {@link ViewComponent#destroy} it reaches those registered since.
60
+ * Children are not traversed either way.
24
61
  *
25
62
  * Assigning a {@link ViewComponent#context} revives the component under the same uuid.
26
63
  */
@@ -40,13 +77,18 @@ export declare class ViewComponent {
40
77
  * parent is destroyed, so they remain reachable.
41
78
  */
42
79
  get autoDestructionOnParentRemoval(): boolean;
43
- constructor(token: string, options?: {
44
- parent?: ViewComponent;
45
- order?: number;
46
- context?: ComponentContext;
47
- uuid?: string;
48
- autoDestructionOnParentRemoval?: boolean;
49
- });
80
+ /**
81
+ * Create a component: `new ViewComponent(token, parent)` hangs it under another component, and
82
+ * `new ViewComponent(token, options)` takes `parent`, `order`, `context`, `uuid` and
83
+ * `autoDestructionOnParentRemoval` as an object.
84
+ *
85
+ * A {@link ViewComponent} given in place of the options object is read as the parent and as
86
+ * nothing else — `uuid`, `order` and `context` come from their usual sources, never from it.
87
+ *
88
+ * A component created without a `token` carries {@link VoidToken} (`'#void'`).
89
+ */
90
+ constructor(token?: string, parent?: ViewComponent);
91
+ constructor(token?: string, options?: ViewComponentOptions);
50
92
  isChildOf(parent: ViewComponent): boolean;
51
93
  removeFromParent(): void;
52
94
  addChild(child: ViewComponent): void;
@@ -55,9 +97,31 @@ export declare class ViewComponent {
55
97
  * A destroyed component always returns `false`.
56
98
  */
57
99
  setProperty<T = unknown>(name: string, value: T, isEqual?: (a: T, b: T) => boolean): boolean;
100
+ /**
101
+ * Mark a property as set without giving it a value. The entity holds the key and reads it as
102
+ * `undefined`; {@link ViewComponent.setProperty} with `undefined` removes it instead.
103
+ *
104
+ * @returns `true` if this differs from the last value written to the change trail.
105
+ * A destroyed component always returns `false`.
106
+ */
107
+ setPropertyWithoutValue(name: string): boolean;
58
108
  removeProperty(name: string): void;
59
109
  dispatchShadowObjectsEvent(type: string, data: unknown, transferables?: Transferable[]): void;
60
110
  dispatchEvent(type: string, data: unknown, traverseChildren: boolean): void;
111
+ /**
112
+ * End the component: it leaves its {@link ComponentContext} and goes silent. Every `on()` and
113
+ * `once()` subscription made on it is removed, and a `dispatchEvent` an integration installed on
114
+ * the instance is dropped with them. A promise from `onceAsync()` is not reached and settles
115
+ * only when the event it waits for arrives.
116
+ *
117
+ * Calling it more than once is safe. Each call takes off what is on the component at that
118
+ * moment — a subscription made afterwards is heard again.
119
+ *
120
+ * {@link ViewComponent.Destroyed} goes out on the component right before the subscriptions come
121
+ * off, so whoever holds something on it hears it. A listener that wants its subscription back
122
+ * does not set it up inside the handler — that one would come off with the rest.
123
+ */
61
124
  destroy(): void;
62
125
  }
126
+ export {};
63
127
  //# sourceMappingURL=ViewComponent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ViewComponent.d.ts","sourceRoot":"","sources":["../../../src/view/ViewComponent.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,gBAAgB,EAAgC,MAAM,uBAAuB,CAAC;AAsBtF,qBAAa,aAAa;;IAWxB,IAAI,IAAI,WAEP;IAED,IAAI,KAAK,IAIQ,MAAM,GAAG,SAAS,CAFlC;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAKlC;IAED,IAAI,MAAM,IAAI,aAAa,GAAG,SAAS,CAEtC;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EAMlD;IAED,IAAI,OAAO,IAAI,gBAAgB,GAAG,SAAS,CAE1C;IAED;;;;OAIG;IACH,IAAI,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAyBvD;IAED;;;;;;;;;;OAUG;IACH,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;;;OAIG;IACH,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAMzC;IAED;;;;;;OAMG;IACH,IAAI,8BAA8B,IAAI,OAAO,CAE5C;gBAGC,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,aAAa,CAAC;QACvB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,gBAAgB,CAAC;QAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,8BAA8B,CAAC,EAAE,OAAO,CAAC;KAC1C;IAwBH,SAAS,CAAC,MAAM,EAAE,aAAa;IAI/B,gBAAgB;IAShB,QAAQ,CAAC,KAAK,EAAE,aAAa;IAuB7B;;;OAGG;IACH,WAAW,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GAAG,OAAO;IAI5F,cAAc,CAAC,IAAI,EAAE,MAAM;IAI3B,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE,YAAY,EAAE;IAItF,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,gBAAgB,EAAE,OAAO;IAUpE,OAAO;CAKR"}
1
+ {"version":3,"file":"ViewComponent.d.ts","sourceRoot":"","sources":["../../../src/view/ViewComponent.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,gBAAgB,EAAgC,MAAM,uBAAuB,CAAC;AA4BtF,UAAU,oBAAoB;IAC5B,MAAM,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACvC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,8BAA8B,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACtD;AAED,qBAAa,aAAa;;IACxB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,QAAQ,CAAC,SAAS,8BAA8B;IAYvD,IAAI,IAAI,WAEP;IAED,IAAI,KAAK,IAIQ,MAAM,GAAG,SAAS,CAFlC;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAKlC;IAED,IAAI,MAAM,IAAI,aAAa,GAAG,SAAS,CAEtC;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EAMlD;IAED,IAAI,OAAO,IAAI,gBAAgB,GAAG,SAAS,CAE1C;IAED;;;;;;;;;OASG;IACH,IAAI,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EAqCvD;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;;;OAIG;IACH,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAMzC;IAED;;;;;;OAMG;IACH,IAAI,8BAA8B,IAAI,OAAO,CAE5C;IAOD;;;;;;;;;OASG;IACH,YAAY,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,EAAE;IACpD,YAAY,KAAK,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,EAAE;IAwB5D,SAAS,CAAC,MAAM,EAAE,aAAa,WAE9B;IAED,gBAAgB,SAOf;IAED,QAAQ,CAAC,KAAK,EAAE,aAAa,QAqB5B;IAED;;;OAGG;IACH,WAAW,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,GAAG,OAAO,CAE3F;IAED;;;;;;OAMG;IACH,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7C;IAED,cAAc,CAAC,IAAI,EAAE,MAAM,QAE1B;IAED,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE,YAAY,EAAE,QAErF;IAED,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,gBAAgB,EAAE,OAAO,QAcnE;IAiBD;;;;;;;;;;;;OAYG;IACH,OAAO,SAiBN;CACF"}
@@ -1,4 +1,4 @@
1
- import { emit, eventize } from "@spearwolf/eventize";
1
+ import { emitSafe, eventize, off } from "@spearwolf/eventize";
2
2
  import { VoidToken } from "../constants.js";
3
3
  import { generateUUID } from "../utils/generateUUID.js";
4
4
  import { ComponentContext, ComponentContextDisposedError } from "./ComponentContext.js";
@@ -15,8 +15,31 @@ function assertUsableAsParent(parent, childContext) {
15
15
  if (parent.context !== childContext) {
16
16
  throw new ViewComponentError("cannot add a child from another context");
17
17
  }
18
+ if (!childContext.hasComponent(parent)) {
19
+ throw new ViewComponentError("cannot add a child to a view component its context does not hold");
20
+ }
18
21
  }
19
22
  class ViewComponent {
23
+ static {
24
+ /**
25
+ * The last thing a component says before {@link ViewComponent#destroy} takes every subscription
26
+ * off it. An integration that put something on the instance — event subscriptions, an own
27
+ * `dispatchEvent` — hears it here and can set that up again on the same component; a context
28
+ * that takes the component back in (`vc.context = ctx`) revives it under the same uuid.
29
+ *
30
+ * It goes out directly on the component rather than through {@link ViewComponent#dispatchEvent},
31
+ * so an own `dispatchEvent` an integration installed does not see it and cannot carry it further.
32
+ * Leaving a context says nothing: `vc.context = null` detaches the component and keeps everything
33
+ * on it.
34
+ *
35
+ * A listener that throws is reported through `console.warn` and costs neither the listeners
36
+ * behind it their turn nor the component the rest of its teardown.
37
+ *
38
+ * Not to be confused with the `Destroyed` exported at the top level of the package: that one is
39
+ * `'destroyed'` and belongs to the worker channel, this one is `'view-component-destroyed'`.
40
+ */
41
+ this.Destroyed = "view-component-destroyed";
42
+ }
20
43
  #uuid;
21
44
  #token;
22
45
  #context;
@@ -49,9 +72,14 @@ class ViewComponent {
49
72
  return this.#context;
50
73
  }
51
74
  /**
52
- * @throws {ComponentContextDisposedError} if the new context has been disposed. The component
53
- * keeps its current context in that case: leaving the old one is only worth it if the new
54
- * one can actually be joined.
75
+ * @throws {ComponentContextDisposedError} if the new {@link ComponentContext} has been disposed.
76
+ * The component keeps its current one in that case: leaving the old one is only worth it if
77
+ * the new one can actually be joined.
78
+ * @throws {ComponentUuidInUseError} if another component of the new {@link ComponentContext}
79
+ * holds this component's uuid. A uuid names one component of a {@link ComponentContext} at a
80
+ * time, and it is free again once its holder has left. This one is thrown after the old
81
+ * {@link ComponentContext} has been left, so the component holds none afterwards — assign one
82
+ * again to take it back in.
55
83
  */
56
84
  set context(context) {
57
85
  const next = context ?? void 0;
@@ -61,8 +89,9 @@ class ViewComponent {
61
89
  `the view component ${this.#uuid} cannot join the component context because it has been disposed`
62
90
  );
63
91
  }
92
+ const previousContext = this.#context;
64
93
  if (this.#context) {
65
- this.destroy();
94
+ this.#leaveContext();
66
95
  }
67
96
  this.#context = next;
68
97
  try {
@@ -71,15 +100,25 @@ class ViewComponent {
71
100
  this.#context = void 0;
72
101
  throw error;
73
102
  }
103
+ if (next != null && previousContext != null) {
104
+ previousContext.transferPropertiesTo(this, next);
105
+ }
74
106
  }
75
107
  /**
76
108
  * A destroyed component is detached from its {@link ComponentContext}: it no longer appears
77
109
  * in any change trail and no longer has a corresponding entity.
78
110
  *
79
- * Every mutation that only concerns the component itself (`token`, `order`, properties, events,
80
- * `removeFromParent`, `destroy`) is silently ignored while destroyed. Operations that would tie
81
- * a second, live component to it (`addChild`, the `parent` setter) throw instead, because
82
- * ignoring them would leave the caller with a wrong picture of the entity tree.
111
+ * Every mutation that only concerns the component itself (`token`, `order`, properties,
112
+ * `removeFromParent`) is silently ignored while destroyed. Operations that would tie a second,
113
+ * live component to it (`addChild`, the `parent` setter) throw instead, because ignoring them
114
+ * would leave the caller with a wrong picture of the entity tree.
115
+ * {@link ViewComponent#destroy} finds nothing left to detach and still takes off whatever lies
116
+ * on the component at that moment.
117
+ *
118
+ * Assigning `null` or `undefined` to {@link ViewComponent#context} reports the same state: it
119
+ * detaches the component without silencing it, so {@link ViewComponent#dispatchEvent} reaches
120
+ * every listener on it. After a {@link ViewComponent#destroy} it reaches those registered since.
121
+ * Children are not traversed either way.
83
122
  *
84
123
  * Assigning a {@link ViewComponent#context} revives the component under the same uuid.
85
124
  */
@@ -132,10 +171,10 @@ class ViewComponent {
132
171
  }
133
172
  removeFromParent() {
134
173
  if (this.#parent) {
135
- this.#context?.removeFromParent(this.uuid, this.#parent);
174
+ this.#context?.removeFromParent(this, this.#parent);
136
175
  this.#parent = void 0;
137
176
  } else {
138
- this.#context?.moveToRoot(this.uuid);
177
+ this.#context?.moveToRoot(this);
139
178
  }
140
179
  }
141
180
  addChild(child) {
@@ -163,6 +202,16 @@ class ViewComponent {
163
202
  setProperty(name, value, isEqual) {
164
203
  return this.#context?.setProperty(this, name, value, isEqual) ?? false;
165
204
  }
205
+ /**
206
+ * Mark a property as set without giving it a value. The entity holds the key and reads it as
207
+ * `undefined`; {@link ViewComponent.setProperty} with `undefined` removes it instead.
208
+ *
209
+ * @returns `true` if this differs from the last value written to the change trail.
210
+ * A destroyed component always returns `false`.
211
+ */
212
+ setPropertyWithoutValue(name) {
213
+ return this.#context?.setPropertyWithoutValue(this, name) ?? false;
214
+ }
166
215
  removeProperty(name) {
167
216
  this.#context?.removeProperty(this, name);
168
217
  }
@@ -170,17 +219,44 @@ class ViewComponent {
170
219
  this.#context?.dispatchShadowObjectsEvent(this, type, data, transferables);
171
220
  }
172
221
  dispatchEvent(type, data, traverseChildren) {
173
- emit(this, type, data);
222
+ emitSafe(this, type, data);
174
223
  if (traverseChildren) {
175
224
  for (const child of this.#context?.getChildren(this) ?? []) {
176
225
  child.dispatchEvent(type, data, traverseChildren);
177
226
  }
178
227
  }
179
228
  }
180
- destroy() {
229
+ /**
230
+ * Leave the context without ending the component. What a consumer put on the instance — event
231
+ * subscriptions, an own `dispatchEvent` — belongs to the component and not to its membership in
232
+ * a context, so a component that is taken back in keeps answering with all of it.
233
+ */
234
+ #leaveContext() {
181
235
  this.removeFromParent();
182
- this.#context?.destroyComponent(this);
236
+ const context = this.#context;
183
237
  this.#context = void 0;
238
+ context?.destroyComponent(this);
239
+ }
240
+ /**
241
+ * End the component: it leaves its {@link ComponentContext} and goes silent. Every `on()` and
242
+ * `once()` subscription made on it is removed, and a `dispatchEvent` an integration installed on
243
+ * the instance is dropped with them. A promise from `onceAsync()` is not reached and settles
244
+ * only when the event it waits for arrives.
245
+ *
246
+ * Calling it more than once is safe. Each call takes off what is on the component at that
247
+ * moment — a subscription made afterwards is heard again.
248
+ *
249
+ * {@link ViewComponent.Destroyed} goes out on the component right before the subscriptions come
250
+ * off, so whoever holds something on it hears it. A listener that wants its subscription back
251
+ * does not set it up inside the handler — that one would come off with the rest.
252
+ */
253
+ destroy() {
254
+ this.#leaveContext();
255
+ if (Object.hasOwn(this, "dispatchEvent")) {
256
+ delete this.dispatchEvent;
257
+ }
258
+ emitSafe(this, ViewComponent.Destroyed);
259
+ off(this);
184
260
  }
185
261
  }
186
262
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/view/ViewComponent.ts"],
4
- "sourcesContent": ["import {emit, eventize} from '@spearwolf/eventize';\nimport {VoidToken} from '../constants.js';\nimport {generateUUID} from '../utils/generateUUID.js';\nimport {ComponentContext, ComponentContextDisposedError} from './ComponentContext.js';\n\nclass ViewComponentError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ViewComponentError';\n }\n}\n\n/**\n * Guards every path that attaches a child to a parent: the constructor, {@link ViewComponent#addChild}\n * and the `parent` setter all funnel through here so that they fail with the same message.\n */\nfunction assertUsableAsParent(parent: ViewComponent, childContext: ComponentContext | undefined) {\n if (parent.context == null) {\n throw new ViewComponentError('cannot add a child to a destroyed view component');\n }\n if (parent.context !== childContext) {\n throw new ViewComponentError('cannot add a child from another context');\n }\n}\n\nexport class ViewComponent {\n readonly #uuid: string;\n\n #token: string;\n\n #context?: ComponentContext;\n\n #parent?: ViewComponent;\n #order = 0;\n readonly #autoDestructionOnParentRemoval: boolean;\n\n get uuid() {\n return this.#uuid;\n }\n\n get token() {\n return this.#token;\n }\n\n set token(token: string | undefined) {\n token ??= VoidToken;\n if (token === this.#token) return;\n this.#token = token;\n this.#context?.changeToken(this, token);\n }\n\n get parent(): ViewComponent | undefined {\n return this.#parent;\n }\n\n set parent(parent: ViewComponent | null | undefined) {\n if (parent) {\n parent.addChild(this);\n } else {\n this.removeFromParent();\n }\n }\n\n get context(): ComponentContext | undefined {\n return this.#context;\n }\n\n /**\n * @throws {ComponentContextDisposedError} if the new context has been disposed. The component\n * keeps its current context in that case: leaving the old one is only worth it if the new\n * one can actually be joined.\n */\n set context(context: ComponentContext | null | undefined) {\n const next = context ?? undefined;\n if (this.#context === next) return;\n\n // checked before the teardown below, so a rejected join costs the component nothing\n if (next?.isDisposed) {\n throw new ComponentContextDisposedError(\n `the view component ${this.#uuid} cannot join the component context because it has been disposed`,\n );\n }\n\n if (this.#context) {\n this.destroy();\n }\n\n this.#context = next;\n\n try {\n next?.addComponent(this);\n } catch (error) {\n // a failed join must not leave a destroyed component pointing at a context that never\n // took it in \u2014 every later mutation would silently go nowhere while `isDestroyed` lies\n this.#context = undefined;\n throw error;\n }\n }\n\n /**\n * A destroyed component is detached from its {@link ComponentContext}: it no longer appears\n * in any change trail and no longer has a corresponding entity.\n *\n * Every mutation that only concerns the component itself (`token`, `order`, properties, events,\n * `removeFromParent`, `destroy`) is silently ignored while destroyed. Operations that would tie\n * a second, live component to it (`addChild`, the `parent` setter) throw instead, because\n * ignoring them would leave the caller with a wrong picture of the entity tree.\n *\n * Assigning a {@link ViewComponent#context} revives the component under the same uuid.\n */\n get isDestroyed(): boolean {\n return this.#context == null;\n }\n\n /**\n * The order property sets the order to lay out a component in a children array of the parent component.\n *\n * Items in a children array are sorted by ascending order value and then by their insertion order.\n */\n get order(): number {\n return this.#order;\n }\n\n set order(order: number | null | undefined) {\n const prevOrder = this.#order;\n this.#order = order ?? 0;\n if (prevOrder !== this.#order) {\n this.#context?.changeOrder(this);\n }\n }\n\n /**\n * Whether the corresponding entity should be destroyed when its parent entity is destroyed.\n *\n * Set via the constructor option of the same name; immutable after creation.\n * Children that opt out (the default) are promoted to root entities when their\n * parent is destroyed, so they remain reachable.\n */\n get autoDestructionOnParentRemoval(): boolean {\n return this.#autoDestructionOnParentRemoval;\n }\n\n constructor(\n token: string,\n options?: {\n parent?: ViewComponent;\n order?: number;\n context?: ComponentContext;\n uuid?: string;\n autoDestructionOnParentRemoval?: boolean;\n },\n ) {\n eventize(this);\n\n if (options instanceof ViewComponent) {\n options = {parent: options};\n }\n\n this.#uuid = options?.uuid ?? generateUUID();\n\n this.#token = token ?? VoidToken;\n this.#order = options?.order ?? 0;\n this.#parent = options?.parent;\n this.#autoDestructionOnParentRemoval = options?.autoDestructionOnParentRemoval ?? false;\n\n const ctx = options?.context ?? ComponentContext.get();\n\n if (this.#parent) {\n assertUsableAsParent(this.#parent, ctx);\n }\n\n this.context = ctx;\n }\n\n isChildOf(parent: ViewComponent) {\n return this.#parent === parent;\n }\n\n removeFromParent() {\n if (this.#parent) {\n this.#context?.removeFromParent(this.uuid, this.#parent);\n this.#parent = undefined;\n } else {\n this.#context?.moveToRoot(this.uuid);\n }\n }\n\n addChild(child: ViewComponent) {\n if (child.#context == null) {\n throw new ViewComponentError('cannot add a destroyed view component as a child');\n }\n\n assertUsableAsParent(this, child.#context);\n\n // walking up via #parent is safe: the entity tree is kept acyclic by exactly this guard\n for (let ancestor: ViewComponent | undefined = this; ancestor != null; ancestor = ancestor.#parent) {\n if (ancestor === child) {\n throw new ViewComponentError(\n 'cannot add the component itself or one of its ancestors as a child: this would create a cycle',\n );\n }\n }\n\n if (!child.isChildOf(this)) {\n child.removeFromParent();\n child.#parent = this;\n this.#context!.addToChildren(this, child);\n }\n }\n\n /**\n * @returns `true` if the value differs from the last one written to the change trail.\n * A destroyed component always returns `false`.\n */\n setProperty<T = unknown>(name: string, value: T, isEqual?: (a: T, b: T) => boolean): boolean {\n return this.#context?.setProperty(this, name, value, isEqual) ?? false;\n }\n\n removeProperty(name: string) {\n this.#context?.removeProperty(this, name);\n }\n\n dispatchShadowObjectsEvent(type: string, data: unknown, transferables?: Transferable[]) {\n this.#context?.dispatchShadowObjectsEvent(this, type, data, transferables);\n }\n\n dispatchEvent(type: string, data: unknown, traverseChildren: boolean) {\n emit(this as ViewComponent, type, data);\n\n if (traverseChildren) {\n for (const child of this.#context?.getChildren(this) ?? []) {\n child.dispatchEvent(type, data, traverseChildren);\n }\n }\n }\n\n destroy() {\n this.removeFromParent();\n this.#context?.destroyComponent(this);\n this.#context = undefined;\n }\n}"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {emitSafe, eventize, off} from '@spearwolf/eventize';\nimport {VoidToken} from '../constants.js';\nimport {generateUUID} from '../utils/generateUUID.js';\nimport {ComponentContext, ComponentContextDisposedError} from './ComponentContext.js';\n\nclass ViewComponentError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ViewComponentError';\n }\n}\n\n/**\n * Guards every path that attaches a child to a parent: the constructor, {@link ViewComponent#addChild}\n * and the `parent` setter all funnel through here so that they fail with the same message.\n */\nfunction assertUsableAsParent(parent: ViewComponent, childContext: ComponentContext | undefined) {\n if (parent.context == null) {\n throw new ViewComponentError('cannot add a child to a destroyed view component');\n }\n if (parent.context !== childContext) {\n throw new ViewComponentError('cannot add a child from another context');\n }\n // the two checks above ask the instance which context it names; this one asks the context\n // whether it holds the instance. Both have to agree before a child is hung on the parent \u2014 a\n // parent the context does not hold would take the child into a children list nobody reads\n if (!childContext.hasComponent(parent)) {\n throw new ViewComponentError('cannot add a child to a view component its context does not hold');\n }\n}\n\ninterface ViewComponentOptions {\n parent?: ViewComponent | undefined;\n order?: number | undefined;\n context?: ComponentContext | undefined;\n uuid?: string | undefined;\n autoDestructionOnParentRemoval?: boolean | undefined;\n}\n\nexport class ViewComponent {\n /**\n * The last thing a component says before {@link ViewComponent#destroy} takes every subscription\n * off it. An integration that put something on the instance \u2014 event subscriptions, an own\n * `dispatchEvent` \u2014 hears it here and can set that up again on the same component; a context\n * that takes the component back in (`vc.context = ctx`) revives it under the same uuid.\n *\n * It goes out directly on the component rather than through {@link ViewComponent#dispatchEvent},\n * so an own `dispatchEvent` an integration installed does not see it and cannot carry it further.\n * Leaving a context says nothing: `vc.context = null` detaches the component and keeps everything\n * on it.\n *\n * A listener that throws is reported through `console.warn` and costs neither the listeners\n * behind it their turn nor the component the rest of its teardown.\n *\n * Not to be confused with the `Destroyed` exported at the top level of the package: that one is\n * `'destroyed'` and belongs to the worker channel, this one is `'view-component-destroyed'`.\n */\n static readonly Destroyed = 'view-component-destroyed';\n\n readonly #uuid: string;\n\n #token: string;\n\n #context?: ComponentContext | undefined;\n\n #parent?: ViewComponent | undefined;\n #order = 0;\n readonly #autoDestructionOnParentRemoval: boolean;\n\n get uuid() {\n return this.#uuid;\n }\n\n get token() {\n return this.#token;\n }\n\n set token(token: string | undefined) {\n token ??= VoidToken;\n if (token === this.#token) return;\n this.#token = token;\n this.#context?.changeToken(this, token);\n }\n\n get parent(): ViewComponent | undefined {\n return this.#parent;\n }\n\n set parent(parent: ViewComponent | null | undefined) {\n if (parent) {\n parent.addChild(this);\n } else {\n this.removeFromParent();\n }\n }\n\n get context(): ComponentContext | undefined {\n return this.#context;\n }\n\n /**\n * @throws {ComponentContextDisposedError} if the new {@link ComponentContext} has been disposed.\n * The component keeps its current one in that case: leaving the old one is only worth it if\n * the new one can actually be joined.\n * @throws {ComponentUuidInUseError} if another component of the new {@link ComponentContext}\n * holds this component's uuid. A uuid names one component of a {@link ComponentContext} at a\n * time, and it is free again once its holder has left. This one is thrown after the old\n * {@link ComponentContext} has been left, so the component holds none afterwards \u2014 assign one\n * again to take it back in.\n */\n set context(context: ComponentContext | null | undefined) {\n const next = context ?? undefined;\n if (this.#context === next) return;\n\n // checked before the teardown below, so a rejected join costs the component nothing\n if (next?.isDisposed) {\n throw new ComponentContextDisposedError(\n `the view component ${this.#uuid} cannot join the component context because it has been disposed`,\n );\n }\n\n const previousContext = this.#context;\n\n if (this.#context) {\n // leaving, not ending: an element that leaves the document hands its context back and takes\n // the same component in again when it returns, so the subscriptions on it have to survive\n this.#leaveContext();\n }\n\n this.#context = next;\n\n try {\n next?.addComponent(this);\n } catch (error) {\n // a failed join must not leave a destroyed component pointing at a context that never\n // took it in \u2014 every later mutation would silently go nowhere while `isDestroyed` lies\n this.#context = undefined;\n throw error;\n }\n\n // after the join, not before it: `addComponent` is what creates the ComponentChanges the\n // properties are written into, so they end up in the same CreateEntities change as the token\n // and the parent rather than in a separate one behind it. A move to no context carries\n // nothing \u2014 there is no receiver for it\n if (next != null && previousContext != null) {\n previousContext.transferPropertiesTo(this, next);\n }\n }\n\n /**\n * A destroyed component is detached from its {@link ComponentContext}: it no longer appears\n * in any change trail and no longer has a corresponding entity.\n *\n * Every mutation that only concerns the component itself (`token`, `order`, properties,\n * `removeFromParent`) is silently ignored while destroyed. Operations that would tie a second,\n * live component to it (`addChild`, the `parent` setter) throw instead, because ignoring them\n * would leave the caller with a wrong picture of the entity tree.\n * {@link ViewComponent#destroy} finds nothing left to detach and still takes off whatever lies\n * on the component at that moment.\n *\n * Assigning `null` or `undefined` to {@link ViewComponent#context} reports the same state: it\n * detaches the component without silencing it, so {@link ViewComponent#dispatchEvent} reaches\n * every listener on it. After a {@link ViewComponent#destroy} it reaches those registered since.\n * Children are not traversed either way.\n *\n * Assigning a {@link ViewComponent#context} revives the component under the same uuid.\n */\n get isDestroyed(): boolean {\n return this.#context == null;\n }\n\n /**\n * The order property sets the order to lay out a component in a children array of the parent component.\n *\n * Items in a children array are sorted by ascending order value and then by their insertion order.\n */\n get order(): number {\n return this.#order;\n }\n\n set order(order: number | null | undefined) {\n const prevOrder = this.#order;\n this.#order = order ?? 0;\n if (prevOrder !== this.#order) {\n this.#context?.changeOrder(this);\n }\n }\n\n /**\n * Whether the corresponding entity should be destroyed when its parent entity is destroyed.\n *\n * Set via the constructor option of the same name; immutable after creation.\n * Children that opt out (the default) are promoted to root entities when their\n * parent is destroyed, so they remain reachable.\n */\n get autoDestructionOnParentRemoval(): boolean {\n return this.#autoDestructionOnParentRemoval;\n }\n\n // the parent form is declared first on purpose: ViewComponent has a member for every optional\n // member of the options type, so without it the compiler resolves `new ViewComponent(t, parent)`\n // structurally against the options object and reads `uuid`, `order` and `context` off the parent\n // \u2014 a meaning the body does not have. An object literal is assignable to neither ViewComponent\n // (private fields) nor the first overload, and lands on the second one\n /**\n * Create a component: `new ViewComponent(token, parent)` hangs it under another component, and\n * `new ViewComponent(token, options)` takes `parent`, `order`, `context`, `uuid` and\n * `autoDestructionOnParentRemoval` as an object.\n *\n * A {@link ViewComponent} given in place of the options object is read as the parent and as\n * nothing else \u2014 `uuid`, `order` and `context` come from their usual sources, never from it.\n *\n * A component created without a `token` carries {@link VoidToken} (`'#void'`).\n */\n constructor(token?: string, parent?: ViewComponent);\n constructor(token?: string, options?: ViewComponentOptions);\n constructor(token?: string, options?: ViewComponent | ViewComponentOptions) {\n eventize(this);\n\n if (options instanceof ViewComponent) {\n options = {parent: options};\n }\n\n this.#uuid = options?.uuid ?? generateUUID();\n\n this.#token = token ?? VoidToken;\n this.#order = options?.order ?? 0;\n this.#parent = options?.parent;\n this.#autoDestructionOnParentRemoval = options?.autoDestructionOnParentRemoval ?? false;\n\n const ctx = options?.context ?? ComponentContext.get();\n\n if (this.#parent) {\n assertUsableAsParent(this.#parent, ctx);\n }\n\n this.context = ctx;\n }\n\n isChildOf(parent: ViewComponent) {\n return this.#parent === parent;\n }\n\n removeFromParent() {\n if (this.#parent) {\n this.#context?.removeFromParent(this, this.#parent);\n this.#parent = undefined;\n } else {\n this.#context?.moveToRoot(this);\n }\n }\n\n addChild(child: ViewComponent) {\n if (child.#context == null) {\n throw new ViewComponentError('cannot add a destroyed view component as a child');\n }\n\n assertUsableAsParent(this, child.#context);\n\n // walking up via #parent is safe: the entity tree is kept acyclic by exactly this guard\n for (let ancestor: ViewComponent | undefined = this; ancestor != null; ancestor = ancestor.#parent) {\n if (ancestor === child) {\n throw new ViewComponentError(\n 'cannot add the component itself or one of its ancestors as a child: this would create a cycle',\n );\n }\n }\n\n if (!child.isChildOf(this)) {\n child.removeFromParent();\n child.#parent = this;\n this.#context!.addToChildren(this, child);\n }\n }\n\n /**\n * @returns `true` if the value differs from the last one written to the change trail.\n * A destroyed component always returns `false`.\n */\n setProperty<T = unknown>(name: string, value: T, isEqual?: (a: T, b: T) => boolean): boolean {\n return this.#context?.setProperty(this, name, value, isEqual) ?? false;\n }\n\n /**\n * Mark a property as set without giving it a value. The entity holds the key and reads it as\n * `undefined`; {@link ViewComponent.setProperty} with `undefined` removes it instead.\n *\n * @returns `true` if this differs from the last value written to the change trail.\n * A destroyed component always returns `false`.\n */\n setPropertyWithoutValue(name: string): boolean {\n return this.#context?.setPropertyWithoutValue(this, name) ?? false;\n }\n\n removeProperty(name: string) {\n this.#context?.removeProperty(this, name);\n }\n\n dispatchShadowObjectsEvent(type: string, data: unknown, transferables?: Transferable[]) {\n this.#context?.dispatchShadowObjectsEvent(this, type, data, transferables);\n }\n\n dispatchEvent(type: string, data: unknown, traverseChildren: boolean) {\n // `emitSafe()`: this is the delivery path of everything the shadow environment sends to the\n // view, and it is a fan-out twice over -- over the listeners of one component, and, with\n // `traverseChildren`, over a whole subtree. Under the plain dispatch one listener that throws\n // would end the delivery and cut off every component below it, and the error would leave\n // through the message channel that has no caller left to catch it. A failure is reported\n // through `console.warn` and costs only itself.\n emitSafe(this as ViewComponent, type, data);\n\n if (traverseChildren) {\n for (const child of this.#context?.getChildren(this) ?? []) {\n child.dispatchEvent(type, data, traverseChildren);\n }\n }\n }\n\n /**\n * Leave the context without ending the component. What a consumer put on the instance \u2014 event\n * subscriptions, an own `dispatchEvent` \u2014 belongs to the component and not to its membership in\n * a context, so a component that is taken back in keeps answering with all of it.\n */\n #leaveContext() {\n this.removeFromParent();\n\n // the context pointer goes first: destroyComponent() detaches every component that still\n // names the context, and would call destroy() on it otherwise\n const context = this.#context;\n this.#context = undefined;\n context?.destroyComponent(this);\n }\n\n /**\n * End the component: it leaves its {@link ComponentContext} and goes silent. Every `on()` and\n * `once()` subscription made on it is removed, and a `dispatchEvent` an integration installed on\n * the instance is dropped with them. A promise from `onceAsync()` is not reached and settles\n * only when the event it waits for arrives.\n *\n * Calling it more than once is safe. Each call takes off what is on the component at that\n * moment \u2014 a subscription made afterwards is heard again.\n *\n * {@link ViewComponent.Destroyed} goes out on the component right before the subscriptions come\n * off, so whoever holds something on it hears it. A listener that wants its subscription back\n * does not set it up inside the handler \u2014 that one would come off with the rest.\n */\n destroy() {\n this.#leaveContext();\n\n // an integration may shadow `dispatchEvent` on the instance; dropping the own property\n // uncovers the method on the prototype again, it does not remove it\n if (Object.hasOwn(this, 'dispatchEvent')) {\n delete (this as {dispatchEvent?: ViewComponent['dispatchEvent']}).dispatchEvent;\n }\n\n // last, and before the subscriptions come off: the component is in its final shape by now, and\n // whoever hears this still hears it. Guarded, because the line below is the point of the whole\n // method: under the plain dispatch a listener that throws would leave through `destroy()` and\n // take the `off()` with it, and the component would end its life holding every subscription it\n // was supposed to be rid of.\n emitSafe(this as ViewComponent, ViewComponent.Destroyed);\n\n off(this);\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/view/cloneChangeTrail.ts"],
4
- "sourcesContent": ["import type {ChangeTrailType} from '../types.js';\n\nexport function cloneChangeTrail(trails: ChangeTrailType): ChangeTrailType {\n return trails.map((trail) => {\n if (trail.transferables && trail.transferables.length > 0) {\n const {transferables, ...data} = trail;\n return structuredClone(data, {transfer: transferables as any});\n } else {\n return structuredClone(trail);\n }\n });\n}"],
5
- "mappings": "AAEO,SAAS,iBAAiB,QAA0C;AACzE,SAAO,OAAO,IAAI,CAAC,UAAU;AAC3B,QAAI,MAAM,iBAAiB,MAAM,cAAc,SAAS,GAAG;AACzD,YAAM,EAAC,eAAe,GAAG,KAAI,IAAI;AACjC,aAAO,gBAAgB,MAAM,EAAC,UAAU,cAAoB,CAAC;AAAA,IAC/D,OAAO;AACL,aAAO,gBAAgB,KAAK;AAAA,IAC9B;AAAA,EACF,CAAC;AACH;",
4
+ "sourcesContent": ["import type {ChangeTrailType} from '../types.js';\n\nexport function cloneChangeTrail(trails: ChangeTrailType): ChangeTrailType {\n return trails.map((trail) => {\n if (trail.transferables && trail.transferables.length > 0) {\n const {transferables, ...data} = trail;\n return structuredClone(data, {transfer: transferables});\n } else {\n return structuredClone(trail);\n }\n });\n}\n"],
5
+ "mappings": "AAEO,SAAS,iBAAiB,QAA0C;AACzE,SAAO,OAAO,IAAI,CAAC,UAAU;AAC3B,QAAI,MAAM,iBAAiB,MAAM,cAAc,SAAS,GAAG;AACzD,YAAM,EAAC,eAAe,GAAG,KAAI,IAAI;AACjC,aAAO,gBAAgB,MAAM,EAAC,UAAU,cAAa,CAAC;AAAA,IACxD,OAAO;AACL,aAAO,gBAAgB,KAAK;AAAA,IAC9B;AAAA,EACF,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -1,10 +1,24 @@
1
1
  import { Kernel, type MessageToViewEvent } from '../in-the-dark/Kernel.js';
2
+ import { ConsoleLogger } from '../utils/ConsoleLogger.js';
3
+ /**
4
+ * A payload this side can read is an object: every branch below takes a `type` off it and
5
+ * then reads further fields. `null`, `undefined`, a number or a string come from someone who
6
+ * does not speak this protocol, and reading through them takes the whole worker down over one
7
+ * message. Deliberately a plain boolean rather than a type predicate: `event.data` is `any` on
8
+ * both call sites, and narrowing it would only cost the branches below the payload types they
9
+ * already have.
10
+ */
11
+ export declare const isReadableMessageData: (data: unknown) => boolean;
2
12
  export interface MessageRouterOptions {
3
13
  kernel?: Kernel;
4
14
  postMessage?: typeof self.postMessage;
5
15
  }
6
16
  export declare class MessageRouter {
7
17
  #private;
18
+ /** The logger this router reports through. */
19
+ get logger(): ConsoleLogger;
20
+ /** Whether this router has been torn down. Once it is, every message that reaches it is discarded. */
21
+ get isDestroyed(): boolean;
8
22
  kernel: Kernel;
9
23
  postMessage: typeof self.postMessage;
10
24
  constructor(options?: MessageRouterOptions);
@@ -1 +1 @@
1
- {"version":3,"file":"MessageRouter.d.ts","sourceRoot":"","sources":["../../../src/worker/MessageRouter.ts"],"names":[],"mappings":"AAYA,OAAO,EAAC,MAAM,EAAE,KAAK,kBAAkB,EAAC,MAAM,0BAA0B,CAAC;AAQzE,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,IAAI,CAAC,WAAW,CAAC;CACvC;AAED,qBAAa,aAAa;;IAGxB,MAAM,EAAE,MAAM,CAAC;IAEf,WAAW,EAAE,OAAO,IAAI,CAAC,WAAW,CAAC;gBAEzB,OAAO,CAAC,EAAE,oBAAoB;IAQ1C,KAAK,CAAC,KAAK,EAAE,YAAY;IAmBzB,eAAe,CAAC,KAAK,EAAE,kBAAkB;CA+C1C"}
1
+ {"version":3,"file":"MessageRouter.d.ts","sourceRoot":"","sources":["../../../src/worker/MessageRouter.ts"],"names":[],"mappings":"AAaA,OAAO,EAAC,MAAM,EAAE,KAAK,kBAAkB,EAAC,MAAM,0BAA0B,CAAC;AAEzE,OAAO,EAAC,aAAa,EAAC,MAAM,2BAA2B,CAAC;AAgBxD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,SAAU,OAAO,KAAG,OAAoD,CAAC;AA2B3G,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,IAAI,CAAC,WAAW,CAAC;CACvC;AAED,qBAAa,aAAa;;IAcxB,8CAA8C;IAC9C,IAAI,MAAM,IAAI,aAAa,CAE1B;IAID,sGAAsG;IACtG,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,MAAM,EAAE,MAAM,CAAC;IAEf,WAAW,EAAE,OAAO,IAAI,CAAC,WAAW,CAAC;IAErC,YAAY,OAAO,CAAC,EAAE,oBAAoB,EAMzC;IAED,KAAK,CAAC,KAAK,EAAE,YAAY,QAiCxB;IAED,eAAe,CAAC,KAAK,EAAE,kBAAkB,QAIxC;CAyFF"}
@@ -1,4 +1,5 @@
1
1
  import { off, on } from "@spearwolf/eventize";
2
+ import { ChangeTrailRefusedError } from "../ChangeTrailRefusedError.js";
2
3
  import {
3
4
  AppliedChangeTrail,
4
5
  ChangeTrail,
@@ -9,34 +10,71 @@ import {
9
10
  MessageToView,
10
11
  ShadowObjectsExport
11
12
  } from "../constants.js";
12
- import { importModule } from "../in-the-dark/importModule.js";
13
+ import { importModule, missingShadowObjectsExportMessage } from "../in-the-dark/importModule.js";
13
14
  import { Kernel } from "../in-the-dark/Kernel.js";
15
+ import { ConsoleLogger } from "../utils/ConsoleLogger.js";
14
16
  import { toUrlString } from "../utils/toUrlString.js";
17
+ const isReadableMessageData = (data) => typeof data === "object" && data !== null;
18
+ const describeError = (error) => {
19
+ try {
20
+ return error instanceof Error ? { error: error.message || String(error), errorName: error.name } : { error: String(error) || "unknown error" };
21
+ } catch {
22
+ return { error: "an error that cannot be described" };
23
+ }
24
+ };
15
25
  class MessageRouter {
16
26
  #importedModules = /* @__PURE__ */ new Set();
27
+ /**
28
+ * Built in the field initializer rather than on first read, the way the lazy getter
29
+ * `WorkerRuntime.logger` has to be: a `MessageRouter` is only ever built from
30
+ * `WorkerRuntime.onmessage`, and only past the branch that answers the `CONSOLE_LOGGER`
31
+ * configuration message and returns -- so by the time this initializer runs, that configuration
32
+ * has already been installed. The `Kernel` this router holds builds its own logger the same way,
33
+ * in its own field initializer, on the same guarantee; which of the two is built first is not
34
+ * something either one depends on.
35
+ */
36
+ #logger = new ConsoleLogger("MessageRouter");
37
+ /** The logger this router reports through. */
38
+ get logger() {
39
+ return this.#logger;
40
+ }
41
+ #isDestroyed = false;
42
+ /** Whether this router has been torn down. Once it is, every message that reaches it is discarded. */
43
+ get isDestroyed() {
44
+ return this.#isDestroyed;
45
+ }
17
46
  constructor(options) {
18
47
  this.kernel = options?.kernel ?? new Kernel();
19
48
  this.postMessage = options?.postMessage ?? self.postMessage.bind(self);
20
49
  on(this.kernel, MessageToView, "onMessageToView", this);
21
50
  }
22
51
  route(event) {
23
- switch (event.data.type) {
52
+ const data = event.data;
53
+ if (!isReadableMessageData(data)) {
54
+ this.logger.debug("discarding a message it cannot read", data);
55
+ return;
56
+ }
57
+ if (this.#isDestroyed) {
58
+ this.logger.debug("discarding a message that arrived after the teardown", data.type);
59
+ return;
60
+ }
61
+ switch (data.type) {
24
62
  case Configure:
25
- this.#configure(event.data);
63
+ this.#configure(data);
26
64
  break;
27
65
  case ChangeTrail:
28
- this.#onChangeTrail(event.data);
66
+ this.#onChangeTrail(data);
29
67
  break;
30
68
  case Destroy:
31
- this.#onDestroy(event.data);
69
+ this.#onDestroy(data);
32
70
  break;
33
71
  default:
34
- console.warn("[MessageRouter] unknown message", event.data.type ?? event.data);
72
+ this.logger.warn("unknown message", data.type ?? data);
35
73
  }
36
74
  }
37
75
  onMessageToView(event) {
38
76
  const { transferables: transfer, ...data } = event;
39
- this.postMessage({ type: MessageToView, data }, { transfer });
77
+ this.postMessage({ type: MessageToView, data }, { transfer: transfer ?? [] });
40
78
  }
41
79
  async #configure(data) {
42
80
  const url = data.importModule;
@@ -46,6 +84,10 @@ class MessageRouter {
46
84
  /* @vite-ignore */
47
85
  toUrlString(url)
48
86
  );
87
+ if (this.#isDestroyed) {
88
+ this.logger.debug("discarding a module that arrived after the teardown", url);
89
+ return;
90
+ }
49
91
  if (module[ShadowObjectsExport]) {
50
92
  await importModule(this.kernel, module[ShadowObjectsExport], this.#importedModules);
51
93
  this.postMessage({ type: ImportedModule, url });
@@ -53,33 +95,51 @@ class MessageRouter {
53
95
  this.postMessage({
54
96
  type: ImportedModule,
55
97
  url,
56
- error: `module has no "${ShadowObjectsExport}" export`
98
+ error: missingShadowObjectsExportMessage
57
99
  });
58
100
  }
59
101
  } catch (error) {
60
- console.error("[MessageRouter] failed to import module", error);
61
- this.postMessage({ type: ImportedModule, url, error: `${error}` });
102
+ this.logger.error("failed to import module", error);
103
+ this.postMessage({ type: ImportedModule, url, ...describeError(error) });
62
104
  }
63
105
  }
64
106
  #onChangeTrail(data) {
65
107
  try {
66
108
  this.kernel.run(data);
67
109
  } catch (error) {
68
- console.error("[MessageRouter] failed to apply change trail", error);
69
- this.postMessage({ type: AppliedChangeTrail, serial: data.serial, error: `${error}` });
110
+ this.logger.error("failed to apply change trail", error);
111
+ if (data.serial != null) {
112
+ const refusal = error instanceof ChangeTrailRefusedError ? error : void 0;
113
+ this.postMessage({
114
+ type: AppliedChangeTrail,
115
+ serial: data.serial,
116
+ // what the entry threw, not the refusal wrapped around it: the number travels in a
117
+ // field of its own, so the reason stays the reason
118
+ ...describeError(refusal?.cause ?? error),
119
+ ...refusal ? { appliedCount: refusal.appliedCount } : {}
120
+ });
121
+ }
122
+ return;
70
123
  }
71
- if (data.serial) {
124
+ if (data.serial != null) {
72
125
  this.postMessage({ type: AppliedChangeTrail, serial: data.serial });
73
126
  }
74
127
  }
75
128
  #onDestroy(data) {
76
- console.debug("[MessageRouter] on destroy", data);
129
+ this.logger.debug("on destroy", data);
130
+ this.#isDestroyed = true;
77
131
  off(this.kernel, this);
132
+ try {
133
+ this.kernel.destroy();
134
+ } catch (error) {
135
+ this.logger.error("failed to tear the kernel down", error);
136
+ }
78
137
  this.#importedModules.clear();
79
138
  this.postMessage({ type: Destroyed });
80
139
  }
81
140
  }
82
141
  export {
83
- MessageRouter
142
+ MessageRouter,
143
+ isReadableMessageData
84
144
  };
85
145
  //# sourceMappingURL=MessageRouter.js.map