@interfere/react 0.1.0-alpha.3 → 0.1.0-alpha.5

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.
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../src/core/plugins/impl/errors.ts"],"sourcesContent":[],"mappings":";;;UA0EiB,SAAA;sCACqB;;AADtC,cAEC,QAFyB,EAEzB,MAD2C,CAAA,QAAA,EAAA,OAAA,CAAA"}
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../src/core/plugins/impl/errors.ts"],"sourcesContent":[],"mappings":";;;UA2EiB,SAAA;sCACqB;;AADtC,cAEC,QAFyB,EAEzB,MAD2C,CAAA,QAAA,EAAA,OAAA,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { isInterfereInternalError } from "../../runtime/native-fetch.mjs";
1
2
  import { createEffectPlugin, defineEvent } from "../plugin-event-system.mjs";
2
3
  import { Effect } from "effect";
3
4
 
@@ -57,6 +58,7 @@ var errors_default = createEffectPlugin("errors", {
57
58
  ctx.span(span, ctx.capture("error", payload));
58
59
  };
59
60
  const api = { captureError: (err, meta) => {
61
+ if (isInterfereInternalError(err)) return;
60
62
  const payload = buildErrorPayload(err instanceof Error ? {
61
63
  message: err.message,
62
64
  stack: err.stack,
@@ -71,6 +73,7 @@ var errors_default = createEffectPlugin("errors", {
71
73
  } : payload, "plugin.error.capture");
72
74
  } };
73
75
  const errorHandler = (event) => {
76
+ if (isInterfereInternalError(event.error)) return;
74
77
  recordOnce(buildErrorPayload({
75
78
  message: event.message,
76
79
  stack: event.error?.stack,
@@ -82,6 +85,7 @@ var errors_default = createEffectPlugin("errors", {
82
85
  };
83
86
  const rejectionHandler = (event) => {
84
87
  const reason = event.reason;
88
+ if (isInterfereInternalError(reason)) return;
85
89
  recordOnce(buildErrorPayload(reason instanceof Error ? {
86
90
  message: `Unhandled Promise Rejection: ${reason.message}`,
87
91
  stack: reason.stack,
@@ -93,6 +97,7 @@ var errors_default = createEffectPlugin("errors", {
93
97
  };
94
98
  const originalFetch = window.fetch.bind(window);
95
99
  const patchedFetch = (...args) => originalFetch(...args).catch((err) => {
100
+ if (isInterfereInternalError(err)) throw err;
96
101
  recordOnce(buildErrorPayload(err instanceof Error ? {
97
102
  message: `Fetch failed: ${err.message}`,
98
103
  stack: err.stack,
@@ -114,20 +119,22 @@ var errors_default = createEffectPlugin("errors", {
114
119
  inConsoleCapture = true;
115
120
  try {
116
121
  const firstError = args.find((a) => a instanceof Error);
117
- const toStringSafe = (a) => {
118
- if (typeof a === "string") return a;
119
- if (a instanceof Error) return a.message;
120
- try {
121
- return JSON.stringify(a);
122
- } catch {
123
- return String(a);
124
- }
125
- };
126
- recordOnce(buildErrorPayload({
127
- message: args.map(toStringSafe).join(" "),
128
- name: firstError?.name ?? "ConsoleError",
129
- stack: firstError?.stack
130
- }), "plugin.error.console_error");
122
+ if (!(firstError && isInterfereInternalError(firstError))) {
123
+ const toStringSafe = (a) => {
124
+ if (typeof a === "string") return a;
125
+ if (a instanceof Error) return a.message;
126
+ try {
127
+ return JSON.stringify(a);
128
+ } catch {
129
+ return String(a);
130
+ }
131
+ };
132
+ recordOnce(buildErrorPayload({
133
+ message: args.map(toStringSafe).join(" "),
134
+ name: firstError?.name ?? "ConsoleError",
135
+ stack: firstError?.stack
136
+ }), "plugin.error.console_error");
137
+ }
131
138
  } finally {
132
139
  inConsoleCapture = false;
133
140
  }
@@ -1 +1 @@
1
- {"version":3,"file":"errors.mjs","names":["frames: IngestedFrame[]","api: ErrorsAPI","originalFetch: typeof window.fetch","patchedFetch: typeof window.fetch","originalConsoleError: typeof console.error"],"sources":["../../../../src/core/plugins/impl/errors.ts"],"sourcesContent":["import type { IngestedFrame } from \"@interfere/types/data/frame\";\nimport type { ErrorEnvelopePayload } from \"@interfere/types/sdk/plugins/payload/errors\";\n\nimport { Effect } from \"effect\";\n\nimport { createEffectPlugin, defineEvent } from \"../plugin-event-system.js\";\n\n// Regex defined at top level for performance\nconst STACK_TRACE_REGEX = /at\\s+(?:(.+?)\\s+\\()?(.+?):(\\d+):(\\d+)\\)?/;\n\nfunction parseStackTrace(stack?: string): IngestedFrame[] {\n if (!stack) {\n return [];\n }\n\n // Basic stack trace parsing - can be enhanced with error-stack-parser library\n const lines = stack.split(\"\\n\");\n const frames: IngestedFrame[] = [];\n\n for (const line of lines) {\n // Match patterns like: \"at functionName (file:line:column)\" or \"at file:line:column\"\n const match = line.match(STACK_TRACE_REGEX);\n\n if (match) {\n frames.push({\n file: { reported: match[2] || null },\n line: { reported: match[3] ? Number.parseInt(match[3], 10) : null },\n column: { reported: match[4] ? Number.parseInt(match[4], 10) : null },\n fn: { reported: match[1] || null },\n });\n }\n }\n\n return frames.length > 0\n ? frames\n : [\n // Fallback frame if we can't parse the stack\n {\n file: { reported: null },\n line: { reported: null },\n column: { reported: null },\n fn: { reported: null },\n },\n ];\n}\n\nfunction buildErrorPayload(error: {\n message?: string;\n name?: string;\n stack?: string;\n filename?: string;\n lineno?: number;\n colno?: number;\n}): ErrorEnvelopePayload {\n const frames = error.stack\n ? parseStackTrace(error.stack)\n : [\n {\n file: { reported: error.filename || null },\n line: { reported: error.lineno || null },\n column: { reported: error.colno || null },\n fn: { reported: null },\n },\n ];\n\n return {\n frames,\n message: error.message || null,\n name: error.name || null,\n stack: error.stack || null,\n errorSource: \"client\",\n };\n}\n\nexport interface ErrorsAPI {\n captureError: (err: unknown, meta?: Record<string, unknown>) => void;\n}\n\nexport default createEffectPlugin(\"errors\", {\n name: \"errors\",\n events: [defineEvent(\"error\").value()],\n setup: (ctx) =>\n Effect.gen(function* () {\n if (typeof window === \"undefined\") {\n ctx.log.debug(\"errors: SSR - skipping setup\");\n return;\n }\n\n // Simple in-memory de-dupe to avoid double reports from multiple hooks\n const recent = new Set<string>();\n\n const DEDUPE_TTL_MS = 2000;\n const STACK_PREFIX_SLICE = 120;\n\n const makeKey = (input: {\n message?: string | null;\n stack?: string | null;\n name?: string | null;\n }) =>\n `${input.name ?? \"\"}|${input.message ?? \"\"}|${input.stack?.slice(0, STACK_PREFIX_SLICE) ?? \"\"}`;\n\n const recordOnce = (\n payload: ReturnType<typeof buildErrorPayload>,\n span: string\n ) => {\n const key = makeKey(payload);\n\n if (recent.has(key)) {\n return;\n }\n\n recent.add(key);\n\n setTimeout(() => recent.delete(key), DEDUPE_TTL_MS);\n\n ctx.span(span, ctx.capture(\"error\", payload));\n };\n\n const api: ErrorsAPI = {\n captureError: (err: unknown, meta?: Record<string, unknown>) => {\n const errorInfo =\n err instanceof Error\n ? { message: err.message, stack: err.stack, name: err.name }\n : { message: String(err), name: \"UnknownError\" };\n\n const payload = buildErrorPayload(errorInfo);\n recordOnce(\n (meta\n ? ({ ...payload, ...meta } as unknown)\n : payload) as ErrorEnvelopePayload,\n \"plugin.error.capture\"\n );\n },\n };\n\n const errorHandler = (event: ErrorEvent) => {\n const payload = buildErrorPayload({\n message: event.message,\n stack: event.error?.stack,\n filename: event.filename,\n lineno: event.lineno,\n colno: event.colno,\n name: event.error?.name,\n });\n\n recordOnce(payload, \"plugin.error.window\");\n };\n\n const rejectionHandler = (event: PromiseRejectionEvent) => {\n const reason = event.reason;\n\n const payload = buildErrorPayload(\n reason instanceof Error\n ? {\n message: `Unhandled Promise Rejection: ${reason.message}`,\n stack: reason.stack,\n name: reason.name,\n }\n : {\n message: `Unhandled Promise Rejection: ${String(reason)}`,\n name: \"UnhandledRejection\",\n }\n );\n\n recordOnce(payload, \"plugin.error.rejection\");\n };\n\n // Network errors: patch fetch to capture rejected requests\n const originalFetch: typeof window.fetch = window.fetch.bind(window);\n\n const patchedFetch: typeof window.fetch = (...args) =>\n originalFetch(...args).catch((err: unknown) => {\n const errorInfo =\n err instanceof Error\n ? {\n message: `Fetch failed: ${err.message}`,\n stack: err.stack,\n name: err.name,\n }\n : { message: `Fetch failed: ${String(err)}`, name: \"FetchError\" };\n const payload = buildErrorPayload(errorInfo);\n recordOnce(payload, \"plugin.error.fetch\");\n throw err;\n });\n\n window.fetch = patchedFetch;\n\n let inConsoleCapture = false;\n\n const originalConsoleError: typeof console.error =\n console.error.bind(console);\n\n console.error = (...args: unknown[]) => {\n try {\n originalConsoleError(...(args as Parameters<typeof console.error>));\n } finally {\n if (!inConsoleCapture) {\n inConsoleCapture = true;\n try {\n const firstError = args.find(\n (a): a is Error => a instanceof Error\n );\n const toStringSafe = (a: unknown): string => {\n if (typeof a === \"string\") {\n return a;\n }\n if (a instanceof Error) {\n return a.message;\n }\n try {\n return JSON.stringify(a);\n } catch {\n return String(a);\n }\n };\n const message = args.map(toStringSafe).join(\" \");\n\n const payload = buildErrorPayload({\n message,\n name: firstError?.name ?? \"ConsoleError\",\n stack: firstError?.stack,\n });\n\n recordOnce(payload, \"plugin.error.console_error\");\n } finally {\n inConsoleCapture = false;\n }\n }\n }\n };\n\n window.addEventListener(\"error\", errorHandler, {\n capture: true,\n });\n\n window.addEventListener(\"unhandledrejection\", rejectionHandler);\n\n return {\n key: \"errors\",\n api,\n cleanup: () => {\n window.removeEventListener(\"error\", errorHandler, {\n capture: true,\n } as EventListenerOptions);\n\n window.removeEventListener(\"unhandledrejection\", rejectionHandler);\n\n window.fetch = originalFetch;\n console.error = originalConsoleError as typeof console.error;\n },\n };\n }),\n});\n"],"mappings":";;;;AAQA,MAAM,oBAAoB;AAE1B,SAAS,gBAAgB,OAAiC;AACxD,KAAI,CAAC,MACH,QAAO,EAAE;CAIX,MAAM,QAAQ,MAAM,MAAM,KAAK;CAC/B,MAAMA,SAA0B,EAAE;AAElC,MAAK,MAAM,QAAQ,OAAO;EAExB,MAAM,QAAQ,KAAK,MAAM,kBAAkB;AAE3C,MAAI,MACF,QAAO,KAAK;GACV,MAAM,EAAE,UAAU,MAAM,MAAM,MAAM;GACpC,MAAM,EAAE,UAAU,MAAM,KAAK,OAAO,SAAS,MAAM,IAAI,GAAG,GAAG,MAAM;GACnE,QAAQ,EAAE,UAAU,MAAM,KAAK,OAAO,SAAS,MAAM,IAAI,GAAG,GAAG,MAAM;GACrE,IAAI,EAAE,UAAU,MAAM,MAAM,MAAM;GACnC,CAAC;;AAIN,QAAO,OAAO,SAAS,IACnB,SACA,CAEE;EACE,MAAM,EAAE,UAAU,MAAM;EACxB,MAAM,EAAE,UAAU,MAAM;EACxB,QAAQ,EAAE,UAAU,MAAM;EAC1B,IAAI,EAAE,UAAU,MAAM;EACvB,CACF;;AAGP,SAAS,kBAAkB,OAOF;AAYvB,QAAO;EACL,QAZa,MAAM,QACjB,gBAAgB,MAAM,MAAM,GAC5B,CACE;GACE,MAAM,EAAE,UAAU,MAAM,YAAY,MAAM;GAC1C,MAAM,EAAE,UAAU,MAAM,UAAU,MAAM;GACxC,QAAQ,EAAE,UAAU,MAAM,SAAS,MAAM;GACzC,IAAI,EAAE,UAAU,MAAM;GACvB,CACF;EAIH,SAAS,MAAM,WAAW;EAC1B,MAAM,MAAM,QAAQ;EACpB,OAAO,MAAM,SAAS;EACtB,aAAa;EACd;;AAOH,qBAAe,mBAAmB,UAAU;CAC1C,MAAM;CACN,QAAQ,CAAC,YAAY,QAAQ,CAAC,OAAO,CAAC;CACtC,QAAQ,QACN,OAAO,IAAI,aAAa;AACtB,MAAI,OAAO,WAAW,aAAa;AACjC,OAAI,IAAI,MAAM,+BAA+B;AAC7C;;EAIF,MAAM,yBAAS,IAAI,KAAa;EAEhC,MAAM,gBAAgB;EACtB,MAAM,qBAAqB;EAE3B,MAAM,WAAW,UAKf,GAAG,MAAM,QAAQ,GAAG,GAAG,MAAM,WAAW,GAAG,GAAG,MAAM,OAAO,MAAM,GAAG,mBAAmB,IAAI;EAE7F,MAAM,cACJ,SACA,SACG;GACH,MAAM,MAAM,QAAQ,QAAQ;AAE5B,OAAI,OAAO,IAAI,IAAI,CACjB;AAGF,UAAO,IAAI,IAAI;AAEf,oBAAiB,OAAO,OAAO,IAAI,EAAE,cAAc;AAEnD,OAAI,KAAK,MAAM,IAAI,QAAQ,SAAS,QAAQ,CAAC;;EAG/C,MAAMC,MAAiB,EACrB,eAAe,KAAc,SAAmC;GAM9D,MAAM,UAAU,kBAJd,eAAe,QACX;IAAE,SAAS,IAAI;IAAS,OAAO,IAAI;IAAO,MAAM,IAAI;IAAM,GAC1D;IAAE,SAAS,OAAO,IAAI;IAAE,MAAM;IAAgB,CAER;AAC5C,cACG,OACI;IAAE,GAAG;IAAS,GAAG;IAAM,GACxB,SACJ,uBACD;KAEJ;EAED,MAAM,gBAAgB,UAAsB;AAU1C,cATgB,kBAAkB;IAChC,SAAS,MAAM;IACf,OAAO,MAAM,OAAO;IACpB,UAAU,MAAM;IAChB,QAAQ,MAAM;IACd,OAAO,MAAM;IACb,MAAM,MAAM,OAAO;IACpB,CAAC,EAEkB,sBAAsB;;EAG5C,MAAM,oBAAoB,UAAiC;GACzD,MAAM,SAAS,MAAM;AAerB,cAbgB,kBACd,kBAAkB,QACd;IACE,SAAS,gCAAgC,OAAO;IAChD,OAAO,OAAO;IACd,MAAM,OAAO;IACd,GACD;IACE,SAAS,gCAAgC,OAAO,OAAO;IACvD,MAAM;IACP,CACN,EAEmB,yBAAyB;;EAI/C,MAAMC,gBAAqC,OAAO,MAAM,KAAK,OAAO;EAEpE,MAAMC,gBAAqC,GAAG,SAC5C,cAAc,GAAG,KAAK,CAAC,OAAO,QAAiB;AAU7C,cADgB,kBAPd,eAAe,QACX;IACE,SAAS,iBAAiB,IAAI;IAC9B,OAAO,IAAI;IACX,MAAM,IAAI;IACX,GACD;IAAE,SAAS,iBAAiB,OAAO,IAAI;IAAI,MAAM;IAAc,CACzB,EACxB,qBAAqB;AACzC,SAAM;IACN;AAEJ,SAAO,QAAQ;EAEf,IAAI,mBAAmB;EAEvB,MAAMC,uBACJ,QAAQ,MAAM,KAAK,QAAQ;AAE7B,UAAQ,SAAS,GAAG,SAAoB;AACtC,OAAI;AACF,yBAAqB,GAAI,KAA0C;aAC3D;AACR,QAAI,CAAC,kBAAkB;AACrB,wBAAmB;AACnB,SAAI;MACF,MAAM,aAAa,KAAK,MACrB,MAAkB,aAAa,MACjC;MACD,MAAM,gBAAgB,MAAuB;AAC3C,WAAI,OAAO,MAAM,SACf,QAAO;AAET,WAAI,aAAa,MACf,QAAO,EAAE;AAEX,WAAI;AACF,eAAO,KAAK,UAAU,EAAE;eAClB;AACN,eAAO,OAAO,EAAE;;;AAWpB,iBANgB,kBAAkB;OAChC,SAHc,KAAK,IAAI,aAAa,CAAC,KAAK,IAAI;OAI9C,MAAM,YAAY,QAAQ;OAC1B,OAAO,YAAY;OACpB,CAAC,EAEkB,6BAA6B;eACzC;AACR,yBAAmB;;;;;AAM3B,SAAO,iBAAiB,SAAS,cAAc,EAC7C,SAAS,MACV,CAAC;AAEF,SAAO,iBAAiB,sBAAsB,iBAAiB;AAE/D,SAAO;GACL,KAAK;GACL;GACA,eAAe;AACb,WAAO,oBAAoB,SAAS,cAAc,EAChD,SAAS,MACV,CAAyB;AAE1B,WAAO,oBAAoB,sBAAsB,iBAAiB;AAElE,WAAO,QAAQ;AACf,YAAQ,QAAQ;;GAEnB;GACD;CACL,CAAC"}
1
+ {"version":3,"file":"errors.mjs","names":["frames: IngestedFrame[]","api: ErrorsAPI","originalFetch: typeof window.fetch","patchedFetch: typeof window.fetch","originalConsoleError: typeof console.error"],"sources":["../../../../src/core/plugins/impl/errors.ts"],"sourcesContent":["import type { IngestedFrame } from \"@interfere/types/data/frame\";\nimport type { ErrorEnvelopePayload } from \"@interfere/types/sdk/plugins/payload/errors\";\n\nimport { Effect } from \"effect\";\n\nimport { isInterfereInternalError } from \"../../runtime/native-fetch.js\";\nimport { createEffectPlugin, defineEvent } from \"../plugin-event-system.js\";\n\n// Regex defined at top level for performance\nconst STACK_TRACE_REGEX = /at\\s+(?:(.+?)\\s+\\()?(.+?):(\\d+):(\\d+)\\)?/;\n\nfunction parseStackTrace(stack?: string): IngestedFrame[] {\n if (!stack) {\n return [];\n }\n\n // Basic stack trace parsing - can be enhanced with error-stack-parser library\n const lines = stack.split(\"\\n\");\n const frames: IngestedFrame[] = [];\n\n for (const line of lines) {\n // Match patterns like: \"at functionName (file:line:column)\" or \"at file:line:column\"\n const match = line.match(STACK_TRACE_REGEX);\n\n if (match) {\n frames.push({\n file: { reported: match[2] || null },\n line: { reported: match[3] ? Number.parseInt(match[3], 10) : null },\n column: { reported: match[4] ? Number.parseInt(match[4], 10) : null },\n fn: { reported: match[1] || null },\n });\n }\n }\n\n return frames.length > 0\n ? frames\n : [\n // Fallback frame if we can't parse the stack\n {\n file: { reported: null },\n line: { reported: null },\n column: { reported: null },\n fn: { reported: null },\n },\n ];\n}\n\nfunction buildErrorPayload(error: {\n message?: string;\n name?: string;\n stack?: string;\n filename?: string;\n lineno?: number;\n colno?: number;\n}): ErrorEnvelopePayload {\n const frames = error.stack\n ? parseStackTrace(error.stack)\n : [\n {\n file: { reported: error.filename || null },\n line: { reported: error.lineno || null },\n column: { reported: error.colno || null },\n fn: { reported: null },\n },\n ];\n\n return {\n frames,\n message: error.message || null,\n name: error.name || null,\n stack: error.stack || null,\n errorSource: \"client\",\n };\n}\n\nexport interface ErrorsAPI {\n captureError: (err: unknown, meta?: Record<string, unknown>) => void;\n}\n\nexport default createEffectPlugin(\"errors\", {\n name: \"errors\",\n events: [defineEvent(\"error\").value()],\n setup: (ctx) =>\n Effect.gen(function* () {\n if (typeof window === \"undefined\") {\n ctx.log.debug(\"errors: SSR - skipping setup\");\n return;\n }\n\n // Simple in-memory de-dupe to avoid double reports from multiple hooks\n const recent = new Set<string>();\n\n const DEDUPE_TTL_MS = 2000;\n const STACK_PREFIX_SLICE = 120;\n\n const makeKey = (input: {\n message?: string | null;\n stack?: string | null;\n name?: string | null;\n }) =>\n `${input.name ?? \"\"}|${input.message ?? \"\"}|${input.stack?.slice(0, STACK_PREFIX_SLICE) ?? \"\"}`;\n\n const recordOnce = (\n payload: ReturnType<typeof buildErrorPayload>,\n span: string\n ) => {\n const key = makeKey(payload);\n\n if (recent.has(key)) {\n return;\n }\n\n recent.add(key);\n\n setTimeout(() => recent.delete(key), DEDUPE_TTL_MS);\n\n ctx.span(span, ctx.capture(\"error\", payload));\n };\n\n const api: ErrorsAPI = {\n captureError: (err: unknown, meta?: Record<string, unknown>) => {\n // Skip SDK-internal errors to prevent infinite recursion\n if (isInterfereInternalError(err)) {\n return;\n }\n\n const errorInfo =\n err instanceof Error\n ? { message: err.message, stack: err.stack, name: err.name }\n : { message: String(err), name: \"UnknownError\" };\n\n const payload = buildErrorPayload(errorInfo);\n recordOnce(\n (meta\n ? ({ ...payload, ...meta } as unknown)\n : payload) as ErrorEnvelopePayload,\n \"plugin.error.capture\"\n );\n },\n };\n\n const errorHandler = (event: ErrorEvent) => {\n // Skip SDK-internal errors to prevent infinite recursion\n if (isInterfereInternalError(event.error)) {\n return;\n }\n\n const payload = buildErrorPayload({\n message: event.message,\n stack: event.error?.stack,\n filename: event.filename,\n lineno: event.lineno,\n colno: event.colno,\n name: event.error?.name,\n });\n\n recordOnce(payload, \"plugin.error.window\");\n };\n\n const rejectionHandler = (event: PromiseRejectionEvent) => {\n const reason = event.reason;\n\n // Skip SDK-internal errors to prevent infinite recursion\n if (isInterfereInternalError(reason)) {\n return;\n }\n\n const payload = buildErrorPayload(\n reason instanceof Error\n ? {\n message: `Unhandled Promise Rejection: ${reason.message}`,\n stack: reason.stack,\n name: reason.name,\n }\n : {\n message: `Unhandled Promise Rejection: ${String(reason)}`,\n name: \"UnhandledRejection\",\n }\n );\n\n recordOnce(payload, \"plugin.error.rejection\");\n };\n\n // Network errors: patch fetch to capture rejected requests\n const originalFetch: typeof window.fetch = window.fetch.bind(window);\n\n const patchedFetch: typeof window.fetch = (...args) =>\n originalFetch(...args).catch((err: unknown) => {\n // Skip SDK-internal errors to prevent infinite recursion\n // This is a secondary safety net; the primary protection is that\n // the HTTP layer uses nativeFetch which bypasses this wrapper entirely.\n if (isInterfereInternalError(err)) {\n throw err;\n }\n\n const errorInfo =\n err instanceof Error\n ? {\n message: `Fetch failed: ${err.message}`,\n stack: err.stack,\n name: err.name,\n }\n : { message: `Fetch failed: ${String(err)}`, name: \"FetchError\" };\n const payload = buildErrorPayload(errorInfo);\n recordOnce(payload, \"plugin.error.fetch\");\n throw err;\n });\n\n window.fetch = patchedFetch;\n\n let inConsoleCapture = false;\n\n const originalConsoleError: typeof console.error =\n console.error.bind(console);\n\n console.error = (...args: unknown[]) => {\n try {\n originalConsoleError(...(args as Parameters<typeof console.error>));\n } finally {\n if (!inConsoleCapture) {\n inConsoleCapture = true;\n try {\n const firstError = args.find(\n (a): a is Error => a instanceof Error\n );\n\n // Skip SDK-internal errors to prevent infinite recursion\n const isInternalError =\n firstError && isInterfereInternalError(firstError);\n\n if (!isInternalError) {\n const toStringSafe = (a: unknown): string => {\n if (typeof a === \"string\") {\n return a;\n }\n if (a instanceof Error) {\n return a.message;\n }\n try {\n return JSON.stringify(a);\n } catch {\n return String(a);\n }\n };\n const message = args.map(toStringSafe).join(\" \");\n\n const payload = buildErrorPayload({\n message,\n name: firstError?.name ?? \"ConsoleError\",\n stack: firstError?.stack,\n });\n\n recordOnce(payload, \"plugin.error.console_error\");\n }\n } finally {\n inConsoleCapture = false;\n }\n }\n }\n };\n\n window.addEventListener(\"error\", errorHandler, {\n capture: true,\n });\n\n window.addEventListener(\"unhandledrejection\", rejectionHandler);\n\n return {\n key: \"errors\",\n api,\n cleanup: () => {\n window.removeEventListener(\"error\", errorHandler, {\n capture: true,\n } as EventListenerOptions);\n\n window.removeEventListener(\"unhandledrejection\", rejectionHandler);\n\n window.fetch = originalFetch;\n console.error = originalConsoleError as typeof console.error;\n },\n };\n }),\n});\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ //#region src/core/runtime/native-fetch.d.ts
2
+ /**
3
+ * Native Fetch Store
4
+ *
5
+ * Captures the pristine `fetch` API before any plugins patch it.
6
+ * This prevents infinite loops when the SDK's own requests fail:
7
+ *
8
+ * 1. Errors plugin patches window.fetch to capture failed requests
9
+ * 2. If SDK uses patched fetch and it fails → captured as error → tries to send → loop
10
+ * 3. By using native fetch for SDK requests, we avoid this recursion
11
+ *
12
+ * This module MUST be imported early, before any plugins initialize.
13
+ *
14
+ * @see https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
15
+ */
16
+ declare const nativeFetch: typeof fetch;
17
+ /**
18
+ * Symbol used to mark SDK-internal errors.
19
+ * When an error has this property set to true, the errors plugin should NOT
20
+ * capture it to avoid infinite recursion.
21
+ */
22
+ declare const INTERFERE_INTERNAL_SYMBOL: unique symbol;
23
+ /**
24
+ * Checks if an error is marked as an SDK-internal error.
25
+ */
26
+ declare function isInterfereInternalError(error: unknown): boolean;
27
+ /**
28
+ * Marks an error as SDK-internal to prevent recursive capture.
29
+ */
30
+ declare function markAsInterfereInternal<T extends Error>(error: T): T;
31
+ //#endregion
32
+ export { INTERFERE_INTERNAL_SYMBOL, isInterfereInternalError, markAsInterfereInternal, nativeFetch };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-fetch.d.mts","names":[],"sources":["../../../src/core/runtime/native-fetch.ts"],"sourcesContent":[],"mappings":";;AAkCA;AAOA;AAKA;AAYA;;;;;;;;;;cAxBa,oBAAW;;;;;;cAOX;;;;iBAKG,wBAAA;;;;iBAYA,kCAAkC,cAAc,IAAI"}
@@ -0,0 +1,49 @@
1
+ //#region src/core/runtime/native-fetch.ts
2
+ /**
3
+ * Native Fetch Store
4
+ *
5
+ * Captures the pristine `fetch` API before any plugins patch it.
6
+ * This prevents infinite loops when the SDK's own requests fail:
7
+ *
8
+ * 1. Errors plugin patches window.fetch to capture failed requests
9
+ * 2. If SDK uses patched fetch and it fails → captured as error → tries to send → loop
10
+ * 3. By using native fetch for SDK requests, we avoid this recursion
11
+ *
12
+ * This module MUST be imported early, before any plugins initialize.
13
+ *
14
+ * @see https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
15
+ */
16
+ /**
17
+ * The pristine, un-patched fetch function.
18
+ * Captured at module load time, before any plugins can patch window.fetch.
19
+ */
20
+ function getPristineFetch() {
21
+ if (typeof window !== "undefined") return window.fetch.bind(window);
22
+ if (typeof globalThis.fetch === "function") return globalThis.fetch.bind(globalThis);
23
+ return (() => {
24
+ throw new Error("fetch is not available in this environment");
25
+ });
26
+ }
27
+ const nativeFetch = getPristineFetch();
28
+ /**
29
+ * Symbol used to mark SDK-internal errors.
30
+ * When an error has this property set to true, the errors plugin should NOT
31
+ * capture it to avoid infinite recursion.
32
+ */
33
+ const INTERFERE_INTERNAL_SYMBOL = Symbol.for("__interfere__");
34
+ /**
35
+ * Checks if an error is marked as an SDK-internal error.
36
+ */
37
+ function isInterfereInternalError(error) {
38
+ return error !== null && typeof error === "object" && INTERFERE_INTERNAL_SYMBOL in error && error[INTERFERE_INTERNAL_SYMBOL] === true;
39
+ }
40
+ /**
41
+ * Marks an error as SDK-internal to prevent recursive capture.
42
+ */
43
+ function markAsInterfereInternal(error) {
44
+ error[INTERFERE_INTERNAL_SYMBOL] = true;
45
+ return error;
46
+ }
47
+
48
+ //#endregion
49
+ export { INTERFERE_INTERNAL_SYMBOL, isInterfereInternalError, markAsInterfereInternal, nativeFetch };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-fetch.mjs","names":[],"sources":["../../../src/core/runtime/native-fetch.ts"],"sourcesContent":["/**\n * Native Fetch Store\n *\n * Captures the pristine `fetch` API before any plugins patch it.\n * This prevents infinite loops when the SDK's own requests fail:\n *\n * 1. Errors plugin patches window.fetch to capture failed requests\n * 2. If SDK uses patched fetch and it fails → captured as error → tries to send → loop\n * 3. By using native fetch for SDK requests, we avoid this recursion\n *\n * This module MUST be imported early, before any plugins initialize.\n *\n * @see https://fetch.spec.whatwg.org/#http-network-or-cache-fetch\n */\n\n/**\n * The pristine, un-patched fetch function.\n * Captured at module load time, before any plugins can patch window.fetch.\n */\nfunction getPristineFetch(): typeof globalThis.fetch {\n if (typeof window !== \"undefined\") {\n return window.fetch.bind(window);\n }\n\n if (typeof globalThis.fetch === \"function\") {\n return globalThis.fetch.bind(globalThis);\n }\n\n // Fallback for environments without fetch (will fail at runtime if used)\n return (() => {\n throw new Error(\"fetch is not available in this environment\");\n }) as typeof globalThis.fetch;\n}\n\nexport const nativeFetch = getPristineFetch();\n\n/**\n * Symbol used to mark SDK-internal errors.\n * When an error has this property set to true, the errors plugin should NOT\n * capture it to avoid infinite recursion.\n */\nexport const INTERFERE_INTERNAL_SYMBOL = Symbol.for(\"__interfere__\");\n\n/**\n * Checks if an error is marked as an SDK-internal error.\n */\nexport function isInterfereInternalError(error: unknown): boolean {\n return (\n error !== null &&\n typeof error === \"object\" &&\n INTERFERE_INTERNAL_SYMBOL in error &&\n (error as Record<symbol, unknown>)[INTERFERE_INTERNAL_SYMBOL] === true\n );\n}\n\n/**\n * Marks an error as SDK-internal to prevent recursive capture.\n */\nexport function markAsInterfereInternal<T extends Error>(error: T): T {\n (error as T & { [INTERFERE_INTERNAL_SYMBOL]: boolean })[\n INTERFERE_INTERNAL_SYMBOL\n ] = true;\n return error;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,SAAS,mBAA4C;AACnD,KAAI,OAAO,WAAW,YACpB,QAAO,OAAO,MAAM,KAAK,OAAO;AAGlC,KAAI,OAAO,WAAW,UAAU,WAC9B,QAAO,WAAW,MAAM,KAAK,WAAW;AAI1C,eAAc;AACZ,QAAM,IAAI,MAAM,6CAA6C;;;AAIjE,MAAa,cAAc,kBAAkB;;;;;;AAO7C,MAAa,4BAA4B,OAAO,IAAI,gBAAgB;;;;AAKpE,SAAgB,yBAAyB,OAAyB;AAChE,QACE,UAAU,QACV,OAAO,UAAU,YACjB,6BAA6B,SAC5B,MAAkC,+BAA+B;;;;;AAOtE,SAAgB,wBAAyC,OAAa;AACpE,CAAC,MACC,6BACE;AACJ,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/effect/errors.ts"],"sourcesContent":[],"mappings":";;;;;;;cAEa,SAAA,SAAkB;;;;;;cAK1B;;;cAEQ,eAAA,SAAwB;;;;AAPrC,CAAA,CAAA,CAAA;cAWK;;;cAEQ,WAAA,SAAoB;;;cAE5B;;;cAEQ,YAAA,SAAqB;;EAVrB,SAAA,SAAgB,EAAA,MAAA,GAAQ,MAAA,GAAA,QAAA,GAAA,QAAA,GAAA,MAAA;AAIhC,CAAA,CAAA,CAAA"}
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/effect/errors.ts"],"sourcesContent":[],"mappings":";;;;;;;cAEa,SAAA,SAAkB;;;;;;cAK1B;;;cAEQ,eAAA,SAAwB;;;;AAPrC,CAAA,CAAA,CAAA;cAWK;;;cAEQ,WAAA,SAAoB;;;cAE5B;;;cAEQ,YAAA,SAAqB;;EAVrB,SAAA,SAAgB,EAAA,MAAA,GAAA,MAAQ,GAAA,QAAA,GAAA,QAAA,GAAA,MAAA;AAIhC,CAAA,CAAA,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"http.layer.d.mts","names":[],"sources":["../../../src/effect/layers/http.layer.ts"],"sourcesContent":[],"mappings":";;;;;;UAYiB,WAAA;sCAEF,eACR,MAAA,CAAO,aAAa;AAH3B;cAIC,mBAFc,kBAAA,eAAA,EAAA,uBAAA,aAAA,CAAA;AACY,cAGd,cAAA,SAAuB,mBAAA,CAHT;;AAC1B;;UAsDS,QAAA;0BACgB,UAAA,CAAW;;AArDrC;AAoDU,cAKG,eAJa,EAAW,CAAA,MAAK,EAIF,SAJE,EAAA,IAAA,CAAA,EAIgB,QAJhB,EAAA,GAIwB,KAAA,CAAA,KAJxB,CAIwB,cAJxB,EAAA,KAAA,EAAA,KAAA,CAAA"}
1
+ {"version":3,"file":"http.layer.d.mts","names":[],"sources":["../../../src/effect/layers/http.layer.ts"],"sourcesContent":[],"mappings":";;;;;;UAaiB,WAAA;sCAEF,eACR,MAAA,CAAO,aAAa;AAH3B;cAIC,mBAFc,kBAAA,eAAA,EAAA,uBAAA,aAAA,CAAA;AACY,cAGd,cAAA,SAAuB,mBAAA,CAHT;;AAC1B;;UAsDS,QAAA;0BACgB,UAAA,CAAW;;AArDrC;AAoDU,cAKG,eAJa,EAAW,CAAA,MAAK,EAIF,SAJE,EAAA,IAAA,CAAA,EAIgB,QAJhB,EAAA,GAIwB,KAAA,CAAA,KAJxB,CAIwB,cAJxB,EAAA,KAAA,EAAA,KAAA,CAAA"}
@@ -1,5 +1,6 @@
1
1
  import { HttpError } from "../errors.mjs";
2
2
  import { deriveIngestTarget } from "../../core/runtime/ingest-target.mjs";
3
+ import { nativeFetch } from "../../core/runtime/native-fetch.mjs";
3
4
  import { Context, Effect, Layer } from "effect";
4
5
  import { DEFAULT_TIMEOUT_MS, withTimeoutFail } from "@interfere/effect-utils/retry";
5
6
 
@@ -56,7 +57,7 @@ const HttpServiceLive = (config, deps) => {
56
57
  const url = target.url;
57
58
  headers.set("traceparent", generateTraceparent());
58
59
  const TIMEOUT_MS = deps?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
59
- const doFetch = deps?.fetch ?? fetch;
60
+ const doFetch = deps?.fetch ?? nativeFetch;
60
61
  const HTTP_METHOD_POST = "POST";
61
62
  const body = JSON.stringify(envelopes);
62
63
  const requestSize = body.length;
@@ -1 +1 @@
1
- {"version":3,"file":"http.layer.mjs","names":["body"],"sources":["../../../src/effect/layers/http.layer.ts"],"sourcesContent":["import {\n DEFAULT_TIMEOUT_MS,\n withTimeoutFail,\n} from \"@interfere/effect-utils/retry\";\nimport type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { Context, Effect, Layer } from \"effect\";\n\nimport { deriveIngestTarget } from \"../../core/runtime/ingest-target.js\";\nimport { HttpError } from \"../errors.js\";\nimport type { SdkConfig } from \"./config.layer.js\";\n\nexport interface HttpService {\n readonly postEnvelopes: (\n envelopes: Envelope[]\n ) => Effect.Effect<void, HttpError>;\n}\n\nexport class HttpServiceTag extends Context.Tag(\"@interfere/react/Http\")<\n HttpServiceTag,\n HttpService\n>() {}\n\n/**\n * Generates a W3C traceparent header\n */\nconst generateTraceparent = (): string => {\n const version = \"00\";\n const TRACE_ID_BYTES = 16;\n const SPAN_ID_BYTES = 8;\n const traceId = randomHex(TRACE_ID_BYTES);\n const spanId = randomHex(SPAN_ID_BYTES);\n const flags = \"01\";\n return `${version}-${traceId}-${spanId}-${flags}`;\n};\n\nfunction randomHex(bytes: number): string {\n const RANDOM_MAX = 256;\n const HEX_BYTE_WIDTH = 2;\n const buf = new Uint8Array(bytes);\n\n if (\n typeof crypto !== \"undefined\" &&\n typeof crypto.getRandomValues === \"function\"\n ) {\n crypto.getRandomValues(buf);\n } else {\n for (let i = 0; i < bytes; i += 1) {\n buf[i] = Math.floor(Math.random() * RANDOM_MAX);\n }\n }\n\n const HEX_BASE = 16;\n return Array.from(buf)\n .map((b) => b.toString(HEX_BASE).padStart(HEX_BYTE_WIDTH, \"0\"))\n .join(\"\");\n}\n\n/**\n * Keepalive limits per Fetch spec (https://fetch.spec.whatwg.org/#http-network-or-cache-fetch):\n * - If sum of body + inflight keepalive bytes > 64KB, returns network error\n * - We use 60KB threshold to leave room for headers/overhead\n * - Also limit concurrent keepalive requests to 15 (browser implementation detail)\n */\nconst KEEPALIVE_BYTE_LIMIT = 60_000;\nconst KEEPALIVE_REQUEST_LIMIT = 15; // Arbitrary limit\n\n/**\n * Creates an HTTP service layer for posting envelopes\n */\ninterface HttpDeps {\n readonly fetch?: typeof globalThis.fetch;\n readonly timeoutMs?: number;\n}\n\nexport const HttpServiceLive = (config: SdkConfig, deps?: HttpDeps) => {\n // Track pending keepalive request state across all concurrent requests.\n // These are closure variables that persist for the lifetime of the layer.\n let pendingBodySize = 0;\n let pendingCount = 0;\n\n return Layer.succeed(HttpServiceTag, {\n postEnvelopes: (envelopes: Envelope[]) => {\n const spanUrl = (() => {\n try {\n return deriveIngestTarget(config).url;\n } catch {\n return \"<config-error>\";\n }\n })();\n\n return Effect.gen(function* () {\n const target = yield* Effect.try({\n try: () => deriveIngestTarget(config),\n catch: (err) =>\n new HttpError({\n status: 0,\n message: String(err instanceof Error ? err.message : err),\n url: \"<config-error>\",\n }),\n });\n\n yield* Effect.logDebug(\"Sending batch to ingest\").pipe(\n Effect.annotateLogs({\n url: target.url,\n envelopeCount: envelopes.length,\n })\n );\n\n const headers = new Headers(target.headers);\n const url = target.url;\n\n // Add traceparent header\n headers.set(\"traceparent\", generateTraceparent());\n\n // Send the request with timeout\n const TIMEOUT_MS = deps?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const doFetch = deps?.fetch ?? fetch;\n const HTTP_METHOD_POST = \"POST\" as const;\n\n const body = JSON.stringify(envelopes);\n const requestSize = body.length;\n\n // Track this request's size for keepalive limit calculation\n pendingBodySize += requestSize;\n pendingCount += 1;\n\n // Keepalive allows requests to complete even when navigating away, but has limits:\n // - Per Fetch spec, if sum of inflight keepalive bytes > 64KB, it fails with network error\n // - The limit is aggregate across ALL concurrent keepalive requests, not per-request\n // - Caused \"Failed to fetch\" (status 0) errors in production when batches were large\n // - We only enable keepalive when under both size and count limits\n // See: https://fetch.spec.whatwg.org/#http-network-or-cache-fetch (step 8.10.5)\n const useKeepalive =\n pendingBodySize <= KEEPALIVE_BYTE_LIMIT &&\n pendingCount <= KEEPALIVE_REQUEST_LIMIT;\n\n const response = yield* withTimeoutFail(\n Effect.tryPromise({\n try: async () => {\n try {\n return await doFetch(url, {\n method: HTTP_METHOD_POST,\n headers,\n body,\n keepalive: useKeepalive,\n });\n } finally {\n // Always decrement counters when request completes (success or failure)\n pendingBodySize -= requestSize;\n pendingCount -= 1;\n }\n },\n catch: (error) =>\n new HttpError({\n status: 0,\n message: `Network error: ${String(error)}`,\n url,\n }),\n }),\n TIMEOUT_MS,\n () =>\n new HttpError({\n status: 0,\n message: \"Network timeout\",\n url,\n })\n );\n\n if (!response.ok) {\n const body = yield* Effect.promise(() => response.text()).pipe(\n Effect.orElse(() => Effect.succeed(\"\"))\n );\n\n // If the server tells us the envelopes are malformed, treat this as a\n // permanent failure so the queue layer can drop them instead of retrying.\n if (response.status === 422) {\n return yield* Effect.fail(\n new HttpError({\n status: response.status,\n message: \"Invalid envelopes (schema mismatch)\",\n url,\n body,\n })\n );\n }\n\n return yield* Effect.fail(\n new HttpError({\n status: response.status,\n message: `Ingest failed: ${response.status} ${response.statusText}`,\n url,\n body,\n })\n );\n }\n }).pipe(\n Effect.withSpan(\"http.postEnvelopes\", {\n attributes: {\n envelopeCount: envelopes.length,\n url: spanUrl,\n },\n })\n );\n },\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"http.layer.mjs","names":["body"],"sources":["../../../src/effect/layers/http.layer.ts"],"sourcesContent":["import {\n DEFAULT_TIMEOUT_MS,\n withTimeoutFail,\n} from \"@interfere/effect-utils/retry\";\nimport type { Envelope } from \"@interfere/types/sdk/envelope\";\n\nimport { Context, Effect, Layer } from \"effect\";\n\nimport { deriveIngestTarget } from \"../../core/runtime/ingest-target.js\";\nimport { nativeFetch } from \"../../core/runtime/native-fetch.js\";\nimport { HttpError } from \"../errors.js\";\nimport type { SdkConfig } from \"./config.layer.js\";\n\nexport interface HttpService {\n readonly postEnvelopes: (\n envelopes: Envelope[]\n ) => Effect.Effect<void, HttpError>;\n}\n\nexport class HttpServiceTag extends Context.Tag(\"@interfere/react/Http\")<\n HttpServiceTag,\n HttpService\n>() {}\n\n/**\n * Generates a W3C traceparent header\n */\nconst generateTraceparent = (): string => {\n const version = \"00\";\n const TRACE_ID_BYTES = 16;\n const SPAN_ID_BYTES = 8;\n const traceId = randomHex(TRACE_ID_BYTES);\n const spanId = randomHex(SPAN_ID_BYTES);\n const flags = \"01\";\n return `${version}-${traceId}-${spanId}-${flags}`;\n};\n\nfunction randomHex(bytes: number): string {\n const RANDOM_MAX = 256;\n const HEX_BYTE_WIDTH = 2;\n const buf = new Uint8Array(bytes);\n\n if (\n typeof crypto !== \"undefined\" &&\n typeof crypto.getRandomValues === \"function\"\n ) {\n crypto.getRandomValues(buf);\n } else {\n for (let i = 0; i < bytes; i += 1) {\n buf[i] = Math.floor(Math.random() * RANDOM_MAX);\n }\n }\n\n const HEX_BASE = 16;\n return Array.from(buf)\n .map((b) => b.toString(HEX_BASE).padStart(HEX_BYTE_WIDTH, \"0\"))\n .join(\"\");\n}\n\n/**\n * Keepalive limits per Fetch spec (https://fetch.spec.whatwg.org/#http-network-or-cache-fetch):\n * - If sum of body + inflight keepalive bytes > 64KB, returns network error\n * - We use 60KB threshold to leave room for headers/overhead\n * - Also limit concurrent keepalive requests to 15 (browser implementation detail)\n */\nconst KEEPALIVE_BYTE_LIMIT = 60_000;\nconst KEEPALIVE_REQUEST_LIMIT = 15; // Arbitrary limit\n\n/**\n * Creates an HTTP service layer for posting envelopes\n */\ninterface HttpDeps {\n readonly fetch?: typeof globalThis.fetch;\n readonly timeoutMs?: number;\n}\n\nexport const HttpServiceLive = (config: SdkConfig, deps?: HttpDeps) => {\n // Track pending keepalive request state across all concurrent requests.\n // These are closure variables that persist for the lifetime of the layer.\n let pendingBodySize = 0;\n let pendingCount = 0;\n\n return Layer.succeed(HttpServiceTag, {\n postEnvelopes: (envelopes: Envelope[]) => {\n const spanUrl = (() => {\n try {\n return deriveIngestTarget(config).url;\n } catch {\n return \"<config-error>\";\n }\n })();\n\n return Effect.gen(function* () {\n const target = yield* Effect.try({\n try: () => deriveIngestTarget(config),\n catch: (err) =>\n new HttpError({\n status: 0,\n message: String(err instanceof Error ? err.message : err),\n url: \"<config-error>\",\n }),\n });\n\n yield* Effect.logDebug(\"Sending batch to ingest\").pipe(\n Effect.annotateLogs({\n url: target.url,\n envelopeCount: envelopes.length,\n })\n );\n\n const headers = new Headers(target.headers);\n const url = target.url;\n\n // Add traceparent header\n headers.set(\"traceparent\", generateTraceparent());\n\n // Send the request with timeout\n const TIMEOUT_MS = deps?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n // Use native fetch to avoid infinite loops when errors plugin patches window.fetch\n // If SDK's own request fails with patched fetch, it would be captured as an error,\n // triggering another send attempt, which could fail and loop indefinitely.\n const doFetch = deps?.fetch ?? nativeFetch;\n const HTTP_METHOD_POST = \"POST\" as const;\n\n const body = JSON.stringify(envelopes);\n const requestSize = body.length;\n\n // Track this request's size for keepalive limit calculation\n pendingBodySize += requestSize;\n pendingCount += 1;\n\n // Keepalive allows requests to complete even when navigating away, but has limits:\n // - Per Fetch spec, if sum of inflight keepalive bytes > 64KB, it fails with network error\n // - The limit is aggregate across ALL concurrent keepalive requests, not per-request\n // - Caused \"Failed to fetch\" (status 0) errors in production when batches were large\n // - We only enable keepalive when under both size and count limits\n // See: https://fetch.spec.whatwg.org/#http-network-or-cache-fetch (step 8.10.5)\n const useKeepalive =\n pendingBodySize <= KEEPALIVE_BYTE_LIMIT &&\n pendingCount <= KEEPALIVE_REQUEST_LIMIT;\n\n const response = yield* withTimeoutFail(\n Effect.tryPromise({\n try: async () => {\n try {\n return await doFetch(url, {\n method: HTTP_METHOD_POST,\n headers,\n body,\n keepalive: useKeepalive,\n });\n } finally {\n // Always decrement counters when request completes (success or failure)\n pendingBodySize -= requestSize;\n pendingCount -= 1;\n }\n },\n catch: (error) =>\n new HttpError({\n status: 0,\n message: `Network error: ${String(error)}`,\n url,\n }),\n }),\n TIMEOUT_MS,\n () =>\n new HttpError({\n status: 0,\n message: \"Network timeout\",\n url,\n })\n );\n\n if (!response.ok) {\n const body = yield* Effect.promise(() => response.text()).pipe(\n Effect.orElse(() => Effect.succeed(\"\"))\n );\n\n // If the server tells us the envelopes are malformed, treat this as a\n // permanent failure so the queue layer can drop them instead of retrying.\n if (response.status === 422) {\n return yield* Effect.fail(\n new HttpError({\n status: response.status,\n message: \"Invalid envelopes (schema mismatch)\",\n url,\n body,\n })\n );\n }\n\n return yield* Effect.fail(\n new HttpError({\n status: response.status,\n message: `Ingest failed: ${response.status} ${response.statusText}`,\n url,\n body,\n })\n );\n }\n }).pipe(\n Effect.withSpan(\"http.postEnvelopes\", {\n attributes: {\n envelopeCount: envelopes.length,\n url: spanUrl,\n },\n })\n );\n },\n });\n};\n"],"mappings":"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"}
package/dist/package.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@interfere/react";
3
- var version = "0.1.0-alpha.3";
3
+ var version = "0.1.0-alpha.5";
4
4
  var package_default = {
5
5
  name,
6
6
  version,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/react",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.5",
4
4
  "license": "MIT",
5
5
  "description": "Build apps that never break.",
6
6
  "keywords": [
@@ -73,7 +73,7 @@
73
73
  "uuid": "^13.0.0",
74
74
  "zod": "^4.3.5",
75
75
  "@interfere/constants": "0.0.2-alpha.0",
76
- "@interfere/effect-utils": "0.0.2-alpha.1",
76
+ "@interfere/effect-utils": "0.0.2-alpha.2",
77
77
  "@interfere/types": "0.1.0-alpha.1"
78
78
  },
79
79
  "peerDependencies": {