@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.
Files changed (107) hide show
  1. package/README.md +3 -8
  2. package/dist/alerting/alert-manager.d.ts +2 -0
  3. package/dist/alerting/alert-manager.js +3 -0
  4. package/dist/cli/api.js +5 -19
  5. package/dist/cli/console.d.ts +12 -0
  6. package/dist/cli/console.js +62 -0
  7. package/dist/cli/generated/operation-catalog.snapshot.json +1061 -90
  8. package/dist/cli/generated/operation-client.generated.d.ts +29 -23
  9. package/dist/cli/generated/operation-client.generated.js +2 -0
  10. package/dist/cli/help-text.d.ts +1 -1
  11. package/dist/cli/help-text.js +1 -0
  12. package/dist/cli/index.d.ts +1 -0
  13. package/dist/cli/index.js +1 -0
  14. package/dist/cli/json-input.d.ts +20 -0
  15. package/dist/cli/json-input.js +33 -0
  16. package/dist/cli/parse-arguments.js +2 -0
  17. package/dist/cli/schedule.js +7 -9
  18. package/dist/cli/types.d.ts +1 -0
  19. package/dist/cli/workflow-commands.js +5 -25
  20. package/dist/cli-main.js +4 -2
  21. package/dist/core/aggregate-validation.d.ts +2 -5
  22. package/dist/core/aggregate-validation.js +2 -10
  23. package/dist/core/atomic-state.js +21 -24
  24. package/dist/core/engine/index.d.ts +3 -0
  25. package/dist/core/engine/index.js +3 -0
  26. package/dist/core/engine/schedules.js +4 -1
  27. package/dist/core/inline-execution-strategy.js +8 -46
  28. package/dist/core/list-filter-validation.d.ts +2 -5
  29. package/dist/core/list-filter-validation.js +2 -10
  30. package/dist/core/types/schedules.d.ts +13 -31
  31. package/dist/core/validation-issues.d.ts +13 -0
  32. package/dist/core/validation-issues.js +9 -0
  33. package/dist/diagnostics/recommendations.js +1 -9
  34. package/dist/http.js +2 -2
  35. package/dist/indexeddb.js +1 -1
  36. package/dist/mcp/session.js +2 -10
  37. package/dist/mcp/tools.js +2 -10
  38. package/dist/mcp/tuple-list-equality.d.ts +1 -0
  39. package/dist/mcp/tuple-list-equality.js +12 -0
  40. package/dist/server/authorization-scope.d.ts +1 -1
  41. package/dist/server/authorization-scope.js +0 -2
  42. package/dist/server/dashboard-assets.d.ts +26 -0
  43. package/dist/server/dashboard-assets.js +95 -0
  44. package/dist/server/index.d.ts +10 -1
  45. package/dist/server/index.js +11 -2
  46. package/dist/server/interactive-operations.js +1 -0
  47. package/dist/server/operation-catalog/types.d.ts +1 -1
  48. package/dist/server/operation-registry.d.ts +2 -31
  49. package/dist/server/operations/bulk-filter-helpers.d.ts +17 -17
  50. package/dist/server/operations/bulk-filter-helpers.js +16 -37
  51. package/dist/server/operations/bulk-mutate-workflow-tags.d.ts +27 -27
  52. package/dist/server/operations/bulk-signal-workflows.d.ts +26 -26
  53. package/dist/server/operations/create-schedule.js +26 -65
  54. package/dist/server/operations/event-stream-contracts.d.ts +20 -0
  55. package/dist/server/operations/event-stream-contracts.js +67 -0
  56. package/dist/server/operations/fleet-events-sse.js +21 -50
  57. package/dist/server/operations/fleet-events-subscription.js +8 -12
  58. package/dist/server/operations/list-alerts.d.ts +26 -0
  59. package/dist/server/operations/list-alerts.js +54 -0
  60. package/dist/server/operations/list-workflows.d.ts +43 -43
  61. package/dist/server/operations/list-workflows.js +9 -30
  62. package/dist/server/operations/purge-workflows.d.ts +11 -11
  63. package/dist/server/operations/schedule-faults.d.ts +9 -1
  64. package/dist/server/operations/schedule-faults.js +51 -0
  65. package/dist/server/operations/schedule-rest-body.d.ts +11 -0
  66. package/dist/server/operations/schedule-rest-body.js +28 -0
  67. package/dist/server/operations/update-schedule.js +13 -72
  68. package/dist/server/operations/workflow-events-sse.js +29 -67
  69. package/dist/server/operations/workflow-events-subscription.js +9 -27
  70. package/dist/server/rest-bindings.js +3 -0
  71. package/dist/server/runtime/task-polling.js +3 -5
  72. package/dist/server/task-state.d.ts +2 -8
  73. package/dist/server/task-state.js +10 -7
  74. package/dist/service-worker/index.d.ts +4 -1
  75. package/dist/service-worker/index.js +2 -2
  76. package/dist/service-worker/setup.d.ts +20 -0
  77. package/dist/service-worker/setup.js +5 -5
  78. package/dist/storage/bun-sql.js +65 -80
  79. package/dist/storage/conditional-batch-body.d.ts +7 -0
  80. package/dist/storage/conditional-batch-body.js +14 -0
  81. package/dist/storage/lmdb.js +1 -1
  82. package/dist/storage/memory.js +1 -1
  83. package/dist/storage/neon.js +2 -2
  84. package/dist/storage/node-sqlite-loader.d.ts +2 -2
  85. package/dist/storage/node-sqlite.js +32 -17
  86. package/dist/storage/postgres.js +2 -2
  87. package/dist/storage/resolve.js +1 -1
  88. package/dist/storage/scoped-storage.js +1 -1
  89. package/dist/storage/testing.js +1 -1
  90. package/dist/storage/turso.js +2 -2
  91. package/dist/version.d.ts +1 -1
  92. package/dist/version.js +1 -1
  93. package/dist/web-extension.js +1 -1
  94. package/dist/worker/execute-with-interceptors.d.ts +1 -1
  95. package/dist/worker/execute-with-interceptors.js +2 -2
  96. package/dist/worker/index.js +5 -5
  97. package/dist/worker/long-poll.js +3 -4
  98. package/dist/worker/protocol-messages.d.ts +11 -19
  99. package/dist/worker/protocol-schemas.d.ts +8 -8
  100. package/dist/worker/protocol-schemas.js +4 -4
  101. package/dist/worker/protocol-task-result.d.ts +1 -0
  102. package/dist/worker/protocol-task-result.js +1 -3
  103. package/dist/worker/protocol.js +1 -1
  104. package/dist/worker/registry/types.d.ts +2 -7
  105. package/dist/worker/registry.d.ts +3 -9
  106. package/dist/worker/registry.js +2 -5
  107. 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 { readRestJsonBody } from "../rest-body.js";
5
- import { invalidParamsFault, isOperationFault, shapeRestFault } from "./operation-helpers.js";
6
- import { mapScheduleErrorToFault, validateScheduleInputCadence } from "./schedule-faults.js";
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 = validateScheduleUpdateOptions(input);
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
- let body;
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
- cronExpression: Object.hasOwn(record, "cronExpression") ? record.cronExpression : void 0,
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"), 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
- });
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 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
- };
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 = isClosableWorkflowEventsIterable(output) ? () => output.close() : async () => {
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
- ...isReplayAwareWorkflowEventsIterable(output) ? { ready: output.replayComplete } : {},
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: z.object({
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: async () => {
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 (rawAttemptToken !== void 0 && (typeof rawAttemptToken !== "string" || rawAttemptToken === ""))
37
- return Response.json({ error: "attemptToken must be a non-empty string when present" }, { status: 400 });
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
- if (inflightRecord.attemptToken !== void 0 && validated.attemptToken !== inflightRecord.attemptToken)
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
- * Unique, unguessable token identifying this dispatch attempt. Rotated on every
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
- export async function getTaskState(storage, operationId) {
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 [inflight, queued, resolved] = await Promise.all([
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,