@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.
Files changed (135) hide show
  1. package/README.md +7 -5
  2. package/dist/cli/generated/operation-client.generated.d.ts +1 -0
  3. package/dist/cli-main.js +63 -63
  4. package/dist/client/event-stream-options.d.ts +58 -0
  5. package/dist/client/event-stream-options.js +0 -0
  6. package/dist/client/event-stream.d.ts +27 -44
  7. package/dist/client/event-stream.js +21 -56
  8. package/dist/client/event-tail-lifecycle.d.ts +21 -0
  9. package/dist/client/event-tail-lifecycle.js +71 -0
  10. package/dist/client/event-tail.d.ts +3 -1
  11. package/dist/client/http-client.d.ts +4 -5
  12. package/dist/client/http-client.js +4 -1
  13. package/dist/client/http-handle.d.ts +4 -5
  14. package/dist/client/http-request.d.ts +7 -0
  15. package/dist/client/index.d.ts +2 -1
  16. package/dist/client/interface.d.ts +23 -7
  17. package/dist/client/local-handles.d.ts +2 -0
  18. package/dist/client/local-handles.js +10 -1
  19. package/dist/client/local.d.ts +3 -3
  20. package/dist/client/open-event-subscription.d.ts +3 -2
  21. package/dist/client/open-event-subscription.js +28 -1
  22. package/dist/client/sse-event-stream.d.ts +27 -0
  23. package/dist/client/sse-event-stream.js +276 -0
  24. package/dist/client/start-body.d.ts +2 -2
  25. package/dist/client/start-body.js +1 -0
  26. package/dist/core/context/activity-retry-state.d.ts +6 -5
  27. package/dist/core/context/activity-retry-state.js +31 -21
  28. package/dist/core/context/durable-activity.d.ts +117 -0
  29. package/dist/core/context/durable-activity.js +79 -0
  30. package/dist/core/context/operation-request.d.ts +15 -0
  31. package/dist/core/context/parallel-operations.js +1 -0
  32. package/dist/core/context/run-operation-cached-request.d.ts +8 -0
  33. package/dist/core/context/run-operation-cached-request.js +59 -0
  34. package/dist/core/context/run-operation.d.ts +23 -5
  35. package/dist/core/context/run-operation.js +62 -47
  36. package/dist/core/engine/activity-heartbeat-tracking.d.ts +8 -17
  37. package/dist/core/engine/activity-heartbeat-tracking.js +7 -1
  38. package/dist/core/engine/activity-reconciliation.d.ts +1 -0
  39. package/dist/core/engine/activity-reconciliation.js +7 -2
  40. package/dist/core/engine/anonymous-signal-sequence.js +6 -4
  41. package/dist/core/engine/async-activity-completion.d.ts +5 -7
  42. package/dist/core/engine/async-activity-completion.js +13 -9
  43. package/dist/core/engine/bulk-operations-purge.js +2 -1
  44. package/dist/core/engine/bulk-operations.js +8 -7
  45. package/dist/core/engine/callback-checkpoint-persistence.d.ts +3 -0
  46. package/dist/core/engine/callback-checkpoint-persistence.js +25 -0
  47. package/dist/core/engine/callback-creators-bundles.js +4 -1
  48. package/dist/core/engine/checkpoint-io.d.ts +4 -1
  49. package/dist/core/engine/checkpoint-io.js +19 -10
  50. package/dist/core/engine/completed-review-storage.d.ts +2 -1
  51. package/dist/core/engine/completed-review-storage.js +4 -3
  52. package/dist/core/engine/construction.js +3 -1
  53. package/dist/core/engine/engine-internal-types.d.ts +2 -0
  54. package/dist/core/engine/errors.d.ts +6 -6
  55. package/dist/core/engine/errors.js +1 -1
  56. package/dist/core/engine/handles.js +1 -1
  57. package/dist/core/engine/index.d.ts +37 -5
  58. package/dist/core/engine/index.js +7 -1
  59. package/dist/core/engine/internals.d.ts +2 -1
  60. package/dist/core/engine/lease-manager.js +2 -2
  61. package/dist/core/engine/lifecycle/recovered-services.js +8 -1
  62. package/dist/core/engine/lifecycle/start-or-signal-create.d.ts +2 -2
  63. package/dist/core/engine/lifecycle/start-or-signal-create.js +17 -15
  64. package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +1 -1
  65. package/dist/core/engine/lifecycle/start-or-signal-resolution.js +23 -5
  66. package/dist/core/engine/lifecycle/start-or-signal.d.ts +6 -4
  67. package/dist/core/engine/lifecycle/start-or-signal.js +21 -4
  68. package/dist/core/engine/memo-durable-activity.d.ts +11 -0
  69. package/dist/core/engine/memo-durable-activity.js +282 -0
  70. package/dist/core/engine/operations-activity.d.ts +5 -1
  71. package/dist/core/engine/operations-activity.js +19 -7
  72. package/dist/core/engine/operations-data.d.ts +4 -1
  73. package/dist/core/engine/operations-data.js +3 -3
  74. package/dist/core/engine/reviews.js +7 -4
  75. package/dist/core/engine/schedule-run.js +12 -3
  76. package/dist/core/engine/schedule-timer.js +2 -0
  77. package/dist/core/engine/storage-io.js +2 -2
  78. package/dist/core/engine/termination/complete.js +3 -3
  79. package/dist/core/json.js +1 -1
  80. package/dist/core/scheduler/scheduler-class.js +2 -1
  81. package/dist/core/start-workflow-validation.d.ts +3 -4
  82. package/dist/core/start-workflow-validation.js +1 -1
  83. package/dist/core/timeouts.d.ts +13 -1
  84. package/dist/core/timeouts.js +4 -1
  85. package/dist/core/types/constants.d.ts +16 -0
  86. package/dist/core/types/options.d.ts +60 -12
  87. package/dist/core/types/services-resolution.d.ts +49 -3
  88. package/dist/core/weft-error.d.ts +1 -1
  89. package/dist/core/weft-error.js +3 -0
  90. package/dist/index.d.ts +5 -4
  91. package/dist/index.js +7 -0
  92. package/dist/json-schema.js +1 -1
  93. package/dist/mcp/cli.js +26 -26
  94. package/dist/server/asyncapi-channels.js +82 -16
  95. package/dist/server/handler/auth-context-principal.d.ts +19 -0
  96. package/dist/server/handler/auth-context-principal.js +25 -0
  97. package/dist/server/handler/index.d.ts +2 -1
  98. package/dist/server/handler/index.js +35 -18
  99. package/dist/server/handler/route-dispatch.d.ts +12 -22
  100. package/dist/server/handler/route-dispatch.js +30 -36
  101. package/dist/server/handler/sse-route-dispatch.d.ts +25 -0
  102. package/dist/server/handler/sse-route-dispatch.js +45 -0
  103. package/dist/server/handler.js +28 -28
  104. package/dist/server/index.d.ts +8 -7
  105. package/dist/server/index.js +17 -17
  106. package/dist/server/operations/fleet-events-sse.d.ts +11 -0
  107. package/dist/server/operations/fleet-events-sse.js +164 -0
  108. package/dist/server/operations/sse-stream.d.ts +43 -0
  109. package/dist/server/operations/sse-stream.js +152 -7
  110. package/dist/server/operations/start-or-signal-workflow.d.ts +2 -0
  111. package/dist/server/operations/start-or-signal-workflow.js +25 -14
  112. package/dist/server/operations/start-workflow-options.d.ts +9 -6
  113. package/dist/server/operations/start-workflow-options.js +13 -0
  114. package/dist/server/operations/workflow-events-sse.d.ts +17 -0
  115. package/dist/server/operations/workflow-events-sse.js +200 -0
  116. package/dist/server/rest-bindings.js +12 -0
  117. package/dist/server/runtime/authentication-bridge.js +4 -0
  118. package/dist/server/runtime/cors.d.ts +1 -1
  119. package/dist/server/runtime/cors.js +6 -1
  120. package/dist/server/runtime/websocket-stream.d.ts +4 -0
  121. package/dist/server/runtime/websocket-stream.js +20 -3
  122. package/dist/server/runtime/websocket-worker.js +7 -2
  123. package/dist/server/serve-internals.d.ts +28 -0
  124. package/dist/server/serve-internals.js +4 -2
  125. package/dist/server/workflow-event-feed.d.ts +1 -0
  126. package/dist/server/workflow-event-feed.js +17 -6
  127. package/dist/service-worker/index.js +28 -28
  128. package/dist/service-worker/setup.d.ts +18 -1
  129. package/dist/service-worker/setup.js +7 -4
  130. package/dist/storage/typed-storage.d.ts +1 -1
  131. package/dist/storage/typed-storage.js +1 -1
  132. package/dist/testing/index.js +27 -27
  133. package/dist/version.d.ts +1 -1
  134. package/dist/version.js +1 -1
  135. package/package.json +1 -1
@@ -0,0 +1,11 @@
1
+ import type { FleetEventEnvelope } from '../fleet-event-feed.ts';
2
+ import type { UnknownRestBinding } from '../rest-bindings.ts';
3
+ type FleetEventsSseOutput = AsyncIterable<FleetEventEnvelope>;
4
+ export declare const fleetEventsSseOperation: import("../operation-catalog.ts").OperationDefinition<{
5
+ workflowId?: string | undefined;
6
+ kind?: "human-review:requested" | "human-review:completed" | "alert:fired" | "alert:resolved" | "activity:started" | "activity:completed" | "activity:async-pending" | "activity:failed" | "task:dead-lettered" | "attributes:changed" | "schedule:fired" | "schedule:missed-fire" | "signal:received" | "signal:delivered" | "checkpoint:size-warning" | "development:warning" | "cleanup:warning" | "storage:size-reported" | "worker:connected" | "worker:disconnected" | "constraint:violated" | "update:received" | "update:completed" | "workflow:started" | "workflow:completed" | "workflow:failed" | "workflow:cancelled" | "workflow:timed-out" | "workflow:resumed" | "workflow:suspended" | "workflow:teardown" | undefined;
7
+ fromCursor?: string | undefined;
8
+ lastEventId?: string | undefined;
9
+ }, FleetEventsSseOutput, FleetEventEnvelope>;
10
+ export declare const fleetEventsSseRestBinding: UnknownRestBinding;
11
+ export {};
@@ -0,0 +1,164 @@
1
+ import { z } from "zod";
2
+ import { defineOperation } from "../operation-registry.js";
3
+ import { EVENTS_READ_EVENT_TYPES } from "../runtime/client-visible-events.js";
4
+ import {
5
+ decodeCursor
6
+ } from "../workflow-event-feed.js";
7
+ import { invalidParamsFault } from "./operation-helpers.js";
8
+ import {
9
+ createEventEnvelopeSSEStream,
10
+ readServerSentEventsCursor,
11
+ requireServerSentEventsAccept,
12
+ shapeServerSentEventsFault,
13
+ SSE_RESPONSE_HEADERS
14
+ } from "./sse-stream.js";
15
+ const INITIAL_CURSOR = "-1", MAX_FLEET_SSE_REPLAY_EVENTS = 1000, cursorSchema = z.string().refine((cursor) => decodeCursor(cursor) !== null, {
16
+ message: "Invalid cursor"
17
+ }), fleetEventKindSchema = z.enum(EVENTS_READ_EVENT_TYPES), fleetEventsSseInputSchema = z.object({
18
+ workflowId: z.string().min(1).optional(),
19
+ kind: fleetEventKindSchema.optional(),
20
+ fromCursor: cursorSchema.optional(),
21
+ lastEventId: cursorSchema.optional()
22
+ }), fleetEventsSseOutputSchema = z.custom(isAsyncIterable, "Expected async iterable fleet event stream"), fleetEventEnvelopeSchema = z.object({
23
+ kind: z.string(),
24
+ workflowId: z.string().optional(),
25
+ sequence: z.number(),
26
+ cursor: z.string(),
27
+ emittedAtMs: z.number(),
28
+ payload: z.unknown()
29
+ });
30
+ export const fleetEventsSseOperation = defineOperation({
31
+ name: "weft.events.sse",
32
+ mcpExposable: !1,
33
+ kind: "stream",
34
+ summary: "Stream fleet events as Server-Sent Events",
35
+ description: "Streams the server-level event feed across workflows. Optional filters narrow delivery by workflow id or event kind after the single fleet cursor has been applied. The stream emits `ping` keepalives while idle.",
36
+ destructive: !1,
37
+ tags: ["Events"],
38
+ inputSchema: fleetEventsSseInputSchema,
39
+ outputSchema: fleetEventsSseOutputSchema,
40
+ eventSchema: fleetEventEnvelopeSchema,
41
+ access: {
42
+ kind: "scoped",
43
+ scopes: { kind: "anyOf", scopes: ["events:read"] }
44
+ },
45
+ producibleFaults: ["InvalidParams", "UnsupportedTransport"],
46
+ discoverable: !0,
47
+ transports: { http: !0, jsonRpcHttp: !1, jsonRpcWebSocket: !1, jsonRpcStdio: !1 },
48
+ unknownKeyPolicy: { http: "strip", jsonRpc: "reject" },
49
+ invoke: async ({ input, engine }) => {
50
+ return createFleetEventsIterable(input, fleetEventStreamOperationContext(engine));
51
+ }
52
+ });
53
+ function fleetEventStreamOperationContext(engine) {
54
+ if (typeof engine !== "object" || engine === null)
55
+ return {};
56
+ return engine;
57
+ }
58
+ function unsupportedEventStreamContextFault(message) {
59
+ return {
60
+ code: "UnsupportedTransport",
61
+ message,
62
+ data: { transport: "http-rest", supported: ["http-rest"] }
63
+ };
64
+ }
65
+ function isAsyncIterable(value) {
66
+ if (typeof value !== "object" || value === null)
67
+ return !1;
68
+ return typeof value[Symbol.asyncIterator] === "function";
69
+ }
70
+ function isClosableFleetEventsIterable(value) {
71
+ return "close" in value && typeof value.close === "function";
72
+ }
73
+ function isReplayAwareFleetEventsIterable(value) {
74
+ return isClosableFleetEventsIterable(value) && "replayComplete" in value && value.replayComplete instanceof Promise;
75
+ }
76
+ function matchesFleetEventFilter(envelope, input) {
77
+ if (input.workflowId !== void 0 && envelope.workflowId !== input.workflowId)
78
+ return !1;
79
+ if (input.kind !== void 0 && envelope.kind !== input.kind)
80
+ return !1;
81
+ return !0;
82
+ }
83
+ function createFleetEventsIterable(input, context) {
84
+ const feed = context.fleetEventFeed;
85
+ if (feed === void 0)
86
+ throw unsupportedEventStreamContextFault("fleet event SSE requires a fleet event feed");
87
+ const controller = new AbortController, replayComplete = Promise.withResolvers();
88
+ let closed = !1;
89
+ const close = async () => {
90
+ if (closed)
91
+ return;
92
+ closed = !0;
93
+ controller.abort();
94
+ }, subscribeOptions = {
95
+ fromCursor: input.lastEventId ?? input.fromCursor ?? INITIAL_CURSOR,
96
+ signal: controller.signal,
97
+ replayLimit: MAX_FLEET_SSE_REPLAY_EVENTS,
98
+ filterEnvelope: (envelope) => matchesFleetEventFilter(envelope, input),
99
+ onReplayComplete: () => replayComplete.resolve(),
100
+ createReplayLimitError: (count, limit) => invalidParamsFault(`Fleet event replay window is ${count} matching events; maximum is ${limit}. Supply a more recent fromCursor.`)
101
+ };
102
+ let source;
103
+ try {
104
+ source = feed.subscribe(subscribeOptions);
105
+ } catch (error) {
106
+ close();
107
+ throw error;
108
+ }
109
+ return {
110
+ async* [Symbol.asyncIterator]() {
111
+ try {
112
+ for await (const envelope of source)
113
+ yield envelope;
114
+ } finally {
115
+ await close();
116
+ }
117
+ },
118
+ replayComplete: replayComplete.promise,
119
+ close
120
+ };
121
+ }
122
+ function shapeFleetEventsSseSuccess(output, request) {
123
+ const close = isClosableFleetEventsIterable(output) ? () => output.close() : async () => {
124
+ return;
125
+ };
126
+ return new Response(createEventEnvelopeSSEStream({
127
+ iterable: output,
128
+ close,
129
+ ...isReplayAwareFleetEventsIterable(output) ? { ready: output.replayComplete } : {},
130
+ signal: request.signal
131
+ }), {
132
+ status: 200,
133
+ headers: SSE_RESPONSE_HEADERS
134
+ });
135
+ }
136
+ function shapeFleetEventsSseFault(fault) {
137
+ return shapeServerSentEventsFault(fault);
138
+ }
139
+ export const fleetEventsSseRestBinding = {
140
+ method: "GET",
141
+ path: "/v1/events/sse",
142
+ pathParamNames: [],
143
+ operationName: "weft.events.sse",
144
+ transportKind: "sse",
145
+ inputSources: {
146
+ workflowId: { kind: "query", queryParam: "workflowId" },
147
+ kind: { kind: "query", queryParam: "kind" },
148
+ fromCursor: { kind: "query", queryParam: "fromCursor" },
149
+ lastEventId: { kind: "header", headerName: "Last-Event-ID" }
150
+ },
151
+ extractInput: async (request) => {
152
+ requireServerSentEventsAccept(request);
153
+ const url = new URL(request.url), workflowId = url.searchParams.get("workflowId") ?? void 0, kind = url.searchParams.get("kind") ?? void 0, lastEventId = readServerSentEventsCursor(request.headers.get("Last-Event-ID")), fromCursor = lastEventId === void 0 ? readServerSentEventsCursor(url.searchParams.get("fromCursor")) : void 0;
154
+ return {
155
+ ...workflowId === void 0 ? {} : { workflowId },
156
+ ...kind === void 0 ? {} : { kind },
157
+ ...fromCursor === void 0 ? {} : { fromCursor },
158
+ ...lastEventId === void 0 ? {} : { lastEventId }
159
+ };
160
+ },
161
+ success: { kind: "streaming", mediaType: "text/event-stream" },
162
+ shapeSuccess: shapeFleetEventsSseSuccess,
163
+ shapeFault: shapeFleetEventsSseFault
164
+ };
@@ -1,4 +1,46 @@
1
1
  import type { StoredStreamChunk } from '../../core/context.ts';
2
+ import type { OperationFault } from '../operation-fault.ts';
3
+ import { type Cursor } from '../workflow-event-feed.ts';
4
+ export type ServerSentEventTimerScheduler = (callback: () => void, intervalMs: number) => () => void;
5
+ export type ServerSentEventHeartbeatOptions = {
6
+ readonly intervalMs: number;
7
+ readonly schedule?: ServerSentEventTimerScheduler;
8
+ readonly now?: () => number;
9
+ };
10
+ type ServerSentEventEnvelope = {
11
+ readonly kind: string;
12
+ readonly cursor: string;
13
+ readonly emittedAtMs: number;
14
+ };
15
+ type EventEnvelopeSSEStreamOptions<TEnvelope extends ServerSentEventEnvelope> = {
16
+ readonly iterable: AsyncIterable<TEnvelope>;
17
+ readonly close: () => Promise<void>;
18
+ readonly ready?: Promise<void>;
19
+ readonly heartbeat?: ServerSentEventHeartbeatOptions;
20
+ readonly signal?: AbortSignal;
21
+ };
22
+ export declare const SSE_ACCEPT_REQUIRED_MESSAGE = "Accept header must include text/event-stream";
23
+ export declare function acceptsServerSentEvents(acceptHeader: string | null): boolean;
24
+ export declare function requireServerSentEventsAccept(request: Request): void;
25
+ /**
26
+ * Validate an SSE reconnect cursor taken from a `Last-Event-ID` header or
27
+ * `fromCursor` query parameter. Returns `undefined` for an absent value and
28
+ * throws an `InvalidParams` fault for a malformed one. Shared by every SSE
29
+ * binding so cursor validation stays defined in exactly one place.
30
+ */
31
+ export declare function readServerSentEventsCursor(value: string | null): Cursor | undefined;
32
+ export declare function shapeServerSentEventsFault(fault: OperationFault): Response;
33
+ /** Format a single Server-Sent Events message. */
34
+ export declare function formatServerSentEvent(event: {
35
+ readonly id?: string;
36
+ readonly event?: string;
37
+ readonly data: string;
38
+ }): string;
39
+ /**
40
+ * Convert a replay-plus-live event envelope iterable into an SSE byte stream.
41
+ * Event envelopes advance the client cursor via `id`; heartbeat pings do not.
42
+ */
43
+ export declare function createEventEnvelopeSSEStream<TEnvelope extends ServerSentEventEnvelope>(options: EventEnvelopeSSEStreamOptions<TEnvelope>): ReadableStream<Uint8Array>;
2
44
  /**
3
45
  * Wrap a list of stored stream chunks as a Server-Sent Events stream. Each
4
46
  * chunk gets emitted as a `token` event keyed by `chunk.sequence`; chunks
@@ -11,3 +53,4 @@ import type { StoredStreamChunk } from '../../core/context.ts';
11
53
  export declare function createStoredChunkSSEStream(chunks: ReadonlyArray<StoredStreamChunk>, mapChunkToText: (chunk: StoredStreamChunk) => string | null): ReadableStream<Uint8Array>;
12
54
  /** Standard headers for an SSE response. */
13
55
  export declare const SSE_RESPONSE_HEADERS: Readonly<Record<string, string>>;
56
+ export {};
@@ -1,5 +1,39 @@
1
- const textEncoder = new TextEncoder;
2
- function formatSSE(event) {
1
+ import { decodeCursor } from "../workflow-event-feed.js";
2
+ import {
3
+ invalidParamsFault,
4
+ isOperationFault,
5
+ jsonErrorResponse,
6
+ shapeRestFault
7
+ } from "./operation-helpers.js";
8
+ const textEncoder = new TextEncoder, DEFAULT_SSE_HEARTBEAT_INTERVAL_MS = 15000;
9
+ export const SSE_ACCEPT_REQUIRED_MESSAGE = "Accept header must include text/event-stream";
10
+ export function acceptsServerSentEvents(acceptHeader) {
11
+ if (acceptHeader === null)
12
+ return !1;
13
+ for (const entry of acceptHeader.split(",")) {
14
+ const mediaType = entry.split(";", 1)[0]?.trim().toLowerCase();
15
+ if (mediaType === "text/event-stream" || mediaType === "text/*" || mediaType === "*/*")
16
+ return !0;
17
+ }
18
+ return !1;
19
+ }
20
+ export function requireServerSentEventsAccept(request) {
21
+ if (!acceptsServerSentEvents(request.headers.get("Accept")))
22
+ throw invalidParamsFault(SSE_ACCEPT_REQUIRED_MESSAGE);
23
+ }
24
+ export function readServerSentEventsCursor(value) {
25
+ if (value === null)
26
+ return;
27
+ if (decodeCursor(value) === null)
28
+ throw invalidParamsFault("Invalid cursor");
29
+ return value;
30
+ }
31
+ export function shapeServerSentEventsFault(fault) {
32
+ if (fault.code === "InvalidParams" && fault.message === SSE_ACCEPT_REQUIRED_MESSAGE)
33
+ return jsonErrorResponse(fault.message, 406);
34
+ return shapeRestFault(fault);
35
+ }
36
+ export function formatServerSentEvent(event) {
3
37
  const lines = [];
4
38
  if (event.id !== void 0)
5
39
  lines.push(`id: ${event.id}`);
@@ -14,6 +48,116 @@ function formatSSE(event) {
14
48
  `) + `
15
49
  `;
16
50
  }
51
+ function scheduleInterval(callback, intervalMs) {
52
+ const timer = setInterval(callback, intervalMs);
53
+ return () => {
54
+ clearInterval(timer);
55
+ };
56
+ }
57
+ function encodeFrame(frame) {
58
+ return textEncoder.encode(formatServerSentEvent(frame));
59
+ }
60
+ function sanitizedErrorFrame(error) {
61
+ if (isOperationFault(error) && error.code !== "EngineFailure")
62
+ return {
63
+ event: "error",
64
+ data: JSON.stringify({
65
+ code: error.code,
66
+ message: error.message,
67
+ data: error.data
68
+ })
69
+ };
70
+ return {
71
+ event: "error",
72
+ data: JSON.stringify({
73
+ code: "EngineFailure",
74
+ message: "Internal server error"
75
+ })
76
+ };
77
+ }
78
+ function heartbeatOptions(options) {
79
+ return {
80
+ intervalMs: options?.intervalMs ?? DEFAULT_SSE_HEARTBEAT_INTERVAL_MS,
81
+ schedule: options?.schedule ?? scheduleInterval,
82
+ now: options?.now ?? Date.now
83
+ };
84
+ }
85
+ export function createEventEnvelopeSSEStream(options) {
86
+ let cleanup = null;
87
+ return new ReadableStream({
88
+ start(controller) {
89
+ let closed = !1, clearHeartbeat = null;
90
+ const closeResources = async () => {
91
+ if (closed)
92
+ return;
93
+ closed = !0;
94
+ if (clearHeartbeat !== null) {
95
+ clearHeartbeat();
96
+ clearHeartbeat = null;
97
+ }
98
+ options.signal?.removeEventListener("abort", onAbort);
99
+ try {
100
+ await options.close();
101
+ } catch {}
102
+ };
103
+ cleanup = closeResources;
104
+ const enqueue = (frame) => {
105
+ if (closed)
106
+ return;
107
+ try {
108
+ controller.enqueue(encodeFrame(frame));
109
+ } catch {
110
+ closeResources();
111
+ }
112
+ }, onAbort = () => {
113
+ closeResources();
114
+ };
115
+ if (options.signal?.aborted) {
116
+ closeResources();
117
+ controller.close();
118
+ return;
119
+ }
120
+ options.signal?.addEventListener("abort", onAbort, { once: !0 });
121
+ const heartbeat = heartbeatOptions(options.heartbeat), enqueuePing = (data) => {
122
+ enqueue({
123
+ event: "ping",
124
+ data: JSON.stringify(data)
125
+ });
126
+ };
127
+ if (options.ready !== void 0)
128
+ options.ready.then(() => {
129
+ enqueuePing({ emittedAtMs: heartbeat.now(), replayComplete: !0 });
130
+ return;
131
+ }, () => {
132
+ return;
133
+ });
134
+ if (heartbeat.intervalMs > 0)
135
+ clearHeartbeat = heartbeat.schedule(() => {
136
+ enqueuePing({ emittedAtMs: heartbeat.now() });
137
+ }, heartbeat.intervalMs);
138
+ (async () => {
139
+ try {
140
+ for await (const envelope of options.iterable)
141
+ enqueue({
142
+ id: envelope.cursor,
143
+ event: envelope.kind,
144
+ data: JSON.stringify(envelope)
145
+ });
146
+ } catch (error) {
147
+ enqueue(sanitizedErrorFrame(error));
148
+ } finally {
149
+ await closeResources();
150
+ try {
151
+ controller.close();
152
+ } catch {}
153
+ }
154
+ })();
155
+ },
156
+ cancel() {
157
+ return cleanup?.();
158
+ }
159
+ });
160
+ }
17
161
  export function createStoredChunkSSEStream(chunks, mapChunkToText) {
18
162
  return new ReadableStream({
19
163
  start(controller) {
@@ -21,13 +165,13 @@ export function createStoredChunkSSEStream(chunks, mapChunkToText) {
21
165
  const text = mapChunkToText(chunk);
22
166
  if (text === null)
23
167
  continue;
24
- controller.enqueue(textEncoder.encode(formatSSE({
168
+ controller.enqueue(textEncoder.encode(formatServerSentEvent({
25
169
  id: String(chunk.sequence),
26
170
  event: "token",
27
171
  data: text
28
172
  })));
29
173
  }
30
- controller.enqueue(textEncoder.encode(formatSSE({
174
+ controller.enqueue(textEncoder.encode(formatServerSentEvent({
31
175
  event: "done",
32
176
  data: ""
33
177
  })));
@@ -36,7 +180,8 @@ export function createStoredChunkSSEStream(chunks, mapChunkToText) {
36
180
  });
37
181
  }
38
182
  export const SSE_RESPONSE_HEADERS = {
39
- "Content-Type": "text/event-stream",
40
- "Cache-Control": "no-cache",
41
- Connection: "keep-alive"
183
+ "Content-Type": "text/event-stream; charset=utf-8",
184
+ "Cache-Control": "no-cache, no-transform",
185
+ Connection: "keep-alive",
186
+ "X-Accel-Buffering": "no"
42
187
  };
@@ -13,6 +13,7 @@ declare const startOrSignalWorkflowInput: z.ZodObject<{
13
13
  tags: z.ZodOptional<z.ZodUnknown>;
14
14
  idempotencyKey: z.ZodOptional<z.ZodUnknown>;
15
15
  searchAttributes: z.ZodOptional<z.ZodUnknown>;
16
+ onTerminalConflict: z.ZodOptional<z.ZodUnknown>;
16
17
  }, z.core.$strip>;
17
18
  declare const startOrSignalWorkflowOutput: z.ZodObject<{
18
19
  id: z.ZodString;
@@ -36,6 +37,7 @@ export declare const startOrSignalWorkflowOperation: import("../operation-catalo
36
37
  tags?: unknown;
37
38
  idempotencyKey?: unknown;
38
39
  searchAttributes?: unknown;
40
+ onTerminalConflict?: unknown;
39
41
  }, {
40
42
  id: string;
41
43
  outcome: "started" | "signalled";
@@ -2,14 +2,15 @@ import { z } from "zod";
2
2
  import {
3
3
  IdempotencyKeyPurgedError,
4
4
  StartOrSignalConflictError,
5
- WorkflowNotRegisteredError
5
+ WorkflowNotRegisteredError,
6
+ WorkflowTeardownPendingError
6
7
  } from "../../core/engine/errors.js";
7
8
  import { runtimeWorkflowEngine } from "../../core/runtime-workflow-engine.js";
8
9
  import { isSignalIdWithinByteLimit } from "../../core/signal-id.js";
9
10
  import { StartWorkflowValidationError } from "../../core/start-workflow-validation.js";
10
11
  import { defineOperation } from "../operation-registry.js";
11
12
  import { invalidParamsFault, shapeRestFault } from "./operation-helpers.js";
12
- import { buildSharedStartWorkflowOptions } from "./start-workflow-options.js";
13
+ import { buildStartOrSignalWorkflowOptions } from "./start-workflow-options.js";
13
14
  import {
14
15
  extractSharedStartWorkflowRestFields,
15
16
  parseStartWorkflowRequestRecord
@@ -26,7 +27,8 @@ const startOrSignalWorkflowInput = z.object({
26
27
  startAfter: z.unknown().optional(),
27
28
  tags: z.unknown().optional(),
28
29
  idempotencyKey: z.unknown().optional(),
29
- searchAttributes: z.unknown().optional()
30
+ searchAttributes: z.unknown().optional(),
31
+ onTerminalConflict: z.unknown().optional()
30
32
  }), startOrSignalWorkflowOutput = z.object({
31
33
  id: z.string(),
32
34
  outcome: z.enum(["started", "signalled"])
@@ -36,12 +38,12 @@ function validateStartOrSignalWorkflowInput(input, lookupSearchAttributeSchema)
36
38
  throw invalidParamsFault("Missing required field: type");
37
39
  let options;
38
40
  try {
39
- options = buildSharedStartWorkflowOptions(input, lookupSearchAttributeSchema(input.type));
41
+ options = buildStartOrSignalWorkflowOptions(input, lookupSearchAttributeSchema(input.type));
40
42
  } catch (error) {
41
43
  const message = error instanceof Error ? error.message : String(error);
42
44
  throw invalidParamsFault(message);
43
45
  }
44
- assertConvergenceTokenProvided(input.signalId, options.idempotencyKey);
46
+ assertConvergenceTokenProvided(input.signalId, options);
45
47
  const signal = {
46
48
  name: input.signalName,
47
49
  payload: input.signalPayload,
@@ -49,22 +51,29 @@ function validateStartOrSignalWorkflowInput(input, lookupSearchAttributeSchema)
49
51
  };
50
52
  return { type: input.type, signal, options };
51
53
  }
52
- function assertConvergenceTokenProvided(signalId, idempotencyKey) {
53
- if (signalId === void 0 && idempotencyKey === void 0)
54
+ function assertConvergenceTokenProvided(signalId, options) {
55
+ if (options.onTerminalConflict === "start-new" && signalId === void 0)
56
+ throw invalidParamsFault("startOrSignal options.onTerminalConflict: 'start-new' requires signalId so concurrent restart-capable callers converge on one deterministic initial signal.");
57
+ if (signalId === void 0 && options.idempotencyKey === void 0)
54
58
  throw invalidParamsFault("startOrSignal requires either signalId or idempotencyKey to identify the signal to deliver. (Concurrent callers converge on one workflow and one signal only with a shared idempotencyKey, or a shared id plus signalId; a bare signalId starts a fresh run per caller.)");
55
- if (signalId !== void 0 && idempotencyKey !== void 0)
59
+ if (signalId !== void 0 && options.idempotencyKey !== void 0)
56
60
  throw invalidParamsFault("startOrSignal does not accept both signalId and idempotencyKey: the signal id derives from the idempotency key for convergence. Provide exactly one.");
57
61
  }
58
62
  function resolveStartOrSignalWorkflowFault(error) {
59
63
  const message = error instanceof Error ? error.message : String(error);
60
64
  if (error instanceof WorkflowNotRegisteredError)
61
65
  throw invalidParamsFault(message, error.code);
62
- if (error instanceof StartOrSignalConflictError || error instanceof IdempotencyKeyPurgedError)
66
+ if (error instanceof StartOrSignalConflictError || error instanceof IdempotencyKeyPurgedError || error instanceof WorkflowTeardownPendingError) {
67
+ const data = {
68
+ reason: message,
69
+ weftCode: error.code
70
+ };
63
71
  throw {
64
72
  code: "Conflict",
65
73
  message,
66
- data: { reason: message, weftCode: error.code }
74
+ data
67
75
  };
76
+ }
68
77
  if (error instanceof StartWorkflowValidationError)
69
78
  throw invalidParamsFault(message);
70
79
  throw {
@@ -77,8 +86,8 @@ export const startOrSignalWorkflowOperation = defineOperation({
77
86
  name: "weft.workflows.startorsignal",
78
87
  mcpExposable: !1,
79
88
  summary: "Atomically start a workflow or signal it if it already exists",
80
- description: 'Start a workflow or deliver a signal to it if it already exists (signal-with-start). An absent target is created and delivered the signal in one batch; a non-terminal target (running, pending, or suspended) is signalled; a terminal target faults with Conflict, as does a spent `idempotencyKey` whose run was purged or swept by retention. Requires `type`, `signalName`, and exactly one of `signalId` or `idempotencyKey` (not both). Accepts `input`, `signalPayload`, and the non-idempotency start options from weft.workflows.start (`id`, `executionTimeout`, `startAt`/`startAfter`, `tags`, `searchAttributes`); `idempotencyKey` is governed solely by the "exactly one of" rule above. Returns the workflow `id`. Concurrent callers converge on one workflow and one signal only with a shared `idempotencyKey`, or a shared `id` plus `signalId`; a bare `signalId` (no `id`/`idempotencyKey`) starts a fresh run per caller and does not converge.',
81
- destructive: !1,
89
+ description: 'Start a workflow or deliver a signal to it if it already exists (signal-with-start). An absent target is created and delivered the signal in one batch; a non-terminal target (running, pending, or suspended) is signalled; a terminal target faults with Conflict unless `onTerminalConflict: "start-new"` is supplied with `id` and `signalId`, in which case the prior terminal run is replaced through the shared terminal-replacement path and the initial signal is delivered to the fresh run. A spent `idempotencyKey` whose run was purged or swept by retention also faults with Conflict. Requires `type`, `signalName`, and exactly one of `signalId` or `idempotencyKey` (not both). Accepts `input`, `signalPayload`, and the non-idempotency start options from weft.workflows.start (`id`, `executionTimeout`, `startAt`/`startAfter`, `tags`, `searchAttributes`) plus `onTerminalConflict`; `idempotencyKey` is governed by the "exactly one of" rule above and cannot be combined with restart. Returns the workflow `id`. Concurrent callers converge on one workflow and one signal only with a shared `idempotencyKey`, or a shared `id` plus `signalId`; a bare `signalId` (no `id`/`idempotencyKey`) starts a fresh run per caller and does not converge.',
90
+ destructive: !0,
82
91
  tags: ["Workflows", "Signals"],
83
92
  inputSchema: startOrSignalWorkflowInput,
84
93
  outputSchema: startOrSignalWorkflowOutput,
@@ -114,7 +123,8 @@ export const startOrSignalWorkflowOperation = defineOperation({
114
123
  startAfter: { kind: "body-field", bodyField: "startAfter" },
115
124
  tags: { kind: "body-field", bodyField: "tags" },
116
125
  idempotencyKey: { kind: "body-field", bodyField: "idempotencyKey" },
117
- searchAttributes: { kind: "body-field", bodyField: "searchAttributes" }
126
+ searchAttributes: { kind: "body-field", bodyField: "searchAttributes" },
127
+ onTerminalConflict: { kind: "body-field", bodyField: "onTerminalConflict" }
118
128
  },
119
129
  extractInput: async (request, _pathParams, context) => {
120
130
  const record = await parseStartWorkflowRequestRecord(request, context);
@@ -122,7 +132,8 @@ export const startOrSignalWorkflowOperation = defineOperation({
122
132
  ...extractSharedStartWorkflowRestFields(record),
123
133
  signalName: record.signalName,
124
134
  signalPayload: record.signalPayload,
125
- signalId: record.signalId
135
+ signalId: record.signalId,
136
+ onTerminalConflict: record.onTerminalConflict
126
137
  };
127
138
  },
128
139
  success: { kind: "json", status: 201 },
@@ -1,4 +1,4 @@
1
- import type { SearchAttributeSchema, SearchAttributeValue, StartOptions } from '../../core/types.ts';
1
+ import type { SearchAttributeSchema, SearchAttributeValue, StartOptions, StartOrSignalOptions } from '../../core/types.ts';
2
2
  /**
3
3
  * The raw, transport-supplied start-option fields shared by `weft.workflows.start`
4
4
  * and `weft.workflows.startorsignal`. Each is `unknown` because validation happens
@@ -14,6 +14,9 @@ export type SharedStartWorkflowOptionInput = {
14
14
  idempotencyKey?: unknown;
15
15
  searchAttributes?: unknown;
16
16
  };
17
+ export type StartOrSignalWorkflowOptionInput = SharedStartWorkflowOptionInput & {
18
+ onTerminalConflict?: unknown;
19
+ };
17
20
  /**
18
21
  * Coerce the shared start-option fields into a validated {@link StartOptions}.
19
22
  * Both start operations call this so they cannot drift in how they validate `id`,
@@ -21,13 +24,13 @@ export type SharedStartWorkflowOptionInput = {
21
24
  * `searchAttributes` (a new field added here covers both surfaces at once). Throws
22
25
  * {@link StartWorkflowValidationError} on any malformed field.
23
26
  *
24
- * `onTerminalConflict` is intentionally NOT part of this shared path: it is an
25
- * in-process `engine.start`-only policy (its purge-and-restart would make the
26
- * transport `weft.workflows.start` operation conditionally destructive and would
27
- * violate `startOrSignal`'s at-most-once identity), so it is not exposed over
28
- * REST/JSON-RPC. See follow-up issue #489.
27
+ * `onTerminalConflict` is intentionally NOT part of this shared path:
28
+ * `weft.workflows.start` remains non-restart-capable over transport. The
29
+ * start-or-signal operation adds its narrower restart policy through
30
+ * {@link buildStartOrSignalWorkflowOptions}.
29
31
  */
30
32
  export declare function buildSharedStartWorkflowOptions(input: SharedStartWorkflowOptionInput, searchAttributeSchema: SearchAttributeSchema | undefined): StartOptions;
33
+ export declare function buildStartOrSignalWorkflowOptions(input: StartOrSignalWorkflowOptionInput, searchAttributeSchema: SearchAttributeSchema | undefined): StartOrSignalOptions;
31
34
  /**
32
35
  * Coerce a transport-supplied `searchAttributes` object into validated
33
36
  * {@link SearchAttributeValue}s. Shared by `weft.workflows.start` and
@@ -1,6 +1,7 @@
1
1
  import { validateAttributeType } from "../../core/search-attributes.js";
2
2
  import {
3
3
  assertExclusiveStartWorkflowOptions,
4
+ assertValidOnTerminalConflict,
4
5
  coerceStartWorkflowDuration,
5
6
  coerceStartWorkflowId,
6
7
  coerceStartWorkflowIdempotencyKey,
@@ -27,6 +28,18 @@ export function buildSharedStartWorkflowOptions(input, searchAttributeSchema) {
27
28
  assertExclusiveStartWorkflowOptions(options.startAt, options.startAfter);
28
29
  return options;
29
30
  }
31
+ export function buildStartOrSignalWorkflowOptions(input, searchAttributeSchema) {
32
+ const options = buildSharedStartWorkflowOptions(input, searchAttributeSchema);
33
+ if (input.onTerminalConflict !== void 0)
34
+ options.onTerminalConflict = coerceStartOrSignalOnTerminalConflict(input.onTerminalConflict, 'Field "onTerminalConflict"');
35
+ assertValidOnTerminalConflict(options);
36
+ return options;
37
+ }
38
+ function coerceStartOrSignalOnTerminalConflict(value, fieldName) {
39
+ if (value === "error" || value === "start-new")
40
+ return value;
41
+ throw new StartWorkflowValidationError(`${fieldName} must be "error" or "start-new"`);
42
+ }
30
43
  export function coerceStartWorkflowSearchAttributes(value, fieldName, schema) {
31
44
  if (typeof value !== "object" || value === null || Array.isArray(value))
32
45
  throw new StartWorkflowValidationError(`${fieldName} must be an object`);
@@ -0,0 +1,17 @@
1
+ import type { UnknownRestBinding } from '../rest-bindings.ts';
2
+ import type { WorkflowStreamConnectionLease } from '../runtime/websocket-stream.ts';
3
+ import { type EventEnvelope, type WorkflowEventFeed } from '../workflow-event-feed.ts';
4
+ export type WorkflowStreamConnectionAcquirer = (workflowId: string) => WorkflowStreamConnectionLease | null;
5
+ export type EventStreamOperationContext = {
6
+ readonly workflowEventFeed?: WorkflowEventFeed;
7
+ readonly acquireWorkflowStreamConnection?: WorkflowStreamConnectionAcquirer;
8
+ };
9
+ type WorkflowEventsSseOutput = AsyncIterable<EventEnvelope>;
10
+ export declare const workflowEventsSseOperation: import("../operation-catalog.ts").OperationDefinition<{
11
+ workflowId: string;
12
+ selector: "events" | "tokens";
13
+ fromCursor?: string | undefined;
14
+ lastEventId?: string | undefined;
15
+ }, WorkflowEventsSseOutput, EventEnvelope>;
16
+ export declare const workflowEventsSseRestBinding: UnknownRestBinding;
17
+ export {};