@lostgradient/weft 0.4.0 → 0.6.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.
@@ -11,5 +11,5 @@ export type { WorkflowEventTail } from './event-tail.ts';
11
11
  export { HttpClient } from './http-client.ts';
12
12
  export { HttpClientError } from './http-request.ts';
13
13
  export type { HttpClientOptions } from './http-request.ts';
14
- export type { ClientHandle, ClientScheduleHandle, ClientStartOptions, UpdateResult, WeftClient, WeftClientActivity, } from './interface.ts';
14
+ export type { ClientHandle, ClientScheduleHandle, ClientStartOptions, StartOrSignalOutcome, UpdateResult, WeftClient, WeftClientActivity, } from './interface.ts';
15
15
  export type { KnownWorkflowName, UnknownNameWhenRegistryEmpty } from './workflow-name-typing.ts';
@@ -32,6 +32,21 @@ export type ClientStartOptions = Omit<StartOptions, 'defer' | 'services'> & {
32
32
  readonly defer?: never;
33
33
  readonly services?: never;
34
34
  };
35
+ /**
36
+ * Which atomic path a {@link WeftClient.startOrSignal} call took, returned
37
+ * alongside the {@link ClientHandle}. `'started'` when the call created the
38
+ * run; `'signalled'` when it delivered a signal to a run that already existed,
39
+ * including losing a concurrent same-key create race and converging onto the
40
+ * winner. Each call receives its OWN handle regardless of the outcome.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import type { StartOrSignalOutcome } from '@lostgradient/weft/client';
45
+ *
46
+ * const outcome: StartOrSignalOutcome = 'started';
47
+ * void outcome;
48
+ * ```
49
+ */
35
50
  export type StartOrSignalOutcome = EngineStartOrSignalOutcome;
36
51
  /**
37
52
  * A reference to a workflow that provides convenience methods.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * In-process handle wrappers for {@link LocalClient}. Split out of
3
+ * `client/local.ts` to keep that module under the per-file line ceiling.
4
+ *
5
+ * @module client/local-handles
6
+ */
7
+ import type { WorkflowHandle } from '../core/engine.ts';
8
+ import type { WeftEventMap } from '../core/events.ts';
9
+ import { ScheduleHandleDelegation, WorkflowHandleDelegation } from './handle-delegation.ts';
10
+ import type { StartOrSignalOutcome } from './interface.ts';
11
+ import type { LocalClient } from './local.ts';
12
+ export declare class LocalHandle extends WorkflowHandleDelegation<LocalClient> {
13
+ #private;
14
+ constructor(handle: WorkflowHandle, client: LocalClient, outcome?: StartOrSignalOutcome);
15
+ result(): Promise<unknown>;
16
+ addEventListener<K extends keyof WeftEventMap>(type: K, listener: (event: WeftEventMap[K]) => void, options?: boolean | AddEventListenerOptions): void;
17
+ removeEventListener<K extends keyof WeftEventMap>(type: K, listener: (event: WeftEventMap[K]) => void, options?: boolean | EventListenerOptions): void;
18
+ [Symbol.dispose](): void;
19
+ }
20
+ export declare class LocalScheduleHandle extends ScheduleHandleDelegation<LocalClient> {
21
+ [Symbol.dispose](): void;
22
+ }
@@ -0,0 +1,23 @@
1
+ import { ScheduleHandleDelegation, WorkflowHandleDelegation } from "./handle-delegation.js";
2
+
3
+ export class LocalHandle extends WorkflowHandleDelegation {
4
+ #handle;
5
+ constructor(handle, client, outcome) {
6
+ super(handle.id, client, outcome);
7
+ this.#handle = handle;
8
+ }
9
+ async result() {
10
+ return this.#handle.result();
11
+ }
12
+ addEventListener(type, listener, options) {
13
+ this.#handle.addEventListener(type, listener, options);
14
+ }
15
+ removeEventListener(type, listener, options) {
16
+ this.#handle.removeEventListener(type, listener, options);
17
+ }
18
+ [Symbol.dispose]() {}
19
+ }
20
+
21
+ export class LocalScheduleHandle extends ScheduleHandleDelegation {
22
+ [Symbol.dispose]() {}
23
+ }
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { type CatalogOperationName, type CatalogOperationTypes, type WeftClient as CatalogOperations } from '../cli/generated/operation-client.generated.ts';
11
11
  import type { Engine } from '../core/engine.ts';
12
- import type { AttributeFilterKey, BulkCancelResult, BulkDeleteResult, BulkRetryFailedResult, BulkSignalResult, BulkTagResult, CoordinatedUpdateResult, ForkOptions, ListFilter, PaginatedResult, PurgeResult, QueryDefinition, RetentionOverview, ReviewListEntry, ReviewListFilter, ScheduleFilter, ScheduleOptions, ScheduleSpec, ScheduleSummary, SearchAttributeValue, SignalDefinition, SignalDeliveryOptions, StartOptions, StartOrSignalSignal, SubmitReviewOptions, TypedListFilter, UpdateDefinition, WorkflowEvent, WorkflowInput, WorkflowOutput, WorkflowRegistry, WorkflowReplay, WorkflowState, WorkflowSummary, WorkflowTimelineEntry } from '../core/types.ts';
12
+ import type { AttributeFilterKey, BulkCancelResult, BulkDeleteResult, BulkRetryFailedResult, BulkSignalResult, BulkTagResult, CoordinatedUpdateResult, DefaultActivityTypes, DefaultWorkflowRegistry, ForkOptions, ListFilter, PaginatedResult, PurgeResult, QueryDefinition, RetentionOverview, ReviewListEntry, ReviewListFilter, ScheduleFilter, ScheduleOptions, ScheduleSpec, ScheduleSummary, SearchAttributeValue, SignalDefinition, SignalDeliveryOptions, StartOptions, StartOrSignalSignal, SubmitReviewOptions, TypedListFilter, UpdateDefinition, WorkflowEvent, WorkflowInput, WorkflowOutput, WorkflowRegistry, WorkflowReplay, WorkflowState, WorkflowSummary, WorkflowTimelineEntry } from '../core/types.ts';
13
13
  import type { WorkflowEventTail } from './event-tail.ts';
14
14
  import type { ClientHandle, ClientScheduleHandle, UpdateResult, WeftClient, WeftClientActivity } from './interface.ts';
15
15
  import type { KnownWorkflowName, UnknownNameWhenRegistryEmpty } from './workflow-name-typing.ts';
@@ -32,7 +32,7 @@ import type { KnownWorkflowName, UnknownNameWhenRegistryEmpty } from './workflow
32
32
  * console.log(await handle.result()); // 'Hello, World!'
33
33
  * ```
34
34
  */
35
- export declare class LocalClient implements WeftClient {
35
+ export declare class LocalClient<TWorkflows extends object = DefaultWorkflowRegistry, TActivities extends object = DefaultActivityTypes> implements WeftClient {
36
36
  #private;
37
37
  /** Typed low-level accessor for every catalog operation, routed in-process. */
38
38
  readonly operations: CatalogOperations;
@@ -53,7 +53,7 @@ export declare class LocalClient implements WeftClient {
53
53
  * ```
54
54
  */
55
55
  readonly activity: WeftClientActivity;
56
- constructor(engine: Engine);
56
+ constructor(engine: Engine<TWorkflows, TActivities> | Engine);
57
57
  call<Name extends CatalogOperationName>(name: Name, input: CatalogOperationTypes[Name]['input']): Promise<CatalogOperationTypes[Name]['output']>;
58
58
  start<TName extends KnownWorkflowName>(type: TName, input: WorkflowInput<WorkflowRegistry, TName>, options?: StartOptions): Promise<ClientHandle<WorkflowOutput<WorkflowRegistry, TName>>>;
59
59
  start<TName extends string>(type: UnknownNameWhenRegistryEmpty<TName>, input: unknown, options?: StartOptions): Promise<ClientHandle>;
@@ -6,31 +6,9 @@ import {
6
6
  runtimeWorkflowEngine
7
7
  } from "../core/runtime-workflow-engine.js";
8
8
  import { messageName } from "../core/types.js";
9
- import { ScheduleHandleDelegation, WorkflowHandleDelegation } from "./handle-delegation.js";
10
9
  import { inProcessCatalogTransport } from "./in-process-operations.js";
11
10
  import { createLocalWorkflowEventTail } from "./local-event-tail.js";
12
-
13
- class LocalHandle extends WorkflowHandleDelegation {
14
- #handle;
15
- constructor(handle, client, outcome) {
16
- super(handle.id, client, outcome);
17
- this.#handle = handle;
18
- }
19
- async result() {
20
- return this.#handle.result();
21
- }
22
- addEventListener(type, listener, options) {
23
- this.#handle.addEventListener(type, listener, options);
24
- }
25
- removeEventListener(type, listener, options) {
26
- this.#handle.removeEventListener(type, listener, options);
27
- }
28
- [Symbol.dispose]() {}
29
- }
30
-
31
- class LocalScheduleHandle extends ScheduleHandleDelegation {
32
- [Symbol.dispose]() {}
33
- }
11
+ import { LocalHandle, LocalScheduleHandle } from "./local-handles.js";
34
12
 
35
13
  export class LocalClient {
36
14
  #engine;
@@ -44,7 +22,7 @@ export class LocalClient {
44
22
  complete: (token, result) => this.#engine.completeAsyncActivity(token, result),
45
23
  completeExceptionally: (token, error) => this.#engine.failAsyncActivity(token, error)
46
24
  };
47
- this.operations = createCatalogWeftClient(CATALOG_OPERATION_NAMES, inProcessCatalogTransport(engine));
25
+ this.operations = createCatalogWeftClient(CATALOG_OPERATION_NAMES, inProcessCatalogTransport(this.#rawEngine));
48
26
  }
49
27
  call(name, input) {
50
28
  return this.operations[name](input);
@@ -15,8 +15,13 @@ type ActivityOperation = Extract<ContextOperationRequest, {
15
15
  * EITHER workflow cancellation OR a per-attempt timeout — without the timeout
16
16
  * reaching back to cancel the whole workflow (which would also poison the next
17
17
  * retry's signal).
18
+ *
19
+ * An optional `coordinatorSignal` (from a `ctx.race` or `ctx.all` coordinator)
20
+ * is also composed in (#584): when a sibling branch wins the race, the coordinator
21
+ * aborts its controller, propagating to the activity's `ctx.signal` for
22
+ * cooperative cancellation of losing activity branches.
18
23
  */
19
- export declare function resolvePerAttemptTimeout(internals: EngineInternals, workflowId: string, operation: ActivityOperation): {
24
+ export declare function resolvePerAttemptTimeout(internals: EngineInternals, workflowId: string, operation: ActivityOperation, coordinatorSignal?: AbortSignal): {
20
25
  perAttemptTimeoutMs: number | undefined;
21
26
  attemptAbortController: AbortController | undefined;
22
27
  activitySignal: AbortSignal;
@@ -2,12 +2,12 @@ import {
2
2
  ActivityPerAttemptTimeoutError,
3
3
  parsePerAttemptTimeoutMs
4
4
  } from "../context/activity-schedule-to-close.js";
5
- export function resolvePerAttemptTimeout(internals, workflowId, operation) {
6
- const workflowAbortController = internals.inlineStrategy?.getAbortController(workflowId), perAttemptTimeoutMs = internals.activityWorkerDispatcher ? void 0 : parsePerAttemptTimeoutMs(operation.options?.timeout), attemptAbortController = perAttemptTimeoutMs === void 0 ? void 0 : new AbortController, activitySignal = composeActivitySignal(workflowAbortController?.signal, attemptAbortController?.signal);
5
+ export function resolvePerAttemptTimeout(internals, workflowId, operation, coordinatorSignal) {
6
+ const workflowAbortController = internals.inlineStrategy?.getAbortController(workflowId), perAttemptTimeoutMs = internals.activityWorkerDispatcher ? void 0 : parsePerAttemptTimeoutMs(operation.options?.timeout), attemptAbortController = perAttemptTimeoutMs === void 0 ? void 0 : new AbortController, activitySignal = composeActivitySignal(workflowAbortController?.signal, attemptAbortController?.signal, coordinatorSignal);
7
7
  return { perAttemptTimeoutMs, attemptAbortController, activitySignal };
8
8
  }
9
- function composeActivitySignal(workflowSignal, attemptSignal) {
10
- const signals = [workflowSignal, attemptSignal].filter((signal) => signal !== void 0), [first] = signals;
9
+ function composeActivitySignal(workflowSignal, attemptSignal, coordinatorSignal) {
10
+ const signals = [workflowSignal, attemptSignal, coordinatorSignal].filter((signal) => signal !== void 0), [first] = signals;
11
11
  if (first === void 0)
12
12
  return new AbortController().signal;
13
13
  if (signals.length === 1)
@@ -19,6 +19,7 @@ export type EngineCreateRuntimeOptions = EngineConstructorOptions & {
19
19
  workflows?: Record<string, AnyWorkflowDefinition> | undefined;
20
20
  recover?: boolean | undefined;
21
21
  acknowledgeUnknownWorkflowTypes?: boolean | undefined;
22
+ startScheduler?: boolean | undefined;
22
23
  };
23
24
  export type NormalizedWorkerExecutionConfiguration = {
24
25
  mode: 'inline';
@@ -29,6 +29,20 @@ export type EngineCreateOptions<TWorkflowDefinitions extends Record<string, AnyW
29
29
  workflows?: TWorkflowDefinitions;
30
30
  /** Activity definitions to register before workflows. */
31
31
  activities?: TActivityDefinitions;
32
+ /**
33
+ * Start the scheduler's durable-timer polling loop after registration so
34
+ * `ctx.sleep(...)` and `engine.schedule(...)` timers fire in a long-lived
35
+ * in-process host. This is independent of `recover`: it controls *whether
36
+ * timers fire*, not *who drives `recoverAll`*.
37
+ *
38
+ * Defaults to `recover !== false`. A host that opts out of auto-recovery
39
+ * with `recover: false` (because it drives its own `recoverAll()` to capture
40
+ * the recovered handles) can still arm the poller by passing
41
+ * `startScheduler: true`. Pass `startScheduler: false` to keep the poller
42
+ * stopped even when recovery runs (tests / `ScopedStorage` engines that tick
43
+ * the scheduler deterministically).
44
+ */
45
+ startScheduler?: boolean;
32
46
  } & ({
33
47
  /**
34
48
  * Recover stored running workflows after registration. Defaults to
@@ -228,6 +228,8 @@ export class Engine extends EventTarget {
228
228
  await engine.#acquireLeaseIfConfigured();
229
229
  if (options.recover !== !1)
230
230
  await engine.recoverAll(options.acknowledgeUnknownWorkflowTypes !== void 0 ? { acknowledgeUnknownWorkflowTypes: options.acknowledgeUnknownWorkflowTypes } : {});
231
+ if (options.startScheduler ?? options.recover !== !1)
232
+ getInternals(engine).scheduler.start();
231
233
  } catch (error) {
232
234
  await engine[Symbol.asyncDispose]();
233
235
  throw error;
@@ -27,7 +27,7 @@ export declare function invokeInlineActivity(internals: EngineInternals, workflo
27
27
  * Execute an activity function, dispatching to a Web Worker pool when
28
28
  * `activityExecution` is configured, or running inline on the main thread.
29
29
  */
30
- export declare function executeActivity(internals: EngineInternals, workflowId: string, operation: ActivityOperation, callbacks: ActivityOperationCallbacks, attempt?: number): Promise<unknown>;
31
- export declare function executeActivityOperationResult(internals: EngineInternals, workflowId: string, operation: ActivityOperation, callbacks: ActivityOperationCallbacks, speculativeState?: SpeculativeExecutionState): Promise<unknown>;
30
+ export declare function executeActivity(internals: EngineInternals, workflowId: string, operation: ActivityOperation, callbacks: ActivityOperationCallbacks, attempt?: number, coordinatorSignal?: AbortSignal): Promise<unknown>;
31
+ export declare function executeActivityOperationResult(internals: EngineInternals, workflowId: string, operation: ActivityOperation, callbacks: ActivityOperationCallbacks, coordinatorSignal?: AbortSignal, speculativeState?: SpeculativeExecutionState): Promise<unknown>;
32
32
  export declare function processActivityOperation(internals: EngineInternals, workflowId: string, operation: ActivityOperation, callbacks: ActivityOperationCallbacks): Promise<void>;
33
33
  export {};
@@ -69,10 +69,10 @@ function getActivityAttempt(operation) {
69
69
  const attempt = operation.attempt;
70
70
  return typeof attempt === "number" && Number.isInteger(attempt) && attempt > 0 ? attempt : 1;
71
71
  }
72
- export async function executeActivity(internals, workflowId, operation, callbacks, attempt = getActivityAttempt(operation)) {
72
+ export async function executeActivity(internals, workflowId, operation, callbacks, attempt = getActivityAttempt(operation), coordinatorSignal) {
73
73
  const activityInput = operation.input, step = operation.step ?? 0;
74
74
  warnIfRetryMissingHeartbeat(internals, workflowId, step, attempt);
75
- const asyncToken = deriveAsyncActivityToken(workflowId, step, attempt), { perAttemptTimeoutMs, attemptAbortController, activitySignal } = resolvePerAttemptTimeout(internals, workflowId, operation), activityContext = buildActivityContext(internals, workflowId, step, activitySignal, () => {
75
+ const asyncToken = deriveAsyncActivityToken(workflowId, step, attempt), { perAttemptTimeoutMs, attemptAbortController, activitySignal } = resolvePerAttemptTimeout(internals, workflowId, operation, coordinatorSignal), activityContext = buildActivityContext(internals, workflowId, step, activitySignal, () => {
76
76
  throw new AsyncActivityDeferral(asyncToken);
77
77
  }), invokeActivity = internals.activityWorkerDispatcher ? (activityName, input) => invokeWorkerActivity(internals, operation.operationId, activityName, input, attempt) : (activityName, input) => withPerAttemptTimeout(invokeInlineActivity(internals, workflowId, operation, activityContext, activityName, input), perAttemptTimeoutMs, activityName, attempt, attemptAbortController), composedActivity = callbacks.getComposedActivityInterceptor(), executeWithActivityInterceptors = async (activityName, input, headers) => {
78
78
  if (!composedActivity)
@@ -107,7 +107,7 @@ export async function executeActivity(internals, workflowId, operation, callback
107
107
  copyActivityHeadersToOperation(operation, headers);
108
108
  return result;
109
109
  }
110
- export async function executeActivityOperationResult(internals, workflowId, operation, callbacks, speculativeState) {
110
+ export async function executeActivityOperationResult(internals, workflowId, operation, callbacks, coordinatorSignal, speculativeState) {
111
111
  const activity = getActivityFunctionWithMetadata(internals, workflowId, operation), idempotencyKey = resolveActivityIdempotencyKey(activity, operation), operationAttempt = getActivityAttempt(operation);
112
112
  if (idempotencyKey !== void 0) {
113
113
  const reference = await buildActivityReconciliationReference(workflowId, operation.activityName, idempotencyKey), started = await resolveStartedActivityReconciliationRecord(internals, workflowId, operation, reference, activity, idempotencyKey, operationAttempt);
@@ -115,7 +115,7 @@ export async function executeActivityOperationResult(internals, workflowId, oper
115
115
  validateActivityResultForReconciliation(started.completedResult, internals.options.payloadSizePolicy.maxBytes);
116
116
  return started.completedResult;
117
117
  }
118
- const result = await executeActivity(internals, workflowId, operation, callbacks, started.attempt);
118
+ const result = await executeActivity(internals, workflowId, operation, callbacks, started.attempt, coordinatorSignal);
119
119
  validateActivityResultForReconciliation(result, internals.options.payloadSizePolicy.maxBytes);
120
120
  await finalizeActivityResult(internals, workflowId, operation, result, activity, idempotencyKey, started.attempt, speculativeState, !0);
121
121
  const completedRecord = createCompletedActivityReconciliationRecord(started, result, internals.options.getNow());
@@ -124,7 +124,7 @@ export async function executeActivityOperationResult(internals, workflowId, oper
124
124
  clearLastHeartbeatForStep(internals, workflowId, operation.step ?? 0);
125
125
  return result;
126
126
  }
127
- const result = await executeActivity(internals, workflowId, operation, callbacks, operationAttempt);
127
+ const result = await executeActivity(internals, workflowId, operation, callbacks, operationAttempt, coordinatorSignal);
128
128
  assertPayloadWithinLimit(result, internals.options.payloadSizePolicy.maxBytes, "activity result");
129
129
  await finalizeActivityResult(internals, workflowId, operation, result, activity, idempotencyKey, operationAttempt, speculativeState);
130
130
  if (!speculativeState)
@@ -192,6 +192,6 @@ export async function executeRunAllOperationResult(internals, workflowId, operat
192
192
  activityName: fn.name,
193
193
  fn,
194
194
  input
195
- }, callbacks.getActivityOperationCallbacks(), speculativeState);
195
+ }, callbacks.getActivityOperationCallbacks(), void 0, speculativeState);
196
196
  });
197
197
  }
@@ -36,7 +36,7 @@ export async function executeSubOperation(internals, workflowId, operation, call
36
36
  }, operation);
37
37
  }
38
38
  const subOperationExecutors = {
39
- activity: (context, operation) => executeActivitySubOperation(context.internals, context.workflowId, operation, context.callbacks, context.speculativeState),
39
+ activity: (context, operation) => executeActivitySubOperation(context.internals, context.workflowId, operation, context.callbacks, context.signal, context.speculativeState),
40
40
  "child-workflow": (context, operation) => executeChildWorkflowSubOperation(context.internals, context.workflowId, operation, context.callbacks),
41
41
  memo: (_context, operation) => Promise.resolve(callMemoFunction(operation.fn)),
42
42
  "state-read": (context, operation) => executeStateReadSubOperation(context.internals, operation),
@@ -50,8 +50,8 @@ const subOperationExecutors = {
50
50
  throw Error("ctx.waitUntil() cannot be used as a ctx.race() / ctx.all() / ctx.speculate() branch. Use `yield* ctx.waitUntil(...)` directly, or gate it behind a signal/update the coordinator resolves.");
51
51
  }
52
52
  };
53
- async function executeActivitySubOperation(internals, workflowId, operation, callbacks, speculativeState) {
54
- return executeActivityOperationResult(internals, workflowId, operation, callbacks.createActivityOperationCallbacks(), speculativeState);
53
+ async function executeActivitySubOperation(internals, workflowId, operation, callbacks, signal, speculativeState) {
54
+ return executeActivityOperationResult(internals, workflowId, operation, callbacks.createActivityOperationCallbacks(), signal, speculativeState);
55
55
  }
56
56
  async function executeChildWorkflowSubOperation(internals, workflowId, operation, callbacks) {
57
57
  return executeChildWorkflow(internals, workflowId, operation, assertChildWorkflowNestingDepth(internals, workflowId), callbacks.createChildWorkflowOperationCallbacks());
package/dist/index.d.ts CHANGED
@@ -108,7 +108,7 @@ export type { RemoteWorkerActivityFunction, RemoteWorkerActivityImplementation,
108
108
  export type { WorkflowEventTail } from './client/event-tail';
109
109
  export { HttpClient, HttpClientError } from './client/index';
110
110
  export type { HttpClientOptions } from './client/index';
111
- export type { ClientHandle, ClientStartOptions, UpdateResult, WeftClient, WeftClientActivity, } from './client/interface';
111
+ export type { ClientHandle, ClientStartOptions, StartOrSignalOutcome, UpdateResult, WeftClient, WeftClientActivity, } from './client/interface';
112
112
  export { LocalClient } from './client/local';
113
113
  export type { KnownWorkflowName, UnknownNameWhenRegistryEmpty, } from './client/workflow-name-typing';
114
114
  export { ConnectionConfigurationError, DEFAULT_WEFT_ADDRESS, resolveConnection, } from './connection';