@rivetkit/workflow-engine 0.0.0-main.6151ab0 → 0.0.0-main.78336a1

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing.ts"],"sourcesContent":["import type { EngineDriver, KVEntry, KVWrite } from \"./driver.js\";\nimport { EvictedError } from \"./errors.js\";\nimport { compareKeys, keyStartsWith, keyToHex } from \"./keys.js\";\nimport type { Message, WorkflowMessageDriver } from \"./types.js\";\nimport { sleep } from \"./utils.js\";\n\ninterface Waiter {\n\tnameSet?: Set<string>;\n\tresolve: () => void;\n\treject: (error: Error) => void;\n\tabortSignal: AbortSignal;\n\tonAbort: () => void;\n}\n\nclass InMemoryWorkflowMessageDriver implements WorkflowMessageDriver {\n\t#messages: Message[] = [];\n\t#waiters = new Set<Waiter>();\n\n\tasync addMessage(message: Message): Promise<void> {\n\t\tthis.#messages.push(message);\n\t\tthis.#notifyWaiters(message.name);\n\t}\n\n\tasync receiveMessages(opts: {\n\t\tnames?: readonly string[];\n\t\tcount: number;\n\t\tcompletable: boolean;\n\t}): Promise<Message[]> {\n\t\tconst limitedCount = Math.max(1, opts.count);\n\t\tconst nameSet =\n\t\t\topts.names && opts.names.length > 0\n\t\t\t\t? new Set(opts.names)\n\t\t\t\t: undefined;\n\t\tconst selected: Array<{ message: Message; index: number }> = [];\n\n\t\tfor (\n\t\t\tlet i = 0;\n\t\t\ti < this.#messages.length && selected.length < limitedCount;\n\t\t\ti++\n\t\t) {\n\t\t\tconst message = this.#messages[i];\n\t\t\tif (nameSet && !nameSet.has(message.name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tselected.push({ message, index: i });\n\t\t}\n\n\t\tif (selected.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tif (!opts.completable) {\n\t\t\tfor (let i = selected.length - 1; i >= 0; i--) {\n\t\t\t\tthis.#messages.splice(selected[i].index, 1);\n\t\t\t}\n\t\t\treturn selected.map((entry) => entry.message);\n\t\t}\n\n\t\treturn selected.map((entry) => {\n\t\t\tconst { message } = entry;\n\t\t\treturn {\n\t\t\t\t...message,\n\t\t\t\tcomplete: async () => {\n\t\t\t\t\tawait this.completeMessage(message.id);\n\t\t\t\t},\n\t\t\t};\n\t\t});\n\t}\n\n\tasync completeMessage(messageId: string): Promise<void> {\n\t\tconst index = this.#messages.findIndex(\n\t\t\t(message) => message.id === messageId,\n\t\t);\n\t\tif (index !== -1) {\n\t\t\tthis.#messages.splice(index, 1);\n\t\t}\n\t}\n\n\tasync waitForMessages(\n\t\tmessageNames: string[],\n\t\tabortSignal: AbortSignal,\n\t): Promise<void> {\n\t\tif (abortSignal.aborted) {\n\t\t\tthrow new EvictedError();\n\t\t}\n\n\t\tconst nameSet =\n\t\t\tmessageNames.length > 0 ? new Set(messageNames) : undefined;\n\t\tif (\n\t\t\tthis.#messages.some((message) =>\n\t\t\t\tnameSet ? nameSet.has(message.name) : true,\n\t\t\t)\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst waiter: Waiter = {\n\t\t\t\tnameSet,\n\t\t\t\tresolve: () => {\n\t\t\t\t\tthis.#removeWaiter(waiter);\n\t\t\t\t\tresolve();\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tthis.#removeWaiter(waiter);\n\t\t\t\t\treject(error);\n\t\t\t\t},\n\t\t\t\tabortSignal,\n\t\t\t\tonAbort: () => {\n\t\t\t\t\twaiter.reject(new EvictedError());\n\t\t\t\t},\n\t\t\t};\n\t\t\tabortSignal.addEventListener(\"abort\", waiter.onAbort, {\n\t\t\t\tonce: true,\n\t\t\t});\n\t\t\tthis.#waiters.add(waiter);\n\t\t});\n\t}\n\n\t#removeWaiter(waiter: Waiter): void {\n\t\tif (this.#waiters.delete(waiter)) {\n\t\t\twaiter.abortSignal.removeEventListener(\"abort\", waiter.onAbort);\n\t\t}\n\t}\n\n\t#notifyWaiters(name: string): void {\n\t\tfor (const waiter of [...this.#waiters]) {\n\t\t\tif (waiter.nameSet && !waiter.nameSet.has(name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twaiter.resolve();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.#messages = [];\n\t\tfor (const waiter of [...this.#waiters]) {\n\t\t\twaiter.reject(new Error(\"cleared\"));\n\t\t}\n\t}\n}\n\n/**\n * In-memory implementation of EngineDriver for testing.\n * Uses binary keys (Uint8Array) with hex encoding for internal Map storage.\n */\nexport class InMemoryDriver implements EngineDriver {\n\t// Map from hex-encoded key to { originalKey, value }\n\tprivate kv = new Map<string, { key: Uint8Array; value: Uint8Array }>();\n\tprivate alarms = new Map<string, number>();\n\t#inMemoryMessageDriver = new InMemoryWorkflowMessageDriver();\n\n\t/** Simulated latency per operation (ms) */\n\tlatency = 10;\n\n\t/** How often the worker polls for work */\n\tworkerPollInterval = 100;\n\tmessageDriver: WorkflowMessageDriver = this.#inMemoryMessageDriver;\n\n\tasync get(key: Uint8Array): Promise<Uint8Array | null> {\n\t\tawait sleep(this.latency);\n\t\tconst entry = this.kv.get(keyToHex(key));\n\t\treturn entry?.value ?? null;\n\t}\n\n\tasync set(key: Uint8Array, value: Uint8Array): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tthis.kv.set(keyToHex(key), { key, value });\n\t}\n\n\tasync delete(key: Uint8Array): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tthis.kv.delete(keyToHex(key));\n\t}\n\n\tasync deletePrefix(prefix: Uint8Array): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tfor (const [hexKey, entry] of this.kv) {\n\t\t\tif (keyStartsWith(entry.key, prefix)) {\n\t\t\t\tthis.kv.delete(hexKey);\n\t\t\t}\n\t\t}\n\t}\n\n\tasync deleteRange(start: Uint8Array, end: Uint8Array): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tfor (const [hexKey, entry] of this.kv) {\n\t\t\tif (\n\t\t\t\tcompareKeys(entry.key, start) >= 0 &&\n\t\t\t\tcompareKeys(entry.key, end) < 0\n\t\t\t) {\n\t\t\t\tthis.kv.delete(hexKey);\n\t\t\t}\n\t\t}\n\t}\n\n\tasync list(prefix: Uint8Array): Promise<KVEntry[]> {\n\t\tawait sleep(this.latency);\n\t\tconst results: KVEntry[] = [];\n\t\tfor (const entry of this.kv.values()) {\n\t\t\tif (keyStartsWith(entry.key, prefix)) {\n\t\t\t\tresults.push({ key: entry.key, value: entry.value });\n\t\t\t}\n\t\t}\n\t\t// Sort by key lexicographically\n\t\treturn results.sort((a, b) => compareKeys(a.key, b.key));\n\t}\n\n\tasync batch(writes: KVWrite[]): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tfor (const { key, value } of writes) {\n\t\t\tthis.kv.set(keyToHex(key), { key, value });\n\t\t}\n\t}\n\n\tasync setAlarm(workflowId: string, wakeAt: number): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tthis.alarms.set(workflowId, wakeAt);\n\t}\n\n\tasync clearAlarm(workflowId: string): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tthis.alarms.delete(workflowId);\n\t}\n\n\tasync waitForMessages(\n\t\tmessageNames: string[],\n\t\tabortSignal: AbortSignal,\n\t): Promise<void> {\n\t\tconst driver = this.messageDriver as WorkflowMessageDriver & {\n\t\t\twaitForMessages?: (\n\t\t\t\tmessageNames: string[],\n\t\t\t\tabortSignal: AbortSignal,\n\t\t\t) => Promise<void>;\n\t\t};\n\t\tif (driver.waitForMessages) {\n\t\t\tawait driver.waitForMessages(messageNames, abortSignal);\n\t\t\treturn;\n\t\t}\n\n\t\twhile (true) {\n\t\t\tif (abortSignal.aborted) {\n\t\t\t\tthrow new EvictedError();\n\t\t\t}\n\t\t\tconst messages = await this.messageDriver.receiveMessages({\n\t\t\t\tnames: messageNames.length > 0 ? messageNames : undefined,\n\t\t\t\tcount: 1,\n\t\t\t\tcompletable: true,\n\t\t\t});\n\t\t\tif (messages.length > 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait sleep(Math.max(1, this.latency));\n\t\t}\n\t}\n\n\t/**\n\t * Get the alarm time for a workflow (for testing).\n\t */\n\tgetAlarm(workflowId: string): number | undefined {\n\t\treturn this.alarms.get(workflowId);\n\t}\n\n\t/**\n\t * Check if any alarms are due and return their workflow IDs.\n\t */\n\tgetDueAlarms(): string[] {\n\t\tconst now = Date.now();\n\t\tconst due: string[] = [];\n\t\tfor (const [workflowId, wakeAt] of this.alarms) {\n\t\t\tif (wakeAt <= now) {\n\t\t\t\tdue.push(workflowId);\n\t\t\t}\n\t\t}\n\t\treturn due;\n\t}\n\n\t/**\n\t * Clear all data (for testing).\n\t */\n\tclear(): void {\n\t\tthis.kv.clear();\n\t\tthis.alarms.clear();\n\t\tthis.#inMemoryMessageDriver.clear();\n\t}\n\n\t/**\n\t * Get a snapshot of all data (for testing/debugging).\n\t */\n\tsnapshot(): {\n\t\tkv: Record<string, Uint8Array>;\n\t\talarms: Record<string, number>;\n\t} {\n\t\tconst kvSnapshot: Record<string, Uint8Array> = {};\n\t\tfor (const [hexKey, entry] of this.kv) {\n\t\t\tkvSnapshot[hexKey] = entry.value;\n\t\t}\n\t\treturn {\n\t\t\tkv: kvSnapshot,\n\t\t\talarms: Object.fromEntries(this.alarms),\n\t\t};\n\t}\n\n\t/**\n\t * Get all hex-encoded keys (for testing).\n\t */\n\tkeys(): string[] {\n\t\treturn [...this.kv.keys()];\n\t}\n}\n\n// Re-export main exports for convenience\nexport * from \"./index.js\";\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/testing.ts"],"sourcesContent":["import type { EngineDriver, KVEntry, KVWrite } from \"./driver.js\";\nimport { EvictedError } from \"./errors.js\";\nimport { compareKeys, keyStartsWith, keyToHex } from \"./keys.js\";\nimport type { Message, WorkflowMessageDriver } from \"./types.js\";\nimport { sleep } from \"./utils.js\";\n\ninterface Waiter {\n\tnameSet?: Set<string>;\n\tresolve: () => void;\n\treject: (error: Error) => void;\n\tabortSignal: AbortSignal;\n\tonAbort: () => void;\n}\n\nclass InMemoryWorkflowMessageDriver implements WorkflowMessageDriver {\n\t#messages: Message[] = [];\n\t#waiters = new Set<Waiter>();\n\n\tasync addMessage(message: Message): Promise<void> {\n\t\tthis.#messages.push(message);\n\t\tthis.#notifyWaiters(message.name);\n\t}\n\n\tasync receiveMessages(opts: {\n\t\tnames?: readonly string[];\n\t\tcount: number;\n\t\tcompletable: boolean;\n\t}): Promise<Message[]> {\n\t\tconst limitedCount = Math.max(1, opts.count);\n\t\tconst nameSet =\n\t\t\topts.names && opts.names.length > 0\n\t\t\t\t? new Set(opts.names)\n\t\t\t\t: undefined;\n\t\tconst selected: Array<{ message: Message; index: number }> = [];\n\n\t\tfor (\n\t\t\tlet i = 0;\n\t\t\ti < this.#messages.length && selected.length < limitedCount;\n\t\t\ti++\n\t\t) {\n\t\t\tconst message = this.#messages[i];\n\t\t\tif (nameSet && !nameSet.has(message.name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tselected.push({ message, index: i });\n\t\t}\n\n\t\tif (selected.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tif (!opts.completable) {\n\t\t\tfor (let i = selected.length - 1; i >= 0; i--) {\n\t\t\t\tthis.#messages.splice(selected[i].index, 1);\n\t\t\t}\n\t\t\treturn selected.map((entry) => entry.message);\n\t\t}\n\n\t\treturn selected.map((entry) => {\n\t\t\tconst { message } = entry;\n\t\t\treturn {\n\t\t\t\t...message,\n\t\t\t\tcomplete: async () => {\n\t\t\t\t\tawait this.completeMessage(message.id);\n\t\t\t\t},\n\t\t\t};\n\t\t});\n\t}\n\n\tasync completeMessage(messageId: string): Promise<void> {\n\t\tconst index = this.#messages.findIndex(\n\t\t\t(message) => message.id === messageId,\n\t\t);\n\t\tif (index !== -1) {\n\t\t\tthis.#messages.splice(index, 1);\n\t\t}\n\t}\n\n\tasync waitForMessages(\n\t\tmessageNames: string[],\n\t\tabortSignal: AbortSignal,\n\t): Promise<void> {\n\t\tif (abortSignal.aborted) {\n\t\t\tthrow new EvictedError();\n\t\t}\n\n\t\tconst nameSet =\n\t\t\tmessageNames.length > 0 ? new Set(messageNames) : undefined;\n\t\tif (\n\t\t\tthis.#messages.some((message) =>\n\t\t\t\tnameSet ? nameSet.has(message.name) : true,\n\t\t\t)\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst waiter: Waiter = {\n\t\t\t\tnameSet,\n\t\t\t\tresolve: () => {\n\t\t\t\t\tthis.#removeWaiter(waiter);\n\t\t\t\t\tresolve();\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tthis.#removeWaiter(waiter);\n\t\t\t\t\treject(error);\n\t\t\t\t},\n\t\t\t\tabortSignal,\n\t\t\t\tonAbort: () => {\n\t\t\t\t\twaiter.reject(new EvictedError());\n\t\t\t\t},\n\t\t\t};\n\t\t\tabortSignal.addEventListener(\"abort\", waiter.onAbort, {\n\t\t\t\tonce: true,\n\t\t\t});\n\t\t\tthis.#waiters.add(waiter);\n\t\t});\n\t}\n\n\t#removeWaiter(waiter: Waiter): void {\n\t\tif (this.#waiters.delete(waiter)) {\n\t\t\twaiter.abortSignal.removeEventListener(\"abort\", waiter.onAbort);\n\t\t}\n\t}\n\n\t#notifyWaiters(name: string): void {\n\t\tfor (const waiter of [...this.#waiters]) {\n\t\t\tif (waiter.nameSet && !waiter.nameSet.has(name)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twaiter.resolve();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.#messages = [];\n\t\tfor (const waiter of [...this.#waiters]) {\n\t\t\twaiter.reject(new Error(\"cleared\"));\n\t\t}\n\t}\n}\n\n/**\n * In-memory implementation of EngineDriver for testing.\n * Uses binary keys (Uint8Array) with hex encoding for internal Map storage.\n */\nexport class InMemoryDriver implements EngineDriver {\n\t// Map from hex-encoded key to { originalKey, value }\n\tprivate kv = new Map<string, { key: Uint8Array; value: Uint8Array }>();\n\tprivate alarms = new Map<string, number>();\n\t#inMemoryMessageDriver = new InMemoryWorkflowMessageDriver();\n\n\t/** Simulated latency per operation (ms) */\n\tlatency = 10;\n\n\t/** How often the worker polls for work */\n\tworkerPollInterval = 100;\n\tmessageDriver: WorkflowMessageDriver = this.#inMemoryMessageDriver;\n\n\tasync get(key: Uint8Array): Promise<Uint8Array | null> {\n\t\tawait sleep(this.latency);\n\t\tconst entry = this.kv.get(keyToHex(key));\n\t\treturn entry?.value ?? null;\n\t}\n\n\tasync set(key: Uint8Array, value: Uint8Array): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tthis.kv.set(keyToHex(key), { key, value });\n\t}\n\n\tasync delete(key: Uint8Array): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tthis.kv.delete(keyToHex(key));\n\t}\n\n\tasync batchDelete(keys: Uint8Array[]): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tfor (const key of keys) {\n\t\t\tthis.kv.delete(keyToHex(key));\n\t\t}\n\t}\n\n\tasync deletePrefix(prefix: Uint8Array): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tfor (const [hexKey, entry] of this.kv) {\n\t\t\tif (keyStartsWith(entry.key, prefix)) {\n\t\t\t\tthis.kv.delete(hexKey);\n\t\t\t}\n\t\t}\n\t}\n\n\tasync deleteRange(start: Uint8Array, end: Uint8Array): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tfor (const [hexKey, entry] of this.kv) {\n\t\t\tif (\n\t\t\t\tcompareKeys(entry.key, start) >= 0 &&\n\t\t\t\tcompareKeys(entry.key, end) < 0\n\t\t\t) {\n\t\t\t\tthis.kv.delete(hexKey);\n\t\t\t}\n\t\t}\n\t}\n\n\tasync list(prefix: Uint8Array): Promise<KVEntry[]> {\n\t\tawait sleep(this.latency);\n\t\tconst results: KVEntry[] = [];\n\t\tfor (const entry of this.kv.values()) {\n\t\t\tif (keyStartsWith(entry.key, prefix)) {\n\t\t\t\tresults.push({ key: entry.key, value: entry.value });\n\t\t\t}\n\t\t}\n\t\t// Sort by key lexicographically\n\t\treturn results.sort((a, b) => compareKeys(a.key, b.key));\n\t}\n\n\tasync batch(writes: KVWrite[]): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tfor (const { key, value } of writes) {\n\t\t\tthis.kv.set(keyToHex(key), { key, value });\n\t\t}\n\t}\n\n\tasync setAlarm(workflowId: string, wakeAt: number): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tthis.alarms.set(workflowId, wakeAt);\n\t}\n\n\tasync clearAlarm(workflowId: string): Promise<void> {\n\t\tawait sleep(this.latency);\n\t\tthis.alarms.delete(workflowId);\n\t}\n\n\tasync waitForMessages(\n\t\tmessageNames: string[],\n\t\tabortSignal: AbortSignal,\n\t): Promise<void> {\n\t\tconst driver = this.messageDriver as WorkflowMessageDriver & {\n\t\t\twaitForMessages?: (\n\t\t\t\tmessageNames: string[],\n\t\t\t\tabortSignal: AbortSignal,\n\t\t\t) => Promise<void>;\n\t\t};\n\t\tif (driver.waitForMessages) {\n\t\t\tawait driver.waitForMessages(messageNames, abortSignal);\n\t\t\treturn;\n\t\t}\n\n\t\twhile (true) {\n\t\t\tif (abortSignal.aborted) {\n\t\t\t\tthrow new EvictedError();\n\t\t\t}\n\t\t\tconst messages = await this.messageDriver.receiveMessages({\n\t\t\t\tnames: messageNames.length > 0 ? messageNames : undefined,\n\t\t\t\tcount: 1,\n\t\t\t\tcompletable: true,\n\t\t\t});\n\t\t\tif (messages.length > 0) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait sleep(Math.max(1, this.latency));\n\t\t}\n\t}\n\n\t/**\n\t * Get the alarm time for a workflow (for testing).\n\t */\n\tgetAlarm(workflowId: string): number | undefined {\n\t\treturn this.alarms.get(workflowId);\n\t}\n\n\t/**\n\t * Check if any alarms are due and return their workflow IDs.\n\t */\n\tgetDueAlarms(): string[] {\n\t\tconst now = Date.now();\n\t\tconst due: string[] = [];\n\t\tfor (const [workflowId, wakeAt] of this.alarms) {\n\t\t\tif (wakeAt <= now) {\n\t\t\t\tdue.push(workflowId);\n\t\t\t}\n\t\t}\n\t\treturn due;\n\t}\n\n\t/**\n\t * Clear all data (for testing).\n\t */\n\tclear(): void {\n\t\tthis.kv.clear();\n\t\tthis.alarms.clear();\n\t\tthis.#inMemoryMessageDriver.clear();\n\t}\n\n\t/**\n\t * Get a snapshot of all data (for testing/debugging).\n\t */\n\tsnapshot(): {\n\t\tkv: Record<string, Uint8Array>;\n\t\talarms: Record<string, number>;\n\t} {\n\t\tconst kvSnapshot: Record<string, Uint8Array> = {};\n\t\tfor (const [hexKey, entry] of this.kv) {\n\t\t\tkvSnapshot[hexKey] = entry.value;\n\t\t}\n\t\treturn {\n\t\t\tkv: kvSnapshot,\n\t\t\talarms: Object.fromEntries(this.alarms),\n\t\t};\n\t}\n\n\t/**\n\t * Get all hex-encoded keys (for testing).\n\t */\n\tkeys(): string[] {\n\t\treturn [...this.kv.keys()];\n\t}\n}\n\n// Re-export main exports for convenience\nexport * from \"./index.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,IAAM,gCAAN,MAAqE;AAAA,EACpE,YAAuB,CAAC;AAAA,EACxB,WAAW,oBAAI,IAAY;AAAA,EAE3B,MAAM,WAAW,SAAiC;AACjD,SAAK,UAAU,KAAK,OAAO;AAC3B,SAAK,eAAe,QAAQ,IAAI;AAAA,EACjC;AAAA,EAEA,MAAM,gBAAgB,MAIC;AACtB,UAAM,eAAe,KAAK,IAAI,GAAG,KAAK,KAAK;AAC3C,UAAM,UACL,KAAK,SAAS,KAAK,MAAM,SAAS,IAC/B,IAAI,IAAI,KAAK,KAAK,IAClB;AACJ,UAAM,WAAuD,CAAC;AAE9D,aACK,IAAI,GACR,IAAI,KAAK,UAAU,UAAU,SAAS,SAAS,cAC/C,KACC;AACD,YAAM,UAAU,KAAK,UAAU,CAAC;AAChC,UAAI,WAAW,CAAC,QAAQ,IAAI,QAAQ,IAAI,GAAG;AAC1C;AAAA,MACD;AACA,eAAS,KAAK,EAAE,SAAS,OAAO,EAAE,CAAC;AAAA,IACpC;AAEA,QAAI,SAAS,WAAW,GAAG;AAC1B,aAAO,CAAC;AAAA,IACT;AAEA,QAAI,CAAC,KAAK,aAAa;AACtB,eAAS,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;AAC9C,aAAK,UAAU,OAAO,SAAS,CAAC,EAAE,OAAO,CAAC;AAAA,MAC3C;AACA,aAAO,SAAS,IAAI,CAAC,UAAU,MAAM,OAAO;AAAA,IAC7C;AAEA,WAAO,SAAS,IAAI,CAAC,UAAU;AAC9B,YAAM,EAAE,QAAQ,IAAI;AACpB,aAAO;AAAA,QACN,GAAG;AAAA,QACH,UAAU,YAAY;AACrB,gBAAM,KAAK,gBAAgB,QAAQ,EAAE;AAAA,QACtC;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEA,MAAM,gBAAgB,WAAkC;AACvD,UAAM,QAAQ,KAAK,UAAU;AAAA,MAC5B,CAAC,YAAY,QAAQ,OAAO;AAAA,IAC7B;AACA,QAAI,UAAU,IAAI;AACjB,WAAK,UAAU,OAAO,OAAO,CAAC;AAAA,IAC/B;AAAA,EACD;AAAA,EAEA,MAAM,gBACL,cACA,aACgB;AAChB,QAAI,YAAY,SAAS;AACxB,YAAM,IAAI,aAAa;AAAA,IACxB;AAEA,UAAM,UACL,aAAa,SAAS,IAAI,IAAI,IAAI,YAAY,IAAI;AACnD,QACC,KAAK,UAAU;AAAA,MAAK,CAAC,YACpB,UAAU,QAAQ,IAAI,QAAQ,IAAI,IAAI;AAAA,IACvC,GACC;AACD;AAAA,IACD;AAEA,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,YAAM,SAAiB;AAAA,QACtB;AAAA,QACA,SAAS,MAAM;AACd,eAAK,cAAc,MAAM;AACzB,kBAAQ;AAAA,QACT;AAAA,QACA,QAAQ,CAAC,UAAU;AAClB,eAAK,cAAc,MAAM;AACzB,iBAAO,KAAK;AAAA,QACb;AAAA,QACA;AAAA,QACA,SAAS,MAAM;AACd,iBAAO,OAAO,IAAI,aAAa,CAAC;AAAA,QACjC;AAAA,MACD;AACA,kBAAY,iBAAiB,SAAS,OAAO,SAAS;AAAA,QACrD,MAAM;AAAA,MACP,CAAC;AACD,WAAK,SAAS,IAAI,MAAM;AAAA,IACzB,CAAC;AAAA,EACF;AAAA,EAEA,cAAc,QAAsB;AACnC,QAAI,KAAK,SAAS,OAAO,MAAM,GAAG;AACjC,aAAO,YAAY,oBAAoB,SAAS,OAAO,OAAO;AAAA,IAC/D;AAAA,EACD;AAAA,EAEA,eAAe,MAAoB;AAClC,eAAW,UAAU,CAAC,GAAG,KAAK,QAAQ,GAAG;AACxC,UAAI,OAAO,WAAW,CAAC,OAAO,QAAQ,IAAI,IAAI,GAAG;AAChD;AAAA,MACD;AACA,aAAO,QAAQ;AAAA,IAChB;AAAA,EACD;AAAA,EAEA,QAAc;AACb,SAAK,YAAY,CAAC;AAClB,eAAW,UAAU,CAAC,GAAG,KAAK,QAAQ,GAAG;AACxC,aAAO,OAAO,IAAI,MAAM,SAAS,CAAC;AAAA,IACnC;AAAA,EACD;AACD;AAMO,IAAM,iBAAN,MAA6C;AAAA;AAAA,EAE3C,KAAK,oBAAI,IAAoD;AAAA,EAC7D,SAAS,oBAAI,IAAoB;AAAA,EACzC,yBAAyB,IAAI,8BAA8B;AAAA;AAAA,EAG3D,UAAU;AAAA;AAAA,EAGV,qBAAqB;AAAA,EACrB,gBAAuC,KAAK;AAAA,EAE5C,MAAM,IAAI,KAA6C;AACtD,UAAM,MAAM,KAAK,OAAO;AACxB,UAAM,QAAQ,KAAK,GAAG,IAAI,SAAS,GAAG,CAAC;AACvC,YAAO,+BAAO,UAAS;AAAA,EACxB;AAAA,EAEA,MAAM,IAAI,KAAiB,OAAkC;AAC5D,UAAM,MAAM,KAAK,OAAO;AACxB,SAAK,GAAG,IAAI,SAAS,GAAG,GAAG,EAAE,KAAK,MAAM,CAAC;AAAA,EAC1C;AAAA,EAEA,MAAM,OAAO,KAAgC;AAC5C,UAAM,MAAM,KAAK,OAAO;AACxB,SAAK,GAAG,OAAO,SAAS,GAAG,CAAC;AAAA,EAC7B;AAAA,EAEA,MAAM,YAAY,MAAmC;AACpD,UAAM,MAAM,KAAK,OAAO;AACxB,eAAW,OAAO,MAAM;AACvB,WAAK,GAAG,OAAO,SAAS,GAAG,CAAC;AAAA,IAC7B;AAAA,EACD;AAAA,EAEA,MAAM,aAAa,QAAmC;AACrD,UAAM,MAAM,KAAK,OAAO;AACxB,eAAW,CAAC,QAAQ,KAAK,KAAK,KAAK,IAAI;AACtC,UAAI,cAAc,MAAM,KAAK,MAAM,GAAG;AACrC,aAAK,GAAG,OAAO,MAAM;AAAA,MACtB;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAM,YAAY,OAAmB,KAAgC;AACpE,UAAM,MAAM,KAAK,OAAO;AACxB,eAAW,CAAC,QAAQ,KAAK,KAAK,KAAK,IAAI;AACtC,UACC,YAAY,MAAM,KAAK,KAAK,KAAK,KACjC,YAAY,MAAM,KAAK,GAAG,IAAI,GAC7B;AACD,aAAK,GAAG,OAAO,MAAM;AAAA,MACtB;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAM,KAAK,QAAwC;AAClD,UAAM,MAAM,KAAK,OAAO;AACxB,UAAM,UAAqB,CAAC;AAC5B,eAAW,SAAS,KAAK,GAAG,OAAO,GAAG;AACrC,UAAI,cAAc,MAAM,KAAK,MAAM,GAAG;AACrC,gBAAQ,KAAK,EAAE,KAAK,MAAM,KAAK,OAAO,MAAM,MAAM,CAAC;AAAA,MACpD;AAAA,IACD;AAEA,WAAO,QAAQ,KAAK,CAAC,GAAG,MAAM,YAAY,EAAE,KAAK,EAAE,GAAG,CAAC;AAAA,EACxD;AAAA,EAEA,MAAM,MAAM,QAAkC;AAC7C,UAAM,MAAM,KAAK,OAAO;AACxB,eAAW,EAAE,KAAK,MAAM,KAAK,QAAQ;AACpC,WAAK,GAAG,IAAI,SAAS,GAAG,GAAG,EAAE,KAAK,MAAM,CAAC;AAAA,IAC1C;AAAA,EACD;AAAA,EAEA,MAAM,SAAS,YAAoB,QAA+B;AACjE,UAAM,MAAM,KAAK,OAAO;AACxB,SAAK,OAAO,IAAI,YAAY,MAAM;AAAA,EACnC;AAAA,EAEA,MAAM,WAAW,YAAmC;AACnD,UAAM,MAAM,KAAK,OAAO;AACxB,SAAK,OAAO,OAAO,UAAU;AAAA,EAC9B;AAAA,EAEA,MAAM,gBACL,cACA,aACgB;AAChB,UAAM,SAAS,KAAK;AAMpB,QAAI,OAAO,iBAAiB;AAC3B,YAAM,OAAO,gBAAgB,cAAc,WAAW;AACtD;AAAA,IACD;AAEA,WAAO,MAAM;AACZ,UAAI,YAAY,SAAS;AACxB,cAAM,IAAI,aAAa;AAAA,MACxB;AACA,YAAM,WAAW,MAAM,KAAK,cAAc,gBAAgB;AAAA,QACzD,OAAO,aAAa,SAAS,IAAI,eAAe;AAAA,QAChD,OAAO;AAAA,QACP,aAAa;AAAA,MACd,CAAC;AACD,UAAI,SAAS,SAAS,GAAG;AACxB;AAAA,MACD;AACA,YAAM,MAAM,KAAK,IAAI,GAAG,KAAK,OAAO,CAAC;AAAA,IACtC;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,YAAwC;AAChD,WAAO,KAAK,OAAO,IAAI,UAAU;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKA,eAAyB;AACxB,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,MAAgB,CAAC;AACvB,eAAW,CAAC,YAAY,MAAM,KAAK,KAAK,QAAQ;AAC/C,UAAI,UAAU,KAAK;AAClB,YAAI,KAAK,UAAU;AAAA,MACpB;AAAA,IACD;AACA,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACb,SAAK,GAAG,MAAM;AACd,SAAK,OAAO,MAAM;AAClB,SAAK,uBAAuB,MAAM;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA,EAKA,WAGE;AACD,UAAM,aAAyC,CAAC;AAChD,eAAW,CAAC,QAAQ,KAAK,KAAK,KAAK,IAAI;AACtC,iBAAW,MAAM,IAAI,MAAM;AAAA,IAC5B;AACA,WAAO;AAAA,MACN,IAAI;AAAA,MACJ,QAAQ,OAAO,YAAY,KAAK,MAAM;AAAA,IACvC;AAAA,EACD;AAAA;AAAA;AAAA;AAAA,EAKA,OAAiB;AAChB,WAAO,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC;AAAA,EAC1B;AACD;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivetkit/workflow-engine",
3
- "version": "0.0.0-main.6151ab0",
3
+ "version": "0.0.0-main.78336a1",
4
4
  "description": "Durable workflow engine with reentrant execution",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -66,5 +66,10 @@
66
66
  "tsup": "^8.4.0",
67
67
  "typescript": "^5.7.3",
68
68
  "vitest": "^3.1.1"
69
+ },
70
+ "repository": {
71
+ "type": "git",
72
+ "url": "https://github.com/rivet-dev/actors.git",
73
+ "directory": "rivetkit-typescript/packages/workflow-engine"
69
74
  }
70
75
  }
package/schemas/serde.ts CHANGED
@@ -18,7 +18,6 @@ import type {
18
18
  EntryMetadata as InternalEntryMetadata,
19
19
  EntryStatus as InternalEntryStatus,
20
20
  Location as InternalLocation,
21
- LoopIterationMarker as InternalLoopIterationMarker,
22
21
  PathSegment as InternalPathSegment,
23
22
  SleepState as InternalSleepState,
24
23
  WorkflowState as InternalWorkflowState,
@@ -27,7 +26,6 @@ import {
27
26
  CURRENT_VERSION,
28
27
  ENTRY_METADATA_VERSIONED,
29
28
  ENTRY_VERSIONED,
30
- WORKFLOW_METADATA_VERSIONED,
31
29
  } from "./versioned.js";
32
30
 
33
31
  // === Helper: ArrayBuffer to/from utilities ===
@@ -74,7 +72,7 @@ function assertString(
74
72
  /**
75
73
  * Validate that a value is a number.
76
74
  */
77
- function assertNumber(
75
+ function _assertNumber(
78
76
  value: unknown,
79
77
  context: string,
80
78
  ): asserts value is number {
@@ -309,6 +307,14 @@ function entryKindToBare(kind: InternalEntryKind): v1.EntryKind {
309
307
  originalName: kind.data.originalName ?? null,
310
308
  },
311
309
  };
310
+ case "version_check":
311
+ return {
312
+ tag: "VersionCheckEntry",
313
+ val: {
314
+ resolved: kind.data.resolved,
315
+ latest: kind.data.latest,
316
+ },
317
+ };
312
318
  }
313
319
  }
314
320
 
@@ -398,6 +404,14 @@ function entryKindFromBare(kind: v1.EntryKind): InternalEntryKind {
398
404
  originalName: kind.val.originalName ?? undefined,
399
405
  },
400
406
  };
407
+ case "VersionCheckEntry":
408
+ return {
409
+ type: "version_check",
410
+ data: {
411
+ resolved: kind.val.resolved,
412
+ latest: kind.val.latest,
413
+ },
414
+ };
401
415
  default:
402
416
  throw new Error(
403
417
  `Unknown entry kind: ${(kind as { tag: string }).tag}`,
package/schemas/v1.bare CHANGED
@@ -122,6 +122,14 @@ type RemovedEntry struct {
122
122
  originalName: optional<str>
123
123
  }
124
124
 
125
+ # MARK: Version Check Entry
126
+ type VersionCheckEntry struct {
127
+ # The version this instance resolved to at this location
128
+ resolved: u32
129
+ # The `latest` value seen when first resolved (diagnostics)
130
+ latest: u32
131
+ }
132
+
125
133
  # MARK: Entry Kind
126
134
  # Type-specific entry data
127
135
  type EntryKind union {
@@ -132,7 +140,8 @@ type EntryKind union {
132
140
  RollbackCheckpointEntry |
133
141
  JoinEntry |
134
142
  RaceEntry |
135
- RemovedEntry
143
+ RemovedEntry |
144
+ VersionCheckEntry
136
145
  }
137
146
 
138
147
  # MARK: Entry
@@ -19,6 +19,7 @@ export type {
19
19
  RemovedEntry,
20
20
  SleepEntry,
21
21
  StepEntry,
22
+ VersionCheckEntry,
22
23
  WorkflowMetadata,
23
24
  } from "../dist/schemas/v1.js";
24
25
 
package/src/context.ts CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  StepExhaustedError,
22
22
  StepFailedError,
23
23
  } from "./errors.js";
24
- import { buildLoopIterationRange, buildEntryMetadataKey } from "./keys.js";
24
+ import { buildEntryMetadataKey, buildLoopIterationRange } from "./keys.js";
25
25
  import {
26
26
  appendLoopIteration,
27
27
  appendName,
@@ -36,13 +36,12 @@ import {
36
36
  flush,
37
37
  getOrCreateMetadata,
38
38
  loadMetadata,
39
- setEntry,
40
39
  type PendingDeletions,
40
+ setEntry,
41
41
  } from "./storage.js";
42
42
  import type {
43
43
  BranchConfig,
44
44
  BranchOutput,
45
- BranchStatus,
46
45
  Entry,
47
46
  EntryKindType,
48
47
  EntryMetadata,
@@ -66,13 +65,12 @@ import type {
66
65
  WorkflowError,
67
66
  WorkflowErrorEvent,
68
67
  WorkflowErrorHandler,
68
+ WorkflowMessageDriver,
69
69
  WorkflowQueue,
70
70
  WorkflowQueueMessage,
71
71
  WorkflowQueueNextBatchOptions,
72
72
  WorkflowQueueNextOptions,
73
- WorkflowMessageDriver,
74
73
  } from "./types.js";
75
- import { sleep } from "./utils.js";
76
74
 
77
75
  /**
78
76
  * Default values for step configuration.
@@ -505,8 +503,7 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
505
503
  * Throws HistoryDivergedError if duplicate detected.
506
504
  */
507
505
  private checkDuplicateName(name: string): void {
508
- const fullKey =
509
- locationToKey(this.storage, this.currentLocation) + "/" + name;
506
+ const fullKey = `${locationToKey(this.storage, this.currentLocation)}/${name}`;
510
507
  if (this.usedNamesInExecution.has(fullKey)) {
511
508
  throw new HistoryDivergedError(
512
509
  `Duplicate entry name "${name}" at location "${locationToKey(this.storage, this.currentLocation)}". ` +
@@ -580,7 +577,7 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
580
577
  const isUnderPrefix =
581
578
  prefix === ""
582
579
  ? true // Root: all keys are children
583
- : key.startsWith(prefix + "/") || key === prefix;
580
+ : key.startsWith(`${prefix}/`) || key === prefix;
584
581
 
585
582
  if (isUnderPrefix) {
586
583
  if (!this.visitedKeys.has(key)) {
@@ -603,25 +600,33 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
603
600
  }
604
601
 
605
602
  /**
606
- * Wait for eviction message.
607
- *
608
- * The event listener uses { once: true } to auto-remove after firing,
609
- * preventing memory leaks if this method is called multiple times.
603
+ * Wait for `ms`, rejecting early with EvictedError if the workflow is
604
+ * evicted. Both the timer and the abort listener are torn down on either
605
+ * outcome, so a completed sleep never leaves a dangling listener on the
606
+ * long-lived run abort signal.
610
607
  */
611
- waitForEviction(): Promise<never> {
612
- return new Promise((_, reject) => {
613
- if (this.abortSignal.aborted) {
614
- reject(new EvictedError());
615
- return;
608
+ private async sleepOrEvict(ms: number): Promise<void> {
609
+ if (this.abortSignal.aborted) {
610
+ throw new EvictedError();
611
+ }
612
+ let timer: ReturnType<typeof setTimeout> | undefined;
613
+ let onAbort: (() => void) | undefined;
614
+ try {
615
+ await new Promise<void>((resolve, reject) => {
616
+ timer = setTimeout(resolve, ms);
617
+ onAbort = () => reject(new EvictedError());
618
+ this.abortSignal.addEventListener("abort", onAbort, {
619
+ once: true,
620
+ });
621
+ });
622
+ } finally {
623
+ if (timer !== undefined) {
624
+ clearTimeout(timer);
616
625
  }
617
- this.abortSignal.addEventListener(
618
- "abort",
619
- () => {
620
- reject(new EvictedError());
621
- },
622
- { once: true },
623
- );
624
- });
626
+ if (onAbort) {
627
+ this.abortSignal.removeEventListener("abort", onAbort);
628
+ }
629
+ }
625
630
  }
626
631
 
627
632
  // === Step ===
@@ -825,10 +830,7 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
825
830
  const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
826
831
 
827
832
  if (metadata.attempts > maxRetries) {
828
- // Prefer step history error, but fall back to metadata since
829
- // driver implementations may persist metadata without the history
830
- // entry error (e.g. partial writes/crashes between attempts).
831
- const lastError = stepData.error ?? metadata.error;
833
+ const lastError = metadata.error;
832
834
  const exhaustedError = new StepExhaustedError(
833
835
  config.name,
834
836
  lastError,
@@ -941,28 +943,26 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
941
943
  });
942
944
  return output;
943
945
  } catch (error) {
944
- // Timeout errors are treated as critical (no retry)
945
- if (error instanceof StepTimeoutError) {
946
- if (entry.kind.type === "step") {
947
- entry.kind.data.error = String(error);
948
- }
949
- entry.dirty = true;
946
+ if (entry.kind.type === "step") {
947
+ entry.kind.data.error = String(error);
948
+ }
949
+ entry.dirty = true;
950
+
951
+ // Timeout errors are treated as critical by default. Steps opt
952
+ // into retrying on timeout with retryOnTimeout: true.
953
+ if (error instanceof StepTimeoutError && !config.retryOnTimeout) {
950
954
  metadata.status = "exhausted";
951
955
  metadata.error = String(error);
952
- await this.flushStorage();
953
956
  await this.notifyStepError(config, metadata.attempts, error, {
954
957
  willRetry: false,
955
958
  });
956
959
  throw markErrorReported(
957
- attachTryStepFailure(
958
- new CriticalError(error.message),
959
- {
960
- kind: "timeout",
961
- stepName: config.name,
962
- attempts: metadata.attempts,
963
- error: extractErrorInfo(error),
964
- },
965
- ),
960
+ attachTryStepFailure(new CriticalError(error.message), {
961
+ kind: "timeout",
962
+ stepName: config.name,
963
+ attempts: metadata.attempts,
964
+ error: extractErrorInfo(error),
965
+ }),
966
966
  );
967
967
  }
968
968
 
@@ -970,13 +970,8 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
970
970
  error instanceof CriticalError ||
971
971
  error instanceof RollbackError
972
972
  ) {
973
- if (entry.kind.type === "step") {
974
- entry.kind.data.error = String(error);
975
- }
976
- entry.dirty = true;
977
973
  metadata.status = "exhausted";
978
974
  metadata.error = String(error);
979
- await this.flushStorage();
980
975
  await this.notifyStepError(config, metadata.attempts, error, {
981
976
  willRetry: false,
982
977
  });
@@ -993,15 +988,10 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
993
988
  );
994
989
  }
995
990
 
996
- if (entry.kind.type === "step") {
997
- entry.kind.data.error = String(error);
998
- }
999
- entry.dirty = true;
1000
991
  const willRetry = metadata.attempts <= maxRetries;
1001
992
  metadata.status = willRetry ? "failed" : "exhausted";
1002
993
  metadata.error = String(error);
1003
994
 
1004
- await this.flushStorage();
1005
995
  if (willRetry) {
1006
996
  const retryDelay = calculateBackoff(
1007
997
  metadata.attempts,
@@ -1026,7 +1016,10 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1026
1016
  attachTryStepFailure(
1027
1017
  new StepExhaustedError(config.name, String(error)),
1028
1018
  {
1029
- kind: "exhausted",
1019
+ kind:
1020
+ error instanceof StepTimeoutError
1021
+ ? "timeout"
1022
+ : "exhausted",
1030
1023
  stepName: config.name,
1031
1024
  attempts: metadata.attempts,
1032
1025
  error: extractErrorInfo(error),
@@ -1045,7 +1038,9 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1045
1038
  *
1046
1039
  * Note: This does NOT cancel the underlying operation. JavaScript Promises
1047
1040
  * cannot be cancelled once started. When a timeout occurs:
1048
- * - The step is marked as failed with StepTimeoutError
1041
+ * - The step is rejected with StepTimeoutError. By default this is treated
1042
+ * as a critical failure with no retry. Set retryOnTimeout: true on the
1043
+ * step config to retry timeouts like any other error.
1049
1044
  * - The underlying async operation continues running in the background
1050
1045
  * - Any side effects from the operation may still occur
1051
1046
  *
@@ -1224,8 +1219,12 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1224
1219
  }
1225
1220
 
1226
1221
  const historyPruneInterval =
1227
- config.historyPruneInterval ?? DEFAULT_LOOP_HISTORY_PRUNE_INTERVAL;
1228
- const historySize = config.historySize ?? historyPruneInterval;
1222
+ config.historyPruneInterval ??
1223
+ config.commitInterval ??
1224
+ config.historyEvery ??
1225
+ DEFAULT_LOOP_HISTORY_PRUNE_INTERVAL;
1226
+ const historySize =
1227
+ config.historySize ?? config.historyKeep ?? historyPruneInterval;
1229
1228
 
1230
1229
  // Track the last iteration we pruned up to so we only delete
1231
1230
  // newly-expired iterations instead of re-scanning from 0.
@@ -1492,7 +1491,7 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
1492
1491
 
1493
1492
  // Short sleep: wait in memory
1494
1493
  if (remaining < this.driver.workerPollInterval) {
1495
- await Promise.race([sleep(remaining), this.waitForEviction()]);
1494
+ await this.sleepOrEvict(remaining);
1496
1495
 
1497
1496
  this.checkEvicted();
1498
1497
 
@@ -2582,4 +2581,97 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
2582
2581
  setEntry(this.storage, location, entry);
2583
2582
  await this.flushStorage();
2584
2583
  }
2584
+
2585
+ // === Version ===
2586
+
2587
+ async getVersion(name: string, latest: number): Promise<number> {
2588
+ this.assertNotInProgress();
2589
+ this.checkEvicted();
2590
+
2591
+ this.entryInProgress = true;
2592
+ try {
2593
+ return await this.executeGetVersion(name, latest);
2594
+ } finally {
2595
+ this.entryInProgress = false;
2596
+ }
2597
+ }
2598
+
2599
+ private async executeGetVersion(
2600
+ name: string,
2601
+ latest: number,
2602
+ ): Promise<number> {
2603
+ if (!Number.isInteger(latest) || latest < 1) {
2604
+ throw new Error(
2605
+ `getVersion("${name}", ${latest}): latest must be an integer >= 1`,
2606
+ );
2607
+ }
2608
+
2609
+ // Check for duplicate name in current execution
2610
+ this.checkDuplicateName(name);
2611
+
2612
+ const location = appendName(this.storage, this.currentLocation, name);
2613
+ const key = locationToKey(this.storage, location);
2614
+ const existing = this.storage.history.entries.get(key);
2615
+
2616
+ // Mark this entry as visited for validateComplete
2617
+ this.markVisited(key);
2618
+
2619
+ this.stopRollbackIfMissing(existing);
2620
+
2621
+ if (existing) {
2622
+ if (existing.kind.type !== "version_check") {
2623
+ throw new HistoryDivergedError(
2624
+ `Expected version_check at ${key}, found ${existing.kind.type}`,
2625
+ );
2626
+ }
2627
+ // Pure replay: this instance is already pinned at this location.
2628
+ return existing.kind.data.resolved;
2629
+ }
2630
+
2631
+ // No recorded version at this location. Decide whether this instance
2632
+ // already executed past this point under older code (old in-flight) or
2633
+ // is reaching it fresh at the live frontier.
2634
+ //
2635
+ // The discriminator is "is there any unvisited history entry under the
2636
+ // current scope?". Entries created earlier in this same run are already
2637
+ // marked visited, so the only unvisited entries under the scope are
2638
+ // leftovers from a prior run, which proves this scope already executed
2639
+ // under code that predates this gate. Such instances resolve to the
2640
+ // implicit floor version 1 (old branch); fresh instances resolve to
2641
+ // `latest`.
2642
+ const resolved = this.hasUnvisitedUnderCurrentScope(key) ? 1 : latest;
2643
+
2644
+ const entry = createEntry(location, {
2645
+ type: "version_check",
2646
+ data: { resolved, latest },
2647
+ });
2648
+ setEntry(this.storage, location, entry);
2649
+ await this.flushStorage();
2650
+
2651
+ return resolved;
2652
+ }
2653
+
2654
+ /**
2655
+ * Returns true if any history entry under the current location scope
2656
+ * (other than `excludeKey`) has not yet been visited this run. Used by
2657
+ * getVersion to detect an old in-flight instance whose scope was already
2658
+ * executed by code that predates a version gate.
2659
+ */
2660
+ private hasUnvisitedUnderCurrentScope(excludeKey: string): boolean {
2661
+ const prefix = locationToKey(this.storage, this.currentLocation);
2662
+
2663
+ for (const key of this.storage.history.entries.keys()) {
2664
+ if (key === excludeKey) {
2665
+ continue;
2666
+ }
2667
+ const isUnderPrefix =
2668
+ prefix === ""
2669
+ ? true
2670
+ : key.startsWith(`${prefix}/`) || key === prefix;
2671
+ if (isUnderPrefix && !this.visitedKeys.has(key)) {
2672
+ return true;
2673
+ }
2674
+ }
2675
+ return false;
2676
+ }
2585
2677
  }
package/src/driver.ts CHANGED
@@ -30,6 +30,12 @@ export interface KVWrite {
30
30
  * See architecture.md "Isolation Model" for details.
31
31
  */
32
32
  export interface EngineDriver {
33
+ /**
34
+ * Requires each logical storage flush to reach `batch` as one indivisible
35
+ * unit. The driver must reject an oversized unit instead of splitting it.
36
+ */
37
+ readonly atomicBatch?: boolean;
38
+
33
39
  // === KV Operations ===
34
40
 
35
41
  /**
@@ -48,6 +54,12 @@ export interface EngineDriver {
48
54
  */
49
55
  delete(key: Uint8Array): Promise<void>;
50
56
 
57
+ /**
58
+ * Batch delete multiple keys in a single operation.
59
+ * Should be atomic if possible.
60
+ */
61
+ batchDelete(keys: Uint8Array[]): Promise<void>;
62
+
51
63
  /**
52
64
  * Delete all keys with a given prefix.
53
65
  */
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export {
13
13
  } from "./context.js";
14
14
  // Driver
15
15
  export type { EngineDriver, KVEntry, KVWrite } from "./driver.js";
16
+ export { extractErrorInfo } from "./error-utils.js";
16
17
  // Errors
17
18
  export {
18
19
  CancelledError,
@@ -29,7 +30,6 @@ export {
29
30
  StepExhaustedError,
30
31
  StepFailedError,
31
32
  } from "./errors.js";
32
- export { extractErrorInfo } from "./error-utils.js";
33
33
 
34
34
  // Location utilities
35
35
  export {
@@ -82,9 +82,6 @@ export type {
82
82
  PathSegment,
83
83
  RaceEntry,
84
84
  RemovedEntry,
85
- WorkflowEntryMetadataSnapshot,
86
- WorkflowHistoryEntry,
87
- WorkflowHistorySnapshot,
88
85
  RollbackCheckpointEntry,
89
86
  RollbackContextInterface,
90
87
  RunWorkflowOptions,
@@ -102,20 +99,23 @@ export type {
102
99
  TryStepFailure,
103
100
  TryStepResult,
104
101
  WorkflowContextInterface,
102
+ WorkflowEntryMetadataSnapshot,
105
103
  WorkflowError,
106
104
  WorkflowErrorEvent,
107
105
  WorkflowErrorHandler,
108
106
  WorkflowFunction,
109
107
  WorkflowHandle,
110
- WorkflowRollbackErrorEvent,
108
+ WorkflowHistoryEntry,
109
+ WorkflowHistorySnapshot,
110
+ WorkflowMessageDriver,
111
111
  WorkflowQueue,
112
112
  WorkflowQueueMessage,
113
113
  WorkflowQueueNextBatchOptions,
114
114
  WorkflowQueueNextOptions,
115
- WorkflowMessageDriver,
116
115
  WorkflowResult,
117
- WorkflowRunMode,
116
+ WorkflowRollbackErrorEvent,
118
117
  WorkflowRunErrorEvent,
118
+ WorkflowRunMode,
119
119
  WorkflowState,
120
120
  WorkflowStepErrorEvent,
121
121
  } from "./types.js";
@@ -185,9 +185,9 @@ import type {
185
185
  RunWorkflowOptions,
186
186
  Storage,
187
187
  WorkflowErrorEvent,
188
- WorkflowHistorySnapshot,
189
188
  WorkflowFunction,
190
189
  WorkflowHandle,
190
+ WorkflowHistorySnapshot,
191
191
  WorkflowMessageDriver,
192
192
  WorkflowResult,
193
193
  WorkflowRunMode,
@@ -532,26 +532,46 @@ async function executeLiveWorkflow<TInput, TOutput>(
532
532
  const hasDeadline = result.sleepUntil !== undefined;
533
533
 
534
534
  if (hasMessages && hasDeadline) {
535
- // Wait for EITHER a message OR the deadline (for queue.next timeout)
535
+ // Wait for EITHER a message OR the deadline (for queue.next timeout).
536
+ // Scope both waiters to a per-iteration controller chained to the run
537
+ // signal so that whichever loses the race is cancelled immediately.
538
+ // Otherwise the loser (a message waiter or an armed sleep timer) stays
539
+ // registered on the long-lived run signal and leaks once per cycle.
540
+ const iterationAbort = new AbortController();
541
+ const onRunAbort = () => iterationAbort.abort();
542
+ if (abortController.signal.aborted) {
543
+ iterationAbort.abort();
544
+ } else {
545
+ abortController.signal.addEventListener("abort", onRunAbort, {
546
+ once: true,
547
+ });
548
+ }
549
+ const messagePromise = awaitWithEviction(
550
+ driver.waitForMessages(
551
+ result.waitingForMessages!,
552
+ iterationAbort.signal,
553
+ ),
554
+ iterationAbort.signal,
555
+ );
556
+ const sleepPromise = waitForSleep(
557
+ runtime,
558
+ result.sleepUntil!,
559
+ iterationAbort.signal,
560
+ );
561
+ // Swallow the loser's rejection once it is aborted below so it does
562
+ // not surface as an unhandled rejection.
563
+ messagePromise.catch(() => {});
564
+ sleepPromise.catch(() => {});
536
565
  try {
537
- const messagePromise = awaitWithEviction(
538
- driver.waitForMessages(
539
- result.waitingForMessages!,
540
- abortController.signal,
541
- ),
542
- abortController.signal,
543
- );
544
- const sleepPromise = waitForSleep(
545
- runtime,
546
- result.sleepUntil!,
547
- abortController.signal,
548
- );
549
566
  await Promise.race([messagePromise, sleepPromise]);
550
567
  } catch (error) {
551
568
  if (error instanceof EvictedError) {
552
569
  return lastResult;
553
570
  }
554
571
  throw error;
572
+ } finally {
573
+ iterationAbort.abort();
574
+ abortController.signal.removeEventListener("abort", onRunAbort);
555
575
  }
556
576
  continue;
557
577
  }
package/src/location.ts CHANGED
@@ -159,7 +159,7 @@ export function getChildEntries(
159
159
  const isChild =
160
160
  parentKey === ""
161
161
  ? true
162
- : key.startsWith(parentKey + "/") || key === parentKey;
162
+ : key.startsWith(`${parentKey}/`) || key === parentKey;
163
163
 
164
164
  if (isChild) {
165
165
  // Return the actual entry's location, not the parent location