@lostgradient/weft 0.14.0 → 0.16.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 +3 -8
- package/dist/alerting/alert-manager.d.ts +2 -0
- package/dist/alerting/alert-manager.js +3 -0
- package/dist/cli/api.js +5 -19
- package/dist/cli/console.d.ts +12 -0
- package/dist/cli/console.js +62 -0
- package/dist/cli/generated/operation-catalog.snapshot.json +1061 -90
- package/dist/cli/generated/operation-client.generated.d.ts +29 -23
- package/dist/cli/generated/operation-client.generated.js +2 -0
- package/dist/cli/help-text.d.ts +1 -1
- package/dist/cli/help-text.js +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/json-input.d.ts +20 -0
- package/dist/cli/json-input.js +33 -0
- package/dist/cli/parse-arguments.js +2 -0
- package/dist/cli/schedule.js +7 -9
- package/dist/cli/types.d.ts +1 -0
- package/dist/cli/workflow-commands.js +5 -25
- package/dist/cli-main.js +4 -2
- package/dist/core/aggregate-validation.d.ts +2 -5
- package/dist/core/aggregate-validation.js +2 -10
- package/dist/core/atomic-state.js +21 -24
- package/dist/core/engine/index.d.ts +3 -0
- package/dist/core/engine/index.js +3 -0
- package/dist/core/engine/schedules.js +4 -1
- package/dist/core/inline-execution-strategy.js +8 -46
- package/dist/core/list-filter-validation.d.ts +2 -5
- package/dist/core/list-filter-validation.js +2 -10
- package/dist/core/types/schedules.d.ts +13 -31
- package/dist/core/validation-issues.d.ts +13 -0
- package/dist/core/validation-issues.js +9 -0
- package/dist/diagnostics/recommendations.js +1 -9
- package/dist/http.js +2 -2
- package/dist/indexeddb.js +1 -1
- package/dist/mcp/session.js +2 -10
- package/dist/mcp/tools.js +2 -10
- package/dist/mcp/tuple-list-equality.d.ts +1 -0
- package/dist/mcp/tuple-list-equality.js +12 -0
- package/dist/server/authorization-scope.d.ts +1 -1
- package/dist/server/authorization-scope.js +0 -2
- package/dist/server/dashboard-assets.d.ts +26 -0
- package/dist/server/dashboard-assets.js +95 -0
- package/dist/server/index.d.ts +10 -1
- package/dist/server/index.js +11 -2
- package/dist/server/interactive-operations.js +1 -0
- package/dist/server/operation-catalog/types.d.ts +1 -1
- package/dist/server/operation-registry.d.ts +2 -31
- package/dist/server/operations/bulk-filter-helpers.d.ts +17 -17
- package/dist/server/operations/bulk-filter-helpers.js +16 -37
- package/dist/server/operations/bulk-mutate-workflow-tags.d.ts +27 -27
- package/dist/server/operations/bulk-signal-workflows.d.ts +26 -26
- package/dist/server/operations/create-schedule.js +26 -65
- package/dist/server/operations/event-stream-contracts.d.ts +20 -0
- package/dist/server/operations/event-stream-contracts.js +67 -0
- package/dist/server/operations/fleet-events-sse.js +21 -50
- package/dist/server/operations/fleet-events-subscription.js +8 -12
- package/dist/server/operations/list-alerts.d.ts +26 -0
- package/dist/server/operations/list-alerts.js +54 -0
- package/dist/server/operations/list-workflows.d.ts +43 -43
- package/dist/server/operations/list-workflows.js +9 -30
- package/dist/server/operations/purge-workflows.d.ts +11 -11
- package/dist/server/operations/schedule-faults.d.ts +9 -1
- package/dist/server/operations/schedule-faults.js +51 -0
- package/dist/server/operations/schedule-rest-body.d.ts +11 -0
- package/dist/server/operations/schedule-rest-body.js +28 -0
- package/dist/server/operations/update-schedule.js +13 -72
- package/dist/server/operations/workflow-events-sse.js +29 -67
- package/dist/server/operations/workflow-events-subscription.js +9 -27
- package/dist/server/rest-bindings.js +3 -0
- package/dist/server/runtime/task-polling.js +3 -5
- package/dist/server/task-state.d.ts +2 -8
- package/dist/server/task-state.js +10 -7
- package/dist/service-worker/index.d.ts +4 -1
- package/dist/service-worker/index.js +2 -2
- package/dist/service-worker/setup.d.ts +20 -0
- package/dist/service-worker/setup.js +5 -5
- package/dist/storage/bun-sql.js +65 -80
- package/dist/storage/conditional-batch-body.d.ts +7 -0
- package/dist/storage/conditional-batch-body.js +14 -0
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon.js +2 -2
- package/dist/storage/node-sqlite-loader.d.ts +2 -2
- package/dist/storage/node-sqlite.js +32 -17
- package/dist/storage/postgres.js +2 -2
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -1
- package/dist/worker/execute-with-interceptors.d.ts +1 -1
- package/dist/worker/execute-with-interceptors.js +2 -2
- package/dist/worker/index.js +5 -5
- package/dist/worker/long-poll.js +3 -4
- package/dist/worker/protocol-messages.d.ts +11 -19
- package/dist/worker/protocol-schemas.d.ts +8 -8
- package/dist/worker/protocol-schemas.js +4 -4
- package/dist/worker/protocol-task-result.d.ts +1 -0
- package/dist/worker/protocol-task-result.js +1 -3
- package/dist/worker/protocol.js +1 -1
- package/dist/worker/registry/types.d.ts +2 -7
- package/dist/worker/registry.d.ts +3 -9
- package/dist/worker/registry.js +2 -5
- package/package.json +5 -1
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { normalizeScheduleUpdateOptions } from "../../core/engine/validation/schedule.js";
|
|
3
2
|
import { defineOperation } from "../operation-registry.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
3
|
+
import { shapeRestFault } from "./operation-helpers.js";
|
|
4
|
+
import {
|
|
5
|
+
mapScheduleErrorToFault,
|
|
6
|
+
validateScheduleInputCadence,
|
|
7
|
+
validateScheduleMutableOptions
|
|
8
|
+
} from "./schedule-faults.js";
|
|
9
|
+
import {
|
|
10
|
+
extractSharedScheduleRestFields,
|
|
11
|
+
parseScheduleRestBodyRequestRecord
|
|
12
|
+
} from "./schedule-rest-body.js";
|
|
7
13
|
const updateScheduleInput = z.object({
|
|
8
14
|
scheduleId: z.string().min(1),
|
|
9
15
|
cronExpression: z.unknown().optional().describe("Cron expression. Supply exactly one of cronExpression or every; runtime validation requires a non-empty string."),
|
|
@@ -13,56 +19,6 @@ const updateScheduleInput = z.object({
|
|
|
13
19
|
backfill: z.unknown().optional(),
|
|
14
20
|
jitter: z.unknown().optional()
|
|
15
21
|
});
|
|
16
|
-
function isScheduleOverlapPolicy(value) {
|
|
17
|
-
return value === "skip" || value === "queue" || value === "cancel-running" || value === "allow";
|
|
18
|
-
}
|
|
19
|
-
function validateDescription(value) {
|
|
20
|
-
if (value === void 0)
|
|
21
|
-
return;
|
|
22
|
-
if (typeof value !== "string")
|
|
23
|
-
throw invalidParamsFault('Field "description" must be a string');
|
|
24
|
-
return value;
|
|
25
|
-
}
|
|
26
|
-
function validateOverlap(value) {
|
|
27
|
-
if (value === void 0)
|
|
28
|
-
return;
|
|
29
|
-
if (typeof value !== "string" || !isScheduleOverlapPolicy(value))
|
|
30
|
-
throw invalidParamsFault('Field "overlap" must be one of skip, queue, cancel-running, allow');
|
|
31
|
-
return value;
|
|
32
|
-
}
|
|
33
|
-
function validateBackfill(value) {
|
|
34
|
-
if (value === void 0)
|
|
35
|
-
return;
|
|
36
|
-
if (typeof value !== "boolean")
|
|
37
|
-
throw invalidParamsFault('Field "backfill" must be a boolean');
|
|
38
|
-
return value;
|
|
39
|
-
}
|
|
40
|
-
function formatJitterValidationMessage(message) {
|
|
41
|
-
const enginePrefix = "Invalid options.jitter: ", hasEnginePrefix = message.startsWith(enginePrefix), wireDetail = (hasEnginePrefix ? message.slice(enginePrefix.length) : message).replaceAll("options.jitter", 'Field "jitter"');
|
|
42
|
-
return hasEnginePrefix ? `Field "jitter" is invalid: ${wireDetail}` : wireDetail;
|
|
43
|
-
}
|
|
44
|
-
function validateJitter(value) {
|
|
45
|
-
if (value === void 0)
|
|
46
|
-
return;
|
|
47
|
-
if (typeof value !== "string" && typeof value !== "number")
|
|
48
|
-
throw invalidParamsFault('Field "jitter" must be a duration string or a number of milliseconds');
|
|
49
|
-
try {
|
|
50
|
-
normalizeScheduleUpdateOptions({ jitter: value });
|
|
51
|
-
} catch (error) {
|
|
52
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
-
throw invalidParamsFault(formatJitterValidationMessage(message));
|
|
54
|
-
}
|
|
55
|
-
return value;
|
|
56
|
-
}
|
|
57
|
-
function validateScheduleUpdateOptions(input) {
|
|
58
|
-
const description = validateDescription(input.description), overlap = validateOverlap(input.overlap), backfill = validateBackfill(input.backfill), jitter = validateJitter(input.jitter);
|
|
59
|
-
return {
|
|
60
|
-
...description !== void 0 ? { description } : {},
|
|
61
|
-
...overlap !== void 0 ? { overlap } : {},
|
|
62
|
-
...backfill !== void 0 ? { backfill } : {},
|
|
63
|
-
...jitter !== void 0 ? { jitter } : {}
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
22
|
export const updateScheduleOperation = defineOperation({
|
|
67
23
|
name: "weft.schedules.update",
|
|
68
24
|
mcpExposable: !1,
|
|
@@ -77,7 +33,7 @@ export const updateScheduleOperation = defineOperation({
|
|
|
77
33
|
transports: { http: !0, jsonRpcHttp: !0, jsonRpcWebSocket: !0, jsonRpcStdio: !0 },
|
|
78
34
|
unknownKeyPolicy: { http: "strip", jsonRpc: "reject" },
|
|
79
35
|
invoke: async ({ input, engine }) => {
|
|
80
|
-
const typedEngine = engine, spec = validateScheduleInputCadence(input), options =
|
|
36
|
+
const typedEngine = engine, spec = validateScheduleInputCadence(input), options = validateScheduleMutableOptions(input);
|
|
81
37
|
try {
|
|
82
38
|
await typedEngine.updateSchedule(input.scheduleId, spec, options);
|
|
83
39
|
return null;
|
|
@@ -100,25 +56,10 @@ export const updateScheduleOperation = defineOperation({
|
|
|
100
56
|
jitter: { kind: "body-field", bodyField: "jitter" }
|
|
101
57
|
},
|
|
102
58
|
extractInput: async (request, pathParams, context) => {
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
body = await readRestJsonBody(request, context);
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if (isOperationFault(error))
|
|
108
|
-
throw error;
|
|
109
|
-
throw invalidParamsFault("Invalid JSON body");
|
|
110
|
-
}
|
|
111
|
-
if (typeof body !== "object" || body === null)
|
|
112
|
-
throw invalidParamsFault("Request body must be a JSON object");
|
|
113
|
-
const record = body;
|
|
59
|
+
const record = await parseScheduleRestBodyRequestRecord(request, context);
|
|
114
60
|
return {
|
|
115
61
|
scheduleId: pathParams.id ?? "",
|
|
116
|
-
|
|
117
|
-
every: Object.hasOwn(record, "every") ? record.every : void 0,
|
|
118
|
-
description: record.description,
|
|
119
|
-
overlap: record.overlap,
|
|
120
|
-
backfill: record.backfill,
|
|
121
|
-
jitter: record.jitter
|
|
62
|
+
...extractSharedScheduleRestFields(record)
|
|
122
63
|
};
|
|
123
64
|
},
|
|
124
65
|
success: { kind: "empty", status: 204 },
|
|
@@ -4,6 +4,13 @@ import { isAuthenticated } from "../principal.js";
|
|
|
4
4
|
import {
|
|
5
5
|
decodeCursor
|
|
6
6
|
} from "../workflow-event-feed.js";
|
|
7
|
+
import {
|
|
8
|
+
createReplayAwareClosableIterable,
|
|
9
|
+
isClosableAsyncIterable,
|
|
10
|
+
isReplayAwareClosableIterable,
|
|
11
|
+
workflowEventEnvelopeSchema,
|
|
12
|
+
workflowEventParameterizedAccess
|
|
13
|
+
} from "./event-stream-contracts.js";
|
|
7
14
|
import { invalidParamsFault, shapeRestFault } from "./operation-helpers.js";
|
|
8
15
|
import {
|
|
9
16
|
createEventEnvelopeSSEStream,
|
|
@@ -19,15 +26,7 @@ const INITIAL_CURSOR = "-1", MAX_WORKFLOW_SSE_REPLAY_EVENTS = 1000, MAXIMUM_WORK
|
|
|
19
26
|
selector: z.enum(["events", "tokens"]).default("events"),
|
|
20
27
|
fromCursor: cursorSchema.optional(),
|
|
21
28
|
lastEventId: cursorSchema.optional()
|
|
22
|
-
}), workflowEventsSseOutputSchema = z.custom(isAsyncIterable, "Expected async iterable workflow event stream")
|
|
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
|
-
});
|
|
29
|
+
}), workflowEventsSseOutputSchema = z.custom(isAsyncIterable, "Expected async iterable workflow event stream");
|
|
31
30
|
export const workflowEventsSseOperation = defineOperation({
|
|
32
31
|
name: "weft.workflows.events.sse",
|
|
33
32
|
mcpExposable: !1,
|
|
@@ -39,20 +38,7 @@ export const workflowEventsSseOperation = defineOperation({
|
|
|
39
38
|
inputSchema: workflowEventsSseInputSchema,
|
|
40
39
|
outputSchema: workflowEventsSseOutputSchema,
|
|
41
40
|
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
|
-
},
|
|
41
|
+
parameterizedAccess: workflowEventParameterizedAccess,
|
|
56
42
|
producibleFaults: ["InvalidParams", "UnsupportedTransport"],
|
|
57
43
|
access: { kind: "authenticated" },
|
|
58
44
|
authorize: async ({ input, principal }) => {
|
|
@@ -97,12 +83,6 @@ function isAsyncIterable(value) {
|
|
|
97
83
|
return !1;
|
|
98
84
|
return typeof value[Symbol.asyncIterator] === "function";
|
|
99
85
|
}
|
|
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
86
|
function createWorkflowEventsIterable(input, context) {
|
|
107
87
|
const feed = context.workflowEventFeed;
|
|
108
88
|
if (feed === void 0)
|
|
@@ -110,51 +90,33 @@ function createWorkflowEventsIterable(input, context) {
|
|
|
110
90
|
const lease = context.acquireWorkflowStreamConnection?.(input.workflowId);
|
|
111
91
|
if (lease === null)
|
|
112
92
|
throw maximumWorkflowStreamsFault();
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
};
|
|
93
|
+
const fromCursor = input.lastEventId ?? input.fromCursor ?? INITIAL_CURSOR, controller = new AbortController;
|
|
94
|
+
return createReplayAwareClosableIterable((onReplayComplete) => {
|
|
95
|
+
const subscribeOptions = {
|
|
96
|
+
workflowId: input.workflowId,
|
|
97
|
+
selector: input.selector,
|
|
98
|
+
fromCursor,
|
|
99
|
+
signal: controller.signal,
|
|
100
|
+
replayLimit: MAX_WORKFLOW_SSE_REPLAY_EVENTS,
|
|
101
|
+
onReplayComplete,
|
|
102
|
+
createReplayLimitError: (count, limit) => invalidParamsFault(`Workflow event replay window is ${count} events; maximum is ${limit}. Supply a more recent fromCursor.`)
|
|
103
|
+
};
|
|
104
|
+
return feed.subscribe(subscribeOptions);
|
|
105
|
+
}, {
|
|
106
|
+
close: () => {
|
|
107
|
+
controller.abort();
|
|
108
|
+
lease?.release();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
149
111
|
}
|
|
150
112
|
function shapeWorkflowEventsSseSuccess(output, request) {
|
|
151
|
-
const close =
|
|
113
|
+
const close = isClosableAsyncIterable(output) ? () => output.close() : async () => {
|
|
152
114
|
return;
|
|
153
115
|
};
|
|
154
116
|
return new Response(createEventEnvelopeSSEStream({
|
|
155
117
|
iterable: output,
|
|
156
118
|
close,
|
|
157
|
-
...
|
|
119
|
+
...isReplayAwareClosableIterable(output) ? { ready: output.replayComplete } : {},
|
|
158
120
|
signal: request.signal
|
|
159
121
|
}), {
|
|
160
122
|
status: 200,
|
|
@@ -4,6 +4,11 @@ import { isAuthenticated } from "../principal.js";
|
|
|
4
4
|
import {
|
|
5
5
|
decodeCursor
|
|
6
6
|
} from "../workflow-event-feed.js";
|
|
7
|
+
import {
|
|
8
|
+
createReplayAwareClosableIterable,
|
|
9
|
+
workflowEventEnvelopeSchema,
|
|
10
|
+
workflowEventParameterizedAccess
|
|
11
|
+
} from "./event-stream-contracts.js";
|
|
7
12
|
import { invalidParamsFault } from "./operation-helpers.js";
|
|
8
13
|
const INITIAL_SUBSCRIPTION_CURSOR = "-1", MAX_WORKFLOW_SUBSCRIPTION_REPLAY_EVENTS = 1000, workflowEventsSubscriptionInput = z.object({
|
|
9
14
|
workflowId: z.string().min(1),
|
|
@@ -24,30 +29,9 @@ export const workflowEventsSubscriptionOperation = defineOperation({
|
|
|
24
29
|
inputSchema: workflowEventsSubscriptionInput,
|
|
25
30
|
outputSchema: workflowEventsSubscriptionEnvelope,
|
|
26
31
|
producibleFaults: ["InvalidParams"],
|
|
27
|
-
eventSchema:
|
|
28
|
-
kind: z.string(),
|
|
29
|
-
workflowId: z.string(),
|
|
30
|
-
selector: z.enum(["events", "tokens"]),
|
|
31
|
-
sequence: z.number(),
|
|
32
|
-
cursor: z.string(),
|
|
33
|
-
emittedAtMs: z.number(),
|
|
34
|
-
payload: z.unknown()
|
|
35
|
-
}),
|
|
32
|
+
eventSchema: workflowEventEnvelopeSchema,
|
|
36
33
|
access: { kind: "authenticated" },
|
|
37
|
-
parameterizedAccess:
|
|
38
|
-
discriminator: "selector",
|
|
39
|
-
defaultValue: "events",
|
|
40
|
-
variants: [
|
|
41
|
-
{
|
|
42
|
-
value: "events",
|
|
43
|
-
access: { kind: "scoped", scopes: { kind: "anyOf", scopes: ["events:read"] } }
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
value: "tokens",
|
|
47
|
-
access: { kind: "scoped", scopes: { kind: "anyOf", scopes: ["streams:read"] } }
|
|
48
|
-
}
|
|
49
|
-
]
|
|
50
|
-
},
|
|
34
|
+
parameterizedAccess: workflowEventParameterizedAccess,
|
|
51
35
|
authorize: async ({ input, principal }) => {
|
|
52
36
|
const requiredScope = workflowSubscriptionScope(input.selector);
|
|
53
37
|
if (!isAuthenticated(principal))
|
|
@@ -71,13 +55,11 @@ export const workflowEventsSubscriptionOperation = defineOperation({
|
|
|
71
55
|
signal: controller.signal,
|
|
72
56
|
replayLimit: MAX_WORKFLOW_SUBSCRIPTION_REPLAY_EVENTS,
|
|
73
57
|
createReplayLimitError: (count, limit) => invalidParamsFault(`Workflow event replay window is ${count} events; maximum is ${limit}. Supply a more recent fromCursor.`)
|
|
74
|
-
}, iterable = feed.subscribe(subscribeOptions);
|
|
58
|
+
}, iterable = createReplayAwareClosableIterable((onReplayComplete) => feed.subscribe({ ...subscribeOptions, onReplayComplete }), { close: () => controller.abort() });
|
|
75
59
|
return {
|
|
76
60
|
envelope: { subscriptionId: `sub_${crypto.randomUUID()}`, cursor: startingCursor },
|
|
77
61
|
iterable,
|
|
78
|
-
close:
|
|
79
|
-
controller.abort();
|
|
80
|
-
}
|
|
62
|
+
close: () => iterable.close()
|
|
81
63
|
};
|
|
82
64
|
}
|
|
83
65
|
});
|
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
getWorkflowTimelineRestBinding
|
|
99
99
|
} from "./operations/get-workflow-timeline.js";
|
|
100
100
|
import { getWorkflowOperation, getWorkflowRestBinding } from "./operations/get-workflow.js";
|
|
101
|
+
import { listAlertsOperation, listAlertsRestBinding } from "./operations/list-alerts.js";
|
|
101
102
|
import {
|
|
102
103
|
listCheckpointsOperation,
|
|
103
104
|
listCheckpointsRestBinding
|
|
@@ -214,6 +215,7 @@ import {
|
|
|
214
215
|
} from "./operations/workflow-events-sse.js";
|
|
215
216
|
import { workflowEventsSubscriptionOperation } from "./operations/workflow-events-subscription.js";
|
|
216
217
|
export const REST_BINDINGS = [
|
|
218
|
+
listAlertsRestBinding,
|
|
217
219
|
startWorkflowRestBinding,
|
|
218
220
|
startOrSignalWorkflowRestBinding,
|
|
219
221
|
recoverAllRestBinding,
|
|
@@ -401,6 +403,7 @@ export function createLiveOperationRegistry(options) {
|
|
|
401
403
|
buildTaskDiagnosticsOperationForRegistry(resolved),
|
|
402
404
|
clearTaskDeadLetterOperation,
|
|
403
405
|
listSchedulesOperation,
|
|
406
|
+
listAlertsOperation,
|
|
404
407
|
getScheduleOperation,
|
|
405
408
|
replayWorkflowOperation,
|
|
406
409
|
storageCapabilities.storageCapabilitiesOperation,
|
|
@@ -33,8 +33,8 @@ function validateTaskResultBody(body) {
|
|
|
33
33
|
if (status !== "completed" && status !== "failed")
|
|
34
34
|
return Response.json({ error: 'status must be "completed" or "failed"' }, { status: 400 });
|
|
35
35
|
const rawAttemptToken = body.attemptToken;
|
|
36
|
-
if (
|
|
37
|
-
return Response.json({ error: "attemptToken must be a non-empty string
|
|
36
|
+
if (typeof rawAttemptToken !== "string" || rawAttemptToken === "")
|
|
37
|
+
return Response.json({ error: "attemptToken must be a non-empty string" }, { status: 400 });
|
|
38
38
|
return {
|
|
39
39
|
operationId,
|
|
40
40
|
status,
|
|
@@ -190,7 +190,5 @@ export async function handleTaskResultRequest(context, options, request, url, pr
|
|
|
190
190
|
function isLongPollCompletionAuthorized(inflightRecord, validated) {
|
|
191
191
|
if (validated.workerId === void 0 || inflightRecord.workerId !== validated.workerId)
|
|
192
192
|
return !1;
|
|
193
|
-
|
|
194
|
-
return !1;
|
|
195
|
-
return !0;
|
|
193
|
+
return validated.attemptToken === inflightRecord.attemptToken;
|
|
196
194
|
}
|
|
@@ -73,14 +73,8 @@ export interface InflightRecord extends TaskLifecycleFields {
|
|
|
73
73
|
workflowId?: string | undefined;
|
|
74
74
|
/** Durable token for the workflow run that dispatched this activity, when known. */
|
|
75
75
|
workflowExecutionToken?: string | undefined;
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
* (re-)dispatch because each dispatch writes a fresh InflightRecord. The
|
|
79
|
-
* long-poll completion handler rejects a result whose echoed token does not
|
|
80
|
-
* match this value. Optional for back-compatible decoding of records written
|
|
81
|
-
* before this field existed; a missing token disables the check for that record.
|
|
82
|
-
*/
|
|
83
|
-
attemptToken?: string | undefined;
|
|
76
|
+
/** Unique, unguessable token identifying this dispatch attempt. */
|
|
77
|
+
attemptToken: string;
|
|
84
78
|
}
|
|
85
79
|
/** Persisted record for a task in the resolved state. */
|
|
86
80
|
export interface ResolvedRecord {
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { decode, encode } from "../core/codec.js";
|
|
2
2
|
import { KEYS } from "../storage/interface.js";
|
|
3
3
|
import { buildResolvedRecord } from "./task-resolved-record.js";
|
|
4
|
-
|
|
4
|
+
async function readTaskStateSnapshot(storage, operationId) {
|
|
5
5
|
const [inflight, queued, resolved] = await Promise.all([
|
|
6
6
|
storage.get(KEYS.operationInflight(operationId)),
|
|
7
7
|
storage.get(KEYS.operationQueued(operationId)),
|
|
8
8
|
storage.get(KEYS.operationResolved(operationId))
|
|
9
9
|
]);
|
|
10
|
+
return { inflight, queued, resolved };
|
|
11
|
+
}
|
|
12
|
+
export async function getTaskState(storage, operationId) {
|
|
13
|
+
const { inflight, queued, resolved } = await readTaskStateSnapshot(storage, operationId);
|
|
10
14
|
if (inflight !== null)
|
|
11
15
|
return "inflight";
|
|
12
16
|
if (queued !== null)
|
|
@@ -16,11 +20,7 @@ export async function getTaskState(storage, operationId) {
|
|
|
16
20
|
return null;
|
|
17
21
|
}
|
|
18
22
|
export async function getExclusiveTaskState(storage, operationId) {
|
|
19
|
-
const
|
|
20
|
-
storage.get(KEYS.operationInflight(operationId)),
|
|
21
|
-
storage.get(KEYS.operationQueued(operationId)),
|
|
22
|
-
storage.get(KEYS.operationResolved(operationId))
|
|
23
|
-
]), states = [];
|
|
23
|
+
const { inflight, queued, resolved } = await readTaskStateSnapshot(storage, operationId), states = [];
|
|
24
24
|
if (inflight !== null)
|
|
25
25
|
states.push("inflight");
|
|
26
26
|
if (queued !== null)
|
|
@@ -37,11 +37,14 @@ export function isQueuedRecord(value) {
|
|
|
37
37
|
const record = value;
|
|
38
38
|
return typeof record.operationId === "string" && typeof record.activityName === "string" && typeof record.queue === "string" && typeof record.attempt === "number" && typeof record.visibilityTimeout === "number" && typeof record.queuedAt === "number";
|
|
39
39
|
}
|
|
40
|
+
function isNonEmptyString(value) {
|
|
41
|
+
return typeof value === "string" && value.length > 0;
|
|
42
|
+
}
|
|
40
43
|
export function isInflightRecord(value) {
|
|
41
44
|
if (value === null || typeof value !== "object")
|
|
42
45
|
return !1;
|
|
43
46
|
const record = value;
|
|
44
|
-
return typeof record.operationId === "string" && typeof record.activityName === "string" && typeof record.queue === "string" && typeof record.attempt === "number" && typeof record.visibilityTimeout === "number" && typeof record.workerId === "string" && typeof record.deadline === "number";
|
|
47
|
+
return typeof record.operationId === "string" && typeof record.activityName === "string" && typeof record.queue === "string" && typeof record.attempt === "number" && typeof record.visibilityTimeout === "number" && typeof record.workerId === "string" && typeof record.deadline === "number" && isNonEmptyString(record.attemptToken);
|
|
45
48
|
}
|
|
46
49
|
export function isResolvedRecord(value) {
|
|
47
50
|
if (value === null || typeof value !== "object")
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { RegistryAgnosticEngine } from '../core/engine';
|
|
10
10
|
import type { ServiceWorkerScheduler } from './scheduler.ts';
|
|
11
|
+
import type { ServiceWorkerHandlerOptions } from './setup.ts';
|
|
11
12
|
import type { MinimalExtendableEvent, MinimalFetchEvent, MinimalPeriodicSyncEvent } from './shared.ts';
|
|
12
13
|
export { ServiceWorkerScheduler } from './scheduler.ts';
|
|
13
14
|
export type { ServiceWorkerSchedulerOptions } from './scheduler.ts';
|
|
@@ -44,6 +45,8 @@ export interface ServiceWorkerOptions {
|
|
|
44
45
|
*/
|
|
45
46
|
engine: RegistryAgnosticEngine;
|
|
46
47
|
pathPrefix?: string;
|
|
48
|
+
/** Handler options supported by the Service Worker runtime. */
|
|
49
|
+
handlerOptions?: ServiceWorkerHandlerOptions;
|
|
47
50
|
}
|
|
48
51
|
/**
|
|
49
52
|
* Create a fetch event handler that intercepts requests matching the given
|
|
@@ -127,4 +130,4 @@ export declare function createLifecycleHandlers(): {
|
|
|
127
130
|
install: (event: MinimalExtendableEvent) => void;
|
|
128
131
|
activate: (event: MinimalExtendableEvent) => void;
|
|
129
132
|
};
|
|
130
|
-
export { setupServiceWorker, type SetupServiceWorkerOptions, type SetupServiceWorkerResult, } from './setup.ts';
|
|
133
|
+
export { setupServiceWorker, type ServiceWorkerHandlerOptions, type SetupServiceWorkerOptions, type SetupServiceWorkerResult, } from './setup.ts';
|
|
@@ -3,12 +3,12 @@ import { buildDelegatedRequest, DEFAULT_PERIODIC_SYNC_TAG, normalizePathPrefix }
|
|
|
3
3
|
export { ServiceWorkerScheduler } from "./scheduler.js";
|
|
4
4
|
export { buildDelegatedRequest, DEFAULT_PERIODIC_SYNC_TAG, normalizePathPrefix } from "./shared.js";
|
|
5
5
|
export function createFetchHandler(options) {
|
|
6
|
-
const { engine } = options, pathPrefix = normalizePathPrefix(options.pathPrefix);
|
|
6
|
+
const { engine, handlerOptions } = options, pathPrefix = normalizePathPrefix(options.pathPrefix);
|
|
7
7
|
return (event) => {
|
|
8
8
|
const delegatedRequest = buildDelegatedRequest(event, pathPrefix);
|
|
9
9
|
if (delegatedRequest === null)
|
|
10
10
|
return;
|
|
11
|
-
event.respondWith(handleRequest(delegatedRequest, engine));
|
|
11
|
+
event.respondWith(handleRequest(delegatedRequest, engine, handlerOptions));
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
export function createPeriodicSyncHandler(scheduler, tag) {
|
|
@@ -11,8 +11,26 @@
|
|
|
11
11
|
* @module service-worker/setup
|
|
12
12
|
*/
|
|
13
13
|
import { Engine, type RegistryAgnosticEngine } from '../core/engine';
|
|
14
|
+
import type { HandlerOptions } from '../server/handler';
|
|
14
15
|
import type { Storage as WeftStorage } from '../storage/interface';
|
|
15
16
|
import { ServiceWorkerScheduler } from './scheduler';
|
|
17
|
+
/**
|
|
18
|
+
* The subset of {@link HandlerOptions} that can be supplied by a Service
|
|
19
|
+
* Worker. Service Workers can inject request authorization and event-stream
|
|
20
|
+
* plumbing, but cannot provide server-owned registries or Bun runtime
|
|
21
|
+
* integrations.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import type { ServiceWorkerHandlerOptions } from '@lostgradient/weft/service-worker';
|
|
26
|
+
*
|
|
27
|
+
* const handlerOptions: ServiceWorkerHandlerOptions = {
|
|
28
|
+
* authContext: { method: 'public' },
|
|
29
|
+
* };
|
|
30
|
+
* void handlerOptions;
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export type ServiceWorkerHandlerOptions = Pick<HandlerOptions, 'authContext' | 'workflowEventFeed' | 'fleetEventFeed' | 'acquireWorkflowStreamConnection'>;
|
|
16
34
|
/**
|
|
17
35
|
* Options for {@link setupServiceWorker}. All fields are optional; the
|
|
18
36
|
* helper supplies sensible defaults (`/weft/` path prefix, `'weft'`
|
|
@@ -58,6 +76,8 @@ export interface SetupServiceWorkerOptions {
|
|
|
58
76
|
* engine's storage when both are provided.
|
|
59
77
|
*/
|
|
60
78
|
storage?: WeftStorage;
|
|
79
|
+
/** Handler options supported by the Service Worker runtime. */
|
|
80
|
+
handlerOptions?: ServiceWorkerHandlerOptions;
|
|
61
81
|
/**
|
|
62
82
|
* Register workflows on the engine before listeners do real work.
|
|
63
83
|
* Resolves before the helper returns. Rejection causes subsequent
|
|
@@ -49,16 +49,16 @@ function resolveStorageAndEngine(options) {
|
|
|
49
49
|
const storage = options.storage ?? new IndexedDBStorage(options.databaseName ?? DEFAULT_DATABASE_NAME);
|
|
50
50
|
return { storage, engine: new Engine({ storage }) };
|
|
51
51
|
}
|
|
52
|
-
function buildFetchListener(pathPrefix, engine, registrationReady) {
|
|
52
|
+
function buildFetchListener(pathPrefix, engine, registrationReady, handlerOptions) {
|
|
53
53
|
return (event) => {
|
|
54
54
|
const delegatedRequest = buildDelegatedRequest(event, pathPrefix);
|
|
55
55
|
if (delegatedRequest === null)
|
|
56
56
|
return;
|
|
57
|
-
event.respondWith(registrationReady.then(() => handleRequest(delegatedRequest, engine)).catch((error) => buildErrorResponse(error instanceof Error ? error : Error(String(error)))));
|
|
57
|
+
event.respondWith(registrationReady.then(() => handleRequest(delegatedRequest, engine, handlerOptions)).catch((error) => buildErrorResponse(error instanceof Error ? error : Error(String(error)))));
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
-
function attachListeners(scope, pathPrefix, periodicSyncTag, engine, scheduler, registrationReady) {
|
|
61
|
-
const fetchListener = buildFetchListener(pathPrefix, engine, registrationReady), periodicSyncListener = (event) => {
|
|
60
|
+
function attachListeners(scope, pathPrefix, periodicSyncTag, engine, scheduler, registrationReady, handlerOptions) {
|
|
61
|
+
const fetchListener = buildFetchListener(pathPrefix, engine, registrationReady, handlerOptions), periodicSyncListener = (event) => {
|
|
62
62
|
if (event.tag !== periodicSyncTag)
|
|
63
63
|
return;
|
|
64
64
|
event.waitUntil(registrationReady.then(() => scheduler.tick()));
|
|
@@ -104,7 +104,7 @@ export function setupServiceWorker(options = {}) {
|
|
|
104
104
|
await engine.recoverAll();
|
|
105
105
|
}
|
|
106
106
|
const registrationReady = Promise.resolve().then(runRegistrationAndRecovery);
|
|
107
|
-
attachListeners(scope, pathPrefix, periodicSyncTag, engine, scheduler, registrationReady);
|
|
107
|
+
attachListeners(scope, pathPrefix, periodicSyncTag, engine, scheduler, registrationReady, options.handlerOptions);
|
|
108
108
|
const result = {
|
|
109
109
|
engine,
|
|
110
110
|
storage,
|