@spearwolf/shadow-objects 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +69 -1
  2. package/README.md +49 -13
  3. package/bundle.js +9 -9
  4. package/package.json +3 -3
  5. package/src/elements/ShaeEntElement.d.ts +4 -4
  6. package/src/elements/ShaeEntElement.d.ts.map +1 -1
  7. package/src/elements/ShaeEntElement.js +4 -5
  8. package/src/elements/ShaeEntElement.js.map +2 -2
  9. package/src/elements/ShaePropElement.d.ts +5 -5
  10. package/src/elements/ShaePropElement.d.ts.map +1 -1
  11. package/src/elements/ShaePropElement.js +2 -2
  12. package/src/elements/ShaePropElement.js.map +2 -2
  13. package/src/elements/ShaeWorkerElement.d.ts.map +1 -1
  14. package/src/elements/ShaeWorkerElement.js +31 -6
  15. package/src/elements/ShaeWorkerElement.js.map +2 -2
  16. package/src/in-the-dark/Kernel.d.ts +5 -0
  17. package/src/in-the-dark/Kernel.d.ts.map +1 -1
  18. package/src/in-the-dark/Kernel.js +49 -24
  19. package/src/in-the-dark/Kernel.js.map +2 -2
  20. package/src/in-the-dark/Registry.d.ts +1 -1
  21. package/src/in-the-dark/Registry.d.ts.map +1 -1
  22. package/src/in-the-dark/Registry.js +24 -20
  23. package/src/in-the-dark/Registry.js.map +2 -2
  24. package/src/in-the-dark/ShadowObject.d.ts +1 -1
  25. package/src/in-the-dark/ShadowObject.d.ts.map +1 -1
  26. package/src/in-the-dark/ShadowObject.js +2 -2
  27. package/src/in-the-dark/ShadowObject.js.map +2 -2
  28. package/src/in-the-dark/SignalsPath.d.ts.map +1 -1
  29. package/src/in-the-dark/SignalsPath.js.map +2 -2
  30. package/src/in-the-dark/importModule.js +3 -3
  31. package/src/in-the-dark/importModule.js.map +2 -2
  32. package/src/types.d.ts +4 -4
  33. package/src/types.d.ts.map +1 -1
  34. package/src/utils/ConsoleLogger.d.ts.map +1 -1
  35. package/src/utils/ConsoleLogger.js +3 -2
  36. package/src/utils/ConsoleLogger.js.map +2 -2
  37. package/src/utils/FrameLoop.d.ts +1 -1
  38. package/src/utils/FrameLoop.d.ts.map +1 -1
  39. package/src/utils/FrameLoop.js +0 -2
  40. package/src/utils/FrameLoop.js.map +2 -2
  41. package/src/utils/attr-utils.d.ts.map +1 -1
  42. package/src/utils/attr-utils.js +1 -1
  43. package/src/utils/attr-utils.js.map +2 -2
  44. package/src/utils/generateUUID.d.ts +1 -1
  45. package/src/utils/generateUUID.d.ts.map +1 -1
  46. package/src/utils/props-utils.d.ts +1 -1
  47. package/src/utils/props-utils.d.ts.map +1 -1
  48. package/src/utils/waitForMessageOfType.d.ts +5 -1
  49. package/src/utils/waitForMessageOfType.d.ts.map +1 -1
  50. package/src/utils/waitForMessageOfType.js +12 -2
  51. package/src/utils/waitForMessageOfType.js.map +2 -2
  52. package/src/view/ComponentChanges.d.ts +4 -1
  53. package/src/view/ComponentChanges.d.ts.map +1 -1
  54. package/src/view/ComponentChanges.js +20 -6
  55. package/src/view/ComponentChanges.js.map +2 -2
  56. package/src/view/ComponentContext.d.ts +43 -1
  57. package/src/view/ComponentContext.d.ts.map +1 -1
  58. package/src/view/ComponentContext.js +123 -47
  59. package/src/view/ComponentContext.js.map +2 -2
  60. package/src/view/ComponentMemory.js +3 -1
  61. package/src/view/ComponentMemory.js.map +2 -2
  62. package/src/view/IShadowObjectEnvProxy.d.ts +6 -0
  63. package/src/view/IShadowObjectEnvProxy.d.ts.map +1 -1
  64. package/src/view/LocalShadowObjectEnv.d.ts +1 -1
  65. package/src/view/LocalShadowObjectEnv.d.ts.map +1 -1
  66. package/src/view/LocalShadowObjectEnv.js +11 -6
  67. package/src/view/LocalShadowObjectEnv.js.map +2 -2
  68. package/src/view/RemoteWorkerEnv.d.ts +42 -0
  69. package/src/view/RemoteWorkerEnv.d.ts.map +1 -1
  70. package/src/view/RemoteWorkerEnv.js +130 -18
  71. package/src/view/RemoteWorkerEnv.js.map +2 -2
  72. package/src/view/ShadowEnv.d.ts +25 -1
  73. package/src/view/ShadowEnv.d.ts.map +1 -1
  74. package/src/view/ShadowEnv.js +103 -29
  75. package/src/view/ShadowEnv.js.map +2 -2
  76. package/src/view/ViewComponent.d.ts +22 -1
  77. package/src/view/ViewComponent.d.ts.map +1 -1
  78. package/src/view/ViewComponent.js +66 -22
  79. package/src/view/ViewComponent.js.map +2 -2
  80. package/src/worker/MessageRouter.d.ts.map +1 -1
  81. package/src/worker/MessageRouter.js +7 -5
  82. package/src/worker/MessageRouter.js.map +2 -2
package/CHANGELOG.md CHANGED
@@ -9,6 +9,74 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
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.
19
+
20
+ - **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$`, the return of `FrameLoop.start()` and of `filterUndefinedProps()` — and `generateUUID()` returns the template literal type of `crypto.randomUUID()` instead of a plain `string`. Consumers compiling with `strictNullChecks` will see new errors where they relied on a value that was never promised — the promise is the fix.
21
+ - **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.
22
+ - **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.
23
+ - **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.
24
+ - **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
+ - **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.
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`.
30
+ - **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
+ - **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
+ - **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.
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.
50
+ - **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
+ - **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
+ - **New (public API):** `ViewComponent.isDestroyed` reports whether the component is detached from its `ComponentContext`.
55
+ - **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
+ - **Bugfix (view components, VIEW-21):** `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.
57
+ - **Bugfix (view components, VIEW-22):** `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.
58
+ - **Tests:** new unit specs for `ComponentChanges` (45 cases: lifecycle flags, the three trail phases, token/parent/order diffing, property change and removal semantics, events and transferables) and `ComponentMemory` (20 cases), neither of which had a dedicated spec before. `ComponentContext` gained a spec covering ordered insertion, tree invariants, `reCreateChanges` and property semantics; `ViewComponent` gained cases for cycle rejection, the destroyed-state contract and token normalization; `ShadowEnv` gained cases for `syncWait()` and `AfterSync`.
59
+ - **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
+ - **Docs (correctness):** `api-reference.md` documented `createEffect` as `(fn: () => void): void`. It returns an `Effect`, accepts an options/dependency argument, and its callback may return a cleanup function; all three are now documented.
61
+ - **Docs (correctness):** every example that read a `createSignal()` result by calling it (`count()`) or updated it with a callback (`count.set(c => c + 1)`) was broken. `createSignal` returns a `Signal` object, which is not callable — reads are `count.get()` (subscribing) or `count.value` (not subscribing) — and `set()` stores a function as the value instead of invoking it as an updater. Corrected in `README.md`, `getting-started.md`, `guides.md`, `best-practices.md` and `api-reference.md`; the callable form belongs to `SignalReader` (`useProperty`/`useContext`) and is now spelled out. The hand-rolled `createSignal` test double in `best-practices.md` §testing modelled both wrong behaviors and would have let broken code pass its tests — replaced with one that mirrors the real API.
62
+ - **Docs (correctness):** the unit-test example in `best-practices.md` §9 used `jest.fn()` and relied on globals. The repo runs vitest — switched to `vi.fn()` with an explicit `import {expect, test, vi} from 'vitest'`, matching how the specs under `src/` import. Verified by running the example verbatim.
63
+ - **Docs (correctness):** the `cheat-sheet.md` API table gave wrong return types for `useContext`/`useParentContext` (`SignalReader`, not the value), `provideContext`/`provideGlobalContext` (`Signal`, not `void`) and `createEffect` (`Effect`, not `void`).
64
+ - **Docs (correctness):** `api-reference.md` documented conditional routing as requiring a token to point at a `'@prop'` route. It does not: `'@prop'` routes apply to *every* entity carrying a truthy property of that name, regardless of token. Corrected, and the previously undocumented token-scoped form `'token@prop'` added (both verified against `Registry.spec.ts`).
65
+ - **Docs (introduction):** the framework introduction was rewritten around the five domains (View, Environment, Kernel, Composition, Shadow Object). `concepts.md` gained the domain table, a "the View owns structure, not behavior" section, the change-trail/sync-tempo section with the race-condition warning, the four Registry module keys, and a new §5 "Invariants". `guides.md` gained "Composing Behavior with Routes". `cheat-sheet.md` gained the six invariants. `best-practices.md` §6 no longer frames local as a development-only mode and now covers sync tempo in imperative code. The package `README.md` was reduced to an npm landing page (description, install, quick example, domain table, links); `docs/README.md` is now pure navigation.
66
+ - **Docs (correctness):** the package `README.md` quick example was not runnable — it used a non-existent `useSignals()` API and called `Registry.define()` as a static method. Replaced with a working `<shae-worker>` + module-`define` example, plus a pointer to `shadowObjects.define()` for runtime registration.
67
+ - **Docs (correctness):** `api-reference.md` §2 documented `useContext()`/`useParentContext()` as returning the value and `provideContext()`/`provideGlobalContext()` as returning `void`. They return a `SignalReader` and a `Signal` respectively; signatures corrected and `symbol` keys documented.
68
+ - **Docs (terminology):** the two unrelated concepts both called "context" are now disambiguated. Entity Context (`provideContext`/`useContext`, DI along the entity tree) vs. `ComponentContext` (View-Layer namespace binding to a Shadow Environment) — cross-referenced notes in `concepts.md`, `api-reference.md`, and `cheat-sheet.md`; headings renamed to "Entity Context".
69
+ - **Docs (links):** fixed three dead links in `getting-started.md` and `concepts.md` still pointing at the pre-flattening `02-guides/` and `03-api/` layout.
70
+ - **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](../../CHANGELOG.md).
71
+
72
+ ## [0.33.0] - 2026-06-19
73
+
74
+ - Runtime dependency bumped: `@spearwolf/signalize` `^0.29.0` → `^0.30.0`.
75
+
76
+ ## [0.32.0] - 2026-05-13
77
+
78
+ - Runtime dependencies bumped: `@spearwolf/eventize` `^4.3.1` → `^5.0.0`, `@spearwolf/signalize` `^0.28.0` → `^0.29.0`. The eventize 5.0.0 major bump only changes `emit()`/`emitAsync()` on *non-eventized* targets (they now duck-type into the target's matching method or `.emit()` instead of throwing); all internal `emit()` call-sites in shadow-objects target eventized objects, so the change is transparent. signalize 0.29.0 itself bundles the same eventize bump plus a docs rewrite — no runtime-behavior change for shadow-objects. Verified with `pnpm cbt` (7 turbo tasks, 191 unit/integration + 44 e2e tests).
79
+
12
80
  ## [0.31.0] - 2026-05-09
13
81
 
14
82
  - **Bugfix (entity lifecycle, KERN-3):** `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.
@@ -135,4 +203,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
135
203
  - enhance the shadow-objects creation api _aka_ `ShadowObjectParams`
136
204
  - added the `useProperties()` function
137
205
  - added the `useResource()` function
138
- - added lots of new tests and improved code coverage
206
+ - added lots of new tests and improved code coverage
package/README.md CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@spearwolf/shadow-objects)](https://www.npmjs.com/package/@spearwolf/shadow-objects)
4
4
 
5
- Shadow Objects is a reactive ECS (Entity Component System) library that decouples business logic from UI rendering. Entities are lightweight game objects; Shadow Objects are ECS components that attach behavior to them. Shadow environments can run on the main thread (local) or in a web worker (remote) -- both are first-class.
5
+ *a reactive entity-component framework that feels at home in the shadows*
6
+
7
+ Shadow Objects is an Entity Component System (ECS) for the browser platform. It separates application logic from its presentation, and not just logically: the logic runs in a Shadow Environment, which lives either on the main thread (`LocalShadowObjectEnv`) or inside a web worker (`RemoteWorkerEnv`). Your Shadow Object code is identical in both cases. Only the proxy gets swapped.
8
+
9
+ Entities are lightweight nodes in a tree. Shadow Objects are ECS components that attach behavior to them. The View is authoritative for structure, not for behavior: it decides which entities exist and which properties they carry, while the Registry decides which Shadow Objects land on them.
6
10
 
7
11
  ## Installation
8
12
 
@@ -12,25 +16,57 @@ npm install @spearwolf/shadow-objects
12
16
 
13
17
  ## Quick Example
14
18
 
15
- ```typescript
16
- import { Registry } from '@spearwolf/shadow-objects';
19
+ ```html
20
+ <!-- index.html -- the view layer -->
21
+ <script type="module">
22
+ import '@spearwolf/shadow-objects/elements.js';
23
+ </script>
17
24
 
18
- // Define a shadow object -- an ECS component with behavior
19
- function MyComponent({ entity, useSignals }) {
20
- const [count, setCount] = useSignals('count', 0);
25
+ <shae-worker src="./my-logic.js"></shae-worker>
26
+
27
+ <shae-ent token="my-component">
28
+ <shae-prop name="step" value="1" type="int"></shae-prop>
29
+ </shae-ent>
30
+ ```
21
31
 
22
- entity.on('increment', () => setCount(count.get() + 1));
32
+ ```javascript
33
+ // my-logic.js -- runs in the shadow environment
34
+ // A shadow object is an ECS component: its body runs once, then it just reacts.
35
+ function MyComponent({useProperty, createSignal, onViewEvent, dispatchMessageToView}) {
36
+ const step = useProperty('step');
37
+ const count = createSignal(0);
23
38
 
24
- return () => {
25
- // cleanup on destroy
26
- };
39
+ onViewEvent((type) => {
40
+ if (type === 'increment') {
41
+ count.set(count.value + (step() ?? 1));
42
+ dispatchMessageToView('count-changed', {value: count.value});
43
+ }
44
+ });
27
45
  }
28
46
 
29
- // Register it: when a view node has the token 'my-component',
30
- // this shadow object runs in the shadow environment
31
- Registry.define('my-component', MyComponent);
47
+ // The module default export is the registry (component manifest):
48
+ // a view node with the token 'my-component' gets this shadow object.
49
+ export default {
50
+ define: {
51
+ 'my-component': MyComponent,
52
+ },
53
+ };
32
54
  ```
33
55
 
56
+ Need to register a shadow object at runtime instead? Use `shadowObjects.define(token, constructor)` from `@spearwolf/shadow-objects/shadow-objects.js`.
57
+
58
+ ## The Five Domains
59
+
60
+ | # | Domain | Responsibility | Where it lives |
61
+ |---|---|---|---|
62
+ | 1 | **View** | Structure, properties, input | always the main thread |
63
+ | 2 | **Environment** | Place of execution, transport | main thread or worker |
64
+ | 3 | **Kernel** | Lifecycle, entity tree | inside the environment |
65
+ | 4 | **Composition** | Registry, token, routing | inside the environment |
66
+ | 5 | **Shadow Object** | Application logic, reactivity, communication | inside the environment |
67
+
68
+ Each domain, what it owns, what it must not touch, and the invariants that hold the whole thing together are written up in the [project README](https://github.com/spearwolf/shadow-objects#the-five-domains) and in [Concepts](./docs/concepts.md).
69
+
34
70
  ## Documentation
35
71
 
36
72
  - [Overview](./docs/README.md)