@lostgradient/weft 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/cli/generated/operation-client.generated.d.ts +1 -0
- package/dist/cli-main.js +63 -63
- package/dist/client/event-stream-options.d.ts +58 -0
- package/dist/client/event-stream-options.js +0 -0
- package/dist/client/event-stream.d.ts +27 -44
- package/dist/client/event-stream.js +21 -56
- package/dist/client/event-tail-lifecycle.d.ts +21 -0
- package/dist/client/event-tail-lifecycle.js +71 -0
- package/dist/client/event-tail.d.ts +3 -1
- package/dist/client/http-client.d.ts +4 -5
- package/dist/client/http-client.js +4 -1
- package/dist/client/http-handle.d.ts +4 -5
- package/dist/client/http-request.d.ts +7 -0
- package/dist/client/index.d.ts +2 -1
- package/dist/client/interface.d.ts +23 -7
- package/dist/client/local-handles.d.ts +2 -0
- package/dist/client/local-handles.js +10 -1
- package/dist/client/local.d.ts +3 -3
- package/dist/client/open-event-subscription.d.ts +3 -2
- package/dist/client/open-event-subscription.js +28 -1
- package/dist/client/sse-event-stream.d.ts +27 -0
- package/dist/client/sse-event-stream.js +276 -0
- package/dist/client/start-body.d.ts +2 -2
- package/dist/client/start-body.js +1 -0
- package/dist/core/engine/construction.js +3 -1
- package/dist/core/engine/engine-internal-types.d.ts +2 -0
- package/dist/core/engine/errors.d.ts +6 -6
- package/dist/core/engine/errors.js +1 -1
- package/dist/core/engine/handles.js +1 -1
- package/dist/core/engine/index.d.ts +6 -5
- package/dist/core/engine/index.js +1 -0
- package/dist/core/engine/lifecycle/recovered-services.js +8 -1
- package/dist/core/engine/lifecycle/start-or-signal-create.d.ts +2 -2
- package/dist/core/engine/lifecycle/start-or-signal-create.js +17 -15
- package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +1 -1
- package/dist/core/engine/lifecycle/start-or-signal-resolution.js +23 -5
- package/dist/core/engine/lifecycle/start-or-signal.d.ts +6 -4
- package/dist/core/engine/lifecycle/start-or-signal.js +21 -4
- package/dist/core/engine/schedule-run.js +12 -3
- package/dist/core/engine/storage-io.js +1 -1
- package/dist/core/engine/termination/complete.js +3 -3
- package/dist/core/scheduler/scheduler-class.js +2 -1
- package/dist/core/start-workflow-validation.d.ts +3 -4
- package/dist/core/start-workflow-validation.js +1 -1
- package/dist/core/timeouts.d.ts +13 -1
- package/dist/core/timeouts.js +4 -1
- package/dist/core/types/constants.d.ts +16 -0
- package/dist/core/types/options.d.ts +60 -12
- package/dist/core/types/services-resolution.d.ts +49 -3
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -0
- package/dist/json-schema.js +1 -1
- package/dist/mcp/cli.js +17 -17
- package/dist/server/asyncapi-channels.js +82 -16
- package/dist/server/handler/auth-context-principal.d.ts +19 -0
- package/dist/server/handler/auth-context-principal.js +25 -0
- package/dist/server/handler/index.d.ts +2 -1
- package/dist/server/handler/index.js +35 -18
- package/dist/server/handler/route-dispatch.d.ts +12 -22
- package/dist/server/handler/route-dispatch.js +30 -36
- package/dist/server/handler/sse-route-dispatch.d.ts +25 -0
- package/dist/server/handler/sse-route-dispatch.js +45 -0
- package/dist/server/handler.js +21 -21
- package/dist/server/index.d.ts +8 -7
- package/dist/server/index.js +15 -15
- package/dist/server/operations/fleet-events-sse.d.ts +11 -0
- package/dist/server/operations/fleet-events-sse.js +164 -0
- package/dist/server/operations/sse-stream.d.ts +43 -0
- package/dist/server/operations/sse-stream.js +152 -7
- package/dist/server/operations/start-or-signal-workflow.d.ts +2 -0
- package/dist/server/operations/start-or-signal-workflow.js +25 -14
- package/dist/server/operations/start-workflow-options.d.ts +9 -6
- package/dist/server/operations/start-workflow-options.js +13 -0
- package/dist/server/operations/workflow-events-sse.d.ts +17 -0
- package/dist/server/operations/workflow-events-sse.js +200 -0
- package/dist/server/rest-bindings.js +12 -0
- package/dist/server/runtime/authentication-bridge.js +4 -0
- package/dist/server/runtime/cors.d.ts +1 -1
- package/dist/server/runtime/cors.js +6 -1
- package/dist/server/runtime/websocket-stream.d.ts +4 -0
- package/dist/server/runtime/websocket-stream.js +20 -3
- package/dist/server/workflow-event-feed.d.ts +1 -0
- package/dist/server/workflow-event-feed.js +17 -6
- package/dist/service-worker/index.js +28 -28
- package/dist/storage/typed-storage.js +1 -1
- package/dist/testing/index.js +17 -17
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* @module client/open-event-subscription
|
|
11
11
|
*/
|
|
12
12
|
import type { WorkflowEvent } from '../core/types.ts';
|
|
13
|
-
import {
|
|
13
|
+
import type { WorkflowEventStreamOptions } from './event-stream-options.ts';
|
|
14
|
+
import type { WorkflowEventTail } from './event-tail.ts';
|
|
14
15
|
/**
|
|
15
16
|
* The streaming-relevant view of an HTTP client: the fields a workflow event
|
|
16
17
|
* subscription needs to open its `/watch` socket and run `getEvents` catch-up.
|
|
@@ -30,4 +31,4 @@ export type WorkflowEventStreamHost = {
|
|
|
30
31
|
* consumers (`tail()`) so the connect catch-up is buffered for the async
|
|
31
32
|
* iterator rather than dropped.
|
|
32
33
|
*/
|
|
33
|
-
export declare function openClientEventSubscription(host: WorkflowEventStreamHost, streamOptions: WorkflowEventStreamOptions, workflowId: string, onEvent: (event: WorkflowEvent) => void, bufferForIteration?: boolean):
|
|
34
|
+
export declare function openClientEventSubscription(host: WorkflowEventStreamHost, streamOptions: WorkflowEventStreamOptions, workflowId: string, onEvent: (event: WorkflowEvent) => void, bufferForIteration?: boolean): WorkflowEventTail;
|
|
@@ -1,5 +1,32 @@
|
|
|
1
|
+
import { detectRuntime } from "../runtime/portable.js";
|
|
1
2
|
import { workflowWatchWebSocketUrl } from "./event-stream-transport.js";
|
|
2
3
|
import { WorkflowEventSubscription } from "./event-stream.js";
|
|
4
|
+
import { SseWorkflowEventSubscription, workflowEventsSseUrl } from "./sse-event-stream.js";
|
|
3
5
|
export function openClientEventSubscription(host, streamOptions, workflowId, onEvent, bufferForIteration = !1) {
|
|
4
|
-
|
|
6
|
+
const options = bufferForIteration ? { ...streamOptions, bufferForIteration } : streamOptions, transport = options.eventTransport ?? "auto";
|
|
7
|
+
if (transport === "sse")
|
|
8
|
+
return openSseEventSubscription(host, workflowId, onEvent, options);
|
|
9
|
+
if (shouldPreferServerSentEventsFallback(host, options, transport))
|
|
10
|
+
return openSseEventSubscription(host, workflowId, onEvent, options);
|
|
11
|
+
try {
|
|
12
|
+
return new WorkflowEventSubscription(workflowWatchWebSocketUrl(host.baseUrl, workflowId), host.headers, workflowId, (id) => host.getEvents(id), onEvent, options);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (transport !== "auto")
|
|
15
|
+
throw error;
|
|
16
|
+
if (!isWebSocketTransportUnavailable(error))
|
|
17
|
+
throw error;
|
|
18
|
+
return openSseEventSubscription(host, workflowId, onEvent, options);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function shouldPreferServerSentEventsFallback(host, options, transport) {
|
|
22
|
+
return transport === "auto" && options.webSocketFactory === void 0 && detectRuntime() !== "bun" && Object.keys(host.headers).length > 0;
|
|
23
|
+
}
|
|
24
|
+
function isWebSocketTransportUnavailable(error) {
|
|
25
|
+
if (!(error instanceof Error))
|
|
26
|
+
return !1;
|
|
27
|
+
const message = error.message.toLowerCase();
|
|
28
|
+
return message.includes("no global websocket") || message.includes("cannot send the configured auth headers over websocket") || message.includes("websocket headers unavailable") || message.includes("websocket constructor has no header support") || message.includes("provide httpclientoptions.websocketfactory");
|
|
29
|
+
}
|
|
30
|
+
function openSseEventSubscription(host, workflowId, onEvent, options) {
|
|
31
|
+
return new SseWorkflowEventSubscription(workflowEventsSseUrl(host.baseUrl, workflowId), host.headers, workflowId, onEvent, options);
|
|
5
32
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WorkflowEvent } from '../core/types.ts';
|
|
2
|
+
import type { StreamCloseReason } from './event-stream.ts';
|
|
3
|
+
import type { WorkflowEventTail } from './event-tail.ts';
|
|
4
|
+
export type ServerSentEventFrame = {
|
|
5
|
+
readonly id?: string;
|
|
6
|
+
readonly event?: string;
|
|
7
|
+
readonly data: string;
|
|
8
|
+
};
|
|
9
|
+
export type ServerSentEventParseResult = {
|
|
10
|
+
readonly frames: ReadonlyArray<ServerSentEventFrame>;
|
|
11
|
+
readonly remainder: string;
|
|
12
|
+
};
|
|
13
|
+
export type SseWorkflowEventStreamOptions = {
|
|
14
|
+
readonly maxReconnectAttempts?: number;
|
|
15
|
+
readonly reconnectBackoffMs?: number;
|
|
16
|
+
readonly bufferForIteration?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare function workflowEventsSseUrl(baseUrl: string, workflowId: string): string;
|
|
19
|
+
export declare function parseServerSentEventChunk(text: string, previousRemainder?: string): ServerSentEventParseResult;
|
|
20
|
+
export declare class SseWorkflowEventSubscription implements WorkflowEventTail {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(url: string, headers: Record<string, string>, workflowId: string, onEvent: (event: WorkflowEvent) => void, options?: SseWorkflowEventStreamOptions);
|
|
23
|
+
get closeReason(): StreamCloseReason | null;
|
|
24
|
+
whenConnected(): Promise<void>;
|
|
25
|
+
close(): void;
|
|
26
|
+
[Symbol.asyncIterator](): AsyncIterator<WorkflowEvent>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { WORKFLOW_TERMINAL_EVENT_TYPES } from "../core/events/workflow-events.js";
|
|
2
|
+
import { WorkflowEventTailLifecycle } from "./event-tail-lifecycle.js";
|
|
3
|
+
const DEFAULT_MAX_RECONNECT_ATTEMPTS = 5, DEFAULT_RECONNECT_BACKOFF_MS = 50;
|
|
4
|
+
export function workflowEventsSseUrl(baseUrl, workflowId) {
|
|
5
|
+
return `${baseUrl.replace(/\/+$/, "")}/v1/workflows/${encodeURIComponent(workflowId)}/events/sse`;
|
|
6
|
+
}
|
|
7
|
+
export function parseServerSentEventChunk(text, previousRemainder = "") {
|
|
8
|
+
const parts = `${previousRemainder}${text}`.replaceAll(`\r
|
|
9
|
+
`, `
|
|
10
|
+
`).replaceAll("\r", `
|
|
11
|
+
`).split(`
|
|
12
|
+
|
|
13
|
+
`), remainder = parts.pop() ?? "";
|
|
14
|
+
return { frames: parts.map(parseServerSentEventFrame).filter((frame) => frame !== null), remainder };
|
|
15
|
+
}
|
|
16
|
+
function parseServerSentEventFrame(block) {
|
|
17
|
+
const parts = {
|
|
18
|
+
id: void 0,
|
|
19
|
+
event: void 0,
|
|
20
|
+
dataLines: []
|
|
21
|
+
};
|
|
22
|
+
for (const rawLine of block.split(`
|
|
23
|
+
`))
|
|
24
|
+
applyServerSentEventLine(parts, rawLine);
|
|
25
|
+
return frameFromParts(parts);
|
|
26
|
+
}
|
|
27
|
+
function applyServerSentEventLine(parts, rawLine) {
|
|
28
|
+
if (rawLine === "" || rawLine.startsWith(":"))
|
|
29
|
+
return;
|
|
30
|
+
const { field, value } = splitServerSentEventLine(rawLine);
|
|
31
|
+
if (field === "id")
|
|
32
|
+
parts.id = value;
|
|
33
|
+
else if (field === "event")
|
|
34
|
+
parts.event = value;
|
|
35
|
+
else if (field === "data")
|
|
36
|
+
parts.dataLines.push(value);
|
|
37
|
+
}
|
|
38
|
+
function splitServerSentEventLine(rawLine) {
|
|
39
|
+
const separator = rawLine.indexOf(":"), field = separator === -1 ? rawLine : rawLine.slice(0, separator), rawValue = separator === -1 ? "" : rawLine.slice(separator + 1);
|
|
40
|
+
return {
|
|
41
|
+
field,
|
|
42
|
+
value: rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function frameFromParts(parts) {
|
|
46
|
+
if (parts.id === void 0 && parts.event === void 0 && parts.dataLines.length === 0)
|
|
47
|
+
return null;
|
|
48
|
+
return {
|
|
49
|
+
...parts.id === void 0 ? {} : { id: parts.id },
|
|
50
|
+
...parts.event === void 0 ? {} : { event: parts.event },
|
|
51
|
+
data: parts.dataLines.join(`
|
|
52
|
+
`)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function isRecord(value) {
|
|
56
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
function parseWorkflowEventEnvelope(data) {
|
|
59
|
+
const parsed = parseJson(data);
|
|
60
|
+
if (!isRecord(parsed))
|
|
61
|
+
return null;
|
|
62
|
+
return workflowEventEnvelopeFromRecord(parsed);
|
|
63
|
+
}
|
|
64
|
+
function isReplayCompletePing(data) {
|
|
65
|
+
const parsed = parseJson(data);
|
|
66
|
+
return isRecord(parsed) && parsed.replayComplete === !0;
|
|
67
|
+
}
|
|
68
|
+
function parseJson(data) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(data);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function workflowEventEnvelopeFromRecord(parsed) {
|
|
76
|
+
if (typeof parsed.kind !== "string")
|
|
77
|
+
return null;
|
|
78
|
+
if (typeof parsed.workflowId !== "string")
|
|
79
|
+
return null;
|
|
80
|
+
if (!isWorkflowEventSelector(parsed.selector))
|
|
81
|
+
return null;
|
|
82
|
+
const sequence = finiteNumber(parsed.sequence);
|
|
83
|
+
if (sequence === null)
|
|
84
|
+
return null;
|
|
85
|
+
if (typeof parsed.cursor !== "string")
|
|
86
|
+
return null;
|
|
87
|
+
const emittedAtMs = finiteNumber(parsed.emittedAtMs);
|
|
88
|
+
if (emittedAtMs === null)
|
|
89
|
+
return null;
|
|
90
|
+
if (!Object.hasOwn(parsed, "payload"))
|
|
91
|
+
return null;
|
|
92
|
+
return {
|
|
93
|
+
kind: parsed.kind,
|
|
94
|
+
workflowId: parsed.workflowId,
|
|
95
|
+
selector: parsed.selector,
|
|
96
|
+
sequence,
|
|
97
|
+
cursor: parsed.cursor,
|
|
98
|
+
emittedAtMs,
|
|
99
|
+
payload: parsed.payload
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function isWorkflowEventSelector(value) {
|
|
103
|
+
return value === "events" || value === "tokens";
|
|
104
|
+
}
|
|
105
|
+
function finiteNumber(value) {
|
|
106
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
107
|
+
}
|
|
108
|
+
function envelopeToWorkflowEvent(envelope) {
|
|
109
|
+
return {
|
|
110
|
+
type: envelope.kind,
|
|
111
|
+
timestamp: envelope.emittedAtMs,
|
|
112
|
+
data: isRecord(envelope.payload) ? envelope.payload : { value: envelope.payload }
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function isEventStreamResponse(response) {
|
|
116
|
+
const contentType = response.headers.get("Content-Type");
|
|
117
|
+
if (contentType === null)
|
|
118
|
+
return !1;
|
|
119
|
+
return contentType.split(";", 1)[0]?.trim().toLowerCase() === "text/event-stream";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class SseWorkflowEventSubscription {
|
|
123
|
+
#url;
|
|
124
|
+
#headers;
|
|
125
|
+
#workflowId;
|
|
126
|
+
#onEvent;
|
|
127
|
+
#maxReconnectAttempts;
|
|
128
|
+
#reconnectBackoffMs;
|
|
129
|
+
#lifecycle;
|
|
130
|
+
#connected = Promise.withResolvers();
|
|
131
|
+
#abortController = null;
|
|
132
|
+
#reconnectAttempts = 0;
|
|
133
|
+
#reconnectTimer = null;
|
|
134
|
+
#lastEventId = null;
|
|
135
|
+
constructor(url, headers, workflowId, onEvent, options) {
|
|
136
|
+
this.#url = url;
|
|
137
|
+
this.#headers = headers;
|
|
138
|
+
this.#workflowId = workflowId;
|
|
139
|
+
this.#onEvent = onEvent;
|
|
140
|
+
this.#maxReconnectAttempts = options?.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
|
|
141
|
+
this.#reconnectBackoffMs = options?.reconnectBackoffMs ?? DEFAULT_RECONNECT_BACKOFF_MS;
|
|
142
|
+
this.#lifecycle = new WorkflowEventTailLifecycle({
|
|
143
|
+
bufferForIteration: options?.bufferForIteration ?? !1,
|
|
144
|
+
onClose: (reason) => this.#onLifecycleClose(reason)
|
|
145
|
+
});
|
|
146
|
+
this.#connect();
|
|
147
|
+
}
|
|
148
|
+
get closeReason() {
|
|
149
|
+
return this.#lifecycle.closeReason;
|
|
150
|
+
}
|
|
151
|
+
whenConnected() {
|
|
152
|
+
return this.#connected.promise;
|
|
153
|
+
}
|
|
154
|
+
async#connect() {
|
|
155
|
+
if (this.#lifecycle.closed)
|
|
156
|
+
return;
|
|
157
|
+
const abortController = new AbortController;
|
|
158
|
+
this.#abortController = abortController;
|
|
159
|
+
try {
|
|
160
|
+
const response = await fetch(this.#url, {
|
|
161
|
+
headers: this.#requestHeaders(),
|
|
162
|
+
signal: abortController.signal
|
|
163
|
+
});
|
|
164
|
+
if (!response.ok || response.body === null) {
|
|
165
|
+
this.#terminate("server-error");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!isEventStreamResponse(response)) {
|
|
169
|
+
this.#terminate("server-error");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
await this.#readResponseBody(response.body);
|
|
173
|
+
if (!this.#lifecycle.closed)
|
|
174
|
+
this.#scheduleReconnect();
|
|
175
|
+
} catch {
|
|
176
|
+
if (!this.#lifecycle.closed)
|
|
177
|
+
this.#scheduleReconnect();
|
|
178
|
+
} finally {
|
|
179
|
+
if (this.#abortController === abortController)
|
|
180
|
+
this.#abortController = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
#requestHeaders() {
|
|
184
|
+
const headers = new Headers(this.#headers);
|
|
185
|
+
headers.set("Accept", "text/event-stream");
|
|
186
|
+
headers.set("Cache-Control", "no-cache");
|
|
187
|
+
if (this.#lastEventId !== null)
|
|
188
|
+
headers.set("Last-Event-ID", this.#lastEventId);
|
|
189
|
+
return headers;
|
|
190
|
+
}
|
|
191
|
+
async#readResponseBody(body) {
|
|
192
|
+
const reader = body.getReader(), decoder = new TextDecoder;
|
|
193
|
+
let remainder = "";
|
|
194
|
+
try {
|
|
195
|
+
while (!this.#lifecycle.closed) {
|
|
196
|
+
const chunk = await reader.read();
|
|
197
|
+
if (chunk.done)
|
|
198
|
+
return;
|
|
199
|
+
const parsed = parseServerSentEventChunk(decoder.decode(chunk.value, { stream: !0 }), remainder);
|
|
200
|
+
remainder = parsed.remainder;
|
|
201
|
+
for (const frame of parsed.frames) {
|
|
202
|
+
const handling = this.#handleFrame(frame);
|
|
203
|
+
if (this.#lifecycle.closed || handling === "server-error")
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
reader.releaseLock();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
#handleFrame(frame) {
|
|
212
|
+
if (frame.event === "ping") {
|
|
213
|
+
this.#markHealthyFrame();
|
|
214
|
+
if (isReplayCompletePing(frame.data))
|
|
215
|
+
this.#markConnected();
|
|
216
|
+
return "healthy";
|
|
217
|
+
}
|
|
218
|
+
if (frame.event === "error") {
|
|
219
|
+
this.#terminate("server-error");
|
|
220
|
+
return "server-error";
|
|
221
|
+
}
|
|
222
|
+
const envelope = parseWorkflowEventEnvelope(frame.data);
|
|
223
|
+
if (envelope === null)
|
|
224
|
+
return "ignored";
|
|
225
|
+
if (envelope.workflowId !== this.#workflowId)
|
|
226
|
+
return "ignored";
|
|
227
|
+
this.#lastEventId = frame.id ?? envelope.cursor;
|
|
228
|
+
this.#markHealthyFrame();
|
|
229
|
+
this.#emit(envelopeToWorkflowEvent(envelope));
|
|
230
|
+
return "healthy";
|
|
231
|
+
}
|
|
232
|
+
#emit(event) {
|
|
233
|
+
if (!this.#lifecycle.emit(event, this.#onEvent))
|
|
234
|
+
return;
|
|
235
|
+
if (WORKFLOW_TERMINAL_EVENT_TYPES.has(event.type))
|
|
236
|
+
this.#terminate("workflow-terminal");
|
|
237
|
+
}
|
|
238
|
+
#scheduleReconnect() {
|
|
239
|
+
if (this.#lifecycle.closed || this.#reconnectTimer !== null)
|
|
240
|
+
return;
|
|
241
|
+
if (this.#reconnectAttempts >= this.#maxReconnectAttempts) {
|
|
242
|
+
this.#terminate("reconnect-exhausted");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
this.#reconnectAttempts += 1;
|
|
246
|
+
const delay = this.#reconnectBackoffMs * this.#reconnectAttempts;
|
|
247
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
248
|
+
this.#reconnectTimer = null;
|
|
249
|
+
this.#connect();
|
|
250
|
+
}, delay);
|
|
251
|
+
}
|
|
252
|
+
#markConnected() {
|
|
253
|
+
this.#connected.resolve();
|
|
254
|
+
}
|
|
255
|
+
#markHealthyFrame() {
|
|
256
|
+
this.#reconnectAttempts = 0;
|
|
257
|
+
}
|
|
258
|
+
#terminate(reason) {
|
|
259
|
+
this.#lifecycle.terminate(reason);
|
|
260
|
+
}
|
|
261
|
+
#onLifecycleClose(_reason) {
|
|
262
|
+
this.#markConnected();
|
|
263
|
+
if (this.#reconnectTimer !== null) {
|
|
264
|
+
clearTimeout(this.#reconnectTimer);
|
|
265
|
+
this.#reconnectTimer = null;
|
|
266
|
+
}
|
|
267
|
+
this.#abortController?.abort();
|
|
268
|
+
this.#abortController = null;
|
|
269
|
+
}
|
|
270
|
+
close() {
|
|
271
|
+
this.#lifecycle.close();
|
|
272
|
+
}
|
|
273
|
+
[Symbol.asyncIterator]() {
|
|
274
|
+
return this.#lifecycle[Symbol.asyncIterator]();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ScheduleSpec, StartOrSignalSignal } from '../core/types.ts';
|
|
2
|
-
import type { ClientStartOptions } from './interface.ts';
|
|
2
|
+
import type { ClientStartOptions, ClientStartOrSignalOptions } from './interface.ts';
|
|
3
3
|
export declare function setIfDefined(body: Record<string, unknown>, key: string, value: unknown): void;
|
|
4
4
|
/**
|
|
5
5
|
* Translate a schedule recurrence specification into the wire body fields the
|
|
@@ -13,4 +13,4 @@ export declare function buildStartBody(type: string, input: unknown, options?: C
|
|
|
13
13
|
* spec into `signalName` / `signalPayload` / `signalId` and carries the same
|
|
14
14
|
* start options as {@link buildStartBody}.
|
|
15
15
|
*/
|
|
16
|
-
export declare function buildStartOrSignalBody(type: string, input: unknown, signal: StartOrSignalSignal, options?:
|
|
16
|
+
export declare function buildStartOrSignalBody(type: string, input: unknown, signal: StartOrSignalSignal, options?: ClientStartOrSignalOptions): Record<string, unknown>;
|
|
@@ -28,5 +28,6 @@ export function buildStartOrSignalBody(type, input, signal, options) {
|
|
|
28
28
|
setIfDefined(body, "signalPayload", signal.payload);
|
|
29
29
|
setIfDefined(body, "signalId", signal.signalId);
|
|
30
30
|
applyStartOptionsToBody(body, options);
|
|
31
|
+
setIfDefined(body, "onTerminalConflict", options?.onTerminalConflict);
|
|
31
32
|
return body;
|
|
32
33
|
}
|
|
@@ -6,6 +6,7 @@ import { WorkerPool } from "../../workers/pool.js";
|
|
|
6
6
|
import { optionalInlineDependencies } from "../inline-execution-strategy.context-options.js";
|
|
7
7
|
import { InlineExecutionStrategy } from "../inline-execution-strategy.js";
|
|
8
8
|
import {
|
|
9
|
+
DEFAULT_POLL_INTERVAL_MS,
|
|
9
10
|
DEFAULT_RETENTION_SWEEP_BATCH_SIZE,
|
|
10
11
|
DEFAULT_RETENTION_SWEEP_INTERVAL_MS
|
|
11
12
|
} from "../types.js";
|
|
@@ -96,7 +97,8 @@ function resolveNumericDefaults(options) {
|
|
|
96
97
|
return {
|
|
97
98
|
checkpointHistory: defaultTo(options?.checkpointHistory, 10),
|
|
98
99
|
checkpointSizeWarningThreshold: defaultTo(options?.checkpointSizeWarningThreshold, 65536),
|
|
99
|
-
maxNestingDepth: defaultTo(options?.maxNestingDepth, 10)
|
|
100
|
+
maxNestingDepth: defaultTo(options?.maxNestingDepth, 10),
|
|
101
|
+
schedulerPollIntervalMs: normalizePositiveSafeIntegerOption(options?.schedulerPollIntervalMs, "options.schedulerPollIntervalMs", DEFAULT_POLL_INTERVAL_MS) ?? DEFAULT_POLL_INTERVAL_MS
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
104
|
function resolveHistoryFields(options) {
|
|
@@ -27,6 +27,8 @@ export interface ResolvedOptions {
|
|
|
27
27
|
checkpointHistory: number;
|
|
28
28
|
checkpointSizeWarningThreshold: number;
|
|
29
29
|
maxNestingDepth: number;
|
|
30
|
+
/** Durable-timer scheduler poll interval (ms). */
|
|
31
|
+
schedulerPollIntervalMs: number;
|
|
30
32
|
broadcastEvents: boolean;
|
|
31
33
|
retention: NormalizedRetentionPolicy | null;
|
|
32
34
|
retentionSweepIntervalMs: number;
|
|
@@ -289,14 +289,14 @@ export declare class ActivityResolutionError extends WeftError<'ActivityResoluti
|
|
|
289
289
|
/**
|
|
290
290
|
* Thrown by {@link Engine.startOrSignal} when the target workflow already exists
|
|
291
291
|
* and is in a terminal state (completed, failed, cancelled, or timed out). A
|
|
292
|
-
* terminal run cannot accept a signal
|
|
293
|
-
*
|
|
294
|
-
*
|
|
292
|
+
* terminal run cannot accept a signal. By default `startOrSignal` surfaces this
|
|
293
|
+
* conflict instead of starting a fresh run under the same id or dropping the
|
|
294
|
+
* signal.
|
|
295
295
|
*
|
|
296
296
|
* Inspect `workflowId` to identify the conflicting run and `status` to see why
|
|
297
|
-
* it was rejected. To deliberately
|
|
298
|
-
*
|
|
299
|
-
*
|
|
297
|
+
* it was rejected. To deliberately reuse a stable id, call `startOrSignal` with
|
|
298
|
+
* `onTerminalConflict: 'start-new'`, an explicit `id`, and a deterministic
|
|
299
|
+
* `signalId`; otherwise choose a different id or idempotency key.
|
|
300
300
|
*
|
|
301
301
|
* @example
|
|
302
302
|
* ```ts
|
|
@@ -118,7 +118,7 @@ export class StartOrSignalConflictError extends WeftError {
|
|
|
118
118
|
workflowId;
|
|
119
119
|
status;
|
|
120
120
|
constructor(workflowId, status) {
|
|
121
|
-
super("StartOrSignalConflictError", `Workflow "${workflowId}" is already in terminal state "${status}" and cannot accept a startOrSignal:
|
|
121
|
+
super("StartOrSignalConflictError", `Workflow "${workflowId}" is already in terminal state "${status}" and cannot accept a startOrSignal. Use options.onTerminalConflict: 'start-new' with an explicit id and signalId, or choose a different id or idempotency key to start a new run.`);
|
|
122
122
|
this.workflowId = workflowId;
|
|
123
123
|
this.status = status;
|
|
124
124
|
}
|
|
@@ -129,7 +129,7 @@ export class WorkflowHandle extends EventTarget {
|
|
|
129
129
|
observer.error?.(event.error);
|
|
130
130
|
} else if (event instanceof WorkflowTimedOutEvent) {
|
|
131
131
|
terminalDelivered = !0;
|
|
132
|
-
observer.error?.(new WorkflowTimeoutError(event.workflowId, event.timeoutType, event.elapsed));
|
|
132
|
+
observer.error?.(new WorkflowTimeoutError(event.workflowId, event.timeoutType, event.elapsed, event.reason));
|
|
133
133
|
}
|
|
134
134
|
};
|
|
135
135
|
this.addEventListener("workflow:failed", errorHandler, { signal: controller.signal });
|
|
@@ -5,7 +5,7 @@ import type { TypedEventTarget, WeftEventMap } from '../events.ts';
|
|
|
5
5
|
import type { Interceptor } from '../interceptor.ts';
|
|
6
6
|
import { type ReviewRequest } from '../review/index.ts';
|
|
7
7
|
import { Scheduler } from '../scheduler.ts';
|
|
8
|
-
import { type AnyActivityDefinition, type AnyWorkflowDefinition, type AttributeFilterKey, type BulkCancelResult, type BulkDeleteResult, type BulkOperationCommitOptions, type BulkOperationDryRunOptions, type BulkOperationDryRunResult, type BulkRetryFailedResult, type BulkSignalAllCommitOptions, type BulkSignalAllDryRunOptions, type BulkSignalResult, type BulkTagResult, type CheckpointState, type CheckpointSummary, type CoordinatedUpdateResult, type DefaultActivityTypes, type DefaultWorkflowRegistry, type ForkOptions, type InferActivityEntries, type InferActivityEntry, type InferWorkflowEntries, type InferWorkflowEntry, type ListFilter, type ListOptions, type PaginatedResult, type PurgeResult, type QueryDefinition, type RegisteredWorkflowDefinition, type RetentionOverview, type ReviewListEntry, type ReviewListFilter, type ScheduleDefinition, type ScheduleFilter, type ScheduleOptions, type ScheduleSpec, type ScheduleSummary, type SearchAttributeValue, type SignalDefinition, type SignalDeliveryOptions, type
|
|
8
|
+
import { type AnyActivityDefinition, type AnyWorkflowDefinition, type AttributeFilterKey, type BulkCancelResult, type BulkDeleteResult, type BulkOperationCommitOptions, type BulkOperationDryRunOptions, type BulkOperationDryRunResult, type BulkRetryFailedResult, type BulkSignalAllCommitOptions, type BulkSignalAllDryRunOptions, type BulkSignalResult, type BulkTagResult, type CheckpointState, type CheckpointSummary, type CoordinatedUpdateResult, type DefaultActivityTypes, type DefaultWorkflowRegistry, type ForkOptions, type InferActivityEntries, type InferActivityEntry, type InferWorkflowEntries, type InferWorkflowEntry, type ListFilter, type ListOptions, type PaginatedResult, type PurgeResult, type QueryDefinition, type RegisteredWorkflowDefinition, type RetentionOverview, type ReviewListEntry, type ReviewListFilter, type ScheduleDefinition, type ScheduleFilter, type ScheduleOptions, type ScheduleSpec, type ScheduleSummary, type SearchAttributeValue, type SignalDefinition, type SignalDeliveryOptions, type StartOrSignalOptions, type StartOrSignalSignal, type StartWorkflowOptions, type SubmitReviewOptions, type TypedListFilter, type UpdateDefinition, type WorkflowEvent, type WorkflowInput, type WorkflowOutput, type WorkflowReplay, type WorkflowServices, type WorkflowServicesUnion, type WorkflowState, type WorkflowSummary, type WorkflowTimelineEntry } from '../types.ts';
|
|
9
9
|
import type { TimerEntry } from '../types/checkpoint.ts';
|
|
10
10
|
import type { WorkflowAlreadyRegistered } from '../types/workflow-builder.ts';
|
|
11
11
|
import { type AggregateOptions, type AggregateResult } from './aggregate.ts';
|
|
@@ -202,8 +202,9 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
|
|
|
202
202
|
* first signal commit in one batch and the freshly-launched run consumes the
|
|
203
203
|
* signal on its first drive. A non-terminal target (running, pending, or
|
|
204
204
|
* suspended) is signalled through the normal signal path; a terminal target
|
|
205
|
-
* throws {@link StartOrSignalConflictError}
|
|
206
|
-
*
|
|
205
|
+
* throws {@link StartOrSignalConflictError} unless
|
|
206
|
+
* `options.onTerminalConflict: 'start-new'` is supplied with an explicit
|
|
207
|
+
* workflow id and deterministic `signal.signalId`.
|
|
207
208
|
*
|
|
208
209
|
* Concurrent callers converge on one workflow and one delivered signal. Pass
|
|
209
210
|
* `options.idempotencyKey` to dedup independent callers (e.g. retried
|
|
@@ -213,8 +214,8 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
|
|
|
213
214
|
* are `options.id` and `options.idempotencyKey`. Requires a storage backend
|
|
214
215
|
* with `conditionalBatch`.
|
|
215
216
|
*/
|
|
216
|
-
startOrSignal<TName extends KnownWorkflowNames<TWorkflows>>(type: TName, input: WorkflowInput<TWorkflows, TName>, signal: StartOrSignalSignal, options?:
|
|
217
|
-
startOrSignal<TName extends string>(type: UnknownWorkflowNameWhenDefaultRegistryIsEmpty<TWorkflows, TName>, input: unknown, signal: StartOrSignalSignal, options?:
|
|
217
|
+
startOrSignal<TName extends KnownWorkflowNames<TWorkflows>>(type: TName, input: WorkflowInput<TWorkflows, TName>, signal: StartOrSignalSignal, options?: StartOrSignalOptions<WorkflowServices<TWorkflows, TName>>): Promise<StartOrSignalResult<WorkflowOutput<TWorkflows, TName>>>;
|
|
218
|
+
startOrSignal<TName extends string>(type: UnknownWorkflowNameWhenDefaultRegistryIsEmpty<TWorkflows, TName>, input: unknown, signal: StartOrSignalSignal, options?: StartOrSignalOptions): Promise<StartOrSignalResult>;
|
|
218
219
|
getHandle(workflowId: string): WorkflowHandle;
|
|
219
220
|
list<const TAttributeKeys extends readonly AttributeFilterKey[] = readonly AttributeFilterKey[]>(filter?: TypedListFilter<TAttributeKeys>, options?: ListOptions): Promise<PaginatedResult<WorkflowSummary>>;
|
|
220
221
|
aggregate(filter: ListFilter | undefined, options: AggregateOptions): Promise<AggregateResult>;
|
|
@@ -289,6 +289,7 @@ export class Engine extends EventTarget {
|
|
|
289
289
|
getInternals(this).scheduler = new Scheduler({
|
|
290
290
|
storage,
|
|
291
291
|
onTimerFired: (entry) => handleTimerFiredFromInternals(getInternals(this), entry, this.#createTimeOperationCallbacks()),
|
|
292
|
+
pollIntervalMs: resolvedOptions.schedulerPollIntervalMs,
|
|
292
293
|
getNow,
|
|
293
294
|
commitTimerCleanup: (operations) => commitFencedEngineWrite(getInternals(this), operations, [], () => Error("Timer cleanup lost its fenced CAS race"))
|
|
294
295
|
});
|
|
@@ -24,7 +24,8 @@ export async function reprovideRecoveredServices(internals, state, failRun, onCo
|
|
|
24
24
|
const resolution = await resolver({
|
|
25
25
|
workflowId: state.id,
|
|
26
26
|
workflowType: state.type,
|
|
27
|
-
input: state.input
|
|
27
|
+
input: state.input,
|
|
28
|
+
launchOptions: launchOptionsFromWorkflowState(state)
|
|
28
29
|
});
|
|
29
30
|
if (resolution.status === "available") {
|
|
30
31
|
internals.workflowServices.set(state.id, resolution.services);
|
|
@@ -47,3 +48,9 @@ export function unavailableServicesError(workflowId, reason) {
|
|
|
47
48
|
function missingServicesResolverReason(workflowId) {
|
|
48
49
|
return `Workflow "${workflowId}" was started with services, but this engine has no resolveWorkflowServices option. Configure EngineOptions.resolveWorkflowServices before recovery so ctx.services can be re-provided.`;
|
|
49
50
|
}
|
|
51
|
+
function launchOptionsFromWorkflowState(state) {
|
|
52
|
+
return {
|
|
53
|
+
id: state.id,
|
|
54
|
+
...state.tags !== void 0 && state.tags.length > 0 ? { tags: [...state.tags] } : {}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module core/engine/lifecycle/start-or-signal-create
|
|
9
9
|
*/
|
|
10
|
-
import type {
|
|
10
|
+
import type { StartOrSignalOptions, StartOrSignalSignal } from '../../types.ts';
|
|
11
11
|
import { type StartOrSignalOutcome, type WorkflowHandle } from '../handles.ts';
|
|
12
12
|
import type { EngineInternals } from '../internals.ts';
|
|
13
13
|
import { type BuildIdempotentStartOperations } from './start-commit.ts';
|
|
@@ -55,4 +55,4 @@ export type StartOrSignalResult<TResult = unknown> = {
|
|
|
55
55
|
* (scan-then-park), and the caller's payload loses to the pre-buffered one by
|
|
56
56
|
* the same first-wins dedup.
|
|
57
57
|
*/
|
|
58
|
-
export declare function createWithSignalOrFallback(internals: EngineInternals, type: string, input: unknown, signalSpec: StartOrSignalSignal, signalId: string, options:
|
|
58
|
+
export declare function createWithSignalOrFallback(internals: EngineInternals, type: string, input: unknown, signalSpec: StartOrSignalSignal, signalId: string, options: StartOrSignalOptions | undefined, callbacks: StartOrSignalCallbacks): Promise<StartOrSignalResult>;
|
|
@@ -44,23 +44,25 @@ export async function createWithSignalOrFallback(internals, type, input, signalS
|
|
|
44
44
|
});
|
|
45
45
|
if (outcome.kind === "created")
|
|
46
46
|
return { handle: outcome.handle, outcome: "started" };
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
outcome: "signalled"
|
|
51
|
-
};
|
|
52
|
-
if (outcome.kind === "lost-caller-id") {
|
|
53
|
-
const resolved = await resolveCallerIdWinnerOrRetry(internals, outcome.id, signalSpec, signalId, callbacks);
|
|
54
|
-
if (resolved !== void 0)
|
|
55
|
-
return { handle: resolved, outcome: "signalled" };
|
|
56
|
-
} else {
|
|
57
|
-
const resolved = await plainCreateBufferedSignalOrResolve(internals, type, input, signalSpec, signalId, options, callbacks);
|
|
58
|
-
if (resolved !== void 0)
|
|
59
|
-
return resolved;
|
|
60
|
-
}
|
|
47
|
+
const resolved = await resolveCreateFallbackOutcome(internals, outcome, type, input, signalSpec, signalId, options, callbacks);
|
|
48
|
+
if (resolved !== void 0)
|
|
49
|
+
return resolved;
|
|
61
50
|
}
|
|
62
51
|
throw Error(`startOrSignal could not create workflow "${options?.id ?? "<generated>"}" after ${CALLER_ID_CREATE_MAX_ATTEMPTS} attempts: each concurrent same-id winner aborted before its durable commit.`);
|
|
63
52
|
}
|
|
53
|
+
async function resolveCreateFallbackOutcome(internals, outcome, type, input, signalSpec, signalId, options, callbacks) {
|
|
54
|
+
if (outcome.kind === "lost-keyed")
|
|
55
|
+
return {
|
|
56
|
+
handle: await resolveWinnerWithSignal(internals, outcome.id, signalSpec, signalId, callbacks, outcome.idempotencyKey),
|
|
57
|
+
outcome: "signalled"
|
|
58
|
+
};
|
|
59
|
+
const allowTerminalRestart = options?.onTerminalConflict === "start-new";
|
|
60
|
+
if (outcome.kind === "lost-caller-id") {
|
|
61
|
+
const resolved = await resolveCallerIdWinnerOrRetry(internals, outcome.id, signalSpec, signalId, callbacks, allowTerminalRestart);
|
|
62
|
+
return resolved === void 0 ? void 0 : { handle: resolved, outcome: "signalled" };
|
|
63
|
+
}
|
|
64
|
+
return plainCreateBufferedSignalOrResolve(internals, type, input, signalSpec, signalId, options, callbacks);
|
|
65
|
+
}
|
|
64
66
|
async function plainCreateBufferedSignalOrResolve(internals, type, input, signalSpec, signalId, options, callbacks) {
|
|
65
67
|
try {
|
|
66
68
|
return {
|
|
@@ -70,7 +72,7 @@ async function plainCreateBufferedSignalOrResolve(internals, type, input, signal
|
|
|
70
72
|
} catch (error) {
|
|
71
73
|
if (!(error instanceof WorkflowAlreadyExistsError))
|
|
72
74
|
throw error;
|
|
73
|
-
const resolved = await resolveCallerIdWinnerOrRetry(internals, error.workflowId, signalSpec, signalId, callbacks);
|
|
75
|
+
const resolved = await resolveCallerIdWinnerOrRetry(internals, error.workflowId, signalSpec, signalId, callbacks, options?.onTerminalConflict === "start-new");
|
|
74
76
|
return resolved === void 0 ? void 0 : { handle: resolved, outcome: "signalled" };
|
|
75
77
|
}
|
|
76
78
|
}
|
|
@@ -47,7 +47,7 @@ export declare function resolveExistingRunOrThrowPurged(internals: EngineInterna
|
|
|
47
47
|
* committing (storage failure, oversized payload, throwing start interceptor): no
|
|
48
48
|
* run exists, so return `undefined` and let the caller retry its own create.
|
|
49
49
|
*/
|
|
50
|
-
export declare function resolveCallerIdWinnerOrRetry(internals: EngineInternals, winnerId: string, signalSpec: StartOrSignalSignal, signalId: string, callbacks: StartOrSignalCallbacks): Promise<WorkflowHandle | undefined>;
|
|
50
|
+
export declare function resolveCallerIdWinnerOrRetry(internals: EngineInternals, winnerId: string, signalSpec: StartOrSignalSignal, signalId: string, callbacks: StartOrSignalCallbacks, allowTerminalRestart?: boolean): Promise<WorkflowHandle | undefined>;
|
|
51
51
|
/**
|
|
52
52
|
* For a workflow that already exists: signal it if non-terminal, conflict if
|
|
53
53
|
* terminal. Returns the handle on a successful signal, or `undefined` when the
|
|
@@ -23,12 +23,30 @@ async function awaitReservationCleared(internals, workflowId) {
|
|
|
23
23
|
await sleep(RESERVATION_CLEAR_RETRY_DELAY_MS);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSpec, signalId, callbacks) {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
|
|
26
|
+
export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSpec, signalId, callbacks, allowTerminalRestart = !1) {
|
|
27
|
+
const state = await loadWorkflowState(internals, winnerId);
|
|
28
|
+
if (state !== null) {
|
|
29
|
+
if (!isTerminalWorkflowStatus(state.status)) {
|
|
30
|
+
await callbacks.signalExistingWorkflow(winnerId, signalSpec.name, signalSpec.payload, signalId);
|
|
31
|
+
return callbacks.getHandle(winnerId);
|
|
32
|
+
}
|
|
33
|
+
if (!allowTerminalRestart)
|
|
34
|
+
throw new StartOrSignalConflictError(winnerId, state.status);
|
|
35
|
+
}
|
|
30
36
|
await awaitReservationCleared(internals, winnerId);
|
|
31
|
-
|
|
37
|
+
const stateAfterReservation = await loadWorkflowState(internals, winnerId);
|
|
38
|
+
if (stateAfterReservation === null)
|
|
39
|
+
return;
|
|
40
|
+
if (isTerminalWorkflowStatus(stateAfterReservation.status)) {
|
|
41
|
+
if (state !== null && isSameTerminalRun(state, stateAfterReservation))
|
|
42
|
+
return;
|
|
43
|
+
throw new StartOrSignalConflictError(winnerId, stateAfterReservation.status);
|
|
44
|
+
}
|
|
45
|
+
await callbacks.signalExistingWorkflow(winnerId, signalSpec.name, signalSpec.payload, signalId);
|
|
46
|
+
return callbacks.getHandle(winnerId);
|
|
47
|
+
}
|
|
48
|
+
function isSameTerminalRun(before, after) {
|
|
49
|
+
return before.id === after.id && before.type === after.type && before.status === after.status && before.createdAt === after.createdAt && before.updatedAt === after.updatedAt && before.terminalCleanupToken === after.terminalCleanupToken;
|
|
32
50
|
}
|
|
33
51
|
export async function signalOrConflictExistingWorkflow(internals, workflowId, signalSpec, signalId, callbacks) {
|
|
34
52
|
const state = await loadWorkflowState(internals, workflowId);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StartOptions, StartOrSignalSignal } from '../../types.ts';
|
|
1
|
+
import type { StartOptions, StartOrSignalOptions, StartOrSignalSignal } from '../../types.ts';
|
|
2
2
|
import { type WorkflowHandle } from '../handles.ts';
|
|
3
3
|
import type { EngineInternals } from '../internals.ts';
|
|
4
4
|
import { type LifecycleCallbacks } from './shared.ts';
|
|
@@ -28,8 +28,10 @@ export declare function startWithIdempotency(internals: EngineInternals, type: s
|
|
|
28
28
|
* - **Non-terminal** (running, pending, suspended) → deliver the signal through
|
|
29
29
|
* the standard engine signal path with the same `signalId`, so it dedups
|
|
30
30
|
* against a create-batch signal a concurrent winner may have written.
|
|
31
|
-
* - **Terminal** → throw {@link StartOrSignalConflictError}
|
|
32
|
-
*
|
|
31
|
+
* - **Terminal** → throw {@link StartOrSignalConflictError} by default. With
|
|
32
|
+
* `options.onTerminalConflict: 'start-new'`, purge the prior terminal run
|
|
33
|
+
* through the shared terminal-replacement path, start a fresh run, and deliver
|
|
34
|
+
* the initial signal in the create batch.
|
|
33
35
|
*
|
|
34
36
|
* Convergence requires a SHARED workflow identity. Concurrent callers converge on
|
|
35
37
|
* one workflow and one signal only when they share an `options.idempotencyKey`
|
|
@@ -43,4 +45,4 @@ export declare function startWithIdempotency(internals: EngineInternals, type: s
|
|
|
43
45
|
* convergence primitive. Use `idempotencyKey` (id-free convergence) or
|
|
44
46
|
* `id` + `signalId` when concurrent callers must converge.
|
|
45
47
|
*/
|
|
46
|
-
export declare function startOrSignal(internals: EngineInternals, type: string, input: unknown, signalSpec: StartOrSignalSignal, options:
|
|
48
|
+
export declare function startOrSignal(internals: EngineInternals, type: string, input: unknown, signalSpec: StartOrSignalSignal, options: StartOrSignalOptions | undefined, callbacks: StartOrSignalCallbacks): Promise<StartOrSignalResult>;
|