@lostgradient/weft 0.5.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.
Files changed (94) hide show
  1. package/README.md +4 -4
  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/engine/construction.d.ts +1 -0
  27. package/dist/core/engine/construction.js +3 -1
  28. package/dist/core/engine/engine-create-types.d.ts +14 -0
  29. package/dist/core/engine/engine-internal-types.d.ts +2 -0
  30. package/dist/core/engine/errors.d.ts +6 -6
  31. package/dist/core/engine/errors.js +1 -1
  32. package/dist/core/engine/handles.js +1 -1
  33. package/dist/core/engine/index.d.ts +6 -5
  34. package/dist/core/engine/index.js +3 -2
  35. package/dist/core/engine/lifecycle/recovered-services.js +8 -1
  36. package/dist/core/engine/lifecycle/start-or-signal-create.d.ts +2 -2
  37. package/dist/core/engine/lifecycle/start-or-signal-create.js +17 -15
  38. package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +1 -1
  39. package/dist/core/engine/lifecycle/start-or-signal-resolution.js +23 -5
  40. package/dist/core/engine/lifecycle/start-or-signal.d.ts +6 -4
  41. package/dist/core/engine/lifecycle/start-or-signal.js +21 -4
  42. package/dist/core/engine/schedule-run.js +12 -3
  43. package/dist/core/engine/storage-io.js +1 -1
  44. package/dist/core/engine/termination/complete.js +3 -3
  45. package/dist/core/scheduler/scheduler-class.js +2 -1
  46. package/dist/core/start-workflow-validation.d.ts +3 -4
  47. package/dist/core/start-workflow-validation.js +1 -1
  48. package/dist/core/timeouts.d.ts +13 -1
  49. package/dist/core/timeouts.js +4 -1
  50. package/dist/core/types/constants.d.ts +16 -0
  51. package/dist/core/types/options.d.ts +60 -12
  52. package/dist/core/types/services-resolution.d.ts +49 -3
  53. package/dist/core/weft-error.d.ts +1 -1
  54. package/dist/core/weft-error.js +1 -0
  55. package/dist/index.d.ts +3 -3
  56. package/dist/index.js +1 -0
  57. package/dist/json-schema.js +1 -1
  58. package/dist/mcp/cli.js +17 -17
  59. package/dist/server/asyncapi-channels.js +82 -16
  60. package/dist/server/handler/auth-context-principal.d.ts +19 -0
  61. package/dist/server/handler/auth-context-principal.js +25 -0
  62. package/dist/server/handler/index.d.ts +2 -1
  63. package/dist/server/handler/index.js +35 -18
  64. package/dist/server/handler/route-dispatch.d.ts +12 -22
  65. package/dist/server/handler/route-dispatch.js +30 -36
  66. package/dist/server/handler/sse-route-dispatch.d.ts +25 -0
  67. package/dist/server/handler/sse-route-dispatch.js +45 -0
  68. package/dist/server/handler.js +21 -21
  69. package/dist/server/index.d.ts +8 -7
  70. package/dist/server/index.js +15 -15
  71. package/dist/server/operations/fleet-events-sse.d.ts +11 -0
  72. package/dist/server/operations/fleet-events-sse.js +164 -0
  73. package/dist/server/operations/sse-stream.d.ts +43 -0
  74. package/dist/server/operations/sse-stream.js +152 -7
  75. package/dist/server/operations/start-or-signal-workflow.d.ts +2 -0
  76. package/dist/server/operations/start-or-signal-workflow.js +25 -14
  77. package/dist/server/operations/start-workflow-options.d.ts +9 -6
  78. package/dist/server/operations/start-workflow-options.js +13 -0
  79. package/dist/server/operations/workflow-events-sse.d.ts +17 -0
  80. package/dist/server/operations/workflow-events-sse.js +200 -0
  81. package/dist/server/rest-bindings.js +12 -0
  82. package/dist/server/runtime/authentication-bridge.js +4 -0
  83. package/dist/server/runtime/cors.d.ts +1 -1
  84. package/dist/server/runtime/cors.js +6 -1
  85. package/dist/server/runtime/websocket-stream.d.ts +4 -0
  86. package/dist/server/runtime/websocket-stream.js +20 -3
  87. package/dist/server/workflow-event-feed.d.ts +1 -0
  88. package/dist/server/workflow-event-feed.js +17 -6
  89. package/dist/service-worker/index.js +28 -28
  90. package/dist/storage/typed-storage.js +1 -1
  91. package/dist/testing/index.js +17 -17
  92. package/dist/version.d.ts +1 -1
  93. package/dist/version.js +1 -1
  94. package/package.json +1 -1
@@ -0,0 +1,58 @@
1
+ import type { WebSocketFactory } from './event-stream-transport.ts';
2
+ /**
3
+ * Transport preference for live workflow event subscriptions opened by
4
+ * {@link HttpClient}. `auto` keeps the WebSocket watch channel as the default
5
+ * and falls back to fetch-based SSE only when WebSocket construction cannot
6
+ * carry the required authentication headers.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { type WorkflowEventTransport } from '@lostgradient/weft';
11
+ *
12
+ * const transport: WorkflowEventTransport = 'sse';
13
+ * void transport;
14
+ * ```
15
+ */
16
+ export type WorkflowEventTransport = 'auto' | 'websocket' | 'sse';
17
+ /**
18
+ * Options for opening a live workflow event subscription from an
19
+ * {@link HttpClient} or {@link HttpHandle}. These options apply to
20
+ * `client.tail(id)`, `handle.tail()`, and handle event listeners.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { HttpClient, type WorkflowEventStreamOptions } from '@lostgradient/weft';
25
+ *
26
+ * const eventStreamOptions = {
27
+ * eventTransport: 'sse',
28
+ * } satisfies WorkflowEventStreamOptions;
29
+ *
30
+ * const client = new HttpClient({
31
+ * baseUrl: 'http://localhost:7233',
32
+ * eventTransport: eventStreamOptions.eventTransport,
33
+ * });
34
+ * void client;
35
+ * ```
36
+ */
37
+ export type WorkflowEventStreamOptions = {
38
+ /** Live event transport selection. Default `auto`. */
39
+ readonly eventTransport?: WorkflowEventTransport;
40
+ /** Maximum reconnect attempts after a dropped socket. Default 5. */
41
+ readonly maxReconnectAttempts?: number;
42
+ /** Base reconnect backoff in milliseconds. Default 50. */
43
+ readonly reconnectBackoffMs?: number;
44
+ /**
45
+ * Constructor override for the underlying socket. Tests inject a fake here;
46
+ * production omits it and the global `WebSocket` is used.
47
+ */
48
+ readonly webSocketFactory?: WebSocketFactory;
49
+ /**
50
+ * Buffer events for async iteration from construction rather than lazily on
51
+ * first iterator pull. `tail()` sets this so the documented
52
+ * `await tail.whenConnected(); for await (...)` pattern still sees the connect
53
+ * catch-up history (which is emitted before the `for await` loop begins).
54
+ * Callback-only subscribers (`HttpHandle.addEventListener`) leave it off so
55
+ * the iterator buffer never accumulates a never-drained queue. Default false.
56
+ */
57
+ readonly bufferForIteration?: boolean;
58
+ };
File without changes
@@ -2,27 +2,31 @@
2
2
  * Live workflow-event streaming for {@link HttpClient}.
3
3
  *
4
4
  * The server broadcasts a workflow's lifecycle events over a per-workflow
5
- * WebSocket channel at `/v1/workflows/:id/watch` (see the server's
6
- * `wireEventBroadcasting`). Each frame is a JSON {@link WorkflowEvent}
7
- * (`{ type, timestamp, data }`) the same shape `getEvents()` returns. This
8
- * module opens that channel and exposes the events through two surfaces: a push
9
- * callback (`onEvent`) used by {@link HttpHandle.addEventListener} so listeners
10
- * fire the moment an event lands instead of on a 2-second poll, and an
11
- * {@link AsyncIterable} used by `client.tail(id)` / `handle.tail()`.
5
+ * WebSocket channel at `/v1/workflows/:id/watch`, with fetch-based SSE at
6
+ * `/v1/workflows/:id/events/sse` for authenticated runtimes that cannot carry
7
+ * headers through WebSocket construction. Each delivered frame becomes a JSON
8
+ * {@link WorkflowEvent} (`{ type, timestamp, data }`) the same shape
9
+ * `getEvents()` returns. This module opens the selected channel and exposes the
10
+ * events through two surfaces: a push callback (`onEvent`) used by
11
+ * {@link HttpHandle.addEventListener} so listeners fire the moment an event
12
+ * lands instead of on a 2-second poll, and an {@link AsyncIterable} used by
13
+ * `client.tail(id)` / `handle.tail()`.
12
14
  *
13
- * **Catch-up + reconnect.** The watch channel is live-only and a dropped socket
14
- * can miss events while disconnected. To close both gaps the subscription
15
- * fetches the persisted event history (`getEvents`) on every (re)connect, emits
16
- * the events past a confirmed-contiguous history watermark, then drops any live
17
- * frame buffered during the fetch that the replayed history already covered (the
18
- * overlap window). Delivery is at-least-once: a failed fetch or a shorter
19
- * compaction-rebased history array may re-deliver a frame once rather than lose
20
- * it. The lone exception is a sequence-less-cursor edge under event-log
21
- * compaction a compacted+regrown log of unchanged length documented on
22
- * `#historyWatermark`; closing it needs a server-exposed event sequence.
23
- * Reconnect attempts back off and are capped; the cap is honored even for
24
- * open-then-close sockets, since the counter resets only after a catch-up proves
25
- * the connection healthy (`#catchUp`).
15
+ * **Catch-up + reconnect.** The WebSocket watch channel is live-only and a
16
+ * dropped socket can miss events while disconnected. To close both gaps the
17
+ * WebSocket subscription fetches the persisted event history (`getEvents`) on
18
+ * every (re)connect, emits the events past a confirmed-contiguous history
19
+ * watermark, then drops any live frame buffered during the fetch that the
20
+ * replayed history already covered (the overlap window). SSE uses the server's
21
+ * cursor-backed replay feed and reconnects with `Last-Event-ID`. Delivery is
22
+ * at-least-once: a failed fetch or a shorter compaction-rebased history array
23
+ * may re-deliver a frame once rather than lose it. The lone WebSocket exception
24
+ * is a sequence-less-cursor edge under event-log compaction — a
25
+ * compacted+regrown log of unchanged length documented on `#historyWatermark`;
26
+ * closing it needs a server-exposed event sequence. Reconnect attempts back off
27
+ * and are capped; for WebSocket the cap is honored even for open-then-close
28
+ * sockets, since the counter resets only after a catch-up proves the connection
29
+ * healthy (`#catchUp`).
26
30
  *
27
31
  * **Clean close.** `close()` closes the socket and resolves the iterable.
28
32
  * Terminal workflow events (`completed`, `failed`, `cancelled`, `timed-out`)
@@ -32,32 +36,11 @@
32
36
  * @module client/event-stream
33
37
  */
34
38
  import type { WorkflowEvent } from '../core/types.ts';
35
- import { type WebSocketFactory } from './event-stream-transport.ts';
36
- /** Reason a {@link WorkflowEventSubscription} terminated. */
37
- export type StreamCloseReason = 'workflow-terminal' | 'client-closed' | 'reconnect-exhausted';
39
+ import type { WorkflowEventStreamOptions } from './event-stream-options.ts';
40
+ import { type StreamCloseReason } from './event-tail-lifecycle.ts';
41
+ export type { StreamCloseReason } from './event-tail-lifecycle.ts';
38
42
  /** Fetches a workflow's persisted event history for connect/reconnect catch-up. */
39
43
  export type EventHistoryFetcher = (workflowId: string) => Promise<WorkflowEvent[]>;
40
- /** Options for opening a workflow event subscription. */
41
- export type WorkflowEventStreamOptions = {
42
- /** Maximum reconnect attempts after a dropped socket. Default 5. */
43
- readonly maxReconnectAttempts?: number;
44
- /** Base reconnect backoff in milliseconds. Default 50. */
45
- readonly reconnectBackoffMs?: number;
46
- /**
47
- * Constructor override for the underlying socket. Tests inject a fake here;
48
- * production omits it and the global `WebSocket` is used.
49
- */
50
- readonly webSocketFactory?: WebSocketFactory;
51
- /**
52
- * Buffer events for async iteration from construction rather than lazily on
53
- * first iterator pull. `tail()` sets this so the documented
54
- * `await tail.whenConnected(); for await (…)` pattern still sees the connect
55
- * catch-up history (which is emitted before the `for await` loop begins).
56
- * Callback-only subscribers (`HttpHandle.addEventListener`) leave it off so
57
- * the iterator buffer never accumulates a never-drained queue. Default false.
58
- */
59
- readonly bufferForIteration?: boolean;
60
- };
61
44
  /**
62
45
  * A live workflow-event subscription over the `/watch` WebSocket channel.
63
46
  * Delivers events to a push callback and to a single async iterator, catching
@@ -4,6 +4,7 @@ import {
4
4
  dropOverlappingLiveFrames,
5
5
  parseWatchFrame
6
6
  } from "./event-stream-transport.js";
7
+ import { WorkflowEventTailLifecycle } from "./event-tail-lifecycle.js";
7
8
  const DEFAULT_MAX_RECONNECT_ATTEMPTS = 5, DEFAULT_RECONNECT_BACKOFF_MS = 50;
8
9
 
9
10
  export class WorkflowEventSubscription {
@@ -15,18 +16,14 @@ export class WorkflowEventSubscription {
15
16
  #maxReconnectAttempts;
16
17
  #reconnectBackoffMs;
17
18
  #onEvent;
19
+ #lifecycle;
18
20
  #socket = null;
19
- #closed = !1;
20
21
  #historyWatermark = 0;
21
22
  #reconnectAttempts = 0;
22
23
  #reconnectTimer = null;
23
24
  #catchUpInFlight = !1;
24
25
  #pendingLive = [];
25
26
  #connectGeneration = 0;
26
- #buffer = [];
27
- #waker = null;
28
- #iterating = !1;
29
- #closeReason = null;
30
27
  #connected = Promise.withResolvers();
31
28
  #connectedSettled = !1;
32
29
  constructor(url, headers, workflowId, fetchHistory, onEvent, options) {
@@ -38,11 +35,14 @@ export class WorkflowEventSubscription {
38
35
  this.#factory = options?.webSocketFactory ?? defaultWebSocketFactory;
39
36
  this.#maxReconnectAttempts = options?.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
40
37
  this.#reconnectBackoffMs = options?.reconnectBackoffMs ?? DEFAULT_RECONNECT_BACKOFF_MS;
41
- this.#iterating = options?.bufferForIteration ?? !1;
38
+ this.#lifecycle = new WorkflowEventTailLifecycle({
39
+ bufferForIteration: options?.bufferForIteration ?? !1,
40
+ onClose: (reason) => this.#onLifecycleClose(reason)
41
+ });
42
42
  this.#connect();
43
43
  }
44
44
  get closeReason() {
45
- return this.#closeReason;
45
+ return this.#lifecycle.closeReason;
46
46
  }
47
47
  whenConnected() {
48
48
  return this.#connected.promise;
@@ -54,7 +54,7 @@ export class WorkflowEventSubscription {
54
54
  this.#connected.resolve();
55
55
  }
56
56
  #connect() {
57
- if (this.#closed)
57
+ if (this.#lifecycle.closed)
58
58
  return;
59
59
  let socket;
60
60
  try {
@@ -67,7 +67,7 @@ export class WorkflowEventSubscription {
67
67
  }
68
68
  this.#socket = socket;
69
69
  socket.addEventListener("open", () => {
70
- if (this.#closed || this.#socket !== socket)
70
+ if (this.#lifecycle.closed || this.#socket !== socket)
71
71
  return;
72
72
  this.#connectGeneration += 1;
73
73
  this.#catchUp();
@@ -86,7 +86,7 @@ export class WorkflowEventSubscription {
86
86
  socket.addEventListener("error", () => {});
87
87
  }
88
88
  async#catchUp() {
89
- if (this.#catchUpInFlight || this.#closed)
89
+ if (this.#catchUpInFlight || this.#lifecycle.closed)
90
90
  return;
91
91
  this.#catchUpInFlight = !0;
92
92
  const generation = this.#connectGeneration;
@@ -95,7 +95,7 @@ export class WorkflowEventSubscription {
95
95
  succeeded = await this.#reconcileHistory(generation);
96
96
  } finally {
97
97
  this.#catchUpInFlight = !1;
98
- if (!this.#closed && generation !== this.#connectGeneration)
98
+ if (!this.#lifecycle.closed && generation !== this.#connectGeneration)
99
99
  this.#catchUp();
100
100
  else {
101
101
  if (succeeded && this.#socket !== null)
@@ -112,7 +112,7 @@ export class WorkflowEventSubscription {
112
112
  fetchSucceeded = !1;
113
113
  history = [];
114
114
  }
115
- if (this.#closed)
115
+ if (this.#lifecycle.closed)
116
116
  return !1;
117
117
  if (generation !== this.#connectGeneration)
118
118
  return !1;
@@ -135,26 +135,20 @@ export class WorkflowEventSubscription {
135
135
  this.#emit(event);
136
136
  }
137
137
  #emit(event) {
138
- if (this.#closed)
138
+ if (this.#lifecycle.closed)
139
139
  return;
140
- try {
141
- this.#onEvent(event);
142
- } catch {}
143
- if (this.#iterating) {
144
- this.#buffer.push(event);
145
- this.#wake();
146
- }
140
+ this.#lifecycle.emit(event, this.#onEvent);
147
141
  if (WORKFLOW_TERMINAL_EVENT_TYPES.has(event.type))
148
142
  this.#terminate("workflow-terminal");
149
143
  }
150
144
  #handleSocketDrop() {
151
145
  this.#socket = null;
152
- if (this.#closed)
146
+ if (this.#lifecycle.closed)
153
147
  return;
154
148
  this.#scheduleReconnect();
155
149
  }
156
150
  #scheduleReconnect() {
157
- if (this.#closed)
151
+ if (this.#lifecycle.closed)
158
152
  return;
159
153
  if (this.#reconnectAttempts >= this.#maxReconnectAttempts) {
160
154
  this.#terminate("reconnect-exhausted");
@@ -168,10 +162,9 @@ export class WorkflowEventSubscription {
168
162
  }, delay);
169
163
  }
170
164
  #terminate(reason) {
171
- if (this.#closed)
172
- return;
173
- this.#closed = !0;
174
- this.#closeReason = reason;
165
+ this.#lifecycle.terminate(reason);
166
+ }
167
+ #onLifecycleClose(_reason) {
175
168
  this.#markConnected();
176
169
  if (this.#reconnectTimer !== null) {
177
170
  clearTimeout(this.#reconnectTimer);
@@ -183,39 +176,11 @@ export class WorkflowEventSubscription {
183
176
  try {
184
177
  socket.close();
185
178
  } catch {}
186
- this.#wake();
187
- }
188
- #wake() {
189
- const waker = this.#waker;
190
- if (waker !== null) {
191
- this.#waker = null;
192
- waker();
193
- }
194
179
  }
195
180
  close() {
196
- this.#terminate("client-closed");
181
+ this.#lifecycle.close();
197
182
  }
198
183
  [Symbol.asyncIterator]() {
199
- this.#iterating = !0;
200
- return this.#iterate();
201
- }
202
- async* #iterate() {
203
- try {
204
- while (!0) {
205
- while (this.#buffer.length > 0)
206
- yield this.#buffer.shift();
207
- if (this.#closed)
208
- return;
209
- await this.#waitForEvent();
210
- }
211
- } finally {
212
- this.#iterating = !1;
213
- this.close();
214
- }
215
- }
216
- #waitForEvent() {
217
- const { promise, resolve } = Promise.withResolvers();
218
- this.#waker = resolve;
219
- return promise;
184
+ return this.#lifecycle[Symbol.asyncIterator]();
220
185
  }
221
186
  }
@@ -0,0 +1,21 @@
1
+ import type { WorkflowEvent } from '../core/types.ts';
2
+ /** Reason a workflow event tail terminated. */
3
+ export type StreamCloseReason = 'workflow-terminal' | 'client-closed' | 'reconnect-exhausted' | 'server-error';
4
+ export type WorkflowEventTailLifecycleOptions = {
5
+ readonly bufferForIteration?: boolean;
6
+ readonly onClose: (reason: StreamCloseReason) => void;
7
+ };
8
+ /**
9
+ * Shared async-iterator lifecycle for client event tails. Transports own
10
+ * connection mechanics; this owns buffering, close state, and wake-up handling.
11
+ */
12
+ export declare class WorkflowEventTailLifecycle implements AsyncIterable<WorkflowEvent> {
13
+ #private;
14
+ constructor(options: WorkflowEventTailLifecycleOptions);
15
+ get closed(): boolean;
16
+ get closeReason(): StreamCloseReason | null;
17
+ emit(event: WorkflowEvent, onEvent: (event: WorkflowEvent) => void): boolean;
18
+ terminate(reason: StreamCloseReason): void;
19
+ close(): void;
20
+ [Symbol.asyncIterator](): AsyncIterator<WorkflowEvent>;
21
+ }
@@ -0,0 +1,71 @@
1
+ export class WorkflowEventTailLifecycle {
2
+ #buffer = [];
3
+ #onClose;
4
+ #closeReason = null;
5
+ #closed = !1;
6
+ #iterating = !1;
7
+ #waker = null;
8
+ constructor(options) {
9
+ this.#onClose = options.onClose;
10
+ this.#iterating = options.bufferForIteration ?? !1;
11
+ }
12
+ get closed() {
13
+ return this.#closed;
14
+ }
15
+ get closeReason() {
16
+ return this.#closeReason;
17
+ }
18
+ emit(event, onEvent) {
19
+ if (this.#closed)
20
+ return !1;
21
+ try {
22
+ onEvent(event);
23
+ } catch {}
24
+ if (this.#iterating) {
25
+ this.#buffer.push(event);
26
+ this.#wake();
27
+ }
28
+ return !0;
29
+ }
30
+ terminate(reason) {
31
+ if (this.#closed)
32
+ return;
33
+ this.#closed = !0;
34
+ this.#closeReason = reason;
35
+ this.#onClose(reason);
36
+ this.#wake();
37
+ }
38
+ close() {
39
+ this.terminate("client-closed");
40
+ }
41
+ [Symbol.asyncIterator]() {
42
+ this.#iterating = !0;
43
+ return this.#iterate();
44
+ }
45
+ async* #iterate() {
46
+ try {
47
+ while (!0) {
48
+ while (this.#buffer.length > 0)
49
+ yield this.#buffer.shift();
50
+ if (this.#closed)
51
+ return;
52
+ await this.#waitForEvent();
53
+ }
54
+ } finally {
55
+ this.#iterating = !1;
56
+ this.close();
57
+ }
58
+ }
59
+ #waitForEvent() {
60
+ const { promise, resolve } = Promise.withResolvers();
61
+ this.#waker = resolve;
62
+ return promise;
63
+ }
64
+ #wake() {
65
+ const waker = this.#waker;
66
+ if (waker !== null) {
67
+ this.#waker = null;
68
+ waker();
69
+ }
70
+ }
71
+ }
@@ -44,7 +44,9 @@ export interface WorkflowEventTail extends AsyncIterable<WorkflowEvent> {
44
44
  * the workflow's persisted history (server mode after the `/watch` socket's
45
45
  * `getEvents` catch-up; library mode after the in-process `engine.getEvents`
46
46
  * replay), so awaiting it may perform async work and a `for await` started
47
- * afterward still sees the replayed history ahead of any live frame.
47
+ * afterward still sees the replayed history ahead of any live frame. Server
48
+ * mode uses the `/watch` socket's `getEvents` catch-up for WebSocket tails
49
+ * and the replay-complete `ping` frame for SSE tails.
48
50
  */
49
51
  whenConnected(): Promise<void>;
50
52
  }
@@ -1,10 +1,9 @@
1
1
  import { type CatalogOperationName, type CatalogOperationTypes, type WeftClient as CatalogOperations } from '../cli/generated/operation-client.generated.ts';
2
2
  import type { StoredStreamChunk } from '../core/context.ts';
3
3
  import type { AttributeFilterKey, BulkCancelResult, BulkDeleteResult, BulkRetryFailedResult, BulkSignalResult, BulkTagResult, CoordinatedUpdateResult, ForkOptions, ListFilter, PaginatedResult, PurgeResult, QueryDefinition, RetentionOverview, ReviewListEntry, ReviewListFilter, ScheduleFilter, ScheduleOptions, ScheduleSpec, ScheduleSummary, SearchAttributeValue, SignalDefinition, SignalDeliveryOptions, StartOrSignalSignal, SubmitReviewOptions, TypedListFilter, UpdateDefinition, WorkflowEvent, WorkflowInput, WorkflowOutput, WorkflowRegistry, WorkflowReplay, WorkflowState, WorkflowSummary, WorkflowTimelineEntry } from '../core/types.ts';
4
- import { type WorkflowEventSubscription } from './event-stream.ts';
5
4
  import type { WorkflowEventTail } from './event-tail.ts';
6
5
  import { type HttpClientOptions } from './http-request.ts';
7
- import type { ClientHandle, ClientScheduleHandle, ClientStartOptions, UpdateResult, WeftClient, WeftClientActivity } from './interface.ts';
6
+ import type { ClientHandle, ClientScheduleHandle, ClientStartOptions, ClientStartOrSignalOptions, UpdateResult, WeftClient, WeftClientActivity } from './interface.ts';
8
7
  import type { KnownWorkflowName, UnknownNameWhenRegistryEmpty } from './workflow-name-typing.ts';
9
8
  /**
10
9
  * Remote Weft client backed by HTTP requests.
@@ -61,8 +60,8 @@ export declare class HttpClient implements WeftClient {
61
60
  call<Name extends CatalogOperationName>(name: Name, input: CatalogOperationTypes[Name]['input']): Promise<CatalogOperationTypes[Name]['output']>;
62
61
  start<TName extends KnownWorkflowName>(type: TName, input: WorkflowInput<WorkflowRegistry, TName>, options?: ClientStartOptions): Promise<ClientHandle<WorkflowOutput<WorkflowRegistry, TName>>>;
63
62
  start<TName extends string>(type: UnknownNameWhenRegistryEmpty<TName>, input: unknown, options?: ClientStartOptions): Promise<ClientHandle>;
64
- startOrSignal<TName extends KnownWorkflowName>(type: TName, input: WorkflowInput<WorkflowRegistry, TName>, signal: StartOrSignalSignal, options?: ClientStartOptions): Promise<ClientHandle<WorkflowOutput<WorkflowRegistry, TName>>>;
65
- startOrSignal<TName extends string>(type: UnknownNameWhenRegistryEmpty<TName>, input: unknown, signal: StartOrSignalSignal, options?: ClientStartOptions): Promise<ClientHandle>;
63
+ startOrSignal<TName extends KnownWorkflowName>(type: TName, input: WorkflowInput<WorkflowRegistry, TName>, signal: StartOrSignalSignal, options?: ClientStartOrSignalOptions): Promise<ClientHandle<WorkflowOutput<WorkflowRegistry, TName>>>;
64
+ startOrSignal<TName extends string>(type: UnknownNameWhenRegistryEmpty<TName>, input: unknown, signal: StartOrSignalSignal, options?: ClientStartOrSignalOptions): Promise<ClientHandle>;
66
65
  schedule<TName extends KnownWorkflowName>(type: TName, input: WorkflowInput<WorkflowRegistry, TName>, spec: string | ScheduleSpec, options?: ScheduleOptions): Promise<ClientScheduleHandle>;
67
66
  schedule<TName extends string>(type: UnknownNameWhenRegistryEmpty<TName>, input: unknown, spec: string | ScheduleSpec, options?: ScheduleOptions): Promise<ClientScheduleHandle>;
68
67
  get(id: string): Promise<WorkflowState | null>;
@@ -110,7 +109,7 @@ export declare class HttpClient implements WeftClient {
110
109
  * path; {@link tail} sets it so the connect catch-up is buffered for the
111
110
  * async iterator instead of dropped.
112
111
  */
113
- openEventSubscription(id: string, onEvent: (event: WorkflowEvent) => void, bufferForIteration?: boolean): WorkflowEventSubscription;
112
+ openEventSubscription(id: string, onEvent: (event: WorkflowEvent) => void, bufferForIteration?: boolean): WorkflowEventTail;
114
113
  tail(id: string): WorkflowEventTail;
115
114
  getTimeline(id: string): Promise<WorkflowTimelineEntry[]>;
116
115
  replayTo(id: string, step: number): Promise<WorkflowReplay | null>;
@@ -62,7 +62,10 @@ export class HttpClient {
62
62
  complete: (token, result) => completeAsyncActivityRequest(this, token, result),
63
63
  completeExceptionally: (token, error) => failAsyncActivityRequest(this, token, error)
64
64
  };
65
- this.#streamOptions = options.webSocketFactory === void 0 ? {} : { webSocketFactory: options.webSocketFactory };
65
+ this.#streamOptions = {
66
+ ...options.eventTransport === void 0 ? {} : { eventTransport: options.eventTransport },
67
+ ...options.webSocketFactory === void 0 ? {} : { webSocketFactory: options.webSocketFactory }
68
+ };
66
69
  }
67
70
  call(name, input) {
68
71
  return this.operations[name](input);
@@ -3,11 +3,10 @@ import { WorkflowHandleDelegation } from './handle-delegation.ts';
3
3
  import type { HttpClient } from './http-client.ts';
4
4
  /**
5
5
  * Server-mode workflow handle. Lifecycle events are delivered push-based over
6
- * the server's per-workflow `/v1/workflows/:id/watch` WebSocket channel rather
7
- * than by polling `getEvents()` on a timer — listeners fire the moment an event
8
- * lands on the server. Each delivered event is re-dispatched as a `CustomEvent`
9
- * whose `detail` is the event's `data`, matching the long-standing handle
10
- * contract.
6
+ * the server's per-workflow WebSocket or SSE event stream rather than by
7
+ * polling `getEvents()` on a timer — listeners fire the moment an event lands
8
+ * on the server. Each delivered event is re-dispatched as a `CustomEvent` whose
9
+ * `detail` is the event's `data`, matching the long-standing handle contract.
11
10
  */
12
11
  export declare class HttpHandle extends WorkflowHandleDelegation<HttpClient> {
13
12
  #private;
@@ -1,6 +1,7 @@
1
1
  import { type FaultCode } from '../core/fault-code.ts';
2
2
  import type { FailureCategory } from '../core/types/identity.ts';
3
3
  import { WeftError, type WeftErrorCode } from '../core/weft-error.ts';
4
+ import type { WorkflowEventTransport } from './event-stream-options.ts';
4
5
  import type { WebSocketFactory } from './event-stream-transport.ts';
5
6
  /**
6
7
  * Configuration for the HTTP client.
@@ -32,6 +33,12 @@ export interface HttpClientOptions {
32
33
  token?: string;
33
34
  /** Optional headers to include on every request (e.g. auth tokens). */
34
35
  headers?: Record<string, string>;
36
+ /**
37
+ * Live workflow-event transport. `auto` prefers WebSocket and falls back to
38
+ * fetch-based SSE when the initial WebSocket constructor cannot carry the
39
+ * configured headers. Defaults to `auto`.
40
+ */
41
+ eventTransport?: WorkflowEventTransport;
35
42
  /**
36
43
  * Override the WebSocket constructor used for live event streaming
37
44
  * (`tail()` / push-based `handle.addEventListener`). Production omits this and
@@ -7,9 +7,10 @@
7
7
  *
8
8
  * @module client/index
9
9
  */
10
+ export type { WorkflowEventStreamOptions, WorkflowEventTransport } from './event-stream-options.ts';
10
11
  export type { WorkflowEventTail } from './event-tail.ts';
11
12
  export { HttpClient } from './http-client.ts';
12
13
  export { HttpClientError } from './http-request.ts';
13
14
  export type { HttpClientOptions } from './http-request.ts';
14
- export type { ClientHandle, ClientScheduleHandle, ClientStartOptions, StartOrSignalOutcome, UpdateResult, WeftClient, WeftClientActivity, } from './interface.ts';
15
+ export type { ClientHandle, ClientScheduleHandle, ClientStartOptions, ClientStartOrSignalOptions, StartOrSignalOutcome, UpdateResult, WeftClient, WeftClientActivity, } from './interface.ts';
15
16
  export type { KnownWorkflowName, UnknownNameWhenRegistryEmpty } from './workflow-name-typing.ts';
@@ -9,7 +9,7 @@ import type { CatalogOperationName, CatalogOperationTypes, WeftClient as Catalog
9
9
  import type { StoredStreamChunk } from '../core/context.ts';
10
10
  import type { StartOrSignalOutcome as EngineStartOrSignalOutcome } from '../core/engine/handles.ts';
11
11
  import type { TypedEventTarget, WeftEventMap } from '../core/events.ts';
12
- import type { AttributeFilterKey, BulkCancelResult, BulkDeleteResult, BulkRetryFailedResult, BulkSignalResult, BulkTagResult, CoordinatedUpdateResult, ForkOptions, ListFilter, PaginatedResult, PurgeResult, QueryDefinition, RetentionOverview, ReviewListEntry, ReviewListFilter, ScheduleFilter, ScheduleOptions, ScheduleSpec, ScheduleSummary, SearchAttributeValue, SignalDefinition, SignalDeliveryOptions, StartOptions, StartOrSignalSignal, SubmitReviewOptions, TypedListFilter, UpdateDefinition, WorkflowEvent, WorkflowInput, WorkflowOutput, WorkflowRegistry, WorkflowReplay, WorkflowState, WorkflowSummary, WorkflowTimelineEntry } from '../core/types.ts';
12
+ import type { AttributeFilterKey, BulkCancelResult, BulkDeleteResult, BulkRetryFailedResult, BulkSignalResult, BulkTagResult, CoordinatedUpdateResult, ForkOptions, ListFilter, PaginatedResult, PurgeResult, QueryDefinition, RetentionOverview, ReviewListEntry, ReviewListFilter, ScheduleFilter, ScheduleOptions, ScheduleSpec, ScheduleSummary, SearchAttributeValue, SignalDefinition, SignalDeliveryOptions, StartOptions, StartOrSignalOptions, StartOrSignalSignal, SubmitReviewOptions, TypedListFilter, UpdateDefinition, WorkflowEvent, WorkflowInput, WorkflowOutput, WorkflowRegistry, WorkflowReplay, WorkflowState, WorkflowSummary, WorkflowTimelineEntry } from '../core/types.ts';
13
13
  import type { WorkflowEventTail } from './event-tail.ts';
14
14
  import type { KnownWorkflowName, UnknownNameWhenRegistryEmpty } from './workflow-name-typing.ts';
15
15
  /**
@@ -32,6 +32,19 @@ export type ClientStartOptions = Omit<StartOptions, 'defer' | 'services'> & {
32
32
  readonly defer?: never;
33
33
  readonly services?: never;
34
34
  };
35
+ /**
36
+ * Remote-capable start-or-signal options, including terminal restart policy.
37
+ * @example
38
+ * ```ts
39
+ * import type { ClientStartOrSignalOptions } from '@lostgradient/weft/client';
40
+ * const options: ClientStartOrSignalOptions = {
41
+ * id: 'github:installations:42:sync',
42
+ * onTerminalConflict: 'start-new',
43
+ * };
44
+ * void options;
45
+ * ```
46
+ */
47
+ export type ClientStartOrSignalOptions = ClientStartOptions & Pick<StartOrSignalOptions, 'onTerminalConflict'>;
35
48
  /**
36
49
  * Which atomic path a {@link WeftClient.startOrSignal} call took, returned
37
50
  * alongside the {@link ClientHandle}. `'started'` when the call created the
@@ -260,7 +273,9 @@ export interface WeftClient {
260
273
  * Atomically start a workflow or signal it if it already exists
261
274
  * (signal-with-start). An absent target is created and delivered the signal in
262
275
  * one batch; a non-terminal target (running, pending, or suspended) is
263
- * signalled; a terminal target is rejected as a conflict.
276
+ * signalled; a terminal target is rejected as a conflict unless
277
+ * `options.onTerminalConflict: 'start-new'` is supplied with an explicit
278
+ * workflow id and deterministic `signal.signalId`.
264
279
  *
265
280
  * The rejection shape is transport-dependent: `LocalClient` throws the typed
266
281
  * `StartOrSignalConflictError` (and `IdempotencyKeyPurgedError` for a spent
@@ -276,10 +291,11 @@ export interface WeftClient {
276
291
  * atomic start-with-one-signal that does NOT converge concurrent callers (each
277
292
  * gets its own run). Supply exactly one of `signal.signalId` or
278
293
  * `options.idempotencyKey`; `options.id` and `options.idempotencyKey` are
279
- * mutually exclusive.
294
+ * mutually exclusive. `options.onTerminalConflict: 'start-new'` is also
295
+ * mutually exclusive with `options.idempotencyKey`.
280
296
  */
281
- startOrSignal<TName extends KnownWorkflowName>(type: TName, input: WorkflowInput<WorkflowRegistry, TName>, signal: StartOrSignalSignal, options?: ClientStartOptions): Promise<ClientHandle<WorkflowOutput<WorkflowRegistry, TName>>>;
282
- startOrSignal<TName extends string>(type: UnknownNameWhenRegistryEmpty<TName>, input: unknown, signal: StartOrSignalSignal, options?: ClientStartOptions): Promise<ClientHandle>;
297
+ startOrSignal<TName extends KnownWorkflowName>(type: TName, input: WorkflowInput<WorkflowRegistry, TName>, signal: StartOrSignalSignal, options?: ClientStartOrSignalOptions): Promise<ClientHandle<WorkflowOutput<WorkflowRegistry, TName>>>;
298
+ startOrSignal<TName extends string>(type: UnknownNameWhenRegistryEmpty<TName>, input: unknown, signal: StartOrSignalSignal, options?: ClientStartOrSignalOptions): Promise<ClientHandle>;
283
299
  /**
284
300
  * Register a recurring schedule (cron string or interval spec) and return a
285
301
  * handle to it.
@@ -368,8 +384,8 @@ export interface WeftClient {
368
384
  /**
369
385
  * Open a live, push-based tail of a workflow's events. Async-iterate the
370
386
  * returned {@link WorkflowEventTail} to consume events as they happen. In
371
- * server mode this rides the per-workflow `/v1/workflows/:id/watch` WebSocket
372
- * channel (replacing the old 2-second poll); in library mode it bridges the
387
+ * server mode this rides the configured per-workflow WebSocket or SSE event
388
+ * stream (replacing the old 2-second poll); in library mode it bridges the
373
389
  * engine's event stream directly. Both transports deliver the same
374
390
  * {@link WorkflowEvent} records and terminate cleanly on completion or close.
375
391
  */
@@ -18,5 +18,7 @@ export declare class LocalHandle extends WorkflowHandleDelegation<LocalClient> {
18
18
  [Symbol.dispose](): void;
19
19
  }
20
20
  export declare class LocalScheduleHandle extends ScheduleHandleDelegation<LocalClient> {
21
+ #private;
22
+ constructor(id: string, client: LocalClient);
21
23
  [Symbol.dispose](): void;
22
24
  }
@@ -19,5 +19,14 @@ export class LocalHandle extends WorkflowHandleDelegation {
19
19
  }
20
20
 
21
21
  export class LocalScheduleHandle extends ScheduleHandleDelegation {
22
- [Symbol.dispose]() {}
22
+ #disposed;
23
+ constructor(id, client) {
24
+ super(id, client);
25
+ this.#disposed = !1;
26
+ }
27
+ [Symbol.dispose]() {
28
+ if (this.#disposed)
29
+ return;
30
+ this.#disposed = !0;
31
+ }
23
32
  }
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { type CatalogOperationName, type CatalogOperationTypes, type WeftClient as CatalogOperations } from '../cli/generated/operation-client.generated.ts';
11
11
  import type { Engine } from '../core/engine.ts';
12
- import type { AttributeFilterKey, BulkCancelResult, BulkDeleteResult, BulkRetryFailedResult, BulkSignalResult, BulkTagResult, CoordinatedUpdateResult, DefaultActivityTypes, DefaultWorkflowRegistry, ForkOptions, ListFilter, PaginatedResult, PurgeResult, QueryDefinition, RetentionOverview, ReviewListEntry, ReviewListFilter, ScheduleFilter, ScheduleOptions, ScheduleSpec, ScheduleSummary, SearchAttributeValue, SignalDefinition, SignalDeliveryOptions, StartOptions, StartOrSignalSignal, SubmitReviewOptions, TypedListFilter, UpdateDefinition, WorkflowEvent, WorkflowInput, WorkflowOutput, WorkflowRegistry, WorkflowReplay, WorkflowState, WorkflowSummary, WorkflowTimelineEntry } from '../core/types.ts';
12
+ import type { AttributeFilterKey, BulkCancelResult, BulkDeleteResult, BulkRetryFailedResult, BulkSignalResult, BulkTagResult, CoordinatedUpdateResult, DefaultActivityTypes, DefaultWorkflowRegistry, ForkOptions, ListFilter, PaginatedResult, PurgeResult, QueryDefinition, RetentionOverview, ReviewListEntry, ReviewListFilter, ScheduleFilter, ScheduleOptions, ScheduleSpec, ScheduleSummary, SearchAttributeValue, SignalDefinition, SignalDeliveryOptions, StartOptions, StartOrSignalOptions, StartOrSignalSignal, SubmitReviewOptions, TypedListFilter, UpdateDefinition, WorkflowEvent, WorkflowInput, WorkflowOutput, WorkflowRegistry, WorkflowReplay, WorkflowState, WorkflowSummary, WorkflowTimelineEntry } from '../core/types.ts';
13
13
  import type { WorkflowEventTail } from './event-tail.ts';
14
14
  import type { ClientHandle, ClientScheduleHandle, UpdateResult, WeftClient, WeftClientActivity } from './interface.ts';
15
15
  import type { KnownWorkflowName, UnknownNameWhenRegistryEmpty } from './workflow-name-typing.ts';
@@ -57,8 +57,8 @@ export declare class LocalClient<TWorkflows extends object = DefaultWorkflowRegi
57
57
  call<Name extends CatalogOperationName>(name: Name, input: CatalogOperationTypes[Name]['input']): Promise<CatalogOperationTypes[Name]['output']>;
58
58
  start<TName extends KnownWorkflowName>(type: TName, input: WorkflowInput<WorkflowRegistry, TName>, options?: StartOptions): Promise<ClientHandle<WorkflowOutput<WorkflowRegistry, TName>>>;
59
59
  start<TName extends string>(type: UnknownNameWhenRegistryEmpty<TName>, input: unknown, options?: StartOptions): Promise<ClientHandle>;
60
- startOrSignal<TName extends KnownWorkflowName>(type: TName, input: WorkflowInput<WorkflowRegistry, TName>, signal: StartOrSignalSignal, options?: StartOptions): Promise<ClientHandle<WorkflowOutput<WorkflowRegistry, TName>>>;
61
- startOrSignal<TName extends string>(type: UnknownNameWhenRegistryEmpty<TName>, input: unknown, signal: StartOrSignalSignal, options?: StartOptions): Promise<ClientHandle>;
60
+ startOrSignal<TName extends KnownWorkflowName>(type: TName, input: WorkflowInput<WorkflowRegistry, TName>, signal: StartOrSignalSignal, options?: StartOrSignalOptions): Promise<ClientHandle<WorkflowOutput<WorkflowRegistry, TName>>>;
61
+ startOrSignal<TName extends string>(type: UnknownNameWhenRegistryEmpty<TName>, input: unknown, signal: StartOrSignalSignal, options?: StartOrSignalOptions): Promise<ClientHandle>;
62
62
  schedule<TName extends KnownWorkflowName>(type: TName, input: WorkflowInput<WorkflowRegistry, TName>, spec: string | ScheduleSpec, options?: ScheduleOptions): Promise<ClientScheduleHandle>;
63
63
  schedule<TName extends string>(type: UnknownNameWhenRegistryEmpty<TName>, input: unknown, spec: string | ScheduleSpec, options?: ScheduleOptions): Promise<ClientScheduleHandle>;
64
64
  get(id: string): Promise<WorkflowState | null>;