@yoltra/devtools-browser-agent 0.3.0 → 0.4.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/dist/devtools-browser-agent.cjs.js +1 -1
- package/dist/devtools-browser-agent.cjs.js.map +1 -1
- package/dist/devtools-browser-agent.esm.js +218 -149
- package/dist/devtools-browser-agent.esm.js.map +1 -1
- package/dist/types/index.d.ts +5 -1
- package/dist/types/postMessage-client.d.ts +45 -0
- package/dist/types/types.d.ts +60 -0
- package/package.json +18 -7
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const O=require("@yoltra/devtools-protocol"),T=require("@yoltra/core"),M="yoltra-devtools-bridge";function P(t,n){if(t===null||typeof t!="object")return!1;const s=t;return s.channel===M&&s.direction===n&&typeof s.data=="string"}const A=0,B=1,D=3;function b(t){return(n,s)=>{const c=t??globalThis.window;if(c===void 0)return{readyState:D,send:()=>{},close:()=>{},dispose:()=>{}};let r=A;const d=u=>{P(u.data,"to-page")&&s.onMessage(u.data.data)};c.addEventListener("message",d),queueMicrotask(()=>{r===A&&(r=B,s.onOpen())});const h=()=>{c.removeEventListener("message",d)};return{get readyState(){return r},send:u=>{if(r!==B)return;const E={channel:M,direction:"to-panel",data:u};c.postMessage(E,"*")},close:()=>{r!==D&&(r=D,h(),s.onClose())},dispose:h}}}const H=(t,n)=>{const s=new WebSocket(t);return s.onopen=()=>n.onOpen(),s.onmessage=c=>n.onMessage(typeof c.data=="string"?c.data:String(c.data)),s.onclose=()=>n.onClose(),s.onerror=()=>n.onError(),{get readyState(){return s.readyState},send:c=>s.send(c),close:(c,r)=>s.close(c,r),dispose:()=>{s.onopen=null,s.onmessage=null,s.onclose=null,s.onerror=null}}};class U extends O.ReconnectingWsClient{constructor(n,s,c,r,d=H){super(n,s,c,r,d)}}const $="__YOLTRA_DEVTOOLS_BRIDGE__",J=6*1024*1024;function Y(t){if(t.socketFactory!==void 0)return t.socketFactory;const n=t.transport??"auto";return n==="websocket"?void 0:n==="bridge"||typeof globalThis=="object"&&globalThis[$]===!0?b():void 0}function q(t,n){const s=n.storeId??t.name,c=n.host??"localhost";let r=0,d=0,h=0;const u=[];let E=0;const f=new Map,R=new Map,x=(a,o)=>{const e=S.sampling;if(!e)return!1;const i=`${a}::${o}`,m=l=>l.some(([y,v])=>(y==="*"||y===a)&&(v==="*"||v===o));if(e.ignore?.some(l=>m(l.keys)))return!0;for(const l of e.throttle??[]){if(!m(l.keys))continue;const y=R.get(i)??0,v=Date.now();return v-y<l.intervalMs?!0:(R.set(i,v),!1)}for(const l of e.skip??[]){if(!m(l.keys))continue;const y=(f.get(i)??0)+1;return y<l.every?(f.set(i,y),!0):(f.set(i,0),!1)}return!1},S={replay:n.allowReplay??!1,stateSnapshot:!0,subscriptionMeta:!0,pipelineMeta:!0,emit:n.allowEmit??!1};n.sampling&&(S.sampling=n.sampling);const k=n.maxSnapshotBytes??J,_=n.sanitize,w=_===void 0?{}:{sanitize:_},L=a=>_===void 0?{}:{sanitize:(o,e)=>_(`${a}${o}`,e)},p=new U(s,t.name,S,{autoReconnect:n.autoReconnect??!0,maxReconnectAttempts:n.maxReconnectAttempts??1/0,baseDelay:n.baseDelay??1e3,maxDelay:n.maxDelay??3e4,...n.authToken!==void 0?{authToken:n.authToken}:{}},Y(n));let C=0;p.onBackpressure(a=>{const o=Date.now();o-C>5e3&&(C=o,console.warn(`[Yoltra DevTools] Backpressure: dropped ${a} event(s) while the hub was unreachable.`))});const g=()=>({timestamp:new Date().toISOString(),sourceId:s,sourceRole:O.DevtoolsRole.STORE});p.onMessage(async a=>{let o;try{o=JSON.parse(a)}catch{return}if(!(o===null||typeof o!="object"||typeof o.type!="string"))try{switch(o.type){case"REQUEST_STATE":{const e=t.getState(),i=T.encodeStateBounded(e,k,w),m={type:"STATE_SNAPSHOT",...g(),storeId:s,state:i.value,version:r,reducerNames:Object.keys(e),...i.truncated?{truncated:!0,...i.note!==void 0?{truncationNote:i.note}:{}}:{}};p.send(JSON.stringify(m));break}case"REQUEST_METRICS":{const e=t.__devtoolsIntrospect(),i=Date.now()-1e3,m=u.filter(j=>j>=i).length,l=Math.max(0,h-d),y=d>0?E/d:0,v={type:"STORE_METRICS",...g(),storeId:s,metrics:{eventCount:d,eventsPerSecond:m,avgProcessingTimeMs:y,dedupHits:e.dedupHits,queueDepth:e.queueDepth,reducerCount:e.reducers.length,effectCount:e.effects.length,middlewareCount:e.middleware.length,subscriberCount:e.event.length+e.coarse,connectorCount:e.atomic.length,middlewareRejections:l}};p.send(JSON.stringify(v));break}case"TIME_TRAVEL":{if(!S.replay||o.state==null)break;t.__applyExternalState(T.decodeState(o.state)),r=o.snapshotVersion??r;const e=t.getState(),i=T.encodeStateBounded(e,k,w),m={type:"STATE_SNAPSHOT",...g(),storeId:s,state:i.value,version:r,reducerNames:Object.keys(e)};p.send(JSON.stringify(m));break}case"EVENT_REPLAY":{S.replay&&t.__replayEvents(o.snapshot,o.events);break}case"EMIT_TO_STORE":{S.emit&&o.event&&await t.emit(o.event.channel,o.event.type,o.event.payload);break}case"REQUEST_SUBSCRIPTIONS":{const e=t.__devtoolsIntrospect(),i={type:"STORE_SUBSCRIPTIONS",...g(),storeId:s,atomic:e.atomic,event:e.event,coarse:e.coarse,effects:e.effects,middleware:e.middleware,reducers:e.reducers};p.send(JSON.stringify(i));break}default:{console.warn(`[Yoltra DevTools] Ignoring unknown message type: ${String(o.type)}`);break}}}catch(e){console.error(`[Yoltra DevTools] Command "${String(o.type)}" failed:`,e)}});const N=t.__yoltraDevtoolsDispose;N&&N();const F=t.instrument(a=>{if(h++,a.committed){d++,E+=a.reduceTimeMs;const e=Date.now();for(u.push(e);u.length>0&&e-u[0]>6e4;)u.shift()}if(x(a.event.channel,a.event.type))return;a.committed&&r++;const o={type:"STORE_EVENT",...g(),storeId:s,event:{id:a.event.id,channel:a.event.channel,type:a.event.type,payload:T.encodeState(a.event.payload,w).value},patches:a.committed?O.patchesFromChange(a.changedPaths,a.prevValues,a.nextValues).map(e=>"value"in e?{...e,value:T.encodeState(e.value,L(e.path)).value}:e):[],snapshotVersion:r,committed:a.committed};p.send(JSON.stringify(o))});p.connect(c,n.port);const I=()=>{F(),p.disconnect(),t.__yoltraDevtoolsDispose=void 0};t.__yoltraDevtoolsDispose=I;const V=t.dispose.bind(t);return t.dispose=()=>{I(),V()},t}exports.BRIDGE_CHANNEL=M;exports.createPostMessageSocketFactory=b;exports.isBridgeMessage=P;exports.withDevtools=q;
|
|
2
2
|
//# sourceMappingURL=devtools-browser-agent.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools-browser-agent.cjs.js","sources":["../src/ws-client.ts","../src/withDevtools.ts"],"sourcesContent":["/**\n * Browser WebSocket transport for the DevTools agent.\n *\n * @remarks\n * Injects the native browser `WebSocket` into the shared, transport-agnostic\n * {@link ReconnectingWsClient} from `@yoltra/devtools-protocol`. There is **no**\n * `ws` dependency here — that is what keeps this package installable in the\n * browser without pulling in a Node-only transport.\n *\n * @module @yoltra/devtools-browser-agent\n */\n\nimport {\n ReconnectingWsClient,\n type DevtoolsSocketFactory,\n type ReconnectingWsConfig,\n type StoreCapabilities,\n} from \"@yoltra/devtools-protocol\";\n\nexport type { ConnectionState } from \"@yoltra/devtools-protocol\";\n\n/** Opens a native browser WebSocket and adapts it to the shared transport. */\nconst browserSocketFactory: DevtoolsSocketFactory = (url, callbacks) => {\n const ws = new WebSocket(url);\n ws.onopen = () => callbacks.onOpen();\n ws.onmessage = (ev: MessageEvent) =>\n callbacks.onMessage(typeof ev.data === \"string\" ? ev.data : String(ev.data));\n ws.onclose = () => callbacks.onClose();\n ws.onerror = () => callbacks.onError();\n return {\n get readyState() {\n return ws.readyState;\n },\n send: (data) => ws.send(data),\n close: (code, reason) => ws.close(code, reason),\n dispose: () => {\n ws.onopen = null;\n ws.onmessage = null;\n ws.onclose = null;\n ws.onerror = null;\n },\n };\n};\n\n/**\n * Browser DevTools WebSocket client (native `WebSocket` transport).\n *\n * @internal\n */\nexport class DevtoolsWsClient extends ReconnectingWsClient {\n constructor(\n storeId: string,\n storeName: string,\n capabilities: StoreCapabilities,\n config: ReconnectingWsConfig,\n socketFactory: DevtoolsSocketFactory = browserSocketFactory,\n ) {\n super(storeId, storeName, capabilities, config, socketFactory);\n }\n}\n","/**\n * Browser DevTools agent entry point -- the {@link withDevtools} wrapper function.\n *\n * @module @yoltra/devtools-browser-agent\n */\n\nimport type { EventMapBase, InstrumentedEvent, StoreInstance } from \"@yoltra/core\";\nimport {\n DevtoolsRole,\n patchesFromChange,\n type BaseMessage,\n type StateSnapshot,\n type StoreCapabilities,\n type StoreEvent,\n type StoreMetrics,\n} from \"@yoltra/devtools-protocol\";\nimport type { DevtoolsWrapperConfig } from \"./types\";\nimport { DevtoolsWsClient } from \"./ws-client\";\n\n/**\n * Wraps a Yoltra store with DevTools instrumentation for browser environments.\n *\n * @remarks\n * - Connects to the DevTools hub via native `WebSocket`.\n * - Observes every event via the typed `store.instrument()` seam — no\n * `as any` bridging, no re-diffing, no full-state clone per event.\n * - Builds precise RFC 6902 patches from the exact changed leaf paths the core\n * reports, and sends a `STORE_EVENT` per event (committed or vetoed).\n * - Handles incoming commands: REQUEST_STATE, REQUEST_METRICS,\n * REQUEST_SUBSCRIPTIONS, TIME_TRAVEL, EVENT_REPLAY, EMIT_TO_STORE.\n * - Returns the **same** store instance (transparent instrumentation).\n * - Auto-reconnects to the hub on disconnect.\n *\n * @typeParam R - Reducer name union.\n * @typeParam S - State record.\n * @typeParam EM - Event map.\n * @param store - The store to instrument.\n * @param config - DevTools wrapper configuration.\n * @returns The same store instance, now instrumented.\n *\n * @example\n * ```ts\n * import { createStore } from '@yoltra/core';\n * import { withDevtools } from '@yoltra/devtools-browser-agent';\n *\n * const store = createStore({ name: 'App', reducer: { ... } });\n * withDevtools(store, { port: 9800 });\n * ```\n *\n * @public\n */\nexport function withDevtools<\n R extends string,\n S extends Record<R, any>,\n EM extends EventMapBase,\n>(store: StoreInstance<R, S, EM>, config: DevtoolsWrapperConfig): StoreInstance<R, S, EM> {\n const storeId = config.storeId ?? store.name;\n const host = config.host ?? \"localhost\";\n let snapshotVersion = 0;\n\n // ── Metrics tracking ──────────────────────────────────────────────────────\n // Counts committed events (events that pass middleware and run reducers).\n let committedEventCount = 0;\n // Counts ALL observed events including those vetoed by middleware.\n let totalAttemptedCount = 0;\n // Timestamps (epoch ms) of recent committed events; pruned to last 60 s.\n const eventTimestamps: number[] = [];\n // Rolling sum of the core-reported reduce time (ms) over committed events.\n let reduceTimeSumMs = 0;\n\n // ── Sampling state ─────────────────────────────────────────────────────────\n // Tracks per-event-key counters and timestamps to enforce the three sampling\n // rule types declared in `capabilities.sampling`:\n // ignore — never forward matching events.\n // skip — forward every Nth matching event (0-based counter).\n // throttle — forward at most once per intervalMs per key.\n const skipCounters = new Map<string, number>(); // current invocation count per key\n const throttleLast = new Map<string, number>(); // last-forwarded epoch ms per key\n\n /**\n * Returns `true` when the event should be suppressed by the sampling config.\n * All three rule types are checked in priority order: ignore → throttle → skip.\n */\n const isSampledOut = (channel: string, type: string): boolean => {\n const sampling = capabilities.sampling;\n if (!sampling) return false;\n\n const eventKey = `${channel}::${type}`;\n\n // Helper: check if any rule's keys array matches this event.\n const matches = (keys: Array<[string, string]>): boolean =>\n keys.some(([c, t]) => (c === \"*\" || c === channel) && (t === \"*\" || t === type));\n\n // 1. Ignore — never forward.\n if (sampling.ignore?.some((r) => matches(r.keys))) return true;\n\n // 2. Throttle — forward only if intervalMs has elapsed since the last send.\n for (const rule of sampling.throttle ?? []) {\n if (!matches(rule.keys)) continue;\n const last = throttleLast.get(eventKey) ?? 0;\n const nowMs = Date.now();\n if (nowMs - last < rule.intervalMs) return true; // within throttle window\n throttleLast.set(eventKey, nowMs);\n return false; // passed throttle; no further rules apply for this event\n }\n\n // 3. Skip — forward every Nth event (counter resets to 0 after firing).\n for (const rule of sampling.skip ?? []) {\n if (!matches(rule.keys)) continue;\n const count = (skipCounters.get(eventKey) ?? 0) + 1;\n if (count < rule.every) {\n skipCounters.set(eventKey, count);\n return true; // suppress until we reach the Nth event\n }\n skipCounters.set(eventKey, 0); // fire — reset counter\n return false;\n }\n\n return false;\n };\n\n // Build capabilities from config\n const capabilities: StoreCapabilities = {\n replay: config.allowReplay ?? false,\n stateSnapshot: true,\n subscriptionMeta: true,\n pipelineMeta: true,\n emit: config.allowEmit ?? false,\n };\n if (config.sampling) {\n capabilities.sampling = config.sampling;\n }\n\n // Create browser WS client\n const wsClient = new DevtoolsWsClient(\n storeId,\n store.name,\n capabilities,\n {\n autoReconnect: config.autoReconnect ?? true,\n maxReconnectAttempts: config.maxReconnectAttempts ?? Infinity,\n baseDelay: config.baseDelay ?? 1000,\n maxDelay: config.maxDelay ?? 30000,\n },\n config.socketFactory,\n );\n\n // Surface backpressure: warn (throttled) instead of dropping events silently\n // when the hub is unreachable and the send buffer overflows.\n let lastBackpressureWarn = 0;\n wsClient.onBackpressure((dropped) => {\n const nowMs = Date.now();\n if (nowMs - lastBackpressureWarn > 5_000) {\n lastBackpressureWarn = nowMs;\n console.warn(\n `[Yoltra DevTools] Backpressure: dropped ${dropped} event(s) while the hub was unreachable.`,\n );\n }\n });\n\n const baseMsg = (): Pick<BaseMessage, \"timestamp\" | \"sourceId\" | \"sourceRole\"> => ({\n timestamp: new Date().toISOString(),\n sourceId: storeId,\n sourceRole: DevtoolsRole.STORE,\n });\n\n // Handle incoming messages from hub\n wsClient.onMessage(async (data: string) => {\n let msg: any;\n try {\n msg = JSON.parse(data);\n } catch {\n return;\n }\n\n // Ingress validation (DEV-3): require a well-formed message with a string\n // `type` discriminant before acting on it (EMIT_TO_STORE forwards straight\n // into store.emit, so a malformed payload must not reach it).\n if (msg === null || typeof msg !== \"object\" || typeof msg.type !== \"string\") return;\n\n switch (msg.type) {\n case \"REQUEST_STATE\": {\n const state = store.getState();\n const response: StateSnapshot = {\n type: \"STATE_SNAPSHOT\",\n ...baseMsg(),\n storeId,\n state: JSON.parse(JSON.stringify(state)),\n version: snapshotVersion,\n reducerNames: Object.keys(state as object),\n };\n wsClient.send(JSON.stringify(response));\n break;\n }\n\n case \"REQUEST_METRICS\": {\n const introspection = store.__devtoolsIntrospect();\n // Events committed in the last 1 second (sliding window).\n const cutoffMs = Date.now() - 1_000;\n const eventsPerSecond = eventTimestamps.filter((t) => t >= cutoffMs).length;\n // Middleware rejections = attempted − committed (floor at 0).\n const middlewareRejections = Math.max(0, totalAttemptedCount - committedEventCount);\n // Real reduce-phase timing, averaged over committed events.\n const avgProcessingTimeMs =\n committedEventCount > 0 ? reduceTimeSumMs / committedEventCount : 0;\n const response: StoreMetrics = {\n type: \"STORE_METRICS\",\n ...baseMsg(),\n storeId,\n metrics: {\n eventCount: committedEventCount,\n eventsPerSecond,\n avgProcessingTimeMs,\n dedupHits: introspection.dedupHits,\n queueDepth: introspection.queueDepth,\n reducerCount: introspection.reducers.length,\n effectCount: introspection.effects.length,\n middlewareCount: introspection.middleware.length,\n subscriberCount: introspection.event.length + introspection.coarse,\n connectorCount: introspection.atomic.length,\n middlewareRejections,\n },\n };\n wsClient.send(JSON.stringify(response));\n break;\n }\n\n case \"TIME_TRAVEL\": {\n // Time-travel replaces the entire state tree — gate on the store's\n // replay capability (default off), same as EVENT_REPLAY. The core seam\n // enforces this too (defense in depth).\n if (!capabilities.replay) break;\n\n // Guard against malformed messages — a null state would corrupt the\n // store and cause \"Cannot read property of undefined\" in reducers.\n if (msg.state == null) break;\n\n store.__applyExternalState(msg.state);\n snapshotVersion = msg.snapshotVersion ?? snapshotVersion;\n\n // Notify the extension of the new state so all UI panels re-render with\n // the time-traveled state.\n const traveledState = store.getState();\n const travelSnapshot: StateSnapshot = {\n type: \"STATE_SNAPSHOT\",\n ...baseMsg(),\n storeId,\n state: JSON.parse(JSON.stringify(traveledState)),\n version: snapshotVersion,\n reducerNames: Object.keys(traveledState as object),\n };\n wsClient.send(JSON.stringify(travelSnapshot));\n break;\n }\n\n case \"EVENT_REPLAY\": {\n if (capabilities.replay) {\n store.__replayEvents(msg.snapshot, msg.events);\n }\n break;\n }\n\n case \"EMIT_TO_STORE\": {\n if (capabilities.emit && msg.event) {\n await store.emit(msg.event.channel, msg.event.type, msg.event.payload);\n }\n break;\n }\n\n case \"REQUEST_SUBSCRIPTIONS\": {\n const introspection = store.__devtoolsIntrospect();\n const response = {\n type: \"STORE_SUBSCRIPTIONS\",\n ...baseMsg(),\n storeId,\n atomic: introspection.atomic,\n event: introspection.event,\n coarse: introspection.coarse,\n effects: introspection.effects,\n middleware: introspection.middleware,\n reducers: introspection.reducers,\n };\n wsClient.send(JSON.stringify(response));\n break;\n }\n\n default: {\n // Exhaustiveness fallback (DEV-3): an unhandled command type is protocol\n // drift (a bug), not routine traffic — surface it instead of dropping it.\n console.warn(`[Yoltra DevTools] Ignoring unknown message type: ${String(msg.type)}`);\n break;\n }\n }\n });\n\n // If this store was already wrapped (HMR / remount / a double call), tear down\n // the previous devtools attachment first so we don't leak the instrument\n // observer + reconnecting socket or double-send every event (DEV-2).\n const existingDispose = (store as unknown as { __yoltraDevtoolsDispose?: () => void })\n .__yoltraDevtoolsDispose;\n if (existingDispose) existingDispose();\n\n // Observe every event through the typed instrumentation seam. This single\n // observer replaces the old interceptor effect + metrics middleware + manual\n // diff + full-state clone: the core hands us the exact changed leaf paths and\n // their old/new values, so we build precise patches with no re-diff.\n const instrumentUnsub = store.instrument((info: InstrumentedEvent<EM>) => {\n totalAttemptedCount++;\n if (info.committed) {\n committedEventCount++;\n reduceTimeSumMs += info.reduceTimeMs;\n const nowMs = Date.now();\n eventTimestamps.push(nowMs);\n // Prune timestamps older than 60 s to bound memory usage.\n while (eventTimestamps.length > 0 && nowMs - eventTimestamps[0]! > 60_000) {\n eventTimestamps.shift();\n }\n }\n\n if (isSampledOut(info.event.channel, info.event.type)) return;\n\n // Only a committed event advances the state version — it carries patches. A\n // vetoed event is logged with committed:false and no bump, so time-travel\n // reconstruction stays correlated (DEV-7). Wire ordering is preserved by the\n // event log's array insertion order, not by this version.\n if (info.committed) snapshotVersion++;\n const storeEvent: StoreEvent = {\n type: \"STORE_EVENT\",\n ...baseMsg(),\n storeId,\n event: {\n id: info.event.id,\n channel: info.event.channel,\n type: info.event.type,\n payload: info.event.payload,\n },\n patches: info.committed\n ? patchesFromChange(info.changedPaths, info.prevValues, info.nextValues)\n : [],\n snapshotVersion,\n committed: info.committed,\n };\n wsClient.send(JSON.stringify(storeEvent));\n });\n\n // Connect to hub\n wsClient.connect(host, config.port);\n\n // Devtools teardown (DEV-2): detach the instrument observer and disconnect the\n // socket. Stored on the store so a later re-wrap tears down this attachment,\n // and folded into store.dispose() so disposing the store also detaches devtools.\n const disposeDevtools = () => {\n instrumentUnsub();\n wsClient.disconnect();\n (store as unknown as { __yoltraDevtoolsDispose?: () => void }).__yoltraDevtoolsDispose =\n undefined;\n };\n (store as unknown as { __yoltraDevtoolsDispose?: () => void }).__yoltraDevtoolsDispose =\n disposeDevtools;\n\n const prevDispose = store.dispose.bind(store);\n store.dispose = () => {\n disposeDevtools();\n prevDispose();\n };\n\n return store;\n}\n"],"names":["browserSocketFactory","url","callbacks","ws","ev","data","code","reason","DevtoolsWsClient","ReconnectingWsClient","storeId","storeName","capabilities","config","socketFactory","withDevtools","store","host","snapshotVersion","committedEventCount","totalAttemptedCount","eventTimestamps","reduceTimeSumMs","skipCounters","throttleLast","isSampledOut","channel","type","sampling","eventKey","matches","keys","c","t","r","rule","last","nowMs","count","wsClient","lastBackpressureWarn","dropped","baseMsg","DevtoolsRole","msg","state","response","introspection","cutoffMs","eventsPerSecond","middlewareRejections","avgProcessingTimeMs","traveledState","travelSnapshot","existingDispose","instrumentUnsub","info","storeEvent","patchesFromChange","disposeDevtools","prevDispose"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"devtools-browser-agent.cjs.js","sources":["../src/postMessage-client.ts","../src/ws-client.ts","../src/withDevtools.ts"],"sourcesContent":["/**\n * `postMessage` transport, for attaching without a hub.\n *\n * @remarks\n * Attaching the panel used to mean running a hub process, editing the application to add\n * `withDevtools()`, and setting `allowReplay` — three steps at the exact moment somebody is\n * deciding whether the tool is worth it, against Redux DevTools' one. The hub earns its keep for\n * Node processes and remote debugging, where a socket is the only way in. For a page being\n * inspected by an extension running in the same browser, it is a server standing between two\n * things already able to talk.\n *\n * This transport is the page half. It frames the same protocol into `window.postMessage`, where\n * a content script can relay it to the panel. Nothing here knows about extensions: it posts to\n * its own window and listens on it, which keeps the module testable and lets any relay — a\n * content script, an iframe host, a test — sit on the other side.\n *\n * @module @yoltra/devtools-browser-agent\n */\n\nimport type {\n DevtoolsSocketCallbacks,\n DevtoolsSocketFactory,\n DevtoolsSocketHandle,\n} from \"@yoltra/devtools-protocol\";\n\n/** Marks a message as belonging to this bridge, so unrelated `postMessage` traffic is ignored. */\nexport const BRIDGE_CHANNEL = \"yoltra-devtools-bridge\" as const;\n\n/** Direction of a bridged frame. */\nexport type BridgeDirection = \"to-panel\" | \"to-page\";\n\n/** One protocol frame travelling over `window.postMessage`. */\nexport interface BridgeMessage {\n readonly channel: typeof BRIDGE_CHANNEL;\n readonly direction: BridgeDirection;\n /** A serialized DevTools protocol message. */\n readonly data: string;\n}\n\n/** `true` when `value` is a bridge frame travelling in `direction`. */\nexport function isBridgeMessage(value: unknown, direction: BridgeDirection): value is BridgeMessage {\n if (value === null || typeof value !== \"object\") return false;\n const msg = value as Record<string, unknown>;\n return (\n msg.channel === BRIDGE_CHANNEL && msg.direction === direction && typeof msg.data === \"string\"\n );\n}\n\n/** The window-like surface this transport needs, so a test can supply its own. */\nexport interface BridgeWindow {\n postMessage(message: unknown, targetOrigin: string): void;\n addEventListener(type: \"message\", listener: (event: MessageEvent) => void): void;\n removeEventListener(type: \"message\", listener: (event: MessageEvent) => void): void;\n}\n\nconst CONNECTING = 0;\nconst OPEN = 1;\nconst CLOSED = 3;\n\n/**\n * Builds a socket factory that carries the protocol over `window.postMessage`.\n *\n * @param target - Window to post to and listen on. Defaults to the global `window`.\n * @returns A factory for `withDevtools({ socketFactory })`.\n *\n * @remarks\n * The socket reports itself open immediately, because `postMessage` has no connection to\n * establish. That is not a claim that anybody is listening — if no relay is present the protocol\n * handshake simply goes unanswered, the client never resolves it, and messages buffer exactly as\n * they do against a hub that is not running. Failure to attach therefore looks the same whether\n * the panel is absent or the hub is down, which is one behaviour to explain rather than two.\n *\n * Messages are posted with a `\"*\"` target origin and filtered by channel on receipt. The page is\n * posting to *itself* for a content script in the same frame to observe; there is no\n * cross-origin recipient to restrict to, and the content script is the trust boundary.\n *\n * @example\n * ```ts\n * withDevtools(store, { socketFactory: createPostMessageSocketFactory() });\n * ```\n *\n * @public\n */\nexport function createPostMessageSocketFactory(target?: BridgeWindow): DevtoolsSocketFactory {\n return (_url: string, callbacks: DevtoolsSocketCallbacks): DevtoolsSocketHandle => {\n const win = target ?? (globalThis as unknown as { window?: BridgeWindow }).window;\n if (win === undefined) {\n // No window to post into: report a closed socket rather than throwing into whatever\n // called us. The client treats it as a failed connection and backs off.\n return {\n readyState: CLOSED,\n send: () => undefined,\n close: () => undefined,\n dispose: () => undefined,\n };\n }\n\n let readyState: number = CONNECTING;\n\n const onMessage = (event: MessageEvent): void => {\n // Only frames addressed to the page, on our channel. Everything else on this window —\n // application traffic, other tools — is none of our business.\n if (!isBridgeMessage(event.data, \"to-page\")) return;\n callbacks.onMessage(event.data.data);\n };\n\n win.addEventListener(\"message\", onMessage);\n\n // Opened on a microtask rather than synchronously, so a caller can finish wiring its\n // handlers before the first callback arrives.\n queueMicrotask(() => {\n if (readyState !== CONNECTING) return;\n readyState = OPEN;\n callbacks.onOpen();\n });\n\n const detach = (): void => {\n win.removeEventListener(\"message\", onMessage);\n };\n\n return {\n get readyState() {\n return readyState;\n },\n send: (data: string) => {\n if (readyState !== OPEN) return;\n const message: BridgeMessage = { channel: BRIDGE_CHANNEL, direction: \"to-panel\", data };\n win.postMessage(message, \"*\");\n },\n close: () => {\n if (readyState === CLOSED) return;\n readyState = CLOSED;\n detach();\n callbacks.onClose();\n },\n dispose: detach,\n };\n };\n}\n","/**\n * Browser WebSocket transport for the DevTools agent.\n *\n * @remarks\n * Injects the native browser `WebSocket` into the shared, transport-agnostic\n * {@link ReconnectingWsClient} from `@yoltra/devtools-protocol`. There is **no**\n * `ws` dependency here — that is what keeps this package installable in the\n * browser without pulling in a Node-only transport.\n *\n * @module @yoltra/devtools-browser-agent\n */\n\nimport {\n ReconnectingWsClient,\n type DevtoolsSocketFactory,\n type ReconnectingWsConfig,\n type StoreCapabilities,\n} from \"@yoltra/devtools-protocol\";\n\nexport type { ConnectionState } from \"@yoltra/devtools-protocol\";\n\n/** Opens a native browser WebSocket and adapts it to the shared transport. */\nconst browserSocketFactory: DevtoolsSocketFactory = (url, callbacks) => {\n const ws = new WebSocket(url);\n ws.onopen = () => callbacks.onOpen();\n ws.onmessage = (ev: MessageEvent) =>\n callbacks.onMessage(typeof ev.data === \"string\" ? ev.data : String(ev.data));\n ws.onclose = () => callbacks.onClose();\n ws.onerror = () => callbacks.onError();\n return {\n get readyState() {\n return ws.readyState;\n },\n send: (data) => ws.send(data),\n close: (code, reason) => ws.close(code, reason),\n dispose: () => {\n ws.onopen = null;\n ws.onmessage = null;\n ws.onclose = null;\n ws.onerror = null;\n },\n };\n};\n\n/**\n * Browser DevTools WebSocket client (native `WebSocket` transport).\n *\n * @internal\n */\nexport class DevtoolsWsClient extends ReconnectingWsClient {\n constructor(\n storeId: string,\n storeName: string,\n capabilities: StoreCapabilities,\n config: ReconnectingWsConfig,\n socketFactory: DevtoolsSocketFactory = browserSocketFactory,\n ) {\n super(storeId, storeName, capabilities, config, socketFactory);\n }\n}\n","/**\n * Browser DevTools agent entry point -- the {@link withDevtools} wrapper function.\n *\n * @module @yoltra/devtools-browser-agent\n */\n\nimport type { EventMapBase, InstrumentedEvent, StoreInstance } from \"@yoltra/core\";\nimport {\n DevtoolsRole,\n patchesFromChange,\n type BaseMessage,\n type DevtoolsSocketFactory,\n type StateSnapshot,\n type StoreCapabilities,\n type StoreEvent,\n type StoreMetrics,\n} from \"@yoltra/devtools-protocol\";\nimport { encodeState, encodeStateBounded, decodeState } from \"@yoltra/core\";\nimport { createPostMessageSocketFactory } from \"./postMessage-client\";\n\n/**\n * Global an extension's content script sets to announce that it is relaying this page.\n *\n * @remarks\n * The content script runs before application code, so by the time a store is created the mark is\n * already there. When it is absent the agent falls back to the hub, which is the right answer\n * for a page nobody is inspecting from an extension.\n */\nconst BRIDGE_MARK = \"__YOLTRA_DEVTOOLS_BRIDGE__\";\n\n/**\n * Byte budget for a state snapshot, under the hub's 8 MiB frame cap.\n *\n * @remarks\n * Deliberately below it rather than equal to it: the snapshot travels inside an envelope with a\n * store id, a version and the reducer names, and a frame that overshoots by those few hundred\n * bytes is refused exactly as one that overshoots by a megabyte.\n */\nconst DEFAULT_MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;\n\n/** Chooses the transport, honouring an explicit one above all. */\nfunction resolveSocketFactory(\n config: DevtoolsWrapperConfig,\n): DevtoolsSocketFactory | undefined {\n if (config.socketFactory !== undefined) return config.socketFactory;\n\n const mode = config.transport ?? \"auto\";\n if (mode === \"websocket\") return undefined; // the client's own default\n if (mode === \"bridge\") return createPostMessageSocketFactory();\n\n const relaying =\n typeof globalThis === \"object\" &&\n (globalThis as Record<string, unknown>)[BRIDGE_MARK] === true;\n return relaying ? createPostMessageSocketFactory() : undefined;\n}\nimport type { DevtoolsWrapperConfig } from \"./types\";\nimport { DevtoolsWsClient } from \"./ws-client\";\n\n/**\n * Wraps a Yoltra store with DevTools instrumentation for browser environments.\n *\n * @remarks\n * - Connects to the DevTools hub via native `WebSocket`.\n * - Observes every event via the typed `store.instrument()` seam — no\n * `as any` bridging, no re-diffing, no full-state clone per event.\n * - Builds precise RFC 6902 patches from the exact changed leaf paths the core\n * reports, and sends a `STORE_EVENT` per event (committed or vetoed).\n * - Handles incoming commands: REQUEST_STATE, REQUEST_METRICS,\n * REQUEST_SUBSCRIPTIONS, TIME_TRAVEL, EVENT_REPLAY, EMIT_TO_STORE.\n * - Returns the **same** store instance (transparent instrumentation).\n * - Auto-reconnects to the hub on disconnect.\n *\n * @typeParam R - Reducer name union.\n * @typeParam S - State record.\n * @typeParam EM - Event map.\n * @param store - The store to instrument.\n * @param config - DevTools wrapper configuration.\n * @returns The same store instance, now instrumented.\n *\n * @example\n * ```ts\n * import { createStore } from '@yoltra/core';\n * import { withDevtools } from '@yoltra/devtools-browser-agent';\n *\n * const store = createStore({ name: 'App', reducer: { ... } });\n * withDevtools(store, { port: 9800 });\n * ```\n *\n * @public\n */\nexport function withDevtools<\n R extends string,\n S extends Record<R, any>,\n EM extends EventMapBase,\n>(store: StoreInstance<R, S, EM>, config: DevtoolsWrapperConfig): StoreInstance<R, S, EM> {\n const storeId = config.storeId ?? store.name;\n const host = config.host ?? \"localhost\";\n let snapshotVersion = 0;\n\n // ── Metrics tracking ──────────────────────────────────────────────────────\n // Counts committed events (events that pass middleware and run reducers).\n let committedEventCount = 0;\n // Counts ALL observed events including those vetoed by middleware.\n let totalAttemptedCount = 0;\n // Timestamps (epoch ms) of recent committed events; pruned to last 60 s.\n const eventTimestamps: number[] = [];\n // Rolling sum of the core-reported reduce time (ms) over committed events.\n let reduceTimeSumMs = 0;\n\n // ── Sampling state ─────────────────────────────────────────────────────────\n // Tracks per-event-key counters and timestamps to enforce the three sampling\n // rule types declared in `capabilities.sampling`:\n // ignore — never forward matching events.\n // skip — forward every Nth matching event (0-based counter).\n // throttle — forward at most once per intervalMs per key.\n const skipCounters = new Map<string, number>(); // current invocation count per key\n const throttleLast = new Map<string, number>(); // last-forwarded epoch ms per key\n\n /**\n * Returns `true` when the event should be suppressed by the sampling config.\n * All three rule types are checked in priority order: ignore → throttle → skip.\n */\n const isSampledOut = (channel: string, type: string): boolean => {\n const sampling = capabilities.sampling;\n if (!sampling) return false;\n\n const eventKey = `${channel}::${type}`;\n\n // Helper: check if any rule's keys array matches this event.\n const matches = (keys: Array<[string, string]>): boolean =>\n keys.some(([c, t]) => (c === \"*\" || c === channel) && (t === \"*\" || t === type));\n\n // 1. Ignore — never forward.\n if (sampling.ignore?.some((r) => matches(r.keys))) return true;\n\n // 2. Throttle — forward only if intervalMs has elapsed since the last send.\n for (const rule of sampling.throttle ?? []) {\n if (!matches(rule.keys)) continue;\n const last = throttleLast.get(eventKey) ?? 0;\n const nowMs = Date.now();\n if (nowMs - last < rule.intervalMs) return true; // within throttle window\n throttleLast.set(eventKey, nowMs);\n return false; // passed throttle; no further rules apply for this event\n }\n\n // 3. Skip — forward every Nth event (counter resets to 0 after firing).\n for (const rule of sampling.skip ?? []) {\n if (!matches(rule.keys)) continue;\n const count = (skipCounters.get(eventKey) ?? 0) + 1;\n if (count < rule.every) {\n skipCounters.set(eventKey, count);\n return true; // suppress until we reach the Nth event\n }\n skipCounters.set(eventKey, 0); // fire — reset counter\n return false;\n }\n\n return false;\n };\n\n // Build capabilities from config\n const capabilities: StoreCapabilities = {\n replay: config.allowReplay ?? false,\n stateSnapshot: true,\n subscriptionMeta: true,\n pipelineMeta: true,\n emit: config.allowEmit ?? false,\n };\n if (config.sampling) {\n capabilities.sampling = config.sampling;\n }\n\n // Create browser WS client\n const maxSnapshotBytes = config.maxSnapshotBytes ?? DEFAULT_MAX_SNAPSHOT_BYTES;\n // One options object for every encode below: the redaction contract is that NOTHING the\n // agent forwards — snapshot, travel snapshot, payload, patch — skips the hook.\n const sanitize = config.sanitize;\n const encodeOptions = sanitize === undefined ? {} : { sanitize };\n // Patch values need their own wrapper: the codec's paths are relative to what it encodes,\n // and a leaf patch encodes a bare scalar whose root path is \"\" — so the hook would never\n // see `/vault/token`. Prefixing the op's own path restores the address a key-name recipe\n // matches on, for the leaf and for anything nested under a subtree replacement alike.\n const patchEncodeOptions = (opPath: string) =>\n sanitize === undefined\n ? {}\n : { sanitize: (path: string, value: unknown) => sanitize(`${opPath}${path}`, value) };\n\n const wsClient = new DevtoolsWsClient(\n storeId,\n store.name,\n capabilities,\n {\n autoReconnect: config.autoReconnect ?? true,\n maxReconnectAttempts: config.maxReconnectAttempts ?? Infinity,\n baseDelay: config.baseDelay ?? 1000,\n maxDelay: config.maxDelay ?? 30000,\n ...(config.authToken !== undefined ? { authToken: config.authToken } : {}),\n },\n resolveSocketFactory(config),\n );\n\n // Surface backpressure: warn (throttled) instead of dropping events silently\n // when the hub is unreachable and the send buffer overflows.\n let lastBackpressureWarn = 0;\n wsClient.onBackpressure((dropped) => {\n const nowMs = Date.now();\n if (nowMs - lastBackpressureWarn > 5_000) {\n lastBackpressureWarn = nowMs;\n console.warn(\n `[Yoltra DevTools] Backpressure: dropped ${dropped} event(s) while the hub was unreachable.`,\n );\n }\n });\n\n const baseMsg = (): Pick<BaseMessage, \"timestamp\" | \"sourceId\" | \"sourceRole\"> => ({\n timestamp: new Date().toISOString(),\n sourceId: storeId,\n sourceRole: DevtoolsRole.STORE,\n });\n\n // Handle incoming messages from hub\n wsClient.onMessage(async (data: string) => {\n let msg: any;\n try {\n msg = JSON.parse(data);\n } catch {\n return;\n }\n\n // Ingress validation (DEV-3): require a well-formed message with a string\n // `type` discriminant before acting on it (EMIT_TO_STORE forwards straight\n // into store.emit, so a malformed payload must not reach it).\n if (msg === null || typeof msg !== \"object\" || typeof msg.type !== \"string\") return;\n\n try {\n switch (msg.type) {\n case \"REQUEST_STATE\": {\n const state = store.getState();\n // Bounded before it is sent. A frame over the hub's cap is rejected and the\n // connection with it, so an oversized snapshot did not fail loudly — it dropped the\n // socket, the client reconnected, asked again, and the panel waited through the loop\n // with nothing on screen to explain it.\n const bounded = encodeStateBounded(state, maxSnapshotBytes, encodeOptions);\n const response: StateSnapshot = {\n type: \"STATE_SNAPSHOT\",\n ...baseMsg(),\n storeId,\n state: bounded.value,\n version: snapshotVersion,\n reducerNames: Object.keys(state as object),\n ...(bounded.truncated\n ? { truncated: true, ...(bounded.note !== undefined ? { truncationNote: bounded.note } : {}) }\n : {}),\n };\n wsClient.send(JSON.stringify(response));\n break;\n }\n\n case \"REQUEST_METRICS\": {\n const introspection = store.__devtoolsIntrospect();\n // Events committed in the last 1 second (sliding window).\n const cutoffMs = Date.now() - 1_000;\n const eventsPerSecond = eventTimestamps.filter((t) => t >= cutoffMs).length;\n // Middleware rejections = attempted − committed (floor at 0).\n const middlewareRejections = Math.max(0, totalAttemptedCount - committedEventCount);\n // Real reduce-phase timing, averaged over committed events.\n const avgProcessingTimeMs =\n committedEventCount > 0 ? reduceTimeSumMs / committedEventCount : 0;\n const response: StoreMetrics = {\n type: \"STORE_METRICS\",\n ...baseMsg(),\n storeId,\n metrics: {\n eventCount: committedEventCount,\n eventsPerSecond,\n avgProcessingTimeMs,\n dedupHits: introspection.dedupHits,\n queueDepth: introspection.queueDepth,\n reducerCount: introspection.reducers.length,\n effectCount: introspection.effects.length,\n middlewareCount: introspection.middleware.length,\n subscriberCount: introspection.event.length + introspection.coarse,\n connectorCount: introspection.atomic.length,\n middlewareRejections,\n },\n };\n wsClient.send(JSON.stringify(response));\n break;\n }\n\n case \"TIME_TRAVEL\": {\n // Time-travel replaces the entire state tree — gate on the store's\n // replay capability (default off), same as EVENT_REPLAY. The core seam\n // enforces this too (defense in depth).\n if (!capabilities.replay) break;\n\n // Guard against malformed messages — a null state would corrupt the\n // store and cause \"Cannot read property of undefined\" in reducers.\n if (msg.state == null) break;\n\n store.__applyExternalState(decodeState(msg.state) as never);\n snapshotVersion = msg.snapshotVersion ?? snapshotVersion;\n\n // Notify the extension of the new state so all UI panels re-render with\n // the time-traveled state.\n const traveledState = store.getState();\n const travelBounded = encodeStateBounded(traveledState, maxSnapshotBytes, encodeOptions);\n const travelSnapshot: StateSnapshot = {\n type: \"STATE_SNAPSHOT\",\n ...baseMsg(),\n storeId,\n state: travelBounded.value,\n version: snapshotVersion,\n reducerNames: Object.keys(traveledState as object),\n };\n wsClient.send(JSON.stringify(travelSnapshot));\n break;\n }\n\n case \"EVENT_REPLAY\": {\n if (capabilities.replay) {\n store.__replayEvents(msg.snapshot, msg.events);\n }\n break;\n }\n\n case \"EMIT_TO_STORE\": {\n if (capabilities.emit && msg.event) {\n await store.emit(msg.event.channel, msg.event.type, msg.event.payload);\n }\n break;\n }\n\n case \"REQUEST_SUBSCRIPTIONS\": {\n const introspection = store.__devtoolsIntrospect();\n const response = {\n type: \"STORE_SUBSCRIPTIONS\",\n ...baseMsg(),\n storeId,\n atomic: introspection.atomic,\n event: introspection.event,\n coarse: introspection.coarse,\n effects: introspection.effects,\n middleware: introspection.middleware,\n reducers: introspection.reducers,\n };\n wsClient.send(JSON.stringify(response));\n break;\n }\n\n default: {\n // Exhaustiveness fallback (DEV-3): an unhandled command type is protocol\n // drift (a bug), not routine traffic — surface it instead of dropping it.\n console.warn(`[Yoltra DevTools] Ignoring unknown message type: ${String(msg.type)}`);\n break;\n }\n }\n } catch (err) {\n // A command handler that throws must not become an unhandled rejection: this\n // callback is async and nothing awaits it, so the failure would surface far from\n // here — or nowhere at all — and the panel would sit waiting for a reply that\n // never comes. Report it and keep the connection usable.\n console.error(\n `[Yoltra DevTools] Command \"${String(msg.type)}\" failed:`,\n err,\n );\n }\n });\n\n // If this store was already wrapped (HMR / remount / a double call), tear down\n // the previous devtools attachment first so we don't leak the instrument\n // observer + reconnecting socket or double-send every event (DEV-2).\n const existingDispose = (store as unknown as { __yoltraDevtoolsDispose?: () => void })\n .__yoltraDevtoolsDispose;\n if (existingDispose) existingDispose();\n\n // Observe every event through the typed instrumentation seam. This single\n // observer replaces the old interceptor effect + metrics middleware + manual\n // diff + full-state clone: the core hands us the exact changed leaf paths and\n // their old/new values, so we build precise patches with no re-diff.\n const instrumentUnsub = store.instrument((info: InstrumentedEvent<EM>) => {\n totalAttemptedCount++;\n if (info.committed) {\n committedEventCount++;\n reduceTimeSumMs += info.reduceTimeMs;\n const nowMs = Date.now();\n eventTimestamps.push(nowMs);\n // Prune timestamps older than 60 s to bound memory usage.\n while (eventTimestamps.length > 0 && nowMs - eventTimestamps[0]! > 60_000) {\n eventTimestamps.shift();\n }\n }\n\n if (isSampledOut(info.event.channel, info.event.type)) return;\n\n // Only a committed event advances the state version — it carries patches. A\n // vetoed event is logged with committed:false and no bump, so time-travel\n // reconstruction stays correlated (DEV-7). Wire ordering is preserved by the\n // event log's array insertion order, not by this version.\n if (info.committed) snapshotVersion++;\n const storeEvent: StoreEvent = {\n type: \"STORE_EVENT\",\n ...baseMsg(),\n storeId,\n event: {\n id: info.event.id,\n channel: info.event.channel,\n type: info.event.type,\n // Encoded like state: a payload is arbitrary application data, so it can hold the same\n // Map, BigInt or cycle that made a bare stringify throw or quietly destroy it.\n payload: encodeState(info.event.payload, encodeOptions).value,\n },\n patches: info.committed\n ? patchesFromChange(info.changedPaths, info.prevValues, info.nextValues).map((op) =>\n \"value\" in op\n ? { ...op, value: encodeState(op.value, patchEncodeOptions(op.path)).value }\n : op,\n )\n : [],\n snapshotVersion,\n committed: info.committed,\n };\n wsClient.send(JSON.stringify(storeEvent));\n });\n\n // Connect to hub\n wsClient.connect(host, config.port);\n\n // Devtools teardown (DEV-2): detach the instrument observer and disconnect the\n // socket. Stored on the store so a later re-wrap tears down this attachment,\n // and folded into store.dispose() so disposing the store also detaches devtools.\n const disposeDevtools = () => {\n instrumentUnsub();\n wsClient.disconnect();\n (store as unknown as { __yoltraDevtoolsDispose?: () => void }).__yoltraDevtoolsDispose =\n undefined;\n };\n (store as unknown as { __yoltraDevtoolsDispose?: () => void }).__yoltraDevtoolsDispose =\n disposeDevtools;\n\n const prevDispose = store.dispose.bind(store);\n store.dispose = () => {\n disposeDevtools();\n prevDispose();\n };\n\n return store;\n}\n"],"names":["BRIDGE_CHANNEL","isBridgeMessage","value","direction","msg","CONNECTING","OPEN","CLOSED","createPostMessageSocketFactory","target","_url","callbacks","win","readyState","onMessage","event","detach","data","message","browserSocketFactory","url","ws","ev","code","reason","DevtoolsWsClient","ReconnectingWsClient","storeId","storeName","capabilities","config","socketFactory","BRIDGE_MARK","DEFAULT_MAX_SNAPSHOT_BYTES","resolveSocketFactory","mode","withDevtools","store","host","snapshotVersion","committedEventCount","totalAttemptedCount","eventTimestamps","reduceTimeSumMs","skipCounters","throttleLast","isSampledOut","channel","type","sampling","eventKey","matches","keys","c","t","r","rule","last","nowMs","count","maxSnapshotBytes","sanitize","encodeOptions","patchEncodeOptions","opPath","path","wsClient","lastBackpressureWarn","dropped","baseMsg","DevtoolsRole","state","bounded","encodeStateBounded","response","introspection","cutoffMs","eventsPerSecond","middlewareRejections","avgProcessingTimeMs","decodeState","traveledState","travelBounded","travelSnapshot","err","existingDispose","instrumentUnsub","info","storeEvent","encodeState","patchesFromChange","op","disposeDevtools","prevDispose"],"mappings":"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"}
|
|
@@ -1,199 +1,268 @@
|
|
|
1
|
-
import { ReconnectingWsClient as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { ReconnectingWsClient as U, DevtoolsRole as $, patchesFromChange as j } from "@yoltra/devtools-protocol";
|
|
2
|
+
import { decodeState as H, encodeStateBounded as k, encodeState as C } from "@yoltra/core";
|
|
3
|
+
const B = "yoltra-devtools-bridge";
|
|
4
|
+
function J(t, n) {
|
|
5
|
+
if (t === null || typeof t != "object") return !1;
|
|
6
|
+
const s = t;
|
|
7
|
+
return s.channel === B && s.direction === n && typeof s.data == "string";
|
|
8
|
+
}
|
|
9
|
+
const N = 0, I = 1, w = 3;
|
|
10
|
+
function A(t) {
|
|
11
|
+
return (n, s) => {
|
|
12
|
+
const c = t ?? globalThis.window;
|
|
13
|
+
if (c === void 0)
|
|
14
|
+
return {
|
|
15
|
+
readyState: w,
|
|
16
|
+
send: () => {
|
|
17
|
+
},
|
|
18
|
+
close: () => {
|
|
19
|
+
},
|
|
20
|
+
dispose: () => {
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
let r = N;
|
|
24
|
+
const u = (d) => {
|
|
25
|
+
J(d.data, "to-page") && s.onMessage(d.data.data);
|
|
26
|
+
};
|
|
27
|
+
c.addEventListener("message", u), queueMicrotask(() => {
|
|
28
|
+
r === N && (r = I, s.onOpen());
|
|
29
|
+
});
|
|
30
|
+
const h = () => {
|
|
31
|
+
c.removeEventListener("message", u);
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
get readyState() {
|
|
35
|
+
return r;
|
|
36
|
+
},
|
|
37
|
+
send: (d) => {
|
|
38
|
+
if (r !== I) return;
|
|
39
|
+
const T = { channel: B, direction: "to-panel", data: d };
|
|
40
|
+
c.postMessage(T, "*");
|
|
41
|
+
},
|
|
42
|
+
close: () => {
|
|
43
|
+
r !== w && (r = w, h(), s.onClose());
|
|
44
|
+
},
|
|
45
|
+
dispose: h
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const Y = (t, n) => {
|
|
50
|
+
const s = new WebSocket(t);
|
|
51
|
+
return s.onopen = () => n.onOpen(), s.onmessage = (c) => n.onMessage(typeof c.data == "string" ? c.data : String(c.data)), s.onclose = () => n.onClose(), s.onerror = () => n.onError(), {
|
|
5
52
|
get readyState() {
|
|
6
53
|
return s.readyState;
|
|
7
54
|
},
|
|
8
55
|
send: (c) => s.send(c),
|
|
9
|
-
close: (c,
|
|
56
|
+
close: (c, r) => s.close(c, r),
|
|
10
57
|
dispose: () => {
|
|
11
58
|
s.onopen = null, s.onmessage = null, s.onclose = null, s.onerror = null;
|
|
12
59
|
}
|
|
13
60
|
};
|
|
14
61
|
};
|
|
15
|
-
class
|
|
16
|
-
constructor(
|
|
17
|
-
super(
|
|
62
|
+
class G extends U {
|
|
63
|
+
constructor(n, s, c, r, u = Y) {
|
|
64
|
+
super(n, s, c, r, u);
|
|
18
65
|
}
|
|
19
66
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
67
|
+
const W = "__YOLTRA_DEVTOOLS_BRIDGE__", q = 6 * 1024 * 1024;
|
|
68
|
+
function z(t) {
|
|
69
|
+
if (t.socketFactory !== void 0) return t.socketFactory;
|
|
70
|
+
const n = t.transport ?? "auto";
|
|
71
|
+
return n === "websocket" ? void 0 : n === "bridge" || typeof globalThis == "object" && globalThis[W] === !0 ? A() : void 0;
|
|
72
|
+
}
|
|
73
|
+
function X(t, n) {
|
|
74
|
+
const s = n.storeId ?? t.name, c = n.host ?? "localhost";
|
|
75
|
+
let r = 0, u = 0, h = 0;
|
|
76
|
+
const d = [];
|
|
77
|
+
let T = 0;
|
|
78
|
+
const f = /* @__PURE__ */ new Map(), D = /* @__PURE__ */ new Map(), x = (a, o) => {
|
|
79
|
+
const e = S.sampling;
|
|
27
80
|
if (!e) return !1;
|
|
28
|
-
const
|
|
29
|
-
if (e.ignore?.some((l) =>
|
|
81
|
+
const i = `${a}::${o}`, m = (l) => l.some(([y, v]) => (y === "*" || y === a) && (v === "*" || v === o));
|
|
82
|
+
if (e.ignore?.some((l) => m(l.keys))) return !0;
|
|
30
83
|
for (const l of e.throttle ?? []) {
|
|
31
|
-
if (!
|
|
32
|
-
const
|
|
33
|
-
return
|
|
84
|
+
if (!m(l.keys)) continue;
|
|
85
|
+
const y = D.get(i) ?? 0, v = Date.now();
|
|
86
|
+
return v - y < l.intervalMs ? !0 : (D.set(i, v), !1);
|
|
34
87
|
}
|
|
35
88
|
for (const l of e.skip ?? []) {
|
|
36
|
-
if (!
|
|
37
|
-
const
|
|
38
|
-
return
|
|
89
|
+
if (!m(l.keys)) continue;
|
|
90
|
+
const y = (f.get(i) ?? 0) + 1;
|
|
91
|
+
return y < l.every ? (f.set(i, y), !0) : (f.set(i, 0), !1);
|
|
39
92
|
}
|
|
40
93
|
return !1;
|
|
41
|
-
},
|
|
42
|
-
replay:
|
|
94
|
+
}, S = {
|
|
95
|
+
replay: n.allowReplay ?? !1,
|
|
43
96
|
stateSnapshot: !0,
|
|
44
97
|
subscriptionMeta: !0,
|
|
45
98
|
pipelineMeta: !0,
|
|
46
|
-
emit:
|
|
99
|
+
emit: n.allowEmit ?? !1
|
|
47
100
|
};
|
|
48
|
-
|
|
49
|
-
const p = new
|
|
101
|
+
n.sampling && (S.sampling = n.sampling);
|
|
102
|
+
const O = n.maxSnapshotBytes ?? q, E = n.sanitize, _ = E === void 0 ? {} : { sanitize: E }, P = (a) => E === void 0 ? {} : { sanitize: (o, e) => E(`${a}${o}`, e) }, p = new G(
|
|
50
103
|
s,
|
|
51
|
-
|
|
52
|
-
|
|
104
|
+
t.name,
|
|
105
|
+
S,
|
|
53
106
|
{
|
|
54
|
-
autoReconnect:
|
|
55
|
-
maxReconnectAttempts:
|
|
56
|
-
baseDelay:
|
|
57
|
-
maxDelay:
|
|
107
|
+
autoReconnect: n.autoReconnect ?? !0,
|
|
108
|
+
maxReconnectAttempts: n.maxReconnectAttempts ?? 1 / 0,
|
|
109
|
+
baseDelay: n.baseDelay ?? 1e3,
|
|
110
|
+
maxDelay: n.maxDelay ?? 3e4,
|
|
111
|
+
...n.authToken !== void 0 ? { authToken: n.authToken } : {}
|
|
58
112
|
},
|
|
59
|
-
|
|
113
|
+
z(n)
|
|
60
114
|
);
|
|
61
|
-
let
|
|
62
|
-
p.onBackpressure((
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
`[Yoltra DevTools] Backpressure: dropped ${
|
|
115
|
+
let b = 0;
|
|
116
|
+
p.onBackpressure((a) => {
|
|
117
|
+
const o = Date.now();
|
|
118
|
+
o - b > 5e3 && (b = o, console.warn(
|
|
119
|
+
`[Yoltra DevTools] Backpressure: dropped ${a} event(s) while the hub was unreachable.`
|
|
66
120
|
));
|
|
67
121
|
});
|
|
68
|
-
const
|
|
122
|
+
const g = () => ({
|
|
69
123
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
70
124
|
sourceId: s,
|
|
71
|
-
sourceRole:
|
|
125
|
+
sourceRole: $.STORE
|
|
72
126
|
});
|
|
73
|
-
p.onMessage(async (
|
|
74
|
-
let
|
|
127
|
+
p.onMessage(async (a) => {
|
|
128
|
+
let o;
|
|
75
129
|
try {
|
|
76
|
-
|
|
130
|
+
o = JSON.parse(a);
|
|
77
131
|
} catch {
|
|
78
132
|
return;
|
|
79
133
|
}
|
|
80
|
-
if (!(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
134
|
+
if (!(o === null || typeof o != "object" || typeof o.type != "string"))
|
|
135
|
+
try {
|
|
136
|
+
switch (o.type) {
|
|
137
|
+
case "REQUEST_STATE": {
|
|
138
|
+
const e = t.getState(), i = k(e, O, _), m = {
|
|
139
|
+
type: "STATE_SNAPSHOT",
|
|
140
|
+
...g(),
|
|
141
|
+
storeId: s,
|
|
142
|
+
state: i.value,
|
|
143
|
+
version: r,
|
|
144
|
+
reducerNames: Object.keys(e),
|
|
145
|
+
...i.truncated ? { truncated: !0, ...i.note !== void 0 ? { truncationNote: i.note } : {} } : {}
|
|
146
|
+
};
|
|
147
|
+
p.send(JSON.stringify(m));
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case "REQUEST_METRICS": {
|
|
151
|
+
const e = t.__devtoolsIntrospect(), i = Date.now() - 1e3, m = d.filter((F) => F >= i).length, l = Math.max(0, h - u), y = u > 0 ? T / u : 0, v = {
|
|
152
|
+
type: "STORE_METRICS",
|
|
153
|
+
...g(),
|
|
154
|
+
storeId: s,
|
|
155
|
+
metrics: {
|
|
156
|
+
eventCount: u,
|
|
157
|
+
eventsPerSecond: m,
|
|
158
|
+
avgProcessingTimeMs: y,
|
|
159
|
+
dedupHits: e.dedupHits,
|
|
160
|
+
queueDepth: e.queueDepth,
|
|
161
|
+
reducerCount: e.reducers.length,
|
|
162
|
+
effectCount: e.effects.length,
|
|
163
|
+
middlewareCount: e.middleware.length,
|
|
164
|
+
subscriberCount: e.event.length + e.coarse,
|
|
165
|
+
connectorCount: e.atomic.length,
|
|
166
|
+
middlewareRejections: l
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
p.send(JSON.stringify(v));
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "TIME_TRAVEL": {
|
|
173
|
+
if (!S.replay || o.state == null) break;
|
|
174
|
+
t.__applyExternalState(H(o.state)), r = o.snapshotVersion ?? r;
|
|
175
|
+
const e = t.getState(), i = k(e, O, _), m = {
|
|
176
|
+
type: "STATE_SNAPSHOT",
|
|
177
|
+
...g(),
|
|
178
|
+
storeId: s,
|
|
179
|
+
state: i.value,
|
|
180
|
+
version: r,
|
|
181
|
+
reducerNames: Object.keys(e)
|
|
182
|
+
};
|
|
183
|
+
p.send(JSON.stringify(m));
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case "EVENT_REPLAY": {
|
|
187
|
+
S.replay && t.__replayEvents(o.snapshot, o.events);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case "EMIT_TO_STORE": {
|
|
191
|
+
S.emit && o.event && await t.emit(o.event.channel, o.event.type, o.event.payload);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "REQUEST_SUBSCRIPTIONS": {
|
|
195
|
+
const e = t.__devtoolsIntrospect(), i = {
|
|
196
|
+
type: "STORE_SUBSCRIPTIONS",
|
|
197
|
+
...g(),
|
|
198
|
+
storeId: s,
|
|
199
|
+
atomic: e.atomic,
|
|
200
|
+
event: e.event,
|
|
201
|
+
coarse: e.coarse,
|
|
202
|
+
effects: e.effects,
|
|
203
|
+
middleware: e.middleware,
|
|
204
|
+
reducers: e.reducers
|
|
205
|
+
};
|
|
206
|
+
p.send(JSON.stringify(i));
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
default: {
|
|
210
|
+
console.warn(`[Yoltra DevTools] Ignoring unknown message type: ${String(o.type)}`);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
156
213
|
}
|
|
214
|
+
} catch (e) {
|
|
215
|
+
console.error(
|
|
216
|
+
`[Yoltra DevTools] Command "${String(o.type)}" failed:`,
|
|
217
|
+
e
|
|
218
|
+
);
|
|
157
219
|
}
|
|
158
220
|
});
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
|
|
221
|
+
const M = t.__yoltraDevtoolsDispose;
|
|
222
|
+
M && M();
|
|
223
|
+
const L = t.instrument((a) => {
|
|
224
|
+
if (h++, a.committed) {
|
|
225
|
+
u++, T += a.reduceTimeMs;
|
|
164
226
|
const e = Date.now();
|
|
165
|
-
for (
|
|
166
|
-
|
|
227
|
+
for (d.push(e); d.length > 0 && e - d[0] > 6e4; )
|
|
228
|
+
d.shift();
|
|
167
229
|
}
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
const
|
|
230
|
+
if (x(a.event.channel, a.event.type)) return;
|
|
231
|
+
a.committed && r++;
|
|
232
|
+
const o = {
|
|
171
233
|
type: "STORE_EVENT",
|
|
172
|
-
...
|
|
234
|
+
...g(),
|
|
173
235
|
storeId: s,
|
|
174
236
|
event: {
|
|
175
|
-
id:
|
|
176
|
-
channel:
|
|
177
|
-
type:
|
|
178
|
-
|
|
237
|
+
id: a.event.id,
|
|
238
|
+
channel: a.event.channel,
|
|
239
|
+
type: a.event.type,
|
|
240
|
+
// Encoded like state: a payload is arbitrary application data, so it can hold the same
|
|
241
|
+
// Map, BigInt or cycle that made a bare stringify throw or quietly destroy it.
|
|
242
|
+
payload: C(a.event.payload, _).value
|
|
179
243
|
},
|
|
180
|
-
patches:
|
|
181
|
-
|
|
182
|
-
|
|
244
|
+
patches: a.committed ? j(a.changedPaths, a.prevValues, a.nextValues).map(
|
|
245
|
+
(e) => "value" in e ? { ...e, value: C(e.value, P(e.path)).value } : e
|
|
246
|
+
) : [],
|
|
247
|
+
snapshotVersion: r,
|
|
248
|
+
committed: a.committed
|
|
183
249
|
};
|
|
184
|
-
p.send(JSON.stringify(
|
|
250
|
+
p.send(JSON.stringify(o));
|
|
185
251
|
});
|
|
186
|
-
p.connect(c,
|
|
187
|
-
const
|
|
188
|
-
|
|
252
|
+
p.connect(c, n.port);
|
|
253
|
+
const R = () => {
|
|
254
|
+
L(), p.disconnect(), t.__yoltraDevtoolsDispose = void 0;
|
|
189
255
|
};
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
return
|
|
193
|
-
|
|
194
|
-
},
|
|
256
|
+
t.__yoltraDevtoolsDispose = R;
|
|
257
|
+
const V = t.dispose.bind(t);
|
|
258
|
+
return t.dispose = () => {
|
|
259
|
+
R(), V();
|
|
260
|
+
}, t;
|
|
195
261
|
}
|
|
196
262
|
export {
|
|
197
|
-
|
|
263
|
+
B as BRIDGE_CHANNEL,
|
|
264
|
+
A as createPostMessageSocketFactory,
|
|
265
|
+
J as isBridgeMessage,
|
|
266
|
+
X as withDevtools
|
|
198
267
|
};
|
|
199
268
|
//# sourceMappingURL=devtools-browser-agent.esm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools-browser-agent.esm.js","sources":["../src/ws-client.ts","../src/withDevtools.ts"],"sourcesContent":["/**\n * Browser WebSocket transport for the DevTools agent.\n *\n * @remarks\n * Injects the native browser `WebSocket` into the shared, transport-agnostic\n * {@link ReconnectingWsClient} from `@yoltra/devtools-protocol`. There is **no**\n * `ws` dependency here — that is what keeps this package installable in the\n * browser without pulling in a Node-only transport.\n *\n * @module @yoltra/devtools-browser-agent\n */\n\nimport {\n ReconnectingWsClient,\n type DevtoolsSocketFactory,\n type ReconnectingWsConfig,\n type StoreCapabilities,\n} from \"@yoltra/devtools-protocol\";\n\nexport type { ConnectionState } from \"@yoltra/devtools-protocol\";\n\n/** Opens a native browser WebSocket and adapts it to the shared transport. */\nconst browserSocketFactory: DevtoolsSocketFactory = (url, callbacks) => {\n const ws = new WebSocket(url);\n ws.onopen = () => callbacks.onOpen();\n ws.onmessage = (ev: MessageEvent) =>\n callbacks.onMessage(typeof ev.data === \"string\" ? ev.data : String(ev.data));\n ws.onclose = () => callbacks.onClose();\n ws.onerror = () => callbacks.onError();\n return {\n get readyState() {\n return ws.readyState;\n },\n send: (data) => ws.send(data),\n close: (code, reason) => ws.close(code, reason),\n dispose: () => {\n ws.onopen = null;\n ws.onmessage = null;\n ws.onclose = null;\n ws.onerror = null;\n },\n };\n};\n\n/**\n * Browser DevTools WebSocket client (native `WebSocket` transport).\n *\n * @internal\n */\nexport class DevtoolsWsClient extends ReconnectingWsClient {\n constructor(\n storeId: string,\n storeName: string,\n capabilities: StoreCapabilities,\n config: ReconnectingWsConfig,\n socketFactory: DevtoolsSocketFactory = browserSocketFactory,\n ) {\n super(storeId, storeName, capabilities, config, socketFactory);\n }\n}\n","/**\n * Browser DevTools agent entry point -- the {@link withDevtools} wrapper function.\n *\n * @module @yoltra/devtools-browser-agent\n */\n\nimport type { EventMapBase, InstrumentedEvent, StoreInstance } from \"@yoltra/core\";\nimport {\n DevtoolsRole,\n patchesFromChange,\n type BaseMessage,\n type StateSnapshot,\n type StoreCapabilities,\n type StoreEvent,\n type StoreMetrics,\n} from \"@yoltra/devtools-protocol\";\nimport type { DevtoolsWrapperConfig } from \"./types\";\nimport { DevtoolsWsClient } from \"./ws-client\";\n\n/**\n * Wraps a Yoltra store with DevTools instrumentation for browser environments.\n *\n * @remarks\n * - Connects to the DevTools hub via native `WebSocket`.\n * - Observes every event via the typed `store.instrument()` seam — no\n * `as any` bridging, no re-diffing, no full-state clone per event.\n * - Builds precise RFC 6902 patches from the exact changed leaf paths the core\n * reports, and sends a `STORE_EVENT` per event (committed or vetoed).\n * - Handles incoming commands: REQUEST_STATE, REQUEST_METRICS,\n * REQUEST_SUBSCRIPTIONS, TIME_TRAVEL, EVENT_REPLAY, EMIT_TO_STORE.\n * - Returns the **same** store instance (transparent instrumentation).\n * - Auto-reconnects to the hub on disconnect.\n *\n * @typeParam R - Reducer name union.\n * @typeParam S - State record.\n * @typeParam EM - Event map.\n * @param store - The store to instrument.\n * @param config - DevTools wrapper configuration.\n * @returns The same store instance, now instrumented.\n *\n * @example\n * ```ts\n * import { createStore } from '@yoltra/core';\n * import { withDevtools } from '@yoltra/devtools-browser-agent';\n *\n * const store = createStore({ name: 'App', reducer: { ... } });\n * withDevtools(store, { port: 9800 });\n * ```\n *\n * @public\n */\nexport function withDevtools<\n R extends string,\n S extends Record<R, any>,\n EM extends EventMapBase,\n>(store: StoreInstance<R, S, EM>, config: DevtoolsWrapperConfig): StoreInstance<R, S, EM> {\n const storeId = config.storeId ?? store.name;\n const host = config.host ?? \"localhost\";\n let snapshotVersion = 0;\n\n // ── Metrics tracking ──────────────────────────────────────────────────────\n // Counts committed events (events that pass middleware and run reducers).\n let committedEventCount = 0;\n // Counts ALL observed events including those vetoed by middleware.\n let totalAttemptedCount = 0;\n // Timestamps (epoch ms) of recent committed events; pruned to last 60 s.\n const eventTimestamps: number[] = [];\n // Rolling sum of the core-reported reduce time (ms) over committed events.\n let reduceTimeSumMs = 0;\n\n // ── Sampling state ─────────────────────────────────────────────────────────\n // Tracks per-event-key counters and timestamps to enforce the three sampling\n // rule types declared in `capabilities.sampling`:\n // ignore — never forward matching events.\n // skip — forward every Nth matching event (0-based counter).\n // throttle — forward at most once per intervalMs per key.\n const skipCounters = new Map<string, number>(); // current invocation count per key\n const throttleLast = new Map<string, number>(); // last-forwarded epoch ms per key\n\n /**\n * Returns `true` when the event should be suppressed by the sampling config.\n * All three rule types are checked in priority order: ignore → throttle → skip.\n */\n const isSampledOut = (channel: string, type: string): boolean => {\n const sampling = capabilities.sampling;\n if (!sampling) return false;\n\n const eventKey = `${channel}::${type}`;\n\n // Helper: check if any rule's keys array matches this event.\n const matches = (keys: Array<[string, string]>): boolean =>\n keys.some(([c, t]) => (c === \"*\" || c === channel) && (t === \"*\" || t === type));\n\n // 1. Ignore — never forward.\n if (sampling.ignore?.some((r) => matches(r.keys))) return true;\n\n // 2. Throttle — forward only if intervalMs has elapsed since the last send.\n for (const rule of sampling.throttle ?? []) {\n if (!matches(rule.keys)) continue;\n const last = throttleLast.get(eventKey) ?? 0;\n const nowMs = Date.now();\n if (nowMs - last < rule.intervalMs) return true; // within throttle window\n throttleLast.set(eventKey, nowMs);\n return false; // passed throttle; no further rules apply for this event\n }\n\n // 3. Skip — forward every Nth event (counter resets to 0 after firing).\n for (const rule of sampling.skip ?? []) {\n if (!matches(rule.keys)) continue;\n const count = (skipCounters.get(eventKey) ?? 0) + 1;\n if (count < rule.every) {\n skipCounters.set(eventKey, count);\n return true; // suppress until we reach the Nth event\n }\n skipCounters.set(eventKey, 0); // fire — reset counter\n return false;\n }\n\n return false;\n };\n\n // Build capabilities from config\n const capabilities: StoreCapabilities = {\n replay: config.allowReplay ?? false,\n stateSnapshot: true,\n subscriptionMeta: true,\n pipelineMeta: true,\n emit: config.allowEmit ?? false,\n };\n if (config.sampling) {\n capabilities.sampling = config.sampling;\n }\n\n // Create browser WS client\n const wsClient = new DevtoolsWsClient(\n storeId,\n store.name,\n capabilities,\n {\n autoReconnect: config.autoReconnect ?? true,\n maxReconnectAttempts: config.maxReconnectAttempts ?? Infinity,\n baseDelay: config.baseDelay ?? 1000,\n maxDelay: config.maxDelay ?? 30000,\n },\n config.socketFactory,\n );\n\n // Surface backpressure: warn (throttled) instead of dropping events silently\n // when the hub is unreachable and the send buffer overflows.\n let lastBackpressureWarn = 0;\n wsClient.onBackpressure((dropped) => {\n const nowMs = Date.now();\n if (nowMs - lastBackpressureWarn > 5_000) {\n lastBackpressureWarn = nowMs;\n console.warn(\n `[Yoltra DevTools] Backpressure: dropped ${dropped} event(s) while the hub was unreachable.`,\n );\n }\n });\n\n const baseMsg = (): Pick<BaseMessage, \"timestamp\" | \"sourceId\" | \"sourceRole\"> => ({\n timestamp: new Date().toISOString(),\n sourceId: storeId,\n sourceRole: DevtoolsRole.STORE,\n });\n\n // Handle incoming messages from hub\n wsClient.onMessage(async (data: string) => {\n let msg: any;\n try {\n msg = JSON.parse(data);\n } catch {\n return;\n }\n\n // Ingress validation (DEV-3): require a well-formed message with a string\n // `type` discriminant before acting on it (EMIT_TO_STORE forwards straight\n // into store.emit, so a malformed payload must not reach it).\n if (msg === null || typeof msg !== \"object\" || typeof msg.type !== \"string\") return;\n\n switch (msg.type) {\n case \"REQUEST_STATE\": {\n const state = store.getState();\n const response: StateSnapshot = {\n type: \"STATE_SNAPSHOT\",\n ...baseMsg(),\n storeId,\n state: JSON.parse(JSON.stringify(state)),\n version: snapshotVersion,\n reducerNames: Object.keys(state as object),\n };\n wsClient.send(JSON.stringify(response));\n break;\n }\n\n case \"REQUEST_METRICS\": {\n const introspection = store.__devtoolsIntrospect();\n // Events committed in the last 1 second (sliding window).\n const cutoffMs = Date.now() - 1_000;\n const eventsPerSecond = eventTimestamps.filter((t) => t >= cutoffMs).length;\n // Middleware rejections = attempted − committed (floor at 0).\n const middlewareRejections = Math.max(0, totalAttemptedCount - committedEventCount);\n // Real reduce-phase timing, averaged over committed events.\n const avgProcessingTimeMs =\n committedEventCount > 0 ? reduceTimeSumMs / committedEventCount : 0;\n const response: StoreMetrics = {\n type: \"STORE_METRICS\",\n ...baseMsg(),\n storeId,\n metrics: {\n eventCount: committedEventCount,\n eventsPerSecond,\n avgProcessingTimeMs,\n dedupHits: introspection.dedupHits,\n queueDepth: introspection.queueDepth,\n reducerCount: introspection.reducers.length,\n effectCount: introspection.effects.length,\n middlewareCount: introspection.middleware.length,\n subscriberCount: introspection.event.length + introspection.coarse,\n connectorCount: introspection.atomic.length,\n middlewareRejections,\n },\n };\n wsClient.send(JSON.stringify(response));\n break;\n }\n\n case \"TIME_TRAVEL\": {\n // Time-travel replaces the entire state tree — gate on the store's\n // replay capability (default off), same as EVENT_REPLAY. The core seam\n // enforces this too (defense in depth).\n if (!capabilities.replay) break;\n\n // Guard against malformed messages — a null state would corrupt the\n // store and cause \"Cannot read property of undefined\" in reducers.\n if (msg.state == null) break;\n\n store.__applyExternalState(msg.state);\n snapshotVersion = msg.snapshotVersion ?? snapshotVersion;\n\n // Notify the extension of the new state so all UI panels re-render with\n // the time-traveled state.\n const traveledState = store.getState();\n const travelSnapshot: StateSnapshot = {\n type: \"STATE_SNAPSHOT\",\n ...baseMsg(),\n storeId,\n state: JSON.parse(JSON.stringify(traveledState)),\n version: snapshotVersion,\n reducerNames: Object.keys(traveledState as object),\n };\n wsClient.send(JSON.stringify(travelSnapshot));\n break;\n }\n\n case \"EVENT_REPLAY\": {\n if (capabilities.replay) {\n store.__replayEvents(msg.snapshot, msg.events);\n }\n break;\n }\n\n case \"EMIT_TO_STORE\": {\n if (capabilities.emit && msg.event) {\n await store.emit(msg.event.channel, msg.event.type, msg.event.payload);\n }\n break;\n }\n\n case \"REQUEST_SUBSCRIPTIONS\": {\n const introspection = store.__devtoolsIntrospect();\n const response = {\n type: \"STORE_SUBSCRIPTIONS\",\n ...baseMsg(),\n storeId,\n atomic: introspection.atomic,\n event: introspection.event,\n coarse: introspection.coarse,\n effects: introspection.effects,\n middleware: introspection.middleware,\n reducers: introspection.reducers,\n };\n wsClient.send(JSON.stringify(response));\n break;\n }\n\n default: {\n // Exhaustiveness fallback (DEV-3): an unhandled command type is protocol\n // drift (a bug), not routine traffic — surface it instead of dropping it.\n console.warn(`[Yoltra DevTools] Ignoring unknown message type: ${String(msg.type)}`);\n break;\n }\n }\n });\n\n // If this store was already wrapped (HMR / remount / a double call), tear down\n // the previous devtools attachment first so we don't leak the instrument\n // observer + reconnecting socket or double-send every event (DEV-2).\n const existingDispose = (store as unknown as { __yoltraDevtoolsDispose?: () => void })\n .__yoltraDevtoolsDispose;\n if (existingDispose) existingDispose();\n\n // Observe every event through the typed instrumentation seam. This single\n // observer replaces the old interceptor effect + metrics middleware + manual\n // diff + full-state clone: the core hands us the exact changed leaf paths and\n // their old/new values, so we build precise patches with no re-diff.\n const instrumentUnsub = store.instrument((info: InstrumentedEvent<EM>) => {\n totalAttemptedCount++;\n if (info.committed) {\n committedEventCount++;\n reduceTimeSumMs += info.reduceTimeMs;\n const nowMs = Date.now();\n eventTimestamps.push(nowMs);\n // Prune timestamps older than 60 s to bound memory usage.\n while (eventTimestamps.length > 0 && nowMs - eventTimestamps[0]! > 60_000) {\n eventTimestamps.shift();\n }\n }\n\n if (isSampledOut(info.event.channel, info.event.type)) return;\n\n // Only a committed event advances the state version — it carries patches. A\n // vetoed event is logged with committed:false and no bump, so time-travel\n // reconstruction stays correlated (DEV-7). Wire ordering is preserved by the\n // event log's array insertion order, not by this version.\n if (info.committed) snapshotVersion++;\n const storeEvent: StoreEvent = {\n type: \"STORE_EVENT\",\n ...baseMsg(),\n storeId,\n event: {\n id: info.event.id,\n channel: info.event.channel,\n type: info.event.type,\n payload: info.event.payload,\n },\n patches: info.committed\n ? patchesFromChange(info.changedPaths, info.prevValues, info.nextValues)\n : [],\n snapshotVersion,\n committed: info.committed,\n };\n wsClient.send(JSON.stringify(storeEvent));\n });\n\n // Connect to hub\n wsClient.connect(host, config.port);\n\n // Devtools teardown (DEV-2): detach the instrument observer and disconnect the\n // socket. Stored on the store so a later re-wrap tears down this attachment,\n // and folded into store.dispose() so disposing the store also detaches devtools.\n const disposeDevtools = () => {\n instrumentUnsub();\n wsClient.disconnect();\n (store as unknown as { __yoltraDevtoolsDispose?: () => void }).__yoltraDevtoolsDispose =\n undefined;\n };\n (store as unknown as { __yoltraDevtoolsDispose?: () => void }).__yoltraDevtoolsDispose =\n disposeDevtools;\n\n const prevDispose = store.dispose.bind(store);\n store.dispose = () => {\n disposeDevtools();\n prevDispose();\n };\n\n return store;\n}\n"],"names":["browserSocketFactory","url","callbacks","ws","ev","data","code","reason","DevtoolsWsClient","ReconnectingWsClient","storeId","storeName","capabilities","config","socketFactory","withDevtools","store","host","snapshotVersion","committedEventCount","totalAttemptedCount","eventTimestamps","reduceTimeSumMs","skipCounters","throttleLast","isSampledOut","channel","type","sampling","eventKey","matches","keys","c","t","r","rule","last","nowMs","count","wsClient","lastBackpressureWarn","dropped","baseMsg","DevtoolsRole","msg","state","response","introspection","cutoffMs","eventsPerSecond","middlewareRejections","avgProcessingTimeMs","traveledState","travelSnapshot","existingDispose","instrumentUnsub","info","storeEvent","patchesFromChange","disposeDevtools","prevDispose"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"devtools-browser-agent.esm.js","sources":["../src/postMessage-client.ts","../src/ws-client.ts","../src/withDevtools.ts"],"sourcesContent":["/**\n * `postMessage` transport, for attaching without a hub.\n *\n * @remarks\n * Attaching the panel used to mean running a hub process, editing the application to add\n * `withDevtools()`, and setting `allowReplay` — three steps at the exact moment somebody is\n * deciding whether the tool is worth it, against Redux DevTools' one. The hub earns its keep for\n * Node processes and remote debugging, where a socket is the only way in. For a page being\n * inspected by an extension running in the same browser, it is a server standing between two\n * things already able to talk.\n *\n * This transport is the page half. It frames the same protocol into `window.postMessage`, where\n * a content script can relay it to the panel. Nothing here knows about extensions: it posts to\n * its own window and listens on it, which keeps the module testable and lets any relay — a\n * content script, an iframe host, a test — sit on the other side.\n *\n * @module @yoltra/devtools-browser-agent\n */\n\nimport type {\n DevtoolsSocketCallbacks,\n DevtoolsSocketFactory,\n DevtoolsSocketHandle,\n} from \"@yoltra/devtools-protocol\";\n\n/** Marks a message as belonging to this bridge, so unrelated `postMessage` traffic is ignored. */\nexport const BRIDGE_CHANNEL = \"yoltra-devtools-bridge\" as const;\n\n/** Direction of a bridged frame. */\nexport type BridgeDirection = \"to-panel\" | \"to-page\";\n\n/** One protocol frame travelling over `window.postMessage`. */\nexport interface BridgeMessage {\n readonly channel: typeof BRIDGE_CHANNEL;\n readonly direction: BridgeDirection;\n /** A serialized DevTools protocol message. */\n readonly data: string;\n}\n\n/** `true` when `value` is a bridge frame travelling in `direction`. */\nexport function isBridgeMessage(value: unknown, direction: BridgeDirection): value is BridgeMessage {\n if (value === null || typeof value !== \"object\") return false;\n const msg = value as Record<string, unknown>;\n return (\n msg.channel === BRIDGE_CHANNEL && msg.direction === direction && typeof msg.data === \"string\"\n );\n}\n\n/** The window-like surface this transport needs, so a test can supply its own. */\nexport interface BridgeWindow {\n postMessage(message: unknown, targetOrigin: string): void;\n addEventListener(type: \"message\", listener: (event: MessageEvent) => void): void;\n removeEventListener(type: \"message\", listener: (event: MessageEvent) => void): void;\n}\n\nconst CONNECTING = 0;\nconst OPEN = 1;\nconst CLOSED = 3;\n\n/**\n * Builds a socket factory that carries the protocol over `window.postMessage`.\n *\n * @param target - Window to post to and listen on. Defaults to the global `window`.\n * @returns A factory for `withDevtools({ socketFactory })`.\n *\n * @remarks\n * The socket reports itself open immediately, because `postMessage` has no connection to\n * establish. That is not a claim that anybody is listening — if no relay is present the protocol\n * handshake simply goes unanswered, the client never resolves it, and messages buffer exactly as\n * they do against a hub that is not running. Failure to attach therefore looks the same whether\n * the panel is absent or the hub is down, which is one behaviour to explain rather than two.\n *\n * Messages are posted with a `\"*\"` target origin and filtered by channel on receipt. The page is\n * posting to *itself* for a content script in the same frame to observe; there is no\n * cross-origin recipient to restrict to, and the content script is the trust boundary.\n *\n * @example\n * ```ts\n * withDevtools(store, { socketFactory: createPostMessageSocketFactory() });\n * ```\n *\n * @public\n */\nexport function createPostMessageSocketFactory(target?: BridgeWindow): DevtoolsSocketFactory {\n return (_url: string, callbacks: DevtoolsSocketCallbacks): DevtoolsSocketHandle => {\n const win = target ?? (globalThis as unknown as { window?: BridgeWindow }).window;\n if (win === undefined) {\n // No window to post into: report a closed socket rather than throwing into whatever\n // called us. The client treats it as a failed connection and backs off.\n return {\n readyState: CLOSED,\n send: () => undefined,\n close: () => undefined,\n dispose: () => undefined,\n };\n }\n\n let readyState: number = CONNECTING;\n\n const onMessage = (event: MessageEvent): void => {\n // Only frames addressed to the page, on our channel. Everything else on this window —\n // application traffic, other tools — is none of our business.\n if (!isBridgeMessage(event.data, \"to-page\")) return;\n callbacks.onMessage(event.data.data);\n };\n\n win.addEventListener(\"message\", onMessage);\n\n // Opened on a microtask rather than synchronously, so a caller can finish wiring its\n // handlers before the first callback arrives.\n queueMicrotask(() => {\n if (readyState !== CONNECTING) return;\n readyState = OPEN;\n callbacks.onOpen();\n });\n\n const detach = (): void => {\n win.removeEventListener(\"message\", onMessage);\n };\n\n return {\n get readyState() {\n return readyState;\n },\n send: (data: string) => {\n if (readyState !== OPEN) return;\n const message: BridgeMessage = { channel: BRIDGE_CHANNEL, direction: \"to-panel\", data };\n win.postMessage(message, \"*\");\n },\n close: () => {\n if (readyState === CLOSED) return;\n readyState = CLOSED;\n detach();\n callbacks.onClose();\n },\n dispose: detach,\n };\n };\n}\n","/**\n * Browser WebSocket transport for the DevTools agent.\n *\n * @remarks\n * Injects the native browser `WebSocket` into the shared, transport-agnostic\n * {@link ReconnectingWsClient} from `@yoltra/devtools-protocol`. There is **no**\n * `ws` dependency here — that is what keeps this package installable in the\n * browser without pulling in a Node-only transport.\n *\n * @module @yoltra/devtools-browser-agent\n */\n\nimport {\n ReconnectingWsClient,\n type DevtoolsSocketFactory,\n type ReconnectingWsConfig,\n type StoreCapabilities,\n} from \"@yoltra/devtools-protocol\";\n\nexport type { ConnectionState } from \"@yoltra/devtools-protocol\";\n\n/** Opens a native browser WebSocket and adapts it to the shared transport. */\nconst browserSocketFactory: DevtoolsSocketFactory = (url, callbacks) => {\n const ws = new WebSocket(url);\n ws.onopen = () => callbacks.onOpen();\n ws.onmessage = (ev: MessageEvent) =>\n callbacks.onMessage(typeof ev.data === \"string\" ? ev.data : String(ev.data));\n ws.onclose = () => callbacks.onClose();\n ws.onerror = () => callbacks.onError();\n return {\n get readyState() {\n return ws.readyState;\n },\n send: (data) => ws.send(data),\n close: (code, reason) => ws.close(code, reason),\n dispose: () => {\n ws.onopen = null;\n ws.onmessage = null;\n ws.onclose = null;\n ws.onerror = null;\n },\n };\n};\n\n/**\n * Browser DevTools WebSocket client (native `WebSocket` transport).\n *\n * @internal\n */\nexport class DevtoolsWsClient extends ReconnectingWsClient {\n constructor(\n storeId: string,\n storeName: string,\n capabilities: StoreCapabilities,\n config: ReconnectingWsConfig,\n socketFactory: DevtoolsSocketFactory = browserSocketFactory,\n ) {\n super(storeId, storeName, capabilities, config, socketFactory);\n }\n}\n","/**\n * Browser DevTools agent entry point -- the {@link withDevtools} wrapper function.\n *\n * @module @yoltra/devtools-browser-agent\n */\n\nimport type { EventMapBase, InstrumentedEvent, StoreInstance } from \"@yoltra/core\";\nimport {\n DevtoolsRole,\n patchesFromChange,\n type BaseMessage,\n type DevtoolsSocketFactory,\n type StateSnapshot,\n type StoreCapabilities,\n type StoreEvent,\n type StoreMetrics,\n} from \"@yoltra/devtools-protocol\";\nimport { encodeState, encodeStateBounded, decodeState } from \"@yoltra/core\";\nimport { createPostMessageSocketFactory } from \"./postMessage-client\";\n\n/**\n * Global an extension's content script sets to announce that it is relaying this page.\n *\n * @remarks\n * The content script runs before application code, so by the time a store is created the mark is\n * already there. When it is absent the agent falls back to the hub, which is the right answer\n * for a page nobody is inspecting from an extension.\n */\nconst BRIDGE_MARK = \"__YOLTRA_DEVTOOLS_BRIDGE__\";\n\n/**\n * Byte budget for a state snapshot, under the hub's 8 MiB frame cap.\n *\n * @remarks\n * Deliberately below it rather than equal to it: the snapshot travels inside an envelope with a\n * store id, a version and the reducer names, and a frame that overshoots by those few hundred\n * bytes is refused exactly as one that overshoots by a megabyte.\n */\nconst DEFAULT_MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;\n\n/** Chooses the transport, honouring an explicit one above all. */\nfunction resolveSocketFactory(\n config: DevtoolsWrapperConfig,\n): DevtoolsSocketFactory | undefined {\n if (config.socketFactory !== undefined) return config.socketFactory;\n\n const mode = config.transport ?? \"auto\";\n if (mode === \"websocket\") return undefined; // the client's own default\n if (mode === \"bridge\") return createPostMessageSocketFactory();\n\n const relaying =\n typeof globalThis === \"object\" &&\n (globalThis as Record<string, unknown>)[BRIDGE_MARK] === true;\n return relaying ? createPostMessageSocketFactory() : undefined;\n}\nimport type { DevtoolsWrapperConfig } from \"./types\";\nimport { DevtoolsWsClient } from \"./ws-client\";\n\n/**\n * Wraps a Yoltra store with DevTools instrumentation for browser environments.\n *\n * @remarks\n * - Connects to the DevTools hub via native `WebSocket`.\n * - Observes every event via the typed `store.instrument()` seam — no\n * `as any` bridging, no re-diffing, no full-state clone per event.\n * - Builds precise RFC 6902 patches from the exact changed leaf paths the core\n * reports, and sends a `STORE_EVENT` per event (committed or vetoed).\n * - Handles incoming commands: REQUEST_STATE, REQUEST_METRICS,\n * REQUEST_SUBSCRIPTIONS, TIME_TRAVEL, EVENT_REPLAY, EMIT_TO_STORE.\n * - Returns the **same** store instance (transparent instrumentation).\n * - Auto-reconnects to the hub on disconnect.\n *\n * @typeParam R - Reducer name union.\n * @typeParam S - State record.\n * @typeParam EM - Event map.\n * @param store - The store to instrument.\n * @param config - DevTools wrapper configuration.\n * @returns The same store instance, now instrumented.\n *\n * @example\n * ```ts\n * import { createStore } from '@yoltra/core';\n * import { withDevtools } from '@yoltra/devtools-browser-agent';\n *\n * const store = createStore({ name: 'App', reducer: { ... } });\n * withDevtools(store, { port: 9800 });\n * ```\n *\n * @public\n */\nexport function withDevtools<\n R extends string,\n S extends Record<R, any>,\n EM extends EventMapBase,\n>(store: StoreInstance<R, S, EM>, config: DevtoolsWrapperConfig): StoreInstance<R, S, EM> {\n const storeId = config.storeId ?? store.name;\n const host = config.host ?? \"localhost\";\n let snapshotVersion = 0;\n\n // ── Metrics tracking ──────────────────────────────────────────────────────\n // Counts committed events (events that pass middleware and run reducers).\n let committedEventCount = 0;\n // Counts ALL observed events including those vetoed by middleware.\n let totalAttemptedCount = 0;\n // Timestamps (epoch ms) of recent committed events; pruned to last 60 s.\n const eventTimestamps: number[] = [];\n // Rolling sum of the core-reported reduce time (ms) over committed events.\n let reduceTimeSumMs = 0;\n\n // ── Sampling state ─────────────────────────────────────────────────────────\n // Tracks per-event-key counters and timestamps to enforce the three sampling\n // rule types declared in `capabilities.sampling`:\n // ignore — never forward matching events.\n // skip — forward every Nth matching event (0-based counter).\n // throttle — forward at most once per intervalMs per key.\n const skipCounters = new Map<string, number>(); // current invocation count per key\n const throttleLast = new Map<string, number>(); // last-forwarded epoch ms per key\n\n /**\n * Returns `true` when the event should be suppressed by the sampling config.\n * All three rule types are checked in priority order: ignore → throttle → skip.\n */\n const isSampledOut = (channel: string, type: string): boolean => {\n const sampling = capabilities.sampling;\n if (!sampling) return false;\n\n const eventKey = `${channel}::${type}`;\n\n // Helper: check if any rule's keys array matches this event.\n const matches = (keys: Array<[string, string]>): boolean =>\n keys.some(([c, t]) => (c === \"*\" || c === channel) && (t === \"*\" || t === type));\n\n // 1. Ignore — never forward.\n if (sampling.ignore?.some((r) => matches(r.keys))) return true;\n\n // 2. Throttle — forward only if intervalMs has elapsed since the last send.\n for (const rule of sampling.throttle ?? []) {\n if (!matches(rule.keys)) continue;\n const last = throttleLast.get(eventKey) ?? 0;\n const nowMs = Date.now();\n if (nowMs - last < rule.intervalMs) return true; // within throttle window\n throttleLast.set(eventKey, nowMs);\n return false; // passed throttle; no further rules apply for this event\n }\n\n // 3. Skip — forward every Nth event (counter resets to 0 after firing).\n for (const rule of sampling.skip ?? []) {\n if (!matches(rule.keys)) continue;\n const count = (skipCounters.get(eventKey) ?? 0) + 1;\n if (count < rule.every) {\n skipCounters.set(eventKey, count);\n return true; // suppress until we reach the Nth event\n }\n skipCounters.set(eventKey, 0); // fire — reset counter\n return false;\n }\n\n return false;\n };\n\n // Build capabilities from config\n const capabilities: StoreCapabilities = {\n replay: config.allowReplay ?? false,\n stateSnapshot: true,\n subscriptionMeta: true,\n pipelineMeta: true,\n emit: config.allowEmit ?? false,\n };\n if (config.sampling) {\n capabilities.sampling = config.sampling;\n }\n\n // Create browser WS client\n const maxSnapshotBytes = config.maxSnapshotBytes ?? DEFAULT_MAX_SNAPSHOT_BYTES;\n // One options object for every encode below: the redaction contract is that NOTHING the\n // agent forwards — snapshot, travel snapshot, payload, patch — skips the hook.\n const sanitize = config.sanitize;\n const encodeOptions = sanitize === undefined ? {} : { sanitize };\n // Patch values need their own wrapper: the codec's paths are relative to what it encodes,\n // and a leaf patch encodes a bare scalar whose root path is \"\" — so the hook would never\n // see `/vault/token`. Prefixing the op's own path restores the address a key-name recipe\n // matches on, for the leaf and for anything nested under a subtree replacement alike.\n const patchEncodeOptions = (opPath: string) =>\n sanitize === undefined\n ? {}\n : { sanitize: (path: string, value: unknown) => sanitize(`${opPath}${path}`, value) };\n\n const wsClient = new DevtoolsWsClient(\n storeId,\n store.name,\n capabilities,\n {\n autoReconnect: config.autoReconnect ?? true,\n maxReconnectAttempts: config.maxReconnectAttempts ?? Infinity,\n baseDelay: config.baseDelay ?? 1000,\n maxDelay: config.maxDelay ?? 30000,\n ...(config.authToken !== undefined ? { authToken: config.authToken } : {}),\n },\n resolveSocketFactory(config),\n );\n\n // Surface backpressure: warn (throttled) instead of dropping events silently\n // when the hub is unreachable and the send buffer overflows.\n let lastBackpressureWarn = 0;\n wsClient.onBackpressure((dropped) => {\n const nowMs = Date.now();\n if (nowMs - lastBackpressureWarn > 5_000) {\n lastBackpressureWarn = nowMs;\n console.warn(\n `[Yoltra DevTools] Backpressure: dropped ${dropped} event(s) while the hub was unreachable.`,\n );\n }\n });\n\n const baseMsg = (): Pick<BaseMessage, \"timestamp\" | \"sourceId\" | \"sourceRole\"> => ({\n timestamp: new Date().toISOString(),\n sourceId: storeId,\n sourceRole: DevtoolsRole.STORE,\n });\n\n // Handle incoming messages from hub\n wsClient.onMessage(async (data: string) => {\n let msg: any;\n try {\n msg = JSON.parse(data);\n } catch {\n return;\n }\n\n // Ingress validation (DEV-3): require a well-formed message with a string\n // `type` discriminant before acting on it (EMIT_TO_STORE forwards straight\n // into store.emit, so a malformed payload must not reach it).\n if (msg === null || typeof msg !== \"object\" || typeof msg.type !== \"string\") return;\n\n try {\n switch (msg.type) {\n case \"REQUEST_STATE\": {\n const state = store.getState();\n // Bounded before it is sent. A frame over the hub's cap is rejected and the\n // connection with it, so an oversized snapshot did not fail loudly — it dropped the\n // socket, the client reconnected, asked again, and the panel waited through the loop\n // with nothing on screen to explain it.\n const bounded = encodeStateBounded(state, maxSnapshotBytes, encodeOptions);\n const response: StateSnapshot = {\n type: \"STATE_SNAPSHOT\",\n ...baseMsg(),\n storeId,\n state: bounded.value,\n version: snapshotVersion,\n reducerNames: Object.keys(state as object),\n ...(bounded.truncated\n ? { truncated: true, ...(bounded.note !== undefined ? { truncationNote: bounded.note } : {}) }\n : {}),\n };\n wsClient.send(JSON.stringify(response));\n break;\n }\n\n case \"REQUEST_METRICS\": {\n const introspection = store.__devtoolsIntrospect();\n // Events committed in the last 1 second (sliding window).\n const cutoffMs = Date.now() - 1_000;\n const eventsPerSecond = eventTimestamps.filter((t) => t >= cutoffMs).length;\n // Middleware rejections = attempted − committed (floor at 0).\n const middlewareRejections = Math.max(0, totalAttemptedCount - committedEventCount);\n // Real reduce-phase timing, averaged over committed events.\n const avgProcessingTimeMs =\n committedEventCount > 0 ? reduceTimeSumMs / committedEventCount : 0;\n const response: StoreMetrics = {\n type: \"STORE_METRICS\",\n ...baseMsg(),\n storeId,\n metrics: {\n eventCount: committedEventCount,\n eventsPerSecond,\n avgProcessingTimeMs,\n dedupHits: introspection.dedupHits,\n queueDepth: introspection.queueDepth,\n reducerCount: introspection.reducers.length,\n effectCount: introspection.effects.length,\n middlewareCount: introspection.middleware.length,\n subscriberCount: introspection.event.length + introspection.coarse,\n connectorCount: introspection.atomic.length,\n middlewareRejections,\n },\n };\n wsClient.send(JSON.stringify(response));\n break;\n }\n\n case \"TIME_TRAVEL\": {\n // Time-travel replaces the entire state tree — gate on the store's\n // replay capability (default off), same as EVENT_REPLAY. The core seam\n // enforces this too (defense in depth).\n if (!capabilities.replay) break;\n\n // Guard against malformed messages — a null state would corrupt the\n // store and cause \"Cannot read property of undefined\" in reducers.\n if (msg.state == null) break;\n\n store.__applyExternalState(decodeState(msg.state) as never);\n snapshotVersion = msg.snapshotVersion ?? snapshotVersion;\n\n // Notify the extension of the new state so all UI panels re-render with\n // the time-traveled state.\n const traveledState = store.getState();\n const travelBounded = encodeStateBounded(traveledState, maxSnapshotBytes, encodeOptions);\n const travelSnapshot: StateSnapshot = {\n type: \"STATE_SNAPSHOT\",\n ...baseMsg(),\n storeId,\n state: travelBounded.value,\n version: snapshotVersion,\n reducerNames: Object.keys(traveledState as object),\n };\n wsClient.send(JSON.stringify(travelSnapshot));\n break;\n }\n\n case \"EVENT_REPLAY\": {\n if (capabilities.replay) {\n store.__replayEvents(msg.snapshot, msg.events);\n }\n break;\n }\n\n case \"EMIT_TO_STORE\": {\n if (capabilities.emit && msg.event) {\n await store.emit(msg.event.channel, msg.event.type, msg.event.payload);\n }\n break;\n }\n\n case \"REQUEST_SUBSCRIPTIONS\": {\n const introspection = store.__devtoolsIntrospect();\n const response = {\n type: \"STORE_SUBSCRIPTIONS\",\n ...baseMsg(),\n storeId,\n atomic: introspection.atomic,\n event: introspection.event,\n coarse: introspection.coarse,\n effects: introspection.effects,\n middleware: introspection.middleware,\n reducers: introspection.reducers,\n };\n wsClient.send(JSON.stringify(response));\n break;\n }\n\n default: {\n // Exhaustiveness fallback (DEV-3): an unhandled command type is protocol\n // drift (a bug), not routine traffic — surface it instead of dropping it.\n console.warn(`[Yoltra DevTools] Ignoring unknown message type: ${String(msg.type)}`);\n break;\n }\n }\n } catch (err) {\n // A command handler that throws must not become an unhandled rejection: this\n // callback is async and nothing awaits it, so the failure would surface far from\n // here — or nowhere at all — and the panel would sit waiting for a reply that\n // never comes. Report it and keep the connection usable.\n console.error(\n `[Yoltra DevTools] Command \"${String(msg.type)}\" failed:`,\n err,\n );\n }\n });\n\n // If this store was already wrapped (HMR / remount / a double call), tear down\n // the previous devtools attachment first so we don't leak the instrument\n // observer + reconnecting socket or double-send every event (DEV-2).\n const existingDispose = (store as unknown as { __yoltraDevtoolsDispose?: () => void })\n .__yoltraDevtoolsDispose;\n if (existingDispose) existingDispose();\n\n // Observe every event through the typed instrumentation seam. This single\n // observer replaces the old interceptor effect + metrics middleware + manual\n // diff + full-state clone: the core hands us the exact changed leaf paths and\n // their old/new values, so we build precise patches with no re-diff.\n const instrumentUnsub = store.instrument((info: InstrumentedEvent<EM>) => {\n totalAttemptedCount++;\n if (info.committed) {\n committedEventCount++;\n reduceTimeSumMs += info.reduceTimeMs;\n const nowMs = Date.now();\n eventTimestamps.push(nowMs);\n // Prune timestamps older than 60 s to bound memory usage.\n while (eventTimestamps.length > 0 && nowMs - eventTimestamps[0]! > 60_000) {\n eventTimestamps.shift();\n }\n }\n\n if (isSampledOut(info.event.channel, info.event.type)) return;\n\n // Only a committed event advances the state version — it carries patches. A\n // vetoed event is logged with committed:false and no bump, so time-travel\n // reconstruction stays correlated (DEV-7). Wire ordering is preserved by the\n // event log's array insertion order, not by this version.\n if (info.committed) snapshotVersion++;\n const storeEvent: StoreEvent = {\n type: \"STORE_EVENT\",\n ...baseMsg(),\n storeId,\n event: {\n id: info.event.id,\n channel: info.event.channel,\n type: info.event.type,\n // Encoded like state: a payload is arbitrary application data, so it can hold the same\n // Map, BigInt or cycle that made a bare stringify throw or quietly destroy it.\n payload: encodeState(info.event.payload, encodeOptions).value,\n },\n patches: info.committed\n ? patchesFromChange(info.changedPaths, info.prevValues, info.nextValues).map((op) =>\n \"value\" in op\n ? { ...op, value: encodeState(op.value, patchEncodeOptions(op.path)).value }\n : op,\n )\n : [],\n snapshotVersion,\n committed: info.committed,\n };\n wsClient.send(JSON.stringify(storeEvent));\n });\n\n // Connect to hub\n wsClient.connect(host, config.port);\n\n // Devtools teardown (DEV-2): detach the instrument observer and disconnect the\n // socket. Stored on the store so a later re-wrap tears down this attachment,\n // and folded into store.dispose() so disposing the store also detaches devtools.\n const disposeDevtools = () => {\n instrumentUnsub();\n wsClient.disconnect();\n (store as unknown as { __yoltraDevtoolsDispose?: () => void }).__yoltraDevtoolsDispose =\n undefined;\n };\n (store as unknown as { __yoltraDevtoolsDispose?: () => void }).__yoltraDevtoolsDispose =\n disposeDevtools;\n\n const prevDispose = store.dispose.bind(store);\n store.dispose = () => {\n disposeDevtools();\n prevDispose();\n };\n\n return store;\n}\n"],"names":["BRIDGE_CHANNEL","isBridgeMessage","value","direction","msg","CONNECTING","OPEN","CLOSED","createPostMessageSocketFactory","target","_url","callbacks","win","readyState","onMessage","event","detach","data","message","browserSocketFactory","url","ws","ev","code","reason","DevtoolsWsClient","ReconnectingWsClient","storeId","storeName","capabilities","config","socketFactory","BRIDGE_MARK","DEFAULT_MAX_SNAPSHOT_BYTES","resolveSocketFactory","mode","withDevtools","store","host","snapshotVersion","committedEventCount","totalAttemptedCount","eventTimestamps","reduceTimeSumMs","skipCounters","throttleLast","isSampledOut","channel","type","sampling","eventKey","matches","keys","c","t","r","rule","last","nowMs","count","maxSnapshotBytes","sanitize","encodeOptions","patchEncodeOptions","opPath","path","wsClient","lastBackpressureWarn","dropped","baseMsg","DevtoolsRole","state","bounded","encodeStateBounded","response","introspection","cutoffMs","eventsPerSecond","middlewareRejections","avgProcessingTimeMs","decodeState","traveledState","travelBounded","travelSnapshot","err","existingDispose","instrumentUnsub","info","storeEvent","encodeState","patchesFromChange","op","disposeDevtools","prevDispose"],"mappings":"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"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
* @module @yoltra/devtools-browser-agent
|
|
3
3
|
*
|
|
4
4
|
* Browser store wrapper for Yoltra DevTools.
|
|
5
|
-
* Connects a Yoltra store to the DevTools
|
|
5
|
+
* Connects a Yoltra store to the DevTools panel — over a `postMessage` bridge when an extension
|
|
6
|
+
* is relaying, or over a WebSocket to a hub.
|
|
6
7
|
*/
|
|
7
8
|
export type { DevtoolsWrapperConfig } from './types';
|
|
8
9
|
export { withDevtools } from './withDevtools';
|
|
10
|
+
/** `postMessage` transport, for attaching without a hub process. */
|
|
11
|
+
export { createPostMessageSocketFactory, isBridgeMessage, BRIDGE_CHANNEL, } from './postMessage-client';
|
|
12
|
+
export type { BridgeDirection, BridgeMessage, BridgeWindow } from './postMessage-client';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { DevtoolsSocketFactory } from '@yoltra/devtools-protocol';
|
|
2
|
+
/** Marks a message as belonging to this bridge, so unrelated `postMessage` traffic is ignored. */
|
|
3
|
+
export declare const BRIDGE_CHANNEL: "yoltra-devtools-bridge";
|
|
4
|
+
/** Direction of a bridged frame. */
|
|
5
|
+
export type BridgeDirection = "to-panel" | "to-page";
|
|
6
|
+
/** One protocol frame travelling over `window.postMessage`. */
|
|
7
|
+
export interface BridgeMessage {
|
|
8
|
+
readonly channel: typeof BRIDGE_CHANNEL;
|
|
9
|
+
readonly direction: BridgeDirection;
|
|
10
|
+
/** A serialized DevTools protocol message. */
|
|
11
|
+
readonly data: string;
|
|
12
|
+
}
|
|
13
|
+
/** `true` when `value` is a bridge frame travelling in `direction`. */
|
|
14
|
+
export declare function isBridgeMessage(value: unknown, direction: BridgeDirection): value is BridgeMessage;
|
|
15
|
+
/** The window-like surface this transport needs, so a test can supply its own. */
|
|
16
|
+
export interface BridgeWindow {
|
|
17
|
+
postMessage(message: unknown, targetOrigin: string): void;
|
|
18
|
+
addEventListener(type: "message", listener: (event: MessageEvent) => void): void;
|
|
19
|
+
removeEventListener(type: "message", listener: (event: MessageEvent) => void): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Builds a socket factory that carries the protocol over `window.postMessage`.
|
|
23
|
+
*
|
|
24
|
+
* @param target - Window to post to and listen on. Defaults to the global `window`.
|
|
25
|
+
* @returns A factory for `withDevtools({ socketFactory })`.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* The socket reports itself open immediately, because `postMessage` has no connection to
|
|
29
|
+
* establish. That is not a claim that anybody is listening — if no relay is present the protocol
|
|
30
|
+
* handshake simply goes unanswered, the client never resolves it, and messages buffer exactly as
|
|
31
|
+
* they do against a hub that is not running. Failure to attach therefore looks the same whether
|
|
32
|
+
* the panel is absent or the hub is down, which is one behaviour to explain rather than two.
|
|
33
|
+
*
|
|
34
|
+
* Messages are posted with a `"*"` target origin and filtered by channel on receipt. The page is
|
|
35
|
+
* posting to *itself* for a content script in the same frame to observe; there is no
|
|
36
|
+
* cross-origin recipient to restrict to, and the content script is the trust boundary.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* withDevtools(store, { socketFactory: createPostMessageSocketFactory() });
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
export declare function createPostMessageSocketFactory(target?: BridgeWindow): DevtoolsSocketFactory;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -57,6 +57,51 @@ export interface DevtoolsWrapperConfig {
|
|
|
57
57
|
* @defaultValue `false`
|
|
58
58
|
*/
|
|
59
59
|
allowEmit?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Shared secret required by a hub that was started with one.
|
|
62
|
+
*
|
|
63
|
+
* @remarks
|
|
64
|
+
* A hub running without a token accepts any local connection, so on a shared or containerised
|
|
65
|
+
* host it should be started with one and every agent given the same value. Omit on a
|
|
66
|
+
* developer machine, where the hub warns at startup that it is open.
|
|
67
|
+
*/
|
|
68
|
+
authToken?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Byte budget for a state snapshot before parts of it are omitted.
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* The hub refuses a frame over its cap and drops the connection, so an oversized snapshot did
|
|
74
|
+
* not surface as an error — the socket closed, the client reconnected and asked again, and the
|
|
75
|
+
* panel waited through the loop. Snapshots are bounded here instead, and a shortened one says
|
|
76
|
+
* so rather than presenting a partial tree as the state.
|
|
77
|
+
*
|
|
78
|
+
* @defaultValue 6291456 (6 MiB, under the hub's 8 MiB frame cap)
|
|
79
|
+
*/
|
|
80
|
+
maxSnapshotBytes?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Redacts a value before it leaves the process.
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* Store state and event payloads frequently hold tokens, session material and personal
|
|
86
|
+
* data, and everything the agent forwards crosses a socket to another process. The hook is
|
|
87
|
+
* applied to **every value the agent encodes** — state snapshots, time-travel snapshots,
|
|
88
|
+
* event payloads and state patches — so nothing crosses unredacted. Return the replacement
|
|
89
|
+
* value, or the value itself to keep it.
|
|
90
|
+
*
|
|
91
|
+
* The `path` is the location within the structure being encoded (for a snapshot, from the
|
|
92
|
+
* state root; for a payload, from the payload root), so a recipe that matches key names
|
|
93
|
+
* covers all of them:
|
|
94
|
+
*
|
|
95
|
+
* ```ts
|
|
96
|
+
* const sanitize = (path: string, value: unknown) =>
|
|
97
|
+
* /token|secret|password|authorization/i.test(path) ? "[redacted]" : value;
|
|
98
|
+
* withDevtools(store, { port: 9800, sanitize });
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* Omitted, nothing is redacted — fine for a laptop loop, not fine for an embedded panel in
|
|
102
|
+
* production or on a shared machine.
|
|
103
|
+
*/
|
|
104
|
+
sanitize?: (path: string, value: unknown) => unknown;
|
|
60
105
|
/**
|
|
61
106
|
* Throttle interval for DevTools updates (ms). `0` disables throttling.
|
|
62
107
|
* @defaultValue `0`
|
|
@@ -97,4 +142,19 @@ export interface DevtoolsWrapperConfig {
|
|
|
97
142
|
* @defaultValue the native browser WebSocket factory
|
|
98
143
|
*/
|
|
99
144
|
socketFactory?: DevtoolsSocketFactory;
|
|
145
|
+
/**
|
|
146
|
+
* How the agent reaches the panel.
|
|
147
|
+
*
|
|
148
|
+
* @remarks
|
|
149
|
+
* - `"auto"` (the default) uses the `postMessage` bridge when an extension has announced
|
|
150
|
+
* itself on the page, and a WebSocket to the hub otherwise. This is what makes attaching a
|
|
151
|
+
* browser panel a single step — install the extension — instead of three.
|
|
152
|
+
* - `"bridge"` forces `postMessage`, for a relay that installs after the store is created.
|
|
153
|
+
* - `"websocket"` forces the hub, which is what a Node process or a remote session needs.
|
|
154
|
+
*
|
|
155
|
+
* Ignored when `socketFactory` is supplied: an explicit transport is always honoured.
|
|
156
|
+
*
|
|
157
|
+
* @defaultValue `"auto"`
|
|
158
|
+
*/
|
|
159
|
+
transport?: "auto" | "bridge" | "websocket";
|
|
100
160
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoltra/devtools-browser-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Browser store agent for Yoltra DevTools — instruments stores and reports to the hub via native WebSocket",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
],
|
|
39
39
|
"sideEffects": false,
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@yoltra/core": "^0.
|
|
41
|
+
"@yoltra/core": "^0.4.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@yoltra/devtools-protocol": "0.
|
|
44
|
+
"@yoltra/devtools-protocol": "0.4.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"typedoc": "^0.28.13",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"vite-plugin-dts": "^4.5.4",
|
|
53
53
|
"vite-plugin-banner": "0.8.1",
|
|
54
54
|
"vitest": "3.2.4",
|
|
55
|
-
"@yoltra/
|
|
56
|
-
"@yoltra/
|
|
55
|
+
"@yoltra/core": "0.4.0",
|
|
56
|
+
"@yoltra/devtools-ui": "0.4.0"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">=18.18"
|
|
@@ -61,13 +61,24 @@
|
|
|
61
61
|
"publishConfig": {
|
|
62
62
|
"access": "public"
|
|
63
63
|
},
|
|
64
|
+
"sizeLimit": [
|
|
65
|
+
{
|
|
66
|
+
"name": "barrel",
|
|
67
|
+
"limitKb": 3,
|
|
68
|
+
"external": [
|
|
69
|
+
"@yoltra/core",
|
|
70
|
+
"@yoltra/devtools-protocol"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
],
|
|
64
74
|
"scripts": {
|
|
65
75
|
"build": "vite build",
|
|
66
76
|
"lint": "node ../../tools/repo-tools/bin/repo-eslint.cjs --report-unused-disable-directives --max-warnings 0",
|
|
67
77
|
"test": "vitest --watch=false",
|
|
68
|
-
"typecheck": "tsc --noEmit",
|
|
78
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.vitest.json --noEmit",
|
|
69
79
|
"docs": "rushx docs:js && rushx docs:md",
|
|
70
80
|
"docs:md": "pnpm typedoc --options ./typedoc.json",
|
|
71
|
-
"docs:js": "pnpm typedoc --options ./typedoc.json --json ./.typedoc/devtools-browser-agent-en.json"
|
|
81
|
+
"docs:js": "pnpm typedoc --options ./typedoc.json --json ./.typedoc/devtools-browser-agent-en.json",
|
|
82
|
+
"size": "node ../../tools/repo-tools/bin/size-check.cjs"
|
|
72
83
|
}
|
|
73
84
|
}
|