@expressive/inspect 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  ---
12
12
 
13
- Query the live model graph of an [Expressive MVC](https://github.com/gabeklein/expressive-mvc) app from a test, a Playwright spec, or the browser console. No UI, no network - a base layer for agents, devtools, and test helpers.
13
+ Query the live model graph of an [Expressive MVC](https://github.com/gabeklein/expressive-mvc) app from a test, a browser driver (Playwright, puppeteer, CDP), or the browser console. No UI, and no network beyond an opt-in dev-server relay - a base layer for agents, devtools, and test helpers.
14
14
 
15
15
  ```bash
16
16
  npm install --save-dev @expressive/inspect
@@ -40,22 +40,41 @@ const frames = await composer.act((s) => s.submit('hi')); // run, settle, see
40
40
  ## Across a boundary
41
41
 
42
42
  ```ts
43
- import { inspect } from '@expressive/inspect/playwright';
43
+ import { inspect } from '@expressive/inspect/bridge';
44
44
 
45
- const api = inspect(page); // Page, Frame, or Locator
45
+ const api = inspect(page); // anything with evaluate(fn, arg)
46
46
  await api.get('Composer.draft');
47
- const frames = await api.around(() => page.click('#submit'));
47
+ const frames = await api.act(() => page.click('#submit'));
48
48
  ```
49
49
 
50
+ A Node process under `--inspect`, or a browser with a debug port: `inspect(await devtools())`, `devtools` from the same entry.
51
+
52
+ ## From a running dev server
53
+
54
+ ```ts
55
+ // vite.config.ts - installs the inspector and relays to open pages
56
+ import inspect from '@expressive/inspect/vite';
57
+
58
+ export default defineConfig({ plugins: [inspect()] });
59
+ ```
60
+
61
+ ```bash
62
+ curl localhost:5173/__inspect # connected pages, iframes included
63
+ curl localhost:5173/__inspect/<id> -d '["get", "Composer.draft"]'
64
+ curl localhost:5173/__inspect/<id> -d '["act", ["call", "Composer.submit", "hi"]]' # { value, frames, settled, pending }
65
+ ```
66
+
67
+ Local callers only; browser and proxied (tunnel) requests are refused.
68
+
50
69
  Console: `__EXPRESSIVE_INSPECT__.get('Composer.draft')`, `__EXPRESSIVE_INSPECT__.journal.record({ level: 'keys', paths: ['Composer.draft'] })`.
51
70
 
52
71
  ## Journal
53
72
 
54
- Off by default. `journal.record({ level, types, paths, keys, calls })`, then `frames({ since })`. A frame is one batch of writes plus its flush; work an effect defers carries a `cause` back to the frame that scheduled it. `export()` emits NDJSON for a sidecar.
73
+ Off by default (the Vite relay records `keys` from load). `journal.record({ level, types, paths, keys, calls })`, then `frames({ since })`. A frame is one batch of writes plus its flush; work an effect defers carries a `cause` back to the frame that scheduled it. `export()` emits NDJSON for a sidecar; `summary()` digests it per instance.
55
74
 
56
75
  ## Orphans
57
76
 
58
- Instances a host never committed - a render thrown away, a StrictMode twin - stay out of `models()` and `tree()`. `orphans()` lists them, `warnings()` counts them.
77
+ Instances a host never committed - a render thrown away, a StrictMode twin - stay out of `models()` and `tree()`. `orphans()` lists them, `health()` counts them - along with loaded mvc copies and caught reports.
59
78
 
60
79
  ## Minified builds
61
80
 
@@ -0,0 +1,98 @@
1
+ //#region src/settle.ts
2
+ const SETTLE_TIMEOUT = 1e3;
3
+ const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
4
+ /** Wait a macrotask at a time until one passes with no new frame. Resolves `false` if `timeout` passes first. */
5
+ async function settle(seq, wait = tick, timeout = SETTLE_TIMEOUT) {
6
+ const end = Date.now() + timeout;
7
+ for (let last = await seq();;) {
8
+ await wait();
9
+ const next = await seq();
10
+ if (next === last) return true;
11
+ if (Date.now() >= end) return false;
12
+ last = next;
13
+ }
14
+ }
15
+ function unsettled(timeout = SETTLE_TIMEOUT) {
16
+ console.warn(`Still active after ${timeout}ms - frames may be incomplete. Narrow the recording ({ record: { types, paths, keys } }) to exclude background work, or pass { timeout }.`);
17
+ }
18
+ /** `until` not met within `timeout` - carries the frames recorded so far and what never arrived. */
19
+ function unreached(timeout, pending, missing, frames, hint = "") {
20
+ const list = pending.map((address) => missing.includes(address) ? `${address} (names no instance)` : address);
21
+ return Object.assign(/* @__PURE__ */ new Error(`Not reached within ${timeout}ms: ${list.join(", ")}.${hint}`), {
22
+ frames,
23
+ pending
24
+ });
25
+ }
26
+
27
+ //#endregion
28
+ //#region src/bracket.ts
29
+ /** Resolve `address.key` to the instance `address` names - label, id, or owner path - so frames match it alone. */
30
+ /** Resolve `address.key` to the instance `address` names - label, id, or owner path - as the `id.key` its frames carry. */
31
+ async function locate(remote, target) {
32
+ const dot = target.lastIndexOf(".");
33
+ const found = await remote.get(target.slice(0, dot));
34
+ if (!found?.$ref) throw new Error(`until ${target}: ${target.slice(0, dot)} names no State.`);
35
+ return `${found.$ref}.${target.slice(dot + 1)}`;
36
+ }
37
+ /**
38
+ * Run `step` recording values, wait for `until`, settle, and return what it produced; the prior recording is restored.
39
+ * `local` steps run here, so activity for `until` counts once their synchronous writes flush, not once they resolve.
40
+ */
41
+ async function bracket(remote, step, wait, { until = [], timeout = SETTLE_TIMEOUT, record } = {}, local) {
42
+ const before = await remote.record();
43
+ const since = await remote.seq();
44
+ const window = {
45
+ ...before,
46
+ ...record
47
+ };
48
+ const filtered = window.types.length + window.paths.length + window.keys.length > 0;
49
+ const values = typeof until == "string" || Array.isArray(until) ? void 0 : until;
50
+ const addresses = values ? [] : [].concat(until);
51
+ const paths = filtered ? [...window.paths, ...addresses.filter((address) => address.indexOf(".") === address.lastIndexOf("."))] : [];
52
+ await remote.record({
53
+ ...record,
54
+ level: "values",
55
+ ...filtered && { paths }
56
+ });
57
+ try {
58
+ const running = step();
59
+ let from;
60
+ if (local) {
61
+ await void 0;
62
+ from = await remote.seq();
63
+ }
64
+ const value = await running;
65
+ from ??= await remote.seq();
66
+ const end = Date.now() + timeout;
67
+ const pending = new Set(values ? Object.keys(values) : addresses);
68
+ const targets = /* @__PURE__ */ new Map();
69
+ for (const address of addresses) targets.set(address, await locate(remote, address));
70
+ if (filtered && addresses.some((address) => address.indexOf(".") !== address.lastIndexOf("."))) await remote.record({ paths: [...paths, ...targets.values()] });
71
+ while (pending.size) {
72
+ if (values) {
73
+ for (const address of pending) if (JSON.stringify(await remote.get(address)) === JSON.stringify(values[address])) pending.delete(address);
74
+ } else for (const frame of await remote.frames({ since: from })) for (const event of frame.events) for (const address of pending) if (targets.get(address) === `${event.id}.${event.key}`) pending.delete(address);
75
+ if (!pending.size || Date.now() >= end) break;
76
+ await wait();
77
+ }
78
+ const settled = !pending.size && await settle(remote.seq, wait, Math.max(0, end - Date.now()));
79
+ const missing = [];
80
+ if (values) {
81
+ for (const address of pending) if (await remote.get(address.slice(0, address.lastIndexOf("."))) === void 0) missing.push(address);
82
+ }
83
+ return {
84
+ value,
85
+ frames: await remote.frames({ since }),
86
+ settled,
87
+ pending: [...pending],
88
+ missing,
89
+ timeout
90
+ };
91
+ } finally {
92
+ await remote.record(before);
93
+ }
94
+ }
95
+
96
+ //#endregion
97
+ export { unsettled as i, tick as n, unreached as r, bracket as t };
98
+ //# sourceMappingURL=bracket-CWa4p56P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bracket-CWa4p56P.js","names":[],"sources":["../src/settle.ts","../src/bracket.ts"],"sourcesContent":["import type { Frame, Options } from './journal';\n\nexport const SETTLE_TIMEOUT = 1000;\n\nexport interface Act {\n /**\n * What to wait for before settling: `Type.key` / `id.key` addresses that must each see a frame after the\n * step's synchronous writes, or `{ [address]: value }` pairs that must each hold that value.\n */\n until?: string | string[] | Record<string, unknown>;\n /** Longest to wait, in ms; default 1000. */\n timeout?: number;\n /** Recording filters for this window, instead of the journal's - `{ paths: [], types: [], keys: [] }` records everything. */\n record?: Omit<Options, 'level'>;\n}\n\nexport const tick = () => new Promise((resolve) => setTimeout(resolve, 0));\n\n/** Wait a macrotask at a time until one passes with no new frame. Resolves `false` if `timeout` passes first. */\nexport async function settle(seq: () => number | Promise<number>, wait: () => unknown = tick, timeout = SETTLE_TIMEOUT) {\n const end = Date.now() + timeout;\n\n for (let last = await seq(); ; ) {\n await wait();\n\n const next = await seq();\n\n if (next === last) return true;\n if (Date.now() >= end) return false;\n\n last = next;\n }\n}\n\nexport function unsettled(timeout = SETTLE_TIMEOUT) {\n console.warn(\n `Still active after ${timeout}ms - frames may be incomplete. Narrow the recording ({ record: { types, paths, keys } }) to exclude background work, or pass { timeout }.`\n );\n}\n\n/** `until` not met within `timeout` - carries the frames recorded so far and what never arrived. */\nexport function unreached(timeout: number, pending: string[], missing: string[], frames: Frame[], hint = '') {\n const list = pending.map((address) => (missing.includes(address) ? `${address} (names no instance)` : address));\n return Object.assign(new Error(`Not reached within ${timeout}ms: ${list.join(', ')}.${hint}`), { frames, pending });\n}\n","import type { Frame, Options, Query } from './journal';\nimport { SETTLE_TIMEOUT, settle, type Act } from './settle';\n\nexport interface Remote {\n record(options?: Options): Required<Options> | Promise<Required<Options>>;\n seq(): number | Promise<number>;\n frames(query: Query): Frame[] | Promise<Frame[]>;\n get(address: string): unknown;\n}\n\n/** Resolve `address.key` to the instance `address` names - label, id, or owner path - so frames match it alone. */\n/** Resolve `address.key` to the instance `address` names - label, id, or owner path - as the `id.key` its frames carry. */\nasync function locate(remote: Remote, target: string) {\n const dot = target.lastIndexOf('.');\n const found = (await remote.get(target.slice(0, dot))) as { $ref?: string } | undefined;\n\n if (!found?.$ref) throw new Error(`until ${target}: ${target.slice(0, dot)} names no State.`);\n\n return `${found.$ref}.${target.slice(dot + 1)}`;\n}\n\n/**\n * Run `step` recording values, wait for `until`, settle, and return what it produced; the prior recording is restored.\n * `local` steps run here, so activity for `until` counts once their synchronous writes flush, not once they resolve.\n */\nexport async function bracket(\n remote: Remote,\n step: () => unknown,\n wait: () => unknown,\n { until = [], timeout = SETTLE_TIMEOUT, record }: Act = {},\n local?: boolean\n) {\n const before = await remote.record();\n const since = await remote.seq();\n const window = { ...before, ...record };\n const filtered = window.types.length + window.paths.length + window.keys.length > 0;\n const values = typeof until == 'string' || Array.isArray(until) ? undefined : until;\n const addresses = values ? [] : ([] as string[]).concat(until as string | string[]);\n const paths = filtered ? [...window.paths, ...addresses.filter((address) => address.indexOf('.') === address.lastIndexOf('.'))] : [];\n\n await remote.record({ ...record, level: 'values', ...(filtered && { paths }) });\n\n try {\n const running = step();\n let from: number | undefined;\n\n if (local) {\n await undefined;\n from = await remote.seq();\n }\n\n const value = await running;\n\n from ??= await remote.seq();\n const end = Date.now() + timeout;\n const pending = new Set(values ? Object.keys(values) : addresses);\n const targets = new Map<string, string>();\n\n for (const address of addresses) targets.set(address, await locate(remote, address));\n\n if (filtered && addresses.some((address) => address.indexOf('.') !== address.lastIndexOf('.')))\n await remote.record({ paths: [...paths, ...targets.values()] });\n\n while (pending.size) {\n if (values) {\n for (const address of pending)\n if (JSON.stringify(await remote.get(address)) === JSON.stringify(values[address])) pending.delete(address);\n } else\n for (const frame of await remote.frames({ since: from }))\n for (const event of frame.events)\n for (const address of pending) if (targets.get(address) === `${event.id}.${event.key}`) pending.delete(address);\n\n if (!pending.size || Date.now() >= end) break;\n\n await wait();\n }\n\n const settled = !pending.size && (await settle(remote.seq, wait, Math.max(0, end - Date.now())));\n const missing: string[] = [];\n\n if (values)\n for (const address of pending)\n if ((await remote.get(address.slice(0, address.lastIndexOf('.')))) === undefined) missing.push(address);\n\n return { value, frames: await remote.frames({ since }), settled, pending: [...pending], missing, timeout };\n } finally {\n await remote.record(before);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import { A as Summary, E as Frame, O as Options, S as Select, k as Query, x as Act } from "./inspect-CEXzL-mC.js";
2
+ import inspect$1 from "./index.js";
3
+
4
+ //#region src/devtools.d.ts
5
+ /** An entry of a CDP endpoint's `/json/list`. */
6
+ interface Target {
7
+ type: string;
8
+ title: string;
9
+ url: string;
10
+ webSocketDebuggerUrl?: string;
11
+ }
12
+ /**
13
+ * Connect to a Chrome DevTools Protocol endpoint - a Node process under `--inspect`, or a browser with a
14
+ * debug port - as an `evaluate` target for `inspect()`. `pick` narrows `/json/list` by predicate, or by a
15
+ * string matched against title and URL; more than one match throws with the list. A `ws://` URL connects directly.
16
+ */
17
+ declare function devtools(endpoint?: string, pick?: string | ((target: Target) => boolean)): Promise<Evaluates & {
18
+ close(): void;
19
+ }>;
20
+ //#endregion
21
+ //#region src/bridge.d.ts
22
+ /**
23
+ * Anything that can run a function in the page: Playwright `Page`, `Frame`, or
24
+ * `Locator` (which passes the element first), puppeteer `Page` or `Frame`.
25
+ */
26
+ interface Evaluates {
27
+ evaluate(fn: (...args: any[]) => unknown, arg?: unknown): Promise<unknown>;
28
+ }
29
+ /**
30
+ * Drive the page's inspector from a test. Every method is one `evaluate`;
31
+ * `act` runs a step recording values, settles, and returns the frames it produced.
32
+ */
33
+ declare function inspect(target: Evaluates): {
34
+ get: (address?: string, select?: Select) => Promise<unknown>;
35
+ set: (address: string, value: unknown) => Promise<void>;
36
+ call: (address: string, ...args: unknown[]) => Promise<unknown>;
37
+ models: () => Promise<ReturnType<typeof inspect$1.models>>;
38
+ tree: () => Promise<ReturnType<typeof inspect$1.tree>>;
39
+ health: () => Promise<ReturnType<typeof inspect$1.health>>;
40
+ journal: {
41
+ record: (options?: Options) => Promise<Required<Options>>;
42
+ seq: () => Promise<number>;
43
+ frames: (query?: Query) => Promise<Frame[]>;
44
+ history: (query: Query) => Promise<ReturnType<typeof inspect$1.journal.history>>;
45
+ export: (query?: Query) => Promise<string>;
46
+ summary: (query?: Query) => Promise<Summary[]>;
47
+ clear: () => Promise<void>;
48
+ };
49
+ act(step: () => unknown, options?: Act): Promise<Frame[]>;
50
+ };
51
+ //#endregion
52
+ export { Evaluates, type Target, devtools, inspect };
53
+ //# sourceMappingURL=bridge.d.ts.map
package/dist/bridge.js ADDED
@@ -0,0 +1,106 @@
1
+ import { i as unsettled, n as tick, r as unreached, t as bracket } from "./bracket-CWa4p56P.js";
2
+ import { t as dispatch } from "./dispatch-Dnd88-J9.js";
3
+
4
+ //#region src/devtools.ts
5
+ const describe = (targets) => targets.map((target) => `\n ${target.type} ${target.title} ${target.url}`).join("");
6
+ /**
7
+ * Connect to a Chrome DevTools Protocol endpoint - a Node process under `--inspect`, or a browser with a
8
+ * debug port - as an `evaluate` target for `inspect()`. `pick` narrows `/json/list` by predicate, or by a
9
+ * string matched against title and URL; more than one match throws with the list. A `ws://` URL connects directly.
10
+ */
11
+ async function devtools(endpoint = "http://127.0.0.1:9229", pick) {
12
+ let url = endpoint;
13
+ if (!endpoint.startsWith("ws")) {
14
+ const targets = (await (await fetch(`${endpoint}/json/list`)).json()).filter((target) => target.webSocketDebuggerUrl);
15
+ const match = typeof pick == "string" ? (target) => target.title.includes(pick) || target.url.includes(pick) : pick;
16
+ const found = match ? targets.filter(match) : targets;
17
+ if (found.length !== 1) throw new Error(found.length ? `Several debug targets at ${endpoint} - narrow with pick, e.g. (t) => t.type === 'page' && t.url.includes('…'):${describe(found)}` : `No debug target at ${endpoint}${typeof pick == "string" ? ` matches "${pick}"` : pick ? " matches pick" : ""}.${describe(targets)}`);
18
+ url = found[0].webSocketDebuggerUrl;
19
+ }
20
+ const socket = new WebSocket(url);
21
+ const waiting = /* @__PURE__ */ new Map();
22
+ let id = 0;
23
+ await new Promise((resolve, reject) => {
24
+ socket.onopen = resolve;
25
+ socket.onerror = () => reject(/* @__PURE__ */ new Error(`Could not connect to ${url}.`));
26
+ });
27
+ socket.onmessage = ({ data }) => {
28
+ const reply = JSON.parse(String(data));
29
+ waiting.get(reply.id)?.(reply);
30
+ waiting.delete(reply.id);
31
+ };
32
+ socket.onclose = () => {
33
+ for (const settle of waiting.values()) settle({ error: { message: "Debug connection closed." } });
34
+ waiting.clear();
35
+ };
36
+ return {
37
+ async evaluate(fn, arg) {
38
+ const { error, result } = await new Promise((resolve) => {
39
+ waiting.set(++id, resolve);
40
+ socket.send(JSON.stringify({
41
+ id,
42
+ method: "Runtime.evaluate",
43
+ params: {
44
+ expression: `(${fn})(${JSON.stringify(arg)})`,
45
+ awaitPromise: true,
46
+ returnByValue: true
47
+ }
48
+ }));
49
+ });
50
+ if (error) throw new Error(error.message);
51
+ const { exceptionDetails } = result;
52
+ if (exceptionDetails) throw new Error(exceptionDetails.exception?.description ?? exceptionDetails.text);
53
+ return result.result.value;
54
+ },
55
+ close() {
56
+ socket.close();
57
+ }
58
+ };
59
+ }
60
+
61
+ //#endregion
62
+ //#region src/bridge.ts
63
+ const BRIDGE_HINT = " On the bridge, activity counts once the step resolves, so a step that awaits the change never counts for an address - use { [address]: value }.";
64
+ /**
65
+ * Drive the page's inspector from a test. Every method is one `evaluate`;
66
+ * `act` runs a step recording values, settles, and returns the frames it produced.
67
+ */
68
+ function inspect(target) {
69
+ const remote = (...path) => (...args) => target.evaluate(dispatch, [path, args]);
70
+ const record = remote("journal", "record");
71
+ const seq = remote("journal", "seq");
72
+ const frames = remote("journal", "frames");
73
+ return {
74
+ get: remote("get"),
75
+ set: remote("set"),
76
+ call: remote("call"),
77
+ models: remote("models"),
78
+ tree: remote("tree"),
79
+ health: remote("health"),
80
+ journal: {
81
+ record,
82
+ seq,
83
+ frames,
84
+ history: remote("journal", "history"),
85
+ export: remote("journal", "export"),
86
+ summary: remote("journal", "summary"),
87
+ clear: remote("journal", "clear")
88
+ },
89
+ async act(step, options = {}) {
90
+ const result = await bracket({
91
+ record,
92
+ seq,
93
+ frames,
94
+ get: remote("get")
95
+ }, step, () => target.evaluate(tick), options);
96
+ const addressed = typeof options.until == "string" || Array.isArray(options.until);
97
+ if (result.pending.length) throw unreached(result.timeout, result.pending, result.missing, result.frames, addressed ? BRIDGE_HINT : "");
98
+ if (!result.settled) unsettled(result.timeout);
99
+ return result.frames;
100
+ }
101
+ };
102
+ }
103
+
104
+ //#endregion
105
+ export { devtools, inspect };
106
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","names":[],"sources":["../src/devtools.ts","../src/bridge.ts"],"sourcesContent":["import type { Evaluates } from './bridge';\n\n/** An entry of a CDP endpoint's `/json/list`. */\nexport interface Target {\n type: string;\n title: string;\n url: string;\n webSocketDebuggerUrl?: string;\n}\n\ntype Reply = {\n error?: { message: string };\n result?: { result: { value?: unknown }; exceptionDetails?: { text: string; exception?: { description?: string } } };\n};\n\nconst describe = (targets: Target[]) => targets.map((target) => `\\n ${target.type} ${target.title} ${target.url}`).join('');\n\n/**\n * Connect to a Chrome DevTools Protocol endpoint - a Node process under `--inspect`, or a browser with a\n * debug port - as an `evaluate` target for `inspect()`. `pick` narrows `/json/list` by predicate, or by a\n * string matched against title and URL; more than one match throws with the list. A `ws://` URL connects directly.\n */\nexport async function devtools(\n endpoint = 'http://127.0.0.1:9229',\n pick?: string | ((target: Target) => boolean)\n): Promise<Evaluates & { close(): void }> {\n let url = endpoint;\n\n if (!endpoint.startsWith('ws')) {\n const targets = ((await (await fetch(`${endpoint}/json/list`)).json()) as Target[]).filter((target) => target.webSocketDebuggerUrl);\n const match =\n typeof pick == 'string' ? (target: Target) => target.title.includes(pick) || target.url.includes(pick) : pick;\n const found = match ? targets.filter(match) : targets;\n\n if (found.length !== 1)\n throw new Error(\n found.length\n ? `Several debug targets at ${endpoint} - narrow with pick, e.g. (t) => t.type === 'page' && t.url.includes('…'):${describe(found)}`\n : `No debug target at ${endpoint}${typeof pick == 'string' ? ` matches \"${pick}\"` : pick ? ' matches pick' : ''}.${describe(targets)}`\n );\n\n url = found[0].webSocketDebuggerUrl!;\n }\n\n const socket = new WebSocket(url);\n const waiting = new Map<number, (reply: Reply) => void>();\n let id = 0;\n\n await new Promise((resolve, reject) => {\n socket.onopen = resolve;\n socket.onerror = () => reject(new Error(`Could not connect to ${url}.`));\n });\n\n socket.onmessage = ({ data }) => {\n const reply = JSON.parse(String(data));\n waiting.get(reply.id)?.(reply);\n waiting.delete(reply.id);\n };\n\n socket.onclose = () => {\n for (const settle of waiting.values()) settle({ error: { message: 'Debug connection closed.' } });\n waiting.clear();\n };\n\n return {\n async evaluate(fn, arg) {\n const { error, result } = await new Promise<Reply>((resolve) => {\n waiting.set(++id, resolve);\n socket.send(\n JSON.stringify({\n id,\n method: 'Runtime.evaluate',\n params: { expression: `(${fn})(${JSON.stringify(arg)})`, awaitPromise: true, returnByValue: true }\n })\n );\n });\n\n if (error) throw new Error(error.message);\n\n const { exceptionDetails } = result!;\n\n if (exceptionDetails) throw new Error(exceptionDetails.exception?.description ?? exceptionDetails.text);\n\n return result!.result.value;\n },\n close() {\n socket.close();\n }\n };\n}\n","import type { inspect as Inspect } from './index';\nimport { dispatch, type Call } from './dispatch';\nimport { bracket } from './bracket';\nimport { tick, unreached, unsettled, type Act } from './settle';\nimport type { Frame, Options, Query, Summary } from './journal';\nimport type { Select } from './serialize';\n\nexport { devtools, type Target } from './devtools';\n\nconst BRIDGE_HINT =\n ' On the bridge, activity counts once the step resolves, so a step that awaits the change never counts for an address - use { [address]: value }.';\n\n/**\n * Anything that can run a function in the page: Playwright `Page`, `Frame`, or\n * `Locator` (which passes the element first), puppeteer `Page` or `Frame`.\n */\nexport interface Evaluates {\n evaluate(fn: (...args: any[]) => unknown, arg?: unknown): Promise<unknown>;\n}\n\n/**\n * Drive the page's inspector from a test. Every method is one `evaluate`;\n * `act` runs a step recording values, settles, and returns the frames it produced.\n */\nexport function inspect(target: Evaluates) {\n const remote =\n <R>(...path: string[]) =>\n (...args: unknown[]) =>\n target.evaluate(dispatch, [path, args] as Call) as Promise<R>;\n\n const record = remote<Required<Options>>('journal', 'record');\n const seq = remote<number>('journal', 'seq');\n const frames = remote<Frame[]>('journal', 'frames');\n\n return {\n get: remote<unknown>('get') as (address?: string, select?: Select) => Promise<unknown>,\n set: remote<void>('set') as (address: string, value: unknown) => Promise<void>,\n call: remote<unknown>('call') as (address: string, ...args: unknown[]) => Promise<unknown>,\n models: remote<ReturnType<typeof Inspect.models>>('models') as () => Promise<ReturnType<typeof Inspect.models>>,\n tree: remote<ReturnType<typeof Inspect.tree>>('tree') as () => Promise<ReturnType<typeof Inspect.tree>>,\n health: remote<ReturnType<typeof Inspect.health>>('health') as () => Promise<ReturnType<typeof Inspect.health>>,\n\n journal: {\n record: record as (options?: Options) => Promise<Required<Options>>,\n seq: seq as () => Promise<number>,\n frames: frames as (query?: Query) => Promise<Frame[]>,\n history: remote<ReturnType<typeof Inspect.journal.history>>('journal', 'history') as (\n query: Query\n ) => Promise<ReturnType<typeof Inspect.journal.history>>,\n export: remote<string>('journal', 'export') as (query?: Query) => Promise<string>,\n summary: remote<Summary[]>('journal', 'summary') as (query?: Query) => Promise<Summary[]>,\n clear: remote<void>('journal', 'clear') as () => Promise<void>\n },\n\n async act(step: () => unknown, options: Act = {}): Promise<Frame[]> {\n const result = await bracket({ record, seq, frames, get: remote('get') }, step, () => target.evaluate(tick), options);\n const addressed = typeof options.until == 'string' || Array.isArray(options.until);\n\n if (result.pending.length)\n throw unreached(result.timeout, result.pending, result.missing, result.frames, addressed ? BRIDGE_HINT : '');\n if (!result.settled) unsettled(result.timeout);\n\n return result.frames;\n }\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ //#region src/dispatch.ts
2
+ /** Serialized by `evaluate` into the page - must stay self-contained. Walks own members of the inspector only. */
3
+ function dispatch(first, second) {
4
+ const call = second ?? first;
5
+ const api = globalThis.__EXPRESSIVE_INSPECT__;
6
+ if (!api) throw new Error("@expressive/inspect is not attached in this page - make '@expressive/inspect/install' the first import of the app entry.");
7
+ let target = api;
8
+ let owner;
9
+ for (const key of call[0]) {
10
+ if (!Object.prototype.hasOwnProperty.call(target, key)) throw new Error(`No inspector method ${call[0].join(".")}.`);
11
+ owner = target;
12
+ target = target[key];
13
+ }
14
+ if (typeof target != "function") throw new Error(`No inspector method ${call[0].join(".")}.`);
15
+ return target.apply(owner, call[1]);
16
+ }
17
+
18
+ //#endregion
19
+ export { dispatch as t };
20
+ //# sourceMappingURL=dispatch-Dnd88-J9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch-Dnd88-J9.js","names":[],"sources":["../src/dispatch.ts"],"sourcesContent":["export type Call = [path: string[], args: unknown[]];\n\n/** Serialized by `evaluate` into the page - must stay self-contained. Walks own members of the inspector only. */\nexport function dispatch(first: unknown, second?: unknown) {\n const call = (second ?? first) as Call;\n const api = (globalThis as { __EXPRESSIVE_INSPECT__?: Record<string, unknown> }).__EXPRESSIVE_INSPECT__;\n\n if (!api)\n throw new Error(\n \"@expressive/inspect is not attached in this page - make '@expressive/inspect/install' the first import of the app entry.\"\n );\n\n let target: unknown = api;\n let owner: unknown;\n\n for (const key of call[0]) {\n if (!Object.prototype.hasOwnProperty.call(target, key))\n throw new Error(`No inspector method ${call[0].join('.')}.`);\n\n owner = target;\n target = (target as Record<string, unknown>)[key];\n }\n\n if (typeof target != 'function') throw new Error(`No inspector method ${call[0].join('.')}.`);\n\n return target.apply(owner, call[1]);\n}\n"],"mappings":";;AAGA,SAAgB,SAAS,OAAgB,QAAkB;CACzD,MAAM,OAAQ,UAAU;CACxB,MAAM,MAAO,WAAoE;AAEjF,KAAI,CAAC,IACH,OAAM,IAAI,MACR,2HACD;CAEH,IAAI,SAAkB;CACtB,IAAI;AAEJ,MAAK,MAAM,OAAO,KAAK,IAAI;AACzB,MAAI,CAAC,OAAO,UAAU,eAAe,KAAK,QAAQ,IAAI,CACpD,OAAM,IAAI,MAAM,uBAAuB,KAAK,GAAG,KAAK,IAAI,CAAC,GAAG;AAE9D,UAAQ;AACR,WAAU,OAAmC;;AAG/C,KAAI,OAAO,UAAU,WAAY,OAAM,IAAI,MAAM,uBAAuB,KAAK,GAAG,KAAK,IAAI,CAAC,GAAG;AAE7F,QAAO,OAAO,MAAM,OAAO,KAAK,GAAG"}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- import { C as Level, E as act, S as Frame, T as Query, _ as warnings, a as Node, b as resolve, c as detach, d as instances, f as models, g as tree, h as set, i as Model, l as find, m as roots, n as serialize, o as attach, p as orphans, r as Instance, s as call, t as parsePath, u as get, v as TypeInfo, w as Options, x as Event, y as label } from "./serialize-FF81tr-L.js";
1
+ import { A as Summary, C as parsePath, D as Level, E as Frame, O as Options, S as Select, T as Event, _ as tree, a as act, b as resolve, c as detach, d as health, f as instances, g as set, h as roots, i as Node, k as Query, l as find, m as orphans, n as Instance, o as attach, p as models, r as Model, s as call, t as Health, u as get, v as TypeInfo, w as serialize, x as Act, y as label } from "./inspect-CEXzL-mC.js";
2
2
 
3
3
  //#region src/index.d.ts
4
4
  declare const journal: {
5
5
  record(options?: Options): Required<Options>;
6
+ clear(): void;
6
7
  frames(query?: Query): Frame[];
7
8
  history(query: Query): Array<{
8
9
  seq: number;
@@ -12,7 +13,7 @@ declare const journal: {
12
13
  downstream(from: number): Frame[];
13
14
  seq(): number;
14
15
  export(query?: Query): string;
15
- clear(): void;
16
+ summary(query?: Query): Summary[];
16
17
  reset(): void;
17
18
  };
18
19
  declare const inspect: {
@@ -22,7 +23,7 @@ declare const inspect: {
22
23
  instances: typeof instances;
23
24
  roots: typeof roots;
24
25
  orphans: typeof orphans;
25
- warnings: typeof warnings;
26
+ health: typeof health;
26
27
  models: typeof models;
27
28
  tree: typeof tree;
28
29
  get: typeof get;
@@ -33,6 +34,7 @@ declare const inspect: {
33
34
  resolve: typeof resolve;
34
35
  journal: {
35
36
  record(options?: Options): Required<Options>;
37
+ clear(): void;
36
38
  frames(query?: Query): Frame[];
37
39
  history(query: Query): Array<{
38
40
  seq: number;
@@ -42,7 +44,7 @@ declare const inspect: {
42
44
  downstream(from: number): Frame[];
43
45
  seq(): number;
44
46
  export(query?: Query): string;
45
- clear(): void;
47
+ summary(query?: Query): Summary[];
46
48
  reset(): void;
47
49
  };
48
50
  };
@@ -50,5 +52,5 @@ declare global {
50
52
  var __EXPRESSIVE_INSPECT__: typeof inspect | undefined;
51
53
  }
52
54
  //#endregion
53
- export { type Event, type Frame, Instance, type Level, type Model, type Node, type Options, type Query, type TypeInfo, act, attach, call, inspect as default, inspect, detach, find, get, instances, journal, label, models, orphans, parsePath, resolve, roots, serialize, set, tree, warnings };
55
+ export { type Act, type Event, type Frame, type Health, Instance, type Level, type Model, type Node, type Options, type Query, type Select, type Summary, type TypeInfo, act, attach, call, inspect as default, inspect, detach, find, get, health, instances, journal, label, models, orphans, parsePath, resolve, roots, serialize, set, tree };
54
56
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as label, a as find, b as serialize, c as models, d as set, f as tree, g as journal$1, h as act, i as detach, l as orphans, m as wrapAll, n as attach, o as get, p as warnings, r as call, s as instances, t as Instance, u as roots, v as resolve, y as parsePath } from "./inspect-D4NC5Mx5.js";
1
+ import { _ as journal$1, a as clearCaught, b as parsePath, c as get, d as models, f as orphans, g as wrapAll, h as tree, i as call, l as health, m as set, n as act, o as detach, p as roots, r as attach, s as find, t as Instance, u as instances, v as label, x as serialize, y as resolve } from "./inspect-FXOcmfKP.js";
2
2
 
3
3
  //#region src/index.ts
4
4
  const journal = {
@@ -7,6 +7,10 @@ const journal = {
7
7
  const config = journal$1.record(options);
8
8
  if (config.calls && config.level !== "off") wrapAll();
9
9
  return config;
10
+ },
11
+ clear() {
12
+ journal$1.clear();
13
+ clearCaught();
10
14
  }
11
15
  };
12
16
  const inspect = {
@@ -16,7 +20,7 @@ const inspect = {
16
20
  instances,
17
21
  roots,
18
22
  orphans,
19
- warnings,
23
+ health,
20
24
  models,
21
25
  tree,
22
26
  get,
@@ -29,5 +33,5 @@ const inspect = {
29
33
  };
30
34
 
31
35
  //#endregion
32
- export { Instance, act, attach, call, inspect as default, inspect, detach, find, get, instances, journal, label, models, orphans, parsePath, resolve, roots, serialize, set, tree, warnings };
36
+ export { Instance, act, attach, call, inspect as default, inspect, detach, find, get, health, instances, journal, label, models, orphans, parsePath, resolve, roots, serialize, set, tree };
33
37
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["base"],"sources":["../src/index.ts"],"sourcesContent":["import { attach, call, detach, find, get, Instance, instances, models, orphans, roots, set, tree, warnings, wrapAll } from './inspect';\nimport { act, journal as base, type Options } from './journal';\nimport { label, resolve } from './types';\n\nexport type { Model, Node } from './inspect';\nexport type { Event, Frame, Level, Options, Query } from './journal';\nexport type { TypeInfo } from './types';\nexport { Instance, act, attach, call, detach, find, get, instances, label, models, orphans, resolve, roots, set, tree, warnings };\nexport { parsePath, serialize } from './serialize';\n\nexport const journal = {\n ...base,\n record(options?: Options) {\n const config = base.record(options);\n if (config.calls && config.level !== 'off') wrapAll();\n return config;\n }\n};\n\nexport const inspect = {\n attach,\n detach,\n find,\n instances,\n roots,\n orphans,\n warnings,\n models,\n tree,\n get,\n set,\n call,\n act,\n label,\n resolve,\n journal\n};\n\ndeclare global {\n var __EXPRESSIVE_INSPECT__: typeof inspect | undefined;\n}\n\nexport default inspect;\n"],"mappings":";;;AAUA,MAAa,UAAU;CACrB,GAAGA;CACH,OAAO,SAAmB;EACxB,MAAM,SAASA,UAAK,OAAO,QAAQ;AACnC,MAAI,OAAO,SAAS,OAAO,UAAU,MAAO,UAAS;AACrD,SAAO;;CAEV;AAED,MAAa,UAAU;CACrB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD"}
1
+ {"version":3,"file":"index.js","names":["base"],"sources":["../src/index.ts"],"sourcesContent":["import { act, attach, call, clearCaught, detach, find, get, health, Instance, instances, models, orphans, roots, set, tree, wrapAll } from './inspect';\nimport { journal as base, type Options } from './journal';\nimport { label, resolve } from './types';\n\nexport type { Health, Model, Node } from './inspect';\nexport type { Event, Frame, Level, Options, Query, Summary } from './journal';\nexport type { Act } from './settle';\nexport type { Select } from './serialize';\nexport type { TypeInfo } from './types';\nexport { Instance, act, attach, call, detach, find, get, health, instances, label, models, orphans, resolve, roots, set, tree };\nexport { parsePath, serialize } from './serialize';\n\nexport const journal = {\n ...base,\n record(options?: Options) {\n const config = base.record(options);\n if (config.calls && config.level !== 'off') wrapAll();\n return config;\n },\n clear() {\n base.clear();\n clearCaught();\n }\n};\n\nexport const inspect = {\n attach,\n detach,\n find,\n instances,\n roots,\n orphans,\n health,\n models,\n tree,\n get,\n set,\n call,\n act,\n label,\n resolve,\n journal\n};\n\ndeclare global {\n var __EXPRESSIVE_INSPECT__: typeof inspect | undefined;\n}\n\nexport default inspect;\n"],"mappings":";;;AAYA,MAAa,UAAU;CACrB,GAAGA;CACH,OAAO,SAAmB;EACxB,MAAM,SAASA,UAAK,OAAO,QAAQ;AACnC,MAAI,OAAO,SAAS,OAAO,UAAU,MAAO,UAAS;AACrD,SAAO;;CAET,QAAQ;AACN,YAAK,OAAO;AACZ,eAAa;;CAEhB;AAED,MAAa,UAAU;CACrB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD"}
@@ -1,4 +1,4 @@
1
- import { State } from "@expressive/mvc";
1
+ import { Caught, State } from "@expressive/mvc";
2
2
 
3
3
  //#region src/journal.d.ts
4
4
  type Level = 'off' | 'keys' | 'values';
@@ -17,7 +17,7 @@ interface Event {
17
17
  id: string;
18
18
  type: string;
19
19
  key: string;
20
- kind: 'update' | 'event' | 'call' | 'destroy';
20
+ kind: 'update' | 'event' | 'call' | 'destroy' | 'caught';
21
21
  value?: unknown;
22
22
  args?: unknown[];
23
23
  }
@@ -28,6 +28,21 @@ interface Frame {
28
28
  cause?: number;
29
29
  events: Event[];
30
30
  }
31
+ interface Summary {
32
+ id: string;
33
+ type: string;
34
+ /** Seq of the last frame touching this instance. */
35
+ last: number;
36
+ /** Updates and events per key, with the last recorded value at `values` level. */
37
+ keys: Record<string, {
38
+ count: number;
39
+ value?: unknown;
40
+ }>;
41
+ calls: Record<string, number>;
42
+ /** `caught` reports. */
43
+ caught: number;
44
+ destroyed: boolean;
45
+ }
31
46
  interface Query {
32
47
  since?: number;
33
48
  type?: string;
@@ -35,8 +50,32 @@ interface Query {
35
50
  key?: string;
36
51
  cause?: number;
37
52
  }
38
- /** Run `work` with recording forced on; returns the frames it produced. */
39
- declare function act(work: () => unknown): Promise<Frame[]>;
53
+ //#endregion
54
+ //#region src/serialize.d.ts
55
+ type Address = {
56
+ target: string;
57
+ path?: string;
58
+ };
59
+ declare function parsePath(raw: string): Address;
60
+ /** `true` takes a value as `serialize` would; an object picks keys and descends, applying to each element of a list. */
61
+ type Select = true | {
62
+ [key: string]: Select;
63
+ };
64
+ /** JSON-safe, size-capped view. Nested States collapse to `$ref`. */
65
+ declare function serialize(value: unknown, depth?: number): unknown;
66
+ //#endregion
67
+ //#region src/settle.d.ts
68
+ interface Act {
69
+ /**
70
+ * What to wait for before settling: `Type.key` / `id.key` addresses that must each see a frame after the
71
+ * step's synchronous writes, or `{ [address]: value }` pairs that must each hold that value.
72
+ */
73
+ until?: string | string[] | Record<string, unknown>;
74
+ /** Longest to wait, in ms; default 1000. */
75
+ timeout?: number;
76
+ /** Recording filters for this window, instead of the journal's - `{ paths: [], types: [], keys: [] }` records everything. */
77
+ record?: Omit<Options, 'level'>;
78
+ }
40
79
  //#endregion
41
80
  //#region src/types.d.ts
42
81
  interface TypeInfo {
@@ -65,6 +104,8 @@ interface Node {
65
104
  type: string;
66
105
  children: Node[];
67
106
  }
107
+ declare const CASES: readonly ["Destroyed", "Inactive", "Getter", "Init", "Effect"];
108
+ type Case = (typeof CASES)[number];
68
109
  /** Navigable view of one instance - identity, ownership, reads, observation. One per state; outlives it. */
69
110
  declare class Instance {
70
111
  readonly state: State;
@@ -83,10 +124,10 @@ declare class Instance {
83
124
  get children(): Instance[];
84
125
  /** First instance in this subtree, depth-first, matching `where` (a label or predicate). */
85
126
  find(where: string | ((instance: Instance) => boolean)): Instance | undefined;
86
- get(path?: string): unknown;
127
+ get(path?: string, select?: Select): unknown;
87
128
  model(): Model;
88
129
  /** Run `work`, settle, and return the frames it produced. */
89
- act(work: (state: State) => unknown): Promise<Frame[]>;
130
+ act(work: (state: State) => unknown, options?: Act): Promise<Frame[]>;
90
131
  /** Call `fn` on each update, and with `null` on destroy; `keys` narrows updates. Returns unsubscribe. */
91
132
  watch(fn: (key: string | null) => void, keys?: string[]): () => void;
92
133
  frames(query?: Query): Frame[];
@@ -98,26 +139,26 @@ declare function find(target: string): Instance | undefined;
98
139
  declare function instances(): Instance[];
99
140
  /** Registered and settled, but never claimed by a host commit, an owner, or root context. */
100
141
  declare function orphans(): Instance[];
101
- /** Counts worth a look: live orphans, and unclaimed instances the collector already reaped. */
102
- declare function warnings(): {
142
+ interface Health {
143
+ /** Live instances no host, owner, or root slot claimed. */
103
144
  orphans: number;
145
+ /** Unclaimed instances the collector already reaped. */
104
146
  collected: number;
105
- };
147
+ /** Loaded copies of `@expressive/mvc` - more than 1 means inspect cannot see every State. */
148
+ copies: number;
149
+ /** `Caught` reports by case, including ones an app handler went on to handle; zeroed by `journal.clear()`. */
150
+ caught: Record<Case, number>;
151
+ }
152
+ /** Counts worth a look before trusting what inspect shows. */
153
+ declare function health(): Health;
106
154
  declare function roots(): Instance[];
107
155
  declare function models(): Model[];
108
156
  declare function tree(): Node[];
109
- declare function get(address?: string): unknown;
157
+ /** Run `work` recording values; returns the frames it produced once `until` holds and activity goes quiet. */
158
+ declare function act(work: () => unknown, options?: Act): Promise<Frame[]>;
159
+ declare function get(address?: string, select?: Select): unknown;
110
160
  declare function set(address: string, value: unknown): void;
111
161
  declare function call(address: string, ...args: unknown[]): Promise<unknown>;
112
162
  //#endregion
113
- //#region src/serialize.d.ts
114
- type Address = {
115
- target: string;
116
- path?: string;
117
- };
118
- declare function parsePath(raw: string): Address;
119
- /** JSON-safe, size-capped view. Nested States collapse to `$ref`. */
120
- declare function serialize(value: unknown, depth?: number): unknown;
121
- //#endregion
122
- export { Level as C, act as E, Frame as S, Query as T, warnings as _, Node as a, resolve as b, detach as c, instances as d, models as f, tree as g, set as h, Model as i, find as l, roots as m, serialize as n, attach as o, orphans as p, Instance as r, call as s, parsePath as t, get as u, TypeInfo as v, Options as w, Event as x, label as y };
123
- //# sourceMappingURL=serialize-FF81tr-L.d.ts.map
163
+ export { Summary as A, parsePath as C, Level as D, Frame as E, Options as O, Select as S, Event as T, tree as _, act as a, resolve as b, detach as c, health as d, instances as f, set as g, roots as h, Node as i, Query as k, find as l, orphans as m, Instance as n, attach as o, models as p, Model as r, call as s, Health as t, get as u, TypeInfo as v, serialize as w, Act as x, label as y };
164
+ //# sourceMappingURL=inspect-CEXzL-mC.d.ts.map