@webpieces/core-util 0.4.956 → 0.4.957

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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/src/http/ApiMethodInfo.d.ts +14 -0
  3. package/src/http/ApiMethodInfo.js +3 -0
  4. package/src/http/ApiMethodInfo.js.map +1 -1
  5. package/src/http/CallRegistry.d.ts +5 -10
  6. package/src/http/CallRegistry.js +21 -43
  7. package/src/http/CallRegistry.js.map +1 -1
  8. package/src/http/CallRegistryCompileAssertions.js +1 -1
  9. package/src/http/CallRegistryCompileAssertions.js.map +1 -1
  10. package/src/http/CallScope.d.ts +33 -0
  11. package/src/http/CallScope.js +70 -0
  12. package/src/http/CallScope.js.map +1 -0
  13. package/src/http/CallStrategy.d.ts +13 -1
  14. package/src/http/CallStrategy.js +4 -1
  15. package/src/http/CallStrategy.js.map +1 -1
  16. package/src/http/ClientRegistry.d.ts +54 -58
  17. package/src/http/ClientRegistry.js +72 -73
  18. package/src/http/ClientRegistry.js.map +1 -1
  19. package/src/http/ErrorClassifier.d.ts +20 -27
  20. package/src/http/ErrorClassifier.js +1 -15
  21. package/src/http/ErrorClassifier.js.map +1 -1
  22. package/src/http/ErrorLadderCompileAssertions.d.ts +1 -0
  23. package/src/http/ErrorLadderCompileAssertions.js +42 -0
  24. package/src/http/ErrorLadderCompileAssertions.js.map +1 -0
  25. package/src/http/ErrorTranslator.d.ts +8 -4
  26. package/src/http/ErrorTranslator.js.map +1 -1
  27. package/src/http/LogApiCall.js +2 -2
  28. package/src/http/LogApiCall.js.map +1 -1
  29. package/src/http/RetryStrategy.js +1 -1
  30. package/src/http/RetryStrategy.js.map +1 -1
  31. package/src/http/StreamErrorControl.d.ts +9 -1
  32. package/src/http/StreamErrorControl.js +14 -3
  33. package/src/http/StreamErrorControl.js.map +1 -1
  34. package/src/http/WebpiecesDefaultErrorClassifier.d.ts +2 -2
  35. package/src/http/WebpiecesDefaultErrorClassifier.js +2 -2
  36. package/src/http/WebpiecesDefaultErrorClassifier.js.map +1 -1
  37. package/src/http/WebpiecesDefaultErrorTranslator.d.ts +2 -1
  38. package/src/http/WebpiecesDefaultErrorTranslator.js +2 -1
  39. package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
  40. package/src/index.d.ts +3 -2
  41. package/src/index.js +3 -2
  42. package/src/index.js.map +1 -1
  43. package/src/ipc/IpcConnection.js +1 -1
  44. package/src/ipc/IpcConnection.js.map +1 -1
  45. package/src/ipc/IpcLogging.js +1 -1
  46. package/src/ipc/IpcLogging.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/core-util",
3
- "version": "0.4.956",
3
+ "version": "0.4.957",
4
4
  "description": "Utility functions for WebPieces - works in browser and Node.js",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -48,12 +48,26 @@ export declare class ApiMethodInfo {
48
48
  * whole call identity travels together. webpieces itself does not act on it; an app's upgrade
49
49
  * logic does. Default false. */
50
50
  readonly allowUpgradeInFlight: boolean;
51
+ /**
52
+ * The contract CLASS (the `@ApiPath` contract, or an SDK-wrapping api-client's abstract
53
+ * `XxxApi`) — the key {@link ClientRegistry.classify} resolves the `CallScope` ladder on
54
+ * (method -> API -> ALL). {@link apiClass} stays the LOGGED name. REQUIRED as a constructor
55
+ * argument so every caller states it: an SDK-wrapping client passes its abstract `XxxApi`, and
56
+ * only a call with no contract class at all (an IPC hop, an MCP protocol step) passes an explicit
57
+ * `undefined`, which reaches only the ALL rung.
58
+ *
59
+ * NON-enumerable on purpose: this object is nested into every `[API-*]` log record as
60
+ * `jsonPayload.api.method`, and a class reference is not log data.
61
+ */
62
+ readonly contract: Function | undefined;
51
63
  constructor(side: ApiSide,
52
64
  /** REQUIRED — the API CONTRACT class name (e.g. 'SaveApi'). Matches client + server so both
53
65
  * sides of one logical call filter together via `jsonPayload.api.method.apiClass`. */
54
66
  apiClass: string,
55
67
  /** REQUIRED — the method on the contract (e.g. 'save'). */
56
68
  methodName: string,
69
+ /** REQUIRED — the contract CLASS, or explicit `undefined`; see {@link ApiMethodInfo.contract}. */
70
+ contract: Function | undefined,
57
71
  /** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no
58
72
  * impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */
59
73
  controllerName?: string | undefined,
@@ -34,6 +34,8 @@ class ApiMethodInfo {
34
34
  apiClass,
35
35
  /** REQUIRED — the method on the contract (e.g. 'save'). */
36
36
  methodName,
37
+ /** REQUIRED — the contract CLASS, or explicit `undefined`; see {@link ApiMethodInfo.contract}. */
38
+ contract,
37
39
  /** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no
38
40
  * impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */
39
41
  controllerName,
@@ -64,6 +66,7 @@ class ApiMethodInfo {
64
66
  this.noLogging = noLogging;
65
67
  this.hideProgress = hideProgress;
66
68
  this.allowUpgradeInFlight = allowUpgradeInFlight;
69
+ Object.defineProperty(this, 'contract', { value: contract, enumerable: false });
67
70
  }
68
71
  }
69
72
  exports.ApiMethodInfo = ApiMethodInfo;
@@ -1 +1 @@
1
- {"version":3,"file":"ApiMethodInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiMethodInfo.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;AAOH,MAAa,aAAa;IAET;IAGA;IAEA;IAGA;IAMA;IAKA;IAIA;IAIA;IA5Bb,YACa,IAAa;IACtB;2FACuF;IAC9E,QAAgB;IACzB,2DAA2D;IAClD,UAAkB;IAC3B;gGAC4F;IACnF,cAAuB;IAChC;;;;wGAIoG;IAC3F,IAAe;IACxB;;;sGAGkG;IACzF,YAAqB,KAAK;IACnC;;+BAE2B;IAClB,eAAwB,KAAK;IACtC;;qCAEiC;IACxB,uBAAgC,KAAK;QA3BrC,SAAI,GAAJ,IAAI,CAAS;QAGb,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,eAAU,GAAV,UAAU,CAAQ;QAGlB,mBAAc,GAAd,cAAc,CAAS;QAMvB,SAAI,GAAJ,IAAI,CAAW;QAKf,cAAS,GAAT,SAAS,CAAiB;QAI1B,iBAAY,GAAZ,YAAY,CAAiB;QAI7B,yBAAoB,GAApB,oBAAoB,CAAiB;IAC/C,CAAC;CACP;AA/BD,sCA+BC","sourcesContent":["/**\n * ApiMethodInfo - the transport-neutral identity of a single API method call, handed to\n * {@link LogApiCallImpl.execute} by every caller (server inbound, http/in-process clients, cloud tasks,\n * and external wrapped clients like the firestore admin client).\n *\n * WHY generic (not RouteMetadata): LogApiCall runs deep in the stack over MANY shapes — HTTP routes,\n * pubsub/queue enqueues, and multi-param external clients returning Promise<void>/Promise<unknown>.\n * None of those own an `httpMethod`/`path`, so LogApiCall must not depend on the HTTP-shaped\n * `RouteMetadata`. This carries only what identifies the call.\n *\n * MATCHING is the point of {@link apiClass}: a CLIENT call and the SERVER handler for the same logical\n * method must log the SAME identity so `jsonPayload.api.method.apiClass=\"SaveApi\"` filters both sides\n * together. The API CONTRACT class name (e.g. 'SaveApi') is available on both sides — the client only\n * ever knows it, and the server carries it alongside its impl name — so it, not the server's impl\n * class, is the required key.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n */\n\nimport {MaskSpec} from \"./LogFieldMask\";\n\n/** Which end of the exchange this process is: the caller ('client') or the handler ('server'). */\nexport type ApiSide = 'client' | 'server';\n\nexport class ApiMethodInfo {\n constructor(\n readonly side: ApiSide,\n /** REQUIRED — the API CONTRACT class name (e.g. 'SaveApi'). Matches client + server so both\n * sides of one logical call filter together via `jsonPayload.api.method.apiClass`. */\n readonly apiClass: string,\n /** REQUIRED — the method on the contract (e.g. 'save'). */\n readonly methodName: string,\n /** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no\n * impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */\n readonly controllerName?: string,\n /** OPTIONAL — which DTO fields to mask in the log lines, and how. Absent = log the DTO\n * verbatim exactly as before (the fast path, no per-field walk). Present = {@link LogApiCallImpl}\n * runs {@link maskedStringify} so a declared-sensitive field (an OAuth refresh token, an\n * id-token JWT) is masked in the log while the REAL value still travels on the wire untouched.\n * Rides the call identity so the spec travels with the api definition, not a global app config. */\n readonly mask?: MaskSpec,\n /** OPTIONAL — true when the route declared `@Endpoint(..., { noLogging: true })` (a log\n * shipper, a log-ingest endpoint): {@link LogApiCallImpl} emits NO `[API-*-req]`/`[API-*-resp-*]`\n * line and stamps no {@link ApiCallInfo} tag for it. Read once at route-build time and handed\n * here, exactly as {@link mask} is. Default (absent/false) = the ordinary, fully-logged call. */\n readonly noLogging: boolean = false,\n /** OPTIONAL — `@Endpoint(..., { hideProgress: true })`, carried from the route so the whole\n * call identity travels together. webpieces itself does not act on it; an app's progress UI\n * does. Default false. */\n readonly hideProgress: boolean = false,\n /** OPTIONAL — `@Endpoint(..., { allowUpgradeInFlight: true })`, carried from the route so the\n * whole call identity travels together. webpieces itself does not act on it; an app's upgrade\n * logic does. Default false. */\n readonly allowUpgradeInFlight: boolean = false,\n ) {}\n}\n"]}
1
+ {"version":3,"file":"ApiMethodInfo.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ApiMethodInfo.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;AAOH,MAAa,aAAa;IAeT;IAGA;IAEA;IAKA;IAMA;IAKA;IAIA;IAIA;IA9Bb,YACa,IAAa;IACtB;2FACuF;IAC9E,QAAgB;IACzB,2DAA2D;IAClD,UAAkB;IAC3B,kGAAkG;IAClG,QAA8B;IAC9B;gGAC4F;IACnF,cAAuB;IAChC;;;;wGAIoG;IAC3F,IAAe;IACxB;;;sGAGkG;IACzF,YAAqB,KAAK;IACnC;;+BAE2B;IAClB,eAAwB,KAAK;IACtC;;qCAEiC;IACxB,uBAAgC,KAAK;QA7BrC,SAAI,GAAJ,IAAI,CAAS;QAGb,aAAQ,GAAR,QAAQ,CAAQ;QAEhB,eAAU,GAAV,UAAU,CAAQ;QAKlB,mBAAc,GAAd,cAAc,CAAS;QAMvB,SAAI,GAAJ,IAAI,CAAW;QAKf,cAAS,GAAT,SAAS,CAAiB;QAI1B,iBAAY,GAAZ,YAAY,CAAiB;QAI7B,yBAAoB,GAApB,oBAAoB,CAAiB;QAE9C,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IACpF,CAAC;CACJ;AAhDD,sCAgDC","sourcesContent":["/**\n * ApiMethodInfo - the transport-neutral identity of a single API method call, handed to\n * {@link LogApiCallImpl.execute} by every caller (server inbound, http/in-process clients, cloud tasks,\n * and external wrapped clients like the firestore admin client).\n *\n * WHY generic (not RouteMetadata): LogApiCall runs deep in the stack over MANY shapes — HTTP routes,\n * pubsub/queue enqueues, and multi-param external clients returning Promise<void>/Promise<unknown>.\n * None of those own an `httpMethod`/`path`, so LogApiCall must not depend on the HTTP-shaped\n * `RouteMetadata`. This carries only what identifies the call.\n *\n * MATCHING is the point of {@link apiClass}: a CLIENT call and the SERVER handler for the same logical\n * method must log the SAME identity so `jsonPayload.api.method.apiClass=\"SaveApi\"` filters both sides\n * together. The API CONTRACT class name (e.g. 'SaveApi') is available on both sides — the client only\n * ever knows it, and the server carries it alongside its impl name — so it, not the server's impl\n * class, is the required key.\n *\n * Per CLAUDE.md: data-only structures are classes, not interfaces.\n */\n\nimport {MaskSpec} from \"./LogFieldMask\";\n\n/** Which end of the exchange this process is: the caller ('client') or the handler ('server'). */\nexport type ApiSide = 'client' | 'server';\n\nexport class ApiMethodInfo {\n /**\n * The contract CLASS (the `@ApiPath` contract, or an SDK-wrapping api-client's abstract\n * `XxxApi`) — the key {@link ClientRegistry.classify} resolves the `CallScope` ladder on\n * (method -> API -> ALL). {@link apiClass} stays the LOGGED name. REQUIRED as a constructor\n * argument so every caller states it: an SDK-wrapping client passes its abstract `XxxApi`, and\n * only a call with no contract class at all (an IPC hop, an MCP protocol step) passes an explicit\n * `undefined`, which reaches only the ALL rung.\n *\n * NON-enumerable on purpose: this object is nested into every `[API-*]` log record as\n * `jsonPayload.api.method`, and a class reference is not log data.\n */\n declare readonly contract: Function | undefined;\n\n constructor(\n readonly side: ApiSide,\n /** REQUIRED — the API CONTRACT class name (e.g. 'SaveApi'). Matches client + server so both\n * sides of one logical call filter together via `jsonPayload.api.method.apiClass`. */\n readonly apiClass: string,\n /** REQUIRED — the method on the contract (e.g. 'save'). */\n readonly methodName: string,\n /** REQUIRED — the contract CLASS, or explicit `undefined`; see {@link ApiMethodInfo.contract}. */\n contract: Function | undefined,\n /** OPTIONAL — server-side impl class (e.g. 'SaveController'). Absent on clients, which have no\n * impl. Surfaces as `jsonPayload.api.method.controllerName` for server-only drill-down. */\n readonly controllerName?: string,\n /** OPTIONAL — which DTO fields to mask in the log lines, and how. Absent = log the DTO\n * verbatim exactly as before (the fast path, no per-field walk). Present = {@link LogApiCallImpl}\n * runs {@link maskedStringify} so a declared-sensitive field (an OAuth refresh token, an\n * id-token JWT) is masked in the log while the REAL value still travels on the wire untouched.\n * Rides the call identity so the spec travels with the api definition, not a global app config. */\n readonly mask?: MaskSpec,\n /** OPTIONAL — true when the route declared `@Endpoint(..., { noLogging: true })` (a log\n * shipper, a log-ingest endpoint): {@link LogApiCallImpl} emits NO `[API-*-req]`/`[API-*-resp-*]`\n * line and stamps no {@link ApiCallInfo} tag for it. Read once at route-build time and handed\n * here, exactly as {@link mask} is. Default (absent/false) = the ordinary, fully-logged call. */\n readonly noLogging: boolean = false,\n /** OPTIONAL — `@Endpoint(..., { hideProgress: true })`, carried from the route so the whole\n * call identity travels together. webpieces itself does not act on it; an app's progress UI\n * does. Default false. */\n readonly hideProgress: boolean = false,\n /** OPTIONAL — `@Endpoint(..., { allowUpgradeInFlight: true })`, carried from the route so the\n * whole call identity travels together. webpieces itself does not act on it; an app's upgrade\n * logic does. Default false. */\n readonly allowUpgradeInFlight: boolean = false,\n ) {\n Object.defineProperty(this, 'contract', { value: contract, enumerable: false });\n }\n}\n"]}
@@ -1,11 +1,6 @@
1
1
  import { Attempt, CallContext, CallStrategy } from './CallStrategy';
2
2
  import { TransportDefault } from './TransportDefault';
3
- /**
4
- * Where a policy applies: every contract (`'ALL'`, which has no method), one contract, or one method
5
- * of one contract. Exported so other per-call registries (e.g. cloudtasks-client's `GcpTaskRegistry`)
6
- * scope their settings with the SAME ladder.
7
- */
8
- export type CallScope = [api: 'ALL'] | [api: Function, methodName?: string];
3
+ import { CallScope } from './CallScope';
9
4
  /**
10
5
  * Shared startup registry for browser RPC, node RPC, and Cloud Tasks ENQUEUE — ONE implementation,
11
6
  * every transport calls {@link CallRegistry.execute}.
@@ -22,14 +17,15 @@ export type CallScope = [api: 'ALL'] | [api: Function, methodName?: string];
22
17
  * retries a `WRITE`, even registered at ALL.
23
18
  */
24
19
  export declare class CallRegistry {
25
- private static all;
26
- private static readonly apis;
20
+ private static readonly ladder;
27
21
  static setTimeout(timeoutMs: number | undefined, ...scope: CallScope): void;
28
22
  static setStrategy(strategy: CallStrategy<unknown> | undefined, ...scope: CallScope): void;
29
23
  /**
30
24
  * Run one call under its registered policy. `context` names the method (its `methodName` selects
31
25
  * the per-method policy) and carries the endpoint's declared operation, so the SAME context a
32
- * strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`.
26
+ * strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`. The context
27
+ * a strategy sees always carries `api` as its {@link CallContext.contract}, so the error
28
+ * classification inside a strategy resolves on the same ladder.
33
29
  *
34
30
  * Every attempt is wrapped so a retried TRANSIENT failure logs one
35
31
  * `WARN [API-client-attempt-TRANSIENT]` line (see {@link LoggedAttempts}).
@@ -43,5 +39,4 @@ export declare class CallRegistry {
43
39
  */
44
40
  static resolveTimeout(api: Function, methodName: string, defaultMs: number): number;
45
41
  static clear(): void;
46
- private static policy;
47
42
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CallRegistry = void 0;
4
+ const CallStrategy_1 = require("./CallStrategy");
4
5
  const CallDeadline_1 = require("./CallDeadline");
5
6
  const ClientRegistry_1 = require("./ClientRegistry");
6
7
  const ErrorClassifier_1 = require("./ErrorClassifier");
@@ -8,6 +9,7 @@ const ApiMethodInfo_1 = require("./ApiMethodInfo");
8
9
  const LogManager_1 = require("../logging/LogManager");
9
10
  const ApiCallLogName_1 = require("./ApiCallLogName");
10
11
  const errorUtils_1 = require("../lib/errorUtils");
12
+ const CallScope_1 = require("./CallScope");
11
13
  // The same logger LogApiCall writes its [API-*] lines to, so an attempt line sits beside the final one.
12
14
  const log = LogManager_1.LogManager.getLogger(ApiCallLogName_1.LOG_API_CALL_LOGGER_NAME);
13
15
  class CallPolicy {
@@ -15,10 +17,6 @@ class CallPolicy {
15
17
  // webpieces-disable no-any-unknown -- global policy spans heterogeneous response DTOs
16
18
  strategy;
17
19
  }
18
- class ApiCallPolicies {
19
- policy = new CallPolicy();
20
- methods = new Map();
21
- }
22
20
  /**
23
21
  * Counts the attempts of ONE call and logs each retried TRANSIENT failure. It wraps the attempt
24
22
  * BEFORE any strategy sees it, so an app-written strategy gets the same lines as `RetryStrategy`.
@@ -48,7 +46,7 @@ class LoggedAttempts {
48
46
  }
49
47
  catch (err) {
50
48
  const error = (0, errorUtils_1.toError)(err);
51
- const methodInfo = new ApiMethodInfo_1.ApiMethodInfo('client', this.context.apiName, this.context.methodName);
49
+ const methodInfo = new ApiMethodInfo_1.ApiMethodInfo('client', this.context.apiName, this.context.methodName, this.context.contract);
52
50
  this.pendingTransient =
53
51
  ClientRegistry_1.ClientRegistry.classify(error, methodInfo) === ErrorClassifier_1.CallOutcome.TRANSIENT_FAILURE
54
52
  ? error
@@ -82,43 +80,46 @@ class LoggedAttempts {
82
80
  * retries a `WRITE`, even registered at ALL.
83
81
  */
84
82
  class CallRegistry {
85
- static all = new CallPolicy();
86
- static apis = new Map();
83
+ static ladder = new CallScope_1.CallScopeLadder(() => new CallPolicy());
87
84
  // webpieces-disable no-function-outside-class -- process-global startup registry
88
85
  static setTimeout(timeoutMs, ...scope) {
89
86
  if (timeoutMs !== undefined)
90
87
  CallDeadline_1.CallDeadline.validate(timeoutMs);
91
- CallRegistry.policy(scope[0], scope[1]).timeoutMs = timeoutMs;
88
+ CallRegistry.ladder.at(...scope).timeoutMs = timeoutMs;
92
89
  }
93
90
  // webpieces-disable no-function-outside-class -- process-global startup registry
94
91
  static setStrategy(
95
92
  // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs
96
93
  strategy, ...scope) {
97
- CallRegistry.policy(scope[0], scope[1]).strategy = strategy;
94
+ CallRegistry.ladder.at(...scope).strategy = strategy;
98
95
  }
99
96
  /**
100
97
  * Run one call under its registered policy. `context` names the method (its `methodName` selects
101
98
  * the per-method policy) and carries the endpoint's declared operation, so the SAME context a
102
- * strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`.
99
+ * strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`. The context
100
+ * a strategy sees always carries `api` as its {@link CallContext.contract}, so the error
101
+ * classification inside a strategy resolves on the same ladder.
103
102
  *
104
103
  * Every attempt is wrapped so a retried TRANSIENT failure logs one
105
104
  * `WARN [API-client-attempt-TRANSIENT]` line (see {@link LoggedAttempts}).
106
105
  */
107
106
  // webpieces-disable no-function-outside-class -- transport-independent policy execution
108
107
  static async execute(api, context, attempt, fallback) {
109
- const logged = new LoggedAttempts(context, attempt).run;
110
- const policies = CallRegistry.apis.get(api);
111
- const method = policies?.methods.get(context.methodName);
112
- const strategy = method?.strategy ?? policies?.policy.strategy ?? CallRegistry.all.strategy;
108
+ const scoped = context.contract === api
109
+ ? context
110
+ : new CallStrategy_1.CallContext(context.apiName, context.methodName, context.operation, api);
111
+ const logged = new LoggedAttempts(scoped, attempt).run;
112
+ const rungs = CallRegistry.ladder.rungs(api, scoped.methodName);
113
+ const strategy = rungs.find((p) => p.strategy !== undefined)?.strategy;
113
114
  if (strategy !== undefined) {
114
- return (await strategy(logged, context));
115
+ return (await strategy(logged, scoped));
115
116
  }
116
- const timeoutMs = method?.timeoutMs ?? policies?.policy.timeoutMs ?? CallRegistry.all.timeoutMs;
117
+ const timeoutMs = rungs.find((p) => p.timeoutMs !== undefined)?.timeoutMs;
117
118
  if (timeoutMs !== undefined) {
118
119
  return logged(timeoutMs);
119
120
  }
120
121
  if (fallback.strategy !== undefined) {
121
- return (await fallback.strategy(logged, context));
122
+ return (await fallback.strategy(logged, scoped));
122
123
  }
123
124
  return logged(fallback.timeoutMs);
124
125
  }
@@ -130,35 +131,12 @@ class CallRegistry {
130
131
  */
131
132
  // webpieces-disable no-function-outside-class -- process-global registry lookup
132
133
  static resolveTimeout(api, methodName, defaultMs) {
133
- const policies = CallRegistry.apis.get(api);
134
- return (policies?.methods.get(methodName)?.timeoutMs ??
135
- policies?.policy.timeoutMs ??
136
- CallRegistry.all.timeoutMs ??
137
- defaultMs);
134
+ const rungs = CallRegistry.ladder.rungs(api, methodName);
135
+ return rungs.find((p) => p.timeoutMs !== undefined)?.timeoutMs ?? defaultMs;
138
136
  }
139
137
  // webpieces-disable no-function-outside-class -- process-global registry reset for tests
140
138
  static clear() {
141
- CallRegistry.all = new CallPolicy();
142
- CallRegistry.apis.clear();
143
- }
144
- // webpieces-disable no-function-outside-class -- process-global registry storage
145
- static policy(api, methodName) {
146
- if (api === 'ALL') {
147
- return CallRegistry.all;
148
- }
149
- let policies = CallRegistry.apis.get(api);
150
- if (!policies) {
151
- policies = new ApiCallPolicies();
152
- CallRegistry.apis.set(api, policies);
153
- }
154
- if (methodName === undefined)
155
- return policies.policy;
156
- let policy = policies.methods.get(methodName);
157
- if (!policy) {
158
- policy = new CallPolicy();
159
- policies.methods.set(methodName, policy);
160
- }
161
- return policy;
139
+ CallRegistry.ladder.clear();
162
140
  }
163
141
  }
164
142
  exports.CallRegistry = CallRegistry;
@@ -1 +1 @@
1
- {"version":3,"file":"CallRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { Attempt, CallContext, CallStrategy } from './CallStrategy';\nimport { CallDeadline } from './CallDeadline';\nimport { TransportDefault } from './TransportDefault';\nimport { ClientRegistry } from './ClientRegistry';\nimport { CallOutcome } from './ErrorClassifier';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { LogManager } from '../logging/LogManager';\nimport { LOG_API_CALL_LOGGER_NAME } from './ApiCallLogName';\nimport { toError } from '../lib/errorUtils';\n\n// The same logger LogApiCall writes its [API-*] lines to, so an attempt line sits beside the final one.\nconst log = LogManager.getLogger(LOG_API_CALL_LOGGER_NAME);\n\n/**\n * Where a policy applies: every contract (`'ALL'`, which has no method), one contract, or one method\n * of one contract. Exported so other per-call registries (e.g. cloudtasks-client's `GcpTaskRegistry`)\n * scope their settings with the SAME ladder.\n */\nexport type CallScope = [api: 'ALL'] | [api: Function, methodName?: string];\n\nclass CallPolicy {\n timeoutMs?: number;\n // webpieces-disable no-any-unknown -- global policy spans heterogeneous response DTOs\n strategy?: CallStrategy<unknown>;\n}\n\nclass ApiCallPolicies {\n readonly policy = new CallPolicy();\n readonly methods = new Map<string, CallPolicy>();\n}\n\n/**\n * Counts the attempts of ONE call and logs each retried TRANSIENT failure. It wraps the attempt\n * BEFORE any strategy sees it, so an app-written strategy gets the same lines as `RetryStrategy`.\n *\n * The line for attempt N is written when attempt N+1 STARTS — i.e. only for an attempt that was\n * actually retried. The last attempt's failure is what escapes, and `LogApiCall`'s final\n * `[API-client-resp-FAIL]` line covers it, so a call that gave up after 4 attempts logs 3 attempt\n * lines and 1 FAIL line, never the last failure twice. An EXPECTED or FAILURE attempt is not logged\n * here either; the final line covers it.\n */\nclass LoggedAttempts<T> {\n private attempts = 0;\n private pendingTransient?: Error;\n\n constructor(\n private readonly context: CallContext,\n private readonly attempt: Attempt<T>,\n ) {}\n\n readonly run: Attempt<T> = async (timeoutMs: number): Promise<T> => {\n this.flushRetried();\n this.attempts += 1;\n // webpieces-disable no-unmanaged-exceptions -- observe the attempt's failure, rethrow it unchanged\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await this.attempt(timeoutMs);\n } catch (err: unknown) {\n const error = toError(err);\n const methodInfo = new ApiMethodInfo('client', this.context.apiName, this.context.methodName);\n this.pendingTransient =\n ClientRegistry.classify(error, methodInfo) === CallOutcome.TRANSIENT_FAILURE\n ? error\n : undefined;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrows the attempt's own failure unchanged\n throw err;\n }\n };\n\n private flushRetried(): void {\n const error = this.pendingTransient;\n if (error === undefined) return;\n this.pendingTransient = undefined;\n log.warn(\n `[API-client-attempt-TRANSIENT] ${this.context.apiName}.${this.context.methodName} ` +\n `attempt ${this.attempts} errorType=${error.constructor.name} error=${error.message}`,\n );\n }\n}\n\n/**\n * Shared startup registry for browser RPC, node RPC, and Cloud Tasks ENQUEUE — ONE implementation,\n * every transport calls {@link CallRegistry.execute}.\n *\n * Ladder: strategy method -> API -> ALL first; ANY strategy replaces the ENTIRE timeout ladder.\n * Otherwise timeout method -> API -> ALL. Otherwise the transport's {@link TransportDefault} (its\n * timeout, and its strategy if it has one). A registered TIMEOUT is therefore a decision for a single\n * attempt and also replaces a transport's default strategy; to keep retrying with a different attempt\n * deadline, register a `RetryStrategy` instead.\n *\n * RPC has no default retry (only the application knows whether repeating a POST is safe); Cloud Tasks\n * enqueue does (see `TransportDefault`). An app opts RPC in with\n * `CallRegistry.setStrategy(new RetryStrategy(...).run, CatalogApi)` — and `RetryStrategy` never\n * retries a `WRITE`, even registered at ALL.\n */\nexport class CallRegistry {\n private static all = new CallPolicy();\n private static readonly apis = new Map<Function, ApiCallPolicies>();\n\n // webpieces-disable no-function-outside-class -- process-global startup registry\n static setTimeout(timeoutMs: number | undefined, ...scope: CallScope): void {\n if (timeoutMs !== undefined) CallDeadline.validate(timeoutMs);\n CallRegistry.policy(scope[0], scope[1]).timeoutMs = timeoutMs;\n }\n\n // webpieces-disable no-function-outside-class -- process-global startup registry\n static setStrategy(\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n strategy: CallStrategy<unknown> | undefined,\n ...scope: CallScope\n ): void {\n CallRegistry.policy(scope[0], scope[1]).strategy = strategy;\n }\n\n /**\n * Run one call under its registered policy. `context` names the method (its `methodName` selects\n * the per-method policy) and carries the endpoint's declared operation, so the SAME context a\n * strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`.\n *\n * Every attempt is wrapped so a retried TRANSIENT failure logs one\n * `WARN [API-client-attempt-TRANSIENT]` line (see {@link LoggedAttempts}).\n */\n // webpieces-disable no-function-outside-class -- transport-independent policy execution\n static async execute<T>(\n api: Function,\n context: CallContext,\n attempt: Attempt<T>,\n fallback: TransportDefault,\n ): Promise<T> {\n const logged = new LoggedAttempts<T>(context, attempt).run;\n const policies = CallRegistry.apis.get(api);\n const method = policies?.methods.get(context.methodName);\n const strategy = method?.strategy ?? policies?.policy.strategy ?? CallRegistry.all.strategy;\n if (strategy !== undefined) {\n return (await strategy(logged, context)) as T;\n }\n const timeoutMs = method?.timeoutMs ?? policies?.policy.timeoutMs ?? CallRegistry.all.timeoutMs;\n if (timeoutMs !== undefined) {\n return logged(timeoutMs);\n }\n if (fallback.strategy !== undefined) {\n return (await fallback.strategy(logged, context)) as T;\n }\n return logged(fallback.timeoutMs);\n }\n\n /**\n * The registered TIMEOUT for a call — method -> API -> ALL -> `defaultMs` — ignoring every\n * strategy. For a transport that hands timing to someone else for this call (Cloud Tasks enqueue\n * with caller-supplied gax `CallOptions`) and still needs ONE safety cap so a hung channel cannot\n * block forever. Raise it with {@link CallRegistry.setTimeout}.\n */\n // webpieces-disable no-function-outside-class -- process-global registry lookup\n static resolveTimeout(api: Function, methodName: string, defaultMs: number): number {\n const policies = CallRegistry.apis.get(api);\n return (\n policies?.methods.get(methodName)?.timeoutMs ??\n policies?.policy.timeoutMs ??\n CallRegistry.all.timeoutMs ??\n defaultMs\n );\n }\n\n // webpieces-disable no-function-outside-class -- process-global registry reset for tests\n static clear(): void {\n CallRegistry.all = new CallPolicy();\n CallRegistry.apis.clear();\n }\n\n // webpieces-disable no-function-outside-class -- process-global registry storage\n private static policy(api: Function | 'ALL', methodName?: string): CallPolicy {\n if (api === 'ALL') {\n return CallRegistry.all;\n }\n let policies = CallRegistry.apis.get(api);\n if (!policies) {\n policies = new ApiCallPolicies();\n CallRegistry.apis.set(api, policies);\n }\n if (methodName === undefined) return policies.policy;\n let policy = policies.methods.get(methodName);\n if (!policy) {\n policy = new CallPolicy();\n policies.methods.set(methodName, policy);\n }\n return policy;\n }\n}\n"]}
1
+ {"version":3,"file":"CallRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { Attempt, CallContext, CallStrategy } from './CallStrategy';\nimport { CallDeadline } from './CallDeadline';\nimport { TransportDefault } from './TransportDefault';\nimport { ClientRegistry } from './ClientRegistry';\nimport { CallOutcome } from './ErrorClassifier';\nimport { ApiMethodInfo } from './ApiMethodInfo';\nimport { LogManager } from '../logging/LogManager';\nimport { LOG_API_CALL_LOGGER_NAME } from './ApiCallLogName';\nimport { toError } from '../lib/errorUtils';\nimport { CallScope, CallScopeLadder } from './CallScope';\n\n// The same logger LogApiCall writes its [API-*] lines to, so an attempt line sits beside the final one.\nconst log = LogManager.getLogger(LOG_API_CALL_LOGGER_NAME);\n\nclass CallPolicy {\n timeoutMs?: number;\n // webpieces-disable no-any-unknown -- global policy spans heterogeneous response DTOs\n strategy?: CallStrategy<unknown>;\n}\n\n/**\n * Counts the attempts of ONE call and logs each retried TRANSIENT failure. It wraps the attempt\n * BEFORE any strategy sees it, so an app-written strategy gets the same lines as `RetryStrategy`.\n *\n * The line for attempt N is written when attempt N+1 STARTS — i.e. only for an attempt that was\n * actually retried. The last attempt's failure is what escapes, and `LogApiCall`'s final\n * `[API-client-resp-FAIL]` line covers it, so a call that gave up after 4 attempts logs 3 attempt\n * lines and 1 FAIL line, never the last failure twice. An EXPECTED or FAILURE attempt is not logged\n * here either; the final line covers it.\n */\nclass LoggedAttempts<T> {\n private attempts = 0;\n private pendingTransient?: Error;\n\n constructor(\n private readonly context: CallContext,\n private readonly attempt: Attempt<T>,\n ) {}\n\n readonly run: Attempt<T> = async (timeoutMs: number): Promise<T> => {\n this.flushRetried();\n this.attempts += 1;\n // webpieces-disable no-unmanaged-exceptions -- observe the attempt's failure, rethrow it unchanged\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await this.attempt(timeoutMs);\n } catch (err: unknown) {\n const error = toError(err);\n const methodInfo = new ApiMethodInfo(\n 'client',\n this.context.apiName,\n this.context.methodName,\n this.context.contract,\n );\n this.pendingTransient =\n ClientRegistry.classify(error, methodInfo) === CallOutcome.TRANSIENT_FAILURE\n ? error\n : undefined;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrows the attempt's own failure unchanged\n throw err;\n }\n };\n\n private flushRetried(): void {\n const error = this.pendingTransient;\n if (error === undefined) return;\n this.pendingTransient = undefined;\n log.warn(\n `[API-client-attempt-TRANSIENT] ${this.context.apiName}.${this.context.methodName} ` +\n `attempt ${this.attempts} errorType=${error.constructor.name} error=${error.message}`,\n );\n }\n}\n\n/**\n * Shared startup registry for browser RPC, node RPC, and Cloud Tasks ENQUEUE — ONE implementation,\n * every transport calls {@link CallRegistry.execute}.\n *\n * Ladder: strategy method -> API -> ALL first; ANY strategy replaces the ENTIRE timeout ladder.\n * Otherwise timeout method -> API -> ALL. Otherwise the transport's {@link TransportDefault} (its\n * timeout, and its strategy if it has one). A registered TIMEOUT is therefore a decision for a single\n * attempt and also replaces a transport's default strategy; to keep retrying with a different attempt\n * deadline, register a `RetryStrategy` instead.\n *\n * RPC has no default retry (only the application knows whether repeating a POST is safe); Cloud Tasks\n * enqueue does (see `TransportDefault`). An app opts RPC in with\n * `CallRegistry.setStrategy(new RetryStrategy(...).run, CatalogApi)` — and `RetryStrategy` never\n * retries a `WRITE`, even registered at ALL.\n */\nexport class CallRegistry {\n private static readonly ladder = new CallScopeLadder<CallPolicy>(() => new CallPolicy());\n\n // webpieces-disable no-function-outside-class -- process-global startup registry\n static setTimeout(timeoutMs: number | undefined, ...scope: CallScope): void {\n if (timeoutMs !== undefined) CallDeadline.validate(timeoutMs);\n CallRegistry.ladder.at(...scope).timeoutMs = timeoutMs;\n }\n\n // webpieces-disable no-function-outside-class -- process-global startup registry\n static setStrategy(\n // webpieces-disable no-any-unknown -- registry strategies span heterogeneous response DTOs\n strategy: CallStrategy<unknown> | undefined,\n ...scope: CallScope\n ): void {\n CallRegistry.ladder.at(...scope).strategy = strategy;\n }\n\n /**\n * Run one call under its registered policy. `context` names the method (its `methodName` selects\n * the per-method policy) and carries the endpoint's declared operation, so the SAME context a\n * strategy sees is the one the transport's deadline puts on an `ApiCallTimeoutError`. The context\n * a strategy sees always carries `api` as its {@link CallContext.contract}, so the error\n * classification inside a strategy resolves on the same ladder.\n *\n * Every attempt is wrapped so a retried TRANSIENT failure logs one\n * `WARN [API-client-attempt-TRANSIENT]` line (see {@link LoggedAttempts}).\n */\n // webpieces-disable no-function-outside-class -- transport-independent policy execution\n static async execute<T>(\n api: Function,\n context: CallContext,\n attempt: Attempt<T>,\n fallback: TransportDefault,\n ): Promise<T> {\n const scoped =\n context.contract === api\n ? context\n : new CallContext(context.apiName, context.methodName, context.operation, api);\n const logged = new LoggedAttempts<T>(scoped, attempt).run;\n const rungs = CallRegistry.ladder.rungs(api, scoped.methodName);\n const strategy = rungs.find((p: CallPolicy) => p.strategy !== undefined)?.strategy;\n if (strategy !== undefined) {\n return (await strategy(logged, scoped)) as T;\n }\n const timeoutMs = rungs.find((p: CallPolicy) => p.timeoutMs !== undefined)?.timeoutMs;\n if (timeoutMs !== undefined) {\n return logged(timeoutMs);\n }\n if (fallback.strategy !== undefined) {\n return (await fallback.strategy(logged, scoped)) as T;\n }\n return logged(fallback.timeoutMs);\n }\n\n /**\n * The registered TIMEOUT for a call — method -> API -> ALL -> `defaultMs` — ignoring every\n * strategy. For a transport that hands timing to someone else for this call (Cloud Tasks enqueue\n * with caller-supplied gax `CallOptions`) and still needs ONE safety cap so a hung channel cannot\n * block forever. Raise it with {@link CallRegistry.setTimeout}.\n */\n // webpieces-disable no-function-outside-class -- process-global registry lookup\n static resolveTimeout(api: Function, methodName: string, defaultMs: number): number {\n const rungs = CallRegistry.ladder.rungs(api, methodName);\n return rungs.find((p: CallPolicy) => p.timeoutMs !== undefined)?.timeoutMs ?? defaultMs;\n }\n\n // webpieces-disable no-function-outside-class -- process-global registry reset for tests\n static clear(): void {\n CallRegistry.ladder.clear();\n }\n}\n"]}
@@ -13,7 +13,7 @@ class CallRegistryCompileAssertions {
13
13
  }
14
14
  /** The transport fallback is a TransportDefault (timeout + optional strategy), never a bare number. */
15
15
  fallbackIsATransportDefault() {
16
- const context = new CallStrategy_1.CallContext('Api', 'work', HttpEndpointOptions_1.WRITE);
16
+ const context = new CallStrategy_1.CallContext('Api', 'work', HttpEndpointOptions_1.WRITE, undefined);
17
17
  void CallRegistry_1.CallRegistry.execute(CallRegistryCompileAssertions, context, (ms) => Promise.resolve(ms),
18
18
  // @ts-expect-error execute(..., defaultMs: number) became execute(..., fallback: TransportDefault)
19
19
  30_000);
@@ -1 +1 @@
1
- {"version":3,"file":"CallRegistryCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallRegistryCompileAssertions.ts"],"names":[],"mappings":";;AAAA,iDAA8C;AAC9C,iDAA6C;AAC7C,+DAA8C;AAE9C,4DAA4D;AAC5D,MAAM,6BAA6B;IAC/B,KAAK;QACD,8CAA8C;QAC9C,2BAAY,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QAC5C,8CAA8C;QAC9C,2BAAY,CAAC,WAAW,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACvD,CAAC;IAED,uGAAuG;IACvG,2BAA2B;QACvB,MAAM,OAAO,GAAG,IAAI,0BAAW,CAAC,KAAK,EAAE,MAAM,EAAE,2BAAK,CAAC,CAAC;QACtD,KAAK,2BAAY,CAAC,OAAO,CACrB,6BAA6B,EAC7B,OAAO,EACP,CAAC,EAAU,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnC,mGAAmG;QACnG,MAAM,CACT,CAAC;IACN,CAAC;CACJ","sourcesContent":["import { CallRegistry } from './CallRegistry';\nimport { CallContext } from './CallStrategy';\nimport { WRITE } from './HttpEndpointOptions';\n\n/** Compiled by tsc (spec files are not), never executed. */\nclass CallRegistryCompileAssertions {\n check(): void {\n // @ts-expect-error ALL cannot select a method\n CallRegistry.setTimeout(100, 'ALL', 'work');\n // @ts-expect-error ALL cannot select a method\n CallRegistry.setStrategy(undefined, 'ALL', 'work');\n }\n\n /** The transport fallback is a TransportDefault (timeout + optional strategy), never a bare number. */\n fallbackIsATransportDefault(): void {\n const context = new CallContext('Api', 'work', WRITE);\n void CallRegistry.execute(\n CallRegistryCompileAssertions,\n context,\n (ms: number) => Promise.resolve(ms),\n // @ts-expect-error execute(..., defaultMs: number) became execute(..., fallback: TransportDefault)\n 30_000,\n );\n }\n}\n"]}
1
+ {"version":3,"file":"CallRegistryCompileAssertions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallRegistryCompileAssertions.ts"],"names":[],"mappings":";;AAAA,iDAA8C;AAC9C,iDAA6C;AAC7C,+DAA8C;AAE9C,4DAA4D;AAC5D,MAAM,6BAA6B;IAC/B,KAAK;QACD,8CAA8C;QAC9C,2BAAY,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;QAC5C,8CAA8C;QAC9C,2BAAY,CAAC,WAAW,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACvD,CAAC;IAED,uGAAuG;IACvG,2BAA2B;QACvB,MAAM,OAAO,GAAG,IAAI,0BAAW,CAAC,KAAK,EAAE,MAAM,EAAE,2BAAK,EAAE,SAAS,CAAC,CAAC;QACjE,KAAK,2BAAY,CAAC,OAAO,CACrB,6BAA6B,EAC7B,OAAO,EACP,CAAC,EAAU,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnC,mGAAmG;QACnG,MAAM,CACT,CAAC;IACN,CAAC;CACJ","sourcesContent":["import { CallRegistry } from './CallRegistry';\nimport { CallContext } from './CallStrategy';\nimport { WRITE } from './HttpEndpointOptions';\n\n/** Compiled by tsc (spec files are not), never executed. */\nclass CallRegistryCompileAssertions {\n check(): void {\n // @ts-expect-error ALL cannot select a method\n CallRegistry.setTimeout(100, 'ALL', 'work');\n // @ts-expect-error ALL cannot select a method\n CallRegistry.setStrategy(undefined, 'ALL', 'work');\n }\n\n /** The transport fallback is a TransportDefault (timeout + optional strategy), never a bare number. */\n fallbackIsATransportDefault(): void {\n const context = new CallContext('Api', 'work', WRITE, undefined);\n void CallRegistry.execute(\n CallRegistryCompileAssertions,\n context,\n (ms: number) => Promise.resolve(ms),\n // @ts-expect-error execute(..., defaultMs: number) became execute(..., fallback: TransportDefault)\n 30_000,\n );\n }\n}\n"]}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Where a policy applies: every contract (`'ALL'`, which has no method), one contract, or one method
3
+ * of one contract. The contract is always the CLASS (the `@ApiPath` contract, or an SDK-wrapping
4
+ * api-client's abstract `XxxApi`), never its name — a string key is one typo away from silently
5
+ * matching nothing.
6
+ *
7
+ * Every per-call startup registry scopes its settings with this ONE ladder: `CallRegistry`
8
+ * (strategy, timeout), `ClientRegistry` (error translator, error classifier) and cloudtasks-client's
9
+ * `GcpTaskRegistry` (customizer, gax). Resolution is always method -> API -> ALL -> the registry's
10
+ * own webpieces default.
11
+ */
12
+ export type CallScope = [api: 'ALL'] | [api: Function, methodName?: string];
13
+ /**
14
+ * The ONE implementation of the {@link CallScope} ladder. It stores one slot per scope (created on
15
+ * first use by `create`) and answers a call's rungs most-specific-first; each registry decides what a
16
+ * rung's slot holds and how a rung "answers" (first value present, or first verdict that does not
17
+ * defer).
18
+ */
19
+ export declare class CallScopeLadder<T> {
20
+ private readonly create;
21
+ private all;
22
+ private readonly apis;
23
+ constructor(create: () => T);
24
+ /** The slot for `scope`, created on first use. Registries write their setting into it. */
25
+ at(...scope: CallScope): T;
26
+ /**
27
+ * The registered slots for one call, most specific first: method, then API, then ALL. A rung with
28
+ * no slot is skipped. `api` undefined (a call with no contract class — an IPC hop, an MCP protocol
29
+ * step, a failure before routing) reaches only the ALL rung.
30
+ */
31
+ rungs(api: Function | undefined, methodName: string | undefined): T[];
32
+ clear(): void;
33
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CallScopeLadder = void 0;
4
+ class ApiRungs {
5
+ api;
6
+ methods = new Map();
7
+ }
8
+ /**
9
+ * The ONE implementation of the {@link CallScope} ladder. It stores one slot per scope (created on
10
+ * first use by `create`) and answers a call's rungs most-specific-first; each registry decides what a
11
+ * rung's slot holds and how a rung "answers" (first value present, or first verdict that does not
12
+ * defer).
13
+ */
14
+ class CallScopeLadder {
15
+ create;
16
+ all;
17
+ apis = new Map();
18
+ constructor(create) {
19
+ this.create = create;
20
+ }
21
+ /** The slot for `scope`, created on first use. Registries write their setting into it. */
22
+ at(...scope) {
23
+ const api = scope[0];
24
+ if (api === 'ALL') {
25
+ if (this.all === undefined)
26
+ this.all = this.create();
27
+ return this.all;
28
+ }
29
+ let rungs = this.apis.get(api);
30
+ if (!rungs) {
31
+ rungs = new ApiRungs();
32
+ this.apis.set(api, rungs);
33
+ }
34
+ const methodName = scope[1];
35
+ if (methodName === undefined) {
36
+ if (rungs.api === undefined)
37
+ rungs.api = this.create();
38
+ return rungs.api;
39
+ }
40
+ let slot = rungs.methods.get(methodName);
41
+ if (slot === undefined) {
42
+ slot = this.create();
43
+ rungs.methods.set(methodName, slot);
44
+ }
45
+ return slot;
46
+ }
47
+ /**
48
+ * The registered slots for one call, most specific first: method, then API, then ALL. A rung with
49
+ * no slot is skipped. `api` undefined (a call with no contract class — an IPC hop, an MCP protocol
50
+ * step, a failure before routing) reaches only the ALL rung.
51
+ */
52
+ rungs(api, methodName) {
53
+ const found = [];
54
+ const rungs = api === undefined ? undefined : this.apis.get(api);
55
+ const method = methodName === undefined ? undefined : rungs?.methods.get(methodName);
56
+ if (method !== undefined)
57
+ found.push(method);
58
+ if (rungs?.api !== undefined)
59
+ found.push(rungs.api);
60
+ if (this.all !== undefined)
61
+ found.push(this.all);
62
+ return found;
63
+ }
64
+ clear() {
65
+ this.all = undefined;
66
+ this.apis.clear();
67
+ }
68
+ }
69
+ exports.CallScopeLadder = CallScopeLadder;
70
+ //# sourceMappingURL=CallScope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CallScope.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallScope.ts"],"names":[],"mappings":";;;AAaA,MAAM,QAAQ;IACV,GAAG,CAAK;IACC,OAAO,GAAG,IAAI,GAAG,EAAa,CAAC;CAC3C;AAED;;;;;GAKG;AACH,MAAa,eAAe;IAIK;IAHrB,GAAG,CAAgB;IACV,IAAI,GAAG,IAAI,GAAG,EAAyB,CAAC;IAEzD,YAA6B,MAAe;QAAf,WAAM,GAAN,MAAM,CAAS;IAAG,CAAC;IAEhD,0FAA0F;IAC1F,EAAE,CAAC,GAAG,KAAgB;QAClB,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACrB,IAAI,GAAG,KAAK,KAAK,EAAE,CAAC;YAChB,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS;gBAAE,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACrD,OAAO,IAAI,CAAC,GAAG,CAAC;QACpB,CAAC;QACD,IAAI,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,KAAK,GAAG,IAAI,QAAQ,EAAK,CAAC;YAC1B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC9B,CAAC;QACD,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS;gBAAE,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACvD,OAAO,KAAK,CAAC,GAAG,CAAC;QACrB,CAAC;QACD,IAAI,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACzC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACrB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QACxC,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,GAAyB,EAAE,UAA8B;QAC3D,MAAM,KAAK,GAAQ,EAAE,CAAC;QACtB,MAAM,KAAK,GAAG,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACrF,IAAI,MAAM,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,KAAK,EAAE,GAAG,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACpD,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjD,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,KAAK;QACD,IAAI,CAAC,GAAG,GAAG,SAAS,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;CACJ;AAlDD,0CAkDC","sourcesContent":["/**\n * Where a policy applies: every contract (`'ALL'`, which has no method), one contract, or one method\n * of one contract. The contract is always the CLASS (the `@ApiPath` contract, or an SDK-wrapping\n * api-client's abstract `XxxApi`), never its name — a string key is one typo away from silently\n * matching nothing.\n *\n * Every per-call startup registry scopes its settings with this ONE ladder: `CallRegistry`\n * (strategy, timeout), `ClientRegistry` (error translator, error classifier) and cloudtasks-client's\n * `GcpTaskRegistry` (customizer, gax). Resolution is always method -> API -> ALL -> the registry's\n * own webpieces default.\n */\nexport type CallScope = [api: 'ALL'] | [api: Function, methodName?: string];\n\nclass ApiRungs<T> {\n api?: T;\n readonly methods = new Map<string, T>();\n}\n\n/**\n * The ONE implementation of the {@link CallScope} ladder. It stores one slot per scope (created on\n * first use by `create`) and answers a call's rungs most-specific-first; each registry decides what a\n * rung's slot holds and how a rung \"answers\" (first value present, or first verdict that does not\n * defer).\n */\nexport class CallScopeLadder<T> {\n private all: T | undefined;\n private readonly apis = new Map<Function, ApiRungs<T>>();\n\n constructor(private readonly create: () => T) {}\n\n /** The slot for `scope`, created on first use. Registries write their setting into it. */\n at(...scope: CallScope): T {\n const api = scope[0];\n if (api === 'ALL') {\n if (this.all === undefined) this.all = this.create();\n return this.all;\n }\n let rungs = this.apis.get(api);\n if (!rungs) {\n rungs = new ApiRungs<T>();\n this.apis.set(api, rungs);\n }\n const methodName = scope[1];\n if (methodName === undefined) {\n if (rungs.api === undefined) rungs.api = this.create();\n return rungs.api;\n }\n let slot = rungs.methods.get(methodName);\n if (slot === undefined) {\n slot = this.create();\n rungs.methods.set(methodName, slot);\n }\n return slot;\n }\n\n /**\n * The registered slots for one call, most specific first: method, then API, then ALL. A rung with\n * no slot is skipped. `api` undefined (a call with no contract class — an IPC hop, an MCP protocol\n * step, a failure before routing) reaches only the ALL rung.\n */\n rungs(api: Function | undefined, methodName: string | undefined): T[] {\n const found: T[] = [];\n const rungs = api === undefined ? undefined : this.apis.get(api);\n const method = methodName === undefined ? undefined : rungs?.methods.get(methodName);\n if (method !== undefined) found.push(method);\n if (rungs?.api !== undefined) found.push(rungs.api);\n if (this.all !== undefined) found.push(this.all);\n return found;\n }\n\n clear(): void {\n this.all = undefined;\n this.apis.clear();\n }\n}\n"]}
@@ -14,7 +14,19 @@ export declare class CallContext {
14
14
  readonly apiName: string;
15
15
  readonly methodName: string;
16
16
  readonly operation: EndpointOperation;
17
- constructor(apiName: string, methodName: string, operation: EndpointOperation);
17
+ /**
18
+ * The contract CLASS the call belongs to — the key of the `CallScope` ladder, so a strategy's
19
+ * per-attempt error classification resolves the classifier registered for THIS contract.
20
+ * `CallRegistry.execute` guarantees it on the context a strategy sees; undefined only for a
21
+ * transport with no contract class (an IPC deadline), which reaches the ALL rung alone.
22
+ *
23
+ * NON-enumerable on purpose: this context rides on `ApiCallTimeoutError`, which is logged and
24
+ * compared as data, and a class reference is not data.
25
+ */
26
+ readonly contract: Function | undefined;
27
+ constructor(apiName: string, methodName: string, operation: EndpointOperation,
28
+ /** REQUIRED — the contract CLASS, or explicit `undefined`; see {@link CallContext.contract}. */
29
+ contract: Function | undefined);
18
30
  }
19
31
  /** Owns all timing and retry decisions. No public cancellation signal. */
20
32
  export type CallStrategy<T> = (call: Attempt<T>, ctx: CallContext) => Promise<T>;
@@ -14,10 +14,13 @@ class CallContext {
14
14
  apiName;
15
15
  methodName;
16
16
  operation;
17
- constructor(apiName, methodName, operation) {
17
+ constructor(apiName, methodName, operation,
18
+ /** REQUIRED — the contract CLASS, or explicit `undefined`; see {@link CallContext.contract}. */
19
+ contract) {
18
20
  this.apiName = apiName;
19
21
  this.methodName = methodName;
20
22
  this.operation = operation;
23
+ Object.defineProperty(this, 'contract', { value: contract, enumerable: false });
21
24
  }
22
25
  }
23
26
  exports.CallContext = CallContext;
@@ -1 +1 @@
1
- {"version":3,"file":"CallStrategy.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallStrategy.ts"],"names":[],"mappings":";;;AAKA;;;;;;;;GAQG;AACH,MAAa,WAAW;IAEA;IACA;IACA;IAHpB,YACoB,OAAe,EACf,UAAkB,EAClB,SAA4B;QAF5B,YAAO,GAAP,OAAO,CAAQ;QACf,eAAU,GAAV,UAAU,CAAQ;QAClB,cAAS,GAAT,SAAS,CAAmB;IAC7C,CAAC;CACP;AAND,kCAMC","sourcesContent":["import { EndpointOperation } from './HttpEndpointOptions';\n\n/** One bounded attempt. Only an explicitly registered strategy may retry it. */\nexport type Attempt<T> = (timeoutMs: number) => Promise<T>;\n\n/**\n * Contract identity for diagnostics; registry keys use the class itself.\n *\n * `operation` is the endpoint's declared `@Endpoint` side-effect contract (`READ`,\n * `WRITE_IDEMPOTENT` or `WRITE`). A {@link CallStrategy} reads it to decide whether a retry is safe,\n * and `ApiCallTimeoutError` reads it to choose the text it shows an end user. A transport with\n * no declared operation (IPC contracts declare none) passes `WRITE`: the conservative answer, since\n * it never invites a blind retry of something that may already have happened.\n */\nexport class CallContext {\n constructor(\n public readonly apiName: string,\n public readonly methodName: string,\n public readonly operation: EndpointOperation,\n ) {}\n}\n\n/** Owns all timing and retry decisions. No public cancellation signal. */\nexport type CallStrategy<T> = (call: Attempt<T>, ctx: CallContext) => Promise<T>;\n"]}
1
+ {"version":3,"file":"CallStrategy.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/CallStrategy.ts"],"names":[],"mappings":";;;AAKA;;;;;;;;GAQG;AACH,MAAa,WAAW;IAaA;IACA;IACA;IAHpB,YACoB,OAAe,EACf,UAAkB,EAClB,SAA4B;IAC5C,gGAAgG;IAChG,QAA8B;QAJd,YAAO,GAAP,OAAO,CAAQ;QACf,eAAU,GAAV,UAAU,CAAQ;QAClB,cAAS,GAAT,SAAS,CAAmB;QAI5C,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IACpF,CAAC;CACJ;AArBD,kCAqBC","sourcesContent":["import { EndpointOperation } from './HttpEndpointOptions';\n\n/** One bounded attempt. Only an explicitly registered strategy may retry it. */\nexport type Attempt<T> = (timeoutMs: number) => Promise<T>;\n\n/**\n * Contract identity for diagnostics; registry keys use the class itself.\n *\n * `operation` is the endpoint's declared `@Endpoint` side-effect contract (`READ`,\n * `WRITE_IDEMPOTENT` or `WRITE`). A {@link CallStrategy} reads it to decide whether a retry is safe,\n * and `ApiCallTimeoutError` reads it to choose the text it shows an end user. A transport with\n * no declared operation (IPC contracts declare none) passes `WRITE`: the conservative answer, since\n * it never invites a blind retry of something that may already have happened.\n */\nexport class CallContext {\n /**\n * The contract CLASS the call belongs to — the key of the `CallScope` ladder, so a strategy's\n * per-attempt error classification resolves the classifier registered for THIS contract.\n * `CallRegistry.execute` guarantees it on the context a strategy sees; undefined only for a\n * transport with no contract class (an IPC deadline), which reaches the ALL rung alone.\n *\n * NON-enumerable on purpose: this context rides on `ApiCallTimeoutError`, which is logged and\n * compared as data, and a class reference is not data.\n */\n declare readonly contract: Function | undefined;\n\n constructor(\n public readonly apiName: string,\n public readonly methodName: string,\n public readonly operation: EndpointOperation,\n /** REQUIRED — the contract CLASS, or explicit `undefined`; see {@link CallContext.contract}. */\n contract: Function | undefined,\n ) {\n Object.defineProperty(this, 'contract', { value: contract, enumerable: false });\n }\n}\n\n/** Owns all timing and retry decisions. No public cancellation signal. */\nexport type CallStrategy<T> = (call: Attempt<T>, ctx: CallContext) => Promise<T>;\n"]}