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
|
@@ -13,6 +13,13 @@ export function generateMockData(schema, options = {}) {
|
|
|
13
13
|
return generateValue(schema, ctx);
|
|
14
14
|
}
|
|
15
15
|
function generateValue(schema, ctx) {
|
|
16
|
+
// Depth guard: after dereferencing, a self-referential schema becomes a
|
|
17
|
+
// circular object reference (not a $ref), so the only thing that stops
|
|
18
|
+
// infinite recursion is the depth cap. Enforce it here for every path,
|
|
19
|
+
// including allOf/oneOf/anyOf below.
|
|
20
|
+
if (ctx.depth > ctx.maxDepth) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
16
23
|
// Handle nullable - only return null if explicitly marked nullable
|
|
17
24
|
// and we're past the top level (depth > 0). Use strict equality check
|
|
18
25
|
// to avoid false positives from undefined/truthy coercion.
|
|
@@ -34,12 +41,14 @@ function generateValue(schema, ctx) {
|
|
|
34
41
|
if (schema.default !== undefined) {
|
|
35
42
|
return schema.default;
|
|
36
43
|
}
|
|
37
|
-
// Check for allOf/oneOf/anyOf
|
|
44
|
+
// Check for allOf/oneOf/anyOf. Each recursion must increment depth so the
|
|
45
|
+
// depth cap can terminate self-referential combinator schemas.
|
|
46
|
+
const deeper = { ...ctx, depth: ctx.depth + 1 };
|
|
38
47
|
if (schema.allOf && schema.allOf.length > 0) {
|
|
39
48
|
// Merge all schemas
|
|
40
49
|
const merged = {};
|
|
41
50
|
for (const subSchema of schema.allOf) {
|
|
42
|
-
const subValue = generateValue(subSchema,
|
|
51
|
+
const subValue = generateValue(subSchema, deeper);
|
|
43
52
|
if (typeof subValue === 'object' && subValue !== null) {
|
|
44
53
|
Object.assign(merged, subValue);
|
|
45
54
|
}
|
|
@@ -47,10 +56,10 @@ function generateValue(schema, ctx) {
|
|
|
47
56
|
return merged;
|
|
48
57
|
}
|
|
49
58
|
if (schema.oneOf && schema.oneOf.length > 0) {
|
|
50
|
-
return generateValue(schema.oneOf[0],
|
|
59
|
+
return generateValue(schema.oneOf[0], deeper);
|
|
51
60
|
}
|
|
52
61
|
if (schema.anyOf && schema.anyOf.length > 0) {
|
|
53
|
-
return generateValue(schema.anyOf[0],
|
|
62
|
+
return generateValue(schema.anyOf[0], deeper);
|
|
54
63
|
}
|
|
55
64
|
// Generate based on type
|
|
56
65
|
switch (schema.type) {
|
package/dist/oas/validator.js
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upper bound on the length of a string we will test against a `pattern`
|
|
3
|
+
* regex. Both the schema pattern and the response value are untrusted, so a
|
|
4
|
+
* pathological pattern (e.g. `(a+)+$`) over a long string can trigger
|
|
5
|
+
* catastrophic backtracking. Anything longer is rejected without running the
|
|
6
|
+
* regex, so validation always returns promptly.
|
|
7
|
+
*/
|
|
8
|
+
const MAX_PATTERN_INPUT_LENGTH = 10_000;
|
|
9
|
+
/**
|
|
10
|
+
* Cache of compiled pattern regexes. Avoids recompiling the same (untrusted)
|
|
11
|
+
* pattern per value, and stores `null` for patterns that fail to compile so we
|
|
12
|
+
* skip them consistently.
|
|
13
|
+
*/
|
|
14
|
+
const patternRegexCache = new Map();
|
|
15
|
+
function getPatternRegex(pattern) {
|
|
16
|
+
const cached = patternRegexCache.get(pattern);
|
|
17
|
+
if (cached !== undefined)
|
|
18
|
+
return cached;
|
|
19
|
+
let regex;
|
|
20
|
+
try {
|
|
21
|
+
regex = new RegExp(pattern);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Invalid regex: don't crash validation, just skip the pattern check.
|
|
25
|
+
regex = null;
|
|
26
|
+
}
|
|
27
|
+
patternRegexCache.set(pattern, regex);
|
|
28
|
+
return regex;
|
|
29
|
+
}
|
|
1
30
|
export function validateResponse(data, schema, path = '') {
|
|
2
31
|
const errors = [];
|
|
3
32
|
validateValue(data, schema, path, errors);
|
|
@@ -79,15 +108,26 @@ function validateString(value, schema, path, errors) {
|
|
|
79
108
|
});
|
|
80
109
|
}
|
|
81
110
|
if (schema.pattern) {
|
|
82
|
-
|
|
83
|
-
|
|
111
|
+
if (value.length > MAX_PATTERN_INPUT_LENGTH) {
|
|
112
|
+
// Refuse to run an untrusted regex over an oversized string (ReDoS guard).
|
|
84
113
|
errors.push({
|
|
85
114
|
path,
|
|
86
|
-
message: `String
|
|
87
|
-
expected:
|
|
88
|
-
actual: value
|
|
115
|
+
message: `String too long to validate against pattern`,
|
|
116
|
+
expected: `<= ${MAX_PATTERN_INPUT_LENGTH} chars`,
|
|
117
|
+
actual: `${value.length} chars`,
|
|
89
118
|
});
|
|
90
119
|
}
|
|
120
|
+
else {
|
|
121
|
+
const regex = getPatternRegex(schema.pattern);
|
|
122
|
+
if (regex && !regex.test(value)) {
|
|
123
|
+
errors.push({
|
|
124
|
+
path,
|
|
125
|
+
message: `String does not match pattern`,
|
|
126
|
+
expected: schema.pattern,
|
|
127
|
+
actual: value,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
91
131
|
}
|
|
92
132
|
}
|
|
93
133
|
function validateNumber(value, schema, path, errors) {
|
|
@@ -16,7 +16,12 @@ export interface ObservabilityEvent<T = unknown> {
|
|
|
16
16
|
mission: string;
|
|
17
17
|
/** ISO timestamp */
|
|
18
18
|
timestamp: string;
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Duration in milliseconds of the operation this event reports, when the
|
|
21
|
+
* payload carries one. Not set by the emitter itself: a generic "time since
|
|
22
|
+
* the previous event of any type" value was misleading, so operation timing
|
|
23
|
+
* now lives in the typed payloads instead.
|
|
24
|
+
*/
|
|
20
25
|
duration?: number;
|
|
21
26
|
/** Event-specific payload */
|
|
22
27
|
payload: T;
|
|
@@ -227,7 +232,6 @@ export declare class ObservabilityEmitter implements EventEmitter {
|
|
|
227
232
|
private handlers;
|
|
228
233
|
private allHandlers;
|
|
229
234
|
private startTime;
|
|
230
|
-
private lastEventTime;
|
|
231
235
|
constructor(executionId: string, mission: string);
|
|
232
236
|
emit<T>(type: EventType, payload: T): void;
|
|
233
237
|
on<T>(type: EventType, handler: EventHandler<T>): () => void;
|
|
@@ -13,24 +13,19 @@ export class ObservabilityEmitter {
|
|
|
13
13
|
handlers = new Map();
|
|
14
14
|
allHandlers = new Set();
|
|
15
15
|
startTime;
|
|
16
|
-
lastEventTime;
|
|
17
16
|
constructor(executionId, mission) {
|
|
18
17
|
this.executionId = executionId;
|
|
19
18
|
this.mission = mission;
|
|
20
19
|
this.startTime = Date.now();
|
|
21
|
-
this.lastEventTime = this.startTime;
|
|
22
20
|
}
|
|
23
21
|
emit(type, payload) {
|
|
24
|
-
const now = Date.now();
|
|
25
22
|
const event = {
|
|
26
23
|
type,
|
|
27
24
|
executionId: this.executionId,
|
|
28
25
|
mission: this.mission,
|
|
29
26
|
timestamp: new Date().toISOString(),
|
|
30
|
-
duration: now - this.lastEventTime,
|
|
31
27
|
payload,
|
|
32
28
|
};
|
|
33
|
-
this.lastEventTime = now;
|
|
34
29
|
// Notify specific handlers
|
|
35
30
|
const typeHandlers = this.handlers.get(type);
|
|
36
31
|
if (typeHandlers) {
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* - Multiple output formats
|
|
8
8
|
* - Integration with event emitter
|
|
9
9
|
*/
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
11
|
+
import { redactSecrets } from '../utils/redact.js';
|
|
10
12
|
// ============================================================================
|
|
11
13
|
// Span Implementation
|
|
12
14
|
// ============================================================================
|
|
@@ -41,7 +43,13 @@ class SpanImpl {
|
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
function generateSpanId() {
|
|
44
|
-
|
|
46
|
+
// CSPRNG-backed, 16 hex chars. Never all-zero (the OTel "invalid" sentinel).
|
|
47
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
48
|
+
const hex = randomBytes(8).toString('hex');
|
|
49
|
+
if (!/^0+$/.test(hex))
|
|
50
|
+
return hex;
|
|
51
|
+
}
|
|
52
|
+
return '1'.padEnd(16, '0');
|
|
45
53
|
}
|
|
46
54
|
// ============================================================================
|
|
47
55
|
// Logger Implementation
|
|
@@ -73,7 +81,7 @@ class StructuredLoggerImpl {
|
|
|
73
81
|
level,
|
|
74
82
|
message,
|
|
75
83
|
timestamp: new Date().toISOString(),
|
|
76
|
-
context: { ...this.context, ...context },
|
|
84
|
+
context: redactSecrets({ ...this.context, ...context }),
|
|
77
85
|
};
|
|
78
86
|
for (const output of this.outputs) {
|
|
79
87
|
try {
|
|
@@ -116,7 +124,7 @@ class StructuredLoggerImpl {
|
|
|
116
124
|
level: 'debug',
|
|
117
125
|
message: `span:end`,
|
|
118
126
|
timestamp: new Date().toISOString(),
|
|
119
|
-
context: { ...this.context, spanName: name, ...spanContext },
|
|
127
|
+
context: redactSecrets({ ...this.context, spanName: name, ...spanContext }),
|
|
120
128
|
spanId,
|
|
121
129
|
parentSpanId,
|
|
122
130
|
duration,
|
|
@@ -154,13 +162,9 @@ export class ConsoleOutput {
|
|
|
154
162
|
const prefix = `[${this.prefix}]`;
|
|
155
163
|
const levelStr = entry.level.toUpperCase().padEnd(5);
|
|
156
164
|
// Format context as key=value pairs
|
|
157
|
-
const contextStr = Object.keys(entry.context).length > 0
|
|
158
|
-
? ` ${formatContext(entry.context)}`
|
|
159
|
-
: '';
|
|
165
|
+
const contextStr = Object.keys(entry.context).length > 0 ? ` ${formatContext(entry.context)}` : '';
|
|
160
166
|
// Format duration if present
|
|
161
|
-
const durationStr = entry.duration !== undefined
|
|
162
|
-
? ` (${entry.duration}ms)`
|
|
163
|
-
: '';
|
|
167
|
+
const durationStr = entry.duration !== undefined ? ` (${entry.duration}ms)` : '';
|
|
164
168
|
const message = `${prefix} ${levelStr} ${entry.message}${contextStr}${durationStr}`;
|
|
165
169
|
switch (entry.level) {
|
|
166
170
|
case 'debug':
|
|
@@ -218,7 +222,10 @@ export class EventOutput {
|
|
|
218
222
|
// Map log entries to appropriate event types based on context
|
|
219
223
|
// This allows logs to flow into the event system
|
|
220
224
|
if (entry.context.eventType) {
|
|
221
|
-
this.emitter.emit(entry.context.eventType, {
|
|
225
|
+
this.emitter.emit(entry.context.eventType, {
|
|
226
|
+
...entry.context,
|
|
227
|
+
message: entry.message,
|
|
228
|
+
});
|
|
222
229
|
}
|
|
223
230
|
}
|
|
224
231
|
}
|
|
@@ -79,6 +79,13 @@ export declare class OTelEventAdapter {
|
|
|
79
79
|
private spanBuilder;
|
|
80
80
|
private spanStack;
|
|
81
81
|
private eventToSpan;
|
|
82
|
+
/**
|
|
83
|
+
* Pending fetch spans keyed by source+path. Fetch start/complete events
|
|
84
|
+
* don't carry a correlation id, so we queue the open span ids per endpoint
|
|
85
|
+
* (FIFO) instead of a single shared `fetch:current` slot. This keeps
|
|
86
|
+
* concurrent/paginated fetches from overwriting and leaking each other.
|
|
87
|
+
*/
|
|
88
|
+
private pendingFetchSpans;
|
|
82
89
|
constructor(traceId?: string);
|
|
83
90
|
/**
|
|
84
91
|
* Process an observability event and update spans
|
|
@@ -90,6 +97,7 @@ export declare class OTelEventAdapter {
|
|
|
90
97
|
private endStageSpan;
|
|
91
98
|
private startStepSpan;
|
|
92
99
|
private endStepSpan;
|
|
100
|
+
private fetchKey;
|
|
93
101
|
private startFetchSpan;
|
|
94
102
|
private endFetchSpan;
|
|
95
103
|
private addEventToCurrentSpan;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* This is a lightweight implementation that doesn't require the full OTel SDK.
|
|
8
8
|
* For production use, consider using the official @opentelemetry packages.
|
|
9
9
|
*/
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// Trace Context Management
|
|
12
13
|
// ============================================================================
|
|
@@ -22,12 +23,21 @@ export function generateTraceId() {
|
|
|
22
23
|
export function generateSpanId() {
|
|
23
24
|
return randomHex(16);
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Generate `length` random hex characters using a CSPRNG. OTel trace/span IDs
|
|
28
|
+
* must be unpredictable and must never be all-zero (the all-zero ID is the
|
|
29
|
+
* "invalid" sentinel in the spec), so we regenerate in the vanishingly
|
|
30
|
+
* unlikely event of an all-zero draw.
|
|
31
|
+
*/
|
|
25
32
|
function randomHex(length) {
|
|
26
|
-
|
|
27
|
-
for (let
|
|
28
|
-
|
|
33
|
+
const byteLength = Math.ceil(length / 2);
|
|
34
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
35
|
+
const hex = randomBytes(byteLength).toString('hex').slice(0, length);
|
|
36
|
+
if (!/^0+$/.test(hex))
|
|
37
|
+
return hex;
|
|
29
38
|
}
|
|
30
|
-
|
|
39
|
+
// Fallback (practically unreachable): force a non-zero leading nibble.
|
|
40
|
+
return '1'.padEnd(length, '0');
|
|
31
41
|
}
|
|
32
42
|
// ============================================================================
|
|
33
43
|
// OpenTelemetry Span Builder
|
|
@@ -112,6 +122,13 @@ export class OTelEventAdapter {
|
|
|
112
122
|
spanBuilder;
|
|
113
123
|
spanStack = [];
|
|
114
124
|
eventToSpan = new Map();
|
|
125
|
+
/**
|
|
126
|
+
* Pending fetch spans keyed by source+path. Fetch start/complete events
|
|
127
|
+
* don't carry a correlation id, so we queue the open span ids per endpoint
|
|
128
|
+
* (FIFO) instead of a single shared `fetch:current` slot. This keeps
|
|
129
|
+
* concurrent/paginated fetches from overwriting and leaking each other.
|
|
130
|
+
*/
|
|
131
|
+
pendingFetchSpans = new Map();
|
|
115
132
|
constructor(traceId) {
|
|
116
133
|
this.spanBuilder = new SpanBuilder(traceId);
|
|
117
134
|
}
|
|
@@ -119,7 +136,7 @@ export class OTelEventAdapter {
|
|
|
119
136
|
* Process an observability event and update spans
|
|
120
137
|
*/
|
|
121
138
|
processEvent(event) {
|
|
122
|
-
const { type
|
|
139
|
+
const { type } = event;
|
|
123
140
|
// Map event types to span operations
|
|
124
141
|
switch (type) {
|
|
125
142
|
case 'mission.start':
|
|
@@ -173,6 +190,7 @@ export class OTelEventAdapter {
|
|
|
173
190
|
error: payload.error,
|
|
174
191
|
});
|
|
175
192
|
this.spanStack.pop();
|
|
193
|
+
this.eventToSpan.delete('mission');
|
|
176
194
|
}
|
|
177
195
|
}
|
|
178
196
|
startStageSpan(event) {
|
|
@@ -198,6 +216,7 @@ export class OTelEventAdapter {
|
|
|
198
216
|
error: payload.error,
|
|
199
217
|
});
|
|
200
218
|
this.spanStack.pop();
|
|
219
|
+
this.eventToSpan.delete(`stage:${payload.stageIndex}`);
|
|
201
220
|
}
|
|
202
221
|
}
|
|
203
222
|
startStepSpan(event) {
|
|
@@ -217,15 +236,20 @@ export class OTelEventAdapter {
|
|
|
217
236
|
}
|
|
218
237
|
endStepSpan(event) {
|
|
219
238
|
const payload = event.payload;
|
|
220
|
-
const
|
|
239
|
+
const key = `step:${payload.actionName}:${payload.stepIndex}`;
|
|
240
|
+
const spanId = this.eventToSpan.get(key);
|
|
221
241
|
if (spanId) {
|
|
222
242
|
this.spanBuilder.endSpan(spanId, {
|
|
223
243
|
status: payload.success ? 'OK' : 'ERROR',
|
|
224
244
|
error: payload.error,
|
|
225
245
|
});
|
|
226
246
|
this.spanStack.pop();
|
|
247
|
+
this.eventToSpan.delete(key);
|
|
227
248
|
}
|
|
228
249
|
}
|
|
250
|
+
fetchKey(source, path) {
|
|
251
|
+
return `${source ?? ''}${path ?? ''}`;
|
|
252
|
+
}
|
|
229
253
|
startFetchSpan(event) {
|
|
230
254
|
const payload = event.payload;
|
|
231
255
|
const parentSpanId = this.spanStack[this.spanStack.length - 1];
|
|
@@ -238,18 +262,29 @@ export class OTelEventAdapter {
|
|
|
238
262
|
'reqon.source': payload.source,
|
|
239
263
|
},
|
|
240
264
|
});
|
|
241
|
-
this.
|
|
265
|
+
const key = this.fetchKey(payload.source, payload.path);
|
|
266
|
+
const queue = this.pendingFetchSpans.get(key);
|
|
267
|
+
if (queue) {
|
|
268
|
+
queue.push(spanId);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
this.pendingFetchSpans.set(key, [spanId]);
|
|
272
|
+
}
|
|
242
273
|
}
|
|
243
274
|
endFetchSpan(event) {
|
|
244
|
-
const
|
|
275
|
+
const payload = event.payload;
|
|
276
|
+
const key = this.fetchKey(payload.source, payload.path);
|
|
277
|
+
const queue = this.pendingFetchSpans.get(key);
|
|
278
|
+
const spanId = queue?.shift();
|
|
279
|
+
if (queue && queue.length === 0) {
|
|
280
|
+
this.pendingFetchSpans.delete(key);
|
|
281
|
+
}
|
|
245
282
|
if (spanId) {
|
|
246
|
-
const payload = event.payload;
|
|
247
283
|
this.spanBuilder.endSpan(spanId, {
|
|
248
284
|
status: payload.error ? 'ERROR' : 'OK',
|
|
249
285
|
error: payload.error,
|
|
250
286
|
attributes: payload.statusCode ? { 'http.status_code': payload.statusCode } : undefined,
|
|
251
287
|
});
|
|
252
|
-
this.eventToSpan.delete('fetch:current');
|
|
253
288
|
}
|
|
254
289
|
}
|
|
255
290
|
addEventToCurrentSpan(event) {
|
|
@@ -115,7 +115,7 @@ export class ActionParser extends FetchParser {
|
|
|
115
115
|
this.consume(TokenType.LBRACE, "Expected '{'");
|
|
116
116
|
const mappings = [];
|
|
117
117
|
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
118
|
-
const field = this.
|
|
118
|
+
const field = this.consumeName('Expected field name').value;
|
|
119
119
|
this.consume(TokenType.COLON, "Expected ':'");
|
|
120
120
|
const expression = this.parseExpression();
|
|
121
121
|
mappings.push({ field, expression });
|
|
@@ -202,7 +202,7 @@ export class ActionParser extends FetchParser {
|
|
|
202
202
|
this.consume(TokenType.LBRACE, "Expected '{'");
|
|
203
203
|
const mappings = [];
|
|
204
204
|
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
205
|
-
const field = this.
|
|
205
|
+
const field = this.consumeName('Expected field name').value;
|
|
206
206
|
this.consume(TokenType.COLON, "Expected ':'");
|
|
207
207
|
const expression = this.parseExpression();
|
|
208
208
|
mappings.push({ field, expression });
|
package/dist/parser/base.d.ts
CHANGED
|
@@ -21,6 +21,13 @@ export declare class ReqonParserBase {
|
|
|
21
21
|
* This is needed because 'get', 'post', etc. are valid variable/store names.
|
|
22
22
|
*/
|
|
23
23
|
protected consumeIdentifier(message: string): Token;
|
|
24
|
+
/**
|
|
25
|
+
* Consume a token in name/property position (after a dot, a map/transform field
|
|
26
|
+
* name, or the type name after `is`). Any reserved keyword's text is a valid name
|
|
27
|
+
* here because the position is unambiguous, so accept any non-EOF token and return
|
|
28
|
+
* it by value.
|
|
29
|
+
*/
|
|
30
|
+
protected consumeName(message: string): Token;
|
|
24
31
|
/**
|
|
25
32
|
* Check if current token is an identifier (including HTTP methods as identifiers)
|
|
26
33
|
*/
|
package/dist/parser/base.js
CHANGED
|
@@ -66,6 +66,17 @@ export class ReqonParserBase {
|
|
|
66
66
|
}
|
|
67
67
|
throw this.error(message);
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Consume a token in name/property position (after a dot, a map/transform field
|
|
71
|
+
* name, or the type name after `is`). Any reserved keyword's text is a valid name
|
|
72
|
+
* here because the position is unambiguous, so accept any non-EOF token and return
|
|
73
|
+
* it by value.
|
|
74
|
+
*/
|
|
75
|
+
consumeName(message) {
|
|
76
|
+
if (this.isAtEnd())
|
|
77
|
+
throw this.error(message);
|
|
78
|
+
return this.advance();
|
|
79
|
+
}
|
|
69
80
|
/**
|
|
70
81
|
* Check if current token is an identifier (including HTTP methods as identifiers)
|
|
71
82
|
*/
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { TokenType } from 'vague-lang';
|
|
2
2
|
import { ReqonTokenType } from '../lexer/tokens.js';
|
|
3
3
|
import { ReqonParserBase } from './base.js';
|
|
4
|
+
// Guards unbounded recursive descent from blowing the JS stack on deeply nested
|
|
5
|
+
// input, surfacing a structured ParseError instead of a raw V8 RangeError.
|
|
6
|
+
const MAX_EXPRESSION_DEPTH = 200;
|
|
4
7
|
export class ReqonExpressionParser extends ReqonParserBase {
|
|
8
|
+
expressionDepth = 0;
|
|
5
9
|
parseExpression() {
|
|
6
|
-
|
|
10
|
+
if (++this.expressionDepth > MAX_EXPRESSION_DEPTH) {
|
|
11
|
+
this.expressionDepth--;
|
|
12
|
+
throw this.error('Expression nesting too deep (maximum depth 200 exceeded)');
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return this.parseTernary();
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
this.expressionDepth--;
|
|
19
|
+
}
|
|
7
20
|
}
|
|
8
21
|
parseLogicalExpression() {
|
|
9
22
|
return this.parseOr();
|
|
@@ -118,7 +131,7 @@ export class ReqonExpressionParser extends ReqonParserBase {
|
|
|
118
131
|
// Check for 'is' type checking: expr is array, expr is string, etc.
|
|
119
132
|
if (this.check(ReqonTokenType.IS)) {
|
|
120
133
|
this.advance(); // consume 'is'
|
|
121
|
-
const typeCheck = this.
|
|
134
|
+
const typeCheck = this.consumeName("Expected type name after 'is'").value;
|
|
122
135
|
return { type: 'IsExpression', operand: left, typeCheck };
|
|
123
136
|
}
|
|
124
137
|
return left;
|
|
@@ -180,7 +193,7 @@ export class ReqonExpressionParser extends ReqonParserBase {
|
|
|
180
193
|
}
|
|
181
194
|
}
|
|
182
195
|
else if (this.match(TokenType.DOT)) {
|
|
183
|
-
const name = this.
|
|
196
|
+
const name = this.consumeName('Expected property name').value;
|
|
184
197
|
if (expr.type === 'Identifier') {
|
|
185
198
|
expr = { type: 'QualifiedName', parts: [expr.name, name] };
|
|
186
199
|
}
|
|
@@ -251,7 +264,7 @@ export class ReqonExpressionParser extends ReqonParserBase {
|
|
|
251
264
|
}
|
|
252
265
|
// .field shorthand
|
|
253
266
|
if (this.match(TokenType.DOT)) {
|
|
254
|
-
const name = this.
|
|
267
|
+
const name = this.consumeName("Expected field name after '.'").value;
|
|
255
268
|
return { type: 'Identifier', name };
|
|
256
269
|
}
|
|
257
270
|
throw this.error(`Unexpected token: ${this.peek().value}`);
|
|
@@ -80,6 +80,7 @@ export class FetchParser extends ScheduleParser {
|
|
|
80
80
|
let until;
|
|
81
81
|
let retry;
|
|
82
82
|
let since;
|
|
83
|
+
let backfill;
|
|
83
84
|
if (this.match(TokenType.LBRACE)) {
|
|
84
85
|
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
85
86
|
const key = this.parseFetchOptionKey();
|
|
@@ -103,6 +104,12 @@ export class FetchParser extends ScheduleParser {
|
|
|
103
104
|
case 'since':
|
|
104
105
|
since = this.parseSinceConfig();
|
|
105
106
|
break;
|
|
107
|
+
case 'backfill':
|
|
108
|
+
backfill = this.match(TokenType.TRUE);
|
|
109
|
+
if (!backfill) {
|
|
110
|
+
this.consume(TokenType.FALSE, "Expected 'true' or 'false'");
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
106
113
|
default:
|
|
107
114
|
throw this.error(`Unknown fetch option: ${key}`);
|
|
108
115
|
}
|
|
@@ -110,7 +117,7 @@ export class FetchParser extends ScheduleParser {
|
|
|
110
117
|
}
|
|
111
118
|
this.consume(TokenType.RBRACE, "Expected '}'");
|
|
112
119
|
}
|
|
113
|
-
return { source, body, headers, paginate, until, retry, since };
|
|
120
|
+
return { source, body, headers, paginate, until, retry, since, backfill };
|
|
114
121
|
}
|
|
115
122
|
/**
|
|
116
123
|
* Parse a fetch option key, handling keyword tokens that can appear as keys
|
|
@@ -243,6 +250,7 @@ export class FetchParser extends ScheduleParser {
|
|
|
243
250
|
let backoff = 'exponential';
|
|
244
251
|
let initialDelay = 1000;
|
|
245
252
|
let maxDelay;
|
|
253
|
+
let timeout;
|
|
246
254
|
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
247
255
|
const key = this.consume(TokenType.IDENTIFIER, 'Expected retry option').value;
|
|
248
256
|
this.consume(TokenType.COLON, "Expected ':'");
|
|
@@ -260,10 +268,13 @@ export class FetchParser extends ScheduleParser {
|
|
|
260
268
|
case 'maxDelay':
|
|
261
269
|
maxDelay = parseInt(this.consume(TokenType.NUMBER, 'Expected number').value, 10);
|
|
262
270
|
break;
|
|
271
|
+
case 'timeout':
|
|
272
|
+
timeout = parseInt(this.consume(TokenType.NUMBER, 'Expected number').value, 10);
|
|
273
|
+
break;
|
|
263
274
|
}
|
|
264
275
|
this.match(TokenType.COMMA);
|
|
265
276
|
}
|
|
266
277
|
this.consume(TokenType.RBRACE, "Expected '}'");
|
|
267
|
-
return { maxAttempts, backoff, initialDelay, maxDelay };
|
|
278
|
+
return { maxAttempts, backoff, initialDelay, maxDelay, timeout };
|
|
268
279
|
}
|
|
269
280
|
}
|
package/dist/pause/index.d.ts
CHANGED
|
@@ -10,5 +10,6 @@ export type { PauseState, PauseResumeTriggerState, PauseCheckpoint } from './sta
|
|
|
10
10
|
export { generatePauseId, parseDuration, formatDuration, createPauseState, isPauseExpired, getRemainingTime, getPauseSummary, } from './state.js';
|
|
11
11
|
export type { PauseStore } from './store.js';
|
|
12
12
|
export { FilePauseStore, MemoryPauseStore } from './store.js';
|
|
13
|
+
export { LogBackedPauseStore } from './log-store.js';
|
|
13
14
|
export type { PauseManagerConfig, CreatePauseOptions, PauseStatus } from './manager.js';
|
|
14
15
|
export { PauseManager, createPauseManager } from './manager.js';
|
package/dist/pause/index.js
CHANGED
|
@@ -8,4 +8,5 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { generatePauseId, parseDuration, formatDuration, createPauseState, isPauseExpired, getRemainingTime, getPauseSummary, } from './state.js';
|
|
10
10
|
export { FilePauseStore, MemoryPauseStore } from './store.js';
|
|
11
|
+
export { LogBackedPauseStore } from './log-store.js';
|
|
11
12
|
export { PauseManager, createPauseManager } from './manager.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log-backed pause store: resource-free long pauses as a view over the
|
|
3
|
+
* execution log.
|
|
4
|
+
*
|
|
5
|
+
* In durable mode the execution log holds the full pause record — a
|
|
6
|
+
* `pause.created` event carries the pause state (deadline, resume triggers,
|
|
7
|
+
* captured checkpoint), and `pause.resumed` records how and when it ended. A
|
|
8
|
+
* paused run, and the timer or webhook it waits on, are therefore reconstructable
|
|
9
|
+
* from the log alone, with no separate pause file. This adapter folds those
|
|
10
|
+
* events back into {@link PauseState}.
|
|
11
|
+
*/
|
|
12
|
+
import type { ExecutionLogStore } from '../execution-log/index.js';
|
|
13
|
+
import type { PauseStore } from './store.js';
|
|
14
|
+
import type { PauseState } from './state.js';
|
|
15
|
+
export declare class LogBackedPauseStore implements PauseStore {
|
|
16
|
+
private log;
|
|
17
|
+
constructor(log: ExecutionLogStore);
|
|
18
|
+
save(pause: PauseState): Promise<void>;
|
|
19
|
+
update(id: string, updates: Partial<PauseState>): Promise<void>;
|
|
20
|
+
load(id: string): Promise<PauseState | null>;
|
|
21
|
+
loadByExecution(executionId: string): Promise<PauseState | null>;
|
|
22
|
+
listActive(): Promise<PauseState[]>;
|
|
23
|
+
listByMission(mission: string): Promise<PauseState[]>;
|
|
24
|
+
findExpired(): Promise<PauseState[]>;
|
|
25
|
+
/** Append-only: deletion is a no-op (history is retained in the log). */
|
|
26
|
+
delete(): Promise<void>;
|
|
27
|
+
private allPauses;
|
|
28
|
+
/**
|
|
29
|
+
* Fold pause events into the latest state per pause id. A `pause.created`
|
|
30
|
+
* seeds the state; later `pause.resumed` events apply the terminal status.
|
|
31
|
+
*/
|
|
32
|
+
private foldPauses;
|
|
33
|
+
}
|