@btravstack/temporal-worker 0.9.0 → 0.10.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 +17 -3
- package/dist/index.cjs +21 -6
- package/dist/index.mjs +22 -7
- package/dist/schedule.cjs +51 -0
- package/dist/schedule.d.cts +50 -0
- package/dist/schedule.d.mts +50 -0
- package/dist/schedule.mjs +50 -0
- package/package.json +33 -16
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
```sh
|
|
14
14
|
pnpm add @btravstack/temporal-worker @btravstack/core @btravstack/config @btravstack/di unthrown \
|
|
15
15
|
@temporalio/worker @temporalio/activity @temporalio/common \
|
|
16
|
-
@temporal-contract/worker @temporal-contract/contract
|
|
16
|
+
@temporal-contract/worker@^8.0.0-beta.7 @temporal-contract/contract@^8.0.0-beta.7
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
All nine are peer dependencies — install them. Node `>=22`.
|
|
@@ -78,9 +78,9 @@ const orderActivities = TemporalActivities(contract)({
|
|
|
78
78
|
inject: { place: PlaceOrder },
|
|
79
79
|
sync: ({ place }) => ({
|
|
80
80
|
placeOrder: {
|
|
81
|
-
place: (
|
|
81
|
+
place: ({ errors, input }) =>
|
|
82
82
|
place
|
|
83
|
-
.execute(TenantId(
|
|
83
|
+
.execute(TenantId(input.tenantId), input.orderId, input.quantity)
|
|
84
84
|
.mapErrCases((matcher) =>
|
|
85
85
|
matcher
|
|
86
86
|
.with(P.tag("DuplicateOrder"), (error) =>
|
|
@@ -132,6 +132,20 @@ the pieces' ports, not what they close over. Two slices claiming one key are
|
|
|
132
132
|
di's duplicate-provider defect at build, which is the point: a workflow's
|
|
133
133
|
activities belong to exactly one slice.
|
|
134
134
|
|
|
135
|
+
Its own test suite needs a **Docker daemon**: the specs run against the shared
|
|
136
|
+
`temporalio/auto-setup` container `internal/test-infra` starts once per machine and every
|
|
137
|
+
workspace reuses. Nothing else here does.
|
|
138
|
+
|
|
139
|
+
## Schedules
|
|
140
|
+
|
|
141
|
+
`@btravstack/temporal-worker/schedule` exports `ensureSchedule` — the typed
|
|
142
|
+
schedule client's `create`, made idempotent, so a deploy that runs again
|
|
143
|
+
updates the schedule instead of failing on `ScheduleAlreadyExistsError`.
|
|
144
|
+
`@temporal-contract/client` is an optional peer reached only through that
|
|
145
|
+
subpath. Why scheduled work costs a cluster here, and when it should not be
|
|
146
|
+
this stack at all:
|
|
147
|
+
[Run something on a schedule](https://btravstack.github.io/btravstack/how-to/run-something-on-a-schedule).
|
|
148
|
+
|
|
135
149
|
## Options
|
|
136
150
|
|
|
137
151
|
`TemporalModule(name)({...})` takes `temporal()`'s options plus `activities`
|
package/dist/index.cjs
CHANGED
|
@@ -20,7 +20,17 @@ let _temporalio_activity = require("@temporalio/activity");
|
|
|
20
20
|
* No `Result`-unwrapping boundary, deliberately: `declareActivitiesHandler` owns
|
|
21
21
|
* the mapping from a settled `Result` to an activity failure.
|
|
22
22
|
*/
|
|
23
|
-
const activityUnits = (host) => (_invocation, next) =>
|
|
23
|
+
const activityUnits = (host, observers) => (_invocation, next) => {
|
|
24
|
+
const settle = (0, _btravstack_core.observe)(observers, {
|
|
25
|
+
component: "temporal",
|
|
26
|
+
name: "attempt",
|
|
27
|
+
attributes: { activity: (0, _temporalio_activity.activityInfo)().activityType }
|
|
28
|
+
});
|
|
29
|
+
return host.run(metaFor(), () => next()).tap(() => settle({ outcome: "ok" })).tapFailure((failure) => settle({
|
|
30
|
+
outcome: "error",
|
|
31
|
+
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
32
|
+
}));
|
|
33
|
+
};
|
|
24
34
|
/**
|
|
25
35
|
* `UnitMeta.id` must be unique per unit, and a workflow id is **not** one: an
|
|
26
36
|
* activity is retried under the same execution. A task token identifies one
|
|
@@ -119,16 +129,21 @@ const temporal = (options) => {
|
|
|
119
129
|
})),
|
|
120
130
|
release: (connection) => connection.close().catch((cause) => heldByWorker(cause) ? void 0 : Promise.reject(cause))
|
|
121
131
|
}),
|
|
132
|
+
_btravstack_di.Provider.member(_btravstack_core.Observers)({
|
|
133
|
+
inject: {},
|
|
134
|
+
value: _btravstack_core.noObserver
|
|
135
|
+
}),
|
|
122
136
|
(0, _btravstack_di.Provider)(TemporalRuntime)({
|
|
123
137
|
inject: {
|
|
124
138
|
connection: TemporalConnection,
|
|
125
139
|
config: TemporalConfig,
|
|
126
|
-
activities
|
|
140
|
+
activities,
|
|
141
|
+
observers: _btravstack_core.Observers
|
|
127
142
|
},
|
|
128
|
-
sync: ({ connection, config: bound, activities: impls }) => ({
|
|
143
|
+
sync: ({ connection, config: bound, activities: impls, observers }) => ({
|
|
129
144
|
name: "temporal",
|
|
130
145
|
resolves: [],
|
|
131
|
-
start: (host) => createWorker(host, connection, bound, impls, options)
|
|
146
|
+
start: (host) => createWorker(host, connection, bound, impls, options, observers)
|
|
132
147
|
})
|
|
133
148
|
})
|
|
134
149
|
],
|
|
@@ -144,12 +159,12 @@ const startFailed = (cause) => new _btravstack_core.RuntimeStartFailed({
|
|
|
144
159
|
cause
|
|
145
160
|
});
|
|
146
161
|
const heldByWorker = (cause) => cause instanceof Error && cause.name === "IllegalStateError";
|
|
147
|
-
const createWorker = (host, connection, config, activities, options) => {
|
|
162
|
+
const createWorker = (host, connection, config, activities, options, observers) => {
|
|
148
163
|
const { taskQueue } = options.contract;
|
|
149
164
|
const { namespace } = config;
|
|
150
165
|
return (0, unthrown.fromThrowable)(() => (0, _temporal_contract_worker_activity.declareActivitiesHandler)({
|
|
151
166
|
contract: options.contract,
|
|
152
|
-
middleware: activityUnits(host),
|
|
167
|
+
middleware: activityUnits(host, observers),
|
|
153
168
|
activities
|
|
154
169
|
}), startFailed)().toAsync().flatMap((handler) => (0, unthrown.fromPromise)(_temporalio_worker.Worker.create({
|
|
155
170
|
connection,
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Module, Port, Provider } from "@btravstack/di";
|
|
2
2
|
import { Config, Env } from "@btravstack/config";
|
|
3
|
-
import { RuntimePort, RuntimeStartFailed, releasedBy } from "@btravstack/core";
|
|
3
|
+
import { Observers, RuntimePort, RuntimeStartFailed, noObserver, observe, releasedBy } from "@btravstack/core";
|
|
4
4
|
import { declareActivitiesHandler } from "@temporal-contract/worker/activity";
|
|
5
5
|
import { msToNumber } from "@temporalio/common";
|
|
6
6
|
import { NativeConnection, Worker } from "@temporalio/worker";
|
|
@@ -19,7 +19,17 @@ import { activityInfo } from "@temporalio/activity";
|
|
|
19
19
|
* No `Result`-unwrapping boundary, deliberately: `declareActivitiesHandler` owns
|
|
20
20
|
* the mapping from a settled `Result` to an activity failure.
|
|
21
21
|
*/
|
|
22
|
-
const activityUnits = (host) => (_invocation, next) =>
|
|
22
|
+
const activityUnits = (host, observers) => (_invocation, next) => {
|
|
23
|
+
const settle = observe(observers, {
|
|
24
|
+
component: "temporal",
|
|
25
|
+
name: "attempt",
|
|
26
|
+
attributes: { activity: activityInfo().activityType }
|
|
27
|
+
});
|
|
28
|
+
return host.run(metaFor(), () => next()).tap(() => settle({ outcome: "ok" })).tapFailure((failure) => settle({
|
|
29
|
+
outcome: "error",
|
|
30
|
+
cause: failure.tag === "Err" ? failure.error : failure.cause
|
|
31
|
+
}));
|
|
32
|
+
};
|
|
23
33
|
/**
|
|
24
34
|
* `UnitMeta.id` must be unique per unit, and a workflow id is **not** one: an
|
|
25
35
|
* activity is retried under the same execution. A task token identifies one
|
|
@@ -118,16 +128,21 @@ const temporal = (options) => {
|
|
|
118
128
|
})),
|
|
119
129
|
release: (connection) => connection.close().catch((cause) => heldByWorker(cause) ? void 0 : Promise.reject(cause))
|
|
120
130
|
}),
|
|
131
|
+
Provider.member(Observers)({
|
|
132
|
+
inject: {},
|
|
133
|
+
value: noObserver
|
|
134
|
+
}),
|
|
121
135
|
Provider(TemporalRuntime)({
|
|
122
136
|
inject: {
|
|
123
137
|
connection: TemporalConnection,
|
|
124
138
|
config: TemporalConfig,
|
|
125
|
-
activities
|
|
139
|
+
activities,
|
|
140
|
+
observers: Observers
|
|
126
141
|
},
|
|
127
|
-
sync: ({ connection, config: bound, activities: impls }) => ({
|
|
142
|
+
sync: ({ connection, config: bound, activities: impls, observers }) => ({
|
|
128
143
|
name: "temporal",
|
|
129
144
|
resolves: [],
|
|
130
|
-
start: (host) => createWorker(host, connection, bound, impls, options)
|
|
145
|
+
start: (host) => createWorker(host, connection, bound, impls, options, observers)
|
|
131
146
|
})
|
|
132
147
|
})
|
|
133
148
|
],
|
|
@@ -143,12 +158,12 @@ const startFailed = (cause) => new RuntimeStartFailed({
|
|
|
143
158
|
cause
|
|
144
159
|
});
|
|
145
160
|
const heldByWorker = (cause) => cause instanceof Error && cause.name === "IllegalStateError";
|
|
146
|
-
const createWorker = (host, connection, config, activities, options) => {
|
|
161
|
+
const createWorker = (host, connection, config, activities, options, observers) => {
|
|
147
162
|
const { taskQueue } = options.contract;
|
|
148
163
|
const { namespace } = config;
|
|
149
164
|
return fromThrowable(() => declareActivitiesHandler({
|
|
150
165
|
contract: options.contract,
|
|
151
|
-
middleware: activityUnits(host),
|
|
166
|
+
middleware: activityUnits(host, observers),
|
|
152
167
|
activities
|
|
153
168
|
}), startFailed)().toAsync().flatMap((handler) => fromPromise(Worker.create({
|
|
154
169
|
connection,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let unthrown = require("unthrown");
|
|
3
|
+
let _temporal_contract_client = require("@temporal-contract/client");
|
|
4
|
+
//#region src/schedule.ts
|
|
5
|
+
/**
|
|
6
|
+
* Register a schedule, idempotently.
|
|
7
|
+
*
|
|
8
|
+
* `TypedScheduleClient.create` answers `ScheduleAlreadyExistsError` for a
|
|
9
|
+
* schedule id already in use, which is correct and is also the wrong shape for
|
|
10
|
+
* the one place schedules are actually registered: a **deploy**, which runs
|
|
11
|
+
* again on every release. So the second run of a correct deploy script fails,
|
|
12
|
+
* and the usual repair is a `try`/ignore that also swallows the case where the
|
|
13
|
+
* schedule exists with the WRONG spec — a cron nobody notices has stopped
|
|
14
|
+
* matching what the code says.
|
|
15
|
+
*
|
|
16
|
+
* This recovers exactly that one error into an `update`. Every other error
|
|
17
|
+
* stays on the channel, still typed.
|
|
18
|
+
*
|
|
19
|
+
* **`spec` is the only field reconciled**, and the rest of an existing
|
|
20
|
+
* schedule is left as it stands. Two different reasons, and they are worth
|
|
21
|
+
* telling apart:
|
|
22
|
+
*
|
|
23
|
+
* - **`state` is deliberately preserved.** A schedule an operator paused stays
|
|
24
|
+
* paused across a deploy, because unpausing it is a decision a person made
|
|
25
|
+
* and a deploy is not the place to reverse it.
|
|
26
|
+
* - **`args` and the rest of the action are preserved because this cannot
|
|
27
|
+
* reconcile them SAFELY.** `create` validates `args` against the workflow's
|
|
28
|
+
* input schema; the handle's `update` takes Temporal's own
|
|
29
|
+
* `ScheduleUpdateOptions` and validates nothing, so writing them here would
|
|
30
|
+
* push unvalidated input at the server through a door the typed client keeps
|
|
31
|
+
* shut. A deploy that changes what a schedule RUNS — its args, its workflow,
|
|
32
|
+
* its policies — should say so explicitly: delete the schedule and create it,
|
|
33
|
+
* or reach `getHandle(id).update(...)` directly and own the shape.
|
|
34
|
+
*
|
|
35
|
+
* So: after this call the schedule FIRES when the arguments say. What it fires
|
|
36
|
+
* with is whatever it already fired with.
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* await ensureSchedule(client.for(orderContract).schedule, "sweepStaleOrders", {
|
|
40
|
+
* scheduleId: "sweep-stale-orders",
|
|
41
|
+
* spec: { cronExpressions: ["0 3 * * *"] },
|
|
42
|
+
* args: [{ olderThanDays: 30 }],
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
const ensureSchedule = (schedules, workflow, options) => schedules.create(workflow, options).map(() => "created").flatMapErrCases((matcher) => matcher.with(unthrown.P.tag(_temporal_contract_client.SCHEDULE_ALREADY_EXISTS_ERROR_TAG), () => schedules.getHandle(options.scheduleId).update((previous) => ({
|
|
47
|
+
...previous,
|
|
48
|
+
spec: options.spec
|
|
49
|
+
})).map(() => "updated")).with(unthrown.P.tag("@temporal-contract/WorkflowNotInContractError"), (error) => (0, unthrown.ErrAsync)(error)).with(unthrown.P.tag("@temporal-contract/WorkflowValidationError"), (error) => (0, unthrown.ErrAsync)(error)));
|
|
50
|
+
//#endregion
|
|
51
|
+
exports.ensureSchedule = ensureSchedule;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ContractDefinition } from "@temporal-contract/contract";
|
|
2
|
+
import { ScheduleNotFoundError, TypedScheduleClient, TypedScheduleCreateOptions, WorkflowNotInContractError, WorkflowValidationError } from "@temporal-contract/client";
|
|
3
|
+
import { AsyncResult } from "unthrown";
|
|
4
|
+
//#region src/schedule.d.ts
|
|
5
|
+
/** Whether the schedule had to be created, or was already there and had its spec reconciled. */
|
|
6
|
+
type ScheduleOutcome = "created" | "updated";
|
|
7
|
+
/**
|
|
8
|
+
* Register a schedule, idempotently.
|
|
9
|
+
*
|
|
10
|
+
* `TypedScheduleClient.create` answers `ScheduleAlreadyExistsError` for a
|
|
11
|
+
* schedule id already in use, which is correct and is also the wrong shape for
|
|
12
|
+
* the one place schedules are actually registered: a **deploy**, which runs
|
|
13
|
+
* again on every release. So the second run of a correct deploy script fails,
|
|
14
|
+
* and the usual repair is a `try`/ignore that also swallows the case where the
|
|
15
|
+
* schedule exists with the WRONG spec — a cron nobody notices has stopped
|
|
16
|
+
* matching what the code says.
|
|
17
|
+
*
|
|
18
|
+
* This recovers exactly that one error into an `update`. Every other error
|
|
19
|
+
* stays on the channel, still typed.
|
|
20
|
+
*
|
|
21
|
+
* **`spec` is the only field reconciled**, and the rest of an existing
|
|
22
|
+
* schedule is left as it stands. Two different reasons, and they are worth
|
|
23
|
+
* telling apart:
|
|
24
|
+
*
|
|
25
|
+
* - **`state` is deliberately preserved.** A schedule an operator paused stays
|
|
26
|
+
* paused across a deploy, because unpausing it is a decision a person made
|
|
27
|
+
* and a deploy is not the place to reverse it.
|
|
28
|
+
* - **`args` and the rest of the action are preserved because this cannot
|
|
29
|
+
* reconcile them SAFELY.** `create` validates `args` against the workflow's
|
|
30
|
+
* input schema; the handle's `update` takes Temporal's own
|
|
31
|
+
* `ScheduleUpdateOptions` and validates nothing, so writing them here would
|
|
32
|
+
* push unvalidated input at the server through a door the typed client keeps
|
|
33
|
+
* shut. A deploy that changes what a schedule RUNS — its args, its workflow,
|
|
34
|
+
* its policies — should say so explicitly: delete the schedule and create it,
|
|
35
|
+
* or reach `getHandle(id).update(...)` directly and own the shape.
|
|
36
|
+
*
|
|
37
|
+
* So: after this call the schedule FIRES when the arguments say. What it fires
|
|
38
|
+
* with is whatever it already fired with.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* await ensureSchedule(client.for(orderContract).schedule, "sweepStaleOrders", {
|
|
42
|
+
* scheduleId: "sweep-stale-orders",
|
|
43
|
+
* spec: { cronExpressions: ["0 3 * * *"] },
|
|
44
|
+
* args: [{ olderThanDays: 30 }],
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare const ensureSchedule: <C extends ContractDefinition, W extends keyof C["workflows"] & string>(schedules: TypedScheduleClient<C>, workflow: W, options: TypedScheduleCreateOptions<C, W>) => AsyncResult<ScheduleOutcome, WorkflowNotInContractError | WorkflowValidationError | ScheduleNotFoundError>;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { ScheduleOutcome, ensureSchedule };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AsyncResult } from "unthrown";
|
|
2
|
+
import { ScheduleNotFoundError, TypedScheduleClient, TypedScheduleCreateOptions, WorkflowNotInContractError, WorkflowValidationError } from "@temporal-contract/client";
|
|
3
|
+
import { ContractDefinition } from "@temporal-contract/contract";
|
|
4
|
+
//#region src/schedule.d.ts
|
|
5
|
+
/** Whether the schedule had to be created, or was already there and had its spec reconciled. */
|
|
6
|
+
type ScheduleOutcome = "created" | "updated";
|
|
7
|
+
/**
|
|
8
|
+
* Register a schedule, idempotently.
|
|
9
|
+
*
|
|
10
|
+
* `TypedScheduleClient.create` answers `ScheduleAlreadyExistsError` for a
|
|
11
|
+
* schedule id already in use, which is correct and is also the wrong shape for
|
|
12
|
+
* the one place schedules are actually registered: a **deploy**, which runs
|
|
13
|
+
* again on every release. So the second run of a correct deploy script fails,
|
|
14
|
+
* and the usual repair is a `try`/ignore that also swallows the case where the
|
|
15
|
+
* schedule exists with the WRONG spec — a cron nobody notices has stopped
|
|
16
|
+
* matching what the code says.
|
|
17
|
+
*
|
|
18
|
+
* This recovers exactly that one error into an `update`. Every other error
|
|
19
|
+
* stays on the channel, still typed.
|
|
20
|
+
*
|
|
21
|
+
* **`spec` is the only field reconciled**, and the rest of an existing
|
|
22
|
+
* schedule is left as it stands. Two different reasons, and they are worth
|
|
23
|
+
* telling apart:
|
|
24
|
+
*
|
|
25
|
+
* - **`state` is deliberately preserved.** A schedule an operator paused stays
|
|
26
|
+
* paused across a deploy, because unpausing it is a decision a person made
|
|
27
|
+
* and a deploy is not the place to reverse it.
|
|
28
|
+
* - **`args` and the rest of the action are preserved because this cannot
|
|
29
|
+
* reconcile them SAFELY.** `create` validates `args` against the workflow's
|
|
30
|
+
* input schema; the handle's `update` takes Temporal's own
|
|
31
|
+
* `ScheduleUpdateOptions` and validates nothing, so writing them here would
|
|
32
|
+
* push unvalidated input at the server through a door the typed client keeps
|
|
33
|
+
* shut. A deploy that changes what a schedule RUNS — its args, its workflow,
|
|
34
|
+
* its policies — should say so explicitly: delete the schedule and create it,
|
|
35
|
+
* or reach `getHandle(id).update(...)` directly and own the shape.
|
|
36
|
+
*
|
|
37
|
+
* So: after this call the schedule FIRES when the arguments say. What it fires
|
|
38
|
+
* with is whatever it already fired with.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* await ensureSchedule(client.for(orderContract).schedule, "sweepStaleOrders", {
|
|
42
|
+
* scheduleId: "sweep-stale-orders",
|
|
43
|
+
* spec: { cronExpressions: ["0 3 * * *"] },
|
|
44
|
+
* args: [{ olderThanDays: 30 }],
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare const ensureSchedule: <C extends ContractDefinition, W extends keyof C["workflows"] & string>(schedules: TypedScheduleClient<C>, workflow: W, options: TypedScheduleCreateOptions<C, W>) => AsyncResult<ScheduleOutcome, WorkflowNotInContractError | WorkflowValidationError | ScheduleNotFoundError>;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { ScheduleOutcome, ensureSchedule };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ErrAsync, P } from "unthrown";
|
|
2
|
+
import { SCHEDULE_ALREADY_EXISTS_ERROR_TAG } from "@temporal-contract/client";
|
|
3
|
+
//#region src/schedule.ts
|
|
4
|
+
/**
|
|
5
|
+
* Register a schedule, idempotently.
|
|
6
|
+
*
|
|
7
|
+
* `TypedScheduleClient.create` answers `ScheduleAlreadyExistsError` for a
|
|
8
|
+
* schedule id already in use, which is correct and is also the wrong shape for
|
|
9
|
+
* the one place schedules are actually registered: a **deploy**, which runs
|
|
10
|
+
* again on every release. So the second run of a correct deploy script fails,
|
|
11
|
+
* and the usual repair is a `try`/ignore that also swallows the case where the
|
|
12
|
+
* schedule exists with the WRONG spec — a cron nobody notices has stopped
|
|
13
|
+
* matching what the code says.
|
|
14
|
+
*
|
|
15
|
+
* This recovers exactly that one error into an `update`. Every other error
|
|
16
|
+
* stays on the channel, still typed.
|
|
17
|
+
*
|
|
18
|
+
* **`spec` is the only field reconciled**, and the rest of an existing
|
|
19
|
+
* schedule is left as it stands. Two different reasons, and they are worth
|
|
20
|
+
* telling apart:
|
|
21
|
+
*
|
|
22
|
+
* - **`state` is deliberately preserved.** A schedule an operator paused stays
|
|
23
|
+
* paused across a deploy, because unpausing it is a decision a person made
|
|
24
|
+
* and a deploy is not the place to reverse it.
|
|
25
|
+
* - **`args` and the rest of the action are preserved because this cannot
|
|
26
|
+
* reconcile them SAFELY.** `create` validates `args` against the workflow's
|
|
27
|
+
* input schema; the handle's `update` takes Temporal's own
|
|
28
|
+
* `ScheduleUpdateOptions` and validates nothing, so writing them here would
|
|
29
|
+
* push unvalidated input at the server through a door the typed client keeps
|
|
30
|
+
* shut. A deploy that changes what a schedule RUNS — its args, its workflow,
|
|
31
|
+
* its policies — should say so explicitly: delete the schedule and create it,
|
|
32
|
+
* or reach `getHandle(id).update(...)` directly and own the shape.
|
|
33
|
+
*
|
|
34
|
+
* So: after this call the schedule FIRES when the arguments say. What it fires
|
|
35
|
+
* with is whatever it already fired with.
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* await ensureSchedule(client.for(orderContract).schedule, "sweepStaleOrders", {
|
|
39
|
+
* scheduleId: "sweep-stale-orders",
|
|
40
|
+
* spec: { cronExpressions: ["0 3 * * *"] },
|
|
41
|
+
* args: [{ olderThanDays: 30 }],
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
const ensureSchedule = (schedules, workflow, options) => schedules.create(workflow, options).map(() => "created").flatMapErrCases((matcher) => matcher.with(P.tag(SCHEDULE_ALREADY_EXISTS_ERROR_TAG), () => schedules.getHandle(options.scheduleId).update((previous) => ({
|
|
46
|
+
...previous,
|
|
47
|
+
spec: options.spec
|
|
48
|
+
})).map(() => "updated")).with(P.tag("@temporal-contract/WorkflowNotInContractError"), (error) => ErrAsync(error)).with(P.tag("@temporal-contract/WorkflowValidationError"), (error) => ErrAsync(error)));
|
|
49
|
+
//#endregion
|
|
50
|
+
export { ensureSchedule };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@btravstack/temporal-worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The Temporal worker runtime for @btravstack/core: one unit per activity attempt, and a drain that honours the kernel's deadline",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graceful-shutdown",
|
|
@@ -41,12 +41,23 @@
|
|
|
41
41
|
"default": "./dist/index.cjs"
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
|
-
"./package.json": "./package.json"
|
|
44
|
+
"./package.json": "./package.json",
|
|
45
|
+
"./schedule": {
|
|
46
|
+
"import": {
|
|
47
|
+
"types": "./dist/schedule.d.mts",
|
|
48
|
+
"default": "./dist/schedule.mjs"
|
|
49
|
+
},
|
|
50
|
+
"require": {
|
|
51
|
+
"types": "./dist/schedule.d.cts",
|
|
52
|
+
"default": "./dist/schedule.cjs"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
45
55
|
},
|
|
46
56
|
"devDependencies": {
|
|
47
57
|
"@btravstack/tsconfig": "0.2.0",
|
|
48
|
-
"@temporal-contract/
|
|
49
|
-
"@temporal-contract/
|
|
58
|
+
"@temporal-contract/client": "8.0.0-beta.7",
|
|
59
|
+
"@temporal-contract/contract": "8.0.0-beta.7",
|
|
60
|
+
"@temporal-contract/worker": "8.0.0-beta.7",
|
|
50
61
|
"@temporalio/activity": "1.23.0",
|
|
51
62
|
"@temporalio/client": "1.23.0",
|
|
52
63
|
"@temporalio/common": "1.23.0",
|
|
@@ -59,30 +70,36 @@
|
|
|
59
70
|
"typescript": "7.0.2",
|
|
60
71
|
"unthrown": "5.7.0",
|
|
61
72
|
"vitest": "4.1.11",
|
|
62
|
-
"zod": "4.5.
|
|
63
|
-
"@btravstack/config": "0.
|
|
64
|
-
"@btravstack/core": "0.
|
|
65
|
-
"@btravstack/di": "0.
|
|
73
|
+
"zod": "4.5.3",
|
|
74
|
+
"@btravstack/config": "0.10.0",
|
|
75
|
+
"@btravstack/core": "0.10.0",
|
|
76
|
+
"@btravstack/di": "0.10.0",
|
|
66
77
|
"@btravstack/internal-test-infra": "0.0.0",
|
|
67
|
-
"@btravstack/testing": "0.
|
|
78
|
+
"@btravstack/testing": "0.10.0"
|
|
68
79
|
},
|
|
69
80
|
"peerDependencies": {
|
|
70
|
-
"@temporal-contract/
|
|
71
|
-
"@temporal-contract/
|
|
81
|
+
"@temporal-contract/client": "^8.0.0-beta.7",
|
|
82
|
+
"@temporal-contract/contract": "^8.0.0-beta.7",
|
|
83
|
+
"@temporal-contract/worker": "^8.0.0-beta.7",
|
|
72
84
|
"@temporalio/activity": "^1.22.0",
|
|
73
85
|
"@temporalio/common": "^1.22.0",
|
|
74
86
|
"@temporalio/worker": "^1.22.0",
|
|
75
87
|
"unthrown": "^5.0.0",
|
|
76
|
-
"@btravstack/config": "^0.
|
|
77
|
-
"@btravstack/core": "^0.
|
|
78
|
-
"@btravstack/di": "^0.
|
|
88
|
+
"@btravstack/config": "^0.10.0",
|
|
89
|
+
"@btravstack/core": "^0.10.0",
|
|
90
|
+
"@btravstack/di": "^0.10.0"
|
|
91
|
+
},
|
|
92
|
+
"peerDependenciesMeta": {
|
|
93
|
+
"@temporal-contract/client": {
|
|
94
|
+
"optional": true
|
|
95
|
+
}
|
|
79
96
|
},
|
|
80
97
|
"engines": {
|
|
81
98
|
"node": ">=22"
|
|
82
99
|
},
|
|
83
100
|
"scripts": {
|
|
84
|
-
"build": "tsdown src/index.ts --format cjs,esm --dts --clean",
|
|
85
|
-
"dev": "tsdown src/index.ts --format cjs,esm --dts --watch",
|
|
101
|
+
"build": "tsdown src/index.ts src/schedule.ts --format cjs,esm --dts --clean",
|
|
102
|
+
"dev": "tsdown src/index.ts src/schedule.ts --format cjs,esm --dts --watch",
|
|
86
103
|
"test": "vitest run --coverage",
|
|
87
104
|
"test:types": "tsc --noEmit -p tsconfig.test-d.json",
|
|
88
105
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json"
|