@btravstack/temporal-worker 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,10 +1,119 @@
1
- import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider, Scope } from "@btravstack/di";
1
+ import { AnyModule, AnyPort, AnyProvider, Exportable, Module, NeedsGate, PortClassOf, PortInstance, Provider, Scope, ServiceOf } from "@btravstack/di";
2
2
  import { ConfigInvalid, Env } from "@btravstack/config";
3
3
  import { Runtime, RuntimePort } from "@btravstack/core";
4
- import { DeclareActivitiesHandlerOptions } from "@temporal-contract/worker/activity";
4
+ import { DeclareActivitiesHandlerOptions, EmptyContext } from "@temporal-contract/worker/activity";
5
5
  import { Duration } from "@temporalio/common";
6
6
  import { NativeConnection, WorkflowBundleWithSourceMap } from "@temporalio/worker";
7
7
  import { ContractDefinition } from "@temporal-contract/contract";
8
+ //#region src/unit.d.ts
9
+ /** What a bound unit module exports — the port instances an activity of that kind may read. */
10
+ type UnitExportsOf<M> = M extends Module<infer X, never, unknown> ? X : never;
11
+ /** The declared `unit:` record, as the services an activity reads off `context.unit`. */
12
+ type UnitRecordOf<U extends Readonly<Record<string, AnyPort>>> = { readonly [N in keyof U]: ServiceOf<InstanceType<U[N]>>; };
13
+ /**
14
+ * A piece injecting a port the bound `unit.activity` module does not export.
15
+ * `unknown` when every declared port is covered — including the empty case, so
16
+ * a worker whose pieces declare no `unit:` is gated on nothing.
17
+ */
18
+ type UnitGate<Unit, Declared> = [Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>] extends [never] ? unknown : {
19
+ readonly "UNIT DOES NOT PROVIDE — a piece injects a port the bound unit module does not export": Exclude<NonNullable<Declared>, UnitExportsOf<Unit>>;
20
+ };
21
+ //#endregion
22
+ //#region src/workflow-activities.d.ts
23
+ /**
24
+ * `ActivitiesOf<C, …>` is a `NoInfer`-wrapped conditional TypeScript refuses to
25
+ * index by a generic key. Routing it through `infer` resolves it to a plain
26
+ * object type first, which IS indexable.
27
+ */
28
+ type ActivitiesRecordOf<C extends ContractDefinition, TContext extends Record<string, unknown> | EmptyContext = EmptyContext> = ActivitiesOf<C, TContext> extends (infer Resolved) ? Resolved : never;
29
+ /** The top-level keys of `C`'s activities record: a workflow that declares activities, or a contract-global activity. */
30
+ type ActivitiesKeyOf<C extends ContractDefinition> = keyof ActivitiesRecordOf<C> & string;
31
+ /**
32
+ * One entry's validated input, reached through the implementation's own second
33
+ * parameter — a workflow key carries a record of implementations, a
34
+ * contract-global one carries the implementation itself. `WorkerInferInput` is
35
+ * declared by `@temporal-contract/worker` and exported from none of its
36
+ * subpaths, so the by-index route is the only one open.
37
+ */
38
+ type InputOfEntry<V> = V extends ((helpers: never, input: infer I) => unknown) ? I : V extends Readonly<Record<string, unknown>> ? InputOfEntry<V[keyof V]> : never;
39
+ /** The validated activity input, typed by the contract: the union of every activity's own input. */
40
+ type ActivityInputOf<C extends ContractDefinition> = InputOfEntry<ActivitiesRecordOf<C>[ActivitiesKeyOf<C>]>;
41
+ /** The seeded port's class, typed for `C`: what `ActivityInput(contract)` answers. */
42
+ type ActivityInputPortOf<C extends ContractDefinition> = PortClassOf<"ActivityInput", ActivityInputOf<C>>;
43
+ /**
44
+ * The seed's port instance, as it appears in a needs union — subtracted from
45
+ * what a bound `unit.activity` module still owes, since the fork is what
46
+ * discharges it.
47
+ */
48
+ type ActivityInputInstance = PortInstance<"ActivityInput", unknown>;
49
+ /**
50
+ * The validated activity input as a port: the one thing the worker seeds the
51
+ * fork with, so a `unit.activity` module derives a tenant — or anything else —
52
+ * from the invocation rather than from an ambient record.
53
+ *
54
+ * ```ts
55
+ * const Input = ActivityInput(orderContract);
56
+ * const ActivityUnit = Module("ActivityUnit")({
57
+ * needs: [Input],
58
+ * provides: [Provider(Tenant)({ inject: { input: Input }, sync: ({ input }) => input.tenantId })],
59
+ * exports: [Tenant],
60
+ * });
61
+ * ```
62
+ *
63
+ * `contract` is read for its TYPE only. One `Port(...)` call fixed per contract
64
+ * at the type level, the move `TemporalActivitiesPort` makes, so a module built
65
+ * for one contract cannot read another's input.
66
+ */
67
+ declare const ActivityInput: <C extends ContractDefinition>(contract: C) => ActivityInputPortOf<C>;
68
+ /** The prefix a piece's port id carries; the composing form strips it to recover the key. */
69
+ declare const WORKFLOW_ACTIVITIES_PREFIX = "TemporalWorkflowActivities:";
70
+ /**
71
+ * The port one piece targets. Its id carries the key, which is what makes two
72
+ * slices claiming one workflow di's duplicate-provider defect rather than a
73
+ * silent merge.
74
+ */
75
+ type WorkflowActivitiesPortOf<C extends ContractDefinition, K extends ActivitiesKeyOf<C>> = PortClassOf<`${typeof WORKFLOW_ACTIVITIES_PREFIX}${K}`, ActivitiesRecordOf<C>[K]>;
76
+ /** What a minted piece returns. */
77
+ type MintedActivities<C extends ContractDefinition, K extends ActivitiesKeyOf<C>, N, U extends Readonly<Record<string, AnyPort>>> = Provider<InstanceType<WorkflowActivitiesPortOf<C, K>>, never, N> & {
78
+ readonly port: WorkflowActivitiesPortOf<C, K>;
79
+ /** The declared `unit:` record, which the wrapper resolves against. */
80
+ readonly unit: U;
81
+ /** Phantom: the ports this piece injects, which the root's `unit.activity` must export. */
82
+ readonly _declared?: InstanceType<U[keyof U]>;
83
+ };
84
+ /** The activities `sync` hands back, typed by the record THIS piece declared. */
85
+ type ScopedActivitiesOf<C extends ContractDefinition, K extends ActivitiesKeyOf<C>, U extends Readonly<Record<string, AnyPort>>> = ActivitiesRecordOf<C, {
86
+ readonly unit: UnitRecordOf<U>;
87
+ }>[K & keyof ActivitiesRecordOf<C, {
88
+ readonly unit: UnitRecordOf<U>;
89
+ }>];
90
+ /**
91
+ * One workflow's activities, as a provider on a port of its own.
92
+ *
93
+ * A worker that polls one queue for several workflows is several pieces, each
94
+ * declaring the services its own activities call;
95
+ * `TemporalActivities(contract)([...])` composes them. `contract` is read for
96
+ * its TYPE only, and types both `key` and the record — so an activity the
97
+ * workflow does not declare is a compile error here rather than at startup.
98
+ *
99
+ * `key` is any top-level key of the activities record, which includes a
100
+ * contract-global activity as well as a workflow; the name is imprecise in that
101
+ * one case deliberately, since narrowing it would lock such a contract out of
102
+ * the split.
103
+ *
104
+ * There is no name to give: the key IS the port's name.
105
+ *
106
+ * `unit` declares the ports the activities read off `context.unit`, resolved
107
+ * out of the fork the attempt opened; the root's `unit.activity` module must
108
+ * export every one of them.
109
+ */
110
+ declare const TemporalWorkflowActivities: <C extends ContractDefinition, const K extends ActivitiesKeyOf<C>>(contract: C, key: K) => <const D extends Readonly<Record<string, AnyPort>>, const U extends Readonly<Record<string, AnyPort>> = Record<never, never>>(options: {
111
+ readonly inject: D;
112
+ /** The unit-scoped ports these activities read off `context.unit`. */
113
+ readonly unit?: U;
114
+ readonly sync: (services: { readonly [N in keyof D]: ServiceOf<InstanceType<D[N]>>; }) => ScopedActivitiesOf<C, K, U>;
115
+ }) => MintedActivities<C, K, InstanceType<D[keyof D]>, U>;
116
+ //#endregion
8
117
  //#region src/temporal-runtime.d.ts
9
118
  /** What the worker publishes once it is polling, read back through `RunningApp.runtimeInfo()`. */
10
119
  type TemporalInfo = {
@@ -58,8 +167,29 @@ declare class TemporalUnreachable extends TemporalUnreachable_base<{
58
167
  }
59
168
  /** The runtime's port: what `temporal()` provides, and what the module `start` boots must export. */
60
169
  declare class TemporalRuntime extends RuntimePort<Runtime<never, TemporalInfo>> {}
61
- /** The activity implementations `declareActivitiesHandler` takes for `C`, with no injected context. */
62
- type ActivitiesOf<C extends ContractDefinition> = DeclareActivitiesHandlerOptions<C>["activities"];
170
+ /**
171
+ * A module `unit.activity` may bind, as the upper bound `temporal()`
172
+ * constrains its own `Unit` type parameter to. `Module`'s `_exports` channel
173
+ * is contravariant, so `Exports = never` — never `unknown` — is what makes a
174
+ * REAL module's own (necessarily narrower) export type assignable to this
175
+ * bound: `(x: Concrete) => void` is assignable to `(x: never) => void`, not
176
+ * to `(x: unknown) => void`.
177
+ */
178
+ type AnyUnitModule = Module<never, never, unknown>;
179
+ /**
180
+ * The needs a bound `unit.activity` module still owes, or `never` when none is
181
+ * bound. `Scope` is excluded, since nothing can ever provide it — the same
182
+ * exemption `NeedsGate` itself carries — and so is the activity input, which
183
+ * the fork's own seed discharges.
184
+ */
185
+ type UnitNeedsOf<Unit> = Unit extends Module<never, never, infer N> ? Exclude<N, Scope | ActivityInputInstance> : never;
186
+ /**
187
+ * The activity implementations `declareActivitiesHandler` takes for `C`.
188
+ * `TContext` is what an implementation reads off `helpers.context` — left
189
+ * empty for the port the composed record lands on, and the declared
190
+ * `{ unit }` record for the piece that declared it.
191
+ */
192
+ type ActivitiesOf<C extends ContractDefinition, TContext extends Record<string, unknown> | EmptyContext = EmptyContext> = DeclareActivitiesHandlerOptions<C, TContext>["activities"];
63
193
  /** The activities port class, typed for `C`: what `TemporalActivities(contract)(…).port` is. */
64
194
  type ActivitiesPortOf<C extends ContractDefinition> = PortClassOf<"TemporalActivities", ActivitiesOf<C>>;
65
195
  /** The activities port's instance for `C` — the module's one need. */
@@ -69,7 +199,7 @@ type ActivitiesInstanceOf<C extends ContractDefinition> = PortInstance<"Temporal
69
199
  * `TemporalModule` — spelled once so the two cannot drift, which is what a
70
200
  * second copy of an option list always eventually does.
71
201
  */
72
- type TemporalTuning = {
202
+ type TemporalTuning<Unit extends AnyUnitModule | undefined = undefined> = {
73
203
  /** Pins `TemporalConfig.address` instead of reading `TEMPORAL_ADDRESS`. */
74
204
  readonly address?: string;
75
205
  /** Pins `TemporalConfig.namespace` instead of reading `TEMPORAL_NAMESPACE`. */
@@ -88,8 +218,17 @@ type TemporalTuning = {
88
218
  * `shutdownGraceTime`.
89
219
  */
90
220
  readonly gracePeriod?: Duration;
221
+ /**
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.
226
+ */
227
+ readonly unit?: {
228
+ readonly activity?: Unit;
229
+ };
91
230
  };
92
- type TemporalOptions<C extends ContractDefinition> = TemporalTuning & {
231
+ type TemporalOptions<C extends ContractDefinition, Unit extends AnyUnitModule | undefined = undefined> = TemporalTuning<Unit> & {
93
232
  /**
94
233
  * The contract; the task queue this worker polls is read off it, and the
95
234
  * activities port is typed by it. The starter calls
@@ -110,56 +249,28 @@ type Provided = TemporalRuntime | TemporalConfig | TemporalConnection;
110
249
  * With both configuration fields pinned the module reads nothing from the
111
250
  * environment; pin only one and the other still comes from it.
112
251
  */
113
- declare const temporal: <C extends ContractDefinition>(options: TemporalOptions<C>) => Module<Provided, ConfigInvalid | TemporalUnreachable, Env | Scope | ActivitiesInstanceOf<C>>;
114
- //#endregion
115
- //#region src/workflow-activities.d.ts
116
- /**
117
- * `ActivitiesOf<C>` is a `NoInfer`-wrapped conditional TypeScript refuses to
118
- * index by a generic key. Routing it through `infer` resolves it to a plain
119
- * object type first, which IS indexable.
120
- */
121
- type ActivitiesRecordOf<C extends ContractDefinition> = ActivitiesOf<C> extends (infer Resolved) ? Resolved : never;
122
- /** The top-level keys of `C`'s activities record: a workflow that declares activities, or a contract-global activity. */
123
- type ActivitiesKeyOf<C extends ContractDefinition> = keyof ActivitiesRecordOf<C> & string;
124
- /** The prefix a piece's port id carries; the composing form strips it to recover the key. */
125
- declare const WORKFLOW_ACTIVITIES_PREFIX = "TemporalWorkflowActivities:";
126
- /**
127
- * The port one piece targets. Its id carries the key, which is what makes two
128
- * slices claiming one workflow di's duplicate-provider defect rather than a
129
- * silent merge.
130
- */
131
- type WorkflowActivitiesPortOf<C extends ContractDefinition, K extends ActivitiesKeyOf<C>> = PortClassOf<`${typeof WORKFLOW_ACTIVITIES_PREFIX}${K}`, ActivitiesRecordOf<C>[K]>;
132
- /**
133
- * One workflow's activities, as a provider on a port of its own.
134
- *
135
- * A worker that polls one queue for several workflows is several pieces, each
136
- * declaring the services its own activities call;
137
- * `TemporalActivities(contract)([...])` composes them. `contract` is read for
138
- * its TYPE only, and types both `key` and the record — so an activity the
139
- * workflow does not declare is a compile error here rather than at startup.
140
- *
141
- * `key` is any top-level key of the activities record, which includes a
142
- * contract-global activity as well as a workflow; the name is imprecise in that
143
- * one case deliberately, since narrowing it would lock such a contract out of
144
- * the split.
145
- *
146
- * There is no name to give: the key IS the port's name.
147
- */
148
- declare const TemporalWorkflowActivities: <C extends ContractDefinition, const K extends ActivitiesKeyOf<C>>(contract: C, key: K) => ReturnType<typeof Provider<WorkflowActivitiesPortOf<C, K>>>;
252
+ 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>>;
149
253
  //#endregion
150
254
  //#region src/temporal-module.d.ts
151
255
  /** The starter's own module, as the sugar adds it to the application's imports. */
152
- type TemporalStarter<C extends ContractDefinition> = Module<TemporalRuntime | TemporalConfig | TemporalConnection, ConfigInvalid | TemporalUnreachable, Env | Scope | ActivitiesInstanceOf<C>>;
256
+ type TemporalStarter<C extends ContractDefinition, Unit extends AnyUnitModule | undefined> = Module<TemporalRuntime | TemporalConfig | TemporalConnection, ConfigInvalid | TemporalUnreachable, Env | Scope | ActivitiesInstanceOf<C> | UnitNeedsOf<Unit>>;
153
257
  /** The application's imports plus the starter — the tuple `Module(name)` is handed. */
154
- type Imports<I extends readonly AnyModule[], C extends ContractDefinition> = readonly [...I, TemporalStarter<C>];
258
+ type Imports<I extends readonly AnyModule[], C extends ContractDefinition, Unit extends AnyUnitModule | undefined> = readonly [...I, TemporalStarter<C, Unit>];
155
259
  /** The activities provider plus the application's own — the tuple `Module(name)` is handed. */
156
260
  type Provides<P extends readonly AnyProvider[], C extends ContractDefinition, ActivitiesError, ActivitiesNeeds> = readonly [Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>, ...P];
157
- 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[]> = TemporalTuning & {
261
+ type TemporalModuleOptions<C extends ContractDefinition, ActivitiesError, ActivitiesNeeds, Declared, Unit extends AnyUnitModule | undefined, I extends readonly AnyModule[], P extends readonly AnyProvider[], X extends readonly Exportable<Imports<I, C, Unit>, Provides<P, C, ActivitiesError, ActivitiesNeeds>>[], N extends readonly AnyPort[]> = TemporalTuning<Unit> & {
158
262
  /** The `temporal-contract` contract; the task queue this worker polls is read off it. */
159
263
  readonly contract: C;
160
264
  readonly workflows: WorkflowSource;
161
- /** The application's activity implementations — what `TemporalActivities(contract)(…)` returns. */
162
- readonly activities: Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds>;
265
+ /**
266
+ * The application's activity implementations — what
267
+ * `TemporalActivities(contract)(…)` returns. Its `_declaredUnit` phantom
268
+ * carries every port its pieces read off `context.unit`, which is what
269
+ * {@link UnitGate} checks `unit.activity` against.
270
+ */
271
+ readonly activities: Provider<ActivitiesInstanceOf<C>, ActivitiesError, ActivitiesNeeds> & {
272
+ readonly _declaredUnit?: Declared;
273
+ };
163
274
  readonly imports?: I;
164
275
  readonly provides?: P;
165
276
  /** The application's own exports; `TemporalRuntime` is added, since `start` resolves it. */
@@ -169,7 +280,7 @@ type TemporalModuleOptions<C extends ContractDefinition, ActivitiesError, Activi
169
280
  * over the augmented tuples below, so forgetting one is an error at THIS call.
170
281
  */
171
282
  readonly needs?: N;
172
- } & NeedsGate<Imports<I, C>, Provides<P, C, ActivitiesError, ActivitiesNeeds>, N>;
283
+ } & NeedsGate<Imports<I, C, Unit>, Provides<P, C, ActivitiesError, ActivitiesNeeds>, N> & UnitGate<Unit, Declared>;
173
284
  /**
174
285
  * `Module(name)({...})` for a Temporal worker deployment: everything a di module
175
286
  * takes, plus the contract, the activities provider and the workflow source. The
@@ -187,11 +298,17 @@ type TemporalModuleOptions<C extends ContractDefinition, ActivitiesError, Activi
187
298
  * await runMain(OrderTemporalWorker);
188
299
  * ```
189
300
  */
190
- 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]>>;
301
+ declare const TemporalModule: <const Name extends string>(name: Name) => <C extends ContractDefinition, ActivitiesError, ActivitiesNeeds, Declared = never, Unit extends AnyUnitModule | undefined = undefined, const I extends readonly AnyModule[] = [], const P extends readonly AnyProvider[] = [], const X extends readonly Exportable<Imports<I, C, Unit>, Provides<P, C, ActivitiesError, ActivitiesNeeds>>[] = [], const N extends readonly AnyPort[] = []>(options: TemporalModuleOptions<C, ActivitiesError, ActivitiesNeeds, Declared, Unit, 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, Unit>])[number] extends (infer T_2) ? T_2 extends (readonly [...I, TemporalStarter<C, Unit>])[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, Unit>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, TemporalStarter<C, Unit>])[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, Unit>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, TemporalStarter<C, Unit>])[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, Unit>])[number] extends (infer T_4) ? T_4 extends (readonly [...I, TemporalStarter<C, Unit>])[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]>>;
191
302
  /** One piece of the activities record — what `TemporalWorkflowActivities(contract, key)(…)` returns. */
192
303
  type PieceOf<C extends ContractDefinition> = { readonly [K in ActivitiesKeyOf<C>]: {
193
304
  readonly port: WorkflowActivitiesPortOf<C, K>;
305
+ /** Phantom, carrying the ports the piece injects off `context.unit`. */
306
+ readonly _declared?: unknown;
194
307
  }; }[ActivitiesKeyOf<C>];
308
+ /** Every port the pieces in `T` inject off `context.unit` — what the root's `unit.activity` must export. */
309
+ type DeclaredOf<T extends readonly {
310
+ readonly _declared?: unknown;
311
+ }[]> = NonNullable<T[number]["_declared"]>;
195
312
  /** The key a piece carries, read back off its port id. */
196
313
  type KeyOfPiece<P> = P extends {
197
314
  readonly port: {
@@ -213,6 +330,29 @@ type Uncovered<C extends ContractDefinition, T extends readonly PieceOf<C>[]> =
213
330
  * hand.
214
331
  */
215
332
  type Refuse<T extends readonly unknown[], Marker extends string, Detail> = T extends readonly [...infer Head, unknown] ? readonly [...Head, readonly [Marker, Detail]] : readonly [readonly [Marker, Detail]];
333
+ /**
334
+ * The record arm: the whole activities record from one `sync`, with one `unit:`
335
+ * record shared by every entry in it.
336
+ *
337
+ * It is `{ inject, unit?, sync }` — di's other arms are gone — for PARITY, not
338
+ * for a type-system reason: `value` could have carried the record just as well,
339
+ * since `U` infers from `unit` and never from the arm. `@btravstack/http-server`'s
340
+ * `api.OrpcRouter(contract)` is `{ inject, unit?, sync }` and so are all three
341
+ * packages' piece factories, so one arm across the family is one surface to
342
+ * learn and one to keep.
343
+ */
344
+ type Whole<C extends ContractDefinition> = <const D extends Readonly<Record<string, AnyPort>>, const U extends Readonly<Record<string, AnyPort>> = Record<never, never>>(options: {
345
+ readonly inject: D;
346
+ /** The unit-scoped ports every activity in the record reads off `context.unit`. */
347
+ readonly unit?: U;
348
+ readonly sync: (services: { readonly [N in keyof D]: ServiceOf<InstanceType<D[N]>>; }) => ActivitiesOf<C, {
349
+ readonly unit: UnitRecordOf<U>;
350
+ }>;
351
+ }) => Provider<ActivitiesInstanceOf<C>, never, InstanceType<D[keyof D]>> & {
352
+ readonly port: ActivitiesPortOf<C>;
353
+ /** Phantom: the declared ports, which `TemporalModule` gates `unit.activity` against. */
354
+ readonly _declaredUnit?: InstanceType<U[keyof U]>;
355
+ };
216
356
  /**
217
357
  * The composing arm. Declared LAST in the intersection below on purpose:
218
358
  * TypeScript reports the last overload's failure, so a non-covering array is
@@ -224,6 +364,8 @@ type Refuse<T extends readonly unknown[], Marker extends string, Detail> = T ext
224
364
  */
225
365
  type Compose<C extends ContractDefinition> = <const T extends readonly PieceOf<C>[]>(pieces: [Uncovered<C, T>] extends [never] ? T : Refuse<T, "UNCOVERED ACTIVITIES — the contract declares a workflow this array does not cover", Uncovered<C, T>>) => Provider<ActivitiesInstanceOf<C>, never, InstanceType<T[number]["port"]>> & {
226
366
  readonly port: ActivitiesPortOf<C>;
367
+ /** Phantom: the union of every piece's declared ports, which `TemporalModule` gates `unit.activity` against. */
368
+ readonly _declaredUnit?: DeclaredOf<T>;
227
369
  };
228
370
  /**
229
371
  * The activities as a provider, from the contract. Two call forms, one port.
@@ -233,13 +375,13 @@ type Compose<C extends ContractDefinition> = <const T extends readonly PieceOf<C
233
375
  * TemporalActivities(orderContract)([fulfillOrder, chargeOrder])
234
376
  * ```
235
377
  *
236
- * The first is di's own `Provider(port)` on the starter's activities port
237
- * typed for the contract. The second takes the pieces
238
- * `TemporalWorkflowActivities(contract, key)` builds: they are the provider's
239
- * deps, keyed by the contract key each piece's port id carries, so the services
240
- * record IS the activities record. Every key must be covered, and two slices
241
- * claiming one key are di's duplicate-provider defect at build.
242
- */
243
- declare const TemporalActivities: <C extends ContractDefinition>(contract: C) => ReturnType<typeof Provider<ActivitiesPortOf<C>>> & Compose<C>;
378
+ * The first is `{ inject, unit?, sync }`, whose `sync` hands back the whole
379
+ * activities record — one `unit:` record for every entry in it. The second
380
+ * takes the pieces `TemporalWorkflowActivities(contract, key)` builds: they are
381
+ * the provider's deps, keyed by the contract key each piece's port id carries,
382
+ * so the services record IS the activities record. Every key must be covered,
383
+ * and two slices claiming one key are di's duplicate-provider defect at build.
384
+ */
385
+ declare const TemporalActivities: <C extends ContractDefinition>(contract: C) => Whole<C> & Compose<C>;
244
386
  //#endregion
245
- export { type ActivitiesInstanceOf, type ActivitiesPortOf, TemporalActivities, TemporalConfig, TemporalConnection, type TemporalInfo, TemporalModule, type TemporalModuleOptions, type TemporalOptions, TemporalRuntime, TemporalUnreachable, TemporalWorkflowActivities, type WorkflowActivitiesPortOf, type WorkflowSource, temporal };
387
+ export { type ActivitiesInstanceOf, type ActivitiesPortOf, ActivityInput, type ActivityInputOf, type ActivityInputPortOf, TemporalActivities, TemporalConfig, TemporalConnection, type TemporalInfo, TemporalModule, type TemporalModuleOptions, type TemporalOptions, TemporalRuntime, TemporalUnreachable, TemporalWorkflowActivities, type WorkflowActivitiesPortOf, type WorkflowSource, temporal };
package/dist/index.mjs CHANGED
@@ -6,10 +6,123 @@ import { msToNumber } from "@temporalio/common";
6
6
  import { NativeConnection, Worker } from "@temporalio/worker";
7
7
  import { TaggedError, fromPromise, fromSafePromise, fromThrowable } from "unthrown";
8
8
  import { activityInfo } from "@temporalio/activity";
9
+ //#region src/unit.ts
10
+ /**
11
+ * Where `activityUnits` leaves the forked context for the piece wrappers to
12
+ * read. A symbol rather than a name: the middleware merges it into the context
13
+ * every activity sees, and a string key would appear on the record an activity
14
+ * written against the `{ inject, sync }` arm destructures.
15
+ */
16
+ const UNIT_SCOPE = Symbol("@btravstack/temporal-worker/unit-scope");
17
+ /**
18
+ * The declared record, as a getter per name resolved on read out of the fork.
19
+ * Neither writable nor configurable — an activity reads what the fork holds, and
20
+ * cannot reshape the record under the next attempt. With no `unit.activity`
21
+ * bound there is nothing to resolve from, so the record is empty whatever was
22
+ * declared.
23
+ */
24
+ const unitRecordOf = (forked, record) => {
25
+ const unit = {};
26
+ if (forked === void 0) return unit;
27
+ for (const [name, port] of Object.entries(record)) Object.defineProperty(unit, name, {
28
+ enumerable: true,
29
+ get: () => forked.get(port)
30
+ });
31
+ return unit;
32
+ };
33
+ /**
34
+ * One entry of the activities record — a contract-global activity's own
35
+ * implementation, or the record of them a workflow key carries — with its
36
+ * piece's declared record put on `context.unit`. Applied once per piece as di
37
+ * constructs it, so an attempt costs one record and one context object.
38
+ */
39
+ const withUnit = (record, entry) => {
40
+ const wrap = (implementation) => (helpers, input) => {
41
+ const { [UNIT_SCOPE]: forked, ...rest } = helpers.context ?? {};
42
+ return implementation({
43
+ ...helpers,
44
+ context: {
45
+ ...rest,
46
+ unit: unitRecordOf(forked, record)
47
+ }
48
+ }, input);
49
+ };
50
+ return typeof entry === "function" ? wrap(entry) : Object.fromEntries(Object.entries(entry).map(([name, implementation]) => [name, wrap(implementation)]));
51
+ };
52
+ //#endregion
53
+ //#region src/workflow-activities.ts
54
+ /** One id, declared once, so no contract instantiating it warns about a duplicate. */
55
+ const ActivityInputPort = Port("ActivityInput");
56
+ /**
57
+ * The validated activity input as a port: the one thing the worker seeds the
58
+ * fork with, so a `unit.activity` module derives a tenant — or anything else —
59
+ * from the invocation rather than from an ambient record.
60
+ *
61
+ * ```ts
62
+ * const Input = ActivityInput(orderContract);
63
+ * const ActivityUnit = Module("ActivityUnit")({
64
+ * needs: [Input],
65
+ * provides: [Provider(Tenant)({ inject: { input: Input }, sync: ({ input }) => input.tenantId })],
66
+ * exports: [Tenant],
67
+ * });
68
+ * ```
69
+ *
70
+ * `contract` is read for its TYPE only. One `Port(...)` call fixed per contract
71
+ * at the type level, the move `TemporalActivitiesPort` makes, so a module built
72
+ * for one contract cannot read another's input.
73
+ */
74
+ const ActivityInput = (contract) => {
75
+ return ActivityInputPort;
76
+ };
77
+ /** The prefix a piece's port id carries; the composing form strips it to recover the key. */
78
+ const WORKFLOW_ACTIVITIES_PREFIX = "TemporalWorkflowActivities:";
79
+ /**
80
+ * One workflow's activities, as a provider on a port of its own.
81
+ *
82
+ * A worker that polls one queue for several workflows is several pieces, each
83
+ * declaring the services its own activities call;
84
+ * `TemporalActivities(contract)([...])` composes them. `contract` is read for
85
+ * its TYPE only, and types both `key` and the record — so an activity the
86
+ * workflow does not declare is a compile error here rather than at startup.
87
+ *
88
+ * `key` is any top-level key of the activities record, which includes a
89
+ * contract-global activity as well as a workflow; the name is imprecise in that
90
+ * one case deliberately, since narrowing it would lock such a contract out of
91
+ * the split.
92
+ *
93
+ * There is no name to give: the key IS the port's name.
94
+ *
95
+ * `unit` declares the ports the activities read off `context.unit`, resolved
96
+ * out of the fork the attempt opened; the root's `unit.activity` module must
97
+ * export every one of them.
98
+ */
99
+ const TemporalWorkflowActivities = (contract, key) => {
100
+ const port = class extends Port(`${WORKFLOW_ACTIVITIES_PREFIX}${key}`) {};
101
+ return (options) => {
102
+ const record = options.unit ?? {};
103
+ return Object.assign(Provider(port)({
104
+ inject: options.inject,
105
+ sync: (services) => withUnit(record, options.sync(services))
106
+ }), { unit: record });
107
+ };
108
+ };
109
+ //#endregion
9
110
  //#region src/activity-units.ts
10
111
  /**
11
- * Open one kernel unit per activity attempt. It injects nothing — `next()`
12
- * unchanged — so the ambient `currentUnit()` record is what an adapter reads.
112
+ * Open one kernel unit per activity attempt, forking `unit` — when one is
113
+ * bound — after the activity is invoked, before it runs; the fork is torn down
114
+ * when the unit closes. It is seeded with the validated input on
115
+ * `ActivityInput(contract)`, so a unit module derives a tenant from the
116
+ * invocation rather than reading one off an ambient record. With no `unit`
117
+ * bound, `next()` runs unchanged, so the ambient `currentUnit()` record is what
118
+ * an adapter reads.
119
+ *
120
+ * The forked context rides the invocation's context under {@link UNIT_SCOPE},
121
+ * where each piece's own wrapper turns it into the typed `context.unit` record
122
+ * that piece declared — the records live with the pieces, so a hand-composed
123
+ * `temporal()` gets them without threading a second option through the starter,
124
+ * and the middleware never has to map Temporal's FLAT activity name back to the
125
+ * workflow key its piece was minted under.
13
126
  *
14
127
  * **That includes the kernel's per-unit `AbortSignal`**, and it is the only
15
128
  * route to it: an activity has no parameter to receive one through. Temporal's
@@ -19,13 +132,13 @@ import { activityInfo } from "@temporalio/activity";
19
132
  * No `Result`-unwrapping boundary, deliberately: `declareActivitiesHandler` owns
20
133
  * the mapping from a settled `Result` to an activity failure.
21
134
  */
22
- const activityUnits = (host, observers) => (_invocation, next) => {
135
+ const activityUnits = (host, observers, unit) => (invocation, next) => {
23
136
  const settle = observe(observers, {
24
137
  component: "temporal",
25
138
  name: "attempt",
26
139
  attributes: { activity: activityInfo().activityType }
27
140
  });
28
- return host.run(metaFor(), () => next()).tap(() => settle({ outcome: "ok" })).tapFailure((failure) => settle({
141
+ return host.run(metaFor(), (scope) => unit === void 0 ? next() : scope.fork(unit, [[ActivityInputPort, invocation.input]]).flatMap((forked) => next({ context: { [UNIT_SCOPE]: forked } }))).tap(() => settle({ outcome: "ok" })).tapFailure((failure) => settle({
29
142
  outcome: "error",
30
143
  cause: failure.tag === "Err" ? failure.error : failure.cause
31
144
  }));
@@ -163,7 +276,7 @@ const createWorker = (host, connection, config, activities, options, observers)
163
276
  const { namespace } = config;
164
277
  return fromThrowable(() => declareActivitiesHandler({
165
278
  contract: options.contract,
166
- middleware: activityUnits(host, observers),
279
+ middleware: activityUnits(host, observers, options.unit?.activity),
167
280
  activities
168
281
  }), startFailed)().toAsync().flatMap((handler) => fromPromise(Worker.create({
169
282
  connection,
@@ -199,30 +312,6 @@ const poll = (worker, taskQueue, namespace) => {
199
312
  };
200
313
  };
201
314
  //#endregion
202
- //#region src/workflow-activities.ts
203
- /** The prefix a piece's port id carries; the composing form strips it to recover the key. */
204
- const WORKFLOW_ACTIVITIES_PREFIX = "TemporalWorkflowActivities:";
205
- /**
206
- * One workflow's activities, as a provider on a port of its own.
207
- *
208
- * A worker that polls one queue for several workflows is several pieces, each
209
- * declaring the services its own activities call;
210
- * `TemporalActivities(contract)([...])` composes them. `contract` is read for
211
- * its TYPE only, and types both `key` and the record — so an activity the
212
- * workflow does not declare is a compile error here rather than at startup.
213
- *
214
- * `key` is any top-level key of the activities record, which includes a
215
- * contract-global activity as well as a workflow; the name is imprecise in that
216
- * one case deliberately, since narrowing it would lock such a contract out of
217
- * the split.
218
- *
219
- * There is no name to give: the key IS the port's name.
220
- */
221
- const TemporalWorkflowActivities = (contract, key) => {
222
- const port = class extends Port(`${WORKFLOW_ACTIVITIES_PREFIX}${key}`) {};
223
- return Provider(port);
224
- };
225
- //#endregion
226
315
  //#region src/temporal-module.ts
227
316
  /**
228
317
  * `Module(name)({...})` for a Temporal worker deployment: everything a di module
@@ -262,12 +351,12 @@ const TemporalModule = (name) => (options) => {
262
351
  * TemporalActivities(orderContract)([fulfillOrder, chargeOrder])
263
352
  * ```
264
353
  *
265
- * The first is di's own `Provider(port)` on the starter's activities port
266
- * typed for the contract. The second takes the pieces
267
- * `TemporalWorkflowActivities(contract, key)` builds: they are the provider's
268
- * deps, keyed by the contract key each piece's port id carries, so the services
269
- * record IS the activities record. Every key must be covered, and two slices
270
- * claiming one key are di's duplicate-provider defect at build.
354
+ * The first is `{ inject, unit?, sync }`, whose `sync` hands back the whole
355
+ * activities record — one `unit:` record for every entry in it. The second
356
+ * takes the pieces `TemporalWorkflowActivities(contract, key)` builds: they are
357
+ * the provider's deps, keyed by the contract key each piece's port id carries,
358
+ * so the services record IS the activities record. Every key must be covered,
359
+ * and two slices claiming one key are di's duplicate-provider defect at build.
271
360
  */
272
361
  const TemporalActivities = (contract) => {
273
362
  const build = Provider(TemporalActivitiesPort);
@@ -275,7 +364,14 @@ const TemporalActivities = (contract) => {
275
364
  inject: Object.fromEntries(pieces.map((piece) => [piece.port.portId.slice(27), piece.port])),
276
365
  sync: (services) => services
277
366
  });
278
- return ((first) => Array.isArray(first) ? compose(first) : build(first));
367
+ const whole = (options) => {
368
+ const record = options.unit ?? {};
369
+ return build({
370
+ inject: options.inject,
371
+ sync: (services) => Object.fromEntries(Object.entries(options.sync(services)).map(([key, entry]) => [key, withUnit(record, entry)]))
372
+ });
373
+ };
374
+ return ((first) => Array.isArray(first) ? compose(first) : whole(first));
279
375
  };
280
376
  //#endregion
281
- export { TemporalActivities, TemporalConfig, TemporalConnection, TemporalModule, TemporalRuntime, TemporalUnreachable, TemporalWorkflowActivities, temporal };
377
+ export { ActivityInput, TemporalActivities, TemporalConfig, TemporalConnection, TemporalModule, TemporalRuntime, TemporalUnreachable, TemporalWorkflowActivities, temporal };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btravstack/temporal-worker",
3
- "version": "0.12.0",
3
+ "version": "0.13.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,10 +54,10 @@
54
54
  }
55
55
  },
56
56
  "devDependencies": {
57
- "@btravstack/tsconfig": "0.2.0",
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",
57
+ "@btravstack/tsconfig": "0.4.0",
58
+ "@temporal-contract/client": "8.0.0-beta.9",
59
+ "@temporal-contract/contract": "8.0.0-beta.9",
60
+ "@temporal-contract/worker": "8.0.0-beta.9",
61
61
  "@temporalio/activity": "1.23.0",
62
62
  "@temporalio/client": "1.23.0",
63
63
  "@temporalio/common": "1.23.0",
@@ -71,23 +71,23 @@
71
71
  "unthrown": "5.7.0",
72
72
  "vitest": "4.1.11",
73
73
  "zod": "4.5.3",
74
- "@btravstack/config": "0.12.0",
75
- "@btravstack/core": "0.12.0",
76
- "@btravstack/di": "0.12.0",
74
+ "@btravstack/config": "0.13.0",
75
+ "@btravstack/core": "0.13.0",
76
+ "@btravstack/di": "0.13.0",
77
77
  "@btravstack/internal-test-infra": "0.0.0",
78
- "@btravstack/testing": "0.12.0"
78
+ "@btravstack/testing": "0.13.0"
79
79
  },
80
80
  "peerDependencies": {
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",
81
+ "@temporal-contract/client": "^8.0.0-beta.9",
82
+ "@temporal-contract/contract": "^8.0.0-beta.9",
83
+ "@temporal-contract/worker": "^8.0.0-beta.9",
84
84
  "@temporalio/activity": "^1.22.0",
85
85
  "@temporalio/common": "^1.22.0",
86
86
  "@temporalio/worker": "^1.22.0",
87
87
  "unthrown": "^5.0.0",
88
- "@btravstack/config": "^0.12.0",
89
- "@btravstack/core": "^0.12.0",
90
- "@btravstack/di": "^0.12.0"
88
+ "@btravstack/config": "^0.13.0",
89
+ "@btravstack/core": "^0.13.0",
90
+ "@btravstack/di": "^0.13.0"
91
91
  },
92
92
  "peerDependenciesMeta": {
93
93
  "@temporal-contract/client": {