reqon-dsl 0.3.0 → 0.4.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 +23 -3
- package/dist/ast/nodes.d.ts +8 -0
- package/dist/auth/circuit-breaker.d.ts +11 -0
- package/dist/auth/circuit-breaker.js +83 -12
- package/dist/auth/credentials.d.ts +6 -1
- package/dist/auth/credentials.js +12 -4
- package/dist/auth/oauth2-provider.js +13 -3
- package/dist/auth/rate-limiter.d.ts +8 -1
- package/dist/auth/rate-limiter.js +30 -10
- package/dist/auth/token-store.js +8 -1
- package/dist/cli.d.ts +11 -1
- package/dist/cli.js +65 -6
- package/dist/config/constants.d.ts +15 -4
- package/dist/config/constants.js +15 -4
- package/dist/control/server.d.ts +17 -0
- package/dist/control/server.js +82 -5
- package/dist/control/types.d.ts +6 -0
- package/dist/debug/cli-debugger.js +8 -3
- package/dist/execution/store.js +2 -2
- package/dist/execution-log/events.d.ts +125 -0
- package/dist/execution-log/events.js +17 -0
- package/dist/execution-log/fold.d.ts +38 -0
- package/dist/execution-log/fold.js +54 -0
- package/dist/execution-log/index.d.ts +18 -0
- package/dist/execution-log/index.js +6 -0
- package/dist/execution-log/postgres-store.d.ts +36 -0
- package/dist/execution-log/postgres-store.js +108 -0
- package/dist/execution-log/resume.d.ts +11 -0
- package/dist/execution-log/resume.js +5 -0
- package/dist/execution-log/sqlite-store.d.ts +16 -0
- package/dist/execution-log/sqlite-store.js +101 -0
- package/dist/execution-log/store.d.ts +72 -0
- package/dist/execution-log/store.js +182 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/interpreter/context.d.ts +15 -0
- package/dist/interpreter/context.js +3 -0
- package/dist/interpreter/evaluator.js +38 -8
- package/dist/interpreter/executor.d.ts +63 -1
- package/dist/interpreter/executor.js +406 -30
- package/dist/interpreter/fetch-handler.d.ts +39 -1
- package/dist/interpreter/fetch-handler.js +84 -15
- package/dist/interpreter/http.d.ts +31 -2
- package/dist/interpreter/http.js +187 -26
- package/dist/interpreter/index.d.ts +3 -3
- package/dist/interpreter/index.js +3 -3
- package/dist/interpreter/pagination.d.ts +1 -1
- package/dist/interpreter/pagination.js +7 -1
- package/dist/interpreter/step-handlers/for-handler.d.ts +3 -0
- package/dist/interpreter/step-handlers/for-handler.js +18 -3
- package/dist/interpreter/step-handlers/match-handler.js +5 -2
- package/dist/interpreter/step-handlers/store-handler.d.ts +7 -1
- package/dist/interpreter/step-handlers/store-handler.js +25 -16
- package/dist/interpreter/step-handlers/validate-handler.js +4 -1
- package/dist/interpreter/step-handlers/webhook-handler.d.ts +1 -0
- package/dist/interpreter/step-handlers/webhook-handler.js +13 -3
- package/dist/interpreter/store-manager.d.ts +1 -1
- package/dist/interpreter/store-manager.js +5 -1
- package/dist/loader/index.js +5 -8
- package/dist/mcp/sandbox.d.ts +41 -0
- package/dist/mcp/sandbox.js +76 -0
- package/dist/mcp/server.js +62 -9
- package/dist/oas/loader.d.ts +13 -1
- package/dist/oas/loader.js +25 -3
- package/dist/oas/mock-generator.js +13 -4
- package/dist/oas/validator.js +45 -5
- package/dist/observability/events.d.ts +6 -2
- package/dist/observability/events.js +0 -5
- package/dist/observability/logger.js +17 -10
- package/dist/observability/otel.d.ts +8 -0
- package/dist/observability/otel.js +45 -10
- package/dist/parser/action-parser.js +2 -2
- package/dist/parser/base.d.ts +7 -0
- package/dist/parser/base.js +11 -0
- package/dist/parser/expressions.d.ts +1 -0
- package/dist/parser/expressions.js +17 -4
- package/dist/parser/fetch-parser.js +13 -2
- package/dist/pause/index.d.ts +1 -0
- package/dist/pause/index.js +1 -0
- package/dist/pause/log-store.d.ts +33 -0
- package/dist/pause/log-store.js +98 -0
- package/dist/pause/manager.d.ts +12 -0
- package/dist/pause/manager.js +77 -28
- package/dist/pause/store.js +5 -3
- package/dist/scheduler/cron-parser.d.ts +10 -3
- package/dist/scheduler/cron-parser.js +227 -48
- package/dist/scheduler/scheduler.js +56 -22
- package/dist/stores/factory.d.ts +6 -0
- package/dist/stores/factory.js +11 -1
- package/dist/stores/file.js +9 -17
- package/dist/stores/memory.js +3 -12
- package/dist/stores/postgrest.d.ts +28 -0
- package/dist/stores/postgrest.js +84 -37
- package/dist/sync/index.d.ts +3 -2
- package/dist/sync/index.js +2 -1
- package/dist/sync/log-store.d.ts +30 -0
- package/dist/sync/log-store.js +45 -0
- package/dist/sync/store.js +1 -1
- package/dist/trace/index.d.ts +2 -0
- package/dist/trace/index.js +1 -0
- package/dist/trace/log-view.d.ts +57 -0
- package/dist/trace/log-view.js +76 -0
- package/dist/trace/recorder.d.ts +5 -1
- package/dist/trace/recorder.js +19 -6
- package/dist/trace/store.d.ts +6 -0
- package/dist/trace/store.js +47 -22
- package/dist/utils/deep-merge.d.ts +10 -0
- package/dist/utils/deep-merge.js +23 -0
- package/dist/utils/file.d.ts +13 -4
- package/dist/utils/file.js +70 -12
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/long-timeout.d.ts +19 -0
- package/dist/utils/long-timeout.js +33 -0
- package/dist/utils/path.d.ts +22 -1
- package/dist/utils/path.js +46 -1
- package/dist/utils/redact.d.ts +22 -0
- package/dist/utils/redact.js +42 -0
- package/dist/webhook/server.d.ts +9 -0
- package/dist/webhook/server.js +115 -30
- package/dist/webhook/types.d.ts +9 -1
- package/package.json +22 -4
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Coerce a value that may be a Date or an ISO string back to a Date. */
|
|
2
|
+
function toDate(value) {
|
|
3
|
+
return value instanceof Date ? value : new Date(value);
|
|
4
|
+
}
|
|
5
|
+
function restoreDates(pause) {
|
|
6
|
+
pause.pausedAt = toDate(pause.pausedAt);
|
|
7
|
+
pause.expiresAt = toDate(pause.expiresAt);
|
|
8
|
+
if (pause.resumedAt)
|
|
9
|
+
pause.resumedAt = toDate(pause.resumedAt);
|
|
10
|
+
return pause;
|
|
11
|
+
}
|
|
12
|
+
export class LogBackedPauseStore {
|
|
13
|
+
log;
|
|
14
|
+
constructor(log) {
|
|
15
|
+
this.log = log;
|
|
16
|
+
}
|
|
17
|
+
async save(pause) {
|
|
18
|
+
await this.log.append({
|
|
19
|
+
executionId: pause.executionId,
|
|
20
|
+
type: 'pause.created',
|
|
21
|
+
pauseId: pause.id,
|
|
22
|
+
pause,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async update(id, updates) {
|
|
26
|
+
const pause = await this.load(id);
|
|
27
|
+
if (!pause)
|
|
28
|
+
throw new Error(`Pause not found: ${id}`);
|
|
29
|
+
const resumedBy = updates.resumedBy ?? 'manual';
|
|
30
|
+
await this.log.append({
|
|
31
|
+
executionId: pause.executionId,
|
|
32
|
+
type: 'pause.resumed',
|
|
33
|
+
pauseId: id,
|
|
34
|
+
resumedBy,
|
|
35
|
+
status: updates.status ?? 'resumed',
|
|
36
|
+
resumedAt: (updates.resumedAt ?? new Date()).toISOString(),
|
|
37
|
+
webhookPayload: updates.webhookPayload,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async load(id) {
|
|
41
|
+
const byId = this.foldPauses(await this.log.listPauses());
|
|
42
|
+
return byId.get(id) ?? null;
|
|
43
|
+
}
|
|
44
|
+
async loadByExecution(executionId) {
|
|
45
|
+
const all = this.foldPauses(await this.log.listPauses());
|
|
46
|
+
for (const pause of all.values()) {
|
|
47
|
+
if (pause.executionId === executionId && pause.status === 'waiting')
|
|
48
|
+
return pause;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
async listActive() {
|
|
53
|
+
return (await this.allPauses())
|
|
54
|
+
.filter((p) => p.status === 'waiting')
|
|
55
|
+
.sort((a, b) => a.expiresAt.getTime() - b.expiresAt.getTime());
|
|
56
|
+
}
|
|
57
|
+
async listByMission(mission) {
|
|
58
|
+
return (await this.allPauses())
|
|
59
|
+
.filter((p) => p.mission === mission)
|
|
60
|
+
.sort((a, b) => b.pausedAt.getTime() - a.pausedAt.getTime());
|
|
61
|
+
}
|
|
62
|
+
async findExpired() {
|
|
63
|
+
const now = new Date();
|
|
64
|
+
return (await this.listActive()).filter((p) => p.expiresAt <= now);
|
|
65
|
+
}
|
|
66
|
+
/** Append-only: deletion is a no-op (history is retained in the log). */
|
|
67
|
+
async delete() { }
|
|
68
|
+
async allPauses() {
|
|
69
|
+
return Array.from(this.foldPauses(await this.log.listPauses()).values());
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Fold pause events into the latest state per pause id. A `pause.created`
|
|
73
|
+
* seeds the state; later `pause.resumed` events apply the terminal status.
|
|
74
|
+
*/
|
|
75
|
+
foldPauses(events) {
|
|
76
|
+
const byId = new Map();
|
|
77
|
+
for (const event of events) {
|
|
78
|
+
if (event.type === 'pause.created' && event.pause) {
|
|
79
|
+
byId.set(event.pauseId, restoreDates({ ...event.pause }));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Apply resumptions after every create is loaded (events aren't ordered).
|
|
83
|
+
for (const event of events) {
|
|
84
|
+
if (event.type !== 'pause.resumed')
|
|
85
|
+
continue;
|
|
86
|
+
const pause = byId.get(event.pauseId);
|
|
87
|
+
if (!pause)
|
|
88
|
+
continue;
|
|
89
|
+
pause.status = event.status ?? 'resumed';
|
|
90
|
+
pause.resumedBy = event.resumedBy;
|
|
91
|
+
if (event.resumedAt)
|
|
92
|
+
pause.resumedAt = new Date(event.resumedAt);
|
|
93
|
+
if (event.webhookPayload !== undefined)
|
|
94
|
+
pause.webhookPayload = event.webhookPayload;
|
|
95
|
+
}
|
|
96
|
+
return byId;
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/pause/manager.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ export declare class PauseManager {
|
|
|
46
46
|
private config;
|
|
47
47
|
private pollTimer?;
|
|
48
48
|
private isRunning;
|
|
49
|
+
private resuming;
|
|
50
|
+
private isCheckingExpired;
|
|
49
51
|
constructor(config: PauseManagerConfig);
|
|
50
52
|
/**
|
|
51
53
|
* Create a new pause and persist state
|
|
@@ -91,6 +93,16 @@ export declare class PauseManager {
|
|
|
91
93
|
* Get pause status summary
|
|
92
94
|
*/
|
|
93
95
|
getStatus(): Promise<PauseStatus>;
|
|
96
|
+
/**
|
|
97
|
+
* Transition a pause to `resumed` and fire `onResume` exactly once.
|
|
98
|
+
*
|
|
99
|
+
* Returns the resumed pause, or `null` if this call lost the race (another
|
|
100
|
+
* resume already claimed it, or it is no longer waiting). The claim on
|
|
101
|
+
* `this.resuming` is synchronous — taken before any `await` — so two
|
|
102
|
+
* concurrent callers (e.g. the timeout poller and an inbound webhook) can't
|
|
103
|
+
* both pass the status check and double-fire the side-effecting tail. The
|
|
104
|
+
* persisted-status re-check makes resume idempotent across poll cycles too.
|
|
105
|
+
*/
|
|
94
106
|
private markResumed;
|
|
95
107
|
private registerWebhookTrigger;
|
|
96
108
|
private cleanupWebhooks;
|
package/dist/pause/manager.js
CHANGED
|
@@ -12,6 +12,11 @@ export class PauseManager {
|
|
|
12
12
|
config;
|
|
13
13
|
pollTimer;
|
|
14
14
|
isRunning = false;
|
|
15
|
+
// Pause ids currently being resumed. Claimed synchronously before any await
|
|
16
|
+
// so a timeout poll and an inbound webhook can't both resume the same pause.
|
|
17
|
+
resuming = new Set();
|
|
18
|
+
// Prevents a slow resume cycle from overlapping the next poll tick.
|
|
19
|
+
isCheckingExpired = false;
|
|
15
20
|
constructor(config) {
|
|
16
21
|
this.config = config;
|
|
17
22
|
}
|
|
@@ -64,7 +69,15 @@ export class PauseManager {
|
|
|
64
69
|
if (pause.status !== 'waiting') {
|
|
65
70
|
throw new Error(`Pause ${pauseId} is not waiting (status: ${pause.status})`);
|
|
66
71
|
}
|
|
67
|
-
|
|
72
|
+
const resumed = await this.markResumed(pause, 'manual');
|
|
73
|
+
if (!resumed) {
|
|
74
|
+
// Lost a race with another resume trigger; return the resolved state.
|
|
75
|
+
const current = await this.config.store.load(pauseId);
|
|
76
|
+
if (!current)
|
|
77
|
+
throw new Error(`Pause not found: ${pauseId}`);
|
|
78
|
+
return current;
|
|
79
|
+
}
|
|
80
|
+
return resumed;
|
|
68
81
|
}
|
|
69
82
|
/**
|
|
70
83
|
* Resume a pause by execution ID
|
|
@@ -109,9 +122,9 @@ export class PauseManager {
|
|
|
109
122
|
if (!webhookTrigger) {
|
|
110
123
|
return false;
|
|
111
124
|
}
|
|
112
|
-
// Mark resumed with webhook payload
|
|
113
|
-
await this.markResumed(pause, 'webhook', payload);
|
|
114
|
-
return
|
|
125
|
+
// Mark resumed with webhook payload; false if another trigger won the race.
|
|
126
|
+
const resumed = await this.markResumed(pause, 'webhook', payload);
|
|
127
|
+
return resumed !== null;
|
|
115
128
|
}
|
|
116
129
|
/**
|
|
117
130
|
* Start monitoring for expired pauses
|
|
@@ -141,13 +154,25 @@ export class PauseManager {
|
|
|
141
154
|
* Check for and process expired pauses
|
|
142
155
|
*/
|
|
143
156
|
async checkExpiredPauses() {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
// Overlap guard: if a prior cycle's resume callbacks are still running when
|
|
158
|
+
// the next poll fires, skip rather than reprocessing the same pauses.
|
|
159
|
+
if (this.isCheckingExpired)
|
|
160
|
+
return [];
|
|
161
|
+
this.isCheckingExpired = true;
|
|
162
|
+
try {
|
|
163
|
+
const expired = await this.config.store.findExpired();
|
|
164
|
+
const resumed = [];
|
|
165
|
+
for (const pause of expired) {
|
|
166
|
+
const updated = await this.markResumed(pause, 'timeout');
|
|
167
|
+
// null means another trigger already resumed this pause — skip it.
|
|
168
|
+
if (updated)
|
|
169
|
+
resumed.push(updated);
|
|
170
|
+
}
|
|
171
|
+
return resumed;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
this.isCheckingExpired = false;
|
|
149
175
|
}
|
|
150
|
-
return resumed;
|
|
151
176
|
}
|
|
152
177
|
/**
|
|
153
178
|
* Get all active pauses
|
|
@@ -184,28 +209,52 @@ export class PauseManager {
|
|
|
184
209
|
})),
|
|
185
210
|
};
|
|
186
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Transition a pause to `resumed` and fire `onResume` exactly once.
|
|
214
|
+
*
|
|
215
|
+
* Returns the resumed pause, or `null` if this call lost the race (another
|
|
216
|
+
* resume already claimed it, or it is no longer waiting). The claim on
|
|
217
|
+
* `this.resuming` is synchronous — taken before any `await` — so two
|
|
218
|
+
* concurrent callers (e.g. the timeout poller and an inbound webhook) can't
|
|
219
|
+
* both pass the status check and double-fire the side-effecting tail. The
|
|
220
|
+
* persisted-status re-check makes resume idempotent across poll cycles too.
|
|
221
|
+
*/
|
|
187
222
|
async markResumed(pause, resumedBy, webhookPayload) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
resumedAt: new Date(),
|
|
191
|
-
resumedBy,
|
|
192
|
-
};
|
|
193
|
-
if (webhookPayload !== undefined) {
|
|
194
|
-
updates.webhookPayload = webhookPayload;
|
|
223
|
+
if (this.resuming.has(pause.id)) {
|
|
224
|
+
return null; // another resume is already in flight for this pause
|
|
195
225
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
226
|
+
this.resuming.add(pause.id);
|
|
227
|
+
try {
|
|
228
|
+
// Re-read the source of truth: a prior cycle may already have resumed it.
|
|
229
|
+
const current = await this.config.store.load(pause.id);
|
|
230
|
+
if (!current || current.status !== 'waiting') {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const updates = {
|
|
234
|
+
status: 'resumed',
|
|
235
|
+
resumedAt: new Date(),
|
|
236
|
+
resumedBy,
|
|
237
|
+
};
|
|
238
|
+
if (webhookPayload !== undefined) {
|
|
239
|
+
updates.webhookPayload = webhookPayload;
|
|
240
|
+
}
|
|
241
|
+
await this.config.store.update(pause.id, updates);
|
|
242
|
+
// Cleanup webhook registrations
|
|
243
|
+
await this.cleanupWebhooks(current);
|
|
244
|
+
const updated = await this.config.store.load(pause.id);
|
|
245
|
+
if (!updated) {
|
|
246
|
+
throw new Error(`Failed to load updated pause: ${pause.id}`);
|
|
247
|
+
}
|
|
248
|
+
this.log(`Pause ${pause.id} resumed by ${resumedBy}`);
|
|
249
|
+
// Trigger callback (the side-effecting pipeline tail) exactly once.
|
|
250
|
+
if (this.config.onResume) {
|
|
251
|
+
await this.config.onResume(updated);
|
|
252
|
+
}
|
|
253
|
+
return updated;
|
|
202
254
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (this.config.onResume) {
|
|
206
|
-
await this.config.onResume(updated);
|
|
255
|
+
finally {
|
|
256
|
+
this.resuming.delete(pause.id);
|
|
207
257
|
}
|
|
208
|
-
return updated;
|
|
209
258
|
}
|
|
210
259
|
async registerWebhookTrigger(executionId, path, timeout) {
|
|
211
260
|
if (!this.config.webhookServer) {
|
package/dist/pause/store.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Stores pause state for resource-free long pauses,
|
|
5
5
|
* enabling resumption after extended periods.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { safeJoin } from '../utils/path.js';
|
|
8
8
|
import { ensureDirectory, writeJsonFile, readJsonFile, listFiles, deleteFile, restoreDates, } from '../utils/file.js';
|
|
9
9
|
/**
|
|
10
10
|
* File-based pause store
|
|
@@ -17,7 +17,7 @@ export class FilePauseStore {
|
|
|
17
17
|
this.initialized = ensureDirectory(this.baseDir);
|
|
18
18
|
}
|
|
19
19
|
getFilePath(id) {
|
|
20
|
-
return
|
|
20
|
+
return safeJoin(this.baseDir, `${id}.json`);
|
|
21
21
|
}
|
|
22
22
|
deserialize(parsed) {
|
|
23
23
|
restoreDates(parsed, ['pausedAt', 'expiresAt', 'resumedAt']);
|
|
@@ -25,7 +25,9 @@ export class FilePauseStore {
|
|
|
25
25
|
}
|
|
26
26
|
async save(pause) {
|
|
27
27
|
await this.initialized;
|
|
28
|
-
|
|
28
|
+
// Pause files hold the captured variables needed to resume (so they can't
|
|
29
|
+
// be redacted); write owner-only (0o600) so they aren't world-readable.
|
|
30
|
+
await writeJsonFile(this.getFilePath(pause.id), pause, true, 0o600);
|
|
29
31
|
}
|
|
30
32
|
async load(id) {
|
|
31
33
|
await this.initialized;
|
|
@@ -3,7 +3,7 @@ import type { ScheduleDefinition, IntervalSchedule } from '../ast/nodes.js';
|
|
|
3
3
|
* Parse a cron expression and calculate the next run time
|
|
4
4
|
*
|
|
5
5
|
* Cron format: "minute hour day-of-month month day-of-week"
|
|
6
|
-
* Supports: numbers, ranges (1-5), steps (
|
|
6
|
+
* Supports: numbers, ranges (1-5), steps (* /5), lists (1,3,5), and wildcards (*)
|
|
7
7
|
*/
|
|
8
8
|
export declare function parseCronExpression(expression: string): CronSchedule;
|
|
9
9
|
interface CronSchedule {
|
|
@@ -12,11 +12,18 @@ interface CronSchedule {
|
|
|
12
12
|
dayOfMonth: number[];
|
|
13
13
|
month: number[];
|
|
14
14
|
dayOfWeek: number[];
|
|
15
|
+
/** True when day-of-month is not `*` (used for POSIX OR matching). */
|
|
16
|
+
dayOfMonthRestricted: boolean;
|
|
17
|
+
/** True when day-of-week is not `*` (used for POSIX OR matching). */
|
|
18
|
+
dayOfWeekRestricted: boolean;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
|
-
* Calculate the next run time for a cron schedule
|
|
21
|
+
* Calculate the next run time for a cron schedule, evaluated against
|
|
22
|
+
* wall-clock time in `timeZone` (default UTC, so DST never shifts the result).
|
|
23
|
+
* Day-of-month and day-of-week follow POSIX: when both are restricted the
|
|
24
|
+
* match is their union (OR), not their intersection.
|
|
18
25
|
*/
|
|
19
|
-
export declare function getNextCronRun(schedule: CronSchedule, after?: Date): Date;
|
|
26
|
+
export declare function getNextCronRun(schedule: CronSchedule, after?: Date, timeZone?: string): Date;
|
|
20
27
|
/**
|
|
21
28
|
* Convert interval schedule to milliseconds
|
|
22
29
|
*/
|