@rivetkit/workflow-engine 0.0.0-main.72cbc7c → 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.
- package/dist/schemas/v1.ts +25 -0
- package/dist/tsup/{chunk-SFJQFMCW.cjs → chunk-32422MOM.cjs} +221 -93
- package/dist/tsup/chunk-32422MOM.cjs.map +1 -0
- package/dist/tsup/{chunk-CIDHCIH7.js → chunk-TVRZA5O5.js} +221 -93
- package/dist/tsup/chunk-TVRZA5O5.js.map +1 -0
- package/dist/tsup/index.cjs +2 -2
- package/dist/tsup/index.cjs.map +1 -1
- package/dist/tsup/index.d.cts +50 -13
- package/dist/tsup/index.d.ts +50 -13
- package/dist/tsup/index.js +1 -1
- package/dist/tsup/testing.cjs +29 -23
- package/dist/tsup/testing.cjs.map +1 -1
- package/dist/tsup/testing.d.cts +1 -0
- package/dist/tsup/testing.d.ts +1 -0
- package/dist/tsup/testing.js +7 -1
- package/dist/tsup/testing.js.map +1 -1
- package/package.json +6 -1
- package/schemas/serde.ts +17 -3
- package/schemas/v1.bare +10 -1
- package/schemas/versioned.ts +1 -0
- package/src/context.ts +140 -49
- package/src/driver.ts +12 -0
- package/src/index.ts +41 -21
- package/src/location.ts +1 -1
- package/src/storage.ts +50 -16
- package/src/testing.ts +7 -0
- package/src/types.ts +20 -3
- package/dist/tsup/chunk-CIDHCIH7.js.map +0 -1
- package/dist/tsup/chunk-SFJQFMCW.cjs.map +0 -1
package/dist/tsup/testing.js.map
CHANGED
|
@@ -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":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rivetkit/workflow-engine",
|
|
3
|
-
"version": "0.0.0-main.
|
|
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
|
|
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
|
package/schemas/versioned.ts
CHANGED
package/src/context.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
StepExhaustedError,
|
|
22
22
|
StepFailedError,
|
|
23
23
|
} from "./errors.js";
|
|
24
|
-
import {
|
|
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
|
|
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
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
618
|
-
"abort",
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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,15 +943,16 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
|
|
|
941
943
|
});
|
|
942
944
|
return output;
|
|
943
945
|
} catch (error) {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
});
|
|
@@ -967,13 +970,8 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
|
|
|
967
970
|
error instanceof CriticalError ||
|
|
968
971
|
error instanceof RollbackError
|
|
969
972
|
) {
|
|
970
|
-
if (entry.kind.type === "step") {
|
|
971
|
-
entry.kind.data.error = String(error);
|
|
972
|
-
}
|
|
973
|
-
entry.dirty = true;
|
|
974
973
|
metadata.status = "exhausted";
|
|
975
974
|
metadata.error = String(error);
|
|
976
|
-
await this.flushStorage();
|
|
977
975
|
await this.notifyStepError(config, metadata.attempts, error, {
|
|
978
976
|
willRetry: false,
|
|
979
977
|
});
|
|
@@ -990,15 +988,10 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
|
|
|
990
988
|
);
|
|
991
989
|
}
|
|
992
990
|
|
|
993
|
-
if (entry.kind.type === "step") {
|
|
994
|
-
entry.kind.data.error = String(error);
|
|
995
|
-
}
|
|
996
|
-
entry.dirty = true;
|
|
997
991
|
const willRetry = metadata.attempts <= maxRetries;
|
|
998
992
|
metadata.status = willRetry ? "failed" : "exhausted";
|
|
999
993
|
metadata.error = String(error);
|
|
1000
994
|
|
|
1001
|
-
await this.flushStorage();
|
|
1002
995
|
if (willRetry) {
|
|
1003
996
|
const retryDelay = calculateBackoff(
|
|
1004
997
|
metadata.attempts,
|
|
@@ -1023,7 +1016,10 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
|
|
|
1023
1016
|
attachTryStepFailure(
|
|
1024
1017
|
new StepExhaustedError(config.name, String(error)),
|
|
1025
1018
|
{
|
|
1026
|
-
kind:
|
|
1019
|
+
kind:
|
|
1020
|
+
error instanceof StepTimeoutError
|
|
1021
|
+
? "timeout"
|
|
1022
|
+
: "exhausted",
|
|
1027
1023
|
stepName: config.name,
|
|
1028
1024
|
attempts: metadata.attempts,
|
|
1029
1025
|
error: extractErrorInfo(error),
|
|
@@ -1042,7 +1038,9 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
|
|
|
1042
1038
|
*
|
|
1043
1039
|
* Note: This does NOT cancel the underlying operation. JavaScript Promises
|
|
1044
1040
|
* cannot be cancelled once started. When a timeout occurs:
|
|
1045
|
-
* - The step is
|
|
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.
|
|
1046
1044
|
* - The underlying async operation continues running in the background
|
|
1047
1045
|
* - Any side effects from the operation may still occur
|
|
1048
1046
|
*
|
|
@@ -1493,7 +1491,7 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
|
|
|
1493
1491
|
|
|
1494
1492
|
// Short sleep: wait in memory
|
|
1495
1493
|
if (remaining < this.driver.workerPollInterval) {
|
|
1496
|
-
await
|
|
1494
|
+
await this.sleepOrEvict(remaining);
|
|
1497
1495
|
|
|
1498
1496
|
this.checkEvicted();
|
|
1499
1497
|
|
|
@@ -2583,4 +2581,97 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
|
|
|
2583
2581
|
setEntry(this.storage, location, entry);
|
|
2584
2582
|
await this.flushStorage();
|
|
2585
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
|
+
}
|
|
2586
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
|
-
|
|
108
|
+
WorkflowHistoryEntry,
|
|
109
|
+
WorkflowHistorySnapshot,
|
|
110
|
+
WorkflowMessageDriver,
|
|
111
111
|
WorkflowQueue,
|
|
112
112
|
WorkflowQueueMessage,
|
|
113
113
|
WorkflowQueueNextBatchOptions,
|
|
114
114
|
WorkflowQueueNextOptions,
|
|
115
|
-
WorkflowMessageDriver,
|
|
116
115
|
WorkflowResult,
|
|
117
|
-
|
|
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
|
|
162
|
+
: key.startsWith(`${parentKey}/`) || key === parentKey;
|
|
163
163
|
|
|
164
164
|
if (isChild) {
|
|
165
165
|
// Return the actual entry's location, not the parent location
|