@rivetkit/workflow-engine 2.1.0-rc.1

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,609 @@
1
+ /**
2
+ * Serialization/deserialization utilities for converting between
3
+ * internal TypeScript types and BARE schema types.
4
+ */
5
+
6
+ import * as cbor from "cbor-x";
7
+ import type * as v1 from "../dist/schemas/v1.js";
8
+ import {
9
+ BranchStatusType as BareBranchStatusType,
10
+ EntryStatus as BareEntryStatus,
11
+ SleepState as BareSleepState,
12
+ } from "../dist/schemas/v1.js";
13
+ import type {
14
+ BranchStatus as InternalBranchStatus,
15
+ BranchStatusType as InternalBranchStatusType,
16
+ Entry as InternalEntry,
17
+ EntryKind as InternalEntryKind,
18
+ EntryMetadata as InternalEntryMetadata,
19
+ EntryStatus as InternalEntryStatus,
20
+ Location as InternalLocation,
21
+ LoopIterationMarker as InternalLoopIterationMarker,
22
+ PathSegment as InternalPathSegment,
23
+ SleepState as InternalSleepState,
24
+ WorkflowState as InternalWorkflowState,
25
+ } from "../src/types.js";
26
+ import {
27
+ CURRENT_VERSION,
28
+ ENTRY_METADATA_VERSIONED,
29
+ ENTRY_VERSIONED,
30
+ WORKFLOW_METADATA_VERSIONED,
31
+ } from "./versioned.js";
32
+
33
+ // === Helper: ArrayBuffer to/from utilities ===
34
+
35
+ function bufferToArrayBuffer(buf: Uint8Array): ArrayBuffer {
36
+ // Create a new ArrayBuffer and copy the data to ensure it's not a SharedArrayBuffer
37
+ const arrayBuffer = new ArrayBuffer(buf.byteLength);
38
+ new Uint8Array(arrayBuffer).set(buf);
39
+ return arrayBuffer;
40
+ }
41
+
42
+ function encodeCbor(value: unknown): ArrayBuffer {
43
+ return bufferToArrayBuffer(cbor.encode(value));
44
+ }
45
+
46
+ function decodeCbor<T>(data: ArrayBuffer): T {
47
+ return cbor.decode(new Uint8Array(data)) as T;
48
+ }
49
+
50
+ /**
51
+ * Validate that a value is a non-null object.
52
+ */
53
+ function assertObject(
54
+ value: unknown,
55
+ context: string,
56
+ ): asserts value is Record<string, unknown> {
57
+ if (typeof value !== "object" || value === null) {
58
+ throw new Error(`${context}: expected object, got ${typeof value}`);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Validate that a value is a string.
64
+ */
65
+ function assertString(
66
+ value: unknown,
67
+ context: string,
68
+ ): asserts value is string {
69
+ if (typeof value !== "string") {
70
+ throw new Error(`${context}: expected string, got ${typeof value}`);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Validate that a value is a number.
76
+ */
77
+ function assertNumber(
78
+ value: unknown,
79
+ context: string,
80
+ ): asserts value is number {
81
+ if (typeof value !== "number") {
82
+ throw new Error(`${context}: expected number, got ${typeof value}`);
83
+ }
84
+ }
85
+
86
+ // === Entry Status Conversion ===
87
+
88
+ function entryStatusToBare(status: InternalEntryStatus): BareEntryStatus {
89
+ switch (status) {
90
+ case "pending":
91
+ return BareEntryStatus.PENDING;
92
+ case "running":
93
+ return BareEntryStatus.RUNNING;
94
+ case "completed":
95
+ return BareEntryStatus.COMPLETED;
96
+ case "failed":
97
+ return BareEntryStatus.FAILED;
98
+ case "exhausted":
99
+ return BareEntryStatus.EXHAUSTED;
100
+ }
101
+ }
102
+
103
+ function entryStatusFromBare(status: BareEntryStatus): InternalEntryStatus {
104
+ switch (status) {
105
+ case BareEntryStatus.PENDING:
106
+ return "pending";
107
+ case BareEntryStatus.RUNNING:
108
+ return "running";
109
+ case BareEntryStatus.COMPLETED:
110
+ return "completed";
111
+ case BareEntryStatus.FAILED:
112
+ return "failed";
113
+ case BareEntryStatus.EXHAUSTED:
114
+ return "exhausted";
115
+ }
116
+ }
117
+
118
+ // === Sleep State Conversion ===
119
+
120
+ function sleepStateToBare(state: InternalSleepState): BareSleepState {
121
+ switch (state) {
122
+ case "pending":
123
+ return BareSleepState.PENDING;
124
+ case "completed":
125
+ return BareSleepState.COMPLETED;
126
+ case "interrupted":
127
+ return BareSleepState.INTERRUPTED;
128
+ }
129
+ }
130
+
131
+ function sleepStateFromBare(state: BareSleepState): InternalSleepState {
132
+ switch (state) {
133
+ case BareSleepState.PENDING:
134
+ return "pending";
135
+ case BareSleepState.COMPLETED:
136
+ return "completed";
137
+ case BareSleepState.INTERRUPTED:
138
+ return "interrupted";
139
+ }
140
+ }
141
+
142
+ // === Branch Status Type Conversion ===
143
+
144
+ function branchStatusTypeToBare(
145
+ status: InternalBranchStatusType,
146
+ ): BareBranchStatusType {
147
+ switch (status) {
148
+ case "pending":
149
+ return BareBranchStatusType.PENDING;
150
+ case "running":
151
+ return BareBranchStatusType.RUNNING;
152
+ case "completed":
153
+ return BareBranchStatusType.COMPLETED;
154
+ case "failed":
155
+ return BareBranchStatusType.FAILED;
156
+ case "cancelled":
157
+ return BareBranchStatusType.CANCELLED;
158
+ }
159
+ }
160
+
161
+ function branchStatusTypeFromBare(
162
+ status: BareBranchStatusType,
163
+ ): InternalBranchStatusType {
164
+ switch (status) {
165
+ case BareBranchStatusType.PENDING:
166
+ return "pending";
167
+ case BareBranchStatusType.RUNNING:
168
+ return "running";
169
+ case BareBranchStatusType.COMPLETED:
170
+ return "completed";
171
+ case BareBranchStatusType.FAILED:
172
+ return "failed";
173
+ case BareBranchStatusType.CANCELLED:
174
+ return "cancelled";
175
+ }
176
+ }
177
+
178
+ // === Location Conversion ===
179
+
180
+ function locationToBare(location: InternalLocation): v1.Location {
181
+ return location.map((segment): v1.PathSegment => {
182
+ if (typeof segment === "number") {
183
+ return { tag: "NameIndex", val: segment };
184
+ }
185
+ return {
186
+ tag: "LoopIterationMarker",
187
+ val: {
188
+ loop: segment.loop,
189
+ iteration: segment.iteration,
190
+ },
191
+ };
192
+ });
193
+ }
194
+
195
+ function locationFromBare(location: v1.Location): InternalLocation {
196
+ return location.map((segment): InternalPathSegment => {
197
+ if (segment.tag === "NameIndex") {
198
+ return segment.val;
199
+ }
200
+ return {
201
+ loop: segment.val.loop,
202
+ iteration: segment.val.iteration,
203
+ };
204
+ });
205
+ }
206
+
207
+ // === Branch Status Conversion ===
208
+
209
+ function branchStatusToBare(status: InternalBranchStatus): v1.BranchStatus {
210
+ return {
211
+ status: branchStatusTypeToBare(status.status),
212
+ output: status.output !== undefined ? encodeCbor(status.output) : null,
213
+ error: status.error ?? null,
214
+ };
215
+ }
216
+
217
+ function branchStatusFromBare(status: v1.BranchStatus): InternalBranchStatus {
218
+ return {
219
+ status: branchStatusTypeFromBare(status.status),
220
+ output: status.output !== null ? decodeCbor(status.output) : undefined,
221
+ error: status.error ?? undefined,
222
+ };
223
+ }
224
+
225
+ // === Entry Kind Conversion ===
226
+
227
+ function entryKindToBare(kind: InternalEntryKind): v1.EntryKind {
228
+ switch (kind.type) {
229
+ case "step":
230
+ return {
231
+ tag: "StepEntry",
232
+ val: {
233
+ output:
234
+ kind.data.output !== undefined
235
+ ? encodeCbor(kind.data.output)
236
+ : null,
237
+ error: kind.data.error ?? null,
238
+ },
239
+ };
240
+ case "loop":
241
+ return {
242
+ tag: "LoopEntry",
243
+ val: {
244
+ state: encodeCbor(kind.data.state),
245
+ iteration: kind.data.iteration,
246
+ output:
247
+ kind.data.output !== undefined
248
+ ? encodeCbor(kind.data.output)
249
+ : null,
250
+ },
251
+ };
252
+ case "sleep":
253
+ return {
254
+ tag: "SleepEntry",
255
+ val: {
256
+ deadline: BigInt(kind.data.deadline),
257
+ state: sleepStateToBare(kind.data.state),
258
+ },
259
+ };
260
+ case "message":
261
+ return {
262
+ tag: "MessageEntry",
263
+ val: {
264
+ name: kind.data.name,
265
+ messageData: encodeCbor(kind.data.data),
266
+ },
267
+ };
268
+ case "rollback_checkpoint":
269
+ return {
270
+ tag: "RollbackCheckpointEntry",
271
+ val: {
272
+ name: kind.data.name,
273
+ },
274
+ };
275
+ case "join":
276
+ return {
277
+ tag: "JoinEntry",
278
+ val: {
279
+ branches: new Map(
280
+ Object.entries(kind.data.branches).map(
281
+ ([name, status]) => [
282
+ name,
283
+ branchStatusToBare(status),
284
+ ],
285
+ ),
286
+ ),
287
+ },
288
+ };
289
+ case "race":
290
+ return {
291
+ tag: "RaceEntry",
292
+ val: {
293
+ winner: kind.data.winner,
294
+ branches: new Map(
295
+ Object.entries(kind.data.branches).map(
296
+ ([name, status]) => [
297
+ name,
298
+ branchStatusToBare(status),
299
+ ],
300
+ ),
301
+ ),
302
+ },
303
+ };
304
+ case "removed":
305
+ return {
306
+ tag: "RemovedEntry",
307
+ val: {
308
+ originalType: kind.data.originalType,
309
+ originalName: kind.data.originalName ?? null,
310
+ },
311
+ };
312
+ }
313
+ }
314
+
315
+ function entryKindFromBare(kind: v1.EntryKind): InternalEntryKind {
316
+ switch (kind.tag) {
317
+ case "StepEntry":
318
+ return {
319
+ type: "step",
320
+ data: {
321
+ output:
322
+ kind.val.output !== null
323
+ ? decodeCbor(kind.val.output)
324
+ : undefined,
325
+ error: kind.val.error ?? undefined,
326
+ },
327
+ };
328
+ case "LoopEntry":
329
+ return {
330
+ type: "loop",
331
+ data: {
332
+ state: decodeCbor(kind.val.state),
333
+ iteration: kind.val.iteration,
334
+ output:
335
+ kind.val.output !== null
336
+ ? decodeCbor(kind.val.output)
337
+ : undefined,
338
+ },
339
+ };
340
+ case "SleepEntry":
341
+ return {
342
+ type: "sleep",
343
+ data: {
344
+ deadline: Number(kind.val.deadline),
345
+ state: sleepStateFromBare(kind.val.state),
346
+ },
347
+ };
348
+ case "MessageEntry":
349
+ return {
350
+ type: "message",
351
+ data: {
352
+ name: kind.val.name,
353
+ data: decodeCbor(kind.val.messageData),
354
+ },
355
+ };
356
+ case "RollbackCheckpointEntry":
357
+ return {
358
+ type: "rollback_checkpoint",
359
+ data: {
360
+ name: kind.val.name,
361
+ },
362
+ };
363
+ case "JoinEntry":
364
+ return {
365
+ type: "join",
366
+ data: {
367
+ branches: Object.fromEntries(
368
+ Array.from(kind.val.branches.entries()).map(
369
+ ([name, status]) => [
370
+ name,
371
+ branchStatusFromBare(status),
372
+ ],
373
+ ),
374
+ ),
375
+ },
376
+ };
377
+ case "RaceEntry":
378
+ return {
379
+ type: "race",
380
+ data: {
381
+ winner: kind.val.winner,
382
+ branches: Object.fromEntries(
383
+ Array.from(kind.val.branches.entries()).map(
384
+ ([name, status]) => [
385
+ name,
386
+ branchStatusFromBare(status),
387
+ ],
388
+ ),
389
+ ),
390
+ },
391
+ };
392
+ case "RemovedEntry":
393
+ return {
394
+ type: "removed",
395
+ data: {
396
+ originalType: kind.val
397
+ .originalType as InternalEntryKind["type"],
398
+ originalName: kind.val.originalName ?? undefined,
399
+ },
400
+ };
401
+ default:
402
+ throw new Error(
403
+ `Unknown entry kind: ${(kind as { tag: string }).tag}`,
404
+ );
405
+ }
406
+ }
407
+
408
+ // === Entry Conversion & Serialization ===
409
+
410
+ function entryToBare(entry: InternalEntry): v1.Entry {
411
+ return {
412
+ id: entry.id,
413
+ location: locationToBare(entry.location),
414
+ kind: entryKindToBare(entry.kind),
415
+ };
416
+ }
417
+
418
+ function entryFromBare(bareEntry: v1.Entry): InternalEntry {
419
+ return {
420
+ id: bareEntry.id,
421
+ location: locationFromBare(bareEntry.location),
422
+ kind: entryKindFromBare(bareEntry.kind),
423
+ dirty: false,
424
+ };
425
+ }
426
+
427
+ export function serializeEntry(entry: InternalEntry): Uint8Array {
428
+ const bareEntry = entryToBare(entry);
429
+ return ENTRY_VERSIONED.serializeWithEmbeddedVersion(
430
+ bareEntry,
431
+ CURRENT_VERSION,
432
+ );
433
+ }
434
+
435
+ export function deserializeEntry(bytes: Uint8Array): InternalEntry {
436
+ const bareEntry = ENTRY_VERSIONED.deserializeWithEmbeddedVersion(bytes);
437
+ return entryFromBare(bareEntry);
438
+ }
439
+
440
+ // === Entry Metadata Conversion & Serialization ===
441
+
442
+ function entryMetadataToBare(
443
+ metadata: InternalEntryMetadata,
444
+ ): v1.EntryMetadata {
445
+ return {
446
+ status: entryStatusToBare(metadata.status),
447
+ error: metadata.error ?? null,
448
+ attempts: metadata.attempts,
449
+ lastAttemptAt: BigInt(metadata.lastAttemptAt),
450
+ createdAt: BigInt(metadata.createdAt),
451
+ completedAt:
452
+ metadata.completedAt !== undefined
453
+ ? BigInt(metadata.completedAt)
454
+ : null,
455
+ rollbackCompletedAt:
456
+ metadata.rollbackCompletedAt !== undefined
457
+ ? BigInt(metadata.rollbackCompletedAt)
458
+ : null,
459
+ rollbackError: metadata.rollbackError ?? null,
460
+ };
461
+ }
462
+
463
+ function entryMetadataFromBare(
464
+ bareMetadata: v1.EntryMetadata,
465
+ ): InternalEntryMetadata {
466
+ return {
467
+ status: entryStatusFromBare(bareMetadata.status),
468
+ error: bareMetadata.error ?? undefined,
469
+ attempts: bareMetadata.attempts,
470
+ lastAttemptAt: Number(bareMetadata.lastAttemptAt),
471
+ createdAt: Number(bareMetadata.createdAt),
472
+ completedAt:
473
+ bareMetadata.completedAt !== null
474
+ ? Number(bareMetadata.completedAt)
475
+ : undefined,
476
+ rollbackCompletedAt:
477
+ bareMetadata.rollbackCompletedAt !== null
478
+ ? Number(bareMetadata.rollbackCompletedAt)
479
+ : undefined,
480
+ rollbackError: bareMetadata.rollbackError ?? undefined,
481
+ dirty: false,
482
+ };
483
+ }
484
+
485
+ export function serializeEntryMetadata(
486
+ metadata: InternalEntryMetadata,
487
+ ): Uint8Array {
488
+ const bareMetadata = entryMetadataToBare(metadata);
489
+ return ENTRY_METADATA_VERSIONED.serializeWithEmbeddedVersion(
490
+ bareMetadata,
491
+ CURRENT_VERSION,
492
+ );
493
+ }
494
+
495
+ export function deserializeEntryMetadata(
496
+ bytes: Uint8Array,
497
+ ): InternalEntryMetadata {
498
+ const bareMetadata =
499
+ ENTRY_METADATA_VERSIONED.deserializeWithEmbeddedVersion(bytes);
500
+ return entryMetadataFromBare(bareMetadata);
501
+ }
502
+
503
+ // === Workflow Metadata Serialization ===
504
+ // Note: These are used for reading/writing individual workflow fields
505
+
506
+ export function serializeWorkflowState(
507
+ state: InternalWorkflowState,
508
+ ): Uint8Array {
509
+ // For simple values, we can encode them directly without the full metadata struct
510
+ // Using a single byte for efficiency
511
+ const encoder = new TextEncoder();
512
+ return encoder.encode(state);
513
+ }
514
+
515
+ export function deserializeWorkflowState(
516
+ bytes: Uint8Array,
517
+ ): InternalWorkflowState {
518
+ const decoder = new TextDecoder();
519
+ const state = decoder.decode(bytes) as InternalWorkflowState;
520
+ const validStates: InternalWorkflowState[] = [
521
+ "pending",
522
+ "running",
523
+ "sleeping",
524
+ "failed",
525
+ "completed",
526
+ "cancelled",
527
+ "rolling_back",
528
+ ];
529
+ if (!validStates.includes(state)) {
530
+ throw new Error(`Invalid workflow state: ${state}`);
531
+ }
532
+ return state;
533
+ }
534
+
535
+ export function serializeWorkflowOutput(output: unknown): Uint8Array {
536
+ return cbor.encode(output);
537
+ }
538
+
539
+ export function deserializeWorkflowOutput<T>(bytes: Uint8Array): T {
540
+ try {
541
+ return cbor.decode(bytes) as T;
542
+ } catch (error) {
543
+ throw new Error(
544
+ `Failed to deserialize workflow output: ${error instanceof Error ? error.message : String(error)}`,
545
+ );
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Structured error type for serialization.
551
+ */
552
+ interface SerializedWorkflowError {
553
+ name: string;
554
+ message: string;
555
+ stack?: string;
556
+ metadata?: Record<string, unknown>;
557
+ }
558
+
559
+ export function serializeWorkflowError(
560
+ error: SerializedWorkflowError,
561
+ ): Uint8Array {
562
+ return cbor.encode(error);
563
+ }
564
+
565
+ export function deserializeWorkflowError(
566
+ bytes: Uint8Array,
567
+ ): SerializedWorkflowError {
568
+ const decoded = cbor.decode(bytes);
569
+ assertObject(decoded, "WorkflowError");
570
+ // Validate required fields
571
+ const obj = decoded as Record<string, unknown>;
572
+ assertString(obj.name, "WorkflowError.name");
573
+ assertString(obj.message, "WorkflowError.message");
574
+ return {
575
+ name: obj.name,
576
+ message: obj.message,
577
+ stack: typeof obj.stack === "string" ? obj.stack : undefined,
578
+ metadata:
579
+ typeof obj.metadata === "object" && obj.metadata !== null
580
+ ? (obj.metadata as Record<string, unknown>)
581
+ : undefined,
582
+ };
583
+ }
584
+
585
+ export function serializeWorkflowInput(input: unknown): Uint8Array {
586
+ return cbor.encode(input);
587
+ }
588
+
589
+ export function deserializeWorkflowInput<T>(bytes: Uint8Array): T {
590
+ try {
591
+ return cbor.decode(bytes) as T;
592
+ } catch (error) {
593
+ throw new Error(
594
+ `Failed to deserialize workflow input: ${error instanceof Error ? error.message : String(error)}`,
595
+ );
596
+ }
597
+ }
598
+
599
+ // === Name Registry Serialization ===
600
+
601
+ export function serializeName(name: string): Uint8Array {
602
+ const encoder = new TextEncoder();
603
+ return encoder.encode(name);
604
+ }
605
+
606
+ export function deserializeName(bytes: Uint8Array): string {
607
+ const decoder = new TextDecoder();
608
+ return decoder.decode(bytes);
609
+ }