@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.
package/src/keys.ts ADDED
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Binary key encoding/decoding using fdb-tuple.
3
+ * All keys are encoded as tuples with integer prefixes for proper sorting.
4
+ */
5
+
6
+ import * as tuple from "fdb-tuple";
7
+ import type { Location, LoopIterationMarker, PathSegment } from "./types.js";
8
+
9
+ // === Key Prefixes ===
10
+ // Using integers for compact encoding and proper sorting
11
+
12
+ export const KEY_PREFIX = {
13
+ NAMES: 1, // Name registry: [1, index]
14
+ HISTORY: 2, // History entries: [2, ...locationSegments]
15
+ WORKFLOW: 3, // Workflow metadata: [3, field]
16
+ ENTRY_METADATA: 4, // Entry metadata: [4, entryId]
17
+ } as const;
18
+
19
+ // Workflow metadata field identifiers
20
+ export const WORKFLOW_FIELD = {
21
+ STATE: 1,
22
+ OUTPUT: 2,
23
+ ERROR: 3,
24
+ INPUT: 4,
25
+ } as const;
26
+
27
+ // === Type Definitions ===
28
+
29
+ // fdb-tuple's TupleItem type - we use a subset
30
+ type TupleItem = string | number | boolean | null | TupleItem[];
31
+
32
+ // === Location Segment Encoding ===
33
+
34
+ /**
35
+ * Convert a path segment to tuple elements.
36
+ * - NameIndex (number) → just the number
37
+ * - LoopIterationMarker → nested tuple [loopIdx, iteration]
38
+ */
39
+ function segmentToTuple(segment: PathSegment): TupleItem {
40
+ if (typeof segment === "number") {
41
+ return segment;
42
+ }
43
+ // LoopIterationMarker
44
+ return [segment.loop, segment.iteration];
45
+ }
46
+
47
+ /**
48
+ * Convert tuple elements back to a path segment.
49
+ */
50
+ function tupleToSegment(element: TupleItem): PathSegment {
51
+ if (typeof element === "number") {
52
+ return element;
53
+ }
54
+ if (Array.isArray(element) && element.length === 2) {
55
+ const [loop, iteration] = element;
56
+ if (typeof loop === "number" && typeof iteration === "number") {
57
+ return { loop, iteration } as LoopIterationMarker;
58
+ }
59
+ }
60
+ throw new Error(
61
+ `Invalid path segment tuple element: ${JSON.stringify(element)}`,
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Convert a location to tuple elements.
67
+ */
68
+ function locationToTupleElements(location: Location): TupleItem[] {
69
+ return location.map(segmentToTuple);
70
+ }
71
+
72
+ /**
73
+ * Convert tuple elements back to a location.
74
+ */
75
+ function tupleElementsToLocation(elements: TupleItem[]): Location {
76
+ return elements.map(tupleToSegment);
77
+ }
78
+
79
+ // === Helper Functions ===
80
+
81
+ /**
82
+ * Convert Buffer to Uint8Array.
83
+ */
84
+ function bufferToUint8Array(buf: Buffer): Uint8Array {
85
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
86
+ }
87
+
88
+ /**
89
+ * Convert Uint8Array to Buffer.
90
+ */
91
+ function uint8ArrayToBuffer(arr: Uint8Array): Buffer {
92
+ return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
93
+ }
94
+
95
+ /**
96
+ * Pack tuple items and return as Uint8Array.
97
+ */
98
+ function pack(items: TupleItem | TupleItem[]): Uint8Array {
99
+ const buf = tuple.pack(items);
100
+ return bufferToUint8Array(buf);
101
+ }
102
+
103
+ /**
104
+ * Unpack a Uint8Array and return tuple items.
105
+ */
106
+ function unpack(data: Uint8Array): TupleItem[] {
107
+ const buf = uint8ArrayToBuffer(data);
108
+ return tuple.unpack(buf) as TupleItem[];
109
+ }
110
+
111
+ // === Key Builders ===
112
+
113
+ /**
114
+ * Build a key for the name registry.
115
+ * Key: [1, index]
116
+ */
117
+ export function buildNameKey(index: number): Uint8Array {
118
+ return pack([KEY_PREFIX.NAMES, index]);
119
+ }
120
+
121
+ /**
122
+ * Build a prefix for listing all names.
123
+ * Prefix: [1]
124
+ */
125
+ export function buildNamePrefix(): Uint8Array {
126
+ return pack([KEY_PREFIX.NAMES]);
127
+ }
128
+
129
+ /**
130
+ * Build a key for a history entry.
131
+ * Key: [2, ...locationSegments]
132
+ */
133
+ export function buildHistoryKey(location: Location): Uint8Array {
134
+ return pack([KEY_PREFIX.HISTORY, ...locationToTupleElements(location)]);
135
+ }
136
+
137
+ /**
138
+ * Build a prefix for listing history entries under a location.
139
+ * Prefix: [2, ...locationSegments]
140
+ */
141
+ export function buildHistoryPrefix(location: Location): Uint8Array {
142
+ return pack([KEY_PREFIX.HISTORY, ...locationToTupleElements(location)]);
143
+ }
144
+
145
+ /**
146
+ * Build a prefix for listing all history entries.
147
+ * Prefix: [2]
148
+ */
149
+ export function buildHistoryPrefixAll(): Uint8Array {
150
+ return pack([KEY_PREFIX.HISTORY]);
151
+ }
152
+
153
+ /**
154
+ * Build a key for workflow state.
155
+ * Key: [3, 1]
156
+ */
157
+ export function buildWorkflowStateKey(): Uint8Array {
158
+ return pack([KEY_PREFIX.WORKFLOW, WORKFLOW_FIELD.STATE]);
159
+ }
160
+
161
+ /**
162
+ * Build a key for workflow output.
163
+ * Key: [3, 2]
164
+ */
165
+ export function buildWorkflowOutputKey(): Uint8Array {
166
+ return pack([KEY_PREFIX.WORKFLOW, WORKFLOW_FIELD.OUTPUT]);
167
+ }
168
+
169
+ /**
170
+ * Build a key for workflow error.
171
+ * Key: [3, 3]
172
+ */
173
+ export function buildWorkflowErrorKey(): Uint8Array {
174
+ return pack([KEY_PREFIX.WORKFLOW, WORKFLOW_FIELD.ERROR]);
175
+ }
176
+
177
+ /**
178
+ * Build a key for workflow input.
179
+ * Key: [3, 4]
180
+ */
181
+ export function buildWorkflowInputKey(): Uint8Array {
182
+ return pack([KEY_PREFIX.WORKFLOW, WORKFLOW_FIELD.INPUT]);
183
+ }
184
+
185
+ /**
186
+ * Build a key for entry metadata.
187
+ * Key: [4, entryId]
188
+ */
189
+ export function buildEntryMetadataKey(entryId: string): Uint8Array {
190
+ return pack([KEY_PREFIX.ENTRY_METADATA, entryId]);
191
+ }
192
+
193
+ /**
194
+ * Build a prefix for listing all entry metadata.
195
+ * Prefix: [4]
196
+ */
197
+ export function buildEntryMetadataPrefix(): Uint8Array {
198
+ return pack([KEY_PREFIX.ENTRY_METADATA]);
199
+ }
200
+
201
+ // === Key Parsers ===
202
+
203
+ /**
204
+ * Parse a name key and return the index.
205
+ * Key: [1, index] → index
206
+ */
207
+ export function parseNameKey(key: Uint8Array): number {
208
+ const elements = unpack(key);
209
+ if (elements.length !== 2 || elements[0] !== KEY_PREFIX.NAMES) {
210
+ throw new Error("Invalid name key");
211
+ }
212
+ return elements[1] as number;
213
+ }
214
+
215
+ /**
216
+ * Parse a history key and return the location.
217
+ * Key: [2, ...segments] → Location
218
+ */
219
+ export function parseHistoryKey(key: Uint8Array): Location {
220
+ const elements = unpack(key);
221
+ if (elements.length < 1 || elements[0] !== KEY_PREFIX.HISTORY) {
222
+ throw new Error("Invalid history key");
223
+ }
224
+ return tupleElementsToLocation(elements.slice(1));
225
+ }
226
+
227
+ /**
228
+ * Parse an entry metadata key and return the entry ID.
229
+ * Key: [4, entryId] → entryId
230
+ */
231
+ export function parseEntryMetadataKey(key: Uint8Array): string {
232
+ const elements = unpack(key);
233
+ if (elements.length !== 2 || elements[0] !== KEY_PREFIX.ENTRY_METADATA) {
234
+ throw new Error("Invalid entry metadata key");
235
+ }
236
+ return elements[1] as string;
237
+ }
238
+
239
+ // === Key Comparison Utilities ===
240
+
241
+ /**
242
+ * Check if a key starts with a prefix.
243
+ */
244
+ export function keyStartsWith(key: Uint8Array, prefix: Uint8Array): boolean {
245
+ if (key.length < prefix.length) {
246
+ return false;
247
+ }
248
+ for (let i = 0; i < prefix.length; i++) {
249
+ if (key[i] !== prefix[i]) {
250
+ return false;
251
+ }
252
+ }
253
+ return true;
254
+ }
255
+
256
+ /**
257
+ * Compare two keys lexicographically.
258
+ * Returns negative if a < b, 0 if a === b, positive if a > b.
259
+ */
260
+ export function compareKeys(a: Uint8Array, b: Uint8Array): number {
261
+ const minLen = Math.min(a.length, b.length);
262
+ for (let i = 0; i < minLen; i++) {
263
+ if (a[i] !== b[i]) {
264
+ return a[i] - b[i];
265
+ }
266
+ }
267
+ return a.length - b.length;
268
+ }
269
+
270
+ /**
271
+ * Convert a key to a hex string for debugging.
272
+ */
273
+ export function keyToHex(key: Uint8Array): string {
274
+ return Array.from(key)
275
+ .map((b) => b.toString(16).padStart(2, "0"))
276
+ .join("");
277
+ }
@@ -0,0 +1,168 @@
1
+ import type {
2
+ Location,
3
+ LoopIterationMarker,
4
+ NameIndex,
5
+ PathSegment,
6
+ Storage,
7
+ } from "./types.js";
8
+
9
+ /**
10
+ * Check if a path segment is a loop iteration marker.
11
+ */
12
+ export function isLoopIterationMarker(
13
+ segment: PathSegment,
14
+ ): segment is LoopIterationMarker {
15
+ return typeof segment === "object" && "loop" in segment;
16
+ }
17
+
18
+ /**
19
+ * Register a name in the registry and return its index.
20
+ * If the name already exists, returns the existing index.
21
+ */
22
+ export function registerName(storage: Storage, name: string): NameIndex {
23
+ const existing = storage.nameRegistry.indexOf(name);
24
+ if (existing !== -1) {
25
+ return existing;
26
+ }
27
+ storage.nameRegistry.push(name);
28
+ return storage.nameRegistry.length - 1;
29
+ }
30
+
31
+ /**
32
+ * Resolve a name index to its string value.
33
+ */
34
+ export function resolveName(storage: Storage, index: NameIndex): string {
35
+ const name = storage.nameRegistry[index];
36
+ if (name === undefined) {
37
+ throw new Error(`Name index ${index} not found in registry`);
38
+ }
39
+ return name;
40
+ }
41
+
42
+ /**
43
+ * Convert a location to a KV key string.
44
+ * Named entries use their string name, loop iterations use ~N format.
45
+ */
46
+ export function locationToKey(storage: Storage, location: Location): string {
47
+ return location
48
+ .map((segment) => {
49
+ if (typeof segment === "number") {
50
+ return resolveName(storage, segment);
51
+ }
52
+ return `~${segment.iteration}`;
53
+ })
54
+ .join("/");
55
+ }
56
+
57
+ /**
58
+ * Append a named segment to a location.
59
+ */
60
+ export function appendName(
61
+ storage: Storage,
62
+ location: Location,
63
+ name: string,
64
+ ): Location {
65
+ const nameIndex = registerName(storage, name);
66
+ return [...location, nameIndex];
67
+ }
68
+
69
+ /**
70
+ * Append a loop iteration segment to a location.
71
+ */
72
+ export function appendLoopIteration(
73
+ storage: Storage,
74
+ location: Location,
75
+ loopName: string,
76
+ iteration: number,
77
+ ): Location {
78
+ const loopIndex = registerName(storage, loopName);
79
+ return [...location, { loop: loopIndex, iteration }];
80
+ }
81
+
82
+ /**
83
+ * Create an empty location (root).
84
+ */
85
+ export function emptyLocation(): Location {
86
+ return [];
87
+ }
88
+
89
+ /**
90
+ * Get the parent location (all segments except the last).
91
+ */
92
+ export function parentLocation(location: Location): Location {
93
+ return location.slice(0, -1);
94
+ }
95
+
96
+ /**
97
+ * Check if one location is a prefix of another.
98
+ */
99
+ export function isLocationPrefix(
100
+ prefix: Location,
101
+ location: Location,
102
+ ): boolean {
103
+ if (prefix.length > location.length) {
104
+ return false;
105
+ }
106
+ for (let i = 0; i < prefix.length; i++) {
107
+ const prefixSegment = prefix[i];
108
+ const locationSegment = location[i];
109
+
110
+ if (typeof prefixSegment === "number" && typeof locationSegment === "number") {
111
+ if (prefixSegment !== locationSegment) {
112
+ return false;
113
+ }
114
+ } else if (
115
+ isLoopIterationMarker(prefixSegment) &&
116
+ isLoopIterationMarker(locationSegment)
117
+ ) {
118
+ if (
119
+ prefixSegment.loop !== locationSegment.loop ||
120
+ prefixSegment.iteration !== locationSegment.iteration
121
+ ) {
122
+ return false;
123
+ }
124
+ } else {
125
+ return false;
126
+ }
127
+ }
128
+ return true;
129
+ }
130
+
131
+ /**
132
+ * Compare two locations for equality.
133
+ */
134
+ export function locationsEqual(a: Location, b: Location): boolean {
135
+ if (a.length !== b.length) {
136
+ return false;
137
+ }
138
+ return isLocationPrefix(a, b);
139
+ }
140
+
141
+ /**
142
+ * Get all entry keys that are children of a given location.
143
+ *
144
+ * Note: Returns a map of key → entry for convenience, not key → location.
145
+ * The location can be retrieved from the entry itself via entry.location.
146
+ */
147
+ export function getChildEntries(
148
+ storage: Storage,
149
+ parentLoc: Location,
150
+ ): Map<string, Location> {
151
+ const parentKey = locationToKey(storage, parentLoc);
152
+ const children = new Map<string, Location>();
153
+
154
+ for (const [key, entry] of storage.history.entries) {
155
+ // Handle empty parent (root) - all entries are children
156
+ const isChild =
157
+ parentKey === ""
158
+ ? true
159
+ : key.startsWith(parentKey + "/") || key === parentKey;
160
+
161
+ if (isChild) {
162
+ // Return the actual entry's location, not the parent location
163
+ children.set(key, entry.location);
164
+ }
165
+ }
166
+
167
+ return children;
168
+ }