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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +815 -0
  3. package/dist/bun-redis.d.ts +1 -0
  4. package/dist/bun-redis.js +3 -0
  5. package/dist/core/define-jobs.d.ts +8 -0
  6. package/dist/core/define-jobs.js +47 -0
  7. package/dist/core/errors.d.ts +21 -0
  8. package/dist/core/errors.js +35 -0
  9. package/dist/core/job.d.ts +9 -0
  10. package/dist/core/job.js +20 -0
  11. package/dist/core/types.d.ts +175 -0
  12. package/dist/core/types.js +1 -0
  13. package/dist/driver/duration.d.ts +2 -0
  14. package/dist/driver/duration.js +23 -0
  15. package/dist/driver/encoding.d.ts +2 -0
  16. package/dist/driver/encoding.js +9 -0
  17. package/dist/driver/keys.d.ts +15 -0
  18. package/dist/driver/keys.js +14 -0
  19. package/dist/driver/records.d.ts +25 -0
  20. package/dist/driver/records.js +61 -0
  21. package/dist/driver/scheduling.d.ts +6 -0
  22. package/dist/driver/scheduling.js +127 -0
  23. package/dist/drivers/bun-redis.d.ts +24 -0
  24. package/dist/drivers/bun-redis.js +21 -0
  25. package/dist/drivers/ioredis.d.ts +16 -0
  26. package/dist/drivers/ioredis.js +31 -0
  27. package/dist/drivers/redis.d.ts +27 -0
  28. package/dist/drivers/redis.js +23 -0
  29. package/dist/drivers/shared.d.ts +21 -0
  30. package/dist/drivers/shared.js +172 -0
  31. package/dist/hono.d.ts +1 -0
  32. package/dist/hono.js +3 -0
  33. package/dist/index.d.ts +4 -0
  34. package/dist/index.js +11 -0
  35. package/dist/integrations/hono.d.ts +10 -0
  36. package/dist/integrations/hono.js +7 -0
  37. package/dist/integrations/next.d.ts +18 -0
  38. package/dist/integrations/tanstack-start.d.ts +20 -0
  39. package/dist/ioredis.d.ts +1 -0
  40. package/dist/ioredis.js +3 -0
  41. package/dist/next.d.ts +1 -0
  42. package/dist/next.js +4 -0
  43. package/dist/redis.d.ts +1 -0
  44. package/dist/redis.js +3 -0
  45. package/dist/runner/create-runner.d.ts +3 -0
  46. package/dist/runner/create-runner.js +104 -0
  47. package/dist/runner/types.d.ts +20 -0
  48. package/dist/runner/types.js +1 -0
  49. package/dist/runner.d.ts +2 -0
  50. package/dist/runner.js +3 -0
  51. package/dist/runtime/create-job-surface.d.ts +2 -0
  52. package/dist/runtime/create-job-surface.js +24 -0
  53. package/dist/runtime/invoke-handler.d.ts +2 -0
  54. package/dist/runtime/invoke-handler.js +7 -0
  55. package/dist/runtime/normalize-jobs.d.ts +5 -0
  56. package/dist/runtime/normalize-jobs.js +14 -0
  57. package/dist/runtime/resolve-context.d.ts +2 -0
  58. package/dist/runtime/resolve-context.js +6 -0
  59. package/dist/runtime/validate.d.ts +2 -0
  60. package/dist/runtime/validate.js +15 -0
  61. package/dist/strategies/delay.d.ts +2 -0
  62. package/dist/strategies/delay.js +32 -0
  63. package/dist/strategies/enqueue.d.ts +2 -0
  64. package/dist/strategies/enqueue.js +30 -0
  65. package/dist/strategies/run.d.ts +2 -0
  66. package/dist/strategies/run.js +10 -0
  67. package/dist/strategies/schedule.d.ts +2 -0
  68. package/dist/strategies/schedule.js +33 -0
  69. package/dist/tanstack-start.d.ts +1 -0
  70. package/dist/tanstack-start.js +4 -0
  71. package/jsr.json +26 -0
  72. package/package.json +135 -0
@@ -0,0 +1 @@
1
+ export { type BunRedisDriverOptions, type BunRedisLikeClient, bunRedisDriver, } from "./drivers/bun-redis.js";
@@ -0,0 +1,3 @@
1
+ export {
2
+ bunRedisDriver
3
+ };
@@ -0,0 +1,8 @@
1
+ import type { DefineJobsFactoryOptions, DefineJobsOptions, EnsureJobContexts, FlowliContextRecord, FlowliRuntime, FlowliRuntimeInternals, JobsRecord } from "./types.js";
2
+ type DefineJobsFunction = {
3
+ <const TJobs extends JobsRecord, TContext extends FlowliContextRecord>(options: DefineJobsFactoryOptions<TJobs, TContext>): FlowliRuntime<TJobs, TContext>;
4
+ withContext<TContext extends FlowliContextRecord>(): <const TJobs extends JobsRecord>(options: DefineJobsOptions<EnsureJobContexts<TJobs, TContext>, TContext>) => FlowliRuntime<EnsureJobContexts<TJobs, TContext>, TContext>;
5
+ };
6
+ export declare const defineJobs: DefineJobsFunction;
7
+ export declare function getFlowliRuntimeInternals<TJobs extends JobsRecord, TContext extends FlowliContextRecord>(runtime: FlowliRuntime<TJobs, TContext>): FlowliRuntimeInternals<TJobs, TContext>;
8
+ export {};
@@ -0,0 +1,47 @@
1
+ import { createJobSurface } from "../runtime/create-job-surface.js";
2
+ import { normalizeJobs } from "../runtime/normalize-jobs.js";
3
+ import { createContextResolver } from "../runtime/resolve-context.js";
4
+ import { createContextualJobFactory } from "./job.js";
5
+ import { FLOWLI_RUNTIME_SYMBOL as runtimeSymbol } from "./types.js";
6
+ function defineJobsFromFactory(options) {
7
+ const resolvedJobs = options.jobs({
8
+ job: createContextualJobFactory(),
9
+ });
10
+ return buildRuntime(resolvedJobs, options.context, options.driver, options.defaults);
11
+ }
12
+ function defineJobsFromObject(options) {
13
+ return buildRuntime(options.jobs, options.context, options.driver, options.defaults);
14
+ }
15
+ function buildRuntime(jobs, context, driver, defaultsInput) {
16
+ const normalized = normalizeJobs(jobs);
17
+ const defaults = {
18
+ maxAttempts: defaultsInput?.maxAttempts ?? 1,
19
+ ...(defaultsInput?.backoff ? { backoff: defaultsInput.backoff } : {}),
20
+ };
21
+ const internals = {
22
+ jobs: normalized.jobs,
23
+ jobsByName: normalized.jobsByName,
24
+ context: createContextResolver(context),
25
+ ...(driver ? { driver } : {}),
26
+ defaults,
27
+ };
28
+ const runtime = {};
29
+ for (const [exportName, definition] of Object.entries(normalized.jobs)) {
30
+ runtime[exportName] = createJobSurface(definition, internals);
31
+ }
32
+ Object.defineProperty(runtime, runtimeSymbol, {
33
+ enumerable: false,
34
+ configurable: false,
35
+ writable: false,
36
+ value: internals,
37
+ });
38
+ return runtime;
39
+ }
40
+ export const defineJobs = Object.assign(defineJobsFromFactory, {
41
+ withContext() {
42
+ return (options) => defineJobsFromObject(options);
43
+ },
44
+ });
45
+ export function getFlowliRuntimeInternals(runtime) {
46
+ return runtime[runtimeSymbol];
47
+ }
@@ -0,0 +1,21 @@
1
+ import type { StandardSchemaIssue } from "./types.js";
2
+ export declare class FlowliError extends Error {
3
+ readonly code: string;
4
+ constructor(code: string, message: string);
5
+ }
6
+ export declare class FlowliDefinitionError extends FlowliError {
7
+ constructor(message: string);
8
+ }
9
+ export declare class FlowliValidationError extends FlowliError {
10
+ readonly issues: ReadonlyArray<StandardSchemaIssue>;
11
+ constructor(message: string, issues: ReadonlyArray<StandardSchemaIssue>);
12
+ }
13
+ export declare class FlowliStrategyError extends FlowliError {
14
+ constructor(message: string);
15
+ }
16
+ export declare class FlowliDriverError extends FlowliError {
17
+ constructor(message: string);
18
+ }
19
+ export declare class FlowliSchedulingError extends FlowliError {
20
+ constructor(message: string);
21
+ }
@@ -0,0 +1,35 @@
1
+ export class FlowliError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.name = new.target.name;
6
+ this.code = code;
7
+ }
8
+ }
9
+ export class FlowliDefinitionError extends FlowliError {
10
+ constructor(message) {
11
+ super("FLOWLI_DEFINITION_ERROR", message);
12
+ }
13
+ }
14
+ export class FlowliValidationError extends FlowliError {
15
+ issues;
16
+ constructor(message, issues) {
17
+ super("FLOWLI_VALIDATION_ERROR", message);
18
+ this.issues = issues;
19
+ }
20
+ }
21
+ export class FlowliStrategyError extends FlowliError {
22
+ constructor(message) {
23
+ super("FLOWLI_STRATEGY_ERROR", message);
24
+ }
25
+ }
26
+ export class FlowliDriverError extends FlowliError {
27
+ constructor(message) {
28
+ super("FLOWLI_DRIVER_ERROR", message);
29
+ }
30
+ }
31
+ export class FlowliSchedulingError extends FlowliError {
32
+ constructor(message) {
33
+ super("FLOWLI_SCHEDULING_ERROR", message);
34
+ }
35
+ }
@@ -0,0 +1,9 @@
1
+ import type { FlowliContextRecord, JobDefinition, JobOptions, StandardSchemaV1 } from "./types.js";
2
+ type JobFactory = {
3
+ <TInputSchema extends StandardSchemaV1<any, any>, TMetaSchema extends StandardSchemaV1<any, any> | undefined, TResult>(name: string, options: JobOptions<TInputSchema, TMetaSchema, FlowliContextRecord, TResult>): JobDefinition<TInputSchema, TMetaSchema, FlowliContextRecord, TResult>;
4
+ withContext<TContext extends FlowliContextRecord>(): <TInputSchema extends StandardSchemaV1<any, any>, TMetaSchema extends StandardSchemaV1<any, any> | undefined, TResult>(name: string, options: JobOptions<TInputSchema, TMetaSchema, TContext, TResult>) => JobDefinition<TInputSchema, TMetaSchema, TContext, TResult>;
5
+ };
6
+ export type ContextualJobFactory<TContext extends FlowliContextRecord> = <TInputSchema extends StandardSchemaV1<any, any>, TMetaSchema extends StandardSchemaV1<any, any> | undefined, TResult>(name: string, options: JobOptions<TInputSchema, TMetaSchema, TContext, TResult>) => JobDefinition<TInputSchema, TMetaSchema, TContext, TResult>;
7
+ export declare const job: JobFactory;
8
+ export declare function createContextualJobFactory<TContext extends FlowliContextRecord>(): ContextualJobFactory<TContext>;
9
+ export {};
@@ -0,0 +1,20 @@
1
+ function createJob(name, options) {
2
+ return {
3
+ __flowli: "job",
4
+ name,
5
+ input: options.input,
6
+ handler: options.handler,
7
+ ...(options.meta ? { meta: options.meta } : {}),
8
+ ...(options.defaults ? { defaults: options.defaults } : {}),
9
+ ...(options.description ? { description: options.description } : {}),
10
+ ...(options.tags ? { tags: options.tags } : {}),
11
+ };
12
+ }
13
+ export const job = Object.assign(createJob, {
14
+ withContext() {
15
+ return (name, options) => createJob(name, options);
16
+ },
17
+ });
18
+ export function createContextualJobFactory() {
19
+ return job.withContext();
20
+ }
@@ -0,0 +1,175 @@
1
+ export interface StandardSchemaIssue {
2
+ message: string;
3
+ path?: ReadonlyArray<unknown>;
4
+ }
5
+ export interface StandardSchemaSuccess<TValue> {
6
+ readonly value: TValue;
7
+ }
8
+ export interface StandardSchemaFailure {
9
+ readonly issues: ReadonlyArray<StandardSchemaIssue>;
10
+ }
11
+ export type StandardSchemaResult<TValue> = StandardSchemaSuccess<TValue> | StandardSchemaFailure;
12
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
13
+ readonly "~standard": {
14
+ readonly version: number;
15
+ readonly vendor?: string;
16
+ readonly validate: (...args: any[]) => unknown | Promise<unknown>;
17
+ readonly types?: {
18
+ readonly input: Input;
19
+ readonly output: Output;
20
+ } | undefined;
21
+ };
22
+ }
23
+ export type InferInput<TSchema extends StandardSchemaV1<any, any>> = TSchema extends StandardSchemaV1<infer TInput, any> ? TInput : never;
24
+ export type InferOutput<TSchema extends StandardSchemaV1<any, any>> = TSchema extends StandardSchemaV1<any, infer TOutput> ? TOutput : never;
25
+ export interface FlowliContextRecord {
26
+ readonly [key: PropertyKey]: unknown;
27
+ }
28
+ export type FlowliContextResolver<TContext extends FlowliContextRecord> = TContext | (() => TContext | Promise<TContext>);
29
+ export type ResolveContext<TResolver> = TResolver extends () => infer TResult ? Awaited<TResult> : TResolver extends FlowliContextRecord ? TResolver : never;
30
+ export interface JobDefaults {
31
+ readonly maxAttempts?: number;
32
+ readonly backoff?: BackoffOptions;
33
+ }
34
+ export interface BackoffOptions {
35
+ readonly type: "fixed" | "exponential";
36
+ readonly delayMs: number;
37
+ }
38
+ export interface FlowliInvocationOptions<TMeta> {
39
+ readonly meta?: TMeta;
40
+ }
41
+ export interface PersistedInvocationOptions<TMeta> extends FlowliInvocationOptions<TMeta>, JobDefaults {
42
+ }
43
+ export interface ScheduleInvocation<TInput, TMeta> {
44
+ readonly key?: string;
45
+ readonly cron: string;
46
+ readonly input: TInput;
47
+ readonly meta?: TMeta;
48
+ }
49
+ export type DelayValue = number | `${number}${"ms" | "s" | "m" | "h" | "d"}`;
50
+ export interface JobReceipt {
51
+ readonly id: string;
52
+ readonly name: string;
53
+ readonly state: JobState;
54
+ readonly scheduledFor: number;
55
+ readonly attemptsMade: number;
56
+ }
57
+ export interface ScheduleReceipt {
58
+ readonly key: string;
59
+ readonly name: string;
60
+ readonly cron: string;
61
+ readonly nextRunAt: number;
62
+ }
63
+ export type JobState = "queued" | "active" | "completed" | "failed" | "scheduled";
64
+ export interface JobHandlerArgs<TInput, TContext, TMeta> {
65
+ readonly input: TInput;
66
+ readonly ctx: TContext;
67
+ readonly meta: TMeta | undefined;
68
+ }
69
+ export interface JobDefinition<TInputSchema extends StandardSchemaV1<any, any>, TMetaSchema extends StandardSchemaV1<any, any> | undefined, TContext extends FlowliContextRecord, TResult> {
70
+ readonly __flowli: "job";
71
+ readonly name: string;
72
+ readonly input: TInputSchema;
73
+ readonly meta?: TMetaSchema;
74
+ readonly handler: (args: JobHandlerArgs<InferOutput<TInputSchema>, TContext, TMetaSchema extends StandardSchemaV1<any, any> ? InferOutput<TMetaSchema> : undefined>) => TResult | Promise<TResult>;
75
+ readonly defaults?: JobDefaults;
76
+ readonly description?: string;
77
+ readonly tags?: ReadonlyArray<string>;
78
+ }
79
+ export interface JobOptions<TInputSchema extends StandardSchemaV1<any, any>, TMetaSchema extends StandardSchemaV1<any, any> | undefined, TContext extends FlowliContextRecord, TResult> {
80
+ readonly input: TInputSchema;
81
+ readonly meta?: TMetaSchema;
82
+ readonly handler: JobDefinition<TInputSchema, TMetaSchema, TContext, TResult>["handler"];
83
+ readonly defaults?: JobDefaults;
84
+ readonly description?: string;
85
+ readonly tags?: ReadonlyArray<string>;
86
+ }
87
+ export type AnyJobDefinition = JobDefinition<StandardSchemaV1<any, any>, StandardSchemaV1<any, any> | undefined, any, unknown>;
88
+ export type JobsRecord = {
89
+ readonly [key: string]: AnyJobDefinition;
90
+ };
91
+ export type JobInput<TJob extends AnyJobDefinition> = InferOutput<TJob["input"]>;
92
+ export type JobMeta<TJob extends AnyJobDefinition> = NonNullable<TJob["meta"]> extends StandardSchemaV1<any, any> ? InferOutput<NonNullable<TJob["meta"]>> : undefined;
93
+ export type JobResult<TJob extends AnyJobDefinition> = Awaited<ReturnType<TJob["handler"]>>;
94
+ export interface FlowliDriver {
95
+ readonly kind: string;
96
+ enqueue(record: PersistedJobRecord): Promise<JobReceipt>;
97
+ registerSchedule(record: ScheduleRecord): Promise<ScheduleReceipt>;
98
+ recoverExpiredLeases(now: number): Promise<number>;
99
+ acquireNextReady(now: number, leaseMs: number): Promise<AcquiredJobRecord | null>;
100
+ renewLease(jobId: string, token: string, leaseMs: number): Promise<boolean>;
101
+ markCompleted(acquired: AcquiredJobRecord, finishedAt: number): Promise<void>;
102
+ markFailed(acquired: AcquiredJobRecord, finishedAt: number, error: PersistedJobError): Promise<"failed" | "retrying">;
103
+ materializeDueSchedules(now: number, leaseMs: number): Promise<number>;
104
+ }
105
+ export interface DefineJobsOptions<TJobs extends JobsRecord, TContext extends FlowliContextRecord> {
106
+ readonly jobs: TJobs;
107
+ readonly context: FlowliContextResolver<TContext>;
108
+ readonly driver?: FlowliDriver;
109
+ readonly defaults?: JobDefaults;
110
+ }
111
+ export interface DefineJobsBuilder<TContext extends FlowliContextRecord> {
112
+ readonly job: <TInputSchema extends StandardSchemaV1<any, any>, TMetaSchema extends StandardSchemaV1<any, any> | undefined, TResult>(name: string, options: JobOptions<TInputSchema, TMetaSchema, TContext, TResult>) => JobDefinition<TInputSchema, TMetaSchema, TContext, TResult>;
113
+ }
114
+ export interface DefineJobsFactoryOptions<TJobs extends JobsRecord, TContext extends FlowliContextRecord> {
115
+ readonly jobs: (builder: DefineJobsBuilder<TContext>) => TJobs;
116
+ readonly context: FlowliContextResolver<TContext>;
117
+ readonly driver?: FlowliDriver;
118
+ readonly defaults?: JobDefaults;
119
+ }
120
+ export type EnsureJobContexts<TJobs extends JobsRecord, TContext extends FlowliContextRecord> = {
121
+ readonly [TKey in keyof TJobs]: TJobs[TKey] extends JobDefinition<StandardSchemaV1<any, any>, StandardSchemaV1<any, any> | undefined, infer TJobContext, unknown> ? TContext extends TJobContext ? TJobs[TKey] : never : never;
122
+ };
123
+ export interface FlowliJobSurface<TJob extends AnyJobDefinition> {
124
+ run(input: JobInput<TJob>, options?: FlowliInvocationOptions<JobMeta<TJob>>): Promise<JobResult<TJob>>;
125
+ enqueue(input: JobInput<TJob>, options?: PersistedInvocationOptions<JobMeta<TJob>>): Promise<JobReceipt>;
126
+ delay(delay: DelayValue, input: JobInput<TJob>, options?: PersistedInvocationOptions<JobMeta<TJob>>): Promise<JobReceipt>;
127
+ schedule(invocation: ScheduleInvocation<JobInput<TJob>, JobMeta<TJob>>): Promise<ScheduleReceipt>;
128
+ }
129
+ export type FlowliRuntime<TJobs extends JobsRecord, TContext extends FlowliContextRecord> = {
130
+ readonly [TKey in keyof TJobs]: FlowliJobSurface<TJobs[TKey]>;
131
+ } & {
132
+ readonly [FLOWLI_RUNTIME_SYMBOL]: FlowliRuntimeInternals<TJobs, TContext>;
133
+ };
134
+ export interface FlowliRuntimeInternals<TJobs extends JobsRecord, TContext extends FlowliContextRecord> {
135
+ readonly jobs: TJobs;
136
+ readonly jobsByName: Map<string, AnyJobDefinition>;
137
+ readonly context: () => Promise<TContext>;
138
+ readonly driver?: FlowliDriver;
139
+ readonly defaults: JobDefaults;
140
+ }
141
+ export declare const FLOWLI_RUNTIME_SYMBOL: unique symbol;
142
+ export interface PersistedJobError {
143
+ readonly code: string;
144
+ readonly message: string;
145
+ }
146
+ export interface PersistedJobRecord {
147
+ readonly id: string;
148
+ readonly name: string;
149
+ readonly input: unknown;
150
+ readonly meta?: unknown;
151
+ readonly state: JobState;
152
+ readonly createdAt: number;
153
+ readonly updatedAt: number;
154
+ readonly scheduledFor: number;
155
+ readonly attemptsMade: number;
156
+ readonly maxAttempts: number;
157
+ readonly backoff?: BackoffOptions;
158
+ readonly lastError?: PersistedJobError;
159
+ }
160
+ export interface AcquiredJobRecord {
161
+ readonly token: string;
162
+ readonly record: PersistedJobRecord;
163
+ }
164
+ export interface ScheduleRecord {
165
+ readonly key: string;
166
+ readonly name: string;
167
+ readonly cron: string;
168
+ readonly input: unknown;
169
+ readonly meta?: unknown;
170
+ readonly maxAttempts: number;
171
+ readonly backoff?: BackoffOptions;
172
+ readonly createdAt: number;
173
+ readonly updatedAt: number;
174
+ readonly nextRunAt: number;
175
+ }
@@ -0,0 +1 @@
1
+ export const FLOWLI_RUNTIME_SYMBOL = Symbol.for("flowli.runtime");
@@ -0,0 +1,2 @@
1
+ import type { DelayValue } from "../core/types.js";
2
+ export declare function parseDelay(delay: DelayValue): number;
@@ -0,0 +1,23 @@
1
+ import { FlowliStrategyError } from "../core/errors.js";
2
+ const DURATION_UNITS = {
3
+ ms: 1,
4
+ s: 1_000,
5
+ m: 60_000,
6
+ h: 3_600_000,
7
+ d: 86_400_000,
8
+ };
9
+ export function parseDelay(delay) {
10
+ if (typeof delay === "number") {
11
+ if (!Number.isFinite(delay) || delay < 0) {
12
+ throw new FlowliStrategyError("Delay must be a non-negative finite number.");
13
+ }
14
+ return delay;
15
+ }
16
+ const match = /^(\d+)(ms|s|m|h|d)$/.exec(delay);
17
+ if (!match) {
18
+ throw new FlowliStrategyError(`Invalid delay value "${delay}". Expected a number or a duration string like "5m".`);
19
+ }
20
+ const value = Number(match[1]);
21
+ const unit = match[2];
22
+ return value * DURATION_UNITS[unit];
23
+ }
@@ -0,0 +1,2 @@
1
+ export declare function encodeJson(value: unknown): string;
2
+ export declare function decodeJson<TValue>(value: string | null): TValue | null;
@@ -0,0 +1,9 @@
1
+ export function encodeJson(value) {
2
+ return JSON.stringify(value);
3
+ }
4
+ export function decodeJson(value) {
5
+ if (value === null) {
6
+ return null;
7
+ }
8
+ return JSON.parse(value);
9
+ }
@@ -0,0 +1,15 @@
1
+ export interface FlowliRedisKeyOptions {
2
+ readonly prefix?: string;
3
+ }
4
+ export interface FlowliRedisKeys {
5
+ readonly job: (id: string) => string;
6
+ readonly pending: string;
7
+ readonly active: string;
8
+ readonly completed: string;
9
+ readonly failed: string;
10
+ readonly schedule: (key: string) => string;
11
+ readonly schedulesDue: string;
12
+ readonly lease: (id: string) => string;
13
+ readonly scheduleLease: (key: string) => string;
14
+ }
15
+ export declare function createRedisKeys(options?: FlowliRedisKeyOptions): FlowliRedisKeys;
@@ -0,0 +1,14 @@
1
+ export function createRedisKeys(options = {}) {
2
+ const base = `flowli:${options.prefix ?? "default"}`;
3
+ return {
4
+ job: (id) => `${base}:job:${id}`,
5
+ pending: `${base}:queue:pending`,
6
+ active: `${base}:queue:active`,
7
+ completed: `${base}:queue:completed`,
8
+ failed: `${base}:queue:failed`,
9
+ schedule: (key) => `${base}:schedule:${key}`,
10
+ schedulesDue: `${base}:schedule:due`,
11
+ lease: (id) => `${base}:lease:${id}`,
12
+ scheduleLease: (key) => `${base}:lease:schedule:${key}`,
13
+ };
14
+ }
@@ -0,0 +1,25 @@
1
+ import type { BackoffOptions, JobReceipt, PersistedJobError, PersistedJobRecord, ScheduleReceipt, ScheduleRecord } from "../core/types.js";
2
+ export declare function createPersistedJobRecord(options: {
3
+ id: string;
4
+ name: string;
5
+ input: unknown;
6
+ meta?: unknown;
7
+ scheduledFor: number;
8
+ maxAttempts: number;
9
+ backoff?: BackoffOptions;
10
+ now: number;
11
+ }): PersistedJobRecord;
12
+ export declare function createJobReceipt(record: PersistedJobRecord): JobReceipt;
13
+ export declare function createScheduleRecord(options: {
14
+ key: string;
15
+ name: string;
16
+ cron: string;
17
+ input: unknown;
18
+ meta?: unknown;
19
+ maxAttempts: number;
20
+ backoff?: BackoffOptions;
21
+ nextRunAt: number;
22
+ now: number;
23
+ }): ScheduleRecord;
24
+ export declare function createScheduleReceipt(record: ScheduleRecord): ScheduleReceipt;
25
+ export declare function createPersistedJobError(error: unknown): PersistedJobError;
@@ -0,0 +1,61 @@
1
+ export function createPersistedJobRecord(options) {
2
+ return {
3
+ id: options.id,
4
+ name: options.name,
5
+ input: options.input,
6
+ state: "queued",
7
+ createdAt: options.now,
8
+ updatedAt: options.now,
9
+ scheduledFor: options.scheduledFor,
10
+ attemptsMade: 0,
11
+ maxAttempts: options.maxAttempts,
12
+ ...(options.meta !== undefined ? { meta: options.meta } : {}),
13
+ ...(options.backoff ? { backoff: options.backoff } : {}),
14
+ };
15
+ }
16
+ export function createJobReceipt(record) {
17
+ return {
18
+ id: record.id,
19
+ name: record.name,
20
+ state: record.state,
21
+ scheduledFor: record.scheduledFor,
22
+ attemptsMade: record.attemptsMade,
23
+ };
24
+ }
25
+ export function createScheduleRecord(options) {
26
+ return {
27
+ key: options.key,
28
+ name: options.name,
29
+ cron: options.cron,
30
+ input: options.input,
31
+ maxAttempts: options.maxAttempts,
32
+ createdAt: options.now,
33
+ updatedAt: options.now,
34
+ nextRunAt: options.nextRunAt,
35
+ ...(options.meta !== undefined ? { meta: options.meta } : {}),
36
+ ...(options.backoff ? { backoff: options.backoff } : {}),
37
+ };
38
+ }
39
+ export function createScheduleReceipt(record) {
40
+ return {
41
+ key: record.key,
42
+ name: record.name,
43
+ cron: record.cron,
44
+ nextRunAt: record.nextRunAt,
45
+ };
46
+ }
47
+ export function createPersistedJobError(error) {
48
+ if (error instanceof Error) {
49
+ const code = "code" in error && typeof error.code === "string"
50
+ ? error.code
51
+ : "FLOWLI_HANDLER_ERROR";
52
+ return {
53
+ code,
54
+ message: error.message,
55
+ };
56
+ }
57
+ return {
58
+ code: "FLOWLI_HANDLER_ERROR",
59
+ message: typeof error === "string" ? error : "Unknown handler error",
60
+ };
61
+ }
@@ -0,0 +1,6 @@
1
+ export declare function validateCron(cron: string): void;
2
+ export declare function getNextCronRun(cron: string, from: number): number;
3
+ export declare function deriveScheduleKey(name: string, cron: string, input: unknown): string;
4
+ export declare function createJobId(): string;
5
+ export declare function createLeaseToken(): string;
6
+ export declare function stableStringify(value: unknown): string;
@@ -0,0 +1,127 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { FlowliSchedulingError } from "../core/errors.js";
3
+ const FIELD_RANGES = [
4
+ { min: 0, max: 59 },
5
+ { min: 0, max: 23 },
6
+ { min: 1, max: 31 },
7
+ { min: 1, max: 12 },
8
+ { min: 0, max: 6 },
9
+ ];
10
+ export function validateCron(cron) {
11
+ parseCron(cron);
12
+ }
13
+ export function getNextCronRun(cron, from) {
14
+ const parsed = parseCron(cron);
15
+ const candidate = new Date(from);
16
+ candidate.setUTCSeconds(0, 0);
17
+ candidate.setUTCMinutes(candidate.getUTCMinutes() + 1);
18
+ for (let offset = 0; offset < 366 * 24 * 60; offset += 1) {
19
+ if (parsed.minute.has(candidate.getUTCMinutes()) &&
20
+ parsed.hour.has(candidate.getUTCHours()) &&
21
+ parsed.dayOfMonth.has(candidate.getUTCDate()) &&
22
+ parsed.month.has(candidate.getUTCMonth() + 1) &&
23
+ parsed.dayOfWeek.has(candidate.getUTCDay())) {
24
+ return candidate.getTime();
25
+ }
26
+ candidate.setUTCMinutes(candidate.getUTCMinutes() + 1);
27
+ }
28
+ throw new FlowliSchedulingError(`Unable to resolve next UTC execution time for cron "${cron}".`);
29
+ }
30
+ export function deriveScheduleKey(name, cron, input) {
31
+ const hash = createHash("sha256");
32
+ hash.update(stableStringify({
33
+ name,
34
+ cron,
35
+ input,
36
+ }));
37
+ return `schedule_${hash.digest("hex").slice(0, 16)}`;
38
+ }
39
+ export function createJobId() {
40
+ return randomUUID();
41
+ }
42
+ export function createLeaseToken() {
43
+ return randomUUID();
44
+ }
45
+ export function stableStringify(value) {
46
+ return JSON.stringify(normalizeValue(value));
47
+ }
48
+ function normalizeValue(value) {
49
+ if (Array.isArray(value)) {
50
+ return value.map((item) => normalizeValue(item));
51
+ }
52
+ if (value && typeof value === "object" && value.constructor === Object) {
53
+ return Object.keys(value)
54
+ .sort()
55
+ .reduce((result, key) => {
56
+ result[key] = normalizeValue(value[key]);
57
+ return result;
58
+ }, {});
59
+ }
60
+ return value;
61
+ }
62
+ function parseCron(cron) {
63
+ const parts = cron.trim().split(/\s+/);
64
+ if (parts.length !== 5) {
65
+ throw new FlowliSchedulingError(`Invalid cron "${cron}". Expected exactly five UTC fields.`);
66
+ }
67
+ return {
68
+ minute: parseField(parts[0], FIELD_RANGES[0].min, FIELD_RANGES[0].max),
69
+ hour: parseField(parts[1], FIELD_RANGES[1].min, FIELD_RANGES[1].max),
70
+ dayOfMonth: parseField(parts[2], FIELD_RANGES[2].min, FIELD_RANGES[2].max),
71
+ month: parseField(parts[3], FIELD_RANGES[3].min, FIELD_RANGES[3].max),
72
+ dayOfWeek: parseField(parts[4], FIELD_RANGES[4].min, FIELD_RANGES[4].max),
73
+ };
74
+ }
75
+ function parseField(field, min, max) {
76
+ if (field === "*") {
77
+ return createRange(min, max);
78
+ }
79
+ const values = new Set();
80
+ for (const segment of field.split(",")) {
81
+ if (/^\d+$/.test(segment)) {
82
+ const value = Number(segment);
83
+ assertRange(value, min, max, field);
84
+ values.add(value);
85
+ continue;
86
+ }
87
+ const stepMatch = /^\*\/(\d+)$/.exec(segment);
88
+ if (stepMatch) {
89
+ const step = Number(stepMatch[1]);
90
+ if (step <= 0) {
91
+ throw new FlowliSchedulingError(`Invalid cron step "${segment}".`);
92
+ }
93
+ for (let value = min; value <= max; value += step) {
94
+ values.add(value);
95
+ }
96
+ continue;
97
+ }
98
+ const rangeMatch = /^(\d+)-(\d+)$/.exec(segment);
99
+ if (rangeMatch) {
100
+ const start = Number(rangeMatch[1]);
101
+ const end = Number(rangeMatch[2]);
102
+ assertRange(start, min, max, field);
103
+ assertRange(end, min, max, field);
104
+ if (start > end) {
105
+ throw new FlowliSchedulingError(`Invalid cron range "${segment}".`);
106
+ }
107
+ for (let value = start; value <= end; value += 1) {
108
+ values.add(value);
109
+ }
110
+ continue;
111
+ }
112
+ throw new FlowliSchedulingError(`Unsupported cron field "${segment}".`);
113
+ }
114
+ return values;
115
+ }
116
+ function createRange(min, max) {
117
+ const values = new Set();
118
+ for (let value = min; value <= max; value += 1) {
119
+ values.add(value);
120
+ }
121
+ return values;
122
+ }
123
+ function assertRange(value, min, max, field) {
124
+ if (value < min || value > max) {
125
+ throw new FlowliSchedulingError(`Value ${value} is out of range for cron field "${field}".`);
126
+ }
127
+ }
@@ -0,0 +1,24 @@
1
+ import type { FlowliDriver } from "../core/types.js";
2
+ import { type RedisCommandAdapter } from "./shared.js";
3
+ export interface BunRedisLikeClient {
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, options?: {
13
+ limit?: {
14
+ offset: number;
15
+ count: number;
16
+ };
17
+ }): Promise<string[]>;
18
+ }
19
+ export interface BunRedisDriverOptions {
20
+ readonly client: BunRedisLikeClient;
21
+ readonly prefix?: string;
22
+ }
23
+ export declare function bunRedisDriver(options: BunRedisDriverOptions): FlowliDriver;
24
+ export declare function createBunRedisAdapter(client: BunRedisLikeClient): RedisCommandAdapter;