@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.
Files changed (245) hide show
  1. package/README.md +5 -0
  2. package/dist/bundle-report.json +584 -226
  3. package/dist/define.cjs +123 -0
  4. package/dist/define.cjs.map +7 -0
  5. package/dist/define.mjs +102 -0
  6. package/dist/define.mjs.map +7 -0
  7. package/dist/drip.cjs +19262 -0
  8. package/dist/drip.cjs.map +7 -0
  9. package/dist/drip.debug.js +1 -1
  10. package/dist/drip.debug.js.map +1 -1
  11. package/dist/drip.heatmap.js +1 -1
  12. package/dist/drip.heatmap.js.map +1 -1
  13. package/dist/drip.js +1 -1
  14. package/dist/drip.js.map +1 -1
  15. package/dist/drip.live.js +1 -1
  16. package/dist/drip.live.js.map +1 -1
  17. package/dist/drip.mjs +19246 -0
  18. package/dist/drip.mjs.map +7 -0
  19. package/dist/drip.replay-recorder.js +2 -0
  20. package/dist/drip.replay-recorder.js.map +1 -0
  21. package/dist/editor.js +147 -142
  22. package/dist/editor.js.map +4 -4
  23. package/dist/flags.cjs +1788 -0
  24. package/dist/flags.cjs.map +7 -0
  25. package/dist/flags.mjs +1765 -0
  26. package/dist/flags.mjs.map +7 -0
  27. package/dist/index.mjs +12 -0
  28. package/dist/index.mjs.map +2 -2
  29. package/dist/metafiles/define.cjs.meta.json +28 -0
  30. package/dist/metafiles/define.mjs.meta.json +32 -0
  31. package/dist/metafiles/drip.cjs.meta.json +3193 -0
  32. package/dist/metafiles/drip.debug.meta.json +2859 -574
  33. package/dist/metafiles/drip.heatmap.meta.json +2732 -460
  34. package/dist/metafiles/drip.live.meta.json +2732 -460
  35. package/dist/metafiles/drip.meta.json +2859 -574
  36. package/dist/metafiles/drip.mjs.meta.json +3235 -0
  37. package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
  38. package/dist/metafiles/editor.meta.json +67 -27
  39. package/dist/metafiles/flags.cjs.meta.json +220 -0
  40. package/dist/metafiles/flags.mjs.meta.json +225 -0
  41. package/dist/metafiles/index.meta.json +2717 -491
  42. package/dist/metafiles/index.mjs.meta.json +14 -4
  43. package/dist/react/index.js +16621 -13539
  44. package/dist/react/index.js.map +4 -4
  45. package/dist/sri.json +20 -15
  46. package/dist/types/activation-runtime.d.ts +91 -0
  47. package/dist/types/analytics-runtime.d.ts +166 -0
  48. package/dist/types/antiflicker.d.ts +9 -3
  49. package/dist/types/assignment-cookie-sync.d.ts +31 -0
  50. package/dist/types/assignment-runtime.d.ts +257 -0
  51. package/dist/types/assignment-store.d.ts +4 -8
  52. package/dist/types/bucket.d.ts +1 -2
  53. package/dist/types/compact-store.d.ts +1 -0
  54. package/dist/types/config-runtime.d.ts +313 -0
  55. package/dist/types/consent-evaluation.d.ts +27 -0
  56. package/dist/types/css.d.ts +5 -3
  57. package/dist/types/debug-loader.d.ts +2 -0
  58. package/dist/types/decide-assignments.d.ts +31 -0
  59. package/dist/types/delivery-timing.d.ts +8 -0
  60. package/dist/types/early-interaction-capture.d.ts +79 -0
  61. package/dist/types/early-reassert.d.ts +32 -0
  62. package/dist/types/edge-runtime.d.ts +11 -0
  63. package/dist/types/editor/api.d.ts +3 -0
  64. package/dist/types/editor/controller.d.ts +3 -1
  65. package/dist/types/editor/publication-result.d.ts +11 -0
  66. package/dist/types/editor/types.d.ts +2 -1
  67. package/dist/types/editor/view-kit.d.ts +1 -1
  68. package/dist/types/endpoint.d.ts +8 -0
  69. package/dist/types/exclusion-groups.d.ts +4 -7
  70. package/dist/types/flags.d.ts +2 -0
  71. package/dist/types/goal-eligibility.d.ts +4 -0
  72. package/dist/types/goals.d.ts +10 -1
  73. package/dist/types/guards.d.ts +7 -7
  74. package/dist/types/handover.d.ts +12 -1
  75. package/dist/types/hash.d.ts +8 -4
  76. package/dist/types/hydration.d.ts +2 -1
  77. package/dist/types/index.d.ts +24 -128
  78. package/dist/types/init-test-helpers.d.ts +48 -0
  79. package/dist/types/inline-bootstrap.d.ts +26 -0
  80. package/dist/types/js.d.ts +3 -1
  81. package/dist/types/money.d.ts +12 -0
  82. package/dist/types/mutate.d.ts +45 -6
  83. package/dist/types/performance.d.ts +2 -0
  84. package/dist/types/persistence-decoders.d.ts +12 -0
  85. package/dist/types/public-api.d.ts +209 -0
  86. package/dist/types/qa-assistant-state.d.ts +34 -0
  87. package/dist/types/react/index.d.ts +28 -3
  88. package/dist/types/reassert-sentinel.d.ts +6 -0
  89. package/dist/types/redirect.d.ts +48 -0
  90. package/dist/types/replay-recorder-entry.d.ts +1 -0
  91. package/dist/types/replay-signal-state.d.ts +6 -0
  92. package/dist/types/replay-signals.d.ts +13 -0
  93. package/dist/types/replay.d.ts +110 -0
  94. package/dist/types/router.d.ts +12 -2
  95. package/dist/types/runtime-state.d.ts +166 -0
  96. package/dist/types/sanitize.d.ts +1 -0
  97. package/dist/types/shadow.d.ts +3 -0
  98. package/dist/types/shopify-cart.d.ts +25 -1
  99. package/dist/types/storefront-vitals.d.ts +24 -0
  100. package/dist/types/targeting-diagnostics.d.ts +1 -0
  101. package/dist/types/targeting.d.ts +16 -2
  102. package/dist/types/track.d.ts +15 -1
  103. package/dist/types/types.d.ts +115 -9
  104. package/dist/types/wait.d.ts +14 -1
  105. package/dist/types/weekday-schedule.d.ts +3 -0
  106. package/package.json +17 -8
  107. package/src/activation-runtime.ts +2737 -0
  108. package/src/analytics-runtime.ts +2175 -0
  109. package/src/antiflicker.test.ts +216 -44
  110. package/src/antiflicker.ts +103 -28
  111. package/src/artifact-concurrency.test.ts +1091 -0
  112. package/src/assignment-cookie-sync.test.ts +202 -0
  113. package/src/assignment-cookie-sync.ts +189 -0
  114. package/src/assignment-runtime.ts +3204 -0
  115. package/src/assignment-store.test.ts +56 -0
  116. package/src/assignment-store.ts +7 -38
  117. package/src/bucket.ts +3 -18
  118. package/src/commerce-analytics-lifecycle.test.ts +75 -15
  119. package/src/commerce-config-lifecycle.test.ts +81 -1
  120. package/src/commerce-price-preserve.test.ts +133 -0
  121. package/src/commerce-price.test.ts +97 -0
  122. package/src/commerce-price.ts +74 -20
  123. package/src/commerce-shipping.test.ts +53 -0
  124. package/src/commerce-shipping.ts +5 -11
  125. package/src/compact-store.ts +15 -0
  126. package/src/config-fetch.test.ts +480 -9
  127. package/src/config-runtime.ts +2070 -0
  128. package/src/consent-evaluation-cookiescript.test.ts +50 -0
  129. package/src/consent-evaluation.ts +138 -0
  130. package/src/css.test.ts +30 -0
  131. package/src/css.ts +120 -6
  132. package/src/cwv.test.ts +60 -77
  133. package/src/debug-loader.test.ts +214 -0
  134. package/src/debug-loader.ts +41 -0
  135. package/src/decide-assignments.test.ts +196 -0
  136. package/src/decide-assignments.ts +204 -0
  137. package/src/delivery-timing.test.ts +104 -0
  138. package/src/delivery-timing.ts +89 -0
  139. package/src/early-interaction-capture.test.ts +451 -0
  140. package/src/early-interaction-capture.ts +230 -0
  141. package/src/early-reassert.test.ts +207 -0
  142. package/src/early-reassert.ts +152 -0
  143. package/src/edge-adoption.test.ts +435 -0
  144. package/src/edge-runtime.ts +30 -0
  145. package/src/editor/api.test.ts +109 -0
  146. package/src/editor/api.ts +34 -3
  147. package/src/editor/controller.test.ts +297 -14
  148. package/src/editor/controller.ts +69 -5
  149. package/src/editor/index.test.ts +2 -0
  150. package/src/editor/index.ts +1 -0
  151. package/src/editor/publication-result.ts +58 -0
  152. package/src/editor/types.ts +10 -1
  153. package/src/editor/view-kit.ts +2 -1
  154. package/src/editor/view.test.ts +108 -0
  155. package/src/editor/view.ts +49 -7
  156. package/src/endpoint.test.ts +15 -0
  157. package/src/endpoint.ts +10 -0
  158. package/src/exclusion-groups.ts +8 -18
  159. package/src/flags.test.ts +88 -1
  160. package/src/flags.ts +29 -29
  161. package/src/global-runtime-reconciler.test.ts +271 -0
  162. package/src/goal-eligibility.test.ts +29 -0
  163. package/src/goal-eligibility.ts +17 -0
  164. package/src/goals.test.ts +144 -1
  165. package/src/goals.ts +97 -18
  166. package/src/guards.test.ts +99 -1
  167. package/src/guards.ts +34 -15
  168. package/src/handover-cache-order.test.ts +6 -3
  169. package/src/handover.test.ts +154 -8
  170. package/src/handover.ts +122 -12
  171. package/src/hash.ts +29 -54
  172. package/src/holdout.test.ts +26 -0
  173. package/src/hydration.test.ts +73 -2
  174. package/src/hydration.ts +18 -9
  175. package/src/index.ts +130 -7287
  176. package/src/init-analytics.test.ts +1694 -0
  177. package/src/init-anti-flicker.test.ts +1211 -0
  178. package/src/init-cart-attributes.test.ts +2003 -0
  179. package/src/init-config-resolution.test.ts +999 -0
  180. package/src/init-consent.test.ts +971 -0
  181. package/src/init-empty.test.ts +11 -5406
  182. package/src/init-integrations.test.ts +846 -0
  183. package/src/init-page-activation.test.ts +5064 -0
  184. package/src/init-qa-loading.test.ts +740 -0
  185. package/src/init-redirects.test.ts +2156 -0
  186. package/src/init-test-helpers.ts +287 -0
  187. package/src/inline-bootstrap.test.ts +184 -0
  188. package/src/inline-bootstrap.ts +73 -0
  189. package/src/js.test.ts +51 -1
  190. package/src/js.ts +86 -2
  191. package/src/money.test.ts +55 -1
  192. package/src/money.ts +104 -2
  193. package/src/money.web-parity.contract.test.ts +85 -0
  194. package/src/mutate.test.ts +474 -10
  195. package/src/mutate.ts +739 -230
  196. package/src/page-trigger.test.ts +218 -8
  197. package/src/performance.ts +10 -0
  198. package/src/persistence-decoders.ts +18 -0
  199. package/src/public-api.ts +1828 -0
  200. package/src/qa-assistant-state.test.ts +249 -3
  201. package/src/qa-assistant-state.ts +146 -6
  202. package/src/qa-assistant-view.test.ts +220 -2
  203. package/src/qa-assistant-view.ts +102 -5
  204. package/src/react/index.test.tsx +37 -0
  205. package/src/react/index.tsx +19 -3
  206. package/src/reassert-sentinel.test.ts +155 -1
  207. package/src/reassert-sentinel.ts +168 -8
  208. package/src/redirect.test.ts +541 -0
  209. package/src/redirect.ts +572 -0
  210. package/src/replay-recorder-entry.ts +115 -0
  211. package/src/replay-signal-state.ts +100 -0
  212. package/src/replay-signals.test.ts +249 -0
  213. package/src/replay-signals.ts +151 -0
  214. package/src/replay.test.ts +764 -0
  215. package/src/replay.ts +514 -0
  216. package/src/router.test.ts +61 -2
  217. package/src/router.ts +50 -12
  218. package/src/runtime-state.ts +507 -0
  219. package/src/runtime.test.ts +174 -7
  220. package/src/runtime.ts +88 -14
  221. package/src/sanitize.test.ts +34 -0
  222. package/src/sanitize.ts +23 -12
  223. package/src/shadow.test.ts +187 -0
  224. package/src/shadow.ts +99 -0
  225. package/src/shopify-cart.test.ts +733 -23
  226. package/src/shopify-cart.ts +402 -49
  227. package/src/storefront-vitals.test.ts +217 -0
  228. package/src/storefront-vitals.ts +206 -0
  229. package/src/surface-signals.ts +6 -0
  230. package/src/targeting-conformance.test.ts +9 -0
  231. package/src/targeting-diagnostics.ts +38 -8
  232. package/src/targeting-parity-corpus.json +559 -0
  233. package/src/targeting-parity-corpus.test.ts +18 -0
  234. package/src/targeting.detailed.test.ts +105 -0
  235. package/src/targeting.test.ts +211 -0
  236. package/src/targeting.ts +88 -207
  237. package/src/track.test.ts +147 -0
  238. package/src/track.ts +125 -26
  239. package/src/types.ts +120 -13
  240. package/src/wait.test.ts +23 -0
  241. package/src/wait.ts +129 -13
  242. package/src/weekday-delivery.test.ts +56 -0
  243. package/src/weekday-schedule-parity-corpus.json +92 -0
  244. package/src/weekday-schedule.test.ts +69 -0
  245. 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 { applyToElement, applyTrackedMutation, resetAppliedMutations } from "./mutate";
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";
@@ -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, { observer }] of roots) {
153
+ for (const [treatedRoot, treated] of roots) {
140
154
  if (treatedRoot.isConnected) continue;
141
- observer.disconnect();
142
- unregisterMutedObserver(observer);
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: { experimentId?: string; variationId?: string },
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 existing = entries.find((entry) => entry.mutation === mutation);
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
- Object.assign(existing, context, { appliedAt: Date.now(), apply });
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 = { el: element, mutation, ...context, appliedAt: Date.now(), apply, times: [], stopped: false };
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;