@adobe/data-lit 0.10.14 → 0.10.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/component/hooks-controller.d.ts +31 -27
- package/dist/hooks/component/hooks-controller.js +64 -63
- package/dist/hooks/component/hooks-controller.js.map +1 -1
- package/dist/hooks/component/hooks-controller.test.js +54 -21
- package/dist/hooks/component/hooks-controller.test.js.map +1 -1
- package/dist/hooks/component/hooks-lifecycle.integration.test.d.ts +1 -0
- package/dist/hooks/component/hooks-lifecycle.integration.test.js +244 -0
- package/dist/hooks/component/hooks-lifecycle.integration.test.js.map +1 -0
- package/dist/hooks/use-connected.d.ts +17 -0
- package/dist/hooks/use-connected.js +44 -16
- package/dist/hooks/use-connected.js.map +1 -1
- package/package.json +2 -2
- package/src/hooks/component/hooks-controller.test.ts +62 -23
- package/src/hooks/component/hooks-controller.ts +65 -63
- package/src/hooks/component/hooks-lifecycle.integration.test.ts +279 -0
- package/src/hooks/use-connected.ts +47 -21
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
// @vitest-environment happy-dom
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
import { LitElement, html } from "lit";
|
|
5
|
+
import { attachDecorator } from "../attach-decorator.js";
|
|
6
|
+
import { withHooks } from "../with-hooks.js";
|
|
7
|
+
import { useObservable } from "../use-observable.js";
|
|
8
|
+
import { useState } from "../use-state.js";
|
|
9
|
+
import { useConnected } from "../use-connected.js";
|
|
10
|
+
import { useEffect } from "../use-effect.js";
|
|
11
|
+
/**
|
|
12
|
+
* Real-DOM regression tests for the two-slot hook lifecycle:
|
|
13
|
+
*
|
|
14
|
+
* - Value/subscription slots (useState, useObservable) SURVIVE a DOM move and
|
|
15
|
+
* are finalized only on a genuine unmount (deferred `isConnected` re-check).
|
|
16
|
+
* - Connectedness edges (useConnected) fire SYNCHRONOUSLY on every raw
|
|
17
|
+
* connect/disconnect edge — including a move — so listener/portal/drag setup
|
|
18
|
+
* tears down and re-attaches deterministically, with no deferral.
|
|
19
|
+
*
|
|
20
|
+
* These pin the contract that neither the 0.10.11 nuke-on-disconnect nor the
|
|
21
|
+
* 0.10.14 defer-everything controller could satisfy simultaneously.
|
|
22
|
+
*/
|
|
23
|
+
const flushMicrotasks = () => new Promise(resolve => queueMicrotask(resolve));
|
|
24
|
+
/** An Observe whose subscribe/unsubscribe churn the test can inspect. */
|
|
25
|
+
function createTrackedObservable(initial = 0) {
|
|
26
|
+
const subscribers = new Set();
|
|
27
|
+
let subscribes = 0;
|
|
28
|
+
let unsubscribes = 0;
|
|
29
|
+
const observable = observer => {
|
|
30
|
+
subscribes++;
|
|
31
|
+
subscribers.add(observer);
|
|
32
|
+
observer(initial);
|
|
33
|
+
return () => {
|
|
34
|
+
unsubscribes++;
|
|
35
|
+
subscribers.delete(observer);
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
observable,
|
|
40
|
+
subscriberCount: () => subscribers.size,
|
|
41
|
+
totalSubscribes: () => subscribes,
|
|
42
|
+
totalUnsubscribes: () => unsubscribes,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Module-scoped probes the element writes into, so a test can read edge/render
|
|
46
|
+
// history without reaching into the element instance.
|
|
47
|
+
let connectEdges = [];
|
|
48
|
+
let disconnectEdges = [];
|
|
49
|
+
let openResources = 0;
|
|
50
|
+
let effectSetups = 0;
|
|
51
|
+
let effectCleanups = 0;
|
|
52
|
+
let renderCount = 0;
|
|
53
|
+
let observableUnderTest;
|
|
54
|
+
class LifecycleProbeElement extends LitElement {
|
|
55
|
+
constructor() {
|
|
56
|
+
super();
|
|
57
|
+
attachDecorator(this, "render", withHooks);
|
|
58
|
+
}
|
|
59
|
+
render() {
|
|
60
|
+
renderCount++;
|
|
61
|
+
// Edge primitive: must fire synchronously on every connect/disconnect edge,
|
|
62
|
+
// and open exactly one resource per connect (balanced by its teardown).
|
|
63
|
+
useConnected(() => {
|
|
64
|
+
connectEdges.push("connect");
|
|
65
|
+
openResources++;
|
|
66
|
+
return () => {
|
|
67
|
+
disconnectEdges.push("disconnect");
|
|
68
|
+
openResources--;
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
// Plain effect slot: React-parity — cleanup on finalization/dep-change,
|
|
72
|
+
// never on a bare disconnect (so it survives a move too).
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
effectSetups++;
|
|
75
|
+
return () => {
|
|
76
|
+
effectCleanups++;
|
|
77
|
+
};
|
|
78
|
+
}, []);
|
|
79
|
+
// Value slot: must survive a move (retain its number across re-parenting).
|
|
80
|
+
const [n] = useState(() => 41);
|
|
81
|
+
// Subscription slot: must survive a move (no unsubscribe/re-subscribe).
|
|
82
|
+
const value = useObservable(observableUnderTest);
|
|
83
|
+
return html `<span>${n}:${value ?? ""}</span>`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
customElements.define("lifecycle-probe-element", LifecycleProbeElement);
|
|
87
|
+
function resetProbes(obs) {
|
|
88
|
+
connectEdges = [];
|
|
89
|
+
disconnectEdges = [];
|
|
90
|
+
openResources = 0;
|
|
91
|
+
effectSetups = 0;
|
|
92
|
+
effectCleanups = 0;
|
|
93
|
+
renderCount = 0;
|
|
94
|
+
observableUnderTest = obs;
|
|
95
|
+
}
|
|
96
|
+
describe("two-slot hook lifecycle — real DOM", () => {
|
|
97
|
+
it("useConnected fires its disconnect edge SYNCHRONOUSLY on a genuine unmount", async () => {
|
|
98
|
+
const { observable } = createTrackedObservable();
|
|
99
|
+
resetProbes(observable);
|
|
100
|
+
const el = document.createElement("lifecycle-probe-element");
|
|
101
|
+
document.body.appendChild(el);
|
|
102
|
+
await el.updateComplete;
|
|
103
|
+
expect(connectEdges).toEqual(["connect"]);
|
|
104
|
+
expect(disconnectEdges).toEqual([]);
|
|
105
|
+
el.remove();
|
|
106
|
+
// The edge is synchronous: no microtask flush before this assertion.
|
|
107
|
+
expect(disconnectEdges).toEqual(["disconnect"]);
|
|
108
|
+
});
|
|
109
|
+
it("useConnected runs setup ONCE across ordinary re-renders — no per-render leak", async () => {
|
|
110
|
+
const { observable } = createTrackedObservable();
|
|
111
|
+
resetProbes(observable);
|
|
112
|
+
const el = document.createElement("lifecycle-probe-element");
|
|
113
|
+
document.body.appendChild(el);
|
|
114
|
+
await el.updateComplete;
|
|
115
|
+
expect(connectEdges).toEqual(["connect"]);
|
|
116
|
+
expect(openResources).toBe(1);
|
|
117
|
+
// Ordinary re-renders (no connect/disconnect edge) re-run the hook body but
|
|
118
|
+
// must NOT re-invoke setup — the teardown handle persists across renders.
|
|
119
|
+
el.requestUpdate();
|
|
120
|
+
await el.updateComplete;
|
|
121
|
+
el.requestUpdate();
|
|
122
|
+
await el.updateComplete;
|
|
123
|
+
expect(connectEdges).toEqual(["connect"]);
|
|
124
|
+
expect(openResources).toBe(1);
|
|
125
|
+
// Unmount tears down exactly the one open resource — nothing leaked.
|
|
126
|
+
el.remove();
|
|
127
|
+
expect(disconnectEdges).toEqual(["disconnect"]);
|
|
128
|
+
expect(openResources).toBe(0);
|
|
129
|
+
});
|
|
130
|
+
it("useConnected churns disconnect→connect SYNCHRONOUSLY on a MOVE (re-parent)", async () => {
|
|
131
|
+
const { observable } = createTrackedObservable();
|
|
132
|
+
resetProbes(observable);
|
|
133
|
+
const parentA = document.createElement("div");
|
|
134
|
+
const parentB = document.createElement("div");
|
|
135
|
+
document.body.append(parentA, parentB);
|
|
136
|
+
const el = document.createElement("lifecycle-probe-element");
|
|
137
|
+
parentA.appendChild(el);
|
|
138
|
+
await el.updateComplete;
|
|
139
|
+
expect(connectEdges).toEqual(["connect"]);
|
|
140
|
+
// MOVE: disconnectedCallback then connectedCallback fire synchronously.
|
|
141
|
+
parentB.appendChild(el);
|
|
142
|
+
expect(el.isConnected).toBe(true);
|
|
143
|
+
// The edge primitive tore down and re-attached, synchronously and in order.
|
|
144
|
+
expect(disconnectEdges).toEqual(["disconnect"]);
|
|
145
|
+
expect(connectEdges).toEqual(["connect", "connect"]);
|
|
146
|
+
});
|
|
147
|
+
it("value + subscription slots SURVIVE a MOVE (state kept, no re-subscribe, no forced render)", async () => {
|
|
148
|
+
const { observable, subscriberCount, totalSubscribes, totalUnsubscribes } = createTrackedObservable();
|
|
149
|
+
resetProbes(observable);
|
|
150
|
+
const parentA = document.createElement("div");
|
|
151
|
+
const parentB = document.createElement("div");
|
|
152
|
+
document.body.append(parentA, parentB);
|
|
153
|
+
const el = document.createElement("lifecycle-probe-element");
|
|
154
|
+
parentA.appendChild(el);
|
|
155
|
+
await el.updateComplete;
|
|
156
|
+
expect(totalSubscribes()).toBe(1);
|
|
157
|
+
const rendersBeforeMove = renderCount;
|
|
158
|
+
parentB.appendChild(el);
|
|
159
|
+
await flushMicrotasks();
|
|
160
|
+
await el.updateComplete;
|
|
161
|
+
// Subscription untouched; useState value preserved; no re-render forced.
|
|
162
|
+
expect(subscriberCount()).toBe(1);
|
|
163
|
+
expect(totalSubscribes()).toBe(1);
|
|
164
|
+
expect(totalUnsubscribes()).toBe(0);
|
|
165
|
+
expect(renderCount).toBe(rendersBeforeMove);
|
|
166
|
+
});
|
|
167
|
+
it("finalizes on a genuine unmount — subscription disposed, no leak", async () => {
|
|
168
|
+
const { observable, subscriberCount, totalUnsubscribes } = createTrackedObservable();
|
|
169
|
+
resetProbes(observable);
|
|
170
|
+
const el = document.createElement("lifecycle-probe-element");
|
|
171
|
+
document.body.appendChild(el);
|
|
172
|
+
await el.updateComplete;
|
|
173
|
+
expect(subscriberCount()).toBe(1);
|
|
174
|
+
el.remove();
|
|
175
|
+
await flushMicrotasks();
|
|
176
|
+
expect(subscriberCount()).toBe(0);
|
|
177
|
+
expect(totalUnsubscribes()).toBe(1);
|
|
178
|
+
});
|
|
179
|
+
it("a genuine unmount→remount (across a task) re-initializes and re-subscribes the slots", async () => {
|
|
180
|
+
const { observable, subscriberCount, totalSubscribes } = createTrackedObservable();
|
|
181
|
+
resetProbes(observable);
|
|
182
|
+
const el = document.createElement("lifecycle-probe-element");
|
|
183
|
+
document.body.appendChild(el);
|
|
184
|
+
await el.updateComplete;
|
|
185
|
+
expect(totalSubscribes()).toBe(1);
|
|
186
|
+
// Unmount, let finalization run (task boundary), then remount.
|
|
187
|
+
el.remove();
|
|
188
|
+
await flushMicrotasks();
|
|
189
|
+
expect(subscriberCount()).toBe(0);
|
|
190
|
+
document.body.appendChild(el);
|
|
191
|
+
await el.updateComplete;
|
|
192
|
+
// Slots were finalized while detached, so the remount re-subscribed.
|
|
193
|
+
expect(subscriberCount()).toBe(1);
|
|
194
|
+
expect(totalSubscribes()).toBe(2);
|
|
195
|
+
// And the edge primitive reconnected.
|
|
196
|
+
expect(connectEdges.filter(e => e === "connect").length).toBe(2);
|
|
197
|
+
});
|
|
198
|
+
it("a genuine unmount→remount to a DIFFERENT parent (across a task) re-initializes and re-subscribes", async () => {
|
|
199
|
+
const { observable, subscriberCount, totalSubscribes } = createTrackedObservable();
|
|
200
|
+
resetProbes(observable);
|
|
201
|
+
const parentA = document.createElement("div");
|
|
202
|
+
const parentB = document.createElement("div");
|
|
203
|
+
document.body.append(parentA, parentB);
|
|
204
|
+
const el = document.createElement("lifecycle-probe-element");
|
|
205
|
+
parentA.appendChild(el);
|
|
206
|
+
await el.updateComplete;
|
|
207
|
+
expect(totalSubscribes()).toBe(1);
|
|
208
|
+
// Unmount from A, let finalization run, then remount under a DIFFERENT parent.
|
|
209
|
+
// The finalization gate reads `isConnected` (not "did it reconnect to the same
|
|
210
|
+
// node"), so a remount anywhere is handled uniformly.
|
|
211
|
+
el.remove();
|
|
212
|
+
await flushMicrotasks();
|
|
213
|
+
expect(subscriberCount()).toBe(0);
|
|
214
|
+
parentB.appendChild(el);
|
|
215
|
+
await el.updateComplete;
|
|
216
|
+
expect(subscriberCount()).toBe(1);
|
|
217
|
+
expect(totalSubscribes()).toBe(2);
|
|
218
|
+
expect(connectEdges.filter(e => e === "connect").length).toBe(2);
|
|
219
|
+
});
|
|
220
|
+
it("plain useEffect cleanup does NOT run on a MOVE, and runs (deferred) on a genuine unmount", async () => {
|
|
221
|
+
const { observable } = createTrackedObservable();
|
|
222
|
+
resetProbes(observable);
|
|
223
|
+
const parentA = document.createElement("div");
|
|
224
|
+
const parentB = document.createElement("div");
|
|
225
|
+
document.body.append(parentA, parentB);
|
|
226
|
+
const el = document.createElement("lifecycle-probe-element");
|
|
227
|
+
parentA.appendChild(el);
|
|
228
|
+
await el.updateComplete;
|
|
229
|
+
expect(effectSetups).toBe(1);
|
|
230
|
+
expect(effectCleanups).toBe(0);
|
|
231
|
+
// MOVE: the effect slot survives untouched — no cleanup, no re-setup.
|
|
232
|
+
parentB.appendChild(el);
|
|
233
|
+
await flushMicrotasks();
|
|
234
|
+
await el.updateComplete;
|
|
235
|
+
expect(effectSetups).toBe(1);
|
|
236
|
+
expect(effectCleanups).toBe(0);
|
|
237
|
+
// UNMOUNT: cleanup runs on the finalization microtask, not synchronously.
|
|
238
|
+
el.remove();
|
|
239
|
+
expect(effectCleanups).toBe(0);
|
|
240
|
+
await flushMicrotasks();
|
|
241
|
+
expect(effectCleanups).toBe(1);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
//# sourceMappingURL=hooks-lifecycle.integration.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks-lifecycle.integration.test.js","sourceRoot":"","sources":["../../../src/hooks/component/hooks-lifecycle.integration.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,2 +1,19 @@
|
|
|
1
1
|
import type { EffectCallback } from "./use-effect.js";
|
|
2
|
+
/**
|
|
3
|
+
* Run `callback` on every DOM connect edge and its returned teardown on every
|
|
4
|
+
* disconnect edge — the connectedness primitive for `window` / `document`
|
|
5
|
+
* listeners, portals, and drag sessions that must attach and detach
|
|
6
|
+
* *synchronously* as the host enters and leaves the DOM.
|
|
7
|
+
*
|
|
8
|
+
* The hooks controller (see `installHooksController`) dispatches the
|
|
9
|
+
* `"connected"` / `"disconnected"` events synchronously on every raw edge,
|
|
10
|
+
* including a DOM move, so a move tears the setup down and re-attaches it
|
|
11
|
+
* deterministically.
|
|
12
|
+
*
|
|
13
|
+
* Setup runs exactly ONCE per connect. The teardown handle lives in a ref that
|
|
14
|
+
* persists across renders, so ordinary re-renders (which re-run this hook body)
|
|
15
|
+
* neither re-invoke setup nor leak — the setup/teardown pair is driven only from
|
|
16
|
+
* the edge, never from the render pass. The callback is read live through a ref,
|
|
17
|
+
* so callers may pass a fresh closure on every render.
|
|
18
|
+
*/
|
|
2
19
|
export declare function useConnected(callback: EffectCallback, dependencies?: unknown[]): void;
|
|
@@ -1,25 +1,53 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import { useEffect } from "./use-effect.js";
|
|
3
|
+
import { useRef } from "./use-ref.js";
|
|
3
4
|
import { Component_stack } from "./component/stack.js";
|
|
5
|
+
/**
|
|
6
|
+
* Run `callback` on every DOM connect edge and its returned teardown on every
|
|
7
|
+
* disconnect edge — the connectedness primitive for `window` / `document`
|
|
8
|
+
* listeners, portals, and drag sessions that must attach and detach
|
|
9
|
+
* *synchronously* as the host enters and leaves the DOM.
|
|
10
|
+
*
|
|
11
|
+
* The hooks controller (see `installHooksController`) dispatches the
|
|
12
|
+
* `"connected"` / `"disconnected"` events synchronously on every raw edge,
|
|
13
|
+
* including a DOM move, so a move tears the setup down and re-attaches it
|
|
14
|
+
* deterministically.
|
|
15
|
+
*
|
|
16
|
+
* Setup runs exactly ONCE per connect. The teardown handle lives in a ref that
|
|
17
|
+
* persists across renders, so ordinary re-renders (which re-run this hook body)
|
|
18
|
+
* neither re-invoke setup nor leak — the setup/teardown pair is driven only from
|
|
19
|
+
* the edge, never from the render pass. The callback is read live through a ref,
|
|
20
|
+
* so callers may pass a fresh closure on every render.
|
|
21
|
+
*/
|
|
4
22
|
export function useConnected(callback, dependencies) {
|
|
5
23
|
const component = Component_stack.active();
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
disconnect = undefined;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
// TODO
|
|
19
|
-
if (component.isConnected) {
|
|
20
|
-
onConnect();
|
|
21
|
-
}
|
|
24
|
+
// Live-read the latest callback so a fresh closure each render is honored
|
|
25
|
+
// without re-registering the edge listeners.
|
|
26
|
+
const callbackRef = useRef(callback);
|
|
27
|
+
callbackRef.current = callback;
|
|
28
|
+
// Persistent teardown handle: the cleanup returned by `callback` while
|
|
29
|
+
// connected, or `undefined` while disconnected. A ref (not a per-render
|
|
30
|
+
// local) so the "already connected" guard holds across renders — this is
|
|
31
|
+
// what makes setup idempotent per connect instead of per render.
|
|
32
|
+
const state = useRef({ disconnect: undefined });
|
|
22
33
|
useEffect(() => {
|
|
34
|
+
const onConnect = () => {
|
|
35
|
+
if (!state.current.disconnect) {
|
|
36
|
+
state.current.disconnect = callbackRef.current();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const onDisconnect = () => {
|
|
40
|
+
if (state.current.disconnect) {
|
|
41
|
+
state.current.disconnect();
|
|
42
|
+
state.current.disconnect = undefined;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
// On first mount the controller dispatches "connected" during
|
|
46
|
+
// `addController`, before this effect attaches its listener, so run setup
|
|
47
|
+
// now if the host is already connected.
|
|
48
|
+
if (component.isConnected) {
|
|
49
|
+
onConnect();
|
|
50
|
+
}
|
|
23
51
|
component.addEventListener("connected", onConnect);
|
|
24
52
|
component.addEventListener("disconnected", onDisconnect);
|
|
25
53
|
return () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-connected.js","sourceRoot":"","sources":["../../src/hooks/use-connected.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAGvD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,UAAU,YAAY,CAAC,QAAwB,EAAE,YAAwB;IAC3E,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,EAAE,CAAC;IAE3C,
|
|
1
|
+
{"version":3,"file":"use-connected.js","sourceRoot":"","sources":["../../src/hooks/use-connected.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAGvD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,YAAY,CAAC,QAAwB,EAAE,YAAwB;IAC3E,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,EAAE,CAAC;IAE3C,0EAA0E;IAC1E,6CAA6C;IAC7C,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrC,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAC;IAE/B,uEAAuE;IACvE,wEAAwE;IACxE,yEAAyE;IACzE,iEAAiE;IACjE,MAAM,KAAK,GAAG,MAAM,CAAsC,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAC;IAErF,SAAS,CAAC,GAAG,EAAE;QACX,MAAM,SAAS,GAAG,GAAG,EAAE;YACnB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;gBAC5B,KAAK,CAAC,OAAO,CAAC,UAAU,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;YACrD,CAAC;QACL,CAAC,CAAC;QACF,MAAM,YAAY,GAAG,GAAG,EAAE;YACtB,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;gBAC3B,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;gBAC3B,KAAK,CAAC,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;YACzC,CAAC;QACL,CAAC,CAAC;QAEF,8DAA8D;QAC9D,0EAA0E;QAC1E,wCAAwC;QACxC,IAAI,SAAS,CAAC,WAAW,EAAE,CAAC;YACxB,SAAS,EAAE,CAAC;QAChB,CAAC;QACD,SAAS,CAAC,gBAAgB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;QACnD,SAAS,CAAC,gBAAgB,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;QAEzD,OAAO,GAAG,EAAE;YACR,SAAS,CAAC,mBAAmB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;YACtD,SAAS,CAAC,mBAAmB,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;YAC5D,YAAY,EAAE,CAAC;QACnB,CAAC,CAAC;IACN,CAAC,EAAE,YAAY,CAAC,CAAC;AACrB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/data-lit",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.15",
|
|
4
4
|
"description": "Adobe data Lit bindings - hooks, elements, decorators",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@adobe/data": "0.10.
|
|
15
|
+
"@adobe/data": "0.10.15"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"lit": "^3.3.1"
|
|
@@ -7,8 +7,10 @@ import { installHooksController } from "./hooks-controller.js";
|
|
|
7
7
|
/**
|
|
8
8
|
* Minimal stand-in for a Lit host: an EventTarget that captures the controller
|
|
9
9
|
* installed on it so the test can drive `hostConnected` / `hostDisconnected`
|
|
10
|
-
* directly (a real Lit element would need a DOM). `
|
|
11
|
-
*
|
|
10
|
+
* directly (a real Lit element would need a DOM). `connect()` / `disconnect()`
|
|
11
|
+
* flip `isConnected` BEFORE dispatching, mirroring the DOM (the callback runs
|
|
12
|
+
* after connectedness has already changed) — the controller's deferred
|
|
13
|
+
* finalization reads `isConnected` to tell a move from a genuine unmount.
|
|
12
14
|
*/
|
|
13
15
|
class FakeHost extends EventTarget implements Component {
|
|
14
16
|
isConnected = true;
|
|
@@ -21,20 +23,21 @@ class FakeHost extends EventTarget implements Component {
|
|
|
21
23
|
addController(controller: ReactiveController) {
|
|
22
24
|
this.controllers.push(controller);
|
|
23
25
|
}
|
|
24
|
-
// Simulate Lit dispatching lifecycle to every registered controller.
|
|
25
26
|
connect() {
|
|
27
|
+
this.isConnected = true;
|
|
26
28
|
for (const c of this.controllers) c.hostConnected?.();
|
|
27
29
|
}
|
|
28
30
|
disconnect() {
|
|
31
|
+
this.isConnected = false;
|
|
29
32
|
for (const c of this.controllers) c.hostDisconnected?.();
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
/** Resolves after the current microtask checkpoint, so deferred
|
|
36
|
+
/** Resolves after the current microtask checkpoint, so deferred finalization can run. */
|
|
34
37
|
const flushMicrotasks = (): Promise<void> => new Promise(resolve => queueMicrotask(resolve));
|
|
35
38
|
|
|
36
39
|
describe("installHooksController", () => {
|
|
37
|
-
it("
|
|
40
|
+
it("finalizes every effect hook and resets the cursor on a genuine disconnect", async () => {
|
|
38
41
|
const host = new FakeHost();
|
|
39
42
|
const disposeA = vi.fn();
|
|
40
43
|
const disposeB = vi.fn();
|
|
@@ -48,8 +51,8 @@ describe("installHooksController", () => {
|
|
|
48
51
|
|
|
49
52
|
installHooksController(host);
|
|
50
53
|
host.disconnect();
|
|
51
|
-
//
|
|
52
|
-
// reconnect
|
|
54
|
+
// Finalization is deferred one microtask (so a move can cancel it); with no
|
|
55
|
+
// reconnect the host is still disconnected, so it runs on the next checkpoint.
|
|
53
56
|
await flushMicrotasks();
|
|
54
57
|
|
|
55
58
|
expect(disposeA).toHaveBeenCalledTimes(1);
|
|
@@ -58,44 +61,80 @@ describe("installHooksController", () => {
|
|
|
58
61
|
expect(host.hookIndex).toBe(0);
|
|
59
62
|
});
|
|
60
63
|
|
|
61
|
-
it("
|
|
64
|
+
it("fires the connect / disconnect EDGE synchronously — no microtask needed", () => {
|
|
65
|
+
const host = new FakeHost();
|
|
66
|
+
const onConnected = vi.fn();
|
|
67
|
+
const onDisconnected = vi.fn();
|
|
68
|
+
host.addEventListener("connected", onConnected);
|
|
69
|
+
host.addEventListener("disconnected", onDisconnected);
|
|
70
|
+
|
|
71
|
+
installHooksController(host);
|
|
72
|
+
host.connect();
|
|
73
|
+
expect(onConnected).toHaveBeenCalledTimes(1);
|
|
74
|
+
|
|
75
|
+
host.disconnect();
|
|
76
|
+
// No flush: the edge is synchronous so connectedness hooks (useConnected)
|
|
77
|
+
// tear down inline. Under the old defer-everything controller this event
|
|
78
|
+
// only fired on a later microtask.
|
|
79
|
+
expect(onDisconnected).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("fires disconnect THEN connect synchronously across a MOVE, and does NOT finalize", async () => {
|
|
62
83
|
const host = new FakeHost();
|
|
63
84
|
const dispose = vi.fn();
|
|
64
85
|
host.hooks = [{ dispose, dependencies: [] }];
|
|
65
86
|
host.hookIndex = 1;
|
|
87
|
+
const edges: string[] = [];
|
|
88
|
+
host.addEventListener("disconnected", () => edges.push("disconnect"));
|
|
89
|
+
host.addEventListener("connected", () => edges.push("connect"));
|
|
66
90
|
|
|
67
91
|
installHooksController(host);
|
|
68
|
-
host.connect(); // first mount
|
|
69
92
|
host.requestUpdate.mockClear();
|
|
70
93
|
|
|
71
|
-
//
|
|
94
|
+
// Move: disconnect immediately followed by reconnect, same task.
|
|
72
95
|
host.disconnect();
|
|
73
96
|
host.connect();
|
|
74
|
-
|
|
97
|
+
expect(edges).toEqual(["disconnect", "connect"]); // both edges, synchronous, in order
|
|
75
98
|
|
|
76
|
-
|
|
99
|
+
await flushMicrotasks();
|
|
100
|
+
// The slot survived: not disposed, and no re-render was forced.
|
|
77
101
|
expect(dispose).not.toHaveBeenCalled();
|
|
78
102
|
expect(host.hooks).toHaveLength(1);
|
|
79
103
|
expect(host.requestUpdate).not.toHaveBeenCalled();
|
|
80
104
|
});
|
|
81
105
|
|
|
82
|
-
it("
|
|
106
|
+
it("a rapid disconnect→connect→disconnect bounce finalizes exactly once (net state wins)", async () => {
|
|
83
107
|
const host = new FakeHost();
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
host.addEventListener("connected", onConnected);
|
|
87
|
-
host.addEventListener("disconnected", onDisconnected);
|
|
108
|
+
const dispose = vi.fn();
|
|
109
|
+
host.hooks = [{ dispose, dependencies: [] }];
|
|
88
110
|
|
|
89
111
|
installHooksController(host);
|
|
112
|
+
// Bounce within one task: only one finalization microtask is queued, and it
|
|
113
|
+
// reads the final isConnected (false) → finalizes once, not per disconnect.
|
|
114
|
+
host.disconnect();
|
|
90
115
|
host.connect();
|
|
91
|
-
expect(onConnected).toHaveBeenCalledTimes(1);
|
|
92
|
-
|
|
93
116
|
host.disconnect();
|
|
94
117
|
await flushMicrotasks();
|
|
95
|
-
|
|
118
|
+
|
|
119
|
+
expect(dispose).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(host.hooks).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("gates finalization on isConnected — a slot that reconnects before the microtask survives", async () => {
|
|
124
|
+
const host = new FakeHost();
|
|
125
|
+
const dispose = vi.fn();
|
|
126
|
+
host.hooks = [{ dispose, dependencies: [] }];
|
|
127
|
+
|
|
128
|
+
installHooksController(host);
|
|
129
|
+
host.disconnect(); // schedules finalization; isConnected now false
|
|
130
|
+
host.connect(); // reconnects in the same task; isConnected back to true
|
|
131
|
+
await flushMicrotasks();
|
|
132
|
+
|
|
133
|
+
expect(dispose).not.toHaveBeenCalled();
|
|
134
|
+
expect(host.hooks).toHaveLength(1);
|
|
96
135
|
});
|
|
97
136
|
|
|
98
|
-
it("forces a re-render only on a genuine RE-connect, not the first connect", async () => {
|
|
137
|
+
it("forces a re-render only on a genuine RE-connect, not the first connect or a move", async () => {
|
|
99
138
|
const host = new FakeHost();
|
|
100
139
|
installHooksController(host);
|
|
101
140
|
|
|
@@ -103,8 +142,8 @@ describe("installHooksController", () => {
|
|
|
103
142
|
expect(host.requestUpdate).not.toHaveBeenCalled();
|
|
104
143
|
|
|
105
144
|
host.disconnect();
|
|
106
|
-
await flushMicrotasks(); // genuine unmount:
|
|
107
|
-
host.connect(); // reconnect re-
|
|
145
|
+
await flushMicrotasks(); // genuine unmount: finalization runs
|
|
146
|
+
host.connect(); // reconnect must re-initialize the finalized hooks
|
|
108
147
|
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
|
|
109
148
|
});
|
|
110
149
|
|