@voyant-travel/workflows-orchestrator 0.107.10
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/LICENSE +201 -0
- package/NOTICE +52 -0
- package/README.md +76 -0
- package/dist/abort-registry.d.ts +6 -0
- package/dist/abort-registry.d.ts.map +1 -0
- package/dist/abort-registry.js +37 -0
- package/dist/concurrency.d.ts +31 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +145 -0
- package/dist/drive.d.ts +67 -0
- package/dist/drive.d.ts.map +1 -0
- package/dist/drive.js +373 -0
- package/dist/driver-inmemory.d.ts +30 -0
- package/dist/driver-inmemory.d.ts.map +1 -0
- package/dist/driver-inmemory.js +394 -0
- package/dist/event-router.d.ts +51 -0
- package/dist/event-router.d.ts.map +1 -0
- package/dist/event-router.js +68 -0
- package/dist/http-step-handler.d.ts +25 -0
- package/dist/http-step-handler.d.ts.map +1 -0
- package/dist/http-step-handler.js +78 -0
- package/dist/in-memory-store.d.ts +5 -0
- package/dist/in-memory-store.d.ts.map +1 -0
- package/dist/in-memory-store.js +41 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/journal-helpers.d.ts +3 -0
- package/dist/journal-helpers.d.ts.map +1 -0
- package/dist/journal-helpers.js +9 -0
- package/dist/orchestrator.d.ts +116 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +411 -0
- package/dist/resume-run.d.ts +40 -0
- package/dist/resume-run.d.ts.map +1 -0
- package/dist/resume-run.js +119 -0
- package/dist/schedule.d.ts +51 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +243 -0
- package/dist/testing/driver-compliance.d.ts +58 -0
- package/dist/testing/driver-compliance.d.ts.map +1 -0
- package/dist/testing/driver-compliance.js +667 -0
- package/dist/types.d.ts +182 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/package.json +51 -0
- package/src/__tests__/orchestrator-test-support.ts +18 -0
- package/src/abort-registry.ts +41 -0
- package/src/concurrency.ts +217 -0
- package/src/drive.ts +477 -0
- package/src/driver-inmemory.ts +511 -0
- package/src/event-router.ts +120 -0
- package/src/http-step-handler.ts +112 -0
- package/src/in-memory-store.ts +44 -0
- package/src/index.ts +73 -0
- package/src/journal-helpers.ts +11 -0
- package/src/orchestrator.ts +527 -0
- package/src/resume-run.ts +162 -0
- package/src/schedule.ts +310 -0
- package/src/testing/driver-compliance.ts +800 -0
- package/src/types.ts +201 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION } from "@voyant-travel/workflows/protocol"
|
|
2
|
+
import type { StepHandler, WorkflowStepRequest, WorkflowStepResponse } from "./types.js"
|
|
3
|
+
|
|
4
|
+
interface StepHandlerError {
|
|
5
|
+
error: string
|
|
6
|
+
message: string
|
|
7
|
+
details?: unknown
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HttpStepTarget {
|
|
11
|
+
url: string
|
|
12
|
+
fetch(request: Request): Promise<Response>
|
|
13
|
+
label?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface HttpStepHandlerDeps {
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the transport target for this workflow-step invocation.
|
|
19
|
+
* Adapters can map `tenantMeta.tenantScript` to a dispatch-namespace
|
|
20
|
+
* binding, a service URL, or a local proxy.
|
|
21
|
+
*/
|
|
22
|
+
resolveTarget: (req: WorkflowStepRequest) => HttpStepTarget | Promise<HttpStepTarget>
|
|
23
|
+
/** Optional HMAC signer for the X-Voyant-Dispatch-Auth header. */
|
|
24
|
+
sign?: (body: string, req: WorkflowStepRequest) => Promise<string> | string
|
|
25
|
+
/** Optional logger for step-level observability. */
|
|
26
|
+
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build a StepHandler that serializes WorkflowStepRequest over HTTP.
|
|
31
|
+
* The concrete transport target is adapter-specific; the request/response
|
|
32
|
+
* mapping is shared across Cloudflare, Node, and future adapters.
|
|
33
|
+
*/
|
|
34
|
+
export function createHttpStepHandler(deps: HttpStepHandlerDeps): StepHandler {
|
|
35
|
+
return async (req: WorkflowStepRequest, stepOpts) => {
|
|
36
|
+
const body = JSON.stringify(req)
|
|
37
|
+
const headers: Record<string, string> = {
|
|
38
|
+
"content-type": "application/json; charset=utf-8",
|
|
39
|
+
"x-voyant-protocol": String(PROTOCOL_VERSION),
|
|
40
|
+
}
|
|
41
|
+
if (deps.sign) {
|
|
42
|
+
headers["x-voyant-dispatch-auth"] = await deps.sign(body, req)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const target = await deps.resolveTarget(req)
|
|
46
|
+
deps.logger?.("info", "http-step: invoking tenant step", {
|
|
47
|
+
target: target.label ?? target.url,
|
|
48
|
+
runId: req.runId,
|
|
49
|
+
workflowId: req.workflowId,
|
|
50
|
+
invocation: req.invocationCount,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
let response: Response
|
|
54
|
+
try {
|
|
55
|
+
response = await target.fetch(
|
|
56
|
+
new Request(target.url, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers,
|
|
59
|
+
body,
|
|
60
|
+
signal: stepOpts?.signal,
|
|
61
|
+
}),
|
|
62
|
+
)
|
|
63
|
+
} catch (err) {
|
|
64
|
+
deps.logger?.("error", "http-step: tenant fetch threw", {
|
|
65
|
+
target: target.label ?? target.url,
|
|
66
|
+
runId: req.runId,
|
|
67
|
+
error: err instanceof Error ? err.message : String(err),
|
|
68
|
+
})
|
|
69
|
+
return {
|
|
70
|
+
status: 502,
|
|
71
|
+
body: {
|
|
72
|
+
error: "tenant_unreachable",
|
|
73
|
+
message: err instanceof Error ? err.message : String(err),
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const text = await response.text()
|
|
79
|
+
let parsed: unknown
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(text)
|
|
82
|
+
} catch {
|
|
83
|
+
return {
|
|
84
|
+
status: 502,
|
|
85
|
+
body: {
|
|
86
|
+
error: "tenant_invalid_response",
|
|
87
|
+
message: `tenant returned non-JSON body (HTTP ${response.status})`,
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (response.status !== 200) {
|
|
93
|
+
return { status: response.status, body: toErrorBody(parsed, response.status) }
|
|
94
|
+
}
|
|
95
|
+
return { status: 200, body: parsed as WorkflowStepResponse }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toErrorBody(parsed: unknown, fallbackStatus: number): StepHandlerError {
|
|
100
|
+
if (
|
|
101
|
+
parsed !== null &&
|
|
102
|
+
typeof parsed === "object" &&
|
|
103
|
+
typeof (parsed as { error?: unknown }).error === "string" &&
|
|
104
|
+
typeof (parsed as { message?: unknown }).message === "string"
|
|
105
|
+
) {
|
|
106
|
+
return parsed as StepHandlerError
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
error: "tenant_error",
|
|
110
|
+
message: `tenant returned HTTP ${fallbackStatus}`,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// A pure in-memory RunRecordStore. Useful for tests and local-only
|
|
2
|
+
// orchestrator harnesses. The production store is Postgres-backed
|
|
3
|
+
// and lives in voyant-cloud.
|
|
4
|
+
|
|
5
|
+
import type { OrchestratorRunStatus, RunRecord, RunRecordStore } from "./types.js"
|
|
6
|
+
|
|
7
|
+
export function createInMemoryRunStore(): RunRecordStore {
|
|
8
|
+
const records = new Map<string, RunRecord>()
|
|
9
|
+
return {
|
|
10
|
+
async get(id) {
|
|
11
|
+
const r = records.get(id)
|
|
12
|
+
return r ? clone(r) : undefined
|
|
13
|
+
},
|
|
14
|
+
async save(record) {
|
|
15
|
+
records.set(record.id, clone(record))
|
|
16
|
+
return clone(record)
|
|
17
|
+
},
|
|
18
|
+
async tryInsert(record) {
|
|
19
|
+
// Atomic-by-construction: Map.has + Map.set inside a single
|
|
20
|
+
// microtask. Concurrent `tryInsert(idA)` calls all schedule on the
|
|
21
|
+
// same JS event loop and only the first one observes the slot
|
|
22
|
+
// empty — subsequent callers see the inserted record.
|
|
23
|
+
const existing = records.get(record.id)
|
|
24
|
+
if (existing) return { record: clone(existing), created: false }
|
|
25
|
+
records.set(record.id, clone(record))
|
|
26
|
+
return { record: clone(record), created: true }
|
|
27
|
+
},
|
|
28
|
+
async list(filter = {}) {
|
|
29
|
+
let out = [...records.values()].map(clone)
|
|
30
|
+
if (filter.workflowId) out = out.filter((r) => r.workflowId === filter.workflowId)
|
|
31
|
+
if (filter.status) out = out.filter((r) => r.status === filter.status)
|
|
32
|
+
out.sort((a, b) => b.startedAt - a.startedAt)
|
|
33
|
+
if (filter.limit !== undefined) out = out.slice(0, filter.limit)
|
|
34
|
+
return out
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clone<T>(value: T): T {
|
|
40
|
+
return structuredClone(value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Keep the unused import happy — guards against accidental type-only drift. */
|
|
44
|
+
export type { OrchestratorRunStatus }
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @voyant-travel/workflows-orchestrator
|
|
2
|
+
//
|
|
3
|
+
// Reference orchestrator for Voyant Workflows. Drives runs through
|
|
4
|
+
// the tenant step handler over the v1 wire protocol. Transport- and
|
|
5
|
+
// storage-agnostic: compose with a RunRecordStore of your choice
|
|
6
|
+
// (in-memory for tests, Postgres-backed for production).
|
|
7
|
+
//
|
|
8
|
+
// See docs/runtime-protocol.md §2 + §5 for the contract this
|
|
9
|
+
// implements and docs/design.md §6 for the broader orchestrator
|
|
10
|
+
// state-machine design.
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
registerRunAbort,
|
|
14
|
+
signalRunAbort,
|
|
15
|
+
unregisterRunAbort,
|
|
16
|
+
} from "./abort-registry.js"
|
|
17
|
+
export {
|
|
18
|
+
type ConcurrencyRunHooks,
|
|
19
|
+
createInProcessConcurrencyCoordinator,
|
|
20
|
+
type InProcessConcurrencyCoordinator,
|
|
21
|
+
type RuntimeConcurrencyPolicy,
|
|
22
|
+
resolveConcurrencyKey,
|
|
23
|
+
WorkflowConcurrencyRejectedError,
|
|
24
|
+
} from "./concurrency.js"
|
|
25
|
+
export { applyWaitpointInjection, type DriveOptions, driveUntilPaused } from "./drive.js"
|
|
26
|
+
export {
|
|
27
|
+
createInMemoryDriver,
|
|
28
|
+
type InMemoryDriverOptions,
|
|
29
|
+
} from "./driver-inmemory.js"
|
|
30
|
+
export {
|
|
31
|
+
type RouteEventArgs,
|
|
32
|
+
type RouterMatch,
|
|
33
|
+
routeEvent,
|
|
34
|
+
} from "./event-router.js"
|
|
35
|
+
export {
|
|
36
|
+
createHttpStepHandler,
|
|
37
|
+
type HttpStepHandlerDeps,
|
|
38
|
+
type HttpStepTarget,
|
|
39
|
+
} from "./http-step-handler.js"
|
|
40
|
+
export { createInMemoryRunStore } from "./in-memory-store.js"
|
|
41
|
+
export { emptyJournal } from "./journal-helpers.js"
|
|
42
|
+
export {
|
|
43
|
+
type CancelArgs,
|
|
44
|
+
cancel,
|
|
45
|
+
type OrchestratorDeps,
|
|
46
|
+
type ResumeArgs,
|
|
47
|
+
type ResumeDueAlarmsArgs,
|
|
48
|
+
resume,
|
|
49
|
+
resumeDueAlarms,
|
|
50
|
+
type TriggerArgs,
|
|
51
|
+
trigger,
|
|
52
|
+
} from "./orchestrator.js"
|
|
53
|
+
export {
|
|
54
|
+
type BuildResumeJournalInput,
|
|
55
|
+
type BuildResumeJournalResult,
|
|
56
|
+
type BuildSeededResumeJournalInput,
|
|
57
|
+
buildResumeJournal,
|
|
58
|
+
buildSeededResumeJournal,
|
|
59
|
+
} from "./resume-run.js"
|
|
60
|
+
export {
|
|
61
|
+
type CronSpec,
|
|
62
|
+
computeNextFire,
|
|
63
|
+
createScheduler,
|
|
64
|
+
manifestScheduleSources,
|
|
65
|
+
nextCronFire,
|
|
66
|
+
parseCron,
|
|
67
|
+
type SchedulableDeclaration,
|
|
68
|
+
type SchedulerDeps,
|
|
69
|
+
type SchedulerHandle,
|
|
70
|
+
type ScheduleSource,
|
|
71
|
+
toMs,
|
|
72
|
+
} from "./schedule.js"
|
|
73
|
+
export * from "./types.js"
|