@tanstack/workflow-vercel 0.0.1
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/README.md +70 -0
- package/dist/index.cjs +8 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/sweep-handler.cjs +87 -0
- package/dist/sweep-handler.cjs.map +1 -0
- package/dist/sweep-handler.d.cts +59 -0
- package/dist/sweep-handler.d.ts +59 -0
- package/dist/sweep-handler.js +85 -0
- package/dist/sweep-handler.js.map +1 -0
- package/package.json +56 -0
- package/src/index.ts +17 -0
- package/src/sweep-handler.ts +188 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @tanstack/workflow-vercel
|
|
2
|
+
|
|
3
|
+
Experimental Vercel host adapter for TanStack Workflow.
|
|
4
|
+
|
|
5
|
+
See the main [Deployment guide](../../docs/guide/deployment.md) and
|
|
6
|
+
[Host adapters API](../../docs/api/host-adapters.md).
|
|
7
|
+
|
|
8
|
+
The adapter wires a `WorkflowRuntimeDefinition` into a Vercel Function. It
|
|
9
|
+
materializes registered workflow schedules into the runtime store, then calls
|
|
10
|
+
`runtime.sweep()` to run due scheduled workflows and resume due timers.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// app/api/workflow/sweep/route.ts
|
|
14
|
+
import { createVercelWorkflowSweepHandler } from '@tanstack/workflow-vercel'
|
|
15
|
+
import { workflowRuntime } from '../../../../src/workflows/runtime.server'
|
|
16
|
+
|
|
17
|
+
export const runtime = 'nodejs'
|
|
18
|
+
export const maxDuration = 60
|
|
19
|
+
|
|
20
|
+
export const GET = createVercelWorkflowSweepHandler({
|
|
21
|
+
runtime: workflowRuntime,
|
|
22
|
+
cronSecret: process.env.CRON_SECRET,
|
|
23
|
+
maxDurationMs: 55_000,
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Configure the Vercel Cron Job in `vercel.json`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"$schema": "https://openapi.vercel.sh/vercel.json",
|
|
32
|
+
"crons": [
|
|
33
|
+
{
|
|
34
|
+
"path": "/api/workflow/sweep",
|
|
35
|
+
"schedule": "*/5 * * * *"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Vercel Cron Jobs invoke the configured path with an HTTP `GET` request. If the
|
|
42
|
+
project defines `CRON_SECRET`, Vercel sends it in the `Authorization` header,
|
|
43
|
+
and the handler can validate it with `cronSecret`.
|
|
44
|
+
|
|
45
|
+
Workflow schedules stay in the runtime registration:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { defineWorkflowRuntime, every } from '@tanstack/workflow-runtime'
|
|
49
|
+
|
|
50
|
+
export const workflowRuntime = defineWorkflowRuntime({
|
|
51
|
+
store,
|
|
52
|
+
workflows: {
|
|
53
|
+
'intent-process': {
|
|
54
|
+
load: async () => intentProcessWorkflow,
|
|
55
|
+
schedules: [
|
|
56
|
+
{
|
|
57
|
+
id: 'intent-process-every-15m',
|
|
58
|
+
schedule: every.minutes(15),
|
|
59
|
+
overlapPolicy: 'skip',
|
|
60
|
+
input: { batchSize: 50 },
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Vercel Hobby projects only support daily cron invocations. For minute-level
|
|
69
|
+
sweeps, deploy on a plan that supports per-minute cron jobs or use a daily
|
|
70
|
+
host cron as a coarse wake-up for schedules that tolerate that cadence.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_sweep_handler = require('./sweep-handler.cjs');
|
|
3
|
+
let _tanstack_workflow_runtime = require("@tanstack/workflow-runtime");
|
|
4
|
+
|
|
5
|
+
exports.createVercelWorkflowCronConfig = require_sweep_handler.createVercelWorkflowCronConfig;
|
|
6
|
+
exports.createVercelWorkflowSweepHandler = require_sweep_handler.createVercelWorkflowSweepHandler;
|
|
7
|
+
exports.materializeWorkflowSchedules = _tanstack_workflow_runtime.materializeWorkflowSchedules;
|
|
8
|
+
exports.vercelWorkflowCronConfig = require_sweep_handler.vercelWorkflowCronConfig;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { MaterializeWorkflowSchedulesOptions, MaterializedWorkflowSchedule, VercelWorkflowCron, VercelWorkflowCronConfig, VercelWorkflowCronConfigOptions, VercelWorkflowSweepHandler, VercelWorkflowSweepHandlerOptions, VercelWorkflowSweepResponse, VercelWorkflowUnauthorizedResponse, createVercelWorkflowCronConfig, createVercelWorkflowSweepHandler, materializeWorkflowSchedules, vercelWorkflowCronConfig } from "./sweep-handler.cjs";
|
|
2
|
+
export { type MaterializeWorkflowSchedulesOptions, type MaterializedWorkflowSchedule, type VercelWorkflowCron, type VercelWorkflowCronConfig, type VercelWorkflowCronConfigOptions, type VercelWorkflowSweepHandler, type VercelWorkflowSweepHandlerOptions, type VercelWorkflowSweepResponse, type VercelWorkflowUnauthorizedResponse, createVercelWorkflowCronConfig, createVercelWorkflowSweepHandler, materializeWorkflowSchedules, vercelWorkflowCronConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { MaterializeWorkflowSchedulesOptions, MaterializedWorkflowSchedule, VercelWorkflowCron, VercelWorkflowCronConfig, VercelWorkflowCronConfigOptions, VercelWorkflowSweepHandler, VercelWorkflowSweepHandlerOptions, VercelWorkflowSweepResponse, VercelWorkflowUnauthorizedResponse, createVercelWorkflowCronConfig, createVercelWorkflowSweepHandler, materializeWorkflowSchedules, vercelWorkflowCronConfig } from "./sweep-handler.js";
|
|
2
|
+
export { type MaterializeWorkflowSchedulesOptions, type MaterializedWorkflowSchedule, type VercelWorkflowCron, type VercelWorkflowCronConfig, type VercelWorkflowCronConfigOptions, type VercelWorkflowSweepHandler, type VercelWorkflowSweepHandlerOptions, type VercelWorkflowSweepResponse, type VercelWorkflowUnauthorizedResponse, createVercelWorkflowCronConfig, createVercelWorkflowSweepHandler, materializeWorkflowSchedules, vercelWorkflowCronConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { createVercelWorkflowCronConfig, createVercelWorkflowSweepHandler, materializeWorkflowSchedules, vercelWorkflowCronConfig } from "./sweep-handler.js";
|
|
2
|
+
|
|
3
|
+
export { createVercelWorkflowCronConfig, createVercelWorkflowSweepHandler, materializeWorkflowSchedules, vercelWorkflowCronConfig };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
let _tanstack_workflow_runtime = require("@tanstack/workflow-runtime");
|
|
2
|
+
|
|
3
|
+
//#region src/sweep-handler.ts
|
|
4
|
+
const DEFAULT_SWEEP_INTERVAL_MINUTES = 5;
|
|
5
|
+
const DEFAULT_SWEEP_PATH = "/api/workflow/sweep";
|
|
6
|
+
const vercelWorkflowCronConfig = createVercelWorkflowCronConfig();
|
|
7
|
+
function createVercelWorkflowCronConfig(options = {}) {
|
|
8
|
+
const path = options.path ?? DEFAULT_SWEEP_PATH;
|
|
9
|
+
if (!path.startsWith("/")) throw new Error("Vercel workflow cron path must start with \"/\".");
|
|
10
|
+
if (options.schedule) return {
|
|
11
|
+
$schema: "https://openapi.vercel.sh/vercel.json",
|
|
12
|
+
crons: [{
|
|
13
|
+
path,
|
|
14
|
+
schedule: options.schedule
|
|
15
|
+
}]
|
|
16
|
+
};
|
|
17
|
+
const everyMinutes = options.everyMinutes ?? DEFAULT_SWEEP_INTERVAL_MINUTES;
|
|
18
|
+
if (!Number.isInteger(everyMinutes) || everyMinutes <= 0) throw new Error("Vercel workflow sweep interval must be a positive integer.");
|
|
19
|
+
return {
|
|
20
|
+
$schema: "https://openapi.vercel.sh/vercel.json",
|
|
21
|
+
crons: [{
|
|
22
|
+
path,
|
|
23
|
+
schedule: everyMinutes === 1 ? "* * * * *" : `*/${everyMinutes} * * * *`
|
|
24
|
+
}]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createVercelWorkflowSweepHandler(options) {
|
|
28
|
+
return async (request) => {
|
|
29
|
+
if (!isAuthorized(request, options.cronSecret)) return Response.json({
|
|
30
|
+
ok: false,
|
|
31
|
+
error: "Unauthorized"
|
|
32
|
+
}, { status: 401 });
|
|
33
|
+
const now = options.now?.() ?? Date.now();
|
|
34
|
+
const materialized = options.materializeSchedules === false ? [] : await (0, _tanstack_workflow_runtime.materializeWorkflowSchedules)(options.runtime, {
|
|
35
|
+
now,
|
|
36
|
+
cronLookbackMs: options.cronLookbackMs
|
|
37
|
+
});
|
|
38
|
+
const leaseOwner = resolveLeaseOwner(options.leaseOwner, request, now);
|
|
39
|
+
const sweepArgs = {
|
|
40
|
+
now,
|
|
41
|
+
leaseOwner,
|
|
42
|
+
limit: options.limit,
|
|
43
|
+
maxScheduledRuns: options.maxScheduledRuns,
|
|
44
|
+
maxTimers: options.maxTimers,
|
|
45
|
+
maxDurationMs: options.maxDurationMs,
|
|
46
|
+
leaseMs: options.leaseMs,
|
|
47
|
+
includeEvents: options.includeEvents ?? false,
|
|
48
|
+
maxEvents: options.maxEvents
|
|
49
|
+
};
|
|
50
|
+
const sweep = await options.runtime.sweep(sweepArgs);
|
|
51
|
+
const response = {
|
|
52
|
+
ok: true,
|
|
53
|
+
now,
|
|
54
|
+
leaseOwner,
|
|
55
|
+
materialized,
|
|
56
|
+
summary: {
|
|
57
|
+
materialized: materialized.length,
|
|
58
|
+
...sweep.summary
|
|
59
|
+
},
|
|
60
|
+
deadlineReached: sweep.deadlineReached,
|
|
61
|
+
remainingMayExist: sweep.remainingMayExist,
|
|
62
|
+
...options.includeSweepResult ? { sweep } : void 0
|
|
63
|
+
};
|
|
64
|
+
return Response.json(response);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function isAuthorized(request, cronSecret) {
|
|
68
|
+
if (!cronSecret) return true;
|
|
69
|
+
return request.headers.get("authorization") === `Bearer ${cronSecret}`;
|
|
70
|
+
}
|
|
71
|
+
function resolveLeaseOwner(leaseOwner, request, now) {
|
|
72
|
+
if (typeof leaseOwner === "string") return leaseOwner;
|
|
73
|
+
if (typeof leaseOwner === "function") return leaseOwner({
|
|
74
|
+
request,
|
|
75
|
+
now
|
|
76
|
+
}) ?? defaultLeaseOwner(request, now);
|
|
77
|
+
return defaultLeaseOwner(request, now);
|
|
78
|
+
}
|
|
79
|
+
function defaultLeaseOwner(request, now) {
|
|
80
|
+
return `vercel:${request.headers.get("x-vercel-id") ?? now}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
exports.createVercelWorkflowCronConfig = createVercelWorkflowCronConfig;
|
|
85
|
+
exports.createVercelWorkflowSweepHandler = createVercelWorkflowSweepHandler;
|
|
86
|
+
exports.vercelWorkflowCronConfig = vercelWorkflowCronConfig;
|
|
87
|
+
//# sourceMappingURL=sweep-handler.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sweep-handler.cjs","names":[],"sources":["../src/sweep-handler.ts"],"sourcesContent":["import { materializeWorkflowSchedules } from '@tanstack/workflow-runtime'\nimport type {\n MaterializedWorkflowSchedule,\n WorkflowRegistrationMap,\n WorkflowRuntimeDefinition,\n WorkflowRuntimeSweepArgs,\n WorkflowRuntimeSweepResult,\n} from '@tanstack/workflow-runtime'\n\nconst DEFAULT_SWEEP_INTERVAL_MINUTES = 5\nconst DEFAULT_SWEEP_PATH = '/api/workflow/sweep'\n\nexport { materializeWorkflowSchedules }\nexport type {\n MaterializedWorkflowSchedule,\n MaterializeWorkflowSchedulesOptions,\n} from '@tanstack/workflow-runtime'\n\nexport interface VercelWorkflowCron {\n path: string\n schedule: string\n}\n\nexport interface VercelWorkflowCronConfig {\n $schema: 'https://openapi.vercel.sh/vercel.json'\n crons: ReadonlyArray<VercelWorkflowCron>\n}\n\nexport interface VercelWorkflowCronConfigOptions {\n path?: string\n schedule?: string\n everyMinutes?: number\n}\n\nexport interface VercelWorkflowSweepResponse {\n ok: true\n now: number\n leaseOwner: string\n materialized: ReadonlyArray<MaterializedWorkflowSchedule>\n summary: VercelWorkflowSweepSummary\n deadlineReached: boolean\n remainingMayExist: boolean\n sweep?: WorkflowRuntimeSweepResult\n}\n\nexport interface VercelWorkflowUnauthorizedResponse {\n ok: false\n error: 'Unauthorized'\n}\n\nexport type VercelWorkflowSweepSummary =\n WorkflowRuntimeSweepResult['summary'] & {\n materialized: number\n }\n\nexport type VercelWorkflowSweepHandler = (request: Request) => Promise<Response>\n\nexport interface VercelWorkflowSweepHandlerOptions<\n TWorkflows extends WorkflowRegistrationMap = WorkflowRegistrationMap,\n> {\n runtime: WorkflowRuntimeDefinition<TWorkflows>\n now?: () => number\n leaseOwner?:\n | string\n | ((args: { request: Request; now: number }) => string | undefined)\n limit?: number\n maxScheduledRuns?: number\n maxTimers?: number\n maxDurationMs?: number\n leaseMs?: number\n includeEvents?: boolean\n maxEvents?: number\n includeSweepResult?: boolean\n materializeSchedules?: boolean\n cronLookbackMs?: number\n cronSecret?: string\n}\n\nexport const vercelWorkflowCronConfig = createVercelWorkflowCronConfig()\n\nexport function createVercelWorkflowCronConfig(\n options: VercelWorkflowCronConfigOptions = {},\n): VercelWorkflowCronConfig {\n const path = options.path ?? DEFAULT_SWEEP_PATH\n if (!path.startsWith('/')) {\n throw new Error('Vercel workflow cron path must start with \"/\".')\n }\n if (options.schedule) {\n return {\n $schema: 'https://openapi.vercel.sh/vercel.json',\n crons: [{ path, schedule: options.schedule }],\n }\n }\n\n const everyMinutes = options.everyMinutes ?? DEFAULT_SWEEP_INTERVAL_MINUTES\n if (!Number.isInteger(everyMinutes) || everyMinutes <= 0) {\n throw new Error(\n 'Vercel workflow sweep interval must be a positive integer.',\n )\n }\n\n return {\n $schema: 'https://openapi.vercel.sh/vercel.json',\n crons: [\n {\n path,\n schedule:\n everyMinutes === 1 ? '* * * * *' : `*/${everyMinutes} * * * *`,\n },\n ],\n }\n}\n\nexport function createVercelWorkflowSweepHandler<\n TWorkflows extends WorkflowRegistrationMap,\n>(\n options: VercelWorkflowSweepHandlerOptions<TWorkflows>,\n): VercelWorkflowSweepHandler {\n return async (request) => {\n if (!isAuthorized(request, options.cronSecret)) {\n return Response.json(\n {\n ok: false,\n error: 'Unauthorized',\n } satisfies VercelWorkflowUnauthorizedResponse,\n { status: 401 },\n )\n }\n\n const now = options.now?.() ?? Date.now()\n const materialized =\n options.materializeSchedules === false\n ? []\n : await materializeWorkflowSchedules(options.runtime, {\n now,\n cronLookbackMs: options.cronLookbackMs,\n })\n const leaseOwner = resolveLeaseOwner(options.leaseOwner, request, now)\n const sweepArgs: WorkflowRuntimeSweepArgs = {\n now,\n leaseOwner,\n limit: options.limit,\n maxScheduledRuns: options.maxScheduledRuns,\n maxTimers: options.maxTimers,\n maxDurationMs: options.maxDurationMs,\n leaseMs: options.leaseMs,\n includeEvents: options.includeEvents ?? false,\n maxEvents: options.maxEvents,\n }\n const sweep = await options.runtime.sweep(sweepArgs)\n const response: VercelWorkflowSweepResponse = {\n ok: true,\n now,\n leaseOwner,\n materialized,\n summary: {\n materialized: materialized.length,\n ...sweep.summary,\n },\n deadlineReached: sweep.deadlineReached,\n remainingMayExist: sweep.remainingMayExist,\n ...(options.includeSweepResult ? { sweep } : undefined),\n }\n\n return Response.json(response)\n }\n}\n\nfunction isAuthorized(request: Request, cronSecret: string | undefined) {\n if (!cronSecret) return true\n return request.headers.get('authorization') === `Bearer ${cronSecret}`\n}\n\nfunction resolveLeaseOwner(\n leaseOwner: VercelWorkflowSweepHandlerOptions['leaseOwner'],\n request: Request,\n now: number,\n) {\n if (typeof leaseOwner === 'string') return leaseOwner\n if (typeof leaseOwner === 'function') {\n return leaseOwner({ request, now }) ?? defaultLeaseOwner(request, now)\n }\n return defaultLeaseOwner(request, now)\n}\n\nfunction defaultLeaseOwner(request: Request, now: number) {\n return `vercel:${request.headers.get('x-vercel-id') ?? now}`\n}\n"],"mappings":";;;AASA,MAAM,iCAAiC;AACvC,MAAM,qBAAqB;AAoE3B,MAAa,2BAA2B,gCAAgC;AAExE,SAAgB,+BACd,UAA2C,EAAE,EACnB;CAC1B,MAAM,OAAO,QAAQ,QAAQ;AAC7B,KAAI,CAAC,KAAK,WAAW,IAAI,CACvB,OAAM,IAAI,MAAM,mDAAiD;AAEnE,KAAI,QAAQ,SACV,QAAO;EACL,SAAS;EACT,OAAO,CAAC;GAAE;GAAM,UAAU,QAAQ;GAAU,CAAC;EAC9C;CAGH,MAAM,eAAe,QAAQ,gBAAgB;AAC7C,KAAI,CAAC,OAAO,UAAU,aAAa,IAAI,gBAAgB,EACrD,OAAM,IAAI,MACR,6DACD;AAGH,QAAO;EACL,SAAS;EACT,OAAO,CACL;GACE;GACA,UACE,iBAAiB,IAAI,cAAc,KAAK,aAAa;GACxD,CACF;EACF;;AAGH,SAAgB,iCAGd,SAC4B;AAC5B,QAAO,OAAO,YAAY;AACxB,MAAI,CAAC,aAAa,SAAS,QAAQ,WAAW,CAC5C,QAAO,SAAS,KACd;GACE,IAAI;GACJ,OAAO;GACR,EACD,EAAE,QAAQ,KAAK,CAChB;EAGH,MAAM,MAAM,QAAQ,OAAO,IAAI,KAAK,KAAK;EACzC,MAAM,eACJ,QAAQ,yBAAyB,QAC7B,EAAE,GACF,mEAAmC,QAAQ,SAAS;GAClD;GACA,gBAAgB,QAAQ;GACzB,CAAC;EACR,MAAM,aAAa,kBAAkB,QAAQ,YAAY,SAAS,IAAI;EACtE,MAAM,YAAsC;GAC1C;GACA;GACA,OAAO,QAAQ;GACf,kBAAkB,QAAQ;GAC1B,WAAW,QAAQ;GACnB,eAAe,QAAQ;GACvB,SAAS,QAAQ;GACjB,eAAe,QAAQ,iBAAiB;GACxC,WAAW,QAAQ;GACpB;EACD,MAAM,QAAQ,MAAM,QAAQ,QAAQ,MAAM,UAAU;EACpD,MAAM,WAAwC;GAC5C,IAAI;GACJ;GACA;GACA;GACA,SAAS;IACP,cAAc,aAAa;IAC3B,GAAG,MAAM;IACV;GACD,iBAAiB,MAAM;GACvB,mBAAmB,MAAM;GACzB,GAAI,QAAQ,qBAAqB,EAAE,OAAO,GAAG;GAC9C;AAED,SAAO,SAAS,KAAK,SAAS;;;AAIlC,SAAS,aAAa,SAAkB,YAAgC;AACtE,KAAI,CAAC,WAAY,QAAO;AACxB,QAAO,QAAQ,QAAQ,IAAI,gBAAgB,KAAK,UAAU;;AAG5D,SAAS,kBACP,YACA,SACA,KACA;AACA,KAAI,OAAO,eAAe,SAAU,QAAO;AAC3C,KAAI,OAAO,eAAe,WACxB,QAAO,WAAW;EAAE;EAAS;EAAK,CAAC,IAAI,kBAAkB,SAAS,IAAI;AAExE,QAAO,kBAAkB,SAAS,IAAI;;AAGxC,SAAS,kBAAkB,SAAkB,KAAa;AACxD,QAAO,UAAU,QAAQ,QAAQ,IAAI,cAAc,IAAI"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { MaterializeWorkflowSchedulesOptions, MaterializedWorkflowSchedule, MaterializedWorkflowSchedule as MaterializedWorkflowSchedule$1, WorkflowRegistrationMap, WorkflowRuntimeDefinition, WorkflowRuntimeSweepResult, materializeWorkflowSchedules } from "@tanstack/workflow-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/sweep-handler.d.ts
|
|
4
|
+
interface VercelWorkflowCron {
|
|
5
|
+
path: string;
|
|
6
|
+
schedule: string;
|
|
7
|
+
}
|
|
8
|
+
interface VercelWorkflowCronConfig {
|
|
9
|
+
$schema: 'https://openapi.vercel.sh/vercel.json';
|
|
10
|
+
crons: ReadonlyArray<VercelWorkflowCron>;
|
|
11
|
+
}
|
|
12
|
+
interface VercelWorkflowCronConfigOptions {
|
|
13
|
+
path?: string;
|
|
14
|
+
schedule?: string;
|
|
15
|
+
everyMinutes?: number;
|
|
16
|
+
}
|
|
17
|
+
interface VercelWorkflowSweepResponse {
|
|
18
|
+
ok: true;
|
|
19
|
+
now: number;
|
|
20
|
+
leaseOwner: string;
|
|
21
|
+
materialized: ReadonlyArray<MaterializedWorkflowSchedule>;
|
|
22
|
+
summary: VercelWorkflowSweepSummary;
|
|
23
|
+
deadlineReached: boolean;
|
|
24
|
+
remainingMayExist: boolean;
|
|
25
|
+
sweep?: WorkflowRuntimeSweepResult;
|
|
26
|
+
}
|
|
27
|
+
interface VercelWorkflowUnauthorizedResponse {
|
|
28
|
+
ok: false;
|
|
29
|
+
error: 'Unauthorized';
|
|
30
|
+
}
|
|
31
|
+
type VercelWorkflowSweepSummary = WorkflowRuntimeSweepResult['summary'] & {
|
|
32
|
+
materialized: number;
|
|
33
|
+
};
|
|
34
|
+
type VercelWorkflowSweepHandler = (request: Request) => Promise<Response>;
|
|
35
|
+
interface VercelWorkflowSweepHandlerOptions<TWorkflows extends WorkflowRegistrationMap = WorkflowRegistrationMap> {
|
|
36
|
+
runtime: WorkflowRuntimeDefinition<TWorkflows>;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
leaseOwner?: string | ((args: {
|
|
39
|
+
request: Request;
|
|
40
|
+
now: number;
|
|
41
|
+
}) => string | undefined);
|
|
42
|
+
limit?: number;
|
|
43
|
+
maxScheduledRuns?: number;
|
|
44
|
+
maxTimers?: number;
|
|
45
|
+
maxDurationMs?: number;
|
|
46
|
+
leaseMs?: number;
|
|
47
|
+
includeEvents?: boolean;
|
|
48
|
+
maxEvents?: number;
|
|
49
|
+
includeSweepResult?: boolean;
|
|
50
|
+
materializeSchedules?: boolean;
|
|
51
|
+
cronLookbackMs?: number;
|
|
52
|
+
cronSecret?: string;
|
|
53
|
+
}
|
|
54
|
+
declare const vercelWorkflowCronConfig: VercelWorkflowCronConfig;
|
|
55
|
+
declare function createVercelWorkflowCronConfig(options?: VercelWorkflowCronConfigOptions): VercelWorkflowCronConfig;
|
|
56
|
+
declare function createVercelWorkflowSweepHandler<TWorkflows extends WorkflowRegistrationMap>(options: VercelWorkflowSweepHandlerOptions<TWorkflows>): VercelWorkflowSweepHandler;
|
|
57
|
+
//#endregion
|
|
58
|
+
export { type MaterializeWorkflowSchedulesOptions, type MaterializedWorkflowSchedule$1 as MaterializedWorkflowSchedule, VercelWorkflowCron, VercelWorkflowCronConfig, VercelWorkflowCronConfigOptions, VercelWorkflowSweepHandler, VercelWorkflowSweepHandlerOptions, VercelWorkflowSweepResponse, VercelWorkflowUnauthorizedResponse, createVercelWorkflowCronConfig, createVercelWorkflowSweepHandler, materializeWorkflowSchedules, vercelWorkflowCronConfig };
|
|
59
|
+
//# sourceMappingURL=sweep-handler.d.cts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { MaterializeWorkflowSchedulesOptions, MaterializedWorkflowSchedule, MaterializedWorkflowSchedule as MaterializedWorkflowSchedule$1, WorkflowRegistrationMap, WorkflowRuntimeDefinition, WorkflowRuntimeSweepResult, materializeWorkflowSchedules } from "@tanstack/workflow-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/sweep-handler.d.ts
|
|
4
|
+
interface VercelWorkflowCron {
|
|
5
|
+
path: string;
|
|
6
|
+
schedule: string;
|
|
7
|
+
}
|
|
8
|
+
interface VercelWorkflowCronConfig {
|
|
9
|
+
$schema: 'https://openapi.vercel.sh/vercel.json';
|
|
10
|
+
crons: ReadonlyArray<VercelWorkflowCron>;
|
|
11
|
+
}
|
|
12
|
+
interface VercelWorkflowCronConfigOptions {
|
|
13
|
+
path?: string;
|
|
14
|
+
schedule?: string;
|
|
15
|
+
everyMinutes?: number;
|
|
16
|
+
}
|
|
17
|
+
interface VercelWorkflowSweepResponse {
|
|
18
|
+
ok: true;
|
|
19
|
+
now: number;
|
|
20
|
+
leaseOwner: string;
|
|
21
|
+
materialized: ReadonlyArray<MaterializedWorkflowSchedule>;
|
|
22
|
+
summary: VercelWorkflowSweepSummary;
|
|
23
|
+
deadlineReached: boolean;
|
|
24
|
+
remainingMayExist: boolean;
|
|
25
|
+
sweep?: WorkflowRuntimeSweepResult;
|
|
26
|
+
}
|
|
27
|
+
interface VercelWorkflowUnauthorizedResponse {
|
|
28
|
+
ok: false;
|
|
29
|
+
error: 'Unauthorized';
|
|
30
|
+
}
|
|
31
|
+
type VercelWorkflowSweepSummary = WorkflowRuntimeSweepResult['summary'] & {
|
|
32
|
+
materialized: number;
|
|
33
|
+
};
|
|
34
|
+
type VercelWorkflowSweepHandler = (request: Request) => Promise<Response>;
|
|
35
|
+
interface VercelWorkflowSweepHandlerOptions<TWorkflows extends WorkflowRegistrationMap = WorkflowRegistrationMap> {
|
|
36
|
+
runtime: WorkflowRuntimeDefinition<TWorkflows>;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
leaseOwner?: string | ((args: {
|
|
39
|
+
request: Request;
|
|
40
|
+
now: number;
|
|
41
|
+
}) => string | undefined);
|
|
42
|
+
limit?: number;
|
|
43
|
+
maxScheduledRuns?: number;
|
|
44
|
+
maxTimers?: number;
|
|
45
|
+
maxDurationMs?: number;
|
|
46
|
+
leaseMs?: number;
|
|
47
|
+
includeEvents?: boolean;
|
|
48
|
+
maxEvents?: number;
|
|
49
|
+
includeSweepResult?: boolean;
|
|
50
|
+
materializeSchedules?: boolean;
|
|
51
|
+
cronLookbackMs?: number;
|
|
52
|
+
cronSecret?: string;
|
|
53
|
+
}
|
|
54
|
+
declare const vercelWorkflowCronConfig: VercelWorkflowCronConfig;
|
|
55
|
+
declare function createVercelWorkflowCronConfig(options?: VercelWorkflowCronConfigOptions): VercelWorkflowCronConfig;
|
|
56
|
+
declare function createVercelWorkflowSweepHandler<TWorkflows extends WorkflowRegistrationMap>(options: VercelWorkflowSweepHandlerOptions<TWorkflows>): VercelWorkflowSweepHandler;
|
|
57
|
+
//#endregion
|
|
58
|
+
export { type MaterializeWorkflowSchedulesOptions, type MaterializedWorkflowSchedule$1 as MaterializedWorkflowSchedule, VercelWorkflowCron, VercelWorkflowCronConfig, VercelWorkflowCronConfigOptions, VercelWorkflowSweepHandler, VercelWorkflowSweepHandlerOptions, VercelWorkflowSweepResponse, VercelWorkflowUnauthorizedResponse, createVercelWorkflowCronConfig, createVercelWorkflowSweepHandler, materializeWorkflowSchedules, vercelWorkflowCronConfig };
|
|
59
|
+
//# sourceMappingURL=sweep-handler.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { materializeWorkflowSchedules } from "@tanstack/workflow-runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/sweep-handler.ts
|
|
4
|
+
const DEFAULT_SWEEP_INTERVAL_MINUTES = 5;
|
|
5
|
+
const DEFAULT_SWEEP_PATH = "/api/workflow/sweep";
|
|
6
|
+
const vercelWorkflowCronConfig = createVercelWorkflowCronConfig();
|
|
7
|
+
function createVercelWorkflowCronConfig(options = {}) {
|
|
8
|
+
const path = options.path ?? DEFAULT_SWEEP_PATH;
|
|
9
|
+
if (!path.startsWith("/")) throw new Error("Vercel workflow cron path must start with \"/\".");
|
|
10
|
+
if (options.schedule) return {
|
|
11
|
+
$schema: "https://openapi.vercel.sh/vercel.json",
|
|
12
|
+
crons: [{
|
|
13
|
+
path,
|
|
14
|
+
schedule: options.schedule
|
|
15
|
+
}]
|
|
16
|
+
};
|
|
17
|
+
const everyMinutes = options.everyMinutes ?? DEFAULT_SWEEP_INTERVAL_MINUTES;
|
|
18
|
+
if (!Number.isInteger(everyMinutes) || everyMinutes <= 0) throw new Error("Vercel workflow sweep interval must be a positive integer.");
|
|
19
|
+
return {
|
|
20
|
+
$schema: "https://openapi.vercel.sh/vercel.json",
|
|
21
|
+
crons: [{
|
|
22
|
+
path,
|
|
23
|
+
schedule: everyMinutes === 1 ? "* * * * *" : `*/${everyMinutes} * * * *`
|
|
24
|
+
}]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createVercelWorkflowSweepHandler(options) {
|
|
28
|
+
return async (request) => {
|
|
29
|
+
if (!isAuthorized(request, options.cronSecret)) return Response.json({
|
|
30
|
+
ok: false,
|
|
31
|
+
error: "Unauthorized"
|
|
32
|
+
}, { status: 401 });
|
|
33
|
+
const now = options.now?.() ?? Date.now();
|
|
34
|
+
const materialized = options.materializeSchedules === false ? [] : await materializeWorkflowSchedules(options.runtime, {
|
|
35
|
+
now,
|
|
36
|
+
cronLookbackMs: options.cronLookbackMs
|
|
37
|
+
});
|
|
38
|
+
const leaseOwner = resolveLeaseOwner(options.leaseOwner, request, now);
|
|
39
|
+
const sweepArgs = {
|
|
40
|
+
now,
|
|
41
|
+
leaseOwner,
|
|
42
|
+
limit: options.limit,
|
|
43
|
+
maxScheduledRuns: options.maxScheduledRuns,
|
|
44
|
+
maxTimers: options.maxTimers,
|
|
45
|
+
maxDurationMs: options.maxDurationMs,
|
|
46
|
+
leaseMs: options.leaseMs,
|
|
47
|
+
includeEvents: options.includeEvents ?? false,
|
|
48
|
+
maxEvents: options.maxEvents
|
|
49
|
+
};
|
|
50
|
+
const sweep = await options.runtime.sweep(sweepArgs);
|
|
51
|
+
const response = {
|
|
52
|
+
ok: true,
|
|
53
|
+
now,
|
|
54
|
+
leaseOwner,
|
|
55
|
+
materialized,
|
|
56
|
+
summary: {
|
|
57
|
+
materialized: materialized.length,
|
|
58
|
+
...sweep.summary
|
|
59
|
+
},
|
|
60
|
+
deadlineReached: sweep.deadlineReached,
|
|
61
|
+
remainingMayExist: sweep.remainingMayExist,
|
|
62
|
+
...options.includeSweepResult ? { sweep } : void 0
|
|
63
|
+
};
|
|
64
|
+
return Response.json(response);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function isAuthorized(request, cronSecret) {
|
|
68
|
+
if (!cronSecret) return true;
|
|
69
|
+
return request.headers.get("authorization") === `Bearer ${cronSecret}`;
|
|
70
|
+
}
|
|
71
|
+
function resolveLeaseOwner(leaseOwner, request, now) {
|
|
72
|
+
if (typeof leaseOwner === "string") return leaseOwner;
|
|
73
|
+
if (typeof leaseOwner === "function") return leaseOwner({
|
|
74
|
+
request,
|
|
75
|
+
now
|
|
76
|
+
}) ?? defaultLeaseOwner(request, now);
|
|
77
|
+
return defaultLeaseOwner(request, now);
|
|
78
|
+
}
|
|
79
|
+
function defaultLeaseOwner(request, now) {
|
|
80
|
+
return `vercel:${request.headers.get("x-vercel-id") ?? now}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
export { createVercelWorkflowCronConfig, createVercelWorkflowSweepHandler, materializeWorkflowSchedules, vercelWorkflowCronConfig };
|
|
85
|
+
//# sourceMappingURL=sweep-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sweep-handler.js","names":[],"sources":["../src/sweep-handler.ts"],"sourcesContent":["import { materializeWorkflowSchedules } from '@tanstack/workflow-runtime'\nimport type {\n MaterializedWorkflowSchedule,\n WorkflowRegistrationMap,\n WorkflowRuntimeDefinition,\n WorkflowRuntimeSweepArgs,\n WorkflowRuntimeSweepResult,\n} from '@tanstack/workflow-runtime'\n\nconst DEFAULT_SWEEP_INTERVAL_MINUTES = 5\nconst DEFAULT_SWEEP_PATH = '/api/workflow/sweep'\n\nexport { materializeWorkflowSchedules }\nexport type {\n MaterializedWorkflowSchedule,\n MaterializeWorkflowSchedulesOptions,\n} from '@tanstack/workflow-runtime'\n\nexport interface VercelWorkflowCron {\n path: string\n schedule: string\n}\n\nexport interface VercelWorkflowCronConfig {\n $schema: 'https://openapi.vercel.sh/vercel.json'\n crons: ReadonlyArray<VercelWorkflowCron>\n}\n\nexport interface VercelWorkflowCronConfigOptions {\n path?: string\n schedule?: string\n everyMinutes?: number\n}\n\nexport interface VercelWorkflowSweepResponse {\n ok: true\n now: number\n leaseOwner: string\n materialized: ReadonlyArray<MaterializedWorkflowSchedule>\n summary: VercelWorkflowSweepSummary\n deadlineReached: boolean\n remainingMayExist: boolean\n sweep?: WorkflowRuntimeSweepResult\n}\n\nexport interface VercelWorkflowUnauthorizedResponse {\n ok: false\n error: 'Unauthorized'\n}\n\nexport type VercelWorkflowSweepSummary =\n WorkflowRuntimeSweepResult['summary'] & {\n materialized: number\n }\n\nexport type VercelWorkflowSweepHandler = (request: Request) => Promise<Response>\n\nexport interface VercelWorkflowSweepHandlerOptions<\n TWorkflows extends WorkflowRegistrationMap = WorkflowRegistrationMap,\n> {\n runtime: WorkflowRuntimeDefinition<TWorkflows>\n now?: () => number\n leaseOwner?:\n | string\n | ((args: { request: Request; now: number }) => string | undefined)\n limit?: number\n maxScheduledRuns?: number\n maxTimers?: number\n maxDurationMs?: number\n leaseMs?: number\n includeEvents?: boolean\n maxEvents?: number\n includeSweepResult?: boolean\n materializeSchedules?: boolean\n cronLookbackMs?: number\n cronSecret?: string\n}\n\nexport const vercelWorkflowCronConfig = createVercelWorkflowCronConfig()\n\nexport function createVercelWorkflowCronConfig(\n options: VercelWorkflowCronConfigOptions = {},\n): VercelWorkflowCronConfig {\n const path = options.path ?? DEFAULT_SWEEP_PATH\n if (!path.startsWith('/')) {\n throw new Error('Vercel workflow cron path must start with \"/\".')\n }\n if (options.schedule) {\n return {\n $schema: 'https://openapi.vercel.sh/vercel.json',\n crons: [{ path, schedule: options.schedule }],\n }\n }\n\n const everyMinutes = options.everyMinutes ?? DEFAULT_SWEEP_INTERVAL_MINUTES\n if (!Number.isInteger(everyMinutes) || everyMinutes <= 0) {\n throw new Error(\n 'Vercel workflow sweep interval must be a positive integer.',\n )\n }\n\n return {\n $schema: 'https://openapi.vercel.sh/vercel.json',\n crons: [\n {\n path,\n schedule:\n everyMinutes === 1 ? '* * * * *' : `*/${everyMinutes} * * * *`,\n },\n ],\n }\n}\n\nexport function createVercelWorkflowSweepHandler<\n TWorkflows extends WorkflowRegistrationMap,\n>(\n options: VercelWorkflowSweepHandlerOptions<TWorkflows>,\n): VercelWorkflowSweepHandler {\n return async (request) => {\n if (!isAuthorized(request, options.cronSecret)) {\n return Response.json(\n {\n ok: false,\n error: 'Unauthorized',\n } satisfies VercelWorkflowUnauthorizedResponse,\n { status: 401 },\n )\n }\n\n const now = options.now?.() ?? Date.now()\n const materialized =\n options.materializeSchedules === false\n ? []\n : await materializeWorkflowSchedules(options.runtime, {\n now,\n cronLookbackMs: options.cronLookbackMs,\n })\n const leaseOwner = resolveLeaseOwner(options.leaseOwner, request, now)\n const sweepArgs: WorkflowRuntimeSweepArgs = {\n now,\n leaseOwner,\n limit: options.limit,\n maxScheduledRuns: options.maxScheduledRuns,\n maxTimers: options.maxTimers,\n maxDurationMs: options.maxDurationMs,\n leaseMs: options.leaseMs,\n includeEvents: options.includeEvents ?? false,\n maxEvents: options.maxEvents,\n }\n const sweep = await options.runtime.sweep(sweepArgs)\n const response: VercelWorkflowSweepResponse = {\n ok: true,\n now,\n leaseOwner,\n materialized,\n summary: {\n materialized: materialized.length,\n ...sweep.summary,\n },\n deadlineReached: sweep.deadlineReached,\n remainingMayExist: sweep.remainingMayExist,\n ...(options.includeSweepResult ? { sweep } : undefined),\n }\n\n return Response.json(response)\n }\n}\n\nfunction isAuthorized(request: Request, cronSecret: string | undefined) {\n if (!cronSecret) return true\n return request.headers.get('authorization') === `Bearer ${cronSecret}`\n}\n\nfunction resolveLeaseOwner(\n leaseOwner: VercelWorkflowSweepHandlerOptions['leaseOwner'],\n request: Request,\n now: number,\n) {\n if (typeof leaseOwner === 'string') return leaseOwner\n if (typeof leaseOwner === 'function') {\n return leaseOwner({ request, now }) ?? defaultLeaseOwner(request, now)\n }\n return defaultLeaseOwner(request, now)\n}\n\nfunction defaultLeaseOwner(request: Request, now: number) {\n return `vercel:${request.headers.get('x-vercel-id') ?? now}`\n}\n"],"mappings":";;;AASA,MAAM,iCAAiC;AACvC,MAAM,qBAAqB;AAoE3B,MAAa,2BAA2B,gCAAgC;AAExE,SAAgB,+BACd,UAA2C,EAAE,EACnB;CAC1B,MAAM,OAAO,QAAQ,QAAQ;AAC7B,KAAI,CAAC,KAAK,WAAW,IAAI,CACvB,OAAM,IAAI,MAAM,mDAAiD;AAEnE,KAAI,QAAQ,SACV,QAAO;EACL,SAAS;EACT,OAAO,CAAC;GAAE;GAAM,UAAU,QAAQ;GAAU,CAAC;EAC9C;CAGH,MAAM,eAAe,QAAQ,gBAAgB;AAC7C,KAAI,CAAC,OAAO,UAAU,aAAa,IAAI,gBAAgB,EACrD,OAAM,IAAI,MACR,6DACD;AAGH,QAAO;EACL,SAAS;EACT,OAAO,CACL;GACE;GACA,UACE,iBAAiB,IAAI,cAAc,KAAK,aAAa;GACxD,CACF;EACF;;AAGH,SAAgB,iCAGd,SAC4B;AAC5B,QAAO,OAAO,YAAY;AACxB,MAAI,CAAC,aAAa,SAAS,QAAQ,WAAW,CAC5C,QAAO,SAAS,KACd;GACE,IAAI;GACJ,OAAO;GACR,EACD,EAAE,QAAQ,KAAK,CAChB;EAGH,MAAM,MAAM,QAAQ,OAAO,IAAI,KAAK,KAAK;EACzC,MAAM,eACJ,QAAQ,yBAAyB,QAC7B,EAAE,GACF,MAAM,6BAA6B,QAAQ,SAAS;GAClD;GACA,gBAAgB,QAAQ;GACzB,CAAC;EACR,MAAM,aAAa,kBAAkB,QAAQ,YAAY,SAAS,IAAI;EACtE,MAAM,YAAsC;GAC1C;GACA;GACA,OAAO,QAAQ;GACf,kBAAkB,QAAQ;GAC1B,WAAW,QAAQ;GACnB,eAAe,QAAQ;GACvB,SAAS,QAAQ;GACjB,eAAe,QAAQ,iBAAiB;GACxC,WAAW,QAAQ;GACpB;EACD,MAAM,QAAQ,MAAM,QAAQ,QAAQ,MAAM,UAAU;EACpD,MAAM,WAAwC;GAC5C,IAAI;GACJ;GACA;GACA;GACA,SAAS;IACP,cAAc,aAAa;IAC3B,GAAG,MAAM;IACV;GACD,iBAAiB,MAAM;GACvB,mBAAmB,MAAM;GACzB,GAAI,QAAQ,qBAAqB,EAAE,OAAO,GAAG;GAC9C;AAED,SAAO,SAAS,KAAK,SAAS;;;AAIlC,SAAS,aAAa,SAAkB,YAAgC;AACtE,KAAI,CAAC,WAAY,QAAO;AACxB,QAAO,QAAQ,QAAQ,IAAI,gBAAgB,KAAK,UAAU;;AAG5D,SAAS,kBACP,YACA,SACA,KACA;AACA,KAAI,OAAO,eAAe,SAAU,QAAO;AAC3C,KAAI,OAAO,eAAe,WACxB,QAAO,WAAW;EAAE;EAAS;EAAK,CAAC,IAAI,kBAAkB,SAAS,IAAI;AAExE,QAAO,kBAAkB,SAAS,IAAI;;AAGxC,SAAS,kBAAkB,SAAkB,KAAa;AACxD,QAAO,UAAU,QAAQ,QAAQ,IAAI,cAAc,IAAI"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanstack/workflow-vercel",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Vercel host adapter for TanStack Workflow runtimes.",
|
|
5
|
+
"author": "Tanner Linsley",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/TanStack/workflow.git",
|
|
10
|
+
"directory": "packages/workflow-vercel"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://tanstack.com/workflow",
|
|
13
|
+
"funding": {
|
|
14
|
+
"type": "github",
|
|
15
|
+
"url": "https://github.com/sponsors/tannerlinsley"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"tanstack",
|
|
19
|
+
"workflow",
|
|
20
|
+
"durable-execution",
|
|
21
|
+
"vercel",
|
|
22
|
+
"serverless"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "premove ./build ./dist",
|
|
26
|
+
"lint": "eslint ./src",
|
|
27
|
+
"lint:fix": "eslint ./src --fix",
|
|
28
|
+
"test:eslint": "eslint ./src",
|
|
29
|
+
"test:lib": "vitest",
|
|
30
|
+
"test:lib:dev": "pnpm test:lib --watch",
|
|
31
|
+
"test:types": "tsc",
|
|
32
|
+
"build": "tsdown"
|
|
33
|
+
},
|
|
34
|
+
"type": "module",
|
|
35
|
+
"main": "./dist/index.cjs",
|
|
36
|
+
"module": "./dist/index.js",
|
|
37
|
+
"types": "./dist/index.d.cts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"import": "./dist/index.js",
|
|
41
|
+
"require": "./dist/index.cjs"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"sideEffects": false,
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist/",
|
|
51
|
+
"src"
|
|
52
|
+
],
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@tanstack/workflow-runtime": "workspace:*"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createVercelWorkflowCronConfig,
|
|
3
|
+
createVercelWorkflowSweepHandler,
|
|
4
|
+
materializeWorkflowSchedules,
|
|
5
|
+
vercelWorkflowCronConfig,
|
|
6
|
+
} from './sweep-handler'
|
|
7
|
+
export type {
|
|
8
|
+
MaterializedWorkflowSchedule,
|
|
9
|
+
MaterializeWorkflowSchedulesOptions,
|
|
10
|
+
VercelWorkflowCron,
|
|
11
|
+
VercelWorkflowCronConfig,
|
|
12
|
+
VercelWorkflowCronConfigOptions,
|
|
13
|
+
VercelWorkflowSweepHandler,
|
|
14
|
+
VercelWorkflowSweepHandlerOptions,
|
|
15
|
+
VercelWorkflowSweepResponse,
|
|
16
|
+
VercelWorkflowUnauthorizedResponse,
|
|
17
|
+
} from './sweep-handler'
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { materializeWorkflowSchedules } from '@tanstack/workflow-runtime'
|
|
2
|
+
import type {
|
|
3
|
+
MaterializedWorkflowSchedule,
|
|
4
|
+
WorkflowRegistrationMap,
|
|
5
|
+
WorkflowRuntimeDefinition,
|
|
6
|
+
WorkflowRuntimeSweepArgs,
|
|
7
|
+
WorkflowRuntimeSweepResult,
|
|
8
|
+
} from '@tanstack/workflow-runtime'
|
|
9
|
+
|
|
10
|
+
const DEFAULT_SWEEP_INTERVAL_MINUTES = 5
|
|
11
|
+
const DEFAULT_SWEEP_PATH = '/api/workflow/sweep'
|
|
12
|
+
|
|
13
|
+
export { materializeWorkflowSchedules }
|
|
14
|
+
export type {
|
|
15
|
+
MaterializedWorkflowSchedule,
|
|
16
|
+
MaterializeWorkflowSchedulesOptions,
|
|
17
|
+
} from '@tanstack/workflow-runtime'
|
|
18
|
+
|
|
19
|
+
export interface VercelWorkflowCron {
|
|
20
|
+
path: string
|
|
21
|
+
schedule: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface VercelWorkflowCronConfig {
|
|
25
|
+
$schema: 'https://openapi.vercel.sh/vercel.json'
|
|
26
|
+
crons: ReadonlyArray<VercelWorkflowCron>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface VercelWorkflowCronConfigOptions {
|
|
30
|
+
path?: string
|
|
31
|
+
schedule?: string
|
|
32
|
+
everyMinutes?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface VercelWorkflowSweepResponse {
|
|
36
|
+
ok: true
|
|
37
|
+
now: number
|
|
38
|
+
leaseOwner: string
|
|
39
|
+
materialized: ReadonlyArray<MaterializedWorkflowSchedule>
|
|
40
|
+
summary: VercelWorkflowSweepSummary
|
|
41
|
+
deadlineReached: boolean
|
|
42
|
+
remainingMayExist: boolean
|
|
43
|
+
sweep?: WorkflowRuntimeSweepResult
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface VercelWorkflowUnauthorizedResponse {
|
|
47
|
+
ok: false
|
|
48
|
+
error: 'Unauthorized'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type VercelWorkflowSweepSummary =
|
|
52
|
+
WorkflowRuntimeSweepResult['summary'] & {
|
|
53
|
+
materialized: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type VercelWorkflowSweepHandler = (request: Request) => Promise<Response>
|
|
57
|
+
|
|
58
|
+
export interface VercelWorkflowSweepHandlerOptions<
|
|
59
|
+
TWorkflows extends WorkflowRegistrationMap = WorkflowRegistrationMap,
|
|
60
|
+
> {
|
|
61
|
+
runtime: WorkflowRuntimeDefinition<TWorkflows>
|
|
62
|
+
now?: () => number
|
|
63
|
+
leaseOwner?:
|
|
64
|
+
| string
|
|
65
|
+
| ((args: { request: Request; now: number }) => string | undefined)
|
|
66
|
+
limit?: number
|
|
67
|
+
maxScheduledRuns?: number
|
|
68
|
+
maxTimers?: number
|
|
69
|
+
maxDurationMs?: number
|
|
70
|
+
leaseMs?: number
|
|
71
|
+
includeEvents?: boolean
|
|
72
|
+
maxEvents?: number
|
|
73
|
+
includeSweepResult?: boolean
|
|
74
|
+
materializeSchedules?: boolean
|
|
75
|
+
cronLookbackMs?: number
|
|
76
|
+
cronSecret?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const vercelWorkflowCronConfig = createVercelWorkflowCronConfig()
|
|
80
|
+
|
|
81
|
+
export function createVercelWorkflowCronConfig(
|
|
82
|
+
options: VercelWorkflowCronConfigOptions = {},
|
|
83
|
+
): VercelWorkflowCronConfig {
|
|
84
|
+
const path = options.path ?? DEFAULT_SWEEP_PATH
|
|
85
|
+
if (!path.startsWith('/')) {
|
|
86
|
+
throw new Error('Vercel workflow cron path must start with "/".')
|
|
87
|
+
}
|
|
88
|
+
if (options.schedule) {
|
|
89
|
+
return {
|
|
90
|
+
$schema: 'https://openapi.vercel.sh/vercel.json',
|
|
91
|
+
crons: [{ path, schedule: options.schedule }],
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const everyMinutes = options.everyMinutes ?? DEFAULT_SWEEP_INTERVAL_MINUTES
|
|
96
|
+
if (!Number.isInteger(everyMinutes) || everyMinutes <= 0) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'Vercel workflow sweep interval must be a positive integer.',
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
$schema: 'https://openapi.vercel.sh/vercel.json',
|
|
104
|
+
crons: [
|
|
105
|
+
{
|
|
106
|
+
path,
|
|
107
|
+
schedule:
|
|
108
|
+
everyMinutes === 1 ? '* * * * *' : `*/${everyMinutes} * * * *`,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function createVercelWorkflowSweepHandler<
|
|
115
|
+
TWorkflows extends WorkflowRegistrationMap,
|
|
116
|
+
>(
|
|
117
|
+
options: VercelWorkflowSweepHandlerOptions<TWorkflows>,
|
|
118
|
+
): VercelWorkflowSweepHandler {
|
|
119
|
+
return async (request) => {
|
|
120
|
+
if (!isAuthorized(request, options.cronSecret)) {
|
|
121
|
+
return Response.json(
|
|
122
|
+
{
|
|
123
|
+
ok: false,
|
|
124
|
+
error: 'Unauthorized',
|
|
125
|
+
} satisfies VercelWorkflowUnauthorizedResponse,
|
|
126
|
+
{ status: 401 },
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const now = options.now?.() ?? Date.now()
|
|
131
|
+
const materialized =
|
|
132
|
+
options.materializeSchedules === false
|
|
133
|
+
? []
|
|
134
|
+
: await materializeWorkflowSchedules(options.runtime, {
|
|
135
|
+
now,
|
|
136
|
+
cronLookbackMs: options.cronLookbackMs,
|
|
137
|
+
})
|
|
138
|
+
const leaseOwner = resolveLeaseOwner(options.leaseOwner, request, now)
|
|
139
|
+
const sweepArgs: WorkflowRuntimeSweepArgs = {
|
|
140
|
+
now,
|
|
141
|
+
leaseOwner,
|
|
142
|
+
limit: options.limit,
|
|
143
|
+
maxScheduledRuns: options.maxScheduledRuns,
|
|
144
|
+
maxTimers: options.maxTimers,
|
|
145
|
+
maxDurationMs: options.maxDurationMs,
|
|
146
|
+
leaseMs: options.leaseMs,
|
|
147
|
+
includeEvents: options.includeEvents ?? false,
|
|
148
|
+
maxEvents: options.maxEvents,
|
|
149
|
+
}
|
|
150
|
+
const sweep = await options.runtime.sweep(sweepArgs)
|
|
151
|
+
const response: VercelWorkflowSweepResponse = {
|
|
152
|
+
ok: true,
|
|
153
|
+
now,
|
|
154
|
+
leaseOwner,
|
|
155
|
+
materialized,
|
|
156
|
+
summary: {
|
|
157
|
+
materialized: materialized.length,
|
|
158
|
+
...sweep.summary,
|
|
159
|
+
},
|
|
160
|
+
deadlineReached: sweep.deadlineReached,
|
|
161
|
+
remainingMayExist: sweep.remainingMayExist,
|
|
162
|
+
...(options.includeSweepResult ? { sweep } : undefined),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return Response.json(response)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isAuthorized(request: Request, cronSecret: string | undefined) {
|
|
170
|
+
if (!cronSecret) return true
|
|
171
|
+
return request.headers.get('authorization') === `Bearer ${cronSecret}`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveLeaseOwner(
|
|
175
|
+
leaseOwner: VercelWorkflowSweepHandlerOptions['leaseOwner'],
|
|
176
|
+
request: Request,
|
|
177
|
+
now: number,
|
|
178
|
+
) {
|
|
179
|
+
if (typeof leaseOwner === 'string') return leaseOwner
|
|
180
|
+
if (typeof leaseOwner === 'function') {
|
|
181
|
+
return leaseOwner({ request, now }) ?? defaultLeaseOwner(request, now)
|
|
182
|
+
}
|
|
183
|
+
return defaultLeaseOwner(request, now)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function defaultLeaseOwner(request: Request, now: number) {
|
|
187
|
+
return `vercel:${request.headers.get('x-vercel-id') ?? now}`
|
|
188
|
+
}
|