@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/procedures/ref.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Enables procedures-as-data: procedures can be passed as inputs to other procedures,
|
|
5
5
|
* composed declaratively via JSON, or imperatively via TypeScript.
|
|
6
6
|
*
|
|
7
|
+
* This module holds the `$proc` form of a program: its keys, its types, the builder, the JSON
|
|
8
|
+
* helpers, the scope that a raw input carries and the raw-input rules. The kernel of coeval runs
|
|
9
|
+
* a program: `program.ts` changes the `$proc` form into a run document (roadmap item M14).
|
|
10
|
+
*
|
|
7
11
|
* Key concepts:
|
|
8
12
|
* - PROCEDURE_SYMBOL: Symbol tag to identify procedure references in JSON/objects
|
|
9
13
|
* - ProcedureRef: A reference to a procedure with pre-bound input
|
|
@@ -11,8 +15,6 @@
|
|
|
11
15
|
* - $name: Names the result of a chain step, for `$ref`
|
|
12
16
|
* - $ref: Reads a name of the scope (a chain step, `$last`, `item`, `acc`, `index`, `input`)
|
|
13
17
|
* - $literal: Gives a value without hydration
|
|
14
|
-
* - hydrateInput: Walks input tree and executes nested procedure references
|
|
15
|
-
* - executeRef: Helper for procedures to execute deferred refs
|
|
16
18
|
* - proc(): Factory function to create procedure references
|
|
17
19
|
*
|
|
18
20
|
* ## Execution Control with $when
|
|
@@ -677,6 +679,10 @@ export interface HydrateOptions {
|
|
|
677
679
|
/**
|
|
678
680
|
* Whether sibling refs run at the same time (default: false). With false, the refs run one
|
|
679
681
|
* after the other, from left to right and depth first, so the order of their effects is known.
|
|
682
|
+
*
|
|
683
|
+
* With true, the refs operate with failFast (D26): after the first error, the other refs stop
|
|
684
|
+
* at their next wait. Before 0.26.0, they went on in the background (`Promise.all`). When their
|
|
685
|
+
* calls must start, give `parallel: false`.
|
|
680
686
|
*/
|
|
681
687
|
parallel?: boolean | undefined;
|
|
682
688
|
|
|
@@ -728,306 +734,9 @@ export interface HydrationEvent {
|
|
|
728
734
|
readonly consumer: AnyProcedureRef | undefined;
|
|
729
735
|
}
|
|
730
736
|
|
|
731
|
-
/** The observers of `observeHydration()`: they get the events of every hydration. */
|
|
732
|
-
const hydrationObservers = new Set<(event: HydrationEvent) => void>();
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* This function adds an observer of every hydration in this process (or worker), and gives the
|
|
736
|
-
* function that removes it. A host that does not call `hydrateInput` itself (the Composer of
|
|
737
|
-
* the site runs `client.exec()`) uses it for its trace. The observer gets the events of all
|
|
738
|
-
* concurrent hydrations: use it where one program runs at a time. For the events of one program,
|
|
739
|
-
* give `onRef` to that program: `client.exec(ref, input, { onRef })` (docs review DOC-10).
|
|
740
|
-
*/
|
|
741
|
-
export function observeHydration(observer: (event: HydrationEvent) => void): () => void {
|
|
742
|
-
hydrationObservers.add(observer);
|
|
743
|
-
return () => {
|
|
744
|
-
hydrationObservers.delete(observer);
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
|
|
748
737
|
/** The maximum nesting of plain data in an input. A deeper input is an error, not a stack overflow. */
|
|
749
738
|
export const MAX_DATA_DEPTH = 1000;
|
|
750
739
|
|
|
751
|
-
/** The objects on the path from the root of the input to a value, to find a cycle. */
|
|
752
|
-
interface Ancestors {
|
|
753
|
-
readonly value: object;
|
|
754
|
-
readonly up: Ancestors | undefined;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* Internal context for hydration.
|
|
759
|
-
*/
|
|
760
|
-
interface HydrateContext {
|
|
761
|
-
executor: RefExecutor;
|
|
762
|
-
maxDepth: number;
|
|
763
|
-
parallel: boolean;
|
|
764
|
-
contextStack: string[];
|
|
765
|
-
scope?: RefScope | undefined;
|
|
766
|
-
rawInput: (path: ProcedurePath) => boolean;
|
|
767
|
-
/** Prevent recursive implicit chain detection */
|
|
768
|
-
skipImplicitChain: boolean;
|
|
769
|
-
/** Inside a deferred ref, a `$ref` name that is not known yet stays for the runner. */
|
|
770
|
-
deferred: boolean;
|
|
771
|
-
/** The `onRef` function of the options */
|
|
772
|
-
onRef: ((event: HydrationEvent) => void) | undefined;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/**
|
|
776
|
-
* Hydrate an input tree by executing any nested procedure references.
|
|
777
|
-
*
|
|
778
|
-
* Walks the input object tree and replaces any ProcedureRef or ProcedureRefJson
|
|
779
|
-
* objects with the result of executing that procedure, respecting $when timing.
|
|
780
|
-
*
|
|
781
|
-
* The rules:
|
|
782
|
-
* - A ref with `$when: "$immediate"` (the default) runs. A ref whose procedure takes its input
|
|
783
|
-
* raw (see `HydrateOptions.rawInput`) gets its input unchanged, with the scope attached.
|
|
784
|
-
* - A ref with `$when: "$never"` stays as it is, with its whole subtree.
|
|
785
|
-
* - A ref with `$when: "$parent"` or `$when: "<name>"` stays data for its runner. Its procedure
|
|
786
|
-
* gets that ref later. The immediate refs of its input run now, unless the procedure takes its
|
|
787
|
-
* input raw. A named ref runs now only when `contextStack` has the name.
|
|
788
|
-
* - A `$ref` object gives a value from the scope. `{ $literal: value }` gives the value unchanged.
|
|
789
|
-
* - An array whose items are all refs that run now, outside the input of a ref, runs as one
|
|
790
|
-
* `client.chain`. An array with a deferred ref is a list.
|
|
791
|
-
* - Values that are not plain data (`Date`, `Map`, typed arrays, class instances) stay unchanged.
|
|
792
|
-
* A subtree without refs stays the same object.
|
|
793
|
-
*
|
|
794
|
-
* @param input - The input object potentially containing procedure references
|
|
795
|
-
* @param executor - Function to execute procedure references
|
|
796
|
-
* @param options - Hydration options
|
|
797
|
-
* @returns The hydrated input with executed procedure refs replaced by their results
|
|
798
|
-
*
|
|
799
|
-
* @example
|
|
800
|
-
* ```typescript
|
|
801
|
-
* const input = {
|
|
802
|
-
* visit: proc(["git", "add"]).input({ all: true }).build(),
|
|
803
|
-
* config: { nested: true },
|
|
804
|
-
* };
|
|
805
|
-
*
|
|
806
|
-
* const hydrated = await hydrateInput(input, async (path, input) => {
|
|
807
|
-
* return await client.call({ path, input });
|
|
808
|
-
* });
|
|
809
|
-
*
|
|
810
|
-
* // hydrated.visit is now the result of executing git.add
|
|
811
|
-
* ```
|
|
812
|
-
*/
|
|
813
|
-
export async function hydrateInput<T>(
|
|
814
|
-
input: T,
|
|
815
|
-
executor: RefExecutor,
|
|
816
|
-
options: HydrateOptions = {}
|
|
817
|
-
): Promise<T> {
|
|
818
|
-
const ctx: HydrateContext = {
|
|
819
|
-
executor,
|
|
820
|
-
maxDepth: options.maxDepth ?? 10,
|
|
821
|
-
parallel: options.parallel ?? false,
|
|
822
|
-
contextStack: options.contextStack ?? [],
|
|
823
|
-
scope: options.scope,
|
|
824
|
-
rawInput: options.rawInput ?? isControlFlowPath,
|
|
825
|
-
skipImplicitChain: false,
|
|
826
|
-
deferred: false,
|
|
827
|
-
onRef: options.onRef,
|
|
828
|
-
};
|
|
829
|
-
return hydrateValue(input, ctx, 0, undefined, 0);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* The path and the input of the root call of a program: the call that `client.exec()` starts.
|
|
834
|
-
* `exec()` and the body of a procedure that `procedure.define` made both use this function, so
|
|
835
|
-
* the same program gives the same result in both (docs review DOC-3: before, a defined body
|
|
836
|
-
* hydrated its root call as a nested ref).
|
|
837
|
-
*
|
|
838
|
-
* The rules of the root call:
|
|
839
|
-
* - A `$when` on the root call does not apply: the caller starts the root call now.
|
|
840
|
-
* - A procedure that takes its input raw (`rawInput`) gets its input unchanged, with the scope
|
|
841
|
-
* attached, and runs its operands itself.
|
|
842
|
-
* - Another procedure gets its input hydrated as a root: a list of calls that run now is an
|
|
843
|
-
* implicit chain, and the nested refs run.
|
|
844
|
-
*
|
|
845
|
-
* The caller then starts the call with the path and the input.
|
|
846
|
-
*/
|
|
847
|
-
export async function prepareRootCall(
|
|
848
|
-
ref: AnyProcedureRef,
|
|
849
|
-
executor: RefExecutor,
|
|
850
|
-
options: Pick<HydrateOptions, "rawInput" | "onRef" | "scope"> = {}
|
|
851
|
-
): Promise<{ path: ProcedurePath; input: unknown }> {
|
|
852
|
-
const root = normalizeRef(ref);
|
|
853
|
-
const rawInput = options.rawInput ?? isControlFlowPath;
|
|
854
|
-
if (rawInput(root.path)) {
|
|
855
|
-
return { path: root.path, input: withScope(root.input, options.scope) };
|
|
856
|
-
}
|
|
857
|
-
const input = await hydrateInput(root.input, executor, { rawInput, onRef: options.onRef, scope: options.scope });
|
|
858
|
-
return { path: root.path, input };
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
/**
|
|
862
|
-
* This function tells the observers that a ref runs now. The consumer is the nearest ref
|
|
863
|
-
* around the ref. An observer that throws does not change the hydration.
|
|
864
|
-
*/
|
|
865
|
-
function notifyRef(ctx: HydrateContext, ref: AnyProcedureRef, path: ProcedurePath, ancestors: Ancestors | undefined): void {
|
|
866
|
-
if (!ctx.onRef && hydrationObservers.size === 0) return;
|
|
867
|
-
let consumer: AnyProcedureRef | undefined;
|
|
868
|
-
for (let current = ancestors; current; current = current.up) {
|
|
869
|
-
if (isAnyProcedureRef(current.value)) {
|
|
870
|
-
consumer = current.value;
|
|
871
|
-
break;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
const event: HydrationEvent = { path, ref, consumer };
|
|
875
|
-
for (const observer of ctx.onRef ? [ctx.onRef, ...hydrationObservers] : hydrationObservers) {
|
|
876
|
-
try {
|
|
877
|
-
observer(event);
|
|
878
|
-
} catch {
|
|
879
|
-
// A trace must not change the program
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
/** The values of `fn` for each item: one after the other, or at the same time. */
|
|
885
|
-
async function eachValue<T>(items: readonly T[], fn: (item: T) => Promise<unknown>, parallel: boolean): Promise<unknown[]> {
|
|
886
|
-
if (parallel) return Promise.all(items.map(fn));
|
|
887
|
-
const results: unknown[] = [];
|
|
888
|
-
for (const item of items) results.push(await fn(item));
|
|
889
|
-
return results;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
/** This function resolves a `$ref` in the scope of the hydration. */
|
|
893
|
-
function resolveRefValue(value: OutputRef, ctx: HydrateContext): unknown {
|
|
894
|
-
// No scope: the object is data (for example a JSON Schema reference), or a later scope binds it
|
|
895
|
-
if (!ctx.scope) return value;
|
|
896
|
-
const { found, value: resolved } = lookupOutputRef(value.$ref, ctx.scope);
|
|
897
|
-
if (found) return resolved;
|
|
898
|
-
// Inside a deferred ref, the runner can bind the name later
|
|
899
|
-
if (ctx.deferred) return value;
|
|
900
|
-
const name = value.$ref.split(".")[0];
|
|
901
|
-
throw new Error(
|
|
902
|
-
`Unknown $ref name "${name}" in "${value.$ref}": no step, item or input has this name here. ` +
|
|
903
|
-
`For a literal $ref object, write { "$literal": { "$ref": ... } }.`
|
|
904
|
-
);
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
/**
|
|
908
|
-
* Internal recursive hydration function.
|
|
909
|
-
*
|
|
910
|
-
* `refDepth` counts the refs around the value. `dataDepth` counts all levels.
|
|
911
|
-
*/
|
|
912
|
-
async function hydrateValue<T>(
|
|
913
|
-
value: T,
|
|
914
|
-
ctx: HydrateContext,
|
|
915
|
-
refDepth: number,
|
|
916
|
-
ancestors: Ancestors | undefined,
|
|
917
|
-
dataDepth: number
|
|
918
|
-
): Promise<T> {
|
|
919
|
-
// Handle null/undefined/primitives
|
|
920
|
-
if (value === null || typeof value !== "object") {
|
|
921
|
-
return value;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (dataDepth > MAX_DATA_DEPTH) {
|
|
925
|
-
throw new Error(`Hydration failed: the input has more than ${MAX_DATA_DEPTH} levels of nesting`);
|
|
926
|
-
}
|
|
927
|
-
for (let current = ancestors; current; current = current.up) {
|
|
928
|
-
if (current.value === value) {
|
|
929
|
-
throw new Error("Hydration failed: the input is cyclic (an object contains itself)");
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// { $literal: value } gives the value without hydration
|
|
934
|
-
if (isLiteral(value)) {
|
|
935
|
-
return value.$literal as T;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// Check if this is an output reference ($ref)
|
|
939
|
-
if (isOutputRef(value)) {
|
|
940
|
-
return resolveRefValue(value, ctx) as T;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// Check if this is a procedure reference ($proc)
|
|
944
|
-
if (isAnyProcedureRef(value)) {
|
|
945
|
-
return hydrateRef(value, ctx, refDepth, ancestors, dataDepth) as Promise<T>;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
const inner: Ancestors = { value, up: ancestors };
|
|
949
|
-
|
|
950
|
-
// Handle arrays
|
|
951
|
-
if (Array.isArray(value)) {
|
|
952
|
-
// An array of procedure refs that all run now is an implicit chain. A deferred ref ($never,
|
|
953
|
-
// $parent, a context name that the runner did not give) is data, so a list with one is a list
|
|
954
|
-
// (docs review DOC-1: before, a list of only $never refs ran as a chain). Inside an implicit
|
|
955
|
-
// chain, the steps do not make another one.
|
|
956
|
-
if (
|
|
957
|
-
!ctx.skipImplicitChain &&
|
|
958
|
-
value.length > 0 &&
|
|
959
|
-
value.every((item) => isAnyProcedureRef(item) && shouldExecuteRef(item, ctx.contextStack, false))
|
|
960
|
-
) {
|
|
961
|
-
// client.chain runs the steps in order: they reach it raw. (Before, hydration ran the steps
|
|
962
|
-
// all at once, and client.chain got their results: deep dive, architecture review 3.4.)
|
|
963
|
-
const implicitChain: ProcedureRefJson = { $proc: ["client", "chain"], input: { steps: value } };
|
|
964
|
-
return hydrateRef(implicitChain, { ...ctx, skipImplicitChain: true }, refDepth, ancestors, dataDepth) as Promise<T>;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
const items = await eachValue(value, (item) => hydrateValue(item, ctx, refDepth, inner, dataDepth + 1), ctx.parallel);
|
|
968
|
-
return (items.every((item, index) => item === value[index]) ? value : items) as T;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// Values that are not plain data (Date, Map, typed arrays, class instances) stay unchanged
|
|
972
|
-
if (!isPlainObject(value)) {
|
|
973
|
-
return value;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// Handle plain objects
|
|
977
|
-
const entries = Object.entries(value);
|
|
978
|
-
const values = await eachValue(entries, ([, val]) => hydrateValue(val, ctx, refDepth, inner, dataDepth + 1), ctx.parallel);
|
|
979
|
-
if (values.every((val, index) => val === entries[index]![1])) {
|
|
980
|
-
return value;
|
|
981
|
-
}
|
|
982
|
-
// Object.fromEntries makes own properties, also for a key such as "__proto__"
|
|
983
|
-
return Object.fromEntries(entries.map(([key], index) => [key, values[index]])) as T;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/** This function hydrates one procedure ref: it runs the ref, or it keeps the ref as data. */
|
|
987
|
-
async function hydrateRef(
|
|
988
|
-
value: AnyProcedureRef,
|
|
989
|
-
ctx: HydrateContext,
|
|
990
|
-
refDepth: number,
|
|
991
|
-
ancestors: Ancestors | undefined,
|
|
992
|
-
dataDepth: number
|
|
993
|
-
): Promise<unknown> {
|
|
994
|
-
const when = getRefWhen(value);
|
|
995
|
-
|
|
996
|
-
// $never: pure data, with its whole subtree (deep dive CORE-11: before, its input was hydrated)
|
|
997
|
-
if (when === WHEN_NEVER) {
|
|
998
|
-
return value;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
if (refDepth >= ctx.maxDepth) {
|
|
1002
|
-
throw new Error(`Hydration depth exceeded maximum of ${ctx.maxDepth}`);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
const ref = normalizeRef(value);
|
|
1006
|
-
const raw = ctx.rawInput(ref.path);
|
|
1007
|
-
const inner: Ancestors = { value, up: ancestors };
|
|
1008
|
-
// The input of a ref is never an implicit chain
|
|
1009
|
-
const inputCtx = (deferred: boolean): HydrateContext => ({ ...ctx, skipImplicitChain: true, deferred });
|
|
1010
|
-
|
|
1011
|
-
// A ref that waits for its runner ($parent, or a name that the runner did not give) is data.
|
|
1012
|
-
// Its own $name is not a context here: hydration of its input does not run refs that wait for
|
|
1013
|
-
// it, at any depth (deep dive CORE-11: before, this depended on the depth of the ref).
|
|
1014
|
-
if (!shouldExecuteRef(value, ctx.contextStack, false)) {
|
|
1015
|
-
if (raw) return value;
|
|
1016
|
-
const input = await hydrateValue(ref.input, inputCtx(true), refDepth + 1, inner, dataDepth + 1);
|
|
1017
|
-
return input === ref.input ? value : { ...value, input };
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// A procedure that takes its input raw gets it unchanged, with the scope of the caller
|
|
1021
|
-
if (raw) {
|
|
1022
|
-
notifyRef(ctx, value, ref.path, ancestors);
|
|
1023
|
-
return ctx.executor(ref.path, withScope(ref.input, ctx.scope));
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
const input = await hydrateValue(ref.input, inputCtx(false), refDepth + 1, inner, dataDepth + 1);
|
|
1027
|
-
notifyRef(ctx, value, ref.path, ancestors);
|
|
1028
|
-
return ctx.executor(ref.path, input);
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
740
|
// =============================================================================
|
|
1032
741
|
// Control-Flow Procedures
|
|
1033
742
|
// =============================================================================
|
|
@@ -1129,64 +838,21 @@ export function takesRawInput(procedure: ProcedureShape): boolean {
|
|
|
1129
838
|
export function rawInputRule(
|
|
1130
839
|
registry: { get(path: ProcedurePath): ProcedureShape | undefined } | undefined
|
|
1131
840
|
): (path: ProcedurePath) => boolean {
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
/**
|
|
1143
|
-
* Execute a deferred procedure reference.
|
|
1144
|
-
*
|
|
1145
|
-
* This is a helper for procedures that receive deferred refs (via $when)
|
|
1146
|
-
* and need to execute them with additional context.
|
|
1147
|
-
*
|
|
1148
|
-
* @param ref - The procedure reference to execute
|
|
1149
|
-
* @param executor - Function to execute the procedure
|
|
1150
|
-
* @param additionalInput - Additional input to merge (e.g., cwd for dag.traverse)
|
|
1151
|
-
* @param options - Hydration options for the input of the ref (for example the runner's `contextStack`)
|
|
1152
|
-
* @returns The result of executing the procedure
|
|
1153
|
-
*
|
|
1154
|
-
* @example
|
|
1155
|
-
* ```typescript
|
|
1156
|
-
* // In dag.traverse, execute a deferred ref with cwd
|
|
1157
|
-
* const result = await executeRef(
|
|
1158
|
-
* input.visit,
|
|
1159
|
-
* (path, input) => ctx.client.call(path, input),
|
|
1160
|
-
* { cwd: node.repoPath }
|
|
1161
|
-
* );
|
|
1162
|
-
* ```
|
|
1163
|
-
*/
|
|
1164
|
-
export async function executeRef<TOutput = unknown>(
|
|
1165
|
-
ref: AnyProcedureRef,
|
|
1166
|
-
executor: RefExecutor,
|
|
1167
|
-
additionalInput?: Record<string, unknown>,
|
|
1168
|
-
options: HydrateOptions = {}
|
|
1169
|
-
): Promise<TOutput> {
|
|
1170
|
-
const normalized = normalizeRef(ref);
|
|
1171
|
-
const baseInput = typeof normalized.input === "object" ? normalized.input : {};
|
|
1172
|
-
|
|
1173
|
-
// Merge additional input
|
|
1174
|
-
const mergedInput = {
|
|
1175
|
-
...(baseInput as Record<string, unknown>),
|
|
1176
|
-
...additionalInput,
|
|
1177
|
-
};
|
|
1178
|
-
|
|
1179
|
-
const raw = (options.rawInput ?? isControlFlowPath)(normalized.path);
|
|
1180
|
-
if (raw) {
|
|
1181
|
-
return executor(normalized.path, withScope(mergedInput, options.scope));
|
|
841
|
+
if (!registry) return isControlFlowPath;
|
|
842
|
+
// One rule for each registry: the rule reads the registry at each call, so it stays current
|
|
843
|
+
let rule = RAW_INPUT_RULES.get(registry);
|
|
844
|
+
if (!rule) {
|
|
845
|
+
rule = (path) => {
|
|
846
|
+
const procedure = registry.get(path);
|
|
847
|
+
return procedure ? takesRawInput(procedure) : isControlFlowPath(path);
|
|
848
|
+
};
|
|
849
|
+
RAW_INPUT_RULES.set(registry, rule);
|
|
1182
850
|
}
|
|
1183
|
-
|
|
1184
|
-
// Hydrate the merged input (execute any nested $immediate refs)
|
|
1185
|
-
const hydratedInput = await hydrateInput(mergedInput, executor, options);
|
|
1186
|
-
|
|
1187
|
-
return executor(normalized.path, hydratedInput);
|
|
851
|
+
return rule;
|
|
1188
852
|
}
|
|
1189
853
|
|
|
854
|
+
const RAW_INPUT_RULES = new WeakMap<object, (path: ProcedurePath) => boolean>();
|
|
855
|
+
|
|
1190
856
|
// =============================================================================
|
|
1191
857
|
// Template Extraction
|
|
1192
858
|
// =============================================================================
|
package/src/protocol.ts
CHANGED
|
@@ -101,6 +101,25 @@ export function errorCodeOf(error: unknown): string {
|
|
|
101
101
|
return typeof code === "string" ? code : "HANDLER_ERROR";
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* The retryable flag of an error that ends a call, on each face (`WIRE-PROTOCOL.md`, section
|
|
106
|
+
* "Error codes"). `items` is the count of the items that went to the caller before the error.
|
|
107
|
+
*
|
|
108
|
+
* - An error with its own boolean flag `retryable` keeps it.
|
|
109
|
+
* - Without a flag, an error before the first item gets the flag of its code in `ERROR_CODES`, or
|
|
110
|
+
* false. This applies before the stream starts (CLI-48) and to an in-band end before the first
|
|
111
|
+
* item (CLI-68). Thus `UNAVAILABLE` is retryable there.
|
|
112
|
+
* - Without a flag, an error after the first item gets false. The reader has items, and a retry
|
|
113
|
+
* can send them again (CLI-58).
|
|
114
|
+
*/
|
|
115
|
+
export function retryableOf(error: unknown, items: number): boolean {
|
|
116
|
+
const own = (error as { retryable?: unknown } | null | undefined)?.retryable;
|
|
117
|
+
if (typeof own === "boolean") return own;
|
|
118
|
+
if (items > 0) return false;
|
|
119
|
+
const code = errorCodeOf(error);
|
|
120
|
+
return isProtocolErrorCode(code) ? ERROR_CODES[code].retryable : false;
|
|
121
|
+
}
|
|
122
|
+
|
|
104
123
|
/** The codes that a server before CLI-4 gave, and the code and the reason of wire v1 for each. */
|
|
105
124
|
const FOLDED_ERROR_CODES: ReadonlyMap<string, { readonly code: ProtocolErrorCode; readonly reason?: string }> = new Map([
|
|
106
125
|
["SERVER_STOPPED", { code: "UNAVAILABLE" }],
|
|
@@ -28,7 +28,7 @@ import { optionalMs, type Duration } from "../duration.js";
|
|
|
28
28
|
import type { CollectionStorage } from "../procedures/collection/storage.js";
|
|
29
29
|
import { TIMER_DEFAULTS } from "../timers.js";
|
|
30
30
|
import { realClock, type Clock } from "../clock.js";
|
|
31
|
-
import {
|
|
31
|
+
import { errorCodeOf, retryableOf } from "../protocol.js";
|
|
32
32
|
import { EventPublisher, type EventPublisherOptions } from "../events/publisher.js";
|
|
33
33
|
import { getGlobalEventBus } from "../events/bus.js";
|
|
34
34
|
import type { CollectionProceduresOptions } from "../procedures/collection/procedures.js";
|
|
@@ -301,6 +301,24 @@ async function* releasedAtEnd<T>(items: AsyncIterable<T>, release: () => void):
|
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
/**
|
|
305
|
+
* The items of the stream of a keyed call. When the stream ends with a retryable error, `forget`
|
|
306
|
+
* runs before the error goes on: the next try with the key then runs again, as after a retryable
|
|
307
|
+
* error response (CLI-68). The flag is the flag of the in-band end on each face (`retryableOf`).
|
|
308
|
+
*/
|
|
309
|
+
async function* forgottenAtRetryableEnd<T>(items: AsyncIterable<T>, forget: () => void): AsyncGenerator<T, void, undefined> {
|
|
310
|
+
let count = 0;
|
|
311
|
+
try {
|
|
312
|
+
for await (const item of items) {
|
|
313
|
+
count++;
|
|
314
|
+
yield item;
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (retryableOf(error, count)) forget();
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
304
322
|
/** The request of credentials without a call: `admits()` and `identify()` give it to the checks of each call. */
|
|
305
323
|
function credentialsRequest(id: string, credentials: Credentials): ServerRequest<unknown> {
|
|
306
324
|
return { id, method: { service: "", operation: "" }, payload: undefined, metadata: {}, credentials };
|
|
@@ -355,17 +373,11 @@ function withRequestTimeout(
|
|
|
355
373
|
* (VALIDATION_ERROR, NOT_EXPOSED, ABORTED and so on). An error of the handler keeps its own code
|
|
356
374
|
* when it is a string, as for a stream (`WIRE-PROTOCOL.md`). Without a code, it is HANDLER_ERROR.
|
|
357
375
|
* An error without a retryable flag gets the flag of its code in `ERROR_CODES`, or false (CLI-48).
|
|
358
|
-
*
|
|
359
|
-
* (CLI-58, `WIRE-PROTOCOL.md`,
|
|
360
|
-
*
|
|
361
|
-
* Deprecated (D13): the flag false for an in-band end without a flag before the first item. From
|
|
362
|
-
* the next MINOR of the client group (CLI-68), that end gets the flag of its code, as this response
|
|
363
|
-
* does. After the first item, the flag stays false. This response does not change.
|
|
376
|
+
* An in-band end before the first item of a stream gets the same default. After the first item,
|
|
377
|
+
* an in-band end without a flag gets false (`retryableOf`, CLI-58, CLI-68, `WIRE-PROTOCOL.md`,
|
|
378
|
+
* section "Error codes").
|
|
364
379
|
*/
|
|
365
380
|
export function errorResponse(id: string, error: unknown): ServerResponse<unknown> {
|
|
366
|
-
// An error that says it is retryable stays retryable: before, every error lost the flag
|
|
367
|
-
// (deep dive TRN-7)
|
|
368
|
-
const retryable = (error as { retryable?: unknown } | undefined)?.retryable;
|
|
369
381
|
// A refusal of the capability says why (CAPABILITIES-2026-10)
|
|
370
382
|
const reason = error instanceof InvocationError ? error.reason : undefined;
|
|
371
383
|
// An error can ask the client to wait before a retry: retryAfterMs, in milliseconds (R12)
|
|
@@ -382,8 +394,10 @@ export function errorResponse(id: string, error: unknown): ServerResponse<unknow
|
|
|
382
394
|
type: "error",
|
|
383
395
|
code,
|
|
384
396
|
message: error instanceof Error ? error.message : String(error),
|
|
385
|
-
//
|
|
386
|
-
|
|
397
|
+
// An error that says it is retryable stays retryable: before, every error lost the flag (deep
|
|
398
|
+
// dive TRN-7). Without a flag, the default of the code: UNAVAILABLE stays retryable, and
|
|
399
|
+
// keeps HTTP 503 (CLI-48). No item went out before this response.
|
|
400
|
+
retryable: retryableOf(error, 0),
|
|
387
401
|
},
|
|
388
402
|
metadata,
|
|
389
403
|
};
|
|
@@ -1047,7 +1061,12 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
1047
1061
|
// A retryable error lets the next try run again: the handler said that a retry is safe
|
|
1048
1062
|
(response) => !(response.status.type === "error" && response.status.retryable),
|
|
1049
1063
|
);
|
|
1050
|
-
if (first)
|
|
1064
|
+
if (first) {
|
|
1065
|
+
// A stream that ends with a retryable error lets the next try run again too. Thus the retry
|
|
1066
|
+
// of a stream that fails before its first item runs the procedure again (CLI-68).
|
|
1067
|
+
const forget = (): void => this.idempotency!.forget(cacheKey, result);
|
|
1068
|
+
return result.then((response) => (response.stream ? { ...response, stream: forgottenAtRetryableEnd(response.stream, forget) } : response));
|
|
1069
|
+
}
|
|
1051
1070
|
const response = await untilAborted(result, request.signal);
|
|
1052
1071
|
if (response === undefined) return errorResponse(request.id, new InvocationError("ABORTED", "The request was aborted", procedure.path));
|
|
1053
1072
|
if (response.stream) {
|