@interfere/react 9.0.1 → 9.0.2

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/package.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
2
  var name = "@interfere/react";
3
- var version = "9.0.1";
3
+ var version = "9.0.2";
4
4
  //#endregion
5
5
  export { name, version };
@@ -11,6 +11,10 @@ interface DeliveryMeta {
11
11
  }
12
12
  declare function buildHeaders(base: Headers, meta?: DeliveryMeta): Record<string, string>;
13
13
  declare function hasServiceWorker(): boolean;
14
+ declare class HttpError extends Error {
15
+ readonly status: number;
16
+ constructor(status: number, message?: string);
17
+ }
14
18
  declare class HttpTransport {
15
19
  private readonly target;
16
20
  private pendingKeepalive;
@@ -18,4 +22,4 @@ declare class HttpTransport {
18
22
  send(envelopes: Envelope[], meta?: DeliveryMeta): Promise<void>;
19
23
  }
20
24
  //#endregion
21
- export { DeliveryMeta, HttpTransport, IngestTarget, buildHeaders, hasServiceWorker };
25
+ export { DeliveryMeta, HttpError, HttpTransport, IngestTarget, buildHeaders, hasServiceWorker };
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.mts","names":[],"sources":["../../src/transport/http.ts"],"mappings":";;;UAUiB,YAAA;EACf,OAAA,EAAS,OAAA;EACT,GAAA;AAAA;AAAA,UAGe,YAAA;EACf,UAAA;EACA,UAAA;AAAA;AAAA,iBAYc,YAAA,CACd,IAAA,EAAM,OAAA,EACN,IAAA,GAAO,YAAA,GACN,MAAA;AAAA,iBAgCa,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"}
1
+ {"version":3,"file":"http.d.mts","names":[],"sources":["../../src/transport/http.ts"],"mappings":";;;UAUiB,YAAA;EACf,OAAA,EAAS,OAAA;EACT,GAAA;AAAA;AAAA,UAGe,YAAA;EACf,UAAA;EACA,UAAA;AAAA;AAAA,iBAYc,YAAA,CACd,IAAA,EAAM,OAAA,EACN,IAAA,GAAO,YAAA,GACN,MAAA;AAAA,iBAgCa,gBAAA,CAAA;AAAA,cAQH,SAAA,SAAkB,KAAA;EAAA,SACpB,MAAA;cAEG,MAAA,UAAgB,OAAA;AAAA;AAAA,cAgBjB,aAAA;EAAA,iBACM,MAAA;EAAA,QACT,gBAAA;cAEI,MAAA,EAAQ,YAAA;EAId,IAAA,CAAK,SAAA,EAAW,QAAA,IAAY,IAAA,GAAO,YAAA,GAAe,OAAA;AAAA"}
@@ -27,8 +27,16 @@ function buildHeaders(base, meta) {
27
27
  function hasServiceWorker() {
28
28
  return typeof navigator !== "undefined" && "serviceWorker" in navigator && navigator.serviceWorker.controller !== null;
29
29
  }
30
+ var HttpError = class extends Error {
31
+ status;
32
+ constructor(status, message) {
33
+ super(message ?? `ingest responded ${status}`);
34
+ this.status = status;
35
+ this.name = "HttpError";
36
+ }
37
+ };
30
38
  function assertOk(response) {
31
- if (!response.ok) throw new Error(`ingest responded ${response.status}`);
39
+ if (!response.ok) throw new HttpError(response.status);
32
40
  }
33
41
  const KEEPALIVE_BUDGET_BYTES = 61440;
34
42
  const MAX_CONCURRENT_KEEPALIVE = 15;
@@ -69,4 +77,4 @@ var HttpTransport = class {
69
77
  }
70
78
  };
71
79
  //#endregion
72
- export { HttpTransport, buildHeaders, hasServiceWorker };
80
+ export { HttpError, HttpTransport, buildHeaders, hasServiceWorker };
@@ -1 +1 @@
1
- {"version":3,"file":"http.mjs","names":[],"sources":["../../src/transport/http.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\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\nfunction getSdkStack(): string[] | undefined {\n if (typeof window !== \"undefined\") {\n return (window as unknown as Record<string, unknown>)[\n \"__INTERFERE_SDK_STACK__\"\n ] as string[] | undefined;\n }\n return;\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 const stack = getSdkStack();\n h[\"x-interfere-sdk-version\"] = stack?.[0] ?? \"unknown\";\n if (stack && stack.length > 1) {\n h[\"x-interfere-sdk-stack\"] = stack.join(\", \");\n }\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":";;;;AAMA,MAAM,MAAM,aAAa,OAAO;AAEhC,MAAM,kBAAkB;AAYxB,SAAS,cAAoC;AAC3C,KAAI,OAAO,WAAW,YACpB,QAAQ,OACN;;AAMN,SAAgB,aACd,MACA,MACwB;CACxB,MAAM,IAA4B,OAAO,YAAY,KAAK,SAAS,CAAC;CAEpE,MAAM,QAAQ,aAAa;AAC3B,GAAE,6BAA6B,QAAQ,MAAM;AAC7C,KAAI,SAAS,MAAM,SAAS,EAC1B,GAAE,2BAA2B,MAAM,KAAK,KAAK;CAG/C,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,YAAS,MANS,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,YAAS,MAPS,MAAM,KAAK,OAAO,KAAK;IACvC,QAAQ;IACR;IACA;IACA,WAAW;IACX,QAAQ,YAAY,QAAQ,gBAAgB;IAC7C,CAAC,CACW;YACL;AACR,OAAI,aACF,MAAK"}
1
+ {"version":3,"file":"http.mjs","names":[],"sources":["../../src/transport/http.ts"],"sourcesContent":["import type { Envelope } from \"@interfere/types/sdk/envelope\";\n\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\nfunction getSdkStack(): string[] | undefined {\n if (typeof window !== \"undefined\") {\n return (window as unknown as Record<string, unknown>)[\n \"__INTERFERE_SDK_STACK__\"\n ] as string[] | undefined;\n }\n return;\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 const stack = getSdkStack();\n h[\"x-interfere-sdk-version\"] = stack?.[0] ?? \"unknown\";\n if (stack && stack.length > 1) {\n h[\"x-interfere-sdk-stack\"] = stack.join(\", \");\n }\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\nexport class HttpError extends Error {\n readonly status: number;\n\n constructor(status: number, message?: string) {\n super(message ?? `ingest responded ${status}`);\n this.status = status;\n this.name = \"HttpError\";\n }\n}\n\nfunction assertOk(response: Response): void {\n if (!response.ok) {\n throw new HttpError(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":";;;;AAMA,MAAM,MAAM,aAAa,OAAO;AAEhC,MAAM,kBAAkB;AAYxB,SAAS,cAAoC;AAC3C,KAAI,OAAO,WAAW,YACpB,QAAQ,OACN;;AAMN,SAAgB,aACd,MACA,MACwB;CACxB,MAAM,IAA4B,OAAO,YAAY,KAAK,SAAS,CAAC;CAEpE,MAAM,QAAQ,aAAa;AAC3B,GAAE,6BAA6B,QAAQ,MAAM;AAC7C,KAAI,SAAS,MAAM,SAAS,EAC1B,GAAE,2BAA2B,MAAM,KAAK,KAAK;CAG/C,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,IAAa,YAAb,cAA+B,MAAM;CACnC;CAEA,YAAY,QAAgB,SAAkB;AAC5C,QAAM,WAAW,oBAAoB,SAAS;AAC9C,OAAK,SAAS;AACd,OAAK,OAAO;;;AAIhB,SAAS,SAAS,UAA0B;AAC1C,KAAI,CAAC,SAAS,GACZ,OAAM,IAAI,UAAU,SAAS,OAAO;;AAIxC,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,YAAS,MANS,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,YAAS,MAPS,MAAM,KAAK,OAAO,KAAK;IACvC,QAAQ;IACR;IACA;IACA,WAAW;IACX,QAAQ,YAAY,QAAQ,gBAAgB;IAC7C,CAAC,CACW;YACL;AACR,OAAI,aACF,MAAK"}
@@ -1 +1 @@
1
- {"version":3,"file":"queue.d.mts","names":[],"sources":["../../src/transport/queue.ts"],"mappings":";;;;UAciB,YAAA;EACf,OAAA;EACA,SAAA;EAF2B;EAI3B,qBAAA;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;EAAA,iBACA,qBAAA;cAEL,IAAA,EAAM,YAAA;EAQlB,KAAA,CAAA;EAeA,OAAA,CAAQ,QAAA,EAAU,QAAA;EAclB,KAAA,CAAA;EA2CA,OAAA,CAAA;EAAA,QAiBQ,KAAA;EAAA,iBASS,kBAAA;EAAA,iBAMA,cAAA;AAAA"}
1
+ {"version":3,"file":"queue.d.mts","names":[],"sources":["../../src/transport/queue.ts"],"mappings":";;;;UAciB,YAAA;EACf,OAAA;EACA,SAAA;EAF2B;EAI3B,qBAAA;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;EAAA,iBACA,qBAAA;cAEL,IAAA,EAAM,YAAA;EAQlB,KAAA,CAAA;EAeA,OAAA,CAAQ,QAAA,EAAU,QAAA;EAclB,KAAA,CAAA;EAqDA,OAAA,CAAA;EAAA,QAiBQ,KAAA;EAAA,iBASS,kBAAA;EAAA,iBAMA,cAAA;AAAA"}
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from "../util/log.mjs";
2
- import { hasServiceWorker } from "./http.mjs";
2
+ import { HttpError, hasServiceWorker } from "./http.mjs";
3
3
  //#region src/transport/queue.ts
4
4
  const log = createLogger("queue");
5
5
  const DEFAULT_BATCH_SIZE = 10;
@@ -56,7 +56,12 @@ var BatchQueue = class {
56
56
  this.transport.send(batch, meta).then(() => {
57
57
  if (this.failures > 0) log.info("send recovered after %d failures", this.failures);
58
58
  this.failures = 0;
59
- }).catch(() => {
59
+ }).catch((err) => {
60
+ if (err instanceof HttpError && err.status >= 400 && err.status < 500) {
61
+ log.debug("batch rejected (%d), dropping %d envelopes", err.status, batch.length);
62
+ this.failures = 0;
63
+ return;
64
+ }
60
65
  if (!this.isServiceWorkerActive()) this.buffer.unshift(...batch);
61
66
  this.failures++;
62
67
  if (this.failures >= BREAKER_THRESHOLD) {
@@ -1 +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 /** @internal Test-only. Override the service worker check. */\n isServiceWorkerActive?: () => boolean;\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 private readonly isServiceWorkerActive: () => boolean;\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 this.isServiceWorkerActive = opts.isServiceWorkerActive ?? hasServiceWorker;\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 (!this.isServiceWorkerActive()) {\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;AAW5B,IAAa,aAAb,MAAwB;CACtB,SAAsC,EAAE;CACxC,QAAuD;CACvD,WAAmB;CACnB,WAAmB;CACnB,cAAsB;CACtB;CACA;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;AACtB,OAAK,wBAAwB,KAAK,yBAAyB;;CAG7D,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,KAAK,uBAAuB,CAC/B,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"}
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 { HttpError, 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 /** @internal Test-only. Override the service worker check. */\n isServiceWorkerActive?: () => boolean;\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 private readonly isServiceWorkerActive: () => boolean;\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 this.isServiceWorkerActive = opts.isServiceWorkerActive ?? hasServiceWorker;\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((err) => {\n if (err instanceof HttpError && err.status >= 400 && err.status < 500) {\n log.debug(\n \"batch rejected (%d), dropping %d envelopes\",\n err.status,\n batch.length\n );\n this.failures = 0;\n return;\n }\n\n if (!this.isServiceWorkerActive()) {\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;AAW5B,IAAa,aAAb,MAAwB;CACtB,SAAsC,EAAE;CACxC,QAAuD;CACvD,WAAmB;CACnB,WAAmB;CACnB,cAAsB;CACtB;CACA;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;AACtB,OAAK,wBAAwB,KAAK,yBAAyB;;CAG7D,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,OAAO,QAAQ;AACd,OAAI,eAAe,aAAa,IAAI,UAAU,OAAO,IAAI,SAAS,KAAK;AACrE,QAAI,MACF,8CACA,IAAI,QACJ,MAAM,OACP;AACD,SAAK,WAAW;AAChB;;AAGF,OAAI,CAAC,KAAK,uBAAuB,CAC/B,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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/react",
3
- "version": "9.0.1",
3
+ "version": "9.0.2",
4
4
  "license": "MIT",
5
5
  "description": "Client-side React SDK for Interfere. Error tracking, session replay, and analytics.",
6
6
  "keywords": [