@shadow-js/devtools 1.0.1 → 1.0.2
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/CHANGELOG.md +16 -0
- package/dist/dev/agent.js +9 -5
- package/dist/dev/agent.js.map +1 -1
- package/dist/dev/backend.d.ts +6 -0
- package/dist/dev/backend.js.map +1 -1
- package/dist/dev/hook.js +6 -5
- package/dist/dev/hook.js.map +1 -1
- package/dist/dev/internal/describe-element.js +109 -0
- package/dist/dev/internal/describe-element.js.map +1 -0
- package/dist/dev/internal/wire-guard.js +143 -0
- package/dist/dev/internal/wire-guard.js.map +1 -0
- package/dist/dev/model.d.ts +2 -0
- package/dist/dev/model.js +10 -4
- package/dist/dev/model.js.map +1 -1
- package/dist/dev/transport.d.ts +62 -2
- package/dist/dev/transport.js +294 -20
- package/dist/dev/transport.js.map +1 -1
- package/dist/prod/agent.js +1 -1
- package/dist/prod/agent.js.map +1 -1
- package/dist/prod/backend.js.map +1 -1
- package/dist/prod/hook.js.map +1 -1
- package/dist/prod/internal/describe-element.js +2 -0
- package/dist/prod/internal/describe-element.js.map +1 -0
- package/dist/prod/internal/wire-guard.js +2 -0
- package/dist/prod/internal/wire-guard.js.map +1 -0
- package/dist/prod/model.js +1 -1
- package/dist/prod/model.js.map +1 -1
- package/dist/prod/transport.js +1 -1
- package/dist/prod/transport.js.map +1 -1
- package/package.json +6 -6
package/dist/prod/transport.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import"./protocol.js";import{InspectorModel as e}from"./model.js";const
|
|
1
|
+
import"./protocol.js";import{InspectorModel as e}from"./model.js";import{isModelState as t,isWireEvent as n}from"./internal/wire-guard.js";const r=`shadowjs-devtools`,i=4e5,a=15e6,o=1024,s=256e6,c=3e4;function l(e){return typeof e==`object`&&!!e}function u(e,t){return l(e)&&e.channel===`shadowjs-devtools`&&e.from===t&&typeof e.v==`number`&&typeof e.type==`string`}function d(e){if(!u(e,`agent`))return!1;switch(e.type){case`hello`:return!0;case`events`:return Array.isArray(e.events);case`response`:return typeof e.id==`number`&&l(e.response);case`chunk`:{let{id:t,part:n,parts:r,size:i,data:a}=e;return typeof t==`number`&&typeof n==`number`&&typeof r==`number`&&typeof i==`number`&&typeof a==`string`&&Number.isInteger(n)&&Number.isInteger(r)&&Number.isInteger(i)&&r>=1&&r<=1024&&n>=0&&n<r&&i>=0&&a.length<=4e5}default:return!1}}function f(e){if(!u(e,`panel`))return!1;switch(e.type){case`hello`:return!0;case`request`:return typeof e.id==`number`&&l(e.request)&&typeof e.request.type==`string`;default:return!1}}function p(){let e=[/* @__PURE__ */ new Set,/* @__PURE__ */ new Set],t=!1,n=n=>({send(r){if(t)return;let i=structuredClone(r);for(let t of[...e[+(n===0)]])t(i)},listen(t){return e[n].add(t),()=>{e[n].delete(t)}},close(){t=!0,e[0].clear(),e[1].clear()}});return[n(0),n(1)]}function m(e){return{send(t){e.postMessage(t)},listen(t){let n=e=>t(e.data);return e.addEventListener(`message`,n),e.start?.(),()=>e.removeEventListener(`message`,n)},close(){e.close?.()}}}function h(e){if(e.origin===`*`)throw TypeError(`createWindowTransport: pass the real origin, not "*".`);let t=e.receiver??globalThis;return{send(t){e.target.postMessage(t,e.origin)},listen(n){let r=t=>{t.origin===e.origin&&t.source===e.target&&n(t.data)};return t.addEventListener(`message`,r),()=>t.removeEventListener(`message`,r)},close(){}}}function g(e,t=e=>new BroadcastChannel(e)){let n=t(e);return{send(e){n.postMessage(e)},listen(e){let t=t=>e(t.data);return n.addEventListener(`message`,t),()=>n.removeEventListener(`message`,t)},close(){n.close()}}}function _(){let e=globalThis.crypto;if(typeof e?.randomUUID==`function`)return e.randomUUID();if(typeof e?.getRandomValues==`function`){let t=e.getRandomValues(/* @__PURE__ */ new Uint8Array(16));return Array.from(t,e=>e.toString(16).padStart(2,`0`)).join(``)}return`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`}function v(e){let t=e.instance;return typeof t==`string`&&t.length>0&&t.length<=128?t:void 0}function y(e,t,n){let r;try{r=JSON.stringify(e)}catch(e){return`The page's answer could not be serialized: ${b(e)}`.slice(0,500)}if(r.length<=n)return;if(r.length>256e6||Math.ceil(r.length/t)>1024)return`The page's answer is larger than the panel accepts.`;let i=[];for(let e=0;e<r.length;){let n=Math.min(r.length,e+t),a=r.charCodeAt(n-1);n<r.length&&a>=55296&&a<=56319&&n-e>1&&--n,i.push(r.slice(e,n)),e=n}return{texts:i,size:r.length}}function b(e){return e instanceof Error?e.message:String(e)}function x(e,t,n={}){let a=n.batch??2e3,o=n.instance??_(),s=Math.max(1,Math.min(Math.floor(n.chunkChars??4e5),i)),c=n.singleResponseChars??15e6,l=e=>t.send({...e,instance:o}),u=(e,t)=>{let n=y(t,s,c);if(n===void 0)l({channel:r,v:1,from:`agent`,type:`response`,id:e,response:t});else if(typeof n==`string`)l({channel:r,v:1,from:`agent`,type:`response`,id:e,response:{ok:!1,error:n}});else for(let t=0;t<n.texts.length;t+=1)l({channel:r,v:1,from:`agent`,type:`chunk`,id:e,part:t,parts:n.texts.length,size:n.size,data:n.texts[t]??``})},d=t.listen(t=>{if(!f(t))return;if(t.type===`hello`){l({channel:r,v:1,from:`agent`,type:`hello`});return}let{request:i}=t,a;i.type===`highlight`?(n.highlight?.(i.id),a={ok:!0,result:null}):a=e.handle(i),u(t.id,a)}),p=e.subscribe(e=>{for(let t=0;t<e.length;t+=a)l({channel:r,v:1,from:`agent`,type:`events`,events:e.slice(t,t+a)})});return()=>{d(),p()}}function S(e){let t;try{t=JSON.parse(e)}catch(e){return{ok:!1,error:`The page's answer could not be read: ${b(e)}`.slice(0,500)}}if(l(t)&&!Array.isArray(t)){if(t.ok===!0)return{ok:!0,result:t.result};if(t.ok===!1&&typeof t.error==`string`)return{ok:!1,error:t.error.slice(0,500)}}return{ok:!1,error:`The page's answer could not be read.`}}const C=5e4;function w(i,a={}){let o=new e(a.limits),s=/* @__PURE__ */ new Set,c=/* @__PURE__ */ new Map,l=/* @__PURE__ */ new Map,u=a.chunkTimeoutMs??3e4,f=[],p=!1,m=1,h=!1,g=!1,_,y,b=()=>void 0,x=new Promise(e=>{b=e}),w=()=>{g||(g=!0,queueMicrotask(()=>{if(g=!1,!h)for(let e of s)e()}))},T=e=>i.send(e),E=e=>{let t=l.get(e);t!==void 0&&(clearTimeout(t.timer),l.delete(e))},D=()=>{for(let e of[...l.keys()])E(e)},O=(e,t)=>{E(e);let n=c.get(e);n!==void 0&&(c.delete(e),n.resolve(t))},k=(e,t)=>O(e,{ok:!1,error:t}),A=e=>setTimeout(()=>k(e,`The page's answer did not arrive in full.`),u),j=(e,t)=>{let n=l.get(e.id);if(n===void 0){if(e.size>256e6){k(e.id,`The page's answer is larger than the panel accepts.`);return}if(l.size>=4){let e=l.keys().next();e.done!==!0&&k(e.value,`The page's answer was given up on: too many answers were arriving at once.`)}n={instance:t,parts:e.parts,size:e.size,pieces:Array(e.parts).fill(void 0),received:0,chars:0,timer:A(e.id)},l.set(e.id,n)}else if(n.instance!==t||n.parts!==e.parts||n.size!==e.size)return;if(n.pieces[e.part]!==void 0)return;if(n.pieces[e.part]=e.data,n.received+=1,n.chars+=e.data.length,n.chars>n.size){k(e.id,`The pieces of the page's answer did not match its declared size.`);return}if(clearTimeout(n.timer),n.timer=A(e.id),n.received<n.parts)return;let r=n.pieces.join(``);r.length===n.size?O(e.id,S(r)):k(e.id,`The pieces of the page's answer did not match its declared size.`)},M=e=>{o.importState(e),y=void 0,p=!0;let t=f;f=[];for(let e of t)o.apply(e);b(),w()},N=e=>{if(h)return Promise.reject(/* @__PURE__ */ Error(`The devtools backend was disposed.`));let t=m;return m+=1,new Promise((n,i)=>{c.set(t,{resolve:n,reject:i}),T({channel:r,v:1,from:`panel`,type:`request`,id:t,request:e})})},P=i.listen(e=>{if(!d(e)||e.v!==1)return;let t=v(e);if(e.type!==`chunk`||c.has(e.id)&&(_===void 0||t===_)){if(t!==void 0&&(_!==void 0&&t!==_?(_=t,p=!1,f=[],D(),F()):_=t),e.type===`events`){let t=e.events.filter(n),r=t[0];if(p&&r!==void 0&&r.seq<=o.lastSeq&&(p=!1,f=[],F()),!p){for(let e of t)f.push(e);f.length>C&&f.splice(0,f.length-C);return}for(let e of t)o.apply(e);w()}else e.type===`chunk`?j(e,t):e.type===`response`&&O(e.id,e.response)}}),F=()=>{N({type:`snapshot`}).then(e=>{e.ok&&t(e.result)?M(e.result):(y=e.ok?`The page sent a state the panel cannot read.`:String(e.error).slice(0,500),w())},()=>{p=!1})};return T({channel:r,v:1,from:`panel`,type:`hello`}),F(),{model:o,ready:x,error:()=>y,subscribe(e){return s.add(e),()=>{s.delete(e)}},request(e){return e.type===`clear`?N(e).then(e=>(e.ok&&(p=!1,F()),e)):N(e)},dispose(){h=!0,P(),D(),s.clear();for(let e of c.values())e.reject(/* @__PURE__ */ Error(`The devtools backend was disposed.`));c.clear(),i.close()}}}export{r as CHANNEL,i as CHUNK_CHARS,c as CHUNK_TIMEOUT_MS,s as MAX_ASSEMBLED_CHARS,o as MAX_CHUNKS,a as SINGLE_RESPONSE_CHARS,w as connectRemote,g as createBroadcastChannelTransport,p as createMemoryTransportPair,m as createPortTransport,h as createWindowTransport,d as isAgentMessage,f as isPanelMessage,x as serveAgent};
|
|
2
2
|
//# sourceMappingURL=transport.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.js","names":["CHANNEL","isRecord","value","isEnvelope","\"channel\"","\"from\"","from","\"v\"","\"type\"","isAgentMessage","\"events\"","\"id\"","\"response\"","isPanelMessage","\"request\"","createMemoryTransportPair","handlers","closed","side","copy","message","handler","index","createPortTransport","port","listener","event","createWindowTransport","options","receiver","createBroadcastChannelTransport","n","channel","factory","name","serveAgent","limit","send","transport","stopListening","raw","response","request","agent","stopStreaming","i","events","connectRemote","model","InspectorModel","listeners","waiting","buffered","synced","nextRequest","disposed","notifyScheduled","resolveReady","ready","resolve","notify","applyState","state","pending","id","req","stop","entry","sync"],"sources":["../../src/transport.ts"],"sourcesContent":["import type { Agent, AgentRequest, AgentResponse } from \"./agent\";\nimport type { Backend, PageHooks } from \"./backend\";\nimport { InspectorModel, type ModelLimits, type ModelState } from \"./model\";\nimport { PROTOCOL_VERSION, type WireEvent } from \"./protocol\";\n\n/**\n * The `channel` field every devtools message carries. It lets the messages share a `postMessage`\n * or `BroadcastChannel` with other traffic: anything without this value is ignored.\n *\n * @example\n * import { CHANNEL } from \"@shadow-js/devtools\";\n * window.addEventListener(\"message\", (event: MessageEvent) => {\n * if (event.data?.channel === CHANNEL) console.log(\"devtools message\", event.data.type);\n * });\n * @since 1.0.0\n */\nexport const CHANNEL = \"shadowjs-devtools\";\n\n/**\n * A message the agent side sends to the panel: the answer to a `hello`, a batch of events, or the\n * answer to a request. All fields are plain data.\n *\n * @since 1.0.0\n */\nexport type AgentMessage =\n | { channel: typeof CHANNEL; v: number; from: \"agent\"; type: \"hello\" }\n | { channel: typeof CHANNEL; v: number; from: \"agent\"; type: \"events\"; events: WireEvent[] }\n | {\n channel: typeof CHANNEL;\n v: number;\n from: \"agent\";\n type: \"response\";\n id: number;\n response: AgentResponse;\n };\n\n/**\n * A message the panel side sends to the agent: a `hello`, or a request. The agent answers a request\n * with a `response` that carries the same `id`. All fields are plain data.\n *\n * @since 1.0.0\n */\nexport type PanelMessage =\n | { channel: typeof CHANNEL; v: number; from: \"panel\"; type: \"hello\" }\n | {\n channel: typeof CHANNEL;\n v: number;\n from: \"panel\";\n type: \"request\";\n id: number;\n request: AgentRequest;\n };\n\n/**\n * A duplex channel of plain messages. Implementations must deliver messages in order; they need not\n * be synchronous. `postMessage`, `MessagePort` and `BroadcastChannel` adapters are provided.\n *\n * @since 1.0.0\n */\nexport interface Transport {\n /**\n * Sends a message to the other end. The message must be plain data that can be structured-cloned.\n */\n send(message: unknown): void;\n /**\n * Starts delivering incoming messages to `handler`, in order.\n *\n * @returns A function that stops this listener.\n */\n listen(handler: (message: unknown) => void): () => void;\n /**\n * Releases what the transport holds. Closing a port or a `BroadcastChannel` closes it; a window\n * transport holds nothing and does nothing.\n */\n close(): void;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction isEnvelope(value: unknown, from: \"agent\" | \"panel\"): value is Record<string, unknown> {\n return (\n isRecord(value) &&\n value[\"channel\"] === CHANNEL &&\n value[\"from\"] === from &&\n typeof value[\"v\"] === \"number\" &&\n typeof value[\"type\"] === \"string\"\n );\n}\n\n/**\n * True for a well-formed message sent by an agent. Anything else on the channel is ignored. The check\n * covers the envelope (`channel`, `from`, `v`, `type`) and the fields each message type needs. It does\n * not validate every event inside an `events` message.\n *\n * @param value A received message.\n * @returns True when `value` is an {@link AgentMessage}.\n * @example\n * import { CHANNEL, isAgentMessage } from \"@shadow-js/devtools\";\n * isAgentMessage({ channel: CHANNEL, v: 1, from: \"agent\", type: \"hello\" }); // true\n * isAgentMessage({ channel: CHANNEL, v: 1, from: \"panel\", type: \"hello\" }); // false\n * @since 1.0.0\n */\nexport function isAgentMessage(value: unknown): value is AgentMessage {\n if (!isEnvelope(value, \"agent\")) return false;\n switch (value[\"type\"]) {\n case \"hello\":\n return true;\n case \"events\":\n return Array.isArray(value[\"events\"]);\n case \"response\":\n return typeof value[\"id\"] === \"number\" && isRecord(value[\"response\"]);\n default:\n return false;\n }\n}\n\n/**\n * True for a well-formed message sent by a panel. Anything else on the channel is ignored. The check\n * covers the envelope and the fields each message type needs, including the request's `type`.\n *\n * @param value A received message.\n * @returns True when `value` is a {@link PanelMessage}.\n * @example\n * import { CHANNEL, isPanelMessage } from \"@shadow-js/devtools\";\n * isPanelMessage({ channel: CHANNEL, v: 1, from: \"panel\", type: \"hello\" }); // true\n * isPanelMessage({ channel: CHANNEL, v: 1, from: \"agent\", type: \"hello\" }); // false\n * @since 1.0.0\n */\nexport function isPanelMessage(value: unknown): value is PanelMessage {\n if (!isEnvelope(value, \"panel\")) return false;\n switch (value[\"type\"]) {\n case \"hello\":\n return true;\n case \"request\":\n return (\n typeof value[\"id\"] === \"number\" &&\n isRecord(value[\"request\"]) &&\n typeof value[\"request\"][\"type\"] === \"string\"\n );\n default:\n return false;\n }\n}\n\n/**\n * Two connected in-memory transports, for tests and for connecting an agent and a panel inside one\n * page. Messages are structured-cloned, so anything that is not plain serializable data fails here\n * exactly as it would over `postMessage`. Closing either end closes both.\n *\n * @returns `[agentSide, panelSide]`.\n * @example\n * import { connectRemote, createAgent, createMemoryTransportPair, serveAgent } from \"@shadow-js/devtools\";\n * const [agentSide, panelSide] = createMemoryTransportPair();\n * const stop = serveAgent(createAgent(), agentSide);\n * const backend = connectRemote(panelSide);\n * await backend.ready;\n * stop();\n * @since 1.0.0\n */\nexport function createMemoryTransportPair(): [Transport, Transport] {\n const handlers: [Set<(message: unknown) => void>, Set<(message: unknown) => void>] = [\n new Set(),\n new Set(),\n ];\n let closed = false;\n const side = (index: 0 | 1): Transport => ({\n send(message): void {\n if (closed) return;\n const copy = structuredClone(message);\n for (const handler of [...handlers[index === 0 ? 1 : 0]]) handler(copy);\n },\n listen(handler): () => void {\n handlers[index].add(handler);\n return () => {\n handlers[index].delete(handler);\n };\n },\n close(): void {\n closed = true;\n handlers[0].clear();\n handlers[1].clear();\n },\n });\n return [side(0), side(1)];\n}\n\n/**\n * The part of a `MessagePort` the adapter uses. A `MessageChannel` end, a worker port and an\n * extension port all fit this shape.\n *\n * @since 1.0.0\n */\nexport interface MessagePortLike {\n /**\n * Posts a message to the other end of the port.\n */\n postMessage(message: unknown): void;\n /**\n * Adds a listener for incoming messages.\n */\n addEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n /**\n * Removes a listener added with `addEventListener`.\n */\n removeEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n /**\n * Starts the port. The adapter calls it after it adds a listener. Optional.\n */\n start?(): void;\n /**\n * Closes the port. Optional.\n */\n close?(): void;\n}\n\n/**\n * A transport over a `MessagePort`: a `MessageChannel` end, a worker port or an extension port.\n * Incoming messages reach the listeners registered with `listen`. `close` closes the port when it\n * supports closing.\n *\n * @param port The port.\n * @returns The transport.\n * @example\n * import { connectRemote, createAgent, createPortTransport, serveAgent } from \"@shadow-js/devtools\";\n * const channel = new MessageChannel();\n * serveAgent(createAgent(), createPortTransport(channel.port1));\n * const backend = connectRemote(createPortTransport(channel.port2));\n * await backend.ready;\n * @since 1.0.0\n */\nexport function createPortTransport(port: MessagePortLike): Transport {\n return {\n send(message): void {\n port.postMessage(message);\n },\n listen(handler): () => void {\n const listener = (event: { data: unknown }): void => handler(event.data);\n port.addEventListener(\"message\", listener);\n port.start?.();\n return () => port.removeEventListener(\"message\", listener);\n },\n close(): void {\n port.close?.();\n },\n };\n}\n\n/**\n * The part of a `Window` the adapter uses: its `postMessage`. A `WindowProxy` such as\n * `iframe.contentWindow` fits this shape.\n *\n * @since 1.0.0\n */\nexport interface WindowLike {\n /**\n * Posts a message to the window. The adapter always passes a real origin as `targetOrigin`.\n */\n postMessage(message: unknown, targetOrigin: string): void;\n}\n\n/**\n * The part of the receiving window the adapter uses: its `message` listeners. `globalThis` in a page\n * or a frame fits this shape.\n *\n * @since 1.0.0\n */\nexport interface WindowEvents {\n /**\n * Adds a listener for `message` events.\n */\n addEventListener(type: \"message\", listener: (event: MessageEvent) => void): void;\n /**\n * Removes a listener added with `addEventListener`.\n */\n removeEventListener(type: \"message\", listener: (event: MessageEvent) => void): void;\n}\n\n/**\n * Options of {@link createWindowTransport}.\n *\n * @since 1.0.0\n */\nexport interface WindowTransportOptions {\n /**\n * The window to talk to: the page being inspected, or the panel's frame.\n */\n target: WindowLike;\n /**\n * The origin of `target`. It is used as `targetOrigin` and to check where messages come from. Never\n * `\"*\"`: {@link createWindowTransport} refuses it.\n */\n origin: string;\n /**\n * Where messages arrive.\n *\n * @default globalThis\n */\n receiver?: WindowEvents;\n}\n\n/**\n * A transport over `window.postMessage` between a page and a panel in another window or frame (a\n * DevTools extension page, a popup). Both sides call it, each naming the other window and its origin.\n * Messages are sent only to `target`, with `origin` as the target origin. Incoming messages are\n * delivered only when their origin equals `origin` and their source is `target`; all others are\n * ignored.\n *\n * @param options Target window, its origin and the receiving window.\n * @returns The transport.\n * @throws {TypeError} When `origin` is `\"*\"`: the messages would reach any origin.\n * @example\n * import { createAgent, createWindowTransport, serveAgent } from \"@shadow-js/devtools\";\n * const frame = document.querySelector(\"iframe\");\n * if (frame?.contentWindow) {\n * const transport = createWindowTransport({ target: frame.contentWindow, origin: \"https://panel.example\" });\n * serveAgent(createAgent(), transport);\n * }\n * @since 1.0.0\n */\nexport function createWindowTransport(options: WindowTransportOptions): Transport {\n if (options.origin === \"*\")\n throw new TypeError('createWindowTransport: pass the real origin, not \"*\".');\n const receiver: WindowEvents = options.receiver ?? (globalThis as unknown as WindowEvents);\n return {\n send(message): void {\n options.target.postMessage(message, options.origin);\n },\n listen(handler): () => void {\n const listener = (event: MessageEvent): void => {\n if (event.origin !== options.origin) return;\n if ((event.source as unknown) !== options.target) return;\n handler(event.data);\n };\n receiver.addEventListener(\"message\", listener);\n return () => receiver.removeEventListener(\"message\", listener);\n },\n close(): void {\n // The window belongs to the page; nothing to release.\n },\n };\n}\n\n/**\n * The part of `BroadcastChannel` the adapter uses.\n *\n * @since 1.0.0\n */\nexport interface BroadcastChannelLike {\n /**\n * Posts a message to every other channel object with the same name.\n */\n postMessage(message: unknown): void;\n /**\n * Adds a listener for incoming messages.\n */\n addEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n /**\n * Removes a listener added with `addEventListener`.\n */\n removeEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n /**\n * Closes the channel object.\n */\n close(): void;\n}\n\n/**\n * A transport over a `BroadcastChannel`, for a panel in another tab or window of the same origin.\n * Several panels can listen to one agent; each message goes to all of them.\n *\n * @param name Channel name. Both sides use the same.\n * @param factory Creates the channel (default `new BroadcastChannel(name)`).\n * @returns The transport.\n * @example\n * import { connectRemote, createBroadcastChannelTransport } from \"@shadow-js/devtools\";\n * const backend = connectRemote(createBroadcastChannelTransport(\"shadowjs-devtools\"));\n * await backend.ready;\n * @since 1.0.0\n */\nexport function createBroadcastChannelTransport(\n name: string,\n factory: (name: string) => BroadcastChannelLike = (n) => new BroadcastChannel(n),\n): Transport {\n const channel = factory(name);\n return {\n send(message): void {\n channel.postMessage(message);\n },\n listen(handler): () => void {\n const listener = (event: { data: unknown }): void => handler(event.data);\n channel.addEventListener(\"message\", listener);\n return () => channel.removeEventListener(\"message\", listener);\n },\n close(): void {\n channel.close();\n },\n };\n}\n\n/**\n * Options of {@link serveAgent}: the page-side hooks and the batch size.\n *\n * @since 1.0.0\n */\nexport interface ServeOptions extends PageHooks {\n /**\n * Largest event batch sent in one message.\n *\n * @default 2000\n */\n batch?: number;\n}\n\n/**\n * Exposes an agent over a transport: forwards its event stream to the panel and answers the panel's\n * requests. The agent keeps recording while no panel listens, and the stream is sent regardless. What\n * happens to events nobody receives depends on the transport. A `highlight` request is passed to\n * `options.highlight` and answered without reaching the agent. Stop serving with the returned function.\n *\n * @param agent The agent in the inspected page.\n * @param transport The channel to the panel.\n * @param options Page-side behavior (highlight overlay) and batch size.\n * @returns A function that stops serving.\n * @example\n * import { createAgent, createMemoryTransportPair, serveAgent } from \"@shadow-js/devtools\";\n * const [pageSide] = createMemoryTransportPair();\n * const stop = serveAgent(createAgent(), pageSide, { highlight: (id) => console.log(\"highlight\", id) });\n * stop();\n * @since 1.0.0\n */\nexport function serveAgent(\n agent: Agent,\n transport: Transport,\n options: ServeOptions = {},\n): () => void {\n const limit = options.batch ?? 2000;\n const send = (message: AgentMessage): void => transport.send(message);\n const stopListening = transport.listen((raw) => {\n if (!isPanelMessage(raw)) return;\n if (raw.type === \"hello\") {\n send({ channel: CHANNEL, v: PROTOCOL_VERSION, from: \"agent\", type: \"hello\" });\n return;\n }\n const { request } = raw;\n let response: AgentResponse;\n if (request.type === \"highlight\") {\n options.highlight?.(request.id);\n response = { ok: true, result: null };\n } else {\n response = agent.handle(request);\n }\n send({\n channel: CHANNEL,\n v: PROTOCOL_VERSION,\n from: \"agent\",\n type: \"response\",\n id: raw.id,\n response,\n });\n });\n const stopStreaming = agent.subscribe((events) => {\n for (let i = 0; i < events.length; i += limit) {\n send({\n channel: CHANNEL,\n v: PROTOCOL_VERSION,\n from: \"agent\",\n type: \"events\",\n events: events.slice(i, i + limit) as WireEvent[],\n });\n }\n });\n return () => {\n stopListening();\n stopStreaming();\n };\n}\n\n/**\n * Options of {@link connectRemote}.\n *\n * @since 1.0.0\n */\nexport interface RemoteOptions {\n /**\n * Capacities of the panel's copy of the model (see {@link ModelLimits}). Fields that are not set keep\n * their defaults.\n */\n limits?: Partial<ModelLimits>;\n}\n\n/**\n * Connects a panel to a remote agent: asks for the complete state, then applies the event stream\n * to a local model. Events that arrive before the state are buffered, and the ones the state already\n * contains are skipped (sequence numbers), so no event is lost or applied twice. Disposing the backend\n * closes the transport.\n *\n * @param transport The channel to the agent.\n * @param options Model limits.\n * @returns A {@link Backend}.\n * @example\n * import { connectRemote, createBroadcastChannelTransport } from \"@shadow-js/devtools\";\n * const backend = connectRemote(createBroadcastChannelTransport(\"shadowjs-devtools\"));\n * await backend.ready;\n * @since 1.0.0\n */\nexport function connectRemote(transport: Transport, options: RemoteOptions = {}): Backend {\n const model = new InspectorModel(options.limits);\n const listeners = new Set<() => void>();\n const waiting = new Map<\n number,\n { resolve: (response: AgentResponse) => void; reject: (error: Error) => void }\n >();\n let buffered: WireEvent[] = [];\n let synced = false;\n let nextRequest = 1;\n let disposed = false;\n let notifyScheduled = false;\n let resolveReady: () => void = () => undefined;\n const ready = new Promise<void>((resolve) => {\n resolveReady = resolve;\n });\n\n const notify = (): void => {\n if (notifyScheduled) return;\n notifyScheduled = true;\n queueMicrotask(() => {\n notifyScheduled = false;\n if (disposed) return;\n for (const listener of listeners) listener();\n });\n };\n\n const send = (message: PanelMessage): void => transport.send(message);\n\n const applyState = (state: ModelState): void => {\n model.importState(state);\n synced = true;\n const pending = buffered;\n buffered = [];\n for (const event of pending) model.apply(event);\n resolveReady();\n notify();\n };\n\n const request = (req: AgentRequest): Promise<AgentResponse> => {\n if (disposed) return Promise.reject(new Error(\"The devtools backend was disposed.\"));\n const id = nextRequest;\n nextRequest += 1;\n return new Promise<AgentResponse>((resolve, reject) => {\n waiting.set(id, { resolve, reject });\n send({\n channel: CHANNEL,\n v: PROTOCOL_VERSION,\n from: \"panel\",\n type: \"request\",\n id,\n request: req,\n });\n });\n };\n\n const stop = transport.listen((raw) => {\n if (!isAgentMessage(raw)) return;\n if (raw.v !== PROTOCOL_VERSION) return;\n if (raw.type === \"events\") {\n if (!synced) {\n buffered.push(...raw.events);\n return;\n }\n for (const event of raw.events) model.apply(event);\n notify();\n return;\n }\n if (raw.type === \"response\") {\n const entry = waiting.get(raw.id);\n if (entry === undefined) return;\n waiting.delete(raw.id);\n entry.resolve(raw.response);\n }\n });\n\n const sync = (): void => {\n void request({ type: \"snapshot\" }).then(\n (response) => {\n if (response.ok) applyState(response.result as ModelState);\n },\n () => {\n // The only rejection is dispose(): there is nothing left to synchronize.\n synced = false;\n },\n );\n };\n\n send({ channel: CHANNEL, v: PROTOCOL_VERSION, from: \"panel\", type: \"hello\" });\n sync();\n\n return {\n model,\n ready,\n subscribe(listener): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n request(req): Promise<AgentResponse> {\n if (req.type === \"clear\") {\n // A clear empties the agent; the panel's copy must follow.\n return request(req).then((response) => {\n if (response.ok) {\n synced = false;\n sync();\n }\n return response;\n });\n }\n return request(req);\n },\n dispose(): void {\n disposed = true;\n stop();\n listeners.clear();\n for (const entry of waiting.values())\n entry.reject(new Error(\"The devtools backend was disposed.\"));\n waiting.clear();\n transport.close();\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"transport.js","names":["CHANNEL","CHUNK_CHARS","SINGLE_RESPONSE_CHARS","MAX_CHUNKS","MAX_ASSEMBLED_CHARS","CHUNK_TIMEOUT_MS","isRecord","value","isEnvelope","\"channel\"","\"from\"","from","\"v\"","\"type\"","isAgentMessage","\"events\"","\"id\"","\"response\"","id","part","parts","size","data","isPanelMessage","\"request\"","createMemoryTransportPair","handlers","closed","side","copy","message","handler","index","createPortTransport","port","listener","event","createWindowTransport","options","receiver","createBroadcastChannelTransport","n","channel","factory","name","newInstanceId","webCrypto","bytes","byte","instanceOf","splitAnswer","text","response","error","errorText","singleChars","pieceChars","texts","start","end","last","serveAgent","limit","instance","send","transport","answer","pieces","stopListening","raw","request","agent","stopStreaming","i","events","parseAnswer","\"ok\"","\"result\"","\"error\"","MAX_BUFFERED_EVENTS","connectRemote","model","InspectorModel","listeners","waiting","sets","chunkTimeout","buffered","synced","nextRequest","disposed","notifyScheduled","failure","resolveReady","ready","resolve","notify","dropSet","set","dropAllSets","settle","entry","giveUp","armTimer","takeChunk","oldest","applyState","state","pending","req","stop","sync","valid","isWireEvent","first","isModelState"],"sources":["../../src/transport.ts"],"sourcesContent":["import type { Agent, AgentRequest, AgentResponse } from \"./agent\";\nimport type { Backend, PageHooks } from \"./backend\";\nimport { isModelState, isWireEvent } from \"./internal/wire-guard\";\nimport { InspectorModel, type ModelLimits, type ModelState } from \"./model\";\nimport { PROTOCOL_VERSION, type WireEvent } from \"./protocol\";\n\n/**\n * The `channel` field every devtools message carries. It lets the messages share a `postMessage`\n * or `BroadcastChannel` with other traffic: anything without this value is ignored.\n *\n * @example\n * import { CHANNEL } from \"@shadow-js/devtools\";\n * window.addEventListener(\"message\", (event: MessageEvent) => {\n * if (event.data?.channel === CHANNEL) console.log(\"devtools message\", event.data.type);\n * });\n * @since 1.0.0\n */\nexport const CHANNEL = \"shadowjs-devtools\";\n\n/**\n * A message the agent side sends to the panel: the answer to a `hello`, a batch of events, or the\n * answer to a request. All fields are plain data.\n *\n * Every message {@link serveAgent} sends carries `instance`, a random id of that served agent. A reloaded\n * page starts a new agent with a new id, so a panel that sees the id change knows the state it holds is\n * from a page that is gone, even when the new agent's sequence numbers happen to be higher than the old\n * ones. The field is optional so that messages of an older agent still pass {@link isAgentMessage}.\n *\n * An answer whose JSON text is longer than {@link SINGLE_RESPONSE_CHARS} (the complete state of a very\n * busy page) is not one `response` message: the agent sends it as `chunk` messages of at most\n * {@link CHUNK_CHARS} characters each, and {@link connectRemote} joins them. Smaller answers keep the\n * single `response` message, so an older panel keeps working with a newer agent for everything it could\n * receive before.\n *\n * @since 1.0.0\n */\nexport type AgentMessage =\n | { channel: typeof CHANNEL; v: number; from: \"agent\"; type: \"hello\"; instance?: string }\n | {\n channel: typeof CHANNEL;\n v: number;\n from: \"agent\";\n type: \"events\";\n events: WireEvent[];\n instance?: string;\n }\n | {\n channel: typeof CHANNEL;\n v: number;\n from: \"agent\";\n type: \"response\";\n id: number;\n response: AgentResponse;\n instance?: string;\n }\n | {\n channel: typeof CHANNEL;\n v: number;\n from: \"agent\";\n type: \"chunk\";\n /** The id of the request the chunked answer belongs to. */\n id: number;\n /** The index of this piece, from 0. */\n part: number;\n /** How many pieces the answer has. */\n parts: number;\n /** The length of the whole JSON text, in characters: the panel checks the pieces add up to it. */\n size: number;\n /** This piece of the JSON text of the answer. */\n data: string;\n instance?: string;\n };\n\n/**\n * A message the panel side sends to the agent: a `hello`, or a request. The agent answers a request\n * with a `response` that carries the same `id`. All fields are plain data.\n *\n * @since 1.0.0\n */\nexport type PanelMessage =\n | { channel: typeof CHANNEL; v: number; from: \"panel\"; type: \"hello\" }\n | {\n channel: typeof CHANNEL;\n v: number;\n from: \"panel\";\n type: \"request\";\n id: number;\n request: AgentRequest;\n };\n\n/**\n * A duplex channel of plain messages. Implementations must deliver messages in order; they need not\n * be synchronous. `postMessage`, `MessagePort` and `BroadcastChannel` adapters are provided.\n *\n * @since 1.0.0\n */\nexport interface Transport {\n /**\n * Sends a message to the other end. The message must be plain data that can be structured-cloned.\n */\n send(message: unknown): void;\n /**\n * Starts delivering incoming messages to `handler`, in order.\n *\n * @returns A function that stops this listener.\n */\n listen(handler: (message: unknown) => void): () => void;\n /**\n * Releases what the transport holds. Closing a port or a `BroadcastChannel` closes it; a window\n * transport holds nothing and does nothing.\n */\n close(): void;\n}\n\n/**\n * Longest piece of a chunked answer, in characters of its JSON text. Twice this (every character escaped\n * when the message is serialized again) plus the envelope still fits the extension bridge's message cap,\n * so a chunk is never refused for its size.\n *\n * @example\n * CHUNK_CHARS; // 400000\n * @since 1.0.0\n */\nexport const CHUNK_CHARS = 400_000;\n\n/**\n * Longest answer the agent still sends as one `response` message, in characters of its JSON text. It is\n * just below the extension bridge's cap for an answer; anything longer is sent as chunks.\n *\n * @example\n * SINGLE_RESPONSE_CHARS; // 15000000\n * @since 1.0.0\n */\nexport const SINGLE_RESPONSE_CHARS = 15_000_000;\n\n/**\n * Most pieces one chunked answer may have. A panel ignores a chunk that claims more.\n *\n * @example\n * MAX_CHUNKS; // 1024\n * @since 1.0.0\n */\nexport const MAX_CHUNKS = 1024;\n\n/**\n * Longest chunked answer a panel assembles, in characters of its JSON text. A declared size above it is\n * refused at the first chunk, before anything is kept.\n *\n * @example\n * MAX_ASSEMBLED_CHARS; // 256000000\n * @since 1.0.0\n */\nexport const MAX_ASSEMBLED_CHARS = 256_000_000;\n\n/**\n * Most chunked answers a panel assembles at the same time. The oldest is given up when another starts.\n *\n * @example\n * MAX_OPEN_CHUNK_SETS; // 4\n * @since 1.0.0\n */\nexport const MAX_OPEN_CHUNK_SETS = 4;\n\n/**\n * How long a panel waits for the next chunk of an incomplete answer, in milliseconds. Each accepted\n * chunk restarts the wait; when it runs out, the request is answered with an error.\n *\n * @example\n * CHUNK_TIMEOUT_MS; // 30000\n * @since 1.0.0\n */\nexport const CHUNK_TIMEOUT_MS = 30_000;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction isEnvelope(value: unknown, from: \"agent\" | \"panel\"): value is Record<string, unknown> {\n return (\n isRecord(value) &&\n value[\"channel\"] === CHANNEL &&\n value[\"from\"] === from &&\n typeof value[\"v\"] === \"number\" &&\n typeof value[\"type\"] === \"string\"\n );\n}\n\n/**\n * True for a well-formed message sent by an agent. Anything else on the channel is ignored. The check\n * covers the envelope (`channel`, `from`, `v`, `type`) and the fields each message type needs, including\n * the bounds of a `chunk` (piece number below the count, count up to {@link MAX_CHUNKS}, text up to\n * {@link CHUNK_CHARS}). It does not validate every event inside an `events` message.\n *\n * @param value A received message.\n * @returns True when `value` is an {@link AgentMessage}.\n * @example\n * import { CHANNEL, isAgentMessage } from \"@shadow-js/devtools\";\n * isAgentMessage({ channel: CHANNEL, v: 1, from: \"agent\", type: \"hello\" }); // true\n * isAgentMessage({ channel: CHANNEL, v: 1, from: \"panel\", type: \"hello\" }); // false\n * @since 1.0.0\n */\nexport function isAgentMessage(value: unknown): value is AgentMessage {\n if (!isEnvelope(value, \"agent\")) return false;\n switch (value[\"type\"]) {\n case \"hello\":\n return true;\n case \"events\":\n return Array.isArray(value[\"events\"]);\n case \"response\":\n return typeof value[\"id\"] === \"number\" && isRecord(value[\"response\"]);\n case \"chunk\": {\n const { id, part, parts, size, data } = value;\n return (\n typeof id === \"number\" &&\n typeof part === \"number\" &&\n typeof parts === \"number\" &&\n typeof size === \"number\" &&\n typeof data === \"string\" &&\n Number.isInteger(part) &&\n Number.isInteger(parts) &&\n Number.isInteger(size) &&\n parts >= 1 &&\n parts <= MAX_CHUNKS &&\n part >= 0 &&\n part < parts &&\n size >= 0 &&\n data.length <= CHUNK_CHARS\n );\n }\n default:\n return false;\n }\n}\n\n/**\n * True for a well-formed message sent by a panel. Anything else on the channel is ignored. The check\n * covers the envelope and the fields each message type needs, including the request's `type`.\n *\n * @param value A received message.\n * @returns True when `value` is a {@link PanelMessage}.\n * @example\n * import { CHANNEL, isPanelMessage } from \"@shadow-js/devtools\";\n * isPanelMessage({ channel: CHANNEL, v: 1, from: \"panel\", type: \"hello\" }); // true\n * isPanelMessage({ channel: CHANNEL, v: 1, from: \"agent\", type: \"hello\" }); // false\n * @since 1.0.0\n */\nexport function isPanelMessage(value: unknown): value is PanelMessage {\n if (!isEnvelope(value, \"panel\")) return false;\n switch (value[\"type\"]) {\n case \"hello\":\n return true;\n case \"request\":\n return (\n typeof value[\"id\"] === \"number\" &&\n isRecord(value[\"request\"]) &&\n typeof value[\"request\"][\"type\"] === \"string\"\n );\n default:\n return false;\n }\n}\n\n/**\n * Two connected in-memory transports, for tests and for connecting an agent and a panel inside one\n * page. Messages are structured-cloned, so anything that is not plain serializable data fails here\n * exactly as it would over `postMessage`. Closing either end closes both.\n *\n * @returns `[agentSide, panelSide]`.\n * @example\n * import { connectRemote, createAgent, createMemoryTransportPair, serveAgent } from \"@shadow-js/devtools\";\n * const [agentSide, panelSide] = createMemoryTransportPair();\n * const stop = serveAgent(createAgent(), agentSide);\n * const backend = connectRemote(panelSide);\n * await backend.ready;\n * stop();\n * @since 1.0.0\n */\nexport function createMemoryTransportPair(): [Transport, Transport] {\n const handlers: [Set<(message: unknown) => void>, Set<(message: unknown) => void>] = [\n new Set(),\n new Set(),\n ];\n let closed = false;\n const side = (index: 0 | 1): Transport => ({\n send(message): void {\n if (closed) return;\n const copy = structuredClone(message);\n for (const handler of [...handlers[index === 0 ? 1 : 0]]) handler(copy);\n },\n listen(handler): () => void {\n handlers[index].add(handler);\n return () => {\n handlers[index].delete(handler);\n };\n },\n close(): void {\n closed = true;\n handlers[0].clear();\n handlers[1].clear();\n },\n });\n return [side(0), side(1)];\n}\n\n/**\n * The part of a `MessagePort` the adapter uses. A `MessageChannel` end, a worker port and an\n * extension port all fit this shape.\n *\n * @since 1.0.0\n */\nexport interface MessagePortLike {\n /**\n * Posts a message to the other end of the port.\n */\n postMessage(message: unknown): void;\n /**\n * Adds a listener for incoming messages.\n */\n addEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n /**\n * Removes a listener added with `addEventListener`.\n */\n removeEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n /**\n * Starts the port. The adapter calls it after it adds a listener. Optional.\n */\n start?(): void;\n /**\n * Closes the port. Optional.\n */\n close?(): void;\n}\n\n/**\n * A transport over a `MessagePort`: a `MessageChannel` end, a worker port or an extension port.\n * Incoming messages reach the listeners registered with `listen`. `close` closes the port when it\n * supports closing.\n *\n * @param port The port.\n * @returns The transport.\n * @example\n * import { connectRemote, createAgent, createPortTransport, serveAgent } from \"@shadow-js/devtools\";\n * const channel = new MessageChannel();\n * serveAgent(createAgent(), createPortTransport(channel.port1));\n * const backend = connectRemote(createPortTransport(channel.port2));\n * await backend.ready;\n * @since 1.0.0\n */\nexport function createPortTransport(port: MessagePortLike): Transport {\n return {\n send(message): void {\n port.postMessage(message);\n },\n listen(handler): () => void {\n const listener = (event: { data: unknown }): void => handler(event.data);\n port.addEventListener(\"message\", listener);\n port.start?.();\n return () => port.removeEventListener(\"message\", listener);\n },\n close(): void {\n port.close?.();\n },\n };\n}\n\n/**\n * The part of a `Window` the adapter uses: its `postMessage`. A `WindowProxy` such as\n * `iframe.contentWindow` fits this shape.\n *\n * @since 1.0.0\n */\nexport interface WindowLike {\n /**\n * Posts a message to the window. The adapter always passes a real origin as `targetOrigin`.\n */\n postMessage(message: unknown, targetOrigin: string): void;\n}\n\n/**\n * The part of the receiving window the adapter uses: its `message` listeners. `globalThis` in a page\n * or a frame fits this shape.\n *\n * @since 1.0.0\n */\nexport interface WindowEvents {\n /**\n * Adds a listener for `message` events.\n */\n addEventListener(type: \"message\", listener: (event: MessageEvent) => void): void;\n /**\n * Removes a listener added with `addEventListener`.\n */\n removeEventListener(type: \"message\", listener: (event: MessageEvent) => void): void;\n}\n\n/**\n * Options of {@link createWindowTransport}.\n *\n * @since 1.0.0\n */\nexport interface WindowTransportOptions {\n /**\n * The window to talk to: the page being inspected, or the panel's frame.\n */\n target: WindowLike;\n /**\n * The origin of `target`. It is used as `targetOrigin` and to check where messages come from. Never\n * `\"*\"`: {@link createWindowTransport} refuses it.\n */\n origin: string;\n /**\n * Where messages arrive.\n *\n * @default globalThis\n */\n receiver?: WindowEvents;\n}\n\n/**\n * A transport over `window.postMessage` between a page and a panel in another window or frame (a\n * DevTools extension page, a popup). Both sides call it, each naming the other window and its origin.\n * Messages are sent only to `target`, with `origin` as the target origin. Incoming messages are\n * delivered only when their origin equals `origin` and their source is `target`; all others are\n * ignored.\n *\n * @param options Target window, its origin and the receiving window.\n * @returns The transport.\n * @throws {TypeError} When `origin` is `\"*\"`: the messages would reach any origin.\n * @example\n * import { createAgent, createWindowTransport, serveAgent } from \"@shadow-js/devtools\";\n * const frame = document.querySelector(\"iframe\");\n * if (frame?.contentWindow) {\n * const transport = createWindowTransport({ target: frame.contentWindow, origin: \"https://panel.example\" });\n * serveAgent(createAgent(), transport);\n * }\n * @since 1.0.0\n */\nexport function createWindowTransport(options: WindowTransportOptions): Transport {\n if (options.origin === \"*\")\n throw new TypeError('createWindowTransport: pass the real origin, not \"*\".');\n const receiver: WindowEvents = options.receiver ?? (globalThis as unknown as WindowEvents);\n return {\n send(message): void {\n options.target.postMessage(message, options.origin);\n },\n listen(handler): () => void {\n const listener = (event: MessageEvent): void => {\n if (event.origin !== options.origin) return;\n if ((event.source as unknown) !== options.target) return;\n handler(event.data);\n };\n receiver.addEventListener(\"message\", listener);\n return () => receiver.removeEventListener(\"message\", listener);\n },\n close(): void {\n // The window belongs to the page; nothing to release.\n },\n };\n}\n\n/**\n * The part of `BroadcastChannel` the adapter uses.\n *\n * @since 1.0.0\n */\nexport interface BroadcastChannelLike {\n /**\n * Posts a message to every other channel object with the same name.\n */\n postMessage(message: unknown): void;\n /**\n * Adds a listener for incoming messages.\n */\n addEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n /**\n * Removes a listener added with `addEventListener`.\n */\n removeEventListener(type: \"message\", listener: (event: { data: unknown }) => void): void;\n /**\n * Closes the channel object.\n */\n close(): void;\n}\n\n/**\n * A transport over a `BroadcastChannel`, for a panel in another tab or window of the same origin.\n * Several panels can listen to one agent; each message goes to all of them.\n *\n * @param name Channel name. Both sides use the same.\n * @param factory Creates the channel (default `new BroadcastChannel(name)`).\n * @returns The transport.\n * @example\n * import { connectRemote, createBroadcastChannelTransport } from \"@shadow-js/devtools\";\n * const backend = connectRemote(createBroadcastChannelTransport(\"shadowjs-devtools\"));\n * await backend.ready;\n * @since 1.0.0\n */\nexport function createBroadcastChannelTransport(\n name: string,\n factory: (name: string) => BroadcastChannelLike = (n) => new BroadcastChannel(n),\n): Transport {\n const channel = factory(name);\n return {\n send(message): void {\n channel.postMessage(message);\n },\n listen(handler): () => void {\n const listener = (event: { data: unknown }): void => handler(event.data);\n channel.addEventListener(\"message\", listener);\n return () => channel.removeEventListener(\"message\", listener);\n },\n close(): void {\n channel.close();\n },\n };\n}\n\n/**\n * Options of {@link serveAgent}: the page-side hooks and the batch size.\n *\n * @since 1.0.0\n */\nexport interface ServeOptions extends PageHooks {\n /**\n * Largest event batch sent in one message.\n *\n * @default 2000\n */\n batch?: number;\n /**\n * The id this served agent puts on every message it sends (see {@link AgentMessage}). Leave it unset:\n * a random id is made for each call. Tests set it to get a known value.\n */\n instance?: string;\n /**\n * Longest piece of a chunked answer, in characters. Values above {@link CHUNK_CHARS} are lowered to\n * it, because the panel and the bridge refuse larger pieces.\n *\n * @default CHUNK_CHARS\n */\n chunkChars?: number;\n /**\n * Longest answer sent as one message, in characters of its JSON text. A longer one is sent as chunks.\n *\n * @default SINGLE_RESPONSE_CHARS\n */\n singleResponseChars?: number;\n}\n\n/**\n * A random id for one served agent. `crypto.randomUUID` needs a secure context, so a page on plain\n * http falls back to `getRandomValues`, and then to a time-and-`Math.random` mix: the id only has to\n * differ between two agent starts, it is not a secret.\n */\nfunction newInstanceId(): string {\n const webCrypto: Crypto | undefined = (globalThis as { crypto?: Crypto }).crypto;\n if (typeof webCrypto?.randomUUID === \"function\") return webCrypto.randomUUID();\n if (typeof webCrypto?.getRandomValues === \"function\") {\n const bytes = webCrypto.getRandomValues(new Uint8Array(16));\n return Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/** Longest instance id the panel keeps: an id from the page is untrusted data. */\nconst MAX_INSTANCE_CHARS = 128;\n\n/** The instance id a message carries, or `undefined` when it has none or the value is not a usable id. */\nfunction instanceOf(message: AgentMessage): string | undefined {\n const id: unknown = message.instance;\n return typeof id === \"string\" && id.length > 0 && id.length <= MAX_INSTANCE_CHARS\n ? id\n : undefined;\n}\n\n/** Longest error text the panel keeps from an answer: the text is untrusted data. */\nconst MAX_ERROR_CHARS = 500;\n\n/**\n * Cuts an answer into pieces when its JSON text is longer than `singleChars`. Returns `undefined` for an answer that\n * goes in one message, a string (the reason) when it cannot be serialized, and the pieces otherwise. A cut never\n * falls between the two halves of a surrogate pair, so every piece is well-formed text.\n */\nfunction splitAnswer(\n response: AgentResponse,\n pieceChars: number,\n singleChars: number,\n): undefined | string | { texts: string[]; size: number } {\n let text: string;\n try {\n text = JSON.stringify(response);\n } catch (error) {\n // A cycle or a BigInt: the panel gets the reason instead of a message the transport would throw on.\n return `The page's answer could not be serialized: ${errorText(error)}`.slice(\n 0,\n MAX_ERROR_CHARS,\n );\n }\n if (text.length <= singleChars) return undefined;\n if (text.length > MAX_ASSEMBLED_CHARS || Math.ceil(text.length / pieceChars) > MAX_CHUNKS) {\n return \"The page's answer is larger than the panel accepts.\";\n }\n const texts: string[] = [];\n for (let start = 0; start < text.length; ) {\n let end = Math.min(text.length, start + pieceChars);\n const last = text.charCodeAt(end - 1);\n // Keep a pair together unless the piece is one unit long (then there is no room to move the cut).\n if (end < text.length && last >= 0xd800 && last <= 0xdbff && end - start > 1) end -= 1;\n texts.push(text.slice(start, end));\n start = end;\n }\n return { texts, size: text.length };\n}\n\nfunction errorText(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Exposes an agent over a transport: forwards its event stream to the panel and answers the panel's\n * requests. The agent keeps recording while no panel listens, and the stream is sent regardless. What\n * happens to events nobody receives depends on the transport. A `highlight` request is passed to\n * `options.highlight` and answered without reaching the agent. Stop serving with the returned function.\n *\n * @param agent The agent in the inspected page.\n * @param transport The channel to the panel.\n * @param options Page-side behavior (highlight overlay) and batch size.\n * @returns A function that stops serving.\n * @example\n * import { createAgent, createMemoryTransportPair, serveAgent } from \"@shadow-js/devtools\";\n * const [pageSide] = createMemoryTransportPair();\n * const stop = serveAgent(createAgent(), pageSide, { highlight: (id) => console.log(\"highlight\", id) });\n * stop();\n * @since 1.0.0\n */\nexport function serveAgent(\n agent: Agent,\n transport: Transport,\n options: ServeOptions = {},\n): () => void {\n const limit = options.batch ?? 2000;\n const instance = options.instance ?? newInstanceId();\n const pieceChars = Math.max(\n 1,\n Math.min(Math.floor(options.chunkChars ?? CHUNK_CHARS), CHUNK_CHARS),\n );\n const singleChars = options.singleResponseChars ?? SINGLE_RESPONSE_CHARS;\n const send = (message: AgentMessage): void => transport.send({ ...message, instance });\n const answer = (id: number, response: AgentResponse): void => {\n const pieces = splitAnswer(response, pieceChars, singleChars);\n if (pieces === undefined) {\n send({\n channel: CHANNEL,\n v: PROTOCOL_VERSION,\n from: \"agent\",\n type: \"response\",\n id,\n response,\n });\n return;\n }\n if (typeof pieces === \"string\") {\n // Reported to the panel, which shows it, instead of thrown into the transport.\n send({\n channel: CHANNEL,\n v: PROTOCOL_VERSION,\n from: \"agent\",\n type: \"response\",\n id,\n response: { ok: false, error: pieces },\n });\n return;\n }\n for (let part = 0; part < pieces.texts.length; part += 1) {\n send({\n channel: CHANNEL,\n v: PROTOCOL_VERSION,\n from: \"agent\",\n type: \"chunk\",\n id,\n part,\n parts: pieces.texts.length,\n size: pieces.size,\n data: pieces.texts[part] ?? \"\",\n });\n }\n };\n const stopListening = transport.listen((raw) => {\n if (!isPanelMessage(raw)) return;\n if (raw.type === \"hello\") {\n send({ channel: CHANNEL, v: PROTOCOL_VERSION, from: \"agent\", type: \"hello\" });\n return;\n }\n const { request } = raw;\n let response: AgentResponse;\n if (request.type === \"highlight\") {\n options.highlight?.(request.id);\n response = { ok: true, result: null };\n } else {\n response = agent.handle(request);\n }\n answer(raw.id, response);\n });\n const stopStreaming = agent.subscribe((events) => {\n for (let i = 0; i < events.length; i += limit) {\n send({\n channel: CHANNEL,\n v: PROTOCOL_VERSION,\n from: \"agent\",\n type: \"events\",\n events: events.slice(i, i + limit) as WireEvent[],\n });\n }\n });\n return () => {\n stopListening();\n stopStreaming();\n };\n}\n\n/** A chunked answer being assembled by {@link connectRemote}. */\ninterface ChunkSet {\n /** The instance of the first piece: later pieces must come from the same agent. */\n readonly instance: string | undefined;\n readonly parts: number;\n /** The declared length of the whole text. */\n readonly size: number;\n readonly pieces: Array<string | undefined>;\n received: number;\n chars: number;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/** Reads the JSON text of a chunked answer; what is not an answer becomes an error answer. */\nfunction parseAnswer(text: string): AgentResponse {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (error) {\n return {\n ok: false,\n error: `The page's answer could not be read: ${errorText(error)}`.slice(0, MAX_ERROR_CHARS),\n };\n }\n if (isRecord(value) && !Array.isArray(value)) {\n if (value[\"ok\"] === true) return { ok: true, result: value[\"result\"] };\n if (value[\"ok\"] === false && typeof value[\"error\"] === \"string\") {\n return { ok: false, error: value[\"error\"].slice(0, MAX_ERROR_CHARS) };\n }\n }\n return { ok: false, error: \"The page's answer could not be read.\" };\n}\n\n/**\n * Most events {@link connectRemote} holds while it waits for the first complete state. The newest are\n * kept: the state names its own `seq`, and an event older than it is skipped anyway.\n */\nconst MAX_BUFFERED_EVENTS = 50_000;\n\n/**\n * Options of {@link connectRemote}.\n *\n * @since 1.0.0\n */\nexport interface RemoteOptions {\n /**\n * Capacities of the panel's copy of the model (see {@link ModelLimits}). Fields that are not set keep\n * their defaults.\n */\n limits?: Partial<ModelLimits>;\n /**\n * How long to wait for the next piece of a chunked answer before giving the request up, in milliseconds.\n *\n * @default CHUNK_TIMEOUT_MS\n */\n chunkTimeoutMs?: number;\n}\n\n/**\n * Connects a panel to a remote agent: asks for the complete state, then applies the event stream\n * to a local model. Events that arrive before the state are buffered, and the ones the state already\n * contains are skipped (sequence numbers), so no event is lost or applied twice. Disposing the backend\n * closes the transport.\n *\n * When the agent cannot give its state (an answer the bridge refused as too large, or a state the panel\n * cannot read), `ready` stays pending and `backend.error()` returns the reason after a change notification,\n * so a panel can show it instead of waiting. A message from another agent instance (a reloaded page)\n * discards the state and asks for a new one.\n *\n * @param transport The channel to the agent.\n * @param options Model limits.\n * @returns A {@link Backend}.\n * @example\n * import { connectRemote, createBroadcastChannelTransport } from \"@shadow-js/devtools\";\n * const backend = connectRemote(createBroadcastChannelTransport(\"shadowjs-devtools\"));\n * await backend.ready;\n * @since 1.0.0\n */\nexport function connectRemote(transport: Transport, options: RemoteOptions = {}): Backend {\n const model = new InspectorModel(options.limits);\n const listeners = new Set<() => void>();\n const waiting = new Map<\n number,\n {\n resolve: (response: AgentResponse) => void;\n reject: (error: Error) => void;\n }\n >();\n // Chunked answers being assembled, by request id. At most MAX_OPEN_CHUNK_SETS, each with a timer.\n const sets = new Map<number, ChunkSet>();\n const chunkTimeout = options.chunkTimeoutMs ?? CHUNK_TIMEOUT_MS;\n let buffered: WireEvent[] = [];\n let synced = false;\n let nextRequest = 1;\n let disposed = false;\n let notifyScheduled = false;\n // The id of the agent whose messages the panel has been following (see `AgentMessage.instance`).\n let instance: string | undefined;\n let failure: string | undefined;\n let resolveReady: () => void = () => undefined;\n const ready = new Promise<void>((resolve) => {\n resolveReady = resolve;\n });\n\n const notify = (): void => {\n if (notifyScheduled) return;\n notifyScheduled = true;\n queueMicrotask(() => {\n notifyScheduled = false;\n if (disposed) return;\n for (const listener of listeners) listener();\n });\n };\n\n const send = (message: PanelMessage): void => transport.send(message);\n\n const dropSet = (id: number): void => {\n const set = sets.get(id);\n if (set === undefined) return;\n clearTimeout(set.timer);\n sets.delete(id);\n };\n const dropAllSets = (): void => {\n for (const id of [...sets.keys()]) dropSet(id);\n };\n /** Answers request `id` with `response` and forgets its pieces. */\n const settle = (id: number, response: AgentResponse): void => {\n dropSet(id);\n const entry = waiting.get(id);\n if (entry === undefined) return;\n waiting.delete(id);\n entry.resolve(response);\n };\n const giveUp = (id: number, error: string): void => settle(id, { ok: false, error });\n const armTimer = (id: number): ReturnType<typeof setTimeout> =>\n setTimeout(() => giveUp(id, \"The page's answer did not arrive in full.\"), chunkTimeout);\n\n /** Takes one chunk of a chunked answer; joins the answer when the last piece is in. */\n const takeChunk = (\n raw: Extract<AgentMessage, { type: \"chunk\" }>,\n from: string | undefined,\n ): void => {\n let set = sets.get(raw.id);\n if (set === undefined) {\n if (raw.size > MAX_ASSEMBLED_CHARS) {\n giveUp(raw.id, \"The page's answer is larger than the panel accepts.\");\n return;\n }\n if (sets.size >= MAX_OPEN_CHUNK_SETS) {\n const oldest = sets.keys().next();\n if (oldest.done !== true) {\n giveUp(\n oldest.value,\n \"The page's answer was given up on: too many answers were arriving at once.\",\n );\n }\n }\n set = {\n instance: from,\n parts: raw.parts,\n size: raw.size,\n pieces: new Array<string | undefined>(raw.parts).fill(undefined),\n received: 0,\n chars: 0,\n timer: armTimer(raw.id),\n };\n sets.set(raw.id, set);\n } else if (set.instance !== from || set.parts !== raw.parts || set.size !== raw.size) {\n // Pieces of another agent, or of another cut of the answer, do not belong to this set.\n return;\n }\n if (set.pieces[raw.part] !== undefined) return;\n set.pieces[raw.part] = raw.data;\n set.received += 1;\n set.chars += raw.data.length;\n if (set.chars > set.size) {\n giveUp(raw.id, \"The pieces of the page's answer did not match its declared size.\");\n return;\n }\n clearTimeout(set.timer);\n set.timer = armTimer(raw.id);\n if (set.received < set.parts) return;\n const text = set.pieces.join(\"\");\n if (text.length !== set.size) {\n giveUp(raw.id, \"The pieces of the page's answer did not match its declared size.\");\n return;\n }\n settle(raw.id, parseAnswer(text));\n };\n\n const applyState = (state: ModelState): void => {\n model.importState(state);\n failure = undefined;\n synced = true;\n const pending = buffered;\n buffered = [];\n for (const event of pending) model.apply(event);\n resolveReady();\n notify();\n };\n\n const request = (req: AgentRequest): Promise<AgentResponse> => {\n if (disposed) return Promise.reject(new Error(\"The devtools backend was disposed.\"));\n const id = nextRequest;\n nextRequest += 1;\n return new Promise<AgentResponse>((resolve, reject) => {\n waiting.set(id, { resolve, reject });\n send({\n channel: CHANNEL,\n v: PROTOCOL_VERSION,\n from: \"panel\",\n type: \"request\",\n id,\n request: req,\n });\n });\n };\n\n const stop = transport.listen((raw) => {\n if (!isAgentMessage(raw)) return;\n if (raw.v !== PROTOCOL_VERSION) return;\n const id = instanceOf(raw);\n if (raw.type === \"chunk\") {\n // A piece is only for a request this panel made, and only from the agent it follows. Anything else is\n // dropped before it can start a set or a timer. A piece of the agent that was replaced by a reload\n // must not make the panel switch back to it, which a whole message from a new agent does by design:\n // a new agent introduces itself with `hello`, events or an answer first.\n if (!waiting.has(raw.id)) return;\n if (instance !== undefined && id !== instance) return;\n }\n if (id !== undefined) {\n if (instance !== undefined && id !== instance) {\n // Another agent answers on the same channel: the page was reloaded. Its sequence numbers need not\n // start below the old ones (it may have recorded a lot before the panel noticed), so the id, not\n // the counter, tells the old state is stale.\n instance = id;\n synced = false;\n buffered = [];\n // Pieces of the old agent's answers can no longer complete anything.\n dropAllSets();\n sync();\n } else {\n instance = id;\n }\n }\n if (raw.type === \"events\") {\n // The page is not trusted by the tools that inspect it: an element that is not an event is skipped\n // here, so the model never reads a missing field and a `NaN` never reaches its sequence counter.\n const valid = raw.events.filter(isWireEvent);\n const first = valid[0];\n if (synced && first !== undefined && first.seq <= model.lastSeq) {\n // The agent's counter only grows, so a batch that starts at or below the last applied number is\n // from another agent: the inspected page was reloaded and its new agent counts from 1 again.\n // Ignoring it would keep the old page on screen, so the panel asks for the new state.\n synced = false;\n buffered = [];\n sync();\n }\n if (!synced) {\n // Bounded: a page whose state never arrives (a refused or oversized answer) must not fill the panel.\n for (const event of valid) buffered.push(event);\n if (buffered.length > MAX_BUFFERED_EVENTS) {\n buffered.splice(0, buffered.length - MAX_BUFFERED_EVENTS);\n }\n return;\n }\n for (const event of valid) model.apply(event);\n notify();\n return;\n }\n if (raw.type === \"chunk\") {\n takeChunk(raw, id);\n return;\n }\n if (raw.type === \"response\") {\n // A whole answer (or the bridge's refusal) ends any pieces collected for the same request.\n settle(raw.id, raw.response);\n }\n });\n\n const sync = (): void => {\n void request({ type: \"snapshot\" }).then(\n (response) => {\n if (response.ok && isModelState(response.result)) {\n applyState(response.result);\n return;\n }\n // Without this the panel would say \"connecting\" for good: tell it why there is no state.\n failure = response.ok\n ? \"The page sent a state the panel cannot read.\"\n : String(response.error).slice(0, MAX_ERROR_CHARS);\n notify();\n },\n () => {\n // The only rejection is dispose(): there is nothing left to synchronize.\n synced = false;\n },\n );\n };\n\n send({ channel: CHANNEL, v: PROTOCOL_VERSION, from: \"panel\", type: \"hello\" });\n sync();\n\n return {\n model,\n ready,\n error: () => failure,\n subscribe(listener): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n request(req): Promise<AgentResponse> {\n if (req.type === \"clear\") {\n // A clear empties the agent; the panel's copy must follow.\n return request(req).then((response) => {\n if (response.ok) {\n synced = false;\n sync();\n }\n return response;\n });\n }\n return request(req);\n },\n dispose(): void {\n disposed = true;\n stop();\n dropAllSets();\n listeners.clear();\n for (const entry of waiting.values())\n entry.reject(new Error(\"The devtools backend was disposed.\"));\n waiting.clear();\n transport.close();\n },\n };\n}\n"],"mappings":"2IAiBA,MAAaA,EAAU,oBA0GVC,EAAc,IAUdC,EAAwB,KASxBC,EAAa,KAUbC,EAAsB,MAmBtBC,EAAmB,IAEhC,SAASC,EAAS,EAAkD,CAClE,OAAO,OAAOC,GAAU,YAAYA,CACtC,CAEA,SAASC,EAAW,EAAgB,EAA2D,CAC7F,OACEF,EAASC,CAAK,GACdA,EAAME,UAAA,qBACNF,EAAMG,OAAYC,GAClB,OAAOJ,EAAMK,GAAS,UACtB,OAAOL,EAAMM,MAAY,QAE7B,CAgBA,SAAgBC,EAAe,EAAuC,CACpE,GAAI,CAACN,EAAWD,EAAO,OAAO,EAAG,MAAO,GACxC,OAAQA,EAAMM,KAAd,CACE,IAAK,QACH,MAAO,GACT,IAAK,SACH,OAAO,MAAM,QAAQN,EAAMQ,MAAS,EACtC,IAAK,WACH,OAAO,OAAOR,EAAMS,IAAU,UAAYV,EAASC,EAAMU,QAAW,EACtE,IAAK,QAAS,CACZ,GAAM,CAAE,KAAI,OAAM,QAAO,OAAM,QAASV,EACxC,OACE,OAAOW,GAAO,UACd,OAAOC,GAAS,UAChB,OAAOC,GAAU,UACjB,OAAOC,GAAS,UAChB,OAAOC,GAAS,UAChB,OAAO,UAAUH,CAAI,GACrB,OAAO,UAAUC,CAAK,GACtB,OAAO,UAAUC,CAAI,GACrBD,GAAS,GACTA,GAAA,MACAD,GAAQ,GACRA,EAAOC,GACPC,GAAQ,GACRC,EAAK,QAAA,GAET,CACA,QACE,MAAO,EACX,CACF,CAcA,SAAgBC,EAAe,EAAuC,CACpE,GAAI,CAACf,EAAWD,EAAO,OAAO,EAAG,MAAO,GACxC,OAAQA,EAAMM,KAAd,CACE,IAAK,QACH,MAAO,GACT,IAAK,UACH,OACE,OAAON,EAAMS,IAAU,UACvBV,EAASC,EAAMiB,OAAU,GACzB,OAAOjB,EAAMiB,QAAWX,MAAY,SAExC,QACE,MAAO,EACX,CACF,CAiBA,SAAgBY,GAAoD,CAClE,IAAMC,EAA+E,iBACnF,IAAI,oBACJ,IAAI,GACN,EACIC,EAAS,GACPC,EAAQ,IAA6B,CACzC,KAAK,EAAe,CAClB,GAAID,EAAQ,OACZ,IAAME,EAAO,gBAAgBC,CAAO,EACpC,IAAK,IAAMC,IAAW,CAAC,GAAGL,EAAS,MAAU,GAAU,EAAGK,EAAQF,CAAI,CACxE,EACA,OAAO,EAAqB,CAE1B,OADAH,EAASM,EAAM,CAAC,IAAID,CAAO,MACd,CACXL,EAASM,EAAM,CAAC,OAAOD,CAAO,CAChC,CACF,EACA,OAAc,CACZ,EAAS,GACTL,EAAS,EAAE,CAAC,MAAM,EAClBA,EAAS,EAAE,CAAC,MAAM,CACpB,CACF,GACA,MAAO,CAACE,EAAK,CAAC,EAAGA,EAAK,CAAC,CAAC,CAC1B,CA8CA,SAAgBK,EAAoB,EAAkC,CACpE,MAAO,CACL,KAAK,EAAe,CAClBC,EAAK,YAAYJ,CAAO,CAC1B,EACA,OAAO,EAAqB,CAC1B,IAAMK,EAAY,GAAmCJ,EAAQK,EAAM,IAAI,EAGvE,OAFAF,EAAK,iBAAiB,UAAWC,CAAQ,EACzCD,EAAK,QAAQ,MACAA,EAAK,oBAAoB,UAAWC,CAAQ,CAC3D,EACA,OAAc,CACZD,EAAK,QAAQ,CACf,CACF,CACF,CA0EA,SAAgBG,EAAsB,EAA4C,CAChF,GAAIC,EAAQ,SAAW,IACrB,MAAU,UAAU,uDAAuD,EAC7E,IAAMC,EAAyBD,EAAQ,UAAa,WACpD,MAAO,CACL,KAAK,EAAe,CAClBA,EAAQ,OAAO,YAAYR,EAASQ,EAAQ,MAAM,CACpD,EACA,OAAO,EAAqB,CAC1B,IAAMH,EAAY,GAA8B,CAC1CC,EAAM,SAAWE,EAAQ,QACxBF,EAAM,SAAuBE,EAAQ,QAC1CP,EAAQK,EAAM,IAAI,CACpB,EAEA,OADAG,EAAS,iBAAiB,UAAWJ,CAAQ,MAChCI,EAAS,oBAAoB,UAAWJ,CAAQ,CAC/D,EACA,OAAc,CAEd,CACF,CACF,CAuCA,SAAgBK,EACd,EACA,EAAmD,GAAM,IAAI,iBAAiBC,CAAC,EACpE,CACX,IAAMC,EAAUC,EAAQC,CAAI,EAC5B,MAAO,CACL,KAAK,EAAe,CAClBF,EAAQ,YAAYZ,CAAO,CAC7B,EACA,OAAO,EAAqB,CAC1B,IAAMK,EAAY,GAAmCJ,EAAQK,EAAM,IAAI,EAEvE,OADAM,EAAQ,iBAAiB,UAAWP,CAAQ,MAC/BO,EAAQ,oBAAoB,UAAWP,CAAQ,CAC9D,EACA,OAAc,CACZO,EAAQ,MAAM,CAChB,CACF,CACF,CAuCA,SAASG,GAAwB,CAC/B,IAAMC,EAAiC,WAAmC,OAC1E,GAAI,OAAOA,GAAW,YAAe,WAAY,OAAOA,EAAU,WAAW,EAC7E,GAAI,OAAOA,GAAW,iBAAoB,WAAY,CACpD,IAAMC,EAAQD,EAAU,gCAAgB,IAAI,WAAW,EAAE,CAAC,EAC1D,OAAO,MAAM,KAAKC,EAAQ,GAASC,EAAK,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CAChF,CACA,MAAO,GAAG,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,GAChH,CAMA,SAASC,EAAW,EAA2C,CAC7D,IAAM/B,EAAcY,EAAQ,SAC5B,OAAO,OAAOZ,GAAO,UAAYA,EAAG,OAAS,GAAKA,EAAG,QAAU,IAC3DA,EACA,IAAA,EACN,CAUA,SAASgC,EACP,EACA,EACA,EACwD,CACxD,IAAIC,EACJ,GAAI,CACF,EAAO,KAAK,UAAUC,CAAQ,CAChC,OAASC,EAAO,CAEd,MAAO,8CAA8CC,EAAUD,CAAK,IAAI,MACtE,EACA,GACF,CACF,CACA,GAAIF,EAAK,QAAUI,EAAa,OAChC,GAAIJ,EAAK,OAAA,OAAgC,KAAK,KAAKA,EAAK,OAASK,CAAU,EAAA,KACzE,MAAO,sDAET,IAAMC,EAAkB,CAAC,EACzB,IAAK,IAAIC,EAAQ,EAAGA,EAAQP,EAAK,QAAU,CACzC,IAAIQ,EAAM,KAAK,IAAIR,EAAK,OAAQO,EAAQF,CAAU,EAC5CI,EAAOT,EAAK,WAAWQ,EAAM,CAAC,EAEhCA,EAAMR,EAAK,QAAUS,GAAQ,OAAUA,GAAQ,OAAUD,EAAMD,EAAQ,GAAG,IAC9ED,EAAM,KAAKN,EAAK,MAAMO,EAAOC,CAAG,CAAC,EACjC,EAAQA,CACV,CACA,MAAO,CAAE,QAAO,KAAMR,EAAK,MAAO,CACpC,CAEA,SAASG,EAAU,EAAwB,CACzC,OAAOD,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAC9D,CAmBA,SAAgBQ,EACd,EACA,EACA,EAAwB,CAAC,EACb,CACZ,IAAMC,EAAQxB,EAAQ,OAAS,IACzByB,EAAWzB,EAAQ,UAAYO,EAAc,EAC7CW,EAAa,KAAK,IACtB,EACA,KAAK,IAAI,KAAK,MAAMlB,EAAQ,YAAA,GAAyB,EAAGrC,CAAW,CACrE,EACMsD,EAAcjB,EAAQ,qBAAA,KACtB0B,EAAQ,GAAgCC,EAAU,KAAK,CAAE,GAAGnC,EAAS,UAAS,CAAC,EAC/EoC,GAAU,EAAY,IAAkC,CAC5D,IAAMC,EAASjB,EAAYE,EAAUI,EAAYD,CAAW,EAC5D,GAAIY,IAAW,IAAA,GACbH,EAAK,CACH,QAAShE,EACT,EAAA,EACA,KAAM,QACN,KAAM,WACN,KACA,UACF,CAAC,OAGH,GAAI,OAAOmE,GAAW,SAEpBH,EAAK,CACH,QAAShE,EACT,EAAA,EACA,KAAM,QACN,KAAM,WACN,KACA,SAAU,CAAE,GAAI,GAAO,MAAOmE,CAAO,CACvC,CAAC,OAGH,IAAK,IAAIhD,EAAO,EAAGA,EAAOgD,EAAO,MAAM,OAAQ,GAAQ,EACrDH,EAAK,CACH,QAAShE,EACT,EAAA,EACA,KAAM,QACN,KAAM,QACN,KACA,OACA,MAAOmE,EAAO,MAAM,OACpB,KAAMA,EAAO,KACb,KAAMA,EAAO,MAAMhD,IAAS,EAC9B,CAAC,CAEL,EACMiD,EAAgBH,EAAU,OAAQ,GAAQ,CAC9C,GAAI,CAAC1C,EAAe8C,CAAG,EAAG,OAC1B,GAAIA,EAAI,OAAS,QAAS,CACxBL,EAAK,CAAE,QAAShE,EAAS,EAAA,EAAqB,KAAM,QAAS,KAAM,OAAQ,CAAC,EAC5E,MACF,CACA,GAAM,CAAE,WAAYqE,EAChBjB,EACAkB,EAAQ,OAAS,aACnBhC,EAAQ,YAAYgC,EAAQ,EAAE,EAC9B,EAAW,CAAE,GAAI,GAAM,OAAQ,IAAK,GAEpC,EAAWC,EAAM,OAAOD,CAAO,EAEjCJ,EAAOG,EAAI,GAAIjB,CAAQ,CACzB,CAAC,EACKoB,EAAgBD,EAAM,UAAW,GAAW,CAChD,IAAK,IAAIE,EAAI,EAAGA,EAAIC,EAAO,OAAQ,GAAKZ,EACtCE,EAAK,CACH,QAAShE,EACT,EAAA,EACA,KAAM,QACN,KAAM,SACN,OAAQ0E,EAAO,MAAMD,EAAGA,EAAIX,CAAK,CACnC,CAAC,CAEL,CAAC,EACD,UAAa,CACXM,EAAc,EACdI,EAAc,CAChB,CACF,CAgBA,SAASG,EAAY,EAA6B,CAChD,IAAIpE,EACJ,GAAI,CACF,EAAQ,KAAK,MAAM4C,CAAI,CACzB,OAASE,EAAO,CACd,MAAO,CACL,GAAI,GACJ,MAAO,wCAAwCC,EAAUD,CAAK,IAAI,MAAM,EAAG,GAAe,CAC5F,CACF,CACA,GAAI/C,EAASC,CAAK,GAAK,CAAC,MAAM,QAAQA,CAAK,EAAG,CAC5C,GAAIA,EAAMqE,KAAU,GAAM,MAAO,CAAE,GAAI,GAAM,OAAQrE,EAAMsE,MAAU,EACrE,GAAItE,EAAMqE,KAAU,IAAS,OAAOrE,EAAMuE,OAAa,SACrD,MAAO,CAAE,GAAI,GAAO,MAAOvE,EAAMuE,MAAS,MAAM,EAAG,GAAe,CAAE,CAExE,CACA,MAAO,CAAE,GAAI,GAAO,MAAO,sCAAuC,CACpE,CAMA,MAAMC,EAAsB,IAyC5B,SAAgBC,EAAc,EAAsB,EAAyB,CAAC,EAAY,CACxF,IAAMC,EAAQ,IAAIC,EAAe5C,EAAQ,MAAM,EACzC6C,kBAAY,IAAI,IAChBC,kBAAU,IAAI,IAQdC,kBAAO,IAAI,IACXC,EAAehD,EAAQ,gBAAA,IACzBiD,EAAwB,CAAC,EACzBC,EAAS,GACTC,EAAc,EACdC,EAAW,GACXC,EAAkB,GAElB5B,EACA6B,EACAC,MAAiC,IAAA,GAC/BC,EAAQ,IAAI,QAAe,GAAY,CAC3C,EAAeC,CACjB,CAAC,EAEKC,MAAqB,CACrBL,IACJ,EAAkB,GAClB,mBAAqB,CAEnB,GADA,EAAkB,GACd,GACJ,IAAK,IAAMxD,KAAYgD,EAAWhD,EAAS,CAC7C,CAAC,EACH,EAEM6B,EAAQ,GAAgCC,EAAU,KAAKnC,CAAO,EAE9DmE,EAAW,GAAqB,CACpC,IAAMC,EAAMb,EAAK,IAAInE,CAAE,EACnBgF,IAAQ,IAAA,KACZ,aAAaA,EAAI,KAAK,EACtBb,EAAK,OAAOnE,CAAE,EAChB,EACMiF,MAA0B,CAC9B,IAAK,IAAMjF,IAAM,CAAC,GAAGmE,EAAK,KAAK,CAAC,EAAGY,EAAQ/E,CAAE,CAC/C,EAEMkF,GAAU,EAAY,IAAkC,CAC5DH,EAAQ/E,CAAE,EACV,IAAMmF,EAAQjB,EAAQ,IAAIlE,CAAE,EACxBmF,IAAU,IAAA,KACdjB,EAAQ,OAAOlE,CAAE,EACjBmF,EAAM,QAAQjD,CAAQ,EACxB,EACMkD,GAAU,EAAY,IAAwBF,EAAOlF,EAAI,CAAE,GAAI,GAAO,OAAM,CAAC,EAC7EqF,EAAY,GAChB,eAAiBD,EAAOpF,EAAI,2CAA2C,EAAGoE,CAAY,EAGlFkB,GACJ,EACA,IACS,CACT,IAAIN,EAAMb,EAAK,IAAIhB,EAAI,EAAE,EACzB,GAAI6B,IAAQ,IAAA,GAAW,CACrB,GAAI7B,EAAI,KAAA,MAA4B,CAClCiC,EAAOjC,EAAI,GAAI,qDAAqD,EACpE,MACF,CACA,GAAIgB,EAAK,MAAA,EAA6B,CACpC,IAAMoB,EAASpB,EAAK,KAAK,CAAC,CAAC,KAAK,EAC5BoB,EAAO,OAAS,IAClBH,EACEG,EAAO,MACP,4EACF,CAEJ,CACA,EAAM,CACJ,SAAU9F,EACV,MAAO0D,EAAI,MACX,KAAMA,EAAI,KACV,OAAY,MAA0BA,EAAI,KAAK,CAAC,CAAC,KAAK,IAAA,EAAS,EAC/D,SAAU,EACV,MAAO,EACP,MAAOkC,EAASlC,EAAI,EAAE,CACxB,EACAgB,EAAK,IAAIhB,EAAI,GAAI6B,CAAG,CACtB,MAAO,GAAIA,EAAI,WAAavF,GAAQuF,EAAI,QAAU7B,EAAI,OAAS6B,EAAI,OAAS7B,EAAI,KAE9E,OAEF,GAAI6B,EAAI,OAAO7B,EAAI,QAAU,IAAA,GAAW,OAIxC,GAHA,EAAI,OAAOA,EAAI,MAAQA,EAAI,KAC3B,EAAI,UAAY,EAChB,EAAI,OAASA,EAAI,KAAK,OAClB6B,EAAI,MAAQA,EAAI,KAAM,CACxBI,EAAOjC,EAAI,GAAI,kEAAkE,EACjF,MACF,CAGA,GAFA,aAAa6B,EAAI,KAAK,EACtB,EAAI,MAAQK,EAASlC,EAAI,EAAE,EACvB6B,EAAI,SAAWA,EAAI,MAAO,OAC9B,IAAM/C,EAAO+C,EAAI,OAAO,KAAK,EAAE,EAC3B/C,EAAK,SAAW+C,EAAI,KAIxBE,EAAO/B,EAAI,GAAIM,EAAYxB,CAAI,CAAC,EAH9BmD,EAAOjC,EAAI,GAAI,kEAAkE,CAIrF,EAEMqC,EAAc,GAA4B,CAC9CzB,EAAM,YAAY0B,CAAK,EACvB,EAAU,IAAA,GACV,EAAS,GACT,IAAMC,EAAUrB,EAChB,EAAW,CAAC,EACZ,IAAK,IAAMnD,KAASwE,EAAS3B,EAAM,MAAM7C,CAAK,EAC9CyD,EAAa,EACbG,EAAO,CACT,EAEM1B,EAAW,GAA8C,CAC7D,GAAIoB,EAAU,OAAO,QAAQ,OAAO,gBAAI,MAAM,oCAAoC,CAAC,EACnF,IAAMxE,EAAKuE,EAEX,MADA,IAAe,EACR,IAAI,SAAwB,EAAS,IAAW,CACrDL,EAAQ,IAAIlE,EAAI,CAAE,UAAS,QAAO,CAAC,EACnC8C,EAAK,CACH,QAAShE,EACT,EAAA,EACA,KAAM,QACN,KAAM,UACN,KACA,QAAS6G,CACX,CAAC,CACH,CAAC,CACH,EAEMC,EAAO7C,EAAU,OAAQ,GAAQ,CAErC,GADI,CAACnD,EAAeuD,CAAG,GACnBA,EAAI,IAAA,EAAwB,OAChC,IAAMnD,EAAK+B,EAAWoB,CAAG,EACzB,GAAIA,EAAI,OAAS,SAKVe,EAAQ,IAAIf,EAAI,EAAE,IACnBN,IAAa,IAAA,IAAa7C,IAAO6C,GAiBvC,IAfI7C,IAAO,IAAA,KACL6C,IAAa,IAAA,IAAa7C,IAAO6C,GAInC,EAAW7C,EACX,EAAS,GACT,EAAW,CAAC,EAEZiF,EAAY,EACZY,EAAK,GAEL,EAAW7F,GAGXmD,EAAI,OAAS,SAAU,CAGzB,IAAM2C,EAAQ3C,EAAI,OAAO,OAAO4C,CAAW,EACrCC,EAAQF,EAAM,GASpB,GARIxB,GAAU0B,IAAU,IAAA,IAAaA,EAAM,KAAOjC,EAAM,UAItD,EAAS,GACT,EAAW,CAAC,EACZ8B,EAAK,GAEH,CAACvB,EAAQ,CAEX,IAAK,IAAMpD,KAAS4E,EAAOzB,EAAS,KAAKnD,CAAK,EAC1CmD,EAAS,OAASR,GACpBQ,EAAS,OAAO,EAAGA,EAAS,OAASR,CAAmB,EAE1D,MACF,CACA,IAAK,IAAM3C,KAAS4E,EAAO/B,EAAM,MAAM7C,CAAK,EAC5C4D,EAAO,CAET,MACI3B,EAAI,OAAS,QACfmC,EAAUnC,EAAKnD,CAAE,EAGfmD,EAAI,OAAS,YAEf+B,EAAO/B,EAAI,GAAIA,EAAI,QAAQ,CAP7B,CASF,CAAC,EAEK0C,MAAmB,CACvBzC,EAAa,CAAE,KAAM,UAAW,CAAC,CAAC,CAAC,KAChC,GAAa,CACRlB,EAAS,IAAM+D,EAAa/D,EAAS,MAAM,EAC7CsD,EAAWtD,EAAS,MAAM,GAI5B,EAAUA,EAAS,GACf,+CACA,OAAOA,EAAS,KAAK,CAAC,CAAC,MAAM,EAAG,GAAe,EACnD4C,EAAO,EACT,MACM,CAEJ,EAAS,EACX,CACF,CACF,EAKA,OAHAhC,EAAK,CAAE,QAAShE,EAAS,EAAA,EAAqB,KAAM,QAAS,KAAM,OAAQ,CAAC,EAC5E+G,EAAK,EAEE,CACL,QACA,QACA,UAAanB,EACb,UAAU,EAAsB,CAE9B,OADAT,EAAU,IAAIhD,CAAQ,MACT,CACXgD,EAAU,OAAOhD,CAAQ,CAC3B,CACF,EACA,QAAQ,EAA6B,CAWnC,OAVI0E,EAAI,OAAS,QAERvC,EAAQuC,CAAG,CAAC,CAAC,KAAM,IACpBzD,EAAS,KACX,EAAS,GACT2D,EAAK,GAEA3D,EACR,EAEIkB,EAAQuC,CAAG,CACpB,EACA,SAAgB,CACd,EAAW,GACXC,EAAK,EACLX,EAAY,EACZhB,EAAU,MAAM,EAChB,IAAK,IAAMkB,KAASjB,EAAQ,OAAO,EACjCiB,EAAM,OAAO,gBAAI,MAAM,oCAAoC,CAAC,EAC9DjB,EAAQ,MAAM,EACdnB,EAAU,MAAM,CAClB,CACF,CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadow-js/devtools",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A development panel that shows the components, signals, effects and updates of a running app",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"check": "npm run lint && npm run typecheck && npm run build && npm test && npm run test:perf && npm run test:gc"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@shadow-js/core": "^1.0.
|
|
43
|
-
"@shadow-js/web": "^1.0.
|
|
42
|
+
"@shadow-js/core": "^1.0.2",
|
|
43
|
+
"@shadow-js/web": "^1.0.2"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@biomejs/biome": "2.5.15",
|
|
47
|
-
"@shadow-js/compiler": "^1.0.
|
|
48
|
-
"@shadow-js/core": "^1.0.
|
|
49
|
-
"@shadow-js/web": "^1.0.
|
|
47
|
+
"@shadow-js/compiler": "^1.0.2",
|
|
48
|
+
"@shadow-js/core": "^1.0.2",
|
|
49
|
+
"@shadow-js/web": "^1.0.2",
|
|
50
50
|
"@types/node": "^24.0.0",
|
|
51
51
|
"happy-dom": "^21.0.3",
|
|
52
52
|
"rolldown": "1.2.13",
|