@lostgradient/weft 0.11.0 → 0.12.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 (83) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/generated/operation-catalog.snapshot.json +4671 -0
  3. package/dist/cli/generated/operation-client.generated.d.ts +1 -0
  4. package/dist/cli/index.js +1 -0
  5. package/dist/cli-main.js +227 -401
  6. package/dist/client/http-operations.js +7 -1
  7. package/dist/client/http-request.d.ts +23 -1
  8. package/dist/client/http-request.js +12 -3
  9. package/dist/client/index.d.ts +2 -0
  10. package/dist/client/index.js +7 -0
  11. package/dist/connection.d.ts +8 -0
  12. package/dist/connection.js +41 -17
  13. package/dist/core/engine/callback-creators-core.js +2 -1
  14. package/dist/core/engine/index.d.ts +43 -0
  15. package/dist/core/engine/lifecycle/shared.d.ts +25 -0
  16. package/dist/core/engine/lifecycle/transition.js +19 -9
  17. package/dist/core/types/definition-schema-to-json.d.ts +11 -0
  18. package/dist/core/types/definition-schema-to-json.js +11 -2
  19. package/dist/http.js +2 -0
  20. package/dist/indexeddb.js +1 -0
  21. package/dist/json-schema.js +3 -3
  22. package/dist/mcp/cli.js +63 -61
  23. package/dist/mcp/http.d.ts +7 -2
  24. package/dist/mcp/session.d.ts +3 -3
  25. package/dist/mcp/session.js +3 -2
  26. package/dist/mcp/stdio.d.ts +7 -2
  27. package/dist/observability/index.js +2 -2
  28. package/dist/runtime/portable.d.ts +33 -0
  29. package/dist/runtime/portable.js +17 -6
  30. package/dist/server/engine-event-feed-backend.d.ts +22 -3
  31. package/dist/server/fleet-event-feed.d.ts +65 -0
  32. package/dist/server/handler/index.d.ts +11 -2
  33. package/dist/server/handler/index.js +7 -0
  34. package/dist/server/handler.js +1 -56
  35. package/dist/server/index.d.ts +12 -1
  36. package/dist/server/index.js +87 -64
  37. package/dist/server/operations/submit-review-decision.d.ts +2 -0
  38. package/dist/server/operations/submit-review-decision.js +19 -2
  39. package/dist/server/replay-live-feed-internals.d.ts +19 -0
  40. package/dist/server/replay-live-feed-internals.js +76 -0
  41. package/dist/server/runtime/authentication-bridge.d.ts +2 -2
  42. package/dist/server/serve-internals.js +3 -3
  43. package/dist/server/workflow-event-feed.d.ts +100 -11
  44. package/dist/server/workflow-event-feed.js +1 -140
  45. package/dist/service-worker/index.d.ts +8 -2
  46. package/dist/service-worker/index.js +37 -56
  47. package/dist/service-worker/setup.d.ts +6 -2
  48. package/dist/storage/auto.js +1 -1
  49. package/dist/storage/bun-sql.js +3 -3
  50. package/dist/storage/cloudflare-durable-object-sql.d.ts +75 -0
  51. package/dist/storage/cloudflare-durable-object-sql.js +0 -0
  52. package/dist/storage/cloudflare-value-codec.d.ts +65 -0
  53. package/dist/storage/cloudflare-value-codec.js +46 -0
  54. package/dist/storage/cloudflare.d.ts +140 -0
  55. package/dist/storage/cloudflare.js +127 -0
  56. package/dist/storage/compressed-storage.js +1 -1
  57. package/dist/storage/http.js +200 -2
  58. package/dist/storage/index.d.ts +1 -0
  59. package/dist/storage/indexeddb.js +311 -1
  60. package/dist/storage/interface.js +1 -1
  61. package/dist/storage/lmdb.js +1 -1
  62. package/dist/storage/memory.js +1 -1
  63. package/dist/storage/neon.js +4 -4
  64. package/dist/storage/node-sqlite.js +3 -3
  65. package/dist/storage/postgres-key-value-queries.d.ts +6 -0
  66. package/dist/storage/postgres-key-value-queries.js +2 -3
  67. package/dist/storage/postgres.js +4 -4
  68. package/dist/storage/resolve.js +1 -1
  69. package/dist/storage/scoped-storage.js +1 -1
  70. package/dist/storage/sql-identifier.d.ts +35 -0
  71. package/dist/storage/sql-identifier.js +5 -0
  72. package/dist/storage/sqlite-key-value-queries.d.ts +11 -6
  73. package/dist/storage/sqlite-key-value-queries.js +3 -3
  74. package/dist/storage/testing.js +1 -1
  75. package/dist/storage/turso.js +2 -2
  76. package/dist/storage/typed-storage.js +138 -2
  77. package/dist/storage/web-extension.js +298 -1
  78. package/dist/testing/index.js +21 -60
  79. package/dist/version.d.ts +1 -1
  80. package/dist/version.js +1 -1
  81. package/dist/web-extension.js +1 -0
  82. package/dist/worker/protocol.js +1 -1
  83. package/package.json +8 -3
@@ -5,6 +5,7 @@ declare const submitReviewDecisionInput: z.ZodObject<{
5
5
  decision: z.ZodOptional<z.ZodUnknown>;
6
6
  reviewer: z.ZodOptional<z.ZodUnknown>;
7
7
  feedback: z.ZodOptional<z.ZodUnknown>;
8
+ sectionDecisions: z.ZodOptional<z.ZodUnknown>;
8
9
  workflowId: z.ZodOptional<z.ZodUnknown>;
9
10
  }, z.core.$strip>;
10
11
  declare const submitReviewDecisionOutput: z.ZodObject<{
@@ -17,6 +18,7 @@ export declare const submitReviewDecisionOperation: import("../operation-catalog
17
18
  decision?: unknown;
18
19
  reviewer?: unknown;
19
20
  feedback?: unknown;
21
+ sectionDecisions?: unknown;
20
22
  workflowId?: unknown;
21
23
  }, {
22
24
  ok: true;
@@ -6,15 +6,28 @@ const VALID_DECISIONS = [
6
6
  "approved",
7
7
  "rejected",
8
8
  "needs-changes"
9
- ], submitReviewDecisionInput = z.object({
9
+ ], sectionDecisionsSchema = z.record(z.string(), z.enum(["approved", "rejected"])), submitReviewDecisionInput = z.object({
10
10
  reviewId: z.string().min(1),
11
11
  decision: z.unknown().optional(),
12
12
  reviewer: z.unknown().optional(),
13
13
  feedback: z.unknown().optional(),
14
+ sectionDecisions: z.unknown().optional(),
14
15
  workflowId: z.unknown().optional()
15
16
  }), submitReviewDecisionOutput = z.object({
16
17
  ok: z.literal(!0)
17
18
  });
19
+ function parseSectionDecisions(sectionDecisions) {
20
+ if (sectionDecisions === void 0)
21
+ return;
22
+ const parsed = sectionDecisionsSchema.safeParse(sectionDecisions);
23
+ if (!parsed.success)
24
+ throw {
25
+ code: "InvalidParams",
26
+ message: 'Field "sectionDecisions" must be a record mapping section names to "approved" or "rejected"',
27
+ data: { issues: [] }
28
+ };
29
+ return parsed.data;
30
+ }
18
31
  function validateReviewDecisionInput(input) {
19
32
  if (typeof input.decision !== "string" || typeof input.reviewer !== "string")
20
33
  throw {
@@ -34,9 +47,11 @@ function validateReviewDecisionInput(input) {
34
47
  message: 'Field "feedback" must be a string when provided',
35
48
  data: { issues: [] }
36
49
  };
37
- const { decision, reviewer } = input, reviewOptions = { decision, reviewer };
50
+ const sectionDecisions = parseSectionDecisions(input.sectionDecisions), decision = input.decision, reviewer = input.reviewer, reviewOptions = { decision, reviewer };
38
51
  if (typeof input.feedback === "string")
39
52
  reviewOptions.feedback = input.feedback;
53
+ if (sectionDecisions !== void 0)
54
+ reviewOptions.sectionDecisions = sectionDecisions;
40
55
  if (typeof input.workflowId === "string")
41
56
  reviewOptions.workflowId = input.workflowId;
42
57
  return { decision, reviewer, reviewOptions };
@@ -94,6 +109,7 @@ export const submitReviewDecisionRestBinding = {
94
109
  decision: { kind: "body-field", bodyField: "decision" },
95
110
  reviewer: { kind: "body-field", bodyField: "reviewer" },
96
111
  feedback: { kind: "body-field", bodyField: "feedback" },
112
+ sectionDecisions: { kind: "body-field", bodyField: "sectionDecisions" },
97
113
  workflowId: { kind: "body-field", bodyField: "workflowId" }
98
114
  },
99
115
  extractInput: async (request, pathParams, context) => {
@@ -107,6 +123,7 @@ export const submitReviewDecisionRestBinding = {
107
123
  decision: record.decision,
108
124
  reviewer: record.reviewer,
109
125
  feedback: record.feedback,
126
+ sectionDecisions: record.sectionDecisions,
110
127
  workflowId: record.workflowId
111
128
  };
112
129
  },
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Private replay/live-drain algorithm shared by every `createReplayLiveFeed()`
3
+ * instance (`workflow-event-feed.ts`'s per-workflow feed and
4
+ * `fleet-event-feed.ts`'s fleet-wide feed). Split out of
5
+ * `workflow-event-feed.ts` purely to keep that file under the repository's
6
+ * implementation-file line limit — nothing here is public API.
7
+ *
8
+ * @module server/replay-live-feed-internals
9
+ */
10
+ import type { ReplayLiveFeedBackend, ReplayLiveSubscribeOptions, SequencedEventEnvelope } from './workflow-event-feed.ts';
11
+ /** Thrown when a replay window exceeds the caller's configured limit. */
12
+ export declare class ReplayWindowExceededError extends Error {
13
+ readonly count: number;
14
+ readonly limit: number;
15
+ constructor(count: number, limit: number);
16
+ }
17
+ export declare function replayUpTo<TEnvelope extends SequencedEventEnvelope>(backend: ReplayLiveFeedBackend<TEnvelope>, afterSequence: number, snapshot: number, signal: AbortSignal | undefined, replayOptions: ReplayLiveSubscribeOptions<TEnvelope> | undefined): AsyncIterable<TEnvelope>;
18
+ export declare function shouldDeliverEnvelope<TEnvelope extends SequencedEventEnvelope>(envelope: TEnvelope, replayOptions: ReplayLiveSubscribeOptions<TEnvelope> | undefined): boolean;
19
+ export declare function drainLive<TEnvelope extends SequencedEventEnvelope>(buffer: TEnvelope[], snapshot: number, signal: AbortSignal | undefined, overflowed: () => boolean, installWaker: (fn: (() => void) | null) => void): AsyncIterable<TEnvelope>;
@@ -0,0 +1,76 @@
1
+ export class ReplayWindowExceededError extends Error {
2
+ count;
3
+ limit;
4
+ constructor(count, limit) {
5
+ super(`Replay window is ${count} events; maximum is ${limit}.`);
6
+ this.count = count;
7
+ this.limit = limit;
8
+ this.name = "ReplayWindowExceededError";
9
+ }
10
+ }
11
+ export async function* replayUpTo(backend, afterSequence, snapshot, signal, replayOptions) {
12
+ let replayCount = 0;
13
+ for await (const envelope of backend.replay({ afterSequence })) {
14
+ if (envelope.sequence > snapshot)
15
+ break;
16
+ if (signal?.aborted)
17
+ return;
18
+ if (!shouldDeliverEnvelope(envelope, replayOptions))
19
+ continue;
20
+ if (shouldCountReplayEnvelope(envelope, replayOptions)) {
21
+ replayCount += 1;
22
+ const replayLimit = replayOptions?.replayLimit;
23
+ if (replayLimit !== void 0 && replayCount > replayLimit)
24
+ throw createReplayLimitError(replayOptions, replayCount, replayLimit);
25
+ }
26
+ yield envelope;
27
+ }
28
+ }
29
+ export function shouldDeliverEnvelope(envelope, replayOptions) {
30
+ return replayOptions?.filterEnvelope?.(envelope) ?? !0;
31
+ }
32
+ function shouldCountReplayEnvelope(envelope, replayOptions) {
33
+ return replayOptions?.countReplayEnvelope?.(envelope) ?? !0;
34
+ }
35
+ function createReplayLimitError(replayOptions, count, limit) {
36
+ return replayOptions?.createReplayLimitError?.(count, limit) ?? new ReplayWindowExceededError(count, limit);
37
+ }
38
+ function flushPendingBuffer(buffer, watermark) {
39
+ const batch = [];
40
+ let newWatermark = watermark, head = buffer.shift();
41
+ while (head !== void 0) {
42
+ if (head.sequence > newWatermark) {
43
+ batch.push(head);
44
+ newWatermark = head.sequence;
45
+ }
46
+ head = buffer.shift();
47
+ }
48
+ return { batch, newWatermark };
49
+ }
50
+ async function armAndWait(buffer, overflowed, signal, installWaker) {
51
+ const armed = new Promise((resolve) => {
52
+ installWaker(resolve);
53
+ });
54
+ if (buffer.length > 0 || overflowed() || signal?.aborted) {
55
+ installWaker(null);
56
+ return;
57
+ }
58
+ await armed;
59
+ }
60
+ export async function* drainLive(buffer, snapshot, signal, overflowed, installWaker) {
61
+ let watermark = snapshot;
62
+ while (!0) {
63
+ if (signal?.aborted || overflowed())
64
+ break;
65
+ const { batch, newWatermark } = flushPendingBuffer(buffer, watermark);
66
+ watermark = newWatermark;
67
+ for (const envelope of batch) {
68
+ if (signal?.aborted || overflowed())
69
+ return;
70
+ yield envelope;
71
+ }
72
+ if (batch.length > 0)
73
+ continue;
74
+ await armAndWait(buffer, overflowed, signal, installWaker);
75
+ }
76
+ }
@@ -1,12 +1,12 @@
1
1
  import type { ServerWebSocket } from 'bun';
2
- import type { Engine } from '../../core/engine.ts';
2
+ import type { RegistryAgnosticEngine } from '../../core/engine.ts';
3
3
  import type { PrometheusExporter } from '../../observability/metrics.ts';
4
4
  import type { AuthConfig } from '../authentication.ts';
5
5
  import { type WebSocketData } from '../json-rpc-websocket-runtime.ts';
6
6
  import type { OpenApiSecuritySchemeName } from '../openapi.ts';
7
7
  import type { ServerContext } from './context.ts';
8
8
  type ServerFetchOptions = {
9
- engine: Engine;
9
+ engine: RegistryAgnosticEngine;
10
10
  prometheusExporter?: PrometheusExporter;
11
11
  discoveryInfo?: import('../discovery-info.ts').DiscoveryInfo;
12
12
  publicOrigin?: string;
@@ -85,7 +85,7 @@ export function resolveNetworkConfig(options) {
85
85
  return { port, hostname, development, tlsOptions, serverOptions, serverMetricsCollector };
86
86
  }
87
87
  export function buildServerContext(options, serverMetricsCollector) {
88
- const workerRegistry = new WorkerRegistry(options.routingPolicy !== void 0 ? { policy: options.routingPolicy } : void 0), taskQueue = new TaskQueue(options.schedulingPolicy !== void 0 ? { schedulingPolicy: options.schedulingPolicy } : void 0), eventFeedBackend = createEngineEventFeedBackend(options.engine);
88
+ const workerRegistry = new WorkerRegistry(options.routingPolicy !== void 0 ? { policy: options.routingPolicy } : void 0), taskQueue = new TaskQueue(options.schedulingPolicy !== void 0 ? { schedulingPolicy: options.schedulingPolicy } : void 0), engine = options.engine, eventFeedBackend = createEngineEventFeedBackend(engine);
89
89
  return {
90
90
  registry: workerRegistry,
91
91
  taskQueue,
@@ -112,12 +112,12 @@ export function buildServerContext(options, serverMetricsCollector) {
112
112
  workflowEventFeed: createWorkflowEventFeed(eventFeedBackend),
113
113
  fleetEventFeed: createFleetEventFeed(options.engine.storage),
114
114
  activeJsonRpcSessions: new Set,
115
- mcpSessionManager: createMcpSessionManager(options.engine),
115
+ mcpSessionManager: createMcpSessionManager(engine),
116
116
  authenticatorPromise: options.auth ? createAuthenticator(options.auth) : null,
117
117
  rateLimiter: options.rateLimit ? createRateLimiter(options.rateLimit) : null,
118
118
  visibilityPollMs: options.visibilityPollIntervalMs ?? 5000,
119
119
  workerReconnectGracePeriodMs: clampWorkerReconnectGracePeriod(options.workerReconnectGracePeriodMs),
120
- payloadSizeMaxBytes: getEnginePayloadSizeMaxBytes(options.engine),
120
+ payloadSizeMaxBytes: getEnginePayloadSizeMaxBytes(engine),
121
121
  pendingWorkerRequeues: new Map,
122
122
  scanRunning: !1,
123
123
  processingOperations: new Set,
@@ -1,7 +1,20 @@
1
1
  import type { WeftEventMap } from '../core/events.ts';
2
2
  /** Discriminator string carried on every feed envelope. */
3
3
  export type FeedEventKind = keyof WeftEventMap | 'workflow:checkpoint' | 'stream:chunk' | (string & {});
4
- /** Opaque cursor. Only `encodeCursor` / `decodeCursor` know the format. */
4
+ /**
5
+ * Opaque cursor into a workflow or fleet event feed. Only `encodeCursor` /
6
+ * `decodeCursor` know the format — treat it as an opaque string, pass it back
7
+ * as `fromCursor` to resume a feed, and never parse it.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import type { Cursor, EventEnvelope } from '@lostgradient/weft/server/handler';
12
+ *
13
+ * declare const envelope: EventEnvelope;
14
+ * const lastCursor: Cursor = envelope.cursor;
15
+ * void lastCursor;
16
+ * ```
17
+ */
5
18
  export type Cursor = string;
6
19
  /** Encode a non-negative integer sequence as an opaque cursor. */
7
20
  export declare function encodeCursor(sequence: number): Cursor;
@@ -11,6 +24,22 @@ export declare function encodeCursor(sequence: number): Cursor;
11
24
  */
12
25
  export declare function decodeCursor(cursor: Cursor): number | null;
13
26
  export type EventSelector = 'events' | 'tokens';
27
+ /**
28
+ * A single committed record from a workflow's event feed — the `events`
29
+ * (durable log entries, e.g. `workflow:checkpoint`) or `tokens` (streamed
30
+ * output chunks) selector, distinguished by `selector`. Returned by
31
+ * `WorkflowEventFeed.replay()` / `WorkflowEventFeed.subscribe()`, and
32
+ * consumed directly by the `/v1/workflows/:id/events/sse` REST route.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import type { EventEnvelope } from '@lostgradient/weft/server/handler';
37
+ *
38
+ * declare const envelope: EventEnvelope;
39
+ * console.log(envelope.selector); // 'events' | 'tokens'
40
+ * console.log(envelope.kind); // e.g. 'workflow:checkpoint'
41
+ * ```
42
+ */
14
43
  export type EventEnvelope = {
15
44
  readonly kind: FeedEventKind;
16
45
  readonly workflowId: string;
@@ -20,6 +49,26 @@ export type EventEnvelope = {
20
49
  readonly emittedAtMs: number;
21
50
  readonly payload: unknown;
22
51
  };
52
+ /**
53
+ * The durable source a `WorkflowEventFeed` replays and subscribes against.
54
+ * Most callers never implement this directly — `createEngineEventFeedBackend()`
55
+ * builds the production, `Engine`-backed implementation. Implement it
56
+ * yourself only to back a feed with a non-`Engine` source (e.g. a test
57
+ * double).
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { Engine, MemoryStorage } from '@lostgradient/weft';
62
+ * import {
63
+ * createEngineEventFeedBackend,
64
+ * type WorkflowEventFeedBackend,
65
+ * } from '@lostgradient/weft/server/handler';
66
+ *
67
+ * const engine = new Engine({ storage: new MemoryStorage() });
68
+ * const backend: WorkflowEventFeedBackend = createEngineEventFeedBackend(engine);
69
+ * void backend;
70
+ * ```
71
+ */
23
72
  export type WorkflowEventFeedBackend = {
24
73
  replay(options: {
25
74
  workflowId: string;
@@ -29,6 +78,29 @@ export type WorkflowEventFeedBackend = {
29
78
  snapshotTailSequence(workflowId: string, selector: EventSelector): Promise<number>;
30
79
  subscribeLive(workflowId: string, selector: EventSelector, listener: (envelope: EventEnvelope) => void): () => void;
31
80
  };
81
+ /**
82
+ * A per-workflow event feed: replay committed history from a cursor, then
83
+ * subscribe for live delivery. This is the shape of
84
+ * `HandlerOptions.workflowEventFeed` — build a real one with
85
+ * `createWorkflowEventFeed()` to drive `/v1/workflows/:id/events/sse`
86
+ * through `handleRequest()` without `serve()`.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * import { Engine, MemoryStorage } from '@lostgradient/weft';
91
+ * import {
92
+ * createEngineEventFeedBackend,
93
+ * createWorkflowEventFeed,
94
+ * type WorkflowEventFeed,
95
+ * } from '@lostgradient/weft/server/handler';
96
+ *
97
+ * const engine = new Engine({ storage: new MemoryStorage() });
98
+ * const workflowEventFeed: WorkflowEventFeed = createWorkflowEventFeed(
99
+ * createEngineEventFeedBackend(engine),
100
+ * );
101
+ * void workflowEventFeed;
102
+ * ```
103
+ */
32
104
  export type WorkflowEventFeed = {
33
105
  replay(options: {
34
106
  workflowId: string;
@@ -74,15 +146,32 @@ export type ReplayLiveSubscribeOptions<TEnvelope extends SequencedEventEnvelope>
74
146
  createReplayLimitError?: (count: number, limit: number) => unknown;
75
147
  onReplayComplete?: () => void;
76
148
  };
77
- export declare class ReplayWindowExceededError extends Error {
78
- readonly count: number;
79
- readonly limit: number;
80
- constructor(count: number, limit: number);
81
- }
82
149
  export declare function createReplayLiveFeed<TEnvelope extends SequencedEventEnvelope>(backend: ReplayLiveFeedBackend<TEnvelope>, options?: WorkflowEventFeedOptions): ReplayLiveFeed<TEnvelope>;
150
+ /**
151
+ * Build a `WorkflowEventFeed` over the given backend. Pass the result as
152
+ * `HandlerOptions.workflowEventFeed` to drive `/v1/workflows/:id/events/sse`
153
+ * through `handleRequest()` directly, without `serve()`. Call once and share
154
+ * the returned feed across every workflow and transport that needs it —
155
+ * `createWorkflowEventFeed()` itself holds no per-workflow state.
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * import { Engine, MemoryStorage } from '@lostgradient/weft';
160
+ * import {
161
+ * createEngineEventFeedBackend,
162
+ * createWorkflowEventFeed,
163
+ * handleRequest,
164
+ * type HandlerOptions,
165
+ * } from '@lostgradient/weft/server/handler';
166
+ *
167
+ * const engine = new Engine({ storage: new MemoryStorage() });
168
+ * const workflowEventFeed = createWorkflowEventFeed(createEngineEventFeedBackend(engine));
169
+ * const options: HandlerOptions = { workflowEventFeed };
170
+ *
171
+ * async function handleWorkflowEventsSse(request: Request): Promise<Response> {
172
+ * return handleRequest(request, engine, options);
173
+ * }
174
+ * void handleWorkflowEventsSse;
175
+ * ```
176
+ */
83
177
  export declare function createWorkflowEventFeed(backend: WorkflowEventFeedBackend, options?: WorkflowEventFeedOptions): WorkflowEventFeed;
84
- export type InMemoryEventBackend = WorkflowEventFeedBackend & {
85
- append(envelope: EventEnvelope): Promise<void>;
86
- emitLive(envelope: EventEnvelope): Promise<void>;
87
- };
88
- export declare function createInMemoryEventBackend(): InMemoryEventBackend;
@@ -1,3 +1,4 @@
1
+ import { drainLive, replayUpTo, shouldDeliverEnvelope } from "./replay-live-feed-internals.js";
1
2
  const CURSOR_PATTERN = /^(?:-1|\d+)$/;
2
3
  export function encodeCursor(sequence) {
3
4
  if (!Number.isInteger(sequence) || sequence < 0)
@@ -18,17 +19,6 @@ function decodeCursorOrThrow(cursor) {
18
19
  throw Error("Invalid cursor");
19
20
  return sequence;
20
21
  }
21
-
22
- export class ReplayWindowExceededError extends Error {
23
- count;
24
- limit;
25
- constructor(count, limit) {
26
- super(`Replay window is ${count} events; maximum is ${limit}.`);
27
- this.count = count;
28
- this.limit = limit;
29
- this.name = "ReplayWindowExceededError";
30
- }
31
- }
32
22
  const DEFAULT_LIVE_BUFFER_SIZE = 1000;
33
23
  export function createReplayLiveFeed(backend, options) {
34
24
  const bufferSize = options?.liveBufferSize ?? DEFAULT_LIVE_BUFFER_SIZE;
@@ -138,132 +128,3 @@ export function createWorkflowEventFeed(backend, options) {
138
128
  dispose() {}
139
129
  };
140
130
  }
141
- async function* replayUpTo(backend, afterSequence, snapshot, signal, replayOptions) {
142
- let replayCount = 0;
143
- for await (const envelope of backend.replay({ afterSequence })) {
144
- if (envelope.sequence > snapshot)
145
- break;
146
- if (signal?.aborted)
147
- return;
148
- if (!shouldDeliverEnvelope(envelope, replayOptions))
149
- continue;
150
- if (shouldCountReplayEnvelope(envelope, replayOptions)) {
151
- replayCount += 1;
152
- const replayLimit = replayOptions?.replayLimit;
153
- if (replayLimit !== void 0 && replayCount > replayLimit)
154
- throw createReplayLimitError(replayOptions, replayCount, replayLimit);
155
- }
156
- yield envelope;
157
- }
158
- }
159
- function shouldDeliverEnvelope(envelope, replayOptions) {
160
- return replayOptions?.filterEnvelope?.(envelope) ?? !0;
161
- }
162
- function shouldCountReplayEnvelope(envelope, replayOptions) {
163
- return replayOptions?.countReplayEnvelope?.(envelope) ?? !0;
164
- }
165
- function createReplayLimitError(replayOptions, count, limit) {
166
- return replayOptions?.createReplayLimitError?.(count, limit) ?? new ReplayWindowExceededError(count, limit);
167
- }
168
- function flushPendingBuffer(buffer, watermark) {
169
- const batch = [];
170
- let newWatermark = watermark, head = buffer.shift();
171
- while (head !== void 0) {
172
- if (head.sequence > newWatermark) {
173
- batch.push(head);
174
- newWatermark = head.sequence;
175
- }
176
- head = buffer.shift();
177
- }
178
- return { batch, newWatermark };
179
- }
180
- async function armAndWait(buffer, overflowed, signal, installWaker) {
181
- const armed = new Promise((resolve) => {
182
- installWaker(resolve);
183
- });
184
- if (buffer.length > 0 || overflowed() || signal?.aborted) {
185
- installWaker(null);
186
- return;
187
- }
188
- await armed;
189
- }
190
- async function* drainLive(buffer, snapshot, signal, overflowed, installWaker) {
191
- let watermark = snapshot;
192
- while (!0) {
193
- if (signal?.aborted || overflowed())
194
- break;
195
- const { batch, newWatermark } = flushPendingBuffer(buffer, watermark);
196
- watermark = newWatermark;
197
- for (const envelope of batch) {
198
- if (signal?.aborted || overflowed())
199
- return;
200
- yield envelope;
201
- }
202
- if (batch.length > 0)
203
- continue;
204
- await armAndWait(buffer, overflowed, signal, installWaker);
205
- }
206
- }
207
- function bucketKey(workflowId, selector) {
208
- return `${workflowId}:${selector}`;
209
- }
210
- export function createInMemoryEventBackend() {
211
- const storage = new Map, listeners = new Map;
212
- function fireLive(envelope) {
213
- const set = listeners.get(bucketKey(envelope.workflowId, envelope.selector));
214
- if (!set)
215
- return;
216
- for (const listener of set)
217
- try {
218
- listener(envelope);
219
- } catch {}
220
- }
221
- return {
222
- async* replay(options) {
223
- const bucket = storage.get(bucketKey(options.workflowId, options.selector));
224
- if (!bucket)
225
- return;
226
- const sorted = [...bucket].toSorted((a, b) => a.sequence - b.sequence);
227
- for (const envelope of sorted)
228
- if (envelope.sequence > options.afterSequence)
229
- yield envelope;
230
- },
231
- async snapshotTailSequence(workflowId, selector) {
232
- const bucket = storage.get(bucketKey(workflowId, selector));
233
- if (!bucket || bucket.length === 0)
234
- return -1;
235
- let max = -1;
236
- for (const envelope of bucket)
237
- if (envelope.sequence > max)
238
- max = envelope.sequence;
239
- return max;
240
- },
241
- subscribeLive(workflowId, selector, listener) {
242
- const k = bucketKey(workflowId, selector);
243
- let set = listeners.get(k);
244
- if (!set) {
245
- set = new Set;
246
- listeners.set(k, set);
247
- }
248
- set.add(listener);
249
- return () => {
250
- set?.delete(listener);
251
- if (set && set.size === 0)
252
- listeners.delete(k);
253
- };
254
- },
255
- async append(envelope) {
256
- const k = bucketKey(envelope.workflowId, envelope.selector);
257
- let bucket = storage.get(k);
258
- if (!bucket) {
259
- bucket = [];
260
- storage.set(k, bucket);
261
- }
262
- bucket.push(envelope);
263
- fireLive(envelope);
264
- },
265
- async emitLive(envelope) {
266
- fireLive(envelope);
267
- }
268
- };
269
- }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module service-worker
8
8
  */
9
- import type { Engine } from '../core/engine';
9
+ import type { RegistryAgnosticEngine } from '../core/engine';
10
10
  import type { ServiceWorkerScheduler } from './scheduler.ts';
11
11
  import type { MinimalExtendableEvent, MinimalFetchEvent, MinimalPeriodicSyncEvent } from './shared.ts';
12
12
  export { ServiceWorkerScheduler } from './scheduler.ts';
@@ -36,7 +36,13 @@ export type { MinimalExtendableEvent, MinimalFetchEvent, MinimalPeriodicSyncEven
36
36
  * ```
37
37
  */
38
38
  export interface ServiceWorkerOptions {
39
- engine: Engine;
39
+ /**
40
+ * The engine this handler delegates workflow requests to. Typed as
41
+ * {@link RegistryAgnosticEngine} (see its JSDoc) rather than the plain
42
+ * default `Engine`, so both `new Engine({ storage })` and
43
+ * `Engine.create({ workflows })` type-check here directly.
44
+ */
45
+ engine: RegistryAgnosticEngine;
40
46
  pathPrefix?: string;
41
47
  }
42
48
  /**