@drip-apex/sdk 0.1.0 → 0.3.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 +19283 -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 +19267 -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 +16636 -13533
- 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 +28 -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 +2071 -0
- package/src/consent-evaluation-borlabs.test.ts +41 -0
- package/src/consent-evaluation-cookiescript.test.ts +50 -0
- package/src/consent-evaluation.ts +151 -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/hash.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { hashToFloat } from "@drip/shared-runtime/bucketing";
|
|
2
|
+
import { edgeRuntimeActive } from "./edge-runtime";
|
|
3
|
+
|
|
4
|
+
// The bucketing kernel lives in @drip/shared-runtime/bucketing (#2476). This
|
|
5
|
+
// module keeps re-exporting it so SDK call sites and the holdout gate below
|
|
6
|
+
// stay unchanged.
|
|
7
|
+
export { hashToFloat, murmurhash3_32_gc, variationHashToFloat } from "@drip/shared-runtime/bucketing";
|
|
8
|
+
|
|
1
9
|
const UID_KEY = "drip_uid";
|
|
2
10
|
|
|
3
11
|
type StorageMode = "localStorage" | "sessionStorage" | "cookie" | "memory";
|
|
@@ -7,62 +15,17 @@ let encodeStorageValues = false;
|
|
|
7
15
|
let cookieDomain = "";
|
|
8
16
|
let memoryUserId: string | null = null;
|
|
9
17
|
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
while (i < bytes) {
|
|
19
|
-
let k1 =
|
|
20
|
-
(key.charCodeAt(i) & 0xff) |
|
|
21
|
-
((key.charCodeAt(++i) & 0xff) << 8) |
|
|
22
|
-
((key.charCodeAt(++i) & 0xff) << 16) |
|
|
23
|
-
((key.charCodeAt(++i) & 0xff) << 24);
|
|
24
|
-
++i;
|
|
25
|
-
|
|
26
|
-
k1 = Math.imul(k1, c1);
|
|
27
|
-
k1 = (k1 << 15) | (k1 >>> 17);
|
|
28
|
-
k1 = Math.imul(k1, c2);
|
|
29
|
-
|
|
30
|
-
h1 ^= k1;
|
|
31
|
-
h1 = (h1 << 13) | (h1 >>> 19);
|
|
32
|
-
h1 = Math.imul(h1, 5) + 0xe6546b64;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let k1 = 0;
|
|
36
|
-
switch (remainder) {
|
|
37
|
-
case 3:
|
|
38
|
-
k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
|
|
39
|
-
case 2:
|
|
40
|
-
k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
|
|
41
|
-
case 1:
|
|
42
|
-
k1 ^= key.charCodeAt(i) & 0xff;
|
|
43
|
-
k1 = Math.imul(k1, c1);
|
|
44
|
-
k1 = (k1 << 15) | (k1 >>> 17);
|
|
45
|
-
k1 = Math.imul(k1, c2);
|
|
46
|
-
h1 ^= k1;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
h1 ^= key.length;
|
|
50
|
-
h1 ^= h1 >>> 16;
|
|
51
|
-
h1 = Math.imul(h1, 0x85ebca6b);
|
|
52
|
-
h1 ^= h1 >>> 13;
|
|
53
|
-
h1 = Math.imul(h1, 0xc2b2ae35);
|
|
54
|
-
h1 ^= h1 >>> 16;
|
|
55
|
-
|
|
56
|
-
return h1 >>> 0;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function hashToFloat(seed: string, value: string): number {
|
|
60
|
-
const hash = murmurhash3_32_gc(seed + value, 1);
|
|
61
|
-
return (hash % 10000) / 10000;
|
|
18
|
+
export function isHoldoutVisitor(
|
|
19
|
+
config: { holdout_v1_enabled?: unknown; holdout_pct?: unknown },
|
|
20
|
+
visitorId: string,
|
|
21
|
+
): boolean {
|
|
22
|
+
const pct = config.holdout_pct;
|
|
23
|
+
return config.holdout_v1_enabled === true && typeof pct === "number" &&
|
|
24
|
+
pct > 0 && pct <= 10 && hashToFloat("apex:global-holdout:v1", visitorId) < pct / 100;
|
|
62
25
|
}
|
|
63
26
|
|
|
64
|
-
export function
|
|
65
|
-
return
|
|
27
|
+
export function applyHoldoutGate<T>(config: Parameters<typeof isHoldoutVisitor>[0], visitorId: string, values: T[]): T[] {
|
|
28
|
+
return isHoldoutVisitor(config, visitorId) ? [] : values;
|
|
66
29
|
}
|
|
67
30
|
|
|
68
31
|
function bytesToHex(bytes: Uint8Array): string {
|
|
@@ -195,6 +158,13 @@ function writeCookie(value: string): void {
|
|
|
195
158
|
|
|
196
159
|
export function getUserId(provided?: string): string {
|
|
197
160
|
if (provided) return provided;
|
|
161
|
+
if (edgeRuntimeActive()) {
|
|
162
|
+
const edgeUserId = readCookie();
|
|
163
|
+
if (edgeUserId) {
|
|
164
|
+
if (readStorage() !== edgeUserId) writeStorage(edgeUserId);
|
|
165
|
+
return edgeUserId;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
198
168
|
const existing = readStorage() || readCookie();
|
|
199
169
|
if (existing) return existing;
|
|
200
170
|
const created = generateId();
|
|
@@ -203,6 +173,11 @@ export function getUserId(provided?: string): string {
|
|
|
203
173
|
return created;
|
|
204
174
|
}
|
|
205
175
|
|
|
176
|
+
/** Reads an existing durable identity without minting or persisting a new one. */
|
|
177
|
+
export function getPersistedUserId(): string | null {
|
|
178
|
+
return readStorage() || readCookie();
|
|
179
|
+
}
|
|
180
|
+
|
|
206
181
|
export function getEphemeralUserId(provided?: string): string {
|
|
207
182
|
return provided || generateId();
|
|
208
183
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { applyHoldoutGate, isHoldoutVisitor } from "./hash";
|
|
3
|
+
|
|
4
|
+
describe("global holdout v1", () => {
|
|
5
|
+
it("is deterministic and sticky for a visitor", () => {
|
|
6
|
+
const config = { holdout_v1_enabled: true, holdout_pct: 5 };
|
|
7
|
+
const first = isHoldoutVisitor(config, "visitor-123");
|
|
8
|
+
expect(isHoldoutVisitor(config, "visitor-123")).toBe(first);
|
|
9
|
+
expect(Array.from({ length: 1000 }, (_, i) => isHoldoutVisitor(config, `v-${i}`)).filter(Boolean).length)
|
|
10
|
+
.toBeGreaterThan(20);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("fails open for disabled, missing, or malformed config", () => {
|
|
14
|
+
expect(isHoldoutVisitor({}, "visitor")).toBe(false);
|
|
15
|
+
expect(isHoldoutVisitor({ holdout_v1_enabled: true }, "visitor")).toBe(false);
|
|
16
|
+
expect(isHoldoutVisitor({ holdout_v1_enabled: true, holdout_pct: 11 }, "visitor")).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("short-circuits every engine input only for holdout visitors", () => {
|
|
20
|
+
const visitor = Array.from({ length: 1000 }, (_, i) => `v-${i}`).find((id) =>
|
|
21
|
+
isHoldoutVisitor({ holdout_v1_enabled: true, holdout_pct: 10 }, id));
|
|
22
|
+
expect(visitor).toBeTruthy();
|
|
23
|
+
expect(applyHoldoutGate({ holdout_v1_enabled: true, holdout_pct: 10 }, visitor!, ["experiment"])).toEqual([]);
|
|
24
|
+
expect(applyHoldoutGate({}, visitor!, ["experiment"])).toEqual(["experiment"]);
|
|
25
|
+
});
|
|
26
|
+
});
|
package/src/hydration.test.ts
CHANGED
|
@@ -136,6 +136,50 @@ describe("hydration coexistence gate", () => {
|
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
+
it("reports head-install delay from the original outer clock", async () => {
|
|
140
|
+
Object.defineProperty(document, "readyState", { value: "loading", configurable: true });
|
|
141
|
+
try {
|
|
142
|
+
const gate = awaitHydration(1_000, { waitForHydration: true });
|
|
143
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
144
|
+
document.body.innerHTML = '<div id="__next"></div>';
|
|
145
|
+
Object.defineProperty(document, "readyState", { value: "interactive", configurable: true });
|
|
146
|
+
document.dispatchEvent(new Event("DOMContentLoaded"));
|
|
147
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
148
|
+
signalHydrated();
|
|
149
|
+
await expect(gate).resolves.toBeUndefined();
|
|
150
|
+
|
|
151
|
+
// The quiet detector may legitimately resolve before the explicit
|
|
152
|
+
// signal; the outer-clock guarantee is that the delay counts from the
|
|
153
|
+
// ORIGINAL start (>= 400ms pre-DCL wait). The restarted-clock bug
|
|
154
|
+
// reported only the post-recursion remainder (~80ms).
|
|
155
|
+
const applyDelayedMs = (window as Window & { __dripPerf?: Record<string, unknown> }).__dripPerf?.apply_delayed_ms;
|
|
156
|
+
expect(applyDelayedMs).toBeGreaterThanOrEqual(400);
|
|
157
|
+
expect(applyDelayedMs).toBeLessThanOrEqual(1_000);
|
|
158
|
+
} finally {
|
|
159
|
+
Object.defineProperty(document, "readyState", { value: "complete", configurable: true });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("does not stamp delay metrics for a synchronous-apply teardown gate", async () => {
|
|
164
|
+
const gate = awaitHydration(500, { waitForHydration: true, recordMetrics: false });
|
|
165
|
+
signalHydrated();
|
|
166
|
+
await expect(gate).resolves.toBeUndefined();
|
|
167
|
+
expect((window as Window & { __dripPerf?: unknown }).__dripPerf).toBeUndefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("keeps the first page-load hydration stamps across later gates", async () => {
|
|
171
|
+
document.body.innerHTML = '<div id="__next"></div>';
|
|
172
|
+
const first = awaitHydration(1_000);
|
|
173
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
174
|
+
signalHydrated();
|
|
175
|
+
await expect(first).resolves.toBeUndefined();
|
|
176
|
+
const initial = { ...(window as Window & { __dripPerf?: Record<string, unknown> }).__dripPerf };
|
|
177
|
+
|
|
178
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
179
|
+
await expect(awaitHydration(1_000)).resolves.toBeUndefined();
|
|
180
|
+
expect((window as Window & { __dripPerf?: Record<string, unknown> }).__dripPerf).toMatchObject(initial);
|
|
181
|
+
});
|
|
182
|
+
|
|
139
183
|
it("fails open at the head-install deadline without a framework marker or DOMContentLoaded", async () => {
|
|
140
184
|
Object.defineProperty(document, "readyState", { value: "loading", configurable: true });
|
|
141
185
|
try {
|
|
@@ -162,16 +206,43 @@ describe("hydration coexistence gate", () => {
|
|
|
162
206
|
await expect(gate).resolves.toBeUndefined();
|
|
163
207
|
});
|
|
164
208
|
|
|
165
|
-
it("
|
|
209
|
+
it("resolves an explicit markerless hydration wait once the DOM is quiet", async () => {
|
|
166
210
|
const gate = awaitHydration(1_000, { waitForHydration: true });
|
|
167
211
|
let resolved = false;
|
|
168
212
|
void gate.then(() => { resolved = true; });
|
|
169
213
|
|
|
170
|
-
await vi.advanceTimersByTimeAsync(
|
|
214
|
+
await vi.advanceTimersByTimeAsync(48);
|
|
171
215
|
expect(resolved).toBe(false);
|
|
172
216
|
|
|
217
|
+
await vi.advanceTimersByTimeAsync(32);
|
|
218
|
+
await expect(gate).resolves.toBeUndefined();
|
|
219
|
+
expect(resolved).toBe(true);
|
|
220
|
+
expect((window as Window & { __dripPerf?: Record<string, unknown> }).__dripPerf?.hydration_timeout).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("bounds a continuously mutating markerless page at the hydration timeout", async () => {
|
|
224
|
+
const gate = awaitHydration(200, { waitForHydration: true });
|
|
225
|
+
|
|
226
|
+
for (let elapsed = 0; elapsed < 200; elapsed += 16) {
|
|
227
|
+
document.body.toggleAttribute("data-mutating");
|
|
228
|
+
await Promise.resolve();
|
|
229
|
+
await vi.advanceTimersByTimeAsync(16);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await expect(gate).resolves.toBeUndefined();
|
|
233
|
+
expect((window as Window & { __dripPerf?: Record<string, unknown> }).__dripPerf).toMatchObject({
|
|
234
|
+
apply_delayed_ms: 200,
|
|
235
|
+
hydration_timeout: true,
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("resolves a markerless hydration wait immediately on an explicit signal", async () => {
|
|
240
|
+
const gate = awaitHydration(1_000, { waitForHydration: true });
|
|
241
|
+
|
|
173
242
|
signalHydrated();
|
|
243
|
+
|
|
174
244
|
await expect(gate).resolves.toBeUndefined();
|
|
245
|
+
expect((window as Window & { __dripPerf?: Record<string, unknown> }).__dripPerf?.hydration_timeout).toBeUndefined();
|
|
175
246
|
});
|
|
176
247
|
|
|
177
248
|
it("waits for two quiet frames and at least 50ms after framework mutations", async () => {
|
package/src/hydration.ts
CHANGED
|
@@ -12,6 +12,10 @@ function perf(): Record<string, unknown> | null {
|
|
|
12
12
|
return target.__dripPerf = target.__dripPerf ?? {};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function stampOnce(metrics: Record<string, unknown>, name: string, value: unknown): void {
|
|
16
|
+
if (!(name in metrics)) metrics[name] = value;
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
export function detectSsrFramework(): "nuxt" | "next" | "server-rendered" | null {
|
|
16
20
|
if (typeof window === "undefined" || typeof document === "undefined") return null;
|
|
17
21
|
const runtime = window as Window & { __NUXT__?: unknown; __NEXT_DATA__?: unknown; __next_f?: unknown };
|
|
@@ -46,9 +50,10 @@ export function signalHydrated(): void {
|
|
|
46
50
|
|
|
47
51
|
export function awaitHydration(
|
|
48
52
|
timeoutMs: number,
|
|
49
|
-
options: { waitForHydration?: boolean } = {},
|
|
53
|
+
options: { waitForHydration?: boolean; recordMetrics?: boolean } = {},
|
|
54
|
+
outerStarted = Date.now(),
|
|
50
55
|
): Promise<void> {
|
|
51
|
-
const started =
|
|
56
|
+
const started = outerStarted;
|
|
52
57
|
const boundedTimeoutMs = Number.isFinite(timeoutMs)
|
|
53
58
|
? Math.max(0, Math.min(10_000, timeoutMs))
|
|
54
59
|
: 3_000;
|
|
@@ -74,7 +79,7 @@ export function awaitHydration(
|
|
|
74
79
|
document.removeEventListener("DOMContentLoaded", proceed);
|
|
75
80
|
signals.delete(proceed);
|
|
76
81
|
const remaining = Math.max(0, boundedTimeoutMs - (Date.now() - started));
|
|
77
|
-
void awaitHydration(remaining, options).then(resolve);
|
|
82
|
+
void awaitHydration(remaining, options, started).then(resolve);
|
|
78
83
|
};
|
|
79
84
|
document.addEventListener("DOMContentLoaded", proceed, { once: true });
|
|
80
85
|
// A framework can call drip.hydrated() before the parser reaches the
|
|
@@ -110,12 +115,12 @@ export function awaitHydration(
|
|
|
110
115
|
holds = 0;
|
|
111
116
|
console.warn("[drip] Mutation hold timed out; applying fail-open");
|
|
112
117
|
}
|
|
113
|
-
const metrics = perf();
|
|
118
|
+
const metrics = options.recordMetrics === false ? undefined : perf();
|
|
114
119
|
if (metrics) {
|
|
115
|
-
if (needsHydration || hydratedAt) metrics
|
|
116
|
-
metrics
|
|
117
|
-
if (hydrationTimeout) metrics
|
|
118
|
-
if (holdTimeout) metrics
|
|
120
|
+
if (needsHydration || hydratedAt) stampOnce(metrics, "hydration_detected_at", hydratedAt || Date.now());
|
|
121
|
+
stampOnce(metrics, "apply_delayed_ms", Math.max(0, Date.now() - started));
|
|
122
|
+
if (hydrationTimeout) stampOnce(metrics, "hydration_timeout", true);
|
|
123
|
+
if (holdTimeout) stampOnce(metrics, "hold_timeout", true);
|
|
119
124
|
}
|
|
120
125
|
signals.delete(check);
|
|
121
126
|
clearTimeout(timer);
|
|
@@ -148,6 +153,10 @@ export function awaitHydration(
|
|
|
148
153
|
check();
|
|
149
154
|
return;
|
|
150
155
|
}
|
|
156
|
+
// requestAnimationFrame is available in supported browsers, but not in
|
|
157
|
+
// every DOM-like host. Keep the explicit-signal/hard-timeout exits active
|
|
158
|
+
// rather than throwing when the quiet detector cannot be scheduled.
|
|
159
|
+
if (typeof requestAnimationFrame === "undefined") return;
|
|
151
160
|
const onRecords: MutationCallback = () => {
|
|
152
161
|
lastMutation = Date.now();
|
|
153
162
|
quietFrames = 0;
|
|
@@ -160,7 +169,7 @@ export function awaitHydration(
|
|
|
160
169
|
|
|
161
170
|
signals.add(check);
|
|
162
171
|
const timer = setTimeout(() => finish(true), boundedTimeoutMs);
|
|
163
|
-
if (needsHydration
|
|
172
|
+
if (needsHydration) {
|
|
164
173
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", beginQuiet, { once: true });
|
|
165
174
|
else beginQuiet();
|
|
166
175
|
}
|