@procedures/client 0.24.1 → 0.26.0
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/CHANGELOG.md +93 -0
- package/README.md +15 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/server/handler.js +7 -2
- package/dist/adapters/http/server/handler.js.map +1 -1
- package/dist/adapters/http/server/ndjson.d.ts +10 -13
- package/dist/adapters/http/server/ndjson.d.ts.map +1 -1
- package/dist/adapters/http/server/ndjson.js +12 -13
- package/dist/adapters/http/server/ndjson.js.map +1 -1
- package/dist/adapters/websocket/shared/rpc.d.ts +5 -8
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.js +11 -14
- package/dist/adapters/websocket/shared/rpc.js.map +1 -1
- package/dist/browser.d.ts +3 -3
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +4 -1
- package/dist/browser.js.map +1 -1
- package/dist/client/client.d.ts +7 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +30 -19
- package/dist/client/client.js.map +1 -1
- package/dist/client/middleware/retry.d.ts.map +1 -1
- package/dist/client/middleware/retry.js +3 -1
- package/dist/client/middleware/retry.js.map +1 -1
- package/dist/procedures/core/index.d.ts +19 -3
- package/dist/procedures/core/index.d.ts.map +1 -1
- package/dist/procedures/core/index.js +53 -178
- package/dist/procedures/core/index.js.map +1 -1
- package/dist/procedures/core/operands.d.ts +12 -13
- package/dist/procedures/core/operands.d.ts.map +1 -1
- package/dist/procedures/core/operands.js +22 -38
- package/dist/procedures/core/operands.js.map +1 -1
- package/dist/procedures/define-procedure.d.ts.map +1 -1
- package/dist/procedures/define-procedure.js +6 -6
- package/dist/procedures/define-procedure.js.map +1 -1
- package/dist/procedures/envelope.d.ts +24 -8
- package/dist/procedures/envelope.d.ts.map +1 -1
- package/dist/procedures/envelope.js +51 -20
- package/dist/procedures/envelope.js.map +1 -1
- package/dist/procedures/idempotency.d.ts +5 -0
- package/dist/procedures/idempotency.d.ts.map +1 -1
- package/dist/procedures/idempotency.js +8 -0
- package/dist/procedures/idempotency.js.map +1 -1
- package/dist/procedures/index.d.ts +3 -1
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +3 -1
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/program.d.ts +251 -0
- package/dist/procedures/program.d.ts.map +1 -0
- package/dist/procedures/program.js +1120 -0
- package/dist/procedures/program.js.map +1 -0
- package/dist/procedures/ref.d.ts +8 -91
- package/dist/procedures/ref.d.ts.map +1 -1
- package/dist/procedures/ref.js +16 -279
- package/dist/procedures/ref.js.map +1 -1
- package/dist/protocol.d.ts +12 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +20 -0
- package/dist/protocol.js.map +1 -1
- package/dist/server/procedure-server.d.ts +3 -6
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +33 -14
- package/dist/server/procedure-server.js.map +1 -1
- package/package.json +2 -1
- package/src/adapters/http/server/handler.ts +8 -2
- package/src/adapters/http/server/ndjson.ts +17 -18
- package/src/adapters/websocket/shared/rpc.ts +11 -14
- package/src/browser.ts +25 -1
- package/src/client/client.ts +35 -23
- package/src/client/middleware/retry.ts +3 -1
- package/src/procedures/core/index.ts +68 -212
- package/src/procedures/core/operands.ts +24 -44
- package/src/procedures/define-procedure.ts +5 -7
- package/src/procedures/envelope.ts +64 -21
- package/src/procedures/idempotency.ts +8 -0
- package/src/procedures/index.ts +34 -4
- package/src/procedures/program.ts +1371 -0
- package/src/procedures/ref.ts +20 -354
- package/src/protocol.ts +19 -0
- package/src/server/procedure-server.ts +32 -13
package/src/browser.ts
CHANGED
|
@@ -220,6 +220,20 @@ export {
|
|
|
220
220
|
extractTemplate,
|
|
221
221
|
parseProcedureJson,
|
|
222
222
|
stringifyProcedureJson,
|
|
223
|
+
// Programs on the coeval kernel (coeval D13, roadmap item M14)
|
|
224
|
+
runProgram,
|
|
225
|
+
runDocument,
|
|
226
|
+
programMachine,
|
|
227
|
+
PROGRAM_WORLD,
|
|
228
|
+
desugarProgram,
|
|
229
|
+
desugarInput,
|
|
230
|
+
desugarControlValue,
|
|
231
|
+
originOf,
|
|
232
|
+
namesOfScope,
|
|
233
|
+
scopeOfNames,
|
|
234
|
+
nameKey,
|
|
235
|
+
nameOfKey,
|
|
236
|
+
documentAt,
|
|
223
237
|
// The envelope form of a program (coeval M7)
|
|
224
238
|
isEnvelopeProgram,
|
|
225
239
|
fromEnvelopeProgram,
|
|
@@ -344,6 +358,15 @@ export type {
|
|
|
344
358
|
ContinueDecision,
|
|
345
359
|
OutputRef,
|
|
346
360
|
RefScope,
|
|
361
|
+
// Programs on the coeval kernel (coeval D13, roadmap item M14)
|
|
362
|
+
ProgramDocument,
|
|
363
|
+
DocumentOrigin,
|
|
364
|
+
DesugarOptions,
|
|
365
|
+
ProgramHost,
|
|
366
|
+
CallEffect,
|
|
367
|
+
CallPosition,
|
|
368
|
+
RunDocumentOptions,
|
|
369
|
+
RunProgramOptions,
|
|
347
370
|
// The envelope form of a program (coeval M7)
|
|
348
371
|
EnvelopeProgram,
|
|
349
372
|
EnvelopeRef,
|
|
@@ -414,7 +437,8 @@ export type { PortLike, PortTransportOptions } from "./adapters/port/transport.j
|
|
|
414
437
|
export { TRACEPARENT_METADATA_KEY, formatTraceparent, parseTraceparent, newTraceId, newSpanId, isW3cIds } from "./tracing/traceparent.js";
|
|
415
438
|
export type { TraceContext } from "./tracing/traceparent.js";
|
|
416
439
|
// Wire protocol v1: the version and the one list of error codes (roadmap R25)
|
|
417
|
-
|
|
440
|
+
// The retryable flag of an error that ends a call: the default of its code before the first item (CLI-68)
|
|
441
|
+
export { PROTOCOL_VERSION, PROTOCOL_HEADER, ERROR_CODES, isProtocolErrorCode, httpStatusOf, protocolVersionOf, retryableOf } from "./protocol.js";
|
|
418
442
|
export type { ProtocolErrorCode, ErrorCodeInfo } from "./protocol.js";
|
|
419
443
|
// The seq of the stream frames: the metadata key of an item after a gap, and the key of an item
|
|
420
444
|
// that the HTTP client read from an NDJSON stream (WIRE-PROTOCOL.md)
|
package/src/client/client.ts
CHANGED
|
@@ -88,12 +88,12 @@ import {
|
|
|
88
88
|
isAnyProcedureRef,
|
|
89
89
|
isPlainObject,
|
|
90
90
|
normalizeRef,
|
|
91
|
-
prepareRootCall,
|
|
92
91
|
rawInputRule,
|
|
93
92
|
type ProcedureRef,
|
|
94
93
|
type AnyProcedureRef,
|
|
95
94
|
type HydrationEvent,
|
|
96
95
|
} from "../procedures/ref.js";
|
|
96
|
+
import { runProgram } from "../procedures/program.js";
|
|
97
97
|
import { fromEnvelopeProgram, isEnvelopeProgram, type EnvelopeProgram } from "../procedures/envelope.js";
|
|
98
98
|
|
|
99
99
|
/** Each element of each array item: the stream form of an `outputMode: "batch"` procedure. */
|
|
@@ -527,8 +527,10 @@ export class Client<TContext = {}> {
|
|
|
527
527
|
options: ExecOptions = {}
|
|
528
528
|
): Promise<TOutput> {
|
|
529
529
|
const caller: CallerContext = callerOf(options);
|
|
530
|
-
|
|
531
|
-
return this.
|
|
530
|
+
// The program is one run of the kernel: its root call is the root document (PROGRAM-FORMAT section 2)
|
|
531
|
+
return (await this.runRef(this.prepareRef(refOrPath, input), caller, (path, value) =>
|
|
532
|
+
this.execInternal(path, value, caller),
|
|
533
|
+
)) as TOutput;
|
|
532
534
|
}
|
|
533
535
|
|
|
534
536
|
/**
|
|
@@ -551,19 +553,42 @@ export class Client<TContext = {}> {
|
|
|
551
553
|
const self = this;
|
|
552
554
|
const caller: CallerContext = callerOf(options);
|
|
553
555
|
return (async function* () {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
+
// The root call gives the items: the kernel gets the stream of the root call as its result
|
|
557
|
+
const items = await self.runRef(self.prepareRef(refOrPath, input), caller, (path, value) =>
|
|
558
|
+
self.execStreamInternal<TOutput>(path, value, caller),
|
|
559
|
+
);
|
|
560
|
+
yield* items as AsyncIterable<TOutput>;
|
|
556
561
|
})();
|
|
557
562
|
}
|
|
558
563
|
|
|
559
564
|
/**
|
|
560
|
-
*
|
|
565
|
+
* This function runs a program on the kernel. The nested calls go through `execInternal`, and
|
|
566
|
+
* `root` starts the root call.
|
|
561
567
|
*/
|
|
562
|
-
private
|
|
568
|
+
private runRef(ref: ProcedureRef, caller: CallerContext, root: (path: ProcedurePath, input: unknown) => unknown): Promise<unknown> {
|
|
569
|
+
// A procedure that takes its input raw (control flow, the `runs-refs` and `raw-input` tags,
|
|
570
|
+
// `procedure.define`) gets its operand refs RAW: it runs them itself, with correct
|
|
571
|
+
// ordering/laziness/error-scoping. Eagerly hydrating here would run the operands up-front
|
|
572
|
+
// and can hijack an all-refs array into an implicit chain that clobbers `steps`. See
|
|
573
|
+
// documentation/BUGS-2026-07.md (C2, H2, H3, M35). The procedure in the registry decides,
|
|
574
|
+
// also for the refs nested in the input (deep dive CORE-5: before, the last path segment did).
|
|
575
|
+
// The body of a defined procedure starts its root call the same way (runProgram, DOC-3)
|
|
576
|
+
return runProgram(ref, {
|
|
577
|
+
executor: <TIn, TOut>(path: ProcedurePath, inp: TIn): Promise<TOut> => this.execInternal<TOut>(path, inp, caller),
|
|
578
|
+
rootExecutor: root,
|
|
579
|
+
rawInput: rawInputRule(this.procedureRegistry),
|
|
580
|
+
onRef: caller.onRef,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* The root call of a program, as a ref: an envelope program changes into the `$proc` form, a
|
|
586
|
+
* path gets its input, and the fields of `input` go over the fields of the input of a ref.
|
|
587
|
+
*/
|
|
588
|
+
private prepareRef(
|
|
563
589
|
refOrPathOrEnvelope: AnyProcedureRef | ProcedurePath | EnvelopeProgram,
|
|
564
590
|
input: unknown,
|
|
565
|
-
|
|
566
|
-
): Promise<{ path: ProcedurePath; input: unknown }> {
|
|
591
|
+
): ProcedureRef {
|
|
567
592
|
// A program in the envelope form (coeval M7) changes into the $proc form first. The root
|
|
568
593
|
// decides the form: an envelope in the input of a $proc program stays data.
|
|
569
594
|
const refOrPath = isEnvelopeProgram(refOrPathOrEnvelope)
|
|
@@ -592,20 +617,7 @@ export class Client<TContext = {}> {
|
|
|
592
617
|
} else {
|
|
593
618
|
throw new Error("exec() requires a ProcedureRef, $proc JSON, or procedure path array");
|
|
594
619
|
}
|
|
595
|
-
|
|
596
|
-
// Create executor function for hydration
|
|
597
|
-
const executor = async <TIn, TOut>(path: ProcedurePath, inp: TIn): Promise<TOut> => {
|
|
598
|
-
return this.execInternal<TOut>(path, inp, caller);
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
// A procedure that takes its input raw (control flow, the `runs-refs` and `raw-input` tags,
|
|
602
|
-
// `procedure.define`) gets its operand refs RAW: it runs them itself, with correct
|
|
603
|
-
// ordering/laziness/error-scoping. Eagerly hydrating here would run the operands up-front
|
|
604
|
-
// and can hijack an all-refs array into an implicit chain that clobbers `steps`. See
|
|
605
|
-
// documentation/BUGS-2026-07.md (C2, H2, H3, M35). The procedure in the registry decides,
|
|
606
|
-
// also for the refs nested in the input (deep dive CORE-5: before, the last path segment did).
|
|
607
|
-
// The body of a defined procedure starts its root call the same way (prepareRootCall, DOC-3)
|
|
608
|
-
return prepareRootCall(ref, executor, { rawInput: rawInputRule(this.procedureRegistry), onRef: caller.onRef });
|
|
620
|
+
return ref;
|
|
609
621
|
}
|
|
610
622
|
|
|
611
623
|
/**
|
|
@@ -280,7 +280,9 @@ export function createRetryMiddleware(options: RetryOptions = {}): TypedClientMi
|
|
|
280
280
|
// The items pass through as they arrive (BUGS-2026-07 M2: before, the middleware
|
|
281
281
|
// collected the whole stream first). A retry is possible only while no item has
|
|
282
282
|
// reached the reader: the first item is a retryable error, or the attempt throws
|
|
283
|
-
// before its first item. After that, a retry would repeat items.
|
|
283
|
+
// before its first item. After that, a retry would repeat items. The server gives an error
|
|
284
|
+
// without a flag before the first item the flag of its code, so a stream that fails with
|
|
285
|
+
// UNAVAILABLE before its first item goes again (CLI-68).
|
|
284
286
|
let started = false;
|
|
285
287
|
let lastItem: ResponseItem<TRes> | undefined;
|
|
286
288
|
let retryItem: ResponseItem<TRes> | undefined;
|
|
@@ -130,17 +130,9 @@ import {
|
|
|
130
130
|
getRefWhen,
|
|
131
131
|
WHEN_NEVER,
|
|
132
132
|
RUNS_REFS_TAG,
|
|
133
|
-
type
|
|
133
|
+
type AnyProcedureRef,
|
|
134
134
|
} from "../ref.js";
|
|
135
|
-
import {
|
|
136
|
-
substituteRefs,
|
|
137
|
-
hydrateOperand,
|
|
138
|
-
callOperand,
|
|
139
|
-
evaluateOperand,
|
|
140
|
-
parentContext,
|
|
141
|
-
scopeWith,
|
|
142
|
-
controlFlowTags,
|
|
143
|
-
} from "./operands.js";
|
|
135
|
+
import { substituteRefs, runControlFlow, controlFlowTags } from "./operands.js";
|
|
144
136
|
|
|
145
137
|
// =============================================================================
|
|
146
138
|
// Chain Procedure
|
|
@@ -182,57 +174,27 @@ const chainProcedure: ChainProcedure = defineProcedure({
|
|
|
182
174
|
tags: controlFlowTags("control-flow"),
|
|
183
175
|
},
|
|
184
176
|
handler: async (input: ChainInput, ctx?: ProcedureContext): Promise<ChainOutput> => {
|
|
185
|
-
const { steps
|
|
177
|
+
const { steps } = input;
|
|
186
178
|
if (!Array.isArray(steps)) {
|
|
187
179
|
throw new Error(`client.chain: steps must be an array, got ${steps === null ? "null" : typeof steps}`);
|
|
188
180
|
}
|
|
189
|
-
|
|
190
|
-
|
|
181
|
+
if (ctx?.client) {
|
|
182
|
+
// The runner of the kind client.chain runs the steps on the kernel (program.ts). A step reads
|
|
183
|
+
// the names of the earlier steps, $last and the names of the caller (the scope of the input).
|
|
184
|
+
return (await runControlFlow("chain", input, ctx)) as ChainOutput;
|
|
185
|
+
}
|
|
186
|
+
// No client context: the result of a step is its input, with the known $refs resolved
|
|
191
187
|
const scope = createRefScope(scopeOf(input));
|
|
192
|
-
|
|
193
|
-
// Extract context to propagate to steps (e.g., cwd, node)
|
|
194
|
-
const { cwd, node } = parentInput as { cwd?: string; node?: unknown };
|
|
195
|
-
|
|
196
|
-
// With passThrough, the value that the next step without input gets (docs review DOC-4:
|
|
197
|
-
// before, the procedure ignored passThrough and initialInput)
|
|
198
|
-
let passed: unknown = passThrough && ctx?.client ? await hydrateOperand(initialInput, ctx, scope) : undefined;
|
|
199
|
-
|
|
188
|
+
const results: unknown[] = [];
|
|
200
189
|
for (const step of steps) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
result = step;
|
|
206
|
-
} else if (!ctx?.client) {
|
|
207
|
-
// No client context: the result of a step is its input, with the known $refs resolved
|
|
208
|
-
result = substituteRefs(isAnyProcedureRef(step) ? normalizeRef(step).input : step, scope);
|
|
209
|
-
} else if (isAnyProcedureRef(step)) {
|
|
210
|
-
// Resolve the $refs of the step's input, run its nested refs, add the parent context (cwd, node).
|
|
211
|
-
// With passThrough, a step with no input gets the passed value as a literal: it ran already.
|
|
212
|
-
const operand =
|
|
213
|
-
passThrough && normalizeRef(step).input === undefined ? { ...step, input: { $literal: passed } } : step;
|
|
214
|
-
result = await callOperand(operand, ctx, { scope, extra: parentContext({ cwd, node }) });
|
|
215
|
-
} else {
|
|
216
|
-
// A $ref, a $literal, or a value that holds refs
|
|
217
|
-
result = await hydrateOperand(step, ctx, scope);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Store named output
|
|
221
|
-
const stepName = isAnyProcedureRef(step) ? normalizeRef(step).$name : undefined;
|
|
222
|
-
if (stepName) {
|
|
223
|
-
scope.outputs.set(stepName, result);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Update $last
|
|
190
|
+
const call = isAnyProcedureRef(step) ? normalizeRef(step) : undefined;
|
|
191
|
+
// A $never step is data, like a $never ref anywhere (docs review DOC-1)
|
|
192
|
+
const result = call && getRefWhen(step as AnyProcedureRef) === WHEN_NEVER ? step : substituteRefs(call ? call.input : step, scope);
|
|
193
|
+
if (call?.$name) scope.outputs.set(call.$name, result);
|
|
227
194
|
scope.last = result;
|
|
228
|
-
passed = result;
|
|
229
195
|
results.push(result);
|
|
230
196
|
}
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
results,
|
|
234
|
-
final: results[results.length - 1],
|
|
235
|
-
};
|
|
197
|
+
return { results, final: results[results.length - 1] };
|
|
236
198
|
},
|
|
237
199
|
});
|
|
238
200
|
|
|
@@ -245,7 +207,13 @@ interface ParallelInput {
|
|
|
245
207
|
tasks: unknown[];
|
|
246
208
|
/** Maximum concurrency (default: unlimited) */
|
|
247
209
|
concurrency?: number;
|
|
248
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* Whether to fail fast on first error (default: false).
|
|
212
|
+
*
|
|
213
|
+
* After the first error, the other tasks stop at their next wait, and their later calls do not
|
|
214
|
+
* start (D26). Before 0.26.0, they went on in the background to their end. When those calls
|
|
215
|
+
* must start, wait for the tasks yourself, or do not use `failFast`.
|
|
216
|
+
*/
|
|
249
217
|
failFast?: boolean;
|
|
250
218
|
}
|
|
251
219
|
|
|
@@ -254,7 +222,12 @@ interface ParallelOutput {
|
|
|
254
222
|
results: unknown[];
|
|
255
223
|
/** Whether all tasks succeeded */
|
|
256
224
|
allSucceeded: boolean;
|
|
257
|
-
/**
|
|
225
|
+
/**
|
|
226
|
+
* Errors from failed tasks.
|
|
227
|
+
*
|
|
228
|
+
* The error text of a task that throws `undefined` is the reason of the kernel (D26). Before
|
|
229
|
+
* 0.26.0, it was `"undefined"`.
|
|
230
|
+
*/
|
|
258
231
|
errors: Array<{ index: number; error: string }>;
|
|
259
232
|
}
|
|
260
233
|
|
|
@@ -282,64 +255,12 @@ const parallelProcedure: ParallelProcedure = defineProcedure({
|
|
|
282
255
|
tags: controlFlowTags("control-flow"),
|
|
283
256
|
},
|
|
284
257
|
handler: async (input: ParallelInput, ctx?: ProcedureContext): Promise<ParallelOutput> => {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// Operands arrive raw (exec() does not pre-hydrate control-flow procedures). Execute
|
|
293
|
-
// task refs concurrently, honoring `concurrency` and `failFast`, and report per-task
|
|
294
|
-
// errors. Other tasks are values: their refs run. See BUGS-2026-07 M35.
|
|
295
|
-
const runTask = async (task: unknown): Promise<unknown> => {
|
|
296
|
-
if (!ctx?.client) {
|
|
297
|
-
return task;
|
|
298
|
-
}
|
|
299
|
-
return evaluateOperand(task, ctx, { scope, extra: parentContext({ cwd, node }) });
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
const results: unknown[] = new Array(tasks.length);
|
|
303
|
-
const errors: Array<{ index: number; error: string }> = [];
|
|
304
|
-
|
|
305
|
-
const limit =
|
|
306
|
-
typeof concurrency === "number" && concurrency > 0
|
|
307
|
-
? Math.min(concurrency, tasks.length)
|
|
308
|
-
: tasks.length;
|
|
309
|
-
|
|
310
|
-
let nextIndex = 0;
|
|
311
|
-
let aborted = false;
|
|
312
|
-
|
|
313
|
-
const worker = async (): Promise<void> => {
|
|
314
|
-
while (!aborted) {
|
|
315
|
-
const index = nextIndex++;
|
|
316
|
-
if (index >= tasks.length) {
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
try {
|
|
320
|
-
results[index] = await runTask(tasks[index]);
|
|
321
|
-
} catch (error) {
|
|
322
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
323
|
-
if (failFast) {
|
|
324
|
-
aborted = true;
|
|
325
|
-
throw error;
|
|
326
|
-
}
|
|
327
|
-
errors.push({ index, error: message });
|
|
328
|
-
results[index] = undefined;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
const workerCount = Math.max(1, limit);
|
|
334
|
-
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
335
|
-
|
|
336
|
-
errors.sort((a, b) => a.index - b.index);
|
|
337
|
-
|
|
338
|
-
return {
|
|
339
|
-
results,
|
|
340
|
-
allSucceeded: errors.length === 0,
|
|
341
|
-
errors,
|
|
342
|
-
};
|
|
258
|
+
// Operands arrive raw (exec() does not pre-hydrate control-flow procedures). The runner of
|
|
259
|
+
// the kind client.parallel runs the tasks at the same time on the kernel, with `concurrency`
|
|
260
|
+
// and `failFast`, and reports the error of each task. See BUGS-2026-07 M35.
|
|
261
|
+
if (ctx?.client) return (await runControlFlow("parallel", input, ctx)) as ParallelOutput;
|
|
262
|
+
// Without a client, nothing can run: each task is its value
|
|
263
|
+
return { results: Array.from(input.tasks), allSucceeded: true, errors: [] };
|
|
343
264
|
},
|
|
344
265
|
});
|
|
345
266
|
|
|
@@ -367,38 +288,15 @@ const conditionalProcedure: ConditionalProcedure = defineProcedure({
|
|
|
367
288
|
tags: controlFlowTags("control-flow"),
|
|
368
289
|
},
|
|
369
290
|
handler: async (input: ConditionalInput, ctx?: ProcedureContext): Promise<unknown> => {
|
|
370
|
-
const { condition: rawCondition, then: thenValue, else: elseValue, ...parentInput } = input as ConditionalInput & {
|
|
371
|
-
cwd?: string;
|
|
372
|
-
node?: unknown;
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
// Extract context to propagate to branches (e.g., cwd, node)
|
|
376
|
-
const { cwd, node } = parentInput;
|
|
377
|
-
const scope = scopeOf(input);
|
|
378
|
-
|
|
379
291
|
// Operands arrive raw (exec() does not pre-hydrate control-flow procedures), so the
|
|
380
|
-
// condition runs
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (condition && typeof condition === "object" && "value" in condition) {
|
|
389
|
-
isTruthy = Boolean((condition as { value: unknown }).value);
|
|
390
|
-
} else {
|
|
391
|
-
isTruthy = Boolean(condition);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Select the branch to execute/return
|
|
395
|
-
const selectedBranch = isTruthy ? thenValue : elseValue;
|
|
396
|
-
|
|
397
|
-
// Only the selected branch runs. Without a client, the branch is the result as it is.
|
|
398
|
-
if (selectedBranch === undefined || !ctx?.client) {
|
|
399
|
-
return selectedBranch;
|
|
400
|
-
}
|
|
401
|
-
return evaluateOperand(selectedBranch, ctx, { scope, extra: parentContext({ cwd, node }) });
|
|
292
|
+
// condition runs in the runner of the kind client.conditional, before its truthiness is
|
|
293
|
+
// known, and only the selected branch runs. See BUGS-2026-07 H2.
|
|
294
|
+
if (ctx?.client) return runControlFlow("conditional", input, ctx);
|
|
295
|
+
// Without a client, the branch is the result as it is. The `value` field of an object decides
|
|
296
|
+
// (a predicate such as git.hasChanges gives { value }).
|
|
297
|
+
const { condition } = input;
|
|
298
|
+
const truthy = condition && typeof condition === "object" && "value" in condition ? Boolean((condition as { value: unknown }).value) : Boolean(condition);
|
|
299
|
+
return truthy ? input.then : input.else;
|
|
402
300
|
},
|
|
403
301
|
});
|
|
404
302
|
|
|
@@ -426,16 +324,17 @@ import {
|
|
|
426
324
|
* is the last value. (Deep dive CORE-1: before, the handler only tested the raw operands, and a
|
|
427
325
|
* ref is always truthy, so no operand ran.)
|
|
428
326
|
*/
|
|
429
|
-
function shortCircuit(stopOn: (value: unknown) => boolean) {
|
|
327
|
+
function shortCircuit(name: "and" | "or", stopOn: (value: unknown) => boolean) {
|
|
430
328
|
return async (input: { values: unknown[]; cwd?: string; node?: unknown }, ctx?: ProcedureContext): Promise<unknown> => {
|
|
431
329
|
const { values } = input;
|
|
432
330
|
if (!Array.isArray(values)) {
|
|
433
331
|
throw new Error(`values must be an array, got ${values === null ? "null" : typeof values}`);
|
|
434
332
|
}
|
|
435
|
-
|
|
333
|
+
// The runner of the kind client.and or client.or runs the operands in order on the kernel
|
|
334
|
+
if (ctx?.client) return runControlFlow(name, input, ctx);
|
|
436
335
|
let last: unknown = undefined;
|
|
437
336
|
for (const operand of values) {
|
|
438
|
-
last =
|
|
337
|
+
last = operand;
|
|
439
338
|
if (stopOn(last)) {
|
|
440
339
|
return last;
|
|
441
340
|
}
|
|
@@ -456,7 +355,7 @@ const andProcedure: AndProcedure = defineProcedure({
|
|
|
456
355
|
input: LogicInputSchema as any,
|
|
457
356
|
output: anyOutput("The first falsy operand value, or the last value"),
|
|
458
357
|
metadata: { ...andMetadata, tags: [...andMetadata.tags, RUNS_REFS_TAG] },
|
|
459
|
-
handler: shortCircuit((value) => !value),
|
|
358
|
+
handler: shortCircuit("and", (value) => !value),
|
|
460
359
|
});
|
|
461
360
|
|
|
462
361
|
interface OrInput {
|
|
@@ -471,7 +370,7 @@ const orProcedure: OrProcedure = defineProcedure({
|
|
|
471
370
|
input: LogicInputSchema as any,
|
|
472
371
|
output: anyOutput("The first truthy operand value, or the last value"),
|
|
473
372
|
metadata: { ...orMetadata, tags: [...orMetadata.tags, RUNS_REFS_TAG] },
|
|
474
|
-
handler: shortCircuit((value) => Boolean(value)),
|
|
373
|
+
handler: shortCircuit("or", (value) => Boolean(value)),
|
|
475
374
|
});
|
|
476
375
|
|
|
477
376
|
interface NotInput {
|
|
@@ -556,27 +455,6 @@ const MapOutputSchema = z.looseObject({
|
|
|
556
455
|
|
|
557
456
|
type MapProcedure = Procedure<MapInput, MapOutput, { description: string; tags: string[] }>;
|
|
558
457
|
|
|
559
|
-
/**
|
|
560
|
-
* This function runs the refs of an `items` operand. It runs each element separately and in
|
|
561
|
-
* order, so an array whose elements are all refs stays an array (hydration makes such an array
|
|
562
|
-
* a chain).
|
|
563
|
-
*/
|
|
564
|
-
async function hydrateItems(items: unknown, ctx: ProcedureContext | undefined, scope: RefScope | undefined): Promise<unknown[]> {
|
|
565
|
-
if (!ctx?.client) return Array.isArray(items) ? items : [];
|
|
566
|
-
let value: unknown;
|
|
567
|
-
if (Array.isArray(items)) {
|
|
568
|
-
const results: unknown[] = [];
|
|
569
|
-
for (const item of items) results.push(await hydrateOperand(item, ctx, scope));
|
|
570
|
-
value = results;
|
|
571
|
-
} else {
|
|
572
|
-
value = await hydrateOperand(items, ctx, scope);
|
|
573
|
-
}
|
|
574
|
-
if (!Array.isArray(value)) {
|
|
575
|
-
throw new Error(`items must be an array, got ${value === null ? "null" : typeof value}`);
|
|
576
|
-
}
|
|
577
|
-
return value;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
458
|
const mapProcedure: MapProcedure = defineProcedure({
|
|
581
459
|
path: ["map"],
|
|
582
460
|
input: MapInputSchema as any,
|
|
@@ -586,23 +464,15 @@ const mapProcedure: MapProcedure = defineProcedure({
|
|
|
586
464
|
tags: controlFlowTags("collection"),
|
|
587
465
|
},
|
|
588
466
|
handler: async (input: MapInput, ctx?: ProcedureContext): Promise<MapOutput> => {
|
|
589
|
-
// map is a control-flow procedure: its operands arrive raw, and
|
|
590
|
-
// Before, the handler only returned the raw items, so
|
|
591
|
-
// function was applied.
|
|
467
|
+
// map is a control-flow procedure: its operands arrive raw, and the runner of the kind
|
|
468
|
+
// client.map runs them on the kernel. Before, the handler only returned the raw items, so
|
|
469
|
+
// their refs never ran and no function was applied.
|
|
592
470
|
if (input.fn !== undefined && !isAnyProcedureRef(input.fn)) {
|
|
593
471
|
// Before, a fn that was not a call was ignored without a word (roadmap R4)
|
|
594
472
|
throw new Error("client.map: fn must be one call ({ $proc, input }), or absent");
|
|
595
473
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (!isAnyProcedureRef(input.fn) || !ctx?.client) {
|
|
599
|
-
return { results: items };
|
|
600
|
-
}
|
|
601
|
-
const results: unknown[] = [];
|
|
602
|
-
for (const [index, item] of items.entries()) {
|
|
603
|
-
results.push(await callOperand(input.fn, ctx, { scope: scopeWith({ [input.as ?? "item"]: item, index }, outer) }));
|
|
604
|
-
}
|
|
605
|
-
return { results };
|
|
474
|
+
if (ctx?.client) return (await runControlFlow("map", input, ctx)) as MapOutput;
|
|
475
|
+
return { results: Array.isArray(input.items) ? input.items : [] };
|
|
606
476
|
},
|
|
607
477
|
});
|
|
608
478
|
|
|
@@ -643,18 +513,10 @@ const reduceProcedure: ReduceProcedure = defineProcedure({
|
|
|
643
513
|
if (!ctx?.client) {
|
|
644
514
|
return input.accumulated ?? input.initial;
|
|
645
515
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
}
|
|
651
|
-
const outer = scopeOf(input);
|
|
652
|
-
const items = await hydrateItems(input.items, ctx, outer);
|
|
653
|
-
let acc = await hydrateOperand(input.initial, ctx, outer);
|
|
654
|
-
for (const [index, item] of items.entries()) {
|
|
655
|
-
acc = await callOperand(input.fn, ctx, { scope: scopeWith({ acc, item, index }, outer) });
|
|
656
|
-
}
|
|
657
|
-
return acc;
|
|
516
|
+
// The runner of the kind client.reduce runs the operands on the kernel. Without fn, the
|
|
517
|
+
// result is the accumulated value, hydrated like any operand. (Docs review DOC-4: before, a
|
|
518
|
+
// call in initial did not run, while map without fn hydrated its items.)
|
|
519
|
+
return runControlFlow("reduce", input, ctx);
|
|
658
520
|
},
|
|
659
521
|
});
|
|
660
522
|
|
|
@@ -752,7 +614,12 @@ interface TryCatchOutput {
|
|
|
752
614
|
success: boolean;
|
|
753
615
|
/** Result value */
|
|
754
616
|
value: unknown;
|
|
755
|
-
/**
|
|
617
|
+
/**
|
|
618
|
+
* Error if failed.
|
|
619
|
+
*
|
|
620
|
+
* The error text of a `try` that throws `undefined` is the reason of the kernel (D26). Before
|
|
621
|
+
* 0.26.0, it was `"undefined"`.
|
|
622
|
+
*/
|
|
756
623
|
error?: string;
|
|
757
624
|
}
|
|
758
625
|
|
|
@@ -773,25 +640,14 @@ const tryCatchProcedure: TryCatchProcedure = defineProcedure({
|
|
|
773
640
|
tags: controlFlowTags("control-flow"),
|
|
774
641
|
},
|
|
775
642
|
handler: async (input: TryCatchInput, ctx?: ProcedureContext): Promise<TryCatchOutput> => {
|
|
776
|
-
const { try: tryValue, catch: catchValue } = input;
|
|
777
|
-
|
|
778
643
|
// Without a client, nothing can run: `try` is the value
|
|
779
644
|
if (!ctx?.client) {
|
|
780
|
-
return { success: true, value:
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// Operands arrive raw (exec() does not pre-hydrate control-flow procedures), so the
|
|
784
|
-
// `try` operand runs HERE inside a real JS try/catch. If it throws, the `catch` operand
|
|
785
|
-
// runs (or its value is the result). See BUGS-2026-07 H3.
|
|
786
|
-
const scope = scopeOf(input);
|
|
787
|
-
try {
|
|
788
|
-
const value = await evaluateOperand(tryValue, ctx, { scope });
|
|
789
|
-
return { success: true, value };
|
|
790
|
-
} catch (error) {
|
|
791
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
792
|
-
const value = await evaluateOperand(catchValue, ctx, { scope: scopeWith({ error: message }, scope) });
|
|
793
|
-
return { success: false, value, error: message };
|
|
645
|
+
return { success: true, value: input.try };
|
|
794
646
|
}
|
|
647
|
+
// Operands arrive raw (exec() does not pre-hydrate control-flow procedures), so the runner of
|
|
648
|
+
// the kind client.tryCatch runs the `try` operand. If it fails, the `catch` operand runs with
|
|
649
|
+
// the name `error`, and its value is the result. See BUGS-2026-07 H3.
|
|
650
|
+
return (await runControlFlow("tryCatch", input, ctx)) as TryCatchOutput;
|
|
795
651
|
},
|
|
796
652
|
});
|
|
797
653
|
|