@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.
- package/CHANGELOG.md +320 -61
- package/README.md +57 -13
- package/bundle.js +5 -54
- package/package.json +7 -3
- package/src/ChangeTrailRefusedError.d.ts +27 -0
- package/src/ChangeTrailRefusedError.d.ts.map +1 -0
- package/src/ChangeTrailRefusedError.js +12 -0
- package/src/ChangeTrailRefusedError.js.map +7 -0
- package/src/EntityUuidInUseError.d.ts +19 -0
- package/src/EntityUuidInUseError.d.ts.map +1 -0
- package/src/EntityUuidInUseError.js +11 -0
- package/src/EntityUuidInUseError.js.map +7 -0
- package/src/WorkerTimeoutError.d.ts +25 -0
- package/src/WorkerTimeoutError.d.ts.map +1 -0
- package/src/WorkerTimeoutError.js +12 -0
- package/src/WorkerTimeoutError.js.map +7 -0
- package/src/bundle.d.ts +1 -1
- package/src/bundle.d.ts.map +1 -1
- package/src/bundle.js +1 -1
- package/src/bundle.js.map +2 -2
- package/src/constants.d.ts +9 -2
- package/src/constants.d.ts.map +1 -1
- package/src/constants.js +1 -1
- package/src/constants.js.map +2 -2
- package/src/create-worker.bundle.d.ts +1 -1
- package/src/create-worker.bundle.d.ts.map +1 -1
- package/src/create-worker.bundle.js.map +1 -1
- package/src/create-worker.d.ts +1 -1
- package/src/create-worker.d.ts.map +1 -1
- package/src/create-worker.js.map +1 -1
- package/src/elements/ShaeElement.d.ts +70 -2
- package/src/elements/ShaeElement.d.ts.map +1 -1
- package/src/elements/ShaeElement.js +129 -25
- package/src/elements/ShaeElement.js.map +3 -3
- package/src/elements/ShaeEntElement.d.ts +39 -1
- package/src/elements/ShaeEntElement.d.ts.map +1 -1
- package/src/elements/ShaeEntElement.js +397 -167
- package/src/elements/ShaeEntElement.js.map +2 -2
- package/src/elements/ShaeLifecycleElement.d.ts +71 -0
- package/src/elements/ShaeLifecycleElement.d.ts.map +1 -0
- package/src/elements/ShaeLifecycleElement.js +83 -0
- package/src/elements/ShaeLifecycleElement.js.map +7 -0
- package/src/elements/ShaePropElement.d.ts +52 -2
- package/src/elements/ShaePropElement.d.ts.map +1 -1
- package/src/elements/ShaePropElement.js +240 -219
- package/src/elements/ShaePropElement.js.map +2 -2
- package/src/elements/ShaeWorkerElement.d.ts +49 -5
- package/src/elements/ShaeWorkerElement.d.ts.map +1 -1
- package/src/elements/ShaeWorkerElement.js +204 -73
- package/src/elements/ShaeWorkerElement.js.map +2 -2
- package/src/elements/constants.d.ts +6 -0
- package/src/elements/constants.d.ts.map +1 -1
- package/src/elements/constants.js +12 -0
- package/src/elements/constants.js.map +2 -2
- package/src/elements/deferredTeardown.d.ts +24 -0
- package/src/elements/deferredTeardown.d.ts.map +1 -0
- package/src/elements/deferredTeardown.js +30 -0
- package/src/elements/deferredTeardown.js.map +7 -0
- package/src/elements/displayContentsRule.d.ts +25 -0
- package/src/elements/displayContentsRule.d.ts.map +1 -0
- package/src/elements/displayContentsRule.js +37 -0
- package/src/elements/displayContentsRule.js.map +7 -0
- package/src/elements/events.d.ts +10 -7
- package/src/elements/events.d.ts.map +1 -1
- package/src/elements/forwardCustomEvents.d.ts +19 -0
- package/src/elements/forwardCustomEvents.d.ts.map +1 -0
- package/src/elements/forwardCustomEvents.js +47 -0
- package/src/elements/forwardCustomEvents.js.map +7 -0
- package/src/elements/hostedSlots.d.ts +36 -0
- package/src/elements/hostedSlots.d.ts.map +1 -0
- package/src/elements/hostedSlots.js +118 -0
- package/src/elements/hostedSlots.js.map +7 -0
- package/src/elements/parentRemoval.d.ts +18 -0
- package/src/elements/parentRemoval.d.ts.map +1 -0
- package/src/elements/parentRemoval.js +60 -0
- package/src/elements/parentRemoval.js.map +7 -0
- package/src/elements/propValueConverters.d.ts +3 -0
- package/src/elements/propValueConverters.d.ts.map +1 -0
- package/src/elements/propValueConverters.js +54 -0
- package/src/elements/propValueConverters.js.map +7 -0
- package/src/elements/requestEntAncestor.d.ts +32 -0
- package/src/elements/requestEntAncestor.d.ts.map +1 -0
- package/src/elements/requestEntAncestor.js +16 -0
- package/src/elements/requestEntAncestor.js.map +7 -0
- package/src/elements.js.map +1 -1
- package/src/in-the-dark/Entity.d.ts +113 -3
- package/src/in-the-dark/Entity.d.ts.map +1 -1
- package/src/in-the-dark/Entity.js +292 -63
- package/src/in-the-dark/Entity.js.map +3 -3
- package/src/in-the-dark/Kernel.d.ts +76 -17
- package/src/in-the-dark/Kernel.d.ts.map +1 -1
- package/src/in-the-dark/Kernel.js +410 -378
- package/src/in-the-dark/Kernel.js.map +3 -3
- package/src/in-the-dark/Registry.d.ts.map +1 -1
- package/src/in-the-dark/Registry.js +83 -14
- package/src/in-the-dark/Registry.js.map +2 -2
- package/src/in-the-dark/ShadowObject.d.ts.map +1 -1
- package/src/in-the-dark/ShadowObject.js +1 -0
- package/src/in-the-dark/ShadowObject.js.map +2 -2
- package/src/in-the-dark/ShadowObjectCreationScope.d.ts +126 -0
- package/src/in-the-dark/ShadowObjectCreationScope.d.ts.map +1 -0
- package/src/in-the-dark/ShadowObjectCreationScope.js +596 -0
- package/src/in-the-dark/ShadowObjectCreationScope.js.map +7 -0
- package/src/in-the-dark/SignalsPath.d.ts.map +1 -1
- package/src/in-the-dark/SignalsPath.js +2 -2
- package/src/in-the-dark/SignalsPath.js.map +2 -2
- package/src/in-the-dark/events.js.map +1 -1
- package/src/in-the-dark/importModule.d.ts +7 -0
- package/src/in-the-dark/importModule.d.ts.map +1 -1
- package/src/in-the-dark/importModule.js +5 -2
- package/src/in-the-dark/importModule.js.map +2 -2
- package/src/index.d.ts +7 -1
- package/src/index.d.ts.map +1 -1
- package/src/index.js +5 -1
- package/src/index.js.map +2 -2
- package/src/shadow-objects.d.ts +2 -0
- package/src/shadow-objects.d.ts.map +1 -1
- package/src/shadow-objects.js +2 -0
- package/src/shadow-objects.js.map +2 -2
- package/src/shadow-objects.worker.js +0 -1
- package/src/shadow-objects.worker.js.map +2 -2
- package/src/shae-ent.js.map +1 -1
- package/src/shae-prop.js +2 -2
- package/src/shae-prop.js.map +2 -2
- package/src/shae-worker.js.map +1 -1
- package/src/types.d.ts +35 -4
- package/src/types.d.ts.map +1 -1
- package/src/utils/ConsoleLogger.d.ts +45 -10
- package/src/utils/ConsoleLogger.d.ts.map +1 -1
- package/src/utils/ConsoleLogger.js +51 -26
- package/src/utils/ConsoleLogger.js.map +2 -2
- package/src/utils/FrameLoop.d.ts +42 -3
- package/src/utils/FrameLoop.d.ts.map +1 -1
- package/src/utils/FrameLoop.js +71 -10
- package/src/utils/FrameLoop.js.map +2 -2
- package/src/utils/MicrotaskCollector.d.ts +45 -0
- package/src/utils/MicrotaskCollector.d.ts.map +1 -0
- package/src/utils/MicrotaskCollector.js +33 -0
- package/src/utils/MicrotaskCollector.js.map +7 -0
- package/src/utils/MicrotaskGate.d.ts +17 -0
- package/src/utils/MicrotaskGate.d.ts.map +1 -0
- package/src/utils/MicrotaskGate.js +20 -0
- package/src/utils/MicrotaskGate.js.map +7 -0
- package/src/utils/array-utils.js.map +1 -1
- package/src/utils/attr-utils.d.ts +6 -0
- package/src/utils/attr-utils.d.ts.map +1 -1
- package/src/utils/attr-utils.js +3 -1
- package/src/utils/attr-utils.js.map +2 -2
- package/src/utils/constants.js.map +1 -1
- package/src/utils/generateUUID.d.ts +11 -0
- package/src/utils/generateUUID.d.ts.map +1 -1
- package/src/utils/generateUUID.js +31 -266
- package/src/utils/generateUUID.js.map +2 -2
- package/src/utils/props-utils.d.ts +3 -2
- package/src/utils/props-utils.d.ts.map +1 -1
- package/src/utils/props-utils.js +13 -24
- package/src/utils/props-utils.js.map +2 -2
- package/src/utils/runGuarded.d.ts +27 -0
- package/src/utils/runGuarded.d.ts.map +1 -0
- package/src/utils/runGuarded.js +11 -0
- package/src/utils/runGuarded.js.map +7 -0
- package/src/utils/toMaybe.d.ts.map +1 -1
- package/src/utils/toMaybe.js.map +1 -1
- package/src/utils/toNamespace.d.ts.map +1 -1
- package/src/utils/toNamespace.js.map +1 -1
- package/src/utils/toUrlString.d.ts +12 -0
- package/src/utils/toUrlString.d.ts.map +1 -1
- package/src/utils/toUrlString.js.map +2 -2
- package/src/utils/waitForMessageOfType.d.ts +12 -1
- package/src/utils/waitForMessageOfType.d.ts.map +1 -1
- package/src/utils/waitForMessageOfType.js +16 -1
- package/src/utils/waitForMessageOfType.js.map +2 -2
- package/src/view/ComponentChanges.d.ts +88 -1
- package/src/view/ComponentChanges.d.ts.map +1 -1
- package/src/view/ComponentChanges.js +243 -73
- package/src/view/ComponentChanges.js.map +2 -2
- package/src/view/ComponentContext.d.ts +206 -16
- package/src/view/ComponentContext.d.ts.map +1 -1
- package/src/view/ComponentContext.js +548 -91
- package/src/view/ComponentContext.js.map +2 -2
- package/src/view/ComponentMemory.d.ts +3 -3
- package/src/view/ComponentMemory.d.ts.map +1 -1
- package/src/view/ComponentMemory.js.map +1 -1
- package/src/view/IShadowObjectEnvProxy.d.ts +29 -3
- package/src/view/IShadowObjectEnvProxy.d.ts.map +1 -1
- package/src/view/LocalShadowObjectEnv.d.ts +1 -1
- package/src/view/LocalShadowObjectEnv.d.ts.map +1 -1
- package/src/view/LocalShadowObjectEnv.js +8 -6
- package/src/view/LocalShadowObjectEnv.js.map +2 -2
- package/src/view/RemoteWorkerEnv.d.ts +89 -5
- package/src/view/RemoteWorkerEnv.d.ts.map +1 -1
- package/src/view/RemoteWorkerEnv.js +195 -77
- package/src/view/RemoteWorkerEnv.js.map +2 -2
- package/src/view/ShadowEnv.d.ts +49 -2
- package/src/view/ShadowEnv.d.ts.map +1 -1
- package/src/view/ShadowEnv.js +223 -60
- package/src/view/ShadowEnv.js.map +2 -2
- package/src/view/ViewComponent.d.ts +78 -14
- package/src/view/ViewComponent.d.ts.map +1 -1
- package/src/view/ViewComponent.js +90 -14
- package/src/view/ViewComponent.js.map +2 -2
- package/src/view/cloneChangeTrail.js.map +2 -2
- package/src/worker/MessageRouter.d.ts +14 -0
- package/src/worker/MessageRouter.d.ts.map +1 -1
- package/src/worker/MessageRouter.js +75 -15
- package/src/worker/MessageRouter.js.map +2 -2
- package/src/worker/WorkerRuntime.d.ts +22 -1
- package/src/worker/WorkerRuntime.d.ts.map +1 -1
- package/src/worker/WorkerRuntime.js +60 -8
- package/src/worker/WorkerRuntime.js.map +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,69 +5,328 @@ All notable changes to [@spearwolf/shadow-objects](https://github.com/spearwolf/
|
|
|
5
5
|
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
> Build-system, monorepo, lint/format, and dev-workflow changes that don't affect the shipped package are tracked in the top-level [`CHANGELOG.md`](
|
|
8
|
+
> Build-system, monorepo, lint/format, and dev-workflow changes that don't affect the shipped package are tracked in the top-level [`CHANGELOG.md`](https://github.com/spearwolf/shadow-objects/blob/main/CHANGELOG.md).
|
|
9
|
+
|
|
10
|
+
## [0.34.0] - 2026-09-02
|
|
11
|
+
|
|
12
|
+
> **Minor, not major.** The package is below `1.0.0`, so the accumulated breaking changes
|
|
13
|
+
> bump the minor position — `0.33.0` → `0.34.0`.
|
|
14
|
+
|
|
15
|
+
### What Changes for Existing Code
|
|
16
|
+
|
|
17
|
+
Twelve areas reach an application that ran against `0.33.0`. Everything else recorded below fixes
|
|
18
|
+
a defect that could not be relied on, adds API, or narrows a declaration.
|
|
19
|
+
|
|
20
|
+
1. **Both runtime dependencies take a major step.** `@spearwolf/eventize@^6.2.0` and
|
|
21
|
+
`@spearwolf/signalize@^1.0.0`, from the `^5.0.0` / `^0.30.0` that `0.33.0` shipped. They
|
|
22
|
+
move as a pair and carry behaviour changes of their own — §Dependencies lists them.
|
|
23
|
+
`dist/bundle.js` grows 139.6 kB → 227.1 kB minified, 42.1 kB → 69.4 kB gzipped.
|
|
24
|
+
2. **A failure is an error object, not a string.** `WorkerDestroyedError`, `WorkerFailedError`,
|
|
25
|
+
`WorkerReportedError`, `WorkerTimeoutError`, `ChangeTrailRefusedError`, `EntityUuidInUseError`
|
|
26
|
+
and `ComponentUuidInUseError` take the place of the bare wording `RemoteWorkerEnv` and
|
|
27
|
+
`ShadowEnv` used to reject with. A `catch` that compared against `'worker was destroyed'`, or
|
|
28
|
+
that printed the reason, no longer matches — the wording is in `error.message`.
|
|
29
|
+
3. **A listener that throws no longer ends the delivery it stands in.** Nine deliveries run on
|
|
30
|
+
guarded dispatch, so a listener that used to be skipped behind a failing one is called from
|
|
31
|
+
here on, and an application that came to rely on that silence sees it run.
|
|
32
|
+
4. **A teardown runs to its end.** A throwing `onDestroy`, `createResource` cleanup or class-side
|
|
33
|
+
`[onDestroy]` costs its own Shadow Object and is reported through the `ConsoleLogger` instead of
|
|
34
|
+
reaching the caller. And `onDestroy` runs at all when a worker environment is torn down, where
|
|
35
|
+
it never used to — code that closes or reports something there is now executed.
|
|
36
|
+
5. **`<shae-prop>` binds elsewhere and carries values it used to drop.** `0`, `false` and `''`
|
|
37
|
+
reach the Shadow Object; a value that does not convert sets no property instead of throwing;
|
|
38
|
+
the element binds to the closest entity the flattened tree shows above it and re-binds when that
|
|
39
|
+
tree changes; and it clears its property when the binding ends.
|
|
40
|
+
6. **`<shae-ent>` reshapes an entity tree it used to leave alone.** A runtime `ns` change carries
|
|
41
|
+
the entity, the entities hanging on it and its properties into the other environment; an
|
|
42
|
+
ancestor registered with `customElements.define()` after the markup stands adopts the entities
|
|
43
|
+
below it; and an element whose `ns` was written before it entered the tree becomes an entity.
|
|
44
|
+
7. **Three timings move.** `onParentChanged` is delivered while `Kernel.setParent()` is still on
|
|
45
|
+
the stack instead of one microtask later; `LocalShadowObjectEnv.applyChangeTrail(trail, true)`
|
|
46
|
+
settles one microtask later than it did; and the adoption in point 6 needs an
|
|
47
|
+
`await Promise.resolve()` in front of a read of `entParentNode` or `viewComponent.parent`.
|
|
48
|
+
8. **`ShadowEnv.syncWait()` rejects for a cycle the Shadow Environment could not apply**, where it
|
|
49
|
+
used to resolve as if the trail had arrived. An `await` without a `catch` now throws, and
|
|
50
|
+
`ShadowEnv.AfterSync` no longer hears such a cycle — `ShadowEnv.SyncFailed` does.
|
|
51
|
+
9. **`FrameLoop.OnFrame` carries a `FrameData` object with its times in seconds**, where a listener
|
|
52
|
+
on `<shae-worker>.frameLoop` used to be handed the raw millisecond timestamp.
|
|
53
|
+
10. **The elements write their markup back.** The three tags canonicalise their reflecting
|
|
54
|
+
attributes when they first connect, so `token=" x "` reads back as `token="x"` and
|
|
55
|
+
`auto-sync="YES"` as `auto-sync="yes"`; and `display: contents` arrives as a stylesheet rule
|
|
56
|
+
rather than an inline style, so `el.style.display` reads empty and a consumer rule of the same
|
|
57
|
+
specificity takes the display over without `!important`.
|
|
58
|
+
11. **Four reads and writes that used to work now fail.** `logger` and `timeouts` are getters
|
|
59
|
+
without setters, so an assignment throws a `TypeError` in strict mode; seven `Kernel`
|
|
60
|
+
bookkeeping methods have left the prototype; `ComponentMemory` is no longer a runtime export of
|
|
61
|
+
the root entry; and the three realm-wide globals carry the package name
|
|
62
|
+
(`__shadowEntsContexts` → `__shadowObjectsContexts`, `SHADOW_ENTS_BUNDLE_LOADED` →
|
|
63
|
+
`SHADOW_OBJECTS_BUNDLE_LOADED`, `Symbol.for('ShadowEntsGlobalNS')` →
|
|
64
|
+
`Symbol.for('ShadowObjectsGlobalNS')`).
|
|
65
|
+
12. **The emitted declarations carry `| undefined` where a value can be missing**, so a consumer
|
|
66
|
+
building with `strictNullChecks` sees new errors while nothing moves at runtime.
|
|
9
67
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
> **Next release: minor.** The package is below `1.0.0`, so the accumulated breaking
|
|
13
|
-
> changes below bump the minor position — `0.33.0` → `0.34.0`. Two of them reach existing
|
|
14
|
-
> consumers: the emitted declarations carry `| undefined` where a value can be missing, so a
|
|
15
|
-
> build with `strictNullChecks` sees new errors; and `RemoteWorkerEnv` rejects with
|
|
16
|
-
> `WorkerDestroyedError` / `WorkerFailedError` instead of the string `'worker was destroyed'`,
|
|
17
|
-
> so a `catch` that compared against that string no longer matches. Everything else in this
|
|
18
|
-
> section is additive or a bugfix.
|
|
68
|
+
### ⚠️ Breaking Changes
|
|
19
69
|
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
70
|
+
- **Breaking (events):** a listener that throws no longer ends the delivery it stands in. Every notification the library fans out runs on eventize's guarded dispatch, so the listeners behind a failing one are served, a `once()` queued behind it is spent, and a retained value is written because the event was delivered. This reaches nine deliveries: the `FrameLoop.OnFrame` frame, the entity-wide `onDestroy` notification and the one sent to a single Shadow Object, `onParentChanged`, the View Layer events an Entity hands to its Shadow Objects, `MessageToView`, `ViewComponent.dispatchEvent()` and `ViewComponent.Destroyed`, `SignalsPath`'s `value`, the five `ShadowEnv` events, and `WorkerLoaded` / `WorkerFailed` on `RemoteWorkerEnv`. What was reported and where is unchanged wherever the library holds context of its own — the `ConsoleLogger`, named by the entity or the Shadow Object — while the three deliveries that own no logger (`FrameLoop`, `ViewComponent`, `SignalsPath`) report through eventize's `console.warn` instead. `ShadowEnv.ProxyFailed` is the one event that still hands the error on to its caller, after every listener has run.
|
|
71
|
+
- **Breaking (`FrameLoop`):** a target that throws no longer stops the loop. It used to carry the error into the `requestAnimationFrame()` callback, past the line asking for the next frame, which left the shared loop of the realm — `<shae-worker>` auto-sync and `<shae-offscreen-canvas>` rendering both ride on it — standing still for the rest of the page with a subscriber count that still named its targets.
|
|
72
|
+
- **Breaking (`RemoteWorkerEnv`):** the recovery pass behind `workerLoaded` and `workerFailed` is gone, and with it its two costs. A `workerLoaded` promise handed out behind a throwing listener settles instead of staying pending until the environment fails or is torn down, and a wildcard listener registered above a throwing one hears the event once rather than twice.
|
|
73
|
+
- **Breaking (logging):** `ConsoleLogger.debug()`, `.info()` and `.warn()` check their own level's getter before printing. A consumer that calls one of them without first asking `isDebug`, `isInfo` or `isWarn` — this library's own call sites and any code built on the exported `ConsoleLogger` alike — used to reach the console regardless of the switches; such a call is now silent while the matching switch is off. `error()` is unaffected: it asks no getter and keeps printing unconditionally, as it always has.
|
|
74
|
+
- **Breaking (elements):** the reflecting attributes reach their canonical spelling when the element first connects. `<shae-ent>` writes `token` and `forward-custom-events` out, `ShaeElement` writes `ns`, `<shae-worker>` writes `auto-sync` — the same writes each of them already made on every later connect, now made on the first one too. What reaches a consumer is markup that no longer keeps a spelling the signal does not agree with: `token=" x "` becomes `token="x"`, `token=""` and `token=" "` lose the attribute, `ns=""` and `ns=" "` lose it as well (the global namespace is a symbol and has no spelling), and `forward-custom-events=" "` becomes the empty string while `forward-custom-events=","` and `forward-custom-events=" , , "` lose the attribute — a list naming no event type forwards nothing, and that is spelled by leaving the attribute out. Code that reads one of these attributes back after connecting sees the canonical form; code that reads it off an element that has not connected yet sees exactly what it wrote. Nothing about the property values changes.
|
|
75
|
+
- **Breaking (elements):** a `<shae-ent>` released while it is still in the document stops answering the parent and host requests that travel past it, and tells whatever was bound to it to look again. An entity or a property below such an element binds to the next one further up, or to nothing, instead of to an element that no longer maintains the binding. This only reaches code that calls `destroy()` by hand on a connected element — along the ordinary way out the element has left the document and answers nothing either way. Such an element does not take its subscriptions up again until it has left the document and come back; re-inserting one that never left is not a way back, because it never reconnects.
|
|
76
|
+
- **Breaking (elements):** `<shae-ent>` and `<shae-prop>` release their subscriptions one microtask after they leave the document and stay out. Every effect, every signal subscription and every event listener the element holds comes off, so nothing on the module level points at it any longer and it can be collected; `isDestroyed` reports the state and `destroy()` does it by hand. The release is reversible — the signals are not destroyed, the element keeps its `ViewComponent`, its uuid and its values, and reconnecting takes every subscription up again — and a move inside a single task never reaches it, so no re-render is affected. What reaches a consumer is what happens to a released element that is driven by hand, and the two elements part company there. A `<shae-ent>` keeps what is written to it: `token`, `ns` and `forward-custom-events` stand in the signal, the element carries them onto its attributes as it reconnects, and they take effect at that point rather than right away — so code that writes one of them and reads the result before re-inserting the element sees the old value until it connects, and `ent.ns` in particular no longer moves the entity into the other `ComponentContext` on the spot. A `<shae-prop>` keeps nothing written that way: connecting reads `name`, `value`, `type` and `no-trim` off the attributes and looks the host up again from where the element stands, so a `prop.value` or `prop.entNode` written while it was released is replaced rather than applied. That is what this element has always done on connect and the release does not change it; it is named here because it is the one place the two behave differently.
|
|
77
|
+
- **Breaking (elements):** `display: contents` arrives as a stylesheet rule, not as an inline style. The first element to connect puts one rule at the top of the document head — or of the shadow root it stands in, since a rule does not cross a shadow boundary — and the selector names the tag it connected under. A `ShaeEntElement` subclass registered under a tag of its own is covered the same way, the moment one of its instances connects: a tag that joins later widens the selector already there rather than adding a second rule, so its place in the cascade never moves. What reaches a consumer: `el.style.display` reads empty and the element carries no `style` attribute, so code that read either sees nothing there; and a rule of the consumer's own with the same specificity — `shae-ent { display: block }` — wins by coming later in the cascade, where overriding an inline style needed `!important`. The rule is prepended for exactly that reason. What the elements render like is unchanged: they still lay out no box of their own.
|
|
78
|
+
- **Breaking (elements):** a numeric `<shae-prop>` type answers with a number or with nothing. `number`, `float`, `int`, `hex`, `oct`, `bin`, their list forms and every numeric typed array report a conversion that comes out as `NaN` through the `ConsoleLogger` at error level and leave the property `undefined`, where such a value used to reach the entity — as a `NaN`, as the `0` a typed-array constructor made of it, or as a `NaN` written into a `Float32Array` buffer. One unreadable segment costs the whole list, the way one unreadable entry has always cost a `bigint64array`. What is tested is the result and not the shape of the input: `type="int" value="12abc"` is `12` as before, and `Infinity` passes. An empty segment — left behind by a leading, trailing or doubled separator, or by a value the trim empties — is `0` where `Number()` reads it (`number`, `number[]` and the numeric typed arrays) and no number at all where `parseFloat()` or `parseInt()` read it (`float`, `float[]`, `int`, `hex`, `oct`, `bin` and their list forms), so `type="hex[]" value="-ff 0a"` and `type="int" value=" "` set nothing. The report goes out at error level and is therefore not gated behind `ConsoleLogger.sharedConfig.enable`. Assigning `Number.NaN` through the `el.value` property is untouched: the conversion applies to strings only.
|
|
79
|
+
- **Breaking (elements):** a `shaeRequestEntParent` event built by hand is only answered when its `detail` carries an `answer(entNode)` callback — that callback is how the answer travels now, and an event without it passes every entity untouched. The `detail` also takes an optional `ns`: without it the closest entity answers whatever namespace it is in, with it only an entity of that namespace does. `requestEntAncestor(requester, request)` builds and dispatches such an event and is exported for anyone driving the lookup by hand, together with the `EntAncestorRequest` type.
|
|
80
|
+
- **Breaking (elements):** `ShaePropElement` no longer carries `isShaeEntElement`. The flag was a copy-paste from `ShaeEntElement` and actively wrong: it let a `<shae-prop>` be mistaken for an entity, so a `<shae-prop>` nested inside another `<shae-prop>` bound to the wrong element. Replaced by `isShaePropElement` — a marker for consumers and tests, not something the host lookup itself reads; the lookup runs on which elements answer the `shaeRequestEntParent` event, not on either flag.
|
|
81
|
+
- **Breaking (elements):** `@spearwolf/shadow-objects/shae-prop.js` defines `<shae-prop>` on import, with no wait on `<shae-ent>`'s registration — the two registration modules place no requirement on each other's order. A consumer importing the subpath on its own gets a working element; a consumer importing both sees no difference. A `<shae-prop>` that connects before any entity above it answers reports that once through the `ConsoleLogger` and finds its host the moment one registers.
|
|
82
|
+
- **Breaking (elements):** a removed `token` attribute takes the token with it. The element read the attribute only while it was there, so removing it left the property and the `ViewComponent` standing on a value that had already vanished from the markup — while `el.token = undefined` cleared both. An entity without a token carries `VoidToken` (`#void`) and matches no Shadow Object; removing the attribute now destroys the Shadow Object that the token had selected.
|
|
83
|
+
- **Breaking (elements):** a `forward-custom-events` list that names no event type forwards nothing. `forward-custom-events=","` and `forwardCustomEvents$.set(new Set())` landed as an empty attribute value, and that is the spelling for "every event"; they now mean what they say. "Forward nothing" still means leaving the attribute out.
|
|
84
|
+
- **Breaking (elements):** the `logger` slot of `<shae-ent>`, `<shae-prop>` and `<shae-worker>` is a getter without a setter, so it is closed at runtime and not only to the type layer. An assignment — `el.logger = myLogger` — throws a `TypeError` in strict mode and does nothing outside it, and every read hands back the same `ConsoleLogger`. On `<shae-worker>` the slot is public; on the other two it is `protected` and meant to be read by a subclass, and a subclass that wants a logger of its own gives it another name, because a `logger` field declaration in a subclass either shadows the getter or throws, depending on how that subclass's class fields are compiled. All three classes are exported from `@spearwolf/shadow-objects`, so this is a change a consumer meets directly. One thing travels with it: `logger` is an accessor on the prototype rather than an own property of the instance, so it is absent from `Object.keys(el)`, from a `{...el}` spread and from `JSON.stringify(el)`.
|
|
85
|
+
- **Breaking (kernel):** the entity tree stays a tree. A parent that is the entity itself or one of its descendants is refused with an error, on both paths that write the parent link — `Kernel.setParent(uuid, parentUuid)` and `entity.parent = …` — and the entity stays attached exactly where it was: same parent, same `order`, same place among its siblings. Such a call used to lay a ring, and a ring is worse than it sounds: every entity in it has a parent, so it drops out of `#rootEntities` and with that out of every sweep the Kernel makes. `upgradeEntities()` never saw it, `destroy()` never took it down, and no `onDestroy` of a Shadow Object in it ever ran. The two primitives below the parent link — `Entity.addChild()` and `ComponentContext.addToChildren()` — write a children list without touching the parent link, and no ancestor walk can see what they do; the four traversals carry that case instead of refusing it, see below — termination is all they carry, though: such a ring, reachable from no root, is not walked by `Kernel.destroy()` either, so its entities go with the bookkeeping and their `onDestroy` does not run.
|
|
86
|
+
- **Breaking (kernel):** `Kernel.traverseLevelOrderBFS()` hands out a fresh array on every call. It used to return the Kernel's own cache, so a caller that sorted, spliced or reversed the result changed what the next call gave out — with `upgradeEntities()` then running its creation pass from the leaves upwards and a Shadow Object appearing on a child before it appeared on its parent. The return type stays `Entity[]`: the list belongs to the caller and may be reordered.
|
|
87
|
+
- **Breaking (kernel):** the seven methods carrying the bookkeeping of the Kernel — `#getEntityGraphNode()`, `#parse()`, `#updateShadowObjects()`, `#constructShadowObject()`, `#createShadowObjects()`, `#attachShadowObject()` and `#destroyShadowObject()` — are private with `#`, not with the `private` keyword, and therefore stand off the prototype at runtime. They reach into the entity entries, the constructor sets kept per entity and the creation scope of a shadow object. TypeScript never allowed a call to one of them from outside the class; JavaScript did, and a call written that way now finds `undefined` instead of running. `dist/src/in-the-dark/Kernel.d.ts` loses its seven `private <name>;` lines and keeps its `#private;` marker; the published file list and the shape of `dist/package.json` are unchanged.
|
|
88
|
+
- **Breaking (kernel):** `Kernel.logger` is a getter without a setter. An assignment — `kernel.logger = myLogger` — throws a `TypeError` in strict mode and does nothing outside it, and every read hands back the same `ConsoleLogger`. The class is reachable through `@spearwolf/shadow-objects/shadow-objects.js`, so this is a change a consumer meets directly; `src/index.ts` does not re-export it. `logger` is an accessor on the prototype rather than an own property of the instance, so it is absent from `Object.keys(kernel)`, from a `{...kernel}` spread and from `JSON.stringify(kernel)`.
|
|
89
|
+
- **Breaking (public API):** one frame loop per module instance, and it is asked for by name. `FrameLoop.get()` answers the shared loop and `<shae-worker>.frameLoop` reads that same one; `new FrameLoop(maxFps)` builds a loop of its own, where the constructor used to answer the shared instance to every caller — which is what lets a capped and an uncapped loop run side by side in one realm. What such a loop emits is a `FrameData` object — `{now, lastNow, frameNo, deltaTime}`, times in seconds, and a loop that has been idle starts a fresh run — that frame is frame one and reports a `deltaTime` of zero, as the very first frame does — where a listener used to receive the raw millisecond timestamp of the animation frame.
|
|
90
|
+
- **Breaking (types):** `ComponentPropertiesType` names both kinds of entry a Change Trail carries — `[name, value]` and a bare `[name]` for a property set to `undefined`. Anything that reads a Change Trail sees the wider type: a `const props: [string, unknown][] = change.properties` no longer compiles, while `entry[1]` still does. Nothing changes at runtime. `Kernel.createEntity`, `Kernel.changeProperties` and `Entity.setProperties` take the wider type too, which is additive for callers.
|
|
91
|
+
- **Breaking (types):** `ShadowObjectConstructor`'s `new (...)` signature returns `object` instead of `{}`. `{}` types as "any non-nullish value", including primitives, which is not what a constructor result is — a class satisfying this interface already only ever returns an object at runtime, so the declaration now says what was already true. Source-breaking: a construct signature whose return type is not assignable to `object` — that is, one returning a primitive — no longer type-checks where it is assigned to `ShadowObjectConstructor`, for example when passed to `shadowObjects.define()` or `registry.define()`. A signature returning `{}` still type-checks — in the return position `{}` is assignable to `object`, a primitive is not. Nothing changes at runtime.
|
|
92
|
+
- **Breaking (view components):** `ComponentContext.clear()`, `ComponentContext.destroyComponent()` and `ComponentContext.removeSubTree()` release the `ViewComponent`s they take down. Such a component reports `isDestroyed === true` and holds no context — the same state `ViewComponent.destroy()` leaves behind and the one `dispose()` has always produced, so every teardown a context does reads alike. What reaches a consumer: `isDestroyed` and `context` read different values; `setProperty` after a `destroyComponent()` returns `false` instead of `true` (nothing was written either way); an `addChild` or `parent = …` on such a component throws the `ViewComponentError` about a destroyed component, where a `clear()` used to raise the plain `Error` about a component the context does not hold and a `destroyComponent()` before the next change trail used to hang the child on a component that was already on its way out, without a word; and `vc.context = ctx` after a `clear()` takes the component back in under the same uuid instead of being an assignment without effect — within the same change trail window creation and destruction cancel each other out, and the entity stays where it is. `clear()` and `dispose()` sweep every `ViewComponent` that has joined the context and has not left it again. `removeSubTree(uuid)` is addressed to an entry and takes down the instance holding it together with its descendants, and does not mean an instance that has left that uuid to a later one. `destroyComponent(component)` is named an instance, releases exactly that instance, and leaves the entry — and the entity — standing where a namesake holds it. `ComponentContext.removeFromParent()` and `ComponentContext.moveToRoot()` now take the `ViewComponent` itself rather than a bare uuid, for the same reason: a component that has left its uuid to a later one no longer moves the entry it left behind, which used to mis-report that entry's live component as newly parentless or newly a root.
|
|
93
|
+
- **Breaking (view components):** `ViewComponent.destroy()` leaves a silent component behind. Every `on(vc, …)` and `once(vc, …)` is taken off, a `dispatchEvent` an integration installed on the instance is dropped, and the method of the prototype applies again. What reaches a consumer: a listener registered before the teardown hears no later `dispatchEvent()` and the unsubscribe function it returned has nothing left to do; a listener registered afterwards is heard as before, because each call takes off what lies on the component in that moment rather than sealing it. The teardown paths of a context come along, because they run through `destroy()`. Leaving a context does not: `vc.context = otherCtx` moves the component, `vc.context = null` detaches it, and a `<shae-ent>` that leaves the document and comes back is exactly that second case — it hands the context back and takes the same component in again on re-append. The teardown announces itself on the component before it takes anything off, so whoever holds something there can set it up again: a `<shae-ent>` does exactly that and has its three re-request subscriptions and its forwarding of custom events back one microtask later, on the same component. A `clear()`, `dispose()`, `destroyComponent()` or `removeSubTree()` therefore leaves no deaf element behind; only the task the teardown itself runs in goes past it — a re-request round broadcast there does not reach the element, and a `vc.dispatchEvent()` there reaches the eventize listeners but leaves no DOM `CustomEvent` behind. The window is named in `docs/api-reference.md`.
|
|
94
|
+
- **Breaking:** `ComponentMemory` is no longer a runtime export of the root entry `@spearwolf/shadow-objects`. The class carries `@internal` and `stripInternal: true` keeps it out of the emitted declarations, so `import {ComponentMemory} from '@spearwolf/shadow-objects'` has always been an error for a TypeScript consumer — the type never existed, only the JavaScript binding did. The module stays reachable as a type export, and the `ComponentState` interface it declares is unaffected.
|
|
95
|
+
- **Breaking (view):** a synchronization cycle ends in exactly one of two events, and `ShadowEnv.syncWait()` follows suit. A cycle the Shadow Environment applied emits `ShadowEnv.AfterSync` and resolves the promise with its change trail; a cycle whose trail the environment could not apply emits the new `ShadowEnv.SyncFailed` and rejects the promise with the reason the proxy gave — a worker that did not confirm within `changeTrailTimeout`, a Kernel error the worker reported back, a proxy whose environment is already gone. Such a cycle used to emit `AfterSync` with the trail that never arrived and resolve as if it had, which left the loss visible only as a `ConsoleLogger` line that is silenced outside localhost. An `await env.syncWait()` without a `catch` therefore throws where it used to carry on, and a listener of `AfterSync` hears the successful cycles and nothing else. The trail of a failed cycle is gone — `buildChangeTrails()` folds every pending change into the state it wrote before the trail leaves — and a re-creation from the Component Memory is what brings it back; it belongs to a fresh proxy, because an environment whose Kernel still holds those uuids refuses every re-created creation. Handing `envProxy` a new proxy stays with the consumer, the same way recovering from a `ProxyFailed` does.
|
|
96
|
+
- **Breaking (environments):** `LocalShadowObjectEnv.importScript()` rejects a module that has no `shadowObjects` export, with the same wording `RemoteWorkerEnv` reports for the same case, instead of resolving silently. An `await env.importScript(url)` without a `catch` therefore throws where it used to carry on, and a `<shae-worker local src="…">` on such a module reports the failure through `logger.error`, where the declarative path stayed silent before.
|
|
97
|
+
- **Breaking (environments):** `RemoteWorkerEnv` rejects a failure the worker reported with a `WorkerReportedError` instead of with the bare wording. Two routes end that way: an `importScript()` on a module the worker could not load, and an `applyChangeTrail(trail, true)` whose confirmation carries a reason without an `appliedCount`. What reaches a consumer: a `catch` that compared the reason against a string, or printed it directly, now holds an object — `error.message` carries the wording, `error.name` the name the error was thrown under inside the worker.
|
|
98
|
+
- **Breaking (environments):** `RemoteWorkerEnv.timeouts`, `RemoteWorkerEnv.logger` and `ShadowEnv.logger` are getters without setters, so the slots are closed at runtime and not only to the type layer. An assignment — `env.timeouts = {…}`, `env.logger = myLogger` — throws a `TypeError` in strict mode and does nothing outside it. Both classes are exported from `@spearwolf/shadow-objects`, so this is a change a consumer meets directly. For `timeouts` the constructor is the way in, where `resolveTimeouts()` vets each of the four values, and the object itself stays frozen, so `env.timeouts.loadTimeout = …` is refused the same way. Every read hands back the same object. One thing travels with the change: the three names are accessors on the prototype rather than own properties of the instance, so they are absent from `Object.keys(env)`, from a `{...env}` spread and from `JSON.stringify(env)`.
|
|
99
|
+
- **Breaking (globals):** the three realm-wide names this package hangs off `globalThis` and the global symbol registry carry the package name: `globalThis.__shadowEntsContexts` becomes `globalThis.__shadowObjectsContexts`, `globalThis.SHADOW_ENTS_BUNDLE_LOADED` becomes `globalThis.SHADOW_OBJECTS_BUNDLE_LOADED`, and `Symbol.for('ShadowEntsGlobalNS')` becomes `Symbol.for('ShadowObjectsGlobalNS')`. A debugging or testing tool of a consumer that reaches for one of the old names by string finds nothing there any more, and two copies of this library loaded side by side no longer share a Component Context registry or a default namespace across that boundary. No old name is kept alongside the new one.
|
|
100
|
+
|
|
101
|
+
### New
|
|
102
|
+
|
|
103
|
+
- **New (public API):** `ViewComponent.setPropertyWithoutValue(name)` and `ComponentContext.setPropertyWithoutValue(component, propKey)` — mark a property as set without giving it a value. The key is there, no value stands behind it: the change trail carries an entry that names only the key, and the Entity reads the property as `undefined` with the key among its property keys. `setProperty(name, undefined)` stays what it was, a removal. That third form of a property entry was readable on the Kernel side and had no writer; it has one now. An `isEqual` function registered for the key is forgotten, the way it is for a `setProperty()` that comes without one, and `ComponentContext.transferPropertiesTo()` carries such a property into the context a component joins as one. Documented in `docs/api-reference.md` and `docs/cheat-sheet.md`.
|
|
104
|
+
- **New (public API):** `ComponentContext.hasComponentState(uuid)` and `ComponentContext.getComponentState(uuid)` — a window into the Component Memory, the state `reCreateChanges()` would rebuild a component from. Reading it used to mean calling `reCreateChanges()` and rebuilding the whole namespace to find out. `getComponentState()` hands back a `ComponentState` — the type is already exported — as a snapshot: the record behind it is rewritten in place as trails come in, so the object and the property list a caller gets are its own, while the property values in it are the ones the memory holds. Documented in `docs/api-reference.md`.
|
|
105
|
+
- **New (subclass API):** `ShaeElement.teardown()` and `ShaeElement.restore()`, both `protected`, are the pair a subclass of `<shae-ent>` or `<shae-worker>` overrides to carry subscriptions of its own through a release. `teardown()` is the overridable half of `destroy()` — the guard that makes the teardown run once, and run once even where releasing something calls back into the element, sits in `destroy()` and has already set the flag by the time `teardown()` runs. `restore()` is its counterpart, called from `connectedCallback` for an element that comes back; it takes the subscriptions up again and writes any signal that reflects into an attribute back out, because `connectedCallback` reads those attributes immediately afterwards. `ShaePropElement` carries the same pair, `teardown()` and `restore()`, though it extends `HTMLElement` directly and inherits neither. A subclass that overrides one without the other works exactly once. Documented in `docs/api-reference.md`.
|
|
106
|
+
- **New (subclass API):** `ShaePropElement.teardown()`, `protected` — the overridable half of `<shae-prop>`'s `destroy()`, the counterpart to its `restore()`, and the same shape `ShaeElement` carries. A subclass releases what it holds there and calls `super.teardown()` last; `destroy()` keeps the guard that makes the teardown run once. Documented in `docs/api-reference.md`.
|
|
107
|
+
- **New (public API):** `destroy()` and `isDestroyed` on `ShaeElement`, and with it on `ShaeEntElement`, `ShaePropElement` and `ShaeWorkerElement`. `destroy()` releases every subscription an element holds; `isDestroyed` reads whether it is released right now. On `<shae-ent>` and `<shae-prop>` this is reversible: the signals stay, and reconnecting takes the subscriptions up again — `isDestroyed` reads `false` and the element carries the same `ViewComponent`, uuid and values it left with. On `<shae-worker>` the two names mean something stronger: that teardown takes the Shadow Environment with it and cannot be undone, and a released `<shae-worker>` put back into the document stays released. Documented in `docs/api-reference.md` and `README.md`.
|
|
108
|
+
- **New (public API):** `ChangeTrailRefusedError` — the reason a Shadow Environment gives when it could apply only part of a change trail. `appliedCount` names the length of the prefix the Kernel applied, `entryCount` the length of the trail, and `cause` carries what the entry threw: the error object itself in a local environment, and across a worker boundary a `WorkerReportedError` carrying its wording and its name, without the class and without the fields that class added. `Kernel.run()` throws it, `LocalShadowObjectEnv` and `RemoteWorkerEnv` reject with it, and `ShadowEnv` reads it. Exported from `@spearwolf/shadow-objects` for the view side and from `@spearwolf/shadow-objects/shadow-objects.js` for the Kernel side, which adds `dist/src/ChangeTrailRefusedError.js` and its declaration to the published file list. Documented in `docs/api-reference.md`, `docs/guides.md` and `docs/cheat-sheet.md`.
|
|
109
|
+
- **New (public API):** `EntityUuidInUseError` — the reason the Kernel gives when it is asked to create an entity under a uuid it already holds one for. `uuid` names that uuid. `Kernel.createEntity()` throws it, and inside a change trail it arrives as the `cause` of the `ChangeTrailRefusedError` — as the object itself in a local environment, and across a worker boundary as a `WorkerReportedError` carrying its wording and its name, without the class and without the `uuid`. Exported from `@spearwolf/shadow-objects` for the view side and from `@spearwolf/shadow-objects/shadow-objects.js` for the Kernel side, which adds `dist/src/EntityUuidInUseError.js` and its declaration to the published file list. Documented in `docs/api-reference.md`, `docs/guides.md` and `docs/cheat-sheet.md`.
|
|
110
|
+
- **New (public API):** `WorkerTimeoutError` — the reason a reply from the worker did not arrive in time. `messageType` names the message that stayed out, `timeout` how many milliseconds were waited for it, so a diagnosis knows which of the four values was in force without reaching for the `timeouts` of the `RemoteWorkerEnv`. Four replies have a deadline: the `Loaded` greeting of the load handshake, the `ImportedModule` answer to an `importScript()`, the `AppliedChangeTrail` confirmation of a change trail sent with `waitForConfirmation`, and the `Destroyed` receipt of a teardown. `waitForMessageOfType()` throws it and `RemoteWorkerEnv` rejects with it. Exported from `@spearwolf/shadow-objects` and not from `@spearwolf/shadow-objects/shadow-objects.js` — it is a view-side class — which adds `dist/src/WorkerTimeoutError.js` and its declaration to the published file list. Documented in `docs/api-reference.md`, `docs/guides.md` and `docs/cheat-sheet.md`.
|
|
111
|
+
- **New (public API):** `ComponentContext.commitChangeTrail(appliedCount, changeTrail?)` — folds the first `appliedCount` entries of the trail the context built last into the state the next trail is diffed against and writes them to the Component Memory; everything behind that line stays pending and goes out again with the next trail. Its counterpart is the renamed parameter of `ComponentContext.buildChangeTrails(commit = true)`, which was `clearChanges`: `buildChangeTrails(false)` now builds without writing the memory and without counting the trail as applied, where it used to write the memory either way. The default call is unchanged in name and in behaviour. `changeTrail` is the trail a call settles — given, the call is ignored unless it is the one the context built last, which keeps a cycle that lost its trail to a later build from drawing a line through that later trail.
|
|
112
|
+
- **New (protocol):** `AppliedChangeTrailEvent.appliedCount` — an optional number a worker sends alongside the `error` of a refused change trail, naming how many entries its Kernel applied. It travels only in a refusal, never in a confirmation: on success the number is the trail's length, which the view already knows. A refusal without the field is read as "nothing is known", which is how an implementation that does not send it keeps behaving exactly as it did.
|
|
113
|
+
- **New (protocol):** `errorName` on `ImportedModuleEvent` and `AppliedChangeTrailEvent` — an optional string standing next to `error`, naming the error the way it named itself in the worker. `error` carries the wording alone, without a class name in front of it, and the view builds an error from the two. An implementation on the other side that sends no `errorName` keeps working: the view reads a missing one as `Error`.
|
|
114
|
+
- **New (public API):** `ShadowEnv.SyncFailed` — the static event name for a cycle whose change trail the Shadow Environment could not apply. Listeners receive the reason, the `ChangeTrailType` that was lost, and the `ShadowEnv`; the environment stays ready, because what failed is the cycle and not the connection. `<shae-worker>` mirrors it onto itself as the `syncfailed` `CustomEvent`, with `shadowEnv`, `reason` and `changeTrail` in the `detail`, `bubbles: false` and without `composed` — the same shape as `proxyfailed`.
|
|
115
|
+
- **New (elements):** `ReRequestEntHostEventName` — the name of the bubbling, composed event a `<shae-ent>` sends when it starts or stops answering host requests. A `<shae-prop>` listens for it and asks again. The name is exported for anyone listening for the event by hand; the event type `ReRequestEntHostEvent` is exported as a type alongside it.
|
|
116
|
+
- **New (public API):** `Kernel.noteEntityTreeChange(uuid)` — tells the Kernel that an entity has changed its place in the entity tree, so the cached traversal is dropped and the set of root entities follows the parent link the entity now carries. `Entity` calls it from every write that moves it, which leaves one kind of caller outside: `Entity.addChild()`, `Entity.removeChild()`, `Entity.resortChildren()` and `ComponentContext.addToChildren()` write or reorder a children list without touching the parent link and report nothing, so whoever writes such a list calls this afterwards to have the next walk read it. The root set is out of that call's reach — it follows the parent link, which such a list never wrote. Documented in `docs/api-reference.md`.
|
|
117
|
+
- **New (public API):** `Kernel.findEntity(uuid)` — the entity behind a uuid, or `undefined` where the Kernel holds none. The counterpart to `getEntity()`, which keeps throwing: a change that describes the entity tree names an entity the view believes to be there, and a uuid the Kernel does not hold is a disagreement the caller has to hear about, while an event carries no structure and may well be addressed to an entity that has been torn down in the meantime. One lookup instead of the two an `hasEntity()` check in front of `getEntity()` costs.
|
|
118
|
+
- **New (public API):** `ViewComponent.Destroyed` — the static event name a component emits on itself immediately before `destroy()` takes every subscription off it. For any integration that keeps something on the instance: subscriptions, an own `dispatchEvent`. It travels via `emit()` rather than `ViewComponent#dispatchEvent`, so an installed `dispatchEvent` neither sees it nor carries it on as a DOM `CustomEvent`, and leaving a context (`vc.context = null`) announces nothing. A listener that wants its subscription back schedules that outside the handler — everything set up inside it comes off with the rest.
|
|
119
|
+
- **New (public API):** `ComponentContext.ReRequestEntHost` — the third static event name a context sends to its components, next to `ReRequestParentRoots` and `ReRequestParent`. It asks the element behind a component to let the properties hanging on it look for their host entity again; `<shae-ent>` answers it on its own. Like the other two it never leaves the view side, not even under `forward-custom-events` without a filter list.
|
|
120
|
+
- **New (public API):** `FrameLoop` — the frame loop behind the frame-based sync of `<shae-worker>`, now available to anyone. `FrameLoop.get()` answers the shared loop, `new FrameLoop(maxFps)` builds an own one, `start(target)` / `stop(target)` subscribe and unsubscribe, `maxFps` caps the rate while the loop runs, and `subscriptionCount` says how many targets listen. The payload type `FrameData` comes with it. Two ways in: the package entry point for the view side, and `@spearwolf/shadow-objects/FrameLoop.js` for code that has no document — a Shadow Object in a worker dies on the entry point, which pulls the Custom Elements along. The shipped manifest carries the matching `"./FrameLoop.js"` key in its `exports` object.
|
|
121
|
+
- **New (public API):** `ComponentContext.transferPropertiesTo(component, target)` — hands the properties a context holds for a component over to the context it has just joined. Called by the `ViewComponent#context` setter; available for anyone driving a context switch by hand.
|
|
24
122
|
- **New (public API):** `WorkerDestroyedError` — the reason a torn-down `RemoteWorkerEnv` turns away every further request. Kept apart from `WorkerFailedError`, which reports the breakdown of the worker itself.
|
|
25
|
-
- **
|
|
26
|
-
- **Bugfix (elements):** `<shae-ent>` threw while detaching from its parent element when it had never been given a `ViewComponent`. Without one there is nothing to detach; the branch is skipped.
|
|
27
|
-
- **Bugfix (elements):** `<shae-worker>.importScript()` rejects with a `ShadowEnvDestroyedError` when the environment is torn down between `ready()` resolving and the import starting, instead of running into a `TypeError`.
|
|
28
|
-
- **Bugfix (logging):** `ConsoleLogger.loadConfig()` threw a `TypeError` when one of the four `ConsoleLogger.styles.*` keys was present in `localStorage`: the style values are read without a converter, and the missing converter was called anyway. Since `loadConfig()` runs from the constructor of the first logger, a single style entry set in the devtools took the whole library down at startup.
|
|
29
|
-
- **Bugfix (worker environments):** `RemoteWorkerEnv` only listened for `message`. A worker that died on an unhandled error, or sent something the structured clone algorithm could not read back, went unnoticed — `applyChangeTrail()` sat out the full 5s, `importScript()` and `start()` the full 60s, and no consumer had any way of learning that the environment was gone. `error` and `messageerror` are now subscribed before the load handshake begins; a failure terminates the worker, sets `isDestroyed`, and rejects everything pending and everything later with a new `WorkerFailedError`.
|
|
123
|
+
- **New (public API):** `WorkerReportedError` — the reason `RemoteWorkerEnv` rejects a request with that the worker turned down: a module that would not import, a change trail its Kernel refused. `message` is the wording from the worker, `name` the name the error was thrown under there, so a caller reads it the same way it reads the name of an error a `LocalShadowObjectEnv` hands it. The class of the original error and the fields it added do not cross the boundary — `instanceof EntityUuidInUseError` is `false` on such a reason and there is no `uuid` field, whatever `name` says; `instanceof WorkerReportedError` is what tells a reported failure apart from one raised on the view side. Exported from `@spearwolf/shadow-objects`; it lives in `dist/src/view/RemoteWorkerEnv.js` and adds nothing to the published file list. Documented in `docs/api-reference.md`.
|
|
30
124
|
- **New (public API):** `RemoteWorkerEnv.WorkerFailed` (retained, payload `WorkerFailedEvent` with `env`, `type`, `message`, `reason`, `event`) and `WorkerFailedError`. `workerLoaded` rejects on a failure instead of hanging forever.
|
|
31
125
|
- **New (public API):** `ShadowEnv.ProxyFailed`, carrying the reason; `ContextLost` follows. A new `envProxy` is the way back: once it is ready the view re-creates its changes from the Component Memory, so the next sync restores every entity in the new environment. `IShadowObjectEnvProxy` gained the optional callback `onProxyFailed`, which `ShadowEnv` installs just like `onMessageToView`. `<shae-worker>` mirrors it as a `proxyfailed` DOM `CustomEvent`, alongside the `contextcreated` and `contextlost` it already dispatched.
|
|
32
|
-
- **
|
|
33
|
-
- **Behavior (worker environments):** a `start()` that fails now terminates the worker it created instead of just dropping the reference.
|
|
34
|
-
- **Bugfix (environments):** `LocalShadowObjectEnv.destroy()` emptied the registry it was working with. Unless a registry is handed to the constructor, that is the default one — shared with every other environment in the thread and with every class registered through `@ShadowObject` or `shadowObjects.define()` — so tearing down a single environment stripped the definitions from all of them. `destroy()` now only clears a registry that belongs to that environment alone; passing `Registry.get()` explicitly still counts as the shared one.
|
|
35
|
-
- **Bugfix (change trail, VIEW-23):** `ComponentChanges.changeToken()` dropped the pending create-token of a component that had not been flushed yet. `#token` starts at `VoidToken` and only tracks the last token *written to a trail*, so resetting the token of a fresh component (`c.token = undefined`, or any assignment of the void token) cleared `#nextToken` and produced a `CreateEntities` change with no `token` field at all — the kernel then registered the entity with `token: undefined` and never looked up a shadow object for it. A pending create now keeps its token, and `makeCreateEntityChange()` never emits a create without one.
|
|
36
|
-
- **Bugfix (view components, VIEW-24):** assigning a `ViewComponent.context` that had been disposed left the component in a state that reported itself as alive. The setter destroyed the component in its old context and *then* let `addComponent()` throw, so `isDestroyed` stayed `false` while every `setProperty` / `removeProperty` / `dispatchShadowObjectsEvent` silently went nowhere. A disposed context is now rejected before the teardown, so the component keeps its current context; any other failure to join leaves the component detached rather than pointing at a context that never took it in.
|
|
37
|
-
- **Bugfix (view components, VIEW-25):** `ComponentContext.changeOrder()` guarded against `dispose()` but not against `clear()`. Changing the `order` of a component after `clear()` pushed its uuid back into `#rootComponents` without a view instance, and the next `clear()` threw `component-context panic: #rootComponents is not empty!`. The guard now asks whether the context still holds the component.
|
|
38
|
-
- **Bugfix (view components, VIEW-26):** `ComponentContext.removeFromParent()` dereferenced the child entry outside the guard that checks it exists. `destroyComponent()` is public and does not detach the component, so `destroyComponent(c); buildChangeTrails(); c.destroy();` threw a `TypeError`. A child the context no longer holds is now ignored, matching the sibling paths.
|
|
39
|
-
- **Bugfix (entity lifecycle, KERN-8):** `Kernel.setParent()` defaulted an absent `order` to `0` and assigned it. A `SetParent` change only carries the order when it actually changed (see VIEW-18), so re-parenting an entity silently reset its order on the kernel side while the view side kept the real value — the two then disagreed, and a `ContextLost` recovery re-created from the wrong state. An absent order now keeps the current one; an explicit one still wins.
|
|
40
|
-
- **Bugfix (elements):** `<shae-worker>` produced an unhandled promise rejection whenever it was connected and disconnected within the same task. `connectedCallback()` autostarts via `start()` and the `src` effect calls `importScript()`; both await `ShadowEnv.ready()`, which rejects with a `ShadowEnvDestroyedError` once the environment is destroyed (see VIEW-8), and neither promise was observed. Both call sites now absorb that rejection — a teardown is silent, anything else is logged. `start()` and `importScript()` still reject for callers that do wait for them.
|
|
41
|
-
- **Breaking (elements):** `ShaePropElement` no longer carries `isShaeEntElement`. The flag was a copy-paste from `ShaeEntElement` and actively wrong: it is the marker `ShaePropElement` itself walks the ancestor chain for to find its host entity, so a `<shae-prop>` nested inside another `<shae-prop>` bound to the wrong element. Replaced by `isShaePropElement`.
|
|
42
|
-
- **Tests:** regression cases for VIEW-23 to VIEW-26 and KERN-8 in the existing specs; two new browser specs in `shadow-objects-testing` (`worker-element-teardown` for the unhandled rejection, `prop-element-host` for the host lookup through a nested `<shae-prop>`). All seven cases were verified to fail against the unfixed sources.
|
|
43
|
-
- **Bugfix (view components, VIEW-14):** `ComponentContext` silently lost components whose `order` fell into the first gap of an existing sibling list. The hand-rolled insertion in `#appendToOrdered` had no fallback when its backwards scan ran off the front, so for three or more siblings and `children[0].order <= order < children[1].order` the component was removed from `#rootComponents` but never added to the parent's children. It stayed in `#components`, was unreachable via BFS, never produced a `CreateEntities` change, and made the next `clear()` throw `component-context panic`. Reachable through `new ViewComponent(t, {parent, order})`, `new ViewComponent(t, {order})` and the `order` setter. Replaced with a linear insertion that skips uuids without a view instance instead of dereferencing them.
|
|
44
|
-
- **Bugfix (view components, VIEW-15):** `addChild()` accepted cycles. `a.addChild(b); b.addChild(a)` emptied `#rootComponents`, made the whole branch invisible to every change trail, and `a.addChild(a)` sent `removeSubTree()` into unbounded recursion. `addChild()` (and therefore the `parent` setter) now rejects the component itself and any of its ancestors with a `ViewComponentError`; the tree is left untouched when the check fires. `removeSubTree()` additionally tracks visited uuids so a pre-existing cycle cannot overflow the stack.
|
|
45
|
-
- **Bugfix (view components, VIEW-16):** `#deleteComponent()` removed a component from `#components` and `#rootComponents` but left its uuid in the parent's children list. `removeSubTree()` on a non-root therefore corrupted that list, and every later `getChildren()` or ordered insertion on it threw a `TypeError`. The uuid is now detached from the parent as well.
|
|
46
|
-
- **Bugfix (view components, VIEW-17):** registering a new `ViewComponent` under a uuid that was already in use reset the children list without telling the children. They kept pointing at the previous instance, were no longer root components, and dropped out of the tree. The previous instance's children are now promoted to root components.
|
|
47
|
-
- **Bugfix (context recovery, VIEW-18):** `ComponentMemory.setParent()` reset `order` to `0` whenever a `SetParent` change carried no order. Since `ComponentChanges` only includes `order` when it actually changed, re-parenting a component made the memory forget its order, and a `ContextLost` recovery re-created the entity with the wrong one. The order is now only overwritten when the change carries it.
|
|
48
|
-
- **Bugfix (sync, VIEW-19):** `ShadowEnv.syncWait()` never settled when the change trail was empty — `AfterSync` was emitted only inside `if (data.length > 0)`, and because the pending promise is cached, every later `syncWait()` returned the same dead promise. `AfterSync` is now emitted on every sync cycle with the (possibly empty) change trail, matching what the docs already promised. `#syncWaitForConfirmation` is reset in that path too, and a sync that finds the environment no longer ready re-arms itself instead of dropping a pending `syncWait()`.
|
|
49
|
-
- **New (public API, LOW-4):** `ComponentContext.dispose()` and `ComponentContext.isDisposed`. Until now a context could only be emptied with `clear()`, never released: it stayed in the global `__shadowEntsContexts` map for the lifetime of the page, so namespaces accumulated and a namespace could not be handed back. `dispose()` destroys every `ViewComponent` the context holds (each one then correctly reports `isDestroyed`), drops the component memory and releases the namespace, so `ComponentContext.get(ns)` creates a fresh context afterwards. A disposed context stays inert -- no components, empty change trails -- and rejects any component that tries to join it with a new `ComponentContextDisposedError`, rather than letting it look alive while never reaching an Entity. Idempotent. `clear()` is unchanged and remains the reusable reset.
|
|
126
|
+
- **New (public API):** `ComponentContext.dispose()` and `ComponentContext.isDisposed`. `clear()` empties a context and keeps it: the context stays registered in the global `__shadowObjectsContexts` map under its namespace, so clearing alone never hands the namespace back. `dispose()` destroys every `ViewComponent` the context holds (each one then correctly reports `isDestroyed`), drops the component memory and releases the namespace, so `ComponentContext.get(ns)` creates a fresh context afterwards. A disposed context stays inert — no components, empty change trails — and rejects any component that tries to join it with a new `ComponentContextDisposedError`, rather than letting it look alive while never reaching an Entity. Idempotent. `clear()` is unchanged and remains the reusable reset.
|
|
50
127
|
- **New (public API):** `ComponentContextDisposedError`.
|
|
51
|
-
- **Bugfix (sync, VIEW-8):** `ShadowEnv.destroy()` left every pending `ready()` and `syncWait()` caller hanging forever. Both promises are built on `onceAsync()` listeners, and `destroy()` removes all listeners via `off(this)` without settling them. They are now rejected with a new `ShadowEnvDestroyedError`; calling `ready()` or `syncWait()` after destruction rejects immediately, `sync()` becomes a no-op, and a sync scheduled before the destroy no longer runs. Rejecting rather than resolving is deliberate: resolving would report a sync that never happened. `destroy()` is now idempotent and no longer destroys the `envProxy` twice (the explicit call plus the one inside the `envProxy` setter).
|
|
52
128
|
- **New (public API):** `ShadowEnvDestroyedError`, the rejection reason for pending `ready()`/`syncWait()` promises when the environment is destroyed.
|
|
53
|
-
- **Behavior (view components, VIEW-20):** the destroyed state of a `ViewComponent` is now a defined contract instead of an accident of which call site used `?.`. Previously `order`, `setProperty`, `removeProperty`, `dispatchShadowObjectsEvent` and `dispatchEvent(…, true)` threw a `TypeError` after `destroy()`, while `token`, `removeFromParent` and `destroy` were silent no-ops and `addChild` threw a misleading "from another context". Now every mutation that only concerns the component itself is ignored, `dispatchEvent` still notifies the component's own listeners without traversing children, and `addChild` / `parent = …` throw a `ViewComponentError` that names the destruction. Assigning a `context` still revives the component under the same uuid.
|
|
54
129
|
- **New (public API):** `ViewComponent.isDestroyed` reports whether the component is detached from its `ComponentContext`.
|
|
55
130
|
- **New (public API):** `ViewComponent.setProperty()` and `ComponentContext.setProperty()` return a `boolean` telling whether the value differed from the last one written to a change trail. Previously the return type was `void` on `ViewComponent` and `false | void` on `ComponentContext`. Backwards-compatible.
|
|
56
|
-
- **
|
|
57
|
-
- **
|
|
58
|
-
- **
|
|
131
|
+
- **New (public API):** the event types in `src/elements/events.ts` — `RequestEntParentEvent`, `ReRequestEntParentEvent`, `ReRequestEntHostEvent` and the `ShadowObjectsEventMap` they sit in — are re-exported as types from `index.ts`. `HTMLElementEventMap` carries `shaeRequestEntParent`, `shaeReRequestEntParent` and `shaeReRequestEntHost` for every consumer of the package, so `el.addEventListener('shaeReRequestEntHost', …)` types its event argument.
|
|
132
|
+
- **New (public API):** the four worker timeouts are configurable. `new RemoteWorkerEnv(options?)` takes `loadTimeout`, `configureTimeout`, `changeTrailTimeout` and `destroyTimeout`, and `<shae-worker>` takes the same four as the attributes `load-timeout`, `configure-timeout`, `change-trail-timeout` and `destroy-timeout`. Every key is decided on its own; a key left out keeps `WorkerLoadTimeout`, `WorkerConfigureTimeout`, `WorkerChangeTrailTimeout` or `WorkerDestroyTimeout`, so a call that sets nothing behaves exactly as before. A valid value is a number of milliseconds from `1` to `2147483647`, close to 25 days — `0` and `Infinity` are refused along with `NaN`, negative numbers and non-numbers, because neither of the two arms a timer and a `destroyTimeout` set to one of them would leave `destroy()` waiting for an acknowledgement a dead worker never sends, with the `terminate()` at the end of that chain never running; and anything above the upper bound is refused because `setTimeout()` truncates its delay into a signed 32-bit field, so such a value comes back out as some other, shorter one — a millisecond past the bound fires at once. A refused value is reported through `env.logger.error` and the constant applies; nothing is thrown, so the constructor and the attribute behave alike. `env.timeouts` is the frozen, read-only record of what an environment actually holds itself to. The two types `WorkerTimeouts` and `RemoteWorkerEnvOptions` are exported. The four attributes are not observed — they are read once, when the environment is built — and under `local` they do nothing and say nothing.
|
|
133
|
+
- **New (logging):** `setConsoleLoggerStorage(config)` is exported alongside `ConsoleLogger` from the `@spearwolf/shadow-objects/ConsoleLogger.js` subpath. It installs a config object as the fallback store directly, bypassing the storage probe. This is the coupling point between `WorkerRuntime` and the logger module: the runtime calls it with the config a `RemoteWorkerEnv` forwards to a worker, where there is no `localStorage` to probe in the first place. The subpath does not hide it, but nothing outside that pair is expected to call it, and the package entry point does not re-export it.
|
|
134
|
+
- **New (kernel):** a node of `Kernel.getEntityGraph()` names a child its walk left out, under `omittedChildren` — the uuid and whether the reason is a uuid the Kernel no longer holds or one the walk has already placed elsewhere in the graph. The field is reachable through a value `getEntityGraph()` returns and through the structural shape of the (unexported) `EntityGraphNode` declaration, not through an importable type: `Kernel` itself is exported from `@spearwolf/shadow-objects/shadow-objects.js`, but `EntityGraphNode` carries no `export` and is not a name a consumer can bring in. Documented in `docs/api-reference.md`.
|
|
135
|
+
- **New (public API):** `<shae-ent auto-destruct>` — the boolean attribute that hands `autoDestructionOnParentRemoval` to the `ViewComponent` the element builds, so an Entity written in markup can say that it goes down with its parent instead of being promoted to a root. Read as a truthy value the way `local`, `no-autostart` and `no-trim` are: `on`, `true`, `yes`, `local`, `1` or the bare attribute set it, everything else leaves it unset. It is not among `observedAttributes` and is read exactly once, where the component is built — the flag is immutable on a `ViewComponent`, so a later write has nowhere to go. The flag is read by the Kernel, on an Entity whose parent the Kernel destroys; taking a `<shae-ent>` subtree out of the document is not such an occasion, because the View Layer detaches a component's children before it destroys the component. Documented in `docs/api-reference.md`, `docs/cheat-sheet.md` and `docs/guides.md`.
|
|
136
|
+
|
|
137
|
+
### Behavior
|
|
138
|
+
|
|
139
|
+
- **Behavior (view):** `ComponentContext` answers for the instance it is given, not for its uuid. `hasComponent()` is the question about membership and reads `false` for a component that has left the context, even while its entry is still standing — `hasComponents()` is the question about the entry, and the two part company for exactly the span between a departure and the change trail that clears the entry away. An entry that survives that span can be taken over by a later component under the same uuid, and `getChildren()`, `isRootComponent()`, `isChildOf()`, `changeOrder()`, `changeToken()`, `setProperty()`, `removeProperty()` and `dispatchShadowObjectsEvent()` no longer touch that entry when they are called with the component that left: each of them does nothing and answers with the empty result of its return type. `addChild()` and `parent = …` throw a `ViewComponentError` naming a parent its context does not hold, alongside the two reasons they already had. `docs/api-reference.md` says so.
|
|
140
|
+
- **Behavior (elements):** an element that becomes an entity while the markup around it already stands asks the entities around it to look for their parent again one microtask later, and everything that becomes an entity in the same task is answered by one round. `customElements.define()` therefore returns before `entParentNode` and `viewComponent.parent` below it have moved — `await Promise.resolve()` before reading either, the same wait a `<shae-prop>` already needs. What the round delivers is unchanged: the hierarchy it leaves behind is the one the element tree shows, and a change trail built in the same task carries it, because the round runs at the head of the build.
|
|
141
|
+
- **Behavior (kernel):** a uuid names one entity at a time. `Kernel.createEntity()` refuses a uuid the Kernel already holds an entity for with an `EntityUuidInUseError`, where the second creation used to take the entry over and leave the entity behind it standing — with its Shadow Objects, its signals and its contexts — out of reach of every teardown, because nothing held that uuid any more. The refusal comes before the call writes anything: the standing entity keeps its Shadow Objects, signals, contexts, token and properties, and a `parentUuid` the refused call named gains no child. The uuid is free again once `destroyEntity()` has been through. In a change trail the trail is refused at that entry, `appliedCount` names the prefix that went through, and `cause` carries the `EntityUuidInUseError` — the object itself locally, and across a worker boundary a `WorkerReportedError` carrying its wording and its name, without the class and without the `uuid`. What follows for the way back: a re-creation from the Component Memory belongs to a fresh proxy — `env.envProxy = new RemoteWorkerEnv()`, into which `ShadowEnv` re-creates the components by itself — while against an environment whose Kernel still holds those uuids it is refused, cycle after cycle. `docs/api-reference.md`, `docs/guides.md` and `docs/cheat-sheet.md` say so.
|
|
142
|
+
- **Behavior (view):** a change trail the Kernel refuses is no longer lost. A trail is applied entry by entry and the first throw ends the run, so what the Kernel holds afterwards is a prefix of it — and the refusal names the length of that prefix. `ShadowEnv` folds exactly those entries into its bookkeeping and leaves the rest pending, so the next cycle carries them again and both sides agree on what is applied. The other side of that promise belongs to the application: a cause that stays put — a token no definition exists for, a Shadow Object whose constructor always throws — refuses every following cycle the same way, where the trail used to be dropped once and the missing state never noticed. `ShadowEnv.SyncFailed` is where that is stopped. A reason that says nothing about how far the Kernel got — a confirmation window that ran out, a `WorkerDestroyedError`, a proxy of someone else's making — still counts the whole trail as applied, which is the safe direction: a worker that timed out may well hold all of it, and a creation sent a second time to a Kernel that holds the entity is refused, so a trail kept pending on a guess would come back to that refusal cycle after cycle. Over a worker the count travels on the confirmed route only: `syncWait()` asks for a confirmation, `sync()` does not, and a trail nobody waits for gets no answer to read a count from.
|
|
143
|
+
- **Behavior (view):** the reason `ShadowEnv.syncWait()` rejects with, and the one `ShadowEnv.SyncFailed` carries, is a `ChangeTrailRefusedError` where the Kernel refused the trail. Across a worker boundary that used to be the bare string the worker sent; locally it was the thrown error itself. Both are now in `cause` — the error object itself locally, and across a worker boundary a `WorkerReportedError` carrying its wording and its name — so the two sides answer alike. Every other reason — `WorkerFailedError`, `WorkerDestroyedError`, `ShadowEnvDestroyedError` — is unchanged; a confirmation window that ran out is a `WorkerTimeoutError`, see the entry above.
|
|
144
|
+
- **Behavior (view):** a `ShadowEnv.AfterSync` or `ShadowEnv.SyncFailed` listener that throws no longer decides the fate of the cycle it was told about. The promise `ShadowEnv.syncWait()` handed out is settled before either event goes out, so it settles whether or not the listeners get through, and the throw is reported through the `ConsoleLogger` instead of leaving `ShadowEnv` as an unhandled rejection. What stays as it is everywhere else in the library: the listeners registered behind the throwing one hear nothing.
|
|
145
|
+
- **Behavior (elements):** `<shae-worker>` takes its subscriptions up when it first connects, and a `start()` made by hand on an element that is out of the document therefore runs ahead of them. Two things are lost in that window and are not replayed: the `contextcreated` DOM event for the environment built there — and `contextlost`, should the environment lose its connection before the element connects — because both name a moment that has passed; and the namespace, where `ns` is changed between that `start()` and the first connect, which leaves `shadowEnv.view` on the context of the namespace the element was started with. A write of `ns` after the first connect moves the view as usual, and assigning `shadowEnv.view` by hand settles it at any point. A `src` set in the same window is caught up on connect. An element that connects before it starts — the declarative path, and the default — meets none of this. Named in `docs/api-reference.md`.
|
|
146
|
+
- **Behavior (elements):** a `<shae-ent>` that comes back into the document with the token it left with no longer queues a synchronization for it. The element hands its entity the token as it reconnects, and where the entity already carries that very token the write and the sync it would have caused are both skipped.
|
|
147
|
+
- **Behavior (elements):** a whitespace-only `value` attribute is the empty string once trimmed, and is converted from there — `<shae-prop type="number" value=" ">` is `0`. Previously it set no property at all. `no-trim` is unaffected and still keeps the whitespace as the value.
|
|
148
|
+
- **Behavior (elements):** a `<shae-prop>` value that cannot be converted into the requested type is reported through the `ConsoleLogger` and sets the property to `undefined`, instead of throwing out of the element. This covers `json`, `bigint`, `bigint64array` and `biguint64array` — the four types whose conversion can fail — and applies to both paths, the `value` attribute and the `value` JS property. Assigning an unconvertible value to `prop.value` therefore no longer raises a `SyntaxError` at the caller. The report goes through `logger.error`, which is not gated behind `ConsoleLogger.sharedConfig.enable`, so a dropped value stays visible outside localhost.
|
|
149
|
+
- **Behavior (environments):** `LocalShadowObjectEnv.applyChangeTrail()` settles one microtask later when called with `waitForConfirmation`, instead of in the same microtask as the call. The kernel still runs synchronously above, before the method returns — only the settling of the returned promise is deferred, so it never settles in the same microtask as the call that made it. This is not the same relative ordering `RemoteWorkerEnv` gives its own promises — that one settles only after its worker round-trip and can land later still.
|
|
150
|
+
- **Behavior (view):** a consumer listening for `ShadowEnv.ProxyFailed` can no longer stop the environment from noticing its own loss — `isReady` drops and `ContextLost` fires even when that listener throws.
|
|
151
|
+
- **Behavior (view components):** the destroyed state of a `ViewComponent` is now a defined contract instead of an accident of which call site used `?.`. Previously `order`, `setProperty`, `removeProperty`, `dispatchShadowObjectsEvent` and `dispatchEvent(…, true)` threw a `TypeError` after `destroy()`, while `token`, `removeFromParent` and `destroy` were silent no-ops and `addChild` threw a misleading "from another context". Now every mutation that only concerns the component itself is ignored, `dispatchEvent` still notifies the component's own listeners without traversing children, and `addChild` / `parent = …` throw a `ViewComponentError` that names the destruction. Assigning a `context` still revives the component under the same uuid.
|
|
152
|
+
- **Behavior (worker environments):** a `start()` that fails now terminates the worker it created instead of just dropping the reference.
|
|
153
|
+
- **Behavior (kernel):** `clearOnDestroy` sets the provided context to `undefined` and every consumer sees that, on both teardown paths — the entity is destroyed, or the Shadow Object leaves the constructor set of an entity that lives on (a token change, a route change). On the second path the clearing used to reach the provider signal alone and never the `useContext` readers below it. Applies to `provideContext` and `provideGlobalContext` alike. Whoever relied on a child keeping the last value after a token change now reads `undefined` there; the way back to the old reading is `{clearOnDestroy: false}`, on an entity that has no second provider of the name — see the entry below.
|
|
154
|
+
- **Behavior (kernel):** an entity that has several Shadow Objects providing the same context name hands that name over when one of them leaves. Consumers then read the value of a provider that is still there — the one attached last that holds a value, which falls back on the order in which the providers took the name rather than on the order in which they last wrote to it — and `undefined` only once the last provider of the name on that entity is gone. This decides the name regardless of what the leaving Shadow Object wrote on its way out, so `{clearOnDestroy: false}` no longer holds the name for a Shadow Object that is gone. It covers `provideContext` and `provideGlobalContext` alike; for the latter the chain across entities is unchanged and still resolves to the first non-empty contribution. Whoever wants the old reading keeps one provider per name and entity.
|
|
155
|
+
- **Behavior (kernel):** an `onDestroy` callback or a `createResource` cleanup that throws no longer stops the rest of `tearDown()` — not the remaining cleanup of the same Shadow Object, not the teardown of the other Shadow Objects on the same entity, and not the destruction of the entity itself. The error is reported through the `ConsoleLogger` instead of reaching whichever call set the teardown going: `destroyEntity()`, `changeToken()`, `changeProperties()` or `upgradeEntities()`. A listener on the entity itself is held to the same guarantee — a callback registered through `on(onDestroy, …)` or `once(onDestroy, …)` on the creation API as much as one put on the entity from the outside at any priority: the kernel runs the creation scopes and the entity's own release itself once the entity-wide notification is through, however that notification ended, so a throw there costs neither. What it does cost is the listeners registered behind it on the same entity, because that notification is a single delivery. It is only reached where the entity is destroyed; where a Shadow Object leaves the constructor set of an entity that lives on, the entity sends no such notification. The entity's own release — `entity[onDestroy]()` — runs once whichever way it is reached, so a repeated call does nothing.
|
|
156
|
+
- **Behavior (kernel):** a shadow-object can carry a listener another shadow-object put on its own `onDestroy` notification (`on(otherShadowObject, onDestroy, …)`), and that notification goes out on both teardown paths: the entity is destroyed, or the shadow-object leaves the constructor set of an entity that stays alive. A throw from such a listener is reported through the `ConsoleLogger` instead of reaching the call that set the teardown going — `destroyEntity()`, `changeToken()`, `changeProperties()` or `upgradeEntities()` — and no longer stops the leaving shadow-object's own teardown or its removal as an entity listener.
|
|
157
|
+
- **Behavior (logging):** a `ConsoleLogger` no longer writes its per-namespace `enable` key into the storage of its host when it is constructed; the key is still read there. Past the storage-capability probe at module load, the only remaining write path is the `globalThis.ConsoleLogger` handle's four setters, which write the four shared switches. A namespace that was turned off through a previous key already sitting in storage keeps reading it as before — what disappears is the key a mere construction used to leave behind.
|
|
158
|
+
- **Behavior (kernel):** the teardown of a Shadow Object writes `undefined` into a provided context signal once, however many times `provideContext` or `provideGlobalContext` were called for that name. One call asking for the clearing is still enough, and no later call takes the request back. A `compare` function on such a signal sees one write at teardown instead of one per call.
|
|
159
|
+
- **Behavior (elements):** `<shae-worker>` refuses a write to its `local` attribute once the environment is built, instead of throwing out of the attribute reaction. The refusal is reported through `logger.error`, not gated behind `ConsoleLogger.sharedConfig.enable`, and the attribute is written back to the canonical spelling of the environment in effect — the bare `local` attribute for a local environment, or its absence for a worker environment. A write that leaves the effective value unmoved (`local` to `local="yes"`) does nothing and reports nothing. Switching environments requires building a new `<shae-worker>` element. Named in `docs/api-reference.md`, `docs/cheat-sheet.md` and `docs/best-practices.md`.
|
|
160
|
+
- **Behavior (view components):** an entity uuid comes from `crypto.getRandomValues()` where `crypto.randomUUID()` is out of reach. `randomUUID()` is bound to a secure context, and a page served over plain `http://` — a LAN address during development is the usual way to meet that — used to name its entities from `Math.random()` without saying so. `getRandomValues()` carries no such restriction and is the same cryptographic generator. The uuid keeps its canonical version-4 form. `Math.random()` remains the last source, for a realm with no Web Crypto API at all, and announces itself once on the console, under the namespace `generateUUID` and through `logger.error` — the one level that checks no getter, so the line reaches every host and not only a loopback one — where it used to stay silent. The 256-entry hex lookup table `generateUUID()` carried for its `Math.random()` formatting is gone, along with the bytes it added to every bundle.
|
|
161
|
+
- **Behavior (view):** a `CreateEntities` change trail entry leaves the `properties` field off where filtering its pending values for `undefined` leaves nothing — an absent field and an empty array say the same thing, and the entry now carries the shorter one. The case arises when a property is set back to `undefined` while the create entry that would carry it is still unconfirmed.
|
|
162
|
+
- **Behavior (view):** `ShadowEnv.ns$` carries the namespace of the `ComponentContext` an environment observes. The `view` setter writes it, so it is the same name `ShadowEnv.get()` finds the environment under, unless another environment has since taken that namespace over; a `view` that moves to another namespace, is cleared, or goes with a `destroy()`, takes the signal along. A consumer that reads or builds an effect on this slot used to find `undefined` for the whole lifetime of an environment; it now reads the namespace, and a fresh one the moment `view` moves. Named in `docs/api-reference.md`.
|
|
163
|
+
- **Behavior (logging):** the shared `ConsoleLogger.sharedConfig.enable` switch, which gates every `debug`, `info` and `warn` line the library reports on its own, turns on for a page whose hostname is exactly `localhost`, `127.0.0.1` or `[::1]` — a name that merely carries one of these as a prefix, such as `localhost.example.com`, does not match.
|
|
164
|
+
- **Behavior (elements):** `<shae-ent>` reports a parent change only where the node it left and the node it landed on are two different nodes; an atomic `moveBefore()` that reorders the element within the same parent no longer reaches an `onParentChanged` override, and the observation of that parent node stands unchanged afterward.
|
|
165
|
+
|
|
166
|
+
### Bugfixes
|
|
167
|
+
|
|
168
|
+
- **Bugfix (view):** `ComponentContext.reCreateChanges()` rebuilds a property that was set without a value as one. It used to read the one-element memory entry as `[key, undefined]` and hand it on as a removal, which the change bookkeeping then discarded as "nothing changed" — the key fell out of the rebuild without a word, and a component that came up on a fresh proxy was missing it.
|
|
169
|
+
- **Bugfix (kernel):** a Shadow Object whose `onViewEvent` callback throws no longer refuses the Change Trail the event arrived in. The delivery is guarded per event now, the way the `onParentChanged` notification has been, so one bad callback costs neither the events behind it nor the whole trail.
|
|
170
|
+
- **Bugfix (kernel):** the creation scopes of a destroyed Entity are reached by the `onDestroy` notification they listen on. They sit at `Priority.Low`, and a listener above them that threw used to end the delivery before their turn; the Kernel's own sweep behind the notification caught that and still does, as the backstop for a listener that unsubscribes the ones behind it and returns normally.
|
|
171
|
+
- **Bugfix (view):** `ViewComponent.destroy()` takes the subscriptions off even when a `ViewComponent.Destroyed` listener throws. The `off()` stands directly behind the announcement, and the error used to carry the method past it — the component ended its life holding every subscription it was meant to be rid of.
|
|
172
|
+
- **Bugfix (view):** a peer re-request round is delivered in full when one of its receivers throws. The failure used to end the round it belonged to, so the elements behind that receiver were never asked.
|
|
173
|
+
- **Bugfix (view):** a `ShadowEnv` that is never used can be collected. The effect behind `ContextCreated` and `ContextLost` is built with the first `view` or `envProxy` the environment is given and taken down again by `destroy()`, so an environment that receives neither half — a `<shae-worker>` that is created and never connected carries one — leaves nothing in signalize's module-level effect queue, and a destroyed environment leaves nothing there either. Measured with `--expose-gc`. That build step runs outside the reactive context of the caller, so the effect belongs to the environment and not to a `createEffect()` the assignment stands in — one part of which is observable: assigning the first `view` or `envProxy` inside an open `batch()` flushes it, so the effects that batch holds back run at that point, while the writes the setter itself makes stay in the batch until it closes. The flush hangs on the first assignment that carries something — a `null` or `undefined` handed to either property builds nothing — and once the effect stands, every further assignment leaves an open batch alone. Named in `docs/api-reference.md` under the `ShadowEnv` properties.
|
|
174
|
+
- **Bugfix (elements):** an element that is created and never put into a document can be collected. `<shae-ent>`, `<shae-prop>` and `<shae-worker>` take their subscriptions up when they first connect rather than when they are built, so a `document.createElement()` that goes nowhere leaves no effect and no `onChange` handler behind — signalize registers each of those in a module-level queue, and one registered from a constructor held its element for the lifetime of the page however little was done with it. The constructor now reads attributes into signals and stops there. Measured with `--expose-gc`: an element of each of the three tags, created and dropped, is gone after a collection, alongside the control element that carries none of this library.
|
|
175
|
+
- **Bugfix (elements):** the subscriptions of an element belong to the element, whoever put it into the document. An `append()` from inside a `createEffect()` of the application used to hand every subscription the connect took up to that effect, whose next run released them again — the element went quiet with `isDestroyed` still reading `false` and nothing reported. All three elements now run their `connectedCallback` outside the caller's reactive context. That context is suspended, not ignored, and one part of it is observable: a `batch()` the caller has open is flushed before the connect runs, so in `batch(() => { someSignal.set(1); container.append(ent); })` the effects of that write now run before the element is appended rather than after the batch closes. Code that relied on a batch holding its effects back across an `append()` sees them earlier.
|
|
176
|
+
- **Bugfix (elements):** `document.createElement('shae-ent' | 'shae-prop' | 'shae-worker')` hands back a live, upgraded element. The Custom Elements specification forbids a constructor from giving its element an attribute; all three wrote one — an inline style is the `style` attribute, and the namespace reflection writes `ns` — so the browser aborted the upgrade and answered with an `HTMLUnknownElement`: no `viewComponent`, no `uuid`, no connection to the Shadow Environment. Every wrapper that builds its elements programmatically instead of parsing markup was hit by it, React, Vue and Svelte among them. The styling now travels as a stylesheet rule (see above), and an attribute a signal wants to reflect before the element first connects is held until it does — the value written back is the one the signal already carries, so nothing observes it as a change. Parsed markup behaves as it did.
|
|
177
|
+
- **Bugfix (kernel):** `Entity.getPropertyWriter(key)` — the write head behind `setProperty()`, reachable on the `Entity` instance `entity.kernel.getEntity(uuid)` hands back but not part of the `EntityApi` surface `entity` itself exposes to a shadow object — drops the cache behind `truthyProps()` on every write, the way `setProperty()` does. That cache answers `#updateShadowObjects()`, which picks the shadow objects of an entity along the property routes of the registry, so a property written through the handed-out writer used to route the entity by a state it no longer had — a `'@prop'` route stayed unmatched until something else dropped the cache. What comes back is a function of the entity rather than the `set` of the signal behind the property, and the signalize helpers that take a signal-like — `isSignal()`, `destroySignal()`, `touch()` — do not recognise it, and never did; `getPropertyReader(key)` is the way to the signal's own reader. No signature change.
|
|
178
|
+
- **Bugfix (kernel):** every route that moves an entity in the entity tree keeps the Kernel's bookkeeping current. `createEntity()`, `setParent()` and `updateOrder()` drop the cached traversal themselves rather than leaving that to the change-trail delivery that happens to call them — the API reference names those three as the way a shadow-object constructor creates and re-parents entities, and an entity created that way used to be missing from `traverseLevelOrderBFS()`, so `upgradeEntities()` gave it no shadow-objects on a later module import and `kernel.destroy()` never reached it: its shadow-objects heard no `onDestroy` and the bookkeeping was cleared out from under them. The set of root entities follows the parent link on every route that writes it, `entity.parent`, `entity.parentUuid` and `entity.removeFromParent()` included; an entity attached through a setter used to stay listed as a root, and a detached one used to be listed nowhere at all, out of reach of every traversal. `Kernel.noteEntityTreeChange(uuid)` is that report and is public, because `Entity` sends it. The routes that still report nothing are written down: `Entity.addChild()`, `Entity.removeChild()`, `Entity.resortChildren()` and `ComponentContext.addToChildren()` write or reorder a children list without a parent link, so a caller doing that drives the report itself — and gets the cache drop out of it, not a place in the root set. `onParentChanged` is unchanged and stays with `Kernel.setParent()`. Documented in `docs/api-reference.md`.
|
|
179
|
+
- **Bugfix (kernel):** a shadow-object whose class-side `[onCreate]` hook throws is taken down again instead of staying on its entity. Attaching is the last step of a creation, and it now goes the same way the constructor call does: the shadow-object hears its `[onDestroy]` hook, every `onDestroy` callback it registered and the `onDestroy` notification other objects listen for, gives up its creation scope with the signals, effects and context feeds in it, and comes off the entity as a listener — then the error travels on to the caller. On a `changeToken()` or `changeProperties()` the entity lives on, and a half-attached shadow-object used to stay standing on it: listed by `findShadowObjects()`, still hearing the entity's events, with effects that kept running. Documented in `docs/api-reference.md` under `onDestroy(callback)` and in the symbol table.
|
|
180
|
+
- **Bugfix (kernel):** `Kernel.changeToken()` rolls an entity back to its previous token and shadow-objects when one of the new token's constructors throws, so it is not left carrying a token whose shadow-objects it is missing. The new token is written before the constructors run, because the constructor set is resolved from it; where a construction throws, the kernel puts the previous token back, takes the shadow-objects of the new token down and builds the ones of the previous token again, before the error reaches the caller. That holds as far as the way back gets: a rebuild that throws in its turn is reported through the `ConsoleLogger` rather than replacing the error the caller is waiting for, and the entity keeps what the rebuild managed. `changeProperties()` takes its shadow-objects back the same way. Three limits are written down in `docs/api-reference.md`: written properties stay written, so a restored shadow-object can stand on an entity whose properties no longer route to its constructor, until the next re-resolution of the set takes it off; and `upgradeEntities()` puts everything its first pass took down out of reach, at the entity whose rebuild throws as much as at every other one — teardown and rebuild are two passes over the entity tree, and the rollback belongs to the second-pass call, which took nothing down of its own. That entity keeps the token it came in with and is left with whatever the first pass spared. No signature changes.
|
|
181
|
+
- **Bugfix (kernel):** `kernel.destroyEntity()` runs to its end whatever a teardown throws. Each shadow-object of the entity is told on its own — the class-side `[onDestroy]` hook and the `onDestroy` notification other objects listen for, each behind a guard of its own — so a shadow-object that throws costs neither its siblings their teardown, nor the creation scopes theirs, nor the entity its own cleanup of properties, subscriptions and contexts. The error is reported through the `ConsoleLogger` rather than handed to the caller, which is the outcome a token change has always given the same throw: one cause, one outcome, whichever path a shadow-object ends on. The kernel's bookkeeping — the constructor map, the entity entry, the root registration and the BFS cache flag — is cleared in a `finally`, so a destroyed entity never stays in the kernel with `hasEntity()` answering `true` and `findShadowObjects()` listing objects that are already torn down. A cascading child whose destruction fails is reported by uuid and leaves its siblings the treatment they were owed. In a worker this keeps the change trail carrying the destruction intact, where the throw used to break it off and reach the view as a rejected trail. No signature changes.
|
|
182
|
+
- **Bugfix (change trail):** `ComponentChanges.changeToken()` dropped the pending create-token of a component that had not been flushed yet. `#token` starts at `VoidToken` and only tracks the last token *written to a trail*, so resetting the token of a fresh component (`c.token = undefined`, or any assignment of the void token) cleared `#nextToken` and produced a `CreateEntities` change with no `token` field at all — the kernel then registered the entity with `token: undefined` and never looked up a shadow object for it. A pending create now keeps its token, and `makeCreateEntityChange()` never emits a create without one.
|
|
183
|
+
- **Bugfix (context recovery):** `ComponentMemory.setParent()` reset `order` to `0` whenever a `SetParent` change carried no order. Since `ComponentChanges` only includes `order` when it actually changed, re-parenting a component made the memory forget its order, and a `ContextLost` recovery re-created the entity with the wrong one. The order is now only overwritten when the change carries it.
|
|
184
|
+
- **Bugfix (decorator):** a class registered through `@ShadowObject` keeps its own name. The decorator hands the Registry a subclass of the decorated class, and that subclass used to answer `.name` with `__ShadowObject` — the name of its own class expression, which shadows the inherited one — so every Kernel diagnostic that names a Shadow Object read `__ShadowObject` rather than the class it meant: the report about a lifecycle hook written as a plain method, the reports about a failing `[onDestroy]` hook and a rolled-back token change, and every teardown step of a creation scope. A stack frame through the constructor follows the same name. A `static displayName` on the decorated class is unaffected — statics are inherited, and the Kernel still prefers it over the name. Documented in `docs/api-reference.md`. No signature changes.
|
|
185
|
+
- **Bugfix (elements):** a `<slot>` that is moved out of one entity takes its projection with it. Everything the slot projects hangs on the closest entity above the slot, and both channels follow that move: a `<shae-prop>` re-binds to the entity the slot arrived under, and so does the `entParentNode` of a projected `<shae-ent>`, together with its place in the entity tree. `slotchange` fires after the move and therefore at the new position, where the entity that lost the projection has no name any more — so that entity hangs its listener on the `<slot>` element itself and hears the event wherever the slot has landed. Whichever of the two sides reaches it first asks every entity and every property in every namespace to look again, and the round runs at most once per `slotchange`. What stands at the destination therefore decides nothing: a slot landing in a part of its shadow root with no entity above it, and `slot.remove()`, are followed like any other move. An entity takes up the slots below it when it enters the tree, so a shadow host that leaves the document and is inserted again answers for what its shadow root holds as it did before — the assignment there does not change on the way, and nothing reports it. The round runs only when the entity above the slot actually changed; a slot reporting its first assignment and a slot that stays where it is and gets new content cost nothing extra. Like every change above an element, the re-binding takes effect one microtask later. Where a projected `<shae-ent>` finds no answering ancestor in its own namespace, it ends up without a parent rather than with the old one. All of it is written down in `docs/api-reference.md`.
|
|
186
|
+
- **Bugfix (elements):** a `<shae-prop>` finds the same entity a `<shae-ent>` in its place would find. Both elements now send one and the same request, so the host is the closest entity above the element in the flattened tree — through shadow roots, along slot projections, across closed boundaries — regardless of its namespace. The lookup used to walk `parentElement`, a chain that ends at the top element of a shadow root: a `<shae-prop>` inside one never reached its host, and where a slot or a closed boundary was involved it bound to an entity further out instead of the one right above it. A move to a position with no entity above it leaves the element without a host, and the property is taken off the entity it left. The rule is written down in `docs/api-reference.md` under `#### Finding the Host Entity`.
|
|
187
|
+
- **Bugfix (elements):** a `<shae-prop>` follows the closest entity above it while it stays where it is. Every way of becoming that entity moves the binding — a custom element whose tag is registered late takes the properties under it along, a shadow root attached afterwards takes over what its slots project, a changed slot assignment is followed, and so is the `<slot>` element moving into another entity — and so does every way of ceasing to be one: when the host entity leaves the tree, the property binds to the next entity above it, or to none if there is none. The host used to be resolved when the element connected and never again, which left a property on an entity that was no longer the closest one, or on none at all, without a message about it. A re-binding takes effect one microtask after the change, not in the same step. A `<shae-prop>` with no entity anywhere above it reports that once through the `ConsoleLogger`, gated behind `ConsoleLogger.sharedConfig.enable` like every `warn`.
|
|
188
|
+
- **Bugfix (elements):** `<shae-ent>.findShadowRootHost()` answers for the tree the element stands in at that moment. An element that has left the tree — removed itself, or carried out with an ancestor — used to keep naming the host it last sat under; it answers `undefined` now, which is what the documentation says of an element outside a shadow root.
|
|
189
|
+
- **Bugfix (elements):** a `<shae-prop>` clears the property it declared when its binding ends. A property belongs to the pair of host entity and name, and that pair is undone in three ways: the element leaves the tree, its `name` attribute changes, or it moves to another entity. All three used to leave the value standing — a removed element left its property behind, a rename added the new name next to the old one, and a move left the property on both entities. A removal and a re-insertion within the same tick stays a move and keeps the property; removing the whole entity writes no property change, because the entity takes its properties with it. Two elements may declare the same name on the same entity — the property is cleared once the last of them lets go, not the first. A cleared property reads `undefined` on the Shadow Object side; the key stays visible in `propKeys()` and `propEntries()`, so a reader from `useProperty()` survives the whole lifecycle. The lifecycle is written down in `docs/api-reference.md`.
|
|
190
|
+
- **Bugfix (elements):** the environment a `<shae-ent>` leaves on a namespace change is told to sync. The destruction of the entity used to sit in the old context until something unrelated happened to flush it, which for an entity without children could be arbitrarily long — the element itself only syncs the namespace it has arrived in.
|
|
191
|
+
- **Bugfix (elements):** a `<shae-ent>` moves under its closest entity ancestor when that ancestor is registered with `customElements.define()` while it already sits in the document. Parent resolution runs on a bubbling, composed event that only listening elements answer, and an element not yet upgraded does not listen — the entity below it stayed attached to the next ancestor up, permanently and without a warning, giving it a wrong Entity Context. Every application that subclasses `ShaeEntElement` or puts entities inside a lazily registered wrapper element is affected. The adjustment covers entities that were roots as well as entities that already had a parent, and it reaches across shadow boundaries. It does not extend to a move that leaves the element connected — see the parent-resolution rules in `docs/api-reference.md`.
|
|
192
|
+
- **Bugfix (elements):** `<shae-ent>` re-binds the entity tree when its `ns` changes at runtime. The element leaves the `ComponentContext` of the old namespace and joins the new one, and both directions of the binding follow: its own `entParentNode` is either resolved anew in the new namespace or honestly empty, and the entities that hung on it are told to look for their closest ancestor again — an element that gains a namespace hands them to the next ancestor, one that loses it takes the entities below it under itself. The way back restores the shape the tree had before. `entParentNode` used to keep pointing at an ancestor from the old namespace while the entity tree had already let go, and the entities below stayed where they were.
|
|
193
|
+
- **Bugfix (elements):** an entity that stays in the tree while the `<shae-ent>` it hung on leaves it looks for the closest ancestor still answering. This is what happens to everything projected into a slot that the departing element holds: removing that element takes the slot along, and the entity below it never sees a lifecycle callback of its own. The entity tree lets go on its own — the departing `ViewComponent` promotes its children to roots — so the element side used to be left pointing at an element that is no longer in the document, with no way back.
|
|
194
|
+
- **Bugfix (elements):** the observation of the parent node follows a `<shae-ent>` to its new position. It watches one specific node, so once the element sits somewhere else the old observation reports a node that is no longer there and every further move goes unseen. This is reachable through a subclass of `ShaeEntElement` that defines `connectedMoveCallback` and is moved with `Element.moveBefore()` — the one path on which the element changes parents without running through `disconnectedCallback`.
|
|
195
|
+
- **Bugfix (elements):** a `<shae-ent>` whose `ns` is set before it enters the tree becomes an entity. Writing `ns` — through the attribute or the JS property — on an element that is constructed but not yet connected moves the component context ahead of the element, and the element used to stay in that namespace forever without a `ViewComponent`: no entity, no properties arriving, no sync, and no message about any of it. The same state was reachable by changing `ns` while the element sat outside the tree, which left it holding a destroyed `ViewComponent` after it came back. Both are resolved when the element connects.
|
|
196
|
+
- **Bugfix (elements):** `<shae-prop>` keeps `0`, `false` and the empty string assigned through the `value` JS property. `ComponentChanges` reads an explicit `undefined` as removing the property, so those three used to leave the Shadow Object without the property at all instead of with a falsy value. An empty `value` attribute still counts as a missing one and sets nothing; the normalization for that case sits where attributes are read, not in the conversion effect.
|
|
197
|
+
- **Bugfix (elements):** `forwardCustomEvents$.set(true)` writes the empty string into the `forward-custom-events` attribute whatever stood there before; a filter list written in the markup is replaced instead of being left standing. The `true` branch of the reflection used to write only when the attribute was absent altogether, so an element that carried `forward-custom-events="a,b"` forwarded every event while its attribute still named two — and a later re-append read that stale list back as the truth. The attribute is the serialized form of the signal, in both directions. A value that already says the same thing as the signal keeps its spelling, so `forward-custom-events=" "` stays as written — in the markup, and on an element already forwarding everything. Normalization is what a changed signal writes back: a `setAttribute` to a whitespace-only value normalizes to `""` where it moves the signal to `true`, coming from a filter list or from nothing.
|
|
198
|
+
- **Bugfix (elements):** the `forward-custom-events` read-back on connect reaches the subscribers of the signal, so what a `<shae-ent>` forwards follows the filter the element carries. The `dispatchEvent` patch on the `ViewComponent` hangs on that signal, and the read-back used to be silent: an element removed and appended again kept forwarding under the filter it was patched with, while signal and attribute had long since settled on something else. What the element actually forwarded was readable from neither. An unchanged filter is not written again — a list read from the attribute is a fresh `Set` each time, and re-writing it would count as a change by identity alone.
|
|
199
|
+
- **Bugfix (elements):** `<shae-ent>` threw a `TypeError` as soon as it was asked for its parent node while sitting outside any tree: the lookup fell back to reading `host` on a `parentNode` that is `null` in exactly that branch. `getParentNodeForObserver()` now goes through the root node and returns `undefined` when there is no parent left. Its return type narrowed to `Node | undefined`, which subclasses see.
|
|
200
|
+
- **Bugfix (elements):** `<shae-ent>` threw while detaching from its parent element when it had never been given a `ViewComponent`. Without one there is nothing to detach; the branch is skipped.
|
|
201
|
+
- **Bugfix (elements):** `<shae-worker>.importScript()` rejects with a `ShadowEnvDestroyedError` when the environment is torn down between `ready()` resolving and the import starting, instead of running into a `TypeError`.
|
|
202
|
+
- **Bugfix (elements):** `<shae-worker>` produced an unhandled promise rejection whenever it was connected and disconnected within the same task. `connectedCallback()` autostarts via `start()` and the `src` effect calls `importScript()`; both await `ShadowEnv.ready()`, which rejects with a `ShadowEnvDestroyedError` once the environment is destroyed, and neither promise was observed. Both call sites now absorb that rejection — a teardown is silent, anything else is logged. `start()` and `importScript()` still reject for callers that do wait for them.
|
|
203
|
+
- **Bugfix (elements):** a `<shae-worker>` that leaves the document and is back in it within the same task keeps its environment. The teardown is deferred by one microtask so that a re-render which removes and re-inserts the element does not cost the Shadow Environment; the flag that microtask reads was only ever cleared on the element's first `start()`, so an element that had already started was destroyed all the same — `ShadowEnv`, proxy and entities gone, and the `ShadowEnvDestroyedError` from the `start()` that followed swallowed as an unobserved rejection. `connectedCallback` clears it now, so the deferral covers the case it exists for. An element still out of the tree when the microtask runs is destroyed as before and stays destroyed: re-inserting it does not revive it — which now includes the element that is started by hand while out of the tree, typically one carrying `no-autostart`: its `start()` no longer holds the teardown off, and it lands in a destroyed environment. `ShaeWorkerElement.destroy()` counts once while it is at it: every call after the first returns without touching anything, whether it comes from the element or from application code. The rule is written down in `docs/api-reference.md` under `### <shae-worker>`.
|
|
204
|
+
- **Bugfix (entity lifecycle):** `Kernel.setParent()` defaulted an absent `order` to `0` and assigned it. A `SetParent` change only carries the order when it actually changed (the view side keeps the matching rule in `ComponentMemory.setParent()`), so re-parenting an entity silently reset its order on the kernel side while the view side kept the real value — the two then disagreed, and a `ContextLost` recovery re-created from the wrong state. An absent order now keeps the current one; an explicit one still wins.
|
|
205
|
+
- **Bugfix (environments):** `LocalShadowObjectEnv.destroy()` emptied the registry it was working with. Unless a registry is handed to the constructor, that is the default one — shared with every other environment in the thread and with every class registered through `@ShadowObject` or `shadowObjects.define()` — so tearing down a single environment stripped the definitions from all of them. `destroy()` now only clears a registry that belongs to that environment alone; passing `Registry.get()` explicitly still counts as the shared one.
|
|
206
|
+
- **Bugfix (kernel):** an Entity Context follows the entity tree in both directions. An entity that already holds a context binds it to the parent it is attached to, whether or not that parent has children already — a context created before the attachment used to stay on the root context when the entity arrived as the first child, so a Shadow Object below it read the value the root provided while its sibling-bearing counterpart read the parent's. On the way out the binding is re-established against the root: an entity detached through `entity.parent = undefined`, and a child promoted to root because its parent was destroyed without the auto-destruction flag, used to hold the last value of the parent they left and saw no later change of the root context at all. Reparenting through `Kernel.setParent()` was unaffected in either direction — it re-subscribes the contexts afterwards. An entity that moves straight from one parent to the next passes no root value on the way: it reads the parent it leaves until the one it joins takes over, which is what `useParentContext()` shows, since that reader hands out the inherited value without the microtask collector `useContext()` has in front of it.
|
|
207
|
+
- **Bugfix (kernel):** `entity.setProperty()` invalidates the cache of truthy property names. That cache answers `Kernel.#updateShadowObjects()`, which picks the Shadow Objects of an entity by the property routes of the registry (`token@prop`); a write through `setProperty()` used to leave it standing, so the next selection ran against a state that no longer existed. Both entry points now clear it — `setProperties()` does so by running each entry through `setProperty()`.
|
|
208
|
+
- **Bugfix (kernel):** an event addressed to an entity the Kernel does not hold is discarded instead of throwing. `dispatchEventsToEntity()` used to raise `entity with uuid "..." not found!`, and `run()` parses a change trail without a guard per entry, so the throw took the entries behind that event down with it: a `CreateEntities` following a stray `SendEvents` never happened.
|
|
209
|
+
- **Bugfix (kernel):** `Kernel.destroy()` reaches every entity. A destroy callback that throws costs its own entity — the failure is logged with the uuid and the sweep carries on — where it used to end the sweep and leave everything behind it standing. Afterwards the Kernel holds no entity at all, `hasEntity()` included, even where a callback broke off halfway. The root contexts are disposed after the entities, not before, so a Shadow Object reading a global context during its own teardown reaches the path the Kernel held rather than a fresh empty one.
|
|
210
|
+
- **Bugfix (kernel):** the four traversals over the children lists run to an end when a children list points back at an ancestor — `Kernel.traverseLevelOrderBFS()`, `Kernel.getEntityGraph()`, `entity.traverse()` and `ComponentContext.traverseLevelOrderBFS()`, each of which used to end in a `RangeError: Maximum call stack size exceeded`. Every node is visited once, `getEntityGraph()` included: an entity already in the graph is not written a second time. The debugging tool is the one that counts here — it is called on a tree that is already broken. Such a list is written by `Entity.addChild()` and `ComponentContext.addToChildren()`, which append a child without touching its parent link, so no check along the parent chain can cover it.
|
|
211
|
+
- **Bugfix (kernel):** moving an entity through `Kernel.setParent()` reaches the `useParentContext()` readers of that entity as one change. The move used to re-bind the contexts a third time after the attachment, which showed the value of the new parent twice — and, on the way up to a root, `undefined` twice. The end values were right either way; what changes is how often a Shadow Object is woken for them.
|
|
212
|
+
- **Bugfix (kernel):** a Shadow Object that leaves the constructor set of a living entity is torn down. A token change or a `'@propName'` route switch takes it off the entity, and everything the creation API handed it goes with it: the `onDestroy(fn)` callbacks run, and the signals, memos, effects, resources, context readers and providers, and the `on()`/`once()`/`onViewEvent()` subscriptions are disposed. The whole teardown used to hang on the destruction of the entity alone, so a Shadow Object that dropped out of the set left its cleanup unrun and its subscriptions live — silently, and for as long as the entity stayed. Cleanup registered against a Shadow Object that comes and goes with a property therefore runs on every departure, once per departure, and once only: a later destruction of the entity does not repeat it.
|
|
213
|
+
- **Bugfix (kernel):** an entity whose creation fails at a shadow-object constructor does not stay in the kernel. `createEntity()` registers the entity before it builds the shadow objects, because a constructor may address the kernel with the uuid of the entity being created — `createEntity(child, token, entity.uuid)` and `setParent(other, entity.uuid)` both look it up there; a throw out of one of them now takes that registration back, together with the root registration and the link to the parent. The shadow objects that already stand go through their regular teardown and hear their `onDestroy`, and a constructor that throws halfway through leaves neither effects nor links behind — the same holds where `changeToken()` and `upgradeEntities()` build shadow objects. The error itself still reaches the caller, so a change trail carrying such a creation is still answered with a rejection. What the rollback covers is that one entity: what its constructor did to other entities before the throw is not taken back, and the teardown of the failed entity walks its children list on top of that — whatever hangs under it is promoted to a root, a child the constructor created there as much as an entity it moved there with `setParent`, which loses the parent it came from; an entity carrying `autoDestructionOnParentRemoval` is destroyed instead, whichever way it got under the failed one.
|
|
214
|
+
- **Bugfix (kernel):** the context values written in one task reach their readers one at a time, each behind a guard of its own. A reader that throws during the hand-over costs only its own context value: the failure is reported through the Kernel's `ConsoleLogger` at error level with the context name and the uuid of the Entity, and the hand-over moves on to the rest. Reading a context inside an effect means that effect runs synchronously during the hand-over, itself running from a `queueMicrotask` callback with no caller behind it — an unguarded throw there is an uncaught exception in the global error handler, not a rejected promise. The values waiting to be handed over are collected per Kernel, so one Shadow Environment cannot delay or lose the context values of another running beside it in the same realm. Documented in `docs/api-reference.md`. No signature changes.
|
|
215
|
+
- **Bugfix (kernel):** an `on()` or `once()` subscription taken through the creation API releases its callback as soon as the caller ends it, rather than only at the teardown of the Shadow Object — a subscription on the Shadow Object's own Entity used to stay booked in the cleanup set for the rest of the object's life however often it was unsubscribed, while one on another target already released there. The creation API also closes with the teardown: past it, no member of it does anything — no subscription is taken, no signal, effect, memo or resource is created, no context is provided or read, no message reaches the view, and a cleanup registered there never runs. Each such call returns an inert but real value instead of throwing — a handle with no subscription behind it, or a destroyed signal reading `undefined` — and is reported once per member and Shadow Object through the `ConsoleLogger` at error level. `entity` is the one member this leaves untouched: it stays the same `EntityApi` instance it always was, and is the one path on which a continuation that kept a reference to the creation API stays attached to the live Kernel past this point. The `onDestroy` callbacks that run while the teardown is under way are not past it: the whole creation API stays open to them, and what they register there is released with everything else before the teardown returns. Documented in `docs/api-reference.md`. No signature changes.
|
|
216
|
+
- **Bugfix (kernel):** a destroyed Kernel holds no entity at all. `Kernel.destroy()` clears the entity map and the set of root entities and marks the cached traversal order stale, and it releases the two arrays behind that order as well — they used to go on holding every entity of the kernel, each of which holds the kernel itself through `entity.kernel`, until some later `traverseLevelOrderBFS()` happened to overwrite them. A caller keeping the `kernel` of a `LocalShadowObjectEnv` past that environment's `destroy()` was therefore left with an array of N torn-down entities that answered nothing. What the walk hands out is unaffected either side of the teardown: a fresh array each call, and an empty one from a destroyed kernel. Documented in `docs/api-reference.md`. No signature changes.
|
|
217
|
+
- **Bugfix (kernel):** a walk over the entity tree drops an entity the Kernel no longer holds instead of ending in `entity with uuid "..." not found!`. A children list keeps naming an entity the Kernel has let go whenever the detachment that would have cut it does not happen: `Entity.addChild()` writes such a list without the parent link `removeFromParent()` follows, and a detachment that throws inside `destroyEntity()` is logged rather than handed on — either way the same call takes the entity out of the Kernel and leaves its name in the list. `Kernel.traverseLevelOrderBFS()` used to look that name up with `getEntity()` and throw, and since `Kernel.destroy()` and `Kernel.upgradeEntities()` both walk from there, one such name took a whole teardown with it: the throw left `destroy()` for its caller, and every entity behind that name in the sweep never heard its `onDestroy`. The walk now follows the rule `getEntityGraph()` already followed over the same tree — the name drops out, and whatever hangs below it drops out with it. Documented in `docs/api-reference.md`. No signature changes.
|
|
218
|
+
- **Bugfix (logging):** `ConsoleLogger.loadConfig()` threw a `TypeError` when one of the four `ConsoleLogger.styles.*` keys was present in `localStorage`: the style values are read without a converter, and the missing converter was called anyway. Since `loadConfig()` runs from the constructor of the first logger, a single style entry set in the devtools took the whole library down at startup.
|
|
219
|
+
- **Bugfix (logging):** the `ConsoleLogger` probes `localStorage` for a usable Storage instead of asking whether the name is defined. Node defines an inert object under that name (without `--localstorage-file`), and every class that carries a logger — `Kernel`, `ShadowEnv`, `RemoteWorkerEnv`, `<shae-prop>`, `<shae-worker>` — builds it in a field initializer, so the very first `new Kernel()` in a node process threw `TypeError: localStorage.getItem is not a function` out of its constructor. That is the kernel side of the package, reachable through `@spearwolf/shadow-objects/shadow-objects.js`; the root export pulls in the custom elements and needs a DOM either way. A host that denies storage access, throwing a `SecurityError` on the property or on the first write, is turned away by the same probe. Wherever no usable Storage is found, the logger config lives in `globalThis.ConsoleLoggerStorage`, as it does in a worker.
|
|
220
|
+
- **Bugfix (logging):** `RemoteWorkerEnv` reads the logger configuration it forwards to the worker through the `ConsoleLogger`'s storage instead of reaching for `globalThis.localStorage` by hand. In a browser that denies storage access the direct read threw out of `start()` and took the worker with it; the shared path has probed the host for a usable Storage and falls back to `globalThis.ConsoleLoggerStorage` when there is none.
|
|
221
|
+
- **Bugfix (sync):** `ShadowEnv.syncWait()` never settled when the change trail was empty — `AfterSync` was emitted only inside `if (data.length > 0)`, and because the pending promise is cached, every later `syncWait()` returned the same dead promise. `AfterSync` is now emitted on every sync cycle with the (possibly empty) change trail, matching what the docs already promised. `#syncWaitForConfirmation` is reset in that path too, and a sync that finds the environment no longer ready re-arms itself instead of dropping a pending `syncWait()`.
|
|
222
|
+
- **Bugfix (sync):** `ShadowEnv.destroy()` left every pending `ready()` and `syncWait()` caller hanging forever. Both promises are built on `onceAsync()` listeners, and `destroy()` removes all listeners via `off(this)` without settling them. They are now rejected with a new `ShadowEnvDestroyedError`; calling `ready()` or `syncWait()` after destruction rejects immediately, `sync()` becomes a no-op, and a sync scheduled before the destroy no longer runs. Rejecting rather than resolving is deliberate: resolving would report a sync that never happened. `destroy()` is now idempotent and no longer destroys the `envProxy` twice (the explicit call plus the one inside the `envProxy` setter).
|
|
223
|
+
- **Bugfix (sync):** a synchronization cycle settles the `ShadowEnv.syncWait()` callers whose changes it actually carries. Which cycle a caller gets is decided at the change trail build: everyone who arrives before it rides the same trail and shares one Promise, and a call from the build on belongs to the cycle behind it — the one that carries the change the caller just made, rather than a trail that is already on its way to the Shadow Environment and cannot contain it. A listener of `ShadowEnv.AfterSync` or `ShadowEnv.SyncFailed` that calls `syncWait()` is on the same footing: the cycle it was told about is over, so it gets the next one instead of a Promise that settles right away. Over a worker, where a full round trip lies between the build and the confirmation, this is the widest window. A cycle whose environment is destroyed while its trail is in flight now ends in silence: `destroy()` has already rejected whoever waited on it with a `ShadowEnvDestroyedError`, so the refusal that follows the teardown is no longer written to the log as a failed change trail.
|
|
224
|
+
- **Bugfix (types):** the `HTMLElementEventMap` augmentation in `src/elements/events.ts` carries event names only. It used to declare `addEventListener` and `dispatchEvent` inside the interface, where they are not overloads but keys, which made two method names into event names. The file is not imported anywhere, not re-exported from `index.ts`, and not reachable through any `exports` path, so the reach was the type checking of this package itself.
|
|
59
225
|
- **Bugfix (types):** `ShadowObjectCreationAPI.createEffect` was typed as `(...args: Parameters<typeof createEffect>)`. `Parameters<>` resolves to the *last* overload of signalize's four, so the type demanded three arguments and rejected the documented `createEffect(callback)` call. Now declared as `typeof createEffect`, which keeps all four overloads. Runtime behavior is unchanged — this only ever affected type checking.
|
|
60
|
-
- **
|
|
61
|
-
- **
|
|
62
|
-
- **
|
|
63
|
-
- **
|
|
64
|
-
- **
|
|
65
|
-
- **
|
|
66
|
-
- **
|
|
67
|
-
- **
|
|
68
|
-
- **
|
|
69
|
-
- **
|
|
70
|
-
- **
|
|
226
|
+
- **Bugfix (view):** the start of a proxy that is no longer the current one has no say over the environment. `ShadowEnv.envProxy` starts every proxy it is handed without waiting, and the result used to land wherever it arrived: a proxy replaced while it was still starting reported the *new* environment ready although nothing had started it — and in the other direction, the one that actually bites, its rejection arrived after the successor had come up and left `proxyReady` at `false`, with an environment that never became ready again, a `ready()` that never resolved and a `failed to start envProxy` in the log about a proxy nobody holds any more. Each assignment now opens a generation, and a start that finishes outside its own is discarded, error message included. Swapping a `RemoteWorkerEnv` that is still in its load handshake for another environment is the case this covers. The environment also stops listening to a proxy it has let go, and the two callbacks come off at their own moment: `onProxyFailed` as soon as its `destroy()` returns, `onMessageToView` behind the messages that teardown queued. What an `onDestroy` sends towards the view therefore still arrives — a `LocalShadowObjectEnv` delivers it — while everything the released proxy says after that finds nobody listening, a failure it reports included. A failure of the current proxy ends its turn as well: a start of its own that resolves afterwards no longer reports the lost environment as ready.
|
|
227
|
+
- **Bugfix (view):** an environment releases a namespace registration only while it holds it. A namespace carries one environment at a time, and assigning a `view` displaces whatever was registered under `view.ns` — the environment says so through the `ConsoleLogger`. The displaced environment used to take the entry along when it let go of its `view`, so `ShadowEnv.get(ns)` answered `undefined` for an environment that was very much alive, and every `<shae-ent>` under that namespace stopped syncing: `ShaeElement` reaches its environment through exactly that lookup. The release is ownership-checked now, in the `view` setter as it has always been in `destroy()`.
|
|
228
|
+
- **Bugfix (view):** a frame loop with no subscribers asks for no frames. The last target to leave used to take the loop off only when it left between two frames; leaving from inside the frame handler — which every `syncShadowObjects()` that takes a `<shae-worker>` out of the document can cause — hit the frame that had already fired and the loop re-armed itself right after, running on for the lifetime of the page with nobody listening. A target that subscribes while the last one leaves inside the same frame keeps the loop running with exactly one pending frame, where two used to be armed and only the second of them could be cancelled again.
|
|
229
|
+
- **Bugfix (view):** a `RemoteWorkerEnv` starts even when the console-logger config stored under `ConsoleLogger.RemoteWorkerEnv.workerConfig` cannot be used. A value that does not parse to a plain JSON object — hand-edited, or overwritten by something else on the origin — counts as no config at all: the worker is configured from the shared logger config, and the storage key is named once through `logger.error`, at error level, the one level this library leaves ungated. Arrays and scalars are ignored the same way a broken value is.
|
|
230
|
+
- **Bugfix (view):** `RemoteWorkerEnv.destroy()` takes effect on an environment that never spawned a worker. The early return for "nothing to tear down" stood ahead of the flag, so `isDestroyed` stayed `false` and a following `start()` spawned a worker into an environment that was supposed to be gone. The teardown now marks the environment first and looks for a worker afterwards, and it stays idempotent: a repeated call sends no second `Destroy` and terminates nothing twice.
|
|
231
|
+
- **Bugfix (view):** a `destroy()` settles what is still waiting on the worker. `start()` caught in the middle of its load handshake, a `workerLoaded` that has not resolved, and an `applyChangeTrail()` or `importScript()` already on the wire are all rejected with a `WorkerDestroyedError` at that moment, instead of sitting out `WorkerLoadTimeout` — or, for `workerLoaded`, never settling at all. A `workerLoaded` read after the teardown rejects the same way rather than handing out an environment that no longer exists. Note the reach on the change-trail side: `destroy()` waits for the worker's `Destroyed` reply or `WorkerDestroyTimeout`, so a worker can still apply a change trail in that window and confirm it — an `applyChangeTrail(trail, true)` that was resolved by such a late confirmation is now rejected instead. Send what has to arrive before tearing the environment down. The `Destroyed` handshake `destroy()` itself waits for keeps its own `WorkerDestroyTimeout`.
|
|
232
|
+
- **Bugfix (view):** a change trail that `buildChangeTrails()` handed out stays as it was handed out. The Component Memory took over the property tuples of a create-entity entry by reference and wrote through them on the next sync tick, so a trail held via `syncWait()` or `ShadowEnv.AfterSync` could show different property values than it did at the moment it arrived. `applyPropsChanges` now copies each tuple it adopts from `changes` instead of sharing it.
|
|
233
|
+
- **Bugfix (view components):** a `ViewComponent` carries its properties into the `ComponentContext` it joins. A namespace change already took the token, the order and `autoDestructionOnParentRemoval` along; the properties stayed behind, and the entity arrived in the other environment as a bare token. They now travel together with the equality function registered for each key, and land in the same `CreateEntities` change as the rest. The function does not decide about the arrival — the target holds nothing for the key yet, so it would be asked whether the value equals `undefined`, and a function comparing by a field would answer yes and drop the property. The value is written first, the rule applies from the next write on. The parent link is the one thing that never travels with the component: leaving a context detaches it, so it joins the new one as a root. Where nothing re-attaches it — a `context` assignment driven by hand, with no element involved — the change trail says so too, and carries no `parentUuid`. On a `<shae-ent>` whose `ns` changes the hierarchy is intact afterwards all the same, by a different route: the element asks for an ancestor again in the new namespace and re-binds the same `ViewComponent` from the element tree before the next trail is built, so that trail does carry the `parentUuid`. An entity whose ancestor stayed behind in the old namespace arrives as a root. A component that leaves its context without joining another carries nothing — there is no receiver for it — and events already dispatched are still delivered in the environment they were addressed to.
|
|
234
|
+
- **Bugfix (view components):** assigning a `ViewComponent.context` that had been disposed left the component in a state that reported itself as alive. The setter destroyed the component in its old context and *then* let `addComponent()` throw, so `isDestroyed` stayed `false` while every `setProperty` / `removeProperty` / `dispatchShadowObjectsEvent` silently went nowhere. A disposed context is now rejected before the teardown, so the component keeps its current context; any other failure to join leaves the component detached rather than pointing at a context that never took it in.
|
|
235
|
+
- **Bugfix (view components):** `ComponentContext.changeOrder()` guarded against `dispose()` but not against `clear()`. Changing the `order` of a component after `clear()` pushed its uuid back into `#rootComponents` without a view instance, and the next `clear()` threw `component-context panic: #rootComponents is not empty!`. The guard now asks whether the context still holds the component.
|
|
236
|
+
- **Bugfix (view components):** `ComponentContext.removeFromParent()` dereferenced the child entry outside the guard that checks it exists. `destroyComponent()` is public and does not detach the component, so `destroyComponent(c); buildChangeTrails(); c.destroy();` threw a `TypeError`. A child the context no longer holds is now ignored, matching the sibling paths.
|
|
237
|
+
- **Bugfix (view components):** `ComponentContext.getChildren()` dereferenced every uuid in a children list without a guard. `addToChildren()` writes a uuid into a children list without the parent link of the child following along, so a later delete has no list to take that uuid out of and the entry behind it goes while the list keeps naming it — a `TypeError` on the next `getChildren()`, and with it on `dispatchEvent(…, true)` and the three `dispatchReRequest…` paths. A uuid without a matching entry is now skipped, the same way the ordered insertion and the breadth-first walk already read a children list. Documented in `docs/api-reference.md`.
|
|
238
|
+
- **Bugfix (view components):** `ComponentContext` silently lost components whose `order` fell into the first gap of an existing sibling list. The hand-rolled insertion in `#appendToOrdered` had no fallback when its backwards scan ran off the front, so for three or more siblings and `children[0].order <= order < children[1].order` the component was removed from `#rootComponents` but never added to the parent's children. It stayed in `#components`, was unreachable via BFS, never produced a `CreateEntities` change, and made the next `clear()` throw `component-context panic`. Reachable through `new ViewComponent(t, {parent, order})`, `new ViewComponent(t, {order})` and the `order` setter. Replaced with a linear insertion that skips uuids without a view instance instead of dereferencing them.
|
|
239
|
+
- **Bugfix (view components):** `addChild()` accepted cycles. `a.addChild(b); b.addChild(a)` emptied `#rootComponents`, made the whole branch invisible to every change trail, and `a.addChild(a)` sent `removeSubTree()` into unbounded recursion. `addChild()` (and therefore the `parent` setter) now rejects the component itself and any of its ancestors with a `ViewComponentError`; the tree is left untouched when the check fires. `removeSubTree()` additionally tracks visited uuids so a pre-existing cycle cannot overflow the stack.
|
|
240
|
+
- **Bugfix (view components):** `#deleteComponent()` removed a component from `#components` and `#rootComponents` but left its uuid in the parent's children list. `removeSubTree()` on a non-root therefore corrupted that list, and every later `getChildren()` or ordered insertion on it threw a `TypeError`. The uuid is now detached from the parent as well.
|
|
241
|
+
- **Bugfix (view components):** registering a new `ViewComponent` under a uuid that was already in use reset the children list without telling the children. They kept pointing at the previous instance, were no longer root components, and dropped out of the tree. The previous instance's children are now promoted to root components.
|
|
242
|
+
- **Bugfix (view components):** a uuid names one `ViewComponent` at a time. `ComponentContext.addComponent()` refuses a uuid that another component of the same context is holding and throws a `ComponentUuidInUseError` — exported from the package, carrying the uuid on `error.uuid`. Two components used to share one entry and one pair of create/destroy counts: the second `create()` put the create count ahead of the destroy count, so the entry survived the departure of both, and `buildChangeTrails()` wrote a `CreateEntities` for an entity no later trail ever took down; on top of that the uuid stood in two children lists at once. Reachable only from a hand-built component with an explicit `uuid` — the elements let `generateUUID()` name theirs. What stays: the uuid is free again as soon as its holder has left the context, so taking a component's uuid over after its `destroy()` still recreates the entity under that uuid, and the entry it left behind still carries the properties for it. A join that is refused leaves the component with no `ComponentContext`, the same price every other refused join asks, the disposed one aside. `ComponentContext.removeSubTree()` drops the entry before it tears the component down rather than after, so a `ViewComponent.Destroyed` listener that assigns the `ComponentContext` back onto the component it is announcing takes it in again under its own uuid: such a component used to end up naming a `ComponentContext` that held nothing behind its uuid, writing into the entry of whoever claimed that uuid next. No signature changes.
|
|
243
|
+
- **Bugfix (view components):** `new ViewComponent(undefined)` left `token` as `undefined` while the change trail correctly reported `#void`, so view state and wire format disagreed. `ComponentChanges.changeToken(undefined)` marked the component dirty but emitted nothing, silently keeping the old token. Both now normalize to `VoidToken`, as the `token` setter always did.
|
|
244
|
+
- **Bugfix (view components):** `setProperty(key, undefined)` and `removeProperty(key)` produced the same `[key, undefined]` entry on the wire but diverged internally — `setProperty` kept the key in the committed property map. A `removeProperty()` after a `setProperty(undefined)` therefore emitted the same change a second time. An explicit `undefined` is now treated as a removal in the committed state as well.
|
|
245
|
+
- **Bugfix (worker):** a change trail is confirmed exactly once. When it failed, a second confirmation without an error followed the error message under the same serial, and which of the two the waiting caller saw first decided whether `syncWait()` rejected or resolved. A trail that carries no serial is not confirmed at all any more, not even when it fails — nobody is waiting on it, and the view side would throw the unsolicited error message against whichever request happens to be running.
|
|
246
|
+
- **Bugfix (worker):** a `Destroy` tears the kernel in the worker down. The entities are destroyed and the `onDestroy` callbacks of their Shadow Objects run before the worker is terminated, so timers, subscriptions, `OffscreenCanvas` and WebGL resources are released instead of staying held until the `terminate()`. The local path already did this; one limit and one difference remain. A callback that throws costs its own entity, not the ones behind it in the order: the sweep carries on and the failure is logged with the uuid, and the confirmation goes out either way, so the view does not sit out its destroy timeout. And a message an `onDestroy` sends towards the view does not arrive in a worker environment, where a `LocalShadowObjectEnv` delivers it: the router unsubscribes from the kernel before taking it down, so that the confirmation is not overtaken by a message from a kernel on its way out. Both are written down in `docs/api-reference.md`.
|
|
247
|
+
- **Bugfix (worker):** a message the worker cannot read — `null`, `undefined`, a number, a string — is discarded with a debug message. The access to `data.type` used to take the worker down with a `TypeError`, which the view side escalated into a `WorkerFailedError` that ended the whole environment. Both entry points, `WorkerRuntime.onmessage` and `MessageRouter.route()`, check for themselves. Behind the teardown the worker discards every further message, a second `Destroy` included: one `Destroy` is answered with exactly one `Destroyed`, the `message` listener comes off the global scope, and neither a later message nor a `start()` brings the runtime back.
|
|
248
|
+
- **Bugfix (worker):** `MessageRouter` answers a failed change trail and a failed module import even when the thrown value cannot be turned into a description — an object whose `toString()` throws, one with no prototype at all. The wording falls back to a fixed `an error that cannot be described`, without a name, instead of leaving the throw to escape past the reply. The view gets a rejection instead of sitting out its `configureTimeout` or `changeTrailTimeout`.
|
|
249
|
+
- **Bugfix (worker environments):** a change trail handed to `RemoteWorkerEnv.applyChangeTrail()` stays complete. The transferables are split off for the message by shallow-copying the entries that carry them; the trail the caller passes on — the one `ShadowEnv.AfterSync` and `syncWait()` hand to the consumers — used to come back with them deleted off its entries. Both environments now hand out the same snapshot.
|
|
250
|
+
- **Bugfix (worker environments):** a confirmation is matched to its request before its error counts. A failed change trail used to reject whichever request happened to be waiting; it now rejects only the one carrying its own serial, and a failed `importScript()` only the import of its own url. Requests running side by side no longer disturb each other.
|
|
251
|
+
- **Bugfix (worker environments):** `RemoteWorkerEnv` takes its `message`, `error` and `messageerror` listeners back off the worker — on teardown, on failure and on a `start()` that fails. They were registered through `bind()`, which left no reference to unsubscribe with, so between `destroy()` and the `terminate()` that follows it the worker held the environment through all three. One listener still stands in that window — the wait for the `Destroyed` acknowledgement registers its own, and it comes off with the acknowledgement or with `WorkerDestroyTimeout`. A message that leaves the worker after the `destroy()` no longer arrives at `onMessageToView`.
|
|
252
|
+
- **Bugfix (worker environments):** a missing `Destroyed` acknowledgement is reported through the logger instead of ending five seconds after the teardown in an unhandled rejection nobody owns. The worker is terminated after `WorkerDestroyTimeout` as before.
|
|
253
|
+
- **Bugfix (worker environments):** `RemoteWorkerEnv.WorkerLoaded` stays announceable even when a listener throws. The throw used to abort the dispatch from inside a microtask — surfacing as an unhandled error with no caller to catch it — and to do so before eventize stored the retained value, so every later read of `workerLoaded` waited for a failure or a teardown instead of resolving. The event is put back and a later subscriber gets it; a `workerLoaded` promise that was already waiting behind the throwing listener stays pending, and a fresh read of `workerLoaded` resolves.
|
|
254
|
+
- **Bugfix (worker environments):** `RemoteWorkerEnv.WorkerFailed` stays announceable even when a listener throws. The throw used to abort the dispatch before eventize stored the retained value, so a consumer subscribing later never learned of the failure although the event is documented as retained. The throw no longer leaves the failure path, and a later subscriber gets the event; the listeners that stood behind the throwing one in the first run do not see it, and that is reported.
|
|
255
|
+
- **Bugfix (worker environments):** `RemoteWorkerEnv.applyChangeTrail()` and `importScript()` reached for a worker that a regular `destroy()` had already released, and threw a `TypeError` out of a method that promises a `Promise`. Both reject with a new `WorkerDestroyedError` now, and a `start()` that is torn down while it waits does the same instead of throwing a bare string.
|
|
256
|
+
- **Bugfix (worker environments):** `RemoteWorkerEnv.start()` on a destroyed environment spawned a fresh worker thread. It only noticed the teardown after the load handshake — up to `WorkerLoadTimeout` later — and then skipped the `terminate()`, leaving a thread running that no reference reached any more. A destroyed environment now turns `start()` away with a `WorkerDestroyedError` before it creates anything.
|
|
257
|
+
- **Bugfix (worker environments):** `RemoteWorkerEnv` only listened for `message`. A worker that died on an unhandled error, or sent something the structured clone algorithm could not read back, went unnoticed — `applyChangeTrail()` sat out the full 5s, `importScript()` and `start()` the full 60s, and no consumer had any way of learning that the environment was gone. `error` and `messageerror` are now subscribed before the load handshake begins; a failure terminates the worker, sets `isDestroyed`, and rejects everything pending and everything later with a new `WorkerFailedError`.
|
|
258
|
+
- **Bugfix (kernel):** `Entity.hasContext()` reads `false` for every context name once the Entity is destroyed. Destruction cleared every context signal but left the map itself standing, so a name the Entity used to carry kept answering `true` after there was nothing left to ask.
|
|
259
|
+
- **Bugfix (view):** `RemoteWorkerEnv` discards a message from the worker whose payload it cannot read, instead of taking a `type` off it. A `null` or `undefined` payload — from a foreign sender on the same channel, or from an older version of the protocol — threw inside the listener of every request still waiting, where it became an uncaught error and left the request to run into its timeout. Such a message is now ignored and the wait stays open for the one it is waiting for, the same answer the worker-side runtime and router give.
|
|
260
|
+
- **Bugfix (kernel):** `kernel.destroyEntity()` no longer loses the rest of a teardown to one failing step below the level `changeToken()` and the shadow-object notifications already covered. Detaching the entity from its parent — `removeFromParent()` — runs behind its own guard, so a throw there still lets every Shadow Object hear its `onDestroy`, every creation scope tear down, and the entity release what it holds. The entity's own release is guarded per step as well: clearing its properties, dropping its listeners, releasing an auto-destruction subscription, tearing down a global context, and releasing a context entry's signals each stand on their own, so a single failing signal or effect cleanup costs only that step — the properties, contexts, parent link and children list behind it still clear. Both kinds of failure are reported through the `ConsoleLogger` instead of reaching the caller of `destroyEntity()`. No signature changes.
|
|
261
|
+
- **Bugfix (kernel):** the lowest priority on an Entity's `onDestroy` notification belongs to whoever registers there. The Entity used to sit on its own notification at `Priority.Min` and to drop every listener on itself as the second step of its release, and eventize breaks a tie in priority by order of registration — the Entity registers in its constructor, so a listener registered at that same priority was unsubscribed before the delivery reached it. The Entity is no longer a listener on its own notification: the Kernel releases it directly, behind the delivery, so a listener at `Priority.Min` runs while the Entity still holds its properties and its contexts. A subscription taken through the creation API is unchanged and still bound to its creation scope, which tears down at `Priority.Low`.
|
|
262
|
+
- **Bugfix (kernel):** `Kernel.setParent()` sends `onParentChanged` before it returns instead of queuing it for the next microtask. An Entity destroyed later in the same task used to drop its listeners before the queued notification ran, so it never heard about its last move; the delivery is now the last thing `setParent()` does, and an Entity destroyed afterwards has already heard. Documented in `docs/api-reference.md`, along with what moves with it: inside a change trail the handler now runs before the next entry of that trail is applied, where it used to run after the whole trail, and inside the `batch()` that trail runs in, where it did not run before — a signal write the handler makes there now settles its dependent effects only once the trail's batch releases, where the same write outside a change trail still settles them right away. A handler reading `useParentContext()` inline is unaffected, in and out of a change trail alike: that reader is a direct link to the parent's own context signal, rebound synchronously before the notification goes out, so it already named the new parent before this change and still does. `useContext()` moves, but only outside a change trail: it settles through a separate one-microtask batch collector that used to run ahead of the (also microtask-queued) notification there and now runs behind it, so a handler reading it inline sees the value the Entity is leaving rather than the one it moved to. Inside a change trail that collector never ran ahead of the notification to begin with — the effect driving it only fires once the trail's `batch()` releases — so a handler reading `useContext()` inline there already saw the value the Entity was leaving before this change, and still does. An effect on either reader is unaffected either way. A handler that throws is reported through `kernel.logger` and reaches neither `setParent()` nor a change trail around it, the same guard `kernel.destroyEntity()` already puts around its own entity-wide notification — and covers the same ground: an error a Kernel call the handler makes throws back into it as much as a throw from the handler itself.
|
|
263
|
+
- **Bugfix (kernel):** the creation scope of a Shadow Object lets go, on `tearDown()`, of the shadow-object it made the API for, of both kernel handles it was bound with (`releaseScope` and `forgetShadowObject`), and of the subscription it held on its entity's `onDestroy`; none of the four used to be released. Untouched by this fix is the scope's own `entity` field, set once at construction and read by `tearDown()` itself for the info line it logs — the entity, and through it the Kernel, stay reachable through that field for as long as anything still holds the scope. That is not a corner case: the creation API hands the entity straight to the Shadow Object's own constructor as `entity`, so a Shadow Object that keeps that reference around already holds its entity directly, scope or no scope, and holding on to the scope itself — a bound creation-API method (`provideContext`, `onDestroy`, …) kept past construction, for instance — reaches the same entity the same way. This fix closes the first four, not the fifth.
|
|
264
|
+
- **Bugfix (elements):** a `<shae-ent>` whose join into its `ComponentContext` is refused no longer lets the `ComponentUuidInUseError` or `ComponentContextDisposedError` escape. `#applyComponentContext()` runs both call sites — `connectedCallback()` and the `componentContext$.onChange` listener — and reports a refusal through the element's own `ConsoleLogger` instead: a custom element reaction has no caller of its own to catch the throw, and a signal effect's throw is collected and re-thrown out of whichever `set()` started the effect chain, `el.ns = …` included. What the `ViewComponent` is left with afterwards depends on which error it was: a uuid already in use costs it whatever context it had, so `isDestroyed` reads `true`; a disposed target is rejected before anything is undone, so the component keeps exactly the context it already had, a live one included, and `isDestroyed` with it. `componentContext` itself always ends up naming the context the join was refused from, and the entity rejoins on the next change of that signal, which a namespace switch or a leave-and-rejoin of the tree both produce; writing the same namespace again is not such a change and leaves the element as it was. Documented in `docs/api-reference.md` under `#### A context the entity cannot join`.
|
|
265
|
+
- **Bugfix (kernel):** `#attachShadowObject()` reports a Shadow Object whose `onCreate`, `onDestroy`, `onParentChanged` or `onViewEvent` is written as a plain method instead of under the matching symbol. All four are symbols; a same-named string key was picked up by nothing and ran silently never, with no way to tell from the outside. The Kernel now checks, at attach time, whether the instance carries a function under one of the four names without carrying one under its symbol, and logs the mismatch through `this.logger.error()` — naming the hook, the symbol to use instead and the Shadow Object's display name — rather than through `logger.warn()`, which stays silent outside `localhost`.
|
|
266
|
+
- **Bugfix (worker):** `MessageRouter` and `WorkerRuntime` report through a `ConsoleLogger` of their own instead of writing to `console` directly, and the worker entry point no longer greets the console unconditionally on load. The two loggers' own per-namespace switch travels as a `MessageRouter.enable` or `WorkerRuntime.enable` key inside the JSON object under `ConsoleLogger.RemoteWorkerEnv.workerConfig` — a worker has no storage of its own to hold the `ConsoleLogger.<namespace>.enable` key the other loggers of this library read theirs from. The seven debug lines report at debug level and the unknown-message warning at warn level, so each hangs on the getter its own method checks; the three error reports stay ungated, the way `RemoteWorkerEnv` keeps its own. Neither logger is built before the worker has processed the `ConsoleLogger` configuration message `RemoteWorkerEnv` sends first: building one earlier would read the shared config on construction and pin its defaults for the whole thread. Documented in `docs/api-reference.md` under Console Logger.
|
|
267
|
+
- **Bugfix (logging):** `setConsoleLoggerStorage()` reaches the loggers of a thread that already has one. Where no usable Storage was found, `ConsoleLogger.sharedConfig` becomes `globalThis.ConsoleLoggerStorage` itself once the first logger of the thread is built, and a later call now writes its values into that same object instead of putting a fresh one in the slot — a `WorkerRuntime` logger built by one of the two guard branches of `onmessage` ahead of the `ConsoleLogger` configuration message now hears every later change to the four shared switches, where it used to keep the defaults for the rest of the thread's life. A logger's own `<namespace>.enable` is still read once, when it is built, so that one flag stays behind for a logger built ahead of its config.
|
|
268
|
+
- **Bugfix (kernel):** the three console lines of the branch both environments share report through a `ConsoleLogger` instead of writing to `console` directly. The deprecated call form of `useProperty()`, `provideContext()`, `provideGlobalContext()`, `useContext()` and `useParentContext()` — a bare compare function where the options object belongs — and a `{compare}` passed to an already cached reader go out through `kernel.logger.error()`: both name a mistake in the calling code, and `error` is the one level this library leaves ungated, so the report arrives wherever the application runs rather than on `localhost` alone. The skipped second import of a module two `extends` chains have in common goes out through `kernel.logger.warn()` behind `isWarn`, because it describes the shape of a module graph and not a mistake. The affected entries in `docs/api-reference.md` name the level and point at the table of call against getter under Console Logger.
|
|
269
|
+
- **Bugfix (kernel):** the deprecation report of the bare-compare call form falls once per Kernel and member name. The list of names already reported belongs to the Kernel, which hands it to every creation scope it builds; it used to live as long as the module, so an application running two Shadow Environments heard about the deprecated form from whichever of them got there first and from no other.
|
|
270
|
+
- **Bugfix (kernel):** the teardown of a Shadow Object reaches every cleanup registered while it is under way. An `onDestroy` callback that a `createEffect` or `createResource` cleanup registers runs, and a subscription or a signal that callback takes in turn is released before the teardown returns. `docs/api-reference.md` says so under "The creation API past the teardown".
|
|
271
|
+
- **Bugfix (kernel):** `Entity.removeChild()` checked membership by uuid but removed by identity, so an entity that is not among the children — one carrying a uuid the list does hold, under a different instance — was found by `#childrenUuids.has()` and then cut out of `#children` by `indexOf(child)`, which answers `-1` and turns `splice(-1, 1)` into a removal of the last child instead of none. The identity now decides both branches: `#children.indexOf(child)` is read once, and only an index other than `-1` removes anything, from both `#children` and `#childrenUuids`. Reachable through the `Entity` instance `kernel.getEntity(uuid)` hands out; `Entity` carries no export of its own.
|
|
272
|
+
- **Bugfix (kernel):** `Kernel.destroy()` takes its own subscriptions off, one microtask after the teardown. Every other teardown in the package — `ViewComponent.destroy()`, `ShadowEnv.destroy()`, `Entity`'s own release, `SignalsPath.dispose()` — releases what it holds through `off(this)`; the Kernel is eventized in its constructor and sends `MessageToView`, but its `destroy()` left that subscription set standing. A caller with its own `on(kernel, MessageToView, …)` therefore stayed subscribed, and the destroyed Kernel went on holding that listener closure, and everything it captured, through it — eventize keeps a subscription at the emitter, not at the caller. The `off(this)` sits behind a `queueMicrotask()` rather than at the end of the method: `dispatchMessageToView()` already queues the delivery of every message a microtask out, so a message an `onDestroy` hands to the view through the creation API during the teardown reaches its listeners before the unsubscribe runs, and only what is dispatched after that finds nobody. The Kernel instance is not sealed forever by this — an `on()` registered once that microtask has run stands — but one registered in the window between `destroy()` returning and that microtask is taken off silently along with everything else, because the collective form the teardown uses removes a subscription regardless of when it was added. Documented in `docs/api-reference.md`.
|
|
273
|
+
- **Bugfix (view):** `ComponentContext.removeFromParent()` and `ComponentContext.moveToRoot()` could put a component under the roots and in a children list at the same time. `removeFromParent()` used to append to `#rootComponents` regardless of what the entry-removal it ran alongside answered, so a call with a `parent` that did not actually hold the component left the component appended to the roots while it stayed in the children list of its real parent — `isRootComponent()` and `isChildOf()` then both answered `true` for it. `moveToRoot()` had the same shape with no condition at all: it never took the component out of any children list. The promotion to the roots now follows the detachment in both methods.
|
|
274
|
+
- **Bugfix (elements):** a `<shae-prop>` that was destroyed by hand while it is still in the document stays released. A re-request of the host arriving before the `destroy()` books a lookup for the next microtask, and that lookup used to run on the destroyed element: it wrote `entNode` and registered the re-request listener again on the host or on the document, so an element that had just let go of everything was listening once more. The lookup now asks whether the element has been torn down, alongside the question whether it is still in the tree. `cheat-sheet.md` already states the rule this restores — a `destroy()` on a connected element leaves it released until it has left and come back.
|
|
275
|
+
- **Bugfix (worker):** a change trail whose serial is `0` is confirmed like any other. The worker answers wherever a serial travelled with the trail, on the applied route and on the refusal route alike, so a proxy implementation that counts from zero gets its answer instead of sitting out `changeTrailTimeout` and reporting a `WorkerTimeoutError` for a trail the Kernel applied cleanly. `RemoteWorkerEnv` counts up from 1 and is unaffected.
|
|
276
|
+
|
|
277
|
+
### Types
|
|
278
|
+
|
|
279
|
+
- **Types (public API):** `ShadowObjectCreationAPI.createSignal` is declared `typeof createSignal` instead of `(...args: Parameters<typeof createSignal<T>>) => ReturnType<typeof createSignal<T>>` — the same fix, for the same reason, as the `createEffect` entry below. `Parameters<>` and `ReturnType<>` collapse an overload set onto its last member, and signalize 1.0 has three: the `{lazy: true}` factory form became a type error, and `createSignal<T>()` without an initial value claimed `Signal<T>` where the runtime hands out a signal holding `undefined`. Both forms now type-check as what they are.
|
|
280
|
+
- **Types (public API):** `provideContext()` and `provideGlobalContext()` accept a `SignalReader<T>` as their source, next to the `SignalReader<T | undefined>` they already took. Handing an existing signal to a context is the documented way to keep it in sync, and it is now typeable.
|
|
281
|
+
- **Types:** `ConsoleLoggerConfig` and `ConsoleLoggerControl`, exported from `ConsoleLogger.ts`, type the shapes behind the two storage-facing globals `globalThis.ConsoleLoggerStorage` and `globalThis.ConsoleLogger`. Neither global is declared ambiently — importing this package does not add `ConsoleLogger` to a consumer's global type space — so a caller reaching for either one still casts `globalThis` locally, the same way `ConsoleLogger.ts` does internally.
|
|
282
|
+
- **Types:** the emitted declarations are as narrow as the values behind them. They carry `| undefined` wherever a value can be missing — visible on `ShaeEntElement.componentContext$` / `viewComponent$` / `token$`, `ShaePropElement.entNode$` / `viewComponent$` / `name$` / `type$`, `ShadowEnv.ns$` and the return of `FrameLoop.start()`. Consumers compiling with `strictNullChecks` will see new errors where they relied on a value that was never promised — the promise is the fix. Two declarations narrow the same way without a consumer reaching them: the return of `filterUndefinedProps()` carries `| undefined`, and `generateUUID()` returns the template literal type of `crypto.randomUUID()` instead of a plain `string`. Neither `src/utils/props-utils.js` nor `src/utils/generateUUID.js` is re-exported from `index.ts` or reachable through the `exports` map, and `ViewComponent.uuid` — the one place a uuid does reach a consumer — is declared `string`.
|
|
283
|
+
- **Types (public API):** `ShaeEntElement.entParentNode` is declared `ShaeEntElement | undefined`. A consumer compiling with `exactOptionalPropertyTypes` may now assign `undefined` to it directly — that is how an entity gives up its parent — where every other configuration already treated the two forms as the same type. No break.
|
|
284
|
+
- **Bugfix (types):** `constants.d.ts` and `elements/ShaeElement.d.ts` resolve under `moduleResolution: NodeNext` and `Node16`. Both carried an `import type` specifier ending in `.ts`, which survives declaration emission verbatim and pointed consumers at a source file this package never ships.
|
|
285
|
+
- **Types (public API):** `MessageToViewEvent.transferables` is declared `Transferable[] | undefined` and `EntityApi.parent` is declared `EntityApi | undefined`. A consumer compiling with `exactOptionalPropertyTypes` may now assign `undefined` to either field directly, where every other configuration already treated the two forms as the same type. No break.
|
|
286
|
+
- **Types (public API):** `MessageToViewEvent.traverseChildren`, `ComponentState.parentUuid` and `.properties`, `IShadowObjectEnvProxy.onMessageToView` and `.onProxyFailed` are declared with `| undefined`, and the `ViewComponent` constructor's options object accepts `undefined` for each of `parent`, `order`, `context`, `uuid` and `autoDestructionOnParentRemoval`. A consumer compiling with `exactOptionalPropertyTypes` may now assign `undefined` to any of these fields directly — for the constructor, an options object built from values that may be missing is now typeable — where every other configuration already treated the two forms as the same type. No break.
|
|
287
|
+
- **Types (public API):** the `ViewComponent` constructor declares both call forms it accepts — `new ViewComponent(token?, parent?)` and `new ViewComponent(token?, options?)` — and `token` is optional in both, as the `token` setter has always taken `string | undefined`. A component passed in place of the options object is read as the parent instead of being resolved structurally against the options type, where the compiler used to read `uuid`, `order` and `context` off it and report a meaning the constructor never had. A call without a token needs no cast: the token lands on `VoidToken` (`'#void'`). No runtime behaviour changes.
|
|
288
|
+
- **Types (public API):** `WorkerTimeoutError.messageType` is declared `WorkerReplyType`, the union of the four constants a `RemoteWorkerEnv` waits a reply for, exported from the package. Reading the field type-checks the same way; a `switch` over it can now be exhaustive; a `WorkerTimeoutError` built with a string that is not one of the four no longer type-checks.
|
|
289
|
+
- **Types (public API):** `ShaeWorkerElement.autoSync`'s setter is declared `string | boolean | number`. Behaviour is unchanged — a string is the value, anything else is read as a flag — but an assignment of any other shape no longer type-checks.
|
|
290
|
+
- **Types (public API):** `ComponentState.order` is declared `number`. Every write path already set it; the `undefined` the type promised never arrived, and a reader loses a branch that was already dead.
|
|
291
|
+
- **Types:** four internal declarations name the type the project already has for them: `useProperty()` on `ShadowObjectCreationScope` is declared `<T = unknown>`, matching `ShadowObjectCreationAPI`; the transfer line in `cloneChangeTrail()` passes `TransferablesType` on without a cast; `MessageRouter` names the destroy payload like its two neighbors; and `waitForMessageOfType()` takes one of the four reply constants, is generic over its guard's payload, and declares its timer handle as the return type of `setTimeout`. None of these are re-exported from `index.ts` or reachable through the `exports` map.
|
|
292
|
+
|
|
293
|
+
### Dependencies
|
|
294
|
+
|
|
295
|
+
- **Dependencies (breaking):** the declared runtime ranges in the published `package.json` move to `@spearwolf/eventize@^6.2.0` and `@spearwolf/signalize@^1.0.0`, from the `^5.0.0` / `^0.30.0` that `0.33.0` shipped. The two move as a pair — signalize 1.0 declares `peerDependencies: {"@spearwolf/eventize": "^6.0.0"}`, which is the range widening that lifts the 5.x holdback. Both libraries key their marker slots with realm-wide symbols, so two majors of either in one consumer tree share a slot per object: eventize 6 answers that with a `TypeError` naming both protocols, signalize 1.0 with two graphs that recognise nothing of each other. Verify with `pnpm why @spearwolf/eventize` after upgrading. Behaviour reaching code built on this package:
|
|
296
|
+
- A bulk `off(target)` clears retained events as well as listeners. Inside the framework this only happens while tearing `ShadowEnv`, `SignalsPath` and `Entity` down, so nothing changes here; a consumer that used `off()` as a listener reset and went on relying on a retained value has to `retain()` again.
|
|
297
|
+
- An event name matching only an inherited `Object.prototype` member — `toString`, `valueOf`, `constructor`, `hasOwnProperty` and their kin — no longer dispatches to that inherited function. One path carries a consumer-chosen string into the event-name slot, and it runs from the Shadow Environment back to the view: `ComponentContext.dispatchMessage()` and `broadcastEvent()` reach `ViewComponent.dispatchEvent()`, which emits the type on the component itself. A message named this way used to end up at the `Object.prototype` member the component inherits and now reaches nothing; a listener explicitly registered for that name still receives it, before and after. The opposite direction is unaffected — `ViewComponent.dispatchShadowObjectsEvent()` and `Entity.dispatchViewEvent()` travel under the `onViewEvent` symbol and carry the string as an argument, so it never becomes an event name and arrives at the Shadow Object unchanged.
|
|
298
|
+
- A retained replay that throws no longer throws out of `on()` / `once()`. The failure goes to `console.warn`, the remaining replays of that batch still run, and the call returns its unsubscribe handle. `RemoteWorkerEnv.workerLoaded` is built on a retained `WorkerLoaded`.
|
|
299
|
+
- An unsubscribe handle is single-shot; a second call is inert instead of releasing a sibling's registration.
|
|
300
|
+
- Several signalize teardown and delivery paths collect their failures into an `AggregateError` instead of throwing the first and abandoning the rest. A `catch` doing an `instanceof` check, or reading `.message`, has to unwrap `error.errors` first — a single failure is still rethrown unchanged.
|
|
301
|
+
- Every message signalize authors is prefixed `[signalize] `, and `batch()` / `beQuiet()` / `hibernate()` refuse an `async` callback outright.
|
|
302
|
+
- **Dependencies (eventize, guarded dispatch):** the library sends its notifications with `emitSafe()` / `emitStrict()` rather than `emit()`. eventize wires the guarded step into its dispatch walk process-wide the first time either is called, so **every** `emit()` in a process that loads this package pays for it — around 29 % on a 64-listener dispatch, on emitters this package never touches. The cost is a threshold, not a scale: using both variants costs what using one costs.
|
|
303
|
+
- **Size:** `dist/bundle.js` grows 139.6 kB → 227.1 kB minified, 42.1 kB → 69.4 kB gzipped. Both dependencies roughly doubled in minified size (eventize 9.5 → 20.8 kB, signalize 14.7 → 30.0 kB) and the bundle carries each of them twice — once directly, once inside the worker source, which sits in the bundle as a plain string literal and becomes a `Blob` and an object URL when the worker is created — which accounts for 53 kB of the 87 kB the bundle gains; the rest is the work listed above. The `dist/` file list and the shape of `dist/package.json` are unchanged; only the two dependency ranges in it move.
|
|
304
|
+
|
|
305
|
+
### Performance
|
|
306
|
+
|
|
307
|
+
- **Performance (view):** the two re-request channels of the View Layer collect and run one round per task instead of one round per arriving entity. A round is a broadcast — every root of the namespace, or every child of one parent — so n entities coming up together used to cost n(n+1)/2 messages, each one a full ancestor request through the DOM. Measured in Chromium via Playwright 1.62.1 on 2026-08-22, one run over the sizes 100/125/150/300/600. 600 roots in one namespace: 180 300 messages and 298,6 ms against a floor of 41,8 ms with the channel switched off, now 600 messages and 44,0 ms. The sibling channel, 600 children under one parent: 179 700 messages and 95,8 ms against a floor of 46,7 ms, now 600 messages and 49,3 ms. The point where the round costs more than a frame moved from about 145 entities in one namespace — it falls between the measured sizes 125 and 150 — to beyond the largest size measured.
|
|
308
|
+
- **Performance (view):** a component that sorts at or after the last member of an ordered uuid list of `ComponentContext` is appended without searching for its place. That is the common case — children arriving in document order, all on the default `order` — and it turns a walk over the whole list into a single lookup. A last uuid whose entry is gone answers nothing, and the scan decides as it always did. Measured 2026-08-31 on node v25.9.0, on the context alone with no DOM around it, over roots created one after another: 600 of them 2.29 ms against 4.44 ms, 1200 of them 7.36 ms against 16.04 ms.
|
|
309
|
+
- **Performance (view):** the two ordered uuid lists of `ComponentContext` — the children of a component and the roots of a namespace — answer membership in one step. Order and membership are written together and only from one place, so the list still carries the order the entity tree is walked in. Together with the append shortcut above this takes the quadratic term out of building siblings: n of them under one parent used to cost n(n+1)/2 steps, a membership scan and a walk for the insertion point over the whole list each, and the build is linear now — 600 roots 1.01 ms against 4.44 ms, 1200 roots 2.03 ms against 16.04 ms, same machine and same day. Both lists are private and nothing on the surface moves.
|
|
310
|
+
- **Performance (elements):** a parent node carries one `MutationObserver` for every `<shae-ent>` watched on it, instead of one per element. The observation belongs to the observer and not to the child it was taken out for, so n elements under one parent used to see every child-list mutation of that node n times, each run walking the whole `removedNodes` list; taking a list of n siblings apart cost n² callback runs. It is one run per mutation now, and the lookup behind it is keyed by the removed node. A new module `src/elements/parentRemoval.ts` holds the register — `watchForRemovalFrom(parent, node, onRemoved)` and `stopWatchingForRemovalFrom(parent, node)` — and adds four files under `dist/src/elements/` to the published list: `parentRemoval.js`, the declaration and a source map beside each. It is not re-exported from `index.ts` and no entry of the `exports` map points at it, so the file list is all a consumer sees of it. Nothing on the surface moves: `getParentNodeForObserver()` and `onParentChanged(newParent, oldParent)` keep their signatures and their meaning, and an element still hears about its own removal and about nobody else's. A throw out of an `onParentChanged` override no longer reaches the shared `MutationObserver` callback uncaught — it is reported through `console.error` and costs only the element it came from, the rest of the same batch runs regardless. A registration ends up on the node the element hangs on: `watchForRemovalFrom()` runs the watchers that have come due for a parent before it takes its own, one of them can be an `onParentChanged` override that moves the element, and the observation follows the element instead of staying on the node it left.
|
|
311
|
+
|
|
312
|
+
### Internal
|
|
313
|
+
|
|
314
|
+
- **Internal (modules):** eleven modules were split out of the files that had grown around them, and each adds its `.js`, its declaration and a source map to the published `dist/` file list: `elements/ShaeLifecycleElement.ts` (the lifecycle all three tags share, with `restore()` and `teardown()` as the extension points a subclass overrides), `elements/hostedSlots.ts` and `elements/forwardCustomEvents.ts` (what `<shae-ent>` does beside its lifecycle), `elements/deferredTeardown.ts`, `elements/displayContentsRule.ts` (a module of its own because `<shae-prop>` needs it and must not pull the View Layer into the `shae-prop.js` subpath), `elements/parentRemoval.ts`, `elements/propValueConverters.ts` (a converter table instead of a 42-case `switch`, measured against it value for value), `utils/MicrotaskCollector.ts` and `utils/MicrotaskGate.ts` (when a deferred batch is emptied and who wraps its delivery), `utils/runGuarded.ts` (the one shape a teardown report takes), and `in-the-dark/ShadowObjectCreationScope.ts` (one scope per Shadow Object, holding the signals, links, subscriptions and cleanups the Creation API hands out until a `tearDown()` that runs once). None of them is re-exported from `index.ts` and no entry of the `exports` map points at any of them, so the file list is all a consumer sees of them. Two teardown reports changed wording along the way: `'rollback of a failed entity creation failed:'` now names the entity it could not destroy, and `'entity teardown failed:'` from `Kernel.destroy()` says it ran during a kernel destroy.
|
|
315
|
+
- **Internal (no surface change):** the `Registry` keeps the tokens it has resolved, keyed by the route and by the truthy properties any routing rule actually mentions, and all four write paths drop what has been kept; its two maps hold what a lookup reads and nothing beside it. `Entity.addChild()` inserts a child at its place among the siblings instead of sorting the whole list per insertion, with the resulting order unchanged. `waitForMessageOfType()` refuses a deadline that is neither `1 … 2147483647` milliseconds nor one of the two values standing for none, `0` and `Infinity`, with a `TypeError` before it sets up a listener or a timer — every option layer above it measures against the same rule, so no call from outside can reach that state. `filterUndefinedProps()` answers `undefined` rather than an empty array where its filter leaves nothing standing, and `propsEqual()`, which nothing called, is gone. `WorkerRuntime.router` is declared `MessageRouter | undefined`, and `MessageRouter.logger` is a getter without a setter, the shape every other logger of this package carries. Three `Error` messages and the doc comments in `ComponentContext` and `Entity` say what the code does, and those comments travel into the published declarations. The four `eslint-disable-next-line` comments naming rules of a linter this repository does not run are removed, with no `biome-ignore` in their place.
|
|
316
|
+
- **Change trail:** building a trail entry and folding it into a component's bookkeeping are two steps. The `make*` methods of `ComponentChanges` read the pending half and leave it where it stands; `commitChange(entry)` folds one applied entry forward and releases exactly the pending values that entry carried, so a value that changes again while the entry is on its way out goes out with the next trail instead of being written off. Whether a component owes anything follows from its written ↔ pending pairs rather than from a counter, and `clear()` — which drops every pending value without writing any of them forward — is the way out for a component whose bookkeeping is replaced wholesale, which is what `ComponentContext.reCreateChanges()` does.
|
|
317
|
+
- **Build:** declarations are emitted by TypeScript 7. The published file list is unchanged and the declarations are byte-identical except for `create-worker.d.ts` / `create-worker.bundle.d.ts`, which now read `declare function _default(): Worker` instead of `declare const _default: () => Worker` — the same type in a different spelling, in a module the `exports` map does not expose.
|
|
318
|
+
- **Packaging:** releases are published from GitHub Actions through npm trusted publishing (OIDC) rather than a long-lived token, so every tarball from this version on carries a provenance attestation linking it to the commit and workflow run that built it. Nothing in the published `dist/` layout changes; details in the [monorepo changelog](https://github.com/spearwolf/shadow-objects/blob/main/CHANGELOG.md).
|
|
319
|
+
|
|
320
|
+
### Docs
|
|
321
|
+
|
|
322
|
+
- **Docs (examples):** every code block in the documentation was extracted and compiled or run, and the defects that made an example fail outright are fixed across `README.md`, the package `README.md`, `getting-started.md`, `concepts.md`, `guides.md`, `best-practices.md`, `cheat-sheet.md` and `api-reference.md`. A `createSignal()` result read by calling it (`count()`) or updated with a callback (`count.set(c => c + 1)`) — a `Signal` is not callable, and `set()` stores the function instead of invoking it. Ten registry manifests exported as `default` where the loader reads the named `shadowObjects` export and nothing else. Two subpath imports the `exports` map does not define (`/view`, `/elements`), and the root entry imported into worker-side code, which dies with `ReferenceError: HTMLElement is not defined`. `onViewEvent` and the lifecycle symbols imported from the main entry, where they are `undefined`. An array handed to `define`, which takes one constructor. `jest.fn()` in a repository that runs vitest. A counter whose `<shae-prop>` carried no `type`, so `'0' + 1` produced `'01'`. And the hand-rolled `createSignal` test double in `best-practices.md` §9, which modelled both wrong behaviours and would have let broken code pass its tests.
|
|
323
|
+
- **Docs (reference):** `api-reference.md` describes the surface the code has. §Kernel carries all 17 public methods where 8 were documented, plus a new section `Two error contracts` naming which path hands its error to the caller and which one reports each step and carries on. `ComponentContext` stands complete for the first time — all 33 public members where 5 were documented — and `<shae-ent>` gained `#### Driving the Lookup by Hand`. §Shadow Object Creation API and §Registry were corrected member by member: `createResource` calls its factory with no arguments and returns a `Signal`, the `createSignal`/`createMemo`/`createEffect` signatures and their return types, the typed `options` of the five signal-returning methods, and a route resolution that includes its starting token and walks breadth-first. The element tables in `api-reference.md` and `cheat-sheet.md` name the truthy-value rule for `local`, `no-autostart`, `no-trim` and `auto-destruct`, the full `auto-sync` value table, the difference between `number` (`Number()`) and `float` (`parseFloat`), the missing `bigint`, `hex`, `oct` and `bin` rows, and the `forward-custom-events` rule that the documented `"true"` never matched. And `api-reference.md` gained a `## Security` section naming the module URL of `<shae-worker>` and `importScript()` a trust boundary, bounded by a Content Security Policy rather than by a check inside the library; both READMEs carry a shorter version.
|
|
324
|
+
- **Docs (structure and terminology):** the introduction is rewritten around the five domains — View, Environment, Kernel, Composition, Shadow Object. `concepts.md` gained the domain table, the change-trail and sync-tempo section with its race-condition warning, and a §5 "Invariants"; `guides.md` gained "Composing Behavior with Routes", `### Registering Your Own Entity Elements` and `### How Many Entities Fit in One Namespace`; the package `README.md` is an npm landing page and `docs/README.md` pure navigation. The two unrelated concepts both called "context" are disambiguated wherever they meet — Entity Context (`provideContext`/`useContext`) against `ComponentContext` — and the string that links a View declaration to a Shadow Object is called Token, with the nine occurrences of "(Component Tag)" gone. The claim that a setup function runs once per entity was corrected at nine places across six files: it runs once per Shadow Object, and an entity can carry several. Three dead links from the pre-flattening layout resolve again, as do the README links on the package's npm page.
|
|
325
|
+
- **Docs (diagrams):** `docs/shadow-theater.svg` and `src/view/ClassGraphOverview.drawio` are removed — one built on an analogy `AGENTS.md` §4 names as one not to use, the other showing a method the package does not have — and the swimlane in `src/view/ShadowEnv.drawio` is labelled `shadow object environment` in all three places its export carries the word. Neither file reached the published package; the `dist/` file list is unchanged.
|
|
326
|
+
|
|
327
|
+
### Tests
|
|
328
|
+
|
|
329
|
+
- **Tests:** regression cases in the existing specs for five of the fixes above — the dropped create-token in `ComponentChanges.changeToken()`, the disposed context accepted by `ViewComponent.context`, `ComponentContext.changeOrder()` after `clear()`, `ComponentContext.removeFromParent()` on a child the context no longer holds, and the order reset in `Kernel.setParent()` — plus two new browser specs in `shadow-objects-testing` (`worker-element-teardown`, `prop-element-host`). All seven were verified to fail against the unfixed sources. New unit specs for `ComponentChanges` (45 cases) and `ComponentMemory` (20 cases), neither of which had a dedicated spec before; `ComponentContext` gained one covering ordered insertion, tree invariants, `reCreateChanges` and property semantics, `ViewComponent` cases for cycle rejection, the destroyed-state contract and token normalization, and `ShadowEnv` cases for `syncWait()` and `AfterSync`.
|
|
71
330
|
|
|
72
331
|
## [0.33.0] - 2026-06-19
|
|
73
332
|
|
|
@@ -79,14 +338,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
79
338
|
|
|
80
339
|
## [0.31.0] - 2026-05-09
|
|
81
340
|
|
|
82
|
-
- **Bugfix (entity lifecycle
|
|
83
|
-
- **Bugfix (entity lifecycle
|
|
84
|
-
- **Bugfix (change trail
|
|
341
|
+
- **Bugfix (entity lifecycle):** `kernel.destroyEntity()` now handles its children explicitly instead of leaving them as orphaned entries inside the kernel. Children with `autoDestructionOnParentRemoval` cascade-destroy together with the parent; all other children are promoted to root entities and remain reachable. This fixes a real leak where children without the auto-destruct flag were left in `kernel.#entities` after the parent was destroyed.
|
|
342
|
+
- **Bugfix (entity lifecycle):** `autoDestructionOnParentRemoval` now survives re-parenting. The subscription is rebound to the new parent on every parent change (`Entity.parentUuid` setter / `Kernel.setParent()`), so a re-parented child is no longer destroyed when its *original* parent dies, and is correctly destroyed with its *current* parent.
|
|
343
|
+
- **Bugfix (change trail):** the `autoDestructionOnParentRemoval` flag is now carried end-to-end through the change-trail pipeline. `ICreateEntitiesChange` exposes a new optional `autoDestructionOnParentRemoval?: boolean` field, `ComponentChanges.create()` accepts it as a 4th parameter, and `Kernel.run()`/`parse()` forwards it to `createEntity()`. Previously the kernel parameter existed but the trail-based path (worker and local env) never set it, so the feature was unreachable in production.
|
|
85
344
|
- **New (public API):** `ViewComponent` accepts an `autoDestructionOnParentRemoval?: boolean` constructor option (also exposed as a read-only getter); the value flows through `ComponentContext.addComponent()` into the change trail, and survives `ContextLost` recovery via `ComponentMemory`. Backwards-compatible (defaults to `false`).
|
|
86
|
-
- **Bugfix (entity lifecycle
|
|
87
|
-
- **Bugfix (registry
|
|
88
|
-
- **DX (creation API
|
|
89
|
-
- **Bugfix (BFS cache
|
|
345
|
+
- **Bugfix (entity lifecycle):** `Entity.parentUuid` setter and `Kernel.setParent()` now resolve the new parent UUID *before* detaching from the current parent. A `setParent` call with an unknown UUID throws as before but the entity stays attached to its original parent instead of being orphaned mid-mutation.
|
|
346
|
+
- **Bugfix (registry):** `Registry.clear()` now also clears the prop-based (`@`-prefix) routes. Previously they accumulated across `clear()` calls, polluting tests and long-lived registries.
|
|
347
|
+
- **DX (creation API):** `useProperty()`, `useContext()`, and `useParentContext()` now warn when a subsequent call passes a different `{compare}` function than the first call. The cached signal is created once with the original options; subsequent calls silently returned the cached reader, which could lead to surprising equality semantics. New behavior: still returns the cached reader, but emits a `console.warn` so the mismatch is visible.
|
|
348
|
+
- **Bugfix (BFS cache):** `kernel.destroyEntity()` now invalidates the BFS traversal cache. Previously, programmatic destruction (e.g. through an auto-destroy listener) could leave `traverseLevelOrderBFS()` returning stale UUIDs.
|
|
90
349
|
- **Bugfix (`./bundle.js` export):** the published `dist/bundle.js` now actually contains the inlined worker and the shae-element registrations. Previous releases shipped a 790-byte stub that only set `globalThis.SHADOW_ENTS_BUNDLE_LOADED = true` because the source-side `package.json#sideEffects` array referenced the (no-longer-emitted) intermediate `build/src/*` paths and the bundle entry was tree-shaken. Consumers using `import '@spearwolf/shadow-objects/bundle.js'` now get the full bundle (~130 KB).
|
|
91
350
|
- **Cleanup:** the published `dist/` no longer contains the leftover `tsconfig.lib.tsbuildinfo` build artifact.
|
|
92
351
|
- Runtime dependencies updated: `@spearwolf/eventize@^4.3.1`, `@spearwolf/signalize@^0.28.0`.
|
|
@@ -203,4 +462,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
203
462
|
- enhance the shadow-objects creation api _aka_ `ShadowObjectParams`
|
|
204
463
|
- added the `useProperties()` function
|
|
205
464
|
- added the `useResource()` function
|
|
206
|
-
- added lots of new tests and improved code coverage
|
|
465
|
+
- added lots of new tests and improved code coverage
|