@interfere/react 3.0.0 → 5.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.
Files changed (98) hide show
  1. package/dist/error-boundary.d.mts +27 -0
  2. package/dist/error-boundary.d.mts.map +1 -0
  3. package/dist/error-boundary.mjs +44 -0
  4. package/dist/error-boundary.mjs.map +1 -0
  5. package/dist/internal/client.d.mts +35 -0
  6. package/dist/internal/client.d.mts.map +1 -0
  7. package/dist/internal/client.mjs +121 -0
  8. package/dist/internal/client.mjs.map +1 -0
  9. package/dist/internal/config.d.mts +10 -0
  10. package/dist/internal/config.d.mts.map +1 -0
  11. package/dist/internal/config.mjs +32 -0
  12. package/dist/internal/config.mjs.map +1 -0
  13. package/dist/internal/consent.d.mts +15 -0
  14. package/dist/internal/consent.d.mts.map +1 -0
  15. package/dist/internal/consent.mjs +25 -0
  16. package/dist/internal/consent.mjs.map +1 -0
  17. package/dist/internal/context.d.mts +6 -0
  18. package/dist/internal/context.d.mts.map +1 -0
  19. package/dist/internal/context.mjs +32 -0
  20. package/dist/internal/context.mjs.map +1 -0
  21. package/dist/internal/envelope.d.mts +14 -0
  22. package/dist/internal/envelope.d.mts.map +1 -0
  23. package/dist/internal/envelope.mjs +22 -0
  24. package/dist/internal/envelope.mjs.map +1 -0
  25. package/dist/internal/errors.d.mts +4 -0
  26. package/dist/internal/errors.d.mts.map +1 -0
  27. package/dist/internal/errors.mjs +4 -0
  28. package/dist/internal/errors.mjs.map +1 -0
  29. package/dist/internal/plugin-runtime.d.mts +29 -0
  30. package/dist/internal/plugin-runtime.d.mts.map +1 -0
  31. package/dist/internal/plugin-runtime.mjs +96 -0
  32. package/dist/internal/plugin-runtime.mjs.map +1 -0
  33. package/dist/internal/sw.d.mts +4 -0
  34. package/dist/internal/sw.d.mts.map +1 -0
  35. package/dist/internal/sw.mjs +10 -0
  36. package/dist/internal/sw.mjs.map +1 -0
  37. package/dist/internal/version.d.mts +4 -0
  38. package/dist/internal/version.d.mts.map +1 -0
  39. package/dist/internal/version.mjs +5 -0
  40. package/dist/internal/version.mjs.map +1 -0
  41. package/dist/package.mjs +5 -0
  42. package/dist/package.mjs.map +1 -0
  43. package/dist/plugins/device.d.mts +6 -0
  44. package/dist/plugins/device.d.mts.map +1 -0
  45. package/dist/plugins/device.mjs +13 -0
  46. package/dist/plugins/device.mjs.map +1 -0
  47. package/dist/plugins/errors.d.mts +6 -0
  48. package/dist/plugins/errors.d.mts.map +1 -0
  49. package/dist/plugins/errors.mjs +59 -0
  50. package/dist/plugins/errors.mjs.map +1 -0
  51. package/dist/plugins/lib/loader.d.mts +10 -0
  52. package/dist/plugins/lib/loader.d.mts.map +1 -0
  53. package/dist/plugins/lib/loader.mjs +43 -0
  54. package/dist/plugins/lib/loader.mjs.map +1 -0
  55. package/dist/plugins/lib/types.d.mts +14 -0
  56. package/dist/plugins/lib/types.d.mts.map +1 -0
  57. package/dist/plugins/lib/types.mjs +1 -0
  58. package/dist/plugins/pages.d.mts +6 -0
  59. package/dist/plugins/pages.d.mts.map +1 -0
  60. package/dist/plugins/pages.mjs +102 -0
  61. package/dist/plugins/pages.mjs.map +1 -0
  62. package/dist/plugins/rage-clicks.d.mts +6 -0
  63. package/dist/plugins/rage-clicks.d.mts.map +1 -0
  64. package/dist/plugins/rage-clicks.mjs +53 -0
  65. package/dist/plugins/rage-clicks.mjs.map +1 -0
  66. package/dist/plugins/replay.d.mts +6 -0
  67. package/dist/plugins/replay.d.mts.map +1 -0
  68. package/dist/plugins/replay.mjs +62 -0
  69. package/dist/plugins/replay.mjs.map +1 -0
  70. package/dist/provider.d.mts +30 -0
  71. package/dist/provider.d.mts.map +1 -0
  72. package/dist/provider.mjs +30 -0
  73. package/dist/provider.mjs.map +1 -0
  74. package/dist/tracking/api.d.mts +17 -0
  75. package/dist/tracking/api.d.mts.map +1 -0
  76. package/dist/tracking/api.mjs +106 -0
  77. package/dist/tracking/api.mjs.map +1 -0
  78. package/dist/tracking/device.d.mts +8 -0
  79. package/dist/tracking/device.d.mts.map +1 -0
  80. package/dist/tracking/device.mjs +76 -0
  81. package/dist/tracking/device.mjs.map +1 -0
  82. package/dist/tracking/session.d.mts +19 -0
  83. package/dist/tracking/session.d.mts.map +1 -0
  84. package/dist/tracking/session.mjs +75 -0
  85. package/dist/tracking/session.mjs.map +1 -0
  86. package/dist/transport/http.d.mts +21 -0
  87. package/dist/transport/http.d.mts.map +1 -0
  88. package/dist/transport/http.mjs +68 -0
  89. package/dist/transport/http.mjs.map +1 -0
  90. package/dist/transport/queue.d.mts +31 -0
  91. package/dist/transport/queue.d.mts.map +1 -0
  92. package/dist/transport/queue.mjs +93 -0
  93. package/dist/transport/queue.mjs.map +1 -0
  94. package/dist/util/log.d.mts +13 -0
  95. package/dist/util/log.d.mts.map +1 -0
  96. package/dist/util/log.mjs +37 -0
  97. package/dist/util/log.mjs.map +1 -0
  98. package/package.json +23 -24
@@ -0,0 +1,106 @@
1
+ import { createLogger } from "../util/log.mjs";
2
+ import { getDeviceId, getFpHash, whenDeviceReady } from "./device.mjs";
3
+ import { buildHeaders } from "../transport/http.mjs";
4
+ import { SessionManager } from "./session.mjs";
5
+ //#region src/tracking/api.ts
6
+ const log = createLogger("tracking");
7
+ let mgr = null;
8
+ let target = null;
9
+ let currentIdentity = null;
10
+ let identifiedSessionId = null;
11
+ let syncedSessionId = null;
12
+ let syncAttemptMs = 0;
13
+ const SYNC_COOLDOWN_MS = 5e3;
14
+ function syncSession(sessionId, deviceId, fpHash) {
15
+ if (!target) return;
16
+ syncAttemptMs = Date.now();
17
+ fetch(target.url, {
18
+ method: "POST",
19
+ headers: buildHeaders(target.headers),
20
+ body: JSON.stringify({
21
+ sessionId,
22
+ deviceId,
23
+ fpHash
24
+ }),
25
+ keepalive: true,
26
+ signal: AbortSignal.timeout(1e4)
27
+ }).then((res) => {
28
+ if (res.ok) syncedSessionId = sessionId;
29
+ }).catch(() => {
30
+ log.warn("session sync failed, will retry");
31
+ });
32
+ }
33
+ function ensureSynced(sessionId) {
34
+ if (syncedSessionId === sessionId) return;
35
+ if (Date.now() - syncAttemptMs < SYNC_COOLDOWN_MS) return;
36
+ syncSession(sessionId, getDeviceId(), getFpHash());
37
+ }
38
+ async function onRotate(sessionId) {
39
+ syncedSessionId = null;
40
+ if (!target) return;
41
+ const deviceId = await whenDeviceReady();
42
+ log.debug("POST session %s (device=%s)", sessionId, deviceId ?? "pending");
43
+ syncSession(sessionId, deviceId, getFpHash());
44
+ }
45
+ function bootstrap(sessionTarget) {
46
+ target = sessionTarget;
47
+ mgr = new SessionManager((id) => {
48
+ onRotate(id).catch(() => {});
49
+ });
50
+ }
51
+ const session = {
52
+ getId() {
53
+ const id = mgr?.getSessionId() ?? null;
54
+ if (id) ensureSynced(id);
55
+ return id;
56
+ },
57
+ getWindowId() {
58
+ return mgr?.getWindowId() ?? null;
59
+ }
60
+ };
61
+ const identity = {
62
+ get() {
63
+ return currentIdentity;
64
+ },
65
+ set(params) {
66
+ if (!(mgr && target)) return;
67
+ const sessionId = mgr.getSessionId();
68
+ if (identifiedSessionId === sessionId) {
69
+ log.debug("skipped, already identified for session %s", sessionId);
70
+ return;
71
+ }
72
+ currentIdentity = params;
73
+ identifiedSessionId = sessionId;
74
+ const deviceId = getDeviceId();
75
+ const fpHash = getFpHash();
76
+ log.info("PUT session %s → user %s", sessionId, params.identifier);
77
+ fetch(target.url, {
78
+ method: "PUT",
79
+ headers: buildHeaders(target.headers),
80
+ body: JSON.stringify({
81
+ sessionId,
82
+ deviceId,
83
+ fpHash,
84
+ ...params
85
+ }),
86
+ keepalive: true,
87
+ signal: AbortSignal.timeout(1e4)
88
+ }).catch(() => {
89
+ identifiedSessionId = null;
90
+ log.warn("identify failed for session %s", sessionId);
91
+ });
92
+ },
93
+ clear() {
94
+ currentIdentity = null;
95
+ identifiedSessionId = null;
96
+ }
97
+ };
98
+ function teardown() {
99
+ identity.clear();
100
+ syncedSessionId = null;
101
+ syncAttemptMs = 0;
102
+ mgr = null;
103
+ target = null;
104
+ }
105
+ //#endregion
106
+ export { bootstrap, identity, session, teardown };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.mjs","names":[],"sources":["../../src/tracking/api.ts"],"sourcesContent":["import type { IdentifyParams } from \"@interfere/types/sdk/identify\";\n\nimport { buildHeaders, type IngestTarget } from \"../transport/http.js\";\nimport { createLogger } from \"../util/log.js\";\nimport { getDeviceId, getFpHash, whenDeviceReady } from \"./device.js\";\nimport { SessionManager } from \"./session.js\";\n\nconst log = createLogger(\"tracking\");\n\nlet mgr: SessionManager | null = null;\nlet target: IngestTarget | null = null;\nlet currentIdentity: IdentifyParams | null = null;\nlet identifiedSessionId: string | null = null;\n\nlet syncedSessionId: string | null = null;\nlet syncAttemptMs = 0;\nconst SYNC_COOLDOWN_MS = 5000;\n\nfunction syncSession(\n sessionId: string,\n deviceId: string | null,\n fpHash: string | null\n): void {\n if (!target) {\n return;\n }\n\n syncAttemptMs = Date.now();\n\n fetch(target.url, {\n method: \"POST\",\n headers: buildHeaders(target.headers),\n body: JSON.stringify({ sessionId, deviceId, fpHash }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n })\n .then((res) => {\n if (res.ok) {\n syncedSessionId = sessionId;\n }\n })\n .catch(() => {\n log.warn(\"session sync failed, will retry\");\n });\n}\n\nfunction ensureSynced(sessionId: string): void {\n if (syncedSessionId === sessionId) {\n return;\n }\n if (Date.now() - syncAttemptMs < SYNC_COOLDOWN_MS) {\n return;\n }\n syncSession(sessionId, getDeviceId(), getFpHash());\n}\n\nasync function onRotate(sessionId: string): Promise<void> {\n syncedSessionId = null;\n if (!target) {\n return;\n }\n const deviceId = await whenDeviceReady();\n log.debug(\"POST session %s (device=%s)\", sessionId, deviceId ?? \"pending\");\n syncSession(sessionId, deviceId, getFpHash());\n}\n\nexport function bootstrap(sessionTarget: IngestTarget): void {\n target = sessionTarget;\n\n mgr = new SessionManager((id) => {\n onRotate(id).catch(() => {\n /* best-effort */\n });\n });\n}\n\nexport const session = {\n getId(): string | null {\n const id = mgr?.getSessionId() ?? null;\n if (id) {\n ensureSynced(id);\n }\n return id;\n },\n\n getWindowId(): string | null {\n return mgr?.getWindowId() ?? null;\n },\n};\n\nexport const identity = {\n get(): IdentifyParams | null {\n return currentIdentity;\n },\n\n set(params: IdentifyParams): void {\n if (!(mgr && target)) {\n return;\n }\n const sessionId = mgr.getSessionId();\n if (identifiedSessionId === sessionId) {\n log.debug(\"skipped, already identified for session %s\", sessionId);\n return;\n }\n\n currentIdentity = params;\n identifiedSessionId = sessionId;\n\n const deviceId = getDeviceId();\n const fpHash = getFpHash();\n log.info(\"PUT session %s → user %s\", sessionId, params.identifier);\n fetch(target.url, {\n method: \"PUT\",\n headers: buildHeaders(target.headers),\n body: JSON.stringify({ sessionId, deviceId, fpHash, ...params }),\n keepalive: true,\n signal: AbortSignal.timeout(10_000),\n }).catch(() => {\n identifiedSessionId = null;\n log.warn(\"identify failed for session %s\", sessionId);\n });\n },\n\n clear(): void {\n currentIdentity = null;\n identifiedSessionId = null;\n },\n};\n\nexport function teardown(): void {\n identity.clear();\n syncedSessionId = null;\n syncAttemptMs = 0;\n mgr = null;\n target = null;\n}\n"],"mappings":";;;;;AAOA,MAAM,MAAM,aAAa,WAAW;AAEpC,IAAI,MAA6B;AACjC,IAAI,SAA8B;AAClC,IAAI,kBAAyC;AAC7C,IAAI,sBAAqC;AAEzC,IAAI,kBAAiC;AACrC,IAAI,gBAAgB;AACpB,MAAM,mBAAmB;AAEzB,SAAS,YACP,WACA,UACA,QACM;AACN,KAAI,CAAC,OACH;AAGF,iBAAgB,KAAK,KAAK;AAE1B,OAAM,OAAO,KAAK;EAChB,QAAQ;EACR,SAAS,aAAa,OAAO,QAAQ;EACrC,MAAM,KAAK,UAAU;GAAE;GAAW;GAAU;GAAQ,CAAC;EACrD,WAAW;EACX,QAAQ,YAAY,QAAQ,IAAO;EACpC,CAAC,CACC,MAAM,QAAQ;AACb,MAAI,IAAI,GACN,mBAAkB;GAEpB,CACD,YAAY;AACX,MAAI,KAAK,kCAAkC;GAC3C;;AAGN,SAAS,aAAa,WAAyB;AAC7C,KAAI,oBAAoB,UACtB;AAEF,KAAI,KAAK,KAAK,GAAG,gBAAgB,iBAC/B;AAEF,aAAY,WAAW,aAAa,EAAE,WAAW,CAAC;;AAGpD,eAAe,SAAS,WAAkC;AACxD,mBAAkB;AAClB,KAAI,CAAC,OACH;CAEF,MAAM,WAAW,MAAM,iBAAiB;AACxC,KAAI,MAAM,+BAA+B,WAAW,YAAY,UAAU;AAC1E,aAAY,WAAW,UAAU,WAAW,CAAC;;AAG/C,SAAgB,UAAU,eAAmC;AAC3D,UAAS;AAET,OAAM,IAAI,gBAAgB,OAAO;AAC/B,WAAS,GAAG,CAAC,YAAY,GAEvB;GACF;;AAGJ,MAAa,UAAU;CACrB,QAAuB;EACrB,MAAM,KAAK,KAAK,cAAc,IAAI;AAClC,MAAI,GACF,cAAa,GAAG;AAElB,SAAO;;CAGT,cAA6B;AAC3B,SAAO,KAAK,aAAa,IAAI;;CAEhC;AAED,MAAa,WAAW;CACtB,MAA6B;AAC3B,SAAO;;CAGT,IAAI,QAA8B;AAChC,MAAI,EAAE,OAAO,QACX;EAEF,MAAM,YAAY,IAAI,cAAc;AACpC,MAAI,wBAAwB,WAAW;AACrC,OAAI,MAAM,8CAA8C,UAAU;AAClE;;AAGF,oBAAkB;AAClB,wBAAsB;EAEtB,MAAM,WAAW,aAAa;EAC9B,MAAM,SAAS,WAAW;AAC1B,MAAI,KAAK,4BAA4B,WAAW,OAAO,WAAW;AAClE,QAAM,OAAO,KAAK;GAChB,QAAQ;GACR,SAAS,aAAa,OAAO,QAAQ;GACrC,MAAM,KAAK,UAAU;IAAE;IAAW;IAAU;IAAQ,GAAG;IAAQ,CAAC;GAChE,WAAW;GACX,QAAQ,YAAY,QAAQ,IAAO;GACpC,CAAC,CAAC,YAAY;AACb,yBAAsB;AACtB,OAAI,KAAK,kCAAkC,UAAU;IACrD;;CAGJ,QAAc;AACZ,oBAAkB;AAClB,wBAAsB;;CAEzB;AAED,SAAgB,WAAiB;AAC/B,UAAS,OAAO;AAChB,mBAAkB;AAClB,iBAAgB;AAChB,OAAM;AACN,UAAS"}
@@ -0,0 +1,8 @@
1
+ //#region src/tracking/device.d.ts
2
+ declare function initDevice(): void;
3
+ declare function getDeviceId(): string | null;
4
+ declare function getFpHash(): string | null;
5
+ declare function whenDeviceReady(): Promise<string | null>;
6
+ declare function resetDevice(): void;
7
+ //#endregion
8
+ export { getDeviceId, getFpHash, initDevice, resetDevice, whenDeviceReady };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.d.mts","names":[],"sources":["../../src/tracking/device.ts"],"mappings":";iBA+CgB,UAAA,CAAA;AAAA,iBAiDA,WAAA,CAAA;AAAA,iBAIA,SAAA,CAAA;AAAA,iBAIA,eAAA,CAAA,GAAmB,OAAA;AAAA,iBAOnB,WAAA,CAAA"}
@@ -0,0 +1,76 @@
1
+ import { createLogger } from "../util/log.mjs";
2
+ //#region src/tracking/device.ts
3
+ const log = createLogger("device");
4
+ const LS_KEY = "interfere:device_id";
5
+ const COOKIE_NAME = "interfere_did";
6
+ const COOKIE_MAX_AGE_DAYS = 400;
7
+ let deviceId = null;
8
+ let fpHash = null;
9
+ let fpPending = null;
10
+ function tryLocalStorage() {
11
+ try {
12
+ const s = globalThis.localStorage;
13
+ const key = "__interfere_device_probe__";
14
+ s.setItem(key, "1");
15
+ s.removeItem(key);
16
+ return s;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ function getCookie(name) {
22
+ if (typeof document === "undefined") return null;
23
+ const match = document.cookie.split("; ").find((c) => c.startsWith(`${name}=`));
24
+ return match ? decodeURIComponent(match.split("=")[1] ?? "") : null;
25
+ }
26
+ function setCookie(name, value) {
27
+ if (typeof document === "undefined") return;
28
+ const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;
29
+ document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;
30
+ }
31
+ function generateId() {
32
+ return crypto.randomUUID();
33
+ }
34
+ function initDevice() {
35
+ if (deviceId) return;
36
+ const ls = tryLocalStorage();
37
+ const fromLs = ls?.getItem(LS_KEY) ?? null;
38
+ const fromCookie = getCookie(COOKIE_NAME);
39
+ deviceId = fromLs ?? fromCookie ?? generateId();
40
+ if (!fromLs && ls) ls.setItem(LS_KEY, deviceId);
41
+ if (!fromCookie) setCookie(COOKIE_NAME, deviceId);
42
+ if (fromLs && !fromCookie) setCookie(COOKIE_NAME, deviceId);
43
+ if (fromCookie && !fromLs && ls) ls.setItem(LS_KEY, deviceId);
44
+ log.debug("device %s (ls=%s cookie=%s)", deviceId, !!fromLs, !!fromCookie);
45
+ initFpHash();
46
+ }
47
+ function initFpHash() {
48
+ if (fpHash || fpPending) return;
49
+ fpPending = (async () => {
50
+ try {
51
+ fpHash = (await (await (await import("@fingerprintjs/fingerprintjs")).load()).get()).visitorId;
52
+ log.debug("fpHash %s", fpHash);
53
+ return fpHash;
54
+ } catch {
55
+ log.warn("fp hash failed");
56
+ return null;
57
+ }
58
+ })();
59
+ }
60
+ function getDeviceId() {
61
+ return deviceId;
62
+ }
63
+ function getFpHash() {
64
+ return fpHash;
65
+ }
66
+ function whenDeviceReady() {
67
+ if (deviceId) return Promise.resolve(deviceId);
68
+ return Promise.resolve(null);
69
+ }
70
+ function resetDevice() {
71
+ deviceId = null;
72
+ fpHash = null;
73
+ fpPending = null;
74
+ }
75
+ //#endregion
76
+ export { getDeviceId, getFpHash, initDevice, resetDevice, whenDeviceReady };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.mjs","names":[],"sources":["../../src/tracking/device.ts"],"sourcesContent":["import { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"device\");\n\nconst LS_KEY = \"interfere:device_id\";\nconst COOKIE_NAME = \"interfere_did\";\nconst COOKIE_MAX_AGE_DAYS = 400;\n\nlet deviceId: string | null = null;\nlet fpHash: string | null = null;\nlet fpPending: Promise<string | null> | null = null;\n\nfunction tryLocalStorage(): Storage | null {\n try {\n const s = globalThis.localStorage;\n const key = \"__interfere_device_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nfunction getCookie(name: string): string | null {\n if (typeof document === \"undefined\") {\n return null;\n }\n const match = document.cookie\n .split(\"; \")\n .find((c) => c.startsWith(`${name}=`));\n return match ? decodeURIComponent(match.split(\"=\")[1] ?? \"\") : null;\n}\n\nfunction setCookie(name: string, value: string): void {\n if (typeof document === \"undefined\") {\n return;\n }\n const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;\n // biome-ignore lint/suspicious/noDocumentCookie: Cookie Store API is async and not universally supported; synchronous access is required here\n document.cookie = `${name}=${encodeURIComponent(value)};path=/;max-age=${maxAge};SameSite=Lax`;\n}\n\nfunction generateId(): string {\n return crypto.randomUUID();\n}\n\nexport function initDevice(): void {\n if (deviceId) {\n return;\n }\n\n const ls = tryLocalStorage();\n const fromLs = ls?.getItem(LS_KEY) ?? null;\n const fromCookie = getCookie(COOKIE_NAME);\n\n deviceId = fromLs ?? fromCookie ?? generateId();\n\n if (!fromLs && ls) {\n ls.setItem(LS_KEY, deviceId);\n }\n if (!fromCookie) {\n setCookie(COOKIE_NAME, deviceId);\n }\n if (fromLs && !fromCookie) {\n setCookie(COOKIE_NAME, deviceId);\n }\n if (fromCookie && !fromLs && ls) {\n ls.setItem(LS_KEY, deviceId);\n }\n\n log.debug(\"device %s (ls=%s cookie=%s)\", deviceId, !!fromLs, !!fromCookie);\n\n initFpHash();\n}\n\nfunction initFpHash(): void {\n if (fpHash || fpPending) {\n return;\n }\n\n fpPending = (async () => {\n try {\n const FingerprintJS = await import(\"@fingerprintjs/fingerprintjs\");\n const fp = await FingerprintJS.load();\n const result = await fp.get();\n fpHash = result.visitorId;\n log.debug(\"fpHash %s\", fpHash);\n return fpHash;\n } catch {\n log.warn(\"fp hash failed\");\n return null;\n }\n })();\n}\n\nexport function getDeviceId(): string | null {\n return deviceId;\n}\n\nexport function getFpHash(): string | null {\n return fpHash;\n}\n\nexport function whenDeviceReady(): Promise<string | null> {\n if (deviceId) {\n return Promise.resolve(deviceId);\n }\n return Promise.resolve(null);\n}\n\nexport function resetDevice(): void {\n deviceId = null;\n fpHash = null;\n fpPending = null;\n}\n"],"mappings":";;AAEA,MAAM,MAAM,aAAa,SAAS;AAElC,MAAM,SAAS;AACf,MAAM,cAAc;AACpB,MAAM,sBAAsB;AAE5B,IAAI,WAA0B;AAC9B,IAAI,SAAwB;AAC5B,IAAI,YAA2C;AAE/C,SAAS,kBAAkC;AACzC,KAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;AACZ,IAAE,QAAQ,KAAK,IAAI;AACnB,IAAE,WAAW,IAAI;AACjB,SAAO;SACD;AACN,SAAO;;;AAIX,SAAS,UAAU,MAA6B;AAC9C,KAAI,OAAO,aAAa,YACtB,QAAO;CAET,MAAM,QAAQ,SAAS,OACpB,MAAM,KAAK,CACX,MAAM,MAAM,EAAE,WAAW,GAAG,KAAK,GAAG,CAAC;AACxC,QAAO,QAAQ,mBAAmB,MAAM,MAAM,IAAI,CAAC,MAAM,GAAG,GAAG;;AAGjE,SAAS,UAAU,MAAc,OAAqB;AACpD,KAAI,OAAO,aAAa,YACtB;CAEF,MAAM,SAAS,sBAAsB,KAAK,KAAK;AAE/C,UAAS,SAAS,GAAG,KAAK,GAAG,mBAAmB,MAAM,CAAC,kBAAkB,OAAO;;AAGlF,SAAS,aAAqB;AAC5B,QAAO,OAAO,YAAY;;AAG5B,SAAgB,aAAmB;AACjC,KAAI,SACF;CAGF,MAAM,KAAK,iBAAiB;CAC5B,MAAM,SAAS,IAAI,QAAQ,OAAO,IAAI;CACtC,MAAM,aAAa,UAAU,YAAY;AAEzC,YAAW,UAAU,cAAc,YAAY;AAE/C,KAAI,CAAC,UAAU,GACb,IAAG,QAAQ,QAAQ,SAAS;AAE9B,KAAI,CAAC,WACH,WAAU,aAAa,SAAS;AAElC,KAAI,UAAU,CAAC,WACb,WAAU,aAAa,SAAS;AAElC,KAAI,cAAc,CAAC,UAAU,GAC3B,IAAG,QAAQ,QAAQ,SAAS;AAG9B,KAAI,MAAM,+BAA+B,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW;AAE1E,aAAY;;AAGd,SAAS,aAAmB;AAC1B,KAAI,UAAU,UACZ;AAGF,cAAa,YAAY;AACvB,MAAI;AAIF,aADe,OADJ,OADW,MAAM,OAAO,iCACJ,MAAM,EACb,KAAK,EACb;AAChB,OAAI,MAAM,aAAa,OAAO;AAC9B,UAAO;UACD;AACN,OAAI,KAAK,iBAAiB;AAC1B,UAAO;;KAEP;;AAGN,SAAgB,cAA6B;AAC3C,QAAO;;AAGT,SAAgB,YAA2B;AACzC,QAAO;;AAGT,SAAgB,kBAA0C;AACxD,KAAI,SACF,QAAO,QAAQ,QAAQ,SAAS;AAElC,QAAO,QAAQ,QAAQ,KAAK;;AAG9B,SAAgB,cAAoB;AAClC,YAAW;AACX,UAAS;AACT,aAAY"}
@@ -0,0 +1,19 @@
1
+ //#region src/tracking/session.d.ts
2
+ type OnRotate = (sessionId: string) => void;
3
+ declare class SessionManager {
4
+ sessionId: string | null;
5
+ windowId: string | null;
6
+ private readonly local;
7
+ private readonly session;
8
+ private readonly onRotate;
9
+ private lastActivityMs;
10
+ constructor(onRotate?: OnRotate);
11
+ getSessionId(): string;
12
+ getWindowId(): string;
13
+ private restore;
14
+ private rotate;
15
+ private touch;
16
+ private isExpired;
17
+ }
18
+ //#endregion
19
+ export { OnRotate, SessionManager };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.mts","names":[],"sources":["../../src/tracking/session.ts"],"mappings":";KAmBY,QAAA,IAAY,SAAA;AAAA,cAEX,cAAA;EACX,SAAA;EACA,QAAA;EAAA,iBACiB,KAAA;EAAA,iBACA,OAAA;EAAA,iBACA,QAAA;EAAA,QACT,cAAA;cAEI,QAAA,GAAW,QAAA;EAOvB,YAAA,CAAA;EAQA,WAAA,CAAA;EAAA,QAgBQ,OAAA;EAAA,QAWA,MAAA;EAAA,QAQA,KAAA;EAAA,QAKA,SAAA;AAAA"}
@@ -0,0 +1,75 @@
1
+ import { v7 } from "uuid";
2
+ //#region src/tracking/session.ts
3
+ const SESSION_ID_KEY = "interfere:session_id";
4
+ const LAST_ACTIVITY_KEY = "interfere:last_activity";
5
+ const WINDOW_ID_KEY = "interfere:window_id";
6
+ const SESSION_TIMEOUT_MS = 1800 * 1e3;
7
+ function tryStorage(type) {
8
+ try {
9
+ const s = globalThis[type];
10
+ const key = "__interfere_probe__";
11
+ s.setItem(key, "1");
12
+ s.removeItem(key);
13
+ return s;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+ var SessionManager = class {
19
+ sessionId = null;
20
+ windowId = null;
21
+ local;
22
+ session;
23
+ onRotate = null;
24
+ lastActivityMs = 0;
25
+ constructor(onRotate) {
26
+ this.local = tryStorage("localStorage");
27
+ this.session = tryStorage("sessionStorage");
28
+ this.onRotate = onRotate ?? null;
29
+ this.restore();
30
+ }
31
+ getSessionId() {
32
+ if (this.sessionId && !this.isExpired()) {
33
+ this.touch();
34
+ return this.sessionId;
35
+ }
36
+ return this.rotate();
37
+ }
38
+ getWindowId() {
39
+ if (this.windowId) return this.windowId;
40
+ const stored = this.session?.getItem(WINDOW_ID_KEY);
41
+ if (stored) {
42
+ this.windowId = stored;
43
+ return stored;
44
+ }
45
+ this.windowId = `win_${crypto.randomUUID()}`;
46
+ this.session?.setItem(WINDOW_ID_KEY, this.windowId);
47
+ return this.windowId;
48
+ }
49
+ restore() {
50
+ const stored = this.local?.getItem(SESSION_ID_KEY);
51
+ if (stored && !this.isExpired()) {
52
+ this.sessionId = stored;
53
+ this.touch();
54
+ } else this.rotate();
55
+ }
56
+ rotate() {
57
+ this.sessionId = v7();
58
+ this.local?.setItem(SESSION_ID_KEY, this.sessionId);
59
+ this.touch();
60
+ this.onRotate?.(this.sessionId);
61
+ return this.sessionId;
62
+ }
63
+ touch() {
64
+ this.lastActivityMs = Date.now();
65
+ this.local?.setItem(LAST_ACTIVITY_KEY, String(this.lastActivityMs));
66
+ }
67
+ isExpired() {
68
+ const raw = this.local?.getItem(LAST_ACTIVITY_KEY);
69
+ const ts = raw ? Number(raw) : this.lastActivityMs;
70
+ if (ts === 0) return true;
71
+ return Date.now() - ts > SESSION_TIMEOUT_MS;
72
+ }
73
+ };
74
+ //#endregion
75
+ export { SessionManager };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.mjs","names":["uuidv7"],"sources":["../../src/tracking/session.ts"],"sourcesContent":["import { v7 as uuidv7 } from \"uuid\";\n\nconst SESSION_ID_KEY = \"interfere:session_id\";\nconst LAST_ACTIVITY_KEY = \"interfere:last_activity\";\nconst WINDOW_ID_KEY = \"interfere:window_id\";\nconst SESSION_TIMEOUT_MS = 30 * 60 * 1000;\n\nfunction tryStorage(type: \"localStorage\" | \"sessionStorage\"): Storage | null {\n try {\n const s = globalThis[type];\n const key = \"__interfere_probe__\";\n s.setItem(key, \"1\");\n s.removeItem(key);\n return s;\n } catch {\n return null;\n }\n}\n\nexport type OnRotate = (sessionId: string) => void;\n\nexport class SessionManager {\n sessionId: string | null = null;\n windowId: string | null = null;\n private readonly local: Storage | null;\n private readonly session: Storage | null;\n private readonly onRotate: OnRotate | null = null;\n private lastActivityMs = 0;\n\n constructor(onRotate?: OnRotate) {\n this.local = tryStorage(\"localStorage\");\n this.session = tryStorage(\"sessionStorage\");\n this.onRotate = onRotate ?? null;\n this.restore();\n }\n\n getSessionId(): string {\n if (this.sessionId && !this.isExpired()) {\n this.touch();\n return this.sessionId;\n }\n return this.rotate();\n }\n\n getWindowId(): string {\n if (this.windowId) {\n return this.windowId;\n }\n\n const stored = this.session?.getItem(WINDOW_ID_KEY);\n if (stored) {\n this.windowId = stored;\n return stored;\n }\n\n this.windowId = `win_${crypto.randomUUID()}`;\n this.session?.setItem(WINDOW_ID_KEY, this.windowId);\n return this.windowId;\n }\n\n private restore(): void {\n const stored = this.local?.getItem(SESSION_ID_KEY);\n\n if (stored && !this.isExpired()) {\n this.sessionId = stored;\n this.touch();\n } else {\n this.rotate();\n }\n }\n\n private rotate(): string {\n this.sessionId = uuidv7();\n this.local?.setItem(SESSION_ID_KEY, this.sessionId);\n this.touch();\n this.onRotate?.(this.sessionId);\n return this.sessionId;\n }\n\n private touch(): void {\n this.lastActivityMs = Date.now();\n this.local?.setItem(LAST_ACTIVITY_KEY, String(this.lastActivityMs));\n }\n\n private isExpired(): boolean {\n const raw = this.local?.getItem(LAST_ACTIVITY_KEY);\n const ts = raw ? Number(raw) : this.lastActivityMs;\n\n if (ts === 0) {\n return true;\n }\n return Date.now() - ts > SESSION_TIMEOUT_MS;\n }\n}\n"],"mappings":";;AAEA,MAAM,iBAAiB;AACvB,MAAM,oBAAoB;AAC1B,MAAM,gBAAgB;AACtB,MAAM,qBAAqB,OAAU;AAErC,SAAS,WAAW,MAAyD;AAC3E,KAAI;EACF,MAAM,IAAI,WAAW;EACrB,MAAM,MAAM;AACZ,IAAE,QAAQ,KAAK,IAAI;AACnB,IAAE,WAAW,IAAI;AACjB,SAAO;SACD;AACN,SAAO;;;AAMX,IAAa,iBAAb,MAA4B;CAC1B,YAA2B;CAC3B,WAA0B;CAC1B;CACA;CACA,WAA6C;CAC7C,iBAAyB;CAEzB,YAAY,UAAqB;AAC/B,OAAK,QAAQ,WAAW,eAAe;AACvC,OAAK,UAAU,WAAW,iBAAiB;AAC3C,OAAK,WAAW,YAAY;AAC5B,OAAK,SAAS;;CAGhB,eAAuB;AACrB,MAAI,KAAK,aAAa,CAAC,KAAK,WAAW,EAAE;AACvC,QAAK,OAAO;AACZ,UAAO,KAAK;;AAEd,SAAO,KAAK,QAAQ;;CAGtB,cAAsB;AACpB,MAAI,KAAK,SACP,QAAO,KAAK;EAGd,MAAM,SAAS,KAAK,SAAS,QAAQ,cAAc;AACnD,MAAI,QAAQ;AACV,QAAK,WAAW;AAChB,UAAO;;AAGT,OAAK,WAAW,OAAO,OAAO,YAAY;AAC1C,OAAK,SAAS,QAAQ,eAAe,KAAK,SAAS;AACnD,SAAO,KAAK;;CAGd,UAAwB;EACtB,MAAM,SAAS,KAAK,OAAO,QAAQ,eAAe;AAElD,MAAI,UAAU,CAAC,KAAK,WAAW,EAAE;AAC/B,QAAK,YAAY;AACjB,QAAK,OAAO;QAEZ,MAAK,QAAQ;;CAIjB,SAAyB;AACvB,OAAK,YAAYA,IAAQ;AACzB,OAAK,OAAO,QAAQ,gBAAgB,KAAK,UAAU;AACnD,OAAK,OAAO;AACZ,OAAK,WAAW,KAAK,UAAU;AAC/B,SAAO,KAAK;;CAGd,QAAsB;AACpB,OAAK,iBAAiB,KAAK,KAAK;AAChC,OAAK,OAAO,QAAQ,mBAAmB,OAAO,KAAK,eAAe,CAAC;;CAGrE,YAA6B;EAC3B,MAAM,MAAM,KAAK,OAAO,QAAQ,kBAAkB;EAClD,MAAM,KAAK,MAAM,OAAO,IAAI,GAAG,KAAK;AAEpC,MAAI,OAAO,EACT,QAAO;AAET,SAAO,KAAK,KAAK,GAAG,KAAK"}
@@ -0,0 +1,21 @@
1
+ import { Envelope } from "@interfere/types/sdk/envelope";
2
+
3
+ //#region src/transport/http.d.ts
4
+ interface IngestTarget {
5
+ headers: Headers;
6
+ url: string;
7
+ }
8
+ interface DeliveryMeta {
9
+ queueDepth: number;
10
+ retryCount: number;
11
+ }
12
+ declare function buildHeaders(base: Headers, meta?: DeliveryMeta): Record<string, string>;
13
+ declare function hasServiceWorker(): boolean;
14
+ declare class HttpTransport {
15
+ private readonly target;
16
+ private pendingKeepalive;
17
+ constructor(target: IngestTarget);
18
+ send(envelopes: Envelope[], meta?: DeliveryMeta): Promise<void>;
19
+ }
20
+ //#endregion
21
+ export { DeliveryMeta, HttpTransport, IngestTarget, buildHeaders, hasServiceWorker };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.mts","names":[],"sources":["../../src/transport/http.ts"],"mappings":";;;UAWiB,YAAA;EACf,OAAA,EAAS,OAAA;EACT,GAAA;AAAA;AAAA,UAGe,YAAA;EACf,UAAA;EACA,UAAA;AAAA;AAAA,iBAGc,YAAA,CACd,IAAA,EAAM,OAAA,EACN,IAAA,GAAO,YAAA,GACN,MAAA;AAAA,iBA4Ba,gBAAA,CAAA;AAAA,cAiBH,aAAA;EAAA,iBACM,MAAA;EAAA,QACT,gBAAA;cAEI,MAAA,EAAQ,YAAA;EAId,IAAA,CAAK,SAAA,EAAW,QAAA,IAAY,IAAA,GAAO,YAAA,GAAe,OAAA;AAAA"}
@@ -0,0 +1,68 @@
1
+ import { PRODUCER_VERSION } from "../internal/version.mjs";
2
+ import { createLogger } from "../util/log.mjs";
3
+ import { getDeviceId } from "../tracking/device.mjs";
4
+ import { session } from "../tracking/api.mjs";
5
+ //#region src/transport/http.ts
6
+ const log = createLogger("http");
7
+ const SEND_TIMEOUT_MS = 1e4;
8
+ function buildHeaders(base, meta) {
9
+ const h = Object.fromEntries(base.entries());
10
+ h["x-interfere-sdk-version"] = PRODUCER_VERSION;
11
+ const sessionId = session.getId();
12
+ if (sessionId) h["x-interfere-session"] = sessionId;
13
+ const windowId = session.getWindowId();
14
+ if (windowId) h["x-interfere-window"] = windowId;
15
+ const deviceId = getDeviceId();
16
+ if (deviceId) h["x-interfere-device"] = deviceId;
17
+ if (meta) {
18
+ h["x-interfere-retry-count"] = String(meta.retryCount);
19
+ h["x-interfere-queue-depth"] = String(meta.queueDepth);
20
+ }
21
+ return h;
22
+ }
23
+ function hasServiceWorker() {
24
+ return typeof navigator !== "undefined" && "serviceWorker" in navigator && navigator.serviceWorker.controller !== null;
25
+ }
26
+ function assertOk(response) {
27
+ if (!response.ok) throw new Error(`ingest responded ${response.status}`);
28
+ }
29
+ const KEEPALIVE_BUDGET_BYTES = 61440;
30
+ const MAX_CONCURRENT_KEEPALIVE = 15;
31
+ var HttpTransport = class {
32
+ target;
33
+ pendingKeepalive = 0;
34
+ constructor(target) {
35
+ this.target = target;
36
+ }
37
+ async send(envelopes, meta) {
38
+ const body = JSON.stringify(envelopes);
39
+ const headers = buildHeaders(this.target.headers, meta);
40
+ if (hasServiceWorker()) {
41
+ log.debug("POST %d envelopes via SW", envelopes.length);
42
+ assertOk(await fetch(this.target.url, {
43
+ method: "POST",
44
+ headers,
45
+ body,
46
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS)
47
+ }));
48
+ return;
49
+ }
50
+ const bytes = new TextEncoder().encode(body).byteLength;
51
+ const useKeepalive = bytes < KEEPALIVE_BUDGET_BYTES && this.pendingKeepalive < MAX_CONCURRENT_KEEPALIVE;
52
+ if (useKeepalive) this.pendingKeepalive++;
53
+ log.debug("POST %d envelopes direct (%d bytes, keepalive=%s)", envelopes.length, bytes, useKeepalive);
54
+ try {
55
+ assertOk(await fetch(this.target.url, {
56
+ method: "POST",
57
+ headers,
58
+ body,
59
+ keepalive: useKeepalive,
60
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS)
61
+ }));
62
+ } finally {
63
+ if (useKeepalive) this.pendingKeepalive--;
64
+ }
65
+ }
66
+ };
67
+ //#endregion
68
+ export { HttpTransport, buildHeaders, hasServiceWorker };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.mjs","names":[],"sources":["../../src/transport/http.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { PRODUCER_VERSION } from \"../internal/version.js\";\nimport { session } from \"../tracking/api.js\";\nimport { getDeviceId } from \"../tracking/device.js\";\nimport { createLogger } from \"../util/log.js\";\n\nconst log = createLogger(\"http\");\n\nconst SEND_TIMEOUT_MS = 10_000;\n\nexport interface IngestTarget {\n headers: Headers;\n url: string;\n}\n\nexport interface DeliveryMeta {\n queueDepth: number;\n retryCount: number;\n}\n\nexport function buildHeaders(\n base: Headers,\n meta?: DeliveryMeta\n): Record<string, string> {\n const h: Record<string, string> = Object.fromEntries(base.entries());\n\n h[\"x-interfere-sdk-version\"] = PRODUCER_VERSION;\n\n const sessionId = session.getId();\n if (sessionId) {\n h[\"x-interfere-session\"] = sessionId;\n }\n\n const windowId = session.getWindowId();\n if (windowId) {\n h[\"x-interfere-window\"] = windowId;\n }\n\n const deviceId = getDeviceId();\n if (deviceId) {\n h[\"x-interfere-device\"] = deviceId;\n }\n\n if (meta) {\n h[\"x-interfere-retry-count\"] = String(meta.retryCount);\n h[\"x-interfere-queue-depth\"] = String(meta.queueDepth);\n }\n\n return h;\n}\n\nexport function hasServiceWorker(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n navigator.serviceWorker.controller !== null\n );\n}\n\nfunction assertOk(response: Response): void {\n if (!response.ok) {\n throw new Error(`ingest responded ${response.status}`);\n }\n}\n\nconst KEEPALIVE_BUDGET_BYTES = 61_440;\nconst MAX_CONCURRENT_KEEPALIVE = 15;\n\nexport class HttpTransport {\n private readonly target: IngestTarget;\n private pendingKeepalive = 0;\n\n constructor(target: IngestTarget) {\n this.target = target;\n }\n\n async send(envelopes: Envelope[], meta?: DeliveryMeta): Promise<void> {\n const body = JSON.stringify(envelopes);\n const headers = buildHeaders(this.target.headers, meta);\n\n if (hasServiceWorker()) {\n log.debug(\"POST %d envelopes via SW\", envelopes.length);\n const res = await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n signal: AbortSignal.timeout(SEND_TIMEOUT_MS),\n });\n assertOk(res);\n return;\n }\n\n const bytes = new TextEncoder().encode(body).byteLength;\n const useKeepalive =\n bytes < KEEPALIVE_BUDGET_BYTES &&\n this.pendingKeepalive < MAX_CONCURRENT_KEEPALIVE;\n\n if (useKeepalive) {\n this.pendingKeepalive++;\n }\n\n log.debug(\n \"POST %d envelopes direct (%d bytes, keepalive=%s)\",\n envelopes.length,\n bytes,\n useKeepalive\n );\n\n try {\n const res = await fetch(this.target.url, {\n method: \"POST\",\n headers,\n body,\n keepalive: useKeepalive,\n signal: AbortSignal.timeout(SEND_TIMEOUT_MS),\n });\n assertOk(res);\n } finally {\n if (useKeepalive) {\n this.pendingKeepalive--;\n }\n }\n }\n}\n"],"mappings":";;;;;AAOA,MAAM,MAAM,aAAa,OAAO;AAEhC,MAAM,kBAAkB;AAYxB,SAAgB,aACd,MACA,MACwB;CACxB,MAAM,IAA4B,OAAO,YAAY,KAAK,SAAS,CAAC;AAEpE,GAAE,6BAA6B;CAE/B,MAAM,YAAY,QAAQ,OAAO;AACjC,KAAI,UACF,GAAE,yBAAyB;CAG7B,MAAM,WAAW,QAAQ,aAAa;AACtC,KAAI,SACF,GAAE,wBAAwB;CAG5B,MAAM,WAAW,aAAa;AAC9B,KAAI,SACF,GAAE,wBAAwB;AAG5B,KAAI,MAAM;AACR,IAAE,6BAA6B,OAAO,KAAK,WAAW;AACtD,IAAE,6BAA6B,OAAO,KAAK,WAAW;;AAGxD,QAAO;;AAGT,SAAgB,mBAA4B;AAC1C,QACE,OAAO,cAAc,eACrB,mBAAmB,aACnB,UAAU,cAAc,eAAe;;AAI3C,SAAS,SAAS,UAA0B;AAC1C,KAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,oBAAoB,SAAS,SAAS;;AAI1D,MAAM,yBAAyB;AAC/B,MAAM,2BAA2B;AAEjC,IAAa,gBAAb,MAA2B;CACzB;CACA,mBAA2B;CAE3B,YAAY,QAAsB;AAChC,OAAK,SAAS;;CAGhB,MAAM,KAAK,WAAuB,MAAoC;EACpE,MAAM,OAAO,KAAK,UAAU,UAAU;EACtC,MAAM,UAAU,aAAa,KAAK,OAAO,SAAS,KAAK;AAEvD,MAAI,kBAAkB,EAAE;AACtB,OAAI,MAAM,4BAA4B,UAAU,OAAO;AAOvD,YANY,MAAM,MAAM,KAAK,OAAO,KAAK;IACvC,QAAQ;IACR;IACA;IACA,QAAQ,YAAY,QAAQ,gBAAgB;IAC7C,CAAC,CACW;AACb;;EAGF,MAAM,QAAQ,IAAI,aAAa,CAAC,OAAO,KAAK,CAAC;EAC7C,MAAM,eACJ,QAAQ,0BACR,KAAK,mBAAmB;AAE1B,MAAI,aACF,MAAK;AAGP,MAAI,MACF,qDACA,UAAU,QACV,OACA,aACD;AAED,MAAI;AAQF,YAPY,MAAM,MAAM,KAAK,OAAO,KAAK;IACvC,QAAQ;IACR;IACA;IACA,WAAW;IACX,QAAQ,YAAY,QAAQ,gBAAgB;IAC7C,CAAC,CACW;YACL;AACR,OAAI,aACF,MAAK"}
@@ -0,0 +1,31 @@
1
+ import { HttpTransport } from "./http.mjs";
2
+ import { Envelope } from "@interfere/types/sdk/envelope";
3
+
4
+ //#region src/transport/queue.d.ts
5
+ interface QueueOptions {
6
+ batchMs?: number;
7
+ batchSize?: number;
8
+ maxBufferSize?: number;
9
+ transport: HttpTransport;
10
+ }
11
+ declare class BatchQueue {
12
+ private readonly buffer;
13
+ private timer;
14
+ private flushing;
15
+ private failures;
16
+ private pausedUntil;
17
+ private readonly batchSize;
18
+ private readonly batchMs;
19
+ private readonly maxBufferSize;
20
+ private readonly transport;
21
+ constructor(opts: QueueOptions);
22
+ start(): void;
23
+ enqueue(envelope: Envelope): void;
24
+ flush(): void;
25
+ dispose(): void;
26
+ private drain;
27
+ private readonly onVisibilityChange;
28
+ private readonly onBeforeUnload;
29
+ }
30
+ //#endregion
31
+ export { BatchQueue, QueueOptions };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.d.mts","names":[],"sources":["../../src/transport/queue.ts"],"mappings":";;;;UAciB,YAAA;EACf,OAAA;EACA,SAAA;EACA,aAAA;EACA,SAAA,EAAW,aAAA;AAAA;AAAA,cAGA,UAAA;EAAA,iBACM,MAAA;EAAA,QACT,KAAA;EAAA,QACA,QAAA;EAAA,QACA,QAAA;EAAA,QACA,WAAA;EAAA,iBACS,SAAA;EAAA,iBACA,OAAA;EAAA,iBACA,aAAA;EAAA,iBACA,SAAA;cAEL,IAAA,EAAM,YAAA;EAOlB,KAAA,CAAA;EAeA,OAAA,CAAQ,QAAA,EAAU,QAAA;EAclB,KAAA,CAAA;EA2CA,OAAA,CAAA;EAAA,QAiBQ,KAAA;EAAA,iBASS,kBAAA;EAAA,iBAMA,cAAA;AAAA"}
@@ -0,0 +1,93 @@
1
+ import { createLogger } from "../util/log.mjs";
2
+ import { hasServiceWorker } from "./http.mjs";
3
+ //#region src/transport/queue.ts
4
+ const log = createLogger("queue");
5
+ const DEFAULT_BATCH_SIZE = 10;
6
+ const DEFAULT_BATCH_MS = 250;
7
+ const DEFAULT_MAX_BUFFER_SIZE = 1e3;
8
+ const BREAKER_THRESHOLD = 5;
9
+ const BREAKER_COOLDOWN_MS = 3e4;
10
+ var BatchQueue = class {
11
+ buffer = [];
12
+ timer = null;
13
+ flushing = false;
14
+ failures = 0;
15
+ pausedUntil = 0;
16
+ batchSize;
17
+ batchMs;
18
+ maxBufferSize;
19
+ transport;
20
+ constructor(opts) {
21
+ this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
22
+ this.batchMs = opts.batchMs ?? DEFAULT_BATCH_MS;
23
+ this.maxBufferSize = opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
24
+ this.transport = opts.transport;
25
+ }
26
+ start() {
27
+ if (this.timer) return;
28
+ this.timer = setInterval(() => {
29
+ this.flush();
30
+ }, this.batchMs);
31
+ if (typeof globalThis.addEventListener === "function") {
32
+ globalThis.addEventListener("visibilitychange", this.onVisibilityChange);
33
+ globalThis.addEventListener("beforeunload", this.onBeforeUnload);
34
+ }
35
+ }
36
+ enqueue(envelope) {
37
+ this.buffer.push(envelope);
38
+ if (this.buffer.length > this.maxBufferSize) {
39
+ const overflow = this.buffer.length - this.maxBufferSize;
40
+ this.buffer.splice(0, overflow);
41
+ log.warn("buffer full, dropped %d oldest envelopes", overflow);
42
+ }
43
+ if (this.buffer.length >= this.batchSize) this.flush();
44
+ }
45
+ flush() {
46
+ if (this.flushing || this.buffer.length === 0) return;
47
+ if (this.failures >= BREAKER_THRESHOLD && Date.now() < this.pausedUntil) return;
48
+ this.flushing = true;
49
+ const batch = this.buffer.splice(0, this.batchSize);
50
+ const meta = {
51
+ retryCount: this.failures,
52
+ queueDepth: this.buffer.length
53
+ };
54
+ this.transport.send(batch, meta).then(() => {
55
+ if (this.failures > 0) log.info("send recovered after %d failures", this.failures);
56
+ this.failures = 0;
57
+ }).catch(() => {
58
+ if (!hasServiceWorker()) this.buffer.unshift(...batch);
59
+ this.failures++;
60
+ if (this.failures >= BREAKER_THRESHOLD) {
61
+ this.pausedUntil = Date.now() + BREAKER_COOLDOWN_MS;
62
+ log.warn("pausing sends for %dms after %d consecutive failures", BREAKER_COOLDOWN_MS, this.failures);
63
+ }
64
+ }).finally(() => {
65
+ this.flushing = false;
66
+ });
67
+ }
68
+ dispose() {
69
+ if (this.timer) {
70
+ clearInterval(this.timer);
71
+ this.timer = null;
72
+ }
73
+ if (typeof globalThis.removeEventListener === "function") {
74
+ globalThis.removeEventListener("visibilitychange", this.onVisibilityChange);
75
+ globalThis.removeEventListener("beforeunload", this.onBeforeUnload);
76
+ }
77
+ this.drain();
78
+ }
79
+ drain() {
80
+ while (this.buffer.length > 0) {
81
+ const batch = this.buffer.splice(0, this.batchSize);
82
+ this.transport.send(batch).catch(() => {});
83
+ }
84
+ }
85
+ onVisibilityChange = () => {
86
+ if (document.visibilityState === "hidden") this.drain();
87
+ };
88
+ onBeforeUnload = () => {
89
+ this.drain();
90
+ };
91
+ };
92
+ //#endregion
93
+ export { BatchQueue };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.mjs","names":[],"sources":["../../src/transport/queue.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { createLogger } from \"../util/log.js\";\nimport { type HttpTransport, hasServiceWorker } from \"./http.js\";\n\nconst log = createLogger(\"queue\");\n\nconst DEFAULT_BATCH_SIZE = 10;\nconst DEFAULT_BATCH_MS = 250;\nconst DEFAULT_MAX_BUFFER_SIZE = 1000;\n\nconst BREAKER_THRESHOLD = 5;\nconst BREAKER_COOLDOWN_MS = 30_000;\n\nexport interface QueueOptions {\n batchMs?: number;\n batchSize?: number;\n maxBufferSize?: number;\n transport: HttpTransport;\n}\n\nexport class BatchQueue {\n private readonly buffer: Envelope[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private flushing = false;\n private failures = 0;\n private pausedUntil = 0;\n private readonly batchSize: number;\n private readonly batchMs: number;\n private readonly maxBufferSize: number;\n private readonly transport: HttpTransport;\n\n constructor(opts: QueueOptions) {\n this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;\n this.batchMs = opts.batchMs ?? DEFAULT_BATCH_MS;\n this.maxBufferSize = opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;\n this.transport = opts.transport;\n }\n\n start(): void {\n if (this.timer) {\n return;\n }\n\n this.timer = setInterval(() => {\n this.flush();\n }, this.batchMs);\n\n if (typeof globalThis.addEventListener === \"function\") {\n globalThis.addEventListener(\"visibilitychange\", this.onVisibilityChange);\n globalThis.addEventListener(\"beforeunload\", this.onBeforeUnload);\n }\n }\n\n enqueue(envelope: Envelope): void {\n this.buffer.push(envelope);\n\n if (this.buffer.length > this.maxBufferSize) {\n const overflow = this.buffer.length - this.maxBufferSize;\n this.buffer.splice(0, overflow);\n log.warn(\"buffer full, dropped %d oldest envelopes\", overflow);\n }\n\n if (this.buffer.length >= this.batchSize) {\n this.flush();\n }\n }\n\n flush(): void {\n if (this.flushing || this.buffer.length === 0) {\n return;\n }\n\n if (this.failures >= BREAKER_THRESHOLD && Date.now() < this.pausedUntil) {\n return;\n }\n\n this.flushing = true;\n const batch = this.buffer.splice(0, this.batchSize);\n const meta = {\n retryCount: this.failures,\n queueDepth: this.buffer.length,\n };\n\n this.transport\n .send(batch, meta)\n .then(() => {\n if (this.failures > 0) {\n log.info(\"send recovered after %d failures\", this.failures);\n }\n this.failures = 0;\n })\n .catch(() => {\n if (!hasServiceWorker()) {\n this.buffer.unshift(...batch);\n }\n this.failures++;\n if (this.failures >= BREAKER_THRESHOLD) {\n this.pausedUntil = Date.now() + BREAKER_COOLDOWN_MS;\n log.warn(\n \"pausing sends for %dms after %d consecutive failures\",\n BREAKER_COOLDOWN_MS,\n this.failures\n );\n }\n })\n .finally(() => {\n this.flushing = false;\n });\n }\n\n dispose(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n\n if (typeof globalThis.removeEventListener === \"function\") {\n globalThis.removeEventListener(\n \"visibilitychange\",\n this.onVisibilityChange\n );\n globalThis.removeEventListener(\"beforeunload\", this.onBeforeUnload);\n }\n\n this.drain();\n }\n\n private drain(): void {\n while (this.buffer.length > 0) {\n const batch = this.buffer.splice(0, this.batchSize);\n this.transport.send(batch).catch(() => {\n /* best-effort — SW BackgroundSync handles persistence */\n });\n }\n }\n\n private readonly onVisibilityChange = (): void => {\n if (document.visibilityState === \"hidden\") {\n this.drain();\n }\n };\n\n private readonly onBeforeUnload = (): void => {\n this.drain();\n };\n}\n"],"mappings":";;;AAKA,MAAM,MAAM,aAAa,QAAQ;AAEjC,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,0BAA0B;AAEhC,MAAM,oBAAoB;AAC1B,MAAM,sBAAsB;AAS5B,IAAa,aAAb,MAAwB;CACtB,SAAsC,EAAE;CACxC,QAAuD;CACvD,WAAmB;CACnB,WAAmB;CACnB,cAAsB;CACtB;CACA;CACA;CACA;CAEA,YAAY,MAAoB;AAC9B,OAAK,YAAY,KAAK,aAAa;AACnC,OAAK,UAAU,KAAK,WAAW;AAC/B,OAAK,gBAAgB,KAAK,iBAAiB;AAC3C,OAAK,YAAY,KAAK;;CAGxB,QAAc;AACZ,MAAI,KAAK,MACP;AAGF,OAAK,QAAQ,kBAAkB;AAC7B,QAAK,OAAO;KACX,KAAK,QAAQ;AAEhB,MAAI,OAAO,WAAW,qBAAqB,YAAY;AACrD,cAAW,iBAAiB,oBAAoB,KAAK,mBAAmB;AACxE,cAAW,iBAAiB,gBAAgB,KAAK,eAAe;;;CAIpE,QAAQ,UAA0B;AAChC,OAAK,OAAO,KAAK,SAAS;AAE1B,MAAI,KAAK,OAAO,SAAS,KAAK,eAAe;GAC3C,MAAM,WAAW,KAAK,OAAO,SAAS,KAAK;AAC3C,QAAK,OAAO,OAAO,GAAG,SAAS;AAC/B,OAAI,KAAK,4CAA4C,SAAS;;AAGhE,MAAI,KAAK,OAAO,UAAU,KAAK,UAC7B,MAAK,OAAO;;CAIhB,QAAc;AACZ,MAAI,KAAK,YAAY,KAAK,OAAO,WAAW,EAC1C;AAGF,MAAI,KAAK,YAAY,qBAAqB,KAAK,KAAK,GAAG,KAAK,YAC1D;AAGF,OAAK,WAAW;EAChB,MAAM,QAAQ,KAAK,OAAO,OAAO,GAAG,KAAK,UAAU;EACnD,MAAM,OAAO;GACX,YAAY,KAAK;GACjB,YAAY,KAAK,OAAO;GACzB;AAED,OAAK,UACF,KAAK,OAAO,KAAK,CACjB,WAAW;AACV,OAAI,KAAK,WAAW,EAClB,KAAI,KAAK,oCAAoC,KAAK,SAAS;AAE7D,QAAK,WAAW;IAChB,CACD,YAAY;AACX,OAAI,CAAC,kBAAkB,CACrB,MAAK,OAAO,QAAQ,GAAG,MAAM;AAE/B,QAAK;AACL,OAAI,KAAK,YAAY,mBAAmB;AACtC,SAAK,cAAc,KAAK,KAAK,GAAG;AAChC,QAAI,KACF,wDACA,qBACA,KAAK,SACN;;IAEH,CACD,cAAc;AACb,QAAK,WAAW;IAChB;;CAGN,UAAgB;AACd,MAAI,KAAK,OAAO;AACd,iBAAc,KAAK,MAAM;AACzB,QAAK,QAAQ;;AAGf,MAAI,OAAO,WAAW,wBAAwB,YAAY;AACxD,cAAW,oBACT,oBACA,KAAK,mBACN;AACD,cAAW,oBAAoB,gBAAgB,KAAK,eAAe;;AAGrE,OAAK,OAAO;;CAGd,QAAsB;AACpB,SAAO,KAAK,OAAO,SAAS,GAAG;GAC7B,MAAM,QAAQ,KAAK,OAAO,OAAO,GAAG,KAAK,UAAU;AACnD,QAAK,UAAU,KAAK,MAAM,CAAC,YAAY,GAErC;;;CAIN,2BAAkD;AAChD,MAAI,SAAS,oBAAoB,SAC/B,MAAK,OAAO;;CAIhB,uBAA8C;AAC5C,OAAK,OAAO"}
@@ -0,0 +1,13 @@
1
+ //#region src/util/log.d.ts
2
+ type LogLevel = "debug" | "info" | "warn" | "error" | "none";
3
+ declare function setLogLevel(level: LogLevel): void;
4
+ declare function getLogLevel(): LogLevel;
5
+ interface Logger {
6
+ debug(...args: unknown[]): void;
7
+ error(...args: unknown[]): void;
8
+ info(...args: unknown[]): void;
9
+ warn(...args: unknown[]): void;
10
+ }
11
+ declare function createLogger(scope: string): Logger;
12
+ //#endregion
13
+ export { LogLevel, Logger, createLogger, getLogLevel, setLogLevel };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.mts","names":[],"sources":["../../src/util/log.ts"],"mappings":";KAAY,QAAA;AAAA,iBAsBI,WAAA,CAAY,KAAA,EAAO,QAAA;AAAA,iBAInB,WAAA,CAAA,GAAe,QAAA;AAAA,UAKd,MAAA;EACf,KAAA,IAAS,IAAA;EACT,KAAA,IAAS,IAAA;EACT,IAAA,IAAQ,IAAA;EACR,IAAA,IAAQ,IAAA;AAAA;AAAA,iBAeM,YAAA,CAAa,KAAA,WAAgB,MAAA"}
@@ -0,0 +1,37 @@
1
+ //#region src/util/log.ts
2
+ const PRIORITY = {
3
+ debug: 0,
4
+ info: 1,
5
+ warn: 2,
6
+ error: 3,
7
+ none: 4
8
+ };
9
+ const CONSOLE_FN = {
10
+ debug: "debug",
11
+ info: "info",
12
+ warn: "warn",
13
+ error: "error"
14
+ };
15
+ let threshold = PRIORITY.warn;
16
+ function setLogLevel(level) {
17
+ threshold = PRIORITY[level];
18
+ }
19
+ function getLogLevel() {
20
+ return Object.entries(PRIORITY).find(([, v]) => v === threshold)?.[0] ?? "warn";
21
+ }
22
+ function emit(level, prefix, args) {
23
+ if (PRIORITY[level] < threshold) return;
24
+ const fn = CONSOLE_FN[level];
25
+ globalThis.console[fn](prefix, ...args);
26
+ }
27
+ function createLogger(scope) {
28
+ const prefix = `[Interfere:${scope}]`;
29
+ return {
30
+ debug: (...args) => emit("debug", prefix, args),
31
+ info: (...args) => emit("info", prefix, args),
32
+ warn: (...args) => emit("warn", prefix, args),
33
+ error: (...args) => emit("error", prefix, args)
34
+ };
35
+ }
36
+ //#endregion
37
+ export { createLogger, getLogLevel, setLogLevel };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.mjs","names":[],"sources":["../../src/util/log.ts"],"sourcesContent":["export type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\" | \"none\";\n\nconst PRIORITY: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n none: 4,\n};\n\nconst CONSOLE_FN: Record<\n Exclude<LogLevel, \"none\">,\n \"debug\" | \"info\" | \"warn\" | \"error\"\n> = {\n debug: \"debug\",\n info: \"info\",\n warn: \"warn\",\n error: \"error\",\n};\n\nlet threshold = PRIORITY.warn;\n\nexport function setLogLevel(level: LogLevel): void {\n threshold = PRIORITY[level];\n}\n\nexport function getLogLevel(): LogLevel {\n const entry = Object.entries(PRIORITY).find(([, v]) => v === threshold);\n return (entry?.[0] ?? \"warn\") as LogLevel;\n}\n\nexport interface Logger {\n debug(...args: unknown[]): void;\n error(...args: unknown[]): void;\n info(...args: unknown[]): void;\n warn(...args: unknown[]): void;\n}\n\nfunction emit(\n level: Exclude<LogLevel, \"none\">,\n prefix: string,\n args: unknown[]\n): void {\n if (PRIORITY[level] < threshold) {\n return;\n }\n const fn = CONSOLE_FN[level];\n globalThis.console[fn](prefix, ...args);\n}\n\nexport function createLogger(scope: string): Logger {\n const prefix = `[Interfere:${scope}]`;\n return {\n debug: (...args) => emit(\"debug\", prefix, args),\n info: (...args) => emit(\"info\", prefix, args),\n warn: (...args) => emit(\"warn\", prefix, args),\n error: (...args) => emit(\"error\", prefix, args),\n };\n}\n"],"mappings":";AAEA,MAAM,WAAqC;CACzC,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACP,MAAM;CACP;AAED,MAAM,aAGF;CACF,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACR;AAED,IAAI,YAAY,SAAS;AAEzB,SAAgB,YAAY,OAAuB;AACjD,aAAY,SAAS;;AAGvB,SAAgB,cAAwB;AAEtC,QADc,OAAO,QAAQ,SAAS,CAAC,MAAM,GAAG,OAAO,MAAM,UAAU,GACvD,MAAM;;AAUxB,SAAS,KACP,OACA,QACA,MACM;AACN,KAAI,SAAS,SAAS,UACpB;CAEF,MAAM,KAAK,WAAW;AACtB,YAAW,QAAQ,IAAI,QAAQ,GAAG,KAAK;;AAGzC,SAAgB,aAAa,OAAuB;CAClD,MAAM,SAAS,cAAc,MAAM;AACnC,QAAO;EACL,QAAQ,GAAG,SAAS,KAAK,SAAS,QAAQ,KAAK;EAC/C,OAAO,GAAG,SAAS,KAAK,QAAQ,QAAQ,KAAK;EAC7C,OAAO,GAAG,SAAS,KAAK,QAAQ,QAAQ,KAAK;EAC7C,QAAQ,GAAG,SAAS,KAAK,SAAS,QAAQ,KAAK;EAChD"}