station-signal 1.0.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/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/adapters/http-trigger.d.ts +19 -0
- package/dist/adapters/http-trigger.d.ts.map +1 -0
- package/dist/adapters/http-trigger.js +63 -0
- package/dist/adapters/http-trigger.js.map +1 -0
- package/dist/adapters/index.d.ts +49 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +6 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/memory.d.ts +33 -0
- package/dist/adapters/memory.d.ts.map +1 -0
- package/dist/adapters/memory.js +144 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/registry.d.ts +17 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +27 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/trigger.d.ts +12 -0
- package/dist/adapters/trigger.d.ts.map +1 -0
- package/dist/adapters/trigger.js +2 -0
- package/dist/adapters/trigger.js.map +1 -0
- package/dist/bootstrap.d.ts +10 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +198 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +52 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +50 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/interval.d.ts +7 -0
- package/dist/interval.d.ts.map +1 -0
- package/dist/interval.js +25 -0
- package/dist/interval.js.map +1 -0
- package/dist/signal-runner.d.ts +91 -0
- package/dist/signal-runner.d.ts.map +1 -0
- package/dist/signal-runner.js +564 -0
- package/dist/signal-runner.js.map +1 -0
- package/dist/signal.d.ts +76 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +209 -0
- package/dist/signal.js.map +1 -0
- package/dist/subscribers/console.d.ts +64 -0
- package/dist/subscribers/console.d.ts.map +1 -0
- package/dist/subscribers/console.js +56 -0
- package/dist/subscribers/console.js.map +1 -0
- package/dist/subscribers/index.d.ts +93 -0
- package/dist/subscribers/index.d.ts.map +1 -0
- package/dist/subscribers/index.js +2 -0
- package/dist/subscribers/index.js.map +1 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +9 -0
- package/dist/util.js.map +1 -0
- package/package.json +43 -0
- package/src/adapters/http-trigger.ts +83 -0
- package/src/adapters/index.ts +64 -0
- package/src/adapters/memory.ts +165 -0
- package/src/adapters/registry.ts +35 -0
- package/src/adapters/trigger.ts +11 -0
- package/src/bootstrap.ts +237 -0
- package/src/config.ts +75 -0
- package/src/errors.ts +60 -0
- package/src/index.ts +37 -0
- package/src/interval.ts +29 -0
- package/src/signal-runner.ts +659 -0
- package/src/signal.ts +294 -0
- package/src/subscribers/console.ts +108 -0
- package/src/subscribers/index.ts +74 -0
- package/src/types.ts +50 -0
- package/src/util.ts +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 porkytheblack
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# station-signal
|
|
2
|
+
|
|
3
|
+
A lightweight, type-safe background job framework for TypeScript. Define signals with Zod schemas, trigger them from anywhere, and let the runner execute each one in an isolated child process with timeout enforcement and automatic retries.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add station-signal
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Defining signals
|
|
12
|
+
|
|
13
|
+
Use the `signal()` builder to define a named signal with a Zod input schema and a handler function.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { signal, z } from "station-signal";
|
|
17
|
+
|
|
18
|
+
export const sendEmail = signal("send-email")
|
|
19
|
+
.input(z.object({ to: z.string().email(), subject: z.string(), body: z.string() }))
|
|
20
|
+
.timeout(30_000)
|
|
21
|
+
.retries(3)
|
|
22
|
+
.run(async (input) => {
|
|
23
|
+
await emailService.send(input.to, input.subject, input.body);
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The full builder chain is:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
signal("name")
|
|
31
|
+
.input(schema) // Required. Zod schema that validates the input.
|
|
32
|
+
.timeout(ms) // Optional. Max execution time in milliseconds (default: 300000).
|
|
33
|
+
.retries(n) // Optional. Number of retries after the first failure (default: 0).
|
|
34
|
+
.every("5m") // Optional. Makes this a recurring signal on an interval.
|
|
35
|
+
.run(fn) // Required. The async handler function. Returns a Signal object.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Method | Required | Description |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `.input(schema)` | Yes | A Zod schema used to validate input at trigger time and before execution. |
|
|
41
|
+
| `.timeout(ms)` | No | Override the default 5-minute timeout, in milliseconds. |
|
|
42
|
+
| `.retries(n)` | No | Number of retries after the first attempt fails. Total attempts = `n + 1`. |
|
|
43
|
+
| `.every(interval)` | No | Schedule the signal to run on a recurring interval (e.g. `"every 5m"`). |
|
|
44
|
+
| `.run(fn)` | Yes | The async function that executes the signal. Finalizes and returns the `Signal` object. |
|
|
45
|
+
|
|
46
|
+
## Triggering signals
|
|
47
|
+
|
|
48
|
+
There are two ways to trigger a signal.
|
|
49
|
+
|
|
50
|
+
### Type-safe trigger
|
|
51
|
+
|
|
52
|
+
Call `.trigger()` directly on a signal object. The input is validated against the Zod schema before being enqueued.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { sendEmail } from "./signals/send-email.js";
|
|
56
|
+
|
|
57
|
+
const entryId = await sendEmail.trigger({
|
|
58
|
+
to: "alice@example.com",
|
|
59
|
+
subject: "Welcome",
|
|
60
|
+
body: "Thanks for signing up.",
|
|
61
|
+
});
|
|
62
|
+
// entryId is a unique string identifying this queue entry
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Dynamic trigger
|
|
66
|
+
|
|
67
|
+
Use `SignalQueue` to trigger signals by name. This is useful when the signal name comes from a variable or external source. No schema validation is performed.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { SignalQueue } from "station-signal";
|
|
71
|
+
|
|
72
|
+
const queue = new SignalQueue();
|
|
73
|
+
const entryId = await queue.trigger("send-email", {
|
|
74
|
+
to: "alice@example.com",
|
|
75
|
+
subject: "Welcome",
|
|
76
|
+
body: "Thanks for signing up.",
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Both approaches return a `Promise<string>` containing the queue entry ID.
|
|
81
|
+
|
|
82
|
+
## Running signals
|
|
83
|
+
|
|
84
|
+
`SignalRunner` polls the adapter for due entries and spawns an isolated child process for each one.
|
|
85
|
+
|
|
86
|
+
### Minimal example
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { SignalRunner } from "station-signal";
|
|
90
|
+
|
|
91
|
+
const runner = new SignalRunner({
|
|
92
|
+
signalsDir: "./src/signals",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await runner.start();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Full options
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { SignalRunner } from "station-signal";
|
|
102
|
+
import { SQLiteAdapter } from "station-adapter-sqlite";
|
|
103
|
+
|
|
104
|
+
const runner = new SignalRunner({
|
|
105
|
+
// Auto-discover all .ts/.js files in this directory (recursive).
|
|
106
|
+
signalsDir: "./src/signals",
|
|
107
|
+
|
|
108
|
+
// Custom adapter for persistence. Defaults to MemoryAdapter.
|
|
109
|
+
adapter: new SQLiteAdapter("jobs.db"),
|
|
110
|
+
|
|
111
|
+
// How often to check for due entries, in milliseconds. Default: 1000.
|
|
112
|
+
pollIntervalMs: 2000,
|
|
113
|
+
|
|
114
|
+
// Default max attempts for signals that don't specify their own. Default: 1.
|
|
115
|
+
maxAttempts: 3,
|
|
116
|
+
|
|
117
|
+
// Path to a module that calls configure(). Imported by the runner on startup
|
|
118
|
+
// AND by every spawned child process before the signal file.
|
|
119
|
+
configModule: "/absolute/path/to/adapter.config.ts",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await runner.start();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Manual registration
|
|
126
|
+
|
|
127
|
+
If you are not using `signalsDir`, you can register signals individually:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const runner = new SignalRunner();
|
|
131
|
+
runner.register("send-email", "/absolute/path/to/signals/send-email.ts");
|
|
132
|
+
runner.register("generate-report", "/absolute/path/to/signals/generate-report.ts");
|
|
133
|
+
await runner.start();
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The `register()` method takes a signal name and the absolute file path to the module that exports the signal.
|
|
137
|
+
|
|
138
|
+
## Recurring signals
|
|
139
|
+
|
|
140
|
+
### Using the builder
|
|
141
|
+
|
|
142
|
+
Add `.every()` to any signal definition to make it recurring. The runner automatically schedules the first run on startup and reschedules after each execution.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
export const healthCheck = signal("health-check")
|
|
146
|
+
.input(z.object({}))
|
|
147
|
+
.every("every 30s")
|
|
148
|
+
.run(async () => {
|
|
149
|
+
await pingAllServices();
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Using SignalQueue
|
|
154
|
+
|
|
155
|
+
Schedule a recurring signal dynamically:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const queue = new SignalQueue();
|
|
159
|
+
await queue.schedule("cleanup-temp-files", "every 1h", {});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Interval format
|
|
163
|
+
|
|
164
|
+
The interval string must match the format `"every <number><unit>"`.
|
|
165
|
+
|
|
166
|
+
| Unit | Meaning | Example |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `s` | Seconds | `"every 30s"` |
|
|
169
|
+
| `m` | Minutes | `"every 5m"` |
|
|
170
|
+
| `h` | Hours | `"every 1h"` |
|
|
171
|
+
| `d` | Days | `"every 7d"` |
|
|
172
|
+
|
|
173
|
+
## Timeout and retries
|
|
174
|
+
|
|
175
|
+
Every signal has a timeout and a maximum number of attempts.
|
|
176
|
+
|
|
177
|
+
| Setting | Default | Builder method |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| Timeout | 300,000ms (5 minutes) | `.timeout(ms)` |
|
|
180
|
+
| Retries | 0 (1 total attempt) | `.retries(n)` |
|
|
181
|
+
|
|
182
|
+
When a signal times out or throws an error and has remaining retry attempts, the runner resets it to "pending" for another try.
|
|
183
|
+
|
|
184
|
+
**Trigger signals**: After exhausting all attempts, the entry is marked as `"failed"` with a `completedAt` timestamp.
|
|
185
|
+
|
|
186
|
+
**Recurring signals**: After exhausting all attempts for a given run, the entry resets its attempt counter to 0 and reschedules the next run based on its interval. Recurring signals never permanently fail.
|
|
187
|
+
|
|
188
|
+
## Adapters
|
|
189
|
+
|
|
190
|
+
Adapters control how queue entries are stored and retrieved.
|
|
191
|
+
|
|
192
|
+
### Setting the global adapter
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { configure } from "station-signal";
|
|
196
|
+
import { SQLiteAdapter } from "station-adapter-sqlite";
|
|
197
|
+
|
|
198
|
+
configure({ adapter: new SQLiteAdapter("jobs.db") });
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The default adapter is `MemoryAdapter`, which stores entries in-process. It requires no configuration but does not persist data across restarts and cannot share state between the runner and its spawned child processes.
|
|
202
|
+
|
|
203
|
+
### The configModule pattern
|
|
204
|
+
|
|
205
|
+
Because `SignalRunner` spawns each signal in a separate child process, you need a way to ensure both the runner and every child process use the same adapter. The `configModule` option solves this:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
// src/adapter.config.ts
|
|
209
|
+
import { configure } from "station-signal";
|
|
210
|
+
import { SQLiteAdapter } from "station-adapter-sqlite";
|
|
211
|
+
|
|
212
|
+
configure({ adapter: new SQLiteAdapter("jobs.db") });
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// src/runner.ts
|
|
217
|
+
import { fileURLToPath } from "node:url";
|
|
218
|
+
import { SignalRunner } from "station-signal";
|
|
219
|
+
|
|
220
|
+
const runner = new SignalRunner({
|
|
221
|
+
signalsDir: "./src/signals",
|
|
222
|
+
configModule: fileURLToPath(new URL("./adapter.config.ts", import.meta.url)),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await runner.start();
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The runner imports `configModule` on startup. Every spawned child process imports it before loading the signal file. This guarantees a consistent adapter everywhere.
|
|
229
|
+
|
|
230
|
+
## Writing a custom adapter
|
|
231
|
+
|
|
232
|
+
Implement the `SignalQueueAdapter` interface:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
interface SignalQueueAdapter {
|
|
236
|
+
add(entry: QueueEntry): Promise<void>;
|
|
237
|
+
remove(id: string): Promise<void>;
|
|
238
|
+
getDue(): Promise<QueueEntry[]>;
|
|
239
|
+
getRunning(): Promise<QueueEntry[]>;
|
|
240
|
+
update(id: string, patch: Partial<QueueEntry>): Promise<void>;
|
|
241
|
+
ping(): Promise<boolean>;
|
|
242
|
+
generateId(): string;
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
| Method | Contract |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `add(entry)` | Store a new queue entry. |
|
|
249
|
+
| `remove(id)` | Delete an entry by its ID. |
|
|
250
|
+
| `getDue()` | Return all pending entries where `nextRunAt` is `null`/`undefined` or `<= now`. |
|
|
251
|
+
| `getRunning()` | Return all entries with status `"running"`. |
|
|
252
|
+
| `update(id, patch)` | Merge the partial patch into the existing entry. |
|
|
253
|
+
| `ping()` | Health check. Return `true` if the adapter is operational. |
|
|
254
|
+
| `generateId()` | Produce a unique string ID for a new queue entry. |
|
|
255
|
+
|
|
256
|
+
## How it works
|
|
257
|
+
|
|
258
|
+
1. `SignalRunner` polls the adapter at a configurable interval, calling `getDue()` to find entries that are ready to execute.
|
|
259
|
+
2. For each due entry, the runner marks it as `"running"` and spawns an isolated child process via `node --import tsx bootstrap.js`.
|
|
260
|
+
3. The child process first imports the `configModule` (if provided) to set up the shared adapter, then imports the signal file.
|
|
261
|
+
4. The bootstrap script finds the matching signal export, validates the input against the signal's Zod schema, and rejects with a `"failed"` status if validation fails.
|
|
262
|
+
5. The signal handler runs under timeout enforcement via `Promise.race`. If it completes in time, the entry is marked `"completed"`. If it throws, the entry is marked `"failed"`.
|
|
263
|
+
6. Back in the runner, `checkTimeouts()` runs each tick to detect entries that have been `"running"` longer than their configured timeout. Timed-out entries are either reset to `"pending"` for a retry or marked `"failed"` (trigger) / rescheduled (recurring) depending on remaining attempts.
|
|
264
|
+
|
|
265
|
+
## Types reference
|
|
266
|
+
|
|
267
|
+
### QueueEntry
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
interface QueueEntry {
|
|
271
|
+
id: string;
|
|
272
|
+
signalName: string;
|
|
273
|
+
kind: QueueEntryKind;
|
|
274
|
+
input: string; // JSON-serialized
|
|
275
|
+
status: EntryStatus;
|
|
276
|
+
attempts: number;
|
|
277
|
+
maxAttempts: number;
|
|
278
|
+
timeout: number; // milliseconds
|
|
279
|
+
interval?: string; // e.g. "every 5m" (recurring only)
|
|
280
|
+
nextRunAt?: Date;
|
|
281
|
+
lastRunAt?: Date;
|
|
282
|
+
startedAt?: Date;
|
|
283
|
+
completedAt?: Date;
|
|
284
|
+
createdAt: Date;
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### EntryStatus
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
type EntryStatus = "pending" | "running" | "completed" | "failed";
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### QueueEntryKind
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
type QueueEntryKind = "trigger" | "recurring";
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Constants
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
const DEFAULT_TIMEOUT_MS = 300_000; // 5 minutes
|
|
304
|
+
const DEFAULT_MAX_ATTEMPTS = 1; // no retry by default
|
|
305
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TriggerAdapter } from "./trigger.js";
|
|
2
|
+
export interface HttpTriggerOptions {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
fetch?: typeof globalThis.fetch;
|
|
7
|
+
}
|
|
8
|
+
export declare class HttpTriggerAdapter implements TriggerAdapter {
|
|
9
|
+
private endpoint;
|
|
10
|
+
private apiKey?;
|
|
11
|
+
private timeout;
|
|
12
|
+
private fetchFn;
|
|
13
|
+
constructor(options: HttpTriggerOptions);
|
|
14
|
+
trigger(signalName: string, input: unknown): Promise<string>;
|
|
15
|
+
triggerBroadcast(broadcastName: string, input: unknown): Promise<string>;
|
|
16
|
+
ping(): Promise<boolean>;
|
|
17
|
+
private headers;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=http-trigger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-trigger.d.ts","sourceRoot":"","sources":["../../src/adapters/http-trigger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAYnD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAED,qBAAa,kBAAmB,YAAW,cAAc;IACvD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAA0B;gBAE7B,OAAO,EAAE,kBAAkB;IAOjC,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAgB5D,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBxE,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IAa9B,OAAO,CAAC,OAAO;CAKhB"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { StationRemoteError } from "../errors.js";
|
|
2
|
+
export class HttpTriggerAdapter {
|
|
3
|
+
endpoint;
|
|
4
|
+
apiKey;
|
|
5
|
+
timeout;
|
|
6
|
+
fetchFn;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.endpoint = options.endpoint.replace(/\/+$/, "");
|
|
9
|
+
this.apiKey = options.apiKey;
|
|
10
|
+
this.timeout = options.timeout ?? 10_000;
|
|
11
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
12
|
+
}
|
|
13
|
+
async trigger(signalName, input) {
|
|
14
|
+
const url = `${this.endpoint}/api/v1/trigger`;
|
|
15
|
+
const response = await this.fetchFn(url, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: this.headers(),
|
|
18
|
+
body: JSON.stringify({ signalName, input }),
|
|
19
|
+
signal: AbortSignal.timeout(this.timeout),
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
const body = await response.json().catch(() => ({}));
|
|
23
|
+
throw new StationRemoteError(response.status, body.error, body.message);
|
|
24
|
+
}
|
|
25
|
+
const body = await response.json();
|
|
26
|
+
return body.data.id;
|
|
27
|
+
}
|
|
28
|
+
async triggerBroadcast(broadcastName, input) {
|
|
29
|
+
const url = `${this.endpoint}/api/v1/trigger-broadcast`;
|
|
30
|
+
const response = await this.fetchFn(url, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: this.headers(),
|
|
33
|
+
body: JSON.stringify({ broadcastName, input }),
|
|
34
|
+
signal: AbortSignal.timeout(this.timeout),
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const body = await response.json().catch(() => ({}));
|
|
38
|
+
throw new StationRemoteError(response.status, body.error, body.message);
|
|
39
|
+
}
|
|
40
|
+
const body = await response.json();
|
|
41
|
+
return body.data.id;
|
|
42
|
+
}
|
|
43
|
+
async ping() {
|
|
44
|
+
try {
|
|
45
|
+
const url = `${this.endpoint}/api/v1/health`;
|
|
46
|
+
const response = await this.fetchFn(url, {
|
|
47
|
+
headers: this.headers(),
|
|
48
|
+
signal: AbortSignal.timeout(5000),
|
|
49
|
+
});
|
|
50
|
+
return response.ok;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
headers() {
|
|
57
|
+
const h = { "Content-Type": "application/json" };
|
|
58
|
+
if (this.apiKey)
|
|
59
|
+
h["Authorization"] = `Bearer ${this.apiKey}`;
|
|
60
|
+
return h;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=http-trigger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-trigger.js","sourceRoot":"","sources":["../../src/adapters/http-trigger.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAkBlD,MAAM,OAAO,kBAAkB;IACrB,QAAQ,CAAS;IACjB,MAAM,CAAU;IAChB,OAAO,CAAS;IAChB,OAAO,CAA0B;IAEzC,YAAY,OAA2B;QACrC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACrD,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,MAAM,CAAC;QACzC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;IACnD,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,UAAkB,EAAE,KAAc;QAC9C,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,QAAQ,iBAAiB,CAAC;QAC9C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;YACvC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE;YACvB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;YAC3C,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;SAC1C,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAc,CAAC;YAClE,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1E,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAiB,CAAC;QAClD,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,aAAqB,EAAE,KAAc;QAC1D,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,QAAQ,2BAA2B,CAAC;QACxD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;YACvC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE;YACvB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;YAC9C,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;SAC1C,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAc,CAAC;YAClE,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1E,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAiB,CAAC;QAClD,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,QAAQ,gBAAgB,CAAC;YAC7C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;gBACvC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE;gBACvB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;aAClC,CAAC,CAAC;YACH,OAAO,QAAQ,CAAC,EAAE,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAEO,OAAO;QACb,MAAM,CAAC,GAA2B,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC;QACzE,IAAI,IAAI,CAAC,MAAM;YAAE,CAAC,CAAC,eAAe,CAAC,GAAG,UAAU,IAAI,CAAC,MAAM,EAAE,CAAC;QAC9D,OAAO,CAAC,CAAC;IACX,CAAC;CACF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Run, RunPatch, RunStatus, Step, StepPatch } from "../types.js";
|
|
2
|
+
export interface SignalQueueAdapter {
|
|
3
|
+
addRun(run: Run): Promise<void>;
|
|
4
|
+
removeRun(id: string): Promise<void>;
|
|
5
|
+
getRunsDue(): Promise<Run[]>;
|
|
6
|
+
getRunsRunning(): Promise<Run[]>;
|
|
7
|
+
getRun(id: string): Promise<Run | null>;
|
|
8
|
+
updateRun(id: string, patch: RunPatch): Promise<void>;
|
|
9
|
+
listRuns(signalName: string): Promise<Run[]>;
|
|
10
|
+
/** Check if any run for the given signal has one of the specified statuses. */
|
|
11
|
+
hasRunWithStatus(signalName: string, statuses: RunStatus[]): Promise<boolean>;
|
|
12
|
+
/** Purge runs in terminal statuses older than the given date. Returns count deleted. */
|
|
13
|
+
purgeRuns(olderThan: Date, statuses: RunStatus[]): Promise<number>;
|
|
14
|
+
addStep(step: Step): Promise<void>;
|
|
15
|
+
updateStep(id: string, patch: StepPatch): Promise<void>;
|
|
16
|
+
getSteps(runId: string): Promise<Step[]>;
|
|
17
|
+
removeSteps(runId: string): Promise<void>;
|
|
18
|
+
generateId(): string;
|
|
19
|
+
ping(): Promise<boolean>;
|
|
20
|
+
close?(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Metadata an adapter carries so child processes can reconstruct it.
|
|
24
|
+
* Adapters that implement SerializableAdapter are fully automatic —
|
|
25
|
+
* no extra runner config needed.
|
|
26
|
+
*/
|
|
27
|
+
export interface AdapterManifest {
|
|
28
|
+
/** Registry name (e.g. "sqlite"). Matches registerAdapter() name. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Serializable options to pass to the factory. */
|
|
31
|
+
options: Record<string, unknown>;
|
|
32
|
+
/**
|
|
33
|
+
* Resolved absolute path/URL to the module that registers this adapter.
|
|
34
|
+
* Only needed for external (non-built-in) adapters.
|
|
35
|
+
*/
|
|
36
|
+
moduleUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Adapters that can be reconstructed in child processes implement this.
|
|
40
|
+
* MemoryAdapter intentionally does NOT implement this since it cannot
|
|
41
|
+
* share state across processes.
|
|
42
|
+
*/
|
|
43
|
+
export interface SerializableAdapter extends SignalQueueAdapter {
|
|
44
|
+
toManifest(): AdapterManifest;
|
|
45
|
+
}
|
|
46
|
+
export declare function isSerializableAdapter(adapter: SignalQueueAdapter): adapter is SerializableAdapter;
|
|
47
|
+
export { MemoryAdapter } from "./memory.js";
|
|
48
|
+
export { registerAdapter, createAdapter, hasAdapter } from "./registry.js";
|
|
49
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7E,MAAM,WAAW,kBAAkB;IAEjC,MAAM,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC7B,cAAc,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IACxC,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAE7C,+EAA+E;IAC/E,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9E,wFAAwF;IACxF,SAAS,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAGnE,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IACzC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAG1C,UAAU,IAAI,MAAM,CAAC;IACrB,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACzB,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAoB,SAAQ,kBAAkB;IAC7D,UAAU,IAAI,eAAe,CAAC;CAC/B;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,IAAI,mBAAmB,CAEhC;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAwDA,MAAM,UAAU,qBAAqB,CACnC,OAA2B;IAE3B,OAAO,OAAQ,OAA+B,CAAC,UAAU,KAAK,UAAU,CAAC;AAC3E,CAAC;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SignalQueueAdapter } from "./index.js";
|
|
2
|
+
import type { Run, RunPatch, RunStatus, Step, StepPatch } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* In-process memory adapter. Useful for single-process scripts and testing.
|
|
5
|
+
* Does NOT implement SerializableAdapter — child processes get their own
|
|
6
|
+
* independent MemoryAdapter. Use SqliteAdapter for cross-process persistence.
|
|
7
|
+
*/
|
|
8
|
+
export declare class MemoryAdapter implements SignalQueueAdapter {
|
|
9
|
+
private runs;
|
|
10
|
+
private steps;
|
|
11
|
+
private maxRuns;
|
|
12
|
+
constructor(options?: {
|
|
13
|
+
maxRuns?: number;
|
|
14
|
+
});
|
|
15
|
+
addRun(run: Run): Promise<void>;
|
|
16
|
+
private evictCompleted;
|
|
17
|
+
removeRun(id: string): Promise<void>;
|
|
18
|
+
getRunsDue(): Promise<Run[]>;
|
|
19
|
+
getRunsRunning(): Promise<Run[]>;
|
|
20
|
+
getRun(id: string): Promise<Run | null>;
|
|
21
|
+
updateRun(id: string, patch: RunPatch): Promise<void>;
|
|
22
|
+
listRuns(signalName: string): Promise<Run[]>;
|
|
23
|
+
hasRunWithStatus(signalName: string, statuses: RunStatus[]): Promise<boolean>;
|
|
24
|
+
purgeRuns(olderThan: Date, statuses: RunStatus[]): Promise<number>;
|
|
25
|
+
addStep(step: Step): Promise<void>;
|
|
26
|
+
updateStep(id: string, patch: StepPatch): Promise<void>;
|
|
27
|
+
getSteps(runId: string): Promise<Step[]>;
|
|
28
|
+
removeSteps(runId: string): Promise<void>;
|
|
29
|
+
ping(): Promise<boolean>;
|
|
30
|
+
generateId(): string;
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/adapters/memory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7E;;;;GAIG;AACH,qBAAa,aAAc,YAAW,kBAAkB;IACtD,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;IAIpC,MAAM,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAOrC,OAAO,CAAC,cAAc;IAwBhB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKpC,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAW5B,cAAc,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAMhC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IAIvC,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAcrD,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAM5C,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ7E,SAAS,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAalE,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlC,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAcvD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAMxC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQzC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IAI9B,UAAU,IAAI,MAAM;IAId,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { registerAdapter } from "./registry.js";
|
|
3
|
+
/**
|
|
4
|
+
* In-process memory adapter. Useful for single-process scripts and testing.
|
|
5
|
+
* Does NOT implement SerializableAdapter — child processes get their own
|
|
6
|
+
* independent MemoryAdapter. Use SqliteAdapter for cross-process persistence.
|
|
7
|
+
*/
|
|
8
|
+
export class MemoryAdapter {
|
|
9
|
+
runs = new Map();
|
|
10
|
+
steps = new Map();
|
|
11
|
+
maxRuns;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.maxRuns = options?.maxRuns ?? 10_000;
|
|
14
|
+
}
|
|
15
|
+
async addRun(run) {
|
|
16
|
+
this.runs.set(run.id, run);
|
|
17
|
+
if (this.runs.size > this.maxRuns) {
|
|
18
|
+
this.evictCompleted();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
evictCompleted() {
|
|
22
|
+
const terminal = [];
|
|
23
|
+
for (const [id, run] of this.runs) {
|
|
24
|
+
if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
|
|
25
|
+
terminal.push(id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Sort oldest first by completedAt
|
|
29
|
+
terminal.sort((a, b) => {
|
|
30
|
+
const ra = this.runs.get(a);
|
|
31
|
+
const rb = this.runs.get(b);
|
|
32
|
+
return (ra.completedAt?.getTime() ?? 0) - (rb.completedAt?.getTime() ?? 0);
|
|
33
|
+
});
|
|
34
|
+
// Evict oldest 10%
|
|
35
|
+
const evictCount = Math.max(1, Math.floor(terminal.length * 0.1));
|
|
36
|
+
for (let i = 0; i < evictCount && i < terminal.length; i++) {
|
|
37
|
+
const id = terminal[i];
|
|
38
|
+
this.runs.delete(id);
|
|
39
|
+
for (const [stepId, step] of this.steps) {
|
|
40
|
+
if (step.runId === id)
|
|
41
|
+
this.steps.delete(stepId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async removeRun(id) {
|
|
46
|
+
this.runs.delete(id);
|
|
47
|
+
await this.removeSteps(id);
|
|
48
|
+
}
|
|
49
|
+
async getRunsDue() {
|
|
50
|
+
const now = new Date();
|
|
51
|
+
return Array.from(this.runs.values())
|
|
52
|
+
.filter((run) => {
|
|
53
|
+
if (run.status !== "pending")
|
|
54
|
+
return false;
|
|
55
|
+
if (!run.nextRunAt)
|
|
56
|
+
return true;
|
|
57
|
+
return run.nextRunAt <= now;
|
|
58
|
+
})
|
|
59
|
+
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
60
|
+
}
|
|
61
|
+
async getRunsRunning() {
|
|
62
|
+
return Array.from(this.runs.values()).filter((run) => run.status === "running");
|
|
63
|
+
}
|
|
64
|
+
async getRun(id) {
|
|
65
|
+
return this.runs.get(id) ?? null;
|
|
66
|
+
}
|
|
67
|
+
async updateRun(id, patch) {
|
|
68
|
+
const run = this.runs.get(id);
|
|
69
|
+
if (run) {
|
|
70
|
+
const rec = run;
|
|
71
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
72
|
+
if (value === undefined) {
|
|
73
|
+
delete rec[key];
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
rec[key] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async listRuns(signalName) {
|
|
82
|
+
return Array.from(this.runs.values()).filter((run) => run.signalName === signalName);
|
|
83
|
+
}
|
|
84
|
+
async hasRunWithStatus(signalName, statuses) {
|
|
85
|
+
const statusSet = new Set(statuses);
|
|
86
|
+
for (const run of this.runs.values()) {
|
|
87
|
+
if (run.signalName === signalName && statusSet.has(run.status))
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
async purgeRuns(olderThan, statuses) {
|
|
93
|
+
const statusSet = new Set(statuses);
|
|
94
|
+
let purged = 0;
|
|
95
|
+
for (const [id, run] of this.runs) {
|
|
96
|
+
if (statusSet.has(run.status) && run.completedAt && run.completedAt < olderThan) {
|
|
97
|
+
this.runs.delete(id);
|
|
98
|
+
await this.removeSteps(id);
|
|
99
|
+
purged++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return purged;
|
|
103
|
+
}
|
|
104
|
+
async addStep(step) {
|
|
105
|
+
this.steps.set(step.id, step);
|
|
106
|
+
}
|
|
107
|
+
async updateStep(id, patch) {
|
|
108
|
+
const step = this.steps.get(id);
|
|
109
|
+
if (step) {
|
|
110
|
+
const rec = step;
|
|
111
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
112
|
+
if (value === undefined) {
|
|
113
|
+
delete rec[key];
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
rec[key] = value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async getSteps(runId) {
|
|
122
|
+
return Array.from(this.steps.values()).filter((step) => step.runId === runId);
|
|
123
|
+
}
|
|
124
|
+
async removeSteps(runId) {
|
|
125
|
+
for (const [id, step] of this.steps) {
|
|
126
|
+
if (step.runId === runId) {
|
|
127
|
+
this.steps.delete(id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async ping() {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
generateId() {
|
|
135
|
+
return randomUUID();
|
|
136
|
+
}
|
|
137
|
+
async close() {
|
|
138
|
+
this.runs.clear();
|
|
139
|
+
this.steps.clear();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Register in the adapter factory for cross-process reconstruction
|
|
143
|
+
registerAdapter("memory", () => new MemoryAdapter());
|
|
144
|
+
//# sourceMappingURL=memory.js.map
|