@lensmcp/react-instrumentation 1.18.4 → 1.18.6

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/lib/flow-fetch.js CHANGED
@@ -1,143 +1 @@
1
- /**
2
- * Flow-header propagation across the network boundary. When a request is
3
- * made while a flow is active (inside `runInFlow`/`withFlow`), inject
4
- * `x-lensmcp-flow-id` + `x-lensmcp-origin-node-id` headers so the backend
5
- * (`TraceInterceptor` in `@lensmcp/nest-instrumentation`) stamps its
6
- * server-request + span + db-query events with the SAME flowId. That is
7
- * what lets `story.compile` and `graph.*` correlate a single user action
8
- * all the way from the click to the DB query.
9
- *
10
- * Works in the browser and in Node (both have a global `fetch`). The
11
- * header is read at *call time* (synchronous), so a `fetch()` invoked
12
- * inside the synchronous portion of a flow is tagged even though the
13
- * awaited continuation runs after the flow stack unwinds.
14
- */
15
- import { activeFlow, beginBackgroundFlow, extendFlowWindow, publish } from './publish.js';
16
- /**
17
- * Monkeypatch `globalThis.fetch` to inject flow headers. Returns an
18
- * uninstall function that restores the original. No-op (returns a no-op
19
- * uninstaller) if there's no global fetch.
20
- */
21
- let pageLoadFlowStarted = false;
22
- export function installFlowFetch(options = {}) {
23
- const g = globalThis;
24
- if (typeof g.fetch !== 'function')
25
- return () => undefined;
26
- // installFlowFetch runs at ENTRY-MODULE eval (the babel transform injects
27
- // it before the root render) — earlier than any mount effect. That makes
28
- // it the one reliable place to declare the PAGE LOAD itself a flow, so the
29
- // boot-time fetches (/me, list queries…) and the renders they trigger all
30
- // stitch into one trace. A refresh without this produced NO flow at all.
31
- if (!pageLoadFlowStarted && typeof location !== 'undefined' && typeof document !== 'undefined') {
32
- pageLoadFlowStarted = true;
33
- beginBackgroundFlow({ originType: 'page-load', originNodeId: `page:${location.pathname}` });
34
- }
35
- const flowHeader = options.flowHeader ?? 'x-lensmcp-flow-id';
36
- const originHeader = options.originHeader ?? 'x-lensmcp-origin-node-id';
37
- const original = g.fetch.bind(globalThis);
38
- // Tell the injected client runtime's own (inner) fetch wrapper to stop
39
- // emitting: from here on THIS wrapper publishes the network events, and
40
- // both firing double-counts every request in the timeline.
41
- globalThis['__LENSMCP_FLOW_FETCH__'] = true;
42
- const patched = ((input, init) => {
43
- // Sync stack (a wrapped handler) OR the causality window (page-load,
44
- // post-response continuations) — both mean this fetch belongs to a flow.
45
- const flow = activeFlow();
46
- if (!flow?.flowId)
47
- return original(input, init);
48
- // Merge onto any caller-supplied headers (+ a Request's own headers).
49
- const requestHeaders = typeof input === 'object' && input !== null && 'headers' in input
50
- ? input.headers
51
- : undefined;
52
- const headers = new Headers(init?.headers ?? requestHeaders ?? undefined);
53
- if (!headers.has(flowHeader))
54
- headers.set(flowHeader, flow.flowId);
55
- if (flow.originNodeId && !headers.has(originHeader)) {
56
- headers.set(originHeader, flow.originNodeId);
57
- }
58
- const result = original(input, { ...init, headers });
59
- publishFetchLeg(flow, input, init, result);
60
- return result;
61
- });
62
- g.fetch = patched;
63
- return () => {
64
- g.fetch = original;
65
- };
66
- }
67
- /**
68
- * Publish the flow's network leg: a start event (synchronous — stamped from
69
- * the flow stack) and a completion event (asynchronous — the flow has
70
- * unwound by then, so its context is attached explicitly). Together with
71
- * the backend's server-request event (same flowId via the header) this is
72
- * what lets a story read click → fetch → server → DB.
73
- */
74
- function publishFetchLeg(flow, input, init, result) {
75
- const url = typeof input === 'string'
76
- ? input
77
- : input instanceof URL
78
- ? input.href
79
- : (input.url ?? String(input));
80
- const method = init?.method ??
81
- (typeof input === 'object' && input !== null && 'method' in input
82
- ? (input.method ?? 'GET')
83
- : 'GET');
84
- const startedAt = Date.now();
85
- const flowContext = {
86
- flowId: flow.flowId,
87
- ...(flow.originType ? { originType: flow.originType } : {}),
88
- ...(flow.originNodeId ? { originNodeId: flow.originNodeId } : {}),
89
- };
90
- publish({
91
- source: 'react',
92
- category: 'network',
93
- severity: 'info',
94
- title: `fetch ${method} ${url}`,
95
- fingerprint: `flow-fetch:${method}:${url}`,
96
- raw: { kind: 'fetch-start', url, method },
97
- });
98
- // Observe on a branch — never alter the caller's promise chain.
99
- void result.then((res) => {
100
- // Re-open the causality window: the state update + re-render that
101
- // consume this response land in the next ticks and belong to this flow.
102
- extendFlowWindow(flow);
103
- publish({
104
- source: 'react',
105
- category: 'network',
106
- // A 4xx/5xx IS an error. This said `warning`, and the runtime reducer
107
- // routes only `severity === 'error'` into
108
- // `runtime://browser/failed-requests` — so every failed request in an
109
- // app using flow-fetch was invisible to the lens. Measured on
110
- // foodguard: 116 non-ok responses (98×401, 10×503, 8×502) captured in
111
- // the event log, none of them reported.
112
- severity: res.ok ? 'info' : 'error',
113
- title: `fetch ${method} ${url} → ${res.status}${res.statusText ? ` ${res.statusText}` : ''}`,
114
- // The STATUS is part of the identity. Without it a 200 and a 401 to
115
- // the same endpoint shared one fingerprint, so five distinct failures
116
- // deduped into whatever happened to be seen first.
117
- fingerprint: `flow-fetch:${method}:${url}:${res.status}`,
118
- context: flowContext,
119
- raw: {
120
- kind: 'fetch-done',
121
- url,
122
- method,
123
- status: res.status,
124
- statusText: res.statusText,
125
- ok: res.ok,
126
- startedAt,
127
- durationMs: Date.now() - startedAt,
128
- },
129
- });
130
- }, (err) => {
131
- extendFlowWindow(flow);
132
- publish({
133
- source: 'react',
134
- category: 'network',
135
- severity: 'error',
136
- title: `fetch ${method} ${url} failed`,
137
- message: err instanceof Error ? err.message : String(err),
138
- fingerprint: `flow-fetch:${method}:${url}:error`,
139
- context: flowContext,
140
- raw: { kind: 'fetch-error', url, method, startedAt, durationMs: Date.now() - startedAt },
141
- });
142
- });
143
- }
1
+ "use strict";var $=Object.defineProperty;var l=(o,e)=>$(o,"name",{value:e,configurable:!0});var m=Object.defineProperty,f=l((o,e)=>m(o,"name",{value:e,configurable:!0}),"f");import{activeFlow as T,beginBackgroundFlow as b,extendFlowWindow as g,publish as h}from"./publish.js";let w=!1;export function installFlowFetch(o={}){const e=globalThis;if(typeof e.fetch!="function")return()=>{};!w&&typeof location<"u"&&typeof document<"u"&&(w=!0,b({originType:"page-load",originNodeId:`page:${location.pathname}`}));const c=o.flowHeader??"x-lensmcp-flow-id",d=o.originHeader??"x-lensmcp-origin-node-id",r=e.fetch.bind(globalThis);globalThis.__LENSMCP_FLOW_FETCH__=!0;const n=f(((i,s)=>{const t=T();if(!t?.flowId)return r(i,s);const y=typeof i=="object"&&i!==null&&"headers"in i?i.headers:void 0,a=new Headers(s?.headers??y??void 0);a.has(c)||a.set(c,t.flowId),t.originNodeId&&!a.has(d)&&a.set(d,t.originNodeId);const u=r(i,{...s,headers:a});return p(t,i,s,u),u}),"patched");return e.fetch=n,()=>{e.fetch=r}}l(installFlowFetch,"installFlowFetch"),f(installFlowFetch,"installFlowFetch");function p(o,e,c,d){const r=typeof e=="string"?e:e instanceof URL?e.href:e.url??String(e),n=c?.method??(typeof e=="object"&&e!==null&&"method"in e?e.method??"GET":"GET"),i=Date.now(),s={flowId:o.flowId,...o.originType?{originType:o.originType}:{},...o.originNodeId?{originNodeId:o.originNodeId}:{}};h({source:"react",category:"network",severity:"info",title:`fetch ${n} ${r}`,fingerprint:`flow-fetch:${n}:${r}`,raw:{kind:"fetch-start",url:r,method:n}}),d.then(t=>{g(o),h({source:"react",category:"network",severity:t.ok?"info":"error",title:`fetch ${n} ${r} \u2192 ${t.status}${t.statusText?` ${t.statusText}`:""}`,fingerprint:`flow-fetch:${n}:${r}:${t.status}`,context:s,raw:{kind:"fetch-done",url:r,method:n,status:t.status,statusText:t.statusText,ok:t.ok,startedAt:i,durationMs:Date.now()-i}})},t=>{g(o),h({source:"react",category:"network",severity:"error",title:`fetch ${n} ${r} failed`,message:t instanceof Error?t.message:String(t),fingerprint:`flow-fetch:${n}:${r}:error`,context:s,raw:{kind:"fetch-error",url:r,method:n,startedAt:i,durationMs:Date.now()-i}})})}l(p,"m"),f(p,"publishFetchLeg");
package/lib/hoc.js CHANGED
@@ -1,45 +1 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Profiler, } from 'react';
3
- import { publish } from './publish.js';
4
- import { makeInstanceId } from './identity.js';
5
- /**
6
- * `withTraceComponent(MyComponent)` — opt-in tracer for projects that
7
- * can't enable the global Babel transform. Wraps the component in a
8
- * Profiler and emits a `render` event on every commit.
9
- */
10
- export function withTraceComponent(Wrapped, opts = {}) {
11
- const name = opts.componentName ?? Wrapped.displayName ?? Wrapped.name ?? 'TracedComponent';
12
- const file = opts.source ?? '(unknown)';
13
- const logicalId = `react:component:${file}:${name}`;
14
- function Traced(props) {
15
- let lastRenderId;
16
- const onRender = (_id, phase, actualDuration, baseDuration, startTime, commitTime) => {
17
- const { instanceId } = makeInstanceId(logicalId);
18
- const record = {
19
- id: instanceId,
20
- parentRenderId: lastRenderId,
21
- componentInstanceId: instanceId,
22
- componentLogicalId: logicalId,
23
- componentName: name,
24
- phase: phase === 'mount' ? 'mount' : 'update',
25
- actualDurationMs: actualDuration,
26
- baseDurationMs: baseDuration,
27
- startTime,
28
- commitTime,
29
- why: [{ type: 'force', reason: 'withTraceComponent' }],
30
- };
31
- lastRenderId = instanceId;
32
- publish({
33
- source: 'react',
34
- category: 'render',
35
- severity: 'info',
36
- title: `render ${name} (${phase})`,
37
- fingerprint: `react-render:${logicalId}`,
38
- raw: { kind: 'render', render: record },
39
- });
40
- };
41
- return (_jsx(Profiler, { id: logicalId, onRender: onRender, children: _jsx(Wrapped, { ...props }) }));
42
- }
43
- Traced.displayName = `Traced(${name})`;
44
- return Traced;
45
- }
1
+ "use strict";var I=Object.defineProperty;var o=(e,n)=>I(e,"name",{value:n,configurable:!0});var T=Object.defineProperty,i=o((e,n)=>T(e,"name",{value:n,configurable:!0}),"t");import{jsx as d}from"react/jsx-runtime";import{Profiler as w}from"react";import{publish as $}from"./publish.js";import{makeInstanceId as g}from"./identity.js";export function withTraceComponent(e,n={}){const r=n.componentName??e.displayName??e.name??"TracedComponent",t=`react:component:${n.source??"(unknown)"}:${r}`;function a(p){let m;return d(w,{id:t,onRender:i((b,s,u,f,l,h)=>{const{instanceId:c}=g(t),y={id:c,parentRenderId:m,componentInstanceId:c,componentLogicalId:t,componentName:r,phase:s==="mount"?"mount":"update",actualDurationMs:u,baseDurationMs:f,startTime:l,commitTime:h,why:[{type:"force",reason:"withTraceComponent"}]};m=c,$({source:"react",category:"render",severity:"info",title:`render ${r} (${s})`,fingerprint:`react-render:${t}`,raw:{kind:"render",render:y}})},"onRender"),children:d(e,{...p})})}return o(a,"a"),i(a,"Traced"),a.displayName=`Traced(${r})`,a}o(withTraceComponent,"withTraceComponent"),i(withTraceComponent,"withTraceComponent");
package/lib/identity.js CHANGED
@@ -1,74 +1 @@
1
- /**
2
- * Identity helpers — produce stable `logicalId` and per-mount
3
- * `instanceId` values for components, hooks, and slots. Mirrors the
4
- * shapes documented in `planning/02-data-model.md#logicalid`.
5
- */
6
- const generations = new Map();
7
- let entropy = 0;
8
- export function nextGeneration(logicalId) {
9
- const g = (generations.get(logicalId) ?? 0) + 1;
10
- generations.set(logicalId, g);
11
- return g;
12
- }
13
- export function shortHash() {
14
- entropy = (entropy + 1) % 0xffffffff;
15
- return ((Date.now() & 0xfff) ^ entropy).toString(36).slice(-4);
16
- }
17
- export function componentLogicalId(file, name) {
18
- return `react:component:${file}:${name}`;
19
- }
20
- export function hookLogicalId(file, componentName, hookName, index) {
21
- return `react:hook:${file}:${componentName}:${hookName}:${index}`;
22
- }
23
- export function slotLogicalId(file, line, kind) {
24
- return `react:slot:${file}:${line}:${kind}`;
25
- }
26
- export function makeInstanceId(logicalId) {
27
- const generation = nextGeneration(logicalId);
28
- return {
29
- instanceId: `${logicalId}#${generation}#${shortHash()}`,
30
- generation,
31
- };
32
- }
33
- /**
34
- * Cheap deterministic hash over an array of dependency values. Used by
35
- * the traced hooks to compute `depsHash` so the reducer can decide
36
- * whether a hook re-ran because of a dep change.
37
- */
38
- export function depsHash(deps) {
39
- if (!deps)
40
- return 'no-deps';
41
- let h = 5381;
42
- for (const dep of deps) {
43
- const repr = canonical(dep);
44
- for (let i = 0; i < repr.length; i++) {
45
- h = (h * 33) ^ repr.charCodeAt(i);
46
- }
47
- }
48
- return (h >>> 0).toString(36);
49
- }
50
- function canonical(v) {
51
- if (v === null)
52
- return 'null';
53
- if (v === undefined)
54
- return 'undefined';
55
- const t = typeof v;
56
- if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint') {
57
- return `${t}:${String(v)}`;
58
- }
59
- if (t === 'function') {
60
- const name = v.name ?? 'anonymous';
61
- return `fn:${name}:${v.toString().length}`;
62
- }
63
- try {
64
- return `obj:${JSON.stringify(v)}`;
65
- }
66
- catch {
67
- return `obj:circular`;
68
- }
69
- }
70
- /** Test hook to reset generation counters between smoke iterations. */
71
- export function resetIdentityState() {
72
- generations.clear();
73
- entropy = 0;
74
- }
1
+ "use strict";var f=Object.defineProperty;var e=(t,n)=>f(t,"name",{value:n,configurable:!0});var l=Object.defineProperty,o=e((t,n)=>l(t,"name",{value:n,configurable:!0}),"e");const u=new Map;let c=0;export function nextGeneration(t){const n=(u.get(t)??0)+1;return u.set(t,n),n}e(nextGeneration,"nextGeneration"),o(nextGeneration,"nextGeneration");export function shortHash(){return c=(c+1)%4294967295,(Date.now()&4095^c).toString(36).slice(-4)}e(shortHash,"shortHash"),o(shortHash,"shortHash");export function componentLogicalId(t,n){return`react:component:${t}:${n}`}e(componentLogicalId,"componentLogicalId"),o(componentLogicalId,"componentLogicalId");export function hookLogicalId(t,n,r,i){return`react:hook:${t}:${n}:${r}:${i}`}e(hookLogicalId,"hookLogicalId"),o(hookLogicalId,"hookLogicalId");export function slotLogicalId(t,n,r){return`react:slot:${t}:${n}:${r}`}e(slotLogicalId,"slotLogicalId"),o(slotLogicalId,"slotLogicalId");export function makeInstanceId(t){const n=nextGeneration(t);return{instanceId:`${t}#${n}#${shortHash()}`,generation:n}}e(makeInstanceId,"makeInstanceId"),o(makeInstanceId,"makeInstanceId");export function depsHash(t){if(!t)return"no-deps";let n=5381;for(const r of t){const i=s(r);for(let a=0;a<i.length;a++)n=n*33^i.charCodeAt(a)}return(n>>>0).toString(36)}e(depsHash,"depsHash"),o(depsHash,"depsHash");function s(t){if(t===null)return"null";if(t===void 0)return"undefined";const n=typeof t;if(n==="string"||n==="number"||n==="boolean"||n==="bigint")return`${n}:${String(t)}`;if(n==="function")return`fn:${t.name??"anonymous"}:${t.toString().length}`;try{return`obj:${JSON.stringify(t)}`}catch{return"obj:circular"}}e(s,"a"),o(s,"canonical");export function resetIdentityState(){u.clear(),c=0}e(resetIdentityState,"resetIdentityState"),o(resetIdentityState,"resetIdentityState");
@@ -1,59 +1 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Profiler, useMemo, useRef, } from 'react';
3
- import { LensmcpContext } from './context.js';
4
- import { publish } from './publish.js';
5
- import { makeInstanceId } from './identity.js';
6
- import { installFiberRenderPublisher } from './fiber-renders.js';
7
- // Per-component render attribution rides the client runtime's DevTools-hook
8
- // bus (see fiber-renders.ts). Module-scope so importing LensmcpRoot (the
9
- // babel autoWrapRoot path) is enough — no API change for consumers.
10
- if (typeof window !== 'undefined')
11
- installFiberRenderPublisher();
12
- /**
13
- * Wraps the React tree in a `<Profiler>` and a context provider so the
14
- * hook helpers can tag their events with the right renderer / page /
15
- * route. Drop-in:
16
- *
17
- * ReactDOM.createRoot(el).render(
18
- * <LensmcpRoot page="home" route="/">
19
- * <App />
20
- * </LensmcpRoot>
21
- * );
22
- */
23
- export function LensmcpRoot(props) {
24
- const rendererId = props.rendererId ?? 'lensmcp-root';
25
- const ctxValue = useMemo(() => ({ rendererId, page: props.page, route: props.route }), [rendererId, props.page, props.route]);
26
- const renderIdsRef = useRef(new Map());
27
- const lastRenderIdRef = useRef(undefined);
28
- const onRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
29
- const logicalId = `render:${rendererId}:${id}`;
30
- const { instanceId } = makeInstanceId(logicalId);
31
- const renderRecord = {
32
- id: instanceId,
33
- parentRenderId: lastRenderIdRef.current,
34
- componentInstanceId: instanceId,
35
- componentLogicalId: logicalId,
36
- componentName: id,
37
- page: props.page,
38
- route: props.route,
39
- rendererId,
40
- phase: phase === 'mount' ? 'mount' : 'update',
41
- actualDurationMs: actualDuration,
42
- baseDurationMs: baseDuration,
43
- startTime,
44
- commitTime,
45
- why: [{ type: 'force', reason: 'profiler-onrender' }],
46
- };
47
- lastRenderIdRef.current = instanceId;
48
- renderIdsRef.current.set(id, instanceId);
49
- publish({
50
- source: 'react',
51
- category: 'render',
52
- severity: 'info',
53
- title: `render ${id} (${phase})`,
54
- fingerprint: `react-render:${logicalId}`,
55
- raw: { kind: 'render', render: renderRecord },
56
- });
57
- };
58
- return (_jsx(LensmcpContext.Provider, { value: ctxValue, children: _jsx(Profiler, { id: rendererId, onRender: onRender, children: props.children }) }));
59
- }
1
+ "use strict";var v=Object.defineProperty;var a=(e,r)=>v(e,"name",{value:r,configurable:!0});var y=Object.defineProperty,d=a((e,r)=>y(e,"name",{value:r,configurable:!0}),"d");import{jsx as c}from"react/jsx-runtime";import{Profiler as R,useMemo as w,useRef as m}from"react";import{LensmcpContext as $}from"./context.js";import{publish as M}from"./publish.js";import{makeInstanceId as x}from"./identity.js";import{installFiberRenderPublisher as L}from"./fiber-renders.js";typeof window<"u"&&L();export function LensmcpRoot(e){const r=e.rendererId??"lensmcp-root",p=w(()=>({rendererId:r,page:e.page,route:e.route}),[r,e.page,e.route]),u=m(new Map),i=m(void 0),f=d((n,s,l,g,I,h)=>{const t=`render:${r}:${n}`,{instanceId:o}=x(t),b={id:o,parentRenderId:i.current,componentInstanceId:o,componentLogicalId:t,componentName:n,page:e.page,route:e.route,rendererId:r,phase:s==="mount"?"mount":"update",actualDurationMs:l,baseDurationMs:g,startTime:I,commitTime:h,why:[{type:"force",reason:"profiler-onrender"}]};i.current=o,u.current.set(n,o),M({source:"react",category:"render",severity:"info",title:`render ${n} (${s})`,fingerprint:`react-render:${t}`,raw:{kind:"render",render:b}})},"onRender");return c($.Provider,{value:p,children:c(R,{id:r,onRender:f,children:e.children})})}a(LensmcpRoot,"LensmcpRoot"),d(LensmcpRoot,"LensmcpRoot");
package/lib/publish.js CHANGED
@@ -1,236 +1 @@
1
- const QUEUE = [];
2
- const MAX_QUEUE = 200;
3
- const FLOW_STACK = [];
4
- /**
5
- * Causality window: the synchronous flow stack misses everything Valtio and
6
- * React deliver asynchronously — subscriber notifications, Profiler commits,
7
- * and the whole post-response wave (state update + re-render after a fetch).
8
- * An interaction OPENS a short attribution window; fetch completion EXTENDS
9
- * it; any publish that has no explicit flow and no sync stack inherits the
10
- * window's flow. Single-user dev semantics: the most recent interaction owns
11
- * ambient activity for a few seconds — exactly how a person reads the app.
12
- */
13
- const FLOW_WINDOW_MS = 4000;
14
- let windowFlow;
15
- export function extendFlowWindow(spec) {
16
- const s = spec ?? currentFlow() ?? windowFlow?.spec;
17
- if (s)
18
- windowFlow = { spec: s, until: Date.now() + FLOW_WINDOW_MS };
19
- }
20
- function windowedFlow() {
21
- return windowFlow && Date.now() < windowFlow.until ? windowFlow.spec : undefined;
22
- }
23
- /** The flow that owns work happening NOW: sync stack first, else the window. */
24
- export function activeFlow() {
25
- return currentFlow() ?? windowedFlow();
26
- }
27
- /**
28
- * Start a flow with no synchronous handler to wrap — page loads, timers,
29
- * websocket pushes. Opens the causality window (so the async work that
30
- * follows inherits the flow) and publishes the origin event that makes the
31
- * flow exist in the reducer.
32
- */
33
- export function beginBackgroundFlow(origin) {
34
- const spec = {
35
- flowId: newFlowId(),
36
- originType: origin.originType,
37
- ...(origin.originNodeId ? { originNodeId: origin.originNodeId } : {}),
38
- };
39
- windowFlow = { spec, until: Date.now() + FLOW_WINDOW_MS };
40
- publish({
41
- source: 'react',
42
- category: 'runtime',
43
- severity: 'info',
44
- title: origin.originType,
45
- fingerprint: `flow-origin:${origin.originNodeId ?? origin.originType}`,
46
- raw: { kind: 'flow-origin', originNodeId: origin.originNodeId },
47
- });
48
- return spec;
49
- }
50
- export function setPublisher(fn) {
51
- globalThis.__LENSMCP_PUBLISH__ = fn;
52
- if (fn) {
53
- while (QUEUE.length) {
54
- const ev = QUEUE.shift();
55
- if (ev) {
56
- try {
57
- fn(ev);
58
- }
59
- catch {
60
- /* swallow */
61
- }
62
- }
63
- }
64
- }
65
- }
66
- /**
67
- * Push a flow onto the module-level stack for the duration of `fn`. Any
68
- * `publish()` calls inside `fn` (including transitively triggered
69
- * Valtio subscribers and React Profiler callbacks that fire synchronously)
70
- * will inherit the flow context.
71
- *
72
- * NB: only synchronous propagation. For async chains (`fetch().then`,
73
- * `setTimeout`, promises) the caller must re-enter the flow with
74
- * `runInFlow` at the continuation site. The Phase 6.5 carryover adds
75
- * automatic continuation tracking via an async-context shim.
76
- */
77
- export function runInFlow(spec, fn) {
78
- FLOW_STACK.push(spec);
79
- windowFlow = { spec, until: Date.now() + FLOW_WINDOW_MS }; // interaction opens the causality window
80
- try {
81
- return fn();
82
- }
83
- finally {
84
- FLOW_STACK.pop();
85
- }
86
- }
87
- export function currentFlow() {
88
- return FLOW_STACK[FLOW_STACK.length - 1];
89
- }
90
- /** Marks a handler as already flow-wrapped so layered wrapping is a no-op. */
91
- const FLOWED = Symbol.for('lensmcp.flowed');
92
- /**
93
- * INPUT-CHANGE taming (the "an AI streamed text into an input → hundreds of flows" fix). Two rules,
94
- * applied ONLY to `originType: 'input-change'` (clicks/submits keep one-flow-per-invocation):
95
- *
96
- * 1. A PROGRAMMATIC change (the event arg is untrusted — `isTrusted === false`) is a CONSEQUENCE,
97
- * never a user intent: it must not mint a flow. The handler runs as-is and its effects inherit the
98
- * ambient flow (the click/submit that started the stream), which is the true causality; a live
99
- * ambient window is extended so a long stream keeps attributing to its trigger.
100
- * 2. A TRUSTED burst on the SAME element (typing) COALESCES: invocations within a sliding window reuse
101
- * the burst's flow and publish NO second origin — one editing burst = one flow, not one per keystroke.
102
- */
103
- const INPUT_COALESCE_MS = 2000;
104
- const inputBursts = new Map();
105
- /** The event's trust, wherever React put it (SyntheticEvent.nativeEvent or a raw DOM event). */
106
- function eventTrust(arg) {
107
- if (arg == null || typeof arg !== 'object')
108
- return undefined;
109
- const ev = arg;
110
- const nested = ev.nativeEvent;
111
- if (nested != null && typeof nested === 'object') {
112
- const trusted = nested.isTrusted;
113
- if (typeof trusted === 'boolean')
114
- return trusted;
115
- }
116
- if (typeof ev.isTrusted === 'boolean')
117
- return ev.isTrusted;
118
- return undefined;
119
- }
120
- function pruneInputBursts(now) {
121
- if (inputBursts.size <= 64)
122
- return;
123
- for (const [key, burst] of inputBursts)
124
- if (now >= burst.until)
125
- inputBursts.delete(key);
126
- }
127
- /**
128
- * Wrap a React event handler so every call runs inside a fresh flow.
129
- * The flowId is generated per invocation; `originType` defaults to
130
- * `'user-click'`. Pass `originNodeId` from the component identity
131
- * (typically the `data-agent-component` value).
132
- *
133
- * The babel transform wraps `on*` JSX values blindly, so this must be
134
- * total: non-function values (e.g. `onClick={maybeUndefined}`) pass
135
- * through unchanged, and an already-flowed handler is returned as-is —
136
- * layered components forwarding the same prop would otherwise nest a
137
- * flow per layer and crash on the inner non-function (`handler is not
138
- * a function`).
139
- */
140
- export function withFlow(handler, spec = {}) {
141
- if (typeof handler !== 'function')
142
- return handler;
143
- const existing = handler;
144
- if (existing[FLOWED])
145
- return handler;
146
- const publishOrigin = () => {
147
- // The origin event — published inside the flow so it carries the
148
- // flowId. It guarantees the flow materialises in the reducer (which
149
- // keys on the first event with a flowId) even when every downstream
150
- // effect lands asynchronously, outside the synchronous flow window.
151
- publish({
152
- source: 'react',
153
- category: 'runtime',
154
- severity: 'info',
155
- title: spec.originType ?? 'user-click',
156
- fingerprint: `flow-origin:${spec.originNodeId ?? 'unknown'}`,
157
- raw: { kind: 'flow-origin', originNodeId: spec.originNodeId },
158
- });
159
- };
160
- const flowed = function flowed(...args) {
161
- if (spec.originType === 'input-change') {
162
- // Rule 1 — a programmatic (untrusted) input change never mints a flow; it inherits the ambient
163
- // one (the interaction that caused it). Keep a LIVE window alive so a long stream stays attributed.
164
- if (eventTrust(args[0]) === false) {
165
- if (currentFlow() == null && windowedFlow() != null)
166
- extendFlowWindow();
167
- return handler(...args);
168
- }
169
- // Rule 2 — a trusted burst on the same element coalesces into ONE flow (sliding window).
170
- const key = spec.originNodeId ?? 'input';
171
- const now = Date.now();
172
- const burst = inputBursts.get(key);
173
- if (burst && now < burst.until) {
174
- burst.until = now + INPUT_COALESCE_MS;
175
- return runInFlow(burst.spec, () => handler(...args)); // same flow, no second origin event
176
- }
177
- const burstSpec = { flowId: newFlowId(), originType: 'input-change', originNodeId: spec.originNodeId };
178
- inputBursts.set(key, { spec: burstSpec, until: now + INPUT_COALESCE_MS });
179
- pruneInputBursts(now);
180
- return runInFlow(burstSpec, () => {
181
- publishOrigin();
182
- return handler(...args);
183
- });
184
- }
185
- const flowId = newFlowId();
186
- return runInFlow({
187
- flowId,
188
- originType: spec.originType ?? 'user-click',
189
- originNodeId: spec.originNodeId,
190
- }, () => {
191
- publishOrigin();
192
- return handler(...args);
193
- });
194
- };
195
- flowed[FLOWED] = true;
196
- return flowed;
197
- }
198
- let flowSeq = 0;
199
- function newFlowId() {
200
- flowSeq = (flowSeq + 1) % 0xffff;
201
- return `flow:${Date.now().toString(36)}:${flowSeq.toString(36)}`;
202
- }
203
- export function publish(env) {
204
- // Sync stack first; otherwise the causality window — unless the caller
205
- // already carries its own flow (e.g. fetch-done with closure context).
206
- const flow = currentFlow() ?? (env.context?.flowId ? undefined : windowedFlow());
207
- const stamped = flow == null
208
- ? env
209
- : {
210
- ...env,
211
- context: {
212
- ...(flow.flowId ? { flowId: flow.flowId } : {}),
213
- ...(flow.originType ? { originType: flow.originType } : {}),
214
- ...(flow.originNodeId ? { originNodeId: flow.originNodeId } : {}),
215
- ...(flow.causedByNodeId ? { causedByNodeId: flow.causedByNodeId } : {}),
216
- ...env.context,
217
- },
218
- };
219
- const fn = globalThis.__LENSMCP_PUBLISH__;
220
- if (fn) {
221
- try {
222
- fn(stamped);
223
- return;
224
- }
225
- catch {
226
- /* fall through to queue */
227
- }
228
- }
229
- if (QUEUE.length >= MAX_QUEUE)
230
- QUEUE.shift();
231
- QUEUE.push(stamped);
232
- }
233
- /** Test hook — only useful in unit tests / smokes. */
234
- export function drainQueue() {
235
- return QUEUE.splice(0, QUEUE.length);
236
- }
1
+ "use strict";var F=Object.defineProperty;var i=(e,o)=>F(e,"name",{value:o,configurable:!0});var v=Object.defineProperty,n=i((e,o)=>v(e,"name",{value:o,configurable:!0}),"t");const u=[],B=200,g=[],S=4e3;let l;export function extendFlowWindow(e){const o=e??currentFlow()??l?.spec;o&&(l={spec:o,until:Date.now()+4e3})}i(extendFlowWindow,"extendFlowWindow"),n(extendFlowWindow,"extendFlowWindow");function p(){return l&&Date.now()<l.until?l.spec:void 0}i(p,"w"),n(p,"windowedFlow");export function activeFlow(){return currentFlow()??p()}i(activeFlow,"activeFlow"),n(activeFlow,"activeFlow");export function beginBackgroundFlow(e){const o={flowId:w(),originType:e.originType,...e.originNodeId?{originNodeId:e.originNodeId}:{}};return l={spec:o,until:Date.now()+4e3},publish({source:"react",category:"runtime",severity:"info",title:e.originType,fingerprint:`flow-origin:${e.originNodeId??e.originType}`,raw:{kind:"flow-origin",originNodeId:e.originNodeId}}),o}i(beginBackgroundFlow,"beginBackgroundFlow"),n(beginBackgroundFlow,"beginBackgroundFlow");export function setPublisher(e){if(globalThis.__LENSMCP_PUBLISH__=e,e)for(;u.length;){const o=u.shift();if(o)try{e(o)}catch{}}}i(setPublisher,"setPublisher"),n(setPublisher,"setPublisher");export function runInFlow(e,o){g.push(e),l={spec:e,until:Date.now()+4e3};try{return o()}finally{g.pop()}}i(runInFlow,"runInFlow"),n(runInFlow,"runInFlow");export function currentFlow(){return g[g.length-1]}i(currentFlow,"currentFlow"),n(currentFlow,"currentFlow");const N=Symbol.for("lensmcp.flowed"),h=2e3,c=new Map;function T(e){if(e==null||typeof e!="object")return;const o=e,t=o.nativeEvent;if(t!=null&&typeof t=="object"){const r=t.isTrusted;if(typeof r=="boolean")return r}if(typeof o.isTrusted=="boolean")return o.isTrusted}i(T,"S"),n(T,"eventTrust");function b(e){if(!(c.size<=64))for(const[o,t]of c)e>=t.until&&c.delete(o)}i(b,"F"),n(b,"pruneInputBursts");export function withFlow(e,o={}){if(typeof e!="function"||e[N])return e;const t=n(()=>{publish({source:"react",category:"runtime",severity:"info",title:o.originType??"user-click",fingerprint:`flow-origin:${o.originNodeId??"unknown"}`,raw:{kind:"flow-origin",originNodeId:o.originNodeId}})},"publishOrigin"),r=n(function(...d){if(o.originType==="input-change"){if(T(d[0])===!1)return currentFlow()==null&&p()!=null&&extendFlowWindow(),e(...d);const I=o.originNodeId??"input",s=Date.now(),f=c.get(I);if(f&&s<f.until)return f.until=s+h,runInFlow(f.spec,()=>e(...d));const y={flowId:w(),originType:"input-change",originNodeId:o.originNodeId};return c.set(I,{spec:y,until:s+h}),b(s),runInFlow(y,()=>(t(),e(...d)))}const x=w();return runInFlow({flowId:x,originType:o.originType??"user-click",originNodeId:o.originNodeId},()=>(t(),e(...d)))},"flowed");return r[N]=!0,r}i(withFlow,"withFlow"),n(withFlow,"withFlow");let a=0;function w(){return a=(a+1)%65535,`flow:${Date.now().toString(36)}:${a.toString(36)}`}i(w,"g"),n(w,"newFlowId");export function publish(e){const o=currentFlow()??(e.context?.flowId?void 0:p()),t=o==null?e:{...e,context:{...o.flowId?{flowId:o.flowId}:{},...o.originType?{originType:o.originType}:{},...o.originNodeId?{originNodeId:o.originNodeId}:{},...o.causedByNodeId?{causedByNodeId:o.causedByNodeId}:{},...e.context}},r=globalThis.__LENSMCP_PUBLISH__;if(r)try{r(t);return}catch{}u.length>=200&&u.shift(),u.push(t)}i(publish,"publish"),n(publish,"publish");export function drainQueue(){return u.splice(0,u.length)}i(drainQueue,"drainQueue"),n(drainQueue,"drainQueue");
package/lib/trace-slot.js CHANGED
@@ -1,67 +1 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Children, Fragment, useEffect, useRef } from 'react';
3
- import { publish } from './publish.js';
4
- import { makeInstanceId } from './identity.js';
5
- import { useLensmcpContext } from './context.js';
6
- /**
7
- * Slot tracker. Emits `slot-content-changed` when the type of the
8
- * primary child changes. The Babel transform (T5.1) wraps JSX
9
- * conditionals with `<TraceSlot id="file:line:conditional">{...}</TraceSlot>`.
10
- */
11
- export function TraceSlot(props) {
12
- const ctx = useLensmcpContext();
13
- const lastTypeRef = useRef(undefined);
14
- const slotInstanceRef = useRef(undefined);
15
- if (!slotInstanceRef.current) {
16
- slotInstanceRef.current = makeInstanceId(`react:slot:${props.id}`).instanceId;
17
- }
18
- const slotInstanceId = slotInstanceRef.current;
19
- const currentType = describeChild(props.children);
20
- useEffect(() => {
21
- if (lastTypeRef.current !== currentType) {
22
- const fromComponent = lastTypeRef.current;
23
- lastTypeRef.current = currentType;
24
- publish({
25
- source: 'react',
26
- category: 'render',
27
- severity: 'info',
28
- title: `slot ${props.id}: ${fromComponent ?? '(none)'} → ${currentType}`,
29
- fingerprint: `slot-change:${props.id}`,
30
- raw: {
31
- kind: 'slot-content-changed',
32
- slot: {
33
- slotLogicalId: `react:slot:${props.id}`,
34
- slotInstanceId,
35
- fromComponent,
36
- toComponent: currentType,
37
- componentInstanceId: ctx.componentInstanceId,
38
- },
39
- },
40
- });
41
- }
42
- }, [currentType, props.id, slotInstanceId, ctx.componentInstanceId]);
43
- // Stable wrapper so React doesn't think the children's parent changed
44
- // when the slot remounts — we want layout to feel transparent.
45
- return _jsx(Fragment, { children: props.children });
46
- }
47
- function describeChild(children) {
48
- const arr = Children.toArray(children);
49
- const first = arr[0];
50
- if (!first)
51
- return '(empty)';
52
- if (typeof first === 'string' || typeof first === 'number')
53
- return '(text)';
54
- if (typeof first === 'boolean')
55
- return '(boolean)';
56
- if (Array.isArray(first))
57
- return '(array)';
58
- if (typeof first === 'object' && first && 'type' in first) {
59
- const t = first.type;
60
- if (typeof t === 'string')
61
- return t;
62
- if (typeof t === 'function')
63
- return t.displayName ?? t.name ?? 'AnonymousComponent';
64
- return 'unknown';
65
- }
66
- return 'unknown';
67
- }
1
+ "use strict";var m=Object.defineProperty;var o=(e,n)=>m(e,"name",{value:n,configurable:!0});var p=Object.defineProperty,a=o((e,n)=>p(e,"name",{value:n,configurable:!0}),"c");import{jsx as d}from"react/jsx-runtime";import{Children as l,Fragment as y,useEffect as I,useRef as u}from"react";import{publish as g}from"./publish.js";import{makeInstanceId as b}from"./identity.js";import{useLensmcpContext as h}from"./context.js";export function TraceSlot(e){const n=h(),t=u(void 0),c=u(void 0);c.current||(c.current=b(`react:slot:${e.id}`).instanceId);const i=c.current,r=f(e.children);return I(()=>{if(t.current!==r){const s=t.current;t.current=r,g({source:"react",category:"render",severity:"info",title:`slot ${e.id}: ${s??"(none)"} \u2192 ${r}`,fingerprint:`slot-change:${e.id}`,raw:{kind:"slot-content-changed",slot:{slotLogicalId:`react:slot:${e.id}`,slotInstanceId:i,fromComponent:s,toComponent:r,componentInstanceId:n.componentInstanceId}}})}},[r,e.id,i,n.componentInstanceId]),d(y,{children:e.children})}o(TraceSlot,"TraceSlot"),a(TraceSlot,"TraceSlot");function f(e){const n=l.toArray(e)[0];if(!n)return"(empty)";if(typeof n=="string"||typeof n=="number")return"(text)";if(typeof n=="boolean")return"(boolean)";if(Array.isArray(n))return"(array)";if(typeof n=="object"&&n&&"type"in n){const t=n.type;return typeof t=="string"?t:typeof t=="function"?t.displayName??t.name??"AnonymousComponent":"unknown"}return"unknown"}o(f,"g"),a(f,"describeChild");