From a1faa59fc04ee8f4ed8a4016e0bcaa78844efed1 Mon Sep 17 00:00:00 2001 From: Kunal Kindra Date: Thu, 3 Sep 2026 09:50:52 -0700 Subject: [PATCH 1/5] fix(data-lit): dispose hooks on element disconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hooks stored every effect cleanup in host.hooks[i].dispose, but useEffect only invoked it on dependency change — never on unmount. So every useObservable / useObservableValues / useEffect subscription leaked when a Lit element left the DOM and kept firing requestUpdate on a detached host. useConnected was likewise inert: nothing dispatched the "connected" / "disconnected" events it listened for. No base element ever overrode disconnectedCallback, so there was no disconnect edge at all — this was never implemented, not a regression. Install a single Lit ReactiveController from the withHooks render wrapper (the seam all three opt-in patterns share: base-class attachDecorator, the @withHooks method decorator, and manual attachDecorator on plain LitElements). On disconnect it dispatches "disconnected" and disposes + clears every hook slot; on reconnect it dispatches "connected" and forces a re-render so hooks re-initialize and re-subscribe (full unmount semantics). Idempotent per host; non-Lit hosts are skipped. Co-Authored-By: Claude Opus 4.8 --- .../hooks/component/hooks-controller.test.ts | 94 +++++++++++++++++++ .../src/hooks/component/hooks-controller.ts | 85 +++++++++++++++++ packages/data-lit/src/hooks/index.ts | 1 + packages/data-lit/src/hooks/with-hooks.ts | 5 + 4 files changed, 185 insertions(+) create mode 100644 packages/data-lit/src/hooks/component/hooks-controller.test.ts create mode 100644 packages/data-lit/src/hooks/component/hooks-controller.ts diff --git a/packages/data-lit/src/hooks/component/hooks-controller.test.ts b/packages/data-lit/src/hooks/component/hooks-controller.test.ts new file mode 100644 index 00000000..0dc0d86c --- /dev/null +++ b/packages/data-lit/src/hooks/component/hooks-controller.test.ts @@ -0,0 +1,94 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. +import { describe, it, expect, vi } from "vitest"; +import type { ReactiveController } from "lit"; +import type { Component } from "./component.js"; +import { installHooksController } from "./hooks-controller.js"; + +/** + * Minimal stand-in for a Lit host: an EventTarget that captures the controller + * installed on it so the test can drive `hostConnected` / `hostDisconnected` + * directly (a real Lit element would need a DOM). `addController` mirrors Lit's + * contract of firing `hostConnected` synchronously when already connected. + */ +class FakeHost extends EventTarget implements Component { + isConnected = true; + hookIndex = 0; + hooks: any[] = []; + updatedListeners = new Set<() => void>(); + requestUpdate = vi.fn(); + controllers: ReactiveController[] = []; + + addController(controller: ReactiveController) { + this.controllers.push(controller); + } + // Simulate Lit dispatching lifecycle to every registered controller. + connect() { + for (const c of this.controllers) c.hostConnected?.(); + } + disconnect() { + for (const c of this.controllers) c.hostDisconnected?.(); + } +} + +describe("installHooksController", () => { + it("disposes every effect hook and resets the cursor on disconnect", () => { + const host = new FakeHost(); + const disposeA = vi.fn(); + const disposeB = vi.fn(); + host.hooks = [ + { dispose: disposeA, dependencies: [] }, + 42, // a value slot (useState) — must be skipped, not crash + { dispose: disposeB, dependencies: [] }, + { current: null }, // a ref slot — no dispose + ]; + host.hookIndex = 4; + + installHooksController(host); + host.disconnect(); + + expect(disposeA).toHaveBeenCalledTimes(1); + expect(disposeB).toHaveBeenCalledTimes(1); + expect(host.hooks).toEqual([]); + expect(host.hookIndex).toBe(0); + }); + + it("dispatches connected / disconnected events across the lifecycle", () => { + const host = new FakeHost(); + const onConnected = vi.fn(); + const onDisconnected = vi.fn(); + host.addEventListener("connected", onConnected); + host.addEventListener("disconnected", onDisconnected); + + installHooksController(host); + host.connect(); + expect(onConnected).toHaveBeenCalledTimes(1); + + host.disconnect(); + expect(onDisconnected).toHaveBeenCalledTimes(1); + }); + + it("forces a re-render only on RE-connect, not the first connect", () => { + const host = new FakeHost(); + installHooksController(host); + + host.connect(); // first mount + expect(host.requestUpdate).not.toHaveBeenCalled(); + + host.disconnect(); + host.connect(); // reconnect + expect(host.requestUpdate).toHaveBeenCalledTimes(1); + }); + + it("is idempotent — repeated installs register a single controller", () => { + const host = new FakeHost(); + installHooksController(host); + installHooksController(host); + installHooksController(host); + expect(host.controllers).toHaveLength(1); + }); + + it("skips hosts that are not reactive controller hosts", () => { + const host = new EventTarget() as unknown as Component; + expect(() => installHooksController(host)).not.toThrow(); + }); +}); diff --git a/packages/data-lit/src/hooks/component/hooks-controller.ts b/packages/data-lit/src/hooks/component/hooks-controller.ts new file mode 100644 index 00000000..190917d9 --- /dev/null +++ b/packages/data-lit/src/hooks/component/hooks-controller.ts @@ -0,0 +1,85 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. + +import type { ReactiveController, ReactiveControllerHost } from "lit"; +import type { Component } from "./component.js"; + +/** + * Per-host flag marking that the hook-lifecycle controller is already + * installed, so repeated renders (and double-wrapped `render` methods) do + * not register duplicate controllers. + */ +const HOOKS_CONTROLLER = Symbol("data-lit.hooksController"); + +type ReactiveHost = Component & ReactiveControllerHost & { [HOOKS_CONTROLLER]?: boolean }; + +function isReactiveHost(host: Component): host is ReactiveHost { + return typeof (host as Partial).addController === "function"; +} + +/** + * Dispose every effect-style hook slot and reset the hook cursor so the next + * render re-initializes hooks from scratch. Only slots that expose a + * `dispose` function (effects, subscriptions) are torn down; value slots + * (state, memo, ref) are simply dropped. + */ +function disposeHooks(host: Component): void { + const { hooks } = host; + if (hooks) { + for (const hook of hooks) { + (hook as { dispose?: () => void } | undefined)?.dispose?.(); + } + } + host.hooks = []; + host.hookIndex = 0; +} + +/** + * Give a hook host a real disconnect edge. + * + * The hook model stores every effect's cleanup in `host.hooks[i].dispose`, + * but {@link useEffect} only invokes it when dependencies change — never when + * the element leaves the DOM. Left alone, every `useObservable` / + * `useObservableValues` / `useEffect` subscription leaks on unmount and keeps + * firing `requestUpdate` on a detached element. `useConnected` is likewise + * inert because nothing dispatches the `"connected"` / `"disconnected"` + * events it listens for. + * + * This installs a single Lit {@link ReactiveController} — the one hook-friendly + * path to a genuine `hostDisconnected` — that: + * - on connect, dispatches `"connected"`, and on every *re*-connect forces a + * re-render so the (previously torn-down) hooks re-initialize and + * re-subscribe; + * - on disconnect, dispatches `"disconnected"` then disposes and clears all + * hook slots (full unmount semantics). + * + * It is installed lazily from inside the wrapped `render` (see {@link withHooks}), + * which runs after `connectedCallback`, so `addController` fires `hostConnected` + * synchronously on the first mount. The install is idempotent per host. + * + * Non-Lit hosts (no `addController`) are skipped; they may dispatch the + * lifecycle events themselves. + */ +export function installHooksController(host: Component): void { + if (!isReactiveHost(host) || host[HOOKS_CONTROLLER]) { + return; + } + host[HOOKS_CONTROLLER] = true; + + let connectedOnce = false; + const controller: ReactiveController = { + hostConnected() { + host.dispatchEvent(new Event("connected")); + if (connectedOnce) { + // Reconnect: the previous disconnect disposed and cleared every + // hook slot, so re-render to re-run hooks and re-subscribe. + host.requestUpdate(); + } + connectedOnce = true; + }, + hostDisconnected() { + host.dispatchEvent(new Event("disconnected")); + disposeHooks(host); + }, + }; + host.addController(controller); +} diff --git a/packages/data-lit/src/hooks/index.ts b/packages/data-lit/src/hooks/index.ts index 544c47b2..f478abdd 100644 --- a/packages/data-lit/src/hooks/index.ts +++ b/packages/data-lit/src/hooks/index.ts @@ -2,6 +2,7 @@ export * from "./component/component.js"; export * from "./component/stack.js"; +export * from "./component/hooks-controller.js"; export * from "./use-state.js"; export * from "./use-effect.js"; export * from "./use-connected.js"; diff --git a/packages/data-lit/src/hooks/with-hooks.ts b/packages/data-lit/src/hooks/with-hooks.ts index 7cd0ae37..e5ab8243 100644 --- a/packages/data-lit/src/hooks/with-hooks.ts +++ b/packages/data-lit/src/hooks/with-hooks.ts @@ -2,6 +2,7 @@ import type { Component } from "./component/component.js"; import { Component_stack } from "./component/stack.js"; +import { installHooksController } from "./component/hooks-controller.js"; export function withHooks( target: object, @@ -10,6 +11,10 @@ export function withHooks( ): TypedPropertyDescriptor<(this: This, ...args: Args) => Return> { const originalMethod = descriptor.value!; descriptor.value = function (this: This, ...args: Args): Return { + // Give the host a real disconnect edge so effect/subscription cleanups + // actually run on unmount. Idempotent per host, so double-wrapped + // `render` methods install it only once. + installHooksController(this); Component_stack.push(this); try { return originalMethod.apply(this, args); From cfbf036a946144235e60510ac77ba72838eab4fc Mon Sep 17 00:00:00 2001 From: Kunal Kindra Date: Thu, 3 Sep 2026 09:50:52 -0700 Subject: [PATCH 2/5] chore(release): 0.10.8 Co-Authored-By: Claude Opus 4.8 --- package.json | 2 +- packages/data-ai/.claude-plugin/plugin.json | 2 +- packages/data-ai/package.json | 2 +- packages/data-gpu/package.json | 2 +- packages/data-lit/package.json | 2 +- packages/data-persistence/package.json | 2 +- packages/data-react/package.json | 2 +- packages/data-solid/package.json | 2 +- packages/data-sync/package.json | 2 +- packages/data-testing/package.json | 2 +- packages/data/package.json | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index e4b0a670..8d5f086b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.10.7", + "version": "0.10.8", "private": true, "engines": { "node": ">=24" diff --git a/packages/data-ai/.claude-plugin/plugin.json b/packages/data-ai/.claude-plugin/plugin.json index ec7ea367..62a20bf4 100644 --- a/packages/data-ai/.claude-plugin/plugin.json +++ b/packages/data-ai/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "adobe-data-ai", - "version": "0.10.7", + "version": "0.10.8", "description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.", "author": { "name": "Adobe" diff --git a/packages/data-ai/package.json b/packages/data-ai/package.json index 47e5d1cc..72a50bcc 100644 --- a/packages/data-ai/package.json +++ b/packages/data-ai/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-ai", - "version": "0.10.7", + "version": "0.10.8", "description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).", "type": "module", "private": false, diff --git a/packages/data-gpu/package.json b/packages/data-gpu/package.json index f7aec11c..0364e43b 100644 --- a/packages/data-gpu/package.json +++ b/packages/data-gpu/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-gpu", - "version": "0.10.7", + "version": "0.10.8", "description": "Adobe data WebGPU plugins and types for graphics and compute", "type": "module", "private": false, diff --git a/packages/data-lit/package.json b/packages/data-lit/package.json index b60ec0e4..c9c522e1 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.10.7", + "version": "0.10.8", "description": "Adobe data Lit bindings - hooks, elements, decorators", "type": "module", "private": false, diff --git a/packages/data-persistence/package.json b/packages/data-persistence/package.json index 0292cad5..81e196cb 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.10.7", + "version": "0.10.8", "description": "Worker-based incremental persistence layer for @adobe/data ECS over OPFS (browser) and node:fs (server).", "type": "module", "sideEffects": false, diff --git a/packages/data-react/package.json b/packages/data-react/package.json index 413e3122..2ae887a1 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.10.7", + "version": "0.10.8", "description": "Adobe data React bindings — hooks and context for ECS database", "type": "module", "private": false, diff --git a/packages/data-solid/package.json b/packages/data-solid/package.json index 4af8c86c..0587e437 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.10.7", + "version": "0.10.8", "description": "Adobe data SolidJS bindings — context and provider for ECS database", "type": "module", "private": false, diff --git a/packages/data-sync/package.json b/packages/data-sync/package.json index 1ce510d7..e1d2e770 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.10.7", + "version": "0.10.8", "description": "Multi-user real-time synchronisation for @adobe/data ECS — server, client, and in-process loopback.", "type": "module", "sideEffects": false, diff --git a/packages/data-testing/package.json b/packages/data-testing/package.json index 4e71176c..781198c6 100644 --- a/packages/data-testing/package.json +++ b/packages/data-testing/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-testing", - "version": "0.10.7", + "version": "0.10.8", "description": "Conformance-testing utilities (Match + Conformance runners) for @adobe/data ECS features", "type": "module", "sideEffects": false, diff --git a/packages/data/package.json b/packages/data/package.json index 20db7c62..acb781b3 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.10.7", + "version": "0.10.8", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false, From 9d40c072e98a2407fc438f008621eeb7a1c229bc Mon Sep 17 00:00:00 2001 From: Monil Date: Fri, 4 Sep 2026 16:43:01 -0500 Subject: [PATCH 3/5] fix: address review comments --- packages/data-lit/README.md | 20 +++- packages/data-lit/package.json | 1 + .../hooks-controller.integration.test.ts | 88 +++++++++++++++++ .../src/hooks/component/hooks-controller.ts | 23 ++++- packages/data-lit/src/hooks/use-connected.ts | 5 +- pnpm-lock.yaml | 94 ++++++++++++++++++- 6 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 packages/data-lit/src/hooks/component/hooks-controller.integration.test.ts diff --git a/packages/data-lit/README.md b/packages/data-lit/README.md index f0e5f689..8f028f9a 100644 --- a/packages/data-lit/README.md +++ b/packages/data-lit/README.md @@ -1,6 +1,7 @@ # @adobe/data-lit -Lit bindings for [@adobe/data](https://www.npmjs.com/package/@adobe/data) — hooks, elements, and decorators for building reactive UIs with the @adobe/data ECS database and observables. +Lit bindings for [@adobe/data](https://www.npmjs.com/package/@adobe/data): hooks, elements, and decorators +for building reactive UIs with the @adobe/data ECS database and observables. ## Install @@ -15,4 +16,21 @@ import { ApplicationHost, DatabaseElement } from "@adobe/data-lit"; import { createDatabase } from "@adobe/data/ecs"; ``` +## Hooks and the disconnect lifecycle + +Hooks (`useState`, `useEffect`, `useObservable`, `useObservableValues`, `useMemo`, `useRef`, ...) follow +React's unmount semantics. When a hook-using element is disconnected from the DOM, **all of its hook state is +reset**: + +- every `useEffect` cleanup runs, so observable subscriptions and event listeners are torn down, and +- all stored values (`useState`, `useMemo`, `useRef`) are discarded. + +On the next connect the element renders fresh from its initial hook state. + +> **Footgun:** because a disconnect is a full reset, a DOM *move* (`el.remove()` then re-appending it, or +> reparenting the element) is treated as a full unmount followed by a remount. Any local UI state held in +> hooks is silently lost across the move: toggle positions, uncontrolled input values, scroll offsets. If a +> piece of state must survive a reparent, keep it in the `@adobe/data` database/service rather than in element +> hook state. + See the `data-lit-todo` sample in this repository for a full example. diff --git a/packages/data-lit/package.json b/packages/data-lit/package.json index 6573f805..a5c7f52b 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -25,6 +25,7 @@ "lit": "^3.3.1" }, "devDependencies": { + "happy-dom": "^20.14.0", "lit": "^3.3.1", "typescript": "^5.8.3", "vitest": "^1.6.0" diff --git a/packages/data-lit/src/hooks/component/hooks-controller.integration.test.ts b/packages/data-lit/src/hooks/component/hooks-controller.integration.test.ts new file mode 100644 index 00000000..4734a36c --- /dev/null +++ b/packages/data-lit/src/hooks/component/hooks-controller.integration.test.ts @@ -0,0 +1,88 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. +// @vitest-environment happy-dom +import { describe, it, expect } from "vitest"; +import { LitElement, html } from "lit"; +import type { Observe } from "@adobe/data/observe"; +import { attachDecorator } from "../attach-decorator.js"; +import { withHooks } from "../with-hooks.js"; +import { useObservable } from "../use-observable.js"; + +/** + * Real-DOM regression test for the hook-disposal fix. Where the sibling + * hooks-controller.test.ts drives a hand-rolled FakeHost, this mounts a genuine + * LitElement in a happy-dom document, so it exercises the actual bug path: + * `withHooks` installs the controller, `addController` fires `hostConnected` + * synchronously during the real connect, and a real `useObservable` subscription + * is torn down when the element leaves the DOM (instead of leaking and firing + * `requestUpdate` on a detached element). + */ + +/** An Observe whose live subscriber count the test can inspect. */ +function createTrackedObservable(): { + observable: Observe; + emit: (value: number) => void; + subscriberCount: () => number; +} { + const subscribers = new Set<(value: number) => void>(); + let current = 0; + const observable: Observe = observer => { + subscribers.add(observer); + observer(current); + return () => { + subscribers.delete(observer); + }; + }; + return { + observable, + emit: value => { + current = value; + for (const observer of subscribers) observer(value); + }, + subscriberCount: () => subscribers.size, + }; +} + +class HookProbeElement extends LitElement { + observable!: Observe; + constructor() { + super(); + attachDecorator(this, "render", withHooks); + } + render() { + const value = useObservable(this.observable); + return html`${value ?? ""}`; + } +} +customElements.define("hook-probe-element", HookProbeElement); + +describe("hook disposal on real DOM disconnect", () => { + it("tears down a useObservable subscription and stops requestUpdate when the element is removed", async () => { + const { observable, emit, subscriberCount } = createTrackedObservable(); + + const el = document.createElement("hook-probe-element") as HookProbeElement; + el.observable = observable; + document.body.appendChild(el); + await el.updateComplete; + + // Mounted: the render subscribed exactly once. + expect(subscriberCount()).toBe(1); + + // Track any requestUpdate that fires AFTER the element is removed. + let updatesAfterRemove = 0; + const originalRequestUpdate = el.requestUpdate.bind(el); + el.requestUpdate = (...args: Parameters) => { + updatesAfterRemove++; + originalRequestUpdate(...args); + }; + + el.remove(); + + // Disconnect ran the effect cleanup: the subscription is gone. + expect(subscriberCount()).toBe(0); + + // A later emit reaches no leaked subscriber, so the detached element is + // never asked to update. Without the disposal fix, this would be >= 1. + emit(99); + expect(updatesAfterRemove).toBe(0); + }); +}); diff --git a/packages/data-lit/src/hooks/component/hooks-controller.ts b/packages/data-lit/src/hooks/component/hooks-controller.ts index 190917d9..4351b30a 100644 --- a/packages/data-lit/src/hooks/component/hooks-controller.ts +++ b/packages/data-lit/src/hooks/component/hooks-controller.ts @@ -13,7 +13,7 @@ const HOOKS_CONTROLLER = Symbol("data-lit.hooksController"); type ReactiveHost = Component & ReactiveControllerHost & { [HOOKS_CONTROLLER]?: boolean }; function isReactiveHost(host: Component): host is ReactiveHost { - return typeof (host as Partial).addController === "function"; + return "addController" in host && typeof host.addController === "function"; } /** @@ -29,6 +29,11 @@ function disposeHooks(host: Component): void { (hook as { dispose?: () => void } | undefined)?.dispose?.(); } } + // Invariant: every live subscription (useEffect / useObservable / ...) exposes a + // `dispose` and is torn down in the loop above, so nothing should fire after this. + // A useState setter closure that still runs post-disconnect would write into the + // emptied array and requestUpdate a detached host. That signals an un-torn-down + // subscription (a bug at the subscription site), not something to guard here. host.hooks = []; host.hookIndex = 0; } @@ -63,11 +68,18 @@ export function installHooksController(host: Component): void { if (!isReactiveHost(host) || host[HOOKS_CONTROLLER]) { return; } + // Mark installed BEFORE addController below: on an already-connected host + // addController fires hostConnected synchronously, and setting the flag first + // guards against a re-entrant render re-installing a duplicate controller. host[HOOKS_CONTROLLER] = true; let connectedOnce = false; const controller: ReactiveController = { hostConnected() { + // The "connected" / "disconnected" events are for EXTERNAL listeners only. + // The internal useConnected does NOT depend on them: this fires before the + // render body attaches any listener, so useConnected is driven by its direct + // isConnected check plus useEffect cleanup instead. host.dispatchEvent(new Event("connected")); if (connectedOnce) { // Reconnect: the previous disconnect disposed and cleared every @@ -81,5 +93,12 @@ export function installHooksController(host: Component): void { disposeHooks(host); }, }; - host.addController(controller); + try { + host.addController(controller); + } catch (error) { + // addController failed, so no controller is registered. Clear the flag so a + // later render can retry the install instead of being permanently skipped. + host[HOOKS_CONTROLLER] = undefined; + throw error; + } } diff --git a/packages/data-lit/src/hooks/use-connected.ts b/packages/data-lit/src/hooks/use-connected.ts index 13cabf9a..358875b2 100644 --- a/packages/data-lit/src/hooks/use-connected.ts +++ b/packages/data-lit/src/hooks/use-connected.ts @@ -21,7 +21,10 @@ export function useConnected(callback: EffectCallback, dependencies?: unknown[]) } } - // TODO + // This direct isConnected check is what actually drives the connect path. The + // controller's "connected" event (see hooks-controller.ts) is dispatched before + // this listener is attached below, so useConnected can't rely on receiving it. + // It checks isConnected synchronously here on first render and after a reconnect. if (component.isConnected) { onConnect(); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed8e1561..f3ff3d1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -241,6 +241,9 @@ importers: specifier: workspace:* version: link:../data devDependencies: + happy-dom: + specifier: ^20.14.0 + version: 20.14.0 lit: specifier: ^3.3.1 version: 3.3.1 @@ -249,7 +252,7 @@ importers: version: 5.8.3 vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@25.6.0)(@vitest/browser@1.6.0)(jsdom@24.1.0) + version: 1.6.0(@types/node@25.6.0)(happy-dom@20.14.0) packages/data-lit-space-rock-game: dependencies: @@ -1992,6 +1995,10 @@ packages: resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} dev: true + /@types/whatwg-mimetype@3.0.2: + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + dev: true + /@types/which@2.0.2: resolution: {integrity: sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==} dev: true @@ -2794,6 +2801,13 @@ packages: engines: {node: '>=8.0.0'} dev: true + /buffer-image-size@0.6.4: + resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} + engines: {node: '>=4.0'} + dependencies: + '@types/node': 25.6.0 + dev: true + /buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} dependencies: @@ -4513,6 +4527,22 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true + /happy-dom@20.14.0: + resolution: {integrity: sha512-4bRh1KzRvKDnFNTlLhzT1RZTpkKhQbQDl9j+7GXszWsvuspYdo29k6OHRf4PwiM6oLb8r/pMWeYiJjkfod5AvQ==} + engines: {node: '>=20.0.0'} + dependencies: + '@types/node': 25.6.0 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + buffer-image-size: 0.6.4 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + /has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -7831,6 +7861,63 @@ packages: - terser dev: true + /vitest@1.6.0(@types/node@25.6.0)(happy-dom@20.14.0): + resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.0 + '@vitest/ui': 1.6.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 25.6.0 + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + acorn-walk: 8.3.5 + chai: 4.5.0 + debug: 4.4.3(supports-color@5.5.0) + execa: 8.0.1 + happy-dom: 20.14.0 + local-pkg: 0.5.1 + magic-string: 0.30.21 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 2.1.1 + tinybench: 2.9.0 + tinypool: 0.8.4 + vite: 5.1.1(@types/node@25.6.0) + vite-node: 1.6.0(@types/node@25.6.0) + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} dev: true @@ -7939,6 +8026,11 @@ packages: iconv-lite: 0.6.3 dev: true + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: true + /whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} From ef1ecae0f7a5ba94c82bc597bef462b0bd190bc5 Mon Sep 17 00:00:00 2001 From: Monil Date: Fri, 4 Sep 2026 17:23:22 -0500 Subject: [PATCH 4/5] chore: reverting an unwanted change --- packages/data-lit/src/hooks/use-connected.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/data-lit/src/hooks/use-connected.ts b/packages/data-lit/src/hooks/use-connected.ts index 358875b2..13cabf9a 100644 --- a/packages/data-lit/src/hooks/use-connected.ts +++ b/packages/data-lit/src/hooks/use-connected.ts @@ -21,10 +21,7 @@ export function useConnected(callback: EffectCallback, dependencies?: unknown[]) } } - // This direct isConnected check is what actually drives the connect path. The - // controller's "connected" event (see hooks-controller.ts) is dispatched before - // this listener is attached below, so useConnected can't rely on receiving it. - // It checks isConnected synchronously here on first render and after a reconnect. + // TODO if (component.isConnected) { onConnect(); } From ae83ba8720678af5db2fe12584f0168c1cb60314 Mon Sep 17 00:00:00 2001 From: Monil Date: Fri, 4 Sep 2026 18:56:41 -0500 Subject: [PATCH 5/5] chore: version bump --- package.json | 2 +- packages/data-ai/.claude-plugin/plugin.json | 2 +- packages/data-ai/package.json | 2 +- packages/data-gpu/package.json | 2 +- packages/data-lit/package.json | 2 +- packages/data-persistence/package.json | 2 +- packages/data-react/package.json | 2 +- packages/data-rpc/package.json | 2 +- packages/data-solid/package.json | 2 +- packages/data-sync/package.json | 2 +- packages/data-testing/package.json | 2 +- packages/data/package.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 92d197cb..f37bc7f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.10.10", + "version": "0.10.11", "private": true, "engines": { "node": ">=24" diff --git a/packages/data-ai/.claude-plugin/plugin.json b/packages/data-ai/.claude-plugin/plugin.json index 39a0d889..474eeba1 100644 --- a/packages/data-ai/.claude-plugin/plugin.json +++ b/packages/data-ai/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "adobe-data-ai", - "version": "0.10.10", + "version": "0.10.11", "description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.", "author": { "name": "Adobe" diff --git a/packages/data-ai/package.json b/packages/data-ai/package.json index 7ed659b0..8ea3f9b5 100644 --- a/packages/data-ai/package.json +++ b/packages/data-ai/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-ai", - "version": "0.10.10", + "version": "0.10.11", "description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).", "type": "module", "private": false, diff --git a/packages/data-gpu/package.json b/packages/data-gpu/package.json index 987f3b4f..9200669b 100644 --- a/packages/data-gpu/package.json +++ b/packages/data-gpu/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-gpu", - "version": "0.10.10", + "version": "0.10.11", "description": "Adobe data WebGPU plugins and types for graphics and compute", "type": "module", "private": false, diff --git a/packages/data-lit/package.json b/packages/data-lit/package.json index a5c7f52b..7ff4755f 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.10.10", + "version": "0.10.11", "description": "Adobe data Lit bindings - hooks, elements, decorators", "type": "module", "private": false, diff --git a/packages/data-persistence/package.json b/packages/data-persistence/package.json index 5ac6b376..5ef892b3 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.10.10", + "version": "0.10.11", "description": "Worker-based incremental persistence layer for @adobe/data ECS over OPFS (browser) and node:fs (server).", "type": "module", "sideEffects": false, diff --git a/packages/data-react/package.json b/packages/data-react/package.json index e4c6e9c8..9549cfa3 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.10.10", + "version": "0.10.11", "description": "Adobe data React bindings — hooks and context for ECS database", "type": "module", "private": false, diff --git a/packages/data-rpc/package.json b/packages/data-rpc/package.json index 3985f6cd..f1fd1019 100644 --- a/packages/data-rpc/package.json +++ b/packages/data-rpc/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-rpc", - "version": "0.10.10", + "version": "0.10.11", "description": "Schema-driven, bidirectional projection of @adobe/data async data services across a boundary (iframe / MessagePort / Worker). Only Data crosses the wire.", "type": "module", "sideEffects": false, diff --git a/packages/data-solid/package.json b/packages/data-solid/package.json index 295f816c..7732ee0a 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.10.10", + "version": "0.10.11", "description": "Adobe data SolidJS bindings — context and provider for ECS database", "type": "module", "private": false, diff --git a/packages/data-sync/package.json b/packages/data-sync/package.json index dc53b89d..cd64ff15 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.10.10", + "version": "0.10.11", "description": "Multi-user real-time synchronisation for @adobe/data ECS — server, client, and in-process loopback.", "type": "module", "sideEffects": false, diff --git a/packages/data-testing/package.json b/packages/data-testing/package.json index 471580f2..fca8c8d0 100644 --- a/packages/data-testing/package.json +++ b/packages/data-testing/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-testing", - "version": "0.10.10", + "version": "0.10.11", "description": "Conformance-testing utilities (Match + Conformance runners) for @adobe/data ECS features", "type": "module", "sideEffects": false, diff --git a/packages/data/package.json b/packages/data/package.json index 49d2ec91..9f78831f 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.10.10", + "version": "0.10.11", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false,