@reliantlabs/forge-web-runtime 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +192 -0
- package/dist/basepath.d.ts +57 -0
- package/dist/basepath.js +102 -0
- package/dist/basepath.js.map +1 -0
- package/dist/error-boundary.d.ts +31 -0
- package/dist/error-boundary.js +62 -0
- package/dist/error-boundary.js.map +1 -0
- package/dist/errors.d.ts +90 -0
- package/dist/errors.js +199 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptors.d.ts +49 -0
- package/dist/interceptors.js +154 -0
- package/dist/interceptors.js.map +1 -0
- package/dist/mock-transport.d.ts +115 -0
- package/dist/mock-transport.js +320 -0
- package/dist/mock-transport.js.map +1 -0
- package/dist/otel.d.ts +31 -0
- package/dist/otel.js +94 -0
- package/dist/otel.js.map +1 -0
- package/dist/providers.d.ts +19 -0
- package/dist/providers.js +25 -0
- package/dist/providers.js.map +1 -0
- package/dist/resource.d.ts +33 -0
- package/dist/resource.js +77 -0
- package/dist/resource.js.map +1 -0
- package/dist/route-guard.d.ts +9 -0
- package/dist/route-guard.js +21 -0
- package/dist/route-guard.js.map +1 -0
- package/dist/service-hooks.d.ts +78 -0
- package/dist/service-hooks.js +112 -0
- package/dist/service-hooks.js.map +1 -0
- package/dist/session.d.ts +45 -0
- package/dist/session.js +79 -0
- package/dist/session.js.map +1 -0
- package/dist/telemetry.d.ts +25 -0
- package/dist/telemetry.js +128 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/toast-host.d.ts +36 -0
- package/dist/toast-host.js +34 -0
- package/dist/toast-host.js.map +1 -0
- package/dist/trace.d.ts +21 -0
- package/dist/trace.js +106 -0
- package/dist/trace.js.map +1 -0
- package/interceptors/package.json +7 -0
- package/package.json +100 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface TelemetryEvent {
|
|
2
|
+
type: "error" | "unhandled-rejection" | "web-vital";
|
|
3
|
+
name: string;
|
|
4
|
+
value?: number;
|
|
5
|
+
message?: string;
|
|
6
|
+
detail?: Record<string, unknown>;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
}
|
|
9
|
+
export interface TelemetryConfig {
|
|
10
|
+
serviceName?: string;
|
|
11
|
+
/** OTLP/HTTP-ish collector URL. Unset = no network export (default). */
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
/** In-process sink for every event. Default: console.debug in dev. */
|
|
14
|
+
onEvent?: (event: TelemetryEvent) => void;
|
|
15
|
+
/** Capture window errors + unhandled rejections. Default true. */
|
|
16
|
+
captureErrors?: boolean;
|
|
17
|
+
/** Capture LCP / CLS / FID via PerformanceObserver. Default true. */
|
|
18
|
+
captureWebVitals?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* initClientTelemetry wires the capture listeners and returns a teardown
|
|
22
|
+
* function. Idempotent and SSR-safe (no-op without a DOM). Call once from the
|
|
23
|
+
* app shell (e.g. inside Providers' mount effect).
|
|
24
|
+
*/
|
|
25
|
+
export declare function initClientTelemetry(config?: TelemetryConfig): () => void;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
let started = false;
|
|
2
|
+
/**
|
|
3
|
+
* initClientTelemetry wires the capture listeners and returns a teardown
|
|
4
|
+
* function. Idempotent and SSR-safe (no-op without a DOM). Call once from the
|
|
5
|
+
* app shell (e.g. inside Providers' mount effect).
|
|
6
|
+
*/
|
|
7
|
+
export function initClientTelemetry(config = {}) {
|
|
8
|
+
if (started || typeof window === "undefined") {
|
|
9
|
+
return () => { };
|
|
10
|
+
}
|
|
11
|
+
started = true;
|
|
12
|
+
const serviceName = config.serviceName ?? "frontend";
|
|
13
|
+
const captureErrors = config.captureErrors ?? true;
|
|
14
|
+
const captureWebVitals = config.captureWebVitals ?? true;
|
|
15
|
+
const dispatch = (event) => {
|
|
16
|
+
if (config.onEvent) {
|
|
17
|
+
config.onEvent(event);
|
|
18
|
+
}
|
|
19
|
+
else if (typeof process !== "undefined" &&
|
|
20
|
+
process.env?.NODE_ENV === "development") {
|
|
21
|
+
// The `typeof` guard keeps this framework-agnostic — Vite SPAs and
|
|
22
|
+
// plain browsers have no `process` at all.
|
|
23
|
+
console.debug("[telemetry]", event.type, event.name, event.value ?? "");
|
|
24
|
+
}
|
|
25
|
+
if (config.endpoint) {
|
|
26
|
+
exportEvent(config.endpoint, serviceName, event);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const teardowns = [];
|
|
30
|
+
if (captureErrors) {
|
|
31
|
+
const onError = (e) => dispatch({
|
|
32
|
+
type: "error",
|
|
33
|
+
name: e.error?.name ?? "Error",
|
|
34
|
+
message: e.message,
|
|
35
|
+
detail: { filename: e.filename, line: e.lineno, col: e.colno },
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
});
|
|
38
|
+
const onRejection = (e) => dispatch({
|
|
39
|
+
type: "unhandled-rejection",
|
|
40
|
+
name: "UnhandledRejection",
|
|
41
|
+
message: String(e.reason),
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
});
|
|
44
|
+
window.addEventListener("error", onError);
|
|
45
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
46
|
+
teardowns.push(() => window.removeEventListener("error", onError));
|
|
47
|
+
teardowns.push(() => window.removeEventListener("unhandledrejection", onRejection));
|
|
48
|
+
}
|
|
49
|
+
if (captureWebVitals && typeof PerformanceObserver === "function") {
|
|
50
|
+
let clsValue = 0;
|
|
51
|
+
const observe = (type, cb) => {
|
|
52
|
+
try {
|
|
53
|
+
const obs = new PerformanceObserver((list) => cb(list.getEntries()));
|
|
54
|
+
obs.observe({ type, buffered: true });
|
|
55
|
+
teardowns.push(() => obs.disconnect());
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Unsupported entry type in this browser — skip silently.
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
observe("largest-contentful-paint", (entries) => {
|
|
62
|
+
const last = entries.at(-1);
|
|
63
|
+
if (last) {
|
|
64
|
+
dispatch({
|
|
65
|
+
type: "web-vital",
|
|
66
|
+
name: "LCP",
|
|
67
|
+
value: Math.round(last.startTime),
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
observe("layout-shift", (entries) => {
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (!entry.hadRecentInput) {
|
|
75
|
+
clsValue += entry.value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
dispatch({
|
|
79
|
+
type: "web-vital",
|
|
80
|
+
name: "CLS",
|
|
81
|
+
value: Number(clsValue.toFixed(4)),
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
// FID (First Input Delay): processingStart − startTime of the first
|
|
86
|
+
// input. This is FID, NOT INP — INP needs the full interaction-latency
|
|
87
|
+
// model from the web-vitals library, which this dependency-free module
|
|
88
|
+
// deliberately doesn't pull in. Reach for `web-vitals` if you need INP.
|
|
89
|
+
observe("first-input", (entries) => {
|
|
90
|
+
const first = entries[0];
|
|
91
|
+
if (first) {
|
|
92
|
+
dispatch({
|
|
93
|
+
type: "web-vital",
|
|
94
|
+
name: "FID",
|
|
95
|
+
value: Math.round(first.processingStart - first.startTime),
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return () => {
|
|
102
|
+
for (const t of teardowns) {
|
|
103
|
+
t();
|
|
104
|
+
}
|
|
105
|
+
started = false;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Best-effort beacon of one event. Never throws into the caller. */
|
|
109
|
+
function exportEvent(endpoint, serviceName, event) {
|
|
110
|
+
try {
|
|
111
|
+
const body = JSON.stringify({ service: serviceName, ...event });
|
|
112
|
+
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
113
|
+
navigator.sendBeacon(endpoint, body);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
void fetch(endpoint, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
body,
|
|
119
|
+
keepalive: true,
|
|
120
|
+
headers: { "Content-Type": "application/json" },
|
|
121
|
+
}).catch(() => { });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Telemetry must never break the app.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// Lightweight client RUM: unhandled errors + core Web Vitals, captured with\n// native browser APIs (no extra dependency). Export is OPT-IN: by default\n// events go to an in-process sink (console.debug in dev). Set `endpoint` to\n// beacon them to a browser-reachable OTLP/HTTP collector (needs CORS on that\n// collector — stand it up before relying on export). This is the config seam;\n// distributed tracing itself (the traceparent on every RPC) is always on and\n// lives in trace.ts — it does NOT depend on this module.\nexport interface TelemetryEvent {\n type: \"error\" | \"unhandled-rejection\" | \"web-vital\";\n name: string;\n value?: number;\n message?: string;\n detail?: Record<string, unknown>;\n timestamp: number;\n}\n\nexport interface TelemetryConfig {\n serviceName?: string;\n /** OTLP/HTTP-ish collector URL. Unset = no network export (default). */\n endpoint?: string;\n /** In-process sink for every event. Default: console.debug in dev. */\n onEvent?: (event: TelemetryEvent) => void;\n /** Capture window errors + unhandled rejections. Default true. */\n captureErrors?: boolean;\n /** Capture LCP / CLS / FID via PerformanceObserver. Default true. */\n captureWebVitals?: boolean;\n}\n\ninterface LayoutShiftEntry extends PerformanceEntry {\n value: number;\n hadRecentInput: boolean;\n}\n\ninterface FirstInputEntry extends PerformanceEntry {\n processingStart: number;\n}\n\n// `process` is a bundler-injected global (Next replaces the literal\n// `process.env.NODE_ENV` at build time), not a browser one. Declaring it\n// locally keeps this package free of a @types/node dependency while leaving\n// the expression textually intact so that replacement still happens.\ndeclare const process:\n | { env?: Record<string, string | undefined> }\n | undefined;\n\nlet started = false;\n\n/**\n * initClientTelemetry wires the capture listeners and returns a teardown\n * function. Idempotent and SSR-safe (no-op without a DOM). Call once from the\n * app shell (e.g. inside Providers' mount effect).\n */\nexport function initClientTelemetry(config: TelemetryConfig = {}): () => void {\n if (started || typeof window === \"undefined\") {\n return () => {};\n }\n started = true;\n\n const serviceName = config.serviceName ?? \"frontend\";\n const captureErrors = config.captureErrors ?? true;\n const captureWebVitals = config.captureWebVitals ?? true;\n\n const dispatch = (event: TelemetryEvent) => {\n if (config.onEvent) {\n config.onEvent(event);\n } else if (\n typeof process !== \"undefined\" &&\n process.env?.NODE_ENV === \"development\"\n ) {\n // The `typeof` guard keeps this framework-agnostic — Vite SPAs and\n // plain browsers have no `process` at all.\n console.debug(\"[telemetry]\", event.type, event.name, event.value ?? \"\");\n }\n if (config.endpoint) {\n exportEvent(config.endpoint, serviceName, event);\n }\n };\n\n const teardowns: Array<() => void> = [];\n\n if (captureErrors) {\n const onError = (e: ErrorEvent) =>\n dispatch({\n type: \"error\",\n name: e.error?.name ?? \"Error\",\n message: e.message,\n detail: { filename: e.filename, line: e.lineno, col: e.colno },\n timestamp: Date.now(),\n });\n const onRejection = (e: PromiseRejectionEvent) =>\n dispatch({\n type: \"unhandled-rejection\",\n name: \"UnhandledRejection\",\n message: String(e.reason),\n timestamp: Date.now(),\n });\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n teardowns.push(() => window.removeEventListener(\"error\", onError));\n teardowns.push(() =>\n window.removeEventListener(\"unhandledrejection\", onRejection),\n );\n }\n\n if (captureWebVitals && typeof PerformanceObserver === \"function\") {\n let clsValue = 0;\n const observe = (\n type: string,\n cb: (entries: PerformanceEntryList) => void,\n ) => {\n try {\n const obs = new PerformanceObserver((list) => cb(list.getEntries()));\n obs.observe({ type, buffered: true });\n teardowns.push(() => obs.disconnect());\n } catch {\n // Unsupported entry type in this browser — skip silently.\n }\n };\n\n observe(\"largest-contentful-paint\", (entries) => {\n const last = entries.at(-1);\n if (last) {\n dispatch({\n type: \"web-vital\",\n name: \"LCP\",\n value: Math.round(last.startTime),\n timestamp: Date.now(),\n });\n }\n });\n\n observe(\"layout-shift\", (entries) => {\n for (const entry of entries as LayoutShiftEntry[]) {\n if (!entry.hadRecentInput) {\n clsValue += entry.value;\n }\n }\n dispatch({\n type: \"web-vital\",\n name: \"CLS\",\n value: Number(clsValue.toFixed(4)),\n timestamp: Date.now(),\n });\n });\n\n // FID (First Input Delay): processingStart − startTime of the first\n // input. This is FID, NOT INP — INP needs the full interaction-latency\n // model from the web-vitals library, which this dependency-free module\n // deliberately doesn't pull in. Reach for `web-vitals` if you need INP.\n observe(\"first-input\", (entries) => {\n const first = entries[0] as FirstInputEntry | undefined;\n if (first) {\n dispatch({\n type: \"web-vital\",\n name: \"FID\",\n value: Math.round(first.processingStart - first.startTime),\n timestamp: Date.now(),\n });\n }\n });\n }\n\n return () => {\n for (const t of teardowns) {\n t();\n }\n started = false;\n };\n}\n\n/** Best-effort beacon of one event. Never throws into the caller. */\nfunction exportEvent(\n endpoint: string,\n serviceName: string,\n event: TelemetryEvent,\n): void {\n try {\n const body = JSON.stringify({ service: serviceName, ...event });\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(endpoint, body);\n } else {\n void fetch(endpoint, {\n method: \"POST\",\n body,\n keepalive: true,\n headers: { \"Content-Type\": \"application/json\" },\n }).catch(() => {});\n }\n } catch {\n // Telemetry must never break the app.\n }\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
export type RuntimeToastVariant = "success" | "error" | "warning" | "info";
|
|
3
|
+
/** A toast as emitted — no identity yet; the host assigns one. */
|
|
4
|
+
export interface RuntimeToastPayload {
|
|
5
|
+
message: string;
|
|
6
|
+
variant?: RuntimeToastVariant;
|
|
7
|
+
duration?: number;
|
|
8
|
+
}
|
|
9
|
+
/** A toast the host is currently holding. */
|
|
10
|
+
export interface RuntimeToast extends RuntimeToastPayload {
|
|
11
|
+
id: string;
|
|
12
|
+
}
|
|
13
|
+
/** What the host hands the app so the app can push events into it. */
|
|
14
|
+
export interface RuntimeToastSink {
|
|
15
|
+
/** Enqueue a toast. */
|
|
16
|
+
onShow: (toast: RuntimeToastPayload) => void;
|
|
17
|
+
/** Dismiss one toast by id, or all of them when id is omitted. */
|
|
18
|
+
onDismiss: (id?: string) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface RuntimeToastHostProps {
|
|
21
|
+
/**
|
|
22
|
+
* Wire the app's event source to the host: called with the sink, returns an
|
|
23
|
+
* unsubscribe function. Memoize it (useCallback) — the host re-subscribes
|
|
24
|
+
* whenever its identity changes.
|
|
25
|
+
*/
|
|
26
|
+
subscribe: (sink: RuntimeToastSink) => () => void;
|
|
27
|
+
/**
|
|
28
|
+
* Render the app's own toast presentation. The runtime owns the queue; the
|
|
29
|
+
* component library owns the markup.
|
|
30
|
+
*/
|
|
31
|
+
render: (props: {
|
|
32
|
+
toasts: RuntimeToast[];
|
|
33
|
+
onDismiss: (id: string) => void;
|
|
34
|
+
}) => ReactNode;
|
|
35
|
+
}
|
|
36
|
+
export declare function RuntimeToastHost({ subscribe, render, }: RuntimeToastHostProps): ReactElement;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.
|
|
4
|
+
//
|
|
5
|
+
// The toast host: the queue and its lifecycle, with NO opinion about how a
|
|
6
|
+
// toast looks or where the events come from. The app supplies both —
|
|
7
|
+
// `subscribe` (its event bus) and `render` (its own toast component from the
|
|
8
|
+
// forkable component library). That is what keeps this forge-owned file
|
|
9
|
+
// independent of files the app is free to restyle, rename, or delete.
|
|
10
|
+
import { useCallback, useEffect, useMemo, useState, } from "react";
|
|
11
|
+
let seq = 0;
|
|
12
|
+
function nextId() {
|
|
13
|
+
seq += 1;
|
|
14
|
+
return `t${seq}-${Date.now()}`;
|
|
15
|
+
}
|
|
16
|
+
export function RuntimeToastHost({ subscribe, render, }) {
|
|
17
|
+
const [toasts, setToasts] = useState([]);
|
|
18
|
+
const dismiss = useCallback((id) => {
|
|
19
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
20
|
+
}, []);
|
|
21
|
+
// Stable across renders: every update is functional, so the sink never
|
|
22
|
+
// needs to close over the current toast list.
|
|
23
|
+
const sink = useMemo(() => ({
|
|
24
|
+
onShow: (toast) => {
|
|
25
|
+
setToasts((prev) => [...prev, { ...toast, id: nextId() }]);
|
|
26
|
+
},
|
|
27
|
+
onDismiss: (id) => {
|
|
28
|
+
setToasts((prev) => (id ? prev.filter((t) => t.id !== id) : []));
|
|
29
|
+
},
|
|
30
|
+
}), []);
|
|
31
|
+
useEffect(() => subscribe(sink), [subscribe, sink]);
|
|
32
|
+
return _jsx(_Fragment, { children: render({ toasts, onDismiss: dismiss }) });
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=toast-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toast-host.js","sourceRoot":"","sources":["../src/toast-host.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,sEAAsE;AACtE,EAAE;AACF,2EAA2E;AAC3E,qEAAqE;AACrE,6EAA6E;AAC7E,wEAAwE;AACxE,sEAAsE;AACtE,OAAO,EACL,WAAW,EACX,SAAS,EACT,OAAO,EACP,QAAQ,GAGT,MAAM,OAAO,CAAC;AAyCf,IAAI,GAAG,GAAG,CAAC,CAAC;AACZ,SAAS,MAAM;IACb,GAAG,IAAI,CAAC,CAAC;IACT,OAAO,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAC/B,SAAS,EACT,MAAM,GACgB;IACtB,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAiB,EAAE,CAAC,CAAC;IAEzD,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,EAAU,EAAE,EAAE;QACzC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACvD,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,uEAAuE;IACvE,8CAA8C;IAC9C,MAAM,IAAI,GAAG,OAAO,CAClB,GAAG,EAAE,CAAC,CAAC;QACL,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YAChB,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;QAC7D,CAAC;QACD,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE;YAChB,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACnE,CAAC;KACF,CAAC,EACF,EAAE,CACH,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC;IAEpD,OAAO,4BAAG,MAAM,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,GAAI,CAAC;AACvD,CAAC","sourcesContent":["\"use client\";\n\n// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// The toast host: the queue and its lifecycle, with NO opinion about how a\n// toast looks or where the events come from. The app supplies both —\n// `subscribe` (its event bus) and `render` (its own toast component from the\n// forkable component library). That is what keeps this forge-owned file\n// independent of files the app is free to restyle, rename, or delete.\nimport {\n useCallback,\n useEffect,\n useMemo,\n useState,\n type ReactElement,\n type ReactNode,\n} from \"react\";\n\nexport type RuntimeToastVariant = \"success\" | \"error\" | \"warning\" | \"info\";\n\n/** A toast as emitted — no identity yet; the host assigns one. */\nexport interface RuntimeToastPayload {\n message: string;\n variant?: RuntimeToastVariant;\n duration?: number;\n}\n\n/** A toast the host is currently holding. */\nexport interface RuntimeToast extends RuntimeToastPayload {\n id: string;\n}\n\n/** What the host hands the app so the app can push events into it. */\nexport interface RuntimeToastSink {\n /** Enqueue a toast. */\n onShow: (toast: RuntimeToastPayload) => void;\n /** Dismiss one toast by id, or all of them when id is omitted. */\n onDismiss: (id?: string) => void;\n}\n\nexport interface RuntimeToastHostProps {\n /**\n * Wire the app's event source to the host: called with the sink, returns an\n * unsubscribe function. Memoize it (useCallback) — the host re-subscribes\n * whenever its identity changes.\n */\n subscribe: (sink: RuntimeToastSink) => () => void;\n /**\n * Render the app's own toast presentation. The runtime owns the queue; the\n * component library owns the markup.\n */\n render: (props: {\n toasts: RuntimeToast[];\n onDismiss: (id: string) => void;\n }) => ReactNode;\n}\n\nlet seq = 0;\nfunction nextId(): string {\n seq += 1;\n return `t${seq}-${Date.now()}`;\n}\n\nexport function RuntimeToastHost({\n subscribe,\n render,\n}: RuntimeToastHostProps): ReactElement {\n const [toasts, setToasts] = useState<RuntimeToast[]>([]);\n\n const dismiss = useCallback((id: string) => {\n setToasts((prev) => prev.filter((t) => t.id !== id));\n }, []);\n\n // Stable across renders: every update is functional, so the sink never\n // needs to close over the current toast list.\n const sink = useMemo<RuntimeToastSink>(\n () => ({\n onShow: (toast) => {\n setToasts((prev) => [...prev, { ...toast, id: nextId() }]);\n },\n onDismiss: (id) => {\n setToasts((prev) => (id ? prev.filter((t) => t.id !== id) : []));\n },\n }),\n [],\n );\n\n useEffect(() => subscribe(sink), [subscribe, sink]);\n\n return <>{render({ toasts, onDismiss: dismiss })}</>;\n}\n"]}
|
package/dist/trace.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Matches a well-formed W3C traceparent: version-traceid-spanid-flags. */
|
|
2
|
+
export declare const TRACEPARENT_RE: RegExp;
|
|
3
|
+
/** Override the sampled flag on synthesized traceparents. Default true. */
|
|
4
|
+
export declare function setTraceSampled(sampled: boolean): void;
|
|
5
|
+
/**
|
|
6
|
+
* freshTraceparent synthesizes a valid W3C traceparent. The trace-flags byte
|
|
7
|
+
* reflects the configurable sampled flag (setTraceSampled; default 01 =
|
|
8
|
+
* sampled). Trace/span ids are re-rolled on the astronomically-unlikely
|
|
9
|
+
* all-zero draw (the spec forbids all-zero ids).
|
|
10
|
+
*/
|
|
11
|
+
export declare function freshTraceparent(): string;
|
|
12
|
+
/**
|
|
13
|
+
* injectTraceContext writes a `traceparent` (and any accompanying W3C
|
|
14
|
+
* context headers, e.g. `tracestate`) onto the outgoing request headers.
|
|
15
|
+
*
|
|
16
|
+
* Prefers an active OpenTelemetry span so a fully-instrumented app stitches
|
|
17
|
+
* the browser and backend spans into one trace; otherwise falls back to a
|
|
18
|
+
* synthesized traceparent so propagation ALWAYS happens. Idempotent per
|
|
19
|
+
* request — safe to call once per attempt.
|
|
20
|
+
*/
|
|
21
|
+
export declare function injectTraceContext(header: Headers): void;
|
package/dist/trace.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.
|
|
2
|
+
//
|
|
3
|
+
// W3C trace-context propagation for the browser.
|
|
4
|
+
//
|
|
5
|
+
// The headline capability: EVERY Connect call carries a valid `traceparent`
|
|
6
|
+
// header, so the backend (which runs otelconnect + otelhttp with a
|
|
7
|
+
// TraceContext propagator) joins the SAME distributed trace. This works
|
|
8
|
+
// WITHOUT any OTLP collector configured — traceparent generation is
|
|
9
|
+
// decoupled from span export.
|
|
10
|
+
//
|
|
11
|
+
// - If the app opted into full OpenTelemetry tracing (src/lib/otel.ts
|
|
12
|
+
// ran initTelemetry with an endpoint, registering a WebTracerProvider),
|
|
13
|
+
// there is an active recording span; we inject ITS context so the
|
|
14
|
+
// browser span and the server span share one trace id.
|
|
15
|
+
// - Otherwise we synthesize a fresh, valid W3C traceparent from a CSPRNG.
|
|
16
|
+
// The backend still roots a trace we can correlate this request to, and
|
|
17
|
+
// no browser-side collector is required.
|
|
18
|
+
import { context, propagation } from "@opentelemetry/api";
|
|
19
|
+
/** Matches a well-formed W3C traceparent: version-traceid-spanid-flags. */
|
|
20
|
+
export const TRACEPARENT_RE = /^00-[0-9a-f]{32}-[0-9a-f]{16}-0[0-9a-f]$/;
|
|
21
|
+
const ZERO_TRACE_ID = "0".repeat(32);
|
|
22
|
+
const ZERO_SPAN_ID = "0".repeat(16);
|
|
23
|
+
/**
|
|
24
|
+
* Whether a SYNTHESIZED traceparent (the no-active-span fallback below)
|
|
25
|
+
* marks the trace as sampled/recorded.
|
|
26
|
+
*
|
|
27
|
+
* Default true — "correlation-safe": a browser-initiated request is always
|
|
28
|
+
* recorded, so it can be joined to its server span even when the backend
|
|
29
|
+
* uses head-based sampling (this module's whole promise is "every call is
|
|
30
|
+
* correlatable"). Flip it to false via setTraceSampled() to instead DEFER to
|
|
31
|
+
* the server's sampler — cheaper at high volume, but a synthesized trace the
|
|
32
|
+
* server samples out leaves that browser request uncorrelated. Only affects
|
|
33
|
+
* the synthesized path; when a real OpenTelemetry span is active, ITS
|
|
34
|
+
* sampling flag is propagated verbatim (see injectTraceContext).
|
|
35
|
+
*/
|
|
36
|
+
let traceSampled = true;
|
|
37
|
+
/** Override the sampled flag on synthesized traceparents. Default true. */
|
|
38
|
+
export function setTraceSampled(sampled) {
|
|
39
|
+
traceSampled = sampled;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* randomHex returns `bytes` random bytes as a lowercase hex string using a
|
|
43
|
+
* CSPRNG. Falls back to Math.random only if Web Crypto is unavailable (old
|
|
44
|
+
* embedded webviews); the fallback is fine for a correlation id that carries
|
|
45
|
+
* no security weight.
|
|
46
|
+
*/
|
|
47
|
+
function randomHex(bytes) {
|
|
48
|
+
const buf = new Uint8Array(bytes);
|
|
49
|
+
const c = globalThis.crypto;
|
|
50
|
+
if (c && typeof c.getRandomValues === "function") {
|
|
51
|
+
c.getRandomValues(buf);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
for (let i = 0; i < bytes; i++) {
|
|
55
|
+
buf[i] = Math.floor(Math.random() * 256);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
let out = "";
|
|
59
|
+
for (const b of buf) {
|
|
60
|
+
out += b.toString(16).padStart(2, "0");
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* freshTraceparent synthesizes a valid W3C traceparent. The trace-flags byte
|
|
66
|
+
* reflects the configurable sampled flag (setTraceSampled; default 01 =
|
|
67
|
+
* sampled). Trace/span ids are re-rolled on the astronomically-unlikely
|
|
68
|
+
* all-zero draw (the spec forbids all-zero ids).
|
|
69
|
+
*/
|
|
70
|
+
export function freshTraceparent() {
|
|
71
|
+
let traceId = randomHex(16);
|
|
72
|
+
while (traceId === ZERO_TRACE_ID) {
|
|
73
|
+
traceId = randomHex(16);
|
|
74
|
+
}
|
|
75
|
+
let spanId = randomHex(8);
|
|
76
|
+
while (spanId === ZERO_SPAN_ID) {
|
|
77
|
+
spanId = randomHex(8);
|
|
78
|
+
}
|
|
79
|
+
const flags = traceSampled ? "01" : "00";
|
|
80
|
+
return `00-${traceId}-${spanId}-${flags}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* injectTraceContext writes a `traceparent` (and any accompanying W3C
|
|
84
|
+
* context headers, e.g. `tracestate`) onto the outgoing request headers.
|
|
85
|
+
*
|
|
86
|
+
* Prefers an active OpenTelemetry span so a fully-instrumented app stitches
|
|
87
|
+
* the browser and backend spans into one trace; otherwise falls back to a
|
|
88
|
+
* synthesized traceparent so propagation ALWAYS happens. Idempotent per
|
|
89
|
+
* request — safe to call once per attempt.
|
|
90
|
+
*/
|
|
91
|
+
export function injectTraceContext(header) {
|
|
92
|
+
const carrier = {};
|
|
93
|
+
propagation.inject(context.active(), carrier, {
|
|
94
|
+
set(c, k, v) {
|
|
95
|
+
c[k] = String(v);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
if (carrier.traceparent && TRACEPARENT_RE.test(carrier.traceparent)) {
|
|
99
|
+
for (const [k, v] of Object.entries(carrier)) {
|
|
100
|
+
header.set(k, v);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
header.set("traceparent", freshTraceparent());
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=trace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.js","sourceRoot":"","sources":["../src/trace.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// W3C trace-context propagation for the browser.\n//\n// The headline capability: EVERY Connect call carries a valid `traceparent`\n// header, so the backend (which runs otelconnect + otelhttp with a\n// TraceContext propagator) joins the SAME distributed trace. This works\n// WITHOUT any OTLP collector configured — traceparent generation is\n// decoupled from span export.\n//\n// - If the app opted into full OpenTelemetry tracing (src/lib/otel.ts\n// ran initTelemetry with an endpoint, registering a WebTracerProvider),\n// there is an active recording span; we inject ITS context so the\n// browser span and the server span share one trace id.\n// - Otherwise we synthesize a fresh, valid W3C traceparent from a CSPRNG.\n// The backend still roots a trace we can correlate this request to, and\n// no browser-side collector is required.\nimport { context, propagation } from \"@opentelemetry/api\";\n\n/** Matches a well-formed W3C traceparent: version-traceid-spanid-flags. */\nexport const TRACEPARENT_RE = /^00-[0-9a-f]{32}-[0-9a-f]{16}-0[0-9a-f]$/;\n\nconst ZERO_TRACE_ID = \"0\".repeat(32);\nconst ZERO_SPAN_ID = \"0\".repeat(16);\n\n/**\n * Whether a SYNTHESIZED traceparent (the no-active-span fallback below)\n * marks the trace as sampled/recorded.\n *\n * Default true — \"correlation-safe\": a browser-initiated request is always\n * recorded, so it can be joined to its server span even when the backend\n * uses head-based sampling (this module's whole promise is \"every call is\n * correlatable\"). Flip it to false via setTraceSampled() to instead DEFER to\n * the server's sampler — cheaper at high volume, but a synthesized trace the\n * server samples out leaves that browser request uncorrelated. Only affects\n * the synthesized path; when a real OpenTelemetry span is active, ITS\n * sampling flag is propagated verbatim (see injectTraceContext).\n */\nlet traceSampled = true;\n\n/** Override the sampled flag on synthesized traceparents. Default true. */\nexport function setTraceSampled(sampled: boolean): void {\n traceSampled = sampled;\n}\n\n/**\n * randomHex returns `bytes` random bytes as a lowercase hex string using a\n * CSPRNG. Falls back to Math.random only if Web Crypto is unavailable (old\n * embedded webviews); the fallback is fine for a correlation id that carries\n * no security weight.\n */\nfunction randomHex(bytes: number): string {\n const buf = new Uint8Array(bytes);\n const c = globalThis.crypto;\n if (c && typeof c.getRandomValues === \"function\") {\n c.getRandomValues(buf);\n } else {\n for (let i = 0; i < bytes; i++) {\n buf[i] = Math.floor(Math.random() * 256);\n }\n }\n let out = \"\";\n for (const b of buf) {\n out += b.toString(16).padStart(2, \"0\");\n }\n return out;\n}\n\n/**\n * freshTraceparent synthesizes a valid W3C traceparent. The trace-flags byte\n * reflects the configurable sampled flag (setTraceSampled; default 01 =\n * sampled). Trace/span ids are re-rolled on the astronomically-unlikely\n * all-zero draw (the spec forbids all-zero ids).\n */\nexport function freshTraceparent(): string {\n let traceId = randomHex(16);\n while (traceId === ZERO_TRACE_ID) {\n traceId = randomHex(16);\n }\n let spanId = randomHex(8);\n while (spanId === ZERO_SPAN_ID) {\n spanId = randomHex(8);\n }\n const flags = traceSampled ? \"01\" : \"00\";\n return `00-${traceId}-${spanId}-${flags}`;\n}\n\n/**\n * injectTraceContext writes a `traceparent` (and any accompanying W3C\n * context headers, e.g. `tracestate`) onto the outgoing request headers.\n *\n * Prefers an active OpenTelemetry span so a fully-instrumented app stitches\n * the browser and backend spans into one trace; otherwise falls back to a\n * synthesized traceparent so propagation ALWAYS happens. Idempotent per\n * request — safe to call once per attempt.\n */\nexport function injectTraceContext(header: Headers): void {\n const carrier: Record<string, string> = {};\n propagation.inject(context.active(), carrier, {\n set(c, k, v) {\n c[k] = String(v);\n },\n });\n\n if (carrier.traceparent && TRACEPARENT_RE.test(carrier.traceparent)) {\n for (const [k, v] of Object.entries(carrier)) {\n header.set(k, v);\n }\n return;\n }\n\n header.set(\"traceparent\", freshTraceparent());\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "Legacy-resolver shim for the '@reliantlabs/forge-web-runtime/interceptors' subpath. Resolvers that read package.json 'exports' never look here. Resolvers that do NOT — TypeScript moduleResolution node10 and Metro with unstable_enablePackageExports off, which is what Expo SDK 52 / React Native 0.76 ship — resolve the subpath as a real directory and read the fields below. Both routes land on the same BUILD OUTPUT under dist/, never on src/: a consumer must never typecheck this package's sources against its own React. See README.md, 'What this package publishes' and 'The DOM-free interceptors entry point'.",
|
|
3
|
+
"types": "../dist/interceptors.d.ts",
|
|
4
|
+
"main": "../dist/interceptors.js",
|
|
5
|
+
"react-native": "../dist/interceptors.js",
|
|
6
|
+
"sideEffects": false
|
|
7
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reliantlabs/forge-web-runtime",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "The forge frontend runtime: Connect interceptor stack, session, error boundary, toast queue, <Resource> container, W3C trace propagation and client RUM. The web twin of forge/pkg.",
|
|
5
|
+
"license": "BUSL-1.1",
|
|
6
|
+
"//publishConfig note": "A SCOPED package defaults to restricted. Restricted breaks `npm install` for anyone outside the org — including every machine that scaffolds a project with a released forge, which resolves this package from the registry by semver range. Declaring access here means a publish cannot accidentally ship private by omitting --access public.",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"interceptors",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./interceptors": {
|
|
25
|
+
"types": "./dist/interceptors.d.ts",
|
|
26
|
+
"default": "./dist/interceptors.js"
|
|
27
|
+
},
|
|
28
|
+
"./mock-transport": {
|
|
29
|
+
"types": "./dist/mock-transport.d.ts",
|
|
30
|
+
"default": "./dist/mock-transport.js"
|
|
31
|
+
},
|
|
32
|
+
"./otel": {
|
|
33
|
+
"types": "./dist/otel.d.ts",
|
|
34
|
+
"default": "./dist/otel.js"
|
|
35
|
+
},
|
|
36
|
+
"./service-hooks": {
|
|
37
|
+
"types": "./dist/service-hooks.d.ts",
|
|
38
|
+
"default": "./dist/service-hooks.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.react18.json --noEmit",
|
|
44
|
+
"build": "node scripts/build.mjs",
|
|
45
|
+
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
46
|
+
"prepare": "node scripts/build.mjs",
|
|
47
|
+
"pretest": "node scripts/build.mjs",
|
|
48
|
+
"test": "vitest run"
|
|
49
|
+
},
|
|
50
|
+
"// peerDependencies note": "@opentelemetry/api is REQUIRED (trace.ts propagates traceparent on every RPC — always on, no collector). The eight SDK packages below it are OPTIONAL: they are imported only by the './otel' subpath, which only the Next.js scaffold reaches. A Vite-SPA or React-Native frontend installs @opentelemetry/api alone and imports the barrel, and must not be told it is missing dependencies it will never resolve.",
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@bufbuild/protobuf": "^2.5.0",
|
|
53
|
+
"@connectrpc/connect": "^2.0.0",
|
|
54
|
+
"@opentelemetry/api": "^1.9.0",
|
|
55
|
+
"@opentelemetry/auto-instrumentations-web": "^0.54.0",
|
|
56
|
+
"@opentelemetry/core": "^2.0.0",
|
|
57
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
|
|
58
|
+
"@opentelemetry/instrumentation": "^0.200.0",
|
|
59
|
+
"@opentelemetry/resources": "^2.0.0",
|
|
60
|
+
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
|
61
|
+
"@opentelemetry/sdk-trace-web": "^2.0.0",
|
|
62
|
+
"@opentelemetry/semantic-conventions": "^1.28.0",
|
|
63
|
+
"@tanstack/react-query": "^5.59.0",
|
|
64
|
+
"react": "^18.3.0 || ^19.0.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"@tanstack/react-query": { "optional": true },
|
|
68
|
+
"@opentelemetry/auto-instrumentations-web": { "optional": true },
|
|
69
|
+
"@opentelemetry/core": { "optional": true },
|
|
70
|
+
"@opentelemetry/exporter-trace-otlp-http": { "optional": true },
|
|
71
|
+
"@opentelemetry/instrumentation": { "optional": true },
|
|
72
|
+
"@opentelemetry/resources": { "optional": true },
|
|
73
|
+
"@opentelemetry/sdk-trace-base": { "optional": true },
|
|
74
|
+
"@opentelemetry/sdk-trace-web": { "optional": true },
|
|
75
|
+
"@opentelemetry/semantic-conventions": { "optional": true }
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@bufbuild/protobuf": "^2.5.0",
|
|
79
|
+
"@connectrpc/connect": "^2.0.0",
|
|
80
|
+
"@opentelemetry/api": "^1.9.0",
|
|
81
|
+
"@opentelemetry/auto-instrumentations-web": "^0.54.0",
|
|
82
|
+
"@opentelemetry/core": "^2.0.0",
|
|
83
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
|
|
84
|
+
"@opentelemetry/instrumentation": "^0.200.0",
|
|
85
|
+
"@opentelemetry/resources": "^2.0.0",
|
|
86
|
+
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
|
87
|
+
"@opentelemetry/sdk-trace-web": "^2.0.0",
|
|
88
|
+
"@opentelemetry/semantic-conventions": "^1.28.0",
|
|
89
|
+
"@tanstack/react-query": "^5.59.0",
|
|
90
|
+
"@types/node": "^22.14.0",
|
|
91
|
+
"@types/react": "^19.1.0",
|
|
92
|
+
"@types/react-18": "npm:@types/react@~18.3.0",
|
|
93
|
+
"react": "^19.1.0",
|
|
94
|
+
"typescript": "^5.8.0",
|
|
95
|
+
"vitest": "^3.1.0"
|
|
96
|
+
},
|
|
97
|
+
"engines": {
|
|
98
|
+
"node": ">=22"
|
|
99
|
+
}
|
|
100
|
+
}
|