@btravstack/temporal-worker 0.14.0 → 0.16.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/dist/index.cjs CHANGED
@@ -151,7 +151,17 @@ const activityUnits = (host, observers, unit) => (invocation, next) => {
151
151
  *
152
152
  * The workflow id becomes the `traceId` — minted outside this process and steady
153
153
  * across every retry, so all attempts join up in a log. An activity with no
154
- * workflow falls back to the activity id.
154
+ * workflow falls back to the activity id; `workflowExecution` is optional
155
+ * because Temporal's own `Info` declares it so, "not set if the activity was
156
+ * not started by a Workflow", which a worker polling a task queue never sees.
157
+ *
158
+ * **A REUSED workflow id merges its runs into one trace**, and that is the
159
+ * trade rather than an oversight: Temporal allows an id again once the previous
160
+ * execution closed, so `order-42` run twice is one `traceId` over two runs.
161
+ * `runId` would separate them and is the wrong correlation for this transport —
162
+ * server-minted, opaque, and not what an operator greps for. A deployment that
163
+ * needs per-run traces gives its workflows unique ids, which is what a Schedule
164
+ * already does.
155
165
  */
156
166
  const metaFor = () => {
157
167
  const info = (0, _temporalio_activity.activityInfo)();
@@ -165,9 +175,9 @@ const metaFor = () => {
165
175
  //#region src/temporal-runtime.ts
166
176
  /**
167
177
  * Where the Temporal service is, as a service: `temporal()` binds it from the
168
- * environment — `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`) and
169
- * `TEMPORAL_NAMESPACE` (default `default`) — unless pinned, and anything else
170
- * in the graph may read it.
178
+ * environment — `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`),
179
+ * `TEMPORAL_NAMESPACE` (default `default`) and the shutdown budgets below —
180
+ * unless pinned, and anything else in the graph may read it.
171
181
  */
172
182
  var TemporalConfig = class extends (0, _btravstack_di.Port)("TemporalConfig") {};
173
183
  /**
@@ -206,14 +216,14 @@ const DEFAULT_FORCE_MS = 15e3;
206
216
  const durationMs = (duration) => duration === void 0 ? void 0 : (0, _temporalio_common.msToNumber)(duration);
207
217
  /**
208
218
  * The Temporal starter: a module providing the runtime, its configuration
209
- * (bound from `TEMPORAL_ADDRESS` / `TEMPORAL_NAMESPACE` unless pinned) and the
210
- * connection (a resource opened with the scope and closed with it; a service
211
- * that will not answer is a modeled `TemporalUnreachable`). Import it next to
212
- * the application, export `TemporalRuntime`, provide the activities — the
219
+ * (bound from the `TEMPORAL_*` variables unless pinned) and the connection (a
220
+ * resource opened with the scope and closed with it; a service that will not
221
+ * answer is a modeled `TemporalUnreachable`). Import it next to the
222
+ * application, export `TemporalRuntime`, provide the activities — the
213
223
  * activities port is a need of this module.
214
224
  *
215
- * With both configuration fields pinned the module reads nothing from the
216
- * environment; pin only one and the other still comes from it.
225
+ * Each configuration field is pinned on its own: a pinned field reads nothing
226
+ * from the environment, and a field left unpinned still does.
217
227
  */
218
228
  const temporal = (options) => {
219
229
  const { address, namespace } = options;
@@ -289,26 +299,39 @@ const createWorker = (host, connection, config, activities, options, observers)
289
299
  shutdownForceTime: config.forceAfterMs
290
300
  }), startFailed)).map((worker) => poll(worker, taskQueue, namespace));
291
301
  };
302
+ /**
303
+ * The `Serving` over a started worker.
304
+ *
305
+ * @internal Exported for `temporal-runtime.spec.ts` alone: the `stopped`
306
+ * channel is about a worker's `run()` REJECTING mid-flight, and there is no way
307
+ * to make a real one do that without taking the shared Temporal server down for
308
+ * every other spec. A stub worker is what reaches that arm.
309
+ */
292
310
  const poll = (worker, taskQueue, namespace) => {
293
311
  const running = (0, unthrown.fromSafePromise)(worker.run());
294
312
  const stopPolling = () => {
295
313
  if (worker.getState() === "RUNNING") worker.shutdown();
296
314
  };
297
315
  let deadline;
298
- const stopped = () => deadline === void 0 ? running : (0, _btravstack_core.releasedBy)(deadline, running);
316
+ const awaitRun = () => deadline === void 0 ? running : (0, _btravstack_core.releasedBy)(deadline, running);
317
+ let asked = false;
318
+ const withdrawn = () => (0, unthrown.fromSafePromise)(new Promise(() => {}));
299
319
  return {
300
320
  info: {
301
321
  taskQueue,
302
322
  namespace
303
323
  },
324
+ stopped: () => running.recoverDefect(() => (0, unthrown.Ok)(void 0)).flatMap(() => asked ? withdrawn() : (0, unthrown.OkAsync)()),
304
325
  drain: (signal) => {
326
+ asked = true;
305
327
  deadline = signal;
306
328
  stopPolling();
307
- return stopped();
329
+ return awaitRun();
308
330
  },
309
331
  stop: () => {
332
+ asked = true;
310
333
  stopPolling();
311
- return stopped();
334
+ return awaitRun();
312
335
  }
313
336
  };
314
337
  };
package/dist/index.d.cts CHANGED
@@ -134,9 +134,9 @@ type WorkflowSource = {
134
134
  declare const TemporalConfig_base: import("@btravstack/di").PortClass<"TemporalConfig">;
135
135
  /**
136
136
  * Where the Temporal service is, as a service: `temporal()` binds it from the
137
- * environment — `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`) and
138
- * `TEMPORAL_NAMESPACE` (default `default`) — unless pinned, and anything else
139
- * in the graph may read it.
137
+ * environment — `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`),
138
+ * `TEMPORAL_NAMESPACE` (default `default`) and the shutdown budgets below —
139
+ * unless pinned, and anything else in the graph may read it.
140
140
  */
141
141
  export declare class TemporalConfig extends TemporalConfig_base<{
142
142
  readonly address: string;
@@ -220,9 +220,10 @@ type TemporalTuning<Unit extends AnyUnitModule | undefined = undefined> = {
220
220
  readonly gracePeriod?: Duration;
221
221
  /**
222
222
  * The unit module the worker forks around every activity attempt it
223
- * dispatches, with no seed. Built after the activity is invoked, torn down
224
- * when the unit closes — the point where a later phase seeds it with the
225
- * workflow's tenant.
223
+ * dispatches, seeded with the validated input on `ActivityInput(contract)`.
224
+ * Built after the activity is invoked, before it runs; torn down when the
225
+ * unit closes. What it exports is what a piece may declare on its own
226
+ * `unit:` record.
226
227
  */
227
228
  readonly unit?: {
228
229
  readonly activity?: Unit;
@@ -240,14 +241,14 @@ type TemporalOptions<C extends ContractDefinition, Unit extends AnyUnitModule |
240
241
  type Provided = TemporalRuntime | TemporalConfig | TemporalConnection;
241
242
  /**
242
243
  * The Temporal starter: a module providing the runtime, its configuration
243
- * (bound from `TEMPORAL_ADDRESS` / `TEMPORAL_NAMESPACE` unless pinned) and the
244
- * connection (a resource opened with the scope and closed with it; a service
245
- * that will not answer is a modeled `TemporalUnreachable`). Import it next to
246
- * the application, export `TemporalRuntime`, provide the activities — the
244
+ * (bound from the `TEMPORAL_*` variables unless pinned) and the connection (a
245
+ * resource opened with the scope and closed with it; a service that will not
246
+ * answer is a modeled `TemporalUnreachable`). Import it next to the
247
+ * application, export `TemporalRuntime`, provide the activities — the
247
248
  * activities port is a need of this module.
248
249
  *
249
- * With both configuration fields pinned the module reads nothing from the
250
- * environment; pin only one and the other still comes from it.
250
+ * Each configuration field is pinned on its own: a pinned field reads nothing
251
+ * from the environment, and a field left unpinned still does.
251
252
  */
252
253
  export declare const temporal: <C extends ContractDefinition, Unit extends AnyUnitModule | undefined = undefined>(options: TemporalOptions<C, Unit>) => Module<Provided, ConfigInvalid | TemporalUnreachable, Env | Scope | ActivitiesInstanceOf<C> | UnitNeedsOf<Unit>>;
253
254
  //#endregion
package/dist/index.d.mts CHANGED
@@ -3,7 +3,7 @@ import { ConfigInvalid, Env } from "@btravstack/config";
3
3
  import { Runtime, RuntimePort } from "@btravstack/core";
4
4
  import { DeclareActivitiesHandlerOptions, EmptyContext } from "@temporal-contract/worker/activity";
5
5
  import { Duration } from "@temporalio/common";
6
- import { NativeConnection, WorkflowBundleWithSourceMap } from "@temporalio/worker";
6
+ import { NativeConnection, Worker, WorkflowBundleWithSourceMap } from "@temporalio/worker";
7
7
  import { ContractDefinition } from "@temporal-contract/contract";
8
8
  //#region src/unit.d.ts
9
9
  /** What a bound unit module exports — the port instances an activity of that kind may read. */
@@ -134,9 +134,9 @@ type WorkflowSource = {
134
134
  declare const TemporalConfig_base: import("@btravstack/di").PortClass<"TemporalConfig">;
135
135
  /**
136
136
  * Where the Temporal service is, as a service: `temporal()` binds it from the
137
- * environment — `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`) and
138
- * `TEMPORAL_NAMESPACE` (default `default`) — unless pinned, and anything else
139
- * in the graph may read it.
137
+ * environment — `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`),
138
+ * `TEMPORAL_NAMESPACE` (default `default`) and the shutdown budgets below —
139
+ * unless pinned, and anything else in the graph may read it.
140
140
  */
141
141
  export declare class TemporalConfig extends TemporalConfig_base<{
142
142
  readonly address: string;
@@ -220,9 +220,10 @@ type TemporalTuning<Unit extends AnyUnitModule | undefined = undefined> = {
220
220
  readonly gracePeriod?: Duration;
221
221
  /**
222
222
  * The unit module the worker forks around every activity attempt it
223
- * dispatches, with no seed. Built after the activity is invoked, torn down
224
- * when the unit closes — the point where a later phase seeds it with the
225
- * workflow's tenant.
223
+ * dispatches, seeded with the validated input on `ActivityInput(contract)`.
224
+ * Built after the activity is invoked, before it runs; torn down when the
225
+ * unit closes. What it exports is what a piece may declare on its own
226
+ * `unit:` record.
226
227
  */
227
228
  readonly unit?: {
228
229
  readonly activity?: Unit;
@@ -240,14 +241,14 @@ type TemporalOptions<C extends ContractDefinition, Unit extends AnyUnitModule |
240
241
  type Provided = TemporalRuntime | TemporalConfig | TemporalConnection;
241
242
  /**
242
243
  * The Temporal starter: a module providing the runtime, its configuration
243
- * (bound from `TEMPORAL_ADDRESS` / `TEMPORAL_NAMESPACE` unless pinned) and the
244
- * connection (a resource opened with the scope and closed with it; a service
245
- * that will not answer is a modeled `TemporalUnreachable`). Import it next to
246
- * the application, export `TemporalRuntime`, provide the activities — the
244
+ * (bound from the `TEMPORAL_*` variables unless pinned) and the connection (a
245
+ * resource opened with the scope and closed with it; a service that will not
246
+ * answer is a modeled `TemporalUnreachable`). Import it next to the
247
+ * application, export `TemporalRuntime`, provide the activities — the
247
248
  * activities port is a need of this module.
248
249
  *
249
- * With both configuration fields pinned the module reads nothing from the
250
- * environment; pin only one and the other still comes from it.
250
+ * Each configuration field is pinned on its own: a pinned field reads nothing
251
+ * from the environment, and a field left unpinned still does.
251
252
  */
252
253
  export declare const temporal: <C extends ContractDefinition, Unit extends AnyUnitModule | undefined = undefined>(options: TemporalOptions<C, Unit>) => Module<Provided, ConfigInvalid | TemporalUnreachable, Env | Scope | ActivitiesInstanceOf<C> | UnitNeedsOf<Unit>>;
253
254
  //#endregion
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import { Observers, RuntimePort, RuntimeStartFailed, noObserver, observe, releas
4
4
  import { declareActivitiesHandler } from "@temporal-contract/worker/activity";
5
5
  import { msToNumber } from "@temporalio/common";
6
6
  import { NativeConnection, Worker } from "@temporalio/worker";
7
- import { TaggedError, fromPromise, fromSafePromise, fromThrowable } from "unthrown";
7
+ import { Ok, OkAsync, TaggedError, fromPromise, fromSafePromise, fromThrowable } from "unthrown";
8
8
  import { activityInfo } from "@temporalio/activity";
9
9
  //#region src/unit.ts
10
10
  /**
@@ -150,7 +150,17 @@ const activityUnits = (host, observers, unit) => (invocation, next) => {
150
150
  *
151
151
  * The workflow id becomes the `traceId` — minted outside this process and steady
152
152
  * across every retry, so all attempts join up in a log. An activity with no
153
- * workflow falls back to the activity id.
153
+ * workflow falls back to the activity id; `workflowExecution` is optional
154
+ * because Temporal's own `Info` declares it so, "not set if the activity was
155
+ * not started by a Workflow", which a worker polling a task queue never sees.
156
+ *
157
+ * **A REUSED workflow id merges its runs into one trace**, and that is the
158
+ * trade rather than an oversight: Temporal allows an id again once the previous
159
+ * execution closed, so `order-42` run twice is one `traceId` over two runs.
160
+ * `runId` would separate them and is the wrong correlation for this transport —
161
+ * server-minted, opaque, and not what an operator greps for. A deployment that
162
+ * needs per-run traces gives its workflows unique ids, which is what a Schedule
163
+ * already does.
154
164
  */
155
165
  const metaFor = () => {
156
166
  const info = activityInfo();
@@ -164,9 +174,9 @@ const metaFor = () => {
164
174
  //#region src/temporal-runtime.ts
165
175
  /**
166
176
  * Where the Temporal service is, as a service: `temporal()` binds it from the
167
- * environment — `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`) and
168
- * `TEMPORAL_NAMESPACE` (default `default`) — unless pinned, and anything else
169
- * in the graph may read it.
177
+ * environment — `TEMPORAL_ADDRESS` (default `127.0.0.1:7233`),
178
+ * `TEMPORAL_NAMESPACE` (default `default`) and the shutdown budgets below —
179
+ * unless pinned, and anything else in the graph may read it.
170
180
  */
171
181
  var TemporalConfig = class extends Port("TemporalConfig") {};
172
182
  /**
@@ -205,14 +215,14 @@ const DEFAULT_FORCE_MS = 15e3;
205
215
  const durationMs = (duration) => duration === void 0 ? void 0 : msToNumber(duration);
206
216
  /**
207
217
  * The Temporal starter: a module providing the runtime, its configuration
208
- * (bound from `TEMPORAL_ADDRESS` / `TEMPORAL_NAMESPACE` unless pinned) and the
209
- * connection (a resource opened with the scope and closed with it; a service
210
- * that will not answer is a modeled `TemporalUnreachable`). Import it next to
211
- * the application, export `TemporalRuntime`, provide the activities — the
218
+ * (bound from the `TEMPORAL_*` variables unless pinned) and the connection (a
219
+ * resource opened with the scope and closed with it; a service that will not
220
+ * answer is a modeled `TemporalUnreachable`). Import it next to the
221
+ * application, export `TemporalRuntime`, provide the activities — the
212
222
  * activities port is a need of this module.
213
223
  *
214
- * With both configuration fields pinned the module reads nothing from the
215
- * environment; pin only one and the other still comes from it.
224
+ * Each configuration field is pinned on its own: a pinned field reads nothing
225
+ * from the environment, and a field left unpinned still does.
216
226
  */
217
227
  const temporal = (options) => {
218
228
  const { address, namespace } = options;
@@ -288,26 +298,39 @@ const createWorker = (host, connection, config, activities, options, observers)
288
298
  shutdownForceTime: config.forceAfterMs
289
299
  }), startFailed)).map((worker) => poll(worker, taskQueue, namespace));
290
300
  };
301
+ /**
302
+ * The `Serving` over a started worker.
303
+ *
304
+ * @internal Exported for `temporal-runtime.spec.ts` alone: the `stopped`
305
+ * channel is about a worker's `run()` REJECTING mid-flight, and there is no way
306
+ * to make a real one do that without taking the shared Temporal server down for
307
+ * every other spec. A stub worker is what reaches that arm.
308
+ */
291
309
  const poll = (worker, taskQueue, namespace) => {
292
310
  const running = fromSafePromise(worker.run());
293
311
  const stopPolling = () => {
294
312
  if (worker.getState() === "RUNNING") worker.shutdown();
295
313
  };
296
314
  let deadline;
297
- const stopped = () => deadline === void 0 ? running : releasedBy(deadline, running);
315
+ const awaitRun = () => deadline === void 0 ? running : releasedBy(deadline, running);
316
+ let asked = false;
317
+ const withdrawn = () => fromSafePromise(new Promise(() => {}));
298
318
  return {
299
319
  info: {
300
320
  taskQueue,
301
321
  namespace
302
322
  },
323
+ stopped: () => running.recoverDefect(() => Ok(void 0)).flatMap(() => asked ? withdrawn() : OkAsync()),
303
324
  drain: (signal) => {
325
+ asked = true;
304
326
  deadline = signal;
305
327
  stopPolling();
306
- return stopped();
328
+ return awaitRun();
307
329
  },
308
330
  stop: () => {
331
+ asked = true;
309
332
  stopPolling();
310
- return stopped();
333
+ return awaitRun();
311
334
  }
312
335
  };
313
336
  };
package/dist/schedule.cjs CHANGED
@@ -16,24 +16,29 @@ let _temporal_contract_client = require("@temporal-contract/client");
16
16
  * This recovers exactly that one error into an `update`. Every other error
17
17
  * stays on the channel, still typed.
18
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:
19
+ * **`spec`, the action's `workflowType` and `args`, and `policies` are
20
+ * reconciled**; everything else about an existing schedule is left as it
21
+ * stands.
22
22
  *
23
+ * - **`args` and `workflowType` are written, and they are checked on the way
24
+ * through.** The typed handle's `update` validates the returned action's
25
+ * `args` against the named workflow's input schema before anything is
26
+ * persisted, and answers `WorkflowValidationError` — the same error `create`
27
+ * answers, already on this channel. So a deploy that changed what the
28
+ * workflow takes is either written or refused, never reported `"updated"`
29
+ * over a server still holding the old action. `workflowType` rides with them
30
+ * because it is what selects the schema those args are checked against.
23
31
  * - **`state` is deliberately preserved.** A schedule an operator paused stays
24
32
  * paused across a deploy, because unpausing it is a decision a person made
25
33
  * 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
+ * - **`memo`, `searchAttributes` and the action's own overrides are
35
+ * preserved**, because rebuilding the action wholesale means reproducing
36
+ * `create`'s own assembly here — the task queue off the contract, the search
37
+ * attribute translation, eight optional overrides — which is a copy that
38
+ * drifts. A deploy changing one of those deletes the schedule and creates it.
34
39
  *
35
- * So: after this call the schedule FIRES when the arguments say. What it fires
36
- * with is whatever it already fired with.
40
+ * So: after this call the schedule FIRES when the arguments say, and RUNS what
41
+ * they say.
37
42
  *
38
43
  * ```ts
39
44
  * await ensureSchedule(client.for(orderContract).schedule, "sweepStaleOrders", {
@@ -45,7 +50,13 @@ let _temporal_contract_client = require("@temporal-contract/client");
45
50
  */
46
51
  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
52
  ...previous,
48
- spec: options.spec
53
+ spec: options.spec,
54
+ action: {
55
+ ...previous.action,
56
+ workflowType: workflow,
57
+ args: [options.args]
58
+ },
59
+ ...options.policies === void 0 ? {} : { policies: options.policies }
49
60
  })).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
61
  //#endregion
51
62
  exports.ensureSchedule = ensureSchedule;
@@ -18,24 +18,29 @@ export type ScheduleOutcome = "created" | "updated";
18
18
  * This recovers exactly that one error into an `update`. Every other error
19
19
  * stays on the channel, still typed.
20
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:
21
+ * **`spec`, the action's `workflowType` and `args`, and `policies` are
22
+ * reconciled**; everything else about an existing schedule is left as it
23
+ * stands.
24
24
  *
25
+ * - **`args` and `workflowType` are written, and they are checked on the way
26
+ * through.** The typed handle's `update` validates the returned action's
27
+ * `args` against the named workflow's input schema before anything is
28
+ * persisted, and answers `WorkflowValidationError` — the same error `create`
29
+ * answers, already on this channel. So a deploy that changed what the
30
+ * workflow takes is either written or refused, never reported `"updated"`
31
+ * over a server still holding the old action. `workflowType` rides with them
32
+ * because it is what selects the schema those args are checked against.
25
33
  * - **`state` is deliberately preserved.** A schedule an operator paused stays
26
34
  * paused across a deploy, because unpausing it is a decision a person made
27
35
  * 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
+ * - **`memo`, `searchAttributes` and the action's own overrides are
37
+ * preserved**, because rebuilding the action wholesale means reproducing
38
+ * `create`'s own assembly here — the task queue off the contract, the search
39
+ * attribute translation, eight optional overrides — which is a copy that
40
+ * drifts. A deploy changing one of those deletes the schedule and creates it.
36
41
  *
37
- * So: after this call the schedule FIRES when the arguments say. What it fires
38
- * with is whatever it already fired with.
42
+ * So: after this call the schedule FIRES when the arguments say, and RUNS what
43
+ * they say.
39
44
  *
40
45
  * ```ts
41
46
  * await ensureSchedule(client.for(orderContract).schedule, "sweepStaleOrders", {
@@ -18,24 +18,29 @@ export type ScheduleOutcome = "created" | "updated";
18
18
  * This recovers exactly that one error into an `update`. Every other error
19
19
  * stays on the channel, still typed.
20
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:
21
+ * **`spec`, the action's `workflowType` and `args`, and `policies` are
22
+ * reconciled**; everything else about an existing schedule is left as it
23
+ * stands.
24
24
  *
25
+ * - **`args` and `workflowType` are written, and they are checked on the way
26
+ * through.** The typed handle's `update` validates the returned action's
27
+ * `args` against the named workflow's input schema before anything is
28
+ * persisted, and answers `WorkflowValidationError` — the same error `create`
29
+ * answers, already on this channel. So a deploy that changed what the
30
+ * workflow takes is either written or refused, never reported `"updated"`
31
+ * over a server still holding the old action. `workflowType` rides with them
32
+ * because it is what selects the schema those args are checked against.
25
33
  * - **`state` is deliberately preserved.** A schedule an operator paused stays
26
34
  * paused across a deploy, because unpausing it is a decision a person made
27
35
  * 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
+ * - **`memo`, `searchAttributes` and the action's own overrides are
37
+ * preserved**, because rebuilding the action wholesale means reproducing
38
+ * `create`'s own assembly here — the task queue off the contract, the search
39
+ * attribute translation, eight optional overrides — which is a copy that
40
+ * drifts. A deploy changing one of those deletes the schedule and creates it.
36
41
  *
37
- * So: after this call the schedule FIRES when the arguments say. What it fires
38
- * with is whatever it already fired with.
42
+ * So: after this call the schedule FIRES when the arguments say, and RUNS what
43
+ * they say.
39
44
  *
40
45
  * ```ts
41
46
  * await ensureSchedule(client.for(orderContract).schedule, "sweepStaleOrders", {
package/dist/schedule.mjs CHANGED
@@ -15,24 +15,29 @@ import { SCHEDULE_ALREADY_EXISTS_ERROR_TAG } from "@temporal-contract/client";
15
15
  * This recovers exactly that one error into an `update`. Every other error
16
16
  * stays on the channel, still typed.
17
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:
18
+ * **`spec`, the action's `workflowType` and `args`, and `policies` are
19
+ * reconciled**; everything else about an existing schedule is left as it
20
+ * stands.
21
21
  *
22
+ * - **`args` and `workflowType` are written, and they are checked on the way
23
+ * through.** The typed handle's `update` validates the returned action's
24
+ * `args` against the named workflow's input schema before anything is
25
+ * persisted, and answers `WorkflowValidationError` — the same error `create`
26
+ * answers, already on this channel. So a deploy that changed what the
27
+ * workflow takes is either written or refused, never reported `"updated"`
28
+ * over a server still holding the old action. `workflowType` rides with them
29
+ * because it is what selects the schema those args are checked against.
22
30
  * - **`state` is deliberately preserved.** A schedule an operator paused stays
23
31
  * paused across a deploy, because unpausing it is a decision a person made
24
32
  * 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
+ * - **`memo`, `searchAttributes` and the action's own overrides are
34
+ * preserved**, because rebuilding the action wholesale means reproducing
35
+ * `create`'s own assembly here — the task queue off the contract, the search
36
+ * attribute translation, eight optional overrides — which is a copy that
37
+ * drifts. A deploy changing one of those deletes the schedule and creates it.
33
38
  *
34
- * So: after this call the schedule FIRES when the arguments say. What it fires
35
- * with is whatever it already fired with.
39
+ * So: after this call the schedule FIRES when the arguments say, and RUNS what
40
+ * they say.
36
41
  *
37
42
  * ```ts
38
43
  * await ensureSchedule(client.for(orderContract).schedule, "sweepStaleOrders", {
@@ -44,7 +49,13 @@ import { SCHEDULE_ALREADY_EXISTS_ERROR_TAG } from "@temporal-contract/client";
44
49
  */
45
50
  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
51
  ...previous,
47
- spec: options.spec
52
+ spec: options.spec,
53
+ action: {
54
+ ...previous.action,
55
+ workflowType: workflow,
56
+ args: [options.args]
57
+ },
58
+ ...options.policies === void 0 ? {} : { policies: options.policies }
48
59
  })).map(() => "updated")).with(P.tag("@temporal-contract/WorkflowNotInContractError"), (error) => ErrAsync(error)).with(P.tag("@temporal-contract/WorkflowValidationError"), (error) => ErrAsync(error)));
49
60
  //#endregion
50
61
  export { ensureSchedule };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/temporal-worker",
3
- "version": "0.14.0",
3
+ "version": "0.16.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",
@@ -54,6 +54,11 @@
54
54
  }
55
55
  },
56
56
  "devDependencies": {
57
+ "@btravstack/config": "0.16.0",
58
+ "@btravstack/core": "0.16.0",
59
+ "@btravstack/di": "0.16.0",
60
+ "@btravstack/internal-test-infra": "0.0.0",
61
+ "@btravstack/testing": "0.16.0",
57
62
  "@btravstack/tsconfig": "0.4.0",
58
63
  "@temporal-contract/client": "8.0.0-beta.9",
59
64
  "@temporal-contract/contract": "8.0.0-beta.9",
@@ -70,24 +75,19 @@
70
75
  "typescript": "7.0.2",
71
76
  "unthrown": "5.8.0",
72
77
  "vitest": "4.1.11",
73
- "zod": "4.5.4",
74
- "@btravstack/config": "0.14.0",
75
- "@btravstack/core": "0.14.0",
76
- "@btravstack/di": "0.14.0",
77
- "@btravstack/internal-test-infra": "0.0.0",
78
- "@btravstack/testing": "0.14.0"
78
+ "zod": "4.5.4"
79
79
  },
80
80
  "peerDependencies": {
81
+ "@btravstack/config": "^0.16.0",
82
+ "@btravstack/core": "^0.16.0",
83
+ "@btravstack/di": "^0.16.0",
81
84
  "@temporal-contract/client": "^8.0.0-beta.9",
82
85
  "@temporal-contract/contract": "^8.0.0-beta.9",
83
86
  "@temporal-contract/worker": "^8.0.0-beta.9",
84
87
  "@temporalio/activity": "^1.22.0",
85
88
  "@temporalio/common": "^1.22.0",
86
89
  "@temporalio/worker": "^1.22.0",
87
- "unthrown": "^5.0.0",
88
- "@btravstack/config": "^0.14.0",
89
- "@btravstack/core": "^0.14.0",
90
- "@btravstack/di": "^0.14.0"
90
+ "unthrown": "^5.0.0"
91
91
  },
92
92
  "peerDependenciesMeta": {
93
93
  "@temporal-contract/client": {
@@ -101,7 +101,6 @@
101
101
  "build": "tsdown src/index.ts src/schedule.ts --format cjs,esm --dts --clean",
102
102
  "dev": "tsdown src/index.ts src/schedule.ts --format cjs,esm --dts --watch",
103
103
  "test": "vitest run --coverage",
104
- "test:types": "tsc --noEmit -p tsconfig.test-d.json",
105
104
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json"
106
105
  }
107
106
  }