@rivetkit/workflow-engine 0.0.0-0-0-0-preview-guard-stops.9d82529
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/dist/schemas/v1.ts +806 -0
- package/dist/tsup/chunk-DTS4S7RT.js +4324 -0
- package/dist/tsup/chunk-DTS4S7RT.js.map +1 -0
- package/dist/tsup/chunk-KLHCG2KY.cjs +4324 -0
- package/dist/tsup/chunk-KLHCG2KY.cjs.map +1 -0
- package/dist/tsup/index.cjs +93 -0
- package/dist/tsup/index.cjs.map +1 -0
- package/dist/tsup/index.d.cts +1047 -0
- package/dist/tsup/index.d.ts +1047 -0
- package/dist/tsup/index.js +93 -0
- package/dist/tsup/index.js.map +1 -0
- package/dist/tsup/testing.cjs +328 -0
- package/dist/tsup/testing.cjs.map +1 -0
- package/dist/tsup/testing.d.cts +53 -0
- package/dist/tsup/testing.d.ts +53 -0
- package/dist/tsup/testing.js +328 -0
- package/dist/tsup/testing.js.map +1 -0
- package/package.json +75 -0
- package/schemas/serde.ts +623 -0
- package/schemas/v1.bare +212 -0
- package/schemas/versioned.ts +108 -0
- package/src/context.ts +2677 -0
- package/src/driver.ts +114 -0
- package/src/error-utils.ts +87 -0
- package/src/errors.ts +171 -0
- package/src/index.ts +1095 -0
- package/src/keys.ts +303 -0
- package/src/location.ts +171 -0
- package/src/storage.ts +468 -0
- package/src/testing.ts +313 -0
- package/src/types.ts +642 -0
- package/src/utils.ts +48 -0
package/src/testing.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import type { EngineDriver, KVEntry, KVWrite } from "./driver.js";
|
|
2
|
+
import { EvictedError } from "./errors.js";
|
|
3
|
+
import { compareKeys, keyStartsWith, keyToHex } from "./keys.js";
|
|
4
|
+
import type { Message, WorkflowMessageDriver } from "./types.js";
|
|
5
|
+
import { sleep } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
interface Waiter {
|
|
8
|
+
nameSet?: Set<string>;
|
|
9
|
+
resolve: () => void;
|
|
10
|
+
reject: (error: Error) => void;
|
|
11
|
+
abortSignal: AbortSignal;
|
|
12
|
+
onAbort: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class InMemoryWorkflowMessageDriver implements WorkflowMessageDriver {
|
|
16
|
+
#messages: Message[] = [];
|
|
17
|
+
#waiters = new Set<Waiter>();
|
|
18
|
+
|
|
19
|
+
async addMessage(message: Message): Promise<void> {
|
|
20
|
+
this.#messages.push(message);
|
|
21
|
+
this.#notifyWaiters(message.name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async receiveMessages(opts: {
|
|
25
|
+
names?: readonly string[];
|
|
26
|
+
count: number;
|
|
27
|
+
completable: boolean;
|
|
28
|
+
}): Promise<Message[]> {
|
|
29
|
+
const limitedCount = Math.max(1, opts.count);
|
|
30
|
+
const nameSet =
|
|
31
|
+
opts.names && opts.names.length > 0
|
|
32
|
+
? new Set(opts.names)
|
|
33
|
+
: undefined;
|
|
34
|
+
const selected: Array<{ message: Message; index: number }> = [];
|
|
35
|
+
|
|
36
|
+
for (
|
|
37
|
+
let i = 0;
|
|
38
|
+
i < this.#messages.length && selected.length < limitedCount;
|
|
39
|
+
i++
|
|
40
|
+
) {
|
|
41
|
+
const message = this.#messages[i];
|
|
42
|
+
if (nameSet && !nameSet.has(message.name)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
selected.push({ message, index: i });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (selected.length === 0) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!opts.completable) {
|
|
53
|
+
for (let i = selected.length - 1; i >= 0; i--) {
|
|
54
|
+
this.#messages.splice(selected[i].index, 1);
|
|
55
|
+
}
|
|
56
|
+
return selected.map((entry) => entry.message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return selected.map((entry) => {
|
|
60
|
+
const { message } = entry;
|
|
61
|
+
return {
|
|
62
|
+
...message,
|
|
63
|
+
complete: async () => {
|
|
64
|
+
await this.completeMessage(message.id);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async completeMessage(messageId: string): Promise<void> {
|
|
71
|
+
const index = this.#messages.findIndex(
|
|
72
|
+
(message) => message.id === messageId,
|
|
73
|
+
);
|
|
74
|
+
if (index !== -1) {
|
|
75
|
+
this.#messages.splice(index, 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async waitForMessages(
|
|
80
|
+
messageNames: string[],
|
|
81
|
+
abortSignal: AbortSignal,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
if (abortSignal.aborted) {
|
|
84
|
+
throw new EvictedError();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const nameSet =
|
|
88
|
+
messageNames.length > 0 ? new Set(messageNames) : undefined;
|
|
89
|
+
if (
|
|
90
|
+
this.#messages.some((message) =>
|
|
91
|
+
nameSet ? nameSet.has(message.name) : true,
|
|
92
|
+
)
|
|
93
|
+
) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await new Promise<void>((resolve, reject) => {
|
|
98
|
+
const waiter: Waiter = {
|
|
99
|
+
nameSet,
|
|
100
|
+
resolve: () => {
|
|
101
|
+
this.#removeWaiter(waiter);
|
|
102
|
+
resolve();
|
|
103
|
+
},
|
|
104
|
+
reject: (error) => {
|
|
105
|
+
this.#removeWaiter(waiter);
|
|
106
|
+
reject(error);
|
|
107
|
+
},
|
|
108
|
+
abortSignal,
|
|
109
|
+
onAbort: () => {
|
|
110
|
+
waiter.reject(new EvictedError());
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
abortSignal.addEventListener("abort", waiter.onAbort, {
|
|
114
|
+
once: true,
|
|
115
|
+
});
|
|
116
|
+
this.#waiters.add(waiter);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#removeWaiter(waiter: Waiter): void {
|
|
121
|
+
if (this.#waiters.delete(waiter)) {
|
|
122
|
+
waiter.abortSignal.removeEventListener("abort", waiter.onAbort);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#notifyWaiters(name: string): void {
|
|
127
|
+
for (const waiter of [...this.#waiters]) {
|
|
128
|
+
if (waiter.nameSet && !waiter.nameSet.has(name)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
waiter.resolve();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
clear(): void {
|
|
136
|
+
this.#messages = [];
|
|
137
|
+
for (const waiter of [...this.#waiters]) {
|
|
138
|
+
waiter.reject(new Error("cleared"));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* In-memory implementation of EngineDriver for testing.
|
|
145
|
+
* Uses binary keys (Uint8Array) with hex encoding for internal Map storage.
|
|
146
|
+
*/
|
|
147
|
+
export class InMemoryDriver implements EngineDriver {
|
|
148
|
+
// Map from hex-encoded key to { originalKey, value }
|
|
149
|
+
private kv = new Map<string, { key: Uint8Array; value: Uint8Array }>();
|
|
150
|
+
private alarms = new Map<string, number>();
|
|
151
|
+
#inMemoryMessageDriver = new InMemoryWorkflowMessageDriver();
|
|
152
|
+
|
|
153
|
+
/** Simulated latency per operation (ms) */
|
|
154
|
+
latency = 10;
|
|
155
|
+
|
|
156
|
+
/** How often the worker polls for work */
|
|
157
|
+
workerPollInterval = 100;
|
|
158
|
+
messageDriver: WorkflowMessageDriver = this.#inMemoryMessageDriver;
|
|
159
|
+
|
|
160
|
+
async get(key: Uint8Array): Promise<Uint8Array | null> {
|
|
161
|
+
await sleep(this.latency);
|
|
162
|
+
const entry = this.kv.get(keyToHex(key));
|
|
163
|
+
return entry?.value ?? null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async set(key: Uint8Array, value: Uint8Array): Promise<void> {
|
|
167
|
+
await sleep(this.latency);
|
|
168
|
+
this.kv.set(keyToHex(key), { key, value });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async delete(key: Uint8Array): Promise<void> {
|
|
172
|
+
await sleep(this.latency);
|
|
173
|
+
this.kv.delete(keyToHex(key));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async deletePrefix(prefix: Uint8Array): Promise<void> {
|
|
177
|
+
await sleep(this.latency);
|
|
178
|
+
for (const [hexKey, entry] of this.kv) {
|
|
179
|
+
if (keyStartsWith(entry.key, prefix)) {
|
|
180
|
+
this.kv.delete(hexKey);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async deleteRange(start: Uint8Array, end: Uint8Array): Promise<void> {
|
|
186
|
+
await sleep(this.latency);
|
|
187
|
+
for (const [hexKey, entry] of this.kv) {
|
|
188
|
+
if (
|
|
189
|
+
compareKeys(entry.key, start) >= 0 &&
|
|
190
|
+
compareKeys(entry.key, end) < 0
|
|
191
|
+
) {
|
|
192
|
+
this.kv.delete(hexKey);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async list(prefix: Uint8Array): Promise<KVEntry[]> {
|
|
198
|
+
await sleep(this.latency);
|
|
199
|
+
const results: KVEntry[] = [];
|
|
200
|
+
for (const entry of this.kv.values()) {
|
|
201
|
+
if (keyStartsWith(entry.key, prefix)) {
|
|
202
|
+
results.push({ key: entry.key, value: entry.value });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Sort by key lexicographically
|
|
206
|
+
return results.sort((a, b) => compareKeys(a.key, b.key));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async batch(writes: KVWrite[]): Promise<void> {
|
|
210
|
+
await sleep(this.latency);
|
|
211
|
+
for (const { key, value } of writes) {
|
|
212
|
+
this.kv.set(keyToHex(key), { key, value });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async setAlarm(workflowId: string, wakeAt: number): Promise<void> {
|
|
217
|
+
await sleep(this.latency);
|
|
218
|
+
this.alarms.set(workflowId, wakeAt);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async clearAlarm(workflowId: string): Promise<void> {
|
|
222
|
+
await sleep(this.latency);
|
|
223
|
+
this.alarms.delete(workflowId);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async waitForMessages(
|
|
227
|
+
messageNames: string[],
|
|
228
|
+
abortSignal: AbortSignal,
|
|
229
|
+
): Promise<void> {
|
|
230
|
+
const driver = this.messageDriver as WorkflowMessageDriver & {
|
|
231
|
+
waitForMessages?: (
|
|
232
|
+
messageNames: string[],
|
|
233
|
+
abortSignal: AbortSignal,
|
|
234
|
+
) => Promise<void>;
|
|
235
|
+
};
|
|
236
|
+
if (driver.waitForMessages) {
|
|
237
|
+
await driver.waitForMessages(messageNames, abortSignal);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
while (true) {
|
|
242
|
+
if (abortSignal.aborted) {
|
|
243
|
+
throw new EvictedError();
|
|
244
|
+
}
|
|
245
|
+
const messages = await this.messageDriver.receiveMessages({
|
|
246
|
+
names: messageNames.length > 0 ? messageNames : undefined,
|
|
247
|
+
count: 1,
|
|
248
|
+
completable: true,
|
|
249
|
+
});
|
|
250
|
+
if (messages.length > 0) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
await sleep(Math.max(1, this.latency));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get the alarm time for a workflow (for testing).
|
|
259
|
+
*/
|
|
260
|
+
getAlarm(workflowId: string): number | undefined {
|
|
261
|
+
return this.alarms.get(workflowId);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Check if any alarms are due and return their workflow IDs.
|
|
266
|
+
*/
|
|
267
|
+
getDueAlarms(): string[] {
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
const due: string[] = [];
|
|
270
|
+
for (const [workflowId, wakeAt] of this.alarms) {
|
|
271
|
+
if (wakeAt <= now) {
|
|
272
|
+
due.push(workflowId);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return due;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Clear all data (for testing).
|
|
280
|
+
*/
|
|
281
|
+
clear(): void {
|
|
282
|
+
this.kv.clear();
|
|
283
|
+
this.alarms.clear();
|
|
284
|
+
this.#inMemoryMessageDriver.clear();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get a snapshot of all data (for testing/debugging).
|
|
289
|
+
*/
|
|
290
|
+
snapshot(): {
|
|
291
|
+
kv: Record<string, Uint8Array>;
|
|
292
|
+
alarms: Record<string, number>;
|
|
293
|
+
} {
|
|
294
|
+
const kvSnapshot: Record<string, Uint8Array> = {};
|
|
295
|
+
for (const [hexKey, entry] of this.kv) {
|
|
296
|
+
kvSnapshot[hexKey] = entry.value;
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
kv: kvSnapshot,
|
|
300
|
+
alarms: Object.fromEntries(this.alarms),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get all hex-encoded keys (for testing).
|
|
306
|
+
*/
|
|
307
|
+
keys(): string[] {
|
|
308
|
+
return [...this.kv.keys()];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Re-export main exports for convenience
|
|
313
|
+
export * from "./index.js";
|