@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.
- package/AI.md +319 -579
- package/README.md +885 -731
- package/dist/browser/index.cjs +1438 -251
- package/dist/browser/index.cjs.map +1 -1
- package/dist/browser/index.mjs +1433 -252
- package/dist/browser/index.mjs.map +1 -1
- package/dist/context.d.ts +31 -0
- package/dist/define.d.ts +9 -0
- package/dist/definers/builders/core.d.ts +30 -0
- package/dist/definers/builders/event.d.ts +12 -0
- package/dist/definers/builders/hook.d.ts +20 -0
- package/dist/definers/builders/middleware.d.ts +39 -0
- package/dist/definers/builders/resource.d.ts +40 -0
- package/dist/definers/builders/tag.d.ts +10 -0
- package/dist/definers/builders/task.d.ts +37 -0
- package/dist/definers/builders/task.phantom.d.ts +27 -0
- package/dist/definers/builders/utils.d.ts +4 -0
- package/dist/definers/defineEvent.d.ts +2 -0
- package/dist/definers/defineHook.d.ts +6 -0
- package/dist/definers/defineOverride.d.ts +17 -0
- package/dist/definers/defineResource.d.ts +2 -0
- package/dist/definers/defineResourceMiddleware.d.ts +2 -0
- package/dist/definers/defineTag.d.ts +12 -0
- package/dist/definers/defineTask.d.ts +18 -0
- package/dist/definers/defineTaskMiddleware.d.ts +2 -0
- package/dist/definers/tools.d.ts +47 -0
- package/dist/defs.d.ts +29 -0
- package/dist/edge/index.cjs +1438 -251
- package/dist/edge/index.cjs.map +1 -1
- package/dist/edge/index.mjs +1433 -252
- package/dist/edge/index.mjs.map +1 -1
- package/dist/errors.d.ts +104 -0
- package/dist/globals/globalEvents.d.ts +8 -0
- package/dist/globals/globalMiddleware.d.ts +31 -0
- package/dist/globals/globalResources.d.ts +32 -0
- package/dist/globals/globalTags.d.ts +11 -0
- package/dist/globals/middleware/cache.middleware.d.ts +27 -0
- package/dist/globals/middleware/requireContext.middleware.d.ts +6 -0
- package/dist/globals/middleware/retry.middleware.d.ts +21 -0
- package/dist/globals/middleware/timeout.middleware.d.ts +9 -0
- package/dist/globals/middleware/tunnel.middleware.d.ts +2 -0
- package/dist/globals/resources/debug/debug.resource.d.ts +7 -0
- package/dist/globals/resources/debug/debug.tag.d.ts +2 -0
- package/dist/globals/resources/debug/debugConfig.resource.d.ts +22 -0
- package/dist/globals/resources/debug/executionTracker.middleware.d.ts +50 -0
- package/dist/globals/resources/debug/globalEvent.hook.d.ts +27 -0
- package/dist/globals/resources/debug/hook.hook.d.ts +30 -0
- package/dist/globals/resources/debug/index.d.ts +6 -0
- package/dist/globals/resources/debug/middleware.hook.d.ts +30 -0
- package/dist/globals/resources/debug/types.d.ts +25 -0
- package/dist/globals/resources/debug/utils.d.ts +2 -0
- package/dist/globals/resources/queue.resource.d.ts +10 -0
- package/dist/globals/resources/tunnel/ejson-extensions.d.ts +1 -0
- package/dist/globals/resources/tunnel/error-utils.d.ts +1 -0
- package/dist/globals/resources/tunnel/plan.d.ts +19 -0
- package/dist/globals/resources/tunnel/protocol.d.ts +40 -0
- package/dist/globals/resources/tunnel/serializer.d.ts +9 -0
- package/dist/globals/resources/tunnel/tunnel.policy.tag.d.ts +18 -0
- package/dist/globals/resources/tunnel/tunnel.tag.d.ts +2 -0
- package/dist/globals/resources/tunnel/types.d.ts +17 -0
- package/dist/globals/tunnels/index.d.ts +23 -0
- package/dist/globals/types.d.ts +1 -0
- package/dist/http-client.d.ts +23 -0
- package/dist/http-fetch-tunnel.resource.d.ts +22 -0
- package/dist/index.d.ts +99 -0
- package/dist/models/DependencyProcessor.d.ts +48 -0
- package/dist/models/EventManager.d.ts +153 -0
- package/dist/models/LogPrinter.d.ts +55 -0
- package/dist/models/Logger.d.ts +85 -0
- package/dist/models/MiddlewareManager.d.ts +86 -0
- package/dist/models/OverrideManager.d.ts +13 -0
- package/dist/models/Queue.d.ts +26 -0
- package/dist/models/ResourceInitializer.d.ts +20 -0
- package/dist/models/RunResult.d.ts +35 -0
- package/dist/models/Semaphore.d.ts +61 -0
- package/dist/models/Store.d.ts +69 -0
- package/dist/models/StoreRegistry.d.ts +43 -0
- package/dist/models/StoreValidator.d.ts +8 -0
- package/dist/models/TaskRunner.d.ts +27 -0
- package/dist/models/UnhandledError.d.ts +11 -0
- package/dist/models/index.d.ts +11 -0
- package/dist/models/utils/findCircularDependencies.d.ts +16 -0
- package/dist/models/utils/safeStringify.d.ts +3 -0
- package/dist/node/exposure/allowList.d.ts +3 -0
- package/dist/node/exposure/authenticator.d.ts +6 -0
- package/dist/node/exposure/cors.d.ts +4 -0
- package/dist/node/exposure/createNodeExposure.d.ts +2 -0
- package/dist/node/exposure/exposureServer.d.ts +18 -0
- package/dist/node/exposure/httpResponse.d.ts +10 -0
- package/dist/node/exposure/logging.d.ts +4 -0
- package/dist/node/exposure/multipart.d.ts +27 -0
- package/dist/node/exposure/requestBody.d.ts +11 -0
- package/dist/node/exposure/requestContext.d.ts +17 -0
- package/dist/node/exposure/requestHandlers.d.ts +24 -0
- package/dist/node/exposure/resourceTypes.d.ts +60 -0
- package/dist/node/exposure/router.d.ts +17 -0
- package/dist/node/exposure/serverLifecycle.d.ts +13 -0
- package/dist/node/exposure/types.d.ts +31 -0
- package/dist/node/exposure/utils.d.ts +17 -0
- package/dist/node/exposure.resource.d.ts +12 -0
- package/dist/node/files.d.ts +9 -0
- package/dist/node/http-smart-client.model.d.ts +22 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/inputFile.model.d.ts +22 -0
- package/dist/node/inputFile.utils.d.ts +14 -0
- package/dist/node/mixed-http-client.node.d.ts +27 -0
- package/dist/node/node.cjs +11168 -0
- package/dist/node/node.cjs.map +1 -0
- package/dist/node/node.d.ts +6 -0
- package/dist/node/node.mjs +11099 -0
- package/dist/node/node.mjs.map +1 -0
- package/dist/node/platform/createFile.d.ts +9 -0
- package/dist/node/tunnel.allowlist.d.ts +7 -0
- package/dist/node/upload/manifest.d.ts +22 -0
- package/dist/platform/adapters/browser.d.ts +14 -0
- package/dist/platform/adapters/edge.d.ts +5 -0
- package/dist/platform/adapters/node-als.d.ts +1 -0
- package/dist/platform/adapters/node.d.ts +15 -0
- package/dist/platform/adapters/universal-generic.d.ts +14 -0
- package/dist/platform/adapters/universal.d.ts +17 -0
- package/dist/platform/createFile.d.ts +10 -0
- package/dist/platform/createWebFile.d.ts +11 -0
- package/dist/platform/factory.d.ts +2 -0
- package/dist/platform/index.d.ts +27 -0
- package/dist/platform/types.d.ts +29 -0
- package/dist/processHooks.d.ts +2 -0
- package/dist/run.d.ts +14 -0
- package/dist/testing.d.ts +25 -0
- package/dist/tools/getCallerFile.d.ts +1 -0
- package/dist/tunnels/buildUniversalManifest.d.ts +24 -0
- package/dist/types/contracts.d.ts +63 -0
- package/dist/types/event.d.ts +74 -0
- package/dist/types/hook.d.ts +23 -0
- package/dist/types/inputFile.d.ts +34 -0
- package/dist/types/meta.d.ts +18 -0
- package/dist/types/resource.d.ts +87 -0
- package/dist/types/resourceMiddleware.d.ts +47 -0
- package/dist/types/runner.d.ts +55 -0
- package/dist/types/storeTypes.d.ts +40 -0
- package/dist/types/symbols.d.ts +28 -0
- package/dist/types/tag.d.ts +46 -0
- package/dist/types/task.d.ts +50 -0
- package/dist/types/taskMiddleware.d.ts +48 -0
- package/dist/types/utilities.d.ts +111 -0
- package/dist/universal/index.cjs +1438 -251
- package/dist/universal/index.cjs.map +1 -1
- package/dist/universal/index.mjs +1433 -252
- package/dist/universal/index.mjs.map +1 -1
- package/package.json +32 -4
- package/dist/index.d.mts +0 -1747
- package/dist/index.unused.js +0 -4466
- package/dist/index.unused.js.map +0 -1
- package/dist/node/index.cjs +0 -4498
- package/dist/node/index.cjs.map +0 -1
- package/dist/node/index.mjs +0 -4466
- package/dist/node/index.mjs.map +0 -1
package/AI.md
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
# BlueLibs Runner:
|
|
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
|
-
|
|
30
|
+
Runner auto-detects its runtime. The Node bundle lives under `@bluelibs/runner/node`; browser helpers are under `@bluelibs/runner/platform`.
|
|
10
31
|
|
|
11
|
-
|
|
32
|
+
## Quick Start
|
|
12
33
|
|
|
13
34
|
```ts
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
+
const runtime = await run(api);
|
|
84
|
+
await runtime.runTask(createUser, { name: "Ada" });
|
|
85
|
+
// runtime.dispose() when you are done.
|
|
86
|
+
```
|
|
23
87
|
|
|
24
|
-
|
|
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
|
-
|
|
92
|
+
## Platform Matrix
|
|
27
93
|
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
+
The fluent API lives under the single `r` namespace:
|
|
42
109
|
|
|
43
110
|
```ts
|
|
44
|
-
import
|
|
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
|
|
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
|
-
|
|
116
|
+
Key rules:
|
|
89
117
|
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
id: "app.events.userRegistered",
|
|
95
|
-
});
|
|
123
|
+
## Core Concepts
|
|
96
124
|
|
|
97
|
-
|
|
98
|
-
id: "app.hooks.sendWelcome",
|
|
99
|
-
on: userRegistered,
|
|
100
|
-
run: async (e) => console.log(`Welcome ${e.data.email}`),
|
|
101
|
-
});
|
|
125
|
+
### Resources
|
|
102
126
|
|
|
103
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
158
|
+
### Tasks
|
|
147
159
|
|
|
148
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
+
### Events and Hooks
|
|
173
188
|
|
|
174
|
-
|
|
189
|
+
Events are strongly typed signals. Hooks listen to them with predictable execution order.
|
|
175
190
|
|
|
176
191
|
```ts
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
import { globals, task } from "@bluelibs/runner";
|
|
223
|
+
### Middleware
|
|
184
224
|
|
|
185
|
-
|
|
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 {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
+
Tags let you annotate definitions with metadata that can be queried later.
|
|
222
263
|
|
|
223
264
|
```ts
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
281
|
+
Retrieve tagged items by using `globals.resources.store` inside a hook or resource and calling `store.getTasksWithTag(tag)`.
|
|
259
282
|
|
|
260
|
-
|
|
283
|
+
## HTTP & Tunnels
|
|
261
284
|
|
|
262
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
319
|
+
Use the unified HTTP client anywhere:
|
|
370
320
|
|
|
371
321
|
```ts
|
|
372
|
-
import {
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
325
|
+
const client = createHttpClient({
|
|
326
|
+
baseUrl: "/__runner",
|
|
327
|
+
auth: { token: "secret" },
|
|
379
328
|
});
|
|
380
329
|
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
-
|
|
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
|
-
|
|
340
|
+
## Serialization
|
|
429
341
|
|
|
430
|
-
-
|
|
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 {
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
overrides: [testEmailer],
|
|
447
|
-
});
|
|
361
|
+
serializer.addType("Distance", (json) => new Distance(json.value, json.unit));
|
|
362
|
+
})
|
|
363
|
+
.build();
|
|
448
364
|
```
|
|
449
365
|
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
495
|
-
|
|
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
|
-
|
|
376
|
+
Example:
|
|
542
377
|
|
|
543
378
|
```ts
|
|
544
|
-
import {
|
|
379
|
+
import { run } from "@bluelibs/runner";
|
|
545
380
|
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
583
|
-
`dependencies: { analytics: analyticsService.optional() }`
|
|
396
|
+
## Advanced Patterns
|
|
584
397
|
|
|
585
|
-
- **
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
403
|
+
## Interop With Classic APIs
|
|
590
404
|
|
|
591
|
-
|
|
405
|
+
Existing code that uses `resource({ ... })`, `task({ ... })`, or `defineX` keeps working. You can gradually migrate:
|
|
592
406
|
|
|
593
407
|
```ts
|
|
594
|
-
import {
|
|
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
|
-
|
|
619
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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.
|