@bluelibs/runner 4.6.1 → 4.7.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.
Files changed (156) hide show
  1. package/AI.md +319 -579
  2. package/README.md +885 -731
  3. package/dist/browser/index.cjs +1438 -251
  4. package/dist/browser/index.cjs.map +1 -1
  5. package/dist/browser/index.mjs +1433 -252
  6. package/dist/browser/index.mjs.map +1 -1
  7. package/dist/context.d.ts +31 -0
  8. package/dist/define.d.ts +9 -0
  9. package/dist/definers/builders/core.d.ts +30 -0
  10. package/dist/definers/builders/event.d.ts +12 -0
  11. package/dist/definers/builders/hook.d.ts +20 -0
  12. package/dist/definers/builders/middleware.d.ts +39 -0
  13. package/dist/definers/builders/resource.d.ts +40 -0
  14. package/dist/definers/builders/tag.d.ts +10 -0
  15. package/dist/definers/builders/task.d.ts +37 -0
  16. package/dist/definers/builders/task.phantom.d.ts +27 -0
  17. package/dist/definers/builders/utils.d.ts +4 -0
  18. package/dist/definers/defineEvent.d.ts +2 -0
  19. package/dist/definers/defineHook.d.ts +6 -0
  20. package/dist/definers/defineOverride.d.ts +17 -0
  21. package/dist/definers/defineResource.d.ts +2 -0
  22. package/dist/definers/defineResourceMiddleware.d.ts +2 -0
  23. package/dist/definers/defineTag.d.ts +12 -0
  24. package/dist/definers/defineTask.d.ts +18 -0
  25. package/dist/definers/defineTaskMiddleware.d.ts +2 -0
  26. package/dist/definers/tools.d.ts +47 -0
  27. package/dist/defs.d.ts +29 -0
  28. package/dist/edge/index.cjs +1438 -251
  29. package/dist/edge/index.cjs.map +1 -1
  30. package/dist/edge/index.mjs +1433 -252
  31. package/dist/edge/index.mjs.map +1 -1
  32. package/dist/errors.d.ts +104 -0
  33. package/dist/globals/globalEvents.d.ts +8 -0
  34. package/dist/globals/globalMiddleware.d.ts +31 -0
  35. package/dist/globals/globalResources.d.ts +32 -0
  36. package/dist/globals/globalTags.d.ts +11 -0
  37. package/dist/globals/middleware/cache.middleware.d.ts +27 -0
  38. package/dist/globals/middleware/requireContext.middleware.d.ts +6 -0
  39. package/dist/globals/middleware/retry.middleware.d.ts +21 -0
  40. package/dist/globals/middleware/timeout.middleware.d.ts +9 -0
  41. package/dist/globals/middleware/tunnel.middleware.d.ts +2 -0
  42. package/dist/globals/resources/debug/debug.resource.d.ts +7 -0
  43. package/dist/globals/resources/debug/debug.tag.d.ts +2 -0
  44. package/dist/globals/resources/debug/debugConfig.resource.d.ts +22 -0
  45. package/dist/globals/resources/debug/executionTracker.middleware.d.ts +50 -0
  46. package/dist/globals/resources/debug/globalEvent.hook.d.ts +27 -0
  47. package/dist/globals/resources/debug/hook.hook.d.ts +30 -0
  48. package/dist/globals/resources/debug/index.d.ts +6 -0
  49. package/dist/globals/resources/debug/middleware.hook.d.ts +30 -0
  50. package/dist/globals/resources/debug/types.d.ts +25 -0
  51. package/dist/globals/resources/debug/utils.d.ts +2 -0
  52. package/dist/globals/resources/queue.resource.d.ts +10 -0
  53. package/dist/globals/resources/tunnel/ejson-extensions.d.ts +1 -0
  54. package/dist/globals/resources/tunnel/error-utils.d.ts +1 -0
  55. package/dist/globals/resources/tunnel/plan.d.ts +19 -0
  56. package/dist/globals/resources/tunnel/protocol.d.ts +40 -0
  57. package/dist/globals/resources/tunnel/serializer.d.ts +9 -0
  58. package/dist/globals/resources/tunnel/tunnel.policy.tag.d.ts +18 -0
  59. package/dist/globals/resources/tunnel/tunnel.tag.d.ts +2 -0
  60. package/dist/globals/resources/tunnel/types.d.ts +17 -0
  61. package/dist/globals/tunnels/index.d.ts +23 -0
  62. package/dist/globals/types.d.ts +1 -0
  63. package/dist/http-client.d.ts +23 -0
  64. package/dist/http-fetch-tunnel.resource.d.ts +22 -0
  65. package/dist/index.d.ts +99 -0
  66. package/dist/models/DependencyProcessor.d.ts +48 -0
  67. package/dist/models/EventManager.d.ts +153 -0
  68. package/dist/models/LogPrinter.d.ts +55 -0
  69. package/dist/models/Logger.d.ts +85 -0
  70. package/dist/models/MiddlewareManager.d.ts +86 -0
  71. package/dist/models/OverrideManager.d.ts +13 -0
  72. package/dist/models/Queue.d.ts +26 -0
  73. package/dist/models/ResourceInitializer.d.ts +20 -0
  74. package/dist/models/RunResult.d.ts +35 -0
  75. package/dist/models/Semaphore.d.ts +61 -0
  76. package/dist/models/Store.d.ts +69 -0
  77. package/dist/models/StoreRegistry.d.ts +43 -0
  78. package/dist/models/StoreValidator.d.ts +8 -0
  79. package/dist/models/TaskRunner.d.ts +27 -0
  80. package/dist/models/UnhandledError.d.ts +11 -0
  81. package/dist/models/index.d.ts +11 -0
  82. package/dist/models/utils/findCircularDependencies.d.ts +16 -0
  83. package/dist/models/utils/safeStringify.d.ts +3 -0
  84. package/dist/node/exposure/allowList.d.ts +3 -0
  85. package/dist/node/exposure/authenticator.d.ts +6 -0
  86. package/dist/node/exposure/cors.d.ts +4 -0
  87. package/dist/node/exposure/createNodeExposure.d.ts +2 -0
  88. package/dist/node/exposure/exposureServer.d.ts +18 -0
  89. package/dist/node/exposure/httpResponse.d.ts +10 -0
  90. package/dist/node/exposure/logging.d.ts +4 -0
  91. package/dist/node/exposure/multipart.d.ts +27 -0
  92. package/dist/node/exposure/requestBody.d.ts +11 -0
  93. package/dist/node/exposure/requestContext.d.ts +17 -0
  94. package/dist/node/exposure/requestHandlers.d.ts +24 -0
  95. package/dist/node/exposure/resourceTypes.d.ts +60 -0
  96. package/dist/node/exposure/router.d.ts +17 -0
  97. package/dist/node/exposure/serverLifecycle.d.ts +13 -0
  98. package/dist/node/exposure/types.d.ts +31 -0
  99. package/dist/node/exposure/utils.d.ts +17 -0
  100. package/dist/node/exposure.resource.d.ts +12 -0
  101. package/dist/node/files.d.ts +9 -0
  102. package/dist/node/http-smart-client.model.d.ts +22 -0
  103. package/dist/node/index.d.ts +1 -0
  104. package/dist/node/inputFile.model.d.ts +22 -0
  105. package/dist/node/inputFile.utils.d.ts +14 -0
  106. package/dist/node/mixed-http-client.node.d.ts +27 -0
  107. package/dist/node/node.cjs +11168 -0
  108. package/dist/node/node.cjs.map +1 -0
  109. package/dist/node/node.d.ts +6 -0
  110. package/dist/node/node.mjs +11099 -0
  111. package/dist/node/node.mjs.map +1 -0
  112. package/dist/node/platform/createFile.d.ts +9 -0
  113. package/dist/node/tunnel.allowlist.d.ts +7 -0
  114. package/dist/node/upload/manifest.d.ts +22 -0
  115. package/dist/platform/adapters/browser.d.ts +14 -0
  116. package/dist/platform/adapters/edge.d.ts +5 -0
  117. package/dist/platform/adapters/node-als.d.ts +1 -0
  118. package/dist/platform/adapters/node.d.ts +15 -0
  119. package/dist/platform/adapters/universal-generic.d.ts +14 -0
  120. package/dist/platform/adapters/universal.d.ts +17 -0
  121. package/dist/platform/createFile.d.ts +10 -0
  122. package/dist/platform/createWebFile.d.ts +11 -0
  123. package/dist/platform/factory.d.ts +2 -0
  124. package/dist/platform/index.d.ts +27 -0
  125. package/dist/platform/types.d.ts +29 -0
  126. package/dist/processHooks.d.ts +2 -0
  127. package/dist/run.d.ts +14 -0
  128. package/dist/testing.d.ts +25 -0
  129. package/dist/tools/getCallerFile.d.ts +1 -0
  130. package/dist/tunnels/buildUniversalManifest.d.ts +24 -0
  131. package/dist/types/contracts.d.ts +63 -0
  132. package/dist/types/event.d.ts +74 -0
  133. package/dist/types/hook.d.ts +23 -0
  134. package/dist/types/inputFile.d.ts +34 -0
  135. package/dist/types/meta.d.ts +18 -0
  136. package/dist/types/resource.d.ts +87 -0
  137. package/dist/types/resourceMiddleware.d.ts +47 -0
  138. package/dist/types/runner.d.ts +55 -0
  139. package/dist/types/storeTypes.d.ts +40 -0
  140. package/dist/types/symbols.d.ts +28 -0
  141. package/dist/types/tag.d.ts +46 -0
  142. package/dist/types/task.d.ts +50 -0
  143. package/dist/types/taskMiddleware.d.ts +48 -0
  144. package/dist/types/utilities.d.ts +111 -0
  145. package/dist/universal/index.cjs +1438 -251
  146. package/dist/universal/index.cjs.map +1 -1
  147. package/dist/universal/index.mjs +1433 -252
  148. package/dist/universal/index.mjs.map +1 -1
  149. package/package.json +32 -4
  150. package/dist/index.d.mts +0 -1747
  151. package/dist/index.unused.js +0 -4466
  152. package/dist/index.unused.js.map +0 -1
  153. package/dist/node/index.cjs +0 -4498
  154. package/dist/node/index.cjs.map +0 -1
  155. package/dist/node/index.mjs +0 -4466
  156. package/dist/node/index.mjs.map +0 -1
package/AI.md CHANGED
@@ -1,4 +1,25 @@
1
- # BlueLibs Runner: Minimal Guide
1
+ # BlueLibs Runner: Fluent Builder Field Guide
2
+
3
+ > Token-friendly (<5000 tokens). This guide spotlights the fluent builder API (`r.*`) that ships with Runner 4.x. Classic `defineX` / `resource({...})` remain supported for backwards compatibility, but fluent builders are the default throughout.
4
+
5
+ ## Table of Contents
6
+ - [Install](#install)
7
+ - [Quick Start](#quick-start)
8
+ - [Platform Matrix](#platform-matrix)
9
+ - [Fluent Builder Primer](#fluent-builder-primer)
10
+ - [Core Concepts](#core-concepts)
11
+ - [Resources](#resources)
12
+ - [Tasks](#tasks)
13
+ - [Events and Hooks](#events-and-hooks)
14
+ - [Middleware](#middleware)
15
+ - [Tags](#tags)
16
+ - [HTTP & Tunnels](#http--tunnels)
17
+ - [Serialization](#serialization)
18
+ - [Testing](#testing)
19
+ - [Observability & Debugging](#observability--debugging)
20
+ - [Advanced Patterns](#advanced-patterns)
21
+ - [Interop With Classic APIs](#interop-with-classic-apis)
22
+ - [Reference Links](#reference-links)
2
23
 
3
24
  ## Install
4
25
 
@@ -6,677 +27,396 @@
6
27
  npm install @bluelibs/runner
7
28
  ```
8
29
 
9
- ## Platform & Browser
30
+ Runner auto-detects its runtime. The Node bundle lives under `@bluelibs/runner/node`; browser helpers are under `@bluelibs/runner/platform`.
10
31
 
11
- - Auto‑detects platform at runtime. In browsers, `exit()` is unsupported and throws. Env reads use `globalThis.__ENV__`, `process.env`, or `globalThis.env`.
32
+ ## Quick Start
12
33
 
13
34
  ```ts
14
- import { setPlatform, PlatformAdapter } from "@bluelibs/runner/platform";
15
- setPlatform(new PlatformAdapter("browser"));
16
- //
17
- globalThis.__ENV__ = { API_URL: "https://example.test" };
18
- ```
35
+ import express from "express";
36
+ import { r, run, globals } from "@bluelibs/runner";
37
+ import { nodeExposure } from "@bluelibs/runner/node";
38
+
39
+ const server = r
40
+ .resource<{ port: number }>("app.server")
41
+ .context(() => ({ app: express() }))
42
+ .init(async ({ port }, _deps, ctx) => {
43
+ ctx.app.use(express.json());
44
+ const listener = ctx.app.listen(port);
45
+ return { ...ctx, listener };
46
+ })
47
+ .dispose(async ({ listener }) => listener.close())
48
+ .build();
49
+
50
+ const createUser = r
51
+ .task("app.tasks.createUser")
52
+ .dependencies({ logger: globals.resources.logger })
53
+ .inputSchema<{ name: string }>({ parse: (value) => value })
54
+ .resultSchema<{ id: string; name: string }>({ parse: (value) => value })
55
+ .run(async ({ input }, { logger }) => {
56
+ await logger.info(`Creating user ${input.name}`);
57
+ return { id: "user-1", name: input.name };
58
+ })
59
+ .build();
60
+
61
+ const api = r
62
+ .resource("app.api")
63
+ .register([
64
+ server.with({ port: 3000 }),
65
+ nodeExposure.with({
66
+ http: { basePath: "/__runner", listen: { port: 3000 } },
67
+ }),
68
+ createUser,
69
+ ])
70
+ .dependencies({ server, createUser })
71
+ .init(async (_config, { server, createUser }) => {
72
+ server.listener.on("listening", () => {
73
+ console.log("Runner HTTP server ready on port 3000");
74
+ });
19
75
 
20
- ## Core Philosophy
76
+ server.app.post("/users", async (req, res) => {
77
+ const user = await createUser(req.body);
78
+ res.json(user);
79
+ });
80
+ })
81
+ .build();
21
82
 
22
- BlueLibs Runner is a **powerful and integrated** framework. It provides a comprehensive set of tools for building robust, testable, and maintainable applications by combining a predictable Dependency Injection (DI) container with a dynamic metadata and eventing system.
83
+ const runtime = await run(api);
84
+ await runtime.runTask(createUser, { name: "Ada" });
85
+ // runtime.dispose() when you are done.
86
+ ```
23
87
 
24
- ## DI Container Guarantees
88
+ - `r.*.with(config)` produces a configured copy of the definition.
89
+ - `run(root)` wires dependencies, runs `init`, emits lifecycle events, and returns helpers such as `runTask`, `getResourceValue`, and `dispose`.
90
+ - Enable verbose logging with `run(root, { debug: "verbose" })`.
25
91
 
26
- This is the foundation of trust for any DI framework.
92
+ ## Platform Matrix
27
93
 
28
- - **Circular Dependencies**: A runtime circular dependency (e.g., `A → B → A`) is a fatal error. The runner **will fail to start** and will throw a descriptive error showing the full dependency chain, forcing you to fix the architecture.
29
- - **Override Precedence**: Overrides are applied top-down. In case of conflicting overrides for the same `id`, the one defined closest to the root `run()` call wins. The "root is the boss."
94
+ | Capability | Node.js | Browser | Workers (e.g. Cloudflare) |
95
+ | --- | --- | --- | --- |
96
+ | `run()` lifecycle | ✅ | ✅ | ✅ |
97
+ | `nodeExposure`, Node tunnels | ✅ | ❌ | ❌ |
98
+ | `createHttpClient` | ✅ | ✅ | ✅ |
99
+ | Duplex upload (`createHttpSmartClient`) | ✅ | ❌ | ❌ |
100
+ | File helpers (`createNodeFile`, `createWebFile`) | Node only | Browser only | Browser only |
101
+ | Async context (AsyncLocalStorage) | ✅ | n/a | n/a |
30
102
 
31
- ## TL;DR
103
+ - Browser environments rely on `globalThis.__ENV__` or `globalThis.env` for configuration; Node uses `process.env`.
104
+ - Runner will throw if you call Node-only helpers in the browser; keep shared code inside `src/` and Node-specific logic under `src/node/`.
32
105
 
33
- - **Lifecycle**: `run() → init resources (deps first) → 'ready' event → dispose() (reverse order)`
34
- - **Tasks**: Functions with DI and middleware. Flow: `call → middleware → input validation → run() → result validation → return`
35
- - **Resources**: Managed singletons (init/dispose).
36
- - **Events**: Decoupled communication. Flow: `emit → validation → find & order hooks → run hooks (stoppable)`
37
- - **Hooks**: Lightweight event listeners. Async and awaited by default.
38
- - **Middleware**: Cross-cutting concerns. Async and awaited by default. Optionally contract-enforcing for input/output.
39
- - **Tags**: Metadata for organizing, filtering, enforcing input/output contracts to tasks or resources.
106
+ ## Fluent Builder Primer
40
107
 
41
- ## Quick Start
108
+ The fluent API lives under the single `r` namespace:
42
109
 
43
110
  ```ts
44
- import express from "express";
45
- import { resource, task, run } from "@bluelibs/runner";
46
-
47
- const server = resource({
48
- id: "app.server",
49
- // "context" is for private state between init() and dispose()
50
- context: () => ({ value: null }),
51
- init: async (config: { port: number }, dependencies, ctx) => {
52
- ctx.value = "some-value"; // Store private state for dispose()
53
-
54
- const app = express();
55
- const server = app.listen(config.port);
56
- return { app, server };
57
- },
58
- dispose: async ({ server }, config, deps, ctx) => server.close(),
59
- });
60
-
61
- const createUser = task({
62
- id: "app.tasks.createUser",
63
- dependencies: { server },
64
- run: async (user: { name: string }, deps) => ({ id: "u1", ...user }),
65
- });
111
+ import { r } from "@bluelibs/runner";
66
112
 
67
- const app = resource({
68
- id: "app",
69
- // Resources with configurations must be registered with with() unless the configuration allows all optional
70
- // All elements must be registered for them to be used in the system
71
- register: [server.with({ port: 3000 }), createUser],
72
- dependencies: { server, createUser },
73
- init: async (_, { server, createUser }) => {
74
- server.app.post("/users", async (req, res) =>
75
- res.json(await createUser(req.body)),
76
- );
77
- },
78
- });
79
-
80
- // Run with optional debug/logs
81
- // If app had a config app.with(config) for 1st arg
82
- await run(app, {
83
- debug: "normal", // "normal" | "verbose" | DebugConfig
84
- logs: { printThreshold: "info", printStrategy: "pretty" },
85
- });
113
+ const task = r.task("demo.tasks.hello").run(async ({ input }) => input).build();
86
114
  ```
87
115
 
88
- ## Events & Hooks
116
+ Key rules:
89
117
 
90
- ```ts
91
- import { event, hook, globals } from "@bluelibs/runner";
118
+ - Builders are immutable; every fluent call returns a new builder with tightened typings.
119
+ - Call `.build()` once you finish configuring the definition.
120
+ - `with(config)` clones the built definition with typed config overrides.
121
+ - Use the same pattern across tasks, resources, hooks, middleware, and tags for consistent DX.
92
122
 
93
- const userRegistered = event<{ userId: string; email: string }>({
94
- id: "app.events.userRegistered",
95
- });
123
+ ## Core Concepts
96
124
 
97
- const sendWelcome = hook({
98
- id: "app.hooks.sendWelcome",
99
- on: userRegistered,
100
- run: async (e) => console.log(`Welcome ${e.data.email}`),
101
- });
125
+ ### Resources
102
126
 
103
- // Wildcard listener
104
- const audit = hook({
105
- id: "app.hooks.audit",
106
- on: "*",
107
- run: (e) => console.log(e.id),
108
- });
109
-
110
- // Exclude internal events from "*"
111
- const internal = event({
112
- id: "app.events.internal",
113
- tags: [globals.tags.excludeFromGlobalHooks],
114
- });
115
-
116
- // Performance: runtime event emission cycle detection
117
- // run(app, { runtimeCycleDetection: true }) // To prevent deadlocks from happening.
118
- ```
119
-
120
- ### Multiple Events per Hook
121
-
122
- Listen to multiple events with type-safe common fields:
127
+ Resources encapsulate long-lived values such as database connections or service facades.
123
128
 
124
129
  ```ts
125
- const h = hook({
126
- id: "app.hooks.multi",
127
- on: [event1, event2, event3],
128
- run: async (ev) => {
129
- // helper utility
130
- if (isOneOf(ev, [event1, event2])) {
131
- // all common fields from event1 and event2, if just event1, it will be just event1
132
- }
133
- },
134
- });
130
+ import { MongoClient } from "mongodb";
131
+ import { r } from "@bluelibs/runner";
132
+
133
+ const database = r
134
+ .resource<{ url: string }>("app.resources.database")
135
+ .init(async ({ url }) => {
136
+ const client = new MongoClient(url);
137
+ await client.connect();
138
+ return client;
139
+ })
140
+ .dispose(async (client) => client.close())
141
+ .build();
142
+
143
+ const userService = r
144
+ .resource("app.resources.userService")
145
+ .dependencies({ database })
146
+ .init(async (_config, { database }) => ({
147
+ async create(user: { email: string }) {
148
+ return database.db().collection("users").insertOne(user);
149
+ },
150
+ }))
151
+ .build();
135
152
  ```
136
153
 
137
- ### Interception APIs
138
-
139
- Low-level interception is available for advanced observability and control:
140
-
141
- - `eventManager.intercept((next, event) => Promise<void>)` — wraps event emission
142
- - `eventManager.interceptHook((next, hook, event) => Promise<any>)` — wraps hook execution
143
- - `middlewareManager.intercept("task" | "resource", (next, input) => Promise<any>)` — wraps middleware execution
144
- - `middlewareManager.interceptMiddleware(middleware, interceptor)` — per-middleware interception
154
+ - `context(fn)` gives you a private object that survives `init` → `dispose`.
155
+ - `configSchema` and `resultSchema` accept anything with a `parse()` method (Zod, custom validators).
156
+ - Register resources inside other resources via `.register([...])`. Repeated calls append unless you pass `{ override: true }`.
145
157
 
146
- Prefer task-level `task.intercept()` for application logic; use the above for cross-cutting concerns.
158
+ ### Tasks
147
159
 
148
- ## Unhandled Errors
149
-
150
- By default, unhandled errors are just logged. You can customize this via `run(app, { onUnhandledError })`:
160
+ Tasks are your business actions. They are plain async functions with DI, middleware, and validation.
151
161
 
152
162
  ```ts
153
- await run(app, {
154
- errorBoundary: true, // Catch process-level errors (default: true)
155
- onUnhandledError: async ({ error, kind, source }) => {
156
- // kind: "task" | "middleware" | "resourceInit" | "hook" | "process" | "run"
157
- // source: optional origin hint (ex: "uncaughtException")
158
- await telemetry.capture(error as Error, { kind, source });
159
-
160
- // Optionally decide on remediation strategy
161
- if (kind === "process") {
162
- // For hard process faults, prefer fast, clean exit after flushing logs
163
- await flushAll();
164
- process.exit(1);
165
- }
166
- },
167
- });
163
+ import { r } from "@bluelibs/runner";
164
+
165
+ const sendEmail = r
166
+ .task("app.tasks.sendEmail")
167
+ .inputSchema<{ to: string; subject: string; body: string }>({
168
+ parse: (value) => value,
169
+ })
170
+ .dependencies({ emailer: userService })
171
+ .middleware((config) => [
172
+ loggingMiddleware.with({ label: "email" }),
173
+ tracingMiddleware,
174
+ ])
175
+ .run(async ({ input }, { emailer }) => {
176
+ await emailer.send(input);
177
+ return { delivered: true };
178
+ })
179
+ .build();
168
180
  ```
169
181
 
170
- If you prefer event-driven handling, you can still emit your own custom events from this callback.
182
+ - `.dependencies()` accepts a literal map or a function `(config) => deps`.
183
+ - `.middleware()` appends by default; pass `{ override: true }` to replace. `.tags()` replaces the list each time.
184
+ - `.dependencies()` appends (shallow-merge) by default on resources, tasks, hooks, and middleware; pass `{ override: true }` to replace. Functions and objects are merged consistently.
185
+ - Provide result validation with `.resultSchema()` when the function returns structured data.
171
186
 
172
- ## Debug (zero‑overhead when disabled)
187
+ ### Events and Hooks
173
188
 
174
- Enable globally at run time:
189
+ Events are strongly typed signals. Hooks listen to them with predictable execution order.
175
190
 
176
191
  ```ts
177
- await run(app, { debug: "verbose" }); // "normal" or DebugConfig
192
+ import { r } from "@bluelibs/runner";
193
+
194
+ const userRegistered = r
195
+ .event("app.events.userRegistered")
196
+ .payloadSchema<{ userId: string; email: string }>({ parse: (v) => v })
197
+ .build();
198
+
199
+ const registerUser = r
200
+ .task("app.tasks.registerUser")
201
+ .dependencies({ userRegistered, userService })
202
+ .run(async ({ input }, deps) => {
203
+ const user = await deps.userService.create(input);
204
+ await deps.userRegistered.emit({ userId: user.id, email: user.email });
205
+ return user;
206
+ })
207
+ .build();
208
+
209
+ const sendWelcomeEmail = r
210
+ .hook("app.hooks.sendWelcomeEmail")
211
+ .on(userRegistered)
212
+ .dependencies({ mailer: sendEmail })
213
+ .run(async (event, { mailer }) => {
214
+ await mailer({ to: event.data.email, subject: "Welcome", body: "🎉" });
215
+ })
216
+ .build();
178
217
  ```
179
218
 
180
- or per‑component via tag:
219
+ - Use `.on(onAnyOf(...))` to listen to several events while keeping inference.
220
+ - Hooks can set `.order(priority)`; lower numbers run first. Call `event.stopPropagation()` inside `run` to cancel downstream hooks.
221
+ - Wildcard hooks use `.on("*")` and receive every emission except events tagged with `globals.tags.excludeFromGlobalHooks`.
181
222
 
182
- ```ts
183
- import { globals, task } from "@bluelibs/runner";
223
+ ### Middleware
184
224
 
185
- const critical = task({
186
- id: "app.tasks.critical",
187
- tags: [globals.tags.debug.with({ logTaskInput: true, logTaskOutput: true })],
188
- run: async () => "ok",
189
- });
190
- ```
191
-
192
- ## Logger (direct API)
225
+ Middleware wraps tasks or resources. Fluent builders live under `r.middleware`.
193
226
 
194
227
  ```ts
195
- import { resource, globals } from "@bluelibs/runner";
196
-
197
- const logsExtension = resource({
198
- id: "app.logs",
199
- dependencies: { logger: globals.resources.logger },
200
- init: async (_, { logger }) => {
201
- logger.info("test", { example: 123 }); // "trace", "debug", "info", "warn", "error", "critical"
202
- const sublogger = logger.with({
203
- source: "app.logs",
204
- additionalContext: {},
205
- });
206
- logger.onLog((log) => {
207
- // ship or transform
208
- });
209
- },
210
- });
228
+ import { r } from "@bluelibs/runner";
229
+ import { globals } from "@bluelibs/runner";
230
+
231
+ const auditTasks = r.middleware
232
+ .task("app.middleware.audit")
233
+ .dependencies({ logger: globals.resources.logger })
234
+ .everywhere((task) => !task.id.startsWith("admin."))
235
+ .run(async ({ task, next }, { logger }) => {
236
+ logger.info(`→ ${task.definition.id}`);
237
+ const result = await next(task.input);
238
+ logger.info(`← ${task.definition.id}`);
239
+ return result;
240
+ })
241
+ .build();
242
+
243
+ const cacheResources = r.middleware
244
+ .resource("app.middleware.cache")
245
+ .configSchema<{ ttl: number }>({ parse: (value) => value })
246
+ .run(async ({ value, next }, _deps, config) => {
247
+ if (value.current) {
248
+ return value.current;
249
+ }
250
+ const computed = await next();
251
+ value.current = computed;
252
+ setTimeout(() => (value.current = null), config.ttl);
253
+ return computed;
254
+ })
255
+ .build();
211
256
  ```
212
257
 
213
- ## AWS Lambda
258
+ Attach middleware using `.middleware([auditTasks])` on the definition that owns it, and register the middleware alongside the target resource or task at the root.
214
259
 
215
- - Cache the runner between warm invocations; do not dispose on each call.
216
- - Disable shutdown hooks (`shutdownHooks: false`) and enable the error boundary.
217
- - Provide a request-scoped context per invocation via `createContext`.
218
- - Parse API Gateway v1/v2 events (handle `requestContext.http.method`/`rawPath` and `httpMethod`/`path`) and base64 bodies.
219
- - Optionally set `context.callbackWaitsForEmptyEventLoop = false` when using long‑lived connections.
260
+ ### Tags
220
261
 
221
- Example outline:
262
+ Tags let you annotate definitions with metadata that can be queried later.
222
263
 
223
264
  ```ts
224
- // bootstrap.ts
225
- import { resource, task, run, createContext } from "@bluelibs/runner";
226
- export const RequestCtx: any = createContext("app.http.request");
227
- // define resources & tasks...
228
- let rrPromise: Promise<any> | null = null;
229
- export async function getRunner() {
230
- if (!rrPromise) {
231
- rrPromise = run(app, { shutdownHooks: false, errorBoundary: true });
232
- }
233
- return rrPromise;
234
- }
235
-
236
- // handler.ts
237
- export const handler = async (event: any, context: any) => {
238
- const rr: any = await getRunner();
239
- const method =
240
- event?.requestContext?.http?.method ?? event?.httpMethod ?? "GET";
241
- const path = event?.rawPath || event?.path || "/";
242
- const rawBody = event?.body
243
- ? event.isBase64Encoded
244
- ? Buffer.from(event.body, "base64").toString("utf8")
245
- : event.body
246
- : undefined;
247
- const body = rawBody ? JSON.parse(rawBody) : undefined;
248
-
249
- return RequestCtx.provide(
250
- { requestId: context?.awsRequestId ?? "local", method, path },
251
- async () => {
252
- // route and call rr.runTask(...)
253
- },
254
- );
255
- };
265
+ import { r, globals } from "@bluelibs/runner";
266
+
267
+ const httpRouteTag = r
268
+ .tag("app.tags.httpRoute")
269
+ .configSchema<{ method: "GET" | "POST"; path: string }>({
270
+ parse: (value) => value,
271
+ })
272
+ .build();
273
+
274
+ const getHealth = r
275
+ .task("app.tasks.getHealth")
276
+ .tags([httpRouteTag.with({ method: "GET", path: "/health" })])
277
+ .run(async () => ({ status: "ok" }))
278
+ .build();
256
279
  ```
257
280
 
258
- ## Middleware (global or local)
281
+ Retrieve tagged items by using `globals.resources.store` inside a hook or resource and calling `store.getTasksWithTag(tag)`.
259
282
 
260
- Middleware now supports type contracts with `<Config, Input, Output>` signature:
283
+ ## HTTP & Tunnels
261
284
 
262
- ```ts
263
- import {
264
- taskMiddleware,
265
- resourceMiddleware,
266
- resource,
267
- task,
268
- globals,
269
- } from "@bluelibs/runner";
270
-
271
- // Custom task middleware with type contracts
272
- const auth = taskMiddleware<
273
- { role: string },
274
- { user: { role: string } },
275
- { user: { role: string; verified: boolean } }
276
- >({
277
- id: "app.middleware.auth",
278
- run: async ({ task, next }, _, cfg) => {
279
- if (task.input?.user?.role !== cfg.role) throw new Error("Unauthorized");
280
- const result = await next(task.input);
281
- return { user: { ...task.input.user, verified: true } };
282
- },
283
- });
285
+ Run Node exposures and connect to remote Runners with fluent resources.
284
286
 
285
- // Resource middleware can augment a resource's behavior after it's initialized.
286
- // For example, this `softDelete` middleware intercepts the `delete` method
287
- // of a resource and replaces it with a non-destructive update.
288
- const softDelete = resourceMiddleware({
289
- id: "app.middleware.softDelete",
290
- run: async ({ resource, next }) => {
291
- const resourceInstance = await next(resource.config); // The original resource instance
292
-
293
- // This example assumes the resource has `update` and `delete` methods.
294
- // A more robust implementation would check for their existence.
295
-
296
- // Monkey-patch the 'delete' method
297
- const originalDelete = resourceInstance.delete;
298
- resourceInstance.delete = async (id: string, ...args) => {
299
- // Instead of deleting, call 'update' to mark as deleted
300
- return resourceInstance.update(id, { deletedAt: new Date() }, ...args);
301
- };
302
-
303
- return resourceInstance;
304
- },
305
- });
306
-
307
- const adminOnly = task({
308
- id: "app.tasks.adminOnly",
309
- middleware: [auth.with({ role: "admin" })],
310
- run: async () => "secret",
311
- });
312
-
313
- // Built-in middleware patterns
314
- const {
315
- task: { retry, timeout, cache },
316
- // available: resource: { retry, timeout, cache } as well, same configs.
317
- } = globals.middleware;
318
-
319
- // Example of custom middleware with full type contracts
320
- const validationMiddleware = taskMiddleware<
321
- { strict: boolean },
322
- { data: unknown },
323
- { data: any; validated: boolean }
324
- >({
325
- id: "app.middleware.validation",
326
- run: async ({ task, next }, _, config) => {
327
- // Validation logic here
328
- const result = await next(task.input);
329
- return { ...result, validated: true };
287
+ ```ts
288
+ import { r, globals } from "@bluelibs/runner";
289
+ import { nodeExposure } from "@bluelibs/runner/node";
290
+
291
+ const httpExposure = nodeExposure.with({
292
+ http: {
293
+ basePath: "/__runner",
294
+ listen: { host: "0.0.0.0", port: 7070 },
295
+ auth: { token: process.env.RUNNER_TOKEN },
330
296
  },
331
297
  });
332
298
 
333
- const resilientTask = task({
334
- id: "app.tasks.resilient",
335
- middleware: [
336
- // Retry with exponential backoff, allow each with timeout
337
- retry.with({
338
- retries: 3,
339
- delayStrategy: (attempt) => 1000 * attempt,
340
- stopRetryIf: (error) => error.message === "Invalid credentials",
299
+ const tunnelClient = r
300
+ .resource("app.tunnels.http")
301
+ .tags([globals.tags.tunnel])
302
+ .init(async () => ({
303
+ mode: "client" as const,
304
+ transport: "http" as const,
305
+ tasks: (task) => task.id.startsWith("remote.tasks."),
306
+ client: globals.tunnels.http.createClient({
307
+ url: process.env.REMOTE_URL ?? "http://127.0.0.1:7070/__runner",
308
+ auth: { token: process.env.RUNNER_TOKEN },
341
309
  }),
342
- // Timeout protection (propose-timeout)
343
- timeout.with({ ttl: 10000 }),
344
- // Caching first (onion-level)
345
- cache.with({
346
- ttl: 60000,
347
- keyBuilder: (taskId, input) => `${taskId}-${JSON.stringify(input)}`,
348
- }),
349
- ],
350
- run: async () => expensiveApiCall(),
351
- });
310
+ }))
311
+ .build();
352
312
 
353
- // Global middleware
354
- const globalTaskMiddleware = taskMiddleware({
355
- id: "...",
356
- everywhere: true, // Use everywhere: (task) => boolean, where true means it gets applied
357
- // ... rest as usual ...
358
- // if you have dependencies as task, exclude them via everywhere filter.
359
- });
360
-
361
- // Global resource middleware (same everywhere semantics)
362
- const globalResourceMiddleware = resourceMiddleware({
363
- id: "...",
364
- everywhere: true, // or: (resource) => boolean
365
- run: async ({ next }) => next(),
366
- });
313
+ const root = r
314
+ .resource("app")
315
+ .register([httpExposure, tunnelClient, getHealth])
316
+ .build();
367
317
  ```
368
318
 
369
- ## Context (request-scoped values)
319
+ Use the unified HTTP client anywhere:
370
320
 
371
321
  ```ts
372
- import { createContext } from "@bluelibs/runner";
373
-
374
- const UserCtx = createContext<{ userId: string }>("app.userContext");
322
+ import { createHttpClient } from "@bluelibs/runner";
323
+ import { createFile as createWebFile } from "@bluelibs/runner/platform/createFile";
375
324
 
376
- // In middleware or entry-point
377
- UserCtx.provide({ userId: "u1" }, async () => {
378
- await someTask(); // has access to the context
325
+ const client = createHttpClient({
326
+ baseUrl: "/__runner",
327
+ auth: { token: "secret" },
379
328
  });
380
329
 
381
- // In a task or hook
382
- const user = UserCtx.use(); // -> { userId: "u1" }
383
-
384
- // In a task definition
385
- const task = {
386
- middleware: [UserCtx.require()], // This middleware works only in tasks.
387
- };
388
- ```
389
-
390
- ## System Shutdown & Error Boundary
391
-
392
- The framework includes built-in support for graceful shutdowns:
393
-
394
- ```ts
395
- const { dispose } = await run(app, {
396
- shutdownHooks: true, // Automatically handle SIGTERM/SIGINT (default: true)
397
- errorBoundary: true, // Catch unhandled errors and rejections (default: true)
398
- });
399
- ```
400
-
401
- ## Type Helpers
330
+ await client.task("app.tasks.getHealth");
402
331
 
403
- Extract generics from tasks, resources, and events without re-declaring their shapes and avoid use of 'any'.
404
-
405
- ```ts
406
- import { task, resource, event } from "@bluelibs/runner";
407
- import type {
408
- ExtractTaskInput, // ExtractTaskInput(typeof myTask)
409
- ExtractTaskOutput,
410
- ExtractResourceConfig,
411
- ExtractResourceValue,
412
- ExtractEventPayload,
413
- } from "@bluelibs/runner";
332
+ const file = createWebFile({ name: "notes.txt" }, new Blob(["Hello"]));
333
+ await client.task("app.tasks.upload", { file });
414
334
  ```
415
335
 
416
- ## Run Options (high‑level)
417
-
418
- - debug: "normal" | "verbose" | DebugConfig
419
- - logs: {
420
- - printThreshold?: LogLevel | null;
421
- - printStrategy?: "pretty" | "json" | "json-pretty" | "plain";
422
- - bufferLogs?: boolean
423
- - }
424
- - errorBoundary: boolean (default true)
425
- - shutdownHooks: boolean (default true)
426
- - onUnhandledError(error) {}
336
+ - `createHttpSmartClient` (Node only) supports duplex streams.
337
+ - For Node-specific features such as `useExposureContext` for handling aborts and streaming in exposed tasks, see TUNNELS.md.
338
+ - Register authentication middleware or rate limiting on the exposure via middleware tags and filters.
427
339
 
428
- Note: `globals` is a convenience object exposing framework internals:
340
+ ## Serialization
429
341
 
430
- - `globals.events` (ready)
431
- - `globals.resources` (store, taskRunner, eventManager, logger, cache, queue)
432
- - `globals.middleware` (retry, cache, timeout, requireContext)
433
- - `globals.tags` (system, debug, excludeFromGlobalHooks)
434
-
435
- ## Overrides
342
+ Runner ships with an EJSON serializer that round-trips Dates, RegExp, binary, and custom shapes across Node and web.
436
343
 
437
344
  ```ts
438
- import { override, resource } from "@bluelibs/runner";
439
-
440
- const emailer = resource({ id: "app.emailer", init: async () => new SMTP() });
441
- const testEmailer = override(emailer, { init: async () => new MockSMTP() });
345
+ import { r, globals } from "@bluelibs/runner";
346
+
347
+ const serializerSetup = r
348
+ .resource("app.serialization")
349
+ .dependencies({ serializer: globals.resources.serializer })
350
+ .init(async (_config, { serializer }) => {
351
+ class Distance {
352
+ constructor(public value: number, public unit: string) {}
353
+ typeName() {
354
+ return "Distance";
355
+ }
356
+ toJSONValue() {
357
+ return { value: this.value, unit: this.unit };
358
+ }
359
+ }
442
360
 
443
- const app = resource({
444
- id: "app",
445
- register: [emailer],
446
- overrides: [testEmailer],
447
- });
361
+ serializer.addType("Distance", (json) => new Distance(json.value, json.unit));
362
+ })
363
+ .build();
448
364
  ```
449
365
 
450
- ## Namespacing
451
-
452
- As your app grows, you'll want consistent naming. Here's the convention that won't drive you crazy:
453
-
454
- | Type | Format |
455
- | ------------------- | ------------------------------------------------ |
456
- | Resources | `{domain}.resources.{resource-name}` |
457
- | Tasks | `{domain}.tasks.{task-name}` |
458
- | Events | `{domain}.events.{event-name}` |
459
- | Hooks | `{domain}.hooks.on-{event-name}` |
460
- | Task Middleware | `{domain}.middleware.task.{middleware-name}` |
461
- | Resource Middleware | `{domain}.middleware.resource.{middleware-name}` |
462
-
463
- We recommend kebab-case for file names and ids. Suffix files with their primitive type: `*.task.ts`, `*.task-middleware.ts`, `*.hook.ts`, etc.
366
+ Use `getDefaultSerializer()` when you need a standalone instance outside DI.
464
367
 
465
- Folders can look something like this: `src/app/users/tasks/create-user.task.ts`. For domain: `app.users` and a task. Use `middleware/task|resource` for middleware files.
466
-
467
- ## Factory Pattern
468
-
469
- Use a resource to act as a factory for creating class instances. The resource is configured once, and the resulting function can be used throughout the app.
470
-
471
- ```ts
472
- const myFactory = resource({
473
- id: "app.factories.myFactory",
474
- init: async (config: SomeConfigType) => {
475
- // The resource's value is a factory function
476
- return (input: any) => {
477
- return new MyClass(input, config.someOption);
478
- };
479
- },
480
- });
481
-
482
- const app = resource({
483
- id: "app",
484
- register: [myFactory.with({ someOption: "configured" })],
485
- dependencies: { myFactory },
486
- init: async (_, { myFactory }) => {
487
- const instance = myFactory({ someInput: "hello" });
488
- },
489
- });
490
- ```
368
+ Note on files: The “File” you see in tunnels is not an EJSON custom type. Runner uses a dedicated $ejson: "File" sentinel in inputs which the tunnel client/server convert to multipart streams via a manifest. We intentionally do not call `EJSON.addType("File", ...)` by default, because file handling is performed by the tunnel layer (manifest hydration and multipart), not by the serializer. Keep using `createWebFile`/`createNodeFile` for uploads; use `EJSON.addType` only for your own domain types.
491
369
 
492
370
  ## Testing
493
371
 
494
- ```ts
495
- import { resource, run, override } from "@bluelibs/runner";
496
-
497
- const app = resource({
498
- id: "app",
499
- register: [
500
- /* tasks/resources */
501
- ],
502
- });
503
- const harness = resource({
504
- id: "test",
505
- register: [app],
506
- overrides: [
507
- /* test overrides */
508
- ],
509
- });
510
-
511
- const rr = await run(harness);
512
- await rr.runTask(id | task, { input: 1 });
513
- // rr.getResourceValue(id | resource)
514
- // await rr.emitEvent(event, payload)
515
- // rr.logger.info("xxx")
516
- await rr.dispose();
517
- ```
518
-
519
- ## Pre‑release (alpha)
520
-
521
- Publish an alpha without affecting latest:
522
-
523
- ```bash
524
- npm version prerelease --preid=alpha
525
- npm run clean && npm run build
526
- npm publish --tag alpha --access public
527
- # consumers: npm i @bluelibs/runner@alpha
528
- ```
529
-
530
- Local test without publishing:
531
-
532
- ```bash
533
- npm pack # then in a demo app: npm i ../@bluelibs-runner-<version>.tgz
534
- # or: npm link / npm link @bluelibs/runner
535
- ```
536
-
537
- Coverage tip: the script name is `npm run coverage`.
538
-
539
- ## Metadata & Tags
372
+ - Use `npm run coverage:ai` to execute the full Jest suite in a token-friendly format. Focused tests can run via `npm run test -- task-name`.
373
+ - In unit tests, prefer running a minimal root resource and call `await run(root)` to get `runTask`, `emitEvent`, or `getResourceValue`.
374
+ - `createTestResource` is available for legacy suites but new code should compose fluent resources directly.
540
375
 
541
- Tags and meta can be applied to all elements.
376
+ Example:
542
377
 
543
378
  ```ts
544
- import { tag, globals, task, resource } from "@bluelibs/runner";
379
+ import { run } from "@bluelibs/runner";
545
380
 
546
- const contractTag = tag<void, void, { result: string }>({ id: "contract" });
547
- const httpRouteTag = tag<{ method: "GET" | "POST"; path: string }>({
548
- id: "httpRoute",
549
- });
550
-
551
- const task = task({
552
- id: "app.tasks.myTask",
553
- tags: [contractTag, httpRouteTag.with({ method: "POST", path: "/do" })],
554
- run: async () => ({ result: "ok" }), // must return { result: string }
555
- meta: {
556
- title: "My Task",
557
- description: "Does something important", // multi-line description, markdown
558
- },
381
+ test("sends welcome email", async () => {
382
+ const app = r.resource("spec.app").register([sendWelcomeEmail, registerUser]).build();
383
+ const runtime = await run(app);
384
+ await runtime.runTask(registerUser, { email: "user@example.com" });
385
+ await runtime.dispose();
559
386
  });
560
387
  ```
561
388
 
562
- Usage:
563
-
564
- ```ts
565
- const onReady = hook({
566
- id: "app.hooks.onReady",
567
- on: globals.events.ready,
568
- dependencies: { store: globals.resources.store },
569
- run: async (_, { store }) => {
570
- // Same concept for resources
571
- const tasks = store.getTasksWithTag(httpRouteTag); // uses httpRouteTag.exists(component);
572
- tasks.forEach((t) => {
573
- const cfg = httpRouteTag.extract(tasks); // { method, path }
574
- // you can even do t
575
- });
576
- },
577
- });
578
- ```
389
+ ## Observability & Debugging
579
390
 
580
- ## Key Patterns & Features
391
+ - Pass `{ debug: "verbose" }` to `run` for structured logs about registration, middleware, and lifecycle events.
392
+ - `globals.resources.logger` exposes the framework logger; register your own logger resource and override it at the root to capture logs centrally.
393
+ - Hooks and tasks emit metadata through `globals.resources.store`. Query it for dashboards or editor plugins.
394
+ - Use middleware for tracing (`r.middleware.task("...").run(...)`) to wrap every task call.
581
395
 
582
- - **Optional Dependencies**: Gracefully handle missing services by defining dependencies as optional. The dependency will be `null` if not registered.
583
- `dependencies: { analytics: analyticsService.optional() }`
396
+ ## Advanced Patterns
584
397
 
585
- - **Stop Propagation**: Prevent other hooks from running for a specific event.
586
- `// inside a hook`
587
- `event.stopPropagation()`
398
+ - **Optional dependencies:** mark dependencies as optional (`analytics: analyticsService.optional()`) so the builder injects `null` when the resource is absent.
399
+ - **Conditional registration:** `.register((config) => (config.enableFeature ? [featureResource] : []))`.
400
+ - **Async coordination:** `Semaphore` and `Queue` live in the main package.
401
+ - **Event safety:** Runner detects event emission cycles and throws an `EventCycleError` with the offending chain.
588
402
 
589
- That’s it. Small surface area, strong primitives, great DX.
403
+ ## Interop With Classic APIs
590
404
 
591
- ## Concurrency: Semaphore & Queue
405
+ Existing code that uses `resource({ ... })`, `task({ ... })`, or `defineX` keeps working. You can gradually migrate:
592
406
 
593
407
  ```ts
594
- import { Semaphore, Queue } from "@bluelibs/runner";
595
-
596
- // Semaphore: limit parallelism
597
- const dbSem = new Semaphore(5);
598
- const users = await dbSem.withPermit(async () =>
599
- db.query("SELECT * FROM users"),
600
- );
601
-
602
- // Queue: FIFO with cooperative cancellation
603
- const queue = new Queue();
604
- const result = await queue.run(async (signal) => {
605
- signal.throwIfAborted();
606
- return await doWork();
607
- });
608
- await queue.dispose({ cancel: true });
609
- ```
610
-
611
- ## Handling Circular Types (even if runtime is fine)
612
-
613
- Rarely, when TypeScript struggles with circular type inference, break the chain with an explicit interface:
614
-
615
- ```ts
616
- import type { IResource } from "@bluelibs/runner";
408
+ import { r, resource as classicResource } from "@bluelibs/runner";
617
409
 
618
- export const cResource = resource({
619
- id: "c.resource",
620
- dependencies: { a: aResource },
621
- init: async (_, { a }) => `C depends on ${a}`,
622
- }) as IResource<void, string>; // void config, returns string
410
+ const classic = classicResource({ id: "legacy", init: async () => "ok" });
411
+ const modern = r.resource("modern").register([classic]).build();
623
412
  ```
624
413
 
625
- ## Validation (optional and library‑agnostic)
626
-
627
- ## Event Cycle Safety
628
-
629
- To prevent event‑driven deadlocks, the runner detects cycles during emission:
630
-
631
- - A cycle occurs when an event emits another event that eventually re‑emits the original event within the same emission chain (for example: `e1 -> e2 -> e1`).
632
- - When a cycle is detected, an `EventCycleError` is thrown with a readable chain to help debugging.
633
- - A hook re‑emitting the same event it currently handles is allowed only when the emission originates from the same hook instance (useful for idempotent/no‑op retries); other cases are blocked.
634
-
635
- Guidance:
414
+ Fluent builders produce the exact same runtime definitions, so you can mix both styles within one project.
636
415
 
637
- - Prefer one‑way flows; avoid mutual cross‑emits between hooks.
638
- - Use `event.stopPropagation()` to short‑circuit handlers when appropriate.
639
- - Use tags (for example, `globals.tags.excludeFromGlobalHooks`) to scope listeners and avoid unintended re‑entry via global hooks.
416
+ ## Reference Links
640
417
 
641
- Interface any library can implement:
642
-
643
- ```ts
644
- interface IValidationSchema<T> {
645
- parse(input: unknown): T;
646
- }
647
- ```
648
-
649
- Works out of the box with Zod (`z.object(...).parse`), and can be adapted for Yup/Joi with small wrappers. As it only needs a parse() method.
650
-
651
- ```ts
652
- import { z } from "zod";
653
-
654
- // Task input/result validation
655
- const inputSchema = z.object({ email: z.string().email() });
656
- const resultSchema = z.object({ id: z.string(), email: z.string().email() });
657
-
658
- task({
659
- // ...
660
- inputSchema, // validates before run
661
- resultSchema, // validates awaited return
662
- });
663
-
664
- resource({
665
- configSchema, // Resource config validation (runs on .with())
666
- resultSchema, // Runs after initialization
667
- });
668
-
669
- event({
670
- payloadSchema, // Runs on event emission
671
- });
672
-
673
- tag({
674
- configSchema, // Tag config validation (runs on .with())
675
- });
676
-
677
- // Middleware config validation (runs on .with())
678
- middleware({
679
- // ...
680
- configSchema, // runs on .with()
681
- });
682
- ```
418
+ - `readmes/FLUENT_BUILDERS.md` deep dive into fluent APIs.
419
+ - `readmes/TUNNELS.md` – streaming, authentication, deployment tips.
420
+ - `README.md` – project overview and additional examples.
421
+ - `AI.md` (this file) – copy/paste friendly summary.
422
+ - `MULTIPLATFORM.md` – cross-platform gotchas and recommended structure.