@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,115 @@
|
|
|
1
|
+
import { type DescMessage } from "@bufbuild/protobuf";
|
|
2
|
+
import { type Transport } from "@connectrpc/connect";
|
|
3
|
+
/**
|
|
4
|
+
* The shape the generated scenario files satisfy. Structural on purpose: the
|
|
5
|
+
* project's own `Scenario` type (src/mocks/scenario-types.ts) carries a
|
|
6
|
+
* richer, per-RPC-typed `handlers` map, and this is the subset the engine
|
|
7
|
+
* dispatches through.
|
|
8
|
+
*/
|
|
9
|
+
export interface MockScenario {
|
|
10
|
+
name: string;
|
|
11
|
+
/** Per-RPC overrides keyed by `${serviceTypeName}/${methodName}`. */
|
|
12
|
+
handlers: Record<string, ((req: never) => unknown) | undefined>;
|
|
13
|
+
/** Forward unmatched RPCs to the real backend (hybrid mode only). */
|
|
14
|
+
passthrough?: boolean;
|
|
15
|
+
/** Non-RPC side effects, run once before the first RPC fires. */
|
|
16
|
+
setup?: () => void;
|
|
17
|
+
}
|
|
18
|
+
/** The generated `src/mocks/scenarios` barrel, as the engine consumes it. */
|
|
19
|
+
export interface MockScenarioRegistry {
|
|
20
|
+
byName(name: string): MockScenario | undefined;
|
|
21
|
+
defaultScenario: MockScenario;
|
|
22
|
+
}
|
|
23
|
+
/** One CRUD arm of an entity's dispatch table. */
|
|
24
|
+
export interface MockListRpc {
|
|
25
|
+
rpc: string;
|
|
26
|
+
responseSchema: DescMessage;
|
|
27
|
+
/**
|
|
28
|
+
* The camelCase name of the list response's repeated field — the ACTUAL
|
|
29
|
+
* proto field (e.g. `keys`), not the camelCased entity plural.
|
|
30
|
+
*/
|
|
31
|
+
itemsField: string;
|
|
32
|
+
}
|
|
33
|
+
export interface MockGetRpc {
|
|
34
|
+
rpc: string;
|
|
35
|
+
responseSchema: DescMessage;
|
|
36
|
+
/** The camelCase field wrapping the entity on the response message. */
|
|
37
|
+
entityField: string;
|
|
38
|
+
}
|
|
39
|
+
export interface MockWriteRpc extends MockGetRpc {
|
|
40
|
+
/**
|
|
41
|
+
* The camelCase field wrapping the entity on the REQUEST message. AIP-134
|
|
42
|
+
* requests nest the entity ({ item: {...}, updateMask }); the generated
|
|
43
|
+
* forms submit it flattened. Both are accepted.
|
|
44
|
+
*/
|
|
45
|
+
requestField: string;
|
|
46
|
+
}
|
|
47
|
+
export interface MockDeleteRpc {
|
|
48
|
+
rpc: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Everything the engine needs to serve one entity's CRUD quintet from
|
|
52
|
+
* fixtures. Generated into `src/lib/mock-transport.ts` from the project's
|
|
53
|
+
* proto descriptors.
|
|
54
|
+
*/
|
|
55
|
+
export interface MockEntityDescriptor {
|
|
56
|
+
/** Fully-qualified proto service name, e.g. "demo.v1.ClinicService". */
|
|
57
|
+
service: string;
|
|
58
|
+
/** Lowercase entity name, used verbatim in the NotFound message. */
|
|
59
|
+
label: string;
|
|
60
|
+
/**
|
|
61
|
+
* The camelCase PRIMARY-KEY field. The session store keys records by this
|
|
62
|
+
* field; hardcoding "id" breaks every surrogate-PK entity (a
|
|
63
|
+
* `usage_event_id` PK projects to a message with no `id` field).
|
|
64
|
+
*/
|
|
65
|
+
pkField: string;
|
|
66
|
+
/** Deterministic seed rows — the same values the dev database is seeded with. */
|
|
67
|
+
fixtures: readonly unknown[];
|
|
68
|
+
/** The entity message schema. Required by the create/update write paths. */
|
|
69
|
+
entitySchema?: DescMessage;
|
|
70
|
+
list?: MockListRpc;
|
|
71
|
+
get?: MockGetRpc;
|
|
72
|
+
create?: MockWriteRpc;
|
|
73
|
+
update?: MockWriteRpc;
|
|
74
|
+
delete?: MockDeleteRpc;
|
|
75
|
+
}
|
|
76
|
+
export interface CreateMockTransportOptions {
|
|
77
|
+
/** The resolved scenario — see {@link resolveActiveScenario}. */
|
|
78
|
+
scenario: MockScenario;
|
|
79
|
+
/** Per-entity fixture dispatch. Omit for a scenario-only project. */
|
|
80
|
+
entities?: readonly MockEntityDescriptor[];
|
|
81
|
+
/**
|
|
82
|
+
* Optional real Connect transport. When the active scenario declares
|
|
83
|
+
* `passthrough: true`, any RPC not matched by a scenario handler is
|
|
84
|
+
* forwarded here instead of falling through to the fixtures.
|
|
85
|
+
*/
|
|
86
|
+
fallback?: Transport;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the scenario the app should run under, and run its `setup()` once.
|
|
90
|
+
*
|
|
91
|
+
* Call this at MODULE SCOPE (the generated `src/lib/mock-transport.ts` does).
|
|
92
|
+
* Reading the URL once — rather than on every RPC — means navigating away
|
|
93
|
+
* from `?scenario=` keeps the scenario active until a full reload, matching
|
|
94
|
+
* the agent-driven flow where the URL is the single source of truth.
|
|
95
|
+
*
|
|
96
|
+
* `setup()` is for non-RPC state (localStorage flags, sessionStorage). It is
|
|
97
|
+
* synchronous and makes no network calls.
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveActiveScenario(registry: MockScenarioRegistry): MockScenario;
|
|
100
|
+
/**
|
|
101
|
+
* Normalize whatever a streaming scenario handler returns into an
|
|
102
|
+
* AsyncIterable<unknown>. Accepts arrays, iterables, async iterables, and
|
|
103
|
+
* promises that resolve to any of the above. Single (non-iterable) values
|
|
104
|
+
* are wrapped as a one-element stream — handy for handlers that conceptually
|
|
105
|
+
* yield once.
|
|
106
|
+
*/
|
|
107
|
+
export declare function toAsyncIterable(value: unknown): AsyncIterable<unknown>;
|
|
108
|
+
/**
|
|
109
|
+
* Create the mock transport.
|
|
110
|
+
*
|
|
111
|
+
* The generated `src/lib/mock-transport.ts` supplies the resolved scenario
|
|
112
|
+
* and the project's entity descriptors; `connect.ts` supplies the optional
|
|
113
|
+
* real transport when running in hybrid mode.
|
|
114
|
+
*/
|
|
115
|
+
export declare function createMockTransport(options: CreateMockTransportOptions): Transport;
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.
|
|
2
|
+
//
|
|
3
|
+
// The mock transport ENGINE. A generated frontend used to carry this entire
|
|
4
|
+
// pipeline as ~300 lines of Tier-1 scaffold; what varies per project is only
|
|
5
|
+
// the table of entities to dispatch on, so the engine lives here and the
|
|
6
|
+
// project keeps a declarative descriptor list.
|
|
7
|
+
//
|
|
8
|
+
// Dispatch order (unchanged from the scaffolded original):
|
|
9
|
+
//
|
|
10
|
+
// 1. A scenario selected via `?scenario=<name>` (or the auto-generated
|
|
11
|
+
// `default` scenario) gets first crack at every unary RPC. Handlers
|
|
12
|
+
// keyed by `${serviceTypeName}/${methodName}` return typed proto
|
|
13
|
+
// messages and short-circuit the rest of the pipeline.
|
|
14
|
+
// 2. If the active scenario has `passthrough: true` AND a fallback
|
|
15
|
+
// transport was supplied (hybrid mode — VITE_MOCK_API=hybrid /
|
|
16
|
+
// NEXT_PUBLIC_MOCK_API=hybrid), unmatched RPCs are forwarded to the
|
|
17
|
+
// real backend. This lets one scenario stub a single endpoint while
|
|
18
|
+
// everything else exercises the live server.
|
|
19
|
+
// 3. Otherwise RPCs not overridden fall through to the deterministic
|
|
20
|
+
// per-entity fixtures described by the entity descriptors.
|
|
21
|
+
// 4. Anything else rejects with Code.Unimplemented so callers see a
|
|
22
|
+
// clear error instead of a silent empty response.
|
|
23
|
+
//
|
|
24
|
+
// Imported through the "@reliantlabs/forge-web-runtime/mock-transport" subpath,
|
|
25
|
+
// never the barrel: a production bundle must be able to shake the fixtures
|
|
26
|
+
// and this engine out entirely, which is why the generated `connect.ts`
|
|
27
|
+
// reaches its own shim through a dynamic `require()` gated on the mock-mode
|
|
28
|
+
// env var.
|
|
29
|
+
import { create } from "@bufbuild/protobuf";
|
|
30
|
+
import { Code, ConnectError, } from "@connectrpc/connect";
|
|
31
|
+
/**
|
|
32
|
+
* Mutable session stores, keyed by descriptor identity.
|
|
33
|
+
*
|
|
34
|
+
* Create/Update/Delete round-trip within one browser session (a created row
|
|
35
|
+
* appears in the next List; a deleted one disappears) and reset to the
|
|
36
|
+
* deterministic fixtures on reload. The scaffolded original got that from
|
|
37
|
+
* module-scope `const store = new Map(...)`; keying off the descriptor
|
|
38
|
+
* object preserves it — a module-level descriptor array hands back the same
|
|
39
|
+
* store on every call, so building the transport twice (hybrid mode, a test
|
|
40
|
+
* swapping transports) does not silently fork the data.
|
|
41
|
+
*/
|
|
42
|
+
const stores = new WeakMap();
|
|
43
|
+
function storeFor(entity) {
|
|
44
|
+
let store = stores.get(entity);
|
|
45
|
+
if (!store) {
|
|
46
|
+
store = new Map(entity.fixtures.map((row) => [
|
|
47
|
+
String(row[entity.pkField]),
|
|
48
|
+
row,
|
|
49
|
+
]));
|
|
50
|
+
stores.set(entity, store);
|
|
51
|
+
}
|
|
52
|
+
return store;
|
|
53
|
+
}
|
|
54
|
+
/** Scenarios whose `setup()` has already run. */
|
|
55
|
+
const initialized = new WeakSet();
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the scenario the app should run under, and run its `setup()` once.
|
|
58
|
+
*
|
|
59
|
+
* Call this at MODULE SCOPE (the generated `src/lib/mock-transport.ts` does).
|
|
60
|
+
* Reading the URL once — rather than on every RPC — means navigating away
|
|
61
|
+
* from `?scenario=` keeps the scenario active until a full reload, matching
|
|
62
|
+
* the agent-driven flow where the URL is the single source of truth.
|
|
63
|
+
*
|
|
64
|
+
* `setup()` is for non-RPC state (localStorage flags, sessionStorage). It is
|
|
65
|
+
* synchronous and makes no network calls.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveActiveScenario(registry) {
|
|
68
|
+
const requested = typeof globalThis !== "undefined" && globalThis.location
|
|
69
|
+
? new URLSearchParams(globalThis.location.search).get("scenario")
|
|
70
|
+
: null;
|
|
71
|
+
// A ternary, not `&&`: with `??` alone the empty-string falsy branch leaks
|
|
72
|
+
// '' into the inferred union, and '' survives `??` (which only replaces
|
|
73
|
+
// null/undefined), so downstream access on .setup / .handlers would fail
|
|
74
|
+
// under strict tsc.
|
|
75
|
+
const active = (requested ? registry.byName(requested) : undefined) ??
|
|
76
|
+
registry.defaultScenario;
|
|
77
|
+
if (!initialized.has(active)) {
|
|
78
|
+
initialized.add(active);
|
|
79
|
+
active.setup?.();
|
|
80
|
+
}
|
|
81
|
+
return active;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Normalize whatever a streaming scenario handler returns into an
|
|
85
|
+
* AsyncIterable<unknown>. Accepts arrays, iterables, async iterables, and
|
|
86
|
+
* promises that resolve to any of the above. Single (non-iterable) values
|
|
87
|
+
* are wrapped as a one-element stream — handy for handlers that conceptually
|
|
88
|
+
* yield once.
|
|
89
|
+
*/
|
|
90
|
+
export async function* toAsyncIterable(value) {
|
|
91
|
+
const resolved = await Promise.resolve(value);
|
|
92
|
+
if (resolved == null)
|
|
93
|
+
return;
|
|
94
|
+
// AsyncIterable
|
|
95
|
+
if (typeof resolved[Symbol.asyncIterator] === "function") {
|
|
96
|
+
for await (const m of resolved)
|
|
97
|
+
yield m;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Iterable (including arrays)
|
|
101
|
+
if (typeof resolved[Symbol.iterator] ===
|
|
102
|
+
"function") {
|
|
103
|
+
for (const m of resolved)
|
|
104
|
+
yield m;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Single value
|
|
108
|
+
yield resolved;
|
|
109
|
+
}
|
|
110
|
+
function makeUnaryResponse(method, message) {
|
|
111
|
+
return {
|
|
112
|
+
service: method.parent,
|
|
113
|
+
method: method,
|
|
114
|
+
stream: false,
|
|
115
|
+
header: new Headers(),
|
|
116
|
+
message: message,
|
|
117
|
+
trailer: new Headers(),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Compile the entity descriptors into a flat `${service}/${rpc}` → handler
|
|
122
|
+
* map. Building it once per transport keeps per-RPC dispatch to a single
|
|
123
|
+
* lookup, exactly like the generated `switch` it replaces.
|
|
124
|
+
*/
|
|
125
|
+
function buildFixtureTable(entities) {
|
|
126
|
+
const table = new Map();
|
|
127
|
+
for (const entity of entities) {
|
|
128
|
+
const pk = entity.pkField;
|
|
129
|
+
const key = (rpc) => `${entity.service}/${rpc}`;
|
|
130
|
+
if (entity.list) {
|
|
131
|
+
const { rpc, responseSchema, itemsField } = entity.list;
|
|
132
|
+
table.set(key(rpc), () => create(responseSchema, {
|
|
133
|
+
[itemsField]: Array.from(storeFor(entity).values()),
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
if (entity.get) {
|
|
137
|
+
const { rpc, responseSchema, entityField } = entity.get;
|
|
138
|
+
table.set(key(rpc), (input) => {
|
|
139
|
+
// Read the key off the SAME field the store is keyed by, not a
|
|
140
|
+
// hardcoded `id` — a surrogate-PK entity would otherwise look up
|
|
141
|
+
// String(undefined) and miss every record.
|
|
142
|
+
const req = input;
|
|
143
|
+
const found = storeFor(entity).get(String(req?.[pk]));
|
|
144
|
+
if (!found) {
|
|
145
|
+
// A miss is NotFound — exactly what the real backend returns.
|
|
146
|
+
// (Falling back to the first fixture made every detail page
|
|
147
|
+
// "work" against the wrong record and hid bad-id bugs.)
|
|
148
|
+
throw new ConnectError(`${entity.label} ${String(req?.[pk])} not found`, Code.NotFound);
|
|
149
|
+
}
|
|
150
|
+
return create(responseSchema, {
|
|
151
|
+
[entityField]: found,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (entity.create && entity.entitySchema) {
|
|
156
|
+
const { rpc, responseSchema, entityField, requestField } = entity.create;
|
|
157
|
+
const entitySchema = entity.entitySchema;
|
|
158
|
+
table.set(key(rpc), (input) => {
|
|
159
|
+
// Create requests carry the entity fields flattened (that's how the
|
|
160
|
+
// generated form submits); a nested { <entity>: {...} } payload is
|
|
161
|
+
// accepted too.
|
|
162
|
+
const req = (input ?? {});
|
|
163
|
+
const init = {
|
|
164
|
+
...(req[requestField] ?? req),
|
|
165
|
+
};
|
|
166
|
+
delete init.$typeName;
|
|
167
|
+
// Mint the primary key on the PK field the store is keyed by — for a
|
|
168
|
+
// surrogate-PK entity this is NOT `id`, and writing a generated `id`
|
|
169
|
+
// would leave the store key undefined so the row never round-trips.
|
|
170
|
+
const id = typeof init[pk] === "string" && init[pk]
|
|
171
|
+
? init[pk]
|
|
172
|
+
: crypto.randomUUID();
|
|
173
|
+
const created = create(entitySchema, {
|
|
174
|
+
...init,
|
|
175
|
+
[pk]: id,
|
|
176
|
+
});
|
|
177
|
+
storeFor(entity).set(String(id), created);
|
|
178
|
+
return create(responseSchema, {
|
|
179
|
+
[entityField]: created,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (entity.update && entity.entitySchema) {
|
|
184
|
+
const { rpc, responseSchema, entityField, requestField } = entity.update;
|
|
185
|
+
const entitySchema = entity.entitySchema;
|
|
186
|
+
table.set(key(rpc), (input) => {
|
|
187
|
+
const req = (input ?? {});
|
|
188
|
+
// AIP-134 requests wrap the entity ({ <entity>: {...}, updateMask })
|
|
189
|
+
// — the id lives inside the wrapper; flat requests carry it at the
|
|
190
|
+
// top level.
|
|
191
|
+
const patch = {
|
|
192
|
+
...(req[requestField] ?? req),
|
|
193
|
+
};
|
|
194
|
+
delete patch.$typeName;
|
|
195
|
+
const id = String(patch[pk] ?? req[pk] ?? "");
|
|
196
|
+
const store = storeFor(entity);
|
|
197
|
+
const existing = store.get(id);
|
|
198
|
+
if (!existing) {
|
|
199
|
+
throw new ConnectError(`${entity.label} ${id} not found`, Code.NotFound);
|
|
200
|
+
}
|
|
201
|
+
const updated = create(entitySchema, {
|
|
202
|
+
...existing,
|
|
203
|
+
...patch,
|
|
204
|
+
[pk]: id,
|
|
205
|
+
});
|
|
206
|
+
store.set(id, updated);
|
|
207
|
+
return create(responseSchema, {
|
|
208
|
+
[entityField]: updated,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (entity.delete) {
|
|
213
|
+
table.set(key(entity.delete.rpc), (input) => {
|
|
214
|
+
// Delete by the SAME field the store is keyed by.
|
|
215
|
+
const req = input;
|
|
216
|
+
storeFor(entity).delete(String(req?.[pk]));
|
|
217
|
+
// Delete returns an empty response — the standard CRUD shape.
|
|
218
|
+
return {};
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return table;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Create the mock transport.
|
|
226
|
+
*
|
|
227
|
+
* The generated `src/lib/mock-transport.ts` supplies the resolved scenario
|
|
228
|
+
* and the project's entity descriptors; `connect.ts` supplies the optional
|
|
229
|
+
* real transport when running in hybrid mode.
|
|
230
|
+
*/
|
|
231
|
+
export function createMockTransport(options) {
|
|
232
|
+
const { scenario, entities = [], fallback } = options;
|
|
233
|
+
const passthrough = scenario.passthrough === true && fallback != null;
|
|
234
|
+
const fixtures = buildFixtureTable(entities);
|
|
235
|
+
// Bind the object literal to a Transport-typed variable rather than
|
|
236
|
+
// casting at the return site. A trailing assertion cast does not propagate
|
|
237
|
+
// Connect's Transport signature backwards into the literal's method
|
|
238
|
+
// bodies, so under `strict` tsc every callback parameter on unary/stream
|
|
239
|
+
// errors with TS7006 ("implicitly has an 'any' type").
|
|
240
|
+
const transport = {
|
|
241
|
+
// Connect v2's Transport.unary signature is
|
|
242
|
+
// (method, signal, timeoutMs, header, input, contextValues)
|
|
243
|
+
// — method first, no separate service arg. `method.parent.typeName` is
|
|
244
|
+
// the fully-qualified service name; that is what handler keys and
|
|
245
|
+
// fixture keys are matched against.
|
|
246
|
+
async unary(method, signal, timeoutMs, header, input, contextValues) {
|
|
247
|
+
const key = `${method.parent.typeName}/${method.name}`;
|
|
248
|
+
// 1) Scenario overlay.
|
|
249
|
+
const handler = scenario.handlers[key];
|
|
250
|
+
if (handler) {
|
|
251
|
+
const result = await handler(input);
|
|
252
|
+
return makeUnaryResponse(method, result);
|
|
253
|
+
}
|
|
254
|
+
// 2) Passthrough: route to the real backend instead of fixtures.
|
|
255
|
+
// Skips fixtures entirely — in hybrid mode the whole point is
|
|
256
|
+
// "use real data for anything not explicitly stubbed".
|
|
257
|
+
if (passthrough) {
|
|
258
|
+
return fallback.unary(method, signal, timeoutMs, header, input, contextValues);
|
|
259
|
+
}
|
|
260
|
+
// 3) Base fixture dispatch, backed by the mutable per-entity stores so
|
|
261
|
+
// writes round-trip within the session. Misses are REAL errors
|
|
262
|
+
// (Code.NotFound), never a silent wrong-record fallback.
|
|
263
|
+
const fixture = fixtures.get(key);
|
|
264
|
+
if (fixture) {
|
|
265
|
+
return makeUnaryResponse(method, fixture(input));
|
|
266
|
+
}
|
|
267
|
+
// 4) Nothing matched. Surface a clear error instead of hanging.
|
|
268
|
+
return Promise.reject(new ConnectError(`mock-transport: no scenario handler or entity fixture for ${key}`, Code.Unimplemented));
|
|
269
|
+
},
|
|
270
|
+
// Streaming: a scenario handler can return an array, iterable, async
|
|
271
|
+
// iterable, or a Promise resolving to any of those. The transport adapts
|
|
272
|
+
// it into the AsyncIterable<Response> shape Connect expects. If no
|
|
273
|
+
// handler matches, reject with Unimplemented — base-fixture streaming is
|
|
274
|
+
// intentionally not served (there is no canonical "list of N messages"
|
|
275
|
+
// shape for an arbitrary streaming RPC).
|
|
276
|
+
//
|
|
277
|
+
// No explicit return-type annotation: the outer `const transport:
|
|
278
|
+
// Transport` binding lets tsc infer the per-callback signature from
|
|
279
|
+
// Connect's generic Transport.stream<I, O>. Annotating this as
|
|
280
|
+
// Promise<StreamResponse<never, never>> makes the passthrough branch
|
|
281
|
+
// ill-typed (TS2322) because fallback.stream returns StreamResponse<I,O>.
|
|
282
|
+
async stream(method, signal, timeoutMs, header, input, contextValues) {
|
|
283
|
+
const key = `${method.parent.typeName}/${method.name}`;
|
|
284
|
+
const handler = scenario.handlers[key];
|
|
285
|
+
if (!handler) {
|
|
286
|
+
// Passthrough mirrors the unary path: in hybrid mode, unmatched
|
|
287
|
+
// streaming RPCs go to the real backend.
|
|
288
|
+
if (passthrough) {
|
|
289
|
+
return fallback.stream(method, signal, timeoutMs, header, input, contextValues);
|
|
290
|
+
}
|
|
291
|
+
return Promise.reject(new ConnectError(`mock-transport: no scenario handler for streaming RPC ${key}`, Code.Unimplemented));
|
|
292
|
+
}
|
|
293
|
+
// Read the request stream eagerly into an array so the scenario handler
|
|
294
|
+
// sees the full client-streaming payload. For server-stream and
|
|
295
|
+
// unary-into-stream-out RPCs this is a single message.
|
|
296
|
+
const requestMessages = [];
|
|
297
|
+
try {
|
|
298
|
+
for await (const m of input) {
|
|
299
|
+
requestMessages.push(m);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// Synthetic clients sometimes pass a single value rather than an
|
|
304
|
+
// iterable; treat that as an empty request payload.
|
|
305
|
+
}
|
|
306
|
+
const handlerInput = requestMessages.length <= 1 ? requestMessages[0] : requestMessages;
|
|
307
|
+
const result = handler(handlerInput);
|
|
308
|
+
return {
|
|
309
|
+
service: method.parent,
|
|
310
|
+
method: method,
|
|
311
|
+
stream: true,
|
|
312
|
+
header: new Headers(),
|
|
313
|
+
message: toAsyncIterable(result),
|
|
314
|
+
trailer: new Headers(),
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
return transport;
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=mock-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-transport.js","sourceRoot":"","sources":["../src/mock-transport.ts"],"names":[],"mappings":"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","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// The mock transport ENGINE. A generated frontend used to carry this entire\n// pipeline as ~300 lines of Tier-1 scaffold; what varies per project is only\n// the table of entities to dispatch on, so the engine lives here and the\n// project keeps a declarative descriptor list.\n//\n// Dispatch order (unchanged from the scaffolded original):\n//\n// 1. A scenario selected via `?scenario=<name>` (or the auto-generated\n// `default` scenario) gets first crack at every unary RPC. Handlers\n// keyed by `${serviceTypeName}/${methodName}` return typed proto\n// messages and short-circuit the rest of the pipeline.\n// 2. If the active scenario has `passthrough: true` AND a fallback\n// transport was supplied (hybrid mode — VITE_MOCK_API=hybrid /\n// NEXT_PUBLIC_MOCK_API=hybrid), unmatched RPCs are forwarded to the\n// real backend. This lets one scenario stub a single endpoint while\n// everything else exercises the live server.\n// 3. Otherwise RPCs not overridden fall through to the deterministic\n// per-entity fixtures described by the entity descriptors.\n// 4. Anything else rejects with Code.Unimplemented so callers see a\n// clear error instead of a silent empty response.\n//\n// Imported through the \"@reliantlabs/forge-web-runtime/mock-transport\" subpath,\n// never the barrel: a production bundle must be able to shake the fixtures\n// and this engine out entirely, which is why the generated `connect.ts`\n// reaches its own shim through a dynamic `require()` gated on the mock-mode\n// env var.\nimport { create, type DescMessage, type MessageInitShape } from \"@bufbuild/protobuf\";\nimport {\n Code,\n ConnectError,\n type Transport,\n type UnaryResponse,\n} from \"@connectrpc/connect\";\n\n/**\n * The shape the generated scenario files satisfy. Structural on purpose: the\n * project's own `Scenario` type (src/mocks/scenario-types.ts) carries a\n * richer, per-RPC-typed `handlers` map, and this is the subset the engine\n * dispatches through.\n */\nexport interface MockScenario {\n name: string;\n /** Per-RPC overrides keyed by `${serviceTypeName}/${methodName}`. */\n handlers: Record<string, ((req: never) => unknown) | undefined>;\n /** Forward unmatched RPCs to the real backend (hybrid mode only). */\n passthrough?: boolean;\n /** Non-RPC side effects, run once before the first RPC fires. */\n setup?: () => void;\n}\n\n/** The generated `src/mocks/scenarios` barrel, as the engine consumes it. */\nexport interface MockScenarioRegistry {\n byName(name: string): MockScenario | undefined;\n defaultScenario: MockScenario;\n}\n\n/** One CRUD arm of an entity's dispatch table. */\nexport interface MockListRpc {\n rpc: string;\n responseSchema: DescMessage;\n /**\n * The camelCase name of the list response's repeated field — the ACTUAL\n * proto field (e.g. `keys`), not the camelCased entity plural.\n */\n itemsField: string;\n}\n\nexport interface MockGetRpc {\n rpc: string;\n responseSchema: DescMessage;\n /** The camelCase field wrapping the entity on the response message. */\n entityField: string;\n}\n\nexport interface MockWriteRpc extends MockGetRpc {\n /**\n * The camelCase field wrapping the entity on the REQUEST message. AIP-134\n * requests nest the entity ({ item: {...}, updateMask }); the generated\n * forms submit it flattened. Both are accepted.\n */\n requestField: string;\n}\n\nexport interface MockDeleteRpc {\n rpc: string;\n}\n\n/**\n * Everything the engine needs to serve one entity's CRUD quintet from\n * fixtures. Generated into `src/lib/mock-transport.ts` from the project's\n * proto descriptors.\n */\nexport interface MockEntityDescriptor {\n /** Fully-qualified proto service name, e.g. \"demo.v1.ClinicService\". */\n service: string;\n /** Lowercase entity name, used verbatim in the NotFound message. */\n label: string;\n /**\n * The camelCase PRIMARY-KEY field. The session store keys records by this\n * field; hardcoding \"id\" breaks every surrogate-PK entity (a\n * `usage_event_id` PK projects to a message with no `id` field).\n */\n pkField: string;\n /** Deterministic seed rows — the same values the dev database is seeded with. */\n fixtures: readonly unknown[];\n /** The entity message schema. Required by the create/update write paths. */\n entitySchema?: DescMessage;\n list?: MockListRpc;\n get?: MockGetRpc;\n create?: MockWriteRpc;\n update?: MockWriteRpc;\n delete?: MockDeleteRpc;\n}\n\nexport interface CreateMockTransportOptions {\n /** The resolved scenario — see {@link resolveActiveScenario}. */\n scenario: MockScenario;\n /** Per-entity fixture dispatch. Omit for a scenario-only project. */\n entities?: readonly MockEntityDescriptor[];\n /**\n * Optional real Connect transport. When the active scenario declares\n * `passthrough: true`, any RPC not matched by a scenario handler is\n * forwarded here instead of falling through to the fixtures.\n */\n fallback?: Transport;\n}\n\n/**\n * Mutable session stores, keyed by descriptor identity.\n *\n * Create/Update/Delete round-trip within one browser session (a created row\n * appears in the next List; a deleted one disappears) and reset to the\n * deterministic fixtures on reload. The scaffolded original got that from\n * module-scope `const store = new Map(...)`; keying off the descriptor\n * object preserves it — a module-level descriptor array hands back the same\n * store on every call, so building the transport twice (hybrid mode, a test\n * swapping transports) does not silently fork the data.\n */\nconst stores = new WeakMap<MockEntityDescriptor, Map<string, unknown>>();\n\nfunction storeFor(entity: MockEntityDescriptor): Map<string, unknown> {\n let store = stores.get(entity);\n if (!store) {\n store = new Map(\n entity.fixtures.map((row) => [\n String((row as Record<string, unknown>)[entity.pkField]),\n row,\n ]),\n );\n stores.set(entity, store);\n }\n return store;\n}\n\n/** Scenarios whose `setup()` has already run. */\nconst initialized = new WeakSet<MockScenario>();\n\n/**\n * Resolve the scenario the app should run under, and run its `setup()` once.\n *\n * Call this at MODULE SCOPE (the generated `src/lib/mock-transport.ts` does).\n * Reading the URL once — rather than on every RPC — means navigating away\n * from `?scenario=` keeps the scenario active until a full reload, matching\n * the agent-driven flow where the URL is the single source of truth.\n *\n * `setup()` is for non-RPC state (localStorage flags, sessionStorage). It is\n * synchronous and makes no network calls.\n */\nexport function resolveActiveScenario(\n registry: MockScenarioRegistry,\n): MockScenario {\n const requested =\n typeof globalThis !== \"undefined\" && globalThis.location\n ? new URLSearchParams(globalThis.location.search).get(\"scenario\")\n : null;\n // A ternary, not `&&`: with `??` alone the empty-string falsy branch leaks\n // '' into the inferred union, and '' survives `??` (which only replaces\n // null/undefined), so downstream access on .setup / .handlers would fail\n // under strict tsc.\n const active =\n (requested ? registry.byName(requested) : undefined) ??\n registry.defaultScenario;\n\n if (!initialized.has(active)) {\n initialized.add(active);\n active.setup?.();\n }\n return active;\n}\n\n/**\n * Normalize whatever a streaming scenario handler returns into an\n * AsyncIterable<unknown>. Accepts arrays, iterables, async iterables, and\n * promises that resolve to any of the above. Single (non-iterable) values\n * are wrapped as a one-element stream — handy for handlers that conceptually\n * yield once.\n */\nexport async function* toAsyncIterable(\n value: unknown,\n): AsyncIterable<unknown> {\n const resolved = await Promise.resolve(value);\n if (resolved == null) return;\n // AsyncIterable\n if (\n typeof (resolved as { [Symbol.asyncIterator]?: unknown })[\n Symbol.asyncIterator\n ] === \"function\"\n ) {\n for await (const m of resolved as AsyncIterable<unknown>) yield m;\n return;\n }\n // Iterable (including arrays)\n if (\n typeof (resolved as { [Symbol.iterator]?: unknown })[Symbol.iterator] ===\n \"function\"\n ) {\n for (const m of resolved as Iterable<unknown>) yield m;\n return;\n }\n // Single value\n yield resolved;\n}\n\nfunction makeUnaryResponse<T>(\n method: { name: string; parent: { typeName: string } },\n message: T,\n): UnaryResponse<never, never> {\n return {\n service: method.parent as never,\n method: method as never,\n stream: false,\n header: new Headers(),\n message: message as never,\n trailer: new Headers(),\n };\n}\n\n/** A fixture-backed handler for one RPC key. */\ntype FixtureHandler = (input: unknown) => unknown;\n\n/**\n * Compile the entity descriptors into a flat `${service}/${rpc}` → handler\n * map. Building it once per transport keeps per-RPC dispatch to a single\n * lookup, exactly like the generated `switch` it replaces.\n */\nfunction buildFixtureTable(\n entities: readonly MockEntityDescriptor[],\n): Map<string, FixtureHandler> {\n const table = new Map<string, FixtureHandler>();\n\n for (const entity of entities) {\n const pk = entity.pkField;\n const key = (rpc: string) => `${entity.service}/${rpc}`;\n\n if (entity.list) {\n const { rpc, responseSchema, itemsField } = entity.list;\n table.set(key(rpc), () =>\n create(responseSchema, {\n [itemsField]: Array.from(storeFor(entity).values()),\n } as MessageInitShape<DescMessage>),\n );\n }\n\n if (entity.get) {\n const { rpc, responseSchema, entityField } = entity.get;\n table.set(key(rpc), (input) => {\n // Read the key off the SAME field the store is keyed by, not a\n // hardcoded `id` — a surrogate-PK entity would otherwise look up\n // String(undefined) and miss every record.\n const req = input as Record<string, unknown> | undefined;\n const found = storeFor(entity).get(String(req?.[pk]));\n if (!found) {\n // A miss is NotFound — exactly what the real backend returns.\n // (Falling back to the first fixture made every detail page\n // \"work\" against the wrong record and hid bad-id bugs.)\n throw new ConnectError(\n `${entity.label} ${String(req?.[pk])} not found`,\n Code.NotFound,\n );\n }\n return create(responseSchema, {\n [entityField]: found,\n } as MessageInitShape<DescMessage>);\n });\n }\n\n if (entity.create && entity.entitySchema) {\n const { rpc, responseSchema, entityField, requestField } = entity.create;\n const entitySchema = entity.entitySchema;\n table.set(key(rpc), (input) => {\n // Create requests carry the entity fields flattened (that's how the\n // generated form submits); a nested { <entity>: {...} } payload is\n // accepted too.\n const req = (input ?? {}) as Record<string, unknown>;\n const init = {\n ...((req[requestField] ?? req) as Record<string, unknown>),\n };\n delete init.$typeName;\n // Mint the primary key on the PK field the store is keyed by — for a\n // surrogate-PK entity this is NOT `id`, and writing a generated `id`\n // would leave the store key undefined so the row never round-trips.\n const id =\n typeof init[pk] === \"string\" && init[pk]\n ? (init[pk] as string)\n : crypto.randomUUID();\n const created = create(entitySchema, {\n ...init,\n [pk]: id,\n } as MessageInitShape<DescMessage>);\n storeFor(entity).set(String(id), created);\n return create(responseSchema, {\n [entityField]: created,\n } as MessageInitShape<DescMessage>);\n });\n }\n\n if (entity.update && entity.entitySchema) {\n const { rpc, responseSchema, entityField, requestField } = entity.update;\n const entitySchema = entity.entitySchema;\n table.set(key(rpc), (input) => {\n const req = (input ?? {}) as Record<string, unknown>;\n // AIP-134 requests wrap the entity ({ <entity>: {...}, updateMask })\n // — the id lives inside the wrapper; flat requests carry it at the\n // top level.\n const patch = {\n ...((req[requestField] ?? req) as Record<string, unknown>),\n };\n delete patch.$typeName;\n const id = String(patch[pk] ?? req[pk] ?? \"\");\n const store = storeFor(entity);\n const existing = store.get(id);\n if (!existing) {\n throw new ConnectError(\n `${entity.label} ${id} not found`,\n Code.NotFound,\n );\n }\n const updated = create(entitySchema, {\n ...(existing as Record<string, unknown>),\n ...patch,\n [pk]: id,\n } as MessageInitShape<DescMessage>);\n store.set(id, updated);\n return create(responseSchema, {\n [entityField]: updated,\n } as MessageInitShape<DescMessage>);\n });\n }\n\n if (entity.delete) {\n table.set(key(entity.delete.rpc), (input) => {\n // Delete by the SAME field the store is keyed by.\n const req = input as Record<string, unknown> | undefined;\n storeFor(entity).delete(String(req?.[pk]));\n // Delete returns an empty response — the standard CRUD shape.\n return {};\n });\n }\n }\n\n return table;\n}\n\n/**\n * Create the mock transport.\n *\n * The generated `src/lib/mock-transport.ts` supplies the resolved scenario\n * and the project's entity descriptors; `connect.ts` supplies the optional\n * real transport when running in hybrid mode.\n */\nexport function createMockTransport(\n options: CreateMockTransportOptions,\n): Transport {\n const { scenario, entities = [], fallback } = options;\n const passthrough = scenario.passthrough === true && fallback != null;\n const fixtures = buildFixtureTable(entities);\n\n // Bind the object literal to a Transport-typed variable rather than\n // casting at the return site. A trailing assertion cast does not propagate\n // Connect's Transport signature backwards into the literal's method\n // bodies, so under `strict` tsc every callback parameter on unary/stream\n // errors with TS7006 (\"implicitly has an 'any' type\").\n const transport: Transport = {\n // Connect v2's Transport.unary signature is\n // (method, signal, timeoutMs, header, input, contextValues)\n // — method first, no separate service arg. `method.parent.typeName` is\n // the fully-qualified service name; that is what handler keys and\n // fixture keys are matched against.\n async unary(method, signal, timeoutMs, header, input, contextValues) {\n const key = `${method.parent.typeName}/${method.name}`;\n\n // 1) Scenario overlay.\n const handler = scenario.handlers[key];\n if (handler) {\n const result = await handler(input as never);\n return makeUnaryResponse(method, result);\n }\n\n // 2) Passthrough: route to the real backend instead of fixtures.\n // Skips fixtures entirely — in hybrid mode the whole point is\n // \"use real data for anything not explicitly stubbed\".\n if (passthrough) {\n return fallback!.unary(\n method,\n signal,\n timeoutMs,\n header,\n input,\n contextValues,\n );\n }\n\n // 3) Base fixture dispatch, backed by the mutable per-entity stores so\n // writes round-trip within the session. Misses are REAL errors\n // (Code.NotFound), never a silent wrong-record fallback.\n const fixture = fixtures.get(key);\n if (fixture) {\n return makeUnaryResponse(method, fixture(input));\n }\n\n // 4) Nothing matched. Surface a clear error instead of hanging.\n return Promise.reject(\n new ConnectError(\n `mock-transport: no scenario handler or entity fixture for ${key}`,\n Code.Unimplemented,\n ),\n );\n },\n\n // Streaming: a scenario handler can return an array, iterable, async\n // iterable, or a Promise resolving to any of those. The transport adapts\n // it into the AsyncIterable<Response> shape Connect expects. If no\n // handler matches, reject with Unimplemented — base-fixture streaming is\n // intentionally not served (there is no canonical \"list of N messages\"\n // shape for an arbitrary streaming RPC).\n //\n // No explicit return-type annotation: the outer `const transport:\n // Transport` binding lets tsc infer the per-callback signature from\n // Connect's generic Transport.stream<I, O>. Annotating this as\n // Promise<StreamResponse<never, never>> makes the passthrough branch\n // ill-typed (TS2322) because fallback.stream returns StreamResponse<I,O>.\n async stream(method, signal, timeoutMs, header, input, contextValues) {\n const key = `${method.parent.typeName}/${method.name}`;\n const handler = scenario.handlers[key];\n if (!handler) {\n // Passthrough mirrors the unary path: in hybrid mode, unmatched\n // streaming RPCs go to the real backend.\n if (passthrough) {\n return fallback!.stream(\n method,\n signal,\n timeoutMs,\n header,\n input,\n contextValues,\n );\n }\n return Promise.reject(\n new ConnectError(\n `mock-transport: no scenario handler for streaming RPC ${key}`,\n Code.Unimplemented,\n ),\n );\n }\n // Read the request stream eagerly into an array so the scenario handler\n // sees the full client-streaming payload. For server-stream and\n // unary-into-stream-out RPCs this is a single message.\n const requestMessages: unknown[] = [];\n try {\n for await (const m of input as AsyncIterable<unknown>) {\n requestMessages.push(m);\n }\n } catch {\n // Synthetic clients sometimes pass a single value rather than an\n // iterable; treat that as an empty request payload.\n }\n const handlerInput =\n requestMessages.length <= 1 ? requestMessages[0] : requestMessages;\n const result = handler(handlerInput as never);\n return {\n service: method.parent as never,\n method: method as never,\n stream: true,\n header: new Headers(),\n message: toAsyncIterable(result) as AsyncIterable<never>,\n trailer: new Headers(),\n };\n },\n };\n return transport;\n}\n"]}
|
package/dist/otel.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { context, propagation, trace } from "@opentelemetry/api";
|
|
2
|
+
export interface BrowserTracingConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Collector base URL. Spans are POSTed to `${endpoint}/v1/traces`. Unset or
|
|
5
|
+
* empty is a no-op — the app runs untraced rather than throwing, which is
|
|
6
|
+
* what makes this safe to call unconditionally from the app shell.
|
|
7
|
+
*/
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
/** `service.name` on every span. */
|
|
10
|
+
serviceName: string;
|
|
11
|
+
/** `service.version` on every span. Defaults to "0.0.0". */
|
|
12
|
+
serviceVersion?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* initBrowserTracing registers the browser tracer provider and the fetch /
|
|
16
|
+
* XHR / document-load auto-instrumentations.
|
|
17
|
+
*
|
|
18
|
+
* No-ops without an `endpoint` (nothing to export to) and without a DOM
|
|
19
|
+
* (SSR / prerender). Idempotent: a second call while a provider is already
|
|
20
|
+
* registered returns without double-registering, which is what survives
|
|
21
|
+
* React StrictMode's double-mount and Fast Refresh.
|
|
22
|
+
*
|
|
23
|
+
* The env vars that carry the endpoint stay in APPLICATION code
|
|
24
|
+
* (`NEXT_PUBLIC_OTEL_ENDPOINT` and friends) — a bundler replaces those
|
|
25
|
+
* literals at build time, and it can only do that where they are written
|
|
26
|
+
* out verbatim.
|
|
27
|
+
*/
|
|
28
|
+
export declare function initBrowserTracing(config: BrowserTracingConfig): void;
|
|
29
|
+
/** The tracer to open manual spans on. */
|
|
30
|
+
export declare function getTracer(name?: string): import("@opentelemetry/api").Tracer;
|
|
31
|
+
export { context, propagation, trace };
|
package/dist/otel.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.
|
|
2
|
+
//
|
|
3
|
+
// OpenTelemetry JS SDK 2.x wiring for the browser: a WebTracerProvider with
|
|
4
|
+
// a batched OTLP/HTTP exporter, W3C trace-context propagation, and the fetch
|
|
5
|
+
// / XHR / document-load auto-instrumentations.
|
|
6
|
+
//
|
|
7
|
+
// This is the EXPORT path. Distributed tracing itself — the traceparent
|
|
8
|
+
// attached to every RPC, which is what makes browser spans join the backend's
|
|
9
|
+
// trace — is always on and lives in trace.ts; it needs no collector and does
|
|
10
|
+
// NOT depend on this module. Reach for this when you additionally want the
|
|
11
|
+
// browser's own spans shipped somewhere.
|
|
12
|
+
//
|
|
13
|
+
// Imported through the "@reliantlabs/forge-web-runtime/otel" subpath, never the
|
|
14
|
+
// barrel. The eight @opentelemetry/* SDK packages below are heavy, and only
|
|
15
|
+
// the Next.js scaffold installs them: a Vite SPA or React Native frontend
|
|
16
|
+
// declares just @opentelemetry/api and imports the barrel, so re-exporting
|
|
17
|
+
// this from index.ts would leave those frontends resolving packages they
|
|
18
|
+
// never installed.
|
|
19
|
+
//
|
|
20
|
+
// Notable migration points from SDK 1.x -> 2.x:
|
|
21
|
+
// - `new Resource({ ... })` is replaced by `resourceFromAttributes({ ... })`.
|
|
22
|
+
// The `Resource` class is no longer exported from `@opentelemetry/resources`.
|
|
23
|
+
// - `provider.addSpanProcessor(...)` is removed; span processors must be
|
|
24
|
+
// passed via the `spanProcessors` option on the `WebTracerProvider`
|
|
25
|
+
// constructor (the provider becomes immutable after construction).
|
|
26
|
+
// - Minimum TS target is ES2022.
|
|
27
|
+
//
|
|
28
|
+
// See https://github.com/open-telemetry/opentelemetry-js/blob/main/doc/upgrade-to-2.x.md
|
|
29
|
+
import { context, propagation, trace } from "@opentelemetry/api";
|
|
30
|
+
import { getWebAutoInstrumentations } from "@opentelemetry/auto-instrumentations-web";
|
|
31
|
+
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
32
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
33
|
+
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
|
34
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
35
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
36
|
+
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
|
|
37
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions";
|
|
38
|
+
let provider = null;
|
|
39
|
+
/**
|
|
40
|
+
* initBrowserTracing registers the browser tracer provider and the fetch /
|
|
41
|
+
* XHR / document-load auto-instrumentations.
|
|
42
|
+
*
|
|
43
|
+
* No-ops without an `endpoint` (nothing to export to) and without a DOM
|
|
44
|
+
* (SSR / prerender). Idempotent: a second call while a provider is already
|
|
45
|
+
* registered returns without double-registering, which is what survives
|
|
46
|
+
* React StrictMode's double-mount and Fast Refresh.
|
|
47
|
+
*
|
|
48
|
+
* The env vars that carry the endpoint stay in APPLICATION code
|
|
49
|
+
* (`NEXT_PUBLIC_OTEL_ENDPOINT` and friends) — a bundler replaces those
|
|
50
|
+
* literals at build time, and it can only do that where they are written
|
|
51
|
+
* out verbatim.
|
|
52
|
+
*/
|
|
53
|
+
export function initBrowserTracing(config) {
|
|
54
|
+
if (!config.endpoint || typeof window === "undefined") {
|
|
55
|
+
return; // No-op when endpoint not configured or SSR
|
|
56
|
+
}
|
|
57
|
+
if (provider) {
|
|
58
|
+
return; // Idempotent — avoid double-registering on HMR/StrictMode remounts
|
|
59
|
+
}
|
|
60
|
+
const resource = resourceFromAttributes({
|
|
61
|
+
[ATTR_SERVICE_NAME]: config.serviceName,
|
|
62
|
+
[ATTR_SERVICE_VERSION]: config.serviceVersion || "0.0.0",
|
|
63
|
+
});
|
|
64
|
+
provider = new WebTracerProvider({
|
|
65
|
+
resource,
|
|
66
|
+
spanProcessors: [
|
|
67
|
+
new BatchSpanProcessor(new OTLPTraceExporter({
|
|
68
|
+
url: `${config.endpoint}/v1/traces`,
|
|
69
|
+
})),
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
// Enable W3C trace context propagation (sends traceparent header)
|
|
73
|
+
propagation.setGlobalPropagator(new W3CTraceContextPropagator());
|
|
74
|
+
provider.register();
|
|
75
|
+
// Auto-instrument fetch, XMLHttpRequest, document load
|
|
76
|
+
registerInstrumentations({
|
|
77
|
+
instrumentations: [
|
|
78
|
+
getWebAutoInstrumentations({
|
|
79
|
+
"@opentelemetry/instrumentation-fetch": {
|
|
80
|
+
propagateTraceHeaderCorsUrls: [/.*/], // Propagate to all URLs
|
|
81
|
+
},
|
|
82
|
+
"@opentelemetry/instrumentation-xml-http-request": {
|
|
83
|
+
propagateTraceHeaderCorsUrls: [/.*/],
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** The tracer to open manual spans on. */
|
|
90
|
+
export function getTracer(name = "default") {
|
|
91
|
+
return trace.getTracer(name);
|
|
92
|
+
}
|
|
93
|
+
export { context, propagation, trace };
|
|
94
|
+
//# sourceMappingURL=otel.js.map
|
package/dist/otel.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otel.js","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,+CAA+C;AAC/C,EAAE;AACF,wEAAwE;AACxE,8EAA8E;AAC9E,6EAA6E;AAC7E,2EAA2E;AAC3E,yCAAyC;AACzC,EAAE;AACF,gFAAgF;AAChF,4EAA4E;AAC5E,0EAA0E;AAC1E,2EAA2E;AAC3E,yEAAyE;AACzE,mBAAmB;AACnB,EAAE;AACF,gDAAgD;AAChD,gFAAgF;AAChF,kFAAkF;AAClF,2EAA2E;AAC3E,wEAAwE;AACxE,uEAAuE;AACvE,mCAAmC;AACnC,EAAE;AACF,yFAAyF;AACzF,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,0BAA0B,EAAE,MAAM,0CAA0C,CAAC;AACtF,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAC5E,OAAO,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EACL,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,qCAAqC,CAAC;AAe7C,IAAI,QAAQ,GAA6B,IAAI,CAAC;AAE9C;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAA4B;IAC7D,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QACtD,OAAO,CAAC,4CAA4C;IACtD,CAAC;IACD,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,CAAC,mEAAmE;IAC7E,CAAC;IAED,MAAM,QAAQ,GAAG,sBAAsB,CAAC;QACtC,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC,WAAW;QACvC,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC,cAAc,IAAI,OAAO;KACzD,CAAC,CAAC;IAEH,QAAQ,GAAG,IAAI,iBAAiB,CAAC;QAC/B,QAAQ;QACR,cAAc,EAAE;YACd,IAAI,kBAAkB,CACpB,IAAI,iBAAiB,CAAC;gBACpB,GAAG,EAAE,GAAG,MAAM,CAAC,QAAQ,YAAY;aACpC,CAAC,CACH;SACF;KACF,CAAC,CAAC;IAEH,kEAAkE;IAClE,WAAW,CAAC,mBAAmB,CAAC,IAAI,yBAAyB,EAAE,CAAC,CAAC;IACjE,QAAQ,CAAC,QAAQ,EAAE,CAAC;IAEpB,uDAAuD;IACvD,wBAAwB,CAAC;QACvB,gBAAgB,EAAE;YAChB,0BAA0B,CAAC;gBACzB,sCAAsC,EAAE;oBACtC,4BAA4B,EAAE,CAAC,IAAI,CAAC,EAAE,wBAAwB;iBAC/D;gBACD,iDAAiD,EAAE;oBACjD,4BAA4B,EAAE,CAAC,IAAI,CAAC;iBACrC;aACF,CAAC;SACH;KACF,CAAC,CAAC;AACL,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,SAAS,CAAC,IAAI,GAAG,SAAS;IACxC,OAAO,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC;AAED,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC","sourcesContent":["// Part of @reliantlabs/forge-web-runtime — the web twin of forge/pkg.\n//\n// OpenTelemetry JS SDK 2.x wiring for the browser: a WebTracerProvider with\n// a batched OTLP/HTTP exporter, W3C trace-context propagation, and the fetch\n// / XHR / document-load auto-instrumentations.\n//\n// This is the EXPORT path. Distributed tracing itself — the traceparent\n// attached to every RPC, which is what makes browser spans join the backend's\n// trace — is always on and lives in trace.ts; it needs no collector and does\n// NOT depend on this module. Reach for this when you additionally want the\n// browser's own spans shipped somewhere.\n//\n// Imported through the \"@reliantlabs/forge-web-runtime/otel\" subpath, never the\n// barrel. The eight @opentelemetry/* SDK packages below are heavy, and only\n// the Next.js scaffold installs them: a Vite SPA or React Native frontend\n// declares just @opentelemetry/api and imports the barrel, so re-exporting\n// this from index.ts would leave those frontends resolving packages they\n// never installed.\n//\n// Notable migration points from SDK 1.x -> 2.x:\n// - `new Resource({ ... })` is replaced by `resourceFromAttributes({ ... })`.\n// The `Resource` class is no longer exported from `@opentelemetry/resources`.\n// - `provider.addSpanProcessor(...)` is removed; span processors must be\n// passed via the `spanProcessors` option on the `WebTracerProvider`\n// constructor (the provider becomes immutable after construction).\n// - Minimum TS target is ES2022.\n//\n// See https://github.com/open-telemetry/opentelemetry-js/blob/main/doc/upgrade-to-2.x.md\nimport { context, propagation, trace } from \"@opentelemetry/api\";\nimport { getWebAutoInstrumentations } from \"@opentelemetry/auto-instrumentations-web\";\nimport { W3CTraceContextPropagator } from \"@opentelemetry/core\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { registerInstrumentations } from \"@opentelemetry/instrumentation\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport { BatchSpanProcessor } from \"@opentelemetry/sdk-trace-base\";\nimport { WebTracerProvider } from \"@opentelemetry/sdk-trace-web\";\nimport {\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION,\n} from \"@opentelemetry/semantic-conventions\";\n\nexport interface BrowserTracingConfig {\n /**\n * Collector base URL. Spans are POSTed to `${endpoint}/v1/traces`. Unset or\n * empty is a no-op — the app runs untraced rather than throwing, which is\n * what makes this safe to call unconditionally from the app shell.\n */\n endpoint?: string;\n /** `service.name` on every span. */\n serviceName: string;\n /** `service.version` on every span. Defaults to \"0.0.0\". */\n serviceVersion?: string;\n}\n\nlet provider: WebTracerProvider | null = null;\n\n/**\n * initBrowserTracing registers the browser tracer provider and the fetch /\n * XHR / document-load auto-instrumentations.\n *\n * No-ops without an `endpoint` (nothing to export to) and without a DOM\n * (SSR / prerender). Idempotent: a second call while a provider is already\n * registered returns without double-registering, which is what survives\n * React StrictMode's double-mount and Fast Refresh.\n *\n * The env vars that carry the endpoint stay in APPLICATION code\n * (`NEXT_PUBLIC_OTEL_ENDPOINT` and friends) — a bundler replaces those\n * literals at build time, and it can only do that where they are written\n * out verbatim.\n */\nexport function initBrowserTracing(config: BrowserTracingConfig): void {\n if (!config.endpoint || typeof window === \"undefined\") {\n return; // No-op when endpoint not configured or SSR\n }\n if (provider) {\n return; // Idempotent — avoid double-registering on HMR/StrictMode remounts\n }\n\n const resource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: config.serviceName,\n [ATTR_SERVICE_VERSION]: config.serviceVersion || \"0.0.0\",\n });\n\n provider = new WebTracerProvider({\n resource,\n spanProcessors: [\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url: `${config.endpoint}/v1/traces`,\n }),\n ),\n ],\n });\n\n // Enable W3C trace context propagation (sends traceparent header)\n propagation.setGlobalPropagator(new W3CTraceContextPropagator());\n provider.register();\n\n // Auto-instrument fetch, XMLHttpRequest, document load\n registerInstrumentations({\n instrumentations: [\n getWebAutoInstrumentations({\n \"@opentelemetry/instrumentation-fetch\": {\n propagateTraceHeaderCorsUrls: [/.*/], // Propagate to all URLs\n },\n \"@opentelemetry/instrumentation-xml-http-request\": {\n propagateTraceHeaderCorsUrls: [/.*/],\n },\n }),\n ],\n });\n}\n\n/** The tracer to open manual spans on. */\nexport function getTracer(name = \"default\") {\n return trace.getTracer(name);\n}\n\nexport { context, propagation, trace };\n"]}
|