@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.
- package/README.md +4 -4
- package/dist/cli/generated/operation-client.generated.d.ts +1 -0
- package/dist/cli-main.js +63 -63
- package/dist/client/event-stream-options.d.ts +58 -0
- package/dist/client/event-stream-options.js +0 -0
- package/dist/client/event-stream.d.ts +27 -44
- package/dist/client/event-stream.js +21 -56
- package/dist/client/event-tail-lifecycle.d.ts +21 -0
- package/dist/client/event-tail-lifecycle.js +71 -0
- package/dist/client/event-tail.d.ts +3 -1
- package/dist/client/http-client.d.ts +4 -5
- package/dist/client/http-client.js +4 -1
- package/dist/client/http-handle.d.ts +4 -5
- package/dist/client/http-request.d.ts +7 -0
- package/dist/client/index.d.ts +2 -1
- package/dist/client/interface.d.ts +23 -7
- package/dist/client/local-handles.d.ts +2 -0
- package/dist/client/local-handles.js +10 -1
- package/dist/client/local.d.ts +3 -3
- package/dist/client/open-event-subscription.d.ts +3 -2
- package/dist/client/open-event-subscription.js +28 -1
- package/dist/client/sse-event-stream.d.ts +27 -0
- package/dist/client/sse-event-stream.js +276 -0
- package/dist/client/start-body.d.ts +2 -2
- package/dist/client/start-body.js +1 -0
- package/dist/core/engine/construction.d.ts +1 -0
- package/dist/core/engine/construction.js +3 -1
- package/dist/core/engine/engine-create-types.d.ts +14 -0
- package/dist/core/engine/engine-internal-types.d.ts +2 -0
- package/dist/core/engine/errors.d.ts +6 -6
- package/dist/core/engine/errors.js +1 -1
- package/dist/core/engine/handles.js +1 -1
- package/dist/core/engine/index.d.ts +6 -5
- package/dist/core/engine/index.js +3 -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/schedule-run.js +12 -3
- package/dist/core/engine/storage-io.js +1 -1
- package/dist/core/engine/termination/complete.js +3 -3
- package/dist/core/scheduler/scheduler-class.js +2 -1
- package/dist/core/start-workflow-validation.d.ts +3 -4
- package/dist/core/start-workflow-validation.js +1 -1
- package/dist/core/timeouts.d.ts +13 -1
- package/dist/core/timeouts.js +4 -1
- package/dist/core/types/constants.d.ts +16 -0
- package/dist/core/types/options.d.ts +60 -12
- package/dist/core/types/services-resolution.d.ts +49 -3
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -0
- package/dist/json-schema.js +1 -1
- package/dist/mcp/cli.js +17 -17
- package/dist/server/asyncapi-channels.js +82 -16
- package/dist/server/handler/auth-context-principal.d.ts +19 -0
- package/dist/server/handler/auth-context-principal.js +25 -0
- package/dist/server/handler/index.d.ts +2 -1
- package/dist/server/handler/index.js +35 -18
- package/dist/server/handler/route-dispatch.d.ts +12 -22
- package/dist/server/handler/route-dispatch.js +30 -36
- package/dist/server/handler/sse-route-dispatch.d.ts +25 -0
- package/dist/server/handler/sse-route-dispatch.js +45 -0
- package/dist/server/handler.js +21 -21
- package/dist/server/index.d.ts +8 -7
- package/dist/server/index.js +15 -15
- package/dist/server/operations/fleet-events-sse.d.ts +11 -0
- package/dist/server/operations/fleet-events-sse.js +164 -0
- package/dist/server/operations/sse-stream.d.ts +43 -0
- package/dist/server/operations/sse-stream.js +152 -7
- package/dist/server/operations/start-or-signal-workflow.d.ts +2 -0
- package/dist/server/operations/start-or-signal-workflow.js +25 -14
- package/dist/server/operations/start-workflow-options.d.ts +9 -6
- package/dist/server/operations/start-workflow-options.js +13 -0
- package/dist/server/operations/workflow-events-sse.d.ts +17 -0
- package/dist/server/operations/workflow-events-sse.js +200 -0
- package/dist/server/rest-bindings.js +12 -0
- package/dist/server/runtime/authentication-bridge.js +4 -0
- package/dist/server/runtime/cors.d.ts +1 -1
- package/dist/server/runtime/cors.js +6 -1
- package/dist/server/runtime/websocket-stream.d.ts +4 -0
- package/dist/server/runtime/websocket-stream.js +20 -3
- package/dist/server/workflow-event-feed.d.ts +1 -0
- package/dist/server/workflow-event-feed.js +17 -6
- package/dist/service-worker/index.js +28 -28
- package/dist/storage/typed-storage.js +1 -1
- package/dist/testing/index.js +17 -17
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineOperation } from "../operation-registry.js";
|
|
3
|
+
import { isAuthenticated } from "../principal.js";
|
|
4
|
+
import {
|
|
5
|
+
decodeCursor
|
|
6
|
+
} from "../workflow-event-feed.js";
|
|
7
|
+
import { invalidParamsFault, jsonErrorResponse } 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_WORKFLOW_SSE_REPLAY_EVENTS = 1000, MAXIMUM_WORKFLOW_STREAMS_MESSAGE = "maximum stream connections per workflow exceeded", cursorSchema = z.string().refine((cursor) => decodeCursor(cursor) !== null, {
|
|
16
|
+
message: "Invalid cursor"
|
|
17
|
+
}), workflowEventsSseInputSchema = z.object({
|
|
18
|
+
workflowId: z.string().min(1),
|
|
19
|
+
selector: z.enum(["events", "tokens"]).default("events"),
|
|
20
|
+
fromCursor: cursorSchema.optional(),
|
|
21
|
+
lastEventId: cursorSchema.optional()
|
|
22
|
+
}), workflowEventsSseOutputSchema = z.custom(isAsyncIterable, "Expected async iterable workflow event stream"), workflowEventEnvelopeSchema = z.object({
|
|
23
|
+
kind: z.string(),
|
|
24
|
+
workflowId: z.string(),
|
|
25
|
+
selector: z.enum(["events", "tokens"]),
|
|
26
|
+
sequence: z.number(),
|
|
27
|
+
cursor: z.string(),
|
|
28
|
+
emittedAtMs: z.number(),
|
|
29
|
+
payload: z.unknown()
|
|
30
|
+
});
|
|
31
|
+
export const workflowEventsSseOperation = defineOperation({
|
|
32
|
+
name: "weft.workflows.events.sse",
|
|
33
|
+
mcpExposable: !1,
|
|
34
|
+
kind: "stream",
|
|
35
|
+
summary: "Stream workflow events as Server-Sent Events",
|
|
36
|
+
description: '`selector: "events"` requires `events:read`; `selector: "tokens"` requires `streams:read`. The stream replays from `fromCursor` or `Last-Event-ID`, then remains open for live events and `ping` keepalives.',
|
|
37
|
+
destructive: !1,
|
|
38
|
+
tags: ["Events"],
|
|
39
|
+
inputSchema: workflowEventsSseInputSchema,
|
|
40
|
+
outputSchema: workflowEventsSseOutputSchema,
|
|
41
|
+
eventSchema: workflowEventEnvelopeSchema,
|
|
42
|
+
parameterizedAccess: {
|
|
43
|
+
discriminator: "selector",
|
|
44
|
+
defaultValue: "events",
|
|
45
|
+
variants: [
|
|
46
|
+
{
|
|
47
|
+
value: "events",
|
|
48
|
+
access: { kind: "scoped", scopes: { kind: "anyOf", scopes: ["events:read"] } }
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
value: "tokens",
|
|
52
|
+
access: { kind: "scoped", scopes: { kind: "anyOf", scopes: ["streams:read"] } }
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
producibleFaults: ["InvalidParams", "UnsupportedTransport"],
|
|
57
|
+
access: { kind: "authenticated" },
|
|
58
|
+
authorize: async ({ input, principal }) => {
|
|
59
|
+
const requiredScope = workflowSseScope(input.selector);
|
|
60
|
+
if (!isAuthenticated(principal))
|
|
61
|
+
return { allowed: !1, classification: "unauthorized", reason: "authentication required" };
|
|
62
|
+
if (principal.hasScope(requiredScope))
|
|
63
|
+
return { allowed: !0 };
|
|
64
|
+
return {
|
|
65
|
+
allowed: !1,
|
|
66
|
+
classification: "forbidden",
|
|
67
|
+
reason: `requires scope: ${requiredScope}`
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
discoverable: !0,
|
|
71
|
+
transports: { http: !0, jsonRpcHttp: !1, jsonRpcWebSocket: !1, jsonRpcStdio: !1 },
|
|
72
|
+
unknownKeyPolicy: { http: "strip", jsonRpc: "reject" },
|
|
73
|
+
invoke: async ({ input, engine }) => {
|
|
74
|
+
return createWorkflowEventsIterable(input, eventStreamOperationContext(engine));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
function workflowSseScope(selector) {
|
|
78
|
+
return selector === "tokens" ? "streams:read" : "events:read";
|
|
79
|
+
}
|
|
80
|
+
function eventStreamOperationContext(engine) {
|
|
81
|
+
if (typeof engine !== "object" || engine === null)
|
|
82
|
+
return {};
|
|
83
|
+
return engine;
|
|
84
|
+
}
|
|
85
|
+
function unsupportedEventStreamContextFault(message) {
|
|
86
|
+
return {
|
|
87
|
+
code: "UnsupportedTransport",
|
|
88
|
+
message,
|
|
89
|
+
data: { transport: "http-rest", supported: ["http-rest"] }
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function maximumWorkflowStreamsFault() {
|
|
93
|
+
return invalidParamsFault(MAXIMUM_WORKFLOW_STREAMS_MESSAGE);
|
|
94
|
+
}
|
|
95
|
+
function isAsyncIterable(value) {
|
|
96
|
+
if (typeof value !== "object" || value === null)
|
|
97
|
+
return !1;
|
|
98
|
+
return typeof value[Symbol.asyncIterator] === "function";
|
|
99
|
+
}
|
|
100
|
+
function isClosableWorkflowEventsIterable(value) {
|
|
101
|
+
return "close" in value && typeof value.close === "function";
|
|
102
|
+
}
|
|
103
|
+
function isReplayAwareWorkflowEventsIterable(value) {
|
|
104
|
+
return isClosableWorkflowEventsIterable(value) && "replayComplete" in value && value.replayComplete instanceof Promise;
|
|
105
|
+
}
|
|
106
|
+
function createWorkflowEventsIterable(input, context) {
|
|
107
|
+
const feed = context.workflowEventFeed;
|
|
108
|
+
if (feed === void 0)
|
|
109
|
+
throw unsupportedEventStreamContextFault("workflow event SSE requires a workflow event feed");
|
|
110
|
+
const lease = context.acquireWorkflowStreamConnection?.(input.workflowId);
|
|
111
|
+
if (lease === null)
|
|
112
|
+
throw maximumWorkflowStreamsFault();
|
|
113
|
+
const controller = new AbortController, replayComplete = Promise.withResolvers();
|
|
114
|
+
let closed = !1;
|
|
115
|
+
const close = async () => {
|
|
116
|
+
if (closed)
|
|
117
|
+
return;
|
|
118
|
+
closed = !0;
|
|
119
|
+
controller.abort();
|
|
120
|
+
lease?.release();
|
|
121
|
+
}, fromCursor = input.lastEventId ?? input.fromCursor ?? INITIAL_CURSOR, subscribeOptions = {
|
|
122
|
+
workflowId: input.workflowId,
|
|
123
|
+
selector: input.selector,
|
|
124
|
+
fromCursor,
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
replayLimit: MAX_WORKFLOW_SSE_REPLAY_EVENTS,
|
|
127
|
+
onReplayComplete: () => replayComplete.resolve(),
|
|
128
|
+
createReplayLimitError: (count, limit) => invalidParamsFault(`Workflow event replay window is ${count} events; maximum is ${limit}. Supply a more recent fromCursor.`)
|
|
129
|
+
};
|
|
130
|
+
let source;
|
|
131
|
+
try {
|
|
132
|
+
source = feed.subscribe(subscribeOptions);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
close();
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
async* [Symbol.asyncIterator]() {
|
|
139
|
+
try {
|
|
140
|
+
for await (const envelope of source)
|
|
141
|
+
yield envelope;
|
|
142
|
+
} finally {
|
|
143
|
+
await close();
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
replayComplete: replayComplete.promise,
|
|
147
|
+
close
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function shapeWorkflowEventsSseSuccess(output, request) {
|
|
151
|
+
const close = isClosableWorkflowEventsIterable(output) ? () => output.close() : async () => {
|
|
152
|
+
return;
|
|
153
|
+
};
|
|
154
|
+
return new Response(createEventEnvelopeSSEStream({
|
|
155
|
+
iterable: output,
|
|
156
|
+
close,
|
|
157
|
+
...isReplayAwareWorkflowEventsIterable(output) ? { ready: output.replayComplete } : {},
|
|
158
|
+
signal: request.signal
|
|
159
|
+
}), {
|
|
160
|
+
status: 200,
|
|
161
|
+
headers: SSE_RESPONSE_HEADERS
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function shapeWorkflowEventsSseFault(fault) {
|
|
165
|
+
if (fault.code === "InvalidParams" && fault.message === MAXIMUM_WORKFLOW_STREAMS_MESSAGE)
|
|
166
|
+
return jsonErrorResponse(fault.message, 429);
|
|
167
|
+
return shapeServerSentEventsFault(fault);
|
|
168
|
+
}
|
|
169
|
+
function readSelector(request) {
|
|
170
|
+
const value = new URL(request.url).searchParams.get("selector");
|
|
171
|
+
if (value === null)
|
|
172
|
+
return;
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
export const workflowEventsSseRestBinding = {
|
|
176
|
+
method: "GET",
|
|
177
|
+
path: "/v1/workflows/:id/events/sse",
|
|
178
|
+
pathParamNames: ["id"],
|
|
179
|
+
operationName: "weft.workflows.events.sse",
|
|
180
|
+
transportKind: "sse",
|
|
181
|
+
inputSources: {
|
|
182
|
+
workflowId: { kind: "path", pathParam: "id" },
|
|
183
|
+
selector: { kind: "query", queryParam: "selector" },
|
|
184
|
+
fromCursor: { kind: "query", queryParam: "fromCursor" },
|
|
185
|
+
lastEventId: { kind: "header", headerName: "Last-Event-ID" }
|
|
186
|
+
},
|
|
187
|
+
extractInput: async (request, pathParams) => {
|
|
188
|
+
requireServerSentEventsAccept(request);
|
|
189
|
+
const url = new URL(request.url), lastEventId = readServerSentEventsCursor(request.headers.get("Last-Event-ID")), fromCursor = lastEventId === void 0 ? readServerSentEventsCursor(url.searchParams.get("fromCursor")) : void 0, selector = readSelector(request);
|
|
190
|
+
return {
|
|
191
|
+
workflowId: pathParams.id ?? "",
|
|
192
|
+
...selector === void 0 ? {} : { selector },
|
|
193
|
+
...fromCursor === void 0 ? {} : { fromCursor },
|
|
194
|
+
...lastEventId === void 0 ? {} : { lastEventId }
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
success: { kind: "streaming", mediaType: "text/event-stream" },
|
|
198
|
+
shapeSuccess: shapeWorkflowEventsSseSuccess,
|
|
199
|
+
shapeFault: shapeWorkflowEventsSseFault
|
|
200
|
+
};
|
|
@@ -45,6 +45,10 @@ import {
|
|
|
45
45
|
createScheduleOperation,
|
|
46
46
|
createScheduleRestBinding
|
|
47
47
|
} from "./operations/create-schedule.js";
|
|
48
|
+
import {
|
|
49
|
+
fleetEventsSseOperation,
|
|
50
|
+
fleetEventsSseRestBinding
|
|
51
|
+
} from "./operations/fleet-events-sse.js";
|
|
48
52
|
import { fleetEventsSubscriptionOperation } from "./operations/fleet-events-subscription.js";
|
|
49
53
|
import { forkWorkflowOperation, forkWorkflowRestBinding } from "./operations/fork-workflow.js";
|
|
50
54
|
import {
|
|
@@ -203,6 +207,10 @@ import {
|
|
|
203
207
|
drainDeploymentOperation,
|
|
204
208
|
drainWorkerOperation
|
|
205
209
|
} from "./operations/worker-drain.js";
|
|
210
|
+
import {
|
|
211
|
+
workflowEventsSseOperation,
|
|
212
|
+
workflowEventsSseRestBinding
|
|
213
|
+
} from "./operations/workflow-events-sse.js";
|
|
206
214
|
import { workflowEventsSubscriptionOperation } from "./operations/workflow-events-subscription.js";
|
|
207
215
|
export const REST_BINDINGS = [
|
|
208
216
|
startWorkflowRestBinding,
|
|
@@ -250,6 +258,8 @@ export const REST_BINDINGS = [
|
|
|
250
258
|
resumeScheduleRestBinding,
|
|
251
259
|
getStreamChunksRestBinding,
|
|
252
260
|
streamWorkflowSseRestBinding,
|
|
261
|
+
workflowEventsSseRestBinding,
|
|
262
|
+
fleetEventsSseRestBinding,
|
|
253
263
|
getTaskDiagnosticsRestBinding,
|
|
254
264
|
clearTaskDeadLetterRestBinding,
|
|
255
265
|
listSchedulesRestBinding,
|
|
@@ -376,6 +386,8 @@ export function createLiveOperationRegistry(options) {
|
|
|
376
386
|
resumeScheduleOperation,
|
|
377
387
|
getStreamChunksOperation,
|
|
378
388
|
streamWorkflowSseOperation,
|
|
389
|
+
workflowEventsSseOperation,
|
|
390
|
+
fleetEventsSseOperation,
|
|
379
391
|
workflowEventsSubscriptionOperation,
|
|
380
392
|
fleetEventsSubscriptionOperation,
|
|
381
393
|
buildTaskDiagnosticsOperationForRegistry(resolved),
|
|
@@ -15,6 +15,7 @@ import { gateRequest } from "./request-gate.js";
|
|
|
15
15
|
import { handleTaskPollRequest, handleTaskResultRequest } from "./task-polling.js";
|
|
16
16
|
import { reassignOrExpireTask } from "./task-reconciliation.js";
|
|
17
17
|
import {
|
|
18
|
+
acquireWorkflowStreamConnection,
|
|
18
19
|
addStreamSocket,
|
|
19
20
|
addWatchSocket,
|
|
20
21
|
removeStreamSocket,
|
|
@@ -96,6 +97,9 @@ function buildHandlerOptions(context, options, authentication) {
|
|
|
96
97
|
...options.maxRequestBodyBytes !== void 0 ? { maxRequestBodyBytes: options.maxRequestBodyBytes } : {},
|
|
97
98
|
operationRegistry: context.liveOperationRegistry,
|
|
98
99
|
restBindings: context.liveRestBindings,
|
|
100
|
+
workflowEventFeed: context.workflowEventFeed,
|
|
101
|
+
fleetEventFeed: context.fleetEventFeed,
|
|
102
|
+
acquireWorkflowStreamConnection: (workflowId) => acquireWorkflowStreamConnection(context, workflowId),
|
|
99
103
|
supportedAuthenticationSchemes: context.supportedAuthenticationSchemes
|
|
100
104
|
};
|
|
101
105
|
}
|
|
@@ -63,7 +63,7 @@ export interface CorsOptions {
|
|
|
63
63
|
readonly allowedOrigins?: ReadonlyArray<string> | ((origin: string) => boolean);
|
|
64
64
|
/** Methods advertised in preflight responses. Defaults to the common verbs plus `OPTIONS`. */
|
|
65
65
|
readonly allowedMethods?: ReadonlyArray<string>;
|
|
66
|
-
/** Request headers a client may send. Defaults to `Authorization, Content-Type`. */
|
|
66
|
+
/** Request headers a client may send. Defaults to `Authorization, Content-Type, Last-Event-ID, Cache-Control`. */
|
|
67
67
|
readonly allowedHeaders?: ReadonlyArray<string>;
|
|
68
68
|
/** Response headers exposed to client scripts via `Access-Control-Expose-Headers`. */
|
|
69
69
|
readonly exposedHeaders?: ReadonlyArray<string>;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
const DEFAULT_ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], DEFAULT_ALLOWED_HEADERS = [
|
|
1
|
+
const DEFAULT_ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], DEFAULT_ALLOWED_HEADERS = [
|
|
2
|
+
"Authorization",
|
|
3
|
+
"Content-Type",
|
|
4
|
+
"Last-Event-ID",
|
|
5
|
+
"Cache-Control"
|
|
6
|
+
], DEFAULT_MAX_AGE_SECONDS = 600, WILDCARD_ORIGIN = "*";
|
|
2
7
|
export function canonicalizeOrigin(value) {
|
|
3
8
|
const trimmed = value.trim();
|
|
4
9
|
if (trimmed === "" || trimmed === "null")
|
|
@@ -8,6 +8,9 @@ import type { ServerContext } from './context.ts';
|
|
|
8
8
|
* workflow cannot consume unbounded server sockets.
|
|
9
9
|
*/
|
|
10
10
|
export declare const DEFAULT_MAX_STREAM_CONNECTIONS_PER_WORKFLOW = 100;
|
|
11
|
+
export type WorkflowStreamConnectionLease = {
|
|
12
|
+
release(): void;
|
|
13
|
+
};
|
|
11
14
|
export declare function sendStreamMessage(ws: ServerWebSocket<WebSocketData>, sequence: number, message: string): void;
|
|
12
15
|
export declare function sendWatchMessage(ws: ServerWebSocket<WebSocketData>, sequence: number, message: string): void;
|
|
13
16
|
export declare function getHighestStoredStreamSequence(engine: Engine, workflowId: string, key: string): Promise<number>;
|
|
@@ -17,6 +20,7 @@ export declare function addWatchSocket(context: ServerContext, workflowId: strin
|
|
|
17
20
|
export declare function removeStreamSocket(context: ServerContext, ws: ServerWebSocket<WebSocketData>): void;
|
|
18
21
|
export declare function removeWatchSocket(context: ServerContext, ws: ServerWebSocket<WebSocketData>): void;
|
|
19
22
|
export declare function removeWorkflowStreamConnection(context: ServerContext, ws: ServerWebSocket<WebSocketData>): void;
|
|
23
|
+
export declare function acquireWorkflowStreamConnection(context: ServerContext, workflowId: string): WorkflowStreamConnectionLease | null;
|
|
20
24
|
export declare function flushPendingStreamMessages(_context: ServerContext, ws: ServerWebSocket<WebSocketData>): void;
|
|
21
25
|
export declare function flushPendingWatchMessages(_context: ServerContext, ws: ServerWebSocket<WebSocketData>): void;
|
|
22
26
|
export declare function publishTokenMessage(context: ServerContext, workflowId: string, sequence: number, message: string): void;
|
|
@@ -86,6 +86,24 @@ export function removeWorkflowStreamConnection(context, ws) {
|
|
|
86
86
|
if (!workflowId || !ws.data.workflowStreamConnectionAccepted)
|
|
87
87
|
return;
|
|
88
88
|
ws.data.workflowStreamConnectionAccepted = !1;
|
|
89
|
+
releaseWorkflowStreamConnection(context, workflowId);
|
|
90
|
+
}
|
|
91
|
+
export function acquireWorkflowStreamConnection(context, workflowId) {
|
|
92
|
+
const currentCount = context.workflowStreamConnectionCounts.get(workflowId), maxConnections = context.maxStreamConnectionsPerWorkflow;
|
|
93
|
+
if ((currentCount ?? 0) >= maxConnections)
|
|
94
|
+
return null;
|
|
95
|
+
context.workflowStreamConnectionCounts.set(workflowId, (currentCount ?? 0) + 1);
|
|
96
|
+
let released = !1;
|
|
97
|
+
return {
|
|
98
|
+
release() {
|
|
99
|
+
if (released)
|
|
100
|
+
return;
|
|
101
|
+
released = !0;
|
|
102
|
+
releaseWorkflowStreamConnection(context, workflowId);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function releaseWorkflowStreamConnection(context, workflowId) {
|
|
89
107
|
const currentCount = context.workflowStreamConnectionCounts.get(workflowId);
|
|
90
108
|
if (currentCount === void 0 || currentCount <= 1) {
|
|
91
109
|
context.workflowStreamConnectionCounts.delete(workflowId);
|
|
@@ -94,12 +112,11 @@ export function removeWorkflowStreamConnection(context, ws) {
|
|
|
94
112
|
context.workflowStreamConnectionCounts.set(workflowId, currentCount - 1);
|
|
95
113
|
}
|
|
96
114
|
function addWorkflowStreamConnection(context, workflowId, ws) {
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
115
|
+
const maxConnections = context.maxStreamConnectionsPerWorkflow;
|
|
116
|
+
if (acquireWorkflowStreamConnection(context, workflowId) === null) {
|
|
99
117
|
ws.close(STREAM_CONNECTION_POLICY_CLOSE_CODE, `maximum stream connections per workflow (${maxConnections}) exceeded`);
|
|
100
118
|
return !1;
|
|
101
119
|
}
|
|
102
|
-
context.workflowStreamConnectionCounts.set(workflowId, currentCount + 1);
|
|
103
120
|
ws.data.workflowStreamConnectionAccepted = !0;
|
|
104
121
|
return !0;
|
|
105
122
|
}
|
|
@@ -72,6 +72,7 @@ export type ReplayLiveSubscribeOptions<TEnvelope extends SequencedEventEnvelope>
|
|
|
72
72
|
filterEnvelope?: (envelope: TEnvelope) => boolean;
|
|
73
73
|
countReplayEnvelope?: (envelope: TEnvelope) => boolean;
|
|
74
74
|
createReplayLimitError?: (count: number, limit: number) => unknown;
|
|
75
|
+
onReplayComplete?: () => void;
|
|
75
76
|
};
|
|
76
77
|
export declare class ReplayWindowExceededError extends Error {
|
|
77
78
|
readonly count: number;
|
|
@@ -44,7 +44,7 @@ export function createReplayLiveFeed(backend, options) {
|
|
|
44
44
|
}
|
|
45
45
|
function subscribe(args) {
|
|
46
46
|
const buffer = [];
|
|
47
|
-
let bufferOverflowed = !1, waker = null;
|
|
47
|
+
let bufferOverflowed = !1, waker = null, cleanedUp = !1;
|
|
48
48
|
const signal = args?.signal, fromCursor = args?.fromCursor, requestedAfter = fromCursor !== void 0 ? decodeCursorOrThrow(fromCursor) : -1, unsubscribe = backend.subscribeLive((envelope) => {
|
|
49
49
|
if (!shouldDeliverEnvelope(envelope, args))
|
|
50
50
|
return;
|
|
@@ -57,14 +57,23 @@ export function createReplayLiveFeed(backend, options) {
|
|
|
57
57
|
waker = null;
|
|
58
58
|
fire();
|
|
59
59
|
}
|
|
60
|
-
}),
|
|
60
|
+
}), wake = () => {
|
|
61
61
|
if (waker) {
|
|
62
62
|
const fire = waker;
|
|
63
63
|
waker = null;
|
|
64
64
|
fire();
|
|
65
65
|
}
|
|
66
|
-
}
|
|
67
|
-
|
|
66
|
+
}, cleanup = () => {
|
|
67
|
+
if (cleanedUp)
|
|
68
|
+
return;
|
|
69
|
+
cleanedUp = !0;
|
|
70
|
+
signal?.removeEventListener("abort", onAbort);
|
|
71
|
+
unsubscribe();
|
|
72
|
+
wake();
|
|
73
|
+
}, onAbort = () => cleanup();
|
|
74
|
+
signal?.addEventListener("abort", onAbort, { once: !0 });
|
|
75
|
+
if (signal?.aborted)
|
|
76
|
+
cleanup();
|
|
68
77
|
async function* generator() {
|
|
69
78
|
try {
|
|
70
79
|
if (signal?.aborted)
|
|
@@ -73,12 +82,12 @@ export function createReplayLiveFeed(backend, options) {
|
|
|
73
82
|
yield* replayUpTo(backend, requestedAfter, snapshot, signal, args);
|
|
74
83
|
if (signal?.aborted)
|
|
75
84
|
return;
|
|
85
|
+
args?.onReplayComplete?.();
|
|
76
86
|
yield* drainLive(buffer, snapshot, signal, () => bufferOverflowed, (w) => {
|
|
77
87
|
waker = w;
|
|
78
88
|
});
|
|
79
89
|
} finally {
|
|
80
|
-
|
|
81
|
-
unsubscribe();
|
|
90
|
+
cleanup();
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
return generator();
|
|
@@ -119,6 +128,8 @@ export function createWorkflowEventFeed(backend, options) {
|
|
|
119
128
|
subscribeOptions.countReplayEnvelope = args.countReplayEnvelope;
|
|
120
129
|
if (args.createReplayLimitError !== void 0)
|
|
121
130
|
subscribeOptions.createReplayLimitError = args.createReplayLimitError;
|
|
131
|
+
if (args.onReplayComplete !== void 0)
|
|
132
|
+
subscribeOptions.onReplayComplete = args.onReplayComplete;
|
|
122
133
|
return createScopedFeed(args.workflowId, args.selector).subscribe(subscribeOptions);
|
|
123
134
|
}
|
|
124
135
|
return {
|