@ewhauser/world-celld 0.1.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.
@@ -0,0 +1,406 @@
1
+ import { CreateEventRequest, Event, Hook, RunCreatedEventRequest, Step, ValidQueueName, WorkflowRun } from "@workflow/world";
2
+ //#region src/worker/router.d.ts
3
+ /** Minimal structural DO namespace type (celld provides the real thing). */
4
+ interface DONamespaceLike {
5
+ idFromName(name: string): {
6
+ toString(): string;
7
+ };
8
+ get(id: {
9
+ toString(): string;
10
+ }): unknown;
11
+ }
12
+ interface WorkerEnv {
13
+ WORKFLOW_DB: DONamespaceLike;
14
+ WORKFLOW_STREAMS: DONamespaceLike;
15
+ WORKFLOW_INDEX: DONamespaceLike;
16
+ WORKFLOW_QUEUE: DONamespaceLike;
17
+ WORLD_SECRET?: string;
18
+ }
19
+ declare function createRouter(env: WorkerEnv): (request: Request) => Promise<Response>;
20
+ //#endregion
21
+ //#region src/apply-event.d.ts
22
+ interface ApplyEventRequest {
23
+ /** The effective run id (already resolved / generated by the caller). */
24
+ runId: string;
25
+ data: CreateEventRequest | RunCreatedEventRequest;
26
+ /**
27
+ * Current holder of the hook token per the global KV token index
28
+ * (hook_created only). `null` means the token is unclaimed.
29
+ */
30
+ tokenHolder?: {
31
+ runId: string;
32
+ hookId: string;
33
+ } | null;
34
+ }
35
+ type ApplyEventErrorCode = 'RUN_NOT_FOUND' | 'STEP_NOT_FOUND' | 'HOOK_NOT_FOUND' | 'ENTITY_CONFLICT' | 'RUN_EXPIRED' | 'TOO_EARLY' | 'RUN_NOT_SUPPORTED' | 'LEGACY_RUN_NOT_SUPPORTED';
36
+ interface ApplyEventFailure {
37
+ ok: false;
38
+ code: ApplyEventErrorCode;
39
+ message: string;
40
+ /** Seconds until retry is allowed (TOO_EARLY only). */
41
+ retryAfterSeconds?: number;
42
+ /** The run's spec version (RUN_NOT_SUPPORTED only). */
43
+ runSpecVersion?: number;
44
+ }
45
+ interface ApplyEventSuccess {
46
+ ok: true;
47
+ /** Absent for idempotent replays that record no new event. */
48
+ event?: Event;
49
+ run?: WorkflowRun;
50
+ step?: Step;
51
+ hook?: Hook;
52
+ /** Hooks whose tokens must be released from the global KV index. */
53
+ releasedHooks: Array<{
54
+ hookId: string;
55
+ token: string;
56
+ }>;
57
+ /** Set when a run entity was created (run_created or resilient bootstrap). */
58
+ runCreated?: {
59
+ workflowName: string;
60
+ createdAt: Date;
61
+ };
62
+ /** Set when the global KV hook index must be (re)written. */
63
+ hookToIndex?: Hook;
64
+ /** All events (ascending), preloaded for run_started responses. */
65
+ events?: Event[];
66
+ }
67
+ type ApplyEventOutcome = ApplyEventSuccess | ApplyEventFailure;
68
+ //#endregion
69
+ //#region src/queue.d.ts
70
+ type Pathname = 'flow' | 'step';
71
+ /**
72
+ * Enqueue request accepted by a QueueDO cell. The message payload travels
73
+ * pre-encoded with the shared tagged-JSON codec (`body`), so the cell can
74
+ * forward it byte-identical to the app's queue handler without re-encoding.
75
+ */
76
+ interface EnqueueRequest {
77
+ messageId: string;
78
+ queueName: ValidQueueName;
79
+ pathname: Pathname;
80
+ /** `stringify(message)` — the exact bytes POSTed to the app on delivery. */
81
+ body: string;
82
+ idempotencyKey?: string;
83
+ delaySeconds?: number;
84
+ /**
85
+ * Delivery configuration. The cell pins this from its first enqueue (the
86
+ * eve-ambient pattern); a later mismatch is rejected as CONFIG_MISMATCH.
87
+ */
88
+ config: QueueCellConfig;
89
+ }
90
+ interface QueueCellConfig {
91
+ /** Base URL of the app; deliveries POST to `${baseUrl}/.well-known/workflow/v1/{flow|step}`. */
92
+ targetBaseUrl: string;
93
+ queueShards: number;
94
+ }
95
+ type EnqueueOutcome = {
96
+ ok: true;
97
+ messageId: string;
98
+ deduped: boolean;
99
+ } | {
100
+ ok: false;
101
+ code: 'CONFIG_MISMATCH';
102
+ message: string;
103
+ };
104
+ //#endregion
105
+ //#region src/config.d.ts
106
+ interface HookTokenOwner {
107
+ runId: string;
108
+ hookId: string;
109
+ }
110
+ //#endregion
111
+ //#region src/worker/do-base.d.ts
112
+ /**
113
+ * Durable Object base class — Node variant.
114
+ *
115
+ * The cell classes extend `DurableObject` so celld's JS RPC works on their
116
+ * stubs. This module is what Node builds (tests, the `./testing` in-process
117
+ * fleet) resolve; the worker build swaps it for do-base-workerd.ts, which
118
+ * re-exports the real `cloudflare:workers` base (see tsdown.config.ts).
119
+ * Behaviorally identical at this layer: the base only assigns ctx/env.
120
+ */
121
+ declare class DurableObject<TEnv = unknown> {
122
+ readonly ctx: DurableObjectState;
123
+ readonly env: TEnv;
124
+ constructor(ctx: DurableObjectState, env: TEnv);
125
+ }
126
+ //#endregion
127
+ //#region src/worker/durable-objects/IndexDO.d.ts
128
+ /**
129
+ * Global index cell, replacing the Workers KV namespace world-cloudflare
130
+ * uses for run listing and hook token/id lookup.
131
+ *
132
+ * A single cell (named `index`) serializes every read and write. Hook-token
133
+ * reservation, publication, and release use storage transactions so
134
+ * concurrent runs cannot both acquire the same token.
135
+ *
136
+ * The RPC surface matches the storage-layer KVNamespace interface bit-for-bit
137
+ * (including MockKVNamespace's pagination contract: cursor = last returned
138
+ * key, `list_complete` reflects whether further keys exist), so the vendored
139
+ * storage.ts works against it unchanged.
140
+ */
141
+ declare class IndexDO extends DurableObject {
142
+ get(key: string): Promise<string | null>;
143
+ put(key: string, value: string): Promise<void>;
144
+ delete(key: string): Promise<void>;
145
+ list(options?: {
146
+ prefix?: string;
147
+ cursor?: string;
148
+ limit?: number;
149
+ reverse?: boolean;
150
+ }): Promise<{
151
+ keys: Array<{
152
+ name: string;
153
+ }>;
154
+ list_complete: boolean;
155
+ cursor?: string;
156
+ }>;
157
+ reserveHookToken(token: string, owner: HookTokenOwner): Promise<{
158
+ claimed: boolean;
159
+ holder?: HookTokenOwner;
160
+ }>;
161
+ finalizeHookIndexes(token: string, hookId: string, serializedHook: string, owner: HookTokenOwner): Promise<void>;
162
+ releaseHookToken(token: string, owner: HookTokenOwner): Promise<void>;
163
+ deleteHookIndexes(token: string, hookId: string, owner: HookTokenOwner): Promise<void>;
164
+ }
165
+ //#endregion
166
+ //#region src/worker/durable-objects/QueueDO.d.ts
167
+ /**
168
+ * Queue cell: the celld replacement for Cloudflare Queues.
169
+ *
170
+ * One cell (`q:<shard>`) owns the full lifecycle of its messages: durable
171
+ * scheduling via the cell alarm, delivery via outbound fetch (the same x-vqs
172
+ * dialect as the in-process test pump), retries with capped exponential
173
+ * backoff, idempotencyKey dedup, and a dead-letter table.
174
+ *
175
+ * Storage layout (KV-style, ordered scans via zero-padded epoch-ms keys):
176
+ * cfg pinned QueueCellConfig (first-enqueue wins)
177
+ * msg:<messageId> MessageRow
178
+ * due:<paddedMs>:<messageId> schedule index -> messageId
179
+ * inflight:<messageId> crash-recovery deadline (epoch ms)
180
+ * key:<idempotencyKey> active dedup claim -> messageId
181
+ * dlq:<paddedMs>:<messageId> DeadLetterRow
182
+ *
183
+ * celld gotchas designed around:
184
+ * - celld#144 (alarm handlers can overlap): the storage-only claim phase runs
185
+ * inside blockConcurrencyWhile; slow delivery I/O runs outside the gate via
186
+ * waitUntil.
187
+ * - six-failure alarm abandonment: the alarm handler never throws for data
188
+ * conditions — backoff reschedules the alarm instead. rearmAlarm() is the
189
+ * recovery hatch, and every enqueue also re-arms.
190
+ * - deploys restart the fleet: in-flight deliveries die with the isolate; the
191
+ * inflight-deadline sweep on the next alarm redelivers (at-least-once).
192
+ */
193
+ interface MessageRow {
194
+ messageId: string;
195
+ queueName: string;
196
+ pathname: 'flow' | 'step';
197
+ /** Pre-encoded tagged-JSON payload — forwarded byte-identical. */
198
+ body: string;
199
+ /**
200
+ * Delivery target captured at enqueue time. Carried per message (not
201
+ * pinned on the cell) so an app redeploy at a new URL never bricks the
202
+ * queue: old messages deliver (or dead-letter) against their original
203
+ * target, new messages against the new one.
204
+ */
205
+ targetBaseUrl: string;
206
+ idempotencyKey?: string;
207
+ /** Completed delivery attempts. */
208
+ attempt: number;
209
+ enqueuedAt: number;
210
+ lastError?: string;
211
+ }
212
+ interface DeadLetterRow extends MessageRow {
213
+ failedAt: number;
214
+ }
215
+ interface QueueStats {
216
+ pending: number;
217
+ inflight: number;
218
+ deadLetters: number;
219
+ alarmAt: number | null;
220
+ }
221
+ declare class QueueDO extends DurableObject {
222
+ #private;
223
+ enqueue(request: EnqueueRequest): Promise<EnqueueOutcome>;
224
+ alarm(): Promise<void>;
225
+ stats(): Promise<QueueStats>;
226
+ listDeadLetters(params?: {
227
+ limit?: number;
228
+ cursor?: string;
229
+ }): Promise<{
230
+ data: DeadLetterRow[];
231
+ cursor: string | null;
232
+ hasMore: boolean;
233
+ }>;
234
+ /** Move a dead letter back to the live queue (attempt count reset). */
235
+ redriveDeadLetter(messageId: string): Promise<{
236
+ ok: boolean;
237
+ }>;
238
+ purgeDeadLetters(): Promise<{
239
+ purged: number;
240
+ }>;
241
+ /**
242
+ * Recovery hatch for celld's six-failure alarm abandonment: derive the due
243
+ * time again and arm it.
244
+ */
245
+ rearmAlarm(): Promise<{
246
+ alarmAt: number | null;
247
+ }>;
248
+ }
249
+ //#endregion
250
+ //#region src/worker/durable-objects/StreamDO.d.ts
251
+ /**
252
+ * Durable Object backing workflow streams.
253
+ *
254
+ * Two roles, selected by the DO name:
255
+ * - `stream:<name>`: chunk storage for a single stream. A monotonic
256
+ * per-stream counter (maintained transactionally inside the DO) assigns
257
+ * each chunk its index; one chunk per storage key.
258
+ * - `run-streams:<runId>`: registry of stream names owned by a run,
259
+ * powering `listStreamsByRunId`.
260
+ */
261
+ declare class StreamDO extends DurableObject {
262
+ private getMeta;
263
+ /**
264
+ * Append a chunk. The index is allocated inside the transaction, so
265
+ * concurrent writers can never collide or skip.
266
+ */
267
+ writeChunk(data: Uint8Array): Promise<number>;
268
+ /**
269
+ * Close the stream (idempotent). Readers observe `done: true` once all
270
+ * previously written chunks have been consumed.
271
+ */
272
+ closeStream(): Promise<void>;
273
+ /**
274
+ * Read up to `limit` chunks starting at `startIndex` (0-based, inclusive).
275
+ */
276
+ getChunks(params: {
277
+ startIndex: number;
278
+ limit: number;
279
+ }): Promise<{
280
+ chunks: Uint8Array[];
281
+ /** Whether the stream is closed. */
282
+ done: boolean;
283
+ /** Index of the last written chunk, -1 when empty. */
284
+ tailIndex: number;
285
+ }>;
286
+ /**
287
+ * Lightweight stream metadata: last chunk index and completion flag.
288
+ */
289
+ getInfo(): Promise<{
290
+ tailIndex: number;
291
+ done: boolean;
292
+ }>;
293
+ /**
294
+ * Register a stream name against this per-run registry instance.
295
+ */
296
+ registerStream(name: string): Promise<void>;
297
+ /**
298
+ * List stream names registered against this per-run registry instance.
299
+ */
300
+ listStreams(): Promise<string[]>;
301
+ }
302
+ //#endregion
303
+ //#region src/worker/durable-objects/WorkflowRunDO.d.ts
304
+ /**
305
+ * Durable Object for managing a single workflow run's state.
306
+ *
307
+ * All event-sourced writes go through {@link applyEvent} inside a single
308
+ * storage transaction: guard checks, the event append, and the entity
309
+ * mutation are atomic even if the DO is evicted mid-operation.
310
+ *
311
+ * Instances named `claim:<queueName>:<idempotencyKey>` are used purely as
312
+ * queue-message dedup claims (see claimInflight/releaseInflight).
313
+ */
314
+ declare class WorkflowRunDO extends DurableObject {
315
+ private schemaReady;
316
+ /**
317
+ * Lazily ensure schema migrations have run.
318
+ * Memoized so it only runs once per DO instantiation.
319
+ */
320
+ private ensureSchema;
321
+ /**
322
+ * Apply an event: validate, guard, append the event and mutate the entity
323
+ * in ONE storage transaction. Guard failures are returned as structured
324
+ * outcomes (not thrown) because typed errors do not survive the RPC
325
+ * boundary with their `name` intact.
326
+ */
327
+ applyEvent(request: ApplyEventRequest): Promise<ApplyEventOutcome>;
328
+ /**
329
+ * Get the workflow run
330
+ */
331
+ getRun(): Promise<WorkflowRun | null>;
332
+ /**
333
+ * Get a specific step
334
+ */
335
+ getStep(stepId: string): Promise<Step | null>;
336
+ /**
337
+ * List steps with real limit/cursor pagination (cursor = last stepId).
338
+ */
339
+ listSteps(params?: {
340
+ limit?: number;
341
+ cursor?: string;
342
+ sortOrder?: 'asc' | 'desc';
343
+ }): Promise<{
344
+ data: Step[];
345
+ cursor: string | null;
346
+ hasMore: boolean;
347
+ }>;
348
+ /**
349
+ * Get a specific event
350
+ */
351
+ getEvent(eventId: string): Promise<Event | null>;
352
+ /**
353
+ * List events with real limit/cursor pagination. Events are keyed by their
354
+ * monotonic ULID eventId, so storage order == event order.
355
+ */
356
+ listEvents(params?: {
357
+ limit?: number;
358
+ cursor?: string;
359
+ sortOrder?: 'asc' | 'desc';
360
+ }): Promise<{
361
+ data: Event[];
362
+ cursor: string | null;
363
+ hasMore: boolean;
364
+ }>;
365
+ /**
366
+ * List hooks with real limit/cursor pagination (cursor = last hookId).
367
+ */
368
+ listHooks(params?: {
369
+ limit?: number;
370
+ cursor?: string;
371
+ sortOrder?: 'asc' | 'desc';
372
+ }): Promise<{
373
+ data: Hook[];
374
+ cursor: string | null;
375
+ hasMore: boolean;
376
+ }>;
377
+ /**
378
+ * Claim a queue-message dedup slot. Used on DO instances dedicated to a
379
+ * single idempotency key (`claim:<queueName>:<idempotencyKey>`).
380
+ *
381
+ * - Unclaimed -> claim and proceed.
382
+ * - Claimed by the SAME messageId -> redelivery of the same message
383
+ * (previous attempt failed transiently) -> proceed.
384
+ * - Claimed by a DIFFERENT messageId -> concurrent duplicate -> reject,
385
+ * unless the claim is stale (holder crashed / went to the DLQ without
386
+ * releasing), in which case the claim is stolen.
387
+ */
388
+ claimInflight(params: {
389
+ messageId: string;
390
+ staleMs: number;
391
+ }): Promise<{
392
+ claimed: boolean;
393
+ }>;
394
+ /**
395
+ * Release a queue-message dedup claim after the message was fully handled.
396
+ */
397
+ releaseInflight(): Promise<void>;
398
+ }
399
+ //#endregion
400
+ //#region src/worker/worker.d.ts
401
+ declare const _default: {
402
+ fetch(request: Request, env: WorkerEnv): Promise<Response>;
403
+ };
404
+ //#endregion
405
+ export { IndexDO, QueueDO, StreamDO, type WorkerEnv, WorkflowRunDO, createRouter, _default as default };
406
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","names":[],"sources":["../src/worker/router.ts","../src/apply-event.ts","../src/queue.ts","../src/config.ts","../src/worker/do-base.ts","../src/worker/durable-objects/IndexDO.ts","../src/worker/durable-objects/QueueDO.ts","../src/worker/durable-objects/StreamDO.ts","../src/worker/durable-objects/WorkflowRunDO.ts","../src/worker/worker.ts"],"mappings":";;;UAmBiB;EACf,WAAW;IAAiB;;EAC5B,IAAI;IAAM;;;UAGK;EACf,aAAa;EACb,kBAAkB;EAClB,gBAAgB;EAChB,gBAAgB;EAChB;;iBAmFc,aAAa,KAAK,aAClB,SAAS,YAAU,QAAQ;;;UC3B1B;;EAEf;EACA,MAAM,qBAAqB;;;;;EAK3B;IAAgB;IAAe;;;KAGrB;UAUK;EACf;EACA,MAAM;EACN;;EAEA;;EAEA;;UAGe;EACf;;EAEA,QAAQ;EACR,MAAM;EACN,OAAO;EACP,OAAO;;EAEP,eAAe;IAAQ;IAAgB;;;EAEvC;IAAe;IAAsB,WAAW;;;EAEhD,cAAc;;EAEd,SAAS;;KAGC,oBAAoB,oBAAoB;;;KC/E/C;;;;;;UAoBY;EACf;EACA,WAAW;EACX,UAAU;;EAEV;EACA;EACA;;;;;EAKA,QAAQ;;UAGO;;EAEf;EACA;;KAGU;EACN;EAAU;EAAmB;;EAC7B;EAAW;EAAyB;;;;UCtEzB;EACf;EACA;;;;;;;;;;;;;cCrBW,cAAc;WAEd,KAAK;WACL,KAAK;EADL,YAAA,KAAK,oBACL,KAAK;;;;;;;;;;;;;;;;;cCoBL,gBAAgB;EACrB,IAAI,cAAc;EAKlB,IAAI,aAAa,gBAAgB;EAIjC,OAAO,cAAc;EAIrB,KAAK;IACT;IACA;IACA;IACA;MACE;IACF,MAAM;MAAQ;;IACd;IACA;;EA2BI,iBACJ,eACA,OAAO,iBACN;IAAU;IAAkB,SAAS;;EAmBlC,oBACJ,eACA,gBACA,wBACA,OAAO,iBACN;EAmBG,iBAAiB,eAAe,OAAO,iBAAiB;EAUxD,kBAAkB,eAAe,gBAAgB,OAAO,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UC3GhE;EACf;EACA;EACA;;EAEA;;;;;;;EAOA;EACA;;EAEA;EACA;EACA;;UAGe,sBAAsB;EACrC;;UAGe;EACf;EACA;EACA;EACA;;cAqCW,gBAAgB;;EAoBrB,QAAQ,SAAS,iBAAiB,QAAQ;EA2D1C,SAAS;EA8NT,SAAS,QAAQ;EAgBjB,gBAAgB;IACpB;IACA;MACE;IAAU,MAAM;IAAiB;IAAuB;;;EAkBtD,kBAAkB,oBAAoB;IAAU;;EAkChD,oBAAoB;IAAU;;;;;;EAe9B,cAAc;IAAU;;;;;;;;;;;;;;;cClcnB,iBAAiB;UACd;;;;;EASR,WAAW,MAAM,aAAa;;;;;EAiB9B,eAAe;;;;EAUf,UAAU;IAAU;IAAoB;MAAkB;IAC9D,QAAQ;;IAER;;IAEA;;;;;EAmBI,WAAW;IAAU;IAAmB;;;;;EAQxC,eAAe,eAAe;;;;EAO9B,eAAe;;;;;;;;;;;;;;cCeV,sBAAsB;UACzB;;;;;UAMA;;;;;;;EAaF,WAAW,SAAS,oBAAoB,QAAQ;;;;EAqBhD,UAAU,QAAQ;;;;EASlB,QAAQ,iBAAiB,QAAQ;;;;EASjC,UAAU;IACd;IACA;IACA;MACE;IAAU,MAAM;IAAQ;IAAuB;;;;;EAiB7C,SAAS,kBAAkB,QAAQ;;;;;EAUnC,WAAW;IACf;IACA;IACA;MACE;IAAU,MAAM;IAAS;IAAuB;;;;;EAiB9C,UAAU;IACd;IACA;IACA;MACE;IAAU,MAAM;IAAQ;IAAuB;;;;;;;;;;;;;EAyB7C,cAAc;IAClB;IACA;MACE;IAAU;;;;;EAoBR,mBAAmB;;;;;EChRJ,MAAA,SAAA,SAAO,KAAO,YAAY,QAAQ"}