@zfdx123/dsh-hooks-ordering 1.0.1

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +257 -0
  3. package/client.js +619 -0
  4. package/cordis.patch.yml +38 -0
  5. package/lib/dag-Bqx-sl71.d.ts +55 -0
  6. package/lib/dag-Bqx-sl71.d.ts.map +1 -0
  7. package/lib/dag-DVhoBjBG.js +48 -0
  8. package/lib/dag-DVhoBjBG.js.map +1 -0
  9. package/lib/dag.d.ts +3 -0
  10. package/lib/dag.js +3 -0
  11. package/lib/index.d.ts +121 -0
  12. package/lib/index.d.ts.map +1 -0
  13. package/lib/index.js +190 -0
  14. package/lib/index.js.map +1 -0
  15. package/lib/serial-Cu7usHjI.js +65 -0
  16. package/lib/serial-Cu7usHjI.js.map +1 -0
  17. package/lib/serial-D8ZJCBKL.d.ts +55 -0
  18. package/lib/serial-D8ZJCBKL.d.ts.map +1 -0
  19. package/lib/serial.d.ts +5 -0
  20. package/lib/serial.js +6 -0
  21. package/lib/service-base-CCmIBwnB.d.ts +100 -0
  22. package/lib/service-base-CCmIBwnB.d.ts.map +1 -0
  23. package/lib/service-base-a5vKg62S.js +139 -0
  24. package/lib/service-base-a5vKg62S.js.map +1 -0
  25. package/lib/service-base.d.ts +4 -0
  26. package/lib/service-base.js +5 -0
  27. package/lib/topo-sort-BZ1fFcTs.d.ts +54 -0
  28. package/lib/topo-sort-BZ1fFcTs.d.ts.map +1 -0
  29. package/lib/topo-sort-CfwYPY4U.js +83 -0
  30. package/lib/topo-sort-CfwYPY4U.js.map +1 -0
  31. package/lib/topo-sort.d.ts +2 -0
  32. package/lib/topo-sort.js +3 -0
  33. package/lib/waterfall-Bu6m9gYc.js +83 -0
  34. package/lib/waterfall-Bu6m9gYc.js.map +1 -0
  35. package/lib/waterfall-_5HkptkS.d.ts +87 -0
  36. package/lib/waterfall-_5HkptkS.d.ts.map +1 -0
  37. package/lib/waterfall.d.ts +5 -0
  38. package/lib/waterfall.js +6 -0
  39. package/package.json +117 -0
  40. package/src/dag.ts +92 -0
  41. package/src/dsh.ts +181 -0
  42. package/src/index.ts +54 -0
  43. package/src/serial.ts +108 -0
  44. package/src/service-base.ts +179 -0
  45. package/src/settings.ts +97 -0
  46. package/src/topo-sort.ts +118 -0
  47. package/src/waterfall.ts +153 -0
@@ -0,0 +1,100 @@
1
+ import { n as Orderable } from "./topo-sort-BZ1fFcTs.js";
2
+ import { r as DagSection } from "./dag-Bqx-sl71.js";
3
+ import { Context, Service } from "@deepseek-ai/cordis";
4
+
5
+ //#region src/service-base.d.ts
6
+
7
+ /**
8
+ * Where a participant runs relative to the native hook chain.
9
+ * - `front`: ahead of every native listener (and, for waterfall, the built-in default).
10
+ * - `back`: behind the native chain (waterfall) / best-effort last (serial).
11
+ */
12
+ type Phase = 'front' | 'back';
13
+ /** Thrown when registering into, or double-controlling, a hook in an unsupported state. */
14
+ declare class HookControlError extends Error {
15
+ /**
16
+ * @param message - the specific control-state violation.
17
+ */
18
+ constructor(message: string);
19
+ }
20
+ /** Optional file logging shared by both services. */
21
+ interface HookOrderingLogConfig {
22
+ /**
23
+ * When set, the constraint DAG (JSON) is written to this file on every
24
+ * registration change, so it always reflects current state. Write failures
25
+ * are reported via `console.warn` and never thrown back into the fiber.
26
+ */
27
+ readonly log?: string;
28
+ }
29
+ /** Per-hook coordinator state: the two ordered phases and the disposer for the installed coordinator(s). */
30
+ interface ControlledHook<E extends Orderable> {
31
+ readonly front: E[];
32
+ readonly back: E[];
33
+ readonly dispose: () => void;
34
+ }
35
+ /**
36
+ * Common base for the hook-ordering services. Subclasses implement
37
+ * {@link install} (register the coordinator listener(s) for one hook and return
38
+ * their disposer) and expose a typed `register`; everything else is shared.
39
+ * @typeParam E - the participant entry type stored per phase.
40
+ */
41
+ declare abstract class HookOrderingBase<E extends Orderable> extends Service {
42
+ protected readonly hooks: Map<string, ControlledHook<E>>;
43
+ /** File the DAG is logged to on every registration change, if configured. */
44
+ readonly log: string | undefined;
45
+ /**
46
+ * @param ctx - the Cordis context to register the service in.
47
+ * @param name - the service name exposed on `ctx`.
48
+ * @param config - optional `log` file for the constraint DAG. May be `null`:
49
+ * a loader row whose `config:` key holds only comments parses as YAML null.
50
+ */
51
+ constructor(ctx: Context, name: string, config?: HookOrderingLogConfig | null);
52
+ /**
53
+ * Install the coordinator listener(s) for one hook, capturing its `front`/
54
+ * `back` lists, and return a disposer removing them. Subclass-specific.
55
+ */
56
+ protected abstract install(hook: string, front: E[], back: E[]): () => void;
57
+ /**
58
+ * Take control of a hook by installing the coordinator listener(s). Called
59
+ * once per hook; participants then register into it. Controlling twice is
60
+ * rejected — a second coordinator would reintroduce the race this removes.
61
+ *
62
+ * @param hook - the event name to control.
63
+ * @returns a disposer that removes the coordinator(s) and forgets the hook.
64
+ * @throws {HookControlError} when the hook is already controlled.
65
+ */
66
+ control(hook: string): () => void;
67
+ /**
68
+ * Shared registration: push the entry into the hook's phase list as a fiber
69
+ * effect, refreshing the DAG log on add and remove.
70
+ *
71
+ * @returns a disposer that unregisters this participant.
72
+ * @throws {HookControlError} when the hook has not been controlled.
73
+ */
74
+ protected registerEntry(hook: string, phase: Phase, entry: E): () => void;
75
+ /**
76
+ * Compute the ordered participant names for one hook phase without running
77
+ * them. Reflects current registrations; useful for tests and diagnostics.
78
+ *
79
+ * @throws {HookControlError} when the hook has not been controlled.
80
+ */
81
+ plan(hook: string, phase: Phase): string[];
82
+ /**
83
+ * Serialize the constraint DAG of every controlled hook (all phases) as
84
+ * pretty-printed JSON. Pure read: does not write the log file or throw on
85
+ * cycles — the graph is most useful precisely when constraints conflict.
86
+ */
87
+ dumpDag(): string;
88
+ /** Flatten every controlled hook into its `front`/`back` DAG sections. */
89
+ protected dagSections(): DagSection[];
90
+ /**
91
+ * Best-effort write of the current DAG to the configured `log` file. Never
92
+ * throws: a failure (e.g. unwritable path) is reported via `console.warn`,
93
+ * because this runs inside registration effects and disposers, where an
94
+ * exception would corrupt fiber teardown.
95
+ */
96
+ protected refreshLog(): void;
97
+ }
98
+ //#endregion
99
+ export { Phase as a, HookOrderingLogConfig as i, HookControlError as n, HookOrderingBase as r, ControlledHook as t };
100
+ //# sourceMappingURL=service-base-CCmIBwnB.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-base-CCmIBwnB.d.ts","names":[],"sources":["../src/service-base.ts"],"sourcesContent":[],"mappings":";;;;;;AAyDA;;;;;AAWmB,KA/CP,KAAA,GA+CO,OAAA,GAAA,MAAA;;AAS+B,cArDrC,gBAAA,SAAyB,KAAA,CAqDY;EAAW;;;EA8DjC,WAAA,CAAA,OAAA,EAAA,MAAA;;;AAlF+C,UAtB1D,qBAAA,CAsB0D;;;;;;;;;UAZ1D,yBAAyB;kBACxB;iBACD;;;;;;;;;uBAUK,2BAA2B,mBAAmB,OAAA;4BAC1C,YAAA,eAAA;;;;;;;;;mBAUP,gCAA+B;;;;;kDASA,WAAW;;;;;;;;;;;;;;;;;;+CAmCd,cAAc;;;;;;;4BA2BjC;;;;;;;;2BAgBD"}
@@ -0,0 +1,139 @@
1
+ import { t as buildDag } from "./dag-DVhoBjBG.js";
2
+ import { r as topoSort } from "./topo-sort-CfwYPY4U.js";
3
+ import { writeFileSync } from "node:fs";
4
+ import { Service } from "@deepseek-ai/cordis";
5
+
6
+ //#region src/service-base.ts
7
+ /** Thrown when registering into, or double-controlling, a hook in an unsupported state. */
8
+ var HookControlError = class extends Error {
9
+ /**
10
+ * @param message - the specific control-state violation.
11
+ */
12
+ constructor(message) {
13
+ super(`hooks-ordering: ${message}`);
14
+ this.name = "HookControlError";
15
+ }
16
+ };
17
+ /**
18
+ * Common base for the hook-ordering services. Subclasses implement
19
+ * {@link install} (register the coordinator listener(s) for one hook and return
20
+ * their disposer) and expose a typed `register`; everything else is shared.
21
+ * @typeParam E - the participant entry type stored per phase.
22
+ */
23
+ var HookOrderingBase = class extends Service {
24
+ hooks = /* @__PURE__ */ new Map();
25
+ /** File the DAG is logged to on every registration change, if configured. */
26
+ log;
27
+ /**
28
+ * @param ctx - the Cordis context to register the service in.
29
+ * @param name - the service name exposed on `ctx`.
30
+ * @param config - optional `log` file for the constraint DAG. May be `null`:
31
+ * a loader row whose `config:` key holds only comments parses as YAML null.
32
+ */
33
+ constructor(ctx, name, config = {}) {
34
+ super(ctx, name);
35
+ this.log = config?.log;
36
+ }
37
+ /**
38
+ * Take control of a hook by installing the coordinator listener(s). Called
39
+ * once per hook; participants then register into it. Controlling twice is
40
+ * rejected — a second coordinator would reintroduce the race this removes.
41
+ *
42
+ * @param hook - the event name to control.
43
+ * @returns a disposer that removes the coordinator(s) and forgets the hook.
44
+ * @throws {HookControlError} when the hook is already controlled.
45
+ */
46
+ control(hook) {
47
+ if (this.hooks.has(hook)) throw new HookControlError(`hook ${JSON.stringify(hook)} is already controlled`);
48
+ const front = [];
49
+ const back = [];
50
+ const removeListeners = this.install(hook, front, back);
51
+ const dispose = () => {
52
+ removeListeners();
53
+ this.hooks.delete(hook);
54
+ this.refreshLog();
55
+ };
56
+ this.hooks.set(hook, {
57
+ front,
58
+ back,
59
+ dispose
60
+ });
61
+ this.refreshLog();
62
+ return dispose;
63
+ }
64
+ /**
65
+ * Shared registration: push the entry into the hook's phase list as a fiber
66
+ * effect, refreshing the DAG log on add and remove.
67
+ *
68
+ * @returns a disposer that unregisters this participant.
69
+ * @throws {HookControlError} when the hook has not been controlled.
70
+ */
71
+ registerEntry(hook, phase, entry) {
72
+ const controlled = this.hooks.get(hook);
73
+ if (controlled === void 0) throw new HookControlError(`hook ${JSON.stringify(hook)} is not controlled; call control(${JSON.stringify(hook)}) first`);
74
+ const list = controlled[phase];
75
+ return this.ctx.effect(() => {
76
+ list.push(entry);
77
+ this.refreshLog();
78
+ return () => {
79
+ const at = list.indexOf(entry);
80
+ if (at >= 0) list.splice(at, 1);
81
+ this.refreshLog();
82
+ };
83
+ }, `${this.name}.register(${JSON.stringify(hook)}, ${JSON.stringify(phase)}, ${JSON.stringify(entry.name)})`);
84
+ }
85
+ /**
86
+ * Compute the ordered participant names for one hook phase without running
87
+ * them. Reflects current registrations; useful for tests and diagnostics.
88
+ *
89
+ * @throws {HookControlError} when the hook has not been controlled.
90
+ */
91
+ plan(hook, phase) {
92
+ const controlled = this.hooks.get(hook);
93
+ if (controlled === void 0) throw new HookControlError(`hook ${JSON.stringify(hook)} is not controlled`);
94
+ return topoSort(controlled[phase]).map((entry) => entry.name);
95
+ }
96
+ /**
97
+ * Serialize the constraint DAG of every controlled hook (all phases) as
98
+ * pretty-printed JSON. Pure read: does not write the log file or throw on
99
+ * cycles — the graph is most useful precisely when constraints conflict.
100
+ */
101
+ dumpDag() {
102
+ return JSON.stringify(buildDag(this.dagSections()), null, 2);
103
+ }
104
+ /** Flatten every controlled hook into its `front`/`back` DAG sections. */
105
+ dagSections() {
106
+ const sections = [];
107
+ for (const [hook, controlled] of this.hooks) {
108
+ sections.push({
109
+ hook,
110
+ phase: "front",
111
+ entries: controlled.front
112
+ });
113
+ sections.push({
114
+ hook,
115
+ phase: "back",
116
+ entries: controlled.back
117
+ });
118
+ }
119
+ return sections;
120
+ }
121
+ /**
122
+ * Best-effort write of the current DAG to the configured `log` file. Never
123
+ * throws: a failure (e.g. unwritable path) is reported via `console.warn`,
124
+ * because this runs inside registration effects and disposers, where an
125
+ * exception would corrupt fiber teardown.
126
+ */
127
+ refreshLog() {
128
+ if (this.log === void 0) return;
129
+ try {
130
+ writeFileSync(this.log, `${this.dumpDag()}\n`);
131
+ } catch (error) {
132
+ console.warn(`hooks-ordering: failed to write DAG log to ${JSON.stringify(this.log)}:`, error);
133
+ }
134
+ }
135
+ };
136
+
137
+ //#endregion
138
+ export { HookOrderingBase as n, HookControlError as t };
139
+ //# sourceMappingURL=service-base-a5vKg62S.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-base-a5vKg62S.js","names":["front: E[]","back: E[]","sections: DagSection[]"],"sources":["../src/service-base.ts"],"sourcesContent":["/**\n * Shared coordinator machinery for the waterfall and serial hook-ordering\n * services. Both manage, per controlled hook, two ordered participant lists\n * (`front`/`back`) plus the disposer for the installed coordinator(s); they\n * differ only in HOW the coordinator listeners are installed and HOW a phase\n * runs (waterfall wraps the native chain via `next()`; serial runs participants\n * ahead of it with bail short-circuiting). Registration, planning, DAG dumping,\n * and optional file logging are identical, so they live here.\n * @module dsh-hooks-ordering/service-base\n */\n\nimport { writeFileSync } from 'node:fs'\nimport { type Context, Service } from '@deepseek-ai/cordis'\nimport { type DagSection, buildDag } from './dag.ts'\nimport { type Orderable, topoSort } from './topo-sort.ts'\n\n/**\n * Where a participant runs relative to the native hook chain.\n * - `front`: ahead of every native listener (and, for waterfall, the built-in default).\n * - `back`: behind the native chain (waterfall) / best-effort last (serial).\n */\nexport type Phase = 'front' | 'back'\n\n/** Thrown when registering into, or double-controlling, a hook in an unsupported state. */\nexport class HookControlError extends Error {\n /**\n * @param message - the specific control-state violation.\n */\n constructor(message: string) {\n super(`hooks-ordering: ${message}`)\n this.name = 'HookControlError'\n }\n}\n\n/** Optional file logging shared by both services. */\nexport interface HookOrderingLogConfig {\n /**\n * When set, the constraint DAG (JSON) is written to this file on every\n * registration change, so it always reflects current state. Write failures\n * are reported via `console.warn` and never thrown back into the fiber.\n */\n readonly log?: string\n}\n\n/** Per-hook coordinator state: the two ordered phases and the disposer for the installed coordinator(s). */\nexport interface ControlledHook<E extends Orderable> {\n readonly front: E[]\n readonly back: E[]\n readonly dispose: () => void\n}\n\n/**\n * Common base for the hook-ordering services. Subclasses implement\n * {@link install} (register the coordinator listener(s) for one hook and return\n * their disposer) and expose a typed `register`; everything else is shared.\n * @typeParam E - the participant entry type stored per phase.\n */\nexport abstract class HookOrderingBase<E extends Orderable> extends Service {\n protected readonly hooks = new Map<string, ControlledHook<E>>()\n /** File the DAG is logged to on every registration change, if configured. */\n readonly log: string | undefined\n\n /**\n * @param ctx - the Cordis context to register the service in.\n * @param name - the service name exposed on `ctx`.\n * @param config - optional `log` file for the constraint DAG. May be `null`:\n * a loader row whose `config:` key holds only comments parses as YAML null.\n */\n constructor(ctx: Context, name: string, config: HookOrderingLogConfig | null = {}) {\n super(ctx, name)\n this.log = config?.log\n }\n\n /**\n * Install the coordinator listener(s) for one hook, capturing its `front`/\n * `back` lists, and return a disposer removing them. Subclass-specific.\n */\n protected abstract install(hook: string, front: E[], back: E[]): () => void\n\n /**\n * Take control of a hook by installing the coordinator listener(s). Called\n * once per hook; participants then register into it. Controlling twice is\n * rejected — a second coordinator would reintroduce the race this removes.\n *\n * @param hook - the event name to control.\n * @returns a disposer that removes the coordinator(s) and forgets the hook.\n * @throws {HookControlError} when the hook is already controlled.\n */\n control(hook: string): () => void {\n if (this.hooks.has(hook)) throw new HookControlError(`hook ${JSON.stringify(hook)} is already controlled`)\n\n const front: E[] = []\n const back: E[] = []\n const removeListeners = this.install(hook, front, back)\n\n const dispose = (): void => {\n removeListeners()\n this.hooks.delete(hook)\n this.refreshLog()\n }\n this.hooks.set(hook, { front, back, dispose })\n this.refreshLog()\n return dispose\n }\n\n /**\n * Shared registration: push the entry into the hook's phase list as a fiber\n * effect, refreshing the DAG log on add and remove.\n *\n * @returns a disposer that unregisters this participant.\n * @throws {HookControlError} when the hook has not been controlled.\n */\n protected registerEntry(hook: string, phase: Phase, entry: E): () => void {\n const controlled = this.hooks.get(hook)\n if (controlled === undefined)\n throw new HookControlError(\n `hook ${JSON.stringify(hook)} is not controlled; call control(${JSON.stringify(hook)}) first`,\n )\n const list = controlled[phase]\n return this.ctx.effect(\n () => {\n list.push(entry)\n this.refreshLog()\n return () => {\n const at = list.indexOf(entry)\n if (at >= 0) list.splice(at, 1)\n this.refreshLog()\n }\n },\n `${this.name}.register(${JSON.stringify(hook)}, ${JSON.stringify(phase)}, ${JSON.stringify(entry.name)})`,\n )\n }\n\n /**\n * Compute the ordered participant names for one hook phase without running\n * them. Reflects current registrations; useful for tests and diagnostics.\n *\n * @throws {HookControlError} when the hook has not been controlled.\n */\n plan(hook: string, phase: Phase): string[] {\n const controlled = this.hooks.get(hook)\n if (controlled === undefined) throw new HookControlError(`hook ${JSON.stringify(hook)} is not controlled`)\n return topoSort(controlled[phase]).map((entry) => entry.name)\n }\n\n /**\n * Serialize the constraint DAG of every controlled hook (all phases) as\n * pretty-printed JSON. Pure read: does not write the log file or throw on\n * cycles — the graph is most useful precisely when constraints conflict.\n */\n dumpDag(): string {\n return JSON.stringify(buildDag(this.dagSections()), null, 2)\n }\n\n /** Flatten every controlled hook into its `front`/`back` DAG sections. */\n protected dagSections(): DagSection[] {\n const sections: DagSection[] = []\n for (const [hook, controlled] of this.hooks) {\n sections.push({ hook, phase: 'front', entries: controlled.front })\n sections.push({ hook, phase: 'back', entries: controlled.back })\n }\n return sections\n }\n\n /**\n * Best-effort write of the current DAG to the configured `log` file. Never\n * throws: a failure (e.g. unwritable path) is reported via `console.warn`,\n * because this runs inside registration effects and disposers, where an\n * exception would corrupt fiber teardown.\n */\n protected refreshLog(): void {\n if (this.log === undefined) return\n try {\n writeFileSync(this.log, `${this.dumpDag()}\\n`)\n } catch (error) {\n console.warn(`hooks-ordering: failed to write DAG log to ${JSON.stringify(this.log)}:`, error)\n }\n }\n}\n"],"mappings":";;;;;;;AAwBA,IAAa,mBAAb,cAAsC,MAAM;;;;CAI1C,YAAY,SAAiB;AAC3B,QAAM,mBAAmB,UAAU;AACnC,OAAK,OAAO;;;;;;;;;AA2BhB,IAAsB,mBAAtB,cAAoE,QAAQ;CAC1E,AAAmB,wBAAQ,IAAI,KAAgC;;CAE/D,AAAS;;;;;;;CAQT,YAAY,KAAc,MAAc,SAAuC,EAAE,EAAE;AACjF,QAAM,KAAK,KAAK;AAChB,OAAK,MAAM,QAAQ;;;;;;;;;;;CAkBrB,QAAQ,MAA0B;AAChC,MAAI,KAAK,MAAM,IAAI,KAAK,CAAE,OAAM,IAAI,iBAAiB,QAAQ,KAAK,UAAU,KAAK,CAAC,wBAAwB;EAE1G,MAAMA,QAAa,EAAE;EACrB,MAAMC,OAAY,EAAE;EACpB,MAAM,kBAAkB,KAAK,QAAQ,MAAM,OAAO,KAAK;EAEvD,MAAM,gBAAsB;AAC1B,oBAAiB;AACjB,QAAK,MAAM,OAAO,KAAK;AACvB,QAAK,YAAY;;AAEnB,OAAK,MAAM,IAAI,MAAM;GAAE;GAAO;GAAM;GAAS,CAAC;AAC9C,OAAK,YAAY;AACjB,SAAO;;;;;;;;;CAUT,AAAU,cAAc,MAAc,OAAc,OAAsB;EACxE,MAAM,aAAa,KAAK,MAAM,IAAI,KAAK;AACvC,MAAI,eAAe,OACjB,OAAM,IAAI,iBACR,QAAQ,KAAK,UAAU,KAAK,CAAC,mCAAmC,KAAK,UAAU,KAAK,CAAC,SACtF;EACH,MAAM,OAAO,WAAW;AACxB,SAAO,KAAK,IAAI,aACR;AACJ,QAAK,KAAK,MAAM;AAChB,QAAK,YAAY;AACjB,gBAAa;IACX,MAAM,KAAK,KAAK,QAAQ,MAAM;AAC9B,QAAI,MAAM,EAAG,MAAK,OAAO,IAAI,EAAE;AAC/B,SAAK,YAAY;;KAGrB,GAAG,KAAK,KAAK,YAAY,KAAK,UAAU,KAAK,CAAC,IAAI,KAAK,UAAU,MAAM,CAAC,IAAI,KAAK,UAAU,MAAM,KAAK,CAAC,GACxG;;;;;;;;CASH,KAAK,MAAc,OAAwB;EACzC,MAAM,aAAa,KAAK,MAAM,IAAI,KAAK;AACvC,MAAI,eAAe,OAAW,OAAM,IAAI,iBAAiB,QAAQ,KAAK,UAAU,KAAK,CAAC,oBAAoB;AAC1G,SAAO,SAAS,WAAW,OAAO,CAAC,KAAK,UAAU,MAAM,KAAK;;;;;;;CAQ/D,UAAkB;AAChB,SAAO,KAAK,UAAU,SAAS,KAAK,aAAa,CAAC,EAAE,MAAM,EAAE;;;CAI9D,AAAU,cAA4B;EACpC,MAAMC,WAAyB,EAAE;AACjC,OAAK,MAAM,CAAC,MAAM,eAAe,KAAK,OAAO;AAC3C,YAAS,KAAK;IAAE;IAAM,OAAO;IAAS,SAAS,WAAW;IAAO,CAAC;AAClE,YAAS,KAAK;IAAE;IAAM,OAAO;IAAQ,SAAS,WAAW;IAAM,CAAC;;AAElE,SAAO;;;;;;;;CAST,AAAU,aAAmB;AAC3B,MAAI,KAAK,QAAQ,OAAW;AAC5B,MAAI;AACF,iBAAc,KAAK,KAAK,GAAG,KAAK,SAAS,CAAC,IAAI;WACvC,OAAO;AACd,WAAQ,KAAK,8CAA8C,KAAK,UAAU,KAAK,IAAI,CAAC,IAAI,MAAM"}
@@ -0,0 +1,4 @@
1
+ import "./topo-sort-BZ1fFcTs.js";
2
+ import "./dag-Bqx-sl71.js";
3
+ import { a as Phase, i as HookOrderingLogConfig, n as HookControlError, r as HookOrderingBase, t as ControlledHook } from "./service-base-CCmIBwnB.js";
4
+ export { ControlledHook, HookControlError, HookOrderingBase, HookOrderingLogConfig, Phase };
@@ -0,0 +1,5 @@
1
+ import "./dag-DVhoBjBG.js";
2
+ import "./topo-sort-CfwYPY4U.js";
3
+ import { n as HookOrderingBase, t as HookControlError } from "./service-base-a5vKg62S.js";
4
+
5
+ export { HookControlError, HookOrderingBase };
@@ -0,0 +1,54 @@
1
+ //#region src/topo-sort.d.ts
2
+ /**
3
+ * Deterministic topological sort for named entries carrying `before`/`after`
4
+ * ordering constraints. Zero runtime dependencies: the sort is a pure function
5
+ * of its input, independent of any Cordis context or plugin load order.
6
+ * @module dsh-hooks-ordering/topo-sort
7
+ */
8
+ /** An entry that can be ordered relative to others by name. */
9
+ interface Orderable {
10
+ /** Unique identifier within one hook phase. Referenced by other entries' `before`/`after`. */
11
+ readonly name: string;
12
+ /** Names this entry must run before. An unknown name imposes no constraint (see {@link topoSort}). */
13
+ readonly before?: readonly string[];
14
+ /** Names this entry must run after. An unknown name imposes no constraint (see {@link topoSort}). */
15
+ readonly after?: readonly string[];
16
+ }
17
+ /** Thrown when `before`/`after` constraints form a cycle, so no total order exists. */
18
+ declare class OrderingCycleError extends Error {
19
+ /** The names still blocked when the sort stalled — the entries on and behind the cycle. */
20
+ readonly cycle: readonly string[];
21
+ /**
22
+ * @param cycle - the names left unresolved by the cycle.
23
+ */
24
+ constructor(cycle: readonly string[]);
25
+ }
26
+ /** Thrown when two entries in one phase share a `name`, which would make references ambiguous. */
27
+ declare class DuplicateNameError extends Error {
28
+ /** The duplicated entry name. */
29
+ readonly duplicate: string;
30
+ /**
31
+ * @param duplicate - the name registered more than once.
32
+ */
33
+ constructor(duplicate: string);
34
+ }
35
+ /**
36
+ * Order entries so every `after` target precedes the entry and every `before`
37
+ * target follows it. Ties (entries with no constraint between them) keep their
38
+ * input order, so the result is stable and independent of registration timing.
39
+ *
40
+ * An unknown name in `before`/`after` (no registered entry owns it) imposes no
41
+ * constraint rather than failing: cross-vendor entries reference optional peers
42
+ * that may not be loaded, so a missing target is a legitimate no-op, not a
43
+ * misconfiguration. A cycle among present entries is fatal — it has no valid
44
+ * order — and throws {@link OrderingCycleError}.
45
+ *
46
+ * @param entries - the entries to order; each `name` must be unique.
47
+ * @returns a new array of the same entries in a constraint-respecting order.
48
+ * @throws {DuplicateNameError} when two entries share a `name`.
49
+ * @throws {OrderingCycleError} when present entries form an ordering cycle.
50
+ */
51
+ declare function topoSort<T extends Orderable>(entries: readonly T[]): T[];
52
+ //#endregion
53
+ export { topoSort as i, Orderable as n, OrderingCycleError as r, DuplicateNameError as t };
54
+ //# sourceMappingURL=topo-sort-BZ1fFcTs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"topo-sort-BZ1fFcTs.d.ts","names":[],"sources":["../src/topo-sort.ts"],"sourcesContent":[],"mappings":";;AAQA;AAUA;AAcA;AA6BA;;;AAAsE,UArDrD,SAAA,CAqDqD;EAAC;;;;;;;;cA3C1D,kBAAA,SAA2B,KAAA;;;;;;;;;cAc3B,kBAAA,SAA2B,KAAA;;;;;;;;;;;;;;;;;;;;;;;;iBA6BxB,mBAAmB,6BAA6B,MAAM"}
@@ -0,0 +1,83 @@
1
+ //#region src/topo-sort.ts
2
+ /** Thrown when `before`/`after` constraints form a cycle, so no total order exists. */
3
+ var OrderingCycleError = class extends Error {
4
+ /** The names still blocked when the sort stalled — the entries on and behind the cycle. */
5
+ cycle;
6
+ /**
7
+ * @param cycle - the names left unresolved by the cycle.
8
+ */
9
+ constructor(cycle) {
10
+ super(`hooks-ordering: constraints form a cycle among: ${cycle.join(", ")}`);
11
+ this.name = "OrderingCycleError";
12
+ this.cycle = cycle;
13
+ }
14
+ };
15
+ /** Thrown when two entries in one phase share a `name`, which would make references ambiguous. */
16
+ var DuplicateNameError = class extends Error {
17
+ /** The duplicated entry name. */
18
+ duplicate;
19
+ /**
20
+ * @param duplicate - the name registered more than once.
21
+ */
22
+ constructor(duplicate) {
23
+ super(`hooks-ordering: duplicate entry name ${JSON.stringify(duplicate)}`);
24
+ this.name = "DuplicateNameError";
25
+ this.duplicate = duplicate;
26
+ }
27
+ };
28
+ /**
29
+ * Order entries so every `after` target precedes the entry and every `before`
30
+ * target follows it. Ties (entries with no constraint between them) keep their
31
+ * input order, so the result is stable and independent of registration timing.
32
+ *
33
+ * An unknown name in `before`/`after` (no registered entry owns it) imposes no
34
+ * constraint rather than failing: cross-vendor entries reference optional peers
35
+ * that may not be loaded, so a missing target is a legitimate no-op, not a
36
+ * misconfiguration. A cycle among present entries is fatal — it has no valid
37
+ * order — and throws {@link OrderingCycleError}.
38
+ *
39
+ * @param entries - the entries to order; each `name` must be unique.
40
+ * @returns a new array of the same entries in a constraint-respecting order.
41
+ * @throws {DuplicateNameError} when two entries share a `name`.
42
+ * @throws {OrderingCycleError} when present entries form an ordering cycle.
43
+ */
44
+ function topoSort(entries) {
45
+ const index = /* @__PURE__ */ new Map();
46
+ entries.forEach((entry, position) => {
47
+ if (index.has(entry.name)) throw new DuplicateNameError(entry.name);
48
+ index.set(entry.name, position);
49
+ });
50
+ const successors = entries.map(() => []);
51
+ const indegree = entries.map(() => 0);
52
+ const addEdge = (fromName, toName) => {
53
+ const from = index.get(fromName);
54
+ const to = index.get(toName);
55
+ if (from === void 0 || to === void 0 || from === to) return;
56
+ successors[from].push(to);
57
+ indegree[to]++;
58
+ };
59
+ entries.forEach((entry) => {
60
+ for (const target of entry.after ?? []) addEdge(target, entry.name);
61
+ for (const target of entry.before ?? []) addEdge(entry.name, target);
62
+ });
63
+ const ready = [];
64
+ const pushReady = (node) => {
65
+ ready.push(node);
66
+ ready.sort((left, right) => left - right);
67
+ };
68
+ indegree.forEach((count, node) => {
69
+ if (count === 0) pushReady(node);
70
+ });
71
+ const result = [];
72
+ while (ready.length > 0) {
73
+ const node = ready.shift();
74
+ result.push(entries[node]);
75
+ for (const successor of successors[node]) if (--indegree[successor] === 0) pushReady(successor);
76
+ }
77
+ if (result.length !== entries.length) throw new OrderingCycleError(entries.filter((_, node) => indegree[node] > 0).map((entry) => entry.name));
78
+ return result;
79
+ }
80
+
81
+ //#endregion
82
+ export { OrderingCycleError as n, topoSort as r, DuplicateNameError as t };
83
+ //# sourceMappingURL=topo-sort-CfwYPY4U.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"topo-sort-CfwYPY4U.js","names":["successors: number[][]","indegree: number[]","ready: number[]","result: T[]"],"sources":["../src/topo-sort.ts"],"sourcesContent":["/**\n * Deterministic topological sort for named entries carrying `before`/`after`\n * ordering constraints. Zero runtime dependencies: the sort is a pure function\n * of its input, independent of any Cordis context or plugin load order.\n * @module dsh-hooks-ordering/topo-sort\n */\n\n/** An entry that can be ordered relative to others by name. */\nexport interface Orderable {\n /** Unique identifier within one hook phase. Referenced by other entries' `before`/`after`. */\n readonly name: string\n /** Names this entry must run before. An unknown name imposes no constraint (see {@link topoSort}). */\n readonly before?: readonly string[]\n /** Names this entry must run after. An unknown name imposes no constraint (see {@link topoSort}). */\n readonly after?: readonly string[]\n}\n\n/** Thrown when `before`/`after` constraints form a cycle, so no total order exists. */\nexport class OrderingCycleError extends Error {\n /** The names still blocked when the sort stalled — the entries on and behind the cycle. */\n readonly cycle: readonly string[]\n /**\n * @param cycle - the names left unresolved by the cycle.\n */\n constructor(cycle: readonly string[]) {\n super(`hooks-ordering: constraints form a cycle among: ${cycle.join(', ')}`)\n this.name = 'OrderingCycleError'\n this.cycle = cycle\n }\n}\n\n/** Thrown when two entries in one phase share a `name`, which would make references ambiguous. */\nexport class DuplicateNameError extends Error {\n /** The duplicated entry name. */\n readonly duplicate: string\n /**\n * @param duplicate - the name registered more than once.\n */\n constructor(duplicate: string) {\n super(`hooks-ordering: duplicate entry name ${JSON.stringify(duplicate)}`)\n this.name = 'DuplicateNameError'\n this.duplicate = duplicate\n }\n}\n\n/**\n * Order entries so every `after` target precedes the entry and every `before`\n * target follows it. Ties (entries with no constraint between them) keep their\n * input order, so the result is stable and independent of registration timing.\n *\n * An unknown name in `before`/`after` (no registered entry owns it) imposes no\n * constraint rather than failing: cross-vendor entries reference optional peers\n * that may not be loaded, so a missing target is a legitimate no-op, not a\n * misconfiguration. A cycle among present entries is fatal — it has no valid\n * order — and throws {@link OrderingCycleError}.\n *\n * @param entries - the entries to order; each `name` must be unique.\n * @returns a new array of the same entries in a constraint-respecting order.\n * @throws {DuplicateNameError} when two entries share a `name`.\n * @throws {OrderingCycleError} when present entries form an ordering cycle.\n */\nexport function topoSort<T extends Orderable>(entries: readonly T[]): T[] {\n const index = new Map<string, number>()\n entries.forEach((entry, position) => {\n if (index.has(entry.name)) throw new DuplicateNameError(entry.name)\n index.set(entry.name, position)\n })\n\n // successors[i] = entries that must run after entry i. indegree[i] = number\n // of entries that must run before entry i.\n const successors: number[][] = entries.map(() => [])\n const indegree: number[] = entries.map(() => 0)\n\n const addEdge = (fromName: string, toName: string): void => {\n const from = index.get(fromName)\n const to = index.get(toName)\n // Unknown endpoint: the referenced peer is not loaded, so no ordering\n // relation exists to enforce.\n if (from === undefined || to === undefined || from === to) return\n successors[from]!.push(to)\n indegree[to]!++\n }\n\n entries.forEach((entry) => {\n for (const target of entry.after ?? []) addEdge(target, entry.name)\n for (const target of entry.before ?? []) addEdge(entry.name, target)\n })\n\n // Kahn's algorithm. The ready set holds nodes with no unmet predecessor; it\n // is kept in ascending input position so equal candidates emit in input\n // order, giving a deterministic, stable result. Per-phase entry counts are\n // small, so a sort on each insertion is simpler than a hand-rolled heap.\n const ready: number[] = []\n const pushReady = (node: number): void => {\n ready.push(node)\n ready.sort((left, right) => left - right)\n }\n indegree.forEach((count, node) => {\n if (count === 0) pushReady(node)\n })\n\n const result: T[] = []\n while (ready.length > 0) {\n const node = ready.shift()!\n result.push(entries[node]!)\n for (const successor of successors[node]!) {\n if (--indegree[successor]! === 0) pushReady(successor)\n }\n }\n\n if (result.length !== entries.length) {\n // The entries still carrying an unmet predecessor are exactly those on or\n // behind the cycle; naming them all is more useful than one arbitrary loop.\n const blocked = entries.filter((_, node) => indegree[node]! > 0).map((entry) => entry.name)\n throw new OrderingCycleError(blocked)\n }\n return result\n}\n"],"mappings":";;AAkBA,IAAa,qBAAb,cAAwC,MAAM;;CAE5C,AAAS;;;;CAIT,YAAY,OAA0B;AACpC,QAAM,mDAAmD,MAAM,KAAK,KAAK,GAAG;AAC5E,OAAK,OAAO;AACZ,OAAK,QAAQ;;;;AAKjB,IAAa,qBAAb,cAAwC,MAAM;;CAE5C,AAAS;;;;CAIT,YAAY,WAAmB;AAC7B,QAAM,wCAAwC,KAAK,UAAU,UAAU,GAAG;AAC1E,OAAK,OAAO;AACZ,OAAK,YAAY;;;;;;;;;;;;;;;;;;;AAoBrB,SAAgB,SAA8B,SAA4B;CACxE,MAAM,wBAAQ,IAAI,KAAqB;AACvC,SAAQ,SAAS,OAAO,aAAa;AACnC,MAAI,MAAM,IAAI,MAAM,KAAK,CAAE,OAAM,IAAI,mBAAmB,MAAM,KAAK;AACnE,QAAM,IAAI,MAAM,MAAM,SAAS;GAC/B;CAIF,MAAMA,aAAyB,QAAQ,UAAU,EAAE,CAAC;CACpD,MAAMC,WAAqB,QAAQ,UAAU,EAAE;CAE/C,MAAM,WAAW,UAAkB,WAAyB;EAC1D,MAAM,OAAO,MAAM,IAAI,SAAS;EAChC,MAAM,KAAK,MAAM,IAAI,OAAO;AAG5B,MAAI,SAAS,UAAa,OAAO,UAAa,SAAS,GAAI;AAC3D,aAAW,MAAO,KAAK,GAAG;AAC1B,WAAS;;AAGX,SAAQ,SAAS,UAAU;AACzB,OAAK,MAAM,UAAU,MAAM,SAAS,EAAE,CAAE,SAAQ,QAAQ,MAAM,KAAK;AACnE,OAAK,MAAM,UAAU,MAAM,UAAU,EAAE,CAAE,SAAQ,MAAM,MAAM,OAAO;GACpE;CAMF,MAAMC,QAAkB,EAAE;CAC1B,MAAM,aAAa,SAAuB;AACxC,QAAM,KAAK,KAAK;AAChB,QAAM,MAAM,MAAM,UAAU,OAAO,MAAM;;AAE3C,UAAS,SAAS,OAAO,SAAS;AAChC,MAAI,UAAU,EAAG,WAAU,KAAK;GAChC;CAEF,MAAMC,SAAc,EAAE;AACtB,QAAO,MAAM,SAAS,GAAG;EACvB,MAAM,OAAO,MAAM,OAAO;AAC1B,SAAO,KAAK,QAAQ,MAAO;AAC3B,OAAK,MAAM,aAAa,WAAW,MACjC,KAAI,EAAE,SAAS,eAAgB,EAAG,WAAU,UAAU;;AAI1D,KAAI,OAAO,WAAW,QAAQ,OAI5B,OAAM,IAAI,mBADM,QAAQ,QAAQ,GAAG,SAAS,SAAS,QAAS,EAAE,CAAC,KAAK,UAAU,MAAM,KAAK,CACtD;AAEvC,QAAO"}
@@ -0,0 +1,2 @@
1
+ import { i as topoSort, n as Orderable, r as OrderingCycleError, t as DuplicateNameError } from "./topo-sort-BZ1fFcTs.js";
2
+ export { DuplicateNameError, Orderable, OrderingCycleError, topoSort };
@@ -0,0 +1,3 @@
1
+ import { n as OrderingCycleError, r as topoSort, t as DuplicateNameError } from "./topo-sort-CfwYPY4U.js";
2
+
3
+ export { DuplicateNameError, OrderingCycleError, topoSort };
@@ -0,0 +1,83 @@
1
+ import { r as topoSort } from "./topo-sort-CfwYPY4U.js";
2
+ import { n as HookOrderingBase, t as HookControlError } from "./service-base-a5vKg62S.js";
3
+
4
+ //#region src/waterfall.ts
5
+ /**
6
+ * Coordinator service registered at `ctx.hooksOrdering`. One instance controls
7
+ * any number of waterfall hooks; each controlled hook owns one bracket listener
8
+ * and two ordered participant lists.
9
+ */
10
+ var HookOrdering = class extends HookOrderingBase {
11
+ /**
12
+ * Hooks that must never carry a participant: their return value is consumed
13
+ * synchronously by the host, so the bracket's await would corrupt it.
14
+ * @see HookOrderingConfig.syncReturnHooks
15
+ */
16
+ syncReturnHooks;
17
+ /**
18
+ * @param ctx - the Cordis context to register the service in.
19
+ * @param config - optional `log` file for the constraint DAG (JSON) and the
20
+ * `syncReturnHooks` admission list; `null` means defaults.
21
+ */
22
+ constructor(ctx, config = {}) {
23
+ super(ctx, "hooksOrdering", config);
24
+ this.syncReturnHooks = new Set(config?.syncReturnHooks ?? []);
25
+ }
26
+ /**
27
+ * Install the single bracket listener. `prepend` places it first among
28
+ * current listeners so its `next()` encloses the rest of the native chain;
29
+ * the listener is an effect on this service's fiber and is removed with it or
30
+ * with the disposer {@link control} returns.
31
+ *
32
+ * The bracket is deliberately NOT `async` at its outermost level. Cordis'
33
+ * `waterfall` returns the outermost listener's value *synchronously*, so an
34
+ * `async` bracket would turn the hook's return value into a Promise for every
35
+ * caller — breaking any hook whose result the caller consumes without
36
+ * awaiting (dsh's `llm/stream` returns an AsyncIterable, and wraps with
37
+ * "not async iterable"). While both phases are empty there is nothing to
38
+ * order, so the chain is handed straight through and the hook's return type
39
+ * is exactly what it was before control was taken.
40
+ */
41
+ install(hook, front, back) {
42
+ const bracket = (...args) => {
43
+ const next = args[args.length - 1];
44
+ if (front.length === 0 && back.length === 0) return next();
45
+ const payload = args.slice(0, -1);
46
+ return (async () => {
47
+ await runPhase(front, payload);
48
+ const result = await next();
49
+ await runPhase(back, payload);
50
+ return result;
51
+ })();
52
+ };
53
+ return this.ctx.on(hook, bracket, { prepend: true });
54
+ }
55
+ /**
56
+ * Register a participant into a controlled hook phase.
57
+ *
58
+ * @param hook - the controlled waterfall event name.
59
+ * @param phase - `front` to run ahead of the native chain, `back` to run behind it.
60
+ * @param entry - the participant, with optional `before`/`after` names and its `run` callback.
61
+ * @returns a disposer that unregisters this participant.
62
+ * @throws {HookControlError} when the hook has not been {@link control}led, or
63
+ * when it is declared in `syncReturnHooks` — there the participant would work
64
+ * but the host's return value would change type, which is the worse failure.
65
+ */
66
+ register(hook, phase, entry) {
67
+ if (this.syncReturnHooks.has(hook)) throw new HookControlError(`hook ${JSON.stringify(hook)} returns to a caller that does not await it, so ordering participants would turn its return value into a Promise and break that caller; remove ${JSON.stringify(hook)} from syncReturnHooks only once the host awaits it`);
68
+ return this.registerEntry(hook, phase, entry);
69
+ }
70
+ };
71
+ /**
72
+ * Run one phase's participants in stable topological order, awaiting each.
73
+ * @param entries - the phase's registered participants.
74
+ * @param payload - the hook payload passed to each `run` callback.
75
+ */
76
+ async function runPhase(entries, payload) {
77
+ for (const entry of topoSort(entries)) await entry.run(...payload);
78
+ }
79
+ var waterfall_default = HookOrdering;
80
+
81
+ //#endregion
82
+ export { waterfall_default as n, HookOrdering as t };
83
+ //# sourceMappingURL=waterfall-Bu6m9gYc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"waterfall-Bu6m9gYc.js","names":[],"sources":["../src/waterfall.ts"],"sourcesContent":["/**\n * `HookOrdering` — deterministic before/after ordering for Cordis waterfall\n * hooks whose participants are contributed by independent plugins.\n *\n * Cordis runs waterfall listeners in registration order (array position, with\n * `prepend` as the only lever), and registration order is driven by\n * inject-dependency activation — non-deterministic between unrelated plugins.\n * A plugin therefore cannot reliably say \"run me after that other plugin\",\n * especially across vendors that do not depend on each other.\n *\n * This service brackets a chosen waterfall hook with ONE prepended listener\n * that exploits the onion model: code before its `next()` runs ahead of the\n * whole native chain, code after runs behind it. Participants register into\n * this coordinator instead of the raw hook, declaring `before`/`after` names,\n * and the coordinator runs them in a stable topological order it fully\n * controls. The serial-dispatch twin lives in `./serial.ts`.\n *\n * @module dsh-hooks-ordering/waterfall\n */\n\nimport { type Context } from '@deepseek-ai/cordis'\nimport { HookControlError, HookOrderingBase, type HookOrderingLogConfig, type Phase } from './service-base.ts'\nimport { type Orderable, topoSort } from './topo-sort.ts'\n\n/**\n * One ordered participant in a controlled waterfall hook phase.\n * @typeParam A - the hook's payload argument tuple (the dispatched args without Cordis' trailing `next`).\n */\nexport interface HookEntry<A extends readonly unknown[] = readonly unknown[]> extends Orderable {\n /** Run this participant with the hook payload. Awaited before the phase proceeds. */\n readonly run: (...args: A) => void | Promise<void>\n}\n\n/** Configuration for the {@link HookOrdering} service. */\nexport interface HookOrderingConfig extends HookOrderingLogConfig {\n /**\n * Hook names whose dispatch return value the CALLER consumes without\n * awaiting — dsh's `llm/stream` (an AsyncIterable iterated directly) and\n * `session-telemetry/record` (a record handed straight to a backend) are the\n * two in the shipped build.\n *\n * A participant cannot be ordered on such a hook. The bracket only preserves\n * the native return value while both phases are empty; with a participant it\n * must await, so it returns a Promise where the caller expects a value — a\n * broken stream or a corrupted record, silently. {@link HookOrdering.register}\n * therefore refuses those hooks, and *controlling* them stays allowed: the\n * bracket is a transparent pass-through and may become useful the day the\n * host starts awaiting that hook (then pass `[]` here, or a set without it).\n */\n readonly syncReturnHooks?: readonly string[]\n}\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n hooksOrdering: HookOrdering\n }\n}\n\n/**\n * Coordinator service registered at `ctx.hooksOrdering`. One instance controls\n * any number of waterfall hooks; each controlled hook owns one bracket listener\n * and two ordered participant lists.\n */\nexport class HookOrdering extends HookOrderingBase<HookEntry> {\n /**\n * Hooks that must never carry a participant: their return value is consumed\n * synchronously by the host, so the bracket's await would corrupt it.\n * @see HookOrderingConfig.syncReturnHooks\n */\n private readonly syncReturnHooks: ReadonlySet<string>\n\n /**\n * @param ctx - the Cordis context to register the service in.\n * @param config - optional `log` file for the constraint DAG (JSON) and the\n * `syncReturnHooks` admission list; `null` means defaults.\n */\n constructor(ctx: Context, config: HookOrderingConfig | null = {}) {\n super(ctx, 'hooksOrdering', config)\n this.syncReturnHooks = new Set(config?.syncReturnHooks ?? [])\n }\n\n /**\n * Install the single bracket listener. `prepend` places it first among\n * current listeners so its `next()` encloses the rest of the native chain;\n * the listener is an effect on this service's fiber and is removed with it or\n * with the disposer {@link control} returns.\n *\n * The bracket is deliberately NOT `async` at its outermost level. Cordis'\n * `waterfall` returns the outermost listener's value *synchronously*, so an\n * `async` bracket would turn the hook's return value into a Promise for every\n * caller — breaking any hook whose result the caller consumes without\n * awaiting (dsh's `llm/stream` returns an AsyncIterable, and wraps with\n * \"not async iterable\"). While both phases are empty there is nothing to\n * order, so the chain is handed straight through and the hook's return type\n * is exactly what it was before control was taken.\n */\n protected install(hook: string, front: HookEntry[], back: HookEntry[]): () => void {\n const bracket = (...args: unknown[]): unknown => {\n const next = args[args.length - 1] as () => unknown\n // Transparent fast path: no participants, so no await is needed and the\n // native return value (promise or not) passes through unchanged.\n if (front.length === 0 && back.length === 0) return next()\n const payload = args.slice(0, -1)\n // With participants the phases must be awaited, so the bracket can only\n // return a promise from here on — inherent to ordering, not a choice.\n return (async (): Promise<unknown> => {\n await runPhase(front, payload)\n const result = await next()\n await runPhase(back, payload)\n return result\n })()\n }\n return this.ctx.on(hook as never, bracket as never, { prepend: true })\n }\n\n /**\n * Register a participant into a controlled hook phase.\n *\n * @param hook - the controlled waterfall event name.\n * @param phase - `front` to run ahead of the native chain, `back` to run behind it.\n * @param entry - the participant, with optional `before`/`after` names and its `run` callback.\n * @returns a disposer that unregisters this participant.\n * @throws {HookControlError} when the hook has not been {@link control}led, or\n * when it is declared in `syncReturnHooks` — there the participant would work\n * but the host's return value would change type, which is the worse failure.\n */\n register<A extends readonly unknown[] = readonly unknown[]>(\n hook: string,\n phase: Phase,\n entry: HookEntry<A>,\n ): () => void {\n if (this.syncReturnHooks.has(hook)) {\n throw new HookControlError(\n `hook ${JSON.stringify(hook)} returns to a caller that does not await it, so ordering participants would turn its return value into a Promise and break that caller; ` +\n `remove ${JSON.stringify(hook)} from syncReturnHooks only once the host awaits it`,\n )\n }\n return this.registerEntry(hook, phase, entry as unknown as HookEntry)\n }\n}\n\n/**\n * Run one phase's participants in stable topological order, awaiting each.\n * @param entries - the phase's registered participants.\n * @param payload - the hook payload passed to each `run` callback.\n */\nasync function runPhase(entries: readonly HookEntry[], payload: readonly unknown[]): Promise<void> {\n for (const entry of topoSort(entries)) {\n await entry.run(...payload)\n }\n}\n\nexport default HookOrdering\n"],"mappings":";;;;;;;;;AA+DA,IAAa,eAAb,cAAkC,iBAA4B;;;;;;CAM5D,AAAiB;;;;;;CAOjB,YAAY,KAAc,SAAoC,EAAE,EAAE;AAChE,QAAM,KAAK,iBAAiB,OAAO;AACnC,OAAK,kBAAkB,IAAI,IAAI,QAAQ,mBAAmB,EAAE,CAAC;;;;;;;;;;;;;;;;;CAkB/D,AAAU,QAAQ,MAAc,OAAoB,MAA+B;EACjF,MAAM,WAAW,GAAG,SAA6B;GAC/C,MAAM,OAAO,KAAK,KAAK,SAAS;AAGhC,OAAI,MAAM,WAAW,KAAK,KAAK,WAAW,EAAG,QAAO,MAAM;GAC1D,MAAM,UAAU,KAAK,MAAM,GAAG,GAAG;AAGjC,WAAQ,YAA8B;AACpC,UAAM,SAAS,OAAO,QAAQ;IAC9B,MAAM,SAAS,MAAM,MAAM;AAC3B,UAAM,SAAS,MAAM,QAAQ;AAC7B,WAAO;OACL;;AAEN,SAAO,KAAK,IAAI,GAAG,MAAe,SAAkB,EAAE,SAAS,MAAM,CAAC;;;;;;;;;;;;;CAcxE,SACE,MACA,OACA,OACY;AACZ,MAAI,KAAK,gBAAgB,IAAI,KAAK,CAChC,OAAM,IAAI,iBACR,QAAQ,KAAK,UAAU,KAAK,CAAC,iJACjB,KAAK,UAAU,KAAK,CAAC,oDAClC;AAEH,SAAO,KAAK,cAAc,MAAM,OAAO,MAA8B;;;;;;;;AASzE,eAAe,SAAS,SAA+B,SAA4C;AACjG,MAAK,MAAM,SAAS,SAAS,QAAQ,CACnC,OAAM,MAAM,IAAI,GAAG,QAAQ;;AAI/B,wBAAe"}
@@ -0,0 +1,87 @@
1
+ import { n as Orderable } from "./topo-sort-BZ1fFcTs.js";
2
+ import { a as Phase, i as HookOrderingLogConfig, r as HookOrderingBase } from "./service-base-CCmIBwnB.js";
3
+ import { Context } from "@deepseek-ai/cordis";
4
+
5
+ //#region src/waterfall.d.ts
6
+
7
+ /**
8
+ * One ordered participant in a controlled waterfall hook phase.
9
+ * @typeParam A - the hook's payload argument tuple (the dispatched args without Cordis' trailing `next`).
10
+ */
11
+ interface HookEntry<A extends readonly unknown[] = readonly unknown[]> extends Orderable {
12
+ /** Run this participant with the hook payload. Awaited before the phase proceeds. */
13
+ readonly run: (...args: A) => void | Promise<void>;
14
+ }
15
+ /** Configuration for the {@link HookOrdering} service. */
16
+ interface HookOrderingConfig extends HookOrderingLogConfig {
17
+ /**
18
+ * Hook names whose dispatch return value the CALLER consumes without
19
+ * awaiting — dsh's `llm/stream` (an AsyncIterable iterated directly) and
20
+ * `session-telemetry/record` (a record handed straight to a backend) are the
21
+ * two in the shipped build.
22
+ *
23
+ * A participant cannot be ordered on such a hook. The bracket only preserves
24
+ * the native return value while both phases are empty; with a participant it
25
+ * must await, so it returns a Promise where the caller expects a value — a
26
+ * broken stream or a corrupted record, silently. {@link HookOrdering.register}
27
+ * therefore refuses those hooks, and *controlling* them stays allowed: the
28
+ * bracket is a transparent pass-through and may become useful the day the
29
+ * host starts awaiting that hook (then pass `[]` here, or a set without it).
30
+ */
31
+ readonly syncReturnHooks?: readonly string[];
32
+ }
33
+ declare module '@deepseek-ai/cordis' {
34
+ interface Context {
35
+ hooksOrdering: HookOrdering;
36
+ }
37
+ }
38
+ /**
39
+ * Coordinator service registered at `ctx.hooksOrdering`. One instance controls
40
+ * any number of waterfall hooks; each controlled hook owns one bracket listener
41
+ * and two ordered participant lists.
42
+ */
43
+ declare class HookOrdering extends HookOrderingBase<HookEntry> {
44
+ /**
45
+ * Hooks that must never carry a participant: their return value is consumed
46
+ * synchronously by the host, so the bracket's await would corrupt it.
47
+ * @see HookOrderingConfig.syncReturnHooks
48
+ */
49
+ private readonly syncReturnHooks;
50
+ /**
51
+ * @param ctx - the Cordis context to register the service in.
52
+ * @param config - optional `log` file for the constraint DAG (JSON) and the
53
+ * `syncReturnHooks` admission list; `null` means defaults.
54
+ */
55
+ constructor(ctx: Context, config?: HookOrderingConfig | null);
56
+ /**
57
+ * Install the single bracket listener. `prepend` places it first among
58
+ * current listeners so its `next()` encloses the rest of the native chain;
59
+ * the listener is an effect on this service's fiber and is removed with it or
60
+ * with the disposer {@link control} returns.
61
+ *
62
+ * The bracket is deliberately NOT `async` at its outermost level. Cordis'
63
+ * `waterfall` returns the outermost listener's value *synchronously*, so an
64
+ * `async` bracket would turn the hook's return value into a Promise for every
65
+ * caller — breaking any hook whose result the caller consumes without
66
+ * awaiting (dsh's `llm/stream` returns an AsyncIterable, and wraps with
67
+ * "not async iterable"). While both phases are empty there is nothing to
68
+ * order, so the chain is handed straight through and the hook's return type
69
+ * is exactly what it was before control was taken.
70
+ */
71
+ protected install(hook: string, front: HookEntry[], back: HookEntry[]): () => void;
72
+ /**
73
+ * Register a participant into a controlled hook phase.
74
+ *
75
+ * @param hook - the controlled waterfall event name.
76
+ * @param phase - `front` to run ahead of the native chain, `back` to run behind it.
77
+ * @param entry - the participant, with optional `before`/`after` names and its `run` callback.
78
+ * @returns a disposer that unregisters this participant.
79
+ * @throws {HookControlError} when the hook has not been {@link control}led, or
80
+ * when it is declared in `syncReturnHooks` — there the participant would work
81
+ * but the host's return value would change type, which is the worse failure.
82
+ */
83
+ register<A extends readonly unknown[] = readonly unknown[]>(hook: string, phase: Phase, entry: HookEntry<A>): () => void;
84
+ }
85
+ //#endregion
86
+ export { HookOrdering as n, HookOrderingConfig as r, HookEntry as t };
87
+ //# sourceMappingURL=waterfall-_5HkptkS.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"waterfall-_5HkptkS.d.ts","names":[],"sources":["../src/waterfall.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AA+DkD,UAnCjC,SAmCiC,CAAA,UAAA,SAAA,OAAA,EAAA,GAAA,SAAA,OAAA,EAAA,CAAA,SAnCoC,SAmCpC,CAAA;;0BAjCxB,aAAa;;;UAItB,kBAAA,SAA2B;;;;;;;;;;;;;;;;;;;mBAoBzB;;;;;;;;cASN,YAAA,SAAqB,iBAAiB;;;;;;;;;;;;mBAahC,kBAAiB;;;;;;;;;;;;;;;;yCAoBK,mBAAmB;;;;;;;;;;;;mFAgCjD,cACA,UAAU"}
@@ -0,0 +1,5 @@
1
+ import "./topo-sort-BZ1fFcTs.js";
2
+ import "./dag-Bqx-sl71.js";
3
+ import "./service-base-CCmIBwnB.js";
4
+ import { n as HookOrdering, r as HookOrderingConfig, t as HookEntry } from "./waterfall-_5HkptkS.js";
5
+ export { HookEntry, HookOrdering, HookOrdering as default, HookOrderingConfig };
@@ -0,0 +1,6 @@
1
+ import "./dag-DVhoBjBG.js";
2
+ import "./topo-sort-CfwYPY4U.js";
3
+ import "./service-base-a5vKg62S.js";
4
+ import { n as waterfall_default, t as HookOrdering } from "./waterfall-Bu6m9gYc.js";
5
+
6
+ export { HookOrdering, waterfall_default as default };