@relayflows/sdk 2.0.51 → 2.0.53
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-budget.d.ts +1 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +3 -3
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +3 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +2 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +93 -12
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-lifecycle.d.ts +28 -3
- package/dist/authored-flow-lifecycle.d.ts.map +1 -1
- package/dist/authored-flow-lifecycle.js +206 -10
- package/dist/authored-flow-lifecycle.js.map +1 -1
- package/dist/authored-node-entry.js +16 -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-body.d.ts +10 -0
- package/dist/authored-parallel-body.d.ts.map +1 -0
- package/dist/authored-parallel-body.js +68 -0
- package/dist/authored-parallel-body.js.map +1 -0
- package/dist/authored-parallel-eligibility.d.ts +22 -0
- package/dist/authored-parallel-eligibility.d.ts.map +1 -0
- package/dist/authored-parallel-eligibility.js +335 -0
- package/dist/authored-parallel-eligibility.js.map +1 -0
- package/dist/authored-promise-graph.d.ts +16 -0
- package/dist/authored-promise-graph.d.ts.map +1 -1
- package/dist/authored-promise-graph.js +36 -0
- package/dist/authored-promise-graph.js.map +1 -1
- package/dist/authored-root-readback.d.ts +1 -2
- package/dist/authored-root-readback.d.ts.map +1 -1
- package/dist/authored-root-readback.js +1 -1
- package/dist/authored-root-readback.js.map +1 -1
- package/dist/authored-root.d.ts +6 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +78 -8
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-graph.d.ts +19 -4
- package/dist/authored-step-graph.d.ts.map +1 -1
- package/dist/authored-step-graph.js +49 -6
- package/dist/authored-step-graph.js.map +1 -1
- package/dist/authored-step-index.d.ts +2 -1
- package/dist/authored-step-index.d.ts.map +1 -1
- package/dist/authored-step-index.js +2 -1
- package/dist/authored-step-index.js.map +1 -1
- package/dist/authored-verdict.d.ts +4 -0
- package/dist/authored-verdict.d.ts.map +1 -1
- package/dist/authored-verdict.js +7 -1
- package/dist/authored-verdict.js.map +1 -1
- package/dist/authored-worker-step.d.ts +2 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +12 -5
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +4 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/failure-kinds.d.ts +1 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +2 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/journal-client.d.ts +3 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +18 -1
- package/dist/journal-client.js.map +1 -1
- package/package.json +3 -2
- package/src/authored-budget.ts +4 -3
- package/src/authored-flow-error.ts +2 -0
- package/src/authored-flow-executor.ts +85 -11
- package/src/authored-flow-lifecycle.ts +215 -9
- package/src/authored-node-entry.ts +17 -0
- package/src/authored-node-runner.ts +24 -3
- package/src/authored-parallel-body.ts +67 -0
- package/src/authored-parallel-eligibility.ts +296 -0
- package/src/authored-promise-graph.ts +35 -0
- package/src/authored-root-readback.ts +1 -2
- package/src/authored-root.ts +80 -9
- package/src/authored-step-graph.ts +50 -5
- package/src/authored-step-index.ts +2 -1
- package/src/authored-verdict.ts +7 -1
- package/src/authored-worker-step.ts +12 -3
- package/src/cli/run.ts +4 -2
- package/src/failure-kinds.ts +2 -0
- package/src/journal-client.ts +19 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage, executionAsyncId } from 'node:async_hooks';
|
|
2
2
|
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
3
3
|
import { AuthoredPromiseGraph } from './authored-promise-graph.js';
|
|
4
|
-
import { AuthoredStepGraph } from './authored-step-graph.js';
|
|
4
|
+
import { AuthoredStepGraph, promiseOutcome } from './authored-step-graph.js';
|
|
5
5
|
import type { AuthoredStepEdges } from './authored-step-index.js';
|
|
6
6
|
|
|
7
7
|
type OperationToken = object;
|
|
@@ -10,12 +10,38 @@ interface PromiseAllGroup {
|
|
|
10
10
|
aggregate: number;
|
|
11
11
|
members: ReadonlySet<number>;
|
|
12
12
|
}
|
|
13
|
+
interface AllSettlementGuard {
|
|
14
|
+
readonly aggregate: Promise<unknown>;
|
|
15
|
+
readonly aggregateId: number;
|
|
16
|
+
readonly combinator: 'all' | 'allSettled';
|
|
17
|
+
readonly members: readonly number[];
|
|
18
|
+
}
|
|
13
19
|
export interface AuthoredOperationInvocation {
|
|
14
20
|
readonly asyncId: number;
|
|
15
21
|
bound: boolean;
|
|
16
22
|
}
|
|
17
23
|
|
|
24
|
+
const nativePromiseConstructor = Promise;
|
|
25
|
+
const nativePromisePrototype = Promise.prototype;
|
|
18
26
|
const nativePromiseThen = Promise.prototype.then;
|
|
27
|
+
const nativePromiseCatch = Promise.prototype.catch;
|
|
28
|
+
const nativePromiseFinally = Promise.prototype.finally;
|
|
29
|
+
const nativePromiseResolve = Promise.resolve;
|
|
30
|
+
const nativePromiseReject = Promise.reject;
|
|
31
|
+
const nativePromisePrototypeConstructorDescriptor = Object.getOwnPropertyDescriptor(Promise.prototype, 'constructor');
|
|
32
|
+
const nativePromisePrototypeConstructor = Promise.prototype.constructor;
|
|
33
|
+
const nativePromiseSpeciesDescriptor = Object.getOwnPropertyDescriptor(Promise, Symbol.species);
|
|
34
|
+
const nativePromiseSpeciesGetter = nativePromiseSpeciesDescriptor?.get;
|
|
35
|
+
const nativeFunctionToString = Function.prototype.toString;
|
|
36
|
+
/** Use the startup snapshot even when a source parser is loaded on demand. */
|
|
37
|
+
export function authoredFunctionSource(value: Function): string {
|
|
38
|
+
return nativeFunctionToString.call(value);
|
|
39
|
+
}
|
|
40
|
+
const nativeArrayIteratorDescriptor = Object.getOwnPropertyDescriptor(Array.prototype, Symbol.iterator);
|
|
41
|
+
const nativeArrayIterator = Array.prototype[Symbol.iterator];
|
|
42
|
+
const nativeArrayIteratorPrototype = Object.getPrototypeOf(nativeArrayIterator.call([])) as Iterator<unknown>;
|
|
43
|
+
const nativeArrayIteratorNextDescriptor = Object.getOwnPropertyDescriptor(nativeArrayIteratorPrototype, 'next');
|
|
44
|
+
const nativeArrayIteratorNext = nativeArrayIteratorPrototype.next;
|
|
19
45
|
const activeLifecycle = new AsyncLocalStorage<AuthoredFlowLifecycle>();
|
|
20
46
|
const stepOwners = new WeakMap<object, {
|
|
21
47
|
lifecycle: AuthoredFlowLifecycle;
|
|
@@ -42,7 +68,8 @@ const stepOwners = new WeakMap<object, {
|
|
|
42
68
|
* claim was wrong: it covered whichever member happened to resolve the
|
|
43
69
|
* aggregate. What is true is narrower and is what the code now implements —
|
|
44
70
|
* these four are exact, and `adoptFromResolvingContext` in the promise graph is
|
|
45
|
-
* a
|
|
71
|
+
* a fallback for aggregates built by hand. Parallel f.run admission additionally
|
|
72
|
+
* rejects unregistered join shapes on its causal path and checks observer identity.
|
|
46
73
|
*
|
|
47
74
|
* The interception is disclosed to authors in `docs/SURFACE.md`. It is spec
|
|
48
75
|
* transparent: `Symbol.iterator` is read exactly once (as the intrinsic does),
|
|
@@ -58,6 +85,51 @@ const nativeCombinators = Object.freeze(
|
|
|
58
85
|
Object.fromEntries(COMBINATORS.map((name) => [name, Promise[name] as unknown as Combinator])),
|
|
59
86
|
) as Readonly<Record<CombinatorName, Combinator>>;
|
|
60
87
|
|
|
88
|
+
const originallyNative = (value: unknown): boolean => typeof value === 'function'
|
|
89
|
+
&& /\{\s*\[native code\]\s*\}/u.test(nativeFunctionToString.call(value));
|
|
90
|
+
const nativeAtModuleLoad = [
|
|
91
|
+
nativePromiseConstructor, nativePromiseThen, nativePromiseCatch, nativePromiseFinally,
|
|
92
|
+
nativePromiseResolve, nativePromiseReject, nativePromisePrototypeConstructor,
|
|
93
|
+
nativePromiseSpeciesGetter, nativeFunctionToString, nativeArrayIterator,
|
|
94
|
+
nativeArrayIteratorNext, ...Object.values(nativeCombinators),
|
|
95
|
+
].every(originallyNative);
|
|
96
|
+
|
|
97
|
+
function sameDescriptor(actual: PropertyDescriptor | undefined, original: PropertyDescriptor | undefined): boolean {
|
|
98
|
+
return actual !== undefined && original !== undefined
|
|
99
|
+
&& actual.configurable === original.configurable && actual.enumerable === original.enumerable
|
|
100
|
+
&& actual.writable === original.writable && actual.value === original.value
|
|
101
|
+
&& actual.get === original.get && actual.set === original.set;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Preloaded code can replace an intrinsic before this module takes its snapshot. */
|
|
105
|
+
function authoredProcessHasPreload(): boolean {
|
|
106
|
+
const preload = (arg: string): boolean => arg === '-r' || arg.startsWith('-r')
|
|
107
|
+
|| ['--import', '--require', '--loader', '--experimental-loader']
|
|
108
|
+
.some(flag => arg === flag || arg.startsWith(`${flag}=`));
|
|
109
|
+
return process.execArgv.some(preload)
|
|
110
|
+
|| nodeOptionTokens(process.env['NODE_OPTIONS'] ?? '').some(preload);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** NODE_OPTIONS is a command-line string, while execArgv is already tokenized. */
|
|
114
|
+
function nodeOptionTokens(options: string): string[] {
|
|
115
|
+
const tokens: string[] = [];
|
|
116
|
+
let token = '';
|
|
117
|
+
let quote: '"' | "'" | undefined;
|
|
118
|
+
let escaped = false;
|
|
119
|
+
for (const char of options) {
|
|
120
|
+
if (escaped) { token += char; escaped = false; continue; }
|
|
121
|
+
if (char === '\\' && quote !== "'") { escaped = true; continue; }
|
|
122
|
+
if (char === quote) { quote = undefined; continue; }
|
|
123
|
+
if ((char === '"' || char === "'") && quote === undefined) { quote = char; continue; }
|
|
124
|
+
if (/\s/u.test(char) && quote === undefined) {
|
|
125
|
+
if (token.length > 0) tokens.push(token);
|
|
126
|
+
token = '';
|
|
127
|
+
} else token += char;
|
|
128
|
+
}
|
|
129
|
+
if (token.length > 0) tokens.push(token);
|
|
130
|
+
return tokens;
|
|
131
|
+
}
|
|
132
|
+
|
|
61
133
|
const observedCombinators: Record<CombinatorName, Combinator> = Object.fromEntries(
|
|
62
134
|
COMBINATORS.map((name) => {
|
|
63
135
|
const native = nativeCombinators[name];
|
|
@@ -65,11 +137,16 @@ const observedCombinators: Record<CombinatorName, Combinator> = Object.fromEntri
|
|
|
65
137
|
this: PromiseConstructor,
|
|
66
138
|
values: Iterable<unknown>,
|
|
67
139
|
): Promise<unknown> {
|
|
140
|
+
const lifecycle = activeLifecycle.getStore();
|
|
141
|
+
const declared = lifecycle?.captureParallelArrayMembers(values);
|
|
68
142
|
const members = collectMembers(values);
|
|
69
|
-
if (members === undefined)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
143
|
+
if (members === undefined) {
|
|
144
|
+
if (declared !== undefined) lifecycle?.refuseUnsafeCombinator(
|
|
145
|
+
`parallel Promise.${name} could not inspect its array members`);
|
|
146
|
+
return native.call(this, values);
|
|
147
|
+
}
|
|
148
|
+
if (lifecycle === undefined) return native.call(this, members);
|
|
149
|
+
return lifecycle.observeCombinator(native, this, members, name, declared);
|
|
73
150
|
};
|
|
74
151
|
Object.defineProperty(observed, 'name', { value: name, configurable: true });
|
|
75
152
|
Object.defineProperty(observed, 'length', { value: 1, configurable: true });
|
|
@@ -79,6 +156,30 @@ const observedCombinators: Record<CombinatorName, Combinator> = Object.fromEntri
|
|
|
79
156
|
|
|
80
157
|
let combinatorObservers = 0;
|
|
81
158
|
|
|
159
|
+
/** An imported module must not substitute any primitive used to construct joins. */
|
|
160
|
+
export function parallelPromiseIntrinsicsIntact(): boolean {
|
|
161
|
+
if (!nativeAtModuleLoad || authoredProcessHasPreload()
|
|
162
|
+
|| Function.prototype.toString !== nativeFunctionToString
|
|
163
|
+
|| !sameDescriptor(Object.getOwnPropertyDescriptor(Array.prototype, Symbol.iterator),
|
|
164
|
+
nativeArrayIteratorDescriptor)
|
|
165
|
+
|| Array.prototype[Symbol.iterator] !== nativeArrayIterator
|
|
166
|
+
|| !sameDescriptor(Object.getOwnPropertyDescriptor(nativeArrayIteratorPrototype, 'next'),
|
|
167
|
+
nativeArrayIteratorNextDescriptor)
|
|
168
|
+
|| nativeArrayIteratorPrototype.next !== nativeArrayIteratorNext
|
|
169
|
+
|| Promise !== nativePromiseConstructor || Promise.prototype !== nativePromisePrototype
|
|
170
|
+
|| !sameDescriptor(Object.getOwnPropertyDescriptor(Promise.prototype, 'constructor'),
|
|
171
|
+
nativePromisePrototypeConstructorDescriptor)
|
|
172
|
+
|| Promise.prototype.constructor !== nativePromisePrototypeConstructor
|
|
173
|
+
|| !sameDescriptor(Object.getOwnPropertyDescriptor(Promise, Symbol.species), nativePromiseSpeciesDescriptor)
|
|
174
|
+
|| Object.getOwnPropertyDescriptor(Promise, Symbol.species)?.get !== nativePromiseSpeciesGetter
|
|
175
|
+
|| Promise.prototype.then !== nativePromiseThen
|
|
176
|
+
|| Promise.prototype.catch !== nativePromiseCatch
|
|
177
|
+
|| Promise.prototype.finally !== nativePromiseFinally
|
|
178
|
+
|| Promise.resolve !== nativePromiseResolve || Promise.reject !== nativePromiseReject) return false;
|
|
179
|
+
return COMBINATORS.every(name => Promise[name] === (combinatorObservers > 0
|
|
180
|
+
? observedCombinators[name] : nativeCombinators[name]) as unknown);
|
|
181
|
+
}
|
|
182
|
+
|
|
82
183
|
/**
|
|
83
184
|
* Drain an iterable into an array, reading `Symbol.iterator` exactly once.
|
|
84
185
|
*
|
|
@@ -125,6 +226,8 @@ export class AuthoredFlowLifecycle {
|
|
|
125
226
|
private readonly invocations = new Map<OperationToken, AuthoredOperationInvocation[]>();
|
|
126
227
|
private readonly promiseAllAggregates = new Map<OperationToken, Set<number>>();
|
|
127
228
|
private readonly promiseAllGroups: PromiseAllGroup[] = [];
|
|
229
|
+
private readonly allSettlementGuards: AllSettlementGuard[] = [];
|
|
230
|
+
private invalidAllSettlement = false;
|
|
128
231
|
private readonly callbackFailures = new Map<OperationToken, unknown>();
|
|
129
232
|
private completionAsyncId: number | undefined;
|
|
130
233
|
private closed = false;
|
|
@@ -132,14 +235,19 @@ export class AuthoredFlowLifecycle {
|
|
|
132
235
|
private registrations = 0;
|
|
133
236
|
private groupsByInvocation: { readonly key: string; readonly groups: Map<number, ReadonlySet<number>> } | undefined;
|
|
134
237
|
|
|
135
|
-
constructor() {
|
|
238
|
+
constructor(private readonly refuseUnknownThenable?: (message: string) => void) {
|
|
136
239
|
this.graph = new AuthoredPromiseGraph(
|
|
137
240
|
() => activeLifecycle.getStore() === this,
|
|
138
241
|
(asyncId) => {
|
|
139
242
|
for (const probe of this.activeResolverProbes) probe.add(asyncId);
|
|
140
243
|
},
|
|
141
244
|
);
|
|
142
|
-
this.stepGraph = new AuthoredStepGraph(
|
|
245
|
+
this.stepGraph = new AuthoredStepGraph(
|
|
246
|
+
(asyncId) => this.graph.causesOf(asyncId),
|
|
247
|
+
undefined,
|
|
248
|
+
(asyncId) => this.graph.wasAdoptedFromResolvingContext(asyncId)
|
|
249
|
+
|| this.graph.hasUnobservedJoinShape(asyncId),
|
|
250
|
+
);
|
|
143
251
|
installPromiseAllObserver();
|
|
144
252
|
try {
|
|
145
253
|
this.graph.enable();
|
|
@@ -153,6 +261,19 @@ export class AuthoredFlowLifecycle {
|
|
|
153
261
|
return activeLifecycle.run(this, body);
|
|
154
262
|
}
|
|
155
263
|
|
|
264
|
+
/** Compare the literal array's indexed members with its iterable view. */
|
|
265
|
+
captureParallelArrayMembers(values: Iterable<unknown>): readonly unknown[] | undefined {
|
|
266
|
+
if (this.refuseUnknownThenable === undefined || !Array.isArray(values)) return undefined;
|
|
267
|
+
const members = new Array<unknown>(values.length);
|
|
268
|
+
for (let i = 0; i < values.length; i++) members[i] = values[i];
|
|
269
|
+
return members;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
refuseUnsafeCombinator(message: string): never {
|
|
273
|
+
this.refuseUnknownThenable?.(message);
|
|
274
|
+
throw new AuthoredFlowExecutionError('step_failed', message);
|
|
275
|
+
}
|
|
276
|
+
|
|
156
277
|
registerStep(step: object, operation: OperationToken): void {
|
|
157
278
|
stepOwners.set(step, { lifecycle: this, operation });
|
|
158
279
|
}
|
|
@@ -161,6 +282,7 @@ export class AuthoredFlowLifecycle {
|
|
|
161
282
|
values: readonly unknown[],
|
|
162
283
|
aggregate: Promise<unknown>,
|
|
163
284
|
combinator: CombinatorName,
|
|
285
|
+
created: ReadonlySet<number>,
|
|
164
286
|
): void {
|
|
165
287
|
const aggregateId = this.graph.idOf(aggregate);
|
|
166
288
|
if (aggregateId === undefined) return;
|
|
@@ -177,6 +299,7 @@ export class AuthoredFlowLifecycle {
|
|
|
177
299
|
const awaited = owner?.lifecycle === this ? this.operationPromises.get(owner.operation) : memberId;
|
|
178
300
|
if (awaited !== undefined) awaitedMembers.push(awaited);
|
|
179
301
|
if (owner?.lifecycle !== this) continue;
|
|
302
|
+
if (awaited !== undefined) this.stepGraph.certifyMember(awaited);
|
|
180
303
|
let operationAggregates = this.promiseAllAggregates.get(owner.operation);
|
|
181
304
|
if (operationAggregates === undefined) {
|
|
182
305
|
operationAggregates = new Set();
|
|
@@ -185,8 +308,53 @@ export class AuthoredFlowLifecycle {
|
|
|
185
308
|
operationAggregates.add(aggregateId);
|
|
186
309
|
}
|
|
187
310
|
this.promiseAllGroups.push({ aggregate: aggregateId, members: memberIds });
|
|
311
|
+
if (this.refuseUnknownThenable !== undefined
|
|
312
|
+
&& (combinator === 'all' || combinator === 'allSettled')) {
|
|
313
|
+
this.allSettlementGuards.push({ aggregate, aggregateId, combinator, members: awaitedMembers });
|
|
314
|
+
}
|
|
188
315
|
this.registrations++;
|
|
189
|
-
this.stepGraph.registerAggregate(aggregateId, aggregate, combinator, awaitedMembers);
|
|
316
|
+
this.stepGraph.registerAggregate(aggregateId, aggregate, combinator, awaitedMembers, created);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
observeCombinator(
|
|
320
|
+
native: Combinator,
|
|
321
|
+
constructor: PromiseConstructor,
|
|
322
|
+
values: readonly unknown[],
|
|
323
|
+
name: CombinatorName,
|
|
324
|
+
declared?: readonly unknown[],
|
|
325
|
+
): Promise<unknown> {
|
|
326
|
+
if (declared !== undefined) {
|
|
327
|
+
if (declared.length !== values.length) this.refuseUnsafeCombinator(
|
|
328
|
+
`parallel Promise.${name} iterated a different set of array members`);
|
|
329
|
+
for (let i = 0; i < declared.length; i++) {
|
|
330
|
+
if (declared[i] !== values[i]) this.refuseUnsafeCombinator(
|
|
331
|
+
`parallel Promise.${name} iterated a different set of array members`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Check before the native combinator reads any unknown `then` getter. A
|
|
335
|
+
// member with no tracked promise id or owned step has no causal identity
|
|
336
|
+
// to expand on a later follow-up. This also covers proxy thenables whose
|
|
337
|
+
// `then` is synthesized by a get trap and has no property descriptor.
|
|
338
|
+
if (this.refuseUnknownThenable !== undefined) {
|
|
339
|
+
for (const value of values) {
|
|
340
|
+
if ((typeof value !== 'object' && typeof value !== 'function') || value === null) continue;
|
|
341
|
+
if (stepOwners.get(value)?.lifecycle === this || this.graph.idOf(value) !== undefined) continue;
|
|
342
|
+
const message = `parallel Promise.${name} contains a member with no tracked provenance`;
|
|
343
|
+
this.refuseUnsafeCombinator(message);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const { value, created } = this.graph.captureCombinator(() => native.call(constructor, values));
|
|
347
|
+
this.registerCombinator(values, value, name, created);
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Mark the exact promise returned by a grammar-certified async branch call. */
|
|
352
|
+
certifyBranchPromise(promise: Promise<unknown>): void {
|
|
353
|
+
const id = this.graph.idOf(promise);
|
|
354
|
+
if (id === undefined) {
|
|
355
|
+
throw new AuthoredFlowExecutionError('step_failed', 'certified branch promise was not tracked');
|
|
356
|
+
}
|
|
357
|
+
this.stepGraph.certifyMember(id);
|
|
190
358
|
}
|
|
191
359
|
|
|
192
360
|
/**
|
|
@@ -215,6 +383,43 @@ export class AuthoredFlowLifecycle {
|
|
|
215
383
|
return this.stepGraph.edgesOf(step);
|
|
216
384
|
}
|
|
217
385
|
|
|
386
|
+
/** Predecessors for a new invocation, before it has an operation promise. */
|
|
387
|
+
invocationPredecessors(): {
|
|
388
|
+
readonly steps: readonly string[];
|
|
389
|
+
readonly truncated: boolean;
|
|
390
|
+
readonly unregisteredAggregate: boolean;
|
|
391
|
+
} {
|
|
392
|
+
const frontier = this.stepGraph.predecessorsAt(executionAsyncId());
|
|
393
|
+
return this.hasInvalidAllSettlement()
|
|
394
|
+
? { ...frontier, unregisteredAggregate: true }
|
|
395
|
+
: frontier;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** A fulfilled all (or settled allSettled) must have waited for every member. */
|
|
399
|
+
private hasInvalidAllSettlement(): boolean {
|
|
400
|
+
if (this.invalidAllSettlement) return true;
|
|
401
|
+
let retained = 0;
|
|
402
|
+
for (const guard of this.allSettlementGuards) {
|
|
403
|
+
if (this.graph.isPending(guard.aggregateId)) {
|
|
404
|
+
this.allSettlementGuards[retained++] = guard;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if ((guard.combinator === 'allSettled' || promiseOutcome(guard.aggregate) === 'fulfilled')
|
|
408
|
+
&& guard.members.some(id => this.graph.isPending(id))) {
|
|
409
|
+
this.invalidAllSettlement = true;
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
// Once an honest aggregate has settled, its invariant cannot change.
|
|
413
|
+
}
|
|
414
|
+
this.allSettlementGuards.length = retained;
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** The observation contract must still be installed when a child is admitted. */
|
|
419
|
+
combinatorsObserved(): boolean {
|
|
420
|
+
return combinatorObservers > 0 && parallelPromiseIntrinsicsIntact();
|
|
421
|
+
}
|
|
422
|
+
|
|
218
423
|
registerInvocation(
|
|
219
424
|
operation: OperationToken,
|
|
220
425
|
asyncId: number,
|
|
@@ -350,6 +555,7 @@ export class AuthoredFlowLifecycle {
|
|
|
350
555
|
this.invocations.clear();
|
|
351
556
|
this.promiseAllAggregates.clear();
|
|
352
557
|
this.promiseAllGroups.length = 0;
|
|
558
|
+
this.allSettlementGuards.length = 0;
|
|
353
559
|
this.groupsByInvocation = undefined;
|
|
354
560
|
this.callbackFailures.clear();
|
|
355
561
|
this.activeResolverProbes.length = 0;
|
|
@@ -2,6 +2,7 @@ import { FLOW_READ_BUDGET_MS, isReadInterruptionError } from './journal-read-pol
|
|
|
2
2
|
import { Worker } from 'node:worker_threads';
|
|
3
3
|
import { createHash, createHmac } from 'node:crypto';
|
|
4
4
|
import { readFileSync, writeSync } from 'node:fs';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
5
6
|
import { JournalClient } from './journal-client.js';
|
|
6
7
|
import { executeAuthoredFlow } from './authored-flow-executor.js';
|
|
7
8
|
import { loadPinnedAuthoredSource } from './authored-source-authority.js';
|
|
@@ -73,6 +74,20 @@ try {
|
|
|
73
74
|
// NODE_OPTIONS remain the child's inherited values.
|
|
74
75
|
const agentEnvironment = validatedLocalAgentEnvironment(request.agentEnvironment);
|
|
75
76
|
const loaded = await loadPinnedAuthoredSource(request.metadata, true);
|
|
77
|
+
if (request.metadata.identityVersion === 2 && request.metadata.executionMode === 'parallel') {
|
|
78
|
+
// Bun admitted the root from the source AST because its Function.toString
|
|
79
|
+
// is reformatted. Reconfirm that exact source and the Node-loaded body
|
|
80
|
+
// before executing any child. A changed file or a changed body fails closed.
|
|
81
|
+
const bytes = readFileSync(request.metadata.flowPath);
|
|
82
|
+
if (createHash('sha256').update(bytes).digest('hex') !== request.metadata.sourceSha256) {
|
|
83
|
+
throw new Error('authored root source authority mismatch');
|
|
84
|
+
}
|
|
85
|
+
const { parallelLoadedBodyEligible } = await import('./authored-parallel-eligibility.js');
|
|
86
|
+
if (!await parallelLoadedBodyEligible(loaded.getDefinition(loaded.handle).body,
|
|
87
|
+
bytes.toString('utf8'), pathToFileURL(request.metadata.flowPath).href, 'transform')) {
|
|
88
|
+
throw new AuthoredFlowExecutionError('step_failed', 'parallel f.run body lost its source certificate');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
76
91
|
if (request.localAgentStream !== request.metadata.localAgentStream) throw new Error('authored root local agent surface mismatch');
|
|
77
92
|
if (request.workerCapacity !== undefined && !isAgentCapacity(request.workerCapacity)) throw new Error('invalid authored worker capacity');
|
|
78
93
|
client = new JournalClient(request.socketPath, { readBudgetMs: FLOW_READ_BUDGET_MS, budgetHandshake: true });
|
|
@@ -81,6 +96,8 @@ try {
|
|
|
81
96
|
request.metadata.inputPresent ? request.metadata.input : undefined, {
|
|
82
97
|
getDefinition: loaded.getDefinition, dataDir: request.dataDir,
|
|
83
98
|
flowPath: request.metadata.flowPath, rootRunId: request.rootRunId,
|
|
99
|
+
executionMode: request.metadata.identityVersion === 2 && request.metadata.executionMode === 'parallel'
|
|
100
|
+
? 'parallel' : 'serial',
|
|
84
101
|
extensions: loaded.extensions,
|
|
85
102
|
flowGraph: loaded.graph,
|
|
86
103
|
localAgentStream: request.localAgentStream, signal: controller.signal,
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
import { HUMAN_WAIT_ID } from './authored-human.js';
|
|
21
21
|
import { assertAuthoredPromiseHooks } from './authored-runtime-capability.js';
|
|
22
22
|
import { settlingCompletion } from './step-settling.js';
|
|
23
|
+
import { readAuthoredStepIndex } from './authored-step-index.js';
|
|
23
24
|
import { isTransportRetries } from './validate.js';
|
|
24
25
|
|
|
25
26
|
let embeddedSource: string | undefined;
|
|
@@ -347,10 +348,16 @@ export async function verifyAuthoredNodeResult(
|
|
|
347
348
|
const authored = result.journalSteps.filter(step => typeof step?.id === 'string' && !isGate(step.id));
|
|
348
349
|
const gates = result.journalSteps.filter(step => typeof step?.id === 'string' && isGate(step.id));
|
|
349
350
|
if (!Number.isSafeInteger(count) || authored.length !== count || isGate(terminal.id)) invalid(`count ${authored.length}!=${count}`);
|
|
351
|
+
const parallel = metadata.identityVersion === 2 && metadata.executionMode === 'parallel';
|
|
350
352
|
const ordinal = (id: string): number => Number(/-([1-9][0-9]*)$/.exec(id)?.[1]);
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
+
// Serial ids have one global ordinal. Parallel ids have one ordinal per
|
|
354
|
+
// causal predecessor, so several valid siblings end in `-1`. Validate the
|
|
355
|
+
// complete parallel claim against the root's durable child index instead.
|
|
356
|
+
const ordered = parallel ? [...authored] : [...authored].sort((a,b)=>ordinal(a.id)-ordinal(b.id));
|
|
353
357
|
const authoredIds = new Set(ordered.map(step => step.id));
|
|
358
|
+
if (parallel && (gates.length !== 0 || authoredIds.size !== count
|
|
359
|
+
|| ordered.some(step => step === terminal ? !/^complete-[1-9][0-9]*$/u.test(step.id)
|
|
360
|
+
: !/^run-(?:[1-9][0-9]*|after-[a-f0-9]{64}-[1-9][0-9]*)$/u.test(step.id)))) invalid('parallel ids');
|
|
354
361
|
for (const gate of gates) {
|
|
355
362
|
if (!authoredIds.has(gate.id.slice(0, -'.gate'.length))) invalid(`orphan ${gate.id}`);
|
|
356
363
|
}
|
|
@@ -361,9 +368,23 @@ export async function verifyAuthoredNodeResult(
|
|
|
361
368
|
await journal.connect();
|
|
362
369
|
try {
|
|
363
370
|
await journal.hello('flows-authored-result-verifier');
|
|
371
|
+
if (parallel) {
|
|
372
|
+
const indexed = await readAuthoredStepIndex(journal, rootRunId);
|
|
373
|
+
const claimed = new Map(ordered.map(step => [step.id, step]));
|
|
374
|
+
if (indexed.length !== count || indexed.some(step => {
|
|
375
|
+
const matching = claimed.get(step.step);
|
|
376
|
+
return matching === undefined || step.state !== 'completed'
|
|
377
|
+
|| step.runId !== matching.runId || step.completionReason !== matching.completionReason
|
|
378
|
+
|| step.label !== matching.label || step.afterTruncated !== matching.afterTruncated
|
|
379
|
+
|| (step.after === undefined ? matching.after !== undefined
|
|
380
|
+
: !Array.isArray(step.after) || !Array.isArray(matching.after) || step.after.length !== matching.after.length
|
|
381
|
+
|| step.after.some((id, index) => id !== matching.after?.[index]));
|
|
382
|
+
})) invalid('parallel index');
|
|
383
|
+
}
|
|
364
384
|
for (const [index, claimed] of [...ordered, ...gates].entries()) {
|
|
365
385
|
if (!claimed || typeof claimed.id !== 'string' || typeof claimed.runId !== 'string'
|
|
366
|
-
|| (index < ordered.length && ordinal(claimed.id) !== index+1)
|
|
386
|
+
|| (!parallel && index < ordered.length && ordinal(claimed.id) !== index+1)
|
|
387
|
+
|| !['success', 'timeout'].includes(claimed.completionReason)
|
|
367
388
|
|| runs.has(claimed.runId)) invalid(`claim ${claimed?.id}`);
|
|
368
389
|
runs.add(claimed.runId);
|
|
369
390
|
const state = await journal.runGet(claimed.runId);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
3
|
+
import { authoredFunctionSource } from './authored-flow-lifecycle.js';
|
|
4
|
+
import { parallelBodySyntaxEligible, unwrapParallelExpression } from './authored-parallel-eligibility.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Execute a certified body with its branch-call promises marked at the call
|
|
8
|
+
* site. A combinator member is not evidence that it is a safe branch: it may
|
|
9
|
+
* be a hand-built join that resolves from whichever child finishes first.
|
|
10
|
+
*
|
|
11
|
+
* The closed grammar has no module captures, so re-evaluating this function
|
|
12
|
+
* preserves its meaning. Unknown source is refused before the first effect.
|
|
13
|
+
*/
|
|
14
|
+
export function instrumentParallelBody<T extends Function>(
|
|
15
|
+
body: T,
|
|
16
|
+
certifyBranch: (promise: Promise<unknown>) => void,
|
|
17
|
+
): T {
|
|
18
|
+
const source = authoredFunctionSource(body);
|
|
19
|
+
if (!parallelBodySyntaxEligible(body)) {
|
|
20
|
+
throw new AuthoredFlowExecutionError('step_failed', 'parallel f.run body lost its source certificate');
|
|
21
|
+
}
|
|
22
|
+
const prefix = 'const body = ';
|
|
23
|
+
const file = ts.createSourceFile('parallel-body.js', `${prefix}${source};`,
|
|
24
|
+
ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS);
|
|
25
|
+
const declaration = file.statements[0];
|
|
26
|
+
if (!declaration || !ts.isVariableStatement(declaration)) throw new Error('certified body parse failed');
|
|
27
|
+
const arrow = declaration.declarationList.declarations[0]?.initializer;
|
|
28
|
+
if (!arrow || !ts.isArrowFunction(arrow) || !ts.isBlock(arrow.body)) throw new Error('certified body changed');
|
|
29
|
+
const branches = new Set<string>();
|
|
30
|
+
for (const statement of arrow.body.statements) {
|
|
31
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
32
|
+
for (const item of statement.declarationList.declarations) {
|
|
33
|
+
if (ts.isIdentifier(item.name) && item.initializer && ts.isArrowFunction(item.initializer)) {
|
|
34
|
+
branches.add(item.name.text);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const calls: Array<{ start: number; end: number }> = [];
|
|
39
|
+
const visit = (node: ts.Node): void => {
|
|
40
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)
|
|
41
|
+
&& ts.isIdentifier(node.expression.expression) && node.expression.expression.text === 'Promise'
|
|
42
|
+
&& node.expression.name.text === 'all' && node.arguments.length === 1) {
|
|
43
|
+
const members = node.arguments[0];
|
|
44
|
+
if (members && ts.isArrayLiteralExpression(members)) {
|
|
45
|
+
for (const outer of members.elements) {
|
|
46
|
+
if (!ts.isExpression(outer)) continue;
|
|
47
|
+
const member = unwrapParallelExpression(outer);
|
|
48
|
+
if (!ts.isCallExpression(member) || member.arguments.length !== 0) continue;
|
|
49
|
+
const named = ts.isIdentifier(member.expression) && branches.has(member.expression.text);
|
|
50
|
+
const inline = ts.isParenthesizedExpression(member.expression)
|
|
51
|
+
&& ts.isArrowFunction(member.expression.expression);
|
|
52
|
+
if (named || inline) calls.push({ start: outer.getStart(file) - prefix.length,
|
|
53
|
+
end: outer.getEnd() - prefix.length });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
ts.forEachChild(node, visit);
|
|
58
|
+
};
|
|
59
|
+
visit(arrow);
|
|
60
|
+
let instrumented = source;
|
|
61
|
+
for (const { start, end } of calls.sort((left, right) => right.start - left.start)) {
|
|
62
|
+
instrumented = `${instrumented.slice(0, start)}__certifyParallelBranch(${instrumented.slice(start, end)})${instrumented.slice(end)}`;
|
|
63
|
+
}
|
|
64
|
+
const factory = new Function('__certifyParallelBranch', `return (${instrumented});`) as
|
|
65
|
+
(record: (promise: Promise<unknown>) => Promise<unknown>) => T;
|
|
66
|
+
return factory((promise) => { certifyBranch(promise); return promise; });
|
|
67
|
+
}
|