@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
package/src/mutate.ts
CHANGED
|
@@ -1,92 +1,84 @@
|
|
|
1
1
|
import { Mutation, MutationAction } from "./types";
|
|
2
|
-
import { sanitizeHtml } from "./sanitize";
|
|
2
|
+
import { isSafeMutationAttribute, sanitizeHtml } from "./sanitize";
|
|
3
3
|
import { runMuted } from "./observer-mute";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
addReassertSentinelOwners,
|
|
6
|
+
replayReassertSentinelEntry,
|
|
7
|
+
resetReassertSentinel,
|
|
8
|
+
resetReassertSentinelForOwner,
|
|
9
|
+
} from "./reassert-sentinel";
|
|
10
|
+
import { extractCurrencyTokens, type CurrencyToken } from "./money";
|
|
11
|
+
import { queryAllDeep } from "./shadow";
|
|
6
12
|
|
|
7
13
|
export type ApplyOptions = {
|
|
8
14
|
root?: Document | HTMLElement;
|
|
9
|
-
|
|
15
|
+
artifactId?: string;
|
|
16
|
+
epoch?: number;
|
|
17
|
+
ownerKeys?: readonly string[];
|
|
18
|
+
onApplied?: (mutation: Mutation, element: Element, index: number, journalSeq?: number) => void;
|
|
10
19
|
onMiss?: (mutation: Mutation, index: number) => void;
|
|
20
|
+
preservePrices?: boolean;
|
|
21
|
+
priceTestExempt?: boolean;
|
|
22
|
+
onPricePreservationViolation?: (input: {
|
|
23
|
+
mutation: Mutation;
|
|
24
|
+
selector: string;
|
|
25
|
+
originalPriceTokens: CurrencyToken[];
|
|
26
|
+
}) => void;
|
|
11
27
|
};
|
|
12
28
|
|
|
13
29
|
const INSERT_ATTR = "data-drip-inserted";
|
|
14
30
|
|
|
15
|
-
type
|
|
16
|
-
kind: "attribute";
|
|
17
|
-
element: Element;
|
|
18
|
-
values: Array<{ name: string; value: string | null }>;
|
|
19
|
-
};
|
|
31
|
+
type ElementReference = { deref: () => Element | undefined };
|
|
20
32
|
|
|
21
|
-
type
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
type ElementBaseline = {
|
|
34
|
+
attributeValues: Map<string, string | null>;
|
|
35
|
+
contentTouched: boolean;
|
|
36
|
+
innerHTML?: string;
|
|
37
|
+
textContent?: string | null;
|
|
38
|
+
styleAttributeTouched: boolean;
|
|
39
|
+
styleAttributeValue: string | null;
|
|
40
|
+
styleValues: Map<string, { value: string; priority: string }>;
|
|
41
|
+
parent: Node | null;
|
|
42
|
+
nextSibling: Node | null;
|
|
26
43
|
};
|
|
27
44
|
|
|
28
|
-
type
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
export type MutationJournalEntry = {
|
|
46
|
+
seq: number;
|
|
47
|
+
artifactId: string;
|
|
48
|
+
owners: Set<string>;
|
|
49
|
+
target: { selector: string; index: number; el: ElementReference };
|
|
50
|
+
mutation: Mutation;
|
|
51
|
+
epoch: number;
|
|
52
|
+
kind: "mutation" | "insert" | "adopt-insert";
|
|
53
|
+
insertKey?: string;
|
|
54
|
+
nodes?: Node[];
|
|
55
|
+
state: "applied" | "invalidated";
|
|
32
56
|
};
|
|
33
57
|
|
|
34
|
-
type
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
nextSibling: Node | null;
|
|
58
|
+
type MutationJournal = {
|
|
59
|
+
entries: MutationJournalEntry[];
|
|
60
|
+
baselines: WeakMap<Element, ElementBaseline>;
|
|
61
|
+
fallbackArtifactId: string;
|
|
39
62
|
};
|
|
40
63
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
let journalSequence = 0;
|
|
65
|
+
let applicationContextSequence = 0;
|
|
66
|
+
// Sequence order is the treatment order. A rollback restores one baseline,
|
|
67
|
+
// removes dead entries, and replays every surviving entry in this order.
|
|
68
|
+
const mutationJournal: MutationJournal = {
|
|
69
|
+
entries: [],
|
|
70
|
+
baselines: new WeakMap(),
|
|
71
|
+
fallbackArtifactId: "runtime",
|
|
44
72
|
};
|
|
45
73
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
| InsertSnapshot;
|
|
52
|
-
|
|
53
|
-
const mutationSnapshots: MutationSnapshot[] = [];
|
|
54
|
-
|
|
55
|
-
const URL_ATTRIBUTES = new Set(["href", "src", "action", "formaction", "xlink:href"]);
|
|
56
|
-
const SAFE_DATA_URL_MEDIA_TYPES = new Set([
|
|
57
|
-
"image/png",
|
|
58
|
-
"image/jpeg",
|
|
59
|
-
"image/gif",
|
|
60
|
-
"image/webp",
|
|
61
|
-
"image/avif",
|
|
62
|
-
]);
|
|
63
|
-
|
|
64
|
-
export function isSafeMutationAttribute(name: string, value: string | null): boolean {
|
|
65
|
-
const normalizedName = name.trim().toLowerCase();
|
|
66
|
-
if (!normalizedName || normalizedName.startsWith("on") || normalizedName === "srcdoc") {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
if (value === null || !URL_ATTRIBUTES.has(normalizedName)) return true;
|
|
70
|
-
const normalizedValue = value.replace(/[\u0000-\u0020]+/g, "").toLowerCase();
|
|
71
|
-
if (normalizedValue.startsWith("javascript:")) return false;
|
|
72
|
-
if (!normalizedValue.startsWith("data:")) return true;
|
|
73
|
-
const mediaType = normalizedValue.slice(5).split(/[;,]/, 1)[0];
|
|
74
|
-
return SAFE_DATA_URL_MEDIA_TYPES.has(mediaType);
|
|
75
|
-
}
|
|
74
|
+
// Reassertion bypasses the normal application chokepoint, so remember mutation
|
|
75
|
+
// objects rejected by the price guard and never replay them later.
|
|
76
|
+
const priceRejectedMutations = new WeakSet<Mutation>();
|
|
77
|
+
|
|
78
|
+
export { isSafeMutationAttribute } from "./sanitize";
|
|
76
79
|
|
|
77
80
|
function sanitizeMutationHtml(html: string): string {
|
|
78
|
-
|
|
79
|
-
if (!isStrictJsPolicy()) return sanitized;
|
|
80
|
-
const template = document.createElement("template");
|
|
81
|
-
template.innerHTML = sanitized;
|
|
82
|
-
for (const element of Array.from(template.content.querySelectorAll("*"))) {
|
|
83
|
-
for (const attribute of Array.from(element.attributes)) {
|
|
84
|
-
if (!isSafeMutationAttribute(attribute.name, attribute.value)) {
|
|
85
|
-
element.removeAttribute(attribute.name);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return template.innerHTML;
|
|
81
|
+
return sanitizeHtml(html);
|
|
90
82
|
}
|
|
91
83
|
|
|
92
84
|
export function normalizeCssPropertyName(name: string): string {
|
|
@@ -120,106 +112,206 @@ export function isInsertAction(action: MutationAction): boolean {
|
|
|
120
112
|
return action === "insertBefore" || action === "insertAfter";
|
|
121
113
|
}
|
|
122
114
|
|
|
123
|
-
function
|
|
124
|
-
|
|
115
|
+
function elementReference(element: Element): ElementReference {
|
|
116
|
+
const WeakRefClass = (globalThis as typeof globalThis & {
|
|
117
|
+
WeakRef?: new (value: Element) => ElementReference;
|
|
118
|
+
}).WeakRef;
|
|
119
|
+
return WeakRefClass ? new WeakRefClass(element) : { deref: () => element };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function mutationAttributeNames(mutation: Mutation): string[] {
|
|
123
|
+
const names = new Set<string>();
|
|
124
|
+
if (mutation.attribute) names.add(mutation.attribute.name);
|
|
125
|
+
Object.keys(mutation.attributes ?? {}).forEach((name) => names.add(name));
|
|
126
|
+
return Array.from(names);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function canonicalAttributeName(element: Element, name: string): string {
|
|
130
|
+
const namespace = element.namespaceURI;
|
|
131
|
+
return namespace === null || namespace === "http://www.w3.org/1999/xhtml"
|
|
132
|
+
? name.trim().toLowerCase()
|
|
133
|
+
: name;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isContentMutation(mutation: Mutation): boolean {
|
|
137
|
+
return mutation.action === "html" || mutation.action === "text" || mutation.action === "setText";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function mutationStyleNames(mutation: Mutation): string[] {
|
|
141
|
+
if (mutation.action !== "style" && mutation.action !== "setStyle") return [];
|
|
142
|
+
return Object.keys(mutation.styles ?? {}).map(normalizeCssPropertyName);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function captureElementBaseline(
|
|
146
|
+
journal: MutationJournal,
|
|
125
147
|
element: Element,
|
|
126
|
-
|
|
127
|
-
):
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
mutation: Mutation,
|
|
149
|
+
): ElementBaseline {
|
|
150
|
+
let baseline = journal.baselines.get(element);
|
|
151
|
+
if (!baseline) {
|
|
152
|
+
baseline = {
|
|
153
|
+
attributeValues: new Map(),
|
|
154
|
+
contentTouched: false,
|
|
155
|
+
styleAttributeTouched: false,
|
|
156
|
+
styleAttributeValue: null,
|
|
157
|
+
styleValues: new Map(),
|
|
158
|
+
parent: element.parentNode,
|
|
159
|
+
nextSibling: element.nextSibling,
|
|
160
|
+
};
|
|
161
|
+
journal.baselines.set(element, baseline);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const attributeNames = mutationAttributeNames(mutation)
|
|
165
|
+
.map((name) => canonicalAttributeName(element, name));
|
|
166
|
+
const styleNames = mutationStyleNames(mutation);
|
|
167
|
+
let capturedStyleAliasNow = false;
|
|
168
|
+
if ((!baseline.styleAttributeTouched) &&
|
|
169
|
+
(styleNames.length > 0 || attributeNames.includes("style"))) {
|
|
170
|
+
baseline.styleAttributeValue = element.getAttribute("style");
|
|
171
|
+
baseline.styleAttributeTouched = true;
|
|
172
|
+
capturedStyleAliasNow = true;
|
|
173
|
+
}
|
|
174
|
+
for (const name of attributeNames) {
|
|
175
|
+
if (!baseline.attributeValues.has(name)) {
|
|
176
|
+
baseline.attributeValues.set(
|
|
177
|
+
name,
|
|
178
|
+
name === "style" && baseline.styleAttributeTouched
|
|
179
|
+
? baseline.styleAttributeValue
|
|
180
|
+
: element.getAttribute(name),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (styleNames.length > 0) {
|
|
185
|
+
let originalStyle: CSSStyleDeclaration;
|
|
186
|
+
if (capturedStyleAliasNow) {
|
|
187
|
+
// This CSSOM is still pristine: the current style mutation has not run.
|
|
188
|
+
// It also keeps non-DOM Element mocks useful when their attribute and
|
|
189
|
+
// CSSStyleDeclaration views are not wired together like a browser's.
|
|
190
|
+
originalStyle = (element as HTMLElement).style;
|
|
191
|
+
} else if (baseline.styleAttributeTouched) {
|
|
192
|
+
const originalStyleElement = document.createElement("div");
|
|
193
|
+
const originalStyleAttribute = baseline.styleAttributeValue;
|
|
194
|
+
if (originalStyleAttribute !== null && originalStyleAttribute !== undefined) {
|
|
195
|
+
originalStyleElement.setAttribute("style", originalStyleAttribute);
|
|
151
196
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
values: Array.from(names).map((name) => ({
|
|
156
|
-
name,
|
|
157
|
-
value: element.getAttribute(name),
|
|
158
|
-
})),
|
|
159
|
-
});
|
|
160
|
-
break;
|
|
197
|
+
originalStyle = originalStyleElement.style;
|
|
198
|
+
} else {
|
|
199
|
+
originalStyle = (element as HTMLElement).style;
|
|
161
200
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
values: names.map((name) => ({
|
|
170
|
-
name,
|
|
171
|
-
value: style.getPropertyValue(name),
|
|
172
|
-
priority: style.getPropertyPriority(name),
|
|
173
|
-
})),
|
|
174
|
-
});
|
|
175
|
-
break;
|
|
201
|
+
for (const name of styleNames) {
|
|
202
|
+
if (!baseline.styleValues.has(name)) {
|
|
203
|
+
baseline.styleValues.set(name, {
|
|
204
|
+
value: originalStyle.getPropertyValue(name),
|
|
205
|
+
priority: originalStyle.getPropertyPriority(name),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
176
208
|
}
|
|
177
|
-
case "remove":
|
|
178
|
-
snapshots.push({
|
|
179
|
-
kind: "remove",
|
|
180
|
-
element,
|
|
181
|
-
parent: element.parentNode,
|
|
182
|
-
nextSibling: element.nextSibling,
|
|
183
|
-
});
|
|
184
|
-
break;
|
|
185
209
|
}
|
|
210
|
+
if (
|
|
211
|
+
!baseline.contentTouched &&
|
|
212
|
+
isContentMutation(mutation)
|
|
213
|
+
) {
|
|
214
|
+
if (typeof element.innerHTML === "string") baseline.innerHTML = element.innerHTML;
|
|
215
|
+
baseline.textContent = element.textContent;
|
|
216
|
+
baseline.contentTouched = true;
|
|
217
|
+
}
|
|
218
|
+
return baseline;
|
|
186
219
|
}
|
|
187
220
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
221
|
+
type BaselineRestoreScope = {
|
|
222
|
+
attributes: Set<string>;
|
|
223
|
+
content: boolean;
|
|
224
|
+
position: boolean;
|
|
225
|
+
styles: Set<string>;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
function restoreScopeForEntry(
|
|
229
|
+
scope: BaselineRestoreScope,
|
|
230
|
+
element: Element,
|
|
231
|
+
entry: MutationJournalEntry,
|
|
232
|
+
): void {
|
|
233
|
+
if (entry.kind !== "mutation") return;
|
|
234
|
+
scope.position = true;
|
|
235
|
+
if (isContentMutation(entry.mutation)) scope.content = true;
|
|
236
|
+
mutationAttributeNames(entry.mutation)
|
|
237
|
+
.forEach((name) => scope.attributes.add(canonicalAttributeName(element, name)));
|
|
238
|
+
mutationStyleNames(entry.mutation).forEach((name) => scope.styles.add(name));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function restoreElementBaseline(
|
|
242
|
+
element: Element,
|
|
243
|
+
baseline: ElementBaseline,
|
|
244
|
+
scope: BaselineRestoreScope,
|
|
245
|
+
): void {
|
|
246
|
+
if (scope.position && baseline.parent && !element.parentNode) {
|
|
247
|
+
try {
|
|
248
|
+
baseline.parent.insertBefore(element, baseline.nextSibling);
|
|
249
|
+
} catch {
|
|
250
|
+
baseline.parent.appendChild(element);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (scope.content && baseline.contentTouched) {
|
|
254
|
+
if (baseline.innerHTML !== undefined) element.innerHTML = baseline.innerHTML;
|
|
255
|
+
else if (baseline.textContent !== undefined) element.textContent = baseline.textContent;
|
|
256
|
+
}
|
|
257
|
+
const style = (element as HTMLElement).style;
|
|
258
|
+
for (const name of scope.styles) {
|
|
259
|
+
const valueAndPriority = baseline.styleValues.get(name);
|
|
260
|
+
if (!valueAndPriority) continue;
|
|
261
|
+
const { value, priority } = valueAndPriority;
|
|
262
|
+
if (value) style.setProperty(name, value, priority);
|
|
263
|
+
else style.removeProperty(name);
|
|
264
|
+
}
|
|
265
|
+
// Restore attributes after CSSOM properties so a tracked `style` attribute
|
|
266
|
+
// preserves its pristine spelling as well as its pristine declarations.
|
|
267
|
+
for (const name of scope.attributes) {
|
|
268
|
+
if (!baseline.attributeValues.has(name)) continue;
|
|
269
|
+
const value = baseline.attributeValues.get(name);
|
|
270
|
+
if (value === null) element.removeAttribute(name);
|
|
271
|
+
else if (value !== undefined) element.setAttribute(name, value);
|
|
220
272
|
}
|
|
221
273
|
}
|
|
222
274
|
|
|
275
|
+
function artifactIdentity(
|
|
276
|
+
journal: MutationJournal,
|
|
277
|
+
ownerKeys: readonly string[],
|
|
278
|
+
artifactId?: string,
|
|
279
|
+
): string {
|
|
280
|
+
return artifactId ?? ownerKeys[0] ?? journal.fallbackArtifactId;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function appendJournalEntry(
|
|
284
|
+
journal: MutationJournal,
|
|
285
|
+
mutation: Mutation,
|
|
286
|
+
element: Element,
|
|
287
|
+
targetIndex: number,
|
|
288
|
+
ownerKeys: readonly string[],
|
|
289
|
+
artifactId: string | undefined,
|
|
290
|
+
kind: MutationJournalEntry["kind"],
|
|
291
|
+
epoch: number,
|
|
292
|
+
insertKey?: string,
|
|
293
|
+
nodes?: Node[],
|
|
294
|
+
): MutationJournalEntry {
|
|
295
|
+
const entry: MutationJournalEntry = {
|
|
296
|
+
seq: ++journalSequence,
|
|
297
|
+
artifactId: artifactIdentity(journal, ownerKeys, artifactId),
|
|
298
|
+
owners: new Set(ownerKeys),
|
|
299
|
+
target: {
|
|
300
|
+
selector: mutation.selector,
|
|
301
|
+
index: targetIndex,
|
|
302
|
+
el: elementReference(element),
|
|
303
|
+
},
|
|
304
|
+
mutation,
|
|
305
|
+
epoch,
|
|
306
|
+
kind,
|
|
307
|
+
insertKey,
|
|
308
|
+
nodes,
|
|
309
|
+
state: "applied",
|
|
310
|
+
};
|
|
311
|
+
journal.entries.push(entry);
|
|
312
|
+
return entry;
|
|
313
|
+
}
|
|
314
|
+
|
|
223
315
|
export function applyToElement(mutation: Mutation, element: Element): void {
|
|
224
316
|
switch (mutation.action) {
|
|
225
317
|
case "html":
|
|
@@ -231,8 +323,7 @@ export function applyToElement(mutation: Mutation, element: Element): void {
|
|
|
231
323
|
break;
|
|
232
324
|
case "attribute":
|
|
233
325
|
if (mutation.attribute) {
|
|
234
|
-
if (
|
|
235
|
-
isSafeMutationAttribute(mutation.attribute.name, mutation.attribute.value)) {
|
|
326
|
+
if (isSafeMutationAttribute(mutation.attribute.name, mutation.attribute.value)) {
|
|
236
327
|
if (mutation.attribute.value === null) {
|
|
237
328
|
element.removeAttribute(mutation.attribute.name);
|
|
238
329
|
} else {
|
|
@@ -242,7 +333,7 @@ export function applyToElement(mutation: Mutation, element: Element): void {
|
|
|
242
333
|
}
|
|
243
334
|
if (mutation.attributes) {
|
|
244
335
|
for (const [name, value] of Object.entries(mutation.attributes)) {
|
|
245
|
-
if (
|
|
336
|
+
if (!isSafeMutationAttribute(name, value)) continue;
|
|
246
337
|
if (value === null) element.removeAttribute(name);
|
|
247
338
|
else element.setAttribute(name, value);
|
|
248
339
|
}
|
|
@@ -287,74 +378,226 @@ export function markInserted(el: Element, key: string): void {
|
|
|
287
378
|
(el as HTMLElement).setAttribute(INSERT_ATTR, key);
|
|
288
379
|
}
|
|
289
380
|
|
|
290
|
-
function
|
|
381
|
+
function collectInsertedNodes(mutation: Mutation, element: Element): Node[] {
|
|
382
|
+
const parent = element.parentNode;
|
|
383
|
+
if (!parent || typeof mutation.html !== "string") return [];
|
|
384
|
+
const previousSibling = element.previousSibling;
|
|
385
|
+
const nextSibling = element.nextSibling;
|
|
386
|
+
applyToElement(mutation, element);
|
|
387
|
+
const nodes: Node[] = [];
|
|
388
|
+
let current = mutation.action === "insertBefore"
|
|
389
|
+
? (previousSibling ? previousSibling.nextSibling : parent.firstChild)
|
|
390
|
+
: element.nextSibling;
|
|
391
|
+
const boundary = mutation.action === "insertBefore" ? element : nextSibling;
|
|
392
|
+
while (current && current !== boundary) {
|
|
393
|
+
nodes.push(current);
|
|
394
|
+
current = current.nextSibling;
|
|
395
|
+
}
|
|
396
|
+
return nodes;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function insertedElements(nodes: readonly Node[]): Element[] {
|
|
400
|
+
return nodes.filter((node): node is Element => node instanceof Element);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function adoptedInsertNodes(
|
|
404
|
+
journal: MutationJournal,
|
|
405
|
+
element: Element,
|
|
406
|
+
key: string,
|
|
407
|
+
): Node[] {
|
|
408
|
+
for (let index = journal.entries.length - 1; index >= 0; index -= 1) {
|
|
409
|
+
const entry = journal.entries[index];
|
|
410
|
+
if (
|
|
411
|
+
entry.insertKey === key &&
|
|
412
|
+
entry.target.el.deref() === element &&
|
|
413
|
+
entry.nodes?.some((node) => node.parentNode)
|
|
414
|
+
) {
|
|
415
|
+
return entry.nodes;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const parent = element.parentElement;
|
|
419
|
+
if (!parent) return [];
|
|
420
|
+
return Array.from(parent.querySelectorAll("[" + INSERT_ATTR + "]")).filter(
|
|
421
|
+
(candidate) => candidate.getAttribute(INSERT_ATTR) === key,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function applyTrackedMutationWithJournal(
|
|
291
426
|
mutation: Mutation,
|
|
292
427
|
element: Element,
|
|
293
|
-
|
|
428
|
+
journal: MutationJournal,
|
|
294
429
|
key?: string,
|
|
295
|
-
|
|
430
|
+
ownerKeys: readonly string[] = [],
|
|
431
|
+
artifactId?: string,
|
|
432
|
+
targetIndex = 0,
|
|
433
|
+
epoch = 0,
|
|
434
|
+
): { inserted: Element | null; entry?: MutationJournalEntry } {
|
|
296
435
|
return runMuted(() => {
|
|
436
|
+
captureElementBaseline(journal, element, mutation);
|
|
297
437
|
if (isInsertAction(mutation.action)) {
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
(node): node is Element => node instanceof Element,
|
|
438
|
+
const nodes = collectInsertedNodes(mutation, element);
|
|
439
|
+
const elements = insertedElements(nodes);
|
|
440
|
+
if (key) elements.forEach((inserted) => markInserted(inserted, key));
|
|
441
|
+
if (!nodes.length) return { inserted: null };
|
|
442
|
+
const entry = appendJournalEntry(
|
|
443
|
+
journal,
|
|
444
|
+
mutation,
|
|
445
|
+
element,
|
|
446
|
+
targetIndex,
|
|
447
|
+
ownerKeys,
|
|
448
|
+
artifactId,
|
|
449
|
+
"insert",
|
|
450
|
+
epoch,
|
|
451
|
+
key,
|
|
452
|
+
nodes,
|
|
314
453
|
);
|
|
315
|
-
|
|
316
|
-
if (insertedNodes.length) snapshots.push({ kind: "insert", nodes: insertedNodes });
|
|
317
|
-
return insertedElements[0] ?? null;
|
|
454
|
+
return { inserted: elements[0] ?? null, entry };
|
|
318
455
|
}
|
|
319
|
-
|
|
456
|
+
const entry = appendJournalEntry(
|
|
457
|
+
journal,
|
|
458
|
+
mutation,
|
|
459
|
+
element,
|
|
460
|
+
targetIndex,
|
|
461
|
+
ownerKeys,
|
|
462
|
+
artifactId,
|
|
463
|
+
"mutation",
|
|
464
|
+
epoch,
|
|
465
|
+
);
|
|
320
466
|
applyToElement(mutation, element);
|
|
321
|
-
return null;
|
|
467
|
+
return { inserted: null, entry };
|
|
322
468
|
});
|
|
323
469
|
}
|
|
324
470
|
|
|
325
|
-
export function applyTrackedMutation(
|
|
326
|
-
|
|
471
|
+
export function applyTrackedMutation(
|
|
472
|
+
mutation: Mutation,
|
|
473
|
+
element: Element,
|
|
474
|
+
key?: string,
|
|
475
|
+
ownerKeys: readonly string[] = [],
|
|
476
|
+
artifactId?: string,
|
|
477
|
+
onJournal?: (entry: MutationJournalEntry) => void,
|
|
478
|
+
epoch = 0,
|
|
479
|
+
): Element | null {
|
|
480
|
+
let targetIndex = 0;
|
|
481
|
+
try {
|
|
482
|
+
targetIndex = queryAllDeep(mutation.selector).indexOf(element);
|
|
483
|
+
if (targetIndex < 0) targetIndex = 0;
|
|
484
|
+
} catch {}
|
|
485
|
+
if (key && isInsertAction(mutation.action) && isInsertDuplicate(element, key)) {
|
|
486
|
+
const nodes = adoptedInsertNodes(mutationJournal, element, key);
|
|
487
|
+
const entry = appendJournalEntry(
|
|
488
|
+
mutationJournal,
|
|
489
|
+
mutation,
|
|
490
|
+
element,
|
|
491
|
+
targetIndex,
|
|
492
|
+
ownerKeys,
|
|
493
|
+
artifactId,
|
|
494
|
+
"adopt-insert",
|
|
495
|
+
epoch,
|
|
496
|
+
key,
|
|
497
|
+
nodes,
|
|
498
|
+
);
|
|
499
|
+
onJournal?.(entry);
|
|
500
|
+
return insertedElements(nodes)[0] ?? null;
|
|
501
|
+
}
|
|
502
|
+
const result = applyTrackedMutationWithJournal(
|
|
503
|
+
mutation,
|
|
504
|
+
element,
|
|
505
|
+
mutationJournal,
|
|
506
|
+
key,
|
|
507
|
+
ownerKeys,
|
|
508
|
+
artifactId,
|
|
509
|
+
targetIndex,
|
|
510
|
+
epoch,
|
|
511
|
+
);
|
|
512
|
+
if (result.entry) onJournal?.(result.entry);
|
|
513
|
+
return result.inserted;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Reject a just-applied guarded mutation without disturbing earlier treatments. */
|
|
517
|
+
export function rejectTrackedMutation(
|
|
518
|
+
entry: MutationJournalEntry | undefined,
|
|
519
|
+
element: Element,
|
|
520
|
+
innerHTML: string,
|
|
521
|
+
): void {
|
|
522
|
+
runMuted(() => { element.innerHTML = innerHTML; });
|
|
523
|
+
if (!entry) return;
|
|
524
|
+
priceRejectedMutations.add(entry.mutation);
|
|
525
|
+
entry.state = "invalidated";
|
|
526
|
+
mutationJournal.entries = mutationJournal.entries.filter((candidate) => candidate !== entry);
|
|
527
|
+
// Fail closed at mutation level: a price-violating mutation is suppressed on
|
|
528
|
+
// every element, so already-applied sibling targets roll back instead of
|
|
529
|
+
// drifting outside reassert coverage.
|
|
530
|
+
const doomed = new Set(
|
|
531
|
+
mutationJournal.entries.filter((candidate) => candidate.mutation === entry.mutation),
|
|
532
|
+
);
|
|
533
|
+
for (const candidate of doomed) candidate.state = "invalidated";
|
|
534
|
+
if (doomed.size > 0) rollbackJournalEntries(mutationJournal, doomed);
|
|
327
535
|
}
|
|
328
536
|
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
// hydration revert restores one of these pre-states (the server original,
|
|
332
|
-
// or — for stacked treatments — an earlier treatment's content), while
|
|
333
|
-
// variant-owned widget activity produces content none of them ever had.
|
|
537
|
+
// The sentinel treats the baseline and earlier journal values as framework
|
|
538
|
+
// restore targets. Other content can be variant-owned widget state.
|
|
334
539
|
export function matchesOriginalInnerHtmlSnapshot(element: Element, current: string): boolean {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
typeof snapshot.value === "string"
|
|
341
|
-
) {
|
|
342
|
-
if (snapshot.value === current) return true;
|
|
540
|
+
if (mutationJournal.baselines.get(element)?.innerHTML === current) return true;
|
|
541
|
+
for (const entry of mutationJournal.entries) {
|
|
542
|
+
if (entry.target.el.deref() !== element || entry.mutation.action !== "html") continue;
|
|
543
|
+
if (typeof entry.mutation.html === "string" && sanitizeMutationHtml(entry.mutation.html) === current) {
|
|
544
|
+
return true;
|
|
343
545
|
}
|
|
344
546
|
}
|
|
345
547
|
return false;
|
|
346
548
|
}
|
|
347
549
|
|
|
348
|
-
function
|
|
550
|
+
function applyPriceGuardedMutation(
|
|
551
|
+
mutation: Mutation,
|
|
552
|
+
element: Element,
|
|
553
|
+
journal: MutationJournal,
|
|
554
|
+
key: string,
|
|
555
|
+
options: ApplyOptions,
|
|
556
|
+
index: number,
|
|
557
|
+
targetIndex: number,
|
|
558
|
+
): void {
|
|
559
|
+
const guardedPrices = extractCurrencyTokens(element.textContent ?? "");
|
|
560
|
+
const guardedSnapshot = guardedPrices.length > 0 ? element.innerHTML : null;
|
|
561
|
+
const result = applyTrackedMutationWithJournal(
|
|
562
|
+
mutation,
|
|
563
|
+
element,
|
|
564
|
+
journal,
|
|
565
|
+
key,
|
|
566
|
+
options.ownerKeys,
|
|
567
|
+
options.artifactId,
|
|
568
|
+
targetIndex,
|
|
569
|
+
options.epoch ?? 0,
|
|
570
|
+
);
|
|
571
|
+
if (guardedSnapshot !== null) {
|
|
572
|
+
const remaining = extractCurrencyTokens(element.textContent ?? "");
|
|
573
|
+
const preserved = guardedPrices.every((token) =>
|
|
574
|
+
remaining.some((candidate) =>
|
|
575
|
+
candidate.symbol === token.symbol && candidate.cents === token.cents));
|
|
576
|
+
if (!preserved) {
|
|
577
|
+
rejectTrackedMutation(result.entry, element, guardedSnapshot);
|
|
578
|
+
options.onPricePreservationViolation?.({
|
|
579
|
+
mutation,
|
|
580
|
+
selector: mutation.selector,
|
|
581
|
+
originalPriceTokens: guardedPrices,
|
|
582
|
+
});
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
options.onApplied?.(mutation, element, index, result.entry?.seq);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function applyMutationsWithJournal(
|
|
349
590
|
mutations: Mutation[],
|
|
350
591
|
options: ApplyOptions,
|
|
351
|
-
|
|
592
|
+
journal: MutationJournal,
|
|
352
593
|
): void {
|
|
353
594
|
const root = options.root || document;
|
|
354
595
|
mutations.forEach((mutation, index) => {
|
|
355
|
-
let elements:
|
|
596
|
+
let elements: Element[];
|
|
356
597
|
try {
|
|
357
|
-
elements = root
|
|
598
|
+
elements = root === document
|
|
599
|
+
? queryAllDeep(mutation.selector)
|
|
600
|
+
: Array.from(root.querySelectorAll(mutation.selector));
|
|
358
601
|
} catch {
|
|
359
602
|
options.onMiss?.(mutation, index);
|
|
360
603
|
return;
|
|
@@ -363,29 +606,77 @@ function applyMutationsWithSnapshots(
|
|
|
363
606
|
options.onMiss?.(mutation, index);
|
|
364
607
|
return;
|
|
365
608
|
}
|
|
366
|
-
elements.forEach((element) => {
|
|
609
|
+
elements.forEach((element, targetIndex) => {
|
|
367
610
|
const key = getMutationKey(mutation, index);
|
|
368
611
|
if (isInsertAction(mutation.action)) {
|
|
369
|
-
if (isInsertDuplicate(element, key))
|
|
370
|
-
|
|
371
|
-
|
|
612
|
+
if (isInsertDuplicate(element, key)) {
|
|
613
|
+
const entry = appendJournalEntry(
|
|
614
|
+
journal,
|
|
615
|
+
mutation,
|
|
616
|
+
element,
|
|
617
|
+
targetIndex,
|
|
618
|
+
options.ownerKeys ?? [],
|
|
619
|
+
options.artifactId,
|
|
620
|
+
"adopt-insert",
|
|
621
|
+
options.epoch ?? 0,
|
|
622
|
+
key,
|
|
623
|
+
adoptedInsertNodes(journal, element, key),
|
|
624
|
+
);
|
|
625
|
+
options.onApplied?.(mutation, element, index, entry.seq);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const result = applyTrackedMutationWithJournal(
|
|
629
|
+
mutation,
|
|
630
|
+
element,
|
|
631
|
+
journal,
|
|
632
|
+
key,
|
|
633
|
+
options.ownerKeys,
|
|
634
|
+
options.artifactId,
|
|
635
|
+
targetIndex,
|
|
636
|
+
options.epoch ?? 0,
|
|
637
|
+
);
|
|
638
|
+
options.onApplied?.(mutation, element, index, result.entry?.seq);
|
|
372
639
|
return;
|
|
373
640
|
}
|
|
374
|
-
|
|
375
|
-
|
|
641
|
+
// Content changes are guarded; hiding or styling a price element can be
|
|
642
|
+
// a legitimate compare-at-price treatment and is intentionally allowed.
|
|
643
|
+
const priceBearingContent = mutation.action === "html"
|
|
644
|
+
|| mutation.action === "text"
|
|
645
|
+
|| mutation.action === "setText";
|
|
646
|
+
if (options.preservePrices && !options.priceTestExempt && priceBearingContent) {
|
|
647
|
+
applyPriceGuardedMutation(mutation, element, journal, key, options, index, targetIndex);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const result = applyTrackedMutationWithJournal(
|
|
651
|
+
mutation,
|
|
652
|
+
element,
|
|
653
|
+
journal,
|
|
654
|
+
key,
|
|
655
|
+
options.ownerKeys,
|
|
656
|
+
options.artifactId,
|
|
657
|
+
targetIndex,
|
|
658
|
+
options.epoch ?? 0,
|
|
659
|
+
);
|
|
660
|
+
options.onApplied?.(mutation, element, index, result.entry?.seq);
|
|
376
661
|
});
|
|
377
662
|
});
|
|
378
663
|
}
|
|
379
664
|
|
|
380
665
|
export function applyMutations(mutations: Mutation[], options: ApplyOptions = {}): void {
|
|
381
|
-
|
|
666
|
+
applyMutationsWithJournal(mutations, options, mutationJournal);
|
|
382
667
|
}
|
|
383
668
|
|
|
384
669
|
export function reassertNonStructuralMutations(mutations: Mutation[], options: ApplyOptions = {}): void {
|
|
385
670
|
const root = options.root || document;
|
|
386
671
|
mutations.forEach((mutation, index) => {
|
|
387
|
-
if (
|
|
388
|
-
|
|
672
|
+
if (
|
|
673
|
+
isInsertAction(mutation.action)
|
|
674
|
+
|| mutation.action === "remove"
|
|
675
|
+
|| priceRejectedMutations.has(mutation)
|
|
676
|
+
) return;
|
|
677
|
+
const elements = root === document
|
|
678
|
+
? queryAllDeep(mutation.selector)
|
|
679
|
+
: Array.from(root.querySelectorAll(mutation.selector));
|
|
389
680
|
elements.forEach((element) => {
|
|
390
681
|
runMuted(() => applyToElement(mutation, element));
|
|
391
682
|
options.onApplied?.(mutation, element, index);
|
|
@@ -393,14 +684,231 @@ export function reassertNonStructuralMutations(mutations: Mutation[], options: A
|
|
|
393
684
|
});
|
|
394
685
|
}
|
|
395
686
|
|
|
396
|
-
|
|
397
|
-
|
|
687
|
+
function resolveJournalTarget(
|
|
688
|
+
journal: MutationJournal,
|
|
689
|
+
entry: MutationJournalEntry,
|
|
690
|
+
): Element | undefined {
|
|
691
|
+
const current = entry.target.el.deref();
|
|
692
|
+
if (current && (entry.mutation.action === "remove" || current.isConnected !== false)) {
|
|
693
|
+
return current;
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
const replacement = queryAllDeep(entry.target.selector)[entry.target.index];
|
|
697
|
+
if (replacement) {
|
|
698
|
+
captureElementBaseline(journal, replacement, entry.mutation);
|
|
699
|
+
entry.target.el = elementReference(replacement);
|
|
700
|
+
return replacement;
|
|
701
|
+
}
|
|
702
|
+
} catch {}
|
|
703
|
+
return current;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function replayInsertEntry(
|
|
707
|
+
journal: MutationJournal,
|
|
708
|
+
entry: MutationJournalEntry,
|
|
709
|
+
element: Element,
|
|
710
|
+
): void {
|
|
711
|
+
const key = entry.insertKey;
|
|
712
|
+
if (key && isInsertDuplicate(element, key)) {
|
|
713
|
+
entry.nodes = adoptedInsertNodes(journal, element, key);
|
|
714
|
+
entry.kind = "adopt-insert";
|
|
715
|
+
entry.state = "applied";
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const nodes = collectInsertedNodes(entry.mutation, element);
|
|
719
|
+
if (key) insertedElements(nodes).forEach((inserted) => markInserted(inserted, key));
|
|
720
|
+
entry.nodes = nodes;
|
|
721
|
+
entry.kind = "insert";
|
|
722
|
+
entry.state = "applied";
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function replayTouchedTargets(
|
|
726
|
+
journal: MutationJournal,
|
|
727
|
+
touched: Set<Element>,
|
|
728
|
+
restoreScopes: Map<Element, BaselineRestoreScope>,
|
|
729
|
+
): void {
|
|
730
|
+
for (const [element, scope] of restoreScopes) {
|
|
731
|
+
const baseline = journal.baselines.get(element);
|
|
732
|
+
if (baseline) restoreElementBaseline(element, baseline, scope);
|
|
733
|
+
}
|
|
734
|
+
for (const entry of journal.entries) {
|
|
735
|
+
const element = resolveJournalTarget(journal, entry);
|
|
736
|
+
if (!element || !touched.has(element)) continue;
|
|
737
|
+
if (entry.kind === "insert" || entry.kind === "adopt-insert") {
|
|
738
|
+
replayInsertEntry(journal, entry, element);
|
|
739
|
+
} else {
|
|
740
|
+
applyToElement(entry.mutation, element);
|
|
741
|
+
entry.state = "applied";
|
|
742
|
+
replayReassertSentinelEntry(entry.seq, element);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function addRestoreScope(
|
|
748
|
+
scopes: Map<Element, BaselineRestoreScope>,
|
|
749
|
+
element: Element,
|
|
750
|
+
entry: MutationJournalEntry,
|
|
751
|
+
): void {
|
|
752
|
+
let scope = scopes.get(element);
|
|
753
|
+
if (!scope) {
|
|
754
|
+
scope = { attributes: new Set(), content: false, position: false, styles: new Set() };
|
|
755
|
+
scopes.set(element, scope);
|
|
756
|
+
}
|
|
757
|
+
restoreScopeForEntry(scope, element, entry);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function pruneElementBaseline(journal: MutationJournal, element: Element): void {
|
|
761
|
+
const baseline = journal.baselines.get(element);
|
|
762
|
+
if (!baseline) return;
|
|
763
|
+
const remaining = journal.entries.filter((entry) => entry.target.el.deref() === element);
|
|
764
|
+
if (remaining.length === 0) {
|
|
765
|
+
journal.baselines.delete(element);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const contentNeeded = remaining.some((entry) =>
|
|
769
|
+
entry.kind === "mutation" && isContentMutation(entry.mutation));
|
|
770
|
+
if (!contentNeeded) {
|
|
771
|
+
baseline.contentTouched = false;
|
|
772
|
+
delete baseline.innerHTML;
|
|
773
|
+
delete baseline.textContent;
|
|
774
|
+
}
|
|
775
|
+
const attributeNames = new Set(remaining.flatMap((entry) =>
|
|
776
|
+
entry.kind === "mutation"
|
|
777
|
+
? mutationAttributeNames(entry.mutation).map((name) => canonicalAttributeName(element, name))
|
|
778
|
+
: []));
|
|
779
|
+
for (const name of baseline.attributeValues.keys()) {
|
|
780
|
+
if (!attributeNames.has(name)) baseline.attributeValues.delete(name);
|
|
781
|
+
}
|
|
782
|
+
const styleNames = new Set(remaining.flatMap((entry) =>
|
|
783
|
+
entry.kind === "mutation" ? mutationStyleNames(entry.mutation) : []));
|
|
784
|
+
for (const name of baseline.styleValues.keys()) {
|
|
785
|
+
if (!styleNames.has(name)) baseline.styleValues.delete(name);
|
|
786
|
+
}
|
|
787
|
+
if (!attributeNames.has("style") && styleNames.size === 0) {
|
|
788
|
+
baseline.styleAttributeTouched = false;
|
|
789
|
+
baseline.styleAttributeValue = null;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function rollbackJournalEntries(
|
|
794
|
+
journal: MutationJournal,
|
|
795
|
+
doomed: Set<MutationJournalEntry>,
|
|
796
|
+
options: { preserveReplacementTargets?: boolean } = {},
|
|
797
|
+
): void {
|
|
798
|
+
if (doomed.size === 0) return;
|
|
799
|
+
const touched = new Set<Element>();
|
|
800
|
+
const restoreScopes = new Map<Element, BaselineRestoreScope>();
|
|
801
|
+
for (const entry of doomed) {
|
|
802
|
+
// A teardown owns the exact node Apex mutated, not a later framework node
|
|
803
|
+
// that happens to match the same selector/index. SPA frameworks commonly
|
|
804
|
+
// replace route subtrees before reInit() reconciles Page activation. Using
|
|
805
|
+
// resolveJournalTarget() here would adopt that replacement and restore the
|
|
806
|
+
// old route's baseline into it (innerHTML/textContent included), detaching
|
|
807
|
+
// the framework's listeners. When the selector now resolves to a different
|
|
808
|
+
// node, the disconnected target has been retired and needs no rollback.
|
|
809
|
+
const element = options.preserveReplacementTargets
|
|
810
|
+
? entry.target.el.deref()
|
|
811
|
+
: resolveJournalTarget(journal, entry);
|
|
812
|
+
let supersededByFrameworkNode = false;
|
|
813
|
+
if (options.preserveReplacementTargets && element?.isConnected === false) {
|
|
814
|
+
try {
|
|
815
|
+
const current = document.querySelectorAll(entry.target.selector)[entry.target.index];
|
|
816
|
+
supersededByFrameworkNode = Boolean(current && current !== element);
|
|
817
|
+
} catch {}
|
|
818
|
+
}
|
|
819
|
+
if (element && !supersededByFrameworkNode) {
|
|
820
|
+
touched.add(element);
|
|
821
|
+
addRestoreScope(restoreScopes, element, entry);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
for (let index = journal.entries.length - 1; index >= 0; index -= 1) {
|
|
825
|
+
if (doomed.has(journal.entries[index])) journal.entries.splice(index, 1);
|
|
826
|
+
}
|
|
398
827
|
runMuted(() => {
|
|
399
|
-
for (
|
|
400
|
-
|
|
828
|
+
for (const entry of doomed) {
|
|
829
|
+
for (const node of entry.nodes ?? []) {
|
|
830
|
+
const survives = journal.entries.some((candidate) => candidate.nodes?.includes(node));
|
|
831
|
+
if (!survives) node.parentNode?.removeChild(node);
|
|
832
|
+
}
|
|
401
833
|
}
|
|
834
|
+
replayTouchedTargets(journal, touched, restoreScopes);
|
|
402
835
|
});
|
|
403
|
-
|
|
836
|
+
touched.forEach((element) => pruneElementBaseline(journal, element));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function resetJournal(journal: MutationJournal): void {
|
|
840
|
+
const doomed = new Set(journal.entries);
|
|
841
|
+
rollbackJournalEntries(journal, doomed);
|
|
842
|
+
journal.baselines = new WeakMap();
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export function resetAppliedMutations(): void {
|
|
846
|
+
resetReassertSentinel();
|
|
847
|
+
resetJournal(mutationJournal);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export function resetAppliedMutationsForOwner(
|
|
851
|
+
ownerKey: string,
|
|
852
|
+
artifactId?: string,
|
|
853
|
+
options: { preserveReplacementTargets?: boolean } = {},
|
|
854
|
+
): void {
|
|
855
|
+
const doomed = new Set<MutationJournalEntry>();
|
|
856
|
+
for (const entry of mutationJournal.entries) {
|
|
857
|
+
if (artifactId !== undefined && entry.artifactId !== artifactId) continue;
|
|
858
|
+
if (!entry.owners.delete(ownerKey)) continue;
|
|
859
|
+
if (entry.owners.size === 0) doomed.add(entry);
|
|
860
|
+
}
|
|
861
|
+
resetReassertSentinelForOwner(ownerKey, artifactId);
|
|
862
|
+
rollbackJournalEntries(mutationJournal, doomed, options);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export function addAppliedMutationOwners(
|
|
866
|
+
existingOwnerKeys: ReadonlySet<string>,
|
|
867
|
+
ownerKeys: readonly string[],
|
|
868
|
+
artifactId?: string,
|
|
869
|
+
): void {
|
|
870
|
+
if (existingOwnerKeys.size === 0 || ownerKeys.length === 0) return;
|
|
871
|
+
for (const entry of mutationJournal.entries) {
|
|
872
|
+
if (artifactId !== undefined && entry.artifactId !== artifactId) continue;
|
|
873
|
+
if (
|
|
874
|
+
!Array.from(existingOwnerKeys).some((ownerKey) =>
|
|
875
|
+
entry.owners.has(ownerKey)
|
|
876
|
+
)
|
|
877
|
+
) continue;
|
|
878
|
+
ownerKeys.forEach((ownerKey) => entry.owners.add(ownerKey));
|
|
879
|
+
}
|
|
880
|
+
addReassertSentinelOwners(existingOwnerKeys, ownerKeys, artifactId);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export function invalidateArtifact(artifactId: string): void {
|
|
884
|
+
mutationJournal.entries.forEach((entry) => {
|
|
885
|
+
if (entry.artifactId === artifactId) entry.state = "invalidated";
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export function replayArtifact(artifactId: string): void {
|
|
890
|
+
const selected = mutationJournal.entries.filter((entry) => entry.artifactId === artifactId);
|
|
891
|
+
if (!selected.length) return;
|
|
892
|
+
const touched = new Set<Element>();
|
|
893
|
+
const restoreScopes = new Map<Element, BaselineRestoreScope>();
|
|
894
|
+
for (const entry of selected) {
|
|
895
|
+
entry.state = "invalidated";
|
|
896
|
+
const element = resolveJournalTarget(mutationJournal, entry);
|
|
897
|
+
if (element) {
|
|
898
|
+
touched.add(element);
|
|
899
|
+
addRestoreScope(restoreScopes, element, entry);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
runMuted(() => replayTouchedTargets(mutationJournal, touched, restoreScopes));
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export function replayForOwner(ownerKey: string): void {
|
|
906
|
+
const artifactIds = new Set(
|
|
907
|
+
mutationJournal.entries
|
|
908
|
+
.filter((entry) => entry.owners.has(ownerKey))
|
|
909
|
+
.map((entry) => entry.artifactId),
|
|
910
|
+
);
|
|
911
|
+
artifactIds.forEach(replayArtifact);
|
|
404
912
|
}
|
|
405
913
|
|
|
406
914
|
export type MutationApplicationContext = {
|
|
@@ -410,32 +918,33 @@ export type MutationApplicationContext = {
|
|
|
410
918
|
};
|
|
411
919
|
|
|
412
920
|
/**
|
|
413
|
-
* Owns only the
|
|
414
|
-
* restores
|
|
415
|
-
* touching shopper assignment mutations tracked by the main SDK runtime.
|
|
921
|
+
* Owns only the journal entries created through this context. Rollback
|
|
922
|
+
* restores its baseline without touching shopper assignment entries.
|
|
416
923
|
*/
|
|
417
924
|
export function createMutationApplicationContext(): MutationApplicationContext {
|
|
418
|
-
const
|
|
925
|
+
const journal: MutationJournal = {
|
|
926
|
+
entries: [],
|
|
927
|
+
baselines: new WeakMap(),
|
|
928
|
+
fallbackArtifactId: "context-" + ++applicationContextSequence,
|
|
929
|
+
};
|
|
419
930
|
return {
|
|
420
931
|
apply(mutations, options = {}) {
|
|
421
|
-
|
|
932
|
+
applyMutationsWithJournal(mutations, options, journal);
|
|
422
933
|
},
|
|
423
934
|
rollback() {
|
|
424
|
-
|
|
425
|
-
for (let i = snapshots.length - 1; i >= 0; i -= 1) {
|
|
426
|
-
restoreSnapshot(snapshots[i]);
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
snapshots.length = 0;
|
|
935
|
+
resetJournal(journal);
|
|
430
936
|
},
|
|
431
937
|
get snapshotCount() {
|
|
432
|
-
return
|
|
938
|
+
return journal.entries.length;
|
|
433
939
|
},
|
|
434
940
|
};
|
|
435
941
|
}
|
|
436
942
|
|
|
437
943
|
/** @internal — exposed for unit testing only. Tree-shaken from production build. */
|
|
438
944
|
export const __test = {
|
|
945
|
+
baselineAttributeNames(element: Element): string[] {
|
|
946
|
+
return Array.from(mutationJournal.baselines.get(element)?.attributeValues.keys() ?? []);
|
|
947
|
+
},
|
|
439
948
|
normalizeCssPropertyName,
|
|
440
949
|
};
|
|
441
950
|
|