@relayflows/sdk 2.0.50 → 2.0.52
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 +97 -14
- 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 +2 -0
- package/dist/authored-node-entry.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 +8 -0
- package/dist/authored-parallel-eligibility.d.ts.map +1 -0
- package/dist/authored-parallel-eligibility.js +287 -0
- package/dist/authored-parallel-eligibility.js.map +1 -0
- package/dist/authored-preflight.js +4 -4
- package/dist/authored-preflight.js.map +1 -1
- 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 +55 -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 +2 -0
- package/dist/authored-verdict.d.ts.map +1 -1
- package/dist/authored-verdict.js +4 -1
- package/dist/authored-verdict.js.map +1 -1
- package/dist/authored-worker-step.d.ts +4 -3
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +23 -10
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check.js +2 -2
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +1 -1
- package/dist/cli/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +8 -7
- package/dist/cli/cli-probe.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/cloud-http.d.ts +1 -0
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +1 -1
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-run.d.ts +1 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +45 -6
- package/dist/cloud-run.js.map +1 -1
- package/dist/cloud-signal.d.ts +12 -0
- package/dist/cloud-signal.d.ts.map +1 -0
- package/dist/cloud-signal.js +117 -0
- package/dist/cloud-signal.js.map +1 -0
- package/dist/cloud-sync.d.ts +8 -1
- package/dist/cloud-sync.d.ts.map +1 -1
- package/dist/cloud-sync.js +36 -4
- package/dist/cloud-sync.js.map +1 -1
- package/dist/communication/environment.d.ts +1 -1
- package/dist/communication/environment.d.ts.map +1 -1
- package/dist/communication/environment.js +26 -3
- package/dist/communication/environment.js.map +1 -1
- package/dist/communication/worker.js +1 -1
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +5 -2
- package/dist/compile.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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +5 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +44 -4
- package/dist/journal-client.js.map +1 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +1 -1
- package/dist/llm-worker.js.map +1 -1
- package/dist/preflight.d.ts +2 -2
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +12 -7
- package/dist/preflight.js.map +1 -1
- package/dist/spec.d.ts +4 -0
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-fields.d.ts +1 -1
- package/dist/step-fields.d.ts.map +1 -1
- package/dist/step-fields.js +1 -0
- package/dist/step-fields.js.map +1 -1
- package/dist/step-secrets.d.ts +20 -0
- package/dist/step-secrets.d.ts.map +1 -0
- package/dist/step-secrets.js +146 -0
- package/dist/step-secrets.js.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +10 -0
- package/dist/validate.js.map +1 -1
- package/dist/worker-cli.d.ts +5 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +43 -14
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker.d.ts +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.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 +89 -13
- package/src/authored-flow-lifecycle.ts +215 -9
- package/src/authored-node-entry.ts +2 -0
- package/src/authored-parallel-body.ts +67 -0
- package/src/authored-parallel-eligibility.ts +245 -0
- package/src/authored-preflight.ts +4 -4
- package/src/authored-promise-graph.ts +35 -0
- package/src/authored-root-readback.ts +1 -2
- package/src/authored-root.ts +62 -9
- package/src/authored-step-graph.ts +50 -5
- package/src/authored-step-index.ts +2 -1
- package/src/authored-verdict.ts +4 -1
- package/src/authored-worker-step.ts +29 -9
- package/src/cli/check.ts +3 -3
- package/src/cli/cli-probe.ts +9 -6
- package/src/cli/run.ts +4 -2
- package/src/cloud-http.ts +1 -1
- package/src/cloud-run.ts +45 -8
- package/src/cloud-signal.ts +110 -0
- package/src/cloud-sync.ts +39 -4
- package/src/communication/environment.ts +28 -3
- package/src/communication/worker.ts +2 -1
- package/src/compile.ts +5 -2
- package/src/failure-kinds.ts +2 -0
- package/src/index.ts +1 -0
- package/src/journal-client.ts +48 -2
- package/src/llm-worker.ts +2 -1
- package/src/preflight.ts +13 -7
- package/src/spec.ts +4 -0
- package/src/step-fields.ts +1 -0
- package/src/step-secrets.ts +145 -0
- package/src/validate.ts +11 -0
- package/src/worker-cli.ts +41 -13
- package/src/worker.ts +3 -2
|
@@ -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;
|
|
@@ -81,6 +81,8 @@ try {
|
|
|
81
81
|
request.metadata.inputPresent ? request.metadata.input : undefined, {
|
|
82
82
|
getDefinition: loaded.getDefinition, dataDir: request.dataDir,
|
|
83
83
|
flowPath: request.metadata.flowPath, rootRunId: request.rootRunId,
|
|
84
|
+
executionMode: request.metadata.identityVersion === 2 && request.metadata.executionMode === 'parallel'
|
|
85
|
+
? 'parallel' : 'serial',
|
|
84
86
|
extensions: loaded.extensions,
|
|
85
87
|
flowGraph: loaded.graph,
|
|
86
88
|
localAgentStream: request.localAgentStream, signal: controller.signal,
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { authoredFunctionSource } from './authored-flow-lifecycle.js';
|
|
3
|
+
|
|
4
|
+
const reservedCertifier = '__certifyParallelBranch';
|
|
5
|
+
|
|
6
|
+
/** Keep admission and call-site instrumentation in agreement on parentheses. */
|
|
7
|
+
export function unwrapParallelExpression(node: ts.Expression): ts.Expression {
|
|
8
|
+
return ts.isParenthesizedExpression(node) ? unwrapParallelExpression(node.expression) : node;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A closed grammar for deterministic Promise.all branches. Unknown bodies stay serial. */
|
|
12
|
+
export function parallelRunEligible(body: Function, moduleSource: string): boolean {
|
|
13
|
+
// Promise.all is intercepted only when `Promise` resolves to the intrinsic.
|
|
14
|
+
// Function.toString cannot see a module-level lexical binding, so inspect
|
|
15
|
+
// the complete source before using the body's narrower grammar.
|
|
16
|
+
const bodySource = authoredFunctionSource(body);
|
|
17
|
+
return moduleUsesIntrinsicPromise(moduleSource, bodySource) && parallelBodySyntaxEligible(body);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Independently re-check the closed body grammar at execution time. */
|
|
21
|
+
export function parallelBodySyntaxEligible(body: Function): boolean {
|
|
22
|
+
const bodySource = authoredFunctionSource(body);
|
|
23
|
+
const file = ts.createSourceFile('authored-body.js', `const body = ${bodySource};`,
|
|
24
|
+
ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS);
|
|
25
|
+
if (file.statements.length !== 1) return false;
|
|
26
|
+
const declaration = file.statements[0];
|
|
27
|
+
if (!declaration || !ts.isVariableStatement(declaration)) return false;
|
|
28
|
+
const expression = declaration.declarationList.declarations[0]?.initializer;
|
|
29
|
+
// Function expressions can obtain the context through `arguments[0]`.
|
|
30
|
+
if (expression === undefined || !ts.isArrowFunction(expression) || !ts.isBlock(expression.body)
|
|
31
|
+
|| expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) !== true) return false;
|
|
32
|
+
const parameter = expression.parameters[0]?.name;
|
|
33
|
+
if (parameter === undefined || !ts.isIdentifier(parameter) || expression.parameters.length !== 1) return false;
|
|
34
|
+
const context = parameter.text;
|
|
35
|
+
if (['Promise', 'globalThis', 'global'].includes(context)) return false;
|
|
36
|
+
// The instrumented body is re-evaluated with a private callback parameter.
|
|
37
|
+
// A lexical binding or reference with that spelling could capture it.
|
|
38
|
+
let namesCertifier = false;
|
|
39
|
+
const checkCertifierName = (node: ts.Node): void => {
|
|
40
|
+
if (ts.isIdentifier(node) && node.text === reservedCertifier) namesCertifier = true;
|
|
41
|
+
ts.forEachChild(node, checkCertifierName);
|
|
42
|
+
};
|
|
43
|
+
checkCertifierName(expression);
|
|
44
|
+
if (namesCertifier) return false;
|
|
45
|
+
const branches = new Map<string, ts.ArrowFunction>();
|
|
46
|
+
const rootValues = new Set<string>();
|
|
47
|
+
let groups = 0;
|
|
48
|
+
let runs = 0;
|
|
49
|
+
|
|
50
|
+
const unwrap = unwrapParallelExpression;
|
|
51
|
+
const method = (node: ts.Expression, owner: string, name: string): node is ts.CallExpression => {
|
|
52
|
+
const value = unwrap(node);
|
|
53
|
+
return ts.isCallExpression(value) && ts.isPropertyAccessExpression(value.expression)
|
|
54
|
+
&& ts.isIdentifier(value.expression.expression) && value.expression.expression.text === owner
|
|
55
|
+
&& value.expression.name.text === name;
|
|
56
|
+
};
|
|
57
|
+
const pure = (node: ts.Expression, values: ReadonlySet<string>): boolean => {
|
|
58
|
+
const value = unwrap(node);
|
|
59
|
+
if (ts.isStringLiteral(value) || ts.isNumericLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)
|
|
60
|
+
|| value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword
|
|
61
|
+
|| value.kind === ts.SyntaxKind.NullKeyword) return true;
|
|
62
|
+
if (ts.isIdentifier(value)) return values.has(value.text);
|
|
63
|
+
if (ts.isTemplateExpression(value)) return value.templateSpans.every(span => pure(span.expression, values));
|
|
64
|
+
if (ts.isPrefixUnaryExpression(value)) return [ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken,
|
|
65
|
+
ts.SyntaxKind.ExclamationToken].includes(value.operator) && pure(value.operand, values);
|
|
66
|
+
if (ts.isBinaryExpression(value)) return [ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken,
|
|
67
|
+
ts.SyntaxKind.AsteriskToken, ts.SyntaxKind.SlashToken, ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
68
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken, ts.SyntaxKind.LessThanToken,
|
|
69
|
+
ts.SyntaxKind.GreaterThanToken].includes(value.operatorToken.kind)
|
|
70
|
+
&& pure(value.left, values) && pure(value.right, values);
|
|
71
|
+
if (ts.isConditionalExpression(value)) return pure(value.condition, values)
|
|
72
|
+
&& pure(value.whenTrue, values) && pure(value.whenFalse, values);
|
|
73
|
+
if (ts.isElementAccessExpression(value)) return value.argumentExpression !== undefined
|
|
74
|
+
&& pure(value.expression, values) && pure(value.argumentExpression, values);
|
|
75
|
+
if (ts.isPropertyAccessExpression(value)) return pure(value.expression, values);
|
|
76
|
+
if (ts.isArrayLiteralExpression(value)) return value.elements.every(element =>
|
|
77
|
+
ts.isExpression(element) && pure(element, values));
|
|
78
|
+
if (ts.isObjectLiteralExpression(value)) return value.properties.every(property =>
|
|
79
|
+
ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))
|
|
80
|
+
&& pure(property.initializer, values));
|
|
81
|
+
return false;
|
|
82
|
+
};
|
|
83
|
+
const run = (node: ts.Expression, values: ReadonlySet<string>): boolean => {
|
|
84
|
+
const value = unwrap(node);
|
|
85
|
+
if (!method(value, context, 'run') || value.arguments.length < 1 || value.arguments.length > 2
|
|
86
|
+
|| !value.arguments.every(argument => pure(argument, values))) return false;
|
|
87
|
+
runs++;
|
|
88
|
+
return true;
|
|
89
|
+
};
|
|
90
|
+
const group = (node: ts.Expression, values: ReadonlySet<string>, depth: number): boolean => {
|
|
91
|
+
const value = unwrap(node);
|
|
92
|
+
if (!method(value, 'Promise', 'all') || value.arguments.length !== 1 || depth > 4) return false;
|
|
93
|
+
groups++;
|
|
94
|
+
const members = value.arguments[0]!;
|
|
95
|
+
if (ts.isArrayLiteralExpression(members)) {
|
|
96
|
+
if (members.elements.length < 2) return false;
|
|
97
|
+
return members.elements.every(member => {
|
|
98
|
+
if (!ts.isExpression(member)) return false;
|
|
99
|
+
if (run(member, values)) return true;
|
|
100
|
+
const call = unwrap(member);
|
|
101
|
+
if (depth === 0 && ts.isCallExpression(call) && ts.isIdentifier(call.expression)
|
|
102
|
+
&& call.arguments.length === 0 && branches.has(call.expression.text)) {
|
|
103
|
+
return branch(branches.get(call.expression.text)!, depth + 1);
|
|
104
|
+
}
|
|
105
|
+
if (depth === 0 && ts.isCallExpression(call) && call.arguments.length === 0
|
|
106
|
+
&& ts.isParenthesizedExpression(call.expression) && ts.isArrowFunction(call.expression.expression)) {
|
|
107
|
+
return branch(call.expression.expression, depth + 1);
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
};
|
|
114
|
+
const awaitStep = (node: ts.Expression, values: ReadonlySet<string>, depth: number): boolean => {
|
|
115
|
+
const value = unwrap(node);
|
|
116
|
+
if (!ts.isAwaitExpression(value)) return false;
|
|
117
|
+
return run(value.expression, values) || group(value.expression, values, depth);
|
|
118
|
+
};
|
|
119
|
+
const statements = (block: ts.Block, values: Set<string>, depth: number): boolean => {
|
|
120
|
+
for (const statement of block.statements) {
|
|
121
|
+
if (ts.isExpressionStatement(statement)) {
|
|
122
|
+
if (awaitStep(statement.expression, values, depth)) continue;
|
|
123
|
+
const value = unwrap(statement.expression);
|
|
124
|
+
if (depth === 0 && method(value, context, 'done') && value.arguments.length >= 1
|
|
125
|
+
&& value.arguments.every(argument => pure(argument, values))
|
|
126
|
+
&& statement === block.statements[block.statements.length - 1]) continue;
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (ts.isReturnStatement(statement) && depth > 0 && statement.expression !== undefined
|
|
130
|
+
&& awaitStep(statement.expression, values, depth)) continue;
|
|
131
|
+
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0) return false;
|
|
132
|
+
for (const item of statement.declarationList.declarations) {
|
|
133
|
+
if (!ts.isIdentifier(item.name) || item.initializer === undefined) return false;
|
|
134
|
+
if (['Promise', 'globalThis', 'global'].includes(item.name.text)) return false;
|
|
135
|
+
if (depth === 0 && ts.isArrowFunction(item.initializer)) {
|
|
136
|
+
if (branches.has(item.name.text) || item.initializer.parameters.length !== 0
|
|
137
|
+
|| item.initializer.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) !== true
|
|
138
|
+
) return false;
|
|
139
|
+
branches.set(item.name.text, item.initializer);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (!awaitStep(item.initializer, values, depth)) return false;
|
|
143
|
+
values.add(item.name.text);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
};
|
|
148
|
+
const branch = (arrow: ts.ArrowFunction, depth: number): boolean => {
|
|
149
|
+
if (arrow.parameters.length !== 0) return false;
|
|
150
|
+
const before = runs;
|
|
151
|
+
const valid = ts.isBlock(arrow.body) ? statements(arrow.body, new Set(rootValues), depth)
|
|
152
|
+
: awaitStep(arrow.body, rootValues, depth);
|
|
153
|
+
// An empty async branch creates a join with no causal step predecessor.
|
|
154
|
+
return valid && runs > before;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (!statements(expression.body, rootValues, 0)) return false;
|
|
158
|
+
return groups > 0 && runs > 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Reject any source that can change what a free `Promise` identifier means. */
|
|
162
|
+
function moduleUsesIntrinsicPromise(source: string, bodySource: string): boolean {
|
|
163
|
+
const file = ts.createSourceFile('authored-module.ts', source, ts.ScriptTarget.ES2022, true, ts.ScriptKind.TS);
|
|
164
|
+
if (((file as ts.SourceFile & { parseDiagnostics?: readonly ts.Diagnostic[] }).parseDiagnostics?.length ?? 0) > 0) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
// Top-level code can alter the constructor or its methods without binding
|
|
168
|
+
// the name. Leave these modules on the established serial scheduler too.
|
|
169
|
+
const touchesPromiseOutsideDefinition = (node: ts.Node): boolean => {
|
|
170
|
+
if (ts.isIdentifier(node) && ['Promise', 'globalThis', 'global'].includes(node.text)) return true;
|
|
171
|
+
return ts.forEachChild(node, touchesPromiseOutsideDefinition) === true;
|
|
172
|
+
};
|
|
173
|
+
let definesDefaultBody = false;
|
|
174
|
+
let importsSurfaceFlow = false;
|
|
175
|
+
for (const statement of file.statements) {
|
|
176
|
+
if (ts.isImportDeclaration(statement)) {
|
|
177
|
+
// Imports can execute code before the body starts. Only the direct,
|
|
178
|
+
// immutable Surface binding can certify the exported body's identity.
|
|
179
|
+
if (statement.moduleSpecifier.getText(file) !== "'@relayflows/surface'"
|
|
180
|
+
&& statement.moduleSpecifier.getText(file) !== '"@relayflows/surface"') return false;
|
|
181
|
+
const bindings = statement.importClause?.namedBindings;
|
|
182
|
+
if (statement.importClause?.name !== undefined || bindings === undefined
|
|
183
|
+
|| !ts.isNamedImports(bindings) || bindings.elements.length !== 1
|
|
184
|
+
|| bindings.elements[0]?.name.text !== 'flow'
|
|
185
|
+
|| bindings.elements[0]?.propertyName !== undefined || importsSurfaceFlow) return false;
|
|
186
|
+
importsSurfaceFlow = true;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (ts.isExportAssignment(statement)) {
|
|
190
|
+
// Only the final body argument is allowed to mention Promise.all. Do
|
|
191
|
+
// not let an expression around flow() mutate Promise while we skip it.
|
|
192
|
+
const call = statement.expression;
|
|
193
|
+
if (statement.isExportEquals || !ts.isCallExpression(call)
|
|
194
|
+
|| !ts.isIdentifier(call.expression) || call.expression.text !== 'flow') return false;
|
|
195
|
+
const args = call.arguments;
|
|
196
|
+
if (args.length !== 2 || !ts.isStringLiteral(args[0]!)) return false;
|
|
197
|
+
if (!ts.isArrowFunction(args[args.length - 1]!)) return false;
|
|
198
|
+
if (args[args.length - 1]!.getText(file) !== bodySource) return false;
|
|
199
|
+
if (args.slice(0, -1).some(touchesPromiseOutsideDefinition)) return false;
|
|
200
|
+
if (definesDefaultBody) return false;
|
|
201
|
+
definesDefaultBody = true;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// The certified body cannot use module captures. Other top-level code is
|
|
205
|
+
// unnecessary for it and may schedule a later mutation through an alias.
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
const forbidden = new Set(['Promise', 'globalThis']);
|
|
209
|
+
const bindsForbidden = (name: ts.BindingName): boolean => ts.isIdentifier(name)
|
|
210
|
+
? forbidden.has(name.text)
|
|
211
|
+
: name.elements.some(element => ts.isBindingElement(element) && bindsForbidden(element.name));
|
|
212
|
+
const isGlobalPromise = (node: ts.Node): boolean => {
|
|
213
|
+
if (ts.isIdentifier(node)) return forbidden.has(node.text);
|
|
214
|
+
if (ts.isPropertyAccessExpression(node)) return ts.isIdentifier(node.expression)
|
|
215
|
+
&& node.expression.text === 'globalThis' && node.name.text === 'Promise';
|
|
216
|
+
return ts.isElementAccessExpression(node) && ts.isIdentifier(node.expression)
|
|
217
|
+
&& node.expression.text === 'globalThis' && ts.isStringLiteral(node.argumentExpression)
|
|
218
|
+
&& node.argumentExpression.text === 'Promise';
|
|
219
|
+
};
|
|
220
|
+
let unsafe = false;
|
|
221
|
+
const visit = (node: ts.Node): void => {
|
|
222
|
+
if (unsafe) return;
|
|
223
|
+
if ((ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isBindingElement(node))
|
|
224
|
+
&& bindsForbidden(node.name)) unsafe = true;
|
|
225
|
+
if ((ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
|
|
226
|
+
|| ts.isClassDeclaration(node) || ts.isClassExpression(node)
|
|
227
|
+
|| ts.isEnumDeclaration(node) || ts.isImportEqualsDeclaration(node)
|
|
228
|
+
|| ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node))
|
|
229
|
+
&& node.name !== undefined && forbidden.has(node.name.text)) unsafe = true;
|
|
230
|
+
if (ts.isImportClause(node) && node.name !== undefined && forbidden.has(node.name.text)) unsafe = true;
|
|
231
|
+
if (ts.isNamespaceImport(node) && forbidden.has(node.name.text)) unsafe = true;
|
|
232
|
+
if (ts.isImportSpecifier(node) && forbidden.has(node.name.text)) unsafe = true;
|
|
233
|
+
if (ts.isBinaryExpression(node)
|
|
234
|
+
&& node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment
|
|
235
|
+
&& node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
|
236
|
+
&& isGlobalPromise(node.left)) unsafe = true;
|
|
237
|
+
if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node))
|
|
238
|
+
&& isGlobalPromise(node.operand)) unsafe = true;
|
|
239
|
+
ts.forEachChild(node, visit);
|
|
240
|
+
};
|
|
241
|
+
visit(file);
|
|
242
|
+
// The body may come from another module, where a free Promise name can be
|
|
243
|
+
// shadowed without appearing in this source. In that case use serial IDs.
|
|
244
|
+
return importsSurfaceFlow && definesDefaultBody && !unsafe;
|
|
245
|
+
}
|