@adobe/data-lit 0.10.14 → 0.10.16

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.
@@ -1,36 +1,46 @@
1
1
  import type { Component } from "./component.js";
2
2
  /**
3
- * Give a hook host a real disconnect edge.
3
+ * Give a hook host a correct lifecycle by separating two concerns that the DOM
4
+ * connect/disconnect edge conflates.
4
5
  *
5
- * The hook model stores every effect's cleanup in `host.hooks[i].dispose`,
6
- * but {@link useEffect} only invokes it when dependencies change — never when
7
- * the element leaves the DOM. Left alone, every `useObservable` /
8
- * `useObservableValues` / `useEffect` subscription leaks on unmount and keeps
9
- * firing `requestUpdate` on a detached element. `useConnected` is likewise
10
- * inert because nothing dispatches the `"connected"` / `"disconnected"`
11
- * events it listens for.
6
+ * A hook host has two kinds of slot:
12
7
  *
13
- * This installs a single Lit {@link ReactiveController} — the one hook-friendly
14
- * path to a genuine `hostDisconnected` — that:
15
- * - on disconnect, *schedules* (on a microtask) dispatching `"disconnected"`,
16
- * disposing every hook slot, and clearing the cursor (full unmount semantics);
17
- * - on connect, if a teardown is still pending it CANCELS it (the disconnect
18
- * was half of a DOM move — hooks are intact, so nothing re-subscribes and no
19
- * re-render is forced); otherwise it dispatches `"connected"` and, on a true
20
- * reconnect, forces a re-render so the torn-down hooks re-initialize.
8
+ * - **Value / subscription slots** — {@link useState}, {@link useRef},
9
+ * {@link useMemo}, and effect-backed subscriptions ({@link useEffect},
10
+ * {@link useObservable}, {@link useObservableValues}). These are *stateful*
11
+ * and *expensive to rebuild*. They must SURVIVE a DOM move and be finalized
12
+ * only when the element is genuinely gone.
13
+ * - **Connectedness edges** — {@link useConnected}, the primitive for window /
14
+ * document listeners, portals, drag sessions: things that track raw DOM
15
+ * connectedness and must attach/detach *synchronously* on every edge.
21
16
  *
22
- * ## Why disposal is deferred
17
+ * The trap: the DOM fires `disconnectedCallback` then `connectedCallback`
18
+ * **synchronously, in the same task,** whenever a connected element is merely
19
+ * *moved* (a Lit `repeat` reorder, a list re-parenting, a drag-reorder). There
20
+ * is no synchronous signal that distinguishes a move from a genuine unmount, so
21
+ * this controller does not try to classify the edge. Instead it handles each
22
+ * concern with the policy that concern actually wants:
23
23
  *
24
- * The DOM fires `disconnectedCallback` then `connectedCallback` **synchronously,
25
- * in the same task,** whenever a connected element is merely *moved* — a Lit
26
- * `repeat` reorder, a list re-parenting, a drag-reorder. Disposing inline would
27
- * tear down and rebuild every subscription (re-running uncached producers) plus
28
- * force a re-render on every such move, with the hook state momentarily reset —
29
- * the studio-page lag, refresh churn, and transient empty lists on project
30
- * switch (FFP-111900). Deferring teardown one microtask lets a synchronous
31
- * reconnect distinguish a move (cancel) from a genuine unmount (let it run), so
32
- * a move is a no-op and only a real unmount disposes. A late reconnect (a
33
- * different task) is treated as unmount+remount, which is correct.
24
+ * - **Edges fire synchronously, every time.** `hostConnected` /
25
+ * `hostDisconnected` dispatch the `"connected"` / `"disconnected"` events
26
+ * inline on every raw edge, so {@link useConnected} tears down and re-attaches
27
+ * deterministically — including across a move (cheap, and exactly what a
28
+ * listener/portal wants).
29
+ * - **Slot finalization is deferred behind an `isConnected` re-check.** A
30
+ * disconnect schedules a microtask; when it runs it disposes the slots only
31
+ * if the host is STILL disconnected. A move has reconnected by then, so its
32
+ * subscription/value slots survive untouched — no re-subscribe, no state
33
+ * reset. A genuine unmount is still disconnected, so it finalizes, preserving
34
+ * the unmount-leak fix.
35
+ * - **Every RE-connect forces one `requestUpdate`.** The DOM does not re-render
36
+ * an element on a bare re-parent, so without this an element whose render
37
+ * reads mutable non-hook state (a plain field, an ancestor value) would show
38
+ * stale output after a move. The re-render is cheap: across a move the slots
39
+ * are preserved, so it does NOT re-subscribe or re-run producers — it is a Lit
40
+ * diff over unchanged hook state. Across a genuine remount the slots were
41
+ * finalized (cleared), so the same re-render re-initializes and re-subscribes
42
+ * them. (The perf win is not re-running producers on a move — never the cheap
43
+ * re-render.)
34
44
  *
35
45
  * It is installed lazily from inside the wrapped `render` (see {@link withHooks}),
36
46
  * which runs after `connectedCallback`, so `addController` fires `hostConnected`
@@ -107,13 +107,16 @@ describe("hook disposal on real DOM disconnect", () => {
107
107
  // The element is still mounted and its subscription is intact...
108
108
  expect(el.isConnected).toBe(true);
109
109
  expect(subscriberCount()).toBe(1);
110
- // ...and, crucially, the move neither disposed nor re-created it. Before the
111
- // deferred-disposal fix, a move disposed and re-subscribed (2 subs / 1 unsub)
112
- // and forced an extra re-render — the source of the studio-page lag, churn,
113
- // and transient empty lists on project switch (FFP-111900).
110
+ // ...and, crucially, the move neither disposed nor re-created the subscription:
111
+ // no re-subscribe, no producer re-run. That is the perf win (before this, a move
112
+ // disposed and re-subscribed — 2 subs / 1 unsub — the source of the studio-page
113
+ // lag, churn, and transient empty lists on project switch, FFP-111900).
114
114
  expect(totalUnsubscribes()).toBe(0);
115
115
  expect(totalSubscribes()).toBe(1);
116
- expect(updatesDuringMove).toBe(0);
116
+ // The reconnect DOES force exactly one re-render — a cheap Lit diff over the
117
+ // preserved subscription (no re-subscribe), required so an element that reads
118
+ // mutable non-hook state in render refreshes after a move.
119
+ expect(updatesDuringMove).toBe(1);
117
120
  });
118
121
  });
119
122
  //# sourceMappingURL=hooks-controller.integration.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks-controller.integration.test.js","sourceRoot":"","sources":["../../../src/hooks/component/hooks-controller.integration.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hooks-controller.integration.test.js","sourceRoot":"","sources":["../../../src/hooks/component/hooks-controller.integration.test.ts"],"names":[],"mappings":"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"}
@@ -9,58 +9,63 @@ function isReactiveHost(host) {
9
9
  return "addController" in host && typeof host.addController === "function";
10
10
  }
11
11
  /**
12
- * Dispose every effect-style hook slot and reset the hook cursor so the next
13
- * render re-initializes hooks from scratch. Only slots that expose a
14
- * `dispose` function (effects, subscriptions) are torn down; value slots
15
- * (state, memo, ref) are simply dropped.
12
+ * Finalize a host: dispose every hook slot's cleanup and clear the slots so the
13
+ * next render re-initializes from scratch. Only slots that expose a `dispose`
14
+ * function (effects, subscriptions) are torn down; value slots (state, memo,
15
+ * ref) are simply dropped.
16
16
  */
17
- function disposeHooks(host) {
17
+ function finalizeHooks(host) {
18
18
  const { hooks } = host;
19
19
  if (hooks) {
20
20
  for (const hook of hooks) {
21
21
  hook?.dispose?.();
22
22
  }
23
23
  }
24
- // Invariant: every live subscription (useEffect / useObservable / ...) exposes a
25
- // `dispose` and is torn down in the loop above, so nothing should fire after this.
26
- // A useState setter closure that still runs post-disconnect would write into the
27
- // emptied array and requestUpdate a detached host. That signals an un-torn-down
28
- // subscription (a bug at the subscription site), not something to guard here.
29
24
  host.hooks = [];
30
25
  host.hookIndex = 0;
31
26
  }
32
27
  /**
33
- * Give a hook host a real disconnect edge.
28
+ * Give a hook host a correct lifecycle by separating two concerns that the DOM
29
+ * connect/disconnect edge conflates.
34
30
  *
35
- * The hook model stores every effect's cleanup in `host.hooks[i].dispose`,
36
- * but {@link useEffect} only invokes it when dependencies change — never when
37
- * the element leaves the DOM. Left alone, every `useObservable` /
38
- * `useObservableValues` / `useEffect` subscription leaks on unmount and keeps
39
- * firing `requestUpdate` on a detached element. `useConnected` is likewise
40
- * inert because nothing dispatches the `"connected"` / `"disconnected"`
41
- * events it listens for.
31
+ * A hook host has two kinds of slot:
42
32
  *
43
- * This installs a single Lit {@link ReactiveController} — the one hook-friendly
44
- * path to a genuine `hostDisconnected` — that:
45
- * - on disconnect, *schedules* (on a microtask) dispatching `"disconnected"`,
46
- * disposing every hook slot, and clearing the cursor (full unmount semantics);
47
- * - on connect, if a teardown is still pending it CANCELS it (the disconnect
48
- * was half of a DOM move — hooks are intact, so nothing re-subscribes and no
49
- * re-render is forced); otherwise it dispatches `"connected"` and, on a true
50
- * reconnect, forces a re-render so the torn-down hooks re-initialize.
33
+ * - **Value / subscription slots** — {@link useState}, {@link useRef},
34
+ * {@link useMemo}, and effect-backed subscriptions ({@link useEffect},
35
+ * {@link useObservable}, {@link useObservableValues}). These are *stateful*
36
+ * and *expensive to rebuild*. They must SURVIVE a DOM move and be finalized
37
+ * only when the element is genuinely gone.
38
+ * - **Connectedness edges** — {@link useConnected}, the primitive for window /
39
+ * document listeners, portals, drag sessions: things that track raw DOM
40
+ * connectedness and must attach/detach *synchronously* on every edge.
51
41
  *
52
- * ## Why disposal is deferred
42
+ * The trap: the DOM fires `disconnectedCallback` then `connectedCallback`
43
+ * **synchronously, in the same task,** whenever a connected element is merely
44
+ * *moved* (a Lit `repeat` reorder, a list re-parenting, a drag-reorder). There
45
+ * is no synchronous signal that distinguishes a move from a genuine unmount, so
46
+ * this controller does not try to classify the edge. Instead it handles each
47
+ * concern with the policy that concern actually wants:
53
48
  *
54
- * The DOM fires `disconnectedCallback` then `connectedCallback` **synchronously,
55
- * in the same task,** whenever a connected element is merely *moved* — a Lit
56
- * `repeat` reorder, a list re-parenting, a drag-reorder. Disposing inline would
57
- * tear down and rebuild every subscription (re-running uncached producers) plus
58
- * force a re-render on every such move, with the hook state momentarily reset —
59
- * the studio-page lag, refresh churn, and transient empty lists on project
60
- * switch (FFP-111900). Deferring teardown one microtask lets a synchronous
61
- * reconnect distinguish a move (cancel) from a genuine unmount (let it run), so
62
- * a move is a no-op and only a real unmount disposes. A late reconnect (a
63
- * different task) is treated as unmount+remount, which is correct.
49
+ * - **Edges fire synchronously, every time.** `hostConnected` /
50
+ * `hostDisconnected` dispatch the `"connected"` / `"disconnected"` events
51
+ * inline on every raw edge, so {@link useConnected} tears down and re-attaches
52
+ * deterministically — including across a move (cheap, and exactly what a
53
+ * listener/portal wants).
54
+ * - **Slot finalization is deferred behind an `isConnected` re-check.** A
55
+ * disconnect schedules a microtask; when it runs it disposes the slots only
56
+ * if the host is STILL disconnected. A move has reconnected by then, so its
57
+ * subscription/value slots survive untouched — no re-subscribe, no state
58
+ * reset. A genuine unmount is still disconnected, so it finalizes, preserving
59
+ * the unmount-leak fix.
60
+ * - **Every RE-connect forces one `requestUpdate`.** The DOM does not re-render
61
+ * an element on a bare re-parent, so without this an element whose render
62
+ * reads mutable non-hook state (a plain field, an ancestor value) would show
63
+ * stale output after a move. The re-render is cheap: across a move the slots
64
+ * are preserved, so it does NOT re-subscribe or re-run producers — it is a Lit
65
+ * diff over unchanged hook state. Across a genuine remount the slots were
66
+ * finalized (cleared), so the same re-render re-initializes and re-subscribes
67
+ * them. (The perf win is not re-running producers on a move — never the cheap
68
+ * re-render.)
64
69
  *
65
70
  * It is installed lazily from inside the wrapped `render` (see {@link withHooks}),
66
71
  * which runs after `connectedCallback`, so `addController` fires `hostConnected`
@@ -77,42 +82,50 @@ export function installHooksController(host) {
77
82
  // addController fires hostConnected synchronously, and setting the flag first
78
83
  // guards against a re-entrant render re-installing a duplicate controller.
79
84
  host[HOOKS_CONTROLLER] = true;
85
+ // First connect is the initial mount (Lit renders it anyway); only a RE-connect
86
+ // needs a forced re-render.
80
87
  let connectedOnce = false;
81
- // True between a disconnect and its scheduled teardown. A reconnect that sees
82
- // this flag set is the tail of a move and cancels the teardown.
83
- let pendingDisposal = false;
88
+ // A finalization microtask is already queued; don't queue a second.
89
+ let finalizationQueued = false;
84
90
  const controller = {
85
91
  hostConnected() {
86
- if (pendingDisposal) {
87
- // Tail of a MOVE: the just-fired disconnect scheduled a teardown that
88
- // has not run yet. Cancel it — the hooks are untouched, so there is
89
- // nothing to re-subscribe and no reason to force a re-render.
90
- pendingDisposal = false;
91
- return;
92
- }
93
- // The "connected" / "disconnected" events are for EXTERNAL listeners only.
94
- // The internal useConnected does NOT depend on them: this fires before the
95
- // render body attaches any listener, so useConnected is driven by its direct
96
- // isConnected check plus useEffect cleanup instead.
92
+ // Fire the connect edge synchronously on EVERY connect (mount / move /
93
+ // remount) so edge hooks (useConnected) run their setup deterministically.
97
94
  host.dispatchEvent(new Event("connected"));
98
95
  if (connectedOnce) {
99
- // Reconnect: the previous disconnect disposed and cleared every
100
- // hook slot, so re-render to re-run hooks and re-subscribe.
96
+ // Re-render on EVERY reconnect — a move as well as a genuine remount.
97
+ // The subscription slots are PRESERVED across a move (finalization is
98
+ // gated on isConnected below), so this re-render does NOT re-subscribe
99
+ // or re-run producers — it is a cheap Lit diff. It is required for
100
+ // correctness: an element whose render reads mutable non-hook state
101
+ // (a plain field, an ancestor value) would otherwise show stale output
102
+ // after a move, since Lit does not re-render on a bare re-parent. On a
103
+ // genuine remount the slots were finalized (cleared), so this same
104
+ // re-render re-initializes and re-subscribes them.
101
105
  host.requestUpdate();
102
106
  }
103
107
  connectedOnce = true;
104
108
  },
105
109
  hostDisconnected() {
106
- // Defer teardown one microtask so a synchronous reconnect (a DOM move)
107
- // can cancel it above. Only a genuine unmount reaches the callback.
108
- pendingDisposal = true;
110
+ // Fire the disconnect edge synchronously on EVERY disconnect (unmount /
111
+ // move) so edge hooks tear down synchronously.
112
+ host.dispatchEvent(new Event("disconnected"));
113
+ // Defer slot finalization behind an isConnected re-check: a DOM move has
114
+ // reconnected by the time the microtask runs, so its value/subscription
115
+ // slots survive untouched; only a genuine unmount (still detached) is
116
+ // finalized. This is what keeps a move from churning subscriptions while
117
+ // still disposing them on a real unmount.
118
+ if (finalizationQueued) {
119
+ return;
120
+ }
121
+ finalizationQueued = true;
109
122
  queueMicrotask(() => {
110
- if (!pendingDisposal) {
123
+ finalizationQueued = false;
124
+ if (host.isConnected) {
125
+ // Moved / reconnected within the task — keep every slot alive.
111
126
  return;
112
127
  }
113
- pendingDisposal = false;
114
- host.dispatchEvent(new Event("disconnected"));
115
- disposeHooks(host);
128
+ finalizeHooks(host);
116
129
  });
117
130
  },
118
131
  };
@@ -1 +1 @@
1
- {"version":3,"file":"hooks-controller.js","sourceRoot":"","sources":["../../../src/hooks/component/hooks-controller.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAKvD;;;;GAIG;AACH,MAAM,gBAAgB,GAAG,MAAM,CAAC,0BAA0B,CAAC,CAAC;AAI5D,SAAS,cAAc,CAAC,IAAe;IACnC,OAAO,eAAe,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,aAAa,KAAK,UAAU,CAAC;AAC/E,CAAC;AAED;;;;;GAKG;AACH,SAAS,YAAY,CAAC,IAAe;IACjC,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IACvB,IAAI,KAAK,EAAE,CAAC;QACR,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACtB,IAA6C,EAAE,OAAO,EAAE,EAAE,CAAC;QAChE,CAAC;IACL,CAAC;IACD,iFAAiF;IACjF,mFAAmF;IACnF,iFAAiF;IACjF,gFAAgF;IAChF,8EAA8E;IAC9E,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;IAChB,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;AACvB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAe;IAClD,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAClD,OAAO;IACX,CAAC;IACD,0EAA0E;IAC1E,8EAA8E;IAC9E,2EAA2E;IAC3E,IAAI,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC;IAE9B,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,8EAA8E;IAC9E,gEAAgE;IAChE,IAAI,eAAe,GAAG,KAAK,CAAC;IAC5B,MAAM,UAAU,GAAuB;QACnC,aAAa;YACT,IAAI,eAAe,EAAE,CAAC;gBAClB,sEAAsE;gBACtE,oEAAoE;gBACpE,8DAA8D;gBAC9D,eAAe,GAAG,KAAK,CAAC;gBACxB,OAAO;YACX,CAAC;YACD,2EAA2E;YAC3E,2EAA2E;YAC3E,6EAA6E;YAC7E,oDAAoD;YACpD,IAAI,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;YAC3C,IAAI,aAAa,EAAE,CAAC;gBAChB,gEAAgE;gBAChE,4DAA4D;gBAC5D,IAAI,CAAC,aAAa,EAAE,CAAC;YACzB,CAAC;YACD,aAAa,GAAG,IAAI,CAAC;QACzB,CAAC;QACD,gBAAgB;YACZ,uEAAuE;YACvE,oEAAoE;YACpE,eAAe,GAAG,IAAI,CAAC;YACvB,cAAc,CAAC,GAAG,EAAE;gBAChB,IAAI,CAAC,eAAe,EAAE,CAAC;oBACnB,OAAO;gBACX,CAAC;gBACD,eAAe,GAAG,KAAK,CAAC;gBACxB,IAAI,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC;gBAC9C,YAAY,CAAC,IAAI,CAAC,CAAC;YACvB,CAAC,CAAC,CAAC;QACP,CAAC;KACJ,CAAC;IACF,IAAI,CAAC;QACD,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IACnC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,4EAA4E;QAC5E,2EAA2E;QAC3E,IAAI,CAAC,gBAAgB,CAAC,GAAG,SAAS,CAAC;QACnC,MAAM,KAAK,CAAC;IAChB,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"hooks-controller.js","sourceRoot":"","sources":["../../../src/hooks/component/hooks-controller.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAKvD;;;;GAIG;AACH,MAAM,gBAAgB,GAAG,MAAM,CAAC,0BAA0B,CAAC,CAAC;AAI5D,SAAS,cAAc,CAAC,IAAe;IACnC,OAAO,eAAe,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,aAAa,KAAK,UAAU,CAAC;AAC/E,CAAC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,IAAe;IAClC,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IACvB,IAAI,KAAK,EAAE,CAAC;QACR,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACtB,IAA6C,EAAE,OAAO,EAAE,EAAE,CAAC;QAChE,CAAC;IACL,CAAC;IACD,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;IAChB,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;AACvB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAe;IAClD,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAClD,OAAO;IACX,CAAC;IACD,0EAA0E;IAC1E,8EAA8E;IAC9E,2EAA2E;IAC3E,IAAI,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC;IAE9B,gFAAgF;IAChF,4BAA4B;IAC5B,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,oEAAoE;IACpE,IAAI,kBAAkB,GAAG,KAAK,CAAC;IAE/B,MAAM,UAAU,GAAuB;QACnC,aAAa;YACT,uEAAuE;YACvE,2EAA2E;YAC3E,IAAI,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;YAC3C,IAAI,aAAa,EAAE,CAAC;gBAChB,sEAAsE;gBACtE,sEAAsE;gBACtE,uEAAuE;gBACvE,mEAAmE;gBACnE,oEAAoE;gBACpE,uEAAuE;gBACvE,uEAAuE;gBACvE,mEAAmE;gBACnE,mDAAmD;gBACnD,IAAI,CAAC,aAAa,EAAE,CAAC;YACzB,CAAC;YACD,aAAa,GAAG,IAAI,CAAC;QACzB,CAAC;QACD,gBAAgB;YACZ,wEAAwE;YACxE,+CAA+C;YAC/C,IAAI,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC;YAC9C,yEAAyE;YACzE,wEAAwE;YACxE,sEAAsE;YACtE,yEAAyE;YACzE,0CAA0C;YAC1C,IAAI,kBAAkB,EAAE,CAAC;gBACrB,OAAO;YACX,CAAC;YACD,kBAAkB,GAAG,IAAI,CAAC;YAC1B,cAAc,CAAC,GAAG,EAAE;gBAChB,kBAAkB,GAAG,KAAK,CAAC;gBAC3B,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;oBACnB,+DAA+D;oBAC/D,OAAO;gBACX,CAAC;gBACD,aAAa,CAAC,IAAI,CAAC,CAAC;YACxB,CAAC,CAAC,CAAC;QACP,CAAC;KACJ,CAAC;IACF,IAAI,CAAC;QACD,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IACnC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,4EAA4E;QAC5E,2EAA2E;QAC3E,IAAI,CAAC,gBAAgB,CAAC,GAAG,SAAS,CAAC;QACnC,MAAM,KAAK,CAAC;IAChB,CAAC;AACL,CAAC"}
@@ -4,8 +4,10 @@ import { installHooksController } from "./hooks-controller.js";
4
4
  /**
5
5
  * Minimal stand-in for a Lit host: an EventTarget that captures the controller
6
6
  * installed on it so the test can drive `hostConnected` / `hostDisconnected`
7
- * directly (a real Lit element would need a DOM). `addController` mirrors Lit's
8
- * contract of firing `hostConnected` synchronously when already connected.
7
+ * directly (a real Lit element would need a DOM). `connect()` / `disconnect()`
8
+ * flip `isConnected` BEFORE dispatching, mirroring the DOM (the callback runs
9
+ * after connectedness has already changed) — the controller's deferred
10
+ * finalization reads `isConnected` to tell a move from a genuine unmount.
9
11
  */
10
12
  class FakeHost extends EventTarget {
11
13
  constructor() {
@@ -20,20 +22,21 @@ class FakeHost extends EventTarget {
20
22
  addController(controller) {
21
23
  this.controllers.push(controller);
22
24
  }
23
- // Simulate Lit dispatching lifecycle to every registered controller.
24
25
  connect() {
26
+ this.isConnected = true;
25
27
  for (const c of this.controllers)
26
28
  c.hostConnected?.();
27
29
  }
28
30
  disconnect() {
31
+ this.isConnected = false;
29
32
  for (const c of this.controllers)
30
33
  c.hostDisconnected?.();
31
34
  }
32
35
  }
33
- /** Resolves after the current microtask checkpoint, so deferred teardown can run. */
36
+ /** Resolves after the current microtask checkpoint, so deferred finalization can run. */
34
37
  const flushMicrotasks = () => new Promise(resolve => queueMicrotask(resolve));
35
38
  describe("installHooksController", () => {
36
- it("disposes every effect hook and resets the cursor on a genuine disconnect", async () => {
39
+ it("finalizes every effect hook and resets the cursor on a genuine disconnect", async () => {
37
40
  const host = new FakeHost();
38
41
  const disposeA = vi.fn();
39
42
  const disposeB = vi.fn();
@@ -46,52 +49,95 @@ describe("installHooksController", () => {
46
49
  host.hookIndex = 4;
47
50
  installHooksController(host);
48
51
  host.disconnect();
49
- // Teardown is deferred one microtask (so a move can cancel it); with no
50
- // reconnect this is a real unmount, so it runs on the next checkpoint.
52
+ // Finalization is deferred one microtask (so a move can cancel it); with no
53
+ // reconnect the host is still disconnected, so it runs on the next checkpoint.
51
54
  await flushMicrotasks();
52
55
  expect(disposeA).toHaveBeenCalledTimes(1);
53
56
  expect(disposeB).toHaveBeenCalledTimes(1);
54
57
  expect(host.hooks).toEqual([]);
55
58
  expect(host.hookIndex).toBe(0);
56
59
  });
57
- it("a synchronous disconnect→reconnect (a MOVE) cancels teardown and forces no re-render", async () => {
60
+ it("fires the connect / disconnect EDGE synchronously — no microtask needed", () => {
61
+ const host = new FakeHost();
62
+ const onConnected = vi.fn();
63
+ const onDisconnected = vi.fn();
64
+ host.addEventListener("connected", onConnected);
65
+ host.addEventListener("disconnected", onDisconnected);
66
+ installHooksController(host);
67
+ host.connect();
68
+ expect(onConnected).toHaveBeenCalledTimes(1);
69
+ host.disconnect();
70
+ // No flush: the edge is synchronous so connectedness hooks (useConnected)
71
+ // tear down inline. Under the old defer-everything controller this event
72
+ // only fired on a later microtask.
73
+ expect(onDisconnected).toHaveBeenCalledTimes(1);
74
+ });
75
+ it("across a MOVE: fires both edges synchronously, PRESERVES the slot, and re-renders on reconnect", async () => {
58
76
  const host = new FakeHost();
59
77
  const dispose = vi.fn();
60
78
  host.hooks = [{ dispose, dependencies: [] }];
61
79
  host.hookIndex = 1;
80
+ const edges = [];
81
+ host.addEventListener("disconnected", () => edges.push("disconnect"));
82
+ host.addEventListener("connected", () => edges.push("connect"));
62
83
  installHooksController(host);
63
- host.connect(); // first mount
84
+ host.connect(); // initial mount
64
85
  host.requestUpdate.mockClear();
65
- // Disconnect immediately followed by reconnect, same task — a DOM move.
86
+ edges.length = 0;
87
+ // Move: disconnect immediately followed by reconnect, same task.
66
88
  host.disconnect();
67
89
  host.connect();
90
+ expect(edges).toEqual(["disconnect", "connect"]); // both edges, synchronous, in order
68
91
  await flushMicrotasks();
69
- // Nothing was torn down and no re-render was forced: the hooks survived.
92
+ // The slot SURVIVED the move — not disposed, not re-created...
70
93
  expect(dispose).not.toHaveBeenCalled();
71
94
  expect(host.hooks).toHaveLength(1);
72
- expect(host.requestUpdate).not.toHaveBeenCalled();
95
+ // ...but the reconnect DID force one re-render. This is correctness, not churn:
96
+ // the preserved slot is not re-subscribed, so it is a cheap Lit diff — required
97
+ // so an element reading mutable non-hook state in render refreshes after a move.
98
+ expect(host.requestUpdate).toHaveBeenCalledTimes(1);
73
99
  });
74
- it("dispatches connected / disconnected events across the lifecycle", async () => {
100
+ it("a rapid disconnect→connect→disconnect bounce finalizes exactly once (net state wins)", async () => {
75
101
  const host = new FakeHost();
76
- const onConnected = vi.fn();
77
- const onDisconnected = vi.fn();
78
- host.addEventListener("connected", onConnected);
79
- host.addEventListener("disconnected", onDisconnected);
102
+ const dispose = vi.fn();
103
+ host.hooks = [{ dispose, dependencies: [] }];
80
104
  installHooksController(host);
105
+ // Bounce within one task: only one finalization microtask is queued, and it
106
+ // reads the final isConnected (false) → finalizes once, not per disconnect.
107
+ host.disconnect();
81
108
  host.connect();
82
- expect(onConnected).toHaveBeenCalledTimes(1);
83
109
  host.disconnect();
84
110
  await flushMicrotasks();
85
- expect(onDisconnected).toHaveBeenCalledTimes(1);
111
+ expect(dispose).toHaveBeenCalledTimes(1);
112
+ expect(host.hooks).toEqual([]);
113
+ });
114
+ it("gates finalization on isConnected — a slot that reconnects before the microtask survives", async () => {
115
+ const host = new FakeHost();
116
+ const dispose = vi.fn();
117
+ host.hooks = [{ dispose, dependencies: [] }];
118
+ installHooksController(host);
119
+ host.disconnect(); // schedules finalization; isConnected now false
120
+ host.connect(); // reconnects in the same task; isConnected back to true
121
+ await flushMicrotasks();
122
+ expect(dispose).not.toHaveBeenCalled();
123
+ expect(host.hooks).toHaveLength(1);
86
124
  });
87
- it("forces a re-render only on a genuine RE-connect, not the first connect", async () => {
125
+ it("forces a re-render on EVERY reconnect — a move and a genuine remount — but not the first connect", async () => {
88
126
  const host = new FakeHost();
89
127
  installHooksController(host);
90
- host.connect(); // first mount
128
+ host.connect(); // first mount — Lit renders it anyway, no forced update
91
129
  expect(host.requestUpdate).not.toHaveBeenCalled();
130
+ // A MOVE (reconnect with slots preserved) forces a re-render.
92
131
  host.disconnect();
93
- await flushMicrotasks(); // genuine unmount: teardown runs
94
- host.connect(); // reconnect re-initializes the torn-down hooks
132
+ host.connect();
133
+ await flushMicrotasks(); // finalization skipped (reconnected in the same task)
134
+ expect(host.requestUpdate).toHaveBeenCalledTimes(1);
135
+ // A genuine unmount+remount (slots finalized) also forces a re-render, which
136
+ // re-initializes and re-subscribes the cleared slots.
137
+ host.requestUpdate.mockClear();
138
+ host.disconnect();
139
+ await flushMicrotasks(); // genuine unmount: finalization runs
140
+ host.connect();
95
141
  expect(host.requestUpdate).toHaveBeenCalledTimes(1);
96
142
  });
97
143
  it("is idempotent — repeated installs register a single controller", () => {
@@ -1 +1 @@
1
- {"version":3,"file":"hooks-controller.test.js","sourceRoot":"","sources":["../../../src/hooks/component/hooks-controller.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hooks-controller.test.js","sourceRoot":"","sources":["../../../src/hooks/component/hooks-controller.test.ts"],"names":[],"mappings":"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"}