@affiliateo/web 3.1.0 → 4.0.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/dist/dev-host.d.ts +30 -0
- package/dist/dev-host.js +48 -0
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +104 -7
- package/dist/types.d.ts +52 -0
- package/package.json +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-host detection for the SDK. Returns true when the current hostname
|
|
3
|
+
* is obviously a developer's machine and not a real production site.
|
|
4
|
+
*
|
|
5
|
+
* The SDK uses this to short-circuit tracking by default (allowLocalhost
|
|
6
|
+
* defaults to false). Stops the "Quegly funnel pollution" pattern where
|
|
7
|
+
* a developer running `npm run dev` against localhost:3000 fires hundreds
|
|
8
|
+
* of pageviews into the production funnel.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: this list is INTENTIONALLY TIGHTER than the server-side
|
|
11
|
+
* `src/lib/analytics/dev-host.ts` helper. The server helper is used for
|
|
12
|
+
* domain-binding logic and can be aggressive about catching staging /
|
|
13
|
+
* preview deploys (.vercel.app, .pages.dev). The SDK helper cannot,
|
|
14
|
+
* because some merchants legitimately run their production site on a
|
|
15
|
+
* .vercel.app subdomain (no custom domain) and we'd silently break their
|
|
16
|
+
* tracking. So we only catch the unambiguous cases:
|
|
17
|
+
*
|
|
18
|
+
* - localhost / 127.0.0.1 / 0.0.0.0 / ::1 (literal local)
|
|
19
|
+
* - RFC1918 private IPs (10.x, 192.168.x, 172.16-31.x)
|
|
20
|
+
* - .local / .test / .example / .invalid / .localhost (RFC-reserved
|
|
21
|
+
* dev TLDs that cannot exist in production)
|
|
22
|
+
* - .lvh.me / .localtest.me (well-known dev wildcards
|
|
23
|
+
* that ALWAYS resolve to 127.0.0.1)
|
|
24
|
+
*
|
|
25
|
+
* NOT included on the SDK side: .ngrok.io, .vercel.app, .netlify.app,
|
|
26
|
+
* .pages.dev, .workers.dev, .trycloudflare.com. A merchant doing a
|
|
27
|
+
* staging-on-Vercel preview deploy should still see tracking work; if
|
|
28
|
+
* they want it OFF for staging they can set allowLocalhost explicitly.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isDevHost(host: string | null | undefined): boolean;
|
package/dist/dev-host.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-host detection for the SDK. Returns true when the current hostname
|
|
3
|
+
* is obviously a developer's machine and not a real production site.
|
|
4
|
+
*
|
|
5
|
+
* The SDK uses this to short-circuit tracking by default (allowLocalhost
|
|
6
|
+
* defaults to false). Stops the "Quegly funnel pollution" pattern where
|
|
7
|
+
* a developer running `npm run dev` against localhost:3000 fires hundreds
|
|
8
|
+
* of pageviews into the production funnel.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: this list is INTENTIONALLY TIGHTER than the server-side
|
|
11
|
+
* `src/lib/analytics/dev-host.ts` helper. The server helper is used for
|
|
12
|
+
* domain-binding logic and can be aggressive about catching staging /
|
|
13
|
+
* preview deploys (.vercel.app, .pages.dev). The SDK helper cannot,
|
|
14
|
+
* because some merchants legitimately run their production site on a
|
|
15
|
+
* .vercel.app subdomain (no custom domain) and we'd silently break their
|
|
16
|
+
* tracking. So we only catch the unambiguous cases:
|
|
17
|
+
*
|
|
18
|
+
* - localhost / 127.0.0.1 / 0.0.0.0 / ::1 (literal local)
|
|
19
|
+
* - RFC1918 private IPs (10.x, 192.168.x, 172.16-31.x)
|
|
20
|
+
* - .local / .test / .example / .invalid / .localhost (RFC-reserved
|
|
21
|
+
* dev TLDs that cannot exist in production)
|
|
22
|
+
* - .lvh.me / .localtest.me (well-known dev wildcards
|
|
23
|
+
* that ALWAYS resolve to 127.0.0.1)
|
|
24
|
+
*
|
|
25
|
+
* NOT included on the SDK side: .ngrok.io, .vercel.app, .netlify.app,
|
|
26
|
+
* .pages.dev, .workers.dev, .trycloudflare.com. A merchant doing a
|
|
27
|
+
* staging-on-Vercel preview deploy should still see tracking work; if
|
|
28
|
+
* they want it OFF for staging they can set allowLocalhost explicitly.
|
|
29
|
+
*/
|
|
30
|
+
const RFC1918_RE = /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/;
|
|
31
|
+
const DEV_TLD_SUFFIXES = ['.local', '.test', '.example', '.invalid', '.localhost'];
|
|
32
|
+
const DEV_HOST_SUFFIXES = ['.lvh.me', '.localtest.me'];
|
|
33
|
+
export function isDevHost(host) {
|
|
34
|
+
if (!host)
|
|
35
|
+
return false;
|
|
36
|
+
const h = host.toLowerCase();
|
|
37
|
+
if (h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0' || h === '::1')
|
|
38
|
+
return true;
|
|
39
|
+
if (RFC1918_RE.test(h))
|
|
40
|
+
return true;
|
|
41
|
+
for (const s of DEV_TLD_SUFFIXES)
|
|
42
|
+
if (h.endsWith(s))
|
|
43
|
+
return true;
|
|
44
|
+
for (const s of DEV_HOST_SUFFIXES)
|
|
45
|
+
if (h.endsWith(s))
|
|
46
|
+
return true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
package/dist/provider.d.ts
CHANGED
|
@@ -18,6 +18,6 @@ export declare function useAffiliateRef(): AffiliateoContextValue;
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
export declare function getAttribution(): Record<string, string>;
|
|
21
|
-
export declare function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, children, }: AffiliateoConfig & {
|
|
21
|
+
export declare function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allowLocalhost, allowIframe, debug, children, }: AffiliateoConfig & {
|
|
22
22
|
children: React.ReactNode;
|
|
23
23
|
}): import("react/jsx-runtime").JSX.Element;
|
package/dist/provider.js
CHANGED
|
@@ -3,6 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { EventQueue } from './queue';
|
|
5
5
|
import { isLikelyBot } from './bot-detect';
|
|
6
|
+
import { isDevHost } from './dev-host';
|
|
6
7
|
const DEFAULT_API_URL = 'https://affiliateo.com';
|
|
7
8
|
const LS_VISITOR_ID = 'affiliateo_visitor_id';
|
|
8
9
|
const LS_REF = 'affiliateo_ref';
|
|
@@ -60,6 +61,7 @@ const AffiliateoContext = createContext({
|
|
|
60
61
|
reset: asyncNoop,
|
|
61
62
|
optOut: syncNoop,
|
|
62
63
|
optIn: syncNoop,
|
|
64
|
+
flush: asyncNoop,
|
|
63
65
|
});
|
|
64
66
|
/**
|
|
65
67
|
* Returns the affiliate ref code and visitor ID.
|
|
@@ -100,7 +102,7 @@ export function getAttribution() {
|
|
|
100
102
|
// mount (host has the call sites) but we can shout in dev so the
|
|
101
103
|
// mistake is visible.
|
|
102
104
|
let providerMountCount = 0;
|
|
103
|
-
export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, children, }) {
|
|
105
|
+
export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, allowLocalhost, allowIframe, debug, children, }) {
|
|
104
106
|
const [state, setState] = useState({
|
|
105
107
|
refCode: null,
|
|
106
108
|
visitorId: null,
|
|
@@ -110,7 +112,22 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, child
|
|
|
110
112
|
reset: asyncNoop,
|
|
111
113
|
optOut: syncNoop,
|
|
112
114
|
optIn: syncNoop,
|
|
115
|
+
flush: asyncNoop,
|
|
113
116
|
});
|
|
117
|
+
// Debug logger. Closes over the `debug` prop so flipping it on/off at
|
|
118
|
+
// runtime (e.g. by changing a feature flag) takes effect on the next
|
|
119
|
+
// render without remounting. eslint-disable for the intentional
|
|
120
|
+
// console.log — that's the whole point of this helper.
|
|
121
|
+
const log = useCallback((msg, data) => {
|
|
122
|
+
if (!debug)
|
|
123
|
+
return;
|
|
124
|
+
// eslint-disable-next-line no-console
|
|
125
|
+
if (data !== undefined)
|
|
126
|
+
console.log('[Affiliateo] ' + msg, data);
|
|
127
|
+
// eslint-disable-next-line no-console
|
|
128
|
+
else
|
|
129
|
+
console.log('[Affiliateo] ' + msg);
|
|
130
|
+
}, [debug]);
|
|
114
131
|
const apiBase = (apiUrl || DEFAULT_API_URL).replace(/\/$/, '');
|
|
115
132
|
// campaignIdRef stays current so callbacks defined inside useEffect
|
|
116
133
|
// always read the latest value, even after a prop change. Closures
|
|
@@ -164,9 +181,10 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, child
|
|
|
164
181
|
// useEffect runs), this noops — that's a vanishingly rare race
|
|
165
182
|
// since track() is virtually always called from a click handler.
|
|
166
183
|
queueRef.current?.enqueue(`${apiBase}/api/v1/web/event`, payload);
|
|
184
|
+
log('track', { event: name, metadata });
|
|
167
185
|
}
|
|
168
186
|
catch { /* swallow */ }
|
|
169
|
-
}, [apiBase]);
|
|
187
|
+
}, [apiBase, log]);
|
|
170
188
|
// identify(userId). user_id only — the SDK does NOT accept, collect,
|
|
171
189
|
// or transmit email or any other PII. Cross-device stitching uses the
|
|
172
190
|
// merchant's own user_id; we never touch identifying personal data.
|
|
@@ -194,9 +212,10 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, child
|
|
|
194
212
|
user_id: uid,
|
|
195
213
|
};
|
|
196
214
|
queueRef.current?.enqueue(`${apiBase}/api/v1/web/identify`, payload);
|
|
215
|
+
log('identify', { user_id: uid });
|
|
197
216
|
}
|
|
198
217
|
catch { /* swallow */ }
|
|
199
|
-
}, [apiBase]);
|
|
218
|
+
}, [apiBase, log]);
|
|
200
219
|
// Manual pageview. Mirrors Datafast's trackPageview(path?). Same payload
|
|
201
220
|
// shape the auto-fired pageview uses (see sendPageview inside the route
|
|
202
221
|
// effect below) so server-side routing / dedup / IP+UA enrichment behave
|
|
@@ -239,9 +258,24 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, child
|
|
|
239
258
|
screen_width: window.innerWidth,
|
|
240
259
|
};
|
|
241
260
|
queueRef.current?.enqueue(`${apiBase}/api/v1/web/event`, payload);
|
|
261
|
+
log('pageview (manual)', { path: pagePath });
|
|
242
262
|
}
|
|
243
263
|
catch { /* swallow */ }
|
|
244
|
-
}, [apiBase]);
|
|
264
|
+
}, [apiBase, log]);
|
|
265
|
+
// flush(): force immediate delivery of all queued events. The queue
|
|
266
|
+
// batches and auto-flushes every 5s; this is the escape hatch for the
|
|
267
|
+
// "I'm about to redirect, don't lose my events" case.
|
|
268
|
+
//
|
|
269
|
+
// Resolves when the underlying queue.flush() completes — success OR
|
|
270
|
+
// failure. Events that fail to deliver stay in the queue and get
|
|
271
|
+
// retried on the next normal flush cycle. Safe to await; never throws.
|
|
272
|
+
const flush = useCallback(async () => {
|
|
273
|
+
try {
|
|
274
|
+
log('flush requested');
|
|
275
|
+
await queueRef.current?.flush();
|
|
276
|
+
}
|
|
277
|
+
catch { /* swallow */ }
|
|
278
|
+
}, [log]);
|
|
245
279
|
// reset(): wipe visitor identity. Mints a fresh UUID and clears the
|
|
246
280
|
// affiliate ref so the next pageview is a brand-new anonymous visitor.
|
|
247
281
|
// The campaign_id stays (it's a property of the site, not the visitor).
|
|
@@ -319,6 +353,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, child
|
|
|
319
353
|
// doesn't crash on `useAffiliateRef().reset()`. They're noops, which
|
|
320
354
|
// is the correct behavior since there's no real visitor to act on.
|
|
321
355
|
if (isLikelyBot()) {
|
|
356
|
+
log('blocked: bot detected');
|
|
322
357
|
setState({
|
|
323
358
|
refCode: null,
|
|
324
359
|
visitorId: null,
|
|
@@ -328,9 +363,69 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, child
|
|
|
328
363
|
reset: asyncNoop,
|
|
329
364
|
optOut: syncNoop,
|
|
330
365
|
optIn: syncNoop,
|
|
366
|
+
flush: asyncNoop,
|
|
331
367
|
});
|
|
332
368
|
return;
|
|
333
369
|
}
|
|
370
|
+
// Dev-host guard. Default behavior is "block localhost / 127.0.0.1 /
|
|
371
|
+
// RFC1918 / .local / .test so merchant doesn't accidentally pollute
|
|
372
|
+
// their production funnel while running `npm run dev`." Merchants who
|
|
373
|
+
// legitimately want dev events flowing (e.g. end-to-end testing
|
|
374
|
+
// against staging that needs server-side events) opt in via
|
|
375
|
+
// <AffiliateoProvider allowLocalhost />.
|
|
376
|
+
//
|
|
377
|
+
// Tighter list than the server-side dev-host helper. see dev-host.ts
|
|
378
|
+
// for the rationale (must not block real merchants on .vercel.app).
|
|
379
|
+
if (!allowLocalhost && isDevHost(window.location.hostname)) {
|
|
380
|
+
log('blocked: localhost (set allowLocalhost={true} on AffiliateoProvider to enable)');
|
|
381
|
+
setState({
|
|
382
|
+
refCode: null,
|
|
383
|
+
visitorId: null,
|
|
384
|
+
track: asyncNoop,
|
|
385
|
+
identify: asyncNoop,
|
|
386
|
+
trackPageview: asyncNoop,
|
|
387
|
+
reset: asyncNoop,
|
|
388
|
+
optOut: syncNoop,
|
|
389
|
+
optIn: syncNoop,
|
|
390
|
+
flush: asyncNoop,
|
|
391
|
+
});
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// Iframe guard. Default behavior is "if we're inside an iframe, don't
|
|
395
|
+
// track" so scrapers, preview tools, and archive crawlers embedding
|
|
396
|
+
// the merchant's site don't count as pageviews. Cross-origin iframe
|
|
397
|
+
// access throws on the property read, which is ITSELF a signal we're
|
|
398
|
+
// inside a foreign frame, so the catch branch also bails out.
|
|
399
|
+
//
|
|
400
|
+
// Same-origin iframe (e.g. merchant embeds their own checkout) lets
|
|
401
|
+
// window.top return without throwing, and we compare against window.self
|
|
402
|
+
// to detect the embedded case. Merchants who legitimately render the
|
|
403
|
+
// SDK inside an iframe (embed widgets shipped to partners) opt in via
|
|
404
|
+
// <AffiliateoProvider allowIframe />.
|
|
405
|
+
if (!allowIframe) {
|
|
406
|
+
let insideIframe = false;
|
|
407
|
+
try {
|
|
408
|
+
insideIframe = window.self !== window.top;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
insideIframe = true;
|
|
412
|
+
}
|
|
413
|
+
if (insideIframe) {
|
|
414
|
+
log('blocked: iframe (set allowIframe={true} on AffiliateoProvider to enable)');
|
|
415
|
+
setState({
|
|
416
|
+
refCode: null,
|
|
417
|
+
visitorId: null,
|
|
418
|
+
track: asyncNoop,
|
|
419
|
+
identify: asyncNoop,
|
|
420
|
+
trackPageview: asyncNoop,
|
|
421
|
+
reset: asyncNoop,
|
|
422
|
+
optOut: syncNoop,
|
|
423
|
+
optIn: syncNoop,
|
|
424
|
+
flush: asyncNoop,
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
334
429
|
// Initialize the event queue. Done inside useEffect (not in a useRef
|
|
335
430
|
// initializer) so the EventQueue constructor — which touches
|
|
336
431
|
// localStorage on hydration — only ever runs client-side. The queue
|
|
@@ -359,7 +454,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, child
|
|
|
359
454
|
setState({
|
|
360
455
|
refCode: null,
|
|
361
456
|
visitorId: optedOutVid,
|
|
362
|
-
track, identify, trackPageview, reset, optOut, optIn,
|
|
457
|
+
track, identify, trackPageview, reset, optOut, optIn, flush,
|
|
363
458
|
});
|
|
364
459
|
// Don't leave a queue running for an opted-out visitor — the timer
|
|
365
460
|
// would just keep waking up to flush nothing.
|
|
@@ -419,8 +514,9 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, child
|
|
|
419
514
|
setState({
|
|
420
515
|
refCode: ref,
|
|
421
516
|
visitorId: vid,
|
|
422
|
-
track, identify, trackPageview, reset, optOut, optIn,
|
|
517
|
+
track, identify, trackPageview, reset, optOut, optIn, flush,
|
|
423
518
|
});
|
|
519
|
+
log('init', { campaign: campaignIdRef.current, visitor: vid, ref: ref || null });
|
|
424
520
|
// Throttle: max 1 pageview per second
|
|
425
521
|
let lastSendTime = 0;
|
|
426
522
|
function sendPageview() {
|
|
@@ -443,6 +539,7 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, child
|
|
|
443
539
|
utm_campaign: params.get('utm_campaign') || '',
|
|
444
540
|
screen_width: window.innerWidth,
|
|
445
541
|
});
|
|
542
|
+
log('pageview', { path: window.location.pathname });
|
|
446
543
|
}
|
|
447
544
|
// Send session_end when user leaves.
|
|
448
545
|
//
|
|
@@ -627,6 +724,6 @@ export function AffiliateoProvider({ campaignId, apiUrl, allowedHostnames, child
|
|
|
627
724
|
queueRef.current = null;
|
|
628
725
|
providerMountCount = Math.max(0, providerMountCount - 1);
|
|
629
726
|
};
|
|
630
|
-
}, [apiBase, allowedHostnames, track, identify, trackPageview, reset, optOut, optIn]);
|
|
727
|
+
}, [apiBase, allowedHostnames, allowLocalhost, allowIframe, log, track, identify, trackPageview, reset, optOut, optIn, flush]);
|
|
631
728
|
return (_jsx(AffiliateoContext.Provider, { value: state, children: children }));
|
|
632
729
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -26,6 +26,37 @@ export interface AffiliateoConfig {
|
|
|
26
26
|
* allowedHostnames={['app.merchant.com', 'docs.merchant.com']}
|
|
27
27
|
* > */
|
|
28
28
|
allowedHostnames?: string[];
|
|
29
|
+
/** Allow tracking when the page is served from a developer machine
|
|
30
|
+
* (localhost, 127.0.0.1, RFC1918 IPs, .local / .test TLDs). Defaults
|
|
31
|
+
* to false so a developer running `npm run dev` doesn't pollute the
|
|
32
|
+
* merchant's production funnel with test pageviews.
|
|
33
|
+
*
|
|
34
|
+
* Set to true ONLY when you actually want dev traffic in your
|
|
35
|
+
* dashboard (e.g. for an end-to-end test that needs the events to
|
|
36
|
+
* land server-side).
|
|
37
|
+
*
|
|
38
|
+
* Example:
|
|
39
|
+
* <AffiliateoProvider campaignId="..." allowLocalhost> */
|
|
40
|
+
allowLocalhost?: boolean;
|
|
41
|
+
/** Allow tracking when the page is rendered inside an iframe. Defaults
|
|
42
|
+
* to false so scrapers, preview tools, and other sites embedding yours
|
|
43
|
+
* don't count as pageviews.
|
|
44
|
+
*
|
|
45
|
+
* Set to true if you legitimately render your site inside iframes
|
|
46
|
+
* (e.g. an embed widget you ship to partners). */
|
|
47
|
+
allowIframe?: boolean;
|
|
48
|
+
/** Print decision logs to the browser console. Defaults to false. When
|
|
49
|
+
* true, the SDK logs every action it takes (init, pageview fired,
|
|
50
|
+
* goal tracked, blocked by bot detection, blocked by localhost, queue
|
|
51
|
+
* flush attempted, etc.). Only the merchant or anyone manually opening
|
|
52
|
+
* DevTools sees these — normal visitors never look at the console.
|
|
53
|
+
*
|
|
54
|
+
* Turn on temporarily when investigating "why aren't my events
|
|
55
|
+
* showing up?". Leave off in production.
|
|
56
|
+
*
|
|
57
|
+
* Example:
|
|
58
|
+
* <AffiliateoProvider campaignId="..." debug> */
|
|
59
|
+
debug?: boolean;
|
|
29
60
|
}
|
|
30
61
|
export interface AffiliateoContextValue {
|
|
31
62
|
/** The affiliate ref code from the URL (if present) */
|
|
@@ -108,4 +139,25 @@ export interface AffiliateoContextValue {
|
|
|
108
139
|
* the cleanest way to bring the SDK back to its normal "auto-fire on
|
|
109
140
|
* mount" state. */
|
|
110
141
|
optIn: () => void;
|
|
142
|
+
/** Force immediate delivery of all queued events. The SDK batches events
|
|
143
|
+
* (track / identify / pageview) and flushes them every 5 seconds; this
|
|
144
|
+
* method flushes RIGHT NOW.
|
|
145
|
+
*
|
|
146
|
+
* Use case: call before a redirect (e.g. window.location.href = stripeUrl)
|
|
147
|
+
* so events queued right before the navigation don't get lost. The
|
|
148
|
+
* default behavior would wait for the 5-second timer, but the page
|
|
149
|
+
* is gone by then.
|
|
150
|
+
*
|
|
151
|
+
* Example:
|
|
152
|
+
* const { track, flush } = useAffiliateRef()
|
|
153
|
+
* async function startCheckout() {
|
|
154
|
+
* track('initiate_checkout')
|
|
155
|
+
* await flush()
|
|
156
|
+
* window.location.href = stripeCheckoutUrl
|
|
157
|
+
* }
|
|
158
|
+
*
|
|
159
|
+
* Returns a Promise that resolves when the flush attempt completes
|
|
160
|
+
* (success OR failure — failed events stay in the queue for the next
|
|
161
|
+
* flush). Safe to await without timeout concerns. */
|
|
162
|
+
flush: () => Promise<void>;
|
|
111
163
|
}
|