@btravstack/temporal-worker 0.11.0 → 0.13.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/README.md +48 -15
- package/dist/index.cjs +133 -36
- package/dist/index.d.cts +200 -58
- package/dist/index.d.mts +200 -58
- package/dist/index.mjs +133 -37
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -36,9 +36,9 @@ class DuplicateOrder extends TaggedError("DuplicateOrder")<{ readonly id: string
|
|
|
36
36
|
class InvalidOrderId extends TaggedError("InvalidOrderId")<{ readonly id: string }> {}
|
|
37
37
|
class InvalidQuantity extends TaggedError("InvalidQuantity")<{ readonly id: string }> {}
|
|
38
38
|
declare const TenantId: (value: string) => string;
|
|
39
|
+
class Tenant extends Port("Tenant")<string> {}
|
|
39
40
|
class PlaceOrder extends Port("PlaceOrder")<{
|
|
40
41
|
readonly execute: (
|
|
41
|
-
tenantId: string,
|
|
42
42
|
id: string,
|
|
43
43
|
quantity: number,
|
|
44
44
|
) => AsyncResult<
|
|
@@ -46,7 +46,9 @@ class PlaceOrder extends Port("PlaceOrder")<{
|
|
|
46
46
|
DuplicateOrder | InvalidOrderId | InvalidQuantity
|
|
47
47
|
>;
|
|
48
48
|
}> {}
|
|
49
|
-
|
|
49
|
+
// The application's own vertical, composed INSIDE the unit: its use case is
|
|
50
|
+
// bound to whichever tenant the attempt named.
|
|
51
|
+
declare const OrderApplicationModule: Module<PlaceOrder, never, Tenant>;
|
|
50
52
|
declare const OrderPersistenceModule: AnyModule;
|
|
51
53
|
|
|
52
54
|
// The contract this README's worker serves — one workflow, one activity, one
|
|
@@ -57,7 +59,7 @@ const contract = defineContract({
|
|
|
57
59
|
placeOrder: defineWorkflow({
|
|
58
60
|
input: z.object({ tenantId: z.uuidv7(), orderId: z.uuidv7(), quantity: z.number() }),
|
|
59
61
|
output: z.object({ id: z.uuidv7() }),
|
|
60
|
-
|
|
62
|
+
startPolicy: "allow-duplicate",
|
|
61
63
|
activities: {
|
|
62
64
|
place: defineActivity({
|
|
63
65
|
input: z.object({ tenantId: z.uuidv7(), orderId: z.uuidv7(), quantity: z.number() }),
|
|
@@ -75,30 +77,49 @@ const contract = defineContract({
|
|
|
75
77
|
});
|
|
76
78
|
|
|
77
79
|
const AppModule = Module("App")({
|
|
78
|
-
imports: [
|
|
79
|
-
exports: [
|
|
80
|
+
imports: [OrderPersistenceModule, observability(), otel()],
|
|
81
|
+
exports: [Logger],
|
|
80
82
|
});
|
|
81
83
|
-->
|
|
82
84
|
|
|
83
85
|
```ts
|
|
84
86
|
import { runMain, Logger } from "@btravstack/core";
|
|
87
|
+
import { Provider } from "@btravstack/di";
|
|
85
88
|
import {
|
|
89
|
+
ActivityInput,
|
|
86
90
|
TemporalActivities,
|
|
87
91
|
TemporalModule,
|
|
88
92
|
} from "@btravstack/temporal-worker";
|
|
89
93
|
import { P } from "unthrown";
|
|
90
94
|
|
|
91
|
-
// The
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
+
// The module forked around every activity attempt, SEEDED with the validated
|
|
96
|
+
// input on `ActivityInput(contract)`. The tenant the contract carries is
|
|
97
|
+
// claimed here once — not at each activity — and the application's vertical
|
|
98
|
+
// is composed over it, so `PlaceOrder` is bound before any activity runs.
|
|
99
|
+
const ActivityUnit = Module("ActivityUnit")({
|
|
100
|
+
needs: [ActivityInput(contract)],
|
|
101
|
+
imports: [OrderApplicationModule],
|
|
102
|
+
provides: [
|
|
103
|
+
Provider(Tenant)({
|
|
104
|
+
inject: { input: ActivityInput(contract) },
|
|
105
|
+
sync: ({ input }) => TenantId(input.tenantId),
|
|
106
|
+
}),
|
|
107
|
+
],
|
|
108
|
+
exports: [Tenant, PlaceOrder],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// The application's half: its activities, on the starter's own activities
|
|
112
|
+
// port, typed by the contract (a worker serves one activities record, so
|
|
113
|
+
// there is nothing to name). What the leaves read off `context.unit` is
|
|
114
|
+
// declared once, beside `inject`.
|
|
95
115
|
const orderActivities = TemporalActivities(contract)({
|
|
96
|
-
inject: {
|
|
97
|
-
|
|
116
|
+
inject: {},
|
|
117
|
+
unit: { place: PlaceOrder },
|
|
118
|
+
sync: () => ({
|
|
98
119
|
placeOrder: {
|
|
99
|
-
place: ({ errors, input }) =>
|
|
100
|
-
place
|
|
101
|
-
.execute(
|
|
120
|
+
place: ({ errors, context, input }) =>
|
|
121
|
+
context.unit.place
|
|
122
|
+
.execute(input.orderId, input.quantity)
|
|
102
123
|
.mapErrCases((matcher) =>
|
|
103
124
|
matcher
|
|
104
125
|
.with(P.tag("DuplicateOrder"), (error) =>
|
|
@@ -124,6 +145,7 @@ const OrderWorker = TemporalModule("OrderWorker")({
|
|
|
124
145
|
workflowsPath: workflowsPathFromURL(import.meta.url, "./workflows.js"),
|
|
125
146
|
},
|
|
126
147
|
imports: [AppModule],
|
|
148
|
+
unit: { activity: ActivityUnit },
|
|
127
149
|
});
|
|
128
150
|
|
|
129
151
|
await runMain(OrderWorker);
|
|
@@ -140,7 +162,7 @@ startup `Err`, not a defect. `runtimeInfo()` reads `{ taskQueue, namespace }`
|
|
|
140
162
|
back once the worker is polling.
|
|
141
163
|
|
|
142
164
|
A worker polling for several workflows can be several slices instead of one
|
|
143
|
-
record: `TemporalWorkflowActivities(contract, key)({ inject: { name: Dep },
|
|
165
|
+
record: `TemporalWorkflowActivities(contract, key)({ inject: { name: Dep }, unit, sync })` mints a
|
|
144
166
|
provider for ONE workflow's activities (or a contract-global activity),
|
|
145
167
|
typed by the key alone, and `TemporalActivities(contract)([...])` composes an
|
|
146
168
|
array of them into the same activities provider `TemporalModule` takes — the
|
|
@@ -153,6 +175,16 @@ di's duplicate-provider defect at build once **both** pieces are discharged as
|
|
|
153
175
|
providers in one graph — wire in only one and the other's implementation is
|
|
154
176
|
silently never registered.
|
|
155
177
|
|
|
178
|
+
A piece's `unit` names the ports its activities read off `context.unit`,
|
|
179
|
+
resolved out of the per-attempt fork of the module bound on `unit.activity` —
|
|
180
|
+
which is seeded with the validated input on `ActivityInput(contract)`, so a unit
|
|
181
|
+
module derives a tenant from the invocation rather than from an ambient record.
|
|
182
|
+
The record arm above takes the same `unit:` — one record for every entry in it —
|
|
183
|
+
so a worker that has not outgrown one function reaches `context.unit` without
|
|
184
|
+
slicing first. The root is checked against what was declared either way: bind a
|
|
185
|
+
module that does not export one of those ports and `TemporalModule` refuses it,
|
|
186
|
+
naming the port.
|
|
187
|
+
|
|
156
188
|
Its own test suite needs a **Docker daemon**: the specs run against the shared
|
|
157
189
|
`temporalio/auto-setup` container `internal/test-infra` starts once per machine and every
|
|
158
190
|
workspace reuses. Nothing else here does.
|
|
@@ -184,6 +216,7 @@ supplies it:
|
|
|
184
216
|
| `namespace` | pins the namespace instead of reading `TEMPORAL_NAMESPACE` |
|
|
185
217
|
| `gracePeriod` | pins `TEMPORAL_GRACE_PERIOD_MS` — Temporal's `shutdownGraceTime`, a `Duration` (default `10_000` ms) |
|
|
186
218
|
| `forceAfter` | pins `TEMPORAL_FORCE_AFTER_MS` — Temporal's `shutdownForceTime` (default `15_000` ms); keep it under `DRAIN_TIMEOUT_MS` |
|
|
219
|
+
| `unit` | `{ activity }` — the module the worker forks around every activity it dispatches, seeded with the validated input |
|
|
187
220
|
|
|
188
221
|
The full table — required/optional, defaults, and the reasoning — lives on
|
|
189
222
|
[the reference page](https://btravstack.github.io/btravstack/reference/temporal-worker),
|
package/dist/index.cjs
CHANGED
|
@@ -7,10 +7,123 @@ let _temporalio_common = require("@temporalio/common");
|
|
|
7
7
|
let _temporalio_worker = require("@temporalio/worker");
|
|
8
8
|
let unthrown = require("unthrown");
|
|
9
9
|
let _temporalio_activity = require("@temporalio/activity");
|
|
10
|
+
//#region src/unit.ts
|
|
11
|
+
/**
|
|
12
|
+
* Where `activityUnits` leaves the forked context for the piece wrappers to
|
|
13
|
+
* read. A symbol rather than a name: the middleware merges it into the context
|
|
14
|
+
* every activity sees, and a string key would appear on the record an activity
|
|
15
|
+
* written against the `{ inject, sync }` arm destructures.
|
|
16
|
+
*/
|
|
17
|
+
const UNIT_SCOPE = Symbol("@btravstack/temporal-worker/unit-scope");
|
|
18
|
+
/**
|
|
19
|
+
* The declared record, as a getter per name resolved on read out of the fork.
|
|
20
|
+
* Neither writable nor configurable — an activity reads what the fork holds, and
|
|
21
|
+
* cannot reshape the record under the next attempt. With no `unit.activity`
|
|
22
|
+
* bound there is nothing to resolve from, so the record is empty whatever was
|
|
23
|
+
* declared.
|
|
24
|
+
*/
|
|
25
|
+
const unitRecordOf = (forked, record) => {
|
|
26
|
+
const unit = {};
|
|
27
|
+
if (forked === void 0) return unit;
|
|
28
|
+
for (const [name, port] of Object.entries(record)) Object.defineProperty(unit, name, {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
get: () => forked.get(port)
|
|
31
|
+
});
|
|
32
|
+
return unit;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* One entry of the activities record — a contract-global activity's own
|
|
36
|
+
* implementation, or the record of them a workflow key carries — with its
|
|
37
|
+
* piece's declared record put on `context.unit`. Applied once per piece as di
|
|
38
|
+
* constructs it, so an attempt costs one record and one context object.
|
|
39
|
+
*/
|
|
40
|
+
const withUnit = (record, entry) => {
|
|
41
|
+
const wrap = (implementation) => (helpers, input) => {
|
|
42
|
+
const { [UNIT_SCOPE]: forked, ...rest } = helpers.context ?? {};
|
|
43
|
+
return implementation({
|
|
44
|
+
...helpers,
|
|
45
|
+
context: {
|
|
46
|
+
...rest,
|
|
47
|
+
unit: unitRecordOf(forked, record)
|
|
48
|
+
}
|
|
49
|
+
}, input);
|
|
50
|
+
};
|
|
51
|
+
return typeof entry === "function" ? wrap(entry) : Object.fromEntries(Object.entries(entry).map(([name, implementation]) => [name, wrap(implementation)]));
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/workflow-activities.ts
|
|
55
|
+
/** One id, declared once, so no contract instantiating it warns about a duplicate. */
|
|
56
|
+
const ActivityInputPort = (0, _btravstack_di.Port)("ActivityInput");
|
|
57
|
+
/**
|
|
58
|
+
* The validated activity input as a port: the one thing the worker seeds the
|
|
59
|
+
* fork with, so a `unit.activity` module derives a tenant — or anything else —
|
|
60
|
+
* from the invocation rather than from an ambient record.
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* const Input = ActivityInput(orderContract);
|
|
64
|
+
* const ActivityUnit = Module("ActivityUnit")({
|
|
65
|
+
* needs: [Input],
|
|
66
|
+
* provides: [Provider(Tenant)({ inject: { input: Input }, sync: ({ input }) => input.tenantId })],
|
|
67
|
+
* exports: [Tenant],
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* `contract` is read for its TYPE only. One `Port(...)` call fixed per contract
|
|
72
|
+
* at the type level, the move `TemporalActivitiesPort` makes, so a module built
|
|
73
|
+
* for one contract cannot read another's input.
|
|
74
|
+
*/
|
|
75
|
+
const ActivityInput = (contract) => {
|
|
76
|
+
return ActivityInputPort;
|
|
77
|
+
};
|
|
78
|
+
/** The prefix a piece's port id carries; the composing form strips it to recover the key. */
|
|
79
|
+
const WORKFLOW_ACTIVITIES_PREFIX = "TemporalWorkflowActivities:";
|
|
80
|
+
/**
|
|
81
|
+
* One workflow's activities, as a provider on a port of its own.
|
|
82
|
+
*
|
|
83
|
+
* A worker that polls one queue for several workflows is several pieces, each
|
|
84
|
+
* declaring the services its own activities call;
|
|
85
|
+
* `TemporalActivities(contract)([...])` composes them. `contract` is read for
|
|
86
|
+
* its TYPE only, and types both `key` and the record — so an activity the
|
|
87
|
+
* workflow does not declare is a compile error here rather than at startup.
|
|
88
|
+
*
|
|
89
|
+
* `key` is any top-level key of the activities record, which includes a
|
|
90
|
+
* contract-global activity as well as a workflow; the name is imprecise in that
|
|
91
|
+
* one case deliberately, since narrowing it would lock such a contract out of
|
|
92
|
+
* the split.
|
|
93
|
+
*
|
|
94
|
+
* There is no name to give: the key IS the port's name.
|
|
95
|
+
*
|
|
96
|
+
* `unit` declares the ports the activities read off `context.unit`, resolved
|
|
97
|
+
* out of the fork the attempt opened; the root's `unit.activity` module must
|
|
98
|
+
* export every one of them.
|
|
99
|
+
*/
|
|
100
|
+
const TemporalWorkflowActivities = (contract, key) => {
|
|
101
|
+
const port = class extends (0, _btravstack_di.Port)(`${WORKFLOW_ACTIVITIES_PREFIX}${key}`) {};
|
|
102
|
+
return (options) => {
|
|
103
|
+
const record = options.unit ?? {};
|
|
104
|
+
return Object.assign((0, _btravstack_di.Provider)(port)({
|
|
105
|
+
inject: options.inject,
|
|
106
|
+
sync: (services) => withUnit(record, options.sync(services))
|
|
107
|
+
}), { unit: record });
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
10
111
|
//#region src/activity-units.ts
|
|
11
112
|
/**
|
|
12
|
-
* Open one kernel unit per activity attempt
|
|
13
|
-
*
|
|
113
|
+
* Open one kernel unit per activity attempt, forking `unit` — when one is
|
|
114
|
+
* bound — after the activity is invoked, before it runs; the fork is torn down
|
|
115
|
+
* when the unit closes. It is seeded with the validated input on
|
|
116
|
+
* `ActivityInput(contract)`, so a unit module derives a tenant from the
|
|
117
|
+
* invocation rather than reading one off an ambient record. With no `unit`
|
|
118
|
+
* bound, `next()` runs unchanged, so the ambient `currentUnit()` record is what
|
|
119
|
+
* an adapter reads.
|
|
120
|
+
*
|
|
121
|
+
* The forked context rides the invocation's context under {@link UNIT_SCOPE},
|
|
122
|
+
* where each piece's own wrapper turns it into the typed `context.unit` record
|
|
123
|
+
* that piece declared — the records live with the pieces, so a hand-composed
|
|
124
|
+
* `temporal()` gets them without threading a second option through the starter,
|
|
125
|
+
* and the middleware never has to map Temporal's FLAT activity name back to the
|
|
126
|
+
* workflow key its piece was minted under.
|
|
14
127
|
*
|
|
15
128
|
* **That includes the kernel's per-unit `AbortSignal`**, and it is the only
|
|
16
129
|
* route to it: an activity has no parameter to receive one through. Temporal's
|
|
@@ -20,13 +133,13 @@ let _temporalio_activity = require("@temporalio/activity");
|
|
|
20
133
|
* No `Result`-unwrapping boundary, deliberately: `declareActivitiesHandler` owns
|
|
21
134
|
* the mapping from a settled `Result` to an activity failure.
|
|
22
135
|
*/
|
|
23
|
-
const activityUnits = (host, observers) => (
|
|
136
|
+
const activityUnits = (host, observers, unit) => (invocation, next) => {
|
|
24
137
|
const settle = (0, _btravstack_core.observe)(observers, {
|
|
25
138
|
component: "temporal",
|
|
26
139
|
name: "attempt",
|
|
27
140
|
attributes: { activity: (0, _temporalio_activity.activityInfo)().activityType }
|
|
28
141
|
});
|
|
29
|
-
return host.run(metaFor(), () => next()).tap(() => settle({ outcome: "ok" })).tapFailure((failure) => settle({
|
|
142
|
+
return host.run(metaFor(), (scope) => unit === void 0 ? next() : scope.fork(unit, [[ActivityInputPort, invocation.input]]).flatMap((forked) => next({ context: { [UNIT_SCOPE]: forked } }))).tap(() => settle({ outcome: "ok" })).tapFailure((failure) => settle({
|
|
30
143
|
outcome: "error",
|
|
31
144
|
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
32
145
|
}));
|
|
@@ -164,7 +277,7 @@ const createWorker = (host, connection, config, activities, options, observers)
|
|
|
164
277
|
const { namespace } = config;
|
|
165
278
|
return (0, unthrown.fromThrowable)(() => (0, _temporal_contract_worker_activity.declareActivitiesHandler)({
|
|
166
279
|
contract: options.contract,
|
|
167
|
-
middleware: activityUnits(host, observers),
|
|
280
|
+
middleware: activityUnits(host, observers, options.unit?.activity),
|
|
168
281
|
activities
|
|
169
282
|
}), startFailed)().toAsync().flatMap((handler) => (0, unthrown.fromPromise)(_temporalio_worker.Worker.create({
|
|
170
283
|
connection,
|
|
@@ -200,30 +313,6 @@ const poll = (worker, taskQueue, namespace) => {
|
|
|
200
313
|
};
|
|
201
314
|
};
|
|
202
315
|
//#endregion
|
|
203
|
-
//#region src/workflow-activities.ts
|
|
204
|
-
/** The prefix a piece's port id carries; the composing form strips it to recover the key. */
|
|
205
|
-
const WORKFLOW_ACTIVITIES_PREFIX = "TemporalWorkflowActivities:";
|
|
206
|
-
/**
|
|
207
|
-
* One workflow's activities, as a provider on a port of its own.
|
|
208
|
-
*
|
|
209
|
-
* A worker that polls one queue for several workflows is several pieces, each
|
|
210
|
-
* declaring the services its own activities call;
|
|
211
|
-
* `TemporalActivities(contract)([...])` composes them. `contract` is read for
|
|
212
|
-
* its TYPE only, and types both `key` and the record — so an activity the
|
|
213
|
-
* workflow does not declare is a compile error here rather than at startup.
|
|
214
|
-
*
|
|
215
|
-
* `key` is any top-level key of the activities record, which includes a
|
|
216
|
-
* contract-global activity as well as a workflow; the name is imprecise in that
|
|
217
|
-
* one case deliberately, since narrowing it would lock such a contract out of
|
|
218
|
-
* the split.
|
|
219
|
-
*
|
|
220
|
-
* There is no name to give: the key IS the port's name.
|
|
221
|
-
*/
|
|
222
|
-
const TemporalWorkflowActivities = (contract, key) => {
|
|
223
|
-
const port = class extends (0, _btravstack_di.Port)(`${WORKFLOW_ACTIVITIES_PREFIX}${key}`) {};
|
|
224
|
-
return (0, _btravstack_di.Provider)(port);
|
|
225
|
-
};
|
|
226
|
-
//#endregion
|
|
227
316
|
//#region src/temporal-module.ts
|
|
228
317
|
/**
|
|
229
318
|
* `Module(name)({...})` for a Temporal worker deployment: everything a di module
|
|
@@ -263,12 +352,12 @@ const TemporalModule = (name) => (options) => {
|
|
|
263
352
|
* TemporalActivities(orderContract)([fulfillOrder, chargeOrder])
|
|
264
353
|
* ```
|
|
265
354
|
*
|
|
266
|
-
* The first is
|
|
267
|
-
*
|
|
268
|
-
* `TemporalWorkflowActivities(contract, key)` builds: they are
|
|
269
|
-
* deps, keyed by the contract key each piece's port id carries,
|
|
270
|
-
* record IS the activities record. Every key must be covered,
|
|
271
|
-
* claiming one key are di's duplicate-provider defect at build.
|
|
355
|
+
* The first is `{ inject, unit?, sync }`, whose `sync` hands back the whole
|
|
356
|
+
* activities record — one `unit:` record for every entry in it. The second
|
|
357
|
+
* takes the pieces `TemporalWorkflowActivities(contract, key)` builds: they are
|
|
358
|
+
* the provider's deps, keyed by the contract key each piece's port id carries,
|
|
359
|
+
* so the services record IS the activities record. Every key must be covered,
|
|
360
|
+
* and two slices claiming one key are di's duplicate-provider defect at build.
|
|
272
361
|
*/
|
|
273
362
|
const TemporalActivities = (contract) => {
|
|
274
363
|
const build = (0, _btravstack_di.Provider)(TemporalActivitiesPort);
|
|
@@ -276,9 +365,17 @@ const TemporalActivities = (contract) => {
|
|
|
276
365
|
inject: Object.fromEntries(pieces.map((piece) => [piece.port.portId.slice(27), piece.port])),
|
|
277
366
|
sync: (services) => services
|
|
278
367
|
});
|
|
279
|
-
|
|
368
|
+
const whole = (options) => {
|
|
369
|
+
const record = options.unit ?? {};
|
|
370
|
+
return build({
|
|
371
|
+
inject: options.inject,
|
|
372
|
+
sync: (services) => Object.fromEntries(Object.entries(options.sync(services)).map(([key, entry]) => [key, withUnit(record, entry)]))
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
return ((first) => Array.isArray(first) ? compose(first) : whole(first));
|
|
280
376
|
};
|
|
281
377
|
//#endregion
|
|
378
|
+
exports.ActivityInput = ActivityInput;
|
|
282
379
|
exports.TemporalActivities = TemporalActivities;
|
|
283
380
|
exports.TemporalConfig = TemporalConfig;
|
|
284
381
|
exports.TemporalConnection = TemporalConnection;
|