@bluelibs/runner 6.3.0 → 6.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/core/SKILL.md +37 -0
- package/.agents/skills/durable-workflows/SKILL.md +117 -0
- package/.agents/skills/remote-lanes/SKILL.md +114 -0
- package/package.json +1 -11
- package/readmes/BENCHMARKS.md +131 -0
- package/readmes/COMPARISON.md +233 -0
- package/readmes/CRITICAL_THINKING.md +49 -0
- package/readmes/DURABLE_WORKFLOWS.md +2270 -0
- package/readmes/DURABLE_WORKFLOWS_AI.md +258 -0
- package/readmes/ENTERPRISE.md +219 -0
- package/readmes/FLUENT_BUILDERS.md +524 -0
- package/readmes/FULL_GUIDE.md +6694 -0
- package/readmes/FUNCTIONAL.md +350 -0
- package/readmes/MULTI_PLATFORM.md +556 -0
- package/readmes/OOP.md +627 -0
- package/readmes/REMOTE_LANES.md +947 -0
- package/readmes/REMOTE_LANES_AI.md +154 -0
- package/readmes/REMOTE_LANES_HTTP_POLICY.md +330 -0
- package/readmes/SERIALIZER_PROTOCOL.md +337 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
# Runner Fluent Builders (r.\*) — End-to-End Guide
|
|
2
|
+
|
|
3
|
+
← [Back to main README](../README.md) | [Fluent Builders section in FULL_GUIDE](./FULL_GUIDE.md#fluent-builders-r)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
This guide shows how to use the new fluent Builder API exposed via a single `r` namespace. Builders are ergonomic, type-safe, and compile to the same definitions used by Runner today, without runtime overhead or breaking changes.
|
|
8
|
+
|
|
9
|
+
### Import
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Match, r, run } from "@bluelibs/runner";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
You'll primarily use `r`:
|
|
16
|
+
|
|
17
|
+
- `r.resource(id)`
|
|
18
|
+
- `r.task(id)`
|
|
19
|
+
- `r.event(id)`
|
|
20
|
+
- `r.hook(id)`
|
|
21
|
+
- `r.tag(id)`
|
|
22
|
+
- `r.middleware.task(id)`
|
|
23
|
+
- `r.middleware.resource(id)`
|
|
24
|
+
- `r.error(id)`
|
|
25
|
+
- `r.asyncContext(id)`
|
|
26
|
+
|
|
27
|
+
Each builder provides a fluent chain to configure dependencies, schemas, middleware, tags, metadata, and implementation functions. Call `.build()` to produce a definition identical to `defineX`.
|
|
28
|
+
|
|
29
|
+
Quick rules of thumb:
|
|
30
|
+
|
|
31
|
+
- `.build()` materializes the definition; register only built items (task/resource/hook/middleware/tag), not builders.
|
|
32
|
+
- When a method accepts a list (for example, `.register()`, `.tags()`, `.middleware()`), you may pass a single item or an array.
|
|
33
|
+
- For resources, repeated `.register()` calls append by default; pass `{ override: true }` to replace.
|
|
34
|
+
- For resources and tasks, repeated `.middleware()` calls append by default; pass `{ override: true }` to replace.
|
|
35
|
+
- For resources, tasks, hooks, events, and middleware, repeated `.tags()` calls append by default; pass `{ override: true }` to replace.
|
|
36
|
+
- For resources, repeated `.overrides()` calls append by default; pass `{ override: true }` to replace.
|
|
37
|
+
- For resources, tasks, hooks and middleware, repeated `.dependencies()` calls append (shallow merge) by default; pass `{ override: true }` to replace. Mixed function/object deps are supported and merged consistently.
|
|
38
|
+
|
|
39
|
+
## Strict Chain Constraints (v1)
|
|
40
|
+
|
|
41
|
+
Runner enforces compile-time chain phases for `r.task`, `r.hook`, `r.resource`, and middleware builders.
|
|
42
|
+
|
|
43
|
+
- `task`: after `.run()`, these are locked: `.dependencies()`, `.inputSchema()`/`.schema()`, `.resultSchema()`, `.middleware()`, `.tags()`. `.meta()`, `.throws()`, `.build()` remain valid.
|
|
44
|
+
- `hook`: `.run()` requires `.on(...)` first. After `.run()`, these are locked: `.on()`, `.dependencies()`, `.tags()`. `.build()` requires both `.on()` and `.run()`.
|
|
45
|
+
- `middleware` (`task` and `resource`): after `.run()`, these are locked: `.dependencies()`, `.configSchema()`/`.schema()`, `.tags()`. `.build()` requires `.run()`.
|
|
46
|
+
- `resource`: after `.init()`, these are locked: `.dependencies()`, `.configSchema()`/`.schema()`, `.resultSchema()`, `.middleware()`, `.tags()`, `.context()`. `.init()` stays optional.
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
r.task("ok")
|
|
52
|
+
.run(async () => "ok")
|
|
53
|
+
.meta({ title: "x" })
|
|
54
|
+
.throws([])
|
|
55
|
+
.build(); // valid
|
|
56
|
+
|
|
57
|
+
r.task("nope")
|
|
58
|
+
.run(async () => "ok")
|
|
59
|
+
.tags([]); // TypeScript error
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Resources
|
|
65
|
+
|
|
66
|
+
Minimal:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// Optionally seed the resource config type at the entry point
|
|
70
|
+
type AppConfig = { feature?: boolean };
|
|
71
|
+
const app = r
|
|
72
|
+
.resource<AppConfig>("app")
|
|
73
|
+
.init(async () => "OK")
|
|
74
|
+
.build();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
With dependencies, tags, middleware, context and schemas:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const svc = resource({
|
|
81
|
+
id: "svc",
|
|
82
|
+
init: async () => ({ add: (a: number, b: number) => a + b }),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const tag = r.tag("my.tag").build();
|
|
86
|
+
|
|
87
|
+
const loggingMw = r.middleware
|
|
88
|
+
.resource("mw.logging")
|
|
89
|
+
.run(async ({ next }) => {
|
|
90
|
+
const out = await next();
|
|
91
|
+
return out;
|
|
92
|
+
})
|
|
93
|
+
.build();
|
|
94
|
+
|
|
95
|
+
const app = r
|
|
96
|
+
.resource("app.composed")
|
|
97
|
+
.register([svc, loggingMw, tag]) // single or array is OK
|
|
98
|
+
.dependencies({ svc })
|
|
99
|
+
.tags([tag])
|
|
100
|
+
.middleware([loggingMw])
|
|
101
|
+
.context(() => ({ reqId: Math.random() }))
|
|
102
|
+
.configSchema({ feature: Boolean }) // or configSchema(zodObject)
|
|
103
|
+
.resultSchema({ status: String }) // or resultSchema(zodObject)
|
|
104
|
+
.init(async (config, deps, resourceContext) => {
|
|
105
|
+
const sum = deps.svc.add(2, 3);
|
|
106
|
+
return {
|
|
107
|
+
status: `id=${resourceContext.reqId}; sum=${sum}; feature=${!!config?.feature}`,
|
|
108
|
+
};
|
|
109
|
+
})
|
|
110
|
+
.build();
|
|
111
|
+
|
|
112
|
+
// Append vs override for register()
|
|
113
|
+
const r1 = r
|
|
114
|
+
.resource("app.register.append")
|
|
115
|
+
.register(svc) // append
|
|
116
|
+
.register([loggingMw, tag]) // append
|
|
117
|
+
.build();
|
|
118
|
+
|
|
119
|
+
const r2 = r
|
|
120
|
+
.resource("app.register.override")
|
|
121
|
+
.register([svc, tag])
|
|
122
|
+
.register(loggingMw, { override: true }) // replace previous registrations
|
|
123
|
+
.build();
|
|
124
|
+
|
|
125
|
+
// Dynamic register: compose functions and arrays
|
|
126
|
+
type Cfg = { flag: boolean };
|
|
127
|
+
const r3 = r
|
|
128
|
+
.resource<Cfg>("app.register.dynamic")
|
|
129
|
+
.register((cfg) => (cfg.flag ? [svc] : [])) // function
|
|
130
|
+
.register(loggingMw) // array/single
|
|
131
|
+
.build();
|
|
132
|
+
// r3.register is a function; r3.register({ flag: true }) => [svc, loggingMw]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Tasks
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const adder = r
|
|
141
|
+
.task("tasks.add")
|
|
142
|
+
.inputSchema({ a: Number, b: Number })
|
|
143
|
+
.run(async (input) => input!.a + input!.b)
|
|
144
|
+
.build();
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
With dependencies, tags, middleware, metadata, and result schema:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
const tmw = r.middleware
|
|
151
|
+
.task("tmw.wrap")
|
|
152
|
+
.run(async ({ next }) => {
|
|
153
|
+
const out = await next();
|
|
154
|
+
return out;
|
|
155
|
+
})
|
|
156
|
+
.build();
|
|
157
|
+
|
|
158
|
+
const calc = r
|
|
159
|
+
.task("tasks.calc")
|
|
160
|
+
.dependencies({ adder })
|
|
161
|
+
.tags([])
|
|
162
|
+
.middleware([tmw])
|
|
163
|
+
.resultSchema(Number)
|
|
164
|
+
.meta({ title: "Calculator" } as any)
|
|
165
|
+
.run(async (n: number, deps) => deps.adder({ a: n, b: 1 }))
|
|
166
|
+
.build();
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Events and Hooks
|
|
172
|
+
|
|
173
|
+
Events:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const userCreated = r
|
|
177
|
+
.event("events.userCreated")
|
|
178
|
+
.payloadSchema({ id: String })
|
|
179
|
+
.tags([])
|
|
180
|
+
.meta({ title: "User Created" } as any)
|
|
181
|
+
.build();
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Hooks:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const listener = r
|
|
188
|
+
.hook("hooks.audit")
|
|
189
|
+
.on(userCreated)
|
|
190
|
+
.order(10)
|
|
191
|
+
.dependencies({})
|
|
192
|
+
.tags([])
|
|
193
|
+
.meta({ title: "Audit" } as any)
|
|
194
|
+
.run(async (ev) => {
|
|
195
|
+
// ev.id, ev.data.id
|
|
196
|
+
})
|
|
197
|
+
.build();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Hooks also support selector-style subscriptions:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const subtreeListener = r
|
|
204
|
+
.hook("hooks.subtree")
|
|
205
|
+
.on(r.subtreeOf(featureResource))
|
|
206
|
+
.run(async (event) => {
|
|
207
|
+
// selector-based hooks trade payload autocomplete for broader matching
|
|
208
|
+
console.log(event.id);
|
|
209
|
+
})
|
|
210
|
+
.build();
|
|
211
|
+
|
|
212
|
+
const predicateListener = r
|
|
213
|
+
.hook("hooks.predicate")
|
|
214
|
+
.on((event) => auditTag.exists(event))
|
|
215
|
+
.run(async (event) => {
|
|
216
|
+
console.log(event.id);
|
|
217
|
+
})
|
|
218
|
+
.build();
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Selector notes:
|
|
222
|
+
|
|
223
|
+
- selectors resolve once at bootstrap against registered events
|
|
224
|
+
- selector matches are narrowed to events visible to the hook
|
|
225
|
+
- exact event refs still fail fast when visibility is violated
|
|
226
|
+
- arrays may mix exact events, `r.subtreeOf(...)`, and predicates, but `"*"` must stay standalone
|
|
227
|
+
|
|
228
|
+
Register and emit via a resource:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const app = resource({ id: "events.app", register: [userCreated, listener] });
|
|
232
|
+
const rr = await run(app);
|
|
233
|
+
await rr.emitEvent(userCreated, { id: "u1" });
|
|
234
|
+
await rr.dispose();
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Async Contexts
|
|
240
|
+
|
|
241
|
+
Use `r.asyncContext(id)` for request-local business state such as tenant ids, auth claims, locale, or request metadata.
|
|
242
|
+
|
|
243
|
+
Builder chain example:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
const requestContextShape = Match.Object({
|
|
247
|
+
requestId: Match.NonEmptyString,
|
|
248
|
+
tenantId: Match.NonEmptyString,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const requestContext = r
|
|
252
|
+
.asyncContext("requestContext")
|
|
253
|
+
.schema(requestContextShape)
|
|
254
|
+
.serialize((value) => JSON.stringify(value))
|
|
255
|
+
.parse((raw) => requestContextShape.parse(JSON.parse(raw)))
|
|
256
|
+
.meta({
|
|
257
|
+
title: "Request Context",
|
|
258
|
+
description: "Per-request business metadata",
|
|
259
|
+
})
|
|
260
|
+
.build();
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Runtime usage:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
const auditTask = r
|
|
267
|
+
.task("tasks.audit")
|
|
268
|
+
.dependencies({ requestContext })
|
|
269
|
+
.middleware([requestContext.require()])
|
|
270
|
+
.run(async (_input, { requestContext }) => {
|
|
271
|
+
return requestContext.use().requestId;
|
|
272
|
+
})
|
|
273
|
+
.build();
|
|
274
|
+
|
|
275
|
+
const app = r
|
|
276
|
+
.resource("app.async-context")
|
|
277
|
+
.register([requestContext, auditTask])
|
|
278
|
+
.build();
|
|
279
|
+
|
|
280
|
+
const runtime = await run(app);
|
|
281
|
+
|
|
282
|
+
await requestContext.provide(
|
|
283
|
+
{ requestId: "req_1", tenantId: "acme" },
|
|
284
|
+
() => runtime.runTask(auditTask),
|
|
285
|
+
);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Key rules:
|
|
289
|
+
|
|
290
|
+
- `.schema()` is an alias for `.configSchema()` and validates values when `provide(...)` is called.
|
|
291
|
+
- Call `.schema()` before custom `.serialize()` or `.parse()`; schema rebinding after transport callbacks is rejected.
|
|
292
|
+
- `.meta()` attaches docs/tooling metadata.
|
|
293
|
+
- `.build()` returns the accessor with `use()`, `tryUse()`, `has()`, `provide()`, `require()`, and `optional()`.
|
|
294
|
+
- Register the built context before injecting it as a required dependency.
|
|
295
|
+
- Use `ctx.optional()` when the dependency may not be registered in a given app.
|
|
296
|
+
- Default serialization uses Runner's serializer; customize it only when a transport boundary needs a stable wire format.
|
|
297
|
+
|
|
298
|
+
Optional dependency example:
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
const maybeAudit = r
|
|
302
|
+
.task("tasks.maybeAudit")
|
|
303
|
+
.dependencies({ requestContext: requestContext.optional() })
|
|
304
|
+
.run(async (_input, { requestContext }) => {
|
|
305
|
+
return requestContext?.tryUse()?.requestId;
|
|
306
|
+
})
|
|
307
|
+
.build();
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Middleware Builders
|
|
313
|
+
|
|
314
|
+
Task middleware:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
const tmw = r.middleware
|
|
318
|
+
.task("tmw.log")
|
|
319
|
+
.dependencies({})
|
|
320
|
+
.configSchema({ level: Match.OneOf("info", "warn", "error") })
|
|
321
|
+
.tags([])
|
|
322
|
+
.meta({ title: "TaskLogger" } as any)
|
|
323
|
+
.run(async ({ next, task }, _deps, config) => {
|
|
324
|
+
return next(task.input);
|
|
325
|
+
})
|
|
326
|
+
.build();
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Resource middleware:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
const rmw = r.middleware
|
|
333
|
+
.resource("rmw.wrap")
|
|
334
|
+
.dependencies({})
|
|
335
|
+
.configSchema({ ttl: Match.Optional(Number) })
|
|
336
|
+
.tags([])
|
|
337
|
+
.meta({ title: "ResourceWrapper" } as any)
|
|
338
|
+
.run(async ({ next }) => next())
|
|
339
|
+
.build();
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Attach to resources or tasks via `.middleware([mw])`.
|
|
343
|
+
|
|
344
|
+
For owner-scoped auto-application and governance, use resource subtree policies:
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
const app = r
|
|
348
|
+
.resource("app")
|
|
349
|
+
.subtree({
|
|
350
|
+
tasks: { middleware: [tmw] },
|
|
351
|
+
resources: { middleware: [rmw] },
|
|
352
|
+
hooks: {
|
|
353
|
+
validate: (hook) =>
|
|
354
|
+
hook.meta?.title
|
|
355
|
+
? []
|
|
356
|
+
: [
|
|
357
|
+
{
|
|
358
|
+
code: "missing-meta-title",
|
|
359
|
+
message: "Hook meta.title required",
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
},
|
|
363
|
+
taskMiddleware: { validate: (mw) => [] },
|
|
364
|
+
resourceMiddleware: { validate: (mw) => [] },
|
|
365
|
+
events: { validate: (event) => [] },
|
|
366
|
+
tags: { validate: (tag) => [] },
|
|
367
|
+
})
|
|
368
|
+
.build();
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Conditional subtree middleware entries are also supported:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
const appWithConditional = r
|
|
375
|
+
.resource("app.conditional")
|
|
376
|
+
.subtree({
|
|
377
|
+
tasks: {
|
|
378
|
+
middleware: [
|
|
379
|
+
{
|
|
380
|
+
use: tmw.with({ mode: "strict" }),
|
|
381
|
+
when: (task) => task.tags.some((tag) => tag.id === "app.tags.strict"),
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
},
|
|
385
|
+
})
|
|
386
|
+
.build();
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
If subtree middleware and local middleware resolve to the same middleware id on one target, Runner fails fast instead of letting the local middleware override it.
|
|
390
|
+
|
|
391
|
+
Use `taskRunner.intercept(interceptor, { when })` for cross-cutting catch-all task interception.
|
|
392
|
+
|
|
393
|
+
Note on `.init()`:
|
|
394
|
+
|
|
395
|
+
- `.init` uses the classic `(config, deps, resourceContext)` signature; destructure inside the body when needed.
|
|
396
|
+
- If you skip seeding a config type, annotate the first argument in `init` and the builder will adopt that type.
|
|
397
|
+
|
|
398
|
+
Note on `.middleware()` and `.tags()`:
|
|
399
|
+
|
|
400
|
+
- You can pass a single item or an array.
|
|
401
|
+
- `.middleware()` and `.tags()` append by default; pass `{ override: true }` to replace the existing list.
|
|
402
|
+
|
|
403
|
+
Append vs override for `.middleware()` and `.overrides()`:
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
// Resource middleware append and override
|
|
407
|
+
const rmid = r.middleware
|
|
408
|
+
.resource("mw.rid")
|
|
409
|
+
.run(async ({ next }) => next())
|
|
410
|
+
.build();
|
|
411
|
+
const app = r
|
|
412
|
+
.resource("app.mw")
|
|
413
|
+
.middleware([rmid]) // append
|
|
414
|
+
.middleware([rmid], { override: true }) // replace
|
|
415
|
+
.build();
|
|
416
|
+
|
|
417
|
+
// Task middleware append
|
|
418
|
+
const tmid = r.middleware
|
|
419
|
+
.task("mw.tid")
|
|
420
|
+
.run(async ({ next, task }) => next(task.input))
|
|
421
|
+
.build();
|
|
422
|
+
const t = r
|
|
423
|
+
.task("tasks.calc")
|
|
424
|
+
.middleware([tmid]) // append
|
|
425
|
+
.build();
|
|
426
|
+
|
|
427
|
+
// Resource overrides append and override
|
|
428
|
+
const res = r
|
|
429
|
+
.resource("app.overrides")
|
|
430
|
+
.overrides([someResource]) // append
|
|
431
|
+
.overrides([anotherResource], { override: true }) // replace
|
|
432
|
+
.build();
|
|
433
|
+
|
|
434
|
+
// Tags append and override
|
|
435
|
+
const tagA = r.tag("tag.A").build();
|
|
436
|
+
const tagB = r.tag("tag.B").build();
|
|
437
|
+
const tagged = r
|
|
438
|
+
.task("app.tags")
|
|
439
|
+
.tags([tagA]) // append
|
|
440
|
+
.tags([tagB]) // append
|
|
441
|
+
// .tags([tagB], { override: true }) // replace
|
|
442
|
+
.build();
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## Running
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
const app = r
|
|
451
|
+
.resource("app")
|
|
452
|
+
.register([])
|
|
453
|
+
.init(async () => "OK")
|
|
454
|
+
.build();
|
|
455
|
+
const rr = await run(app);
|
|
456
|
+
const value = rr.value; // "OK"
|
|
457
|
+
await rr.dispose();
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Tasks from runtime:
|
|
461
|
+
|
|
462
|
+
```ts
|
|
463
|
+
const task = r
|
|
464
|
+
.task("t")
|
|
465
|
+
.run(async (n: number) => n + 1)
|
|
466
|
+
.build();
|
|
467
|
+
const root = resource({ id: "root", register: [task] });
|
|
468
|
+
const rr = await run(root);
|
|
469
|
+
const result = await rr.runTask(task, 1); // 2
|
|
470
|
+
await rr.dispose();
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## Type Safety Highlights
|
|
476
|
+
|
|
477
|
+
- Builder generics propagate across the chain: config, value/result, dependencies, context, meta, tags, and middleware are strongly typed.
|
|
478
|
+
- You can pre-seed a resource's config type at the entry point: `r.resource<MyConfig>(id)` — this provides typed `config` for `.dependencies((config) => ...)` and `.register((config) => ...)` callables.
|
|
479
|
+
- Resource `.init` follows `(config, deps, resourceContext)`; task `.run` still supports the object-style helper `({ input, deps })` and will adopt the typed first parameter when you skip `.configSchema()`.
|
|
480
|
+
- Tags and middleware must be registered; otherwise, sanity checks will fail at runtime. Builders keep tag and middleware types intact for compile-time checks.
|
|
481
|
+
- Schemas can be passed as plain objects with `parse` or libraries like `zod`—inference will flow accordingly.
|
|
482
|
+
|
|
483
|
+
Cheat sheet:
|
|
484
|
+
|
|
485
|
+
- Resource `.register()` accepts item | item[] | (config) => item | item[]
|
|
486
|
+
- Default = append; `{ override: true }` replaces prior registrations
|
|
487
|
+
- Tags and middleware accept single or array; repeated calls append by default; pass `{ override: true }` to replace
|
|
488
|
+
- Always call `.build()` and register built definitions
|
|
489
|
+
|
|
490
|
+
For deeper contract tests, see `src/__tests__/typesafety.test.ts` and the builder tests under `src/__tests__/definers/`.
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## Migration Notes
|
|
495
|
+
|
|
496
|
+
- Existing `defineX` APIs remain. Builders are sugar and compile to the same definitions.
|
|
497
|
+
- Import the single namespace `r` for all builders. You can still import `resource`, `task`, etc., to reference classic definitions or for mixing.
|
|
498
|
+
- No runtime overhead, no breaking changes. Builders are fully tree-shakeable.
|
|
499
|
+
Dependencies append examples:
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
// Resource deps: function + object merge
|
|
503
|
+
const app = r
|
|
504
|
+
.resource("app.deps")
|
|
505
|
+
.dependencies((cfg) => ({ svcA }))
|
|
506
|
+
.dependencies({ svcB }) // merged with previous
|
|
507
|
+
.build();
|
|
508
|
+
|
|
509
|
+
// Task deps: append and override
|
|
510
|
+
const t = r
|
|
511
|
+
.task("tasks.t")
|
|
512
|
+
.dependencies({ a })
|
|
513
|
+
.dependencies({ b }) // append
|
|
514
|
+
.dependencies({ c }, { override: true }) // replace with only c
|
|
515
|
+
.build();
|
|
516
|
+
|
|
517
|
+
// Hook deps
|
|
518
|
+
const hk = r
|
|
519
|
+
.hook("hooks.h")
|
|
520
|
+
.on(ev)
|
|
521
|
+
.dependencies({ a })
|
|
522
|
+
.dependencies({ b })
|
|
523
|
+
.build();
|
|
524
|
+
```
|