runlater-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # runlater
2
+
3
+ Delayed tasks, cron jobs, and reliable webhooks for any Node.js app. No Redis. No infrastructure. Just HTTP.
4
+
5
+ [Documentation](https://runlater.eu/docs) | [Dashboard](https://runlater.eu)
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install runlater
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```js
16
+ import { Runlater } from "runlater"
17
+
18
+ const rl = new Runlater({ apiKey: process.env.RUNLATER_KEY })
19
+
20
+ // Fire-and-forget with retries
21
+ await rl.send("https://myapp.com/api/process-order", {
22
+ body: { orderId: 123 },
23
+ retries: 5,
24
+ })
25
+
26
+ // Run in 10 minutes
27
+ await rl.delay("https://myapp.com/api/send-reminder", {
28
+ delay: "10m",
29
+ body: { userId: 456 },
30
+ })
31
+
32
+ // Run at a specific time
33
+ await rl.schedule("https://myapp.com/api/trial-expired", {
34
+ at: "2026-03-15T09:00:00Z",
35
+ body: { userId: 789 },
36
+ })
37
+
38
+ // Recurring cron job
39
+ await rl.cron("daily-report", {
40
+ url: "https://myapp.com/api/report",
41
+ schedule: "0 9 * * *",
42
+ })
43
+ ```
44
+
45
+ ## Why Runlater?
46
+
47
+ - **No infrastructure** — no Redis, no SQS, no cron containers
48
+ - **Works everywhere** — Vercel, Netlify, Cloudflare Workers, Express, any Node.js app
49
+ - **EU-hosted** — GDPR-native, data never leaves Europe
50
+ - **Reliable** — automatic retries with exponential backoff
51
+ - **Observable** — execution history, status codes, and error logs in the dashboard
52
+
53
+ ## API
54
+
55
+ ### `rl.send(url, options?)`
56
+
57
+ Execute a request immediately with reliable delivery.
58
+
59
+ ```js
60
+ const result = await rl.send("https://myapp.com/api/webhook", {
61
+ method: "POST", // default: "POST"
62
+ headers: { "X-Custom": "value" },
63
+ body: { key: "value" }, // automatically JSON-serialized
64
+ retries: 5, // default: server default
65
+ timeout: 30000, // ms, default: 30000
66
+ queue: "emails", // optional: serialize execution within a queue
67
+ callback: "https://myapp.com/api/on-complete", // optional: receive result
68
+ })
69
+ // => { task_id, execution_id, status, scheduled_for }
70
+ ```
71
+
72
+ ### `rl.delay(url, options)`
73
+
74
+ Execute a request after a delay.
75
+
76
+ ```js
77
+ await rl.delay("https://myapp.com/api/remind", {
78
+ delay: "10m", // "30s", "5m", "2h", "1d", or seconds as number
79
+ body: { userId: 123 },
80
+ })
81
+ ```
82
+
83
+ ### `rl.schedule(url, options)`
84
+
85
+ Execute a request at a specific time.
86
+
87
+ ```js
88
+ await rl.schedule("https://myapp.com/api/expire", {
89
+ at: new Date("2026-03-15T09:00:00Z"), // Date object or ISO string
90
+ body: { subscriptionId: "sub_123" },
91
+ })
92
+ ```
93
+
94
+ ### `rl.cron(name, options)`
95
+
96
+ Create or update a recurring cron task.
97
+
98
+ ```js
99
+ await rl.cron("weekly-digest", {
100
+ url: "https://myapp.com/api/digest",
101
+ schedule: "0 9 * * MON", // every Monday at 9am
102
+ method: "POST",
103
+ enabled: true,
104
+ })
105
+ ```
106
+
107
+ ### Task management
108
+
109
+ ```js
110
+ // List all tasks
111
+ const { data, has_more } = await rl.tasks.list({ limit: 20 })
112
+
113
+ // Get a specific task
114
+ const task = await rl.tasks.get("task-id")
115
+
116
+ // Trigger a task manually
117
+ await rl.tasks.trigger("task-id")
118
+
119
+ // View execution history
120
+ const executions = await rl.tasks.executions("task-id")
121
+
122
+ // Delete a task
123
+ await rl.tasks.delete("task-id")
124
+ ```
125
+
126
+ ### Monitors (dead man's switch)
127
+
128
+ ```js
129
+ // Create a monitor — alerts you if a ping is missed
130
+ const monitor = await rl.monitors.create({
131
+ name: "nightly-backup",
132
+ schedule: "0 2 * * *",
133
+ grace: 600, // 10 min grace period
134
+ })
135
+
136
+ // Ping it from your cron job
137
+ await fetch(monitor.ping_url)
138
+ ```
139
+
140
+ ### Declarative sync
141
+
142
+ Push your task configuration from code. Matched by name.
143
+
144
+ ```js
145
+ await rl.sync({
146
+ tasks: [
147
+ {
148
+ url: "https://myapp.com/api/report",
149
+ schedule: "0 9 * * *",
150
+ },
151
+ ],
152
+ deleteRemoved: true, // remove tasks not in this list
153
+ })
154
+ ```
155
+
156
+ ## Frameworks
157
+
158
+ ### Next.js (App Router)
159
+
160
+ ```js
161
+ // app/api/orders/route.ts
162
+ import { Runlater } from "runlater"
163
+
164
+ const rl = new Runlater({ apiKey: process.env.RUNLATER_KEY })
165
+
166
+ export async function POST(req: Request) {
167
+ const order = await req.json()
168
+
169
+ // Process immediately, return fast
170
+ await rl.send("https://myapp.com/api/fulfill-order", {
171
+ body: order,
172
+ retries: 5,
173
+ })
174
+
175
+ return Response.json({ status: "queued" })
176
+ }
177
+ ```
178
+
179
+ ### Express
180
+
181
+ ```js
182
+ import express from "express"
183
+ import { Runlater } from "runlater"
184
+
185
+ const app = express()
186
+ const rl = new Runlater({ apiKey: process.env.RUNLATER_KEY })
187
+
188
+ app.post("/orders", async (req, res) => {
189
+ // Send confirmation email in 5 minutes
190
+ await rl.delay("https://myapp.com/api/send-confirmation", {
191
+ delay: "5m",
192
+ body: { orderId: req.body.id },
193
+ })
194
+
195
+ res.json({ status: "ok" })
196
+ })
197
+ ```
198
+
199
+ ## Requirements
200
+
201
+ - Node.js 18+ (uses native `fetch`)
202
+ - [Runlater account](https://runlater.eu) (free tier: 10k requests/month)
203
+
204
+ ## License
205
+
206
+ MIT
@@ -0,0 +1,239 @@
1
+ interface RunlaterOptions {
2
+ apiKey: string;
3
+ baseUrl?: string;
4
+ }
5
+ interface SendOptions {
6
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
7
+ headers?: Record<string, string>;
8
+ body?: unknown;
9
+ retries?: number;
10
+ timeout?: number;
11
+ queue?: string;
12
+ callback?: string;
13
+ idempotencyKey?: string;
14
+ }
15
+ interface DelayOptions extends SendOptions {
16
+ delay: string | number;
17
+ }
18
+ interface ScheduleOptions extends SendOptions {
19
+ at: string | Date;
20
+ }
21
+ interface CronOptions {
22
+ url: string;
23
+ schedule: string;
24
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
25
+ headers?: Record<string, string>;
26
+ body?: unknown;
27
+ timeout?: number;
28
+ retries?: number;
29
+ queue?: string;
30
+ callback?: string;
31
+ enabled?: boolean;
32
+ }
33
+ interface TaskResponse {
34
+ task_id: string;
35
+ execution_id: string;
36
+ status: string;
37
+ scheduled_for: string;
38
+ }
39
+ interface CronResponse {
40
+ id: string;
41
+ name: string;
42
+ url: string;
43
+ method: string;
44
+ cron_expression: string;
45
+ enabled: boolean;
46
+ next_run_at: string | null;
47
+ inserted_at: string;
48
+ updated_at: string;
49
+ }
50
+ interface Task {
51
+ id: string;
52
+ name: string;
53
+ url: string;
54
+ method: string;
55
+ headers: Record<string, string>;
56
+ body: string | null;
57
+ schedule_type: string;
58
+ cron_expression: string | null;
59
+ scheduled_at: string | null;
60
+ enabled: boolean;
61
+ muted: boolean;
62
+ timeout_ms: number;
63
+ retry_attempts: number;
64
+ callback_url: string | null;
65
+ queue: string | null;
66
+ next_run_at: string | null;
67
+ inserted_at: string;
68
+ updated_at: string;
69
+ }
70
+ interface Execution {
71
+ id: string;
72
+ status: "pending" | "running" | "success" | "failed" | "timeout";
73
+ scheduled_for: string;
74
+ started_at: string | null;
75
+ finished_at: string | null;
76
+ status_code: number | null;
77
+ duration_ms: number | null;
78
+ error_message: string | null;
79
+ attempt: number;
80
+ }
81
+ interface ListOptions {
82
+ queue?: string;
83
+ limit?: number;
84
+ offset?: number;
85
+ }
86
+ interface ListResponse<T> {
87
+ data: T[];
88
+ has_more: boolean;
89
+ limit: number;
90
+ offset: number;
91
+ }
92
+ interface TriggerResponse {
93
+ execution_id: string;
94
+ status: string;
95
+ scheduled_for: string;
96
+ }
97
+ interface Monitor {
98
+ id: string;
99
+ name: string;
100
+ ping_token: string;
101
+ ping_url: string;
102
+ schedule_type: "cron" | "interval";
103
+ cron_expression: string | null;
104
+ interval_seconds: number | null;
105
+ grace_period_seconds: number;
106
+ status: "new" | "up" | "down" | "paused";
107
+ enabled: boolean;
108
+ muted: boolean;
109
+ last_ping_at: string | null;
110
+ next_expected_at: string | null;
111
+ inserted_at: string;
112
+ updated_at: string;
113
+ }
114
+ interface CreateMonitorOptions {
115
+ name: string;
116
+ schedule: string;
117
+ interval?: number;
118
+ grace?: number;
119
+ enabled?: boolean;
120
+ }
121
+ interface SyncOptions {
122
+ tasks?: CronOptions[];
123
+ monitors?: CreateMonitorOptions[];
124
+ deleteRemoved?: boolean;
125
+ }
126
+ interface SyncResponse {
127
+ tasks: {
128
+ created: string[];
129
+ updated: string[];
130
+ deleted: string[];
131
+ };
132
+ monitors: {
133
+ created: string[];
134
+ updated: string[];
135
+ deleted: string[];
136
+ };
137
+ created_count: number;
138
+ updated_count: number;
139
+ deleted_count: number;
140
+ }
141
+ declare class RunlaterError extends Error {
142
+ status: number;
143
+ code: string;
144
+ constructor(status: number, code: string, message: string);
145
+ }
146
+
147
+ declare class Runlater {
148
+ private apiKey;
149
+ private baseUrl;
150
+ tasks: Tasks;
151
+ monitors: Monitors;
152
+ constructor(options: RunlaterOptions | string);
153
+ /**
154
+ * Send a request immediately with reliable delivery and retries.
155
+ *
156
+ * ```js
157
+ * await rl.send("https://myapp.com/api/process", {
158
+ * body: { orderId: 123 },
159
+ * retries: 5
160
+ * })
161
+ * ```
162
+ */
163
+ send(url: string, options?: SendOptions): Promise<TaskResponse>;
164
+ /**
165
+ * Execute a request after a delay.
166
+ *
167
+ * ```js
168
+ * await rl.delay("https://myapp.com/api/remind", {
169
+ * delay: "10m",
170
+ * body: { userId: 456 }
171
+ * })
172
+ * ```
173
+ *
174
+ * Delay accepts:
175
+ * - Strings: "30s", "5m", "2h", "1d"
176
+ * - Numbers: seconds (e.g. 3600 for 1 hour)
177
+ */
178
+ delay(url: string, options: DelayOptions): Promise<TaskResponse>;
179
+ /**
180
+ * Execute a request at a specific time.
181
+ *
182
+ * ```js
183
+ * await rl.schedule("https://myapp.com/api/expire", {
184
+ * at: "2026-03-15T09:00:00Z",
185
+ * body: { trialId: 789 }
186
+ * })
187
+ * ```
188
+ */
189
+ schedule(url: string, options: ScheduleOptions): Promise<TaskResponse>;
190
+ /**
191
+ * Create or update a recurring cron task.
192
+ *
193
+ * ```js
194
+ * await rl.cron("daily-report", {
195
+ * url: "https://myapp.com/api/report",
196
+ * schedule: "0 9 * * *"
197
+ * })
198
+ * ```
199
+ */
200
+ cron(name: string, options: CronOptions): Promise<CronResponse>;
201
+ /**
202
+ * Declaratively sync tasks and monitors. Matched by name.
203
+ *
204
+ * ```js
205
+ * await rl.sync({
206
+ * tasks: [
207
+ * { name: "daily-report", url: "https://...", schedule: "0 9 * * *" }
208
+ * ],
209
+ * deleteRemoved: true
210
+ * })
211
+ * ```
212
+ */
213
+ sync(options: SyncOptions): Promise<SyncResponse>;
214
+ private buildTaskBody;
215
+ /** @internal */
216
+ request<T>(method: string, path: string, options?: {
217
+ body?: unknown;
218
+ idempotencyKey?: string;
219
+ }): Promise<T>;
220
+ }
221
+ declare class Tasks {
222
+ private client;
223
+ constructor(client: Runlater);
224
+ list(options?: ListOptions): Promise<ListResponse<Task>>;
225
+ get(id: string): Promise<Task>;
226
+ delete(id: string): Promise<void>;
227
+ trigger(id: string): Promise<TriggerResponse>;
228
+ executions(id: string, limit?: number): Promise<Execution[]>;
229
+ }
230
+ declare class Monitors {
231
+ private client;
232
+ constructor(client: Runlater);
233
+ list(): Promise<Monitor[]>;
234
+ get(id: string): Promise<Monitor>;
235
+ create(options: CreateMonitorOptions): Promise<Monitor>;
236
+ delete(id: string): Promise<void>;
237
+ }
238
+
239
+ export { type CreateMonitorOptions, type CronOptions, type CronResponse, type DelayOptions, type Execution, type ListOptions, type ListResponse, type Monitor, Runlater, RunlaterError, type RunlaterOptions, type ScheduleOptions, type SendOptions, type SyncOptions, type SyncResponse, type Task, type TaskResponse, type TriggerResponse };
@@ -0,0 +1,239 @@
1
+ interface RunlaterOptions {
2
+ apiKey: string;
3
+ baseUrl?: string;
4
+ }
5
+ interface SendOptions {
6
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
7
+ headers?: Record<string, string>;
8
+ body?: unknown;
9
+ retries?: number;
10
+ timeout?: number;
11
+ queue?: string;
12
+ callback?: string;
13
+ idempotencyKey?: string;
14
+ }
15
+ interface DelayOptions extends SendOptions {
16
+ delay: string | number;
17
+ }
18
+ interface ScheduleOptions extends SendOptions {
19
+ at: string | Date;
20
+ }
21
+ interface CronOptions {
22
+ url: string;
23
+ schedule: string;
24
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
25
+ headers?: Record<string, string>;
26
+ body?: unknown;
27
+ timeout?: number;
28
+ retries?: number;
29
+ queue?: string;
30
+ callback?: string;
31
+ enabled?: boolean;
32
+ }
33
+ interface TaskResponse {
34
+ task_id: string;
35
+ execution_id: string;
36
+ status: string;
37
+ scheduled_for: string;
38
+ }
39
+ interface CronResponse {
40
+ id: string;
41
+ name: string;
42
+ url: string;
43
+ method: string;
44
+ cron_expression: string;
45
+ enabled: boolean;
46
+ next_run_at: string | null;
47
+ inserted_at: string;
48
+ updated_at: string;
49
+ }
50
+ interface Task {
51
+ id: string;
52
+ name: string;
53
+ url: string;
54
+ method: string;
55
+ headers: Record<string, string>;
56
+ body: string | null;
57
+ schedule_type: string;
58
+ cron_expression: string | null;
59
+ scheduled_at: string | null;
60
+ enabled: boolean;
61
+ muted: boolean;
62
+ timeout_ms: number;
63
+ retry_attempts: number;
64
+ callback_url: string | null;
65
+ queue: string | null;
66
+ next_run_at: string | null;
67
+ inserted_at: string;
68
+ updated_at: string;
69
+ }
70
+ interface Execution {
71
+ id: string;
72
+ status: "pending" | "running" | "success" | "failed" | "timeout";
73
+ scheduled_for: string;
74
+ started_at: string | null;
75
+ finished_at: string | null;
76
+ status_code: number | null;
77
+ duration_ms: number | null;
78
+ error_message: string | null;
79
+ attempt: number;
80
+ }
81
+ interface ListOptions {
82
+ queue?: string;
83
+ limit?: number;
84
+ offset?: number;
85
+ }
86
+ interface ListResponse<T> {
87
+ data: T[];
88
+ has_more: boolean;
89
+ limit: number;
90
+ offset: number;
91
+ }
92
+ interface TriggerResponse {
93
+ execution_id: string;
94
+ status: string;
95
+ scheduled_for: string;
96
+ }
97
+ interface Monitor {
98
+ id: string;
99
+ name: string;
100
+ ping_token: string;
101
+ ping_url: string;
102
+ schedule_type: "cron" | "interval";
103
+ cron_expression: string | null;
104
+ interval_seconds: number | null;
105
+ grace_period_seconds: number;
106
+ status: "new" | "up" | "down" | "paused";
107
+ enabled: boolean;
108
+ muted: boolean;
109
+ last_ping_at: string | null;
110
+ next_expected_at: string | null;
111
+ inserted_at: string;
112
+ updated_at: string;
113
+ }
114
+ interface CreateMonitorOptions {
115
+ name: string;
116
+ schedule: string;
117
+ interval?: number;
118
+ grace?: number;
119
+ enabled?: boolean;
120
+ }
121
+ interface SyncOptions {
122
+ tasks?: CronOptions[];
123
+ monitors?: CreateMonitorOptions[];
124
+ deleteRemoved?: boolean;
125
+ }
126
+ interface SyncResponse {
127
+ tasks: {
128
+ created: string[];
129
+ updated: string[];
130
+ deleted: string[];
131
+ };
132
+ monitors: {
133
+ created: string[];
134
+ updated: string[];
135
+ deleted: string[];
136
+ };
137
+ created_count: number;
138
+ updated_count: number;
139
+ deleted_count: number;
140
+ }
141
+ declare class RunlaterError extends Error {
142
+ status: number;
143
+ code: string;
144
+ constructor(status: number, code: string, message: string);
145
+ }
146
+
147
+ declare class Runlater {
148
+ private apiKey;
149
+ private baseUrl;
150
+ tasks: Tasks;
151
+ monitors: Monitors;
152
+ constructor(options: RunlaterOptions | string);
153
+ /**
154
+ * Send a request immediately with reliable delivery and retries.
155
+ *
156
+ * ```js
157
+ * await rl.send("https://myapp.com/api/process", {
158
+ * body: { orderId: 123 },
159
+ * retries: 5
160
+ * })
161
+ * ```
162
+ */
163
+ send(url: string, options?: SendOptions): Promise<TaskResponse>;
164
+ /**
165
+ * Execute a request after a delay.
166
+ *
167
+ * ```js
168
+ * await rl.delay("https://myapp.com/api/remind", {
169
+ * delay: "10m",
170
+ * body: { userId: 456 }
171
+ * })
172
+ * ```
173
+ *
174
+ * Delay accepts:
175
+ * - Strings: "30s", "5m", "2h", "1d"
176
+ * - Numbers: seconds (e.g. 3600 for 1 hour)
177
+ */
178
+ delay(url: string, options: DelayOptions): Promise<TaskResponse>;
179
+ /**
180
+ * Execute a request at a specific time.
181
+ *
182
+ * ```js
183
+ * await rl.schedule("https://myapp.com/api/expire", {
184
+ * at: "2026-03-15T09:00:00Z",
185
+ * body: { trialId: 789 }
186
+ * })
187
+ * ```
188
+ */
189
+ schedule(url: string, options: ScheduleOptions): Promise<TaskResponse>;
190
+ /**
191
+ * Create or update a recurring cron task.
192
+ *
193
+ * ```js
194
+ * await rl.cron("daily-report", {
195
+ * url: "https://myapp.com/api/report",
196
+ * schedule: "0 9 * * *"
197
+ * })
198
+ * ```
199
+ */
200
+ cron(name: string, options: CronOptions): Promise<CronResponse>;
201
+ /**
202
+ * Declaratively sync tasks and monitors. Matched by name.
203
+ *
204
+ * ```js
205
+ * await rl.sync({
206
+ * tasks: [
207
+ * { name: "daily-report", url: "https://...", schedule: "0 9 * * *" }
208
+ * ],
209
+ * deleteRemoved: true
210
+ * })
211
+ * ```
212
+ */
213
+ sync(options: SyncOptions): Promise<SyncResponse>;
214
+ private buildTaskBody;
215
+ /** @internal */
216
+ request<T>(method: string, path: string, options?: {
217
+ body?: unknown;
218
+ idempotencyKey?: string;
219
+ }): Promise<T>;
220
+ }
221
+ declare class Tasks {
222
+ private client;
223
+ constructor(client: Runlater);
224
+ list(options?: ListOptions): Promise<ListResponse<Task>>;
225
+ get(id: string): Promise<Task>;
226
+ delete(id: string): Promise<void>;
227
+ trigger(id: string): Promise<TriggerResponse>;
228
+ executions(id: string, limit?: number): Promise<Execution[]>;
229
+ }
230
+ declare class Monitors {
231
+ private client;
232
+ constructor(client: Runlater);
233
+ list(): Promise<Monitor[]>;
234
+ get(id: string): Promise<Monitor>;
235
+ create(options: CreateMonitorOptions): Promise<Monitor>;
236
+ delete(id: string): Promise<void>;
237
+ }
238
+
239
+ export { type CreateMonitorOptions, type CronOptions, type CronResponse, type DelayOptions, type Execution, type ListOptions, type ListResponse, type Monitor, Runlater, RunlaterError, type RunlaterOptions, type ScheduleOptions, type SendOptions, type SyncOptions, type SyncResponse, type Task, type TaskResponse, type TriggerResponse };
package/dist/index.js ADDED
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Runlater: () => Runlater,
24
+ RunlaterError: () => RunlaterError
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/types.ts
29
+ var RunlaterError = class extends Error {
30
+ status;
31
+ code;
32
+ constructor(status, code, message) {
33
+ super(message);
34
+ this.name = "RunlaterError";
35
+ this.status = status;
36
+ this.code = code;
37
+ }
38
+ };
39
+
40
+ // src/index.ts
41
+ var DEFAULT_BASE_URL = "https://runlater.eu";
42
+ var Runlater = class {
43
+ apiKey;
44
+ baseUrl;
45
+ tasks;
46
+ monitors;
47
+ constructor(options) {
48
+ if (typeof options === "string") {
49
+ this.apiKey = options;
50
+ this.baseUrl = DEFAULT_BASE_URL;
51
+ } else {
52
+ this.apiKey = options.apiKey;
53
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
54
+ }
55
+ this.tasks = new Tasks(this);
56
+ this.monitors = new Monitors(this);
57
+ }
58
+ /**
59
+ * Send a request immediately with reliable delivery and retries.
60
+ *
61
+ * ```js
62
+ * await rl.send("https://myapp.com/api/process", {
63
+ * body: { orderId: 123 },
64
+ * retries: 5
65
+ * })
66
+ * ```
67
+ */
68
+ async send(url, options = {}) {
69
+ const res = await this.request("POST", "/tasks", {
70
+ body: this.buildTaskBody(url, options),
71
+ idempotencyKey: options.idempotencyKey
72
+ });
73
+ return res.data;
74
+ }
75
+ /**
76
+ * Execute a request after a delay.
77
+ *
78
+ * ```js
79
+ * await rl.delay("https://myapp.com/api/remind", {
80
+ * delay: "10m",
81
+ * body: { userId: 456 }
82
+ * })
83
+ * ```
84
+ *
85
+ * Delay accepts:
86
+ * - Strings: "30s", "5m", "2h", "1d"
87
+ * - Numbers: seconds (e.g. 3600 for 1 hour)
88
+ */
89
+ async delay(url, options) {
90
+ const res = await this.request("POST", "/tasks", {
91
+ body: {
92
+ ...this.buildTaskBody(url, options),
93
+ delay: formatDelay(options.delay)
94
+ },
95
+ idempotencyKey: options.idempotencyKey
96
+ });
97
+ return res.data;
98
+ }
99
+ /**
100
+ * Execute a request at a specific time.
101
+ *
102
+ * ```js
103
+ * await rl.schedule("https://myapp.com/api/expire", {
104
+ * at: "2026-03-15T09:00:00Z",
105
+ * body: { trialId: 789 }
106
+ * })
107
+ * ```
108
+ */
109
+ async schedule(url, options) {
110
+ const at = options.at instanceof Date ? options.at.toISOString() : options.at;
111
+ const res = await this.request("POST", "/tasks", {
112
+ body: {
113
+ ...this.buildTaskBody(url, options),
114
+ run_at: at
115
+ },
116
+ idempotencyKey: options.idempotencyKey
117
+ });
118
+ return res.data;
119
+ }
120
+ /**
121
+ * Create or update a recurring cron task.
122
+ *
123
+ * ```js
124
+ * await rl.cron("daily-report", {
125
+ * url: "https://myapp.com/api/report",
126
+ * schedule: "0 9 * * *"
127
+ * })
128
+ * ```
129
+ */
130
+ async cron(name, options) {
131
+ const res = await this.request("POST", "/tasks", {
132
+ body: {
133
+ name,
134
+ url: options.url,
135
+ method: options.method ?? "GET",
136
+ cron: options.schedule,
137
+ headers: options.headers,
138
+ body: options.body != null ? JSON.stringify(options.body) : void 0,
139
+ timeout_ms: options.timeout,
140
+ retry_attempts: options.retries,
141
+ queue: options.queue,
142
+ callback_url: options.callback,
143
+ enabled: options.enabled
144
+ }
145
+ });
146
+ return res.data;
147
+ }
148
+ /**
149
+ * Declaratively sync tasks and monitors. Matched by name.
150
+ *
151
+ * ```js
152
+ * await rl.sync({
153
+ * tasks: [
154
+ * { name: "daily-report", url: "https://...", schedule: "0 9 * * *" }
155
+ * ],
156
+ * deleteRemoved: true
157
+ * })
158
+ * ```
159
+ */
160
+ async sync(options) {
161
+ const body = {};
162
+ if (options.tasks) {
163
+ body.tasks = options.tasks.map((t) => ({
164
+ name: t.url,
165
+ // name defaults to url if not in CronOptions
166
+ url: t.url,
167
+ method: t.method ?? "GET",
168
+ schedule_type: "cron",
169
+ cron_expression: t.schedule,
170
+ headers: t.headers,
171
+ body: t.body != null ? JSON.stringify(t.body) : void 0,
172
+ timeout_ms: t.timeout,
173
+ retry_attempts: t.retries,
174
+ queue: t.queue,
175
+ callback_url: t.callback,
176
+ enabled: t.enabled
177
+ }));
178
+ }
179
+ if (options.monitors) {
180
+ body.monitors = options.monitors.map((m) => ({
181
+ name: m.name,
182
+ schedule_type: m.interval ? "interval" : "cron",
183
+ cron_expression: m.schedule,
184
+ interval_seconds: m.interval,
185
+ grace_period_seconds: m.grace,
186
+ enabled: m.enabled
187
+ }));
188
+ }
189
+ if (options.deleteRemoved) {
190
+ body.delete_removed = true;
191
+ }
192
+ const res = await this.request("PUT", "/sync", { body });
193
+ return res.data;
194
+ }
195
+ // --- Internal ---
196
+ buildTaskBody(url, options) {
197
+ return {
198
+ url,
199
+ method: options.method ?? "POST",
200
+ headers: options.headers,
201
+ body: options.body != null ? JSON.stringify(options.body) : void 0,
202
+ timeout_ms: options.timeout,
203
+ retry_attempts: options.retries,
204
+ queue: options.queue,
205
+ callback_url: options.callback
206
+ };
207
+ }
208
+ /** @internal */
209
+ async request(method, path, options = {}) {
210
+ const headers = {
211
+ Authorization: `Bearer ${this.apiKey}`,
212
+ "Content-Type": "application/json",
213
+ "User-Agent": "runlater-node/0.1.0"
214
+ };
215
+ if (options.idempotencyKey) {
216
+ headers["Idempotency-Key"] = options.idempotencyKey;
217
+ }
218
+ const response = await fetch(`${this.baseUrl}/api/v1${path}`, {
219
+ method,
220
+ headers,
221
+ body: options.body != null ? JSON.stringify(options.body) : void 0
222
+ });
223
+ if (!response.ok) {
224
+ const errorBody = await response.json().catch(() => ({}));
225
+ const error = errorBody.error;
226
+ throw new RunlaterError(
227
+ response.status,
228
+ error?.code ?? "unknown_error",
229
+ error?.message ?? `HTTP ${response.status}`
230
+ );
231
+ }
232
+ if (response.status === 204) {
233
+ return {};
234
+ }
235
+ return await response.json();
236
+ }
237
+ };
238
+ var Tasks = class {
239
+ constructor(client) {
240
+ this.client = client;
241
+ }
242
+ async list(options = {}) {
243
+ const params = new URLSearchParams();
244
+ if (options.queue != null) params.set("queue", options.queue);
245
+ if (options.limit != null) params.set("limit", String(options.limit));
246
+ if (options.offset != null) params.set("offset", String(options.offset));
247
+ const query = params.toString();
248
+ return this.client.request("GET", `/tasks${query ? `?${query}` : ""}`);
249
+ }
250
+ async get(id) {
251
+ const res = await this.client.request("GET", `/tasks/${id}`);
252
+ return res.data;
253
+ }
254
+ async delete(id) {
255
+ await this.client.request("DELETE", `/tasks/${id}`);
256
+ }
257
+ async trigger(id) {
258
+ const res = await this.client.request(
259
+ "POST",
260
+ `/tasks/${id}/trigger`
261
+ );
262
+ return res.data;
263
+ }
264
+ async executions(id, limit = 50) {
265
+ const res = await this.client.request(
266
+ "GET",
267
+ `/tasks/${id}/executions?limit=${limit}`
268
+ );
269
+ return res.data;
270
+ }
271
+ };
272
+ var Monitors = class {
273
+ constructor(client) {
274
+ this.client = client;
275
+ }
276
+ async list() {
277
+ const res = await this.client.request("GET", "/monitors");
278
+ return res.data;
279
+ }
280
+ async get(id) {
281
+ const res = await this.client.request("GET", `/monitors/${id}`);
282
+ return res.data;
283
+ }
284
+ async create(options) {
285
+ const res = await this.client.request("POST", "/monitors", {
286
+ body: {
287
+ name: options.name,
288
+ schedule_type: options.interval ? "interval" : "cron",
289
+ cron_expression: options.schedule,
290
+ interval_seconds: options.interval,
291
+ grace_period_seconds: options.grace,
292
+ enabled: options.enabled
293
+ }
294
+ });
295
+ return res.data;
296
+ }
297
+ async delete(id) {
298
+ await this.client.request("DELETE", `/monitors/${id}`);
299
+ }
300
+ };
301
+ function formatDelay(delay) {
302
+ if (typeof delay === "number") return `${delay}s`;
303
+ return delay;
304
+ }
305
+ // Annotate the CommonJS export names for ESM import in node:
306
+ 0 && (module.exports = {
307
+ Runlater,
308
+ RunlaterError
309
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,281 @@
1
+ // src/types.ts
2
+ var RunlaterError = class extends Error {
3
+ status;
4
+ code;
5
+ constructor(status, code, message) {
6
+ super(message);
7
+ this.name = "RunlaterError";
8
+ this.status = status;
9
+ this.code = code;
10
+ }
11
+ };
12
+
13
+ // src/index.ts
14
+ var DEFAULT_BASE_URL = "https://runlater.eu";
15
+ var Runlater = class {
16
+ apiKey;
17
+ baseUrl;
18
+ tasks;
19
+ monitors;
20
+ constructor(options) {
21
+ if (typeof options === "string") {
22
+ this.apiKey = options;
23
+ this.baseUrl = DEFAULT_BASE_URL;
24
+ } else {
25
+ this.apiKey = options.apiKey;
26
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
27
+ }
28
+ this.tasks = new Tasks(this);
29
+ this.monitors = new Monitors(this);
30
+ }
31
+ /**
32
+ * Send a request immediately with reliable delivery and retries.
33
+ *
34
+ * ```js
35
+ * await rl.send("https://myapp.com/api/process", {
36
+ * body: { orderId: 123 },
37
+ * retries: 5
38
+ * })
39
+ * ```
40
+ */
41
+ async send(url, options = {}) {
42
+ const res = await this.request("POST", "/tasks", {
43
+ body: this.buildTaskBody(url, options),
44
+ idempotencyKey: options.idempotencyKey
45
+ });
46
+ return res.data;
47
+ }
48
+ /**
49
+ * Execute a request after a delay.
50
+ *
51
+ * ```js
52
+ * await rl.delay("https://myapp.com/api/remind", {
53
+ * delay: "10m",
54
+ * body: { userId: 456 }
55
+ * })
56
+ * ```
57
+ *
58
+ * Delay accepts:
59
+ * - Strings: "30s", "5m", "2h", "1d"
60
+ * - Numbers: seconds (e.g. 3600 for 1 hour)
61
+ */
62
+ async delay(url, options) {
63
+ const res = await this.request("POST", "/tasks", {
64
+ body: {
65
+ ...this.buildTaskBody(url, options),
66
+ delay: formatDelay(options.delay)
67
+ },
68
+ idempotencyKey: options.idempotencyKey
69
+ });
70
+ return res.data;
71
+ }
72
+ /**
73
+ * Execute a request at a specific time.
74
+ *
75
+ * ```js
76
+ * await rl.schedule("https://myapp.com/api/expire", {
77
+ * at: "2026-03-15T09:00:00Z",
78
+ * body: { trialId: 789 }
79
+ * })
80
+ * ```
81
+ */
82
+ async schedule(url, options) {
83
+ const at = options.at instanceof Date ? options.at.toISOString() : options.at;
84
+ const res = await this.request("POST", "/tasks", {
85
+ body: {
86
+ ...this.buildTaskBody(url, options),
87
+ run_at: at
88
+ },
89
+ idempotencyKey: options.idempotencyKey
90
+ });
91
+ return res.data;
92
+ }
93
+ /**
94
+ * Create or update a recurring cron task.
95
+ *
96
+ * ```js
97
+ * await rl.cron("daily-report", {
98
+ * url: "https://myapp.com/api/report",
99
+ * schedule: "0 9 * * *"
100
+ * })
101
+ * ```
102
+ */
103
+ async cron(name, options) {
104
+ const res = await this.request("POST", "/tasks", {
105
+ body: {
106
+ name,
107
+ url: options.url,
108
+ method: options.method ?? "GET",
109
+ cron: options.schedule,
110
+ headers: options.headers,
111
+ body: options.body != null ? JSON.stringify(options.body) : void 0,
112
+ timeout_ms: options.timeout,
113
+ retry_attempts: options.retries,
114
+ queue: options.queue,
115
+ callback_url: options.callback,
116
+ enabled: options.enabled
117
+ }
118
+ });
119
+ return res.data;
120
+ }
121
+ /**
122
+ * Declaratively sync tasks and monitors. Matched by name.
123
+ *
124
+ * ```js
125
+ * await rl.sync({
126
+ * tasks: [
127
+ * { name: "daily-report", url: "https://...", schedule: "0 9 * * *" }
128
+ * ],
129
+ * deleteRemoved: true
130
+ * })
131
+ * ```
132
+ */
133
+ async sync(options) {
134
+ const body = {};
135
+ if (options.tasks) {
136
+ body.tasks = options.tasks.map((t) => ({
137
+ name: t.url,
138
+ // name defaults to url if not in CronOptions
139
+ url: t.url,
140
+ method: t.method ?? "GET",
141
+ schedule_type: "cron",
142
+ cron_expression: t.schedule,
143
+ headers: t.headers,
144
+ body: t.body != null ? JSON.stringify(t.body) : void 0,
145
+ timeout_ms: t.timeout,
146
+ retry_attempts: t.retries,
147
+ queue: t.queue,
148
+ callback_url: t.callback,
149
+ enabled: t.enabled
150
+ }));
151
+ }
152
+ if (options.monitors) {
153
+ body.monitors = options.monitors.map((m) => ({
154
+ name: m.name,
155
+ schedule_type: m.interval ? "interval" : "cron",
156
+ cron_expression: m.schedule,
157
+ interval_seconds: m.interval,
158
+ grace_period_seconds: m.grace,
159
+ enabled: m.enabled
160
+ }));
161
+ }
162
+ if (options.deleteRemoved) {
163
+ body.delete_removed = true;
164
+ }
165
+ const res = await this.request("PUT", "/sync", { body });
166
+ return res.data;
167
+ }
168
+ // --- Internal ---
169
+ buildTaskBody(url, options) {
170
+ return {
171
+ url,
172
+ method: options.method ?? "POST",
173
+ headers: options.headers,
174
+ body: options.body != null ? JSON.stringify(options.body) : void 0,
175
+ timeout_ms: options.timeout,
176
+ retry_attempts: options.retries,
177
+ queue: options.queue,
178
+ callback_url: options.callback
179
+ };
180
+ }
181
+ /** @internal */
182
+ async request(method, path, options = {}) {
183
+ const headers = {
184
+ Authorization: `Bearer ${this.apiKey}`,
185
+ "Content-Type": "application/json",
186
+ "User-Agent": "runlater-node/0.1.0"
187
+ };
188
+ if (options.idempotencyKey) {
189
+ headers["Idempotency-Key"] = options.idempotencyKey;
190
+ }
191
+ const response = await fetch(`${this.baseUrl}/api/v1${path}`, {
192
+ method,
193
+ headers,
194
+ body: options.body != null ? JSON.stringify(options.body) : void 0
195
+ });
196
+ if (!response.ok) {
197
+ const errorBody = await response.json().catch(() => ({}));
198
+ const error = errorBody.error;
199
+ throw new RunlaterError(
200
+ response.status,
201
+ error?.code ?? "unknown_error",
202
+ error?.message ?? `HTTP ${response.status}`
203
+ );
204
+ }
205
+ if (response.status === 204) {
206
+ return {};
207
+ }
208
+ return await response.json();
209
+ }
210
+ };
211
+ var Tasks = class {
212
+ constructor(client) {
213
+ this.client = client;
214
+ }
215
+ async list(options = {}) {
216
+ const params = new URLSearchParams();
217
+ if (options.queue != null) params.set("queue", options.queue);
218
+ if (options.limit != null) params.set("limit", String(options.limit));
219
+ if (options.offset != null) params.set("offset", String(options.offset));
220
+ const query = params.toString();
221
+ return this.client.request("GET", `/tasks${query ? `?${query}` : ""}`);
222
+ }
223
+ async get(id) {
224
+ const res = await this.client.request("GET", `/tasks/${id}`);
225
+ return res.data;
226
+ }
227
+ async delete(id) {
228
+ await this.client.request("DELETE", `/tasks/${id}`);
229
+ }
230
+ async trigger(id) {
231
+ const res = await this.client.request(
232
+ "POST",
233
+ `/tasks/${id}/trigger`
234
+ );
235
+ return res.data;
236
+ }
237
+ async executions(id, limit = 50) {
238
+ const res = await this.client.request(
239
+ "GET",
240
+ `/tasks/${id}/executions?limit=${limit}`
241
+ );
242
+ return res.data;
243
+ }
244
+ };
245
+ var Monitors = class {
246
+ constructor(client) {
247
+ this.client = client;
248
+ }
249
+ async list() {
250
+ const res = await this.client.request("GET", "/monitors");
251
+ return res.data;
252
+ }
253
+ async get(id) {
254
+ const res = await this.client.request("GET", `/monitors/${id}`);
255
+ return res.data;
256
+ }
257
+ async create(options) {
258
+ const res = await this.client.request("POST", "/monitors", {
259
+ body: {
260
+ name: options.name,
261
+ schedule_type: options.interval ? "interval" : "cron",
262
+ cron_expression: options.schedule,
263
+ interval_seconds: options.interval,
264
+ grace_period_seconds: options.grace,
265
+ enabled: options.enabled
266
+ }
267
+ });
268
+ return res.data;
269
+ }
270
+ async delete(id) {
271
+ await this.client.request("DELETE", `/monitors/${id}`);
272
+ }
273
+ };
274
+ function formatDelay(delay) {
275
+ if (typeof delay === "number") return `${delay}s`;
276
+ return delay;
277
+ }
278
+ export {
279
+ Runlater,
280
+ RunlaterError
281
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "runlater-js",
3
+ "version": "0.1.0",
4
+ "description": "Delayed tasks, cron jobs, and reliable webhooks. No infrastructure required.",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "devDependencies": {
23
+ "tsup": "^8.0.0",
24
+ "typescript": "^5.4.0"
25
+ },
26
+ "keywords": [
27
+ "cron",
28
+ "queue",
29
+ "task",
30
+ "webhook",
31
+ "delay",
32
+ "scheduler",
33
+ "background-jobs",
34
+ "serverless"
35
+ ],
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/runlater-eu/runlater-js"
40
+ },
41
+ "homepage": "https://runlater.eu",
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ }
45
+ }