@usejourney/core 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/LICENSE +21 -0
- package/dist/index.d.ts +696 -0
- package/dist/index.js +1217 -0
- package/package.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
// src/endpoint.ts
|
|
2
|
+
function isEndpointRef(e) {
|
|
3
|
+
return typeof e.operationId === "string";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
8
|
+
import { dirname, isAbsolute, join as join2 } from "path";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
// src/env.ts
|
|
12
|
+
import { readFile, readdir } from "fs/promises";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
var KEY = /* @__PURE__ */ Symbol.for("@usejourney/core::env-state");
|
|
15
|
+
var globals = globalThis;
|
|
16
|
+
var shared = globals[KEY] ?? (globals[KEY] = {});
|
|
17
|
+
function setActiveEnvironment(name, values) {
|
|
18
|
+
shared.active = { name, values };
|
|
19
|
+
}
|
|
20
|
+
function clearActiveEnvironment() {
|
|
21
|
+
delete shared.active;
|
|
22
|
+
}
|
|
23
|
+
function env(key) {
|
|
24
|
+
if (!shared.active) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`env(${JSON.stringify(key)}) called with no active environment. Pass --env <name> or set one via setActiveEnvironment().`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const value = shared.active.values[key];
|
|
30
|
+
if (value === void 0) {
|
|
31
|
+
throw new Error(`env: key "${key}" not found in environment "${shared.active.name}"`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function tryEnv(key) {
|
|
36
|
+
return shared.active?.values[key];
|
|
37
|
+
}
|
|
38
|
+
async function loadEnvironment(environmentsDir, name) {
|
|
39
|
+
const path = join(environmentsDir, `${name}.json`);
|
|
40
|
+
let raw;
|
|
41
|
+
try {
|
|
42
|
+
raw = await readFile(path, "utf8");
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new Error(`Could not read environment file ${path}: ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(raw);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
throw new Error(`Environment file ${path} is not valid JSON: ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
53
|
+
throw new Error(`Environment file ${path} must contain a JSON object`);
|
|
54
|
+
}
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
57
|
+
out[k] = typeof v === "string" ? v : JSON.stringify(v);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
async function listEnvironments(environmentsDir) {
|
|
62
|
+
try {
|
|
63
|
+
const entries = await readdir(environmentsDir);
|
|
64
|
+
return entries.filter((e) => e.endsWith(".json")).map((e) => e.slice(0, -".json".length)).sort();
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (err.code === "ENOENT") return [];
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/config.ts
|
|
72
|
+
var JourneyConfigSchema = z.object({
|
|
73
|
+
name: z.string().min(1).optional(),
|
|
74
|
+
spec: z.string().min(1).default("openapi.yaml"),
|
|
75
|
+
generatedDir: z.string().min(1).default("generated"),
|
|
76
|
+
journeysDir: z.string().min(1).default("journeys"),
|
|
77
|
+
environmentsDir: z.string().min(1).default("environments"),
|
|
78
|
+
defaultEnvironment: z.string().min(1).optional(),
|
|
79
|
+
baseUrl: z.string().url().optional(),
|
|
80
|
+
runHistoryKeepCount: z.number().int().min(0).default(20),
|
|
81
|
+
tlsRejectUnauthorized: z.boolean().default(true)
|
|
82
|
+
}).strict();
|
|
83
|
+
function resolveRelative(projectDir, value) {
|
|
84
|
+
return isAbsolute(value) ? value : join2(projectDir, value);
|
|
85
|
+
}
|
|
86
|
+
function resolveBaseUrl(config) {
|
|
87
|
+
return config.baseUrl ?? tryEnv("BASE_URL");
|
|
88
|
+
}
|
|
89
|
+
function resolveConfigPaths(loaded) {
|
|
90
|
+
const { config, projectDir } = loaded;
|
|
91
|
+
return {
|
|
92
|
+
specPath: resolveRelative(projectDir, config.spec),
|
|
93
|
+
generatedDir: resolveRelative(projectDir, config.generatedDir),
|
|
94
|
+
journeysDir: resolveRelative(projectDir, config.journeysDir),
|
|
95
|
+
environmentsDir: resolveRelative(projectDir, config.environmentsDir)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function loadConfig(projectDir) {
|
|
99
|
+
const configPath = join2(projectDir, "journey.config.json");
|
|
100
|
+
let raw;
|
|
101
|
+
try {
|
|
102
|
+
raw = await readFile2(configPath, "utf8");
|
|
103
|
+
} catch (err) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Could not read journey.config.json at ${configPath}: ${err.message}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(raw);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
throw new Error(`journey.config.json is not valid JSON: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
const result = JourneyConfigSchema.safeParse(parsed);
|
|
115
|
+
if (!result.success) {
|
|
116
|
+
const issues = result.error.issues.map((i) => ` - ${i.path.join(".") || "<root>"}: ${i.message}`).join("\n");
|
|
117
|
+
throw new Error(`journey.config.json failed validation:
|
|
118
|
+
${issues}`);
|
|
119
|
+
}
|
|
120
|
+
return { config: result.data, projectDir, configPath };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/expect.ts
|
|
124
|
+
var AssertionError = class extends Error {
|
|
125
|
+
constructor(message) {
|
|
126
|
+
super(message);
|
|
127
|
+
this.name = "AssertionError";
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
function format(value) {
|
|
131
|
+
try {
|
|
132
|
+
return JSON.stringify(value);
|
|
133
|
+
} catch {
|
|
134
|
+
return String(value);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function deepEqual(a, b) {
|
|
138
|
+
if (Object.is(a, b)) return true;
|
|
139
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
140
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
141
|
+
const ak = Object.keys(a);
|
|
142
|
+
const bk = Object.keys(b);
|
|
143
|
+
if (ak.length !== bk.length) return false;
|
|
144
|
+
for (const k of ak) {
|
|
145
|
+
if (!deepEqual(a[k], b[k])) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
function expect(value) {
|
|
152
|
+
return {
|
|
153
|
+
toBe(expected) {
|
|
154
|
+
if (!Object.is(value, expected)) {
|
|
155
|
+
throw new AssertionError(`expected ${format(value)} to be ${format(expected)}`);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
toEqual(expected) {
|
|
159
|
+
if (!deepEqual(value, expected)) {
|
|
160
|
+
throw new AssertionError(`expected ${format(value)} to equal ${format(expected)}`);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
toBeDefined() {
|
|
164
|
+
if (value === void 0) {
|
|
165
|
+
throw new AssertionError(`expected value to be defined`);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
toContain(expected) {
|
|
169
|
+
if (typeof value === "string") {
|
|
170
|
+
if (typeof expected !== "string" || !value.includes(expected)) {
|
|
171
|
+
throw new AssertionError(`expected ${format(value)} to contain ${format(expected)}`);
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
if (!value.some((v) => deepEqual(v, expected))) {
|
|
177
|
+
throw new AssertionError(`expected array to contain ${format(expected)}`);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
throw new AssertionError(`toContain is only supported on strings and arrays`);
|
|
182
|
+
},
|
|
183
|
+
toMatch(expected) {
|
|
184
|
+
if (typeof value !== "string") {
|
|
185
|
+
throw new AssertionError(`toMatch is only supported on strings`);
|
|
186
|
+
}
|
|
187
|
+
const re = typeof expected === "string" ? new RegExp(expected) : expected;
|
|
188
|
+
if (!re.test(value)) {
|
|
189
|
+
throw new AssertionError(`expected ${format(value)} to match ${re}`);
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
toBeGreaterThan(n) {
|
|
193
|
+
assertNumber(value, "toBeGreaterThan");
|
|
194
|
+
if (!(value > n)) {
|
|
195
|
+
throw new AssertionError(`expected ${format(value)} to be greater than ${n}`);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
toBeGreaterThanOrEqual(n) {
|
|
199
|
+
assertNumber(value, "toBeGreaterThanOrEqual");
|
|
200
|
+
if (!(value >= n)) {
|
|
201
|
+
throw new AssertionError(`expected ${format(value)} to be greater than or equal to ${n}`);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
toBeLessThan(n) {
|
|
205
|
+
assertNumber(value, "toBeLessThan");
|
|
206
|
+
if (!(value < n)) {
|
|
207
|
+
throw new AssertionError(`expected ${format(value)} to be less than ${n}`);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
toBeLessThanOrEqual(n) {
|
|
211
|
+
assertNumber(value, "toBeLessThanOrEqual");
|
|
212
|
+
if (!(value <= n)) {
|
|
213
|
+
throw new AssertionError(`expected ${format(value)} to be less than or equal to ${n}`);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
toHaveLength(n) {
|
|
217
|
+
const len = value?.length;
|
|
218
|
+
if (typeof len !== "number") {
|
|
219
|
+
throw new AssertionError(
|
|
220
|
+
`toHaveLength is only supported on strings, arrays, and objects with a numeric .length`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (len !== n) {
|
|
224
|
+
throw new AssertionError(`expected ${format(value)} to have length ${n}, got ${len}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function assertNumber(value, matcher) {
|
|
230
|
+
if (typeof value !== "number") {
|
|
231
|
+
throw new AssertionError(`${matcher} is only supported on numbers (got ${typeof value})`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/history.ts
|
|
236
|
+
import { mkdir, readdir as readdir2, readFile as readFile3, unlink, writeFile } from "fs/promises";
|
|
237
|
+
import { join as join3 } from "path";
|
|
238
|
+
function makeId() {
|
|
239
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
240
|
+
const id = ts.replace(/:/g, "-");
|
|
241
|
+
return { id, timestamp: ts };
|
|
242
|
+
}
|
|
243
|
+
async function writeRun(cacheDir, results) {
|
|
244
|
+
const runsDir = join3(cacheDir, "runs");
|
|
245
|
+
await mkdir(runsDir, { recursive: true });
|
|
246
|
+
const { id, timestamp } = makeId();
|
|
247
|
+
const record = { id, timestamp, results };
|
|
248
|
+
await writeFile(join3(runsDir, `${id}.run.json`), JSON.stringify(record, null, 2), "utf8");
|
|
249
|
+
return record;
|
|
250
|
+
}
|
|
251
|
+
async function listRuns(cacheDir) {
|
|
252
|
+
const runsDir = join3(cacheDir, "runs");
|
|
253
|
+
let files;
|
|
254
|
+
try {
|
|
255
|
+
files = await readdir2(runsDir);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
if (err.code === "ENOENT") return [];
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
const runs = [];
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
if (!file.endsWith(".run.json")) continue;
|
|
263
|
+
try {
|
|
264
|
+
const raw = await readFile3(join3(runsDir, file), "utf8");
|
|
265
|
+
const record = JSON.parse(raw);
|
|
266
|
+
runs.push({
|
|
267
|
+
id: record.id,
|
|
268
|
+
timestamp: record.timestamp,
|
|
269
|
+
journeyNames: record.results.map((r) => r.name),
|
|
270
|
+
ok: record.results.every((r) => r.ok),
|
|
271
|
+
durationMs: record.results.reduce((a, r) => a + (r.durationMs ?? 0), 0),
|
|
272
|
+
stepCount: record.results.reduce((a, r) => a + r.steps.length, 0)
|
|
273
|
+
});
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
runs.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
278
|
+
return runs;
|
|
279
|
+
}
|
|
280
|
+
async function readRun(cacheDir, id) {
|
|
281
|
+
const file = join3(cacheDir, "runs", `${id}.run.json`);
|
|
282
|
+
try {
|
|
283
|
+
const raw = await readFile3(file, "utf8");
|
|
284
|
+
return JSON.parse(raw);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (err.code === "ENOENT") return void 0;
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function pruneRuns(cacheDir, keep) {
|
|
291
|
+
if (keep <= 0) return 0;
|
|
292
|
+
const runs = await listRuns(cacheDir);
|
|
293
|
+
if (runs.length <= keep) return 0;
|
|
294
|
+
const toDelete = runs.slice(keep);
|
|
295
|
+
const runsDir = join3(cacheDir, "runs");
|
|
296
|
+
let deleted = 0;
|
|
297
|
+
for (const run of toDelete) {
|
|
298
|
+
try {
|
|
299
|
+
await unlink(join3(runsDir, `${run.id}.run.json`));
|
|
300
|
+
deleted++;
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return deleted;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/logger.ts
|
|
308
|
+
var SECRET_HEADERS = [
|
|
309
|
+
"authorization",
|
|
310
|
+
"cookie",
|
|
311
|
+
"set-cookie",
|
|
312
|
+
"x-api-key",
|
|
313
|
+
"x-auth-token",
|
|
314
|
+
"proxy-authorization"
|
|
315
|
+
];
|
|
316
|
+
function describeError(err, depth = 3) {
|
|
317
|
+
const parts = [];
|
|
318
|
+
let cur = err;
|
|
319
|
+
while (cur !== null && cur !== void 0 && parts.length < depth) {
|
|
320
|
+
const e = cur;
|
|
321
|
+
const msg = typeof e.message === "string" ? e.message : String(cur);
|
|
322
|
+
const code = typeof e.code === "string" ? ` (${e.code})` : "";
|
|
323
|
+
parts.push(`${msg}${code}`);
|
|
324
|
+
cur = e.cause;
|
|
325
|
+
}
|
|
326
|
+
return parts.length === 0 ? String(err) : parts.join(" \u2190 ");
|
|
327
|
+
}
|
|
328
|
+
function maskHeaders(headers, masks = SECRET_HEADERS) {
|
|
329
|
+
const lower2 = masks.map((m) => m.toLowerCase());
|
|
330
|
+
const out = {};
|
|
331
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
332
|
+
out[k] = lower2.includes(k.toLowerCase()) ? "***" : v;
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
function fmtBody(body, max) {
|
|
337
|
+
if (body === void 0 || body === null) return "";
|
|
338
|
+
const s = typeof body === "string" ? body : JSON.stringify(body);
|
|
339
|
+
return s.length > max ? `${s.slice(0, max)}\u2026 (${s.length - max} more chars)` : s;
|
|
340
|
+
}
|
|
341
|
+
function createConsoleLogger(opts = {}) {
|
|
342
|
+
const write = opts.write ?? ((line) => console.error(line));
|
|
343
|
+
const mask = opts.mask !== false;
|
|
344
|
+
const max = opts.maxBodyChars ?? 1024;
|
|
345
|
+
return {
|
|
346
|
+
onRequest(req) {
|
|
347
|
+
write(`\u2192 ${req.method} ${req.url}`);
|
|
348
|
+
const headers = mask ? maskHeaders(req.headers) : req.headers;
|
|
349
|
+
const headerKeys = Object.keys(headers);
|
|
350
|
+
if (headerKeys.length > 0) write(` headers ${JSON.stringify(headers)}`);
|
|
351
|
+
const body = fmtBody(req.body, max);
|
|
352
|
+
if (body) write(` body ${body}`);
|
|
353
|
+
},
|
|
354
|
+
onResponse(req, res) {
|
|
355
|
+
write(`\u2190 ${res.status} ${req.method} ${req.url} (${res.durationMs}ms)`);
|
|
356
|
+
const body = fmtBody(res.body, max);
|
|
357
|
+
if (body) write(` body ${body}`);
|
|
358
|
+
},
|
|
359
|
+
onError(req, err, durationMs) {
|
|
360
|
+
write(`\u2717 ${req.method} ${req.url} failed after ${durationMs}ms: ${describeError(err)}`);
|
|
361
|
+
},
|
|
362
|
+
info(msg) {
|
|
363
|
+
write(msg);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function loggerFromEnv(env2 = process.env) {
|
|
368
|
+
const debug = env2.DEBUG;
|
|
369
|
+
if (!debug) return void 0;
|
|
370
|
+
const tags = debug.split(",").map((t) => t.trim().toLowerCase());
|
|
371
|
+
if (tags.some((t) => t === "*" || t === "journey" || t === "journey:*")) {
|
|
372
|
+
return createConsoleLogger();
|
|
373
|
+
}
|
|
374
|
+
return void 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/http.ts
|
|
378
|
+
function resolveUrl(endpoint, ctx, params, query) {
|
|
379
|
+
const base = isEndpointRef(endpoint) ? ctx.baseUrl : endpoint.baseUrl ?? ctx.baseUrl;
|
|
380
|
+
if (!base) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`No base URL configured. Set \`baseUrl\` in journey.config.json or on the endpoint descriptor.`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
let path = endpoint.path;
|
|
386
|
+
if (params) {
|
|
387
|
+
for (const [k, v] of Object.entries(params)) {
|
|
388
|
+
path = path.replace(`{${k}}`, encodeURIComponent(String(v)));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const missing = path.match(/\{([^}]+)\}/);
|
|
392
|
+
if (missing) {
|
|
393
|
+
throw new Error(`Missing path param "${missing[1]}" for ${endpoint.method} ${endpoint.path}`);
|
|
394
|
+
}
|
|
395
|
+
let url;
|
|
396
|
+
if (path === "") {
|
|
397
|
+
url = new URL(base);
|
|
398
|
+
} else {
|
|
399
|
+
const baseWithSlash = base.endsWith("/") ? base : `${base}/`;
|
|
400
|
+
const relPath = path.startsWith("/") ? path.slice(1) : path;
|
|
401
|
+
url = new URL(relPath, baseWithSlash);
|
|
402
|
+
}
|
|
403
|
+
if (query) {
|
|
404
|
+
for (const [k, v] of Object.entries(query)) {
|
|
405
|
+
if (v !== void 0) url.searchParams.append(k, String(v));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return url.toString();
|
|
409
|
+
}
|
|
410
|
+
function buildRequest(opts, ctx) {
|
|
411
|
+
const url = resolveUrl(opts.endpoint, ctx, opts.params, opts.query);
|
|
412
|
+
const headers = {
|
|
413
|
+
...ctx.defaultHeaders ?? {},
|
|
414
|
+
...opts.headers ?? {}
|
|
415
|
+
};
|
|
416
|
+
const hasBody = opts.body !== void 0;
|
|
417
|
+
if (hasBody && !("content-type" in lower(headers))) {
|
|
418
|
+
headers["Content-Type"] = "application/json";
|
|
419
|
+
}
|
|
420
|
+
const spec = { method: opts.endpoint.method, url, headers };
|
|
421
|
+
if (hasBody) spec.body = opts.body;
|
|
422
|
+
if (opts.timeoutMs !== void 0) spec.timeoutMs = opts.timeoutMs;
|
|
423
|
+
return spec;
|
|
424
|
+
}
|
|
425
|
+
function lower(h) {
|
|
426
|
+
const out = {};
|
|
427
|
+
for (const [k, v] of Object.entries(h)) out[k.toLowerCase()] = v;
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
async function execute(req, ctx) {
|
|
431
|
+
const logReq = {
|
|
432
|
+
method: req.method,
|
|
433
|
+
url: req.url,
|
|
434
|
+
headers: req.headers,
|
|
435
|
+
...req.body !== void 0 ? { body: req.body } : {}
|
|
436
|
+
};
|
|
437
|
+
ctx.logger?.onRequest?.(logReq);
|
|
438
|
+
const f = ctx.fetchImpl ?? fetch;
|
|
439
|
+
const init = { method: req.method, headers: req.headers };
|
|
440
|
+
if (req.body !== void 0) {
|
|
441
|
+
init.body = typeof req.body === "string" ? req.body : JSON.stringify(req.body);
|
|
442
|
+
}
|
|
443
|
+
if (ctx.dispatcher !== void 0) {
|
|
444
|
+
init.dispatcher = ctx.dispatcher;
|
|
445
|
+
}
|
|
446
|
+
let timer;
|
|
447
|
+
const signals = [];
|
|
448
|
+
if (req.timeoutMs !== void 0) {
|
|
449
|
+
const abort = new AbortController();
|
|
450
|
+
timer = setTimeout(() => abort.abort(), req.timeoutMs);
|
|
451
|
+
signals.push(abort.signal);
|
|
452
|
+
}
|
|
453
|
+
if (ctx.signal) signals.push(ctx.signal);
|
|
454
|
+
if (signals.length === 1) init.signal = signals[0];
|
|
455
|
+
else if (signals.length > 1) init.signal = AbortSignal.any(signals);
|
|
456
|
+
const started = Date.now();
|
|
457
|
+
try {
|
|
458
|
+
const res = await f(req.url, init);
|
|
459
|
+
const headers = {};
|
|
460
|
+
res.headers.forEach((v, k) => {
|
|
461
|
+
headers[k] = v;
|
|
462
|
+
});
|
|
463
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
464
|
+
const body = contentType.includes("json") ? await res.json().catch(() => null) : await res.text();
|
|
465
|
+
const wrapped = { status: res.status, headers, body };
|
|
466
|
+
ctx.logger?.onResponse?.(logReq, { ...wrapped, durationMs: Date.now() - started });
|
|
467
|
+
return wrapped;
|
|
468
|
+
} catch (err) {
|
|
469
|
+
ctx.logger?.onError?.(logReq, err, Date.now() - started);
|
|
470
|
+
throw err;
|
|
471
|
+
} finally {
|
|
472
|
+
if (timer) clearTimeout(timer);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/cache.ts
|
|
477
|
+
import { createHash } from "crypto";
|
|
478
|
+
import { mkdir as mkdir2, readFile as readFile4, rm, writeFile as writeFile2 } from "fs/promises";
|
|
479
|
+
import { join as join4 } from "path";
|
|
480
|
+
function subJourneyCacheKey(journeyName, resolvedKey) {
|
|
481
|
+
return `${journeyName}:${resolvedKey}`;
|
|
482
|
+
}
|
|
483
|
+
function isExpired(entry) {
|
|
484
|
+
return entry.expiresAt !== void 0 && Date.now() >= entry.expiresAt;
|
|
485
|
+
}
|
|
486
|
+
function entryFor(value, ttlMs) {
|
|
487
|
+
return ttlMs !== void 0 ? { value, expiresAt: Date.now() + ttlMs } : { value };
|
|
488
|
+
}
|
|
489
|
+
var MemorySubJourneyCache = class {
|
|
490
|
+
store = /* @__PURE__ */ new Map();
|
|
491
|
+
get(key) {
|
|
492
|
+
const entry = this.store.get(key);
|
|
493
|
+
if (!entry) return void 0;
|
|
494
|
+
if (isExpired(entry)) {
|
|
495
|
+
this.store.delete(key);
|
|
496
|
+
return void 0;
|
|
497
|
+
}
|
|
498
|
+
return entry;
|
|
499
|
+
}
|
|
500
|
+
set(key, value, ttlMs) {
|
|
501
|
+
this.store.set(key, entryFor(value, ttlMs));
|
|
502
|
+
}
|
|
503
|
+
clear() {
|
|
504
|
+
this.store.clear();
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
var DiskSubJourneyCache = class {
|
|
508
|
+
constructor(dir) {
|
|
509
|
+
this.dir = dir;
|
|
510
|
+
}
|
|
511
|
+
dir;
|
|
512
|
+
pathFor(key) {
|
|
513
|
+
return join4(this.dir, `${createHash("sha256").update(key).digest("hex")}.json`);
|
|
514
|
+
}
|
|
515
|
+
async get(key) {
|
|
516
|
+
const path = this.pathFor(key);
|
|
517
|
+
let raw;
|
|
518
|
+
try {
|
|
519
|
+
raw = await readFile4(path, "utf8");
|
|
520
|
+
} catch {
|
|
521
|
+
return void 0;
|
|
522
|
+
}
|
|
523
|
+
let entry;
|
|
524
|
+
try {
|
|
525
|
+
entry = JSON.parse(raw);
|
|
526
|
+
} catch {
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
if (isExpired(entry)) {
|
|
530
|
+
await rm(path, { force: true });
|
|
531
|
+
return void 0;
|
|
532
|
+
}
|
|
533
|
+
return entry;
|
|
534
|
+
}
|
|
535
|
+
async set(key, value, ttlMs) {
|
|
536
|
+
await mkdir2(this.dir, { recursive: true });
|
|
537
|
+
await writeFile2(this.pathFor(key), JSON.stringify(entryFor(value, ttlMs)), "utf8");
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
function createSubJourneyCache(mode, opts) {
|
|
541
|
+
switch (mode) {
|
|
542
|
+
case "off":
|
|
543
|
+
return void 0;
|
|
544
|
+
case "disk":
|
|
545
|
+
return new DiskSubJourneyCache(opts.diskDir);
|
|
546
|
+
case "run":
|
|
547
|
+
case "process":
|
|
548
|
+
return new MemorySubJourneyCache();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/runtime.ts
|
|
553
|
+
var STATE_KEY = /* @__PURE__ */ Symbol.for("@usejourney/core::runtime-state");
|
|
554
|
+
var globals2 = globalThis;
|
|
555
|
+
var state = globals2[STATE_KEY] ?? (globals2[STATE_KEY] = {
|
|
556
|
+
registry: [],
|
|
557
|
+
collecting: void 0,
|
|
558
|
+
currentCtx: void 0,
|
|
559
|
+
childOutputs: []
|
|
560
|
+
});
|
|
561
|
+
var MAX_SUB_JOURNEY_DEPTH = 8;
|
|
562
|
+
function getCurrentCtx() {
|
|
563
|
+
return state.currentCtx;
|
|
564
|
+
}
|
|
565
|
+
function journey(name, optionsOrBody, maybeBody) {
|
|
566
|
+
const bodyFn = typeof optionsOrBody === "function" ? optionsOrBody : maybeBody;
|
|
567
|
+
const options = typeof optionsOrBody === "function" ? void 0 : optionsOrBody;
|
|
568
|
+
if (options && options.reusable === true) {
|
|
569
|
+
const reusable = options;
|
|
570
|
+
const def2 = {
|
|
571
|
+
name,
|
|
572
|
+
body: bodyFn,
|
|
573
|
+
options: {
|
|
574
|
+
reusable: true,
|
|
575
|
+
...reusable.inputs ? { inputs: reusable.inputs } : {},
|
|
576
|
+
...reusable.outputs ? { outputs: reusable.outputs } : {}
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
const handle = {
|
|
580
|
+
name,
|
|
581
|
+
...reusable.inputs ? { inputs: reusable.inputs } : {},
|
|
582
|
+
...reusable.outputs ? { outputs: reusable.outputs } : {},
|
|
583
|
+
__def: def2
|
|
584
|
+
};
|
|
585
|
+
return handle;
|
|
586
|
+
}
|
|
587
|
+
const entryOpts = options;
|
|
588
|
+
const def = {
|
|
589
|
+
name,
|
|
590
|
+
body: bodyFn,
|
|
591
|
+
...entryOpts ? { options: entryOpts } : {}
|
|
592
|
+
};
|
|
593
|
+
state.registry.push(def);
|
|
594
|
+
}
|
|
595
|
+
function step(name, options) {
|
|
596
|
+
if (!state.collecting) {
|
|
597
|
+
throw new Error(`step(${JSON.stringify(name)}) called outside a journey(...) body`);
|
|
598
|
+
}
|
|
599
|
+
state.collecting.push({
|
|
600
|
+
kind: "step",
|
|
601
|
+
def: { name, options }
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function invokeJourney(handle, opts = {}) {
|
|
605
|
+
if (!state.collecting) {
|
|
606
|
+
throw new Error(
|
|
607
|
+
`invokeJourney(${JSON.stringify(handle.name)}) called outside a journey(...) body`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
const callDef = {
|
|
611
|
+
handle
|
|
612
|
+
};
|
|
613
|
+
if (opts.name !== void 0) callDef.name = opts.name;
|
|
614
|
+
if (opts.inputs !== void 0) callDef.inputs = opts.inputs;
|
|
615
|
+
if (opts.cacheKey !== void 0) {
|
|
616
|
+
const ck = opts.cacheKey;
|
|
617
|
+
callDef.cacheKey = typeof ck === "function" ? ck : ck;
|
|
618
|
+
}
|
|
619
|
+
if (opts.cacheTtlMs !== void 0) callDef.cacheTtlMs = opts.cacheTtlMs;
|
|
620
|
+
if (opts.cache !== void 0) callDef.cache = opts.cache;
|
|
621
|
+
if (opts.assert !== void 0) {
|
|
622
|
+
callDef.assert = opts.assert;
|
|
623
|
+
}
|
|
624
|
+
if (opts.after !== void 0) {
|
|
625
|
+
callDef.after = opts.after;
|
|
626
|
+
}
|
|
627
|
+
state.collecting.push({ kind: "sub", def: callDef });
|
|
628
|
+
}
|
|
629
|
+
function output(value) {
|
|
630
|
+
if (state.childOutputs.length === 0) {
|
|
631
|
+
state.currentCtx?.logger?.onLog?.({
|
|
632
|
+
level: "warn",
|
|
633
|
+
text: "output() called outside a reusable journey; ignored"
|
|
634
|
+
});
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const slot = state.childOutputs[state.childOutputs.length - 1];
|
|
638
|
+
if (slot.written) {
|
|
639
|
+
state.currentCtx?.logger?.onLog?.({
|
|
640
|
+
level: "warn",
|
|
641
|
+
text: "output() called more than once in a single sub-journey; last value wins"
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
slot.value = value;
|
|
645
|
+
slot.written = true;
|
|
646
|
+
}
|
|
647
|
+
function getRegisteredJourneys() {
|
|
648
|
+
return state.registry;
|
|
649
|
+
}
|
|
650
|
+
function clearRegistry() {
|
|
651
|
+
state.registry.length = 0;
|
|
652
|
+
}
|
|
653
|
+
async function collectPipeline(def) {
|
|
654
|
+
return collectPipelineWithInput(def, void 0);
|
|
655
|
+
}
|
|
656
|
+
async function collectSubPipeline(call) {
|
|
657
|
+
let input;
|
|
658
|
+
try {
|
|
659
|
+
input = call.inputs !== void 0 ? await resolveLazy(call.inputs) : void 0;
|
|
660
|
+
} catch {
|
|
661
|
+
input = void 0;
|
|
662
|
+
}
|
|
663
|
+
return collectPipelineWithInput(call.handle.__def, input);
|
|
664
|
+
}
|
|
665
|
+
async function collectPipelineWithInput(def, input) {
|
|
666
|
+
const nodes = [];
|
|
667
|
+
const prev = state.collecting;
|
|
668
|
+
state.collecting = nodes;
|
|
669
|
+
try {
|
|
670
|
+
await def.body(input);
|
|
671
|
+
} finally {
|
|
672
|
+
state.collecting = prev;
|
|
673
|
+
}
|
|
674
|
+
return nodes;
|
|
675
|
+
}
|
|
676
|
+
async function resolveLazy(v) {
|
|
677
|
+
if (v === void 0) return void 0;
|
|
678
|
+
return typeof v === "function" ? await v() : v;
|
|
679
|
+
}
|
|
680
|
+
async function discoverChildNodes(call) {
|
|
681
|
+
let input;
|
|
682
|
+
try {
|
|
683
|
+
input = call.inputs !== void 0 ? await resolveLazy(call.inputs) : void 0;
|
|
684
|
+
} catch {
|
|
685
|
+
input = void 0;
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
return await collectPipelineWithInput(call.handle.__def, input);
|
|
689
|
+
} catch {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async function planPipeline(nodes, depth) {
|
|
694
|
+
const out = [];
|
|
695
|
+
for (const node of nodes) {
|
|
696
|
+
if (node.kind === "step") {
|
|
697
|
+
out.push({
|
|
698
|
+
kind: "step",
|
|
699
|
+
name: node.def.name,
|
|
700
|
+
method: node.def.options.endpoint.method,
|
|
701
|
+
path: node.def.options.endpoint.path
|
|
702
|
+
});
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const planned = {
|
|
706
|
+
kind: "sub",
|
|
707
|
+
name: node.def.name ?? node.def.handle.name
|
|
708
|
+
};
|
|
709
|
+
const childNodes = depth < MAX_SUB_JOURNEY_DEPTH ? await discoverChildNodes(node.def) : null;
|
|
710
|
+
if (childNodes) {
|
|
711
|
+
planned.children = await planPipeline(childNodes, depth + 1);
|
|
712
|
+
} else {
|
|
713
|
+
planned.incomplete = true;
|
|
714
|
+
}
|
|
715
|
+
out.push(planned);
|
|
716
|
+
}
|
|
717
|
+
return out;
|
|
718
|
+
}
|
|
719
|
+
async function planJourney(def) {
|
|
720
|
+
const nodes = await collectPipeline(def);
|
|
721
|
+
return planPipeline(nodes, 0);
|
|
722
|
+
}
|
|
723
|
+
async function executeStepNode(s, ctx, meta, stepIdx) {
|
|
724
|
+
const start = Date.now();
|
|
725
|
+
ctx.logger?.onStepStart?.({
|
|
726
|
+
runId: meta.runId,
|
|
727
|
+
journeyIdx: meta.journeyIdx,
|
|
728
|
+
journeyName: meta.journeyName,
|
|
729
|
+
stepIdx,
|
|
730
|
+
name: s.name
|
|
731
|
+
});
|
|
732
|
+
try {
|
|
733
|
+
const headers = await resolveLazy(s.options.headers);
|
|
734
|
+
const query = await resolveLazy(s.options.query);
|
|
735
|
+
const body = await resolveLazy(s.options.body);
|
|
736
|
+
const params = await resolveLazy(s.options.params);
|
|
737
|
+
const req = buildRequest(
|
|
738
|
+
{
|
|
739
|
+
endpoint: s.options.endpoint,
|
|
740
|
+
...params !== void 0 ? { params } : {},
|
|
741
|
+
...query !== void 0 ? { query } : {},
|
|
742
|
+
...headers !== void 0 ? { headers } : {},
|
|
743
|
+
...body !== void 0 ? { body } : {},
|
|
744
|
+
...s.options.timeoutMs !== void 0 ? { timeoutMs: s.options.timeoutMs } : {}
|
|
745
|
+
},
|
|
746
|
+
ctx
|
|
747
|
+
);
|
|
748
|
+
const response = await execute(req, ctx);
|
|
749
|
+
if (s.options.assert) await s.options.assert(response);
|
|
750
|
+
if (s.options.after) await s.options.after(response);
|
|
751
|
+
const durationMs = Date.now() - start;
|
|
752
|
+
ctx.logger?.onStepEnd?.({
|
|
753
|
+
runId: meta.runId,
|
|
754
|
+
journeyIdx: meta.journeyIdx,
|
|
755
|
+
stepIdx,
|
|
756
|
+
ok: true,
|
|
757
|
+
durationMs
|
|
758
|
+
});
|
|
759
|
+
return {
|
|
760
|
+
name: s.name,
|
|
761
|
+
ok: true,
|
|
762
|
+
request: { method: req.method, url: req.url },
|
|
763
|
+
response,
|
|
764
|
+
durationMs
|
|
765
|
+
};
|
|
766
|
+
} catch (err) {
|
|
767
|
+
const durationMs = Date.now() - start;
|
|
768
|
+
const error = describeError(err);
|
|
769
|
+
ctx.logger?.onStepEnd?.({
|
|
770
|
+
runId: meta.runId,
|
|
771
|
+
journeyIdx: meta.journeyIdx,
|
|
772
|
+
stepIdx,
|
|
773
|
+
ok: false,
|
|
774
|
+
durationMs,
|
|
775
|
+
error
|
|
776
|
+
});
|
|
777
|
+
return { name: s.name, ok: false, error, durationMs };
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function resolveCacheKey(cacheKey, input) {
|
|
781
|
+
if (cacheKey === void 0) return void 0;
|
|
782
|
+
if (typeof cacheKey === "function") return cacheKey(input);
|
|
783
|
+
return cacheKey;
|
|
784
|
+
}
|
|
785
|
+
async function executeSubNode(call, ctx, meta, stepIdx, upToStepIdx, depth, execState) {
|
|
786
|
+
const displayName = call.name ?? call.handle.name;
|
|
787
|
+
const start = Date.now();
|
|
788
|
+
const firstChildStepIdx = stepIdx + 1;
|
|
789
|
+
let input;
|
|
790
|
+
try {
|
|
791
|
+
input = call.inputs !== void 0 ? await resolveLazy(call.inputs) : void 0;
|
|
792
|
+
if (call.handle.inputs) {
|
|
793
|
+
input = call.handle.inputs.parse(input);
|
|
794
|
+
}
|
|
795
|
+
} catch (err) {
|
|
796
|
+
const durationMs2 = Date.now() - start;
|
|
797
|
+
const error = `sub-journey "${call.handle.name}" input validation failed: ${describeError(err)}`;
|
|
798
|
+
ctx.logger?.onGroupStart?.({
|
|
799
|
+
runId: meta.runId,
|
|
800
|
+
journeyIdx: meta.journeyIdx,
|
|
801
|
+
name: displayName,
|
|
802
|
+
childJourneyName: call.handle.name,
|
|
803
|
+
stepIdx,
|
|
804
|
+
firstChildStepIdx,
|
|
805
|
+
cacheStatus: "miss"
|
|
806
|
+
});
|
|
807
|
+
ctx.logger?.onGroupEnd?.({
|
|
808
|
+
runId: meta.runId,
|
|
809
|
+
journeyIdx: meta.journeyIdx,
|
|
810
|
+
name: displayName,
|
|
811
|
+
childJourneyName: call.handle.name,
|
|
812
|
+
stepIdx,
|
|
813
|
+
lastChildStepIdx: stepIdx,
|
|
814
|
+
ok: false,
|
|
815
|
+
durationMs: durationMs2,
|
|
816
|
+
error
|
|
817
|
+
});
|
|
818
|
+
return { name: displayName, ok: false, error, durationMs: durationMs2, kind: "sub", children: [] };
|
|
819
|
+
}
|
|
820
|
+
const resolvedKey = resolveCacheKey(call.cacheKey, input);
|
|
821
|
+
const cache = ctx.subJourneyCache;
|
|
822
|
+
const cacheKeyStr = cache !== void 0 && resolvedKey !== void 0 && call.cache !== "off" ? subJourneyCacheKey(call.handle.name, resolvedKey) : void 0;
|
|
823
|
+
if (cache !== void 0 && cacheKeyStr !== void 0) {
|
|
824
|
+
const entry = await cache.get(cacheKeyStr);
|
|
825
|
+
if (entry !== void 0) {
|
|
826
|
+
ctx.logger?.onGroupStart?.({
|
|
827
|
+
runId: meta.runId,
|
|
828
|
+
journeyIdx: meta.journeyIdx,
|
|
829
|
+
name: displayName,
|
|
830
|
+
childJourneyName: call.handle.name,
|
|
831
|
+
stepIdx,
|
|
832
|
+
firstChildStepIdx,
|
|
833
|
+
cacheStatus: "hit",
|
|
834
|
+
resolvedKey
|
|
835
|
+
});
|
|
836
|
+
let cachedOutput = entry.value;
|
|
837
|
+
let ok = true;
|
|
838
|
+
let error;
|
|
839
|
+
if (call.handle.outputs) {
|
|
840
|
+
try {
|
|
841
|
+
cachedOutput = call.handle.outputs.parse(entry.value);
|
|
842
|
+
} catch (err) {
|
|
843
|
+
ok = false;
|
|
844
|
+
error = `sub-journey "${call.handle.name}" cached output failed validation: ${describeError(err)}`;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (ok) {
|
|
848
|
+
try {
|
|
849
|
+
if (call.assert) await call.assert(cachedOutput);
|
|
850
|
+
if (call.after) await call.after(cachedOutput);
|
|
851
|
+
} catch (err) {
|
|
852
|
+
ok = false;
|
|
853
|
+
error = describeError(err);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const durationMs2 = Date.now() - start;
|
|
857
|
+
ctx.logger?.onGroupEnd?.({
|
|
858
|
+
runId: meta.runId,
|
|
859
|
+
journeyIdx: meta.journeyIdx,
|
|
860
|
+
name: displayName,
|
|
861
|
+
childJourneyName: call.handle.name,
|
|
862
|
+
stepIdx,
|
|
863
|
+
lastChildStepIdx: stepIdx,
|
|
864
|
+
ok,
|
|
865
|
+
durationMs: durationMs2,
|
|
866
|
+
...error !== void 0 ? { error } : {}
|
|
867
|
+
});
|
|
868
|
+
return {
|
|
869
|
+
name: displayName,
|
|
870
|
+
ok,
|
|
871
|
+
durationMs: durationMs2,
|
|
872
|
+
kind: "sub",
|
|
873
|
+
children: [],
|
|
874
|
+
cacheStatus: "hit",
|
|
875
|
+
...error !== void 0 ? { error } : {}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const groupStart = {
|
|
880
|
+
runId: meta.runId,
|
|
881
|
+
journeyIdx: meta.journeyIdx,
|
|
882
|
+
name: displayName,
|
|
883
|
+
childJourneyName: call.handle.name,
|
|
884
|
+
stepIdx,
|
|
885
|
+
firstChildStepIdx,
|
|
886
|
+
cacheStatus: "miss",
|
|
887
|
+
...resolvedKey !== void 0 ? { resolvedKey } : {}
|
|
888
|
+
};
|
|
889
|
+
ctx.logger?.onGroupStart?.(groupStart);
|
|
890
|
+
let childNodes;
|
|
891
|
+
const prevCollecting = state.collecting;
|
|
892
|
+
state.collecting = [];
|
|
893
|
+
try {
|
|
894
|
+
await call.handle.__def.body(input);
|
|
895
|
+
childNodes = state.collecting;
|
|
896
|
+
} finally {
|
|
897
|
+
state.collecting = prevCollecting;
|
|
898
|
+
}
|
|
899
|
+
const slot = { value: void 0, written: false };
|
|
900
|
+
state.childOutputs.push(slot);
|
|
901
|
+
const children = [];
|
|
902
|
+
let childOk = true;
|
|
903
|
+
let childError;
|
|
904
|
+
try {
|
|
905
|
+
const childExecState = { counter: stepIdx + 1, ok: true, halt: false };
|
|
906
|
+
await executePipeline(childNodes, ctx, meta, upToStepIdx, depth + 1, childExecState, children);
|
|
907
|
+
childOk = childExecState.ok;
|
|
908
|
+
execState.counter = childExecState.counter;
|
|
909
|
+
if (!childOk) {
|
|
910
|
+
const failed = children.find((c) => !c.ok);
|
|
911
|
+
childError = failed ? `sub-journey "${call.handle.name}" failed at step "${failed.name}": ${failed.error ?? "unknown error"}` : `sub-journey "${call.handle.name}" failed`;
|
|
912
|
+
}
|
|
913
|
+
if (childExecState.halt) execState.halt = true;
|
|
914
|
+
} finally {
|
|
915
|
+
state.childOutputs.pop();
|
|
916
|
+
}
|
|
917
|
+
let validatedOutput = slot.written ? slot.value : void 0;
|
|
918
|
+
if (childOk) {
|
|
919
|
+
if (call.handle.outputs) {
|
|
920
|
+
if (!slot.written) {
|
|
921
|
+
childOk = false;
|
|
922
|
+
childError = `sub-journey "${call.handle.name}" did not call output() before completing`;
|
|
923
|
+
} else {
|
|
924
|
+
try {
|
|
925
|
+
validatedOutput = call.handle.outputs.parse(slot.value);
|
|
926
|
+
} catch (err) {
|
|
927
|
+
childOk = false;
|
|
928
|
+
childError = `sub-journey "${call.handle.name}" output validation failed: ${describeError(err)}`;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (childOk && cache !== void 0 && cacheKeyStr !== void 0) {
|
|
934
|
+
const ttlMs = call.cacheTtlMs ?? ctx.subJourneyCacheTtlMs;
|
|
935
|
+
await cache.set(cacheKeyStr, validatedOutput, ttlMs);
|
|
936
|
+
}
|
|
937
|
+
if (childOk) {
|
|
938
|
+
try {
|
|
939
|
+
if (call.assert) await call.assert(validatedOutput);
|
|
940
|
+
if (call.after) await call.after(validatedOutput);
|
|
941
|
+
} catch (err) {
|
|
942
|
+
childOk = false;
|
|
943
|
+
childError = describeError(err);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const durationMs = Date.now() - start;
|
|
947
|
+
const lastChildStepIdx = children.length > 0 ? execState.counter - 1 : stepIdx;
|
|
948
|
+
const groupEnd = {
|
|
949
|
+
runId: meta.runId,
|
|
950
|
+
journeyIdx: meta.journeyIdx,
|
|
951
|
+
name: displayName,
|
|
952
|
+
childJourneyName: call.handle.name,
|
|
953
|
+
stepIdx,
|
|
954
|
+
lastChildStepIdx,
|
|
955
|
+
ok: childOk,
|
|
956
|
+
durationMs,
|
|
957
|
+
...childError !== void 0 ? { error: childError } : {}
|
|
958
|
+
};
|
|
959
|
+
ctx.logger?.onGroupEnd?.(groupEnd);
|
|
960
|
+
return {
|
|
961
|
+
name: displayName,
|
|
962
|
+
ok: childOk,
|
|
963
|
+
durationMs,
|
|
964
|
+
kind: "sub",
|
|
965
|
+
children,
|
|
966
|
+
cacheStatus: "miss",
|
|
967
|
+
...childError !== void 0 ? { error: childError } : {}
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
async function executePipeline(nodes, ctx, meta, upToStepIdx, depth, execState, out) {
|
|
971
|
+
if (depth > MAX_SUB_JOURNEY_DEPTH) {
|
|
972
|
+
throw new Error(
|
|
973
|
+
`sub-journey recursion depth exceeded (max ${MAX_SUB_JOURNEY_DEPTH}); check for cycles`
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
for (const node of nodes) {
|
|
977
|
+
if (ctx.signal?.aborted) {
|
|
978
|
+
execState.ok = false;
|
|
979
|
+
execState.halt = true;
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
if (upToStepIdx !== void 0 && execState.counter > upToStepIdx) {
|
|
983
|
+
execState.halt = true;
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
const stepIdx = execState.counter++;
|
|
987
|
+
if (node.kind === "step") {
|
|
988
|
+
const r = await executeStepNode(node.def, ctx, meta, stepIdx);
|
|
989
|
+
out.push(r);
|
|
990
|
+
if (!r.ok) {
|
|
991
|
+
execState.ok = false;
|
|
992
|
+
execState.halt = true;
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
} else {
|
|
996
|
+
const r = await executeSubNode(node.def, ctx, meta, stepIdx, upToStepIdx, depth, execState);
|
|
997
|
+
out.push(r);
|
|
998
|
+
if (!r.ok) {
|
|
999
|
+
execState.ok = false;
|
|
1000
|
+
execState.halt = true;
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async function runJourney(def, ctx, opts = {}) {
|
|
1007
|
+
const nodes = await collectPipeline(def);
|
|
1008
|
+
const runId = opts.runId ?? newRunId();
|
|
1009
|
+
const journeyIdx = opts.journeyIdx ?? 0;
|
|
1010
|
+
const stepIdxOffset = opts.stepIdxOffset ?? 0;
|
|
1011
|
+
ctx.logger?.onPlanned?.({
|
|
1012
|
+
runId,
|
|
1013
|
+
journeyIdx,
|
|
1014
|
+
journeyName: def.name,
|
|
1015
|
+
stepIdxOffset,
|
|
1016
|
+
steps: await planPipeline(nodes, 0)
|
|
1017
|
+
});
|
|
1018
|
+
const journeyStart = Date.now();
|
|
1019
|
+
const meta = { runId, journeyIdx, journeyName: def.name };
|
|
1020
|
+
const execState = { counter: stepIdxOffset, ok: true, halt: false };
|
|
1021
|
+
const results = [];
|
|
1022
|
+
const prevCtx = state.currentCtx;
|
|
1023
|
+
state.currentCtx = ctx;
|
|
1024
|
+
try {
|
|
1025
|
+
await executePipeline(nodes, ctx, meta, opts.upToStepIdx, 0, execState, results);
|
|
1026
|
+
} finally {
|
|
1027
|
+
state.currentCtx = prevCtx;
|
|
1028
|
+
}
|
|
1029
|
+
return {
|
|
1030
|
+
name: def.name,
|
|
1031
|
+
ok: execState.ok,
|
|
1032
|
+
steps: results,
|
|
1033
|
+
durationMs: Date.now() - journeyStart
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
async function runAllRegistered(ctx, opts = {}) {
|
|
1037
|
+
const defs = state.registry.slice();
|
|
1038
|
+
for (const def of defs) {
|
|
1039
|
+
if (def.options?.inputs !== void 0 || def.options?.outputs !== void 0) {
|
|
1040
|
+
throw new Error(
|
|
1041
|
+
`journey "${def.name}" declares an inputs/outputs schema but is registered as an entry. Mark it { reusable: true } and invoke it via invokeJourney(handle, ...), or remove the schema.`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
clearRegistry();
|
|
1046
|
+
const runId = opts.runId ?? newRunId();
|
|
1047
|
+
ctx.logger?.onRunStart?.({ runId, journeyNames: defs.map((d) => d.name) });
|
|
1048
|
+
const runStart = Date.now();
|
|
1049
|
+
const results = [];
|
|
1050
|
+
let ok = true;
|
|
1051
|
+
let stepIdxOffset = 0;
|
|
1052
|
+
for (let journeyIdx = 0; journeyIdx < defs.length; journeyIdx++) {
|
|
1053
|
+
const def = defs[journeyIdx];
|
|
1054
|
+
const result = await runJourney(def, ctx, {
|
|
1055
|
+
runId,
|
|
1056
|
+
journeyIdx,
|
|
1057
|
+
stepIdxOffset,
|
|
1058
|
+
...opts.upToStepIdx !== void 0 ? { upToStepIdx: opts.upToStepIdx } : {}
|
|
1059
|
+
});
|
|
1060
|
+
results.push(result);
|
|
1061
|
+
if (!result.ok) ok = false;
|
|
1062
|
+
stepIdxOffset += countLeaves(result.steps);
|
|
1063
|
+
if (opts.upToStepIdx !== void 0 && stepIdxOffset > opts.upToStepIdx) break;
|
|
1064
|
+
if (ctx.signal?.aborted) {
|
|
1065
|
+
ok = false;
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
ctx.logger?.onRunEnd?.({
|
|
1070
|
+
runId,
|
|
1071
|
+
ok,
|
|
1072
|
+
durationMs: Date.now() - runStart,
|
|
1073
|
+
results: results.map((r) => ({ name: r.name, ok: r.ok }))
|
|
1074
|
+
});
|
|
1075
|
+
return results;
|
|
1076
|
+
}
|
|
1077
|
+
function countLeaves(steps) {
|
|
1078
|
+
let n = 0;
|
|
1079
|
+
for (const s of steps) {
|
|
1080
|
+
n += 1;
|
|
1081
|
+
if (s.children) n += countLeaves(s.children);
|
|
1082
|
+
}
|
|
1083
|
+
return n;
|
|
1084
|
+
}
|
|
1085
|
+
function newRunId() {
|
|
1086
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
1087
|
+
return globalThis.crypto.randomUUID();
|
|
1088
|
+
}
|
|
1089
|
+
return `run_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/fetch.ts
|
|
1093
|
+
async function fetch2(input, init) {
|
|
1094
|
+
const ctx = getCurrentCtx();
|
|
1095
|
+
const logger = ctx?.logger;
|
|
1096
|
+
const realFetch = globalThis.fetch;
|
|
1097
|
+
const mergedInit = mergeAbortSignal(init, ctx?.signal);
|
|
1098
|
+
if (!logger) {
|
|
1099
|
+
return realFetch(input, mergedInit);
|
|
1100
|
+
}
|
|
1101
|
+
const reqLog = buildRequestLog(input, mergedInit);
|
|
1102
|
+
logger.onRequest?.(reqLog);
|
|
1103
|
+
const started = Date.now();
|
|
1104
|
+
let res;
|
|
1105
|
+
try {
|
|
1106
|
+
res = await realFetch(input, mergedInit);
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
logger.onError?.(reqLog, err, Date.now() - started);
|
|
1109
|
+
throw err;
|
|
1110
|
+
}
|
|
1111
|
+
logger.onResponse?.(reqLog, await buildResponseLog(res, Date.now() - started));
|
|
1112
|
+
return res;
|
|
1113
|
+
}
|
|
1114
|
+
function mergeAbortSignal(init, runSignal) {
|
|
1115
|
+
if (!runSignal) return init;
|
|
1116
|
+
const userSignal = init?.signal ?? void 0;
|
|
1117
|
+
const merged = userSignal ? AbortSignal.any([userSignal, runSignal]) : runSignal;
|
|
1118
|
+
return { ...init ?? {}, signal: merged };
|
|
1119
|
+
}
|
|
1120
|
+
function buildRequestLog(input, init) {
|
|
1121
|
+
const isRequest = typeof Request !== "undefined" && input instanceof Request;
|
|
1122
|
+
const method = (init?.method ?? (isRequest ? input.method : "GET")).toUpperCase();
|
|
1123
|
+
const url = isRequest ? input.url : typeof input === "string" ? input : input instanceof URL ? input.toString() : String(input);
|
|
1124
|
+
const headers = headersToRecord(init?.headers ?? (isRequest ? input.headers : void 0));
|
|
1125
|
+
const log = { method, url, headers };
|
|
1126
|
+
if (init?.body !== void 0 && init?.body !== null) log.body = bodyForLog(init.body);
|
|
1127
|
+
return log;
|
|
1128
|
+
}
|
|
1129
|
+
async function buildResponseLog(res, durationMs) {
|
|
1130
|
+
const headers = {};
|
|
1131
|
+
res.headers.forEach((v, k) => {
|
|
1132
|
+
headers[k] = v;
|
|
1133
|
+
});
|
|
1134
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
1135
|
+
let body = "<unreadable>";
|
|
1136
|
+
try {
|
|
1137
|
+
const cloned = res.clone();
|
|
1138
|
+
body = contentType.includes("json") ? await cloned.json().catch(() => null) : await cloned.text();
|
|
1139
|
+
} catch {
|
|
1140
|
+
body = "<unreadable>";
|
|
1141
|
+
}
|
|
1142
|
+
return { status: res.status, headers, body, durationMs };
|
|
1143
|
+
}
|
|
1144
|
+
function headersToRecord(headers) {
|
|
1145
|
+
if (!headers) return {};
|
|
1146
|
+
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
1147
|
+
const out = {};
|
|
1148
|
+
headers.forEach((v, k) => {
|
|
1149
|
+
out[k] = v;
|
|
1150
|
+
});
|
|
1151
|
+
return out;
|
|
1152
|
+
}
|
|
1153
|
+
if (Array.isArray(headers)) {
|
|
1154
|
+
const out = {};
|
|
1155
|
+
for (const [k, v] of headers) out[k] = v;
|
|
1156
|
+
return out;
|
|
1157
|
+
}
|
|
1158
|
+
return { ...headers };
|
|
1159
|
+
}
|
|
1160
|
+
function bodyForLog(body) {
|
|
1161
|
+
if (typeof body === "string") {
|
|
1162
|
+
try {
|
|
1163
|
+
return JSON.parse(body);
|
|
1164
|
+
} catch {
|
|
1165
|
+
return body;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return "<binary>";
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/index.ts
|
|
1172
|
+
import { z as z2 } from "zod";
|
|
1173
|
+
export {
|
|
1174
|
+
AssertionError,
|
|
1175
|
+
DiskSubJourneyCache,
|
|
1176
|
+
JourneyConfigSchema,
|
|
1177
|
+
MemorySubJourneyCache,
|
|
1178
|
+
SECRET_HEADERS,
|
|
1179
|
+
buildRequest,
|
|
1180
|
+
clearActiveEnvironment,
|
|
1181
|
+
clearRegistry,
|
|
1182
|
+
collectPipeline,
|
|
1183
|
+
collectSubPipeline,
|
|
1184
|
+
createConsoleLogger,
|
|
1185
|
+
createSubJourneyCache,
|
|
1186
|
+
describeError,
|
|
1187
|
+
env,
|
|
1188
|
+
execute,
|
|
1189
|
+
expect,
|
|
1190
|
+
fetch2 as fetch,
|
|
1191
|
+
getCurrentCtx,
|
|
1192
|
+
getRegisteredJourneys,
|
|
1193
|
+
invokeJourney,
|
|
1194
|
+
isEndpointRef,
|
|
1195
|
+
journey,
|
|
1196
|
+
listEnvironments,
|
|
1197
|
+
listRuns,
|
|
1198
|
+
loadConfig,
|
|
1199
|
+
loadEnvironment,
|
|
1200
|
+
loggerFromEnv,
|
|
1201
|
+
maskHeaders,
|
|
1202
|
+
output,
|
|
1203
|
+
planJourney,
|
|
1204
|
+
pruneRuns,
|
|
1205
|
+
readRun,
|
|
1206
|
+
resolveBaseUrl,
|
|
1207
|
+
resolveConfigPaths,
|
|
1208
|
+
resolveUrl,
|
|
1209
|
+
runAllRegistered,
|
|
1210
|
+
runJourney,
|
|
1211
|
+
setActiveEnvironment,
|
|
1212
|
+
step,
|
|
1213
|
+
subJourneyCacheKey,
|
|
1214
|
+
tryEnv,
|
|
1215
|
+
writeRun,
|
|
1216
|
+
z2 as z
|
|
1217
|
+
};
|