@workflow/world-local 4.1.1 → 4.1.2
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/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/fs.d.ts +56 -2
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +112 -9
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -3
- package/dist/index.js.map +1 -1
- package/dist/queue.d.ts.map +1 -1
- package/dist/queue.js +31 -1
- package/dist/queue.js.map +1 -1
- package/dist/storage/events-storage.d.ts.map +1 -1
- package/dist/storage/events-storage.js +33 -6
- package/dist/storage/events-storage.js.map +1 -1
- package/dist/storage/hooks-storage.d.ts.map +1 -1
- package/dist/storage/hooks-storage.js +2 -1
- package/dist/storage/hooks-storage.js.map +1 -1
- package/dist/storage/index.d.ts +10 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +8 -10
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/legacy.d.ts.map +1 -1
- package/dist/storage/legacy.js +8 -4
- package/dist/storage/legacy.js.map +1 -1
- package/dist/storage/runs-storage.d.ts +25 -3
- package/dist/storage/runs-storage.d.ts.map +1 -1
- package/dist/storage/runs-storage.js +5 -2
- package/dist/storage/runs-storage.js.map +1 -1
- package/dist/storage/steps-storage.d.ts.map +1 -1
- package/dist/storage/steps-storage.js +6 -1
- package/dist/storage/steps-storage.js.map +1 -1
- package/dist/streamer.d.ts.map +1 -1
- package/dist/streamer.js +6 -1
- package/dist/streamer.js.map +1 -1
- package/package.json +5 -5
package/dist/config.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ export type Config = {
|
|
|
3
3
|
dataDir: string;
|
|
4
4
|
port?: number;
|
|
5
5
|
baseUrl?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Whether start() should re-enqueue pending/running runs from storage.
|
|
8
|
+
* Defaults to true. Test harnesses that always start from a clean slate can
|
|
9
|
+
* disable recovery to avoid replaying stale runs.
|
|
10
|
+
*/
|
|
11
|
+
recoverActiveRuns?: boolean;
|
|
6
12
|
/**
|
|
7
13
|
* Optional tag to scope filesystem operations.
|
|
8
14
|
* When set, files are written as `{id}.{tag}.json` and `clear()` only deletes
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,2BAA2B,QAAQ,CAAC;AAMjD,MAAM,MAAM,MAAM,GAAG;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,eAAO,MAAM,MAAM;;CAKjB,CAAC;AAEH;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAyB7E"}
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,2BAA2B,QAAQ,CAAC;AAMjD,MAAM,MAAM,MAAM,GAAG;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,eAAO,MAAM,MAAM;;CAKjB,CAAC;AAEH;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAyB7E"}
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,iBAAiB,GAAG,GAAG,EAAE;IAC7B,OAAO,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,gBAAgB,CAAC;AACjE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AAEjD,MAAM,iBAAiB,GAAG,GAAG,EAAE;IAC7B,OAAO,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;AAC7C,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,iBAAiB,GAAG,GAAG,EAAE;IAC7B,OAAO,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,gBAAgB,CAAC;AACjE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AAEjD,MAAM,iBAAiB,GAAG,GAAG,EAAE;IAC7B,OAAO,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;AAC7C,CAAC,CAAC;AA0BF,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,CAAS,GAAG,EAAE;IACtC,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAC;IACpC,MAAM,OAAO,GAAG,iBAAiB,EAAE,CAAC;IAEpC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC9B,CAAC,CAAC,CAAC;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,MAAuB;IAC1D,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,OAAO,MAAM,CAAC,OAAO,CAAC;IACxB,CAAC;IAED,wEAAwE;IACxE,2EAA2E;IAC3E,IAAI,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,CAAC;QACxC,OAAO,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;IAC7C,CAAC;IAED,IAAI,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACpC,OAAO,oBAAoB,MAAM,CAAC,IAAI,EAAE,CAAC;IAC3C,CAAC;IAED,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACrB,OAAO,oBAAoB,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IAChD,CAAC;IAED,MAAM,YAAY,GAAG,MAAM,eAAe,EAAE,CAAC;IAC7C,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO,oBAAoB,YAAY,EAAE,CAAC;IAC5C,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;AACpE,CAAC","sourcesContent":["import { getWorkflowPort } from '@workflow/utils/get-port';\nimport { once } from './util.js';\n\nconst getDataDirFromEnv = () => {\n return process.env.WORKFLOW_LOCAL_DATA_DIR || '.workflow-data';\n};\n\nexport const DEFAULT_RESOLVE_DATA_OPTION = 'all';\n\nconst getBaseUrlFromEnv = () => {\n return process.env.WORKFLOW_LOCAL_BASE_URL;\n};\n\nexport type Config = {\n dataDir: string;\n port?: number;\n baseUrl?: string;\n /**\n * Whether start() should re-enqueue pending/running runs from storage.\n * Defaults to true. Test harnesses that always start from a clean slate can\n * disable recovery to avoid replaying stale runs.\n */\n recoverActiveRuns?: boolean;\n /**\n * Optional tag to scope filesystem operations.\n * When set, files are written as `{id}.{tag}.json` and `clear()` only deletes\n * files matching this tag. Used by vitest to isolate test data in the shared\n * `.workflow-data` directory.\n */\n tag?: string;\n /**\n * Override the flush interval (in ms) for buffered stream writes.\n * Default is 10ms. Set to 0 for immediate flushing.\n */\n streamFlushIntervalMs?: number;\n};\n\nexport const config = once<Config>(() => {\n const dataDir = getDataDirFromEnv();\n const baseUrl = getBaseUrlFromEnv();\n\n return { dataDir, baseUrl };\n});\n\n/**\n * Resolves the base URL for queue requests following the priority order:\n * 1. config.baseUrl (highest priority - full override from args)\n * 2. WORKFLOW_LOCAL_BASE_URL env var (checked directly to handle late env var setting)\n * 3. config.port (explicit port override from args)\n * 4. PORT env var (explicit configuration)\n * 5. Auto-detected port via getPort (detect actual listening port)\n */\nexport async function resolveBaseUrl(config: Partial<Config>): Promise<string> {\n if (config.baseUrl) {\n return config.baseUrl;\n }\n\n // Check env var directly in case it was set after the config was cached\n // This is important for CLI tools that set the env var after module import\n if (process.env.WORKFLOW_LOCAL_BASE_URL) {\n return process.env.WORKFLOW_LOCAL_BASE_URL;\n }\n\n if (typeof config.port === 'number') {\n return `http://localhost:${config.port}`;\n }\n\n if (process.env.PORT) {\n return `http://localhost:${process.env.PORT}`;\n }\n\n const detectedPort = await getWorkflowPort();\n if (detectedPort) {\n return `http://localhost:${detectedPort}`;\n }\n\n throw new Error('Unable to resolve base URL for workflow queue.');\n}\n"]}
|
package/dist/fs.d.ts
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
|
+
import { WorkflowWorldError } from '@workflow/errors';
|
|
1
2
|
import type { PaginatedResponse } from '@workflow/world';
|
|
2
3
|
import { z } from 'zod';
|
|
4
|
+
/**
|
|
5
|
+
* Thrown when a caller-supplied entity ID contains characters that would
|
|
6
|
+
* escape the storage root (path traversal) or otherwise produce an unsafe
|
|
7
|
+
* filename. Extends {@link WorkflowWorldError} so the platform's standard
|
|
8
|
+
* error-to-HTTP mapping handles it alongside other world-layer errors.
|
|
9
|
+
*/
|
|
10
|
+
export declare class UnsafeEntityIdError extends WorkflowWorldError {
|
|
11
|
+
constructor(kind: string, value: string);
|
|
12
|
+
static is(value: unknown): value is UnsafeEntityIdError;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validate that a string is safe to embed in a filesystem path as a single
|
|
16
|
+
* path component. Rejects values that are:
|
|
17
|
+
* - empty
|
|
18
|
+
* - starting with `.` (blocks `.`, `..`, `.locks`, `.tmp`, and other
|
|
19
|
+
* hidden or reserved filenames)
|
|
20
|
+
* - containing `.` (would collide with the `.tag` / `.json` suffix the
|
|
21
|
+
* tagging logic strips from filenames; see {@link stripTag} /
|
|
22
|
+
* {@link getObjectCreatedAt})
|
|
23
|
+
* - containing `/`, `\`, or a NUL byte
|
|
24
|
+
*
|
|
25
|
+
* This is the primary defense against path-traversal attacks where a
|
|
26
|
+
* request body supplies a `runId` / `stepId` / `correlationId` containing
|
|
27
|
+
* `../` sequences to read or write files outside the storage root.
|
|
28
|
+
* {@link resolveWithinBase} provides a belt-and-suspenders containment
|
|
29
|
+
* check at the point of `path.join` for defense in depth.
|
|
30
|
+
*
|
|
31
|
+
* @param kind - Human-readable label used in the error message (e.g. "runId")
|
|
32
|
+
* @param value - The value to validate; throws {@link UnsafeEntityIdError}
|
|
33
|
+
* if the value is not safe.
|
|
34
|
+
*/
|
|
35
|
+
export declare function assertSafeEntityId(kind: string, value: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Join `basedir` with an entity-relative path and assert the result stays
|
|
38
|
+
* inside `basedir`. Complements {@link assertSafeEntityId}: per-entry-point
|
|
39
|
+
* validation is the primary defense, and this final check catches any path
|
|
40
|
+
* escape that slipped past (e.g. a new call site that forgot to validate,
|
|
41
|
+
* or an unusual character combination on a future filesystem). Throws
|
|
42
|
+
* {@link UnsafeEntityIdError} if the joined path escapes `basedir`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveWithinBase(basedir: string, ...segments: string[]): string;
|
|
3
45
|
/**
|
|
4
46
|
* Clear the created files cache. Useful for testing or when files are deleted externally.
|
|
5
47
|
*/
|
|
@@ -11,10 +53,21 @@ export { ulidToDate } from '@workflow/world';
|
|
|
11
53
|
* `wrun_ABC` → `wrun_ABC` (no-op if no tag)
|
|
12
54
|
*/
|
|
13
55
|
export declare function stripTag(fileId: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Check whether a fileId belongs to a specific tag.
|
|
58
|
+
* `wrun_ABC.vitest-0` belongs to `vitest-0`.
|
|
59
|
+
* Untagged fileIds never match a tag-scoped lookup.
|
|
60
|
+
*/
|
|
61
|
+
export declare function hasTag(fileId: string, tag: string): boolean;
|
|
14
62
|
/**
|
|
15
63
|
* Build the file path for an entity, with optional tag embedded in the filename.
|
|
16
|
-
* `taggedPath('runs', 'wrun_ABC', 'vitest-0')` →
|
|
17
|
-
* `taggedPath('runs', 'wrun_ABC')` →
|
|
64
|
+
* `taggedPath('/data', 'runs', 'wrun_ABC', 'vitest-0')` → `/data/runs/wrun_ABC.vitest-0.json`
|
|
65
|
+
* `taggedPath('/data', 'runs', 'wrun_ABC')` → `/data/runs/wrun_ABC.json`
|
|
66
|
+
*
|
|
67
|
+
* The `fileId` and `tag` are validated with {@link assertSafeEntityId} and
|
|
68
|
+
* the result is containment-checked with {@link resolveWithinBase} to
|
|
69
|
+
* prevent path-traversal attacks when values are derived from untrusted
|
|
70
|
+
* request input.
|
|
18
71
|
*/
|
|
19
72
|
export declare function taggedPath(basedir: string, entityDir: string, fileId: string, tag?: string): string;
|
|
20
73
|
/**
|
|
@@ -72,6 +125,7 @@ interface PaginatedFileSystemQueryConfig<T> {
|
|
|
72
125
|
directory: string;
|
|
73
126
|
schema: z.ZodType<T>;
|
|
74
127
|
filePrefix?: string;
|
|
128
|
+
fileIdFilter?: (fileId: string) => boolean;
|
|
75
129
|
filter?: (item: T) => boolean;
|
|
76
130
|
sortOrder?: 'asc' | 'desc';
|
|
77
131
|
limit?: number;
|
package/dist/fs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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"}
|
package/dist/fs.js
CHANGED
|
@@ -1,9 +1,82 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { EntityConflictError } from '@workflow/errors';
|
|
3
|
+
import { EntityConflictError, WorkflowWorldError } from '@workflow/errors';
|
|
4
4
|
import { monotonicFactory } from 'ulid';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
const ulid = monotonicFactory(() => Math.random());
|
|
7
|
+
/**
|
|
8
|
+
* Truncate a possibly-untrusted value for inclusion in an error message.
|
|
9
|
+
* Keeps error output actionable for developers in dev mode while limiting
|
|
10
|
+
* the amount of attacker-controlled data reflected back if a `world-local`
|
|
11
|
+
* backend is ever run in a context that surfaces these messages to clients.
|
|
12
|
+
*/
|
|
13
|
+
function truncateForError(value) {
|
|
14
|
+
const s = typeof value === 'string' ? value : String(value);
|
|
15
|
+
const MAX = 48;
|
|
16
|
+
return s.length > MAX ? `${s.slice(0, MAX)}…` : s;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Thrown when a caller-supplied entity ID contains characters that would
|
|
20
|
+
* escape the storage root (path traversal) or otherwise produce an unsafe
|
|
21
|
+
* filename. Extends {@link WorkflowWorldError} so the platform's standard
|
|
22
|
+
* error-to-HTTP mapping handles it alongside other world-layer errors.
|
|
23
|
+
*/
|
|
24
|
+
export class UnsafeEntityIdError extends WorkflowWorldError {
|
|
25
|
+
constructor(kind, value) {
|
|
26
|
+
super(`Unsafe ${kind} "${truncateForError(value)}": must not be empty, contain ".", "/", "\\", or null bytes`);
|
|
27
|
+
this.name = 'UnsafeEntityIdError';
|
|
28
|
+
}
|
|
29
|
+
static is(value) {
|
|
30
|
+
return value instanceof Error && value.name === 'UnsafeEntityIdError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate that a string is safe to embed in a filesystem path as a single
|
|
35
|
+
* path component. Rejects values that are:
|
|
36
|
+
* - empty
|
|
37
|
+
* - starting with `.` (blocks `.`, `..`, `.locks`, `.tmp`, and other
|
|
38
|
+
* hidden or reserved filenames)
|
|
39
|
+
* - containing `.` (would collide with the `.tag` / `.json` suffix the
|
|
40
|
+
* tagging logic strips from filenames; see {@link stripTag} /
|
|
41
|
+
* {@link getObjectCreatedAt})
|
|
42
|
+
* - containing `/`, `\`, or a NUL byte
|
|
43
|
+
*
|
|
44
|
+
* This is the primary defense against path-traversal attacks where a
|
|
45
|
+
* request body supplies a `runId` / `stepId` / `correlationId` containing
|
|
46
|
+
* `../` sequences to read or write files outside the storage root.
|
|
47
|
+
* {@link resolveWithinBase} provides a belt-and-suspenders containment
|
|
48
|
+
* check at the point of `path.join` for defense in depth.
|
|
49
|
+
*
|
|
50
|
+
* @param kind - Human-readable label used in the error message (e.g. "runId")
|
|
51
|
+
* @param value - The value to validate; throws {@link UnsafeEntityIdError}
|
|
52
|
+
* if the value is not safe.
|
|
53
|
+
*/
|
|
54
|
+
export function assertSafeEntityId(kind, value) {
|
|
55
|
+
if (value.length === 0 ||
|
|
56
|
+
value.startsWith('.') ||
|
|
57
|
+
value.includes('/') ||
|
|
58
|
+
value.includes('\\') ||
|
|
59
|
+
value.includes('\0') ||
|
|
60
|
+
value.includes('.')) {
|
|
61
|
+
throw new UnsafeEntityIdError(kind, value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Join `basedir` with an entity-relative path and assert the result stays
|
|
66
|
+
* inside `basedir`. Complements {@link assertSafeEntityId}: per-entry-point
|
|
67
|
+
* validation is the primary defense, and this final check catches any path
|
|
68
|
+
* escape that slipped past (e.g. a new call site that forgot to validate,
|
|
69
|
+
* or an unusual character combination on a future filesystem). Throws
|
|
70
|
+
* {@link UnsafeEntityIdError} if the joined path escapes `basedir`.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveWithinBase(basedir, ...segments) {
|
|
73
|
+
const resolvedBase = path.resolve(basedir);
|
|
74
|
+
const joined = path.resolve(basedir, ...segments);
|
|
75
|
+
if (joined !== resolvedBase && !joined.startsWith(resolvedBase + path.sep)) {
|
|
76
|
+
throw new UnsafeEntityIdError('path', segments.join('/'));
|
|
77
|
+
}
|
|
78
|
+
return joined;
|
|
79
|
+
}
|
|
7
80
|
const isWindows = process.platform === 'win32';
|
|
8
81
|
/**
|
|
9
82
|
* Execute a filesystem operation with retry logic on Windows.
|
|
@@ -58,14 +131,30 @@ const TAG_PATTERN = /\.[a-zA-Z][a-zA-Z0-9-]*$/;
|
|
|
58
131
|
export function stripTag(fileId) {
|
|
59
132
|
return fileId.replace(TAG_PATTERN, '');
|
|
60
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Check whether a fileId belongs to a specific tag.
|
|
136
|
+
* `wrun_ABC.vitest-0` belongs to `vitest-0`.
|
|
137
|
+
* Untagged fileIds never match a tag-scoped lookup.
|
|
138
|
+
*/
|
|
139
|
+
export function hasTag(fileId, tag) {
|
|
140
|
+
return fileId.endsWith(`.${tag}`);
|
|
141
|
+
}
|
|
61
142
|
/**
|
|
62
143
|
* Build the file path for an entity, with optional tag embedded in the filename.
|
|
63
|
-
* `taggedPath('runs', 'wrun_ABC', 'vitest-0')` →
|
|
64
|
-
* `taggedPath('runs', 'wrun_ABC')` →
|
|
144
|
+
* `taggedPath('/data', 'runs', 'wrun_ABC', 'vitest-0')` → `/data/runs/wrun_ABC.vitest-0.json`
|
|
145
|
+
* `taggedPath('/data', 'runs', 'wrun_ABC')` → `/data/runs/wrun_ABC.json`
|
|
146
|
+
*
|
|
147
|
+
* The `fileId` and `tag` are validated with {@link assertSafeEntityId} and
|
|
148
|
+
* the result is containment-checked with {@link resolveWithinBase} to
|
|
149
|
+
* prevent path-traversal attacks when values are derived from untrusted
|
|
150
|
+
* request input.
|
|
65
151
|
*/
|
|
66
152
|
export function taggedPath(basedir, entityDir, fileId, tag) {
|
|
153
|
+
assertSafeEntityId('fileId', fileId);
|
|
154
|
+
if (tag !== undefined)
|
|
155
|
+
assertSafeEntityId('tag', tag);
|
|
67
156
|
const filename = tag ? `${fileId}.${tag}.json` : `${fileId}.json`;
|
|
68
|
-
return
|
|
157
|
+
return resolveWithinBase(basedir, entityDir, filename);
|
|
69
158
|
}
|
|
70
159
|
/**
|
|
71
160
|
* Read a JSON entity with tagged fallback.
|
|
@@ -73,12 +162,15 @@ export function taggedPath(basedir, entityDir, fileId, tag) {
|
|
|
73
162
|
* untagged path (so a tagged world can read entities written without a tag).
|
|
74
163
|
*/
|
|
75
164
|
export async function readJSONWithFallback(basedir, entityDir, fileId, schema, tag) {
|
|
165
|
+
assertSafeEntityId('fileId', fileId);
|
|
166
|
+
if (tag !== undefined)
|
|
167
|
+
assertSafeEntityId('tag', tag);
|
|
76
168
|
if (tag) {
|
|
77
|
-
const result = await readJSON(
|
|
169
|
+
const result = await readJSON(resolveWithinBase(basedir, entityDir, `${fileId}.${tag}.json`), schema);
|
|
78
170
|
if (result !== null)
|
|
79
171
|
return result;
|
|
80
172
|
}
|
|
81
|
-
return readJSON(
|
|
173
|
+
return readJSON(resolveWithinBase(basedir, entityDir, `${fileId}.json`), schema);
|
|
82
174
|
}
|
|
83
175
|
/**
|
|
84
176
|
* List all filenames in a directory that have a specific tag.
|
|
@@ -267,18 +359,29 @@ function createCursor(timestamp, id) {
|
|
|
267
359
|
return id ? `${timestamp.toISOString()}|${id}` : timestamp.toISOString();
|
|
268
360
|
}
|
|
269
361
|
export async function paginatedFileSystemQuery(config) {
|
|
270
|
-
const { directory, schema, filePrefix, filter, sortOrder = 'desc', limit = 20, cursor, getCreatedAt, getId, } = config;
|
|
362
|
+
const { directory, schema, filePrefix, fileIdFilter, filter, sortOrder = 'desc', limit = 20, cursor, getCreatedAt, getId, } = config;
|
|
363
|
+
// Validate filePrefix (typically `${runId}-`) so request-derived prefixes
|
|
364
|
+
// consistently reject unsafe characters. filePrefix is only used below to
|
|
365
|
+
// filter readdir() results by prefix — it doesn't participate in path
|
|
366
|
+
// construction — but keeping the validation rule uniform across the
|
|
367
|
+
// storage layer avoids special cases and catches bad values earlier.
|
|
368
|
+
if (filePrefix !== undefined) {
|
|
369
|
+
assertSafeEntityId('filePrefix', filePrefix);
|
|
370
|
+
}
|
|
271
371
|
// 1. Get all JSON files in directory
|
|
272
372
|
const fileIds = await listJSONFiles(directory);
|
|
273
373
|
// 2. Filter by prefix if provided
|
|
274
374
|
const relevantFileIds = filePrefix
|
|
275
375
|
? fileIds.filter((fileId) => fileId.startsWith(filePrefix))
|
|
276
376
|
: fileIds;
|
|
377
|
+
const filteredFileIds = fileIdFilter
|
|
378
|
+
? relevantFileIds.filter(fileIdFilter)
|
|
379
|
+
: relevantFileIds;
|
|
277
380
|
// 3. ULID Optimization: Filter by cursor using filename timestamps before loading JSON
|
|
278
381
|
const parsedCursor = parseCursor(cursor);
|
|
279
|
-
let candidateFileIds =
|
|
382
|
+
let candidateFileIds = filteredFileIds;
|
|
280
383
|
if (parsedCursor) {
|
|
281
|
-
candidateFileIds =
|
|
384
|
+
candidateFileIds = filteredFileIds.filter((fileId) => {
|
|
282
385
|
const filenameDate = getCreatedAt(`${fileId}.json`);
|
|
283
386
|
if (filenameDate) {
|
|
284
387
|
// Use filename timestamp for cursor filtering
|
package/dist/fs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { EntityConflictError } from '@workflow/errors';\nimport type { PaginatedResponse } from '@workflow/world';\nimport { monotonicFactory } from 'ulid';\nimport { z } from 'zod';\n\nconst ulid = monotonicFactory(() => Math.random());\n\nconst isWindows = process.platform === 'win32';\n\n/**\n * Execute a filesystem operation with retry logic on Windows.\n * On Windows, file operations can fail with EPERM/EBUSY/EACCES when files\n * are briefly locked by another process or antivirus. This wrapper adds\n * exponential backoff retry logic. On non-Windows platforms, executes directly.\n */\nasync function withWindowsRetry<T>(\n fn: () => Promise<T>,\n maxRetries = 5\n): Promise<T> {\n if (!isWindows) return fn();\n\n const retryableErrors = ['EPERM', 'EBUSY', 'EACCES'];\n const baseDelayMs = 10;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error: any) {\n const isRetryable =\n attempt < maxRetries && retryableErrors.includes(error.code);\n if (!isRetryable) throw error;\n // Exponential backoff with jitter\n const delay = baseDelayMs * 2 ** attempt + Math.random() * baseDelayMs;\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n // TypeScript: unreachable, but satisfies return type\n throw new Error('Retry loop exited unexpectedly');\n}\n\n// In-memory cache of created files to avoid expensive fs.access() calls\n// This is safe because we only write once per file path (no overwrites without explicit flag)\nconst createdFilesCache = new Set<string>();\n\n/**\n * Clear the created files cache. Useful for testing or when files are deleted externally.\n */\nexport function clearCreatedFilesCache(): void {\n createdFilesCache.clear();\n}\n\nexport { ulidToDate } from '@workflow/world';\n\n/**\n * Regex matching a tag suffix on a fileId (after `.json` has been stripped).\n * E.g., `wrun_ABC.vitest-0` → the `.vitest-0` part.\n * Tags start with a letter and contain alphanumeric chars and hyphens.\n * Entity IDs (ULIDs, step_N, etc.) never contain dots, so the first dot\n * always marks the tag boundary.\n */\nconst TAG_PATTERN = /\\.[a-zA-Z][a-zA-Z0-9-]*$/;\n\n/**\n * Strip a tag suffix from a fileId.\n * `wrun_ABC.vitest-0` → `wrun_ABC`\n * `wrun_ABC` → `wrun_ABC` (no-op if no tag)\n */\nexport function stripTag(fileId: string): string {\n return fileId.replace(TAG_PATTERN, '');\n}\n\n/**\n * Build the file path for an entity, with optional tag embedded in the filename.\n * `taggedPath('runs', 'wrun_ABC', 'vitest-0')` → `runs/wrun_ABC.vitest-0.json`\n * `taggedPath('runs', 'wrun_ABC')` → `runs/wrun_ABC.json`\n */\nexport function taggedPath(\n basedir: string,\n entityDir: string,\n fileId: string,\n tag?: string\n): string {\n const filename = tag ? `${fileId}.${tag}.json` : `${fileId}.json`;\n return path.join(basedir, entityDir, filename);\n}\n\n/**\n * Read a JSON entity with tagged fallback.\n * When a tag is set, tries the tagged path first, then falls back to the\n * untagged path (so a tagged world can read entities written without a tag).\n */\nexport async function readJSONWithFallback<T>(\n basedir: string,\n entityDir: string,\n fileId: string,\n schema: z.ZodType<T>,\n tag?: string\n): Promise<T | null> {\n if (tag) {\n const result = await readJSON(\n path.join(basedir, entityDir, `${fileId}.${tag}.json`),\n schema\n );\n if (result !== null) return result;\n }\n return readJSON(path.join(basedir, entityDir, `${fileId}.json`), schema);\n}\n\n/**\n * List all filenames in a directory that have a specific tag.\n * Returns full filenames (e.g., `wrun_ABC.vitest-0.json`).\n */\nexport async function listTaggedFiles(\n dirPath: string,\n tag: string\n): Promise<string[]> {\n const suffix = `.${tag}.json`;\n try {\n const files = await fs.readdir(dirPath);\n return files.filter((f) => f.endsWith(suffix));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return [];\n throw error;\n }\n}\n\n/**\n * List all filenames in a directory that have a specific tag and extension.\n * Returns full filenames (e.g., `stream-chnk_ABC.vitest-0.bin`).\n */\nexport async function listTaggedFilesByExtension(\n dirPath: string,\n tag: string,\n extension: string\n): Promise<string[]> {\n const suffix = `.${tag}${extension}`;\n try {\n const files = await fs.readdir(dirPath);\n return files.filter((f) => f.endsWith(suffix));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return [];\n throw error;\n }\n}\n\nexport async function ensureDir(dirPath: string): Promise<void> {\n try {\n await fs.mkdir(dirPath, { recursive: true });\n } catch (_error) {\n // Ignore if already exists\n }\n}\n\ninterface WriteOptions {\n overwrite?: boolean;\n}\n\n/**\n * Custom JSON replacer that encodes Uint8Array as base64 strings.\n * Format: { __type: 'Uint8Array', data: '<base64>' }\n */\nexport function jsonReplacer(_key: string, value: unknown): unknown {\n if (value instanceof Uint8Array) {\n return {\n __type: 'Uint8Array',\n data: Buffer.from(value).toString('base64'),\n };\n }\n return value;\n}\n\n/**\n * Custom JSON reviver that decodes base64 strings back to Uint8Array.\n */\nexport function jsonReviver(_key: string, value: unknown): unknown {\n if (\n value !== null &&\n typeof value === 'object' &&\n (value as any).__type === 'Uint8Array' &&\n typeof (value as any).data === 'string'\n ) {\n return new Uint8Array(Buffer.from((value as any).data, 'base64'));\n }\n return value;\n}\n\nexport async function writeJSON(\n filePath: string,\n data: any,\n opts?: WriteOptions\n): Promise<void> {\n return write(filePath, JSON.stringify(data, jsonReplacer, 2), opts);\n}\n\n/**\n * Writes data to a file using atomic write-rename pattern.\n *\n * Note: While this function uses temp files to avoid partial writes,\n * it does not provide protection against concurrent writes from multiple\n * processes. In a multi-writer scenario, the last writer wins.\n * For production use with multiple writers, consider using a proper\n * database or locking mechanism.\n */\nexport async function write(\n filePath: string,\n data: string | Buffer,\n opts?: WriteOptions\n): Promise<void> {\n if (!opts?.overwrite) {\n // Fast path: check in-memory cache first to avoid expensive fs.access() calls\n // This provides significant performance improvement when creating many files\n if (createdFilesCache.has(filePath)) {\n throw new EntityConflictError(\n `File ${filePath} already exists and 'overwrite' is false`\n );\n }\n\n // Slow path: check filesystem for files created before this process started\n try {\n await fs.access(filePath);\n // File exists on disk, add to cache for future checks\n createdFilesCache.add(filePath);\n throw new EntityConflictError(\n `File ${filePath} already exists and 'overwrite' is false`\n );\n } catch (error: any) {\n // If file doesn't exist (ENOENT), continue with write\n if (error.code !== 'ENOENT') {\n throw error;\n }\n }\n }\n\n const tempPath = `${filePath}.tmp.${ulid()}`;\n let tempFileCreated = false;\n try {\n await ensureDir(path.dirname(filePath));\n await fs.writeFile(tempPath, data);\n tempFileCreated = true;\n await withWindowsRetry(() => fs.rename(tempPath, filePath));\n // Track this file in cache so future writes know it exists\n createdFilesCache.add(filePath);\n } catch (error) {\n // Only try to clean up temp file if it was actually created\n if (tempFileCreated) {\n await withWindowsRetry(() => fs.unlink(tempPath), 3).catch(() => {});\n }\n throw error;\n }\n}\n\nexport async function readJSON<T>(\n filePath: string,\n decoder: z.ZodType<T>\n): Promise<T | null> {\n try {\n const content = await fs.readFile(filePath, 'utf-8');\n return decoder.parse(JSON.parse(content, jsonReviver));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return null;\n throw error;\n }\n}\n\nexport async function readBuffer(filePath: string): Promise<Buffer> {\n const content = await fs.readFile(filePath);\n return content;\n}\n\nexport async function deleteJSON(filePath: string): Promise<void> {\n try {\n await fs.unlink(filePath);\n } catch (error) {\n if ((error as any).code !== 'ENOENT') throw error;\n }\n}\n\n/**\n * Atomically create a file using O_CREAT | O_EXCL flags.\n * Returns true if the file was created, false if it already exists.\n * This is atomic at the OS level, safe for concurrent access.\n */\nexport async function writeExclusive(\n filePath: string,\n data: string\n): Promise<boolean> {\n await ensureDir(path.dirname(filePath));\n try {\n await fs.writeFile(filePath, data, { flag: 'wx' });\n return true;\n } catch (error: any) {\n if (error.code === 'EEXIST') {\n return false;\n }\n throw error;\n }\n}\n\nexport async function listJSONFiles(dirPath: string): Promise<string[]> {\n return listFilesByExtension(dirPath, '.json');\n}\n\nexport async function listFilesByExtension(\n dirPath: string,\n extension: string\n): Promise<string[]> {\n try {\n const files = await fs.readdir(dirPath);\n return files\n .filter((f) => f.endsWith(extension))\n .map((f) => f.slice(0, -extension.length));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return [];\n throw error;\n }\n}\n\ninterface PaginatedFileSystemQueryConfig<T> {\n directory: string;\n schema: z.ZodType<T>;\n filePrefix?: string;\n filter?: (item: T) => boolean;\n sortOrder?: 'asc' | 'desc';\n limit?: number;\n cursor?: string;\n getCreatedAt(filename: string): Date | null;\n getId?(item: T): string;\n}\n// Cursor format: \"timestamp|id\" for tie-breaking\ninterface ParsedCursor {\n timestamp: Date;\n id: string | null;\n}\n\nfunction parseCursor(cursor: string | undefined): ParsedCursor | null {\n if (!cursor) return null;\n\n const parts = cursor.split('|');\n return {\n timestamp: new Date(parts[0]),\n id: parts[1] || null,\n };\n}\n\nfunction createCursor(timestamp: Date, id: string | undefined): string {\n return id ? `${timestamp.toISOString()}|${id}` : timestamp.toISOString();\n}\n\nexport async function paginatedFileSystemQuery<T extends { createdAt: Date }>(\n config: PaginatedFileSystemQueryConfig<T>\n): Promise<PaginatedResponse<T>> {\n const {\n directory,\n schema,\n filePrefix,\n filter,\n sortOrder = 'desc',\n limit = 20,\n cursor,\n getCreatedAt,\n getId,\n } = config;\n\n // 1. Get all JSON files in directory\n const fileIds = await listJSONFiles(directory);\n\n // 2. Filter by prefix if provided\n const relevantFileIds = filePrefix\n ? fileIds.filter((fileId) => fileId.startsWith(filePrefix))\n : fileIds;\n\n // 3. ULID Optimization: Filter by cursor using filename timestamps before loading JSON\n const parsedCursor = parseCursor(cursor);\n let candidateFileIds = relevantFileIds;\n\n if (parsedCursor) {\n candidateFileIds = relevantFileIds.filter((fileId) => {\n const filenameDate = getCreatedAt(`${fileId}.json`);\n if (filenameDate) {\n // Use filename timestamp for cursor filtering\n // We need to be careful here: if parsedCursor has an ID (for tie-breaking),\n // we need to include items with the same timestamp for later ID-based filtering.\n // If no ID, we can use strict inequality for optimization.\n const cursorTime = parsedCursor.timestamp.getTime();\n const fileTime = filenameDate.getTime();\n\n if (parsedCursor.id) {\n // Tie-breaking mode: include items at or near cursor timestamp\n return sortOrder === 'desc'\n ? fileTime <= cursorTime\n : fileTime >= cursorTime;\n } else {\n // No tie-breaking: strict inequality\n return sortOrder === 'desc'\n ? fileTime < cursorTime\n : fileTime > cursorTime;\n }\n }\n // Can't extract timestamp from filename (e.g., steps use sequential IDs).\n // Include the file and defer to JSON-based filtering below.\n return true;\n });\n }\n\n // 4. Load files individually and collect valid items\n const validItems: T[] = [];\n\n for (const fileId of candidateFileIds) {\n const filePath = path.join(directory, `${fileId}.json`);\n let item: T | null = null;\n try {\n item = await readJSON(filePath, schema);\n } catch (error: unknown) {\n // We don't expect zod errors to happen, but if the JSON does get malformed,\n // we skip the item. Preferably, we'd have a way to mark items as malformed,\n // so that the UI can display them as such, with richer messaging. In the meantime,\n // we just log a warning and skip the item.\n if (error instanceof z.ZodError) {\n console.warn(\n `Skipping item ${fileId} due to malformed JSON: ${error.message}`\n );\n continue;\n }\n throw error;\n }\n\n if (item) {\n // Apply custom filter early if provided\n if (filter && !filter(item)) continue;\n\n // Double-check cursor filtering with actual createdAt from JSON\n // (in case ULID timestamp differs from stored createdAt)\n if (parsedCursor) {\n const itemTime = item.createdAt.getTime();\n const cursorTime = parsedCursor.timestamp.getTime();\n\n if (sortOrder === 'desc') {\n // For descending order, skip items >= cursor\n if (itemTime > cursorTime) continue;\n // If timestamps are equal, use ID for tie-breaking (skip if ID >= cursorId)\n if (itemTime === cursorTime && parsedCursor.id && getId) {\n const itemId = getId(item);\n if (itemId >= parsedCursor.id) continue;\n }\n } else {\n // For ascending order, skip items <= cursor\n if (itemTime < cursorTime) continue;\n // If timestamps are equal, use ID for tie-breaking (skip if ID <= cursorId)\n if (itemTime === cursorTime && parsedCursor.id && getId) {\n const itemId = getId(item);\n if (itemId <= parsedCursor.id) continue;\n }\n }\n }\n\n validItems.push(item);\n }\n }\n\n // 5. Sort by createdAt (and by ID for tie-breaking if getId is provided)\n validItems.sort((a, b) => {\n const aTime = a.createdAt.getTime();\n const bTime = b.createdAt.getTime();\n const timeComparison = sortOrder === 'asc' ? aTime - bTime : bTime - aTime;\n\n // If timestamps are equal and we have getId, use ID for stable sorting\n if (timeComparison === 0 && getId) {\n const aId = getId(a);\n const bId = getId(b);\n return sortOrder === 'asc'\n ? aId.localeCompare(bId)\n : bId.localeCompare(aId);\n }\n\n return timeComparison;\n });\n\n // 6. Apply pagination\n const hasMore = validItems.length > limit;\n const items = hasMore ? validItems.slice(0, limit) : validItems;\n const nextCursor =\n items.length > 0\n ? createCursor(\n items[items.length - 1].createdAt,\n getId?.(items[items.length - 1])\n )\n : null;\n\n return {\n data: items,\n cursor: nextCursor,\n hasMore,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fs.js","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { EntityConflictError, WorkflowWorldError } from '@workflow/errors';\nimport type { PaginatedResponse } from '@workflow/world';\nimport { monotonicFactory } from 'ulid';\nimport { z } from 'zod';\n\nconst ulid = monotonicFactory(() => Math.random());\n\n/**\n * Truncate a possibly-untrusted value for inclusion in an error message.\n * Keeps error output actionable for developers in dev mode while limiting\n * the amount of attacker-controlled data reflected back if a `world-local`\n * backend is ever run in a context that surfaces these messages to clients.\n */\nfunction truncateForError(value: unknown): string {\n const s = typeof value === 'string' ? value : String(value);\n const MAX = 48;\n return s.length > MAX ? `${s.slice(0, MAX)}…` : s;\n}\n\n/**\n * Thrown when a caller-supplied entity ID contains characters that would\n * escape the storage root (path traversal) or otherwise produce an unsafe\n * filename. Extends {@link WorkflowWorldError} so the platform's standard\n * error-to-HTTP mapping handles it alongside other world-layer errors.\n */\nexport class UnsafeEntityIdError extends WorkflowWorldError {\n constructor(kind: string, value: string) {\n super(\n `Unsafe ${kind} \"${truncateForError(value)}\": must not be empty, contain \".\", \"/\", \"\\\\\", or null bytes`\n );\n this.name = 'UnsafeEntityIdError';\n }\n\n static is(value: unknown): value is UnsafeEntityIdError {\n return value instanceof Error && value.name === 'UnsafeEntityIdError';\n }\n}\n\n/**\n * Validate that a string is safe to embed in a filesystem path as a single\n * path component. Rejects values that are:\n * - empty\n * - starting with `.` (blocks `.`, `..`, `.locks`, `.tmp`, and other\n * hidden or reserved filenames)\n * - containing `.` (would collide with the `.tag` / `.json` suffix the\n * tagging logic strips from filenames; see {@link stripTag} /\n * {@link getObjectCreatedAt})\n * - containing `/`, `\\`, or a NUL byte\n *\n * This is the primary defense against path-traversal attacks where a\n * request body supplies a `runId` / `stepId` / `correlationId` containing\n * `../` sequences to read or write files outside the storage root.\n * {@link resolveWithinBase} provides a belt-and-suspenders containment\n * check at the point of `path.join` for defense in depth.\n *\n * @param kind - Human-readable label used in the error message (e.g. \"runId\")\n * @param value - The value to validate; throws {@link UnsafeEntityIdError}\n * if the value is not safe.\n */\nexport function assertSafeEntityId(kind: string, value: string): void {\n if (\n value.length === 0 ||\n value.startsWith('.') ||\n value.includes('/') ||\n value.includes('\\\\') ||\n value.includes('\\0') ||\n value.includes('.')\n ) {\n throw new UnsafeEntityIdError(kind, value);\n }\n}\n\n/**\n * Join `basedir` with an entity-relative path and assert the result stays\n * inside `basedir`. Complements {@link assertSafeEntityId}: per-entry-point\n * validation is the primary defense, and this final check catches any path\n * escape that slipped past (e.g. a new call site that forgot to validate,\n * or an unusual character combination on a future filesystem). Throws\n * {@link UnsafeEntityIdError} if the joined path escapes `basedir`.\n */\nexport function resolveWithinBase(\n basedir: string,\n ...segments: string[]\n): string {\n const resolvedBase = path.resolve(basedir);\n const joined = path.resolve(basedir, ...segments);\n if (joined !== resolvedBase && !joined.startsWith(resolvedBase + path.sep)) {\n throw new UnsafeEntityIdError('path', segments.join('/'));\n }\n return joined;\n}\n\nconst isWindows = process.platform === 'win32';\n\n/**\n * Execute a filesystem operation with retry logic on Windows.\n * On Windows, file operations can fail with EPERM/EBUSY/EACCES when files\n * are briefly locked by another process or antivirus. This wrapper adds\n * exponential backoff retry logic. On non-Windows platforms, executes directly.\n */\nasync function withWindowsRetry<T>(\n fn: () => Promise<T>,\n maxRetries = 5\n): Promise<T> {\n if (!isWindows) return fn();\n\n const retryableErrors = ['EPERM', 'EBUSY', 'EACCES'];\n const baseDelayMs = 10;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await fn();\n } catch (error: any) {\n const isRetryable =\n attempt < maxRetries && retryableErrors.includes(error.code);\n if (!isRetryable) throw error;\n // Exponential backoff with jitter\n const delay = baseDelayMs * 2 ** attempt + Math.random() * baseDelayMs;\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n // TypeScript: unreachable, but satisfies return type\n throw new Error('Retry loop exited unexpectedly');\n}\n\n// In-memory cache of created files to avoid expensive fs.access() calls\n// This is safe because we only write once per file path (no overwrites without explicit flag)\nconst createdFilesCache = new Set<string>();\n\n/**\n * Clear the created files cache. Useful for testing or when files are deleted externally.\n */\nexport function clearCreatedFilesCache(): void {\n createdFilesCache.clear();\n}\n\nexport { ulidToDate } from '@workflow/world';\n\n/**\n * Regex matching a tag suffix on a fileId (after `.json` has been stripped).\n * E.g., `wrun_ABC.vitest-0` → the `.vitest-0` part.\n * Tags start with a letter and contain alphanumeric chars and hyphens.\n * Entity IDs (ULIDs, step_N, etc.) never contain dots, so the first dot\n * always marks the tag boundary.\n */\nconst TAG_PATTERN = /\\.[a-zA-Z][a-zA-Z0-9-]*$/;\n\n/**\n * Strip a tag suffix from a fileId.\n * `wrun_ABC.vitest-0` → `wrun_ABC`\n * `wrun_ABC` → `wrun_ABC` (no-op if no tag)\n */\nexport function stripTag(fileId: string): string {\n return fileId.replace(TAG_PATTERN, '');\n}\n\n/**\n * Check whether a fileId belongs to a specific tag.\n * `wrun_ABC.vitest-0` belongs to `vitest-0`.\n * Untagged fileIds never match a tag-scoped lookup.\n */\nexport function hasTag(fileId: string, tag: string): boolean {\n return fileId.endsWith(`.${tag}`);\n}\n\n/**\n * Build the file path for an entity, with optional tag embedded in the filename.\n * `taggedPath('/data', 'runs', 'wrun_ABC', 'vitest-0')` → `/data/runs/wrun_ABC.vitest-0.json`\n * `taggedPath('/data', 'runs', 'wrun_ABC')` → `/data/runs/wrun_ABC.json`\n *\n * The `fileId` and `tag` are validated with {@link assertSafeEntityId} and\n * the result is containment-checked with {@link resolveWithinBase} to\n * prevent path-traversal attacks when values are derived from untrusted\n * request input.\n */\nexport function taggedPath(\n basedir: string,\n entityDir: string,\n fileId: string,\n tag?: string\n): string {\n assertSafeEntityId('fileId', fileId);\n if (tag !== undefined) assertSafeEntityId('tag', tag);\n const filename = tag ? `${fileId}.${tag}.json` : `${fileId}.json`;\n return resolveWithinBase(basedir, entityDir, filename);\n}\n\n/**\n * Read a JSON entity with tagged fallback.\n * When a tag is set, tries the tagged path first, then falls back to the\n * untagged path (so a tagged world can read entities written without a tag).\n */\nexport async function readJSONWithFallback<T>(\n basedir: string,\n entityDir: string,\n fileId: string,\n schema: z.ZodType<T>,\n tag?: string\n): Promise<T | null> {\n assertSafeEntityId('fileId', fileId);\n if (tag !== undefined) assertSafeEntityId('tag', tag);\n if (tag) {\n const result = await readJSON(\n resolveWithinBase(basedir, entityDir, `${fileId}.${tag}.json`),\n schema\n );\n if (result !== null) return result;\n }\n return readJSON(\n resolveWithinBase(basedir, entityDir, `${fileId}.json`),\n schema\n );\n}\n\n/**\n * List all filenames in a directory that have a specific tag.\n * Returns full filenames (e.g., `wrun_ABC.vitest-0.json`).\n */\nexport async function listTaggedFiles(\n dirPath: string,\n tag: string\n): Promise<string[]> {\n const suffix = `.${tag}.json`;\n try {\n const files = await fs.readdir(dirPath);\n return files.filter((f) => f.endsWith(suffix));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return [];\n throw error;\n }\n}\n\n/**\n * List all filenames in a directory that have a specific tag and extension.\n * Returns full filenames (e.g., `stream-chnk_ABC.vitest-0.bin`).\n */\nexport async function listTaggedFilesByExtension(\n dirPath: string,\n tag: string,\n extension: string\n): Promise<string[]> {\n const suffix = `.${tag}${extension}`;\n try {\n const files = await fs.readdir(dirPath);\n return files.filter((f) => f.endsWith(suffix));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return [];\n throw error;\n }\n}\n\nexport async function ensureDir(dirPath: string): Promise<void> {\n try {\n await fs.mkdir(dirPath, { recursive: true });\n } catch (_error) {\n // Ignore if already exists\n }\n}\n\ninterface WriteOptions {\n overwrite?: boolean;\n}\n\n/**\n * Custom JSON replacer that encodes Uint8Array as base64 strings.\n * Format: { __type: 'Uint8Array', data: '<base64>' }\n */\nexport function jsonReplacer(_key: string, value: unknown): unknown {\n if (value instanceof Uint8Array) {\n return {\n __type: 'Uint8Array',\n data: Buffer.from(value).toString('base64'),\n };\n }\n return value;\n}\n\n/**\n * Custom JSON reviver that decodes base64 strings back to Uint8Array.\n */\nexport function jsonReviver(_key: string, value: unknown): unknown {\n if (\n value !== null &&\n typeof value === 'object' &&\n (value as any).__type === 'Uint8Array' &&\n typeof (value as any).data === 'string'\n ) {\n return new Uint8Array(Buffer.from((value as any).data, 'base64'));\n }\n return value;\n}\n\nexport async function writeJSON(\n filePath: string,\n data: any,\n opts?: WriteOptions\n): Promise<void> {\n return write(filePath, JSON.stringify(data, jsonReplacer, 2), opts);\n}\n\n/**\n * Writes data to a file using atomic write-rename pattern.\n *\n * Note: While this function uses temp files to avoid partial writes,\n * it does not provide protection against concurrent writes from multiple\n * processes. In a multi-writer scenario, the last writer wins.\n * For production use with multiple writers, consider using a proper\n * database or locking mechanism.\n */\nexport async function write(\n filePath: string,\n data: string | Buffer,\n opts?: WriteOptions\n): Promise<void> {\n if (!opts?.overwrite) {\n // Fast path: check in-memory cache first to avoid expensive fs.access() calls\n // This provides significant performance improvement when creating many files\n if (createdFilesCache.has(filePath)) {\n throw new EntityConflictError(\n `File ${filePath} already exists and 'overwrite' is false`\n );\n }\n\n // Slow path: check filesystem for files created before this process started\n try {\n await fs.access(filePath);\n // File exists on disk, add to cache for future checks\n createdFilesCache.add(filePath);\n throw new EntityConflictError(\n `File ${filePath} already exists and 'overwrite' is false`\n );\n } catch (error: any) {\n // If file doesn't exist (ENOENT), continue with write\n if (error.code !== 'ENOENT') {\n throw error;\n }\n }\n }\n\n const tempPath = `${filePath}.tmp.${ulid()}`;\n let tempFileCreated = false;\n try {\n await ensureDir(path.dirname(filePath));\n await fs.writeFile(tempPath, data);\n tempFileCreated = true;\n await withWindowsRetry(() => fs.rename(tempPath, filePath));\n // Track this file in cache so future writes know it exists\n createdFilesCache.add(filePath);\n } catch (error) {\n // Only try to clean up temp file if it was actually created\n if (tempFileCreated) {\n await withWindowsRetry(() => fs.unlink(tempPath), 3).catch(() => {});\n }\n throw error;\n }\n}\n\nexport async function readJSON<T>(\n filePath: string,\n decoder: z.ZodType<T>\n): Promise<T | null> {\n try {\n const content = await fs.readFile(filePath, 'utf-8');\n return decoder.parse(JSON.parse(content, jsonReviver));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return null;\n throw error;\n }\n}\n\nexport async function readBuffer(filePath: string): Promise<Buffer> {\n const content = await fs.readFile(filePath);\n return content;\n}\n\nexport async function deleteJSON(filePath: string): Promise<void> {\n try {\n await fs.unlink(filePath);\n } catch (error) {\n if ((error as any).code !== 'ENOENT') throw error;\n }\n}\n\n/**\n * Atomically create a file using O_CREAT | O_EXCL flags.\n * Returns true if the file was created, false if it already exists.\n * This is atomic at the OS level, safe for concurrent access.\n */\nexport async function writeExclusive(\n filePath: string,\n data: string\n): Promise<boolean> {\n await ensureDir(path.dirname(filePath));\n try {\n await fs.writeFile(filePath, data, { flag: 'wx' });\n return true;\n } catch (error: any) {\n if (error.code === 'EEXIST') {\n return false;\n }\n throw error;\n }\n}\n\nexport async function listJSONFiles(dirPath: string): Promise<string[]> {\n return listFilesByExtension(dirPath, '.json');\n}\n\nexport async function listFilesByExtension(\n dirPath: string,\n extension: string\n): Promise<string[]> {\n try {\n const files = await fs.readdir(dirPath);\n return files\n .filter((f) => f.endsWith(extension))\n .map((f) => f.slice(0, -extension.length));\n } catch (error) {\n if ((error as any).code === 'ENOENT') return [];\n throw error;\n }\n}\n\ninterface PaginatedFileSystemQueryConfig<T> {\n directory: string;\n schema: z.ZodType<T>;\n filePrefix?: string;\n fileIdFilter?: (fileId: string) => boolean;\n filter?: (item: T) => boolean;\n sortOrder?: 'asc' | 'desc';\n limit?: number;\n cursor?: string;\n getCreatedAt(filename: string): Date | null;\n getId?(item: T): string;\n}\n// Cursor format: \"timestamp|id\" for tie-breaking\ninterface ParsedCursor {\n timestamp: Date;\n id: string | null;\n}\n\nfunction parseCursor(cursor: string | undefined): ParsedCursor | null {\n if (!cursor) return null;\n\n const parts = cursor.split('|');\n return {\n timestamp: new Date(parts[0]),\n id: parts[1] || null,\n };\n}\n\nfunction createCursor(timestamp: Date, id: string | undefined): string {\n return id ? `${timestamp.toISOString()}|${id}` : timestamp.toISOString();\n}\n\nexport async function paginatedFileSystemQuery<T extends { createdAt: Date }>(\n config: PaginatedFileSystemQueryConfig<T>\n): Promise<PaginatedResponse<T>> {\n const {\n directory,\n schema,\n filePrefix,\n fileIdFilter,\n filter,\n sortOrder = 'desc',\n limit = 20,\n cursor,\n getCreatedAt,\n getId,\n } = config;\n\n // Validate filePrefix (typically `${runId}-`) so request-derived prefixes\n // consistently reject unsafe characters. filePrefix is only used below to\n // filter readdir() results by prefix — it doesn't participate in path\n // construction — but keeping the validation rule uniform across the\n // storage layer avoids special cases and catches bad values earlier.\n if (filePrefix !== undefined) {\n assertSafeEntityId('filePrefix', filePrefix);\n }\n\n // 1. Get all JSON files in directory\n const fileIds = await listJSONFiles(directory);\n\n // 2. Filter by prefix if provided\n const relevantFileIds = filePrefix\n ? fileIds.filter((fileId) => fileId.startsWith(filePrefix))\n : fileIds;\n const filteredFileIds = fileIdFilter\n ? relevantFileIds.filter(fileIdFilter)\n : relevantFileIds;\n\n // 3. ULID Optimization: Filter by cursor using filename timestamps before loading JSON\n const parsedCursor = parseCursor(cursor);\n let candidateFileIds = filteredFileIds;\n\n if (parsedCursor) {\n candidateFileIds = filteredFileIds.filter((fileId) => {\n const filenameDate = getCreatedAt(`${fileId}.json`);\n if (filenameDate) {\n // Use filename timestamp for cursor filtering\n // We need to be careful here: if parsedCursor has an ID (for tie-breaking),\n // we need to include items with the same timestamp for later ID-based filtering.\n // If no ID, we can use strict inequality for optimization.\n const cursorTime = parsedCursor.timestamp.getTime();\n const fileTime = filenameDate.getTime();\n\n if (parsedCursor.id) {\n // Tie-breaking mode: include items at or near cursor timestamp\n return sortOrder === 'desc'\n ? fileTime <= cursorTime\n : fileTime >= cursorTime;\n } else {\n // No tie-breaking: strict inequality\n return sortOrder === 'desc'\n ? fileTime < cursorTime\n : fileTime > cursorTime;\n }\n }\n // Can't extract timestamp from filename (e.g., steps use sequential IDs).\n // Include the file and defer to JSON-based filtering below.\n return true;\n });\n }\n\n // 4. Load files individually and collect valid items\n const validItems: T[] = [];\n\n for (const fileId of candidateFileIds) {\n const filePath = path.join(directory, `${fileId}.json`);\n let item: T | null = null;\n try {\n item = await readJSON(filePath, schema);\n } catch (error: unknown) {\n // We don't expect zod errors to happen, but if the JSON does get malformed,\n // we skip the item. Preferably, we'd have a way to mark items as malformed,\n // so that the UI can display them as such, with richer messaging. In the meantime,\n // we just log a warning and skip the item.\n if (error instanceof z.ZodError) {\n console.warn(\n `Skipping item ${fileId} due to malformed JSON: ${error.message}`\n );\n continue;\n }\n throw error;\n }\n\n if (item) {\n // Apply custom filter early if provided\n if (filter && !filter(item)) continue;\n\n // Double-check cursor filtering with actual createdAt from JSON\n // (in case ULID timestamp differs from stored createdAt)\n if (parsedCursor) {\n const itemTime = item.createdAt.getTime();\n const cursorTime = parsedCursor.timestamp.getTime();\n\n if (sortOrder === 'desc') {\n // For descending order, skip items >= cursor\n if (itemTime > cursorTime) continue;\n // If timestamps are equal, use ID for tie-breaking (skip if ID >= cursorId)\n if (itemTime === cursorTime && parsedCursor.id && getId) {\n const itemId = getId(item);\n if (itemId >= parsedCursor.id) continue;\n }\n } else {\n // For ascending order, skip items <= cursor\n if (itemTime < cursorTime) continue;\n // If timestamps are equal, use ID for tie-breaking (skip if ID <= cursorId)\n if (itemTime === cursorTime && parsedCursor.id && getId) {\n const itemId = getId(item);\n if (itemId <= parsedCursor.id) continue;\n }\n }\n }\n\n validItems.push(item);\n }\n }\n\n // 5. Sort by createdAt (and by ID for tie-breaking if getId is provided)\n validItems.sort((a, b) => {\n const aTime = a.createdAt.getTime();\n const bTime = b.createdAt.getTime();\n const timeComparison = sortOrder === 'asc' ? aTime - bTime : bTime - aTime;\n\n // If timestamps are equal and we have getId, use ID for stable sorting\n if (timeComparison === 0 && getId) {\n const aId = getId(a);\n const bId = getId(b);\n return sortOrder === 'asc'\n ? aId.localeCompare(bId)\n : bId.localeCompare(aId);\n }\n\n return timeComparison;\n });\n\n // 6. Apply pagination\n const hasMore = validItems.length > limit;\n const items = hasMore ? validItems.slice(0, limit) : validItems;\n const nextCursor =\n items.length > 0\n ? createCursor(\n items[items.length - 1].createdAt,\n getId?.(items[items.length - 1])\n )\n : null;\n\n return {\n data: items,\n cursor: nextCursor,\n hasMore,\n };\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type LocalWorld = World & {
|
|
|
16
16
|
* @param args.dataDir - Directory for storing workflow data (default: `.workflow-data/`)
|
|
17
17
|
* @param args.port - Port override for queue transport (default: auto-detected)
|
|
18
18
|
* @param args.baseUrl - Full base URL override for queue transport (default: `http://localhost:{port}`)
|
|
19
|
+
* @param args.recoverActiveRuns - Whether `start()` should re-enqueue pending/running runs from storage (default: `true`)
|
|
19
20
|
* @param args.tag - Optional tag to scope files (e.g., `vitest-0`). When set, files are written
|
|
20
21
|
* as `{id}.{tag}.json` and `clear()` only deletes files matching this tag.
|
|
21
22
|
* @throws {DataDirAccessError} If the data directory cannot be created or accessed
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAY1C,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAM7D,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,KAAK,aAAa,EAClB,YAAY,GACb,MAAM,WAAW,CAAC;AAEnB,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG;IAC/B,+EAA+E;IAC/E,eAAe,CACb,MAAM,EAAE,aAAa,GAAG,iBAAiB,EACzC,OAAO,EAAE,aAAa,GACrB,IAAI,CAAC;IACR,qEAAqE;IACrE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,CA6GnE"}
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { rm } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { reenqueueActiveRuns, SPEC_VERSION_CURRENT } from '@workflow/world';
|
|
5
5
|
import { config } from './config.js';
|
|
6
|
-
import { clearCreatedFilesCache, deleteJSON, listTaggedFiles, listTaggedFilesByExtension, readJSON, } from './fs.js';
|
|
6
|
+
import { clearCreatedFilesCache, deleteJSON, hasTag, listTaggedFiles, listTaggedFilesByExtension, readJSON, } from './fs.js';
|
|
7
7
|
import { initDataDir } from './init.js';
|
|
8
8
|
import { instrumentObject } from './instrumentObject.js';
|
|
9
9
|
import { createQueue } from './queue.js';
|
|
@@ -19,6 +19,7 @@ export { DataDirAccessError, DataDirVersionError, ensureDataDir, initDataDir, pa
|
|
|
19
19
|
* @param args.dataDir - Directory for storing workflow data (default: `.workflow-data/`)
|
|
20
20
|
* @param args.port - Port override for queue transport (default: auto-detected)
|
|
21
21
|
* @param args.baseUrl - Full base URL override for queue transport (default: `http://localhost:{port}`)
|
|
22
|
+
* @param args.recoverActiveRuns - Whether `start()` should re-enqueue pending/running runs from storage (default: `true`)
|
|
22
23
|
* @param args.tag - Optional tag to scope files (e.g., `vitest-0`). When set, files are written
|
|
23
24
|
* as `{id}.{tag}.json` and `clear()` only deletes files matching this tag.
|
|
24
25
|
* @throws {DataDirAccessError} If the data directory cannot be created or accessed
|
|
@@ -32,10 +33,11 @@ export function createLocalWorld(args) {
|
|
|
32
33
|
const tag = mergedConfig.tag;
|
|
33
34
|
const queue = createQueue(mergedConfig);
|
|
34
35
|
const storage = createStorage(mergedConfig.dataDir, tag);
|
|
36
|
+
const recoverActiveRuns = mergedConfig.recoverActiveRuns ?? true;
|
|
35
37
|
return {
|
|
36
38
|
specVersion: SPEC_VERSION_CURRENT,
|
|
37
39
|
...queue,
|
|
38
|
-
...
|
|
40
|
+
...storage,
|
|
39
41
|
...instrumentObject('world.streams', {
|
|
40
42
|
...createStreamer(mergedConfig.dataDir, tag),
|
|
41
43
|
...(mergedConfig.streamFlushIntervalMs !== undefined && {
|
|
@@ -44,7 +46,19 @@ export function createLocalWorld(args) {
|
|
|
44
46
|
}),
|
|
45
47
|
async start() {
|
|
46
48
|
await initDataDir(mergedConfig.dataDir);
|
|
47
|
-
|
|
49
|
+
if (!recoverActiveRuns) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const recoveryRuns = tag
|
|
53
|
+
? {
|
|
54
|
+
...storage.runs,
|
|
55
|
+
list: ((params) => storage.runs.list({
|
|
56
|
+
...params,
|
|
57
|
+
fileIdFilter: (fileId) => hasTag(fileId, tag),
|
|
58
|
+
})),
|
|
59
|
+
}
|
|
60
|
+
: storage.runs;
|
|
61
|
+
await reenqueueActiveRuns(recoveryRuns, queue.queue, 'world-local');
|
|
48
62
|
},
|
|
49
63
|
async close() {
|
|
50
64
|
await queue.close();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport { rm } from 'node:fs/promises';\nimport path from 'node:path';\nimport type { World } from '@workflow/world';\nimport { reenqueueActiveRuns, SPEC_VERSION_CURRENT } from '@workflow/world';\nimport type { Config } from './config.js';\nimport { config } from './config.js';\nimport {\n clearCreatedFilesCache,\n deleteJSON,\n listTaggedFiles,\n listTaggedFilesByExtension,\n readJSON,\n} from './fs.js';\nimport { initDataDir } from './init.js';\nimport { instrumentObject } from './instrumentObject.js';\nimport { createQueue, type DirectHandler } from './queue.js';\nimport { hashToken } from './storage/helpers.js';\nimport { createStorage } from './storage.js';\nimport { createStreamer } from './streamer.js';\n\n// Re-export init types and utilities for consumers\nexport {\n DataDirAccessError,\n DataDirVersionError,\n ensureDataDir,\n initDataDir,\n type ParsedVersion,\n parseVersion,\n} from './init.js';\n\nexport type { DirectHandler } from './queue.js';\n\nexport type LocalWorld = World & {\n /** Register a direct in-process handler for a queue prefix, bypassing HTTP. */\n registerHandler(\n prefix: '__wkf_step_' | '__wkf_workflow_',\n handler: DirectHandler\n ): void;\n /** Clear all workflow data (runs, steps, events, hooks, streams). */\n clear(): Promise<void>;\n};\n\n/**\n * Creates a local world instance that combines queue, storage, and streamer functionalities.\n *\n * @param args - Optional configuration object\n * @param args.dataDir - Directory for storing workflow data (default: `.workflow-data/`)\n * @param args.port - Port override for queue transport (default: auto-detected)\n * @param args.baseUrl - Full base URL override for queue transport (default: `http://localhost:{port}`)\n * @param args.tag - Optional tag to scope files (e.g., `vitest-0`). When set, files are written\n * as `{id}.{tag}.json` and `clear()` only deletes files matching this tag.\n * @throws {DataDirAccessError} If the data directory cannot be created or accessed\n * @throws {DataDirVersionError} If the data directory version is incompatible\n */\nexport function createLocalWorld(args?: Partial<Config>): LocalWorld {\n const definedArgs = args\n ? Object.fromEntries(\n Object.entries(args).filter(([, value]) => value !== undefined)\n )\n : {};\n const mergedConfig = { ...config.value, ...definedArgs };\n const tag = mergedConfig.tag;\n const queue = createQueue(mergedConfig);\n const storage = createStorage(mergedConfig.dataDir, tag);\n return {\n specVersion: SPEC_VERSION_CURRENT,\n ...queue,\n ...createStorage(mergedConfig.dataDir, tag),\n ...instrumentObject('world.streams', {\n ...createStreamer(mergedConfig.dataDir, tag),\n ...(mergedConfig.streamFlushIntervalMs !== undefined && {\n streamFlushIntervalMs: mergedConfig.streamFlushIntervalMs,\n }),\n }),\n async start() {\n await initDataDir(mergedConfig.dataDir);\n await reenqueueActiveRuns(storage.runs, queue.queue, 'world-local');\n },\n async close() {\n await queue.close();\n },\n async clear() {\n if (tag) {\n // Selectively delete only files matching this tag\n const basedir = mergedConfig.dataDir;\n\n // Delete hook token constraint files BEFORE deleting the hooks,\n // since we need to read each hook to extract its token hash.\n // Constraint files are untagged ({sha256}.json) so listTaggedFiles\n // won't find them — we must resolve them via the hook data.\n const hooksDir = path.join(basedir, 'hooks');\n const taggedHookFiles = await listTaggedFiles(hooksDir, tag);\n const { HookSchema } = await import('@workflow/world');\n await Promise.all(\n taggedHookFiles.map(async (hookFile) => {\n const hook = await readJSON(\n path.join(hooksDir, hookFile),\n HookSchema\n );\n if (hook?.token) {\n await deleteJSON(\n path.join(hooksDir, 'tokens', `${hashToken(hook.token)}.json`)\n );\n }\n })\n );\n\n // Delete tagged entity files across all directories\n const entityDirs = [\n 'runs',\n 'steps',\n 'events',\n 'hooks',\n 'waits',\n 'streams/runs',\n ];\n await Promise.all(\n entityDirs.map(async (dir) => {\n const fullDir = path.join(basedir, dir);\n const files = await listTaggedFiles(fullDir, tag);\n await Promise.all(\n files.map((f) => deleteJSON(path.join(fullDir, f)))\n );\n })\n );\n // Clean up lock files used for atomic terminal-state guards\n await fs\n .rm(path.join(basedir, '.locks'), { recursive: true, force: true })\n .catch(() => {});\n // Delete tagged stream chunks (.{tag}.bin files)\n const chunksDir = path.join(basedir, 'streams', 'chunks');\n const taggedBinFiles = await listTaggedFilesByExtension(\n chunksDir,\n tag,\n '.bin'\n );\n await Promise.all(\n taggedBinFiles.map((f) =>\n fs.unlink(path.join(chunksDir, f)).catch(() => {})\n )\n );\n // Clear the in-memory write cache so deleted paths are forgotten\n clearCreatedFilesCache();\n } else {\n await rm(mergedConfig.dataDir, { recursive: true, force: true });\n await initDataDir(mergedConfig.dataDir);\n }\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport { rm } from 'node:fs/promises';\nimport path from 'node:path';\nimport type { World } from '@workflow/world';\nimport { reenqueueActiveRuns, SPEC_VERSION_CURRENT } from '@workflow/world';\nimport type { Config } from './config.js';\nimport { config } from './config.js';\nimport {\n clearCreatedFilesCache,\n deleteJSON,\n hasTag,\n listTaggedFiles,\n listTaggedFilesByExtension,\n readJSON,\n} from './fs.js';\nimport { initDataDir } from './init.js';\nimport { instrumentObject } from './instrumentObject.js';\nimport { createQueue, type DirectHandler } from './queue.js';\nimport { hashToken } from './storage/helpers.js';\nimport { createStorage } from './storage.js';\nimport { createStreamer } from './streamer.js';\n\n// Re-export init types and utilities for consumers\nexport {\n DataDirAccessError,\n DataDirVersionError,\n ensureDataDir,\n initDataDir,\n type ParsedVersion,\n parseVersion,\n} from './init.js';\n\nexport type { DirectHandler } from './queue.js';\n\nexport type LocalWorld = World & {\n /** Register a direct in-process handler for a queue prefix, bypassing HTTP. */\n registerHandler(\n prefix: '__wkf_step_' | '__wkf_workflow_',\n handler: DirectHandler\n ): void;\n /** Clear all workflow data (runs, steps, events, hooks, streams). */\n clear(): Promise<void>;\n};\n\n/**\n * Creates a local world instance that combines queue, storage, and streamer functionalities.\n *\n * @param args - Optional configuration object\n * @param args.dataDir - Directory for storing workflow data (default: `.workflow-data/`)\n * @param args.port - Port override for queue transport (default: auto-detected)\n * @param args.baseUrl - Full base URL override for queue transport (default: `http://localhost:{port}`)\n * @param args.recoverActiveRuns - Whether `start()` should re-enqueue pending/running runs from storage (default: `true`)\n * @param args.tag - Optional tag to scope files (e.g., `vitest-0`). When set, files are written\n * as `{id}.{tag}.json` and `clear()` only deletes files matching this tag.\n * @throws {DataDirAccessError} If the data directory cannot be created or accessed\n * @throws {DataDirVersionError} If the data directory version is incompatible\n */\nexport function createLocalWorld(args?: Partial<Config>): LocalWorld {\n const definedArgs = args\n ? Object.fromEntries(\n Object.entries(args).filter(([, value]) => value !== undefined)\n )\n : {};\n const mergedConfig = { ...config.value, ...definedArgs };\n const tag = mergedConfig.tag;\n const queue = createQueue(mergedConfig);\n const storage = createStorage(mergedConfig.dataDir, tag);\n const recoverActiveRuns = mergedConfig.recoverActiveRuns ?? true;\n return {\n specVersion: SPEC_VERSION_CURRENT,\n ...queue,\n ...storage,\n ...instrumentObject('world.streams', {\n ...createStreamer(mergedConfig.dataDir, tag),\n ...(mergedConfig.streamFlushIntervalMs !== undefined && {\n streamFlushIntervalMs: mergedConfig.streamFlushIntervalMs,\n }),\n }),\n async start() {\n await initDataDir(mergedConfig.dataDir);\n if (!recoverActiveRuns) {\n return;\n }\n const recoveryRuns = tag\n ? {\n ...storage.runs,\n list: ((params) =>\n storage.runs.list({\n ...params,\n fileIdFilter: (fileId) => hasTag(fileId, tag),\n })) as typeof storage.runs.list,\n }\n : storage.runs;\n await reenqueueActiveRuns(recoveryRuns, queue.queue, 'world-local');\n },\n async close() {\n await queue.close();\n },\n async clear() {\n if (tag) {\n // Selectively delete only files matching this tag\n const basedir = mergedConfig.dataDir;\n\n // Delete hook token constraint files BEFORE deleting the hooks,\n // since we need to read each hook to extract its token hash.\n // Constraint files are untagged ({sha256}.json) so listTaggedFiles\n // won't find them — we must resolve them via the hook data.\n const hooksDir = path.join(basedir, 'hooks');\n const taggedHookFiles = await listTaggedFiles(hooksDir, tag);\n const { HookSchema } = await import('@workflow/world');\n await Promise.all(\n taggedHookFiles.map(async (hookFile) => {\n const hook = await readJSON(\n path.join(hooksDir, hookFile),\n HookSchema\n );\n if (hook?.token) {\n await deleteJSON(\n path.join(hooksDir, 'tokens', `${hashToken(hook.token)}.json`)\n );\n }\n })\n );\n\n // Delete tagged entity files across all directories\n const entityDirs = [\n 'runs',\n 'steps',\n 'events',\n 'hooks',\n 'waits',\n 'streams/runs',\n ];\n await Promise.all(\n entityDirs.map(async (dir) => {\n const fullDir = path.join(basedir, dir);\n const files = await listTaggedFiles(fullDir, tag);\n await Promise.all(\n files.map((f) => deleteJSON(path.join(fullDir, f)))\n );\n })\n );\n // Clean up lock files used for atomic terminal-state guards\n await fs\n .rm(path.join(basedir, '.locks'), { recursive: true, force: true })\n .catch(() => {});\n // Delete tagged stream chunks (.{tag}.bin files)\n const chunksDir = path.join(basedir, 'streams', 'chunks');\n const taggedBinFiles = await listTaggedFilesByExtension(\n chunksDir,\n tag,\n '.bin'\n );\n await Promise.all(\n taggedBinFiles.map((f) =>\n fs.unlink(path.join(chunksDir, f)).catch(() => {})\n )\n );\n // Clear the in-memory write cache so deleted paths are forgotten\n clearCreatedFilesCache();\n } else {\n await rm(mergedConfig.dataDir, { recursive: true, force: true });\n await initDataDir(mergedConfig.dataDir);\n }\n },\n };\n}\n"]}
|
package/dist/queue.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,KAAK,KAAK,EAAkB,MAAM,iBAAiB,CAAC;AAKxE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAiD1C,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEhE,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG;IAC/B,kDAAkD;IAClD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,+EAA+E;IAC/E,eAAe,CACb,MAAM,EAAE,aAAa,GAAG,iBAAiB,EACzC,OAAO,EAAE,aAAa,GACrB,IAAI,CAAC;CACT,CAAC;
|
|
1
|
+
{"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,KAAK,KAAK,EAAkB,MAAM,iBAAiB,CAAC;AAKxE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAiD1C,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEhE,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG;IAC/B,kDAAkD;IAClD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,+EAA+E;IAC/E,eAAe,CACb,MAAM,EAAE,aAAa,GAAG,iBAAiB,EACzC,OAAO,EAAE,aAAa,GACrB,IAAI,CAAC;CACT,CAAC;AAuCF,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,UAAU,CAoQ/D"}
|
package/dist/queue.js
CHANGED
|
@@ -45,6 +45,22 @@ const MAX_SAFE_TIMEOUT_MS = 2147483647;
|
|
|
45
45
|
const DEFAULT_CONCURRENCY_LIMIT = 1000;
|
|
46
46
|
const WORKFLOW_LOCAL_QUEUE_CONCURRENCY = parseInt(process.env.WORKFLOW_LOCAL_QUEUE_CONCURRENCY ?? '0', 10) ||
|
|
47
47
|
DEFAULT_CONCURRENCY_LIMIT;
|
|
48
|
+
const DETACHED_ARRAYBUFFER_ERROR = 'Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer';
|
|
49
|
+
const PROXY_HANDLER_DOCS_URL = 'https://workflow-sdk.dev/docs/getting-started/next#configure-proxy-handler';
|
|
50
|
+
function isDetachedArrayBufferQueueError(error) {
|
|
51
|
+
let current = error;
|
|
52
|
+
const visited = new Set();
|
|
53
|
+
while (current && typeof current === 'object' && !visited.has(current)) {
|
|
54
|
+
visited.add(current);
|
|
55
|
+
if ('message' in current &&
|
|
56
|
+
typeof current.message === 'string' &&
|
|
57
|
+
current.message.includes(DETACHED_ARRAYBUFFER_ERROR)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
current = 'cause' in current ? current.cause : undefined;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
48
64
|
function getQueueRoute(queueName) {
|
|
49
65
|
if (queueName.startsWith('__wkf_step_')) {
|
|
50
66
|
return { pathname: 'step', prefix: '__wkf_step_' };
|
|
@@ -185,7 +201,21 @@ export function createQueue(config) {
|
|
|
185
201
|
// These are expected and should not cause unhandled rejection warnings
|
|
186
202
|
const isAbortError = err?.name === 'AbortError' || err?.name === 'ResponseAborted';
|
|
187
203
|
if (!isAbortError) {
|
|
188
|
-
|
|
204
|
+
if (isDetachedArrayBufferQueueError(err)) {
|
|
205
|
+
console.error(`[local world] Queue operation failed: detected "${DETACHED_ARRAYBUFFER_ERROR}". ` +
|
|
206
|
+
"This usually means a Next.js proxy/middleware consumed Workflow's internal " +
|
|
207
|
+
'request before the executor could read it. Exclude `/.well-known/workflow/*` ' +
|
|
208
|
+
`from your matcher. See ${PROXY_HANDLER_DOCS_URL}`, {
|
|
209
|
+
queueName,
|
|
210
|
+
messageId,
|
|
211
|
+
...(runId && { runId }),
|
|
212
|
+
...(stepId && { stepId }),
|
|
213
|
+
originalError: err,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
console.error('[local world] Queue operation failed:', err);
|
|
218
|
+
}
|
|
189
219
|
}
|
|
190
220
|
})
|
|
191
221
|
.finally(() => {
|