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 +206 -0
- package/dist/index.d.mts +239 -0
- package/dist/index.d.ts +239 -0
- package/dist/index.js +309 -0
- package/dist/index.mjs +281 -0
- package/package.json +45 -0
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
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|