@relayflows/sdk 2.0.52 → 2.0.54
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/authored-node-entry.js +14 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +26 -3
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-parallel-eligibility.d.ts +14 -0
- package/dist/authored-parallel-eligibility.d.ts.map +1 -1
- package/dist/authored-parallel-eligibility.js +63 -15
- package/dist/authored-parallel-eligibility.js.map +1 -1
- package/dist/authored-root.d.ts +2 -2
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +31 -8
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-verdict.d.ts +3 -1
- package/dist/authored-verdict.d.ts.map +1 -1
- package/dist/authored-verdict.js +5 -2
- package/dist/authored-verdict.js.map +1 -1
- package/dist/cloud-signal.d.ts +4 -2
- package/dist/cloud-signal.d.ts.map +1 -1
- package/dist/cloud-signal.js +26 -81
- package/dist/cloud-signal.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-node-entry.ts +15 -0
- package/src/authored-node-runner.ts +24 -3
- package/src/authored-parallel-eligibility.ts +65 -14
- package/src/authored-root.ts +27 -9
- package/src/authored-verdict.ts +5 -2
- package/src/cloud-signal.ts +28 -74
package/src/authored-root.ts
CHANGED
|
@@ -3,10 +3,11 @@ import { loadPinnedAuthoredSource } from './authored-source-authority.js';
|
|
|
3
3
|
import { assertAuthoredRuntimeAvailable, runAuthoredInNode } from './authored-node-runner.js';
|
|
4
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
5
|
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
6
7
|
import { canonicalize } from './canonical.js';
|
|
7
8
|
import { compileSpec, toKernelSpec } from './compile.js';
|
|
8
9
|
import { executeAuthoredFlow, type AuthoredFlowExecutionResult, type AuthoredFlowSuspendedResult } from './authored-flow-executor.js';
|
|
9
|
-
import { AUTHORED_PARALLEL_ROOT_KIND, AUTHORED_ROOT_KIND } from './authored-verdict.js';
|
|
10
|
+
import { AUTHORED_PARALLEL_ROOT_KIND, AUTHORED_PARALLEL_ROOT_KIND_V3, AUTHORED_ROOT_KIND } from './authored-verdict.js';
|
|
10
11
|
import {
|
|
11
12
|
loadAuthoredFlow,
|
|
12
13
|
type LoadedAuthoredFlow,
|
|
@@ -43,7 +44,7 @@ export interface AuthoredRootExtension {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export interface AuthoredRootMetadata {
|
|
46
|
-
readonly kind: typeof ROOT_KIND | typeof AUTHORED_PARALLEL_ROOT_KIND;
|
|
47
|
+
readonly kind: typeof ROOT_KIND | typeof AUTHORED_PARALLEL_ROOT_KIND | typeof AUTHORED_PARALLEL_ROOT_KIND_V3;
|
|
47
48
|
readonly flowName: string;
|
|
48
49
|
readonly flowPath: string;
|
|
49
50
|
readonly sourceSha256: string;
|
|
@@ -95,18 +96,34 @@ export async function executeDurableAuthoredFlow(
|
|
|
95
96
|
})));
|
|
96
97
|
const definition = loaded.getDefinition(loaded.handle);
|
|
97
98
|
let parallelEligible = false;
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
let parallelIneligibleReason: string | undefined;
|
|
100
|
+
if (loaded.graph.length !== 1) parallelIneligibleReason = 'source_graph';
|
|
101
|
+
else if ((loaded.extensions?.length ?? 0) !== 0) parallelIneligibleReason = 'extensions';
|
|
102
|
+
else if (definition.header.budget !== undefined) parallelIneligibleReason = 'shared_budget';
|
|
103
|
+
else if (!parallelPromiseIntrinsicsIntact()) parallelIneligibleReason = 'runtime_intrinsics';
|
|
104
|
+
else {
|
|
100
105
|
// The parser is needed only for an authored run, never for an ordinary
|
|
101
106
|
// CLI check or watch reload. Recheck intrinsics after the async import;
|
|
102
107
|
// author code could have replaced them while it was loading.
|
|
103
|
-
const {
|
|
108
|
+
const { parallelLoadedBodyEligible, parallelPinnedSourceEligible } = await import('./authored-parallel-eligibility.js');
|
|
104
109
|
parallelEligible = parallelPromiseIntrinsicsIntact()
|
|
105
|
-
&&
|
|
110
|
+
&& (process.versions['bun'] === undefined
|
|
111
|
+
// Native imports strip by default, or transform with Node's opt-in
|
|
112
|
+
// flag. Require exact identity with one of those loader outputs.
|
|
113
|
+
? await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'strip')
|
|
114
|
+
|| await parallelLoadedBodyEligible(definition.body, source.toString('utf8'), pathToFileURL(loaded.sourcePath).href, 'transform')
|
|
115
|
+
: parallelPinnedSourceEligible(source.toString('utf8')));
|
|
116
|
+
if (!parallelEligible) parallelIneligibleReason = parallelPromiseIntrinsicsIntact()
|
|
117
|
+
? 'source_certificate' : 'runtime_intrinsics';
|
|
118
|
+
}
|
|
119
|
+
// Opt-in operator diagnostic only: changing root metadata would change its
|
|
120
|
+
// admission spec hash and could disrupt a 2.0.52 in-flight serial root.
|
|
121
|
+
if (parallelIneligibleReason !== undefined && process.env['FLOWS_PARALLEL_DEBUG'] === '1') {
|
|
122
|
+
process.stderr.write(`[flows] parallel f.run admission: serial (${parallelIneligibleReason})\n`);
|
|
106
123
|
}
|
|
107
124
|
const executionMode = parallelEligible ? 'parallel' : 'serial';
|
|
108
125
|
const metadata: AuthoredRootMetadata = Object.freeze({
|
|
109
|
-
kind: executionMode === 'parallel' ?
|
|
126
|
+
kind: executionMode === 'parallel' ? AUTHORED_PARALLEL_ROOT_KIND_V3 : ROOT_KIND,
|
|
110
127
|
flowName: definition.name,
|
|
111
128
|
flowPath: loaded.sourcePath,
|
|
112
129
|
sourceSha256: sha256(source),
|
|
@@ -648,8 +665,9 @@ function isRootMetadata(value: unknown): value is AuthoredRootMetadata {
|
|
|
648
665
|
const root = value as Partial<AuthoredRootMetadata>;
|
|
649
666
|
const surface = root.surface as Partial<SurfaceModuleAuthority> | undefined;
|
|
650
667
|
const sources = root.sources;
|
|
651
|
-
return (root.kind === ROOT_KIND || root.kind === AUTHORED_PARALLEL_ROOT_KIND
|
|
652
|
-
|
|
668
|
+
return (root.kind === ROOT_KIND || root.kind === AUTHORED_PARALLEL_ROOT_KIND
|
|
669
|
+
|| root.kind === AUTHORED_PARALLEL_ROOT_KIND_V3)
|
|
670
|
+
&& (root.kind === ROOT_KIND
|
|
653
671
|
|| (root.identityVersion === 2 && root.executionMode === 'parallel'))
|
|
654
672
|
&& typeof root.flowName === 'string'
|
|
655
673
|
&& typeof root.flowPath === 'string' && /^[a-f0-9]{64}$/.test(root.sourceSha256 ?? '')
|
package/src/authored-verdict.ts
CHANGED
|
@@ -21,8 +21,10 @@ import type { JournalEvent } from './journal-reader.js';
|
|
|
21
21
|
|
|
22
22
|
/** The `instruction` discriminator `authored-root.ts` writes into the root step. */
|
|
23
23
|
export const AUTHORED_ROOT_KIND = 'relayflows.authored-root.v1';
|
|
24
|
-
/**
|
|
24
|
+
/** Parallel roots admitted before 2.0.53; the 2.0.52 CLI recognizes these. */
|
|
25
25
|
export const AUTHORED_PARALLEL_ROOT_KIND = 'relayflows.authored-root.parallel.v2';
|
|
26
|
+
/** Newly admitted parallel roots must be refused by the 2.0.52 CLI on rollback. */
|
|
27
|
+
export const AUTHORED_PARALLEL_ROOT_KIND_V3 = 'relayflows.authored-root.parallel.v3';
|
|
26
28
|
|
|
27
29
|
export interface AuthoredVerdict {
|
|
28
30
|
readonly reason: LoweredCompletionReason;
|
|
@@ -79,7 +81,8 @@ export function declaresAuthoredRoot(spawned: JournalEvent): boolean {
|
|
|
79
81
|
if (typeof step['instruction'] !== 'string') return false;
|
|
80
82
|
try {
|
|
81
83
|
const kind = record(JSON.parse(step['instruction']))?.['kind'];
|
|
82
|
-
return kind === AUTHORED_ROOT_KIND || kind === AUTHORED_PARALLEL_ROOT_KIND
|
|
84
|
+
return kind === AUTHORED_ROOT_KIND || kind === AUTHORED_PARALLEL_ROOT_KIND
|
|
85
|
+
|| kind === AUTHORED_PARALLEL_ROOT_KIND_V3;
|
|
83
86
|
} catch {
|
|
84
87
|
return false;
|
|
85
88
|
}
|
package/src/cloud-signal.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { HOSTED_SIGNAL_READ_TOKEN_ENV, holdSignalWait, signalPath as sharedSignalPath,
|
|
3
|
+
type SignalErrorFactory } from '@relayflows/surface/signals';
|
|
4
|
+
import { CloudFlowError, cloudFetch, isCloudRecord, resolveCloudConnection,
|
|
5
|
+
type CloudConnectionOptions } from './cloud-http.js';
|
|
4
6
|
|
|
5
|
-
const SIGNAL_NAME = /^[A-Za-z0-9_-]{1,80}$/u;
|
|
6
7
|
const MAX_BYTES = 4 * 1024;
|
|
7
|
-
|
|
8
|
-
const
|
|
8
|
+
|
|
9
|
+
const cloudSignalError: SignalErrorFactory = (code, message, status) => new CloudFlowError(code, message, status);
|
|
9
10
|
|
|
10
11
|
function signalPath(runId: string, name: string): string {
|
|
11
|
-
|
|
12
|
-
if (!SIGNAL_NAME.test(name)) {
|
|
13
|
-
throw new CloudFlowError('invalid_input', 'Signal name must contain only letters, digits, underscores, or hyphens (1–80 characters).');
|
|
14
|
-
}
|
|
15
|
-
return `/api/v1/workflows/runs/${runId}/signals/${name}`;
|
|
12
|
+
return sharedSignalPath(runId, name, cloudSignalError);
|
|
16
13
|
}
|
|
17
14
|
|
|
18
15
|
/** Store a run-scoped completion. Retrying the same JSON payload is safe. */
|
|
@@ -38,73 +35,30 @@ export async function postCloudSignal(
|
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
/**
|
|
41
|
-
* Wait on Cloud's held GET.
|
|
42
|
-
*
|
|
38
|
+
* Wait on Cloud's held GET. The wire behaviour (validation, held GET, fast-204
|
|
39
|
+
* backoff, error classification) is `@relayflows/surface/signals`' single
|
|
40
|
+
* implementation, so the SDK and hosted flows cannot drift. The SDK keeps its
|
|
41
|
+
* own credential resolution and throws `CloudFlowError`.
|
|
43
42
|
*/
|
|
44
43
|
export async function waitForCloudSignal(
|
|
45
44
|
runId: string, name: string, options: CloudConnectionOptions = {},
|
|
46
45
|
): Promise<unknown> {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const hostedToken = process.env[HOSTED_SIGNAL_READ_TOKEN_ENV];
|
|
63
|
-
if (!(error instanceof CloudFlowError) || error.code !== 'configuration'
|
|
64
|
-
|| error.reason !== 'auth_missing' || options.token !== undefined
|
|
65
|
-
|| process.env['FLOWS_CLOUD_TOKEN'] !== undefined || hostedToken === undefined) throw error;
|
|
66
|
-
connection = await resolveCloudConnection({ ...options, token: hostedToken });
|
|
67
|
-
}
|
|
68
|
-
const { baseUrl, token } = connection;
|
|
69
|
-
const deadline = AbortSignal.timeout(requestTimeout);
|
|
70
|
-
const requestStartedAt = performance.now();
|
|
71
|
-
let response: Response;
|
|
72
|
-
try {
|
|
73
|
-
response = await fetch(`${baseUrl}${path}?waitMs=${HELD_GET_MS}`, {
|
|
74
|
-
method: 'GET',
|
|
75
|
-
headers: { authorization: `Bearer ${token}` },
|
|
76
|
-
signal: options.signal ? AbortSignal.any([options.signal, deadline]) : deadline,
|
|
77
|
-
redirect: 'error',
|
|
78
|
-
});
|
|
79
|
-
} catch (error) {
|
|
80
|
-
options.signal?.throwIfAborted();
|
|
81
|
-
throw transportError(deadline.aborted ? deadline.reason : error);
|
|
82
|
-
}
|
|
83
|
-
if (response.status === 204) {
|
|
84
|
-
if (performance.now() - requestStartedAt < 1_000) {
|
|
85
|
-
try {
|
|
86
|
-
await delay(fast204BackoffMs, undefined, { signal: options.signal });
|
|
87
|
-
} catch (error) {
|
|
88
|
-
options.signal?.throwIfAborted();
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
fast204BackoffMs = Math.min(fast204BackoffMs * 2, 5_000);
|
|
92
|
-
} else {
|
|
93
|
-
fast204BackoffMs = 250;
|
|
46
|
+
return holdSignalWait(runId, name, { signal: options.signal, requestTimeoutMs: options.requestTimeoutMs }, {
|
|
47
|
+
error: cloudSignalError,
|
|
48
|
+
sleep: (ms, signal) => delay(ms, undefined, { signal }),
|
|
49
|
+
connection: async () => {
|
|
50
|
+
try {
|
|
51
|
+
return await resolveCloudConnection(options);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// The hosted run's read-only credential is only a fallback for waits.
|
|
54
|
+
// Explicit Cloud credentials and stored logins retain precedence, and
|
|
55
|
+
// postCloudSignal never sees this token.
|
|
56
|
+
const hostedToken = process.env[HOSTED_SIGNAL_READ_TOKEN_ENV];
|
|
57
|
+
if (!(error instanceof CloudFlowError) || error.code !== 'configuration'
|
|
58
|
+
|| error.reason !== 'auth_missing' || options.token !== undefined
|
|
59
|
+
|| process.env['FLOWS_CLOUD_TOKEN'] !== undefined || hostedToken === undefined) throw error;
|
|
60
|
+
return resolveCloudConnection({ ...options, token: hostedToken });
|
|
94
61
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!response.ok) throw new CloudFlowError('http_error', `Cloud signal wait failed with HTTP ${response.status}.`, response.status);
|
|
98
|
-
let body: unknown;
|
|
99
|
-
try { body = await response.json(); }
|
|
100
|
-
catch (error) {
|
|
101
|
-
options.signal?.throwIfAborted();
|
|
102
|
-
if (!(error instanceof SyntaxError)) throw transportError(deadline.aborted ? deadline.reason : error);
|
|
103
|
-
throw new CloudFlowError('invalid_response', 'Cloud returned an invalid signal.');
|
|
104
|
-
}
|
|
105
|
-
if (!isCloudRecord(body) || !Object.hasOwn(body, 'payload') || typeof body.acceptedAt !== 'number') {
|
|
106
|
-
throw new CloudFlowError('invalid_response', 'Cloud returned an invalid signal.');
|
|
107
|
-
}
|
|
108
|
-
return body.payload;
|
|
109
|
-
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
110
64
|
}
|