@btravstack/temporal-worker 0.3.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/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/index.cjs +261 -0
- package/dist/index.d.cts +219 -0
- package/dist/index.d.mts +219 -0
- package/dist/index.mjs +253 -0
- package/package.json +90 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benoit TRAVERS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @btravstack/temporal-worker
|
|
2
|
+
|
|
3
|
+
> The **worker half** of Temporal for [`@btravstack/core`](../core) โ starting
|
|
4
|
+
> workflows is a client's job, and belongs to `@temporal-contract/client`. One module
|
|
5
|
+
> providing the runtime, its configuration and its connection; one unit per
|
|
6
|
+
> activity attempt; and a drain that releases the kernel at the **kernel's**
|
|
7
|
+
> deadline, not Temporal's `shutdownForceTime`.
|
|
8
|
+
|
|
9
|
+
๐ **[Documentation](https://btravstack.github.io/btravstack/how-to/run-a-temporal-worker)** ยท
|
|
10
|
+
[Reference](https://btravstack.github.io/btravstack/reference/temporal-worker) ยท
|
|
11
|
+
[API Reference](https://btravstack.github.io/btravstack/api/temporal-worker/)
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pnpm add @btravstack/temporal-worker @btravstack/core @btravstack/config @btravstack/di unthrown \
|
|
15
|
+
@temporalio/worker @temporalio/activity @temporalio/common \
|
|
16
|
+
@temporal-contract/worker @temporal-contract/contract
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
All nine are peer dependencies โ install them. Node `>=20`.
|
|
20
|
+
|
|
21
|
+
## A worked example
|
|
22
|
+
|
|
23
|
+
<!-- doctest: prelude
|
|
24
|
+
import { defineActivity, defineContract, defineWorkflow } from "@temporal-contract/contract";
|
|
25
|
+
import { workflowsPathFromURL } from "@temporal-contract/worker/worker";
|
|
26
|
+
import { Module } from "@btravstack/di";
|
|
27
|
+
import { PlaceOrder, OrderRepository, Outbox, OrderApplicationModule } from "@btravstack/example-order-application";
|
|
28
|
+
import { OrderPersistenceModule } from "@btravstack/example-order-infrastructure";
|
|
29
|
+
import { TenantId } from "@btravstack/example-order-domain";
|
|
30
|
+
import { observability } from "@btravstack/observability";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
|
|
33
|
+
// The contract this README's worker serves โ one workflow, one activity, one
|
|
34
|
+
// modeled contract error.
|
|
35
|
+
const contract = defineContract({
|
|
36
|
+
taskQueue: "orders",
|
|
37
|
+
workflows: {
|
|
38
|
+
placeOrder: defineWorkflow({
|
|
39
|
+
input: z.object({ tenantId: z.uuidv7(), orderId: z.uuidv7(), quantity: z.number() }),
|
|
40
|
+
output: z.object({ id: z.uuidv7() }),
|
|
41
|
+
idempotency: "allow-duplicate",
|
|
42
|
+
activities: {
|
|
43
|
+
place: defineActivity({
|
|
44
|
+
input: z.object({ tenantId: z.uuidv7(), orderId: z.uuidv7(), quantity: z.number() }),
|
|
45
|
+
output: z.object({ id: z.uuidv7() }),
|
|
46
|
+
errors: {
|
|
47
|
+
OrderAlreadyPlaced: { data: z.object({ id: z.uuidv7() }), nonRetryable: true },
|
|
48
|
+
InvalidOrderId: { data: z.object({ id: z.string() }), nonRetryable: true },
|
|
49
|
+
InvalidQuantity: { data: z.object({ id: z.uuidv7() }), nonRetryable: true },
|
|
50
|
+
},
|
|
51
|
+
activityOptions: { startToCloseTimeout: "30 seconds" },
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const AppModule = Module("App")({
|
|
59
|
+
imports: [OrderApplicationModule, OrderPersistenceModule, observability()],
|
|
60
|
+
exports: [PlaceOrder, OrderRepository, Outbox, Logger],
|
|
61
|
+
});
|
|
62
|
+
-->
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { runMain, Logger } from "@btravstack/core";
|
|
66
|
+
import {
|
|
67
|
+
TemporalActivities,
|
|
68
|
+
TemporalModule,
|
|
69
|
+
} from "@btravstack/temporal-worker";
|
|
70
|
+
import { P } from "unthrown";
|
|
71
|
+
|
|
72
|
+
// The application's half: its activities, as a service built from the use
|
|
73
|
+
// cases it declares โ closures over them, no context read at call time โ on
|
|
74
|
+
// the starter's own activities port, typed by the contract (a worker serves
|
|
75
|
+
// one activities record, so there is nothing to name).
|
|
76
|
+
const orderActivities = TemporalActivities(contract)(
|
|
77
|
+
{ place: PlaceOrder },
|
|
78
|
+
{
|
|
79
|
+
sync: ({ place }) => ({
|
|
80
|
+
placeOrder: {
|
|
81
|
+
place: (args, { errors }) =>
|
|
82
|
+
place
|
|
83
|
+
.execute(TenantId(args.tenantId), args.orderId, args.quantity)
|
|
84
|
+
.mapErrCases((matcher) =>
|
|
85
|
+
matcher
|
|
86
|
+
.with(P.tag("DuplicateOrder"), (error) =>
|
|
87
|
+
errors.OrderAlreadyPlaced({ id: error.id }),
|
|
88
|
+
)
|
|
89
|
+
.with(P.tag("InvalidOrderId"), (error) =>
|
|
90
|
+
errors.InvalidOrderId({ id: error.id }),
|
|
91
|
+
)
|
|
92
|
+
.with(P.tag("InvalidQuantity"), (error) =>
|
|
93
|
+
errors.InvalidQuantity({ id: error.id }),
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// The composition root: a di module, plus the contract, the activities
|
|
102
|
+
// provider and the workflow source โ and nothing else to know.
|
|
103
|
+
const OrderWorker = TemporalModule("OrderWorker")({
|
|
104
|
+
contract,
|
|
105
|
+
activities: orderActivities,
|
|
106
|
+
workflows: {
|
|
107
|
+
workflowsPath: workflowsPathFromURL(import.meta.url, "./workflows.js"),
|
|
108
|
+
},
|
|
109
|
+
imports: [AppModule],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await runMain(OrderWorker);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`TemporalModule` imports the starter โ `TemporalRuntime`, `TemporalConfig`
|
|
116
|
+
bound from `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`) and
|
|
117
|
+
`TEMPORAL_NAMESPACE` (default `default`), `TemporalConnection` opened as a
|
|
118
|
+
resource of the graph and closed on every exit path โ provides the activities
|
|
119
|
+
and exports the runtime port. A service that will not answer is a modeled
|
|
120
|
+
`TemporalUnreachable`, exit `1`; the starter calls `declareActivitiesHandler`
|
|
121
|
+
itself, inside its own error qualifier, so a contract it cannot satisfy is a
|
|
122
|
+
startup `Err`, not a defect. `runtimeInfo()` reads `{ taskQueue, namespace }`
|
|
123
|
+
back once the worker is polling.
|
|
124
|
+
|
|
125
|
+
A worker polling for several workflows can be several slices instead of one
|
|
126
|
+
record: `TemporalWorkflowActivities(contract, key)({ name: Dep }, arm)` mints a
|
|
127
|
+
provider for ONE workflow's activities (or a contract-global activity),
|
|
128
|
+
typed by the key alone, and `TemporalActivities(contract)([...])` composes an
|
|
129
|
+
array of them into the same activities provider `TemporalModule` takes โ the
|
|
130
|
+
array must cover every key the contract declares, and each piece's own port
|
|
131
|
+
must still be discharged (`provides`), since the composed provider's deps are
|
|
132
|
+
the pieces' ports, not what they close over. Two slices claiming one key are
|
|
133
|
+
di's duplicate-provider defect at build, which is the point: a workflow's
|
|
134
|
+
activities belong to exactly one slice.
|
|
135
|
+
|
|
136
|
+
## Options
|
|
137
|
+
|
|
138
|
+
`TemporalModule(name)({...})` takes `temporal()`'s options plus `activities`
|
|
139
|
+
and the module lists (`imports`, `provides`, `exports`, `needs`). The
|
|
140
|
+
`temporal()` primitive does not take `activities` as an option โ it **needs**
|
|
141
|
+
the activities provider on its own port, which is how the composition root
|
|
142
|
+
supplies it:
|
|
143
|
+
|
|
144
|
+
| Option | What it is |
|
|
145
|
+
| ------------- | ------------------------------------------------------------------------------------------------------ |
|
|
146
|
+
| `contract` | the `temporal-contract` contract; the task queue this worker polls is read off it |
|
|
147
|
+
| `activities` | the activities provider โ what `TemporalActivities(contract)(...)` returns for this contract |
|
|
148
|
+
| `workflows` | where the sandbox's code comes from: `{ workflowsPath }` or `{ workflowBundle }` |
|
|
149
|
+
| `address` | pins the service instead of reading `TEMPORAL_ADDRESS` |
|
|
150
|
+
| `namespace` | pins the namespace instead of reading `TEMPORAL_NAMESPACE` |
|
|
151
|
+
| `gracePeriod` | Temporal's `shutdownGraceTime`, a `Duration` (default `"10 seconds"`) |
|
|
152
|
+
| `forceAfter` | Temporal's `shutdownForceTime`, a `Duration` (default `"15 seconds"`) โ keep it under `drainTimeoutMs` |
|
|
153
|
+
|
|
154
|
+
The full table โ required/optional, defaults, and the reasoning โ lives on
|
|
155
|
+
[the reference page](https://btravstack.github.io/btravstack/reference/temporal-worker),
|
|
156
|
+
which is this list's one detailed home.
|
|
157
|
+
|
|
158
|
+
## The drain
|
|
159
|
+
|
|
160
|
+
`worker.shutdown()` stops polling at once, but `run()` resolves only when the
|
|
161
|
+
last in-flight activity has finished โ on Temporal's own `shutdownForceTime`.
|
|
162
|
+
`Serving.drain(signal)` races that wait against the kernel's deadline and
|
|
163
|
+
returns at whichever comes first: the kernel gets its thread back on time, the
|
|
164
|
+
work is reported `abandoned`, and the worker keeps winding down on Temporal's
|
|
165
|
+
clock until the process exits. `@temporalio/worker` exposes no public forced
|
|
166
|
+
shutdown, so "stop waiting" is the escalation. `Result` โ activity failure is
|
|
167
|
+
`declareActivitiesHandler`'s and is deliberately not duplicated here. The rest
|
|
168
|
+
is on the [documentation site](https://btravstack.github.io/btravstack/reference/temporal-worker).
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
[MIT](./LICENSE) ยฉ Benoit TRAVERS
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _btravstack_di = require("@btravstack/di");
|
|
3
|
+
let _btravstack_config = require("@btravstack/config");
|
|
4
|
+
let _btravstack_core = require("@btravstack/core");
|
|
5
|
+
let _temporal_contract_worker_activity = require("@temporal-contract/worker/activity");
|
|
6
|
+
let _temporalio_worker = require("@temporalio/worker");
|
|
7
|
+
let unthrown = require("unthrown");
|
|
8
|
+
let _temporalio_activity = require("@temporalio/activity");
|
|
9
|
+
//#region src/activity-units.ts
|
|
10
|
+
/**
|
|
11
|
+
* Open one kernel unit per activity attempt. It injects nothing โ `next()`
|
|
12
|
+
* unchanged โ so the ambient `currentUnit()` record is what an adapter reads.
|
|
13
|
+
*
|
|
14
|
+
* **That includes the kernel's per-unit `AbortSignal`**, and it is the only
|
|
15
|
+
* route to it: an activity has no parameter to receive one through. Temporal's
|
|
16
|
+
* own `Context.current().cancellationSignal` is a DIFFERENT clock, so the two
|
|
17
|
+
* are honoured together rather than one standing in for the other.
|
|
18
|
+
*
|
|
19
|
+
* No `Result`-unwrapping boundary, deliberately: `declareActivitiesHandler` owns
|
|
20
|
+
* the mapping from a settled `Result` to an activity failure.
|
|
21
|
+
*/
|
|
22
|
+
const activityUnits = (host) => (_invocation, next) => host.run(metaFor(), () => next());
|
|
23
|
+
/**
|
|
24
|
+
* `UnitMeta.id` must be unique per unit, and a workflow id is **not** one: an
|
|
25
|
+
* activity is retried under the same execution. A task token identifies one
|
|
26
|
+
* attempt, so its uniqueness is Temporal's guarantee rather than ours.
|
|
27
|
+
*
|
|
28
|
+
* The workflow id becomes the `traceId` โ minted outside this process and steady
|
|
29
|
+
* across every retry, so all attempts join up in a log. An activity with no
|
|
30
|
+
* workflow falls back to the activity id.
|
|
31
|
+
*/
|
|
32
|
+
const metaFor = () => {
|
|
33
|
+
const info = (0, _temporalio_activity.activityInfo)();
|
|
34
|
+
return {
|
|
35
|
+
kind: "activity",
|
|
36
|
+
id: info.base64TaskToken,
|
|
37
|
+
traceId: info.workflowExecution?.workflowId ?? info.activityId
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/temporal-runtime.ts
|
|
42
|
+
/**
|
|
43
|
+
* Where the Temporal service is, as a service: `temporal()` binds it from the
|
|
44
|
+
* environment โ `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`) and
|
|
45
|
+
* `TEMPORAL_NAMESPACE` (default `default`) โ unless pinned, and anything else
|
|
46
|
+
* in the graph may read it.
|
|
47
|
+
*/
|
|
48
|
+
var TemporalConfig = class extends (0, _btravstack_di.Port)("TemporalConfig") {};
|
|
49
|
+
/**
|
|
50
|
+
* The connection, as a resource of the graph: di opens it with the scope and
|
|
51
|
+
* closes it on every exit path, startup failure included.
|
|
52
|
+
*/
|
|
53
|
+
var TemporalConnection = class extends (0, _btravstack_di.Port)("TemporalConnection") {};
|
|
54
|
+
/**
|
|
55
|
+
* The service at `TemporalConfig.address` did not answer. Modeled rather than
|
|
56
|
+
* left a defect because an operator *can* act on it โ the address is wrong or
|
|
57
|
+
* the service is down, and neither is a bug in this code โ so `runMain` exits
|
|
58
|
+
* `1`, a startup `Err`, not the `70` a defect earns.
|
|
59
|
+
*/
|
|
60
|
+
var TemporalUnreachable = class extends (0, unthrown.TaggedError)("TemporalUnreachable") {
|
|
61
|
+
message = `the Temporal service at ${this.address} did not answer`;
|
|
62
|
+
};
|
|
63
|
+
/** The runtime's port: what `temporal()` provides, and what the module `start` boots must export. */
|
|
64
|
+
var TemporalRuntime = class extends _btravstack_core.RuntimePort {};
|
|
65
|
+
/**
|
|
66
|
+
* The activities' port โ one id, the starter's own, which an application never
|
|
67
|
+
* names. Generic at the value level (one `Port(...)` call, so no duplicate-id
|
|
68
|
+
* warning however many contracts instantiate it) and fixed per contract at the
|
|
69
|
+
* type level through `ActivitiesPortOf<C>`, so a provider built for one contract
|
|
70
|
+
* cannot be handed to a module declaring another. Exported for this package's
|
|
71
|
+
* tests, not from `index.ts`.
|
|
72
|
+
*/
|
|
73
|
+
const TemporalActivitiesPort = (0, _btravstack_di.Port)("TemporalActivities");
|
|
74
|
+
const DEFAULT_GRACE = "10 seconds";
|
|
75
|
+
const DEFAULT_FORCE = "15 seconds";
|
|
76
|
+
/**
|
|
77
|
+
* The Temporal starter: a module providing the runtime, its configuration
|
|
78
|
+
* (bound from `TEMPORAL_ADDRESS` / `TEMPORAL_NAMESPACE` unless pinned) and the
|
|
79
|
+
* connection (a resource opened with the scope and closed with it; a service
|
|
80
|
+
* that will not answer is a modeled `TemporalUnreachable`). Import it next to
|
|
81
|
+
* the application, export `TemporalRuntime`, provide the activities โ the
|
|
82
|
+
* activities port is a need of this module.
|
|
83
|
+
*
|
|
84
|
+
* With both configuration fields pinned the module reads nothing from the
|
|
85
|
+
* environment; pin only one and the other still comes from it.
|
|
86
|
+
*/
|
|
87
|
+
const temporal = (options) => {
|
|
88
|
+
const { address, namespace } = options;
|
|
89
|
+
const activities = TemporalActivitiesPort;
|
|
90
|
+
const config = address !== void 0 && namespace !== void 0 ? (0, _btravstack_di.Provider)(TemporalConfig)({ value: {
|
|
91
|
+
address,
|
|
92
|
+
namespace
|
|
93
|
+
} }) : _btravstack_config.Config.provider(TemporalConfig)(_btravstack_config.Config.object({
|
|
94
|
+
address: _btravstack_config.Config.pinned(address, _btravstack_config.Config.string("TEMPORAL_ADDRESS", { default: "127.0.0.1:7233" })),
|
|
95
|
+
namespace: _btravstack_config.Config.pinned(namespace, _btravstack_config.Config.string("TEMPORAL_NAMESPACE", { default: "default" }))
|
|
96
|
+
}));
|
|
97
|
+
return (0, _btravstack_di.Module)("Temporal")({
|
|
98
|
+
needs: [_btravstack_config.Env, activities],
|
|
99
|
+
provides: [
|
|
100
|
+
config,
|
|
101
|
+
(0, _btravstack_di.Provider)(TemporalConnection)({ config: TemporalConfig }, {
|
|
102
|
+
acquire: ({ config: bound }) => (0, unthrown.fromPromise)(_temporalio_worker.NativeConnection.connect({ address: bound.address }), (cause) => new TemporalUnreachable({
|
|
103
|
+
address: bound.address,
|
|
104
|
+
cause
|
|
105
|
+
})),
|
|
106
|
+
release: (connection) => connection.close().catch((cause) => heldByWorker(cause) ? void 0 : Promise.reject(cause))
|
|
107
|
+
}),
|
|
108
|
+
(0, _btravstack_di.Provider)(TemporalRuntime)({
|
|
109
|
+
connection: TemporalConnection,
|
|
110
|
+
config: TemporalConfig,
|
|
111
|
+
activities
|
|
112
|
+
}, { sync: ({ connection, config: bound, activities: impls }) => ({
|
|
113
|
+
name: "temporal",
|
|
114
|
+
resolves: [],
|
|
115
|
+
start: (host) => createWorker(host, connection, bound, impls, options)
|
|
116
|
+
}) })
|
|
117
|
+
],
|
|
118
|
+
exports: [
|
|
119
|
+
TemporalRuntime,
|
|
120
|
+
TemporalConfig,
|
|
121
|
+
TemporalConnection
|
|
122
|
+
]
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
const startFailed = (cause) => new _btravstack_core.RuntimeStartFailed({
|
|
126
|
+
runtime: "temporal",
|
|
127
|
+
cause
|
|
128
|
+
});
|
|
129
|
+
const heldByWorker = (cause) => cause instanceof Error && cause.name === "IllegalStateError";
|
|
130
|
+
const createWorker = (host, connection, config, activities, options) => {
|
|
131
|
+
const { taskQueue } = options.contract;
|
|
132
|
+
const { namespace } = config;
|
|
133
|
+
return (0, unthrown.fromThrowable)(() => (0, _temporal_contract_worker_activity.declareActivitiesHandler)({
|
|
134
|
+
contract: options.contract,
|
|
135
|
+
middleware: activityUnits(host),
|
|
136
|
+
activities
|
|
137
|
+
}), startFailed)().toAsync().flatMap((handler) => (0, unthrown.fromPromise)(_temporalio_worker.Worker.create({
|
|
138
|
+
connection,
|
|
139
|
+
namespace,
|
|
140
|
+
taskQueue,
|
|
141
|
+
...options.workflows,
|
|
142
|
+
activities: handler,
|
|
143
|
+
shutdownGraceTime: options.gracePeriod ?? DEFAULT_GRACE,
|
|
144
|
+
shutdownForceTime: options.forceAfter ?? DEFAULT_FORCE
|
|
145
|
+
}), startFailed)).map((worker) => poll(worker, taskQueue, namespace));
|
|
146
|
+
};
|
|
147
|
+
const poll = (worker, taskQueue, namespace) => {
|
|
148
|
+
const running = (0, unthrown.fromSafePromise)(worker.run());
|
|
149
|
+
const stopPolling = () => {
|
|
150
|
+
if (worker.getState() === "RUNNING") worker.shutdown();
|
|
151
|
+
};
|
|
152
|
+
let deadline;
|
|
153
|
+
const stopped = () => deadline === void 0 ? running : (0, _btravstack_core.releasedBy)(deadline, running);
|
|
154
|
+
return {
|
|
155
|
+
info: {
|
|
156
|
+
taskQueue,
|
|
157
|
+
namespace
|
|
158
|
+
},
|
|
159
|
+
drain: (signal) => {
|
|
160
|
+
deadline = signal;
|
|
161
|
+
stopPolling();
|
|
162
|
+
return stopped();
|
|
163
|
+
},
|
|
164
|
+
stop: () => {
|
|
165
|
+
stopPolling();
|
|
166
|
+
return stopped();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/workflow-activities.ts
|
|
172
|
+
/** The prefix a piece's port id carries; the composing form strips it to recover the key. */
|
|
173
|
+
const WORKFLOW_ACTIVITIES_PREFIX = "TemporalWorkflowActivities:";
|
|
174
|
+
/**
|
|
175
|
+
* One workflow's activities, as a provider on a port of its own.
|
|
176
|
+
*
|
|
177
|
+
* A worker that polls one queue for several workflows is several pieces, each
|
|
178
|
+
* declaring the services its own activities call;
|
|
179
|
+
* `TemporalActivities(contract)([...])` composes them. `contract` is read for
|
|
180
|
+
* its TYPE only, and types both `key` and the record โ so an activity the
|
|
181
|
+
* workflow does not declare is a compile error here rather than at startup.
|
|
182
|
+
*
|
|
183
|
+
* `key` is any top-level key of the activities record, which includes a
|
|
184
|
+
* contract-global activity as well as a workflow; the name is imprecise in that
|
|
185
|
+
* one case deliberately, since narrowing it would lock such a contract out of
|
|
186
|
+
* the split.
|
|
187
|
+
*
|
|
188
|
+
* There is no name to give: the key IS the port's name.
|
|
189
|
+
*/
|
|
190
|
+
const TemporalWorkflowActivities = (contract, key) => {
|
|
191
|
+
const port = class extends (0, _btravstack_di.Port)(`${WORKFLOW_ACTIVITIES_PREFIX}${key}`) {};
|
|
192
|
+
return (0, _btravstack_di.Provider)(port);
|
|
193
|
+
};
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/temporal-module.ts
|
|
196
|
+
/**
|
|
197
|
+
* `Module(name)({...})` for a Temporal worker deployment: everything a di module
|
|
198
|
+
* takes, plus the contract, the activities provider and the workflow source. The
|
|
199
|
+
* sugar imports the starter, provides the activities and exports
|
|
200
|
+
* `TemporalRuntime`, handing back exactly the module `Module(...)` would have
|
|
201
|
+
* declared over the augmented tuples.
|
|
202
|
+
*
|
|
203
|
+
* ```ts
|
|
204
|
+
* export const OrderTemporalWorker = TemporalModule("OrderTemporalWorker")({
|
|
205
|
+
* contract: orderContract,
|
|
206
|
+
* activities: orderActivities,
|
|
207
|
+
* workflows: { workflowsPath: workflowsPathFromURL(import.meta.url, "./workflows.js") },
|
|
208
|
+
* imports: [OrderApplicationModule, OrderPersistenceModule, FulfillmentModule],
|
|
209
|
+
* });
|
|
210
|
+
* await runMain(OrderTemporalWorker);
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
const TemporalModule = (name) => (options) => {
|
|
214
|
+
const { contract, activities, workflows, address, namespace, gracePeriod, forceAfter } = options;
|
|
215
|
+
const imports = options.imports ?? [];
|
|
216
|
+
const provides = options.provides ?? [];
|
|
217
|
+
const exports = options.exports ?? [];
|
|
218
|
+
const starter = temporal({
|
|
219
|
+
contract,
|
|
220
|
+
workflows,
|
|
221
|
+
...address === void 0 ? {} : { address },
|
|
222
|
+
...namespace === void 0 ? {} : { namespace },
|
|
223
|
+
...gracePeriod === void 0 ? {} : { gracePeriod },
|
|
224
|
+
...forceAfter === void 0 ? {} : { forceAfter }
|
|
225
|
+
});
|
|
226
|
+
return (0, _btravstack_di.Module)(name)({
|
|
227
|
+
imports: [...imports, starter],
|
|
228
|
+
provides: [activities, ...provides],
|
|
229
|
+
exports: [TemporalRuntime, ...exports],
|
|
230
|
+
needs: options.needs ?? []
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* The activities as a provider, from the contract. Three call forms, one port.
|
|
235
|
+
*
|
|
236
|
+
* ```ts
|
|
237
|
+
* TemporalActivities(orderContract)({ place: PlaceOrder }, { sync: ({ place }) => ({ fulfillOrder: { โฆ } }) })
|
|
238
|
+
* TemporalActivities(orderContract)([fulfillOrder, chargeOrder])
|
|
239
|
+
* ```
|
|
240
|
+
*
|
|
241
|
+
* The first two are di's own `Provider(port)` on the starter's activities port
|
|
242
|
+
* typed for the contract. The third takes the pieces
|
|
243
|
+
* `TemporalWorkflowActivities(contract, key)` builds: they are the provider's
|
|
244
|
+
* deps, keyed by the contract key each piece's port id carries, so the services
|
|
245
|
+
* record IS the activities record. Every key must be covered, and two slices
|
|
246
|
+
* claiming one key are di's duplicate-provider defect at build.
|
|
247
|
+
*/
|
|
248
|
+
const TemporalActivities = (contract) => {
|
|
249
|
+
const build = (0, _btravstack_di.Provider)(TemporalActivitiesPort);
|
|
250
|
+
const compose = (pieces) => build(Object.fromEntries(pieces.map((piece) => [piece.port.portId.slice(27), piece.port])), { sync: (services) => services });
|
|
251
|
+
return ((first, second) => second === void 0 && Array.isArray(first) ? compose(first) : build(first, second));
|
|
252
|
+
};
|
|
253
|
+
//#endregion
|
|
254
|
+
exports.TemporalActivities = TemporalActivities;
|
|
255
|
+
exports.TemporalConfig = TemporalConfig;
|
|
256
|
+
exports.TemporalConnection = TemporalConnection;
|
|
257
|
+
exports.TemporalModule = TemporalModule;
|
|
258
|
+
exports.TemporalRuntime = TemporalRuntime;
|
|
259
|
+
exports.TemporalUnreachable = TemporalUnreachable;
|
|
260
|
+
exports.TemporalWorkflowActivities = TemporalWorkflowActivities;
|
|
261
|
+
exports.temporal = temporal;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { ConfigInvalid, Env } from "@btravstack/config";
|
|
2
|
+
import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider, Scope } from "@btravstack/di";
|
|
3
|
+
import { ContractDefinition } from "@temporal-contract/contract";
|
|
4
|
+
import { Duration } from "@temporalio/common";
|
|
5
|
+
import { Runtime, RuntimePort } from "@btravstack/core";
|
|
6
|
+
import { DeclareActivitiesHandlerOptions } from "@temporal-contract/worker/activity";
|
|
7
|
+
import { NativeConnection, WorkflowBundleWithSourceMap } from "@temporalio/worker";
|
|
8
|
+
//#region src/temporal-runtime.d.ts
|
|
9
|
+
/** What the worker publishes once it is polling, read back through `RunningApp.runtimeInfo()`. */
|
|
10
|
+
type TemporalInfo = {
|
|
11
|
+
readonly taskQueue: string;
|
|
12
|
+
readonly namespace: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Where the workflow sandbox's code comes from. Two arms because the two
|
|
16
|
+
* callers genuinely differ: a process points at the module and lets Temporal
|
|
17
|
+
* bundle it, while a spec hands over a bundle it built and memoised once โ
|
|
18
|
+
* bundling per test is the most expensive thing a suite does.
|
|
19
|
+
*/
|
|
20
|
+
type WorkflowSource = {
|
|
21
|
+
readonly workflowsPath: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly workflowBundle: WorkflowBundleWithSourceMap;
|
|
24
|
+
};
|
|
25
|
+
declare const TemporalConfig_base: import("@btravstack/di").PortClass<"TemporalConfig">;
|
|
26
|
+
/**
|
|
27
|
+
* Where the Temporal service is, as a service: `temporal()` binds it from the
|
|
28
|
+
* environment โ `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`) and
|
|
29
|
+
* `TEMPORAL_NAMESPACE` (default `default`) โ unless pinned, and anything else
|
|
30
|
+
* in the graph may read it.
|
|
31
|
+
*/
|
|
32
|
+
declare class TemporalConfig extends TemporalConfig_base<{
|
|
33
|
+
readonly address: string;
|
|
34
|
+
readonly namespace: string;
|
|
35
|
+
}> {}
|
|
36
|
+
declare const TemporalConnection_base: import("@btravstack/di").PortClass<"TemporalConnection">;
|
|
37
|
+
/**
|
|
38
|
+
* The connection, as a resource of the graph: di opens it with the scope and
|
|
39
|
+
* closes it on every exit path, startup failure included.
|
|
40
|
+
*/
|
|
41
|
+
declare class TemporalConnection extends TemporalConnection_base<NativeConnection> {}
|
|
42
|
+
declare const TemporalUnreachable_base: import("unthrown").TaggedErrorConstructor<"TemporalUnreachable">;
|
|
43
|
+
/**
|
|
44
|
+
* The service at `TemporalConfig.address` did not answer. Modeled rather than
|
|
45
|
+
* left a defect because an operator *can* act on it โ the address is wrong or
|
|
46
|
+
* the service is down, and neither is a bug in this code โ so `runMain` exits
|
|
47
|
+
* `1`, a startup `Err`, not the `70` a defect earns.
|
|
48
|
+
*/
|
|
49
|
+
declare class TemporalUnreachable extends TemporalUnreachable_base<{
|
|
50
|
+
readonly address: string;
|
|
51
|
+
readonly cause: unknown;
|
|
52
|
+
}> {
|
|
53
|
+
message: string;
|
|
54
|
+
}
|
|
55
|
+
/** The runtime's port: what `temporal()` provides, and what the module `start` boots must export. */
|
|
56
|
+
declare class TemporalRuntime extends RuntimePort<Runtime<never, TemporalInfo>> {}
|
|
57
|
+
/** The activity implementations `declareActivitiesHandler` takes for `C`, with no injected context. */
|
|
58
|
+
type ActivitiesOf<C extends ContractDefinition> = DeclareActivitiesHandlerOptions<C>["activities"];
|
|
59
|
+
/** The activities port class, typed for `C`: what `TemporalActivities(contract)(โฆ).port` is. */
|
|
60
|
+
type ActivitiesPortOf<C extends ContractDefinition> = PortClassOf<"TemporalActivities", ActivitiesOf<C>>;
|
|
61
|
+
/** The activities port's instance for `C` โ the module's one need. */
|
|
62
|
+
type ActivitiesInstanceOf<C extends ContractDefinition> = PortInstance<"TemporalActivities", ActivitiesOf<C>>;
|
|
63
|
+
type TemporalOptions<C extends ContractDefinition> = {
|
|
64
|
+
/**
|
|
65
|
+
* The contract; the task queue this worker polls is read off it, and the
|
|
66
|
+
* activities port is typed by it. The starter calls
|
|
67
|
+
* `declareActivitiesHandler` itself, with its unit middleware in place.
|
|
68
|
+
*/
|
|
69
|
+
readonly contract: C;
|
|
70
|
+
readonly workflows: WorkflowSource;
|
|
71
|
+
/** Pins `TemporalConfig.address` instead of reading `TEMPORAL_ADDRESS`. */
|
|
72
|
+
readonly address?: string;
|
|
73
|
+
/** Pins `TemporalConfig.namespace` instead of reading `TEMPORAL_NAMESPACE`. */
|
|
74
|
+
readonly namespace?: string;
|
|
75
|
+
/** Temporal's `shutdownForceTime`. Default `15 seconds`. Keep it at or below the kernel's `drainTimeoutMs`. */
|
|
76
|
+
readonly forceAfter?: Duration;
|
|
77
|
+
/** Temporal's `shutdownGraceTime`. Default `10 seconds`. */
|
|
78
|
+
readonly gracePeriod?: Duration;
|
|
79
|
+
};
|
|
80
|
+
type Provided = TemporalRuntime | TemporalConfig | TemporalConnection;
|
|
81
|
+
/**
|
|
82
|
+
* The Temporal starter: a module providing the runtime, its configuration
|
|
83
|
+
* (bound from `TEMPORAL_ADDRESS` / `TEMPORAL_NAMESPACE` unless pinned) and the
|
|
84
|
+
* connection (a resource opened with the scope and closed with it; a service
|
|
85
|
+
* that will not answer is a modeled `TemporalUnreachable`). Import it next to
|
|
86
|
+
* the application, export `TemporalRuntime`, provide the activities โ the
|
|
87
|
+
* activities port is a need of this module.
|
|
88
|
+
*
|
|
89
|
+
* With both configuration fields pinned the module reads nothing from the
|
|
90
|
+
* environment; pin only one and the other still comes from it.
|
|
91
|
+
*/
|
|
92
|
+
declare const temporal: <C extends ContractDefinition>(options: TemporalOptions<C>) => Module<Provided, ConfigInvalid | TemporalUnreachable, Env | Scope | ActivitiesInstanceOf<C>>;
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/workflow-activities.d.ts
|
|
95
|
+
/**
|
|
96
|
+
* `ActivitiesOf<C>` is a `NoInfer`-wrapped conditional TypeScript refuses to
|
|
97
|
+
* index by a generic key. Routing it through `infer` resolves it to a plain
|
|
98
|
+
* object type first, which IS indexable.
|
|
99
|
+
*/
|
|
100
|
+
type ActivitiesRecordOf<C extends ContractDefinition> = ActivitiesOf<C> extends (infer Resolved) ? Resolved : never;
|
|
101
|
+
/** The top-level keys of `C`'s activities record: a workflow that declares activities, or a contract-global activity. */
|
|
102
|
+
type ActivitiesKeyOf<C extends ContractDefinition> = keyof ActivitiesRecordOf<C> & string;
|
|
103
|
+
/** The prefix a piece's port id carries; the composing form strips it to recover the key. */
|
|
104
|
+
declare const WORKFLOW_ACTIVITIES_PREFIX = "TemporalWorkflowActivities:";
|
|
105
|
+
/**
|
|
106
|
+
* The port one piece targets. Its id carries the key, which is what makes two
|
|
107
|
+
* slices claiming one workflow di's duplicate-provider defect rather than a
|
|
108
|
+
* silent merge.
|
|
109
|
+
*/
|
|
110
|
+
type WorkflowActivitiesPortOf<C extends ContractDefinition, K extends ActivitiesKeyOf<C>> = PortClassOf<`${typeof WORKFLOW_ACTIVITIES_PREFIX}${K}`, ActivitiesRecordOf<C>[K]>;
|
|
111
|
+
/**
|
|
112
|
+
* One workflow's activities, as a provider on a port of its own.
|
|
113
|
+
*
|
|
114
|
+
* A worker that polls one queue for several workflows is several pieces, each
|
|
115
|
+
* declaring the services its own activities call;
|
|
116
|
+
* `TemporalActivities(contract)([...])` composes them. `contract` is read for
|
|
117
|
+
* its TYPE only, and types both `key` and the record โ so an activity the
|
|
118
|
+
* workflow does not declare is a compile error here rather than at startup.
|
|
119
|
+
*
|
|
120
|
+
* `key` is any top-level key of the activities record, which includes a
|
|
121
|
+
* contract-global activity as well as a workflow; the name is imprecise in that
|
|
122
|
+
* one case deliberately, since narrowing it would lock such a contract out of
|
|
123
|
+
* the split.
|
|
124
|
+
*
|
|
125
|
+
* There is no name to give: the key IS the port's name.
|
|
126
|
+
*/
|
|
127
|
+
declare const TemporalWorkflowActivities: <C extends ContractDefinition, const K extends ActivitiesKeyOf<C>>(contract: C, key: K) => ReturnType<typeof Provider<WorkflowActivitiesPortOf<C, K>>>;
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/temporal-module.d.ts
|
|
130
|
+
/** The starter's own module, as the sugar adds it to the application's imports. */
|
|
131
|
+
type TemporalStarter<C extends ContractDefinition> = Module<TemporalRuntime | TemporalConfig | TemporalConnection, ConfigInvalid | TemporalUnreachable, Env | Scope | ActivitiesInstanceOf<C>>;
|
|
132
|
+
/** The application's imports plus the starter โ the tuple `Module(name)` is handed. */
|
|
133
|
+
type Imports<I extends readonly AnyModule[], C extends ContractDefinition> = readonly [...I, TemporalStarter<C>];
|
|
134
|
+
/** The activities provider plus the application's own โ the tuple `Module(name)` is handed. */
|
|
135
|
+
type Provides<P extends readonly AnyProvider[], C extends ContractDefinition, ActivitiesError, ActivitiesNeeds> = readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P];
|
|
136
|
+
type TemporalModuleOptions<C extends ContractDefinition, ActivitiesError, ActivitiesNeeds, I extends readonly AnyModule[], P extends readonly AnyProvider[], X extends readonly Exportable<Imports<I, C>, Provides<P, C, ActivitiesError, ActivitiesNeeds>>[], N extends readonly AnyPort[]> = {
|
|
137
|
+
/** The `temporal-contract` contract; the task queue this worker polls is read off it. */
|
|
138
|
+
readonly contract: C;
|
|
139
|
+
/** The application's activity implementations โ what `TemporalActivities(contract)(โฆ)` returns. */
|
|
140
|
+
readonly activities: Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>;
|
|
141
|
+
readonly workflows: WorkflowSource;
|
|
142
|
+
/** Pins `TemporalConfig.address` instead of reading `TEMPORAL_ADDRESS`. */
|
|
143
|
+
readonly address?: string;
|
|
144
|
+
/** Pins `TemporalConfig.namespace` instead of reading `TEMPORAL_NAMESPACE`. */
|
|
145
|
+
readonly namespace?: string;
|
|
146
|
+
/** Temporal's `shutdownGraceTime`. Default `10 seconds`. */
|
|
147
|
+
readonly gracePeriod?: Duration;
|
|
148
|
+
/** Temporal's `shutdownForceTime`. Default `15 seconds`. Keep it at or below the kernel's `drainTimeoutMs`. */
|
|
149
|
+
readonly forceAfter?: Duration;
|
|
150
|
+
readonly imports?: I;
|
|
151
|
+
readonly provides?: P;
|
|
152
|
+
/** The application's own exports; `TemporalRuntime` is added, since `start` resolves it. */
|
|
153
|
+
readonly exports?: X;
|
|
154
|
+
/**
|
|
155
|
+
* What this root's OWN providers expect from outside. di's gate is re-stated
|
|
156
|
+
* over the augmented tuples below, so forgetting one is an error at THIS call.
|
|
157
|
+
*/
|
|
158
|
+
readonly needs?: N;
|
|
159
|
+
} & NeedsGate<Imports<I, C>, Provides<P, C, ActivitiesError, ActivitiesNeeds>, N>;
|
|
160
|
+
/**
|
|
161
|
+
* `Module(name)({...})` for a Temporal worker deployment: everything a di module
|
|
162
|
+
* takes, plus the contract, the activities provider and the workflow source. The
|
|
163
|
+
* sugar imports the starter, provides the activities and exports
|
|
164
|
+
* `TemporalRuntime`, handing back exactly the module `Module(...)` would have
|
|
165
|
+
* declared over the augmented tuples.
|
|
166
|
+
*
|
|
167
|
+
* ```ts
|
|
168
|
+
* export const OrderTemporalWorker = TemporalModule("OrderTemporalWorker")({
|
|
169
|
+
* contract: orderContract,
|
|
170
|
+
* activities: orderActivities,
|
|
171
|
+
* workflows: { workflowsPath: workflowsPathFromURL(import.meta.url, "./workflows.js") },
|
|
172
|
+
* imports: [OrderApplicationModule, OrderPersistenceModule, FulfillmentModule],
|
|
173
|
+
* });
|
|
174
|
+
* await runMain(OrderTemporalWorker);
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
declare const TemporalModule: <const Name extends string>(name: Name) => <C extends ContractDefinition, ActivitiesError, ActivitiesNeeds, const I extends readonly AnyModule[] = [], const P extends readonly AnyProvider[] = [], const X extends readonly Exportable<Imports<I, C>, Provides<P, C, ActivitiesError, ActivitiesNeeds>>[] = [], const N extends readonly AnyPort[] = []>(options: TemporalModuleOptions<C, ActivitiesError, ActivitiesNeeds, I, P, X, N>) => Module<(Extract<(readonly [typeof TemporalRuntime, ...X])[number], AnyModule> extends (infer T) ? T extends Extract<(readonly [typeof TemporalRuntime, ...X])[number], AnyModule> ? T extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[0] | ((readonly [typeof TemporalRuntime, ...X])[number] extends (infer E_1) ? E_1 extends AnyPort ? InstanceType<E_1> : E_1 extends AnyProvider ? InstanceType<E_1["port"]> : never : never), ((readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] ? T_1 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[1] | ((readonly [...I, TemporalStarter<C>])[number] extends (infer T_2) ? T_2 extends (readonly [...I, TemporalStarter<C>])[number] ? T_2 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[1], Exclude<((readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] extends (infer T_3) ? T_3 extends (readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] ? T_3 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[2], ((readonly [...I, TemporalStarter<C>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, TemporalStarter<C>])[number] ? T_4 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[0] | ((readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] extends (infer T_3) ? T_3 extends (readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] ? T_3 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[0]> | Exclude<((readonly [...I, TemporalStarter<C>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, TemporalStarter<C>])[number] ? T_4 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[2], ((readonly [...I, TemporalStarter<C>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, TemporalStarter<C>])[number] ? T_4 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[0] | ((readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] extends (infer T_3) ? T_3 extends (readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] ? T_3 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[0]>>;
|
|
178
|
+
/** One piece of the activities record โ what `TemporalWorkflowActivities(contract, key)(โฆ)` returns. */
|
|
179
|
+
type PieceOf<C extends ContractDefinition> = { readonly [K in ActivitiesKeyOf<C>]: {
|
|
180
|
+
readonly port: WorkflowActivitiesPortOf<C, K>;
|
|
181
|
+
}; }[ActivitiesKeyOf<C>];
|
|
182
|
+
/** The key a piece carries, read back off its port id. */
|
|
183
|
+
type KeyOfPiece<P> = P extends {
|
|
184
|
+
readonly port: {
|
|
185
|
+
readonly portId: `${typeof WORKFLOW_ACTIVITIES_PREFIX}${infer K}`;
|
|
186
|
+
};
|
|
187
|
+
} ? K : never;
|
|
188
|
+
/** The record's top-level keys no piece in `T` covers. */
|
|
189
|
+
type Uncovered<C extends ContractDefinition, T extends readonly PieceOf<C>[]> = Exclude<ActivitiesKeyOf<C>, KeyOfPiece<T[number]>>;
|
|
190
|
+
/**
|
|
191
|
+
* The composing arm. Declared LAST in the intersection below on purpose:
|
|
192
|
+
* TypeScript reports the last overload's failure, so a non-covering array is
|
|
193
|
+
* refused against the `"UNCOVERED ACTIVITIES โ โฆ"` marker rather than degrading
|
|
194
|
+
* to di's `Qualification`, which names nothing.
|
|
195
|
+
*
|
|
196
|
+
* The marker is a sentence rather than a bare label because it prints last,
|
|
197
|
+
* after the caller's own several-hundred-character piece type.
|
|
198
|
+
*/
|
|
199
|
+
type Compose<C extends ContractDefinition> = <const T extends readonly PieceOf<C>[]>(pieces: [Uncovered<C, T>] extends [never] ? T : readonly ["UNCOVERED ACTIVITIES โ the contract declares a workflow this array does not cover", Uncovered<C, T>]) => Provider<ActivitiesInstanceOf<C>, never, InstanceType<T[number]["port"]>> & {
|
|
200
|
+
readonly port: ActivitiesPortOf<C>;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* The activities as a provider, from the contract. Three call forms, one port.
|
|
204
|
+
*
|
|
205
|
+
* ```ts
|
|
206
|
+
* TemporalActivities(orderContract)({ place: PlaceOrder }, { sync: ({ place }) => ({ fulfillOrder: { โฆ } }) })
|
|
207
|
+
* TemporalActivities(orderContract)([fulfillOrder, chargeOrder])
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* The first two are di's own `Provider(port)` on the starter's activities port
|
|
211
|
+
* typed for the contract. The third takes the pieces
|
|
212
|
+
* `TemporalWorkflowActivities(contract, key)` builds: they are the provider's
|
|
213
|
+
* deps, keyed by the contract key each piece's port id carries, so the services
|
|
214
|
+
* record IS the activities record. Every key must be covered, and two slices
|
|
215
|
+
* claiming one key are di's duplicate-provider defect at build.
|
|
216
|
+
*/
|
|
217
|
+
declare const TemporalActivities: <C extends ContractDefinition>(contract: C) => ReturnType<typeof Provider<ActivitiesPortOf<C>>> & Compose<C>;
|
|
218
|
+
//#endregion
|
|
219
|
+
export { type ActivitiesInstanceOf, type ActivitiesPortOf, TemporalActivities, TemporalConfig, TemporalConnection, type TemporalInfo, TemporalModule, type TemporalModuleOptions, type TemporalOptions, TemporalRuntime, TemporalUnreachable, TemporalWorkflowActivities, type WorkflowActivitiesPortOf, type WorkflowSource, temporal };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider, Scope } from "@btravstack/di";
|
|
2
|
+
import { ConfigInvalid, Env } from "@btravstack/config";
|
|
3
|
+
import { Runtime, RuntimePort } from "@btravstack/core";
|
|
4
|
+
import { DeclareActivitiesHandlerOptions } from "@temporal-contract/worker/activity";
|
|
5
|
+
import { NativeConnection, WorkflowBundleWithSourceMap } from "@temporalio/worker";
|
|
6
|
+
import { ContractDefinition } from "@temporal-contract/contract";
|
|
7
|
+
import { Duration } from "@temporalio/common";
|
|
8
|
+
//#region src/temporal-runtime.d.ts
|
|
9
|
+
/** What the worker publishes once it is polling, read back through `RunningApp.runtimeInfo()`. */
|
|
10
|
+
type TemporalInfo = {
|
|
11
|
+
readonly taskQueue: string;
|
|
12
|
+
readonly namespace: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Where the workflow sandbox's code comes from. Two arms because the two
|
|
16
|
+
* callers genuinely differ: a process points at the module and lets Temporal
|
|
17
|
+
* bundle it, while a spec hands over a bundle it built and memoised once โ
|
|
18
|
+
* bundling per test is the most expensive thing a suite does.
|
|
19
|
+
*/
|
|
20
|
+
type WorkflowSource = {
|
|
21
|
+
readonly workflowsPath: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly workflowBundle: WorkflowBundleWithSourceMap;
|
|
24
|
+
};
|
|
25
|
+
declare const TemporalConfig_base: import("@btravstack/di").PortClass<"TemporalConfig">;
|
|
26
|
+
/**
|
|
27
|
+
* Where the Temporal service is, as a service: `temporal()` binds it from the
|
|
28
|
+
* environment โ `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`) and
|
|
29
|
+
* `TEMPORAL_NAMESPACE` (default `default`) โ unless pinned, and anything else
|
|
30
|
+
* in the graph may read it.
|
|
31
|
+
*/
|
|
32
|
+
declare class TemporalConfig extends TemporalConfig_base<{
|
|
33
|
+
readonly address: string;
|
|
34
|
+
readonly namespace: string;
|
|
35
|
+
}> {}
|
|
36
|
+
declare const TemporalConnection_base: import("@btravstack/di").PortClass<"TemporalConnection">;
|
|
37
|
+
/**
|
|
38
|
+
* The connection, as a resource of the graph: di opens it with the scope and
|
|
39
|
+
* closes it on every exit path, startup failure included.
|
|
40
|
+
*/
|
|
41
|
+
declare class TemporalConnection extends TemporalConnection_base<NativeConnection> {}
|
|
42
|
+
declare const TemporalUnreachable_base: import("unthrown").TaggedErrorConstructor<"TemporalUnreachable">;
|
|
43
|
+
/**
|
|
44
|
+
* The service at `TemporalConfig.address` did not answer. Modeled rather than
|
|
45
|
+
* left a defect because an operator *can* act on it โ the address is wrong or
|
|
46
|
+
* the service is down, and neither is a bug in this code โ so `runMain` exits
|
|
47
|
+
* `1`, a startup `Err`, not the `70` a defect earns.
|
|
48
|
+
*/
|
|
49
|
+
declare class TemporalUnreachable extends TemporalUnreachable_base<{
|
|
50
|
+
readonly address: string;
|
|
51
|
+
readonly cause: unknown;
|
|
52
|
+
}> {
|
|
53
|
+
message: string;
|
|
54
|
+
}
|
|
55
|
+
/** The runtime's port: what `temporal()` provides, and what the module `start` boots must export. */
|
|
56
|
+
declare class TemporalRuntime extends RuntimePort<Runtime<never, TemporalInfo>> {}
|
|
57
|
+
/** The activity implementations `declareActivitiesHandler` takes for `C`, with no injected context. */
|
|
58
|
+
type ActivitiesOf<C extends ContractDefinition> = DeclareActivitiesHandlerOptions<C>["activities"];
|
|
59
|
+
/** The activities port class, typed for `C`: what `TemporalActivities(contract)(โฆ).port` is. */
|
|
60
|
+
type ActivitiesPortOf<C extends ContractDefinition> = PortClassOf<"TemporalActivities", ActivitiesOf<C>>;
|
|
61
|
+
/** The activities port's instance for `C` โ the module's one need. */
|
|
62
|
+
type ActivitiesInstanceOf<C extends ContractDefinition> = PortInstance<"TemporalActivities", ActivitiesOf<C>>;
|
|
63
|
+
type TemporalOptions<C extends ContractDefinition> = {
|
|
64
|
+
/**
|
|
65
|
+
* The contract; the task queue this worker polls is read off it, and the
|
|
66
|
+
* activities port is typed by it. The starter calls
|
|
67
|
+
* `declareActivitiesHandler` itself, with its unit middleware in place.
|
|
68
|
+
*/
|
|
69
|
+
readonly contract: C;
|
|
70
|
+
readonly workflows: WorkflowSource;
|
|
71
|
+
/** Pins `TemporalConfig.address` instead of reading `TEMPORAL_ADDRESS`. */
|
|
72
|
+
readonly address?: string;
|
|
73
|
+
/** Pins `TemporalConfig.namespace` instead of reading `TEMPORAL_NAMESPACE`. */
|
|
74
|
+
readonly namespace?: string;
|
|
75
|
+
/** Temporal's `shutdownForceTime`. Default `15 seconds`. Keep it at or below the kernel's `drainTimeoutMs`. */
|
|
76
|
+
readonly forceAfter?: Duration;
|
|
77
|
+
/** Temporal's `shutdownGraceTime`. Default `10 seconds`. */
|
|
78
|
+
readonly gracePeriod?: Duration;
|
|
79
|
+
};
|
|
80
|
+
type Provided = TemporalRuntime | TemporalConfig | TemporalConnection;
|
|
81
|
+
/**
|
|
82
|
+
* The Temporal starter: a module providing the runtime, its configuration
|
|
83
|
+
* (bound from `TEMPORAL_ADDRESS` / `TEMPORAL_NAMESPACE` unless pinned) and the
|
|
84
|
+
* connection (a resource opened with the scope and closed with it; a service
|
|
85
|
+
* that will not answer is a modeled `TemporalUnreachable`). Import it next to
|
|
86
|
+
* the application, export `TemporalRuntime`, provide the activities โ the
|
|
87
|
+
* activities port is a need of this module.
|
|
88
|
+
*
|
|
89
|
+
* With both configuration fields pinned the module reads nothing from the
|
|
90
|
+
* environment; pin only one and the other still comes from it.
|
|
91
|
+
*/
|
|
92
|
+
declare const temporal: <C extends ContractDefinition>(options: TemporalOptions<C>) => Module<Provided, ConfigInvalid | TemporalUnreachable, Env | Scope | ActivitiesInstanceOf<C>>;
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/workflow-activities.d.ts
|
|
95
|
+
/**
|
|
96
|
+
* `ActivitiesOf<C>` is a `NoInfer`-wrapped conditional TypeScript refuses to
|
|
97
|
+
* index by a generic key. Routing it through `infer` resolves it to a plain
|
|
98
|
+
* object type first, which IS indexable.
|
|
99
|
+
*/
|
|
100
|
+
type ActivitiesRecordOf<C extends ContractDefinition> = ActivitiesOf<C> extends (infer Resolved) ? Resolved : never;
|
|
101
|
+
/** The top-level keys of `C`'s activities record: a workflow that declares activities, or a contract-global activity. */
|
|
102
|
+
type ActivitiesKeyOf<C extends ContractDefinition> = keyof ActivitiesRecordOf<C> & string;
|
|
103
|
+
/** The prefix a piece's port id carries; the composing form strips it to recover the key. */
|
|
104
|
+
declare const WORKFLOW_ACTIVITIES_PREFIX = "TemporalWorkflowActivities:";
|
|
105
|
+
/**
|
|
106
|
+
* The port one piece targets. Its id carries the key, which is what makes two
|
|
107
|
+
* slices claiming one workflow di's duplicate-provider defect rather than a
|
|
108
|
+
* silent merge.
|
|
109
|
+
*/
|
|
110
|
+
type WorkflowActivitiesPortOf<C extends ContractDefinition, K extends ActivitiesKeyOf<C>> = PortClassOf<`${typeof WORKFLOW_ACTIVITIES_PREFIX}${K}`, ActivitiesRecordOf<C>[K]>;
|
|
111
|
+
/**
|
|
112
|
+
* One workflow's activities, as a provider on a port of its own.
|
|
113
|
+
*
|
|
114
|
+
* A worker that polls one queue for several workflows is several pieces, each
|
|
115
|
+
* declaring the services its own activities call;
|
|
116
|
+
* `TemporalActivities(contract)([...])` composes them. `contract` is read for
|
|
117
|
+
* its TYPE only, and types both `key` and the record โ so an activity the
|
|
118
|
+
* workflow does not declare is a compile error here rather than at startup.
|
|
119
|
+
*
|
|
120
|
+
* `key` is any top-level key of the activities record, which includes a
|
|
121
|
+
* contract-global activity as well as a workflow; the name is imprecise in that
|
|
122
|
+
* one case deliberately, since narrowing it would lock such a contract out of
|
|
123
|
+
* the split.
|
|
124
|
+
*
|
|
125
|
+
* There is no name to give: the key IS the port's name.
|
|
126
|
+
*/
|
|
127
|
+
declare const TemporalWorkflowActivities: <C extends ContractDefinition, const K extends ActivitiesKeyOf<C>>(contract: C, key: K) => ReturnType<typeof Provider<WorkflowActivitiesPortOf<C, K>>>;
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/temporal-module.d.ts
|
|
130
|
+
/** The starter's own module, as the sugar adds it to the application's imports. */
|
|
131
|
+
type TemporalStarter<C extends ContractDefinition> = Module<TemporalRuntime | TemporalConfig | TemporalConnection, ConfigInvalid | TemporalUnreachable, Env | Scope | ActivitiesInstanceOf<C>>;
|
|
132
|
+
/** The application's imports plus the starter โ the tuple `Module(name)` is handed. */
|
|
133
|
+
type Imports<I extends readonly AnyModule[], C extends ContractDefinition> = readonly [...I, TemporalStarter<C>];
|
|
134
|
+
/** The activities provider plus the application's own โ the tuple `Module(name)` is handed. */
|
|
135
|
+
type Provides<P extends readonly AnyProvider[], C extends ContractDefinition, ActivitiesError, ActivitiesNeeds> = readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P];
|
|
136
|
+
type TemporalModuleOptions<C extends ContractDefinition, ActivitiesError, ActivitiesNeeds, I extends readonly AnyModule[], P extends readonly AnyProvider[], X extends readonly Exportable<Imports<I, C>, Provides<P, C, ActivitiesError, ActivitiesNeeds>>[], N extends readonly AnyPort[]> = {
|
|
137
|
+
/** The `temporal-contract` contract; the task queue this worker polls is read off it. */
|
|
138
|
+
readonly contract: C;
|
|
139
|
+
/** The application's activity implementations โ what `TemporalActivities(contract)(โฆ)` returns. */
|
|
140
|
+
readonly activities: Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>;
|
|
141
|
+
readonly workflows: WorkflowSource;
|
|
142
|
+
/** Pins `TemporalConfig.address` instead of reading `TEMPORAL_ADDRESS`. */
|
|
143
|
+
readonly address?: string;
|
|
144
|
+
/** Pins `TemporalConfig.namespace` instead of reading `TEMPORAL_NAMESPACE`. */
|
|
145
|
+
readonly namespace?: string;
|
|
146
|
+
/** Temporal's `shutdownGraceTime`. Default `10 seconds`. */
|
|
147
|
+
readonly gracePeriod?: Duration;
|
|
148
|
+
/** Temporal's `shutdownForceTime`. Default `15 seconds`. Keep it at or below the kernel's `drainTimeoutMs`. */
|
|
149
|
+
readonly forceAfter?: Duration;
|
|
150
|
+
readonly imports?: I;
|
|
151
|
+
readonly provides?: P;
|
|
152
|
+
/** The application's own exports; `TemporalRuntime` is added, since `start` resolves it. */
|
|
153
|
+
readonly exports?: X;
|
|
154
|
+
/**
|
|
155
|
+
* What this root's OWN providers expect from outside. di's gate is re-stated
|
|
156
|
+
* over the augmented tuples below, so forgetting one is an error at THIS call.
|
|
157
|
+
*/
|
|
158
|
+
readonly needs?: N;
|
|
159
|
+
} & NeedsGate<Imports<I, C>, Provides<P, C, ActivitiesError, ActivitiesNeeds>, N>;
|
|
160
|
+
/**
|
|
161
|
+
* `Module(name)({...})` for a Temporal worker deployment: everything a di module
|
|
162
|
+
* takes, plus the contract, the activities provider and the workflow source. The
|
|
163
|
+
* sugar imports the starter, provides the activities and exports
|
|
164
|
+
* `TemporalRuntime`, handing back exactly the module `Module(...)` would have
|
|
165
|
+
* declared over the augmented tuples.
|
|
166
|
+
*
|
|
167
|
+
* ```ts
|
|
168
|
+
* export const OrderTemporalWorker = TemporalModule("OrderTemporalWorker")({
|
|
169
|
+
* contract: orderContract,
|
|
170
|
+
* activities: orderActivities,
|
|
171
|
+
* workflows: { workflowsPath: workflowsPathFromURL(import.meta.url, "./workflows.js") },
|
|
172
|
+
* imports: [OrderApplicationModule, OrderPersistenceModule, FulfillmentModule],
|
|
173
|
+
* });
|
|
174
|
+
* await runMain(OrderTemporalWorker);
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
declare const TemporalModule: <const Name extends string>(name: Name) => <C extends ContractDefinition, ActivitiesError, ActivitiesNeeds, const I extends readonly AnyModule[] = [], const P extends readonly AnyProvider[] = [], const X extends readonly Exportable<Imports<I, C>, Provides<P, C, ActivitiesError, ActivitiesNeeds>>[] = [], const N extends readonly AnyPort[] = []>(options: TemporalModuleOptions<C, ActivitiesError, ActivitiesNeeds, I, P, X, N>) => Module<(Extract<(readonly [typeof TemporalRuntime, ...X])[number], AnyModule> extends (infer T) ? T extends Extract<(readonly [typeof TemporalRuntime, ...X])[number], AnyModule> ? T extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[0] | ((readonly [typeof TemporalRuntime, ...X])[number] extends (infer E_1) ? E_1 extends AnyPort ? InstanceType<E_1> : E_1 extends AnyProvider ? InstanceType<E_1["port"]> : never : never), ((readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] extends (infer T_1) ? T_1 extends (readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] ? T_1 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[1] | ((readonly [...I, TemporalStarter<C>])[number] extends (infer T_2) ? T_2 extends (readonly [...I, TemporalStarter<C>])[number] ? T_2 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[1], Exclude<((readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] extends (infer T_3) ? T_3 extends (readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] ? T_3 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[2], ((readonly [...I, TemporalStarter<C>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, TemporalStarter<C>])[number] ? T_4 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[0] | ((readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] extends (infer T_3) ? T_3 extends (readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] ? T_3 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[0]> | Exclude<((readonly [...I, TemporalStarter<C>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, TemporalStarter<C>])[number] ? T_4 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[2], ((readonly [...I, TemporalStarter<C>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, TemporalStarter<C>])[number] ? T_4 extends Module<infer X_1, infer E, infer N_1> ? readonly [X_1, E, N_1] : never : never : never)[0] | ((readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] extends (infer T_3) ? T_3 extends (readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P])[number] ? T_3 extends Provider<infer P_1, infer E_2, infer N_2> ? readonly [P_1, E_2, N_2] : never : never : never)[0]>>;
|
|
178
|
+
/** One piece of the activities record โ what `TemporalWorkflowActivities(contract, key)(โฆ)` returns. */
|
|
179
|
+
type PieceOf<C extends ContractDefinition> = { readonly [K in ActivitiesKeyOf<C>]: {
|
|
180
|
+
readonly port: WorkflowActivitiesPortOf<C, K>;
|
|
181
|
+
}; }[ActivitiesKeyOf<C>];
|
|
182
|
+
/** The key a piece carries, read back off its port id. */
|
|
183
|
+
type KeyOfPiece<P> = P extends {
|
|
184
|
+
readonly port: {
|
|
185
|
+
readonly portId: `${typeof WORKFLOW_ACTIVITIES_PREFIX}${infer K}`;
|
|
186
|
+
};
|
|
187
|
+
} ? K : never;
|
|
188
|
+
/** The record's top-level keys no piece in `T` covers. */
|
|
189
|
+
type Uncovered<C extends ContractDefinition, T extends readonly PieceOf<C>[]> = Exclude<ActivitiesKeyOf<C>, KeyOfPiece<T[number]>>;
|
|
190
|
+
/**
|
|
191
|
+
* The composing arm. Declared LAST in the intersection below on purpose:
|
|
192
|
+
* TypeScript reports the last overload's failure, so a non-covering array is
|
|
193
|
+
* refused against the `"UNCOVERED ACTIVITIES โ โฆ"` marker rather than degrading
|
|
194
|
+
* to di's `Qualification`, which names nothing.
|
|
195
|
+
*
|
|
196
|
+
* The marker is a sentence rather than a bare label because it prints last,
|
|
197
|
+
* after the caller's own several-hundred-character piece type.
|
|
198
|
+
*/
|
|
199
|
+
type Compose<C extends ContractDefinition> = <const T extends readonly PieceOf<C>[]>(pieces: [Uncovered<C, T>] extends [never] ? T : readonly ["UNCOVERED ACTIVITIES โ the contract declares a workflow this array does not cover", Uncovered<C, T>]) => Provider<ActivitiesInstanceOf<C>, never, InstanceType<T[number]["port"]>> & {
|
|
200
|
+
readonly port: ActivitiesPortOf<C>;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* The activities as a provider, from the contract. Three call forms, one port.
|
|
204
|
+
*
|
|
205
|
+
* ```ts
|
|
206
|
+
* TemporalActivities(orderContract)({ place: PlaceOrder }, { sync: ({ place }) => ({ fulfillOrder: { โฆ } }) })
|
|
207
|
+
* TemporalActivities(orderContract)([fulfillOrder, chargeOrder])
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* The first two are di's own `Provider(port)` on the starter's activities port
|
|
211
|
+
* typed for the contract. The third takes the pieces
|
|
212
|
+
* `TemporalWorkflowActivities(contract, key)` builds: they are the provider's
|
|
213
|
+
* deps, keyed by the contract key each piece's port id carries, so the services
|
|
214
|
+
* record IS the activities record. Every key must be covered, and two slices
|
|
215
|
+
* claiming one key are di's duplicate-provider defect at build.
|
|
216
|
+
*/
|
|
217
|
+
declare const TemporalActivities: <C extends ContractDefinition>(contract: C) => ReturnType<typeof Provider<ActivitiesPortOf<C>>> & Compose<C>;
|
|
218
|
+
//#endregion
|
|
219
|
+
export { type ActivitiesInstanceOf, type ActivitiesPortOf, TemporalActivities, TemporalConfig, TemporalConnection, type TemporalInfo, TemporalModule, type TemporalModuleOptions, type TemporalOptions, TemporalRuntime, TemporalUnreachable, TemporalWorkflowActivities, type WorkflowActivitiesPortOf, type WorkflowSource, temporal };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Module, Port, Provider } from "@btravstack/di";
|
|
2
|
+
import { Config, Env } from "@btravstack/config";
|
|
3
|
+
import { RuntimePort, RuntimeStartFailed, releasedBy } from "@btravstack/core";
|
|
4
|
+
import { declareActivitiesHandler } from "@temporal-contract/worker/activity";
|
|
5
|
+
import { NativeConnection, Worker } from "@temporalio/worker";
|
|
6
|
+
import { TaggedError, fromPromise, fromSafePromise, fromThrowable } from "unthrown";
|
|
7
|
+
import { activityInfo } from "@temporalio/activity";
|
|
8
|
+
//#region src/activity-units.ts
|
|
9
|
+
/**
|
|
10
|
+
* Open one kernel unit per activity attempt. It injects nothing โ `next()`
|
|
11
|
+
* unchanged โ so the ambient `currentUnit()` record is what an adapter reads.
|
|
12
|
+
*
|
|
13
|
+
* **That includes the kernel's per-unit `AbortSignal`**, and it is the only
|
|
14
|
+
* route to it: an activity has no parameter to receive one through. Temporal's
|
|
15
|
+
* own `Context.current().cancellationSignal` is a DIFFERENT clock, so the two
|
|
16
|
+
* are honoured together rather than one standing in for the other.
|
|
17
|
+
*
|
|
18
|
+
* No `Result`-unwrapping boundary, deliberately: `declareActivitiesHandler` owns
|
|
19
|
+
* the mapping from a settled `Result` to an activity failure.
|
|
20
|
+
*/
|
|
21
|
+
const activityUnits = (host) => (_invocation, next) => host.run(metaFor(), () => next());
|
|
22
|
+
/**
|
|
23
|
+
* `UnitMeta.id` must be unique per unit, and a workflow id is **not** one: an
|
|
24
|
+
* activity is retried under the same execution. A task token identifies one
|
|
25
|
+
* attempt, so its uniqueness is Temporal's guarantee rather than ours.
|
|
26
|
+
*
|
|
27
|
+
* The workflow id becomes the `traceId` โ minted outside this process and steady
|
|
28
|
+
* across every retry, so all attempts join up in a log. An activity with no
|
|
29
|
+
* workflow falls back to the activity id.
|
|
30
|
+
*/
|
|
31
|
+
const metaFor = () => {
|
|
32
|
+
const info = activityInfo();
|
|
33
|
+
return {
|
|
34
|
+
kind: "activity",
|
|
35
|
+
id: info.base64TaskToken,
|
|
36
|
+
traceId: info.workflowExecution?.workflowId ?? info.activityId
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/temporal-runtime.ts
|
|
41
|
+
/**
|
|
42
|
+
* Where the Temporal service is, as a service: `temporal()` binds it from the
|
|
43
|
+
* environment โ `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`) and
|
|
44
|
+
* `TEMPORAL_NAMESPACE` (default `default`) โ unless pinned, and anything else
|
|
45
|
+
* in the graph may read it.
|
|
46
|
+
*/
|
|
47
|
+
var TemporalConfig = class extends Port("TemporalConfig") {};
|
|
48
|
+
/**
|
|
49
|
+
* The connection, as a resource of the graph: di opens it with the scope and
|
|
50
|
+
* closes it on every exit path, startup failure included.
|
|
51
|
+
*/
|
|
52
|
+
var TemporalConnection = class extends Port("TemporalConnection") {};
|
|
53
|
+
/**
|
|
54
|
+
* The service at `TemporalConfig.address` did not answer. Modeled rather than
|
|
55
|
+
* left a defect because an operator *can* act on it โ the address is wrong or
|
|
56
|
+
* the service is down, and neither is a bug in this code โ so `runMain` exits
|
|
57
|
+
* `1`, a startup `Err`, not the `70` a defect earns.
|
|
58
|
+
*/
|
|
59
|
+
var TemporalUnreachable = class extends TaggedError("TemporalUnreachable") {
|
|
60
|
+
message = `the Temporal service at ${this.address} did not answer`;
|
|
61
|
+
};
|
|
62
|
+
/** The runtime's port: what `temporal()` provides, and what the module `start` boots must export. */
|
|
63
|
+
var TemporalRuntime = class extends RuntimePort {};
|
|
64
|
+
/**
|
|
65
|
+
* The activities' port โ one id, the starter's own, which an application never
|
|
66
|
+
* names. Generic at the value level (one `Port(...)` call, so no duplicate-id
|
|
67
|
+
* warning however many contracts instantiate it) and fixed per contract at the
|
|
68
|
+
* type level through `ActivitiesPortOf<C>`, so a provider built for one contract
|
|
69
|
+
* cannot be handed to a module declaring another. Exported for this package's
|
|
70
|
+
* tests, not from `index.ts`.
|
|
71
|
+
*/
|
|
72
|
+
const TemporalActivitiesPort = Port("TemporalActivities");
|
|
73
|
+
const DEFAULT_GRACE = "10 seconds";
|
|
74
|
+
const DEFAULT_FORCE = "15 seconds";
|
|
75
|
+
/**
|
|
76
|
+
* The Temporal starter: a module providing the runtime, its configuration
|
|
77
|
+
* (bound from `TEMPORAL_ADDRESS` / `TEMPORAL_NAMESPACE` unless pinned) and the
|
|
78
|
+
* connection (a resource opened with the scope and closed with it; a service
|
|
79
|
+
* that will not answer is a modeled `TemporalUnreachable`). Import it next to
|
|
80
|
+
* the application, export `TemporalRuntime`, provide the activities โ the
|
|
81
|
+
* activities port is a need of this module.
|
|
82
|
+
*
|
|
83
|
+
* With both configuration fields pinned the module reads nothing from the
|
|
84
|
+
* environment; pin only one and the other still comes from it.
|
|
85
|
+
*/
|
|
86
|
+
const temporal = (options) => {
|
|
87
|
+
const { address, namespace } = options;
|
|
88
|
+
const activities = TemporalActivitiesPort;
|
|
89
|
+
const config = address !== void 0 && namespace !== void 0 ? Provider(TemporalConfig)({ value: {
|
|
90
|
+
address,
|
|
91
|
+
namespace
|
|
92
|
+
} }) : Config.provider(TemporalConfig)(Config.object({
|
|
93
|
+
address: Config.pinned(address, Config.string("TEMPORAL_ADDRESS", { default: "127.0.0.1:7233" })),
|
|
94
|
+
namespace: Config.pinned(namespace, Config.string("TEMPORAL_NAMESPACE", { default: "default" }))
|
|
95
|
+
}));
|
|
96
|
+
return Module("Temporal")({
|
|
97
|
+
needs: [Env, activities],
|
|
98
|
+
provides: [
|
|
99
|
+
config,
|
|
100
|
+
Provider(TemporalConnection)({ config: TemporalConfig }, {
|
|
101
|
+
acquire: ({ config: bound }) => fromPromise(NativeConnection.connect({ address: bound.address }), (cause) => new TemporalUnreachable({
|
|
102
|
+
address: bound.address,
|
|
103
|
+
cause
|
|
104
|
+
})),
|
|
105
|
+
release: (connection) => connection.close().catch((cause) => heldByWorker(cause) ? void 0 : Promise.reject(cause))
|
|
106
|
+
}),
|
|
107
|
+
Provider(TemporalRuntime)({
|
|
108
|
+
connection: TemporalConnection,
|
|
109
|
+
config: TemporalConfig,
|
|
110
|
+
activities
|
|
111
|
+
}, { sync: ({ connection, config: bound, activities: impls }) => ({
|
|
112
|
+
name: "temporal",
|
|
113
|
+
resolves: [],
|
|
114
|
+
start: (host) => createWorker(host, connection, bound, impls, options)
|
|
115
|
+
}) })
|
|
116
|
+
],
|
|
117
|
+
exports: [
|
|
118
|
+
TemporalRuntime,
|
|
119
|
+
TemporalConfig,
|
|
120
|
+
TemporalConnection
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
const startFailed = (cause) => new RuntimeStartFailed({
|
|
125
|
+
runtime: "temporal",
|
|
126
|
+
cause
|
|
127
|
+
});
|
|
128
|
+
const heldByWorker = (cause) => cause instanceof Error && cause.name === "IllegalStateError";
|
|
129
|
+
const createWorker = (host, connection, config, activities, options) => {
|
|
130
|
+
const { taskQueue } = options.contract;
|
|
131
|
+
const { namespace } = config;
|
|
132
|
+
return fromThrowable(() => declareActivitiesHandler({
|
|
133
|
+
contract: options.contract,
|
|
134
|
+
middleware: activityUnits(host),
|
|
135
|
+
activities
|
|
136
|
+
}), startFailed)().toAsync().flatMap((handler) => fromPromise(Worker.create({
|
|
137
|
+
connection,
|
|
138
|
+
namespace,
|
|
139
|
+
taskQueue,
|
|
140
|
+
...options.workflows,
|
|
141
|
+
activities: handler,
|
|
142
|
+
shutdownGraceTime: options.gracePeriod ?? DEFAULT_GRACE,
|
|
143
|
+
shutdownForceTime: options.forceAfter ?? DEFAULT_FORCE
|
|
144
|
+
}), startFailed)).map((worker) => poll(worker, taskQueue, namespace));
|
|
145
|
+
};
|
|
146
|
+
const poll = (worker, taskQueue, namespace) => {
|
|
147
|
+
const running = fromSafePromise(worker.run());
|
|
148
|
+
const stopPolling = () => {
|
|
149
|
+
if (worker.getState() === "RUNNING") worker.shutdown();
|
|
150
|
+
};
|
|
151
|
+
let deadline;
|
|
152
|
+
const stopped = () => deadline === void 0 ? running : releasedBy(deadline, running);
|
|
153
|
+
return {
|
|
154
|
+
info: {
|
|
155
|
+
taskQueue,
|
|
156
|
+
namespace
|
|
157
|
+
},
|
|
158
|
+
drain: (signal) => {
|
|
159
|
+
deadline = signal;
|
|
160
|
+
stopPolling();
|
|
161
|
+
return stopped();
|
|
162
|
+
},
|
|
163
|
+
stop: () => {
|
|
164
|
+
stopPolling();
|
|
165
|
+
return stopped();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/workflow-activities.ts
|
|
171
|
+
/** The prefix a piece's port id carries; the composing form strips it to recover the key. */
|
|
172
|
+
const WORKFLOW_ACTIVITIES_PREFIX = "TemporalWorkflowActivities:";
|
|
173
|
+
/**
|
|
174
|
+
* One workflow's activities, as a provider on a port of its own.
|
|
175
|
+
*
|
|
176
|
+
* A worker that polls one queue for several workflows is several pieces, each
|
|
177
|
+
* declaring the services its own activities call;
|
|
178
|
+
* `TemporalActivities(contract)([...])` composes them. `contract` is read for
|
|
179
|
+
* its TYPE only, and types both `key` and the record โ so an activity the
|
|
180
|
+
* workflow does not declare is a compile error here rather than at startup.
|
|
181
|
+
*
|
|
182
|
+
* `key` is any top-level key of the activities record, which includes a
|
|
183
|
+
* contract-global activity as well as a workflow; the name is imprecise in that
|
|
184
|
+
* one case deliberately, since narrowing it would lock such a contract out of
|
|
185
|
+
* the split.
|
|
186
|
+
*
|
|
187
|
+
* There is no name to give: the key IS the port's name.
|
|
188
|
+
*/
|
|
189
|
+
const TemporalWorkflowActivities = (contract, key) => {
|
|
190
|
+
const port = class extends Port(`${WORKFLOW_ACTIVITIES_PREFIX}${key}`) {};
|
|
191
|
+
return Provider(port);
|
|
192
|
+
};
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/temporal-module.ts
|
|
195
|
+
/**
|
|
196
|
+
* `Module(name)({...})` for a Temporal worker deployment: everything a di module
|
|
197
|
+
* takes, plus the contract, the activities provider and the workflow source. The
|
|
198
|
+
* sugar imports the starter, provides the activities and exports
|
|
199
|
+
* `TemporalRuntime`, handing back exactly the module `Module(...)` would have
|
|
200
|
+
* declared over the augmented tuples.
|
|
201
|
+
*
|
|
202
|
+
* ```ts
|
|
203
|
+
* export const OrderTemporalWorker = TemporalModule("OrderTemporalWorker")({
|
|
204
|
+
* contract: orderContract,
|
|
205
|
+
* activities: orderActivities,
|
|
206
|
+
* workflows: { workflowsPath: workflowsPathFromURL(import.meta.url, "./workflows.js") },
|
|
207
|
+
* imports: [OrderApplicationModule, OrderPersistenceModule, FulfillmentModule],
|
|
208
|
+
* });
|
|
209
|
+
* await runMain(OrderTemporalWorker);
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
const TemporalModule = (name) => (options) => {
|
|
213
|
+
const { contract, activities, workflows, address, namespace, gracePeriod, forceAfter } = options;
|
|
214
|
+
const imports = options.imports ?? [];
|
|
215
|
+
const provides = options.provides ?? [];
|
|
216
|
+
const exports = options.exports ?? [];
|
|
217
|
+
const starter = temporal({
|
|
218
|
+
contract,
|
|
219
|
+
workflows,
|
|
220
|
+
...address === void 0 ? {} : { address },
|
|
221
|
+
...namespace === void 0 ? {} : { namespace },
|
|
222
|
+
...gracePeriod === void 0 ? {} : { gracePeriod },
|
|
223
|
+
...forceAfter === void 0 ? {} : { forceAfter }
|
|
224
|
+
});
|
|
225
|
+
return Module(name)({
|
|
226
|
+
imports: [...imports, starter],
|
|
227
|
+
provides: [activities, ...provides],
|
|
228
|
+
exports: [TemporalRuntime, ...exports],
|
|
229
|
+
needs: options.needs ?? []
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* The activities as a provider, from the contract. Three call forms, one port.
|
|
234
|
+
*
|
|
235
|
+
* ```ts
|
|
236
|
+
* TemporalActivities(orderContract)({ place: PlaceOrder }, { sync: ({ place }) => ({ fulfillOrder: { โฆ } }) })
|
|
237
|
+
* TemporalActivities(orderContract)([fulfillOrder, chargeOrder])
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* The first two are di's own `Provider(port)` on the starter's activities port
|
|
241
|
+
* typed for the contract. The third takes the pieces
|
|
242
|
+
* `TemporalWorkflowActivities(contract, key)` builds: they are the provider's
|
|
243
|
+
* deps, keyed by the contract key each piece's port id carries, so the services
|
|
244
|
+
* record IS the activities record. Every key must be covered, and two slices
|
|
245
|
+
* claiming one key are di's duplicate-provider defect at build.
|
|
246
|
+
*/
|
|
247
|
+
const TemporalActivities = (contract) => {
|
|
248
|
+
const build = Provider(TemporalActivitiesPort);
|
|
249
|
+
const compose = (pieces) => build(Object.fromEntries(pieces.map((piece) => [piece.port.portId.slice(27), piece.port])), { sync: (services) => services });
|
|
250
|
+
return ((first, second) => second === void 0 && Array.isArray(first) ? compose(first) : build(first, second));
|
|
251
|
+
};
|
|
252
|
+
//#endregion
|
|
253
|
+
export { TemporalActivities, TemporalConfig, TemporalConnection, TemporalModule, TemporalRuntime, TemporalUnreachable, TemporalWorkflowActivities, temporal };
|
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@btravstack/temporal-worker",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "The Temporal worker runtime for @btravstack/core: one unit per activity attempt, and a drain that honours the kernel's deadline",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"graceful-shutdown",
|
|
7
|
+
"lifecycle",
|
|
8
|
+
"runtime",
|
|
9
|
+
"temporal",
|
|
10
|
+
"typescript",
|
|
11
|
+
"unthrown",
|
|
12
|
+
"worker"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/btravstack/btravstack#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/btravstack/btravstack/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Benoit TRAVERS <benoit.travers.fr@gmail.com>",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/btravstack/btravstack.git",
|
|
23
|
+
"directory": "packages/temporal-worker"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.mjs",
|
|
32
|
+
"types": "./dist/index.d.cts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"import": {
|
|
36
|
+
"types": "./dist/index.d.mts",
|
|
37
|
+
"default": "./dist/index.mjs"
|
|
38
|
+
},
|
|
39
|
+
"require": {
|
|
40
|
+
"types": "./dist/index.d.cts",
|
|
41
|
+
"default": "./dist/index.cjs"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"./package.json": "./package.json"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@btravstack/tsconfig": "0.2.0",
|
|
48
|
+
"@temporal-contract/contract": "8.0.0-beta.5",
|
|
49
|
+
"@temporal-contract/worker": "8.0.0-beta.5",
|
|
50
|
+
"@temporalio/activity": "1.22.0",
|
|
51
|
+
"@temporalio/client": "1.22.0",
|
|
52
|
+
"@temporalio/common": "1.22.0",
|
|
53
|
+
"@temporalio/worker": "1.22.0",
|
|
54
|
+
"@temporalio/workflow": "1.22.0",
|
|
55
|
+
"@types/node": "26.2.0",
|
|
56
|
+
"@unthrown/vitest": "5.7.0",
|
|
57
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
58
|
+
"tsdown": "0.22.14",
|
|
59
|
+
"typescript": "7.0.2",
|
|
60
|
+
"unthrown": "5.7.0",
|
|
61
|
+
"vitest": "4.1.11",
|
|
62
|
+
"zod": "4.4.3",
|
|
63
|
+
"@btravstack/core": "0.3.0",
|
|
64
|
+
"@btravstack/di": "0.3.0",
|
|
65
|
+
"@btravstack/testing": "0.3.0",
|
|
66
|
+
"@btravstack/config": "0.3.0",
|
|
67
|
+
"@btravstack/internal-test-infra": "0.0.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"@temporal-contract/contract": "^8.0.0-beta",
|
|
71
|
+
"@temporal-contract/worker": "^8.0.0-beta",
|
|
72
|
+
"@temporalio/activity": "^1.22.0",
|
|
73
|
+
"@temporalio/common": "^1.22.0",
|
|
74
|
+
"@temporalio/worker": "^1.22.0",
|
|
75
|
+
"unthrown": "^5.0.0",
|
|
76
|
+
"@btravstack/config": "^0.3.0",
|
|
77
|
+
"@btravstack/di": "^0.3.0",
|
|
78
|
+
"@btravstack/core": "^0.3.0"
|
|
79
|
+
},
|
|
80
|
+
"engines": {
|
|
81
|
+
"node": ">=20"
|
|
82
|
+
},
|
|
83
|
+
"scripts": {
|
|
84
|
+
"build": "tsdown src/index.ts --format cjs,esm --dts --clean",
|
|
85
|
+
"dev": "tsdown src/index.ts --format cjs,esm --dts --watch",
|
|
86
|
+
"test": "vitest run --coverage",
|
|
87
|
+
"test:types": "tsc --noEmit -p tsconfig.test-d.json",
|
|
88
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json"
|
|
89
|
+
}
|
|
90
|
+
}
|