@yoltra/devtools-browser-agent 0.7.0 β 0.8.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/LICENSE +21 -0
- package/README.es.md +5 -0
- package/README.md +5 -0
- package/dist/devtools-browser-agent.cjs +1 -1
- package/dist/devtools-browser-agent.cjs.map +1 -1
- package/dist/devtools-browser-agent.mjs +140 -126
- package/dist/devtools-browser-agent.mjs.map +1 -1
- package/dist/types/types.d.ts +11 -0
- package/package.json +9 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Yoltra β Copyright (c) 2026 Manu Ramirez <@pixerael>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.es.md
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
> π π²π½ VersiΓ³n en EspaΓ±ol | [ πΊπΈ English Version](./README.md)
|
|
6
6
|
|
|
7
|
+
[](https://www.npmjs.com/package/@yoltra/devtools-browser-agent)
|
|
8
|
+
[](https://www.npmjs.com/package/@yoltra/devtools-browser-agent)
|
|
9
|
+
[](https://www.npmjs.com/package/@yoltra/devtools-browser-agent)
|
|
10
|
+
[](https://github.com/yoltra/yoltra/blob/main/LICENSE)
|
|
11
|
+
|
|
7
12
|
**Agente de DevTools para el navegador β conecta un store de Yoltra al hub de DevTools desde el
|
|
8
13
|
navegador.**
|
|
9
14
|
|
package/README.md
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
> | π
|
|
7
7
|
> [ πΊπΈ English Version](./README.md)
|
|
8
8
|
|
|
9
|
+
[](https://www.npmjs.com/package/@yoltra/devtools-browser-agent)
|
|
10
|
+
[](https://www.npmjs.com/package/@yoltra/devtools-browser-agent)
|
|
11
|
+
[](https://www.npmjs.com/package/@yoltra/devtools-browser-agent)
|
|
12
|
+
[](https://github.com/yoltra/yoltra/blob/main/LICENSE)
|
|
13
|
+
|
|
9
14
|
**Browser DevTools agent β connect a Yoltra store to the DevTools hub from the browser.**
|
|
10
15
|
|
|
11
16
|
`@yoltra/devtools-browser-agent` transparently instruments a Yoltra store so every event, state
|
|
@@ -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 D=require("@yoltra/devtools-protocol"),E=require("@yoltra/core"),M="yoltra-devtools-bridge";function L(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 P=0,x=1,w=3;function O(t){return(n,s)=>{const i=t??globalThis.window;if(i===void 0)return{readyState:w,send:()=>{},close:()=>{},dispose:()=>{}};let c=P;const p=m=>{L(m.data,"to-page")&&s.onMessage(m.data.data)};i.addEventListener("message",p),queueMicrotask(()=>{c===P&&(c=x,s.onOpen())});const h=()=>{i.removeEventListener("message",p)};return{get readyState(){return c},send:m=>{if(c!==x)return;const T={channel:M,direction:"to-panel",data:m};i.postMessage(T,"*")},close:()=>{c!==w&&(c=w,h(),s.onClose())},dispose:h}}}const Y=(t,n)=>{const s=new WebSocket(t);return s.onopen=()=>n.onOpen(),s.onmessage=i=>n.onMessage(typeof i.data=="string"?i.data:String(i.data)),s.onclose=()=>n.onClose(),s.onerror=()=>n.onError(),{get readyState(){return s.readyState},send:i=>s.send(i),close:(i,c)=>s.close(i,c),dispose:()=>{s.onopen=null,s.onmessage=null,s.onclose=null,s.onerror=null}}};class $ extends D.ReconnectingWsClient{constructor(n,s,i,c,p=Y){super(n,s,i,c,p)}}const q="__YOLTRA_DEVTOOLS_BRIDGE__",G=6*1024*1024,W=512*1024;function z(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[q]===!0?O():void 0}function Q(t,n){const s=n.storeId??t.name,i=n.host??"localhost";let c=0,p=0,h=0;const m=[];let T=0;const _=new Map,R=new Map,F=(e,o)=>{const a=S.sampling;if(!a)return!1;const l=`${e}::${o}`,d=r=>r.some(([u,v])=>(u==="*"||u===e)&&(v==="*"||v===o));if(a.ignore?.some(r=>d(r.keys)))return!0;for(const r of a.throttle??[]){if(!d(r.keys))continue;const u=R.get(l)??0,v=Date.now();return v-u<r.intervalMs?!0:(R.set(l,v),!1)}for(const r of a.skip??[]){if(!d(r.keys))continue;const u=(_.get(l)??0)+1;return u<r.every?(_.set(l,u),!0):(_.set(l,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??G,C=n.maxEventBytes??W,f=n.sanitize,b=f===void 0?{}:{sanitize:f},U=e=>f===void 0?{}:{sanitize:(o,a)=>f(`${e}${o}`,a)},y=new $(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}:{}},z(n));let N=0;y.onBackpressure(e=>{const o=Date.now();o-N>5e3&&(N=o,console.warn(`[Yoltra DevTools] Backpressure: dropped ${e} event(s) while the hub was unreachable.`))});const g=()=>({timestamp:new Date().toISOString(),sourceId:s,sourceRole:D.DevtoolsRole.STORE});y.onMessage(async e=>{let o;try{o=JSON.parse(e)}catch{return}if(!(o===null||typeof o!="object"||typeof o.type!="string"))try{switch(o.type){case"REQUEST_STATE":{const a=t.getState(),l=E.encodeStateBounded(a,k,b),d={type:"STATE_SNAPSHOT",...g(),storeId:s,state:l.value,version:c,reducerNames:Object.keys(a),...l.truncated?{truncated:!0,...l.note!==void 0?{truncationNote:l.note}:{}}:{}};y.send(JSON.stringify(d));break}case"REQUEST_METRICS":{const a=t.__devtoolsIntrospect(),l=Date.now()-1e3,d=m.filter(J=>J>=l).length,r=Math.max(0,h-p),u=p>0?T/p:0,v={type:"STORE_METRICS",...g(),storeId:s,metrics:{eventCount:p,eventsPerSecond:d,avgProcessingTimeMs:u,dedupHits:a.dedupHits,queueDepth:a.queueDepth,reducerCount:a.reducers.length,effectCount:a.effects.length,middlewareCount:a.middleware.length,subscriberCount:a.event.length+a.coarse,connectorCount:a.atomic.length,middlewareRejections:r}};y.send(JSON.stringify(v));break}case"TIME_TRAVEL":{if(!S.replay||o.state==null)break;t.__applyExternalState(E.decodeState(o.state)),c=o.snapshotVersion??c;const a=t.getState(),l=E.encodeStateBounded(a,k,b),d={type:"STATE_SNAPSHOT",...g(),storeId:s,state:l.value,version:c,reducerNames:Object.keys(a)};y.send(JSON.stringify(d));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":{y.send(JSON.stringify(I()));break}default:{console.warn(`[Yoltra DevTools] Ignoring unknown message type: ${String(o.type)}`);break}}}catch(a){console.error(`[Yoltra DevTools] Command "${String(o.type)}" failed:`,a)}});const B=t.__yoltraDevtoolsDispose;B&&B();const V=t.instrument(e=>{if(h++,e.committed){p++,T+=e.reduceTimeMs;const r=Date.now();for(m.push(r);m.length>0&&r-m[0]>6e4;)m.shift()}if(F(e.event.channel,e.event.type))return;e.committed&&c++;const o=E.encodeStateBounded(e.event.payload,C,b);let a=!1;const l=e.committed?D.patchesFromChange(e.changedPaths,e.prevValues,e.nextValues).map(r=>{if(!("value"in r))return r;const u=E.encodeStateBounded(r.value,C,U(r.path));return u.truncated&&(a=!0),{...r,value:u.value}}):[],d={type:"STORE_EVENT",...g(),storeId:s,event:{id:e.event.id,channel:e.event.channel,type:e.event.type,payload:o.value,...o.truncated?{truncated:!0}:{}},patches:l,...a?{patchesTruncated:!0}:{},snapshotVersion:c,committed:e.committed};y.send(JSON.stringify(d))}),I=()=>{const e=t.__devtoolsIntrospect();return{type:"STORE_SUBSCRIPTIONS",...g(),storeId:s,atomic:e.atomic,event:e.event,coarse:e.coarse,effects:e.effects,middleware:e.middleware,reducers:e.reducers}},j=t.onRegistrationChange(e=>{e.every(o=>o.origin==="internal")||y.send(JSON.stringify(I()))});y.connect(i,n.port);const A=()=>{V(),j(),y.disconnect(),t.__yoltraDevtoolsDispose=void 0};t.__yoltraDevtoolsDispose=A;const H=t.dispose.bind(t);return t.dispose=()=>{A(),H()},t}exports.BRIDGE_CHANNEL=M;exports.createPostMessageSocketFactory=O;exports.isBridgeMessage=L;exports.withDevtools=Q;
|
|
2
2
|
//# sourceMappingURL=devtools-browser-agent.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools-browser-agent.cjs","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
|
+
{"version":3,"file":"devtools-browser-agent.cjs","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 { 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/**\n * Per-event byte cap for a payload or a single patch value.\n *\n * @remarks\n * Far below the snapshot cap, because events are frequent and a snapshot is not. Unbounded,\n * one oversized payload produced a frame past the hub's 8 MiB limit, and `ws` answers that\n * by closing the connection rather than dropping the message: a single large emit ended the\n * devtools session. Faithful binary encoding makes this reachable in ordinary use, since an\n * `ArrayBuffer` now carries its bytes instead of serializing to `{}`.\n */\nconst DEFAULT_MAX_EVENT_BYTES = 512 * 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 const maxEventBytes = config.maxEventBytes ?? DEFAULT_MAX_EVENT_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 wsClient.send(JSON.stringify(subscriptionsFrame()));\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 // Bounded like a snapshot. A payload is arbitrary application data and can be any\n // size, and the hub answers an oversized frame by closing the socket rather than\n // dropping the message.\n const boundedPayload = encodeStateBounded(\n info.event.payload,\n maxEventBytes,\n encodeOptions,\n );\n let patchesTruncated = false;\n const boundedPatches = info.committed\n ? patchesFromChange(info.changedPaths, info.prevValues, info.nextValues).map((op) => {\n if (!(\"value\" in op)) return op;\n const b = encodeStateBounded(op.value, maxEventBytes, patchEncodeOptions(op.path));\n if (b.truncated) patchesTruncated = true;\n return { ...op, value: b.value };\n })\n : [];\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: boundedPayload.value,\n ...(boundedPayload.truncated ? { truncated: true } : {}),\n },\n patches: boundedPatches,\n ...(patchesTruncated ? { patchesTruncated: true } : {}),\n snapshotVersion,\n committed: info.committed,\n };\n wsClient.send(JSON.stringify(storeEvent));\n });\n\n /** The `STORE_SUBSCRIPTIONS` frame, built from a fresh introspection. */\n const subscriptionsFrame = () => {\n const introspection = store.__devtoolsIntrospect();\n return {\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 };\n\n // Pushed, not only answered. `__devtoolsIntrospect()` is a pull, so until now the panel's\n // subscription list went stale the moment anything was registered at runtime: a decoration\n // mounting a slice, a hot reload, an `onEvent` added by a component. The panel had no way\n // to know and no reason to ask again.\n // The transport buffers while disconnected, so this needs no connection check. Each frame\n // is a full snapshot, so if several are buffered only the last one carries anything the\n // earlier ones did not.\n const registrationUnsub = store.onRegistrationChange((changes) => {\n // `internal` registrations are the store's own machinery, and `store.call()` mounts and\n // unmounts a reply listener per call. Forwarding those turned ordinary request/response\n // traffic into two whole-store snapshots per call, which is a lot of hub bandwidth to\n // describe something a panel does not display.\n if (changes.every((c) => c.origin === \"internal\")) return;\n wsClient.send(JSON.stringify(subscriptionsFrame()));\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 registrationUnsub();\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","DEFAULT_MAX_EVENT_BYTES","resolveSocketFactory","mode","withDevtools","store","host","snapshotVersion","committedEventCount","totalAttemptedCount","eventTimestamps","reduceTimeSumMs","skipCounters","throttleLast","isSampledOut","channel","type","sampling","eventKey","matches","keys","c","t","rule","last","nowMs","count","maxSnapshotBytes","maxEventBytes","sanitize","encodeOptions","patchEncodeOptions","opPath","path","wsClient","lastBackpressureWarn","dropped","baseMsg","DevtoolsRole","state","bounded","encodeStateBounded","response","introspection","cutoffMs","eventsPerSecond","middlewareRejections","avgProcessingTimeMs","decodeState","traveledState","travelBounded","travelSnapshot","subscriptionsFrame","err","existingDispose","instrumentUnsub","info","boundedPayload","patchesTruncated","boundedPatches","patchesFromChange","op","b","storeEvent","registrationUnsub","changes","disposeDevtools","prevDispose"],"mappings":"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"}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { ReconnectingWsClient as
|
|
2
|
-
import { decodeState as H, encodeStateBounded as
|
|
3
|
-
const
|
|
4
|
-
function
|
|
1
|
+
import { ReconnectingWsClient as Y, DevtoolsRole as $, patchesFromChange as j } from "@yoltra/devtools-protocol";
|
|
2
|
+
import { decodeState as H, encodeStateBounded as f } from "@yoltra/core";
|
|
3
|
+
const x = "yoltra-devtools-bridge";
|
|
4
|
+
function G(t, n) {
|
|
5
5
|
if (t === null || typeof t != "object") return !1;
|
|
6
6
|
const s = t;
|
|
7
|
-
return s.channel ===
|
|
7
|
+
return s.channel === x && s.direction === n && typeof s.data == "string";
|
|
8
8
|
}
|
|
9
|
-
const
|
|
10
|
-
function
|
|
9
|
+
const I = 0, A = 1, b = 3;
|
|
10
|
+
function B(t) {
|
|
11
11
|
return (n, s) => {
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
12
|
+
const i = t ?? globalThis.window;
|
|
13
|
+
if (i === void 0)
|
|
14
14
|
return {
|
|
15
|
-
readyState:
|
|
15
|
+
readyState: b,
|
|
16
16
|
send: () => {
|
|
17
17
|
},
|
|
18
18
|
close: () => {
|
|
@@ -20,75 +20,75 @@ function A(t) {
|
|
|
20
20
|
dispose: () => {
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
-
let
|
|
24
|
-
const
|
|
25
|
-
|
|
23
|
+
let c = I;
|
|
24
|
+
const p = (m) => {
|
|
25
|
+
G(m.data, "to-page") && s.onMessage(m.data.data);
|
|
26
26
|
};
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
i.addEventListener("message", p), queueMicrotask(() => {
|
|
28
|
+
c === I && (c = A, s.onOpen());
|
|
29
29
|
});
|
|
30
30
|
const h = () => {
|
|
31
|
-
|
|
31
|
+
i.removeEventListener("message", p);
|
|
32
32
|
};
|
|
33
33
|
return {
|
|
34
34
|
get readyState() {
|
|
35
|
-
return
|
|
35
|
+
return c;
|
|
36
36
|
},
|
|
37
|
-
send: (
|
|
38
|
-
if (
|
|
39
|
-
const T = { channel:
|
|
40
|
-
|
|
37
|
+
send: (m) => {
|
|
38
|
+
if (c !== A) return;
|
|
39
|
+
const T = { channel: x, direction: "to-panel", data: m };
|
|
40
|
+
i.postMessage(T, "*");
|
|
41
41
|
},
|
|
42
42
|
close: () => {
|
|
43
|
-
|
|
43
|
+
c !== b && (c = b, h(), s.onClose());
|
|
44
44
|
},
|
|
45
45
|
dispose: h
|
|
46
46
|
};
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
const
|
|
49
|
+
const W = (t, n) => {
|
|
50
50
|
const s = new WebSocket(t);
|
|
51
|
-
return s.onopen = () => n.onOpen(), s.onmessage = (
|
|
51
|
+
return s.onopen = () => n.onOpen(), s.onmessage = (i) => n.onMessage(typeof i.data == "string" ? i.data : String(i.data)), s.onclose = () => n.onClose(), s.onerror = () => n.onError(), {
|
|
52
52
|
get readyState() {
|
|
53
53
|
return s.readyState;
|
|
54
54
|
},
|
|
55
|
-
send: (
|
|
56
|
-
close: (
|
|
55
|
+
send: (i) => s.send(i),
|
|
56
|
+
close: (i, c) => s.close(i, c),
|
|
57
57
|
dispose: () => {
|
|
58
58
|
s.onopen = null, s.onmessage = null, s.onclose = null, s.onerror = null;
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
61
|
};
|
|
62
|
-
class
|
|
63
|
-
constructor(n, s,
|
|
64
|
-
super(n, s,
|
|
62
|
+
class q extends Y {
|
|
63
|
+
constructor(n, s, i, c, p = W) {
|
|
64
|
+
super(n, s, i, c, p);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
const
|
|
68
|
-
function
|
|
67
|
+
const z = "__YOLTRA_DEVTOOLS_BRIDGE__", Q = 6 * 1024 * 1024, K = 512 * 1024;
|
|
68
|
+
function X(t) {
|
|
69
69
|
if (t.socketFactory !== void 0) return t.socketFactory;
|
|
70
70
|
const n = t.transport ?? "auto";
|
|
71
|
-
return n === "websocket" ? void 0 : n === "bridge" || typeof globalThis == "object" && globalThis[
|
|
71
|
+
return n === "websocket" ? void 0 : n === "bridge" || typeof globalThis == "object" && globalThis[z] === !0 ? B() : void 0;
|
|
72
72
|
}
|
|
73
|
-
function
|
|
74
|
-
const s = n.storeId ?? t.name,
|
|
75
|
-
let
|
|
76
|
-
const
|
|
73
|
+
function te(t, n) {
|
|
74
|
+
const s = n.storeId ?? t.name, i = n.host ?? "localhost";
|
|
75
|
+
let c = 0, p = 0, h = 0;
|
|
76
|
+
const m = [];
|
|
77
77
|
let T = 0;
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
for (const
|
|
84
|
-
if (!
|
|
85
|
-
const
|
|
86
|
-
return v -
|
|
78
|
+
const _ = /* @__PURE__ */ new Map(), D = /* @__PURE__ */ new Map(), P = (e, o) => {
|
|
79
|
+
const a = S.sampling;
|
|
80
|
+
if (!a) return !1;
|
|
81
|
+
const l = `${e}::${o}`, d = (r) => r.some(([u, v]) => (u === "*" || u === e) && (v === "*" || v === o));
|
|
82
|
+
if (a.ignore?.some((r) => d(r.keys))) return !0;
|
|
83
|
+
for (const r of a.throttle ?? []) {
|
|
84
|
+
if (!d(r.keys)) continue;
|
|
85
|
+
const u = D.get(l) ?? 0, v = Date.now();
|
|
86
|
+
return v - u < r.intervalMs ? !0 : (D.set(l, v), !1);
|
|
87
87
|
}
|
|
88
|
-
for (const
|
|
89
|
-
if (!
|
|
90
|
-
const
|
|
91
|
-
return
|
|
88
|
+
for (const r of a.skip ?? []) {
|
|
89
|
+
if (!d(r.keys)) continue;
|
|
90
|
+
const u = (_.get(l) ?? 0) + 1;
|
|
91
|
+
return u < r.every ? (_.set(l, u), !0) : (_.set(l, 0), !1);
|
|
92
92
|
}
|
|
93
93
|
return !1;
|
|
94
94
|
}, S = {
|
|
@@ -99,7 +99,7 @@ function X(t, n) {
|
|
|
99
99
|
emit: n.allowEmit ?? !1
|
|
100
100
|
};
|
|
101
101
|
n.sampling && (S.sampling = n.sampling);
|
|
102
|
-
const O = n.maxSnapshotBytes ??
|
|
102
|
+
const O = n.maxSnapshotBytes ?? Q, M = n.maxEventBytes ?? K, E = n.sanitize, w = E === void 0 ? {} : { sanitize: E }, L = (e) => E === void 0 ? {} : { sanitize: (o, a) => E(`${e}${o}`, a) }, y = new q(
|
|
103
103
|
s,
|
|
104
104
|
t.name,
|
|
105
105
|
S,
|
|
@@ -110,13 +110,13 @@ function X(t, n) {
|
|
|
110
110
|
maxDelay: n.maxDelay ?? 3e4,
|
|
111
111
|
...n.authToken !== void 0 ? { authToken: n.authToken } : {}
|
|
112
112
|
},
|
|
113
|
-
|
|
113
|
+
X(n)
|
|
114
114
|
);
|
|
115
|
-
let
|
|
116
|
-
|
|
115
|
+
let R = 0;
|
|
116
|
+
y.onBackpressure((e) => {
|
|
117
117
|
const o = Date.now();
|
|
118
|
-
o -
|
|
119
|
-
`[Yoltra DevTools] Backpressure: dropped ${
|
|
118
|
+
o - R > 5e3 && (R = o, console.warn(
|
|
119
|
+
`[Yoltra DevTools] Backpressure: dropped ${e} event(s) while the hub was unreachable.`
|
|
120
120
|
));
|
|
121
121
|
});
|
|
122
122
|
const g = () => ({
|
|
@@ -124,10 +124,10 @@ function X(t, n) {
|
|
|
124
124
|
sourceId: s,
|
|
125
125
|
sourceRole: $.STORE
|
|
126
126
|
});
|
|
127
|
-
|
|
127
|
+
y.onMessage(async (e) => {
|
|
128
128
|
let o;
|
|
129
129
|
try {
|
|
130
|
-
o = JSON.parse(
|
|
130
|
+
o = JSON.parse(e);
|
|
131
131
|
} catch {
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
@@ -135,52 +135,52 @@ function X(t, n) {
|
|
|
135
135
|
try {
|
|
136
136
|
switch (o.type) {
|
|
137
137
|
case "REQUEST_STATE": {
|
|
138
|
-
const
|
|
138
|
+
const a = t.getState(), l = f(a, O, w), d = {
|
|
139
139
|
type: "STATE_SNAPSHOT",
|
|
140
140
|
...g(),
|
|
141
141
|
storeId: s,
|
|
142
|
-
state:
|
|
143
|
-
version:
|
|
144
|
-
reducerNames: Object.keys(
|
|
145
|
-
...
|
|
142
|
+
state: l.value,
|
|
143
|
+
version: c,
|
|
144
|
+
reducerNames: Object.keys(a),
|
|
145
|
+
...l.truncated ? { truncated: !0, ...l.note !== void 0 ? { truncationNote: l.note } : {} } : {}
|
|
146
146
|
};
|
|
147
|
-
|
|
147
|
+
y.send(JSON.stringify(d));
|
|
148
148
|
break;
|
|
149
149
|
}
|
|
150
150
|
case "REQUEST_METRICS": {
|
|
151
|
-
const
|
|
151
|
+
const a = t.__devtoolsIntrospect(), l = Date.now() - 1e3, d = m.filter((J) => J >= l).length, r = Math.max(0, h - p), u = p > 0 ? T / p : 0, v = {
|
|
152
152
|
type: "STORE_METRICS",
|
|
153
153
|
...g(),
|
|
154
154
|
storeId: s,
|
|
155
155
|
metrics: {
|
|
156
|
-
eventCount:
|
|
157
|
-
eventsPerSecond:
|
|
158
|
-
avgProcessingTimeMs:
|
|
159
|
-
dedupHits:
|
|
160
|
-
queueDepth:
|
|
161
|
-
reducerCount:
|
|
162
|
-
effectCount:
|
|
163
|
-
middlewareCount:
|
|
164
|
-
subscriberCount:
|
|
165
|
-
connectorCount:
|
|
166
|
-
middlewareRejections:
|
|
156
|
+
eventCount: p,
|
|
157
|
+
eventsPerSecond: d,
|
|
158
|
+
avgProcessingTimeMs: u,
|
|
159
|
+
dedupHits: a.dedupHits,
|
|
160
|
+
queueDepth: a.queueDepth,
|
|
161
|
+
reducerCount: a.reducers.length,
|
|
162
|
+
effectCount: a.effects.length,
|
|
163
|
+
middlewareCount: a.middleware.length,
|
|
164
|
+
subscriberCount: a.event.length + a.coarse,
|
|
165
|
+
connectorCount: a.atomic.length,
|
|
166
|
+
middlewareRejections: r
|
|
167
167
|
}
|
|
168
168
|
};
|
|
169
|
-
|
|
169
|
+
y.send(JSON.stringify(v));
|
|
170
170
|
break;
|
|
171
171
|
}
|
|
172
172
|
case "TIME_TRAVEL": {
|
|
173
173
|
if (!S.replay || o.state == null) break;
|
|
174
|
-
t.__applyExternalState(H(o.state)),
|
|
175
|
-
const
|
|
174
|
+
t.__applyExternalState(H(o.state)), c = o.snapshotVersion ?? c;
|
|
175
|
+
const a = t.getState(), l = f(a, O, w), d = {
|
|
176
176
|
type: "STATE_SNAPSHOT",
|
|
177
177
|
...g(),
|
|
178
178
|
storeId: s,
|
|
179
|
-
state:
|
|
180
|
-
version:
|
|
181
|
-
reducerNames: Object.keys(
|
|
179
|
+
state: l.value,
|
|
180
|
+
version: c,
|
|
181
|
+
reducerNames: Object.keys(a)
|
|
182
182
|
};
|
|
183
|
-
|
|
183
|
+
y.send(JSON.stringify(d));
|
|
184
184
|
break;
|
|
185
185
|
}
|
|
186
186
|
case "EVENT_REPLAY": {
|
|
@@ -192,18 +192,7 @@ function X(t, n) {
|
|
|
192
192
|
break;
|
|
193
193
|
}
|
|
194
194
|
case "REQUEST_SUBSCRIPTIONS": {
|
|
195
|
-
|
|
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));
|
|
195
|
+
y.send(JSON.stringify(C()));
|
|
207
196
|
break;
|
|
208
197
|
}
|
|
209
198
|
default: {
|
|
@@ -211,58 +200,83 @@ function X(t, n) {
|
|
|
211
200
|
break;
|
|
212
201
|
}
|
|
213
202
|
}
|
|
214
|
-
} catch (
|
|
203
|
+
} catch (a) {
|
|
215
204
|
console.error(
|
|
216
205
|
`[Yoltra DevTools] Command "${String(o.type)}" failed:`,
|
|
217
|
-
|
|
206
|
+
a
|
|
218
207
|
);
|
|
219
208
|
}
|
|
220
209
|
});
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
if (h++,
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
for (
|
|
228
|
-
|
|
210
|
+
const k = t.__yoltraDevtoolsDispose;
|
|
211
|
+
k && k();
|
|
212
|
+
const F = t.instrument((e) => {
|
|
213
|
+
if (h++, e.committed) {
|
|
214
|
+
p++, T += e.reduceTimeMs;
|
|
215
|
+
const r = Date.now();
|
|
216
|
+
for (m.push(r); m.length > 0 && r - m[0] > 6e4; )
|
|
217
|
+
m.shift();
|
|
229
218
|
}
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
const o =
|
|
219
|
+
if (P(e.event.channel, e.event.type)) return;
|
|
220
|
+
e.committed && c++;
|
|
221
|
+
const o = f(
|
|
222
|
+
e.event.payload,
|
|
223
|
+
M,
|
|
224
|
+
w
|
|
225
|
+
);
|
|
226
|
+
let a = !1;
|
|
227
|
+
const l = e.committed ? j(e.changedPaths, e.prevValues, e.nextValues).map((r) => {
|
|
228
|
+
if (!("value" in r)) return r;
|
|
229
|
+
const u = f(r.value, M, L(r.path));
|
|
230
|
+
return u.truncated && (a = !0), { ...r, value: u.value };
|
|
231
|
+
}) : [], d = {
|
|
233
232
|
type: "STORE_EVENT",
|
|
234
233
|
...g(),
|
|
235
234
|
storeId: s,
|
|
236
235
|
event: {
|
|
237
|
-
id:
|
|
238
|
-
channel:
|
|
239
|
-
type:
|
|
236
|
+
id: e.event.id,
|
|
237
|
+
channel: e.event.channel,
|
|
238
|
+
type: e.event.type,
|
|
240
239
|
// Encoded like state: a payload is arbitrary application data, so it can hold the same
|
|
241
240
|
// Map, BigInt or cycle that made a bare stringify throw or quietly destroy it.
|
|
242
|
-
payload:
|
|
241
|
+
payload: o.value,
|
|
242
|
+
...o.truncated ? { truncated: !0 } : {}
|
|
243
243
|
},
|
|
244
|
-
patches:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
244
|
+
patches: l,
|
|
245
|
+
...a ? { patchesTruncated: !0 } : {},
|
|
246
|
+
snapshotVersion: c,
|
|
247
|
+
committed: e.committed
|
|
248
|
+
};
|
|
249
|
+
y.send(JSON.stringify(d));
|
|
250
|
+
}), C = () => {
|
|
251
|
+
const e = t.__devtoolsIntrospect();
|
|
252
|
+
return {
|
|
253
|
+
type: "STORE_SUBSCRIPTIONS",
|
|
254
|
+
...g(),
|
|
255
|
+
storeId: s,
|
|
256
|
+
atomic: e.atomic,
|
|
257
|
+
event: e.event,
|
|
258
|
+
coarse: e.coarse,
|
|
259
|
+
effects: e.effects,
|
|
260
|
+
middleware: e.middleware,
|
|
261
|
+
reducers: e.reducers
|
|
249
262
|
};
|
|
250
|
-
|
|
263
|
+
}, U = t.onRegistrationChange((e) => {
|
|
264
|
+
e.every((o) => o.origin === "internal") || y.send(JSON.stringify(C()));
|
|
251
265
|
});
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
266
|
+
y.connect(i, n.port);
|
|
267
|
+
const N = () => {
|
|
268
|
+
F(), U(), y.disconnect(), t.__yoltraDevtoolsDispose = void 0;
|
|
255
269
|
};
|
|
256
|
-
t.__yoltraDevtoolsDispose =
|
|
270
|
+
t.__yoltraDevtoolsDispose = N;
|
|
257
271
|
const V = t.dispose.bind(t);
|
|
258
272
|
return t.dispose = () => {
|
|
259
|
-
|
|
273
|
+
N(), V();
|
|
260
274
|
}, t;
|
|
261
275
|
}
|
|
262
276
|
export {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
277
|
+
x as BRIDGE_CHANNEL,
|
|
278
|
+
B as createPostMessageSocketFactory,
|
|
279
|
+
G as isBridgeMessage,
|
|
280
|
+
te as withDevtools
|
|
267
281
|
};
|
|
268
282
|
//# sourceMappingURL=devtools-browser-agent.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools-browser-agent.mjs","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
|
+
{"version":3,"file":"devtools-browser-agent.mjs","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 { 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/**\n * Per-event byte cap for a payload or a single patch value.\n *\n * @remarks\n * Far below the snapshot cap, because events are frequent and a snapshot is not. Unbounded,\n * one oversized payload produced a frame past the hub's 8 MiB limit, and `ws` answers that\n * by closing the connection rather than dropping the message: a single large emit ended the\n * devtools session. Faithful binary encoding makes this reachable in ordinary use, since an\n * `ArrayBuffer` now carries its bytes instead of serializing to `{}`.\n */\nconst DEFAULT_MAX_EVENT_BYTES = 512 * 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 const maxEventBytes = config.maxEventBytes ?? DEFAULT_MAX_EVENT_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 wsClient.send(JSON.stringify(subscriptionsFrame()));\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 // Bounded like a snapshot. A payload is arbitrary application data and can be any\n // size, and the hub answers an oversized frame by closing the socket rather than\n // dropping the message.\n const boundedPayload = encodeStateBounded(\n info.event.payload,\n maxEventBytes,\n encodeOptions,\n );\n let patchesTruncated = false;\n const boundedPatches = info.committed\n ? patchesFromChange(info.changedPaths, info.prevValues, info.nextValues).map((op) => {\n if (!(\"value\" in op)) return op;\n const b = encodeStateBounded(op.value, maxEventBytes, patchEncodeOptions(op.path));\n if (b.truncated) patchesTruncated = true;\n return { ...op, value: b.value };\n })\n : [];\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: boundedPayload.value,\n ...(boundedPayload.truncated ? { truncated: true } : {}),\n },\n patches: boundedPatches,\n ...(patchesTruncated ? { patchesTruncated: true } : {}),\n snapshotVersion,\n committed: info.committed,\n };\n wsClient.send(JSON.stringify(storeEvent));\n });\n\n /** The `STORE_SUBSCRIPTIONS` frame, built from a fresh introspection. */\n const subscriptionsFrame = () => {\n const introspection = store.__devtoolsIntrospect();\n return {\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 };\n\n // Pushed, not only answered. `__devtoolsIntrospect()` is a pull, so until now the panel's\n // subscription list went stale the moment anything was registered at runtime: a decoration\n // mounting a slice, a hot reload, an `onEvent` added by a component. The panel had no way\n // to know and no reason to ask again.\n // The transport buffers while disconnected, so this needs no connection check. Each frame\n // is a full snapshot, so if several are buffered only the last one carries anything the\n // earlier ones did not.\n const registrationUnsub = store.onRegistrationChange((changes) => {\n // `internal` registrations are the store's own machinery, and `store.call()` mounts and\n // unmounts a reply listener per call. Forwarding those turned ordinary request/response\n // traffic into two whole-store snapshots per call, which is a lot of hub bandwidth to\n // describe something a panel does not display.\n if (changes.every((c) => c.origin === \"internal\")) return;\n wsClient.send(JSON.stringify(subscriptionsFrame()));\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 registrationUnsub();\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","DEFAULT_MAX_EVENT_BYTES","resolveSocketFactory","mode","withDevtools","store","host","snapshotVersion","committedEventCount","totalAttemptedCount","eventTimestamps","reduceTimeSumMs","skipCounters","throttleLast","isSampledOut","channel","type","sampling","eventKey","matches","keys","c","t","rule","last","nowMs","count","maxSnapshotBytes","maxEventBytes","sanitize","encodeOptions","patchEncodeOptions","opPath","path","wsClient","lastBackpressureWarn","dropped","baseMsg","DevtoolsRole","state","bounded","encodeStateBounded","response","introspection","cutoffMs","eventsPerSecond","middlewareRejections","avgProcessingTimeMs","decodeState","traveledState","travelBounded","travelSnapshot","subscriptionsFrame","err","existingDispose","instrumentUnsub","info","boundedPayload","patchesTruncated","boundedPatches","patchesFromChange","op","b","storeEvent","registrationUnsub","changes","disposeDevtools","prevDispose"],"mappings":"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"}
|
package/dist/types/types.d.ts
CHANGED
|
@@ -78,6 +78,17 @@ export interface DevtoolsWrapperConfig {
|
|
|
78
78
|
* @defaultValue 6291456 (6 MiB, under the hub's 8 MiB frame cap)
|
|
79
79
|
*/
|
|
80
80
|
maxSnapshotBytes?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Byte cap for a single event payload or patch value.
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* Separate from, and far below, {@link maxSnapshotBytes}: events are frequent and a
|
|
86
|
+
* snapshot is not. An oversized frame is answered by the hub closing the socket, not by
|
|
87
|
+
* dropping the message, so one large emit used to end the session.
|
|
88
|
+
*
|
|
89
|
+
* @defaultValue 524288 (512 KiB)
|
|
90
|
+
*/
|
|
91
|
+
maxEventBytes?: number;
|
|
81
92
|
/**
|
|
82
93
|
* Redacts a value before it leaves the process.
|
|
83
94
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yoltra/devtools-browser-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
],
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "https://github.com/yoltra/yoltra.git"
|
|
20
|
+
"url": "https://github.com/yoltra/yoltra.git",
|
|
21
|
+
"directory": "devtools/devtools-browser-agent"
|
|
21
22
|
},
|
|
22
23
|
"bugs": {
|
|
23
24
|
"url": "https://github.com/yoltra/yoltra/issues"
|
|
@@ -34,14 +35,15 @@
|
|
|
34
35
|
}
|
|
35
36
|
},
|
|
36
37
|
"files": [
|
|
37
|
-
"dist"
|
|
38
|
+
"dist",
|
|
39
|
+
"LICENSE"
|
|
38
40
|
],
|
|
39
41
|
"sideEffects": false,
|
|
40
42
|
"peerDependencies": {
|
|
41
|
-
"@yoltra/core": "^0.
|
|
43
|
+
"@yoltra/core": "^0.8.0"
|
|
42
44
|
},
|
|
43
45
|
"dependencies": {
|
|
44
|
-
"@yoltra/devtools-protocol": "0.
|
|
46
|
+
"@yoltra/devtools-protocol": "0.8.0"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@vitest/coverage-v8": "3.2.7",
|
|
@@ -53,8 +55,8 @@
|
|
|
53
55
|
"vite-plugin-banner": "0.8.1",
|
|
54
56
|
"vite-plugin-dts": "^4.5.4",
|
|
55
57
|
"vitest": "3.2.7",
|
|
56
|
-
"@yoltra/
|
|
57
|
-
"@yoltra/
|
|
58
|
+
"@yoltra/core": "0.8.0",
|
|
59
|
+
"@yoltra/devtools-ui": "0.8.0"
|
|
58
60
|
},
|
|
59
61
|
"engines": {
|
|
60
62
|
"node": ">=18"
|