@corgelabs/corge 0.0.1 → 1.0.0
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/ai/index.d.ts +3 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +3 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/schema.d.ts +83 -0
- package/dist/ai/schema.d.ts.map +1 -0
- package/dist/ai/schema.js +19 -0
- package/dist/ai/schema.js.map +1 -0
- package/dist/ai/service.d.ts +14 -0
- package/dist/ai/service.d.ts.map +1 -0
- package/dist/ai/service.js +266 -0
- package/dist/ai/service.js.map +1 -0
- package/dist/ai/utils.d.ts +10 -0
- package/dist/ai/utils.d.ts.map +1 -0
- package/dist/ai/utils.js +27 -0
- package/dist/ai/utils.js.map +1 -0
- package/dist/attachments/index.d.ts +4 -0
- package/dist/attachments/index.d.ts.map +1 -0
- package/dist/attachments/index.js +4 -0
- package/dist/attachments/index.js.map +1 -0
- package/dist/attachments/routes.d.ts +75 -0
- package/dist/attachments/routes.d.ts.map +1 -0
- package/dist/attachments/routes.js +18 -0
- package/dist/attachments/routes.js.map +1 -0
- package/dist/attachments/schema.d.ts +61 -0
- package/dist/attachments/schema.d.ts.map +1 -0
- package/dist/attachments/schema.js +30 -0
- package/dist/attachments/schema.js.map +1 -0
- package/dist/attachments/service.d.ts +27 -0
- package/dist/attachments/service.d.ts.map +1 -0
- package/dist/attachments/service.js +224 -0
- package/dist/attachments/service.js.map +1 -0
- package/dist/attachments/utils.d.ts +9 -0
- package/dist/attachments/utils.d.ts.map +1 -0
- package/dist/attachments/utils.js +80 -0
- package/dist/attachments/utils.js.map +1 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +4 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oidc.d.ts +25 -0
- package/dist/auth/oidc.d.ts.map +1 -0
- package/dist/auth/oidc.js +298 -0
- package/dist/auth/oidc.js.map +1 -0
- package/dist/auth/recovery.d.ts +41 -0
- package/dist/auth/recovery.d.ts.map +1 -0
- package/dist/auth/recovery.js +250 -0
- package/dist/auth/recovery.js.map +1 -0
- package/dist/auth/routes.d.ts +226 -0
- package/dist/auth/routes.d.ts.map +1 -0
- package/dist/auth/routes.js +96 -0
- package/dist/auth/routes.js.map +1 -0
- package/dist/auth/schema.d.ts +158 -0
- package/dist/auth/schema.d.ts.map +1 -0
- package/dist/auth/schema.js +82 -0
- package/dist/auth/schema.js.map +1 -0
- package/dist/auth/service.d.ts +96 -0
- package/dist/auth/service.d.ts.map +1 -0
- package/dist/auth/service.js +129 -0
- package/dist/auth/service.js.map +1 -0
- package/dist/auth/session.d.ts +51 -0
- package/dist/auth/session.d.ts.map +1 -0
- package/dist/auth/session.js +344 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/auth/utils.d.ts +17 -0
- package/dist/auth/utils.d.ts.map +1 -0
- package/dist/auth/utils.js +96 -0
- package/dist/auth/utils.js.map +1 -0
- package/dist/bin/corge.js +547 -0
- package/dist/chat/context.d.ts +10 -0
- package/dist/chat/context.d.ts.map +1 -0
- package/dist/chat/context.js +221 -0
- package/dist/chat/context.js.map +1 -0
- package/dist/chat/index.d.ts +4 -0
- package/dist/chat/index.d.ts.map +1 -0
- package/dist/chat/index.js +4 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/routes.d.ts +160 -0
- package/dist/chat/routes.d.ts.map +1 -0
- package/dist/chat/routes.js +210 -0
- package/dist/chat/routes.js.map +1 -0
- package/dist/chat/schema.d.ts +285 -0
- package/dist/chat/schema.d.ts.map +1 -0
- package/dist/chat/schema.js +74 -0
- package/dist/chat/schema.js.map +1 -0
- package/dist/chat/service.d.ts +64 -0
- package/dist/chat/service.d.ts.map +1 -0
- package/dist/chat/service.js +279 -0
- package/dist/chat/service.js.map +1 -0
- package/dist/chat/stream.d.ts +53 -0
- package/dist/chat/stream.d.ts.map +1 -0
- package/dist/chat/stream.js +339 -0
- package/dist/chat/stream.js.map +1 -0
- package/dist/chat/utils.d.ts +61 -0
- package/dist/chat/utils.d.ts.map +1 -0
- package/dist/chat/utils.js +285 -0
- package/dist/chat/utils.js.map +1 -0
- package/dist/cli/builder.d.ts +6 -0
- package/dist/cli/builder.d.ts.map +1 -0
- package/dist/cli/builder.js +91 -0
- package/dist/cli/builder.js.map +1 -0
- package/dist/cli/deployer.d.ts +2 -0
- package/dist/cli/deployer.d.ts.map +1 -0
- package/dist/cli/deployer.js +152 -0
- package/dist/cli/deployer.js.map +1 -0
- package/dist/cli/dev-server.d.ts +2 -0
- package/dist/cli/dev-server.d.ts.map +1 -0
- package/dist/cli/dev-server.js +70 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/env.d.ts +4 -0
- package/dist/cli/env.d.ts.map +1 -0
- package/dist/cli/env.js +68 -0
- package/dist/cli/env.js.map +1 -0
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +103 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/config.d.ts +262 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +212 -0
- package/dist/config.js.map +1 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +410 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +294 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/service.d.ts +42 -0
- package/dist/db/service.d.ts.map +1 -0
- package/dist/db/service.js +372 -0
- package/dist/db/service.js.map +1 -0
- package/dist/db/utils.d.ts +25 -0
- package/dist/db/utils.d.ts.map +1 -0
- package/dist/db/utils.js +188 -0
- package/dist/db/utils.js.map +1 -0
- package/dist/emails/index.d.ts +4 -0
- package/dist/emails/index.d.ts.map +1 -0
- package/dist/emails/index.js +4 -0
- package/dist/emails/index.js.map +1 -0
- package/dist/emails/routes.d.ts +191 -0
- package/dist/emails/routes.d.ts.map +1 -0
- package/dist/emails/routes.js +39 -0
- package/dist/emails/routes.js.map +1 -0
- package/dist/emails/schema.d.ts +237 -0
- package/dist/emails/schema.d.ts.map +1 -0
- package/dist/emails/schema.js +98 -0
- package/dist/emails/schema.js.map +1 -0
- package/dist/emails/service.d.ts +22 -0
- package/dist/emails/service.d.ts.map +1 -0
- package/dist/emails/service.js +337 -0
- package/dist/emails/service.js.map +1 -0
- package/dist/emails/utils.d.ts +49 -0
- package/dist/emails/utils.d.ts.map +1 -0
- package/dist/emails/utils.js +341 -0
- package/dist/emails/utils.js.map +1 -0
- package/dist/files/index.d.ts +4 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +4 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/routes.d.ts +72 -0
- package/dist/files/routes.d.ts.map +1 -0
- package/dist/files/routes.js +38 -0
- package/dist/files/routes.js.map +1 -0
- package/dist/files/schema.d.ts +57 -0
- package/dist/files/schema.d.ts.map +1 -0
- package/dist/files/schema.js +39 -0
- package/dist/files/schema.js.map +1 -0
- package/dist/files/service.d.ts +27 -0
- package/dist/files/service.d.ts.map +1 -0
- package/dist/files/service.js +175 -0
- package/dist/files/service.js.map +1 -0
- package/dist/files/utils.d.ts +3 -0
- package/dist/files/utils.d.ts.map +1 -0
- package/dist/files/utils.js +63 -0
- package/dist/files/utils.js.map +1 -0
- package/dist/index.d.ts +875 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9919 -0
- package/dist/index.js.map +7 -0
- package/dist/payments/billing.d.ts +29 -0
- package/dist/payments/billing.d.ts.map +1 -0
- package/dist/payments/billing.js +378 -0
- package/dist/payments/billing.js.map +1 -0
- package/dist/payments/index.d.ts +4 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +4 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/payments/routes.d.ts +340 -0
- package/dist/payments/routes.d.ts.map +1 -0
- package/dist/payments/routes.js +56 -0
- package/dist/payments/routes.js.map +1 -0
- package/dist/payments/schema.d.ts +412 -0
- package/dist/payments/schema.d.ts.map +1 -0
- package/dist/payments/schema.js +260 -0
- package/dist/payments/schema.js.map +1 -0
- package/dist/payments/service.d.ts +31 -0
- package/dist/payments/service.d.ts.map +1 -0
- package/dist/payments/service.js +322 -0
- package/dist/payments/service.js.map +1 -0
- package/dist/payments/utils.d.ts +61 -0
- package/dist/payments/utils.d.ts.map +1 -0
- package/dist/payments/utils.js +369 -0
- package/dist/payments/utils.js.map +1 -0
- package/dist/router/guard.d.ts +19 -0
- package/dist/router/guard.d.ts.map +1 -0
- package/dist/router/guard.js +243 -0
- package/dist/router/guard.js.map +1 -0
- package/dist/router/index.d.ts +3 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +3 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/schema.d.ts +103 -0
- package/dist/router/schema.d.ts.map +1 -0
- package/dist/router/schema.js +282 -0
- package/dist/router/schema.js.map +1 -0
- package/dist/router/service.d.ts +25 -0
- package/dist/router/service.d.ts.map +1 -0
- package/dist/router/service.js +339 -0
- package/dist/router/service.js.map +1 -0
- package/dist/tasks/index.d.ts +4 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/tasks/routes.d.ts +54 -0
- package/dist/tasks/routes.d.ts.map +1 -0
- package/dist/tasks/routes.js +195 -0
- package/dist/tasks/routes.js.map +1 -0
- package/dist/tasks/schema.d.ts +261 -0
- package/dist/tasks/schema.d.ts.map +1 -0
- package/dist/tasks/schema.js +35 -0
- package/dist/tasks/schema.js.map +1 -0
- package/dist/tasks/service.d.ts +32 -0
- package/dist/tasks/service.d.ts.map +1 -0
- package/dist/tasks/service.js +201 -0
- package/dist/tasks/service.js.map +1 -0
- package/dist/tasks/steps.d.ts +18 -0
- package/dist/tasks/steps.d.ts.map +1 -0
- package/dist/tasks/steps.js +365 -0
- package/dist/tasks/steps.js.map +1 -0
- package/dist/tasks/utils.d.ts +75 -0
- package/dist/tasks/utils.d.ts.map +1 -0
- package/dist/tasks/utils.js +311 -0
- package/dist/tasks/utils.js.map +1 -0
- package/dist/tasks/watcher.d.ts +13 -0
- package/dist/tasks/watcher.d.ts.map +1 -0
- package/dist/tasks/watcher.js +255 -0
- package/dist/tasks/watcher.js.map +1 -0
- package/dist/tasks/worker.d.ts +28 -0
- package/dist/tasks/worker.d.ts.map +1 -0
- package/dist/tasks/worker.js +326 -0
- package/dist/tasks/worker.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/schema.d.ts +79 -0
- package/dist/utils/schema.d.ts.map +1 -0
- package/dist/utils/schema.js +41 -0
- package/dist/utils/schema.js.map +1 -0
- package/dist/utils/service.d.ts +32 -0
- package/dist/utils/service.d.ts.map +1 -0
- package/dist/utils/service.js +384 -0
- package/dist/utils/service.js.map +1 -0
- package/docs/SDK_SPEC.md +1645 -0
- package/infra/firestore.base.indexes.json +138 -0
- package/infra/firestore.base.rules +152 -0
- package/infra/storage.base.rules +8 -0
- package/package.json +59 -13
- package/src/ai/index.ts +2 -0
- package/src/ai/schema.ts +64 -0
- package/src/ai/service.ts +317 -0
- package/src/ai/utils.ts +38 -0
- package/src/attachments/index.ts +3 -0
- package/src/attachments/routes.ts +28 -0
- package/src/attachments/schema.ts +45 -0
- package/src/attachments/service.ts +315 -0
- package/src/attachments/utils.ts +115 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/oidc.ts +419 -0
- package/src/auth/recovery.ts +365 -0
- package/src/auth/routes.ts +142 -0
- package/src/auth/schema.ts +181 -0
- package/src/auth/service.ts +183 -0
- package/src/auth/session.ts +414 -0
- package/src/auth/utils.ts +106 -0
- package/src/chat/context.ts +310 -0
- package/src/chat/index.ts +3 -0
- package/src/chat/routes.ts +264 -0
- package/src/chat/schema.ts +295 -0
- package/src/chat/service.ts +360 -0
- package/src/chat/stream.ts +481 -0
- package/src/chat/utils.ts +370 -0
- package/src/cli/builder.ts +93 -0
- package/src/cli/deployer.ts +184 -0
- package/src/cli/dev-server.ts +77 -0
- package/src/cli/env.ts +89 -0
- package/src/cli/init.ts +125 -0
- package/src/config.ts +447 -0
- package/src/db/index.ts +2 -0
- package/src/db/schema.ts +409 -0
- package/src/db/service.ts +477 -0
- package/src/db/utils.ts +250 -0
- package/src/emails/index.ts +3 -0
- package/src/emails/routes.ts +64 -0
- package/src/emails/schema.ts +239 -0
- package/src/emails/service.ts +488 -0
- package/src/emails/utils.ts +449 -0
- package/src/files/index.ts +3 -0
- package/src/files/routes.ts +44 -0
- package/src/files/schema.ts +76 -0
- package/src/files/service.ts +238 -0
- package/src/files/utils.ts +77 -0
- package/src/index.ts +425 -0
- package/src/payments/billing.ts +488 -0
- package/src/payments/index.ts +3 -0
- package/src/payments/routes.ts +92 -0
- package/src/payments/schema.ts +401 -0
- package/src/payments/service.ts +445 -0
- package/src/payments/utils.ts +472 -0
- package/src/router/guard.ts +319 -0
- package/src/router/index.ts +2 -0
- package/src/router/schema.ts +458 -0
- package/src/router/service.ts +453 -0
- package/src/tasks/index.ts +11 -0
- package/src/tasks/routes.ts +230 -0
- package/src/tasks/schema.ts +310 -0
- package/src/tasks/service.ts +254 -0
- package/src/tasks/steps.ts +472 -0
- package/src/tasks/utils.ts +445 -0
- package/src/tasks/watcher.ts +307 -0
- package/src/tasks/worker.ts +461 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/schema.ts +69 -0
- package/src/utils/service.ts +461 -0
- package/tests/e2e.test.ts +2531 -0
- package/tsconfig.base.json +12 -0
- package/README.md +0 -2
- package/index.d.ts +0 -1
- package/index.js +0 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { Timestamp, type DocumentReference, type Firestore } from 'firebase-admin/firestore'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { CORE_CONFIG, type DbRole } from '../config'
|
|
4
|
+
import type { EntryId } from '../db'
|
|
5
|
+
import type { RouteMode } from '../router'
|
|
6
|
+
import type { Duration } from '../utils'
|
|
7
|
+
import {
|
|
8
|
+
StaleWorkerEpochError,
|
|
9
|
+
TaskWaitDetachedSignal,
|
|
10
|
+
TaskYieldSignal,
|
|
11
|
+
type ClaimCheckPointer,
|
|
12
|
+
type StepContext,
|
|
13
|
+
type StepRetryPolicy,
|
|
14
|
+
type TaskRecord,
|
|
15
|
+
type WorkflowContext,
|
|
16
|
+
type WorkflowTargetRef,
|
|
17
|
+
type WorkflowTaskPayload
|
|
18
|
+
} from './schema'
|
|
19
|
+
|
|
20
|
+
export interface StepRunnerState {
|
|
21
|
+
mode: RouteMode
|
|
22
|
+
docRef?: DocumentReference
|
|
23
|
+
steps: Record<string, unknown>
|
|
24
|
+
expiresAt?: Timestamp
|
|
25
|
+
isDev: boolean
|
|
26
|
+
signals: Record<string, unknown>
|
|
27
|
+
wfCtx?: WorkflowContext
|
|
28
|
+
canYield: boolean
|
|
29
|
+
compensatedSteps: Record<string, boolean>
|
|
30
|
+
replayOnly: boolean
|
|
31
|
+
claimedEpoch?: number
|
|
32
|
+
fencedError: Error | null
|
|
33
|
+
activePersistPromise: Promise<void> | null
|
|
34
|
+
pendingPersist: boolean
|
|
35
|
+
writes: Promise<unknown>[]
|
|
36
|
+
rollbacks: { name: string; run: () => Promise<unknown> }[]
|
|
37
|
+
callCounts: Map<string, number>
|
|
38
|
+
abortController?: AbortController
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const isTaskYieldSignal = (err: unknown): err is TaskYieldSignal =>
|
|
42
|
+
err instanceof TaskYieldSignal || Boolean(err && typeof err === 'object' && (err as any).isTaskYield === true)
|
|
43
|
+
|
|
44
|
+
export const isClaimCheckPointer = (val: unknown): val is ClaimCheckPointer =>
|
|
45
|
+
Boolean(val && typeof val === 'object' && typeof (val as any).__claimCheck === 'string')
|
|
46
|
+
|
|
47
|
+
export const isTaskWaitDetachedSignal = (err: unknown): err is TaskWaitDetachedSignal =>
|
|
48
|
+
err instanceof TaskWaitDetachedSignal ||
|
|
49
|
+
Boolean(err && typeof err === 'object' && (err as { isTaskWaitDetached?: unknown }).isTaskWaitDetached === true)
|
|
50
|
+
|
|
51
|
+
export const zodToJsonSchemaSafe = (schema?: z.ZodType<any>): Record<string, unknown> | null => {
|
|
52
|
+
if (!schema) return null
|
|
53
|
+
try {
|
|
54
|
+
const raw = z.toJSONSchema(schema, { unrepresentable: 'any' }) as Record<string, unknown>
|
|
55
|
+
const { $schema: _$schema, ...rest } = raw
|
|
56
|
+
return rest
|
|
57
|
+
} catch {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const resolveTaskDeadline = (workflowName: string, deadlineSeconds?: number): number =>
|
|
63
|
+
deadlineSeconds ??
|
|
64
|
+
(workflowName === 'scan-attachment'
|
|
65
|
+
? CORE_CONFIG.tasks.scanDeadlineSeconds
|
|
66
|
+
: CORE_CONFIG.tasks.defaultDeadlineSeconds)
|
|
67
|
+
|
|
68
|
+
export const getQueueLoc = (): string => `projects/${CORE_CONFIG.gcpProject}/locations/${CORE_CONFIG.region}`
|
|
69
|
+
|
|
70
|
+
export const getQueueBase = (isDev = false): string =>
|
|
71
|
+
`${getQueueLoc()}/queues/${isDev ? CORE_CONFIG.queueBaseNames.dev : CORE_CONFIG.queueBaseNames.prod}`
|
|
72
|
+
|
|
73
|
+
export const getQueueParent = (idx: number, isDev = false): string => {
|
|
74
|
+
const base = getQueueBase(isDev)
|
|
75
|
+
return idx === 0 ? base : `${base}-${String(idx).padStart(2, '0')}`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const getWorkerUri = (isDev = false): string =>
|
|
79
|
+
isDev ? CORE_CONFIG.workerUris.dev : CORE_CONFIG.workerUris.prod
|
|
80
|
+
|
|
81
|
+
export const isTerminalRecord = (d: TaskRecord<unknown> | undefined): d is TaskRecord<unknown> =>
|
|
82
|
+
Boolean(d && (d.status === 'completed' || d.status === 'errored' || d.status === 'cancelled'))
|
|
83
|
+
|
|
84
|
+
export const setTaskDoc = (docRef: DocumentReference, patch: Record<string, unknown>): Promise<void> =>
|
|
85
|
+
docRef
|
|
86
|
+
.set(patch, { mergeFields: Object.keys(patch).filter((k) => patch[k] !== undefined) })
|
|
87
|
+
.then(() => {})
|
|
88
|
+
.catch(() => {})
|
|
89
|
+
|
|
90
|
+
export const resolveTargetName = (target: string | WorkflowTargetRef<any, any>): string => {
|
|
91
|
+
if (typeof target === 'string') return target
|
|
92
|
+
if (target.workflowName) return target.workflowName
|
|
93
|
+
throw new Error('Workflow target must specify a workflowName or route key')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const isFencedOut = (
|
|
97
|
+
claimedEpoch: number | undefined,
|
|
98
|
+
docEpoch: number | undefined,
|
|
99
|
+
docStatus: string | undefined,
|
|
100
|
+
requireRunning = false
|
|
101
|
+
): boolean => {
|
|
102
|
+
if (claimedEpoch !== undefined && docEpoch !== undefined && docEpoch !== claimedEpoch) return true
|
|
103
|
+
if (requireRunning) return docStatus !== 'running'
|
|
104
|
+
return (
|
|
105
|
+
docStatus === 'compensating' ||
|
|
106
|
+
docStatus === 'cancelled' ||
|
|
107
|
+
docStatus === 'completed' ||
|
|
108
|
+
docStatus === 'errored'
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const createStaleEpochError = (
|
|
113
|
+
taskId: string | undefined,
|
|
114
|
+
claimedEpoch: number | undefined,
|
|
115
|
+
actualEpoch: number | undefined,
|
|
116
|
+
status?: string
|
|
117
|
+
): StaleWorkerEpochError =>
|
|
118
|
+
new StaleWorkerEpochError(
|
|
119
|
+
`Worker epoch ${claimedEpoch ?? 'unknown'} superseded by epoch ${actualEpoch ?? 'unknown'} (status=${status ?? 'unknown'})`,
|
|
120
|
+
{
|
|
121
|
+
...(taskId ? { taskId } : {}),
|
|
122
|
+
...(claimedEpoch !== undefined ? { claimedEpoch } : {}),
|
|
123
|
+
...(actualEpoch !== undefined ? { actualEpoch } : {})
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const compensationLocks = new Map<string, Promise<void>>()
|
|
128
|
+
|
|
129
|
+
export const runSerializedCompensation = async (taskId: string, fn: () => Promise<void>): Promise<void> => {
|
|
130
|
+
const prev = compensationLocks.get(taskId) ?? Promise.resolve()
|
|
131
|
+
const next = prev.catch(() => {}).then(fn)
|
|
132
|
+
compensationLocks.set(taskId, next)
|
|
133
|
+
try {
|
|
134
|
+
await next
|
|
135
|
+
} finally {
|
|
136
|
+
if (compensationLocks.get(taskId) === next) compensationLocks.delete(taskId)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const executeCompensateState = async (s: StepRunnerState): Promise<void> => {
|
|
141
|
+
await Promise.allSettled(s.writes)
|
|
142
|
+
if (!s.rollbacks.length) return
|
|
143
|
+
if (s.docRef) {
|
|
144
|
+
const snap = await s.docRef.get().catch(() => null)
|
|
145
|
+
const remoteComp = snap?.data()?.compensatedSteps as Record<string, boolean> | undefined
|
|
146
|
+
if (remoteComp && typeof remoteComp === 'object') Object.assign(s.compensatedSteps, remoteComp)
|
|
147
|
+
}
|
|
148
|
+
for (const item of [...s.rollbacks].reverse()) {
|
|
149
|
+
if (s.compensatedSteps[item.name]) {
|
|
150
|
+
delete s.steps[item.name]
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
await item.run()
|
|
154
|
+
s.compensatedSteps[item.name] = true
|
|
155
|
+
delete s.steps[item.name]
|
|
156
|
+
if (s.docRef) {
|
|
157
|
+
const patch: Record<string, unknown> = {
|
|
158
|
+
compensatedSteps: { ...s.compensatedSteps },
|
|
159
|
+
steps: { ...s.steps },
|
|
160
|
+
updatedAt: new Date().toISOString(),
|
|
161
|
+
...(s.expiresAt ? { expiresAt: s.expiresAt } : {})
|
|
162
|
+
}
|
|
163
|
+
await s.docRef.set(patch, { mergeFields: Object.keys(patch) })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const runStepWithRetry = async <T>(
|
|
169
|
+
fn: (ctx: StepContext) => Promise<T>,
|
|
170
|
+
retryOrCount: number | StepRetryPolicy = 2,
|
|
171
|
+
delayMs = 1000,
|
|
172
|
+
stepId = 'step',
|
|
173
|
+
timeout: Duration | undefined,
|
|
174
|
+
parentSignal: AbortSignal | undefined,
|
|
175
|
+
parseDuration: (d: Duration | undefined, fallbackMs: number) => number,
|
|
176
|
+
isNonRetryableError: (err: unknown) => boolean
|
|
177
|
+
): Promise<T> => {
|
|
178
|
+
const policy: StepRetryPolicy =
|
|
179
|
+
typeof retryOrCount === 'number'
|
|
180
|
+
? { maxAttempts: retryOrCount + 1, initialInterval: delayMs, backoffCoefficient: 1.5 }
|
|
181
|
+
: retryOrCount
|
|
182
|
+
const maxAttempts = Math.max(1, policy.maxAttempts ?? 3)
|
|
183
|
+
const initialMs = parseDuration(policy.initialInterval, delayMs)
|
|
184
|
+
const backoff = policy.backoffCoefficient ?? 1.5
|
|
185
|
+
const maxIntervalMs = policy.maxInterval ? parseDuration(policy.maxInterval, 30_000) : 30_000
|
|
186
|
+
const useJitter = policy.jitter !== false
|
|
187
|
+
const timeoutMs = timeout ? parseDuration(timeout, 0) : 0
|
|
188
|
+
|
|
189
|
+
let lastErr: unknown
|
|
190
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
191
|
+
const controller = new AbortController()
|
|
192
|
+
const onParentAbort = () => controller.abort(parentSignal?.reason)
|
|
193
|
+
if (parentSignal) {
|
|
194
|
+
if (parentSignal.aborted) controller.abort(parentSignal.reason)
|
|
195
|
+
else parentSignal.addEventListener('abort', onParentAbort, { once: true })
|
|
196
|
+
}
|
|
197
|
+
const timer =
|
|
198
|
+
timeoutMs > 0
|
|
199
|
+
? setTimeout(() => controller.abort(new Error(`Step "${stepId}" timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
200
|
+
: undefined
|
|
201
|
+
try {
|
|
202
|
+
const result = await fn({ signal: controller.signal, attempt, stepId })
|
|
203
|
+
if (timer) clearTimeout(timer)
|
|
204
|
+
if (parentSignal) parentSignal.removeEventListener('abort', onParentAbort)
|
|
205
|
+
return result
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (timer) clearTimeout(timer)
|
|
208
|
+
if (parentSignal) parentSignal.removeEventListener('abort', onParentAbort)
|
|
209
|
+
if (isTaskYieldSignal(err)) throw err
|
|
210
|
+
lastErr = err
|
|
211
|
+
const errName = err instanceof Error ? err.name : ''
|
|
212
|
+
if (isNonRetryableError(err) || (errName && policy.nonRetryableErrorTypes?.includes(errName))) throw err
|
|
213
|
+
if (attempt < maxAttempts) {
|
|
214
|
+
const rawWait = Math.min(maxIntervalMs, initialMs * Math.pow(backoff, attempt - 1))
|
|
215
|
+
const waitMs = useJitter ? Math.round(rawWait * (0.75 + Math.random() * 0.5)) : Math.round(rawWait)
|
|
216
|
+
await new Promise((r) => setTimeout(r, waitMs))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
throw lastErr
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export const claimTaskEpochTx = (
|
|
224
|
+
db: Firestore,
|
|
225
|
+
docRef: DocumentReference,
|
|
226
|
+
payload: WorkflowTaskPayload,
|
|
227
|
+
attempt: number,
|
|
228
|
+
resolveRole: (userId?: EntryId, explicitRole?: DbRole) => DbRole
|
|
229
|
+
) =>
|
|
230
|
+
db.runTransaction(async (tx) => {
|
|
231
|
+
const data = (await tx.get(docRef)).data()
|
|
232
|
+
const currentStatus = data?.status as string | undefined
|
|
233
|
+
if (['completed', 'errored', 'cancelled', 'compensating'].includes(String(currentStatus))) {
|
|
234
|
+
return { aborted: true as const }
|
|
235
|
+
}
|
|
236
|
+
const currentEpoch = typeof data?.epoch === 'number' ? data.epoch : 0
|
|
237
|
+
if (attempt === 0 && payload.expectedEpoch !== undefined && currentEpoch !== payload.expectedEpoch) {
|
|
238
|
+
return { aborted: true as const }
|
|
239
|
+
}
|
|
240
|
+
const claimedEpoch = currentEpoch + 1
|
|
241
|
+
const activeRunId = crypto.randomUUID()
|
|
242
|
+
const startedAt = Date.now()
|
|
243
|
+
const expiresAt = Timestamp.fromMillis(startedAt + CORE_CONFIG.tasks.ttlMs)
|
|
244
|
+
const effectiveParams = (data?.params as Record<string, unknown> | undefined) ?? payload.params
|
|
245
|
+
const userId: EntryId = typeof data?.userId === 'string' ? data.userId : CORE_CONFIG.auth.adminUserId
|
|
246
|
+
const userRole = resolveRole(userId, data?.userRole as DbRole | undefined)
|
|
247
|
+
const steps: Record<string, unknown> = { ...((data?.steps as Record<string, unknown> | undefined) ?? {}) }
|
|
248
|
+
const signals: Record<string, unknown> = { ...((data?.signals as Record<string, unknown> | undefined) ?? {}) }
|
|
249
|
+
const compensatedSteps: Record<string, boolean> = {
|
|
250
|
+
...((data?.compensatedSteps as Record<string, boolean> | undefined) ?? {})
|
|
251
|
+
}
|
|
252
|
+
tx.set(
|
|
253
|
+
docRef,
|
|
254
|
+
{
|
|
255
|
+
taskId: payload.taskId,
|
|
256
|
+
workflowName: payload.workflowName,
|
|
257
|
+
userId,
|
|
258
|
+
userRole,
|
|
259
|
+
status: 'running',
|
|
260
|
+
epoch: claimedEpoch,
|
|
261
|
+
activeRunId,
|
|
262
|
+
lastHeartbeatAt: startedAt,
|
|
263
|
+
params: effectiveParams,
|
|
264
|
+
steps,
|
|
265
|
+
compensatedSteps,
|
|
266
|
+
updatedAt: new Date(startedAt).toISOString(),
|
|
267
|
+
expiresAt
|
|
268
|
+
},
|
|
269
|
+
{ merge: true }
|
|
270
|
+
)
|
|
271
|
+
return {
|
|
272
|
+
aborted: false as const,
|
|
273
|
+
claimedEpoch,
|
|
274
|
+
startedAt,
|
|
275
|
+
expiresAt,
|
|
276
|
+
effectiveParams,
|
|
277
|
+
userId,
|
|
278
|
+
userRole,
|
|
279
|
+
steps,
|
|
280
|
+
signals,
|
|
281
|
+
compensatedSteps
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
export const startTaskHeartbeatTimer = (
|
|
286
|
+
db: Firestore,
|
|
287
|
+
docRef: DocumentReference,
|
|
288
|
+
taskId: string,
|
|
289
|
+
claimedEpoch: number,
|
|
290
|
+
expiresAt: Timestamp,
|
|
291
|
+
controller: AbortController
|
|
292
|
+
): ReturnType<typeof setInterval> => {
|
|
293
|
+
const timer = setInterval(() => {
|
|
294
|
+
void db
|
|
295
|
+
.runTransaction(async (tx) => {
|
|
296
|
+
const data = (await tx.get(docRef)).data()
|
|
297
|
+
if (data?.epoch !== claimedEpoch || data?.status !== 'running') {
|
|
298
|
+
controller.abort(
|
|
299
|
+
new StaleWorkerEpochError(`Heartbeat detected stale epoch on ${taskId}`, {
|
|
300
|
+
taskId,
|
|
301
|
+
claimedEpoch,
|
|
302
|
+
actualEpoch: typeof data?.epoch === 'number' ? data.epoch : undefined
|
|
303
|
+
})
|
|
304
|
+
)
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
tx.set(docRef, { lastHeartbeatAt: Date.now(), updatedAt: new Date().toISOString(), expiresAt }, { merge: true })
|
|
308
|
+
})
|
|
309
|
+
.catch(() => {})
|
|
310
|
+
}, CORE_CONFIG.tasks.heartbeatIntervalMs)
|
|
311
|
+
timer.unref?.()
|
|
312
|
+
return timer
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export const commitTaskCompletionTx = (
|
|
316
|
+
db: Firestore,
|
|
317
|
+
docRef: DocumentReference,
|
|
318
|
+
baseMeta: Record<string, unknown>,
|
|
319
|
+
taskId: string,
|
|
320
|
+
claimedEpoch: number,
|
|
321
|
+
storedResponse: unknown,
|
|
322
|
+
steps: Record<string, unknown>,
|
|
323
|
+
expiresAt: Timestamp
|
|
324
|
+
): Promise<void> =>
|
|
325
|
+
db.runTransaction(async (tx) => {
|
|
326
|
+
const data = (await tx.get(docRef)).data()
|
|
327
|
+
const docEpoch = typeof data?.epoch === 'number' ? data.epoch : undefined
|
|
328
|
+
if (docEpoch !== claimedEpoch || data?.status !== 'running') {
|
|
329
|
+
throw new StaleWorkerEpochError(`Completion fenced on ${taskId}`, {
|
|
330
|
+
taskId,
|
|
331
|
+
claimedEpoch,
|
|
332
|
+
actualEpoch: docEpoch
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
tx.set(
|
|
336
|
+
docRef,
|
|
337
|
+
{
|
|
338
|
+
...baseMeta,
|
|
339
|
+
status: 'completed',
|
|
340
|
+
epoch: claimedEpoch,
|
|
341
|
+
activeRunId: null,
|
|
342
|
+
response: storedResponse,
|
|
343
|
+
steps,
|
|
344
|
+
lastHeartbeatAt: Date.now(),
|
|
345
|
+
updatedAt: new Date().toISOString(),
|
|
346
|
+
expiresAt
|
|
347
|
+
},
|
|
348
|
+
{ merge: true }
|
|
349
|
+
)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
export const commitTaskYieldTx = async (
|
|
353
|
+
db: Firestore,
|
|
354
|
+
docRef: DocumentReference,
|
|
355
|
+
baseMeta: Record<string, unknown>,
|
|
356
|
+
taskId: string,
|
|
357
|
+
claimedEpoch: number,
|
|
358
|
+
err: TaskYieldSignal,
|
|
359
|
+
nextEpoch: number,
|
|
360
|
+
nextParams: Record<string, unknown>,
|
|
361
|
+
nextSteps: Record<string, unknown>,
|
|
362
|
+
nextStatus: string,
|
|
363
|
+
expiresAt: Timestamp
|
|
364
|
+
): Promise<void> => {
|
|
365
|
+
const yieldCommitted = await db.runTransaction(async (tx) => {
|
|
366
|
+
const data = (await tx.get(docRef)).data()
|
|
367
|
+
const docEpoch = typeof data?.epoch === 'number' ? data.epoch : undefined
|
|
368
|
+
if (docEpoch !== claimedEpoch || data?.status !== 'running') {
|
|
369
|
+
return { fenced: true as const, docEpoch }
|
|
370
|
+
}
|
|
371
|
+
tx.set(
|
|
372
|
+
docRef,
|
|
373
|
+
{
|
|
374
|
+
...baseMeta,
|
|
375
|
+
status: nextStatus,
|
|
376
|
+
epoch: nextEpoch,
|
|
377
|
+
activeRunId: null,
|
|
378
|
+
params: nextParams,
|
|
379
|
+
steps: nextSteps,
|
|
380
|
+
...(err.reason === 'waiting_signal' ? { waitingSignal: err.meta.signalName ?? null } : {}),
|
|
381
|
+
updatedAt: new Date().toISOString(),
|
|
382
|
+
expiresAt
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
mergeFields: [
|
|
386
|
+
'taskId',
|
|
387
|
+
'workflowName',
|
|
388
|
+
'userId',
|
|
389
|
+
'userRole',
|
|
390
|
+
'status',
|
|
391
|
+
'epoch',
|
|
392
|
+
'activeRunId',
|
|
393
|
+
'params',
|
|
394
|
+
'steps',
|
|
395
|
+
...(err.reason === 'waiting_signal' ? ['waitingSignal'] : []),
|
|
396
|
+
'updatedAt',
|
|
397
|
+
'expiresAt'
|
|
398
|
+
]
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
return { fenced: false as const }
|
|
402
|
+
})
|
|
403
|
+
if (yieldCommitted.fenced) {
|
|
404
|
+
throw new StaleWorkerEpochError(`Yield fenced on ${taskId}`, {
|
|
405
|
+
taskId,
|
|
406
|
+
claimedEpoch,
|
|
407
|
+
actualEpoch: yieldCommitted.docEpoch
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export const commitTaskCompensatingTx = async (
|
|
413
|
+
db: Firestore,
|
|
414
|
+
docRef: DocumentReference,
|
|
415
|
+
taskId: string,
|
|
416
|
+
claimedEpoch: number,
|
|
417
|
+
expiresAt: Timestamp
|
|
418
|
+
): Promise<void> => {
|
|
419
|
+
const compTransition = await db.runTransaction(async (tx) => {
|
|
420
|
+
const data = (await tx.get(docRef)).data()
|
|
421
|
+
const docEpoch = typeof data?.epoch === 'number' ? data.epoch : undefined
|
|
422
|
+
if (docEpoch !== claimedEpoch || data?.status !== 'running') {
|
|
423
|
+
return { fenced: true as const, docEpoch }
|
|
424
|
+
}
|
|
425
|
+
tx.set(
|
|
426
|
+
docRef,
|
|
427
|
+
{
|
|
428
|
+
status: 'compensating',
|
|
429
|
+
epoch: claimedEpoch + 1,
|
|
430
|
+
activeRunId: null,
|
|
431
|
+
updatedAt: new Date().toISOString(),
|
|
432
|
+
expiresAt
|
|
433
|
+
},
|
|
434
|
+
{ merge: true }
|
|
435
|
+
)
|
|
436
|
+
return { fenced: false as const }
|
|
437
|
+
})
|
|
438
|
+
if (compTransition.fenced) {
|
|
439
|
+
throw new StaleWorkerEpochError(`Error compensation fenced on ${taskId}`, {
|
|
440
|
+
taskId,
|
|
441
|
+
claimedEpoch,
|
|
442
|
+
actualEpoch: compTransition.docEpoch
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
}
|