omkit 0.8.15 → 0.8.17
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 +413 -411
- package/dist/core/ActionRun.d.ts +4 -0
- package/dist/core/ActionRun.js +74 -7
- package/dist/core/refTrace.d.ts +22 -0
- package/dist/core/refTrace.js +71 -0
- package/dist/foundation/LogEntry.d.ts +21 -1
- package/dist/output/writers/report/report.css +493 -433
- package/dist/output/writers/report/report.js +537 -322
- package/package.json +1 -1
package/dist/core/ActionRun.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export declare class ActionRun<Result = unknown, Events extends object = object,
|
|
|
72
72
|
private attached;
|
|
73
73
|
private observed;
|
|
74
74
|
private refObserved;
|
|
75
|
+
/** The traced handle every `.ref` reader shares (see {@link traceHandle}); made on first read. */
|
|
76
|
+
private tracedRef;
|
|
75
77
|
private cancelLogged;
|
|
76
78
|
private armedExec;
|
|
77
79
|
private armedArgs;
|
|
@@ -123,4 +125,6 @@ export declare class ActionRun<Result = unknown, Events extends object = object,
|
|
|
123
125
|
*/
|
|
124
126
|
private lifecycle;
|
|
125
127
|
private log;
|
|
128
|
+
/** A cross-node interaction, on this (the targeted) node's log; `source` is the link's op. */
|
|
129
|
+
private link;
|
|
126
130
|
}
|
package/dist/core/ActionRun.js
CHANGED
|
@@ -2,6 +2,9 @@ import { defer } from "../foundation/Deferred.js";
|
|
|
2
2
|
import { events } from "../foundation/events.js";
|
|
3
3
|
import { CancelledError } from "../foundation/CancelledError.js";
|
|
4
4
|
import { renderValue } from "../foundation/format.js";
|
|
5
|
+
import { callerSite } from "../foundation/callsite.js";
|
|
6
|
+
import { currentNode } from "./context.js";
|
|
7
|
+
import { traceHandle, REF_CALL_CAP } from "./refTrace.js";
|
|
5
8
|
import { createProc } from "../system/proc.js";
|
|
6
9
|
import { FolderCache } from "../system/fs/FolderCache.js";
|
|
7
10
|
/**
|
|
@@ -45,6 +48,8 @@ export class ActionRun {
|
|
|
45
48
|
attached = false;
|
|
46
49
|
observed = false;
|
|
47
50
|
refObserved = false;
|
|
51
|
+
/** The traced handle every `.ref` reader shares (see {@link traceHandle}); made on first read. */
|
|
52
|
+
tracedRef;
|
|
48
53
|
cancelLogged = false;
|
|
49
54
|
// The armed body + args, set at launch and run once by `commit()` on a microtask — the
|
|
50
55
|
// window during which `withCache` may still wrap the body. `committed` closes that window.
|
|
@@ -108,7 +113,23 @@ export class ActionRun {
|
|
|
108
113
|
/** The attached handle; rejects on failure/cancel. */
|
|
109
114
|
get ref() {
|
|
110
115
|
this.refObserved = true;
|
|
111
|
-
|
|
116
|
+
// One traced handle for every reader — identity holds across reads, and each method call
|
|
117
|
+
// another node makes on it is recorded here as a `ref` link.
|
|
118
|
+
if (!this.tracedRef) {
|
|
119
|
+
this.tracedRef = this.handle.promise.then((h) => traceHandle(h, {
|
|
120
|
+
call: (c) => this.link(`${c.key}() ← ${c.from} · ${c.ms}ms${c.outcome === "failed" ? " · failed" : ""}`, {
|
|
121
|
+
op: "ref",
|
|
122
|
+
from: c.from,
|
|
123
|
+
...(c.site ? { site: c.site } : {}),
|
|
124
|
+
key: c.key,
|
|
125
|
+
ms: c.ms,
|
|
126
|
+
outcome: c.outcome,
|
|
127
|
+
}),
|
|
128
|
+
capped: () => this.log("info", "ref", `ref calls past ${REF_CALL_CAP} are not recorded`),
|
|
129
|
+
}));
|
|
130
|
+
void this.tracedRef.catch(() => { });
|
|
131
|
+
}
|
|
132
|
+
return this.tracedRef;
|
|
112
133
|
}
|
|
113
134
|
on(key, handler) {
|
|
114
135
|
// A late `on("error")` (e.g. attached after an `await`, once the body has already
|
|
@@ -119,9 +140,29 @@ export class ActionRun {
|
|
|
119
140
|
this.emitter.listen(key, handler);
|
|
120
141
|
}
|
|
121
142
|
once(key) {
|
|
143
|
+
// Who waits, from where — recorded on this node as a `once` link when the wait ends.
|
|
144
|
+
const from = currentNode.getStore();
|
|
145
|
+
const site = callerSite();
|
|
146
|
+
const t0 = Date.now();
|
|
147
|
+
const note = (outcome) => {
|
|
148
|
+
if (!from || from.id === this.id)
|
|
149
|
+
return;
|
|
150
|
+
const ms = Date.now() - t0;
|
|
151
|
+
this.link(`${String(key)} ← ${from.id} · ${outcome} after ${ms}ms`, {
|
|
152
|
+
op: "once",
|
|
153
|
+
from: from.id,
|
|
154
|
+
...(site ? { site } : {}),
|
|
155
|
+
key: String(key),
|
|
156
|
+
ms,
|
|
157
|
+
outcome,
|
|
158
|
+
});
|
|
159
|
+
};
|
|
122
160
|
// `done` is the outcome itself — identical to `.result` (resolve the value, reject on failure).
|
|
123
|
-
if (key === "done")
|
|
124
|
-
|
|
161
|
+
if (key === "done") {
|
|
162
|
+
const outcome = this.result;
|
|
163
|
+
outcome.then(() => note("fired"), () => note("failed"));
|
|
164
|
+
return outcome;
|
|
165
|
+
}
|
|
125
166
|
// Awaiting an event observes the activity: resolve the payload when it fires, but if the
|
|
126
167
|
// activity settles first, resolve `undefined` on success and **reject** on failure/cancel —
|
|
127
168
|
// so an event gate surfaces a failure instead of hanging (or, for `error`, hand back the error).
|
|
@@ -130,16 +171,21 @@ export class ActionRun {
|
|
|
130
171
|
let done = false;
|
|
131
172
|
const win = () => (done ? false : (done = true));
|
|
132
173
|
this.emitter.listenOnce(key, (p) => {
|
|
133
|
-
if (win())
|
|
134
|
-
|
|
174
|
+
if (!win())
|
|
175
|
+
return;
|
|
176
|
+
note("fired");
|
|
177
|
+
resolve(p);
|
|
135
178
|
});
|
|
136
179
|
this.settled.promise.then(() => {
|
|
137
|
-
if (win())
|
|
138
|
-
|
|
180
|
+
if (!win())
|
|
181
|
+
return;
|
|
182
|
+
note("ended");
|
|
183
|
+
resolve(undefined);
|
|
139
184
|
}, (error) => {
|
|
140
185
|
if (!win())
|
|
141
186
|
return;
|
|
142
187
|
// Awaiting the `error` event hands back the error; any other gate rejects on failure.
|
|
188
|
+
note(key === "error" ? "fired" : "failed");
|
|
143
189
|
if (key === "error")
|
|
144
190
|
resolve(error);
|
|
145
191
|
else
|
|
@@ -165,6 +211,16 @@ export class ActionRun {
|
|
|
165
211
|
return this;
|
|
166
212
|
}
|
|
167
213
|
cancel() {
|
|
214
|
+
// A cancel from another node is a link: who stopped this one, and from where.
|
|
215
|
+
const from = currentNode.getStore();
|
|
216
|
+
if (from && from.id !== this.id && this.parentId !== null && this.status === "running") {
|
|
217
|
+
const site = callerSite();
|
|
218
|
+
this.link(`by ${from.id}${site ? ` @ ${site}` : ""}`, {
|
|
219
|
+
op: "cancel",
|
|
220
|
+
from: from.id,
|
|
221
|
+
...(site ? { site } : {}),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
168
224
|
this.noteCancelled(); // a direct cancel of this activity
|
|
169
225
|
this.controller.abort();
|
|
170
226
|
}
|
|
@@ -335,6 +391,17 @@ export class ActionRun {
|
|
|
335
391
|
log(level, source, message) {
|
|
336
392
|
this.store.append({ nodeId: this.id, path: this.path, level, source, message });
|
|
337
393
|
}
|
|
394
|
+
/** A cross-node interaction, on this (the targeted) node's log; `source` is the link's op. */
|
|
395
|
+
link(message, link) {
|
|
396
|
+
this.store.append({
|
|
397
|
+
nodeId: this.id,
|
|
398
|
+
path: this.path,
|
|
399
|
+
level: "link",
|
|
400
|
+
source: link.op,
|
|
401
|
+
message,
|
|
402
|
+
link,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
338
405
|
}
|
|
339
406
|
const messageOf = (error) => error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
340
407
|
const firstLine = (message) => message.split("\n", 1)[0] ?? message;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Calls recorded per handle; past this the trace goes quiet, so a polling loop can't flood the log. */
|
|
2
|
+
export declare const REF_CALL_CAP = 500;
|
|
3
|
+
/** One method call on a traced handle, as {@link traceHandle} reports it. */
|
|
4
|
+
export interface RefCall {
|
|
5
|
+
/** The calling node's id. */
|
|
6
|
+
readonly from: string;
|
|
7
|
+
readonly site: string | undefined;
|
|
8
|
+
readonly key: string;
|
|
9
|
+
readonly ms: number;
|
|
10
|
+
readonly outcome: "ok" | "failed";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Wrap an attached handle so each public method call on it is reported — who called (the
|
|
14
|
+
* ambient node), from where, how long it took, and whether it threw. Shallow by design: what
|
|
15
|
+
* a method returns is handed back untraced. Methods run bound to the real object, so private
|
|
16
|
+
* fields keep working; symbol, `_`-prefixed and {@link QUIET} members pass through unrecorded.
|
|
17
|
+
* A primitive or nullish handle is returned as is.
|
|
18
|
+
*/
|
|
19
|
+
export declare const traceHandle: <H>(handle: H, report: {
|
|
20
|
+
call: (call: RefCall) => void;
|
|
21
|
+
capped: () => void;
|
|
22
|
+
}) => H;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { callerSite } from "../foundation/callsite.js";
|
|
2
|
+
import { currentNode } from "./context.js";
|
|
3
|
+
/** Calls recorded per handle; past this the trace goes quiet, so a polling loop can't flood the log. */
|
|
4
|
+
export const REF_CALL_CAP = 500;
|
|
5
|
+
/** Members never recorded: promise/serialization/inspection plumbing, not the caller's intent. */
|
|
6
|
+
const QUIET = new Set([
|
|
7
|
+
"then",
|
|
8
|
+
"catch",
|
|
9
|
+
"finally",
|
|
10
|
+
"toJSON",
|
|
11
|
+
"toString",
|
|
12
|
+
"valueOf",
|
|
13
|
+
"inspect",
|
|
14
|
+
"constructor",
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Wrap an attached handle so each public method call on it is reported — who called (the
|
|
18
|
+
* ambient node), from where, how long it took, and whether it threw. Shallow by design: what
|
|
19
|
+
* a method returns is handed back untraced. Methods run bound to the real object, so private
|
|
20
|
+
* fields keep working; symbol, `_`-prefixed and {@link QUIET} members pass through unrecorded.
|
|
21
|
+
* A primitive or nullish handle is returned as is.
|
|
22
|
+
*/
|
|
23
|
+
export const traceHandle = (handle, report) => {
|
|
24
|
+
if (handle === null || (typeof handle !== "object" && typeof handle !== "function"))
|
|
25
|
+
return handle;
|
|
26
|
+
const cache = new Map();
|
|
27
|
+
let count = 0;
|
|
28
|
+
return new Proxy(handle, {
|
|
29
|
+
get(target, prop) {
|
|
30
|
+
const value = Reflect.get(target, prop, target);
|
|
31
|
+
if (typeof value !== "function")
|
|
32
|
+
return value;
|
|
33
|
+
// a non-configurable, read-only property must come back as itself (a Proxy invariant)
|
|
34
|
+
const own = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
35
|
+
if (own && !own.configurable && !own.writable)
|
|
36
|
+
return value;
|
|
37
|
+
const hit = cache.get(prop);
|
|
38
|
+
if (hit && hit.orig === value)
|
|
39
|
+
return hit.fn;
|
|
40
|
+
const name = String(prop);
|
|
41
|
+
const quiet = typeof prop === "symbol" || name.startsWith("_") || QUIET.has(name);
|
|
42
|
+
const method = value;
|
|
43
|
+
const fn = (...args) => {
|
|
44
|
+
const from = currentNode.getStore()?.id;
|
|
45
|
+
if (quiet || !from || count >= REF_CALL_CAP)
|
|
46
|
+
return method.apply(target, args);
|
|
47
|
+
if (++count === REF_CALL_CAP)
|
|
48
|
+
report.capped();
|
|
49
|
+
const site = callerSite();
|
|
50
|
+
const t0 = Date.now();
|
|
51
|
+
const done = (outcome) => report.call({ from, site, key: name, ms: Date.now() - t0, outcome });
|
|
52
|
+
let out;
|
|
53
|
+
try {
|
|
54
|
+
out = method.apply(target, args);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
done("failed");
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (out !== null && typeof out?.then === "function") {
|
|
61
|
+
out.then(() => done("ok"), () => done("failed"));
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
done("ok");
|
|
65
|
+
return out;
|
|
66
|
+
};
|
|
67
|
+
cache.set(prop, { orig: value, fn });
|
|
68
|
+
return fn;
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
};
|
|
@@ -13,11 +13,31 @@ export interface LogEntry {
|
|
|
13
13
|
readonly nodeId: string;
|
|
14
14
|
/** The owning node's execution path (`main/chromePage_9f3c`). */
|
|
15
15
|
readonly path: string;
|
|
16
|
-
/** `info` | `error` | `event` | `assert` | `snapshot` | `artifact` | `tag` | `child` | `run`. */
|
|
16
|
+
/** `info` | `error` | `event` | `assert` | `snapshot` | `artifact` | `tag` | `child` | `run` | `link`. */
|
|
17
17
|
readonly level: string;
|
|
18
18
|
/** Finer origin within the node (proc name, `event`, `run`, …). */
|
|
19
19
|
readonly source: string;
|
|
20
20
|
readonly message: string;
|
|
21
|
+
/** Set on `level: "link"` entries — the other side of a cross-node interaction. */
|
|
22
|
+
readonly link?: LogLink;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A cross-node interaction, recorded on the node it targets: another node waited on one of
|
|
26
|
+
* its events (`once`), cancelled it (`cancel`), or called a method on its attached handle
|
|
27
|
+
* (`ref`). The entry's `ts` is when the interaction ended, so a timed one spans `ts - ms … ts`.
|
|
28
|
+
*/
|
|
29
|
+
export interface LogLink {
|
|
30
|
+
readonly op: "once" | "cancel" | "ref";
|
|
31
|
+
/** The calling node's id. */
|
|
32
|
+
readonly from: string;
|
|
33
|
+
/** `file:line` of the call, when the stack had one. */
|
|
34
|
+
readonly site?: string;
|
|
35
|
+
/** The awaited event key (`once`) or the called method (`ref`). */
|
|
36
|
+
readonly key?: string;
|
|
37
|
+
/** How long the wait or call lasted (`once`, `ref`). */
|
|
38
|
+
readonly ms?: number;
|
|
39
|
+
/** How it ended — `fired` | `ended` | `failed` for a once, `ok` | `failed` for a ref call. */
|
|
40
|
+
readonly outcome?: string;
|
|
21
41
|
}
|
|
22
42
|
/** What an appender supplies; the store stamps `sequence` and `ts`. */
|
|
23
43
|
export type LogInput = Omit<LogEntry, "sequence" | "ts">;
|