@drip-apex/sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/bundle-report.json +584 -226
- package/dist/define.cjs +123 -0
- package/dist/define.cjs.map +7 -0
- package/dist/define.mjs +102 -0
- package/dist/define.mjs.map +7 -0
- package/dist/drip.cjs +19262 -0
- package/dist/drip.cjs.map +7 -0
- package/dist/drip.debug.js +1 -1
- package/dist/drip.debug.js.map +1 -1
- package/dist/drip.heatmap.js +1 -1
- package/dist/drip.heatmap.js.map +1 -1
- package/dist/drip.js +1 -1
- package/dist/drip.js.map +1 -1
- package/dist/drip.live.js +1 -1
- package/dist/drip.live.js.map +1 -1
- package/dist/drip.mjs +19246 -0
- package/dist/drip.mjs.map +7 -0
- package/dist/drip.replay-recorder.js +2 -0
- package/dist/drip.replay-recorder.js.map +1 -0
- package/dist/editor.js +147 -142
- package/dist/editor.js.map +4 -4
- package/dist/flags.cjs +1788 -0
- package/dist/flags.cjs.map +7 -0
- package/dist/flags.mjs +1765 -0
- package/dist/flags.mjs.map +7 -0
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +2 -2
- package/dist/metafiles/define.cjs.meta.json +28 -0
- package/dist/metafiles/define.mjs.meta.json +32 -0
- package/dist/metafiles/drip.cjs.meta.json +3193 -0
- package/dist/metafiles/drip.debug.meta.json +2859 -574
- package/dist/metafiles/drip.heatmap.meta.json +2732 -460
- package/dist/metafiles/drip.live.meta.json +2732 -460
- package/dist/metafiles/drip.meta.json +2859 -574
- package/dist/metafiles/drip.mjs.meta.json +3235 -0
- package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
- package/dist/metafiles/editor.meta.json +67 -27
- package/dist/metafiles/flags.cjs.meta.json +220 -0
- package/dist/metafiles/flags.mjs.meta.json +225 -0
- package/dist/metafiles/index.meta.json +2717 -491
- package/dist/metafiles/index.mjs.meta.json +14 -4
- package/dist/react/index.js +16621 -13539
- package/dist/react/index.js.map +4 -4
- package/dist/sri.json +20 -15
- package/dist/types/activation-runtime.d.ts +91 -0
- package/dist/types/analytics-runtime.d.ts +166 -0
- package/dist/types/antiflicker.d.ts +9 -3
- package/dist/types/assignment-cookie-sync.d.ts +31 -0
- package/dist/types/assignment-runtime.d.ts +257 -0
- package/dist/types/assignment-store.d.ts +4 -8
- package/dist/types/bucket.d.ts +1 -2
- package/dist/types/compact-store.d.ts +1 -0
- package/dist/types/config-runtime.d.ts +313 -0
- package/dist/types/consent-evaluation.d.ts +27 -0
- package/dist/types/css.d.ts +5 -3
- package/dist/types/debug-loader.d.ts +2 -0
- package/dist/types/decide-assignments.d.ts +31 -0
- package/dist/types/delivery-timing.d.ts +8 -0
- package/dist/types/early-interaction-capture.d.ts +79 -0
- package/dist/types/early-reassert.d.ts +32 -0
- package/dist/types/edge-runtime.d.ts +11 -0
- package/dist/types/editor/api.d.ts +3 -0
- package/dist/types/editor/controller.d.ts +3 -1
- package/dist/types/editor/publication-result.d.ts +11 -0
- package/dist/types/editor/types.d.ts +2 -1
- package/dist/types/editor/view-kit.d.ts +1 -1
- package/dist/types/endpoint.d.ts +8 -0
- package/dist/types/exclusion-groups.d.ts +4 -7
- package/dist/types/flags.d.ts +2 -0
- package/dist/types/goal-eligibility.d.ts +4 -0
- package/dist/types/goals.d.ts +10 -1
- package/dist/types/guards.d.ts +7 -7
- package/dist/types/handover.d.ts +12 -1
- package/dist/types/hash.d.ts +8 -4
- package/dist/types/hydration.d.ts +2 -1
- package/dist/types/index.d.ts +24 -128
- package/dist/types/init-test-helpers.d.ts +48 -0
- package/dist/types/inline-bootstrap.d.ts +26 -0
- package/dist/types/js.d.ts +3 -1
- package/dist/types/money.d.ts +12 -0
- package/dist/types/mutate.d.ts +45 -6
- package/dist/types/performance.d.ts +2 -0
- package/dist/types/persistence-decoders.d.ts +12 -0
- package/dist/types/public-api.d.ts +209 -0
- package/dist/types/qa-assistant-state.d.ts +34 -0
- package/dist/types/react/index.d.ts +28 -3
- package/dist/types/reassert-sentinel.d.ts +6 -0
- package/dist/types/redirect.d.ts +48 -0
- package/dist/types/replay-recorder-entry.d.ts +1 -0
- package/dist/types/replay-signal-state.d.ts +6 -0
- package/dist/types/replay-signals.d.ts +13 -0
- package/dist/types/replay.d.ts +110 -0
- package/dist/types/router.d.ts +12 -2
- package/dist/types/runtime-state.d.ts +166 -0
- package/dist/types/sanitize.d.ts +1 -0
- package/dist/types/shadow.d.ts +3 -0
- package/dist/types/shopify-cart.d.ts +25 -1
- package/dist/types/storefront-vitals.d.ts +24 -0
- package/dist/types/targeting-diagnostics.d.ts +1 -0
- package/dist/types/targeting.d.ts +16 -2
- package/dist/types/track.d.ts +15 -1
- package/dist/types/types.d.ts +115 -9
- package/dist/types/wait.d.ts +14 -1
- package/dist/types/weekday-schedule.d.ts +3 -0
- package/package.json +17 -8
- package/src/activation-runtime.ts +2737 -0
- package/src/analytics-runtime.ts +2175 -0
- package/src/antiflicker.test.ts +216 -44
- package/src/antiflicker.ts +103 -28
- package/src/artifact-concurrency.test.ts +1091 -0
- package/src/assignment-cookie-sync.test.ts +202 -0
- package/src/assignment-cookie-sync.ts +189 -0
- package/src/assignment-runtime.ts +3204 -0
- package/src/assignment-store.test.ts +56 -0
- package/src/assignment-store.ts +7 -38
- package/src/bucket.ts +3 -18
- package/src/commerce-analytics-lifecycle.test.ts +75 -15
- package/src/commerce-config-lifecycle.test.ts +81 -1
- package/src/commerce-price-preserve.test.ts +133 -0
- package/src/commerce-price.test.ts +97 -0
- package/src/commerce-price.ts +74 -20
- package/src/commerce-shipping.test.ts +53 -0
- package/src/commerce-shipping.ts +5 -11
- package/src/compact-store.ts +15 -0
- package/src/config-fetch.test.ts +480 -9
- package/src/config-runtime.ts +2070 -0
- package/src/consent-evaluation-cookiescript.test.ts +50 -0
- package/src/consent-evaluation.ts +138 -0
- package/src/css.test.ts +30 -0
- package/src/css.ts +120 -6
- package/src/cwv.test.ts +60 -77
- package/src/debug-loader.test.ts +214 -0
- package/src/debug-loader.ts +41 -0
- package/src/decide-assignments.test.ts +196 -0
- package/src/decide-assignments.ts +204 -0
- package/src/delivery-timing.test.ts +104 -0
- package/src/delivery-timing.ts +89 -0
- package/src/early-interaction-capture.test.ts +451 -0
- package/src/early-interaction-capture.ts +230 -0
- package/src/early-reassert.test.ts +207 -0
- package/src/early-reassert.ts +152 -0
- package/src/edge-adoption.test.ts +435 -0
- package/src/edge-runtime.ts +30 -0
- package/src/editor/api.test.ts +109 -0
- package/src/editor/api.ts +34 -3
- package/src/editor/controller.test.ts +297 -14
- package/src/editor/controller.ts +69 -5
- package/src/editor/index.test.ts +2 -0
- package/src/editor/index.ts +1 -0
- package/src/editor/publication-result.ts +58 -0
- package/src/editor/types.ts +10 -1
- package/src/editor/view-kit.ts +2 -1
- package/src/editor/view.test.ts +108 -0
- package/src/editor/view.ts +49 -7
- package/src/endpoint.test.ts +15 -0
- package/src/endpoint.ts +10 -0
- package/src/exclusion-groups.ts +8 -18
- package/src/flags.test.ts +88 -1
- package/src/flags.ts +29 -29
- package/src/global-runtime-reconciler.test.ts +271 -0
- package/src/goal-eligibility.test.ts +29 -0
- package/src/goal-eligibility.ts +17 -0
- package/src/goals.test.ts +144 -1
- package/src/goals.ts +97 -18
- package/src/guards.test.ts +99 -1
- package/src/guards.ts +34 -15
- package/src/handover-cache-order.test.ts +6 -3
- package/src/handover.test.ts +154 -8
- package/src/handover.ts +122 -12
- package/src/hash.ts +29 -54
- package/src/holdout.test.ts +26 -0
- package/src/hydration.test.ts +73 -2
- package/src/hydration.ts +18 -9
- package/src/index.ts +130 -7287
- package/src/init-analytics.test.ts +1694 -0
- package/src/init-anti-flicker.test.ts +1211 -0
- package/src/init-cart-attributes.test.ts +2003 -0
- package/src/init-config-resolution.test.ts +999 -0
- package/src/init-consent.test.ts +971 -0
- package/src/init-empty.test.ts +11 -5406
- package/src/init-integrations.test.ts +846 -0
- package/src/init-page-activation.test.ts +5064 -0
- package/src/init-qa-loading.test.ts +740 -0
- package/src/init-redirects.test.ts +2156 -0
- package/src/init-test-helpers.ts +287 -0
- package/src/inline-bootstrap.test.ts +184 -0
- package/src/inline-bootstrap.ts +73 -0
- package/src/js.test.ts +51 -1
- package/src/js.ts +86 -2
- package/src/money.test.ts +55 -1
- package/src/money.ts +104 -2
- package/src/money.web-parity.contract.test.ts +85 -0
- package/src/mutate.test.ts +474 -10
- package/src/mutate.ts +739 -230
- package/src/page-trigger.test.ts +218 -8
- package/src/performance.ts +10 -0
- package/src/persistence-decoders.ts +18 -0
- package/src/public-api.ts +1828 -0
- package/src/qa-assistant-state.test.ts +249 -3
- package/src/qa-assistant-state.ts +146 -6
- package/src/qa-assistant-view.test.ts +220 -2
- package/src/qa-assistant-view.ts +102 -5
- package/src/react/index.test.tsx +37 -0
- package/src/react/index.tsx +19 -3
- package/src/reassert-sentinel.test.ts +155 -1
- package/src/reassert-sentinel.ts +168 -8
- package/src/redirect.test.ts +541 -0
- package/src/redirect.ts +572 -0
- package/src/replay-recorder-entry.ts +115 -0
- package/src/replay-signal-state.ts +100 -0
- package/src/replay-signals.test.ts +249 -0
- package/src/replay-signals.ts +151 -0
- package/src/replay.test.ts +764 -0
- package/src/replay.ts +514 -0
- package/src/router.test.ts +61 -2
- package/src/router.ts +50 -12
- package/src/runtime-state.ts +507 -0
- package/src/runtime.test.ts +174 -7
- package/src/runtime.ts +88 -14
- package/src/sanitize.test.ts +34 -0
- package/src/sanitize.ts +23 -12
- package/src/shadow.test.ts +187 -0
- package/src/shadow.ts +99 -0
- package/src/shopify-cart.test.ts +733 -23
- package/src/shopify-cart.ts +402 -49
- package/src/storefront-vitals.test.ts +217 -0
- package/src/storefront-vitals.ts +206 -0
- package/src/surface-signals.ts +6 -0
- package/src/targeting-conformance.test.ts +9 -0
- package/src/targeting-diagnostics.ts +38 -8
- package/src/targeting-parity-corpus.json +559 -0
- package/src/targeting-parity-corpus.test.ts +18 -0
- package/src/targeting.detailed.test.ts +105 -0
- package/src/targeting.test.ts +211 -0
- package/src/targeting.ts +88 -207
- package/src/track.test.ts +147 -0
- package/src/track.ts +125 -26
- package/src/types.ts +120 -13
- package/src/wait.test.ts +23 -0
- package/src/wait.ts +129 -13
- package/src/weekday-delivery.test.ts +56 -0
- package/src/weekday-schedule-parity-corpus.json +92 -0
- package/src/weekday-schedule.test.ts +69 -0
- package/src/weekday-schedule.ts +20 -0
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
applyToElement,
|
|
5
|
+
applyTrackedMutation,
|
|
6
|
+
replayArtifact,
|
|
7
|
+
resetAppliedMutations,
|
|
8
|
+
resetAppliedMutationsForOwner,
|
|
9
|
+
} from "./mutate";
|
|
4
10
|
import { runMuted } from "./observer-mute";
|
|
5
11
|
import { resetReassertSentinel, trackAppliedMutation } from "./reassert-sentinel";
|
|
6
12
|
import type { Mutation } from "./types";
|
|
@@ -61,6 +67,79 @@ describe("reassert sentinel", () => {
|
|
|
61
67
|
expect(read(element)).toBe("Variant");
|
|
62
68
|
});
|
|
63
69
|
|
|
70
|
+
it("does not reassert a torn-down owner's mutation while another owner remains live", async () => {
|
|
71
|
+
const tornDown = document.createElement("div");
|
|
72
|
+
const stillLive = document.createElement("div");
|
|
73
|
+
document.body.append(tornDown, stillLive);
|
|
74
|
+
const removedMutation = {
|
|
75
|
+
selector: ".removed-page",
|
|
76
|
+
action: "text",
|
|
77
|
+
text: "Removed treatment",
|
|
78
|
+
} as Mutation;
|
|
79
|
+
const liveMutation = {
|
|
80
|
+
selector: ".live-page",
|
|
81
|
+
action: "text",
|
|
82
|
+
text: "Live treatment",
|
|
83
|
+
} as Mutation;
|
|
84
|
+
|
|
85
|
+
runMuted(() => applyTrackedMutation(removedMutation, tornDown, undefined, ["page-a"]));
|
|
86
|
+
trackAppliedMutation(
|
|
87
|
+
tornDown,
|
|
88
|
+
removedMutation,
|
|
89
|
+
{ ownerKeys: ["page-a"] },
|
|
90
|
+
(target, applied) => applyToElement(applied, target),
|
|
91
|
+
);
|
|
92
|
+
runMuted(() => applyTrackedMutation(liveMutation, stillLive, undefined, ["page-b"]));
|
|
93
|
+
trackAppliedMutation(
|
|
94
|
+
stillLive,
|
|
95
|
+
liveMutation,
|
|
96
|
+
{ ownerKeys: ["page-b"] },
|
|
97
|
+
(target, applied) => applyToElement(applied, target),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
resetAppliedMutationsForOwner("page-a");
|
|
101
|
+
tornDown.textContent = "Framework update";
|
|
102
|
+
stillLive.textContent = "Framework update";
|
|
103
|
+
await runNextFrame();
|
|
104
|
+
|
|
105
|
+
expect(tornDown.textContent).toBe("Framework update");
|
|
106
|
+
expect(stillLive.textContent).toBe("Live treatment");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("retargets the journal sentinel when replay replaces the treated element", async () => {
|
|
110
|
+
const original = document.createElement("div");
|
|
111
|
+
original.id = "target";
|
|
112
|
+
document.body.appendChild(original);
|
|
113
|
+
const mutation = {
|
|
114
|
+
selector: "#target",
|
|
115
|
+
action: "text",
|
|
116
|
+
text: "Treatment",
|
|
117
|
+
} as Mutation;
|
|
118
|
+
let journalSeq: number | undefined;
|
|
119
|
+
runMuted(() => applyTrackedMutation(
|
|
120
|
+
mutation,
|
|
121
|
+
original,
|
|
122
|
+
undefined,
|
|
123
|
+
["page-a"],
|
|
124
|
+
"artifact-a",
|
|
125
|
+
(entry) => { journalSeq = entry.seq; },
|
|
126
|
+
));
|
|
127
|
+
trackAppliedMutation(
|
|
128
|
+
original,
|
|
129
|
+
mutation,
|
|
130
|
+
{ journalSeq, artifactId: "artifact-a", ownerKeys: ["page-a"] },
|
|
131
|
+
(target, applied) => applyToElement(applied, target),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
original.outerHTML = '<div id="target">Framework replacement</div>';
|
|
135
|
+
replayArtifact("artifact-a");
|
|
136
|
+
const replacement = document.getElementById("target")!;
|
|
137
|
+
replacement.textContent = "Later framework update";
|
|
138
|
+
await runNextFrame();
|
|
139
|
+
|
|
140
|
+
expect(replacement.textContent).toBe("Treatment");
|
|
141
|
+
});
|
|
142
|
+
|
|
64
143
|
it("re-applies an in-place hydration revert to the original content", async () => {
|
|
65
144
|
const element = document.createElement("div");
|
|
66
145
|
element.id = "target";
|
|
@@ -96,6 +175,81 @@ describe("reassert sentinel", () => {
|
|
|
96
175
|
expect(element.querySelector("img")!.className).toBe("variant");
|
|
97
176
|
});
|
|
98
177
|
|
|
178
|
+
it("survives a React-style child-node replacement and fails without the sentinel", async () => {
|
|
179
|
+
const enabled = document.createElement("div");
|
|
180
|
+
const disabled = document.createElement("div");
|
|
181
|
+
enabled.innerHTML = "<span>Control</span>";
|
|
182
|
+
disabled.innerHTML = "<span>Control</span>";
|
|
183
|
+
document.body.append(enabled, disabled);
|
|
184
|
+
const mutation = { selector: ".react-owned", action: "html", html: "<strong>Variant</strong>" } as Mutation;
|
|
185
|
+
applyAndTrack(enabled, mutation);
|
|
186
|
+
runMuted(() => applyTrackedMutation(mutation, disabled));
|
|
187
|
+
|
|
188
|
+
// React can reconcile a subtree by replacing the treatment's child nodes.
|
|
189
|
+
enabled.replaceChildren(Object.assign(document.createElement("span"), { textContent: "Control" }));
|
|
190
|
+
disabled.replaceChildren(Object.assign(document.createElement("span"), { textContent: "Control" }));
|
|
191
|
+
await runNextFrame();
|
|
192
|
+
|
|
193
|
+
expect(enabled.innerHTML).toBe("<strong>Variant</strong>");
|
|
194
|
+
expect(disabled.innerHTML).toBe("<span>Control</span>");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("survives a Svelte/Vue-style descendant patch and fails without the sentinel", async () => {
|
|
198
|
+
const enabled = document.createElement("div");
|
|
199
|
+
const disabled = document.createElement("div");
|
|
200
|
+
enabled.innerHTML = '<span data-state="control">Control</span>';
|
|
201
|
+
disabled.innerHTML = '<span data-state="control">Control</span>';
|
|
202
|
+
document.body.append(enabled, disabled);
|
|
203
|
+
const mutation = {
|
|
204
|
+
selector: ".svelte-vue-owned",
|
|
205
|
+
action: "html",
|
|
206
|
+
html: '<span data-state="variant">Variant</span>',
|
|
207
|
+
} as Mutation;
|
|
208
|
+
applyAndTrack(enabled, mutation);
|
|
209
|
+
runMuted(() => applyTrackedMutation(mutation, disabled));
|
|
210
|
+
|
|
211
|
+
for (const root of [enabled, disabled]) {
|
|
212
|
+
const descendant = root.querySelector("span")!;
|
|
213
|
+
descendant.setAttribute("data-state", "control");
|
|
214
|
+
descendant.textContent = "Control";
|
|
215
|
+
}
|
|
216
|
+
await runNextFrame();
|
|
217
|
+
|
|
218
|
+
expect(enabled.innerHTML).toBe('<span data-state="variant">Variant</span>');
|
|
219
|
+
expect(disabled.innerHTML).toBe('<span data-state="control">Control</span>');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("coalesces a rapid framework burst so the twelve-entry cap preserves a late revert", async () => {
|
|
223
|
+
const enabled = document.createElement("div");
|
|
224
|
+
const disabled = document.createElement("div");
|
|
225
|
+
document.body.append(enabled, disabled);
|
|
226
|
+
const mutation = { selector: ".burst-owned", action: "text", text: "Variant" } as Mutation;
|
|
227
|
+
runMuted(() => applyTrackedMutation(mutation, enabled));
|
|
228
|
+
runMuted(() => applyTrackedMutation(mutation, disabled));
|
|
229
|
+
const reapply = vi.fn((target: Element, applied: Mutation) => applyToElement(applied, target));
|
|
230
|
+
trackAppliedMutation(enabled, mutation, {}, reapply);
|
|
231
|
+
|
|
232
|
+
// Twelve writes in one render turn must spend one times[] entry, not the
|
|
233
|
+
// full entry.times.length === 12 loop cap.
|
|
234
|
+
for (let index = 0; index < 12; index += 1) {
|
|
235
|
+
enabled.textContent = `Framework burst ${index}`;
|
|
236
|
+
disabled.textContent = `Framework burst ${index}`;
|
|
237
|
+
}
|
|
238
|
+
await runNextFrame();
|
|
239
|
+
expect(enabled.textContent).toBe("Variant");
|
|
240
|
+
expect(reapply).toHaveBeenCalledTimes(1);
|
|
241
|
+
expect((window as Window & { __dripPerf?: Record<string, unknown> }).__dripPerf).toMatchObject({ reapply_count: 1 });
|
|
242
|
+
|
|
243
|
+
enabled.textContent = "Legitimate late revert";
|
|
244
|
+
disabled.textContent = "Legitimate late revert";
|
|
245
|
+
await runNextFrame();
|
|
246
|
+
|
|
247
|
+
expect(enabled.textContent).toBe("Variant");
|
|
248
|
+
expect(disabled.textContent).toBe("Legitimate late revert");
|
|
249
|
+
expect(reapply).toHaveBeenCalledTimes(2);
|
|
250
|
+
expect((window as Window & { __dripPerf?: Record<string, unknown> }).__dripPerf).toMatchObject({ reapply_count: 2 });
|
|
251
|
+
});
|
|
252
|
+
|
|
99
253
|
it("re-applies an added-only restore of removed original html", async () => {
|
|
100
254
|
const element = document.createElement("div");
|
|
101
255
|
element.id = "target";
|
package/src/reassert-sentinel.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import type { Mutation } from "./types";
|
|
2
2
|
import { matchesOriginalInnerHtmlSnapshot, normalizeCssPropertyName } from "./mutate";
|
|
3
3
|
import { registerMutedObserver, runMuted, unregisterMutedObserver } from "./observer-mute";
|
|
4
|
+
import { onShadowMutations, shadowRootFor } from "./shadow";
|
|
4
5
|
|
|
5
6
|
type Entry = {
|
|
6
7
|
el: Element;
|
|
7
8
|
mutation: Mutation;
|
|
9
|
+
artifactId?: string;
|
|
10
|
+
journalSeq?: number;
|
|
11
|
+
ownerKeys: Set<string>;
|
|
12
|
+
requiresOwner: boolean;
|
|
8
13
|
experimentId?: string;
|
|
9
14
|
variationId?: string;
|
|
10
15
|
appliedAt: number;
|
|
11
16
|
apply: (element: Element, mutation: Mutation) => void;
|
|
12
17
|
times: number[];
|
|
13
18
|
stopped: boolean;
|
|
19
|
+
shadowRoot?: ShadowRoot;
|
|
20
|
+
shadowIndex?: number;
|
|
14
21
|
appliedNodes?: ChildNode[];
|
|
15
22
|
appliedStyles?: Array<[string, string]>;
|
|
16
23
|
};
|
|
@@ -61,6 +68,7 @@ const pending = new Set<Entry>();
|
|
|
61
68
|
const deepChecks = new Set<Entry>();
|
|
62
69
|
const roots = new Map<Element, TreatedRoot>();
|
|
63
70
|
let frame: number | null = null;
|
|
71
|
+
let stopShadowListener: (() => void) | null = null;
|
|
64
72
|
|
|
65
73
|
function perf(): Record<string, unknown> | null {
|
|
66
74
|
if (typeof window === "undefined") return null;
|
|
@@ -100,7 +108,7 @@ function schedule(): void {
|
|
|
100
108
|
frame = null;
|
|
101
109
|
const now = Date.now();
|
|
102
110
|
for (const entry of pending) {
|
|
103
|
-
if (entry.stopped) continue;
|
|
111
|
+
if (entry.stopped || (entry.requiresOwner && entry.ownerKeys.size === 0)) continue;
|
|
104
112
|
entry.times = entry.times.filter((at) => now - at < 10_000);
|
|
105
113
|
if (entry.times.length === 12) {
|
|
106
114
|
entry.stopped = true;
|
|
@@ -134,12 +142,23 @@ function schedule(): void {
|
|
|
134
142
|
});
|
|
135
143
|
}
|
|
136
144
|
|
|
145
|
+
function canResurrectViaShadow(treated: TreatedRoot): boolean {
|
|
146
|
+
return treated.entries.some(
|
|
147
|
+
(entry) => !entry.stopped && entry.shadowRoot !== undefined && entry.shadowRoot.host.isConnected,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
137
151
|
function observe(element: Element): void {
|
|
138
152
|
if (typeof MutationObserver === "undefined" || roots.has(element)) return;
|
|
139
|
-
for (const [treatedRoot,
|
|
153
|
+
for (const [treatedRoot, treated] of roots) {
|
|
140
154
|
if (treatedRoot.isConnected) continue;
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
// A disconnected root whose entries live in a still-connected shadow root
|
|
156
|
+
// stays registered: the shadow listener re-locates those entries on a
|
|
157
|
+
// later widget re-render, and this sweep would otherwise drop a sibling
|
|
158
|
+
// entry whose replacement was not findable in the same mutation batch.
|
|
159
|
+
if (canResurrectViaShadow(treated)) continue;
|
|
160
|
+
treated.observer.disconnect();
|
|
161
|
+
unregisterMutedObserver(treated.observer);
|
|
143
162
|
roots.delete(treatedRoot);
|
|
144
163
|
}
|
|
145
164
|
const entries: Entry[] = [];
|
|
@@ -157,27 +176,168 @@ function observe(element: Element): void {
|
|
|
157
176
|
observer.observe(element, { attributes: true, characterData: true, childList: true, subtree: true });
|
|
158
177
|
}
|
|
159
178
|
|
|
179
|
+
function ensureShadowObserver(): void {
|
|
180
|
+
stopShadowListener ??= onShadowMutations((_records, root) => {
|
|
181
|
+
for (const [rootElement, treated] of Array.from(roots.entries())) {
|
|
182
|
+
for (const entry of Array.from(treated.entries)) {
|
|
183
|
+
if (entry.stopped || entry.shadowRoot !== root || entry.el.isConnected) continue;
|
|
184
|
+
try {
|
|
185
|
+
const candidates = Array.from(root.querySelectorAll(entry.mutation.selector));
|
|
186
|
+
const replacement = candidates[entry.shadowIndex ?? 0];
|
|
187
|
+
if (!replacement || replacement === entry.el) continue;
|
|
188
|
+
// Migrate the entry to the replacement's root (mirrors
|
|
189
|
+
// replayReassertSentinelEntry): an entry left keyed under the
|
|
190
|
+
// disconnected element is purged by the next observe() sweep.
|
|
191
|
+
const index = treated.entries.indexOf(entry);
|
|
192
|
+
if (index >= 0) removeEntry(treated.entries, index);
|
|
193
|
+
if (treated.entries.length === 0) {
|
|
194
|
+
treated.observer.disconnect();
|
|
195
|
+
unregisterMutedObserver(treated.observer);
|
|
196
|
+
roots.delete(rootElement);
|
|
197
|
+
}
|
|
198
|
+
observe(replacement);
|
|
199
|
+
entry.el = replacement;
|
|
200
|
+
entry.shadowIndex = candidates.indexOf(replacement);
|
|
201
|
+
roots.get(replacement)?.entries.push(entry);
|
|
202
|
+
markPending(entry);
|
|
203
|
+
} catch {}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (pending.size) schedule();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
160
210
|
export function trackAppliedMutation(
|
|
161
211
|
element: Element,
|
|
162
212
|
mutation: Mutation,
|
|
163
|
-
context: {
|
|
213
|
+
context: {
|
|
214
|
+
experimentId?: string;
|
|
215
|
+
variationId?: string;
|
|
216
|
+
journalSeq?: number;
|
|
217
|
+
artifactId?: string;
|
|
218
|
+
ownerKeys?: readonly string[];
|
|
219
|
+
},
|
|
164
220
|
apply: (element: Element, mutation: Mutation) => void,
|
|
165
221
|
): void {
|
|
166
222
|
if (mutation.action === "remove" || mutation.action === "insertBefore" || mutation.action === "insertAfter") return;
|
|
167
223
|
observe(element);
|
|
168
224
|
const entries = roots.get(element)?.entries;
|
|
169
225
|
if (!entries) return;
|
|
170
|
-
const
|
|
226
|
+
const shadowRoot = shadowRootFor(element);
|
|
227
|
+
if (shadowRoot) ensureShadowObserver();
|
|
228
|
+
const existing = entries.find((entry) => context.journalSeq !== undefined
|
|
229
|
+
? entry.journalSeq === context.journalSeq
|
|
230
|
+
: entry.mutation === mutation);
|
|
171
231
|
if (existing) {
|
|
172
|
-
|
|
232
|
+
context.ownerKeys?.forEach((ownerKey) => existing.ownerKeys.add(ownerKey));
|
|
233
|
+
Object.assign(existing, {
|
|
234
|
+
experimentId: context.experimentId,
|
|
235
|
+
variationId: context.variationId,
|
|
236
|
+
journalSeq: context.journalSeq,
|
|
237
|
+
artifactId: context.artifactId,
|
|
238
|
+
appliedAt: Date.now(),
|
|
239
|
+
apply,
|
|
240
|
+
});
|
|
173
241
|
captureAppliedNodes(existing);
|
|
174
242
|
} else {
|
|
175
|
-
const entry: Entry = {
|
|
243
|
+
const entry: Entry = {
|
|
244
|
+
el: element,
|
|
245
|
+
mutation,
|
|
246
|
+
experimentId: context.experimentId,
|
|
247
|
+
variationId: context.variationId,
|
|
248
|
+
journalSeq: context.journalSeq,
|
|
249
|
+
artifactId: context.artifactId,
|
|
250
|
+
ownerKeys: new Set(context.ownerKeys ?? []),
|
|
251
|
+
requiresOwner: context.ownerKeys !== undefined,
|
|
252
|
+
appliedAt: Date.now(),
|
|
253
|
+
apply,
|
|
254
|
+
times: [],
|
|
255
|
+
stopped: false,
|
|
256
|
+
shadowRoot: shadowRoot ?? undefined,
|
|
257
|
+
shadowIndex: shadowRoot
|
|
258
|
+
? Array.from(shadowRoot.querySelectorAll(mutation.selector)).indexOf(element)
|
|
259
|
+
: undefined,
|
|
260
|
+
};
|
|
176
261
|
captureAppliedNodes(entry);
|
|
177
262
|
entries.push(entry);
|
|
178
263
|
}
|
|
179
264
|
}
|
|
180
265
|
|
|
266
|
+
function removeEntry(entries: Entry[], index: number): void {
|
|
267
|
+
const [entry] = entries.splice(index, 1);
|
|
268
|
+
pending.delete(entry);
|
|
269
|
+
deepChecks.delete(entry);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function addReassertSentinelOwners(
|
|
273
|
+
existingOwnerKeys: ReadonlySet<string>,
|
|
274
|
+
ownerKeys: readonly string[],
|
|
275
|
+
artifactId?: string,
|
|
276
|
+
): void {
|
|
277
|
+
if (!existingOwnerKeys.size || !ownerKeys.length) return;
|
|
278
|
+
for (const { entries } of roots.values()) {
|
|
279
|
+
for (const entry of entries) {
|
|
280
|
+
if (artifactId !== undefined && entry.artifactId !== artifactId) continue;
|
|
281
|
+
if (!Array.from(existingOwnerKeys).some((ownerKey) => entry.ownerKeys.has(ownerKey))) continue;
|
|
282
|
+
ownerKeys.forEach((ownerKey) => entry.ownerKeys.add(ownerKey));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function resetReassertSentinelForOwner(
|
|
288
|
+
ownerKey: string,
|
|
289
|
+
artifactId?: string,
|
|
290
|
+
): void {
|
|
291
|
+
for (const [element, root] of Array.from(roots.entries())) {
|
|
292
|
+
for (let index = root.entries.length - 1; index >= 0; index -= 1) {
|
|
293
|
+
const entry = root.entries[index];
|
|
294
|
+
if (artifactId !== undefined && entry.artifactId !== artifactId) continue;
|
|
295
|
+
if (!entry.ownerKeys.delete(ownerKey)) continue;
|
|
296
|
+
if (entry.requiresOwner && entry.ownerKeys.size === 0) removeEntry(root.entries, index);
|
|
297
|
+
}
|
|
298
|
+
if (root.entries.length) continue;
|
|
299
|
+
root.observer.disconnect();
|
|
300
|
+
unregisterMutedObserver(root.observer);
|
|
301
|
+
roots.delete(element);
|
|
302
|
+
}
|
|
303
|
+
if (roots.size === 0 && frame !== null) {
|
|
304
|
+
cancelAnimationFrame(frame);
|
|
305
|
+
frame = null;
|
|
306
|
+
pending.clear();
|
|
307
|
+
deepChecks.clear();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function replayReassertSentinelEntry(
|
|
312
|
+
journalSeq: number,
|
|
313
|
+
element: Element,
|
|
314
|
+
): void {
|
|
315
|
+
for (const [currentElement, root] of Array.from(roots.entries())) {
|
|
316
|
+
const index = root.entries.findIndex((entry) => entry.journalSeq === journalSeq);
|
|
317
|
+
if (index < 0) continue;
|
|
318
|
+
const entry = root.entries[index];
|
|
319
|
+
if (entry.el === element) {
|
|
320
|
+
entry.appliedAt = Date.now();
|
|
321
|
+
captureAppliedNodes(entry);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
removeEntry(root.entries, index);
|
|
325
|
+
if (root.entries.length === 0) {
|
|
326
|
+
root.observer.disconnect();
|
|
327
|
+
unregisterMutedObserver(root.observer);
|
|
328
|
+
roots.delete(currentElement);
|
|
329
|
+
}
|
|
330
|
+
observe(element);
|
|
331
|
+
entry.el = element;
|
|
332
|
+
entry.appliedAt = Date.now();
|
|
333
|
+
entry.times = [];
|
|
334
|
+
entry.stopped = false;
|
|
335
|
+
captureAppliedNodes(entry);
|
|
336
|
+
roots.get(element)?.entries.push(entry);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
181
341
|
export function resetReassertSentinel(): void {
|
|
182
342
|
if (frame !== null) cancelAnimationFrame(frame);
|
|
183
343
|
frame = null;
|