posipaki 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugins/debug-logger.d.ts +28 -1
- package/dist/plugins/debug-logger.d.ts.map +1 -1
- package/dist/plugins/debug-logger.js +54 -15
- package/dist/plugins/debug-logger.js.map +1 -1
- package/dist/testing/index.js +13 -1
- package/dist/testing/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/pname-match-ddweNtrI.js +0 -16
- package/dist/pname-match-ddweNtrI.js.map +0 -1
|
@@ -13,19 +13,46 @@ interface DebugLogFn {
|
|
|
13
13
|
interface MessageLogFn {
|
|
14
14
|
(message: Message): void;
|
|
15
15
|
}
|
|
16
|
+
interface LifecycleLogFn {
|
|
17
|
+
(event: string, detail?: unknown): void;
|
|
18
|
+
}
|
|
16
19
|
interface Logger {
|
|
17
20
|
debug: DebugLogFn;
|
|
18
21
|
info: DebugLogFn;
|
|
19
22
|
warn: DebugLogFn;
|
|
20
23
|
error: DebugLogFn;
|
|
21
24
|
msg: MessageLogFn;
|
|
25
|
+
lifecycle: LifecycleLogFn;
|
|
22
26
|
}
|
|
23
27
|
type LoggerFactory = (name: string) => Logger;
|
|
28
|
+
/**
|
|
29
|
+
* Filter a message before it is logged. Return a (possibly shrunk) message,
|
|
30
|
+
* or `null` to skip it entirely. Used to keep large payloads — a full
|
|
31
|
+
* conversation history, a big tool result — out of the log scroll.
|
|
32
|
+
*/
|
|
33
|
+
type MsgFilter = (msg: Message) => Message | null;
|
|
24
34
|
interface DebugLoggerOpts {
|
|
35
|
+
/** Message types to skip entirely (in addition to any `msgFilter`). */
|
|
25
36
|
ignore?: string[];
|
|
26
37
|
factory?: LoggerFactory;
|
|
38
|
+
/** Shrink or skip a message before it is logged via onMessage/onEmit.
|
|
39
|
+
* Defaults to {@link defaultMsgFilter}. */
|
|
40
|
+
msgFilter?: MsgFilter;
|
|
27
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Default message filter: shrink oversized string/array fields in place so a
|
|
44
|
+
* single message (a full conversation history, a big tool result) can't flood
|
|
45
|
+
* the log. Returns the message unchanged when nothing needs shrinking.
|
|
46
|
+
*/
|
|
47
|
+
declare function defaultMsgFilter(msg: Message): Message;
|
|
48
|
+
/**
|
|
49
|
+
* A debug/logging plugin that decorates `this.log` and observes the actor
|
|
50
|
+
* lifecycle (started / stopping / stopped / child-exited / error) plus message
|
|
51
|
+
* traffic. Hook registration is no longer gated by `DEBUG` — the supplied
|
|
52
|
+
* factory decides what to actually emit, so a ringbuffer (or anything else)
|
|
53
|
+
* can gate output at runtime.
|
|
54
|
+
*/
|
|
28
55
|
declare function debugLogger(opts?: DebugLoggerOpts): ActorPlugin;
|
|
29
56
|
//#endregion
|
|
30
|
-
export { DebugLogFn, DebugLoggerOpts, Logger, LoggerFactory, MessageLogFn, debugLogger };
|
|
57
|
+
export { DebugLogFn, DebugLoggerOpts, LifecycleLogFn, Logger, LoggerFactory, MessageLogFn, MsgFilter, debugLogger, defaultMsgFilter };
|
|
31
58
|
//# sourceMappingURL=debug-logger.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"debug-logger.d.ts","names":[],"sources":["../../src/plugins/debug-logger.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"debug-logger.d.ts","names":[],"sources":["../../src/plugins/debug-logger.ts"],"mappings":";;;;;YAKY,cAAA;IACR,GAAA,EAAK,MAAM;EAAA;AAAA;AAAA,UAIE,UAAA;EAAA,CACd,OAAA,aAAoB,IAAI;AAAA;AAAA,UAEV,YAAA;EAAA,CACd,OAAgB,EAAP,OAAO;AAAA;AAAA,UAEF,cAAA;EAAA,CACd,KAAA,UAAe,MAAgB;AAAA;AAAA,UAGjB,MAAA;EACf,KAAA,EAAO,UAAA;EACP,IAAA,EAAM,UAAA;EACN,IAAA,EAAM,UAAA;EACN,KAAA,EAAO,UAAA;EACP,GAAA,EAAK,YAAA;EACL,SAAA,EAAW,cAAA;AAAA;AAAA,KAED,aAAA,IAAiB,IAAA,aAAiB,MAAM;;;;AAXlB;AAGlC;KAeY,SAAA,IAAa,GAAA,EAAK,OAAA,KAAY,OAAO;AAAA,UAEhC,eAAA;EAhBR;EAkBP,MAAA;EACA,OAAA,GAAU,aAAA;EAhBH;;EAmBP,SAAA,GAAY,SAAS;AAAA;;;;;;iBAYP,gBAAA,CAAiB,GAAA,EAAK,OAAA,GAAU,OAAO;;;;;;;;iBAoCvC,WAAA,CAAY,IAAA,GAAO,eAAA,GAAkB,WAAW"}
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import { r as mergeConfigs } from "../hooks-V-PEPJHv.js";
|
|
2
|
-
import { t as pnameMatch } from "../pname-match-ddweNtrI.js";
|
|
3
2
|
//#region src/plugins/debug-logger.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
/** Fields larger than this are shrunk by {@link defaultMsgFilter}. */
|
|
4
|
+
const MAX_STRING = 256;
|
|
5
|
+
const MAX_ARRAY = 16;
|
|
6
|
+
/**
|
|
7
|
+
* Default message filter: shrink oversized string/array fields in place so a
|
|
8
|
+
* single message (a full conversation history, a big tool result) can't flood
|
|
9
|
+
* the log. Returns the message unchanged when nothing needs shrinking.
|
|
10
|
+
*/
|
|
11
|
+
function defaultMsgFilter(msg) {
|
|
12
|
+
const src = msg;
|
|
13
|
+
const shrunk = { ...src };
|
|
14
|
+
let changed = false;
|
|
15
|
+
for (const key of Object.keys(src)) {
|
|
16
|
+
const v = src[key];
|
|
17
|
+
if (typeof v === "string" && v.length > MAX_STRING) {
|
|
18
|
+
shrunk[key] = `${v.slice(0, MAX_STRING)}… (${v.length} chars)`;
|
|
19
|
+
changed = true;
|
|
20
|
+
} else if (Array.isArray(v) && v.length > MAX_ARRAY) {
|
|
21
|
+
shrunk[key] = `[${v.length} items]`;
|
|
22
|
+
changed = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return changed ? shrunk : msg;
|
|
8
26
|
}
|
|
9
27
|
function defaultFactory(name) {
|
|
10
28
|
return {
|
|
@@ -12,38 +30,59 @@ function defaultFactory(name) {
|
|
|
12
30
|
msg: (msg) => console.debug(`[${name}] ←`, msg),
|
|
13
31
|
info: (...a) => console.info(`[${name}]`, ...a),
|
|
14
32
|
warn: (...a) => console.warn(`[${name}]`, ...a),
|
|
15
|
-
error: (...a) => console.error(`[${name}]`, ...a)
|
|
33
|
+
error: (...a) => console.error(`[${name}]`, ...a),
|
|
34
|
+
lifecycle: (event, detail) => console.debug(`[${name}] lifecycle ${event}`, detail ?? "")
|
|
16
35
|
};
|
|
17
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* A debug/logging plugin that decorates `this.log` and observes the actor
|
|
39
|
+
* lifecycle (started / stopping / stopped / child-exited / error) plus message
|
|
40
|
+
* traffic. Hook registration is no longer gated by `DEBUG` — the supplied
|
|
41
|
+
* factory decides what to actually emit, so a ringbuffer (or anything else)
|
|
42
|
+
* can gate output at runtime.
|
|
43
|
+
*/
|
|
18
44
|
function debugLogger(opts) {
|
|
19
45
|
const ignoreSet = new Set(opts?.ignore ?? []);
|
|
20
46
|
const factory = opts?.factory ?? defaultFactory;
|
|
47
|
+
const msgFilter = opts?.msgFilter ?? defaultMsgFilter;
|
|
21
48
|
return async function debugLoggerPlugin(config) {
|
|
22
49
|
const name = config.name ?? "actor";
|
|
23
|
-
const pats = patterns();
|
|
24
50
|
const log = factory(name);
|
|
25
51
|
let result = mergeConfigs(config, {
|
|
26
52
|
methods: { ...config.methods },
|
|
27
53
|
$decorate: { log }
|
|
28
54
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
55
|
+
result = mergeConfigs(result, {
|
|
56
|
+
afterStart() {
|
|
57
|
+
log.lifecycle("started");
|
|
32
58
|
},
|
|
33
|
-
|
|
34
|
-
log.
|
|
59
|
+
beforeEnd(reason) {
|
|
60
|
+
log.lifecycle("stopping", reason);
|
|
61
|
+
},
|
|
62
|
+
afterEnd(reason) {
|
|
63
|
+
log.lifecycle("stopped", reason);
|
|
35
64
|
},
|
|
36
65
|
onChildExit(childName) {
|
|
37
|
-
log.
|
|
66
|
+
log.lifecycle("child-exited", childName);
|
|
38
67
|
},
|
|
39
68
|
onError(err) {
|
|
40
|
-
log.error(`${err
|
|
69
|
+
log.error(`${err?.message ?? err}`);
|
|
70
|
+
},
|
|
71
|
+
onMessage(msg) {
|
|
72
|
+
if (ignoreSet.has(msg.type)) return;
|
|
73
|
+
const m = msgFilter ? msgFilter(msg) : msg;
|
|
74
|
+
if (m) log.msg(m);
|
|
75
|
+
},
|
|
76
|
+
onEmit(msg) {
|
|
77
|
+
if (ignoreSet.has(msg.type)) return;
|
|
78
|
+
const m = msgFilter ? msgFilter(msg) : msg;
|
|
79
|
+
if (m) log.debug(`${name} → ${m.type}`, m);
|
|
41
80
|
}
|
|
42
81
|
});
|
|
43
82
|
return result;
|
|
44
83
|
};
|
|
45
84
|
}
|
|
46
85
|
//#endregion
|
|
47
|
-
export { debugLogger };
|
|
86
|
+
export { debugLogger, defaultMsgFilter };
|
|
48
87
|
|
|
49
88
|
//# sourceMappingURL=debug-logger.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"debug-logger.js","names":[],"sources":["../../src/plugins/debug-logger.ts"],"sourcesContent":["import type { ActorPlugin } from \"../hooks\";\nimport { mergeConfigs } from \"../hooks\";\nimport type { Message } from \"../types\";\
|
|
1
|
+
{"version":3,"file":"debug-logger.js","names":[],"sources":["../../src/plugins/debug-logger.ts"],"sourcesContent":["import type { ActorPlugin } from \"../hooks\";\nimport { mergeConfigs } from \"../hooks\";\nimport type { Message } from \"../types\";\n\ndeclare module \"../index\" {\n interface ActorDecorated {\n log: Logger;\n }\n}\n\nexport interface DebugLogFn {\n (message: string, ...args: unknown[]): void;\n}\nexport interface MessageLogFn {\n (message: Message): void;\n}\nexport interface LifecycleLogFn {\n (event: string, detail?: unknown): void;\n}\n\nexport interface Logger {\n debug: DebugLogFn;\n info: DebugLogFn;\n warn: DebugLogFn;\n error: DebugLogFn;\n msg: MessageLogFn;\n lifecycle: LifecycleLogFn;\n}\nexport type LoggerFactory = (name: string) => Logger;\n\n/**\n * Filter a message before it is logged. Return a (possibly shrunk) message,\n * or `null` to skip it entirely. Used to keep large payloads — a full\n * conversation history, a big tool result — out of the log scroll.\n */\nexport type MsgFilter = (msg: Message) => Message | null;\n\nexport interface DebugLoggerOpts {\n /** Message types to skip entirely (in addition to any `msgFilter`). */\n ignore?: string[];\n factory?: LoggerFactory;\n /** Shrink or skip a message before it is logged via onMessage/onEmit.\n * Defaults to {@link defaultMsgFilter}. */\n msgFilter?: MsgFilter;\n}\n\n/** Fields larger than this are shrunk by {@link defaultMsgFilter}. */\nconst MAX_STRING = 256;\nconst MAX_ARRAY = 16;\n\n/**\n * Default message filter: shrink oversized string/array fields in place so a\n * single message (a full conversation history, a big tool result) can't flood\n * the log. Returns the message unchanged when nothing needs shrinking.\n */\nexport function defaultMsgFilter(msg: Message): Message {\n const src = msg as unknown as Record<string, unknown>;\n const shrunk: Record<string, unknown> = { ...src };\n let changed = false;\n for (const key of Object.keys(src)) {\n const v = src[key];\n if (typeof v === \"string\" && v.length > MAX_STRING) {\n shrunk[key] = `${v.slice(0, MAX_STRING)}… (${v.length} chars)`;\n changed = true;\n } else if (Array.isArray(v) && v.length > MAX_ARRAY) {\n shrunk[key] = `[${v.length} items]`;\n changed = true;\n }\n }\n return changed ? (shrunk as unknown as Message) : msg;\n}\n\nfunction defaultFactory(name: string): Logger {\n return {\n debug: (...a: unknown[]) => console.debug(`[${name}]`, ...a),\n msg: (msg: Message) => console.debug(`[${name}] ←`, msg),\n info: (...a: unknown[]) => console.info(`[${name}]`, ...a),\n warn: (...a: unknown[]) => console.warn(`[${name}]`, ...a),\n error: (...a: unknown[]) => console.error(`[${name}]`, ...a),\n lifecycle: (event: string, detail?: unknown) =>\n console.debug(`[${name}] lifecycle ${event}`, detail ?? \"\"),\n };\n}\n\n/**\n * A debug/logging plugin that decorates `this.log` and observes the actor\n * lifecycle (started / stopping / stopped / child-exited / error) plus message\n * traffic. Hook registration is no longer gated by `DEBUG` — the supplied\n * factory decides what to actually emit, so a ringbuffer (or anything else)\n * can gate output at runtime.\n */\nexport function debugLogger(opts?: DebugLoggerOpts): ActorPlugin {\n const ignoreSet = new Set(opts?.ignore ?? []);\n const factory = opts?.factory ?? defaultFactory;\n const msgFilter = opts?.msgFilter ?? defaultMsgFilter;\n return async function debugLoggerPlugin(config) {\n const name: string = config.name ?? \"actor\";\n const log = factory(name);\n\n let result = mergeConfigs(config, {\n methods: { ...config.methods },\n $decorate: { log },\n });\n\n result = mergeConfigs(result, {\n afterStart() {\n log.lifecycle(\"started\");\n },\n beforeEnd(reason: unknown) {\n log.lifecycle(\"stopping\", reason);\n },\n afterEnd(reason: unknown) {\n log.lifecycle(\"stopped\", reason);\n },\n onChildExit(childName: string) {\n log.lifecycle(\"child-exited\", childName);\n },\n onError(err: unknown) {\n log.error(`${(err as Error)?.message ?? err}`);\n },\n onMessage(msg: Message) {\n if (ignoreSet.has(msg.type)) return;\n const m = msgFilter ? msgFilter(msg) : msg;\n if (m) log.msg(m);\n },\n onEmit(msg: Message) {\n if (ignoreSet.has(msg.type)) return;\n const m = msgFilter ? msgFilter(msg) : msg;\n if (m) log.debug(`${name} → ${m.type}`, m);\n },\n });\n\n return result;\n };\n}\n"],"mappings":";;;AA+CA,MAAM,aAAa;AACnB,MAAM,YAAY;;;;;;AAOlB,SAAgB,iBAAiB,KAAuB;CACtD,MAAM,MAAM;CACZ,MAAM,SAAkC,EAAE,GAAG,IAAI;CACjD,IAAI,UAAU;CACd,KAAK,MAAM,OAAO,OAAO,KAAK,GAAG,GAAG;EAClC,MAAM,IAAI,IAAI;EACd,IAAI,OAAO,MAAM,YAAY,EAAE,SAAS,YAAY;GAClD,OAAO,OAAO,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,KAAK,EAAE,OAAO;GACtD,UAAU;EACZ,OAAO,IAAI,MAAM,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW;GACnD,OAAO,OAAO,IAAI,EAAE,OAAO;GAC3B,UAAU;EACZ;CACF;CACA,OAAO,UAAW,SAAgC;AACpD;AAEA,SAAS,eAAe,MAAsB;CAC5C,OAAO;EACL,QAAQ,GAAG,MAAiB,QAAQ,MAAM,IAAI,KAAK,IAAI,GAAG,CAAC;EAC3D,MAAM,QAAiB,QAAQ,MAAM,IAAI,KAAK,MAAM,GAAG;EACvD,OAAO,GAAG,MAAiB,QAAQ,KAAK,IAAI,KAAK,IAAI,GAAG,CAAC;EACzD,OAAO,GAAG,MAAiB,QAAQ,KAAK,IAAI,KAAK,IAAI,GAAG,CAAC;EACzD,QAAQ,GAAG,MAAiB,QAAQ,MAAM,IAAI,KAAK,IAAI,GAAG,CAAC;EAC3D,YAAY,OAAe,WACzB,QAAQ,MAAM,IAAI,KAAK,cAAc,SAAS,UAAU,EAAE;CAC9D;AACF;;;;;;;;AASA,SAAgB,YAAY,MAAqC;CAC/D,MAAM,YAAY,IAAI,IAAI,MAAM,UAAU,CAAC,CAAC;CAC5C,MAAM,UAAU,MAAM,WAAW;CACjC,MAAM,YAAY,MAAM,aAAa;CACrC,OAAO,eAAe,kBAAkB,QAAQ;EAC9C,MAAM,OAAe,OAAO,QAAQ;EACpC,MAAM,MAAM,QAAQ,IAAI;EAExB,IAAI,SAAS,aAAa,QAAQ;GAChC,SAAS,EAAE,GAAG,OAAO,QAAQ;GAC7B,WAAW,EAAE,IAAI;EACnB,CAAC;EAED,SAAS,aAAa,QAAQ;GAC5B,aAAa;IACX,IAAI,UAAU,SAAS;GACzB;GACA,UAAU,QAAiB;IACzB,IAAI,UAAU,YAAY,MAAM;GAClC;GACA,SAAS,QAAiB;IACxB,IAAI,UAAU,WAAW,MAAM;GACjC;GACA,YAAY,WAAmB;IAC7B,IAAI,UAAU,gBAAgB,SAAS;GACzC;GACA,QAAQ,KAAc;IACpB,IAAI,MAAM,GAAI,KAAe,WAAW,KAAK;GAC/C;GACA,UAAU,KAAc;IACtB,IAAI,UAAU,IAAI,IAAI,IAAI,GAAG;IAC7B,MAAM,IAAI,YAAY,UAAU,GAAG,IAAI;IACvC,IAAI,GAAG,IAAI,IAAI,CAAC;GAClB;GACA,OAAO,KAAc;IACnB,IAAI,UAAU,IAAI,IAAI,IAAI,GAAG;IAC7B,MAAM,IAAI,YAAY,UAAU,GAAG,IAAI;IACvC,IAAI,GAAG,IAAI,MAAM,GAAG,KAAK,KAAK,EAAE,QAAQ,CAAC;GAC3C;EACF,CAAC;EAED,OAAO;CACT;AACF"}
|
package/dist/testing/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { r as mergeConfigs } from "../hooks-V-PEPJHv.js";
|
|
2
|
-
import { t as pnameMatch } from "../pname-match-ddweNtrI.js";
|
|
3
2
|
//#region src/testing/msg-matcher.ts
|
|
4
3
|
function shallowMatch(msg, spec) {
|
|
5
4
|
for (const key of Object.keys(spec)) if (msg[key] !== spec[key]) return false;
|
|
@@ -16,6 +15,19 @@ function toMatcher(spec) {
|
|
|
16
15
|
return (msg) => shallowMatch(msg, spec);
|
|
17
16
|
}
|
|
18
17
|
//#endregion
|
|
18
|
+
//#region src/testing/pname-match.ts
|
|
19
|
+
/** True if `name` matches any of `patterns`. */
|
|
20
|
+
function pnameMatch(name, patterns) {
|
|
21
|
+
for (const p of patterns) {
|
|
22
|
+
if (p === "*") return true;
|
|
23
|
+
if (p.endsWith(":*")) {
|
|
24
|
+
const prefix = p.slice(0, -2);
|
|
25
|
+
if (name === prefix || name.startsWith(prefix + ":")) return true;
|
|
26
|
+
} else if (p === name) return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
19
31
|
//#region src/testing/test-plugin.ts
|
|
20
32
|
function createCollector(filter, opts) {
|
|
21
33
|
const messages = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/testing/msg-matcher.ts","../../src/testing/test-plugin.ts"],"sourcesContent":["// ── Message Matcher ──────────────────────────────────────────────────────\n//\n// Canonical form: `fn(msg, history) => boolean`. `msg` is the just-arrived\n// message (also the last element of `history`); `history` is the full\n// accumulated list *including* `msg`.\n//\n// Two shortcuts:\n// - a message literal `{ type: \"X\" }` → matches the latest message\n// - a list `[{...}, {...}]` → matches the tail of history,\n// in order, from oldest to newest\n\nimport type { Message } from \"../types.js\";\n\nexport type Matcher<M extends Message> = (msg: M, history: M[]) => boolean;\n\nexport type MatchSpec<M extends Message> =\n | Matcher<M>\n | Partial<M>\n | Array<Partial<M>>;\n\nfunction shallowMatch<M extends Message>(msg: M, spec: Partial<M>): boolean {\n for (const key of Object.keys(spec) as Array<keyof M>) {\n if (msg[key] !== spec[key]) return false;\n }\n return true;\n}\n\n/** Convert a matcher spec to a canonical matcher function. */\nexport function toMatcher<M extends Message>(spec: MatchSpec<M>): Matcher<M> {\n if (typeof spec === \"function\") return spec as Matcher<M>;\n\n if (Array.isArray(spec)) {\n // Sequence: the tail of history must equal `spec`, in order.\n return (_msg, history) => {\n if (history.length < spec.length) return false;\n const tail = history.slice(-spec.length);\n return spec.every((s, i) => shallowMatch(tail[i], s));\n };\n }\n\n // Single literal: the latest message must shallow-match.\n return (msg) => shallowMatch(msg, spec);\n}\n","// ── Test Plugins ─────────────────────────────────────────────────────────\n//\n// createCollector: per-actor message collector plugin.\n// createRootTracker: global process tracker for cleanup.\n\nimport { mergeConfigs, type ActorPlugin } from \"../hooks.js\";\nimport type { Message, StopMessage, SenderInfo, ProcessCtx } from \"../types.js\";\nimport { toMatcher, type MatchSpec } from \"./msg-matcher.js\";\nimport { pnameMatch } from \"./pname-match.js\";\n\n// ── types ────────────────────────────────────────────────────────────────\n\nexport interface MatchResult {\n ok: boolean;\n detail?: string;\n}\n\nexport interface Collector<M extends Message> {\n plugin: ActorPlugin;\n messages: M[];\n resolved(): Promise<MatchResult>;\n next(filter: MatchSpec<M>): Promise<MatchResult>;\n reset(filter?: MatchSpec<M>): void;\n}\n\nexport interface RootTracker {\n plugin: ActorPlugin;\n stopAll(): Promise<void>;\n}\n\n// ── createCollector ──────────────────────────────────────────────────────\n\nexport function createCollector<M extends Message>(\n filter: MatchSpec<M>,\n opts?: { scope?: string | string[] },\n): Collector<M> {\n const messages: M[] = [];\n let matcher = toMatcher(filter);\n // Captured from the root's beforeStart; used as the default scope.\n let defaultScope: string[] = [];\n // The plugin is installed on the root first (spawn assembles before any\n // child is forked), so the first install's beforeStart is the root.\n let installed = false;\n let waiters: Array<(result: MatchResult) => void> = [];\n\n function scopePatterns(): string[] {\n if (opts?.scope === undefined) return defaultScope;\n return Array.isArray(opts.scope) ? opts.scope : [opts.scope];\n }\n\n function checkAndNotify() {\n const last = messages[messages.length - 1];\n if (last !== undefined && matcher(last, messages)) {\n const ws = waiters.splice(0);\n for (const w of ws) w({ ok: true });\n }\n }\n\n function resolved(): Promise<MatchResult> {\n return new Promise((resolve) => {\n const last = messages[messages.length - 1];\n if (last !== undefined && matcher(last, messages)) {\n resolve({ ok: true });\n return;\n }\n waiters.push(resolve);\n });\n }\n\n const collectorPlugin: ActorPlugin = (config) => {\n const isRoot = !installed;\n installed = true;\n return mergeConfigs(config, {\n beforeStart() {\n if (isRoot) defaultScope = [this.name];\n },\n onEmit(msg: Message, sender: SenderInfo) {\n if (!pnameMatch(sender.fromName, scopePatterns())) return;\n messages.push(msg as M);\n checkAndNotify();\n },\n beforeEnd() {\n const ws = waiters.splice(0);\n for (const w of ws) w({ ok: false, detail: \"actor exited before match\" });\n },\n });\n };\n\n // Named function so framework deduplication works.\n Object.defineProperty(collectorPlugin, \"name\", { value: \"messageCollector\" });\n\n return {\n plugin: collectorPlugin,\n messages,\n resolved,\n next(nextFilter) {\n matcher = toMatcher(nextFilter);\n return resolved();\n },\n reset(newFilter) {\n if (newFilter) matcher = toMatcher(newFilter);\n },\n };\n}\n\n// ── createRootTracker ────────────────────────────────────────────────────\n\nexport function createRootTracker(): RootTracker {\n const roots = new Set<{ sendSelf: (msg: StopMessage) => void }>();\n\n const rootTrackerPlugin: ActorPlugin = (config) =>\n mergeConfigs(config, {\n beforeStart(this: { ctx: ProcessCtx<unknown, unknown, Message, Message> }) {\n roots.add(this.ctx);\n },\n afterEnd(this: { ctx: ProcessCtx<unknown, unknown, Message, Message> }) {\n roots.delete(this.ctx);\n },\n });\n\n Object.defineProperty(rootTrackerPlugin, \"name\", { value: \"rootTracker\" });\n\n return {\n plugin: rootTrackerPlugin,\n async stopAll() {\n for (const root of roots) {\n try {\n root.sendSelf({ type: \"STOP\" });\n } catch {}\n }\n roots.clear();\n },\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/testing/msg-matcher.ts","../../src/testing/pname-match.ts","../../src/testing/test-plugin.ts"],"sourcesContent":["// ── Message Matcher ──────────────────────────────────────────────────────\n//\n// Canonical form: `fn(msg, history) => boolean`. `msg` is the just-arrived\n// message (also the last element of `history`); `history` is the full\n// accumulated list *including* `msg`.\n//\n// Two shortcuts:\n// - a message literal `{ type: \"X\" }` → matches the latest message\n// - a list `[{...}, {...}]` → matches the tail of history,\n// in order, from oldest to newest\n\nimport type { Message } from \"../types.js\";\n\nexport type Matcher<M extends Message> = (msg: M, history: M[]) => boolean;\n\nexport type MatchSpec<M extends Message> =\n | Matcher<M>\n | Partial<M>\n | Array<Partial<M>>;\n\nfunction shallowMatch<M extends Message>(msg: M, spec: Partial<M>): boolean {\n for (const key of Object.keys(spec) as Array<keyof M>) {\n if (msg[key] !== spec[key]) return false;\n }\n return true;\n}\n\n/** Convert a matcher spec to a canonical matcher function. */\nexport function toMatcher<M extends Message>(spec: MatchSpec<M>): Matcher<M> {\n if (typeof spec === \"function\") return spec as Matcher<M>;\n\n if (Array.isArray(spec)) {\n // Sequence: the tail of history must equal `spec`, in order.\n return (_msg, history) => {\n if (history.length < spec.length) return false;\n const tail = history.slice(-spec.length);\n return spec.every((s, i) => shallowMatch(tail[i], s));\n };\n }\n\n // Single literal: the latest message must shallow-match.\n return (msg) => shallowMatch(msg, spec);\n}\n","// ── Process Name Match ───────────────────────────────────────────────────\n//\n// Tree-prefix matching for posipaki process names (`parent:child:...`).\n//\n// Pattern language (same as debug-logger's DEBUG env filtering):\n// - \"*\" matches any name\n// - \"name\" matches exactly\n// - \"prefix:*\" matches `prefix` and `prefix:` + anything below it\n\n/** True if `name` matches any of `patterns`. */\nexport function pnameMatch(name: string, patterns: string[]): boolean {\n for (const p of patterns) {\n if (p === \"*\") return true;\n if (p.endsWith(\":*\")) {\n const prefix = p.slice(0, -2);\n if (name === prefix || name.startsWith(prefix + \":\")) return true;\n } else if (p === name) {\n return true;\n }\n }\n return false;\n}\n","// ── Test Plugins ─────────────────────────────────────────────────────────\n//\n// createCollector: per-actor message collector plugin.\n// createRootTracker: global process tracker for cleanup.\n\nimport { mergeConfigs, type ActorPlugin } from \"../hooks.js\";\nimport type { Message, StopMessage, SenderInfo, ProcessCtx } from \"../types.js\";\nimport { toMatcher, type MatchSpec } from \"./msg-matcher.js\";\nimport { pnameMatch } from \"./pname-match.js\";\n\n// ── types ────────────────────────────────────────────────────────────────\n\nexport interface MatchResult {\n ok: boolean;\n detail?: string;\n}\n\nexport interface Collector<M extends Message> {\n plugin: ActorPlugin;\n messages: M[];\n resolved(): Promise<MatchResult>;\n next(filter: MatchSpec<M>): Promise<MatchResult>;\n reset(filter?: MatchSpec<M>): void;\n}\n\nexport interface RootTracker {\n plugin: ActorPlugin;\n stopAll(): Promise<void>;\n}\n\n// ── createCollector ──────────────────────────────────────────────────────\n\nexport function createCollector<M extends Message>(\n filter: MatchSpec<M>,\n opts?: { scope?: string | string[] },\n): Collector<M> {\n const messages: M[] = [];\n let matcher = toMatcher(filter);\n // Captured from the root's beforeStart; used as the default scope.\n let defaultScope: string[] = [];\n // The plugin is installed on the root first (spawn assembles before any\n // child is forked), so the first install's beforeStart is the root.\n let installed = false;\n let waiters: Array<(result: MatchResult) => void> = [];\n\n function scopePatterns(): string[] {\n if (opts?.scope === undefined) return defaultScope;\n return Array.isArray(opts.scope) ? opts.scope : [opts.scope];\n }\n\n function checkAndNotify() {\n const last = messages[messages.length - 1];\n if (last !== undefined && matcher(last, messages)) {\n const ws = waiters.splice(0);\n for (const w of ws) w({ ok: true });\n }\n }\n\n function resolved(): Promise<MatchResult> {\n return new Promise((resolve) => {\n const last = messages[messages.length - 1];\n if (last !== undefined && matcher(last, messages)) {\n resolve({ ok: true });\n return;\n }\n waiters.push(resolve);\n });\n }\n\n const collectorPlugin: ActorPlugin = (config) => {\n const isRoot = !installed;\n installed = true;\n return mergeConfigs(config, {\n beforeStart() {\n if (isRoot) defaultScope = [this.name];\n },\n onEmit(msg: Message, sender: SenderInfo) {\n if (!pnameMatch(sender.fromName, scopePatterns())) return;\n messages.push(msg as M);\n checkAndNotify();\n },\n beforeEnd() {\n const ws = waiters.splice(0);\n for (const w of ws) w({ ok: false, detail: \"actor exited before match\" });\n },\n });\n };\n\n // Named function so framework deduplication works.\n Object.defineProperty(collectorPlugin, \"name\", { value: \"messageCollector\" });\n\n return {\n plugin: collectorPlugin,\n messages,\n resolved,\n next(nextFilter) {\n matcher = toMatcher(nextFilter);\n return resolved();\n },\n reset(newFilter) {\n if (newFilter) matcher = toMatcher(newFilter);\n },\n };\n}\n\n// ── createRootTracker ────────────────────────────────────────────────────\n\nexport function createRootTracker(): RootTracker {\n const roots = new Set<{ sendSelf: (msg: StopMessage) => void }>();\n\n const rootTrackerPlugin: ActorPlugin = (config) =>\n mergeConfigs(config, {\n beforeStart(this: { ctx: ProcessCtx<unknown, unknown, Message, Message> }) {\n roots.add(this.ctx);\n },\n afterEnd(this: { ctx: ProcessCtx<unknown, unknown, Message, Message> }) {\n roots.delete(this.ctx);\n },\n });\n\n Object.defineProperty(rootTrackerPlugin, \"name\", { value: \"rootTracker\" });\n\n return {\n plugin: rootTrackerPlugin,\n async stopAll() {\n for (const root of roots) {\n try {\n root.sendSelf({ type: \"STOP\" });\n } catch {}\n }\n roots.clear();\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
//#region src/testing/pname-match.ts
|
|
2
|
-
/** True if `name` matches any of `patterns`. */
|
|
3
|
-
function pnameMatch(name, patterns) {
|
|
4
|
-
for (const p of patterns) {
|
|
5
|
-
if (p === "*") return true;
|
|
6
|
-
if (p.endsWith(":*")) {
|
|
7
|
-
const prefix = p.slice(0, -2);
|
|
8
|
-
if (name === prefix || name.startsWith(prefix + ":")) return true;
|
|
9
|
-
} else if (p === name) return true;
|
|
10
|
-
}
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
//#endregion
|
|
14
|
-
export { pnameMatch as t };
|
|
15
|
-
|
|
16
|
-
//# sourceMappingURL=pname-match-ddweNtrI.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pname-match-ddweNtrI.js","names":[],"sources":["../src/testing/pname-match.ts"],"sourcesContent":["// ── Process Name Match ───────────────────────────────────────────────────\n//\n// Tree-prefix matching for posipaki process names (`parent:child:...`).\n//\n// Pattern language (same as debug-logger's DEBUG env filtering):\n// - \"*\" matches any name\n// - \"name\" matches exactly\n// - \"prefix:*\" matches `prefix` and `prefix:` + anything below it\n\n/** True if `name` matches any of `patterns`. */\nexport function pnameMatch(name: string, patterns: string[]): boolean {\n for (const p of patterns) {\n if (p === \"*\") return true;\n if (p.endsWith(\":*\")) {\n const prefix = p.slice(0, -2);\n if (name === prefix || name.startsWith(prefix + \":\")) return true;\n } else if (p === name) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":";;AAUA,SAAgB,WAAW,MAAc,UAA6B;CACpE,KAAK,MAAM,KAAK,UAAU;EACxB,IAAI,MAAM,KAAK,OAAO;EACtB,IAAI,EAAE,SAAS,IAAI,GAAG;GACpB,MAAM,SAAS,EAAE,MAAM,GAAG,EAAE;GAC5B,IAAI,SAAS,UAAU,KAAK,WAAW,SAAS,GAAG,GAAG,OAAO;EAC/D,OAAO,IAAI,MAAM,MACf,OAAO;CAEX;CACA,OAAO;AACT"}
|