flowli 0.2.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/LICENSE +21 -0
- package/README.md +815 -0
- package/dist/bun-redis.d.ts +1 -0
- package/dist/bun-redis.js +3 -0
- package/dist/core/define-jobs.d.ts +8 -0
- package/dist/core/define-jobs.js +47 -0
- package/dist/core/errors.d.ts +21 -0
- package/dist/core/errors.js +35 -0
- package/dist/core/job.d.ts +9 -0
- package/dist/core/job.js +20 -0
- package/dist/core/types.d.ts +175 -0
- package/dist/core/types.js +1 -0
- package/dist/driver/duration.d.ts +2 -0
- package/dist/driver/duration.js +23 -0
- package/dist/driver/encoding.d.ts +2 -0
- package/dist/driver/encoding.js +9 -0
- package/dist/driver/keys.d.ts +15 -0
- package/dist/driver/keys.js +14 -0
- package/dist/driver/records.d.ts +25 -0
- package/dist/driver/records.js +61 -0
- package/dist/driver/scheduling.d.ts +6 -0
- package/dist/driver/scheduling.js +127 -0
- package/dist/drivers/bun-redis.d.ts +24 -0
- package/dist/drivers/bun-redis.js +21 -0
- package/dist/drivers/ioredis.d.ts +16 -0
- package/dist/drivers/ioredis.js +31 -0
- package/dist/drivers/redis.d.ts +27 -0
- package/dist/drivers/redis.js +23 -0
- package/dist/drivers/shared.d.ts +21 -0
- package/dist/drivers/shared.js +172 -0
- package/dist/hono.d.ts +1 -0
- package/dist/hono.js +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -0
- package/dist/integrations/hono.d.ts +10 -0
- package/dist/integrations/hono.js +7 -0
- package/dist/integrations/next.d.ts +18 -0
- package/dist/integrations/tanstack-start.d.ts +20 -0
- package/dist/ioredis.d.ts +1 -0
- package/dist/ioredis.js +3 -0
- package/dist/next.d.ts +1 -0
- package/dist/next.js +4 -0
- package/dist/redis.d.ts +1 -0
- package/dist/redis.js +3 -0
- package/dist/runner/create-runner.d.ts +3 -0
- package/dist/runner/create-runner.js +104 -0
- package/dist/runner/types.d.ts +20 -0
- package/dist/runner/types.js +1 -0
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +3 -0
- package/dist/runtime/create-job-surface.d.ts +2 -0
- package/dist/runtime/create-job-surface.js +24 -0
- package/dist/runtime/invoke-handler.d.ts +2 -0
- package/dist/runtime/invoke-handler.js +7 -0
- package/dist/runtime/normalize-jobs.d.ts +5 -0
- package/dist/runtime/normalize-jobs.js +14 -0
- package/dist/runtime/resolve-context.d.ts +2 -0
- package/dist/runtime/resolve-context.js +6 -0
- package/dist/runtime/validate.d.ts +2 -0
- package/dist/runtime/validate.js +15 -0
- package/dist/strategies/delay.d.ts +2 -0
- package/dist/strategies/delay.js +32 -0
- package/dist/strategies/enqueue.d.ts +2 -0
- package/dist/strategies/enqueue.js +30 -0
- package/dist/strategies/run.d.ts +2 -0
- package/dist/strategies/run.js +10 -0
- package/dist/strategies/schedule.d.ts +2 -0
- package/dist/strategies/schedule.js +33 -0
- package/dist/tanstack-start.d.ts +1 -0
- package/dist/tanstack-start.js +4 -0
- package/jsr.json +26 -0
- package/package.json +135 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createRedisDriver } from "./shared.js";
|
|
2
|
+
export function bunRedisDriver(options) {
|
|
3
|
+
return createRedisDriver({
|
|
4
|
+
kind: "bun-redis",
|
|
5
|
+
...(options.prefix ? { prefix: options.prefix } : {}),
|
|
6
|
+
commands: createBunRedisAdapter(options.client),
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export function createBunRedisAdapter(client) {
|
|
10
|
+
return {
|
|
11
|
+
get: (key) => client.get(key),
|
|
12
|
+
set: (key, value, options) => client.set(key, value, {
|
|
13
|
+
...(options?.nx ? { nx: options.nx } : {}),
|
|
14
|
+
...(options?.px !== undefined ? { px: options.px } : {}),
|
|
15
|
+
}),
|
|
16
|
+
del: (key) => client.del(key),
|
|
17
|
+
zadd: (key, score, member) => client.zadd(key, score, member),
|
|
18
|
+
zrem: (key, member) => client.zrem(key, member),
|
|
19
|
+
zrangebyscore: (key, min, max, limit) => client.zrangebyscore(key, min, max, limit ? { limit } : undefined),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FlowliDriver } from "../core/types.js";
|
|
2
|
+
import { type RedisCommandAdapter } from "./shared.js";
|
|
3
|
+
export interface IoredisLikeClient {
|
|
4
|
+
get(key: string): Promise<string | null>;
|
|
5
|
+
set(key: string, value: string, modeOrOptions?: "PX" | "NX" | undefined, ttlOrMode?: number | "NX" | "PX", maybeMode?: "NX" | "PX"): Promise<"OK" | null>;
|
|
6
|
+
del(key: string): Promise<number>;
|
|
7
|
+
zadd(key: string, score: number, member: string): Promise<number>;
|
|
8
|
+
zrem(key: string, member: string): Promise<number>;
|
|
9
|
+
zrangebyscore(key: string, min: number | string, max: number | string, limitToken?: "LIMIT", offset?: number, count?: number): Promise<string[]>;
|
|
10
|
+
}
|
|
11
|
+
export interface IoredisDriverOptions {
|
|
12
|
+
readonly client: IoredisLikeClient;
|
|
13
|
+
readonly prefix?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function ioredisDriver(options: IoredisDriverOptions): FlowliDriver;
|
|
16
|
+
export declare function createIoredisAdapter(client: IoredisLikeClient): RedisCommandAdapter;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createRedisDriver } from "./shared.js";
|
|
2
|
+
export function ioredisDriver(options) {
|
|
3
|
+
return createRedisDriver({
|
|
4
|
+
kind: "ioredis",
|
|
5
|
+
...(options.prefix ? { prefix: options.prefix } : {}),
|
|
6
|
+
commands: createIoredisAdapter(options.client),
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export function createIoredisAdapter(client) {
|
|
10
|
+
return {
|
|
11
|
+
get: (key) => client.get(key),
|
|
12
|
+
async set(key, value, options) {
|
|
13
|
+
if (options?.nx && options?.px !== undefined) {
|
|
14
|
+
return client.set(key, value, "PX", options.px, "NX");
|
|
15
|
+
}
|
|
16
|
+
if (options?.nx) {
|
|
17
|
+
return client.set(key, value, "NX");
|
|
18
|
+
}
|
|
19
|
+
if (options?.px !== undefined) {
|
|
20
|
+
return client.set(key, value, "PX", options.px);
|
|
21
|
+
}
|
|
22
|
+
return client.set(key, value);
|
|
23
|
+
},
|
|
24
|
+
del: (key) => client.del(key),
|
|
25
|
+
zadd: (key, score, member) => client.zadd(key, score, member),
|
|
26
|
+
zrem: (key, member) => client.zrem(key, member),
|
|
27
|
+
zrangebyscore: (key, min, max, limit) => limit
|
|
28
|
+
? client.zrangebyscore(key, min, max, "LIMIT", limit.offset, limit.count)
|
|
29
|
+
: client.zrangebyscore(key, min, max),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FlowliDriver } from "../core/types.js";
|
|
2
|
+
import { type RedisCommandAdapter } from "./shared.js";
|
|
3
|
+
export interface NodeRedisLikeClient {
|
|
4
|
+
get(key: string): Promise<string | null>;
|
|
5
|
+
set(key: string, value: string, options?: {
|
|
6
|
+
NX?: boolean;
|
|
7
|
+
PX?: number;
|
|
8
|
+
}): Promise<string | null>;
|
|
9
|
+
del(key: string): Promise<number>;
|
|
10
|
+
zAdd(key: string, members: ReadonlyArray<{
|
|
11
|
+
score: number;
|
|
12
|
+
value: string;
|
|
13
|
+
}>): Promise<number>;
|
|
14
|
+
zRem(key: string, member: string): Promise<number>;
|
|
15
|
+
zRangeByScore(key: string, min: number, max: number, options?: {
|
|
16
|
+
LIMIT?: {
|
|
17
|
+
offset: number;
|
|
18
|
+
count: number;
|
|
19
|
+
};
|
|
20
|
+
}): Promise<string[]>;
|
|
21
|
+
}
|
|
22
|
+
export interface RedisDriverOptions {
|
|
23
|
+
readonly client: NodeRedisLikeClient;
|
|
24
|
+
readonly prefix?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function redisDriver(options: RedisDriverOptions): FlowliDriver;
|
|
27
|
+
export declare function createNodeRedisAdapter(client: NodeRedisLikeClient): RedisCommandAdapter;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createRedisDriver } from "./shared.js";
|
|
2
|
+
export function redisDriver(options) {
|
|
3
|
+
return createRedisDriver({
|
|
4
|
+
kind: "redis",
|
|
5
|
+
...(options.prefix ? { prefix: options.prefix } : {}),
|
|
6
|
+
commands: createNodeRedisAdapter(options.client),
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export function createNodeRedisAdapter(client) {
|
|
10
|
+
return {
|
|
11
|
+
get: (key) => client.get(key),
|
|
12
|
+
set: (key, value, options) => client
|
|
13
|
+
.set(key, value, {
|
|
14
|
+
...(options?.nx ? { NX: options.nx } : {}),
|
|
15
|
+
...(options?.px !== undefined ? { PX: options.px } : {}),
|
|
16
|
+
})
|
|
17
|
+
.then((result) => (result === null ? null : "OK")),
|
|
18
|
+
del: (key) => client.del(key),
|
|
19
|
+
zadd: (key, score, member) => client.zAdd(key, [{ score, value: member }]),
|
|
20
|
+
zrem: (key, member) => client.zRem(key, member),
|
|
21
|
+
zrangebyscore: (key, min, max, limit) => client.zRangeByScore(key, min, max, limit ? { LIMIT: limit } : undefined),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FlowliDriver } from "../core/types.js";
|
|
2
|
+
import { type FlowliRedisKeyOptions } from "../driver/keys.js";
|
|
3
|
+
export interface RedisCommandAdapter {
|
|
4
|
+
get(key: string): Promise<string | null>;
|
|
5
|
+
set(key: string, value: string, options?: {
|
|
6
|
+
nx?: boolean;
|
|
7
|
+
px?: number;
|
|
8
|
+
}): Promise<"OK" | null>;
|
|
9
|
+
del(key: string): Promise<number>;
|
|
10
|
+
zadd(key: string, score: number, member: string): Promise<number>;
|
|
11
|
+
zrem(key: string, member: string): Promise<number>;
|
|
12
|
+
zrangebyscore(key: string, min: number, max: number, limit?: {
|
|
13
|
+
offset: number;
|
|
14
|
+
count: number;
|
|
15
|
+
}): Promise<string[]>;
|
|
16
|
+
}
|
|
17
|
+
export interface SharedRedisDriverOptions extends FlowliRedisKeyOptions {
|
|
18
|
+
readonly kind: string;
|
|
19
|
+
readonly commands: RedisCommandAdapter;
|
|
20
|
+
}
|
|
21
|
+
export declare function createRedisDriver(options: SharedRedisDriverOptions): FlowliDriver;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { decodeJson, encodeJson } from "../driver/encoding.js";
|
|
2
|
+
import { createRedisKeys } from "../driver/keys.js";
|
|
3
|
+
import { createJobReceipt, createPersistedJobRecord, createScheduleReceipt, } from "../driver/records.js";
|
|
4
|
+
import { createJobId, createLeaseToken, getNextCronRun, } from "../driver/scheduling.js";
|
|
5
|
+
export function createRedisDriver(options) {
|
|
6
|
+
const keys = createRedisKeys(options.prefix ? { prefix: options.prefix } : undefined);
|
|
7
|
+
const commands = options.commands;
|
|
8
|
+
return {
|
|
9
|
+
kind: options.kind,
|
|
10
|
+
async enqueue(record) {
|
|
11
|
+
await writeJob(record);
|
|
12
|
+
await commands.zadd(keys.pending, record.scheduledFor, record.id);
|
|
13
|
+
return createJobReceipt(record);
|
|
14
|
+
},
|
|
15
|
+
async registerSchedule(record) {
|
|
16
|
+
await commands.set(keys.schedule(record.key), encodeJson(record));
|
|
17
|
+
await commands.zadd(keys.schedulesDue, record.nextRunAt, record.key);
|
|
18
|
+
return createScheduleReceipt(record);
|
|
19
|
+
},
|
|
20
|
+
async acquireNextReady(now, leaseMs) {
|
|
21
|
+
const candidates = await commands.zrangebyscore(keys.pending, Number.NEGATIVE_INFINITY, now, {
|
|
22
|
+
offset: 0,
|
|
23
|
+
count: 25,
|
|
24
|
+
});
|
|
25
|
+
for (const jobId of candidates) {
|
|
26
|
+
const token = createLeaseToken();
|
|
27
|
+
const locked = await commands.set(keys.lease(jobId), token, {
|
|
28
|
+
nx: true,
|
|
29
|
+
px: leaseMs,
|
|
30
|
+
});
|
|
31
|
+
if (!locked) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const removed = await commands.zrem(keys.pending, jobId);
|
|
35
|
+
if (removed === 0) {
|
|
36
|
+
await commands.del(keys.lease(jobId));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const record = await readJob(jobId);
|
|
40
|
+
if (!record) {
|
|
41
|
+
await commands.del(keys.lease(jobId));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const acquiredRecord = {
|
|
45
|
+
...record,
|
|
46
|
+
state: "active",
|
|
47
|
+
attemptsMade: record.attemptsMade + 1,
|
|
48
|
+
updatedAt: now,
|
|
49
|
+
};
|
|
50
|
+
await writeJob(acquiredRecord);
|
|
51
|
+
await commands.zadd(keys.active, now, jobId);
|
|
52
|
+
return {
|
|
53
|
+
token,
|
|
54
|
+
record: acquiredRecord,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
},
|
|
59
|
+
async renewLease(jobId, token, leaseMs) {
|
|
60
|
+
const current = await commands.get(keys.lease(jobId));
|
|
61
|
+
if (current !== token) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const updated = await commands.set(keys.lease(jobId), token, {
|
|
65
|
+
px: leaseMs,
|
|
66
|
+
});
|
|
67
|
+
return updated === "OK";
|
|
68
|
+
},
|
|
69
|
+
async markCompleted(acquired, finishedAt) {
|
|
70
|
+
const completedRecord = {
|
|
71
|
+
...acquired.record,
|
|
72
|
+
state: "completed",
|
|
73
|
+
updatedAt: finishedAt,
|
|
74
|
+
};
|
|
75
|
+
await writeJob(completedRecord);
|
|
76
|
+
await commands.zrem(keys.active, completedRecord.id);
|
|
77
|
+
await commands.zadd(keys.completed, finishedAt, completedRecord.id);
|
|
78
|
+
await commands.del(keys.lease(completedRecord.id));
|
|
79
|
+
},
|
|
80
|
+
async markFailed(acquired, finishedAt, error) {
|
|
81
|
+
const shouldRetry = acquired.record.attemptsMade < acquired.record.maxAttempts;
|
|
82
|
+
const retryAt = shouldRetry
|
|
83
|
+
? finishedAt + computeBackoff(acquired.record, finishedAt)
|
|
84
|
+
: finishedAt;
|
|
85
|
+
const nextRecord = {
|
|
86
|
+
...acquired.record,
|
|
87
|
+
state: shouldRetry ? "queued" : "failed",
|
|
88
|
+
scheduledFor: retryAt,
|
|
89
|
+
updatedAt: finishedAt,
|
|
90
|
+
lastError: error,
|
|
91
|
+
};
|
|
92
|
+
await writeJob(nextRecord);
|
|
93
|
+
await commands.zrem(keys.active, nextRecord.id);
|
|
94
|
+
if (shouldRetry) {
|
|
95
|
+
await commands.zadd(keys.pending, retryAt, nextRecord.id);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
await commands.zadd(keys.failed, finishedAt, nextRecord.id);
|
|
99
|
+
}
|
|
100
|
+
await commands.del(keys.lease(nextRecord.id));
|
|
101
|
+
return shouldRetry ? "retrying" : "failed";
|
|
102
|
+
},
|
|
103
|
+
async materializeDueSchedules(now, leaseMs) {
|
|
104
|
+
const dueKeys = await commands.zrangebyscore(keys.schedulesDue, Number.NEGATIVE_INFINITY, now, { offset: 0, count: 100 });
|
|
105
|
+
let created = 0;
|
|
106
|
+
for (const scheduleKey of dueKeys) {
|
|
107
|
+
const token = createLeaseToken();
|
|
108
|
+
const locked = await commands.set(keys.scheduleLease(scheduleKey), token, {
|
|
109
|
+
nx: true,
|
|
110
|
+
px: leaseMs,
|
|
111
|
+
});
|
|
112
|
+
if (!locked) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const schedule = await readSchedule(scheduleKey);
|
|
116
|
+
if (!schedule) {
|
|
117
|
+
await commands.zrem(keys.schedulesDue, scheduleKey);
|
|
118
|
+
await commands.del(keys.scheduleLease(scheduleKey));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (schedule.nextRunAt > now) {
|
|
122
|
+
await commands.del(keys.scheduleLease(scheduleKey));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const jobRecord = createPersistedJobRecord({
|
|
126
|
+
id: createJobId(),
|
|
127
|
+
name: schedule.name,
|
|
128
|
+
input: schedule.input,
|
|
129
|
+
meta: schedule.meta,
|
|
130
|
+
scheduledFor: now,
|
|
131
|
+
maxAttempts: schedule.maxAttempts,
|
|
132
|
+
...(schedule.backoff ? { backoff: schedule.backoff } : {}),
|
|
133
|
+
now,
|
|
134
|
+
});
|
|
135
|
+
await writeJob(jobRecord);
|
|
136
|
+
await commands.zadd(keys.pending, now, jobRecord.id);
|
|
137
|
+
created += 1;
|
|
138
|
+
const nextRunAt = getNextCronRun(schedule.cron, now);
|
|
139
|
+
const nextSchedule = {
|
|
140
|
+
...schedule,
|
|
141
|
+
nextRunAt,
|
|
142
|
+
updatedAt: now,
|
|
143
|
+
};
|
|
144
|
+
await commands.set(keys.schedule(schedule.key), encodeJson(nextSchedule));
|
|
145
|
+
await commands.zrem(keys.schedulesDue, scheduleKey);
|
|
146
|
+
await commands.zadd(keys.schedulesDue, nextRunAt, scheduleKey);
|
|
147
|
+
await commands.del(keys.scheduleLease(scheduleKey));
|
|
148
|
+
}
|
|
149
|
+
return created;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
async function writeJob(record) {
|
|
153
|
+
await commands.set(keys.job(record.id), encodeJson(record));
|
|
154
|
+
}
|
|
155
|
+
async function readJob(jobId) {
|
|
156
|
+
return decodeJson(await commands.get(keys.job(jobId)));
|
|
157
|
+
}
|
|
158
|
+
async function readSchedule(key) {
|
|
159
|
+
return decodeJson(await commands.get(keys.schedule(key)));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function computeBackoff(record, finishedAt) {
|
|
163
|
+
void finishedAt;
|
|
164
|
+
if (!record.backoff) {
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
if (record.backoff.type === "fixed") {
|
|
168
|
+
return record.backoff.delayMs;
|
|
169
|
+
}
|
|
170
|
+
const exponent = Math.max(record.attemptsMade - 1, 0);
|
|
171
|
+
return record.backoff.delayMs * Math.max(1, 2 ** exponent);
|
|
172
|
+
}
|
package/dist/hono.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type HonoFlowliVariables, type HonoJobsOptions, type HonoLikeContext, type HonoLikeNext, honoJobs, } from "./integrations/hono.js";
|
package/dist/hono.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { defineJobs } from "./core/define-jobs.js";
|
|
2
|
+
export { FlowliDefinitionError, FlowliDriverError, FlowliError, FlowliSchedulingError, FlowliStrategyError, FlowliValidationError, } from "./core/errors.js";
|
|
3
|
+
export { createContextualJobFactory, job } from "./core/job.js";
|
|
4
|
+
export type { BackoffOptions, DefineJobsBuilder, DelayValue, FlowliContextRecord, FlowliContextResolver, FlowliDriver, FlowliInvocationOptions, FlowliJobSurface, FlowliRuntime, JobDefaults, JobDefinition, JobHandlerArgs, JobReceipt, ScheduleInvocation, ScheduleReceipt, StandardSchemaIssue, StandardSchemaV1, } from "./core/types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FlowliContextRecord, FlowliRuntime, JobsRecord } from "../core/types.js";
|
|
2
|
+
export interface HonoLikeContext {
|
|
3
|
+
set(key: string, value: unknown): void;
|
|
4
|
+
}
|
|
5
|
+
export type HonoLikeNext = () => Promise<unknown>;
|
|
6
|
+
export interface HonoJobsOptions<TKey extends string = "flowli"> {
|
|
7
|
+
readonly key?: TKey;
|
|
8
|
+
}
|
|
9
|
+
export type HonoFlowliVariables<TFlowli, TKey extends string = "flowli"> = Record<TKey, TFlowli>;
|
|
10
|
+
export declare function honoJobs<TJobs extends JobsRecord, TContext extends FlowliContextRecord, TKey extends string = "flowli">(flowli: FlowliRuntime<TJobs, TContext>, options?: HonoJobsOptions<TKey>): (context: HonoLikeContext, next: HonoLikeNext) => Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FlowliContextRecord, FlowliRuntime, JobsRecord } from "../core/types.js";
|
|
2
|
+
export type NextRouteParams = Record<string, string | ReadonlyArray<string> | undefined>;
|
|
3
|
+
export interface NextRouteContext<TParams extends NextRouteParams = NextRouteParams> {
|
|
4
|
+
readonly params?: TParams | Promise<TParams>;
|
|
5
|
+
}
|
|
6
|
+
export interface NextRouteHandlerArgs<TFlowli, TParams extends NextRouteParams = NextRouteParams, TRequest extends Request = Request> {
|
|
7
|
+
readonly request: TRequest;
|
|
8
|
+
readonly context: NextRouteContext<TParams>;
|
|
9
|
+
readonly params: TParams | undefined;
|
|
10
|
+
readonly flowli: TFlowli;
|
|
11
|
+
}
|
|
12
|
+
export type NextRouteHandler<TFlowli, TParams extends NextRouteParams = NextRouteParams, TRequest extends Request = Request, TResult = Response> = (args: NextRouteHandlerArgs<TFlowli, TParams, TRequest>) => TResult | Promise<TResult>;
|
|
13
|
+
export interface NextActionTools<TFlowli> {
|
|
14
|
+
readonly flowli: TFlowli;
|
|
15
|
+
}
|
|
16
|
+
export type NextActionHandler<TFlowli, TArgs extends ReadonlyArray<unknown>, TResult> = (tools: NextActionTools<TFlowli>, ...args: TArgs) => TResult | Promise<TResult>;
|
|
17
|
+
export declare function nextRoute<TJobs extends JobsRecord, TContext extends FlowliContextRecord, TParams extends NextRouteParams = NextRouteParams, TRequest extends Request = Request, TResult = Response>(flowli: FlowliRuntime<TJobs, TContext>, handler: NextRouteHandler<FlowliRuntime<TJobs, TContext>, TParams, TRequest, TResult>): (request: TRequest, context?: NextRouteContext<TParams>) => Promise<Awaited<TResult>>;
|
|
18
|
+
export declare function nextAction<TJobs extends JobsRecord, TContext extends FlowliContextRecord, TArgs extends ReadonlyArray<unknown>, TResult>(flowli: FlowliRuntime<TJobs, TContext>, handler: NextActionHandler<FlowliRuntime<TJobs, TContext>, TArgs, TResult>): (...args: TArgs) => Promise<Awaited<TResult>>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FlowliContextRecord, FlowliRuntime, JobsRecord } from "../core/types.js";
|
|
2
|
+
export type TanStackStartRouteParams = Record<string, string | ReadonlyArray<string> | undefined>;
|
|
3
|
+
export interface TanStackStartRouteContext {
|
|
4
|
+
readonly [key: PropertyKey]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface TanStackStartRouteHandlerArgs<TFlowli, TParams extends TanStackStartRouteParams = TanStackStartRouteParams, TContext extends TanStackStartRouteContext = TanStackStartRouteContext, TRequest extends Request = Request> {
|
|
7
|
+
readonly request: TRequest;
|
|
8
|
+
readonly params: TParams;
|
|
9
|
+
readonly context: TContext;
|
|
10
|
+
readonly flowli: TFlowli;
|
|
11
|
+
}
|
|
12
|
+
export type TanStackStartRouteHandler<TFlowli, TParams extends TanStackStartRouteParams = TanStackStartRouteParams, TContext extends TanStackStartRouteContext = TanStackStartRouteContext, TRequest extends Request = Request, TResult = Response> = (args: TanStackStartRouteHandlerArgs<TFlowli, TParams, TContext, TRequest>) => TResult | Promise<TResult>;
|
|
13
|
+
export interface TanStackStartServerFnTools<TFlowli> {
|
|
14
|
+
readonly flowli: TFlowli;
|
|
15
|
+
}
|
|
16
|
+
type StripFlowli<TArgs extends object> = Omit<TArgs, keyof TanStackStartServerFnTools<unknown>>;
|
|
17
|
+
export type TanStackStartServerFnHandler<TFlowli, TArgs extends TanStackStartServerFnTools<TFlowli>, TResult> = (args: TArgs) => TResult | Promise<TResult>;
|
|
18
|
+
export declare function tanstackStartRoute<TJobs extends JobsRecord, TContext extends FlowliContextRecord, TParams extends TanStackStartRouteParams = TanStackStartRouteParams, TRouteContext extends TanStackStartRouteContext = TanStackStartRouteContext, TRequest extends Request = Request, TResult = Response>(flowli: FlowliRuntime<TJobs, TContext>, handler: TanStackStartRouteHandler<FlowliRuntime<TJobs, TContext>, TParams, TRouteContext, TRequest, TResult>): (args: Omit<TanStackStartRouteHandlerArgs<FlowliRuntime<TJobs, TContext>, TParams, TRouteContext, TRequest>, "flowli">) => Promise<Awaited<TResult>>;
|
|
19
|
+
export declare function tanstackStartServerFn<TJobs extends JobsRecord, TContext extends FlowliContextRecord, TArgs extends TanStackStartServerFnTools<FlowliRuntime<TJobs, TContext>>, TResult>(flowli: FlowliRuntime<TJobs, TContext>, handler: TanStackStartServerFnHandler<FlowliRuntime<TJobs, TContext>, TArgs, TResult>): (args: StripFlowli<TArgs>) => Promise<Awaited<TResult>>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type IoredisDriverOptions, type IoredisLikeClient, ioredisDriver, } from "./drivers/ioredis.js";
|
package/dist/ioredis.js
ADDED
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type NextActionHandler, type NextActionTools, type NextRouteContext, type NextRouteHandler, type NextRouteHandlerArgs, type NextRouteParams, nextAction, nextRoute, } from "./integrations/next.js";
|
package/dist/next.js
ADDED
package/dist/redis.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type NodeRedisLikeClient, type RedisDriverOptions, redisDriver, } from "./drivers/redis.js";
|
package/dist/redis.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { FlowliContextRecord, JobsRecord } from "../core/types.js";
|
|
2
|
+
import type { FlowliRunner, RunnerOptions } from "./types.js";
|
|
3
|
+
export declare function createRunner<TJobs extends JobsRecord, TContext extends FlowliContextRecord>(options: RunnerOptions<TJobs, TContext>): FlowliRunner;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { getFlowliRuntimeInternals } from "../core/define-jobs.js";
|
|
2
|
+
import { FlowliDriverError, FlowliStrategyError } from "../core/errors.js";
|
|
3
|
+
import { createPersistedJobError } from "../driver/records.js";
|
|
4
|
+
import { invokeHandler } from "../runtime/invoke-handler.js";
|
|
5
|
+
import { validateWithSchema } from "../runtime/validate.js";
|
|
6
|
+
export function createRunner(options) {
|
|
7
|
+
const internals = getFlowliRuntimeInternals(options.flowli);
|
|
8
|
+
if (!internals.driver) {
|
|
9
|
+
throw new FlowliDriverError("createRunner() requires a Flowli runtime with a configured driver.");
|
|
10
|
+
}
|
|
11
|
+
const driver = internals.driver;
|
|
12
|
+
const concurrency = Math.max(options.concurrency ?? 1, 1);
|
|
13
|
+
const pollIntervalMs = Math.max(options.pollIntervalMs ?? 1_000, 10);
|
|
14
|
+
const leaseMs = Math.max(options.leaseMs ?? 30_000, 1_000);
|
|
15
|
+
const maxJobsPerTick = Math.max(options.maxJobsPerTick ?? concurrency, 1);
|
|
16
|
+
let running = false;
|
|
17
|
+
let timer;
|
|
18
|
+
let ticking = false;
|
|
19
|
+
return {
|
|
20
|
+
get running() {
|
|
21
|
+
return running;
|
|
22
|
+
},
|
|
23
|
+
async runOnce() {
|
|
24
|
+
if (ticking) {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
ticking = true;
|
|
28
|
+
try {
|
|
29
|
+
await driver.materializeDueSchedules(Date.now(), leaseMs);
|
|
30
|
+
let processed = 0;
|
|
31
|
+
while (processed < maxJobsPerTick) {
|
|
32
|
+
const batch = await Promise.all(Array.from({ length: Math.min(concurrency, maxJobsPerTick - processed) }, () => processNext()));
|
|
33
|
+
const completed = batch.reduce((count, value) => count + value, 0);
|
|
34
|
+
processed += completed;
|
|
35
|
+
if (completed === 0) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return processed;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
ticking = false;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async start() {
|
|
46
|
+
if (running) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
running = true;
|
|
50
|
+
const loop = async () => {
|
|
51
|
+
if (!running) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await this.runOnce();
|
|
55
|
+
timer = setTimeout(loop, pollIntervalMs);
|
|
56
|
+
};
|
|
57
|
+
await loop();
|
|
58
|
+
},
|
|
59
|
+
async stop() {
|
|
60
|
+
running = false;
|
|
61
|
+
if (timer) {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
timer = undefined;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
async function processNext() {
|
|
68
|
+
const acquired = await driver.acquireNextReady(Date.now(), leaseMs);
|
|
69
|
+
if (!acquired) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
const job = internals.jobsByName.get(acquired.record.name);
|
|
73
|
+
if (!job) {
|
|
74
|
+
throw new FlowliStrategyError(`No registered job named "${acquired.record.name}" found for runner execution.`);
|
|
75
|
+
}
|
|
76
|
+
const heartbeat = setInterval(() => {
|
|
77
|
+
void driver.renewLease(acquired.record.id, acquired.token, leaseMs);
|
|
78
|
+
}, Math.max(Math.floor(leaseMs / 2), 250));
|
|
79
|
+
try {
|
|
80
|
+
await options.hooks?.onJobStarted?.(acquired.record.id, acquired.record.name);
|
|
81
|
+
await executePersistedJob(job, options.flowli, acquired.record.input, acquired.record.meta);
|
|
82
|
+
clearInterval(heartbeat);
|
|
83
|
+
await driver.markCompleted(acquired, Date.now());
|
|
84
|
+
await options.hooks?.onJobCompleted?.(acquired.record.id, acquired.record.name);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
clearInterval(heartbeat);
|
|
89
|
+
const serialized = createPersistedJobError(error);
|
|
90
|
+
await driver.markFailed(acquired, Date.now(), serialized);
|
|
91
|
+
await options.hooks?.onJobFailed?.(acquired.record.id, acquired.record.name, serialized);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function executePersistedJob(job, flowli, input, meta) {
|
|
97
|
+
const internals = getFlowliRuntimeInternals(flowli);
|
|
98
|
+
const validatedInput = await validateWithSchema(job.input, input, `${job.name} input`);
|
|
99
|
+
const validatedMeta = job.meta
|
|
100
|
+
? await validateWithSchema(job.meta, meta, `${job.name} meta`)
|
|
101
|
+
: undefined;
|
|
102
|
+
const context = await internals.context();
|
|
103
|
+
await invokeHandler(job, validatedInput, context, validatedMeta);
|
|
104
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FlowliContextRecord, FlowliRuntime, JobsRecord, PersistedJobError } from "../core/types.js";
|
|
2
|
+
export interface RunnerHooks {
|
|
3
|
+
readonly onJobStarted?: (jobId: string, jobName: string) => void | Promise<void>;
|
|
4
|
+
readonly onJobCompleted?: (jobId: string, jobName: string) => void | Promise<void>;
|
|
5
|
+
readonly onJobFailed?: (jobId: string, jobName: string, error: PersistedJobError) => void | Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface RunnerOptions<TJobs extends JobsRecord, TContext extends FlowliContextRecord> {
|
|
8
|
+
readonly flowli: FlowliRuntime<TJobs, TContext>;
|
|
9
|
+
readonly concurrency?: number;
|
|
10
|
+
readonly pollIntervalMs?: number;
|
|
11
|
+
readonly leaseMs?: number;
|
|
12
|
+
readonly maxJobsPerTick?: number;
|
|
13
|
+
readonly hooks?: RunnerHooks;
|
|
14
|
+
}
|
|
15
|
+
export interface FlowliRunner {
|
|
16
|
+
readonly running: boolean;
|
|
17
|
+
runOnce(): Promise<number>;
|
|
18
|
+
start(): Promise<void>;
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/runner.d.ts
ADDED
package/dist/runner.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { AnyJobDefinition, FlowliContextRecord, FlowliJobSurface, FlowliRuntimeInternals } from "../core/types.js";
|
|
2
|
+
export declare function createJobSurface<TJob extends AnyJobDefinition, TContext extends FlowliContextRecord>(job: TJob, internals: FlowliRuntimeInternals<Record<string, TJob>, TContext>): FlowliJobSurface<TJob>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { delayStrategy } from "../strategies/delay.js";
|
|
2
|
+
import { enqueueStrategy } from "../strategies/enqueue.js";
|
|
3
|
+
import { runStrategy } from "../strategies/run.js";
|
|
4
|
+
import { scheduleStrategy } from "../strategies/schedule.js";
|
|
5
|
+
export function createJobSurface(job, internals) {
|
|
6
|
+
const defaults = mergeDefaults(internals.defaults, job.defaults);
|
|
7
|
+
return {
|
|
8
|
+
run: (input, options) => runStrategy(job, internals, defaults, input, options?.meta),
|
|
9
|
+
enqueue: (input, options) => enqueueStrategy(job, internals, defaults, Date.now(), input, options),
|
|
10
|
+
delay: (delay, input, options) => delayStrategy(job, internals, defaults, Date.now(), delay, input, options),
|
|
11
|
+
schedule: (invocation) => scheduleStrategy(job, internals, defaults, Date.now(), invocation),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function mergeDefaults(globalDefaults, jobDefaults) {
|
|
15
|
+
return {
|
|
16
|
+
...(jobDefaults?.maxAttempts !== undefined ||
|
|
17
|
+
globalDefaults.maxAttempts !== undefined
|
|
18
|
+
? { maxAttempts: jobDefaults?.maxAttempts ?? globalDefaults.maxAttempts }
|
|
19
|
+
: {}),
|
|
20
|
+
...((jobDefaults?.backoff ?? globalDefaults.backoff)
|
|
21
|
+
? { backoff: jobDefaults?.backoff ?? globalDefaults.backoff }
|
|
22
|
+
: {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { AnyJobDefinition, FlowliContextRecord, JobMeta, JobResult } from "../core/types.js";
|
|
2
|
+
export declare function invokeHandler<TJob extends AnyJobDefinition, TContext extends FlowliContextRecord>(job: TJob, input: unknown, context: TContext, meta: JobMeta<TJob>): Promise<JobResult<TJob>>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FlowliDefinitionError } from "../core/errors.js";
|
|
2
|
+
export function normalizeJobs(jobs) {
|
|
3
|
+
const jobsByName = new Map();
|
|
4
|
+
for (const [exportName, definition] of Object.entries(jobs)) {
|
|
5
|
+
if (!definition || definition.__flowli !== "job") {
|
|
6
|
+
throw new FlowliDefinitionError(`Expected "${exportName}" to be a Flowli job definition.`);
|
|
7
|
+
}
|
|
8
|
+
if (jobsByName.has(definition.name)) {
|
|
9
|
+
throw new FlowliDefinitionError(`Duplicate job name "${definition.name}" found in registry.`);
|
|
10
|
+
}
|
|
11
|
+
jobsByName.set(definition.name, definition);
|
|
12
|
+
}
|
|
13
|
+
return { jobs, jobsByName };
|
|
14
|
+
}
|