@workflow/web-shared 4.0.1-beta.9 → 4.1.0-beta.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/api/workflow-api-client.d.ts +325 -85
- package/dist/api/workflow-api-client.d.ts.map +1 -1
- package/dist/api/workflow-api-client.js +370 -214
- package/dist/api/workflow-api-client.js.map +1 -1
- package/dist/api/workflow-server-actions.d.ts +136 -3
- package/dist/api/workflow-server-actions.d.ts.map +1 -1
- package/dist/api/workflow-server-actions.js +649 -116
- package/dist/api/workflow-server-actions.js.map +1 -1
- package/dist/components/ui/card.d.ts +9 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +18 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/error-card.d.ts +15 -0
- package/dist/components/ui/error-card.d.ts.map +1 -0
- package/dist/components/ui/error-card.js +14 -0
- package/dist/components/ui/error-card.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +3 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +7 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/error-boundary.d.ts +28 -0
- package/dist/error-boundary.d.ts.map +1 -0
- package/dist/error-boundary.js +51 -0
- package/dist/error-boundary.js.map +1 -0
- package/dist/event-list-view.d.ts +13 -0
- package/dist/event-list-view.d.ts.map +1 -0
- package/dist/event-list-view.js +183 -0
- package/dist/event-list-view.js.map +1 -0
- package/dist/hook-actions.d.ts +59 -0
- package/dist/hook-actions.d.ts.map +1 -0
- package/dist/hook-actions.js +76 -0
- package/dist/hook-actions.js.map +1 -0
- package/dist/hooks/use-dark-mode.d.ts +9 -0
- package/dist/hooks/use-dark-mode.d.ts.map +1 -0
- package/dist/hooks/use-dark-mode.js +30 -0
- package/dist/hooks/use-dark-mode.js.map +1 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/event-analysis.d.ts +55 -0
- package/dist/lib/event-analysis.d.ts.map +1 -0
- package/dist/lib/event-analysis.js +161 -0
- package/dist/lib/event-analysis.js.map +1 -0
- package/dist/lib/utils.d.ts +44 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +109 -0
- package/dist/lib/utils.js.map +1 -1
- package/dist/run-trace-view.d.ts.map +1 -1
- package/dist/run-trace-view.js +1 -1
- package/dist/run-trace-view.js.map +1 -1
- package/dist/sidebar/attribute-panel.d.ts +12 -2
- package/dist/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/sidebar/attribute-panel.js +368 -23
- package/dist/sidebar/attribute-panel.js.map +1 -1
- package/dist/sidebar/conversation-view.d.ts +7 -0
- package/dist/sidebar/conversation-view.d.ts.map +1 -0
- package/dist/sidebar/conversation-view.js +125 -0
- package/dist/sidebar/conversation-view.js.map +1 -0
- package/dist/sidebar/detail-card.d.ts.map +1 -1
- package/dist/sidebar/detail-card.js +2 -2
- package/dist/sidebar/detail-card.js.map +1 -1
- package/dist/sidebar/entity-detail-panel.d.ts +12 -0
- package/dist/sidebar/entity-detail-panel.d.ts.map +1 -0
- package/dist/sidebar/entity-detail-panel.js +190 -0
- package/dist/sidebar/entity-detail-panel.js.map +1 -0
- package/dist/sidebar/events-list.d.ts +2 -1
- package/dist/sidebar/events-list.d.ts.map +1 -1
- package/dist/sidebar/events-list.js +11 -10
- package/dist/sidebar/events-list.js.map +1 -1
- package/dist/sidebar/resolve-hook-modal.d.ts +16 -0
- package/dist/sidebar/resolve-hook-modal.d.ts.map +1 -0
- package/dist/sidebar/resolve-hook-modal.js +74 -0
- package/dist/sidebar/resolve-hook-modal.js.map +1 -0
- package/dist/stream-viewer.d.ts +13 -0
- package/dist/stream-viewer.d.ts.map +1 -0
- package/dist/stream-viewer.js +109 -0
- package/dist/stream-viewer.js.map +1 -0
- package/dist/trace-viewer/components/markers.d.ts.map +1 -1
- package/dist/trace-viewer/components/markers.js +3 -2
- package/dist/trace-viewer/components/markers.js.map +1 -1
- package/dist/trace-viewer/components/node.d.ts.map +1 -1
- package/dist/trace-viewer/components/node.js +1 -0
- package/dist/trace-viewer/components/node.js.map +1 -1
- package/dist/trace-viewer/components/search.d.ts.map +1 -1
- package/dist/trace-viewer/components/search.js +1 -0
- package/dist/trace-viewer/components/search.js.map +1 -1
- package/dist/trace-viewer/components/span-detail-panel.js +2 -2
- package/dist/trace-viewer/components/span-detail-panel.js.map +1 -1
- package/dist/trace-viewer/context.d.ts.map +1 -1
- package/dist/trace-viewer/context.js +1 -0
- package/dist/trace-viewer/context.js.map +1 -1
- package/dist/trace-viewer/trace-viewer.module.css +47 -30
- package/dist/trace-viewer/types.d.ts +11 -0
- package/dist/trace-viewer/types.d.ts.map +1 -1
- package/dist/trace-viewer/util/timing.d.ts +7 -1
- package/dist/trace-viewer/util/timing.d.ts.map +1 -1
- package/dist/trace-viewer/util/timing.js +7 -12
- package/dist/trace-viewer/util/timing.js.map +1 -1
- package/dist/trace-viewer/util/tree.d.ts.map +1 -1
- package/dist/trace-viewer/util/tree.js +4 -0
- package/dist/trace-viewer/util/tree.js.map +1 -1
- package/dist/trace-viewer/util/use-immediate-style.d.ts.map +1 -1
- package/dist/trace-viewer/util/use-immediate-style.js +1 -0
- package/dist/trace-viewer/util/use-immediate-style.js.map +1 -1
- package/dist/trace-viewer/util/use-streaming-spans.d.ts.map +1 -1
- package/dist/trace-viewer/util/use-streaming-spans.js +2 -1
- package/dist/trace-viewer/util/use-streaming-spans.js.map +1 -1
- package/dist/trace-viewer/util/use-trackpad-zoom.d.ts.map +1 -1
- package/dist/trace-viewer/util/use-trackpad-zoom.js +1 -0
- package/dist/trace-viewer/util/use-trackpad-zoom.js.map +1 -1
- package/dist/trace-viewer/worker.js +1 -1
- package/dist/trace-viewer/worker.js.map +1 -1
- package/dist/workflow-trace-view.d.ts +3 -1
- package/dist/workflow-trace-view.d.ts.map +1 -1
- package/dist/workflow-trace-view.js +28 -11
- package/dist/workflow-trace-view.js.map +1 -1
- package/dist/workflow-traces/event-colors.d.ts +1 -1
- package/dist/workflow-traces/event-colors.js +2 -2
- package/dist/workflow-traces/event-colors.js.map +1 -1
- package/dist/workflow-traces/trace-colors.d.ts.map +1 -1
- package/dist/workflow-traces/trace-colors.js +1 -3
- package/dist/workflow-traces/trace-colors.js.map +1 -1
- package/dist/workflow-traces/trace-span-construction.d.ts +18 -3
- package/dist/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/workflow-traces/trace-span-construction.js +84 -31
- package/dist/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/workflow-traces/trace-time-utils.d.ts +2 -2
- package/dist/workflow-traces/trace-time-utils.d.ts.map +1 -1
- package/dist/workflow-traces/trace-time-utils.js +9 -0
- package/dist/workflow-traces/trace-time-utils.js.map +1 -1
- package/package.json +24 -14
- package/dist/sidebar/workflow-detail-panel.d.ts +0 -8
- package/dist/sidebar/workflow-detail-panel.d.ts.map +0 -1
- package/dist/sidebar/workflow-detail-panel.js +0 -56
- package/dist/sidebar/workflow-detail-panel.js.map +0 -1
|
@@ -1,47 +1,379 @@
|
|
|
1
1
|
'use server';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { hydrateResourceIO } from '@workflow/core/observability';
|
|
3
|
-
import { createWorld, start } from '@workflow/core/runtime';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { createWorld, healthCheck, resumeHook as resumeHookRuntime, start, } from '@workflow/core/runtime';
|
|
6
|
+
import { getDeserializeStream, getExternalRevivers, } from '@workflow/core/serialization';
|
|
7
|
+
import { WorkflowAPIError, WorkflowRunNotFoundError } from '@workflow/errors';
|
|
8
|
+
import { findWorkflowDataDir } from '@workflow/utils/check-data-dir';
|
|
9
|
+
import { isLegacySpecVersion, SPEC_VERSION_LEGACY, } from '@workflow/world';
|
|
10
|
+
import { createVercelWorld } from '@workflow/world-vercel';
|
|
11
|
+
/**
|
|
12
|
+
* Map from WORKFLOW_TARGET_WORLD value to human-readable display name
|
|
13
|
+
*/
|
|
14
|
+
function getBackendDisplayName(targetWorld) {
|
|
15
|
+
if (!targetWorld)
|
|
16
|
+
return 'Local';
|
|
17
|
+
switch (targetWorld) {
|
|
18
|
+
case 'local':
|
|
19
|
+
return 'Local';
|
|
20
|
+
case 'vercel':
|
|
21
|
+
return 'Vercel';
|
|
22
|
+
case '@workflow/world-postgres':
|
|
23
|
+
case 'postgres':
|
|
24
|
+
return 'PostgreSQL';
|
|
25
|
+
default:
|
|
26
|
+
// For custom worlds, try to make a readable name
|
|
27
|
+
if (targetWorld.startsWith('@')) {
|
|
28
|
+
// Extract package name without scope for display
|
|
29
|
+
const parts = targetWorld.split('/');
|
|
30
|
+
return parts[parts.length - 1] || targetWorld;
|
|
31
|
+
}
|
|
32
|
+
return targetWorld;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function getEffectiveBackendId() {
|
|
36
|
+
const targetWorld = process.env.WORKFLOW_TARGET_WORLD;
|
|
37
|
+
if (targetWorld) {
|
|
38
|
+
return targetWorld;
|
|
39
|
+
}
|
|
40
|
+
// Match @workflow/core/runtime defaulting: vercel if VERCEL_DEPLOYMENT_ID is set, else local.
|
|
41
|
+
return process.env.VERCEL_DEPLOYMENT_ID ? 'vercel' : 'local';
|
|
42
|
+
}
|
|
43
|
+
function getObservabilityCwd() {
|
|
44
|
+
const raw = process.env.WORKFLOW_OBSERVABILITY_CWD;
|
|
45
|
+
if (!raw) {
|
|
46
|
+
return process.cwd();
|
|
47
|
+
}
|
|
48
|
+
return path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Ensure local-world env is derived consistently when running `packages/web` directly.
|
|
52
|
+
*
|
|
53
|
+
* Without this, the UI may *display* a dataDir detected from WORKFLOW_OBSERVABILITY_CWD,
|
|
54
|
+
* while the actual World reads from `WORKFLOW_LOCAL_DATA_DIR` (defaulting to `.workflow-data`
|
|
55
|
+
* under the web package cwd), resulting in "no runs" even though data exists.
|
|
56
|
+
*/
|
|
57
|
+
async function ensureLocalWorldDataDirEnv() {
|
|
58
|
+
if (process.env.WORKFLOW_LOCAL_DATA_DIR)
|
|
59
|
+
return;
|
|
60
|
+
const cwd = getObservabilityCwd();
|
|
61
|
+
const info = await findWorkflowDataDir(cwd);
|
|
62
|
+
// Prefer a discovered workflow-data directory (e.g. `.next/workflow-data`).
|
|
63
|
+
if (info.dataDir) {
|
|
64
|
+
process.env.WORKFLOW_LOCAL_DATA_DIR = info.dataDir;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Fall back to a canonical location under the target project directory.
|
|
68
|
+
process.env.WORKFLOW_LOCAL_DATA_DIR = path.resolve(cwd, '.workflow-data');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Extract hostname from a database URL without exposing credentials.
|
|
72
|
+
*/
|
|
73
|
+
function extractHostnameFromUrl(url) {
|
|
74
|
+
if (!url)
|
|
75
|
+
return undefined;
|
|
76
|
+
try {
|
|
77
|
+
const parsed = new URL(url);
|
|
78
|
+
return parsed.hostname || undefined;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Extract database name from a URL where pathname is like "/dbname".
|
|
86
|
+
* (Works for postgres/mongodb-style URLs; returns undefined when not applicable.)
|
|
87
|
+
*/
|
|
88
|
+
function extractDatabaseFromUrl(url) {
|
|
89
|
+
if (!url)
|
|
90
|
+
return undefined;
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(url);
|
|
93
|
+
const dbName = parsed.pathname?.slice(1);
|
|
94
|
+
return dbName || undefined;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Keep this list in sync with `worlds-manifest.json` env + credentialsNote.
|
|
101
|
+
const WORLD_ENV_ALLOWLIST_BY_TARGET_WORLD = {
|
|
102
|
+
// Official
|
|
103
|
+
local: [
|
|
104
|
+
'WORKFLOW_TARGET_WORLD',
|
|
105
|
+
'WORKFLOW_LOCAL_DATA_DIR',
|
|
106
|
+
'WORKFLOW_MANIFEST_PATH',
|
|
107
|
+
'WORKFLOW_OBSERVABILITY_CWD',
|
|
108
|
+
'PORT',
|
|
109
|
+
],
|
|
110
|
+
'@workflow/world-local': [
|
|
111
|
+
'WORKFLOW_TARGET_WORLD',
|
|
112
|
+
'WORKFLOW_LOCAL_DATA_DIR',
|
|
113
|
+
'WORKFLOW_MANIFEST_PATH',
|
|
114
|
+
'WORKFLOW_OBSERVABILITY_CWD',
|
|
115
|
+
'PORT',
|
|
116
|
+
],
|
|
117
|
+
postgres: ['WORKFLOW_TARGET_WORLD', 'WORKFLOW_POSTGRES_URL'],
|
|
118
|
+
'@workflow/world-postgres': [
|
|
119
|
+
'WORKFLOW_TARGET_WORLD',
|
|
120
|
+
'WORKFLOW_POSTGRES_URL',
|
|
121
|
+
],
|
|
122
|
+
vercel: [
|
|
123
|
+
'WORKFLOW_TARGET_WORLD',
|
|
124
|
+
'WORKFLOW_VERCEL_ENV',
|
|
125
|
+
'WORKFLOW_VERCEL_TEAM',
|
|
126
|
+
'WORKFLOW_VERCEL_PROJECT',
|
|
127
|
+
'WORKFLOW_VERCEL_AUTH_TOKEN',
|
|
128
|
+
],
|
|
129
|
+
'@workflow/world-vercel': [
|
|
130
|
+
'WORKFLOW_TARGET_WORLD',
|
|
131
|
+
'WORKFLOW_VERCEL_ENV',
|
|
132
|
+
'WORKFLOW_VERCEL_TEAM',
|
|
133
|
+
'WORKFLOW_VERCEL_PROJECT',
|
|
134
|
+
'WORKFLOW_VERCEL_AUTH_TOKEN',
|
|
135
|
+
],
|
|
136
|
+
// Community (from worlds-manifest.json)
|
|
137
|
+
'@workflow-worlds/starter': ['WORKFLOW_TARGET_WORLD'],
|
|
138
|
+
'@workflow-worlds/turso': [
|
|
139
|
+
'WORKFLOW_TARGET_WORLD',
|
|
140
|
+
'WORKFLOW_TURSO_DATABASE_URL',
|
|
141
|
+
],
|
|
142
|
+
'@workflow-worlds/mongodb': [
|
|
143
|
+
'WORKFLOW_TARGET_WORLD',
|
|
144
|
+
'WORKFLOW_MONGODB_URI',
|
|
145
|
+
'WORKFLOW_MONGODB_DATABASE_NAME',
|
|
146
|
+
],
|
|
147
|
+
'@workflow-worlds/redis': ['WORKFLOW_TARGET_WORLD', 'WORKFLOW_REDIS_URI'],
|
|
148
|
+
'workflow-world-jazz': [
|
|
149
|
+
'WORKFLOW_TARGET_WORLD',
|
|
150
|
+
// credentialsNote:
|
|
151
|
+
'JAZZ_API_KEY',
|
|
152
|
+
'JAZZ_WORKER_ACCOUNT',
|
|
153
|
+
'JAZZ_WORKER_SECRET',
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
function getAllowedEnvKeysForBackend(backendId) {
|
|
157
|
+
return (WORLD_ENV_ALLOWLIST_BY_TARGET_WORLD[backendId] ?? ['WORKFLOW_TARGET_WORLD']);
|
|
158
|
+
}
|
|
159
|
+
// Keep this list in sync with `worlds-manifest.json` env + credentialsNote.
|
|
160
|
+
//
|
|
161
|
+
// IMPORTANT: This is intentionally explicit (no heuristics). We only redact values for env
|
|
162
|
+
// vars that are known + whitelisted and that we *know* contain secrets/credentials.
|
|
163
|
+
const WORLD_SENSITIVE_ENV_KEYS = new Set([
|
|
164
|
+
// Official
|
|
165
|
+
'WORKFLOW_POSTGRES_URL',
|
|
166
|
+
'WORKFLOW_VERCEL_AUTH_TOKEN',
|
|
167
|
+
// Community
|
|
168
|
+
'WORKFLOW_TURSO_DATABASE_URL',
|
|
169
|
+
'WORKFLOW_MONGODB_URI',
|
|
170
|
+
'WORKFLOW_REDIS_URI',
|
|
171
|
+
'JAZZ_API_KEY',
|
|
172
|
+
'JAZZ_WORKER_SECRET',
|
|
173
|
+
]);
|
|
174
|
+
function isSet(value) {
|
|
175
|
+
return value !== undefined && value !== null && value !== '';
|
|
176
|
+
}
|
|
177
|
+
function deriveDbInfoForKey(key, value) {
|
|
178
|
+
// Only attempt for URL-like strings.
|
|
179
|
+
if (!value.includes(':'))
|
|
180
|
+
return null;
|
|
181
|
+
try {
|
|
182
|
+
const parsed = new URL(value);
|
|
183
|
+
const protocol = (parsed.protocol || '').replace(':', '');
|
|
184
|
+
// file: URIs are not useful for hostname/db display
|
|
185
|
+
if (protocol === 'file')
|
|
186
|
+
return null;
|
|
187
|
+
const hostname = extractHostnameFromUrl(value);
|
|
188
|
+
const database = extractDatabaseFromUrl(value);
|
|
189
|
+
const out = {};
|
|
190
|
+
if (hostname)
|
|
191
|
+
out[`derived.${key}.hostname`] = hostname;
|
|
192
|
+
if (database)
|
|
193
|
+
out[`derived.${key}.database`] = database;
|
|
194
|
+
if (protocol)
|
|
195
|
+
out[`derived.${key}.protocol`] = protocol;
|
|
196
|
+
return Object.keys(out).length ? out : null;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function getLocalDisplayInfo() {
|
|
203
|
+
const cwd = getObservabilityCwd();
|
|
204
|
+
const dataDirInfo = await findWorkflowDataDir(cwd);
|
|
205
|
+
const out = {
|
|
206
|
+
'local.shortName': dataDirInfo.shortName,
|
|
207
|
+
'local.projectDir': dataDirInfo.projectDir,
|
|
208
|
+
};
|
|
209
|
+
if (dataDirInfo.dataDir) {
|
|
210
|
+
out['local.dataDirPath'] = dataDirInfo.dataDir;
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
function collectAllowedEnv(allowedKeys) {
|
|
215
|
+
const publicEnv = {};
|
|
216
|
+
const sensitiveEnvKeys = [];
|
|
217
|
+
const derivedDisplayInfo = {};
|
|
218
|
+
for (const key of allowedKeys) {
|
|
219
|
+
const value = process.env[key];
|
|
220
|
+
if (!isSet(value))
|
|
221
|
+
continue;
|
|
222
|
+
if (WORLD_SENSITIVE_ENV_KEYS.has(key)) {
|
|
223
|
+
sensitiveEnvKeys.push(key);
|
|
224
|
+
const derived = deriveDbInfoForKey(key, value);
|
|
225
|
+
if (derived)
|
|
226
|
+
Object.assign(derivedDisplayInfo, derived);
|
|
7
227
|
continue;
|
|
8
228
|
}
|
|
9
|
-
|
|
229
|
+
publicEnv[key] = value;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
publicEnv,
|
|
233
|
+
sensitiveEnvKeys: Array.from(new Set(sensitiveEnvKeys)).sort(),
|
|
234
|
+
derivedDisplayInfo,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get public configuration info that is safe to send to the client.
|
|
239
|
+
*
|
|
240
|
+
* This is the ONLY server action that intentionally exposes env-derived data,
|
|
241
|
+
* and that data is strictly whitelisted per world backend.
|
|
242
|
+
*/
|
|
243
|
+
export async function getPublicServerConfig() {
|
|
244
|
+
const backendId = getEffectiveBackendId();
|
|
245
|
+
const backendDisplayName = getBackendDisplayName(backendId);
|
|
246
|
+
const allowedKeys = getAllowedEnvKeysForBackend(backendId);
|
|
247
|
+
const { publicEnv, sensitiveEnvKeys, derivedDisplayInfo } = collectAllowedEnv(allowedKeys);
|
|
248
|
+
const displayInfo = { ...derivedDisplayInfo };
|
|
249
|
+
if (backendId === 'local' || backendId === '@workflow/world-local') {
|
|
250
|
+
Object.assign(displayInfo, await getLocalDisplayInfo());
|
|
251
|
+
}
|
|
252
|
+
const config = {
|
|
253
|
+
backendDisplayName,
|
|
254
|
+
backendId,
|
|
255
|
+
publicEnv,
|
|
256
|
+
sensitiveEnvKeys,
|
|
257
|
+
displayInfo: Object.keys(displayInfo).length ? displayInfo : undefined,
|
|
258
|
+
};
|
|
259
|
+
// Provide defaults for commonly expected keys without revealing extra secrets.
|
|
260
|
+
if ((backendId === 'vercel' || backendId === '@workflow/world-vercel') &&
|
|
261
|
+
!publicEnv.WORKFLOW_VERCEL_ENV) {
|
|
262
|
+
config.publicEnv.WORKFLOW_VERCEL_ENV = 'production';
|
|
10
263
|
}
|
|
11
|
-
return
|
|
264
|
+
return config;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Cache for World instances.
|
|
268
|
+
*
|
|
269
|
+
* IMPORTANT:
|
|
270
|
+
* - We only cache non-vercel worlds.
|
|
271
|
+
* - Cache keys are derived from **server-side** WORKFLOW_* env vars only.
|
|
272
|
+
*/
|
|
273
|
+
const worldCache = new Map();
|
|
274
|
+
/**
|
|
275
|
+
* Get or create a World instance based on configuration.
|
|
276
|
+
*
|
|
277
|
+
* The @workflow/web UI should always pass `{}` for envMap.
|
|
278
|
+
*/
|
|
279
|
+
async function getWorldFromEnv(userEnvMap) {
|
|
280
|
+
const backendId = getEffectiveBackendId();
|
|
281
|
+
const isVercelWorld = ['vercel', '@workflow/world-vercel'].includes(backendId);
|
|
282
|
+
// For the vercel world specifically, we do not cache the world,
|
|
283
|
+
// and allow user-provided env, as it can be a multi-tenant environment,
|
|
284
|
+
// and we instantiate the world per-user directly to avoid having to set
|
|
285
|
+
// process.env.
|
|
286
|
+
if (isVercelWorld) {
|
|
287
|
+
return createVercelWorld({
|
|
288
|
+
token: userEnvMap.WORKFLOW_VERCEL_AUTH_TOKEN ||
|
|
289
|
+
process.env.WORKFLOW_VERCEL_AUTH_TOKEN,
|
|
290
|
+
projectConfig: {
|
|
291
|
+
environment: userEnvMap.WORKFLOW_VERCEL_ENV || process.env.WORKFLOW_VERCEL_ENV,
|
|
292
|
+
projectId: userEnvMap.WORKFLOW_VERCEL_PROJECT ||
|
|
293
|
+
process.env.WORKFLOW_VERCEL_PROJECT,
|
|
294
|
+
teamId: userEnvMap.WORKFLOW_VERCEL_TEAM || process.env.WORKFLOW_VERCEL_TEAM,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// For other worlds, we intentionally do not trust or apply client-provided env,
|
|
299
|
+
// to avoid potential security risks in self-hosted scenarios.
|
|
300
|
+
// Ensure local-world reads from the same project directory the UI is inspecting.
|
|
301
|
+
if (backendId === 'local' || backendId === '@workflow/world-local') {
|
|
302
|
+
await ensureLocalWorldDataDirEnv();
|
|
303
|
+
}
|
|
304
|
+
// Cache key derived ONLY from WORKFLOW_* env vars.
|
|
305
|
+
const workflowEnvEntries = Object.entries(process.env).filter(([key]) => key.startsWith('WORKFLOW_'));
|
|
306
|
+
workflowEnvEntries.sort(([a], [b]) => a.localeCompare(b));
|
|
307
|
+
const cacheKey = JSON.stringify(Object.fromEntries(workflowEnvEntries));
|
|
308
|
+
const cachedWorld = worldCache.get(cacheKey);
|
|
309
|
+
if (cachedWorld) {
|
|
310
|
+
return cachedWorld;
|
|
311
|
+
}
|
|
312
|
+
const world = createWorld();
|
|
313
|
+
worldCache.set(cacheKey, world);
|
|
314
|
+
return world;
|
|
12
315
|
}
|
|
13
316
|
/**
|
|
14
317
|
* Creates a structured error object from a caught error
|
|
15
318
|
*/
|
|
16
|
-
function createServerActionError(error, operation,
|
|
319
|
+
function createServerActionError(error, operation, requestParams) {
|
|
17
320
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
321
|
+
console.error(`[web-api] ${operation} error:`, err);
|
|
322
|
+
let errorResponse;
|
|
323
|
+
if (WorkflowAPIError.is(error)) {
|
|
324
|
+
// If the World threw the error on fetch/fs.read, we add that data
|
|
325
|
+
// to the error object
|
|
326
|
+
errorResponse = {
|
|
327
|
+
message: getUserFacingErrorMessage(err, error.status),
|
|
328
|
+
layer: 'API',
|
|
329
|
+
cause: err.stack || err.message,
|
|
330
|
+
request: {
|
|
331
|
+
operation,
|
|
332
|
+
params: requestParams ?? {},
|
|
333
|
+
status: error.status,
|
|
334
|
+
url: error.url,
|
|
335
|
+
code: error.code ?? undefined,
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
else if (WorkflowRunNotFoundError.is(error)) {
|
|
340
|
+
// The World might repackage the error as a WorkflowRunNotFoundError
|
|
341
|
+
errorResponse = {
|
|
342
|
+
message: getUserFacingErrorMessage(error, 404),
|
|
343
|
+
layer: 'API',
|
|
344
|
+
cause: err.stack || err.message,
|
|
345
|
+
request: { operation, status: 404, params: requestParams ?? {} },
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
errorResponse = {
|
|
350
|
+
message: getUserFacingErrorMessage(err),
|
|
351
|
+
layer: 'server',
|
|
352
|
+
cause: err.stack || err.message,
|
|
353
|
+
request: { status: 500, operation, params: requestParams ?? {} },
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
success: false,
|
|
358
|
+
error: errorResponse,
|
|
28
359
|
};
|
|
29
|
-
return actionError;
|
|
30
360
|
}
|
|
31
361
|
/**
|
|
32
362
|
* Converts an error into a user-facing message
|
|
33
363
|
*/
|
|
34
|
-
function
|
|
364
|
+
function getUserFacingErrorMessage(error, status) {
|
|
365
|
+
if (!status) {
|
|
366
|
+
return `Error creating response: ${error.message}`;
|
|
367
|
+
}
|
|
35
368
|
// Check for common error patterns
|
|
36
|
-
if (
|
|
369
|
+
if (status === 403 || status === 401) {
|
|
37
370
|
return 'Access denied. Please check your credentials and permissions.';
|
|
38
371
|
}
|
|
39
|
-
if (
|
|
372
|
+
if (status === 404) {
|
|
40
373
|
return 'The requested resource was not found.';
|
|
41
374
|
}
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
return 'An internal server error occurred. Please try again later.';
|
|
375
|
+
if (status === 500) {
|
|
376
|
+
return 'Error connecting to World backend, please try again later.';
|
|
45
377
|
}
|
|
46
378
|
if (error.message?.includes('Network') || error.message?.includes('fetch')) {
|
|
47
379
|
return 'Network error. Please check your connection and try again.';
|
|
@@ -49,6 +381,12 @@ function getUserFacingMessage(error) {
|
|
|
49
381
|
// Return the original message for other errors
|
|
50
382
|
return error.message || 'An unexpected error occurred';
|
|
51
383
|
}
|
|
384
|
+
const toJSONCompatible = (data) => {
|
|
385
|
+
if (data && typeof data === 'object') {
|
|
386
|
+
return JSON.parse(JSON.stringify(data));
|
|
387
|
+
}
|
|
388
|
+
return data;
|
|
389
|
+
};
|
|
52
390
|
const hydrate = (data) => {
|
|
53
391
|
try {
|
|
54
392
|
return hydrateResourceIO(data);
|
|
@@ -63,6 +401,7 @@ const hydrate = (data) => {
|
|
|
63
401
|
* @returns ServerActionResult with success=true and the data
|
|
64
402
|
*/
|
|
65
403
|
function createResponse(data) {
|
|
404
|
+
data = toJSONCompatible(data);
|
|
66
405
|
return {
|
|
67
406
|
success: true,
|
|
68
407
|
data,
|
|
@@ -74,7 +413,7 @@ function createResponse(data) {
|
|
|
74
413
|
export async function fetchRuns(worldEnv, params) {
|
|
75
414
|
const { cursor, sortOrder = 'desc', limit = 10, workflowName, status, } = params;
|
|
76
415
|
try {
|
|
77
|
-
const world = getWorldFromEnv(worldEnv);
|
|
416
|
+
const world = await getWorldFromEnv(worldEnv);
|
|
78
417
|
const result = await world.runs.list({
|
|
79
418
|
...(workflowName ? { workflowName } : {}),
|
|
80
419
|
...(status ? { status: status } : {}),
|
|
@@ -88,11 +427,7 @@ export async function fetchRuns(worldEnv, params) {
|
|
|
88
427
|
});
|
|
89
428
|
}
|
|
90
429
|
catch (error) {
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
success: false,
|
|
94
|
-
error: createServerActionError(error, 'world.runs.list', params),
|
|
95
|
-
};
|
|
430
|
+
return createServerActionError(error, 'world.runs.list', params);
|
|
96
431
|
}
|
|
97
432
|
}
|
|
98
433
|
/**
|
|
@@ -100,20 +435,16 @@ export async function fetchRuns(worldEnv, params) {
|
|
|
100
435
|
*/
|
|
101
436
|
export async function fetchRun(worldEnv, runId, resolveData = 'all') {
|
|
102
437
|
try {
|
|
103
|
-
const world = getWorldFromEnv(worldEnv);
|
|
438
|
+
const world = await getWorldFromEnv(worldEnv);
|
|
104
439
|
const run = await world.runs.get(runId, { resolveData });
|
|
105
440
|
const hydratedRun = hydrate(run);
|
|
106
441
|
return createResponse(hydratedRun);
|
|
107
442
|
}
|
|
108
443
|
catch (error) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
runId,
|
|
114
|
-
resolveData,
|
|
115
|
-
}),
|
|
116
|
-
};
|
|
444
|
+
return createServerActionError(error, 'world.runs.get', {
|
|
445
|
+
runId,
|
|
446
|
+
resolveData,
|
|
447
|
+
});
|
|
117
448
|
}
|
|
118
449
|
}
|
|
119
450
|
/**
|
|
@@ -122,27 +453,24 @@ export async function fetchRun(worldEnv, runId, resolveData = 'all') {
|
|
|
122
453
|
export async function fetchSteps(worldEnv, runId, params) {
|
|
123
454
|
const { cursor, sortOrder = 'asc', limit = 100 } = params;
|
|
124
455
|
try {
|
|
125
|
-
const world = getWorldFromEnv(worldEnv);
|
|
456
|
+
const world = await getWorldFromEnv(worldEnv);
|
|
126
457
|
const result = await world.steps.list({
|
|
127
458
|
runId,
|
|
128
459
|
pagination: { cursor, limit, sortOrder },
|
|
129
460
|
resolveData: 'none',
|
|
130
461
|
});
|
|
131
462
|
return createResponse({
|
|
463
|
+
// StepWithoutData has undefined input/output, but after hydration the structure is compatible
|
|
132
464
|
data: result.data.map(hydrate),
|
|
133
465
|
cursor: result.cursor ?? undefined,
|
|
134
466
|
hasMore: result.hasMore,
|
|
135
467
|
});
|
|
136
468
|
}
|
|
137
469
|
catch (error) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
runId,
|
|
143
|
-
...params,
|
|
144
|
-
}),
|
|
145
|
-
};
|
|
470
|
+
return createServerActionError(error, 'world.steps.list', {
|
|
471
|
+
runId,
|
|
472
|
+
...params,
|
|
473
|
+
});
|
|
146
474
|
}
|
|
147
475
|
}
|
|
148
476
|
/**
|
|
@@ -150,20 +478,17 @@ export async function fetchSteps(worldEnv, runId, params) {
|
|
|
150
478
|
*/
|
|
151
479
|
export async function fetchStep(worldEnv, runId, stepId, resolveData = 'all') {
|
|
152
480
|
try {
|
|
153
|
-
const world = getWorldFromEnv(worldEnv);
|
|
481
|
+
const world = await getWorldFromEnv(worldEnv);
|
|
154
482
|
const step = await world.steps.get(runId, stepId, { resolveData });
|
|
155
|
-
|
|
483
|
+
const hydratedStep = hydrate(step);
|
|
484
|
+
return createResponse(hydratedStep);
|
|
156
485
|
}
|
|
157
486
|
catch (error) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
stepId,
|
|
164
|
-
resolveData,
|
|
165
|
-
}),
|
|
166
|
-
};
|
|
487
|
+
return createServerActionError(error, 'world.steps.get', {
|
|
488
|
+
runId,
|
|
489
|
+
stepId,
|
|
490
|
+
resolveData,
|
|
491
|
+
});
|
|
167
492
|
}
|
|
168
493
|
}
|
|
169
494
|
/**
|
|
@@ -172,7 +497,7 @@ export async function fetchStep(worldEnv, runId, stepId, resolveData = 'all') {
|
|
|
172
497
|
export async function fetchEvents(worldEnv, runId, params) {
|
|
173
498
|
const { cursor, sortOrder = 'asc', limit = 1000 } = params;
|
|
174
499
|
try {
|
|
175
|
-
const world = getWorldFromEnv(worldEnv);
|
|
500
|
+
const world = await getWorldFromEnv(worldEnv);
|
|
176
501
|
const result = await world.events.list({
|
|
177
502
|
runId,
|
|
178
503
|
pagination: { cursor, limit, sortOrder },
|
|
@@ -185,14 +510,10 @@ export async function fetchEvents(worldEnv, runId, params) {
|
|
|
185
510
|
});
|
|
186
511
|
}
|
|
187
512
|
catch (error) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
runId,
|
|
193
|
-
...params,
|
|
194
|
-
}),
|
|
195
|
-
};
|
|
513
|
+
return createServerActionError(error, 'world.events.list', {
|
|
514
|
+
runId,
|
|
515
|
+
...params,
|
|
516
|
+
});
|
|
196
517
|
}
|
|
197
518
|
}
|
|
198
519
|
/**
|
|
@@ -201,24 +522,23 @@ export async function fetchEvents(worldEnv, runId, params) {
|
|
|
201
522
|
export async function fetchEventsByCorrelationId(worldEnv, correlationId, params) {
|
|
202
523
|
const { cursor, sortOrder = 'asc', limit = 1000, withData = false } = params;
|
|
203
524
|
try {
|
|
204
|
-
const world = getWorldFromEnv(worldEnv);
|
|
525
|
+
const world = await getWorldFromEnv(worldEnv);
|
|
205
526
|
const result = await world.events.listByCorrelationId({
|
|
206
527
|
correlationId,
|
|
207
528
|
pagination: { cursor, limit, sortOrder },
|
|
208
529
|
resolveData: withData ? 'all' : 'none',
|
|
209
530
|
});
|
|
210
531
|
return createResponse({
|
|
211
|
-
data: result.data,
|
|
532
|
+
data: result.data.map(hydrate),
|
|
212
533
|
cursor: result.cursor ?? undefined,
|
|
213
534
|
hasMore: result.hasMore,
|
|
214
535
|
});
|
|
215
536
|
}
|
|
216
537
|
catch (error) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
};
|
|
538
|
+
return createServerActionError(error, 'world.events.listByCorrelationId', {
|
|
539
|
+
correlationId,
|
|
540
|
+
...params,
|
|
541
|
+
});
|
|
222
542
|
}
|
|
223
543
|
}
|
|
224
544
|
/**
|
|
@@ -227,7 +547,7 @@ export async function fetchEventsByCorrelationId(worldEnv, correlationId, params
|
|
|
227
547
|
export async function fetchHooks(worldEnv, params) {
|
|
228
548
|
const { runId, cursor, sortOrder = 'desc', limit = 10 } = params;
|
|
229
549
|
try {
|
|
230
|
-
const world = getWorldFromEnv(worldEnv);
|
|
550
|
+
const world = await getWorldFromEnv(worldEnv);
|
|
231
551
|
const result = await world.hooks.list({
|
|
232
552
|
...(runId ? { runId } : {}),
|
|
233
553
|
pagination: { cursor, limit, sortOrder },
|
|
@@ -240,11 +560,7 @@ export async function fetchHooks(worldEnv, params) {
|
|
|
240
560
|
});
|
|
241
561
|
}
|
|
242
562
|
catch (error) {
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
success: false,
|
|
246
|
-
error: createServerActionError(error, 'world.hooks.list', params),
|
|
247
|
-
};
|
|
563
|
+
return createServerActionError(error, 'world.hooks.list', params);
|
|
248
564
|
}
|
|
249
565
|
}
|
|
250
566
|
/**
|
|
@@ -252,19 +568,15 @@ export async function fetchHooks(worldEnv, params) {
|
|
|
252
568
|
*/
|
|
253
569
|
export async function fetchHook(worldEnv, hookId, resolveData = 'all') {
|
|
254
570
|
try {
|
|
255
|
-
const world = getWorldFromEnv(worldEnv);
|
|
571
|
+
const world = await getWorldFromEnv(worldEnv);
|
|
256
572
|
const hook = await world.hooks.get(hookId, { resolveData });
|
|
257
573
|
return createResponse(hydrate(hook));
|
|
258
574
|
}
|
|
259
575
|
catch (error) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
hookId,
|
|
265
|
-
resolveData,
|
|
266
|
-
}),
|
|
267
|
-
};
|
|
576
|
+
return createServerActionError(error, 'world.hooks.get', {
|
|
577
|
+
hookId,
|
|
578
|
+
resolveData,
|
|
579
|
+
});
|
|
268
580
|
}
|
|
269
581
|
}
|
|
270
582
|
/**
|
|
@@ -272,16 +584,20 @@ export async function fetchHook(worldEnv, hookId, resolveData = 'all') {
|
|
|
272
584
|
*/
|
|
273
585
|
export async function cancelRun(worldEnv, runId) {
|
|
274
586
|
try {
|
|
275
|
-
const world = getWorldFromEnv(worldEnv);
|
|
276
|
-
await world.runs.
|
|
587
|
+
const world = await getWorldFromEnv(worldEnv);
|
|
588
|
+
const run = await world.runs.get(runId, { resolveData: 'none' });
|
|
589
|
+
const compatMode = isLegacySpecVersion(run.specVersion);
|
|
590
|
+
const eventData = {
|
|
591
|
+
eventType: 'run_cancelled',
|
|
592
|
+
specVersion: run.specVersion || 1,
|
|
593
|
+
};
|
|
594
|
+
await world.events.create(runId, eventData, { v1Compat: compatMode });
|
|
277
595
|
return createResponse(undefined);
|
|
278
596
|
}
|
|
279
597
|
catch (error) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
error: createServerActionError(error, 'world.runs.cancel', { runId }),
|
|
284
|
-
};
|
|
598
|
+
return createServerActionError(error, 'world.events.create', {
|
|
599
|
+
runId,
|
|
600
|
+
});
|
|
285
601
|
}
|
|
286
602
|
}
|
|
287
603
|
/**
|
|
@@ -289,40 +605,257 @@ export async function cancelRun(worldEnv, runId) {
|
|
|
289
605
|
*
|
|
290
606
|
* This requires the ID of an existing run of which to re-use the deployment ID of.
|
|
291
607
|
*/
|
|
292
|
-
export async function recreateRun(worldEnv, runId) {
|
|
608
|
+
export async function recreateRun(worldEnv, runId, deploymentId) {
|
|
293
609
|
try {
|
|
294
|
-
const world = getWorldFromEnv({ ...worldEnv });
|
|
610
|
+
const world = await getWorldFromEnv({ ...worldEnv });
|
|
295
611
|
const run = await world.runs.get(runId);
|
|
612
|
+
// Get original input/output
|
|
296
613
|
const hydratedRun = hydrate(run);
|
|
297
|
-
|
|
614
|
+
// Preserve original specVersion - if undefined (legacy v1), use SPEC_VERSION_LEGACY
|
|
298
615
|
const newRun = await start({ workflowId: run.workflowName }, hydratedRun.input, {
|
|
299
|
-
deploymentId,
|
|
616
|
+
deploymentId: deploymentId ?? run.deploymentId,
|
|
617
|
+
world,
|
|
618
|
+
specVersion: run.specVersion ?? SPEC_VERSION_LEGACY,
|
|
300
619
|
});
|
|
301
620
|
return createResponse(newRun.runId);
|
|
302
621
|
}
|
|
303
622
|
catch (error) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
623
|
+
return createServerActionError(error, 'recreateRun', { runId });
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Re-enqueue a workflow run.
|
|
628
|
+
*
|
|
629
|
+
* This re-enqueues the workflow orchestration layer. It's a no-op unless the workflow
|
|
630
|
+
* got stuck due to an implementation issue in the World. Useful for debugging custom Worlds.
|
|
631
|
+
*/
|
|
632
|
+
export async function reenqueueRun(worldEnv, runId) {
|
|
633
|
+
try {
|
|
634
|
+
const world = await getWorldFromEnv({ ...worldEnv });
|
|
635
|
+
const run = await world.runs.get(runId);
|
|
636
|
+
const deploymentId = run.deploymentId;
|
|
637
|
+
await world.queue(`__wkf_workflow_${run.workflowName}`, {
|
|
638
|
+
runId,
|
|
639
|
+
}, {
|
|
640
|
+
deploymentId,
|
|
641
|
+
});
|
|
642
|
+
return createResponse(undefined);
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
return createServerActionError(error, 'reenqueueRun', { runId });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Wake up a workflow run by interrupting pending sleep() calls.
|
|
650
|
+
*
|
|
651
|
+
* This finds wait_created events without matching wait_completed events,
|
|
652
|
+
* creates wait_completed events for them, and then re-enqueues the run.
|
|
653
|
+
*
|
|
654
|
+
* @param worldEnv - Environment configuration for the World
|
|
655
|
+
* @param runId - The run ID to wake up
|
|
656
|
+
* @param options - Optional settings to narrow down targeting (specific correlation IDs)
|
|
657
|
+
*/
|
|
658
|
+
export async function wakeUpRun(worldEnv, runId, options) {
|
|
659
|
+
try {
|
|
660
|
+
const world = await getWorldFromEnv({ ...worldEnv });
|
|
661
|
+
const run = await world.runs.get(runId);
|
|
662
|
+
const deploymentId = run.deploymentId;
|
|
663
|
+
const compatMode = isLegacySpecVersion(run.specVersion);
|
|
664
|
+
// Fetch all events for the run
|
|
665
|
+
const eventsResult = await world.events.list({
|
|
666
|
+
runId,
|
|
667
|
+
pagination: { limit: 1000 },
|
|
668
|
+
resolveData: 'none',
|
|
669
|
+
});
|
|
670
|
+
// Find wait_created events without matching wait_completed events
|
|
671
|
+
const waitCreatedEvents = eventsResult.data.filter((e) => e.eventType === 'wait_created');
|
|
672
|
+
const waitCompletedCorrelationIds = new Set(eventsResult.data
|
|
673
|
+
.filter((e) => e.eventType === 'wait_completed')
|
|
674
|
+
.map((e) => e.correlationId));
|
|
675
|
+
let pendingWaits = waitCreatedEvents.filter((e) => !waitCompletedCorrelationIds.has(e.correlationId));
|
|
676
|
+
// If specific correlation IDs are provided, filter to only those
|
|
677
|
+
if (options?.correlationIds && options.correlationIds.length > 0) {
|
|
678
|
+
const targetCorrelationIds = new Set(options.correlationIds);
|
|
679
|
+
pendingWaits = pendingWaits.filter((e) => e.correlationId && targetCorrelationIds.has(e.correlationId));
|
|
680
|
+
}
|
|
681
|
+
// Create wait_completed events for each pending wait
|
|
682
|
+
for (const waitEvent of pendingWaits) {
|
|
683
|
+
if (waitEvent.correlationId) {
|
|
684
|
+
// For v2, include specVersion in event data; for v1Compat, it's not needed
|
|
685
|
+
const eventData = compatMode
|
|
686
|
+
? {
|
|
687
|
+
eventType: 'wait_completed',
|
|
688
|
+
correlationId: waitEvent.correlationId,
|
|
689
|
+
}
|
|
690
|
+
: {
|
|
691
|
+
eventType: 'wait_completed',
|
|
692
|
+
correlationId: waitEvent.correlationId,
|
|
693
|
+
specVersion: run.specVersion,
|
|
694
|
+
};
|
|
695
|
+
await world.events.create(runId, eventData, { v1Compat: compatMode });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Re-enqueue the run to wake it up
|
|
699
|
+
if (pendingWaits.length > 0) {
|
|
700
|
+
await world.queue(`__wkf_workflow_${run.workflowName}`, {
|
|
701
|
+
runId,
|
|
702
|
+
}, {
|
|
703
|
+
deploymentId,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
return createResponse({ stoppedCount: pendingWaits.length });
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
return createServerActionError(error, 'wakeUpRun', {
|
|
710
|
+
runId,
|
|
711
|
+
correlationIds: options?.correlationIds,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Resume a hook by sending a payload.
|
|
717
|
+
*
|
|
718
|
+
* This sends a payload to a hook identified by its token, which resumes
|
|
719
|
+
* the associated workflow run. The payload will be available as the return
|
|
720
|
+
* value of the `createHook()` call in the workflow.
|
|
721
|
+
*
|
|
722
|
+
* @param worldEnv - Environment configuration for the World
|
|
723
|
+
* @param token - The hook token
|
|
724
|
+
* @param payload - The JSON payload to send to the hook
|
|
725
|
+
*/
|
|
726
|
+
export async function resumeHook(worldEnv, token, payload) {
|
|
727
|
+
try {
|
|
728
|
+
// Initialize the world so resumeHookRuntime can access it
|
|
729
|
+
await getWorldFromEnv({ ...worldEnv });
|
|
730
|
+
const hook = await resumeHookRuntime(token, payload);
|
|
731
|
+
return createResponse({
|
|
732
|
+
hookId: hook.hookId,
|
|
733
|
+
runId: hook.runId,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
return createServerActionError(error, 'resumeHook', {
|
|
738
|
+
token,
|
|
739
|
+
});
|
|
309
740
|
}
|
|
310
741
|
}
|
|
311
742
|
export async function readStreamServerAction(env, streamId, startIndex) {
|
|
312
743
|
try {
|
|
313
|
-
const world = getWorldFromEnv(env);
|
|
744
|
+
const world = await getWorldFromEnv(env);
|
|
745
|
+
// We should probably use getRun().getReadable() instead, to make the UI
|
|
746
|
+
// more consistent with runtime behavior, and also expose a "replay" and "startIndex",
|
|
747
|
+
// feature, to allow for testing World behavior.
|
|
314
748
|
const stream = await world.readFromStream(streamId, startIndex);
|
|
315
|
-
|
|
749
|
+
const revivers = getExternalRevivers(globalThis, [], '');
|
|
750
|
+
const transform = getDeserializeStream(revivers);
|
|
751
|
+
return stream.pipeThrough(transform);
|
|
316
752
|
}
|
|
317
753
|
catch (error) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
754
|
+
const actionError = createServerActionError(error, 'world.readFromStream', {
|
|
755
|
+
streamId,
|
|
756
|
+
startIndex,
|
|
757
|
+
});
|
|
758
|
+
if (!actionError.success) {
|
|
759
|
+
return actionError.error;
|
|
760
|
+
}
|
|
761
|
+
// Shouldn't happen, this is just a type guard
|
|
762
|
+
throw new Error();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* List all stream IDs for a run
|
|
767
|
+
*/
|
|
768
|
+
export async function fetchStreams(env, runId) {
|
|
769
|
+
try {
|
|
770
|
+
const world = await getWorldFromEnv(env);
|
|
771
|
+
const streams = await world.listStreamsByRunId(runId);
|
|
772
|
+
return createResponse(streams);
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
return createServerActionError(error, 'world.listStreamsByRunId', {
|
|
776
|
+
runId,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Fetch the workflows manifest from the workflow route directory
|
|
782
|
+
* The manifest is generated at build time and contains static structure info about workflows
|
|
783
|
+
*
|
|
784
|
+
* Configuration priority:
|
|
785
|
+
* 1. WORKFLOW_MANIFEST_PATH - explicit path to the manifest file
|
|
786
|
+
* 2. Standard Next.js app router locations (app/.well-known/workflow/v1/manifest.json)
|
|
787
|
+
* 3. WORKFLOW_EMBEDDED_DATA_DIR - legacy data directory
|
|
788
|
+
*/
|
|
789
|
+
export async function fetchWorkflowsManifest(_worldEnv) {
|
|
790
|
+
const cwd = getObservabilityCwd();
|
|
791
|
+
// Helper to resolve path (absolute or relative to cwd)
|
|
792
|
+
const resolvePath = (p) => path.isAbsolute(p) ? p : path.join(cwd, p);
|
|
793
|
+
// Build list of paths to try, in priority order
|
|
794
|
+
const manifestPaths = [];
|
|
795
|
+
// 1. Explicit manifest path configuration (highest priority)
|
|
796
|
+
if (process.env.WORKFLOW_MANIFEST_PATH) {
|
|
797
|
+
manifestPaths.push(resolvePath(process.env.WORKFLOW_MANIFEST_PATH));
|
|
798
|
+
}
|
|
799
|
+
// 2. Standard Next.js app router locations
|
|
800
|
+
manifestPaths.push(path.join(cwd, 'app/.well-known/workflow/v1/manifest.json'), path.join(cwd, 'src/app/.well-known/workflow/v1/manifest.json'));
|
|
801
|
+
// 3. Legacy data directory locations
|
|
802
|
+
if (process.env.WORKFLOW_EMBEDDED_DATA_DIR) {
|
|
803
|
+
manifestPaths.push(path.join(resolvePath(process.env.WORKFLOW_EMBEDDED_DATA_DIR), 'manifest.json'));
|
|
804
|
+
}
|
|
805
|
+
// Try each path until we find the manifest
|
|
806
|
+
for (const manifestPath of manifestPaths) {
|
|
807
|
+
try {
|
|
808
|
+
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
809
|
+
const manifest = JSON.parse(content);
|
|
810
|
+
return createResponse(manifest);
|
|
811
|
+
}
|
|
812
|
+
catch (_err) {
|
|
813
|
+
// Continue to next path
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// If no manifest found, return an empty manifest
|
|
817
|
+
// This allows the UI to work without workflows graph data
|
|
818
|
+
return createResponse({
|
|
819
|
+
version: '1.0.0',
|
|
820
|
+
steps: {},
|
|
821
|
+
workflows: {},
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Run a queue-based health check on a workflow endpoint.
|
|
826
|
+
*
|
|
827
|
+
* This sends a health check message through the Queue infrastructure,
|
|
828
|
+
* bypassing Vercel Deployment Protection. The endpoint processes the
|
|
829
|
+
* message and writes a response to a stream, which we then read to
|
|
830
|
+
* verify the endpoint is healthy.
|
|
831
|
+
*
|
|
832
|
+
* @param worldEnv - Environment configuration for the World
|
|
833
|
+
* @param endpoint - Which endpoint to check: 'workflow' or 'step'
|
|
834
|
+
* @param options - Optional configuration (timeout in ms)
|
|
835
|
+
*/
|
|
836
|
+
export async function runHealthCheck(worldEnv, endpoint, options) {
|
|
837
|
+
const startTime = Date.now();
|
|
838
|
+
try {
|
|
839
|
+
const world = await getWorldFromEnv(worldEnv);
|
|
840
|
+
const result = await healthCheck(world, endpoint, options);
|
|
841
|
+
const latencyMs = Date.now() - startTime;
|
|
842
|
+
return createResponse({
|
|
843
|
+
...result,
|
|
844
|
+
latencyMs,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
const latencyMs = Date.now() - startTime;
|
|
849
|
+
// For health check failures, we want to return success=true with healthy=false
|
|
850
|
+
// so the UI can display the error properly, rather than propagating the server
|
|
851
|
+
// action error. This allows the health check result to be parsed by the UI
|
|
852
|
+
// even when the endpoint is down or unreachable.
|
|
853
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
854
|
+
return createResponse({
|
|
855
|
+
healthy: false,
|
|
856
|
+
error: errorMessage,
|
|
857
|
+
latencyMs,
|
|
858
|
+
});
|
|
326
859
|
}
|
|
327
860
|
}
|
|
328
861
|
//# sourceMappingURL=workflow-server-actions.js.map
|