@lostgradient/weft 0.6.0 → 0.8.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 +7 -5
- 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/context/activity-retry-state.d.ts +6 -5
- package/dist/core/context/activity-retry-state.js +31 -21
- package/dist/core/context/durable-activity.d.ts +117 -0
- package/dist/core/context/durable-activity.js +79 -0
- package/dist/core/context/operation-request.d.ts +15 -0
- package/dist/core/context/parallel-operations.js +1 -0
- package/dist/core/context/run-operation-cached-request.d.ts +8 -0
- package/dist/core/context/run-operation-cached-request.js +59 -0
- package/dist/core/context/run-operation.d.ts +23 -5
- package/dist/core/context/run-operation.js +62 -47
- package/dist/core/engine/activity-heartbeat-tracking.d.ts +8 -17
- package/dist/core/engine/activity-heartbeat-tracking.js +7 -1
- package/dist/core/engine/activity-reconciliation.d.ts +1 -0
- package/dist/core/engine/activity-reconciliation.js +7 -2
- package/dist/core/engine/anonymous-signal-sequence.js +6 -4
- package/dist/core/engine/async-activity-completion.d.ts +5 -7
- package/dist/core/engine/async-activity-completion.js +13 -9
- package/dist/core/engine/bulk-operations-purge.js +2 -1
- package/dist/core/engine/bulk-operations.js +8 -7
- package/dist/core/engine/callback-checkpoint-persistence.d.ts +3 -0
- package/dist/core/engine/callback-checkpoint-persistence.js +25 -0
- package/dist/core/engine/callback-creators-bundles.js +4 -1
- package/dist/core/engine/checkpoint-io.d.ts +4 -1
- package/dist/core/engine/checkpoint-io.js +19 -10
- package/dist/core/engine/completed-review-storage.d.ts +2 -1
- package/dist/core/engine/completed-review-storage.js +4 -3
- 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 +37 -5
- package/dist/core/engine/index.js +7 -1
- package/dist/core/engine/internals.d.ts +2 -1
- package/dist/core/engine/lease-manager.js +2 -2
- 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/memo-durable-activity.d.ts +11 -0
- package/dist/core/engine/memo-durable-activity.js +282 -0
- package/dist/core/engine/operations-activity.d.ts +5 -1
- package/dist/core/engine/operations-activity.js +19 -7
- package/dist/core/engine/operations-data.d.ts +4 -1
- package/dist/core/engine/operations-data.js +3 -3
- package/dist/core/engine/reviews.js +7 -4
- package/dist/core/engine/schedule-run.js +12 -3
- package/dist/core/engine/schedule-timer.js +2 -0
- package/dist/core/engine/storage-io.js +2 -2
- package/dist/core/engine/termination/complete.js +3 -3
- package/dist/core/json.js +1 -1
- 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 +3 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +7 -0
- package/dist/json-schema.js +1 -1
- package/dist/mcp/cli.js +26 -26
- 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 +28 -28
- package/dist/server/index.d.ts +8 -7
- package/dist/server/index.js +17 -17
- 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/runtime/websocket-worker.js +7 -2
- package/dist/server/serve-internals.d.ts +28 -0
- package/dist/server/serve-internals.js +4 -2
- 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/service-worker/setup.d.ts +18 -1
- package/dist/service-worker/setup.js +7 -4
- package/dist/storage/typed-storage.d.ts +1 -1
- package/dist/storage/typed-storage.js +1 -1
- package/dist/testing/index.js +27 -27
- 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
|
}
|
|
@@ -10,6 +10,7 @@ import type { ContextInternals } from './internals.ts';
|
|
|
10
10
|
export declare const ACTIVITY_RETRY_STATE_LOCAL_KEY = "__weftActivityRetryState";
|
|
11
11
|
declare const ACTIVITY_RETRY_STATE_VERSION = 1;
|
|
12
12
|
export declare const MAX_CHECKPOINTED_RETRY_ATTEMPT = 10000;
|
|
13
|
+
export type ActivityRetryStateKey = number | string;
|
|
13
14
|
export interface ActivityRetryState {
|
|
14
15
|
version: typeof ACTIVITY_RETRY_STATE_VERSION;
|
|
15
16
|
attempts: Record<string, number>;
|
|
@@ -24,15 +25,15 @@ export interface ActivityRetryState {
|
|
|
24
25
|
dispatchedAt?: Record<string, number>;
|
|
25
26
|
}
|
|
26
27
|
export declare function isActivityRetryState(value: unknown): value is ActivityRetryState;
|
|
27
|
-
export declare function readActivityRetryAttempt(internals: ContextInternals,
|
|
28
|
-
export declare function readCompletedRetrySleepCount(internals: ContextInternals,
|
|
29
|
-
export declare function writeActivityRetryAttempt(internals: ContextInternals,
|
|
30
|
-
export declare function completeActivityRetryAttempt(internals: ContextInternals,
|
|
28
|
+
export declare function readActivityRetryAttempt(internals: ContextInternals, key: ActivityRetryStateKey): number | undefined;
|
|
29
|
+
export declare function readCompletedRetrySleepCount(internals: ContextInternals, key: ActivityRetryStateKey): number;
|
|
30
|
+
export declare function writeActivityRetryAttempt(internals: ContextInternals, key: ActivityRetryStateKey, attempt: number): void;
|
|
31
|
+
export declare function completeActivityRetryAttempt(internals: ContextInternals, key: ActivityRetryStateKey, completedRetrySleepCount: number): void;
|
|
31
32
|
/**
|
|
32
33
|
* Read the first-dispatch anchor for a step's `scheduleToCloseTimeout`, writing
|
|
33
34
|
* `now` on the first dispatch. Read-first so a crash/replay never resets the
|
|
34
35
|
* window: the budget is measured from the original dispatch, and the anchor
|
|
35
36
|
* lands in the checkpoint committed at the dispatch `yield` that follows.
|
|
36
37
|
*/
|
|
37
|
-
export declare function readOrInitActivityDispatchedAt(internals: ContextInternals,
|
|
38
|
+
export declare function readOrInitActivityDispatchedAt(internals: ContextInternals, key: ActivityRetryStateKey, now: number): number;
|
|
38
39
|
export {};
|
|
@@ -7,29 +7,39 @@ function isRecord(value) {
|
|
|
7
7
|
export function isActivityRetryState(value) {
|
|
8
8
|
return isRecord(value) && value.version === ACTIVITY_RETRY_STATE_VERSION && isRecord(value.attempts) && (value.completedRetrySleeps === void 0 || isRecord(value.completedRetrySleeps)) && (value.dispatchedAt === void 0 || isRecord(value.dispatchedAt));
|
|
9
9
|
}
|
|
10
|
-
function
|
|
10
|
+
function retryStateSlotKey(key) {
|
|
11
|
+
if (typeof key === "number")
|
|
12
|
+
return String(key);
|
|
13
|
+
if (key.length === 0)
|
|
14
|
+
throw Error("Invalid empty checkpointed activity retry state key");
|
|
15
|
+
return key;
|
|
16
|
+
}
|
|
17
|
+
function retryStateKeyLabel(key) {
|
|
18
|
+
return typeof key === "number" ? `step ${String(key)}` : `key ${key}`;
|
|
19
|
+
}
|
|
20
|
+
function assertValidRetryAttempt(attempt, key) {
|
|
11
21
|
if (!Number.isInteger(attempt) || attempt <= 1 || attempt > MAX_CHECKPOINTED_RETRY_ATTEMPT)
|
|
12
|
-
throw Error(`Invalid checkpointed activity retry attempt ${String(attempt)} for
|
|
22
|
+
throw Error(`Invalid checkpointed activity retry attempt ${String(attempt)} for ${retryStateKeyLabel(key)}`);
|
|
13
23
|
}
|
|
14
|
-
export function readActivityRetryAttempt(internals,
|
|
24
|
+
export function readActivityRetryAttempt(internals, key) {
|
|
15
25
|
const state = internals.checkpointLocals[ACTIVITY_RETRY_STATE_LOCAL_KEY];
|
|
16
26
|
if (!isActivityRetryState(state))
|
|
17
27
|
return;
|
|
18
|
-
const attempt = state.attempts[
|
|
28
|
+
const attempt = state.attempts[retryStateSlotKey(key)];
|
|
19
29
|
if (attempt === void 0)
|
|
20
30
|
return;
|
|
21
|
-
assertValidRetryAttempt(attempt,
|
|
31
|
+
assertValidRetryAttempt(attempt, key);
|
|
22
32
|
return attempt;
|
|
23
33
|
}
|
|
24
|
-
export function readCompletedRetrySleepCount(internals,
|
|
34
|
+
export function readCompletedRetrySleepCount(internals, key) {
|
|
25
35
|
const state = internals.checkpointLocals[ACTIVITY_RETRY_STATE_LOCAL_KEY];
|
|
26
36
|
if (!isActivityRetryState(state))
|
|
27
37
|
return 0;
|
|
28
|
-
const count = state.completedRetrySleeps?.[
|
|
38
|
+
const count = state.completedRetrySleeps?.[retryStateSlotKey(key)];
|
|
29
39
|
if (count === void 0)
|
|
30
40
|
return 0;
|
|
31
41
|
if (!Number.isInteger(count) || count < 0 || count > MAX_CHECKPOINTED_RETRY_ATTEMPT)
|
|
32
|
-
throw Error(`Invalid checkpointed activity retry sleep count ${String(count)} for
|
|
42
|
+
throw Error(`Invalid checkpointed activity retry sleep count ${String(count)} for ${retryStateKeyLabel(key)}`);
|
|
33
43
|
return count;
|
|
34
44
|
}
|
|
35
45
|
function currentRetryState(internals) {
|
|
@@ -52,38 +62,38 @@ function writeRetryStateSlot(internals, attempts, completedRetrySleeps, dispatch
|
|
|
52
62
|
}
|
|
53
63
|
};
|
|
54
64
|
}
|
|
55
|
-
export function writeActivityRetryAttempt(internals,
|
|
56
|
-
assertValidRetryAttempt(attempt,
|
|
65
|
+
export function writeActivityRetryAttempt(internals, key, attempt) {
|
|
66
|
+
assertValidRetryAttempt(attempt, key);
|
|
57
67
|
const current = currentRetryState(internals), attempts = current ? { ...current.attempts } : {};
|
|
58
|
-
attempts[
|
|
68
|
+
attempts[retryStateSlotKey(key)] = attempt;
|
|
59
69
|
writeRetryStateSlot(internals, attempts, current?.completedRetrySleeps, current?.dispatchedAt);
|
|
60
70
|
}
|
|
61
|
-
function clearActivityRetryAttempt(internals,
|
|
71
|
+
function clearActivityRetryAttempt(internals, key) {
|
|
62
72
|
const current = currentRetryState(internals);
|
|
63
73
|
if (!current)
|
|
64
74
|
return;
|
|
65
75
|
const attempts = { ...current.attempts };
|
|
66
|
-
delete attempts[
|
|
76
|
+
delete attempts[retryStateSlotKey(key)];
|
|
67
77
|
writeRetryStateSlot(internals, attempts, current.completedRetrySleeps, current.dispatchedAt);
|
|
68
78
|
}
|
|
69
|
-
export function completeActivityRetryAttempt(internals,
|
|
79
|
+
export function completeActivityRetryAttempt(internals, key, completedRetrySleepCount) {
|
|
70
80
|
if (!Number.isInteger(completedRetrySleepCount) || completedRetrySleepCount < 0 || completedRetrySleepCount > MAX_CHECKPOINTED_RETRY_ATTEMPT)
|
|
71
|
-
throw Error(`Invalid completed activity retry sleep count ${String(completedRetrySleepCount)} for
|
|
72
|
-
clearActivityRetryAttempt(internals,
|
|
81
|
+
throw Error(`Invalid completed activity retry sleep count ${String(completedRetrySleepCount)} for ${retryStateKeyLabel(key)}`);
|
|
82
|
+
clearActivityRetryAttempt(internals, key);
|
|
73
83
|
if (completedRetrySleepCount === 0)
|
|
74
84
|
return;
|
|
75
85
|
const current = currentRetryState(internals), attempts = current ? { ...current.attempts } : {}, completedRetrySleeps = current ? { ...current.completedRetrySleeps } : {};
|
|
76
|
-
completedRetrySleeps[
|
|
86
|
+
completedRetrySleeps[retryStateSlotKey(key)] = completedRetrySleepCount;
|
|
77
87
|
writeRetryStateSlot(internals, attempts, completedRetrySleeps, current?.dispatchedAt);
|
|
78
88
|
}
|
|
79
|
-
export function readOrInitActivityDispatchedAt(internals,
|
|
80
|
-
const current = currentRetryState(internals), existing = current?.dispatchedAt?.[
|
|
89
|
+
export function readOrInitActivityDispatchedAt(internals, key, now) {
|
|
90
|
+
const current = currentRetryState(internals), slotKey = retryStateSlotKey(key), existing = current?.dispatchedAt?.[slotKey];
|
|
81
91
|
if (typeof existing === "number" && Number.isFinite(existing))
|
|
82
92
|
return existing;
|
|
83
93
|
if (existing !== void 0)
|
|
84
|
-
throw Error(`Invalid checkpointed activity dispatch anchor ${String(existing)} for
|
|
94
|
+
throw Error(`Invalid checkpointed activity dispatch anchor ${String(existing)} for ${retryStateKeyLabel(key)}`);
|
|
85
95
|
const attempts = current ? { ...current.attempts } : {}, dispatchedAt = current?.dispatchedAt ? { ...current.dispatchedAt } : {};
|
|
86
|
-
dispatchedAt[
|
|
96
|
+
dispatchedAt[slotKey] = now;
|
|
87
97
|
writeRetryStateSlot(internals, attempts, current?.completedRetrySleeps, dispatchedAt);
|
|
88
98
|
return now;
|
|
89
99
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ActivityCallable, ActivityCallOptions } from '../types.ts';
|
|
2
|
+
import { WeftError } from '../weft-error.ts';
|
|
3
|
+
type BareActivityFunction<TInput, TResult> = ((input: TInput) => Promise<TResult> | TResult) & {
|
|
4
|
+
execute?: never;
|
|
5
|
+
};
|
|
6
|
+
type BareNoInputActivityFunction<TResult> = (() => Promise<TResult> | TResult) & {
|
|
7
|
+
execute?: never;
|
|
8
|
+
};
|
|
9
|
+
export type DurableActivityInput = string | ActivityCallable<void, unknown> | BareNoInputActivityFunction<unknown> | ActivityCallable<unknown, unknown> | BareActivityFunction<unknown, unknown>;
|
|
10
|
+
export interface DurableActivityInvocation {
|
|
11
|
+
activity: DurableActivityInput;
|
|
12
|
+
arguments: readonly unknown[];
|
|
13
|
+
callerStack: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DurableActivityScope {
|
|
16
|
+
dispatch<TResult>(invocation: DurableActivityInvocation): Promise<TResult>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Thrown when `durableActivity()` is called without an active inline `ctx.memo`
|
|
20
|
+
* durable-activity scope, or after that scope has already closed.
|
|
21
|
+
*
|
|
22
|
+
* The error usually means helper code escaped the memo callback boundary, ran
|
|
23
|
+
* under Worker execution mode, or started a helper activity without awaiting it
|
|
24
|
+
* before the memo callback returned.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { durableActivity, DurableActivityScopeError } from '@lostgradient/weft';
|
|
29
|
+
*
|
|
30
|
+
* async function sendOutsideWorkflow(): Promise<void> {
|
|
31
|
+
* try {
|
|
32
|
+
* await durableActivity('sendEmail', { userId: 'user-1' });
|
|
33
|
+
* } catch (error) {
|
|
34
|
+
* if (error instanceof DurableActivityScopeError) {
|
|
35
|
+
* console.error(error.message);
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* void sendOutsideWorkflow;
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare class DurableActivityScopeError extends WeftError<'DurableActivityScopeError'> {
|
|
44
|
+
constructor(message: string);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Thrown when helper-launched activity code reaches an activity feature that
|
|
48
|
+
* cannot safely be represented inside a plain async memo callback.
|
|
49
|
+
*
|
|
50
|
+
* The first unsupported feature is `ActivityContext.completeAsync()`: async
|
|
51
|
+
* activity completion must use `yield* ctx.run()` directly so the parked
|
|
52
|
+
* completion token stays attached to a normal workflow operation.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { durableActivity, DurableActivityUnsupportedError, workflow } from '@lostgradient/weft';
|
|
57
|
+
*
|
|
58
|
+
* export const completionWorkflow = workflow({ name: 'manual-completion' }).execute(async function* (
|
|
59
|
+
* ctx,
|
|
60
|
+
* ) {
|
|
61
|
+
* try {
|
|
62
|
+
* yield* ctx.memo('manual-completion-helper', async () =>
|
|
63
|
+
* durableActivity('manualCompletionActivity', undefined, {
|
|
64
|
+
* idempotencyKey: 'manual-completion:1',
|
|
65
|
+
* }),
|
|
66
|
+
* );
|
|
67
|
+
* } catch (error) {
|
|
68
|
+
* if (error instanceof DurableActivityUnsupportedError) {
|
|
69
|
+
* console.error(error.message);
|
|
70
|
+
* }
|
|
71
|
+
* }
|
|
72
|
+
* });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare class DurableActivityUnsupportedError extends WeftError<'DurableActivityUnsupportedError'> {
|
|
76
|
+
constructor(message: string);
|
|
77
|
+
}
|
|
78
|
+
export declare function runWithDurableActivityScope<TResult>(scope: DurableActivityScope, execute: () => TResult): TResult;
|
|
79
|
+
/**
|
|
80
|
+
* Run a durable activity from plain async helper code while an inline workflow
|
|
81
|
+
* is executing a `ctx.memo()` callback.
|
|
82
|
+
*
|
|
83
|
+
* `durableActivity()` gives shared async helpers the same activity dispatch
|
|
84
|
+
* machinery as `yield* ctx.run()` without converting the helper stack to
|
|
85
|
+
* generators. It is intentionally scoped: call it only from code awaited by one
|
|
86
|
+
* inline `ctx.memo()` callback, and await each helper activity sequentially
|
|
87
|
+
* before the callback returns. Activities with `idempotencyKey` get immediate
|
|
88
|
+
* lease-fenced result durability for the crash window between the activity
|
|
89
|
+
* returning and the memo checkpoint commit; unkeyed helper activities keep the
|
|
90
|
+
* normal at-least-once crash behavior.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* import { durableActivity, workflow } from '@lostgradient/weft';
|
|
95
|
+
*
|
|
96
|
+
* async function reserveInventory(input: { orderId: string }): Promise<{ reservationId: string }> {
|
|
97
|
+
* return { reservationId: input.orderId };
|
|
98
|
+
* }
|
|
99
|
+
*
|
|
100
|
+
* export const checkout = workflow({ name: 'checkout' }).execute(async function* (
|
|
101
|
+
* ctx,
|
|
102
|
+
* input: { orderId: string },
|
|
103
|
+
* ) {
|
|
104
|
+
* return yield* ctx.memo('reserve-inventory', async () =>
|
|
105
|
+
* durableActivity(reserveInventory, { orderId: input.orderId }, {
|
|
106
|
+
* idempotencyKey: `reserve:${input.orderId}`,
|
|
107
|
+
* }),
|
|
108
|
+
* );
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export declare function durableActivity<TResult = unknown>(name: string, input?: unknown, options?: ActivityCallOptions): Promise<TResult>;
|
|
113
|
+
export declare function durableActivity<TResult>(fn: ActivityCallable<void, TResult>, options?: ActivityCallOptions): Promise<TResult>;
|
|
114
|
+
export declare function durableActivity<TResult>(fn: BareNoInputActivityFunction<TResult>, options?: ActivityCallOptions): Promise<TResult>;
|
|
115
|
+
export declare function durableActivity<TInput, TResult>(fn: ActivityCallable<TInput, TResult>, input: TInput, options?: ActivityCallOptions): Promise<TResult>;
|
|
116
|
+
export declare function durableActivity<TInput, TResult>(fn: BareActivityFunction<TInput, TResult>, input: TInput, options?: ActivityCallOptions): Promise<TResult>;
|
|
117
|
+
export {};
|