@webpieces/core-util 0.4.955 → 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
@@ -1,5 +1,6 @@
1
1
  import { ErrorTranslator } from './ErrorTranslator';
2
- import { CallOutcome, ErrorClassifier, KeyedErrorClassifier } from './ErrorClassifier';
2
+ import { CallOutcome, ErrorClassifier } from './ErrorClassifier';
3
+ import { CallScope } from './CallScope';
3
4
  import { ApiMethodInfo } from './ApiMethodInfo';
4
5
  /**
5
6
  * Derives a base URL for a service name that has NO registered mapping — the pluggable half of
@@ -54,35 +55,26 @@ export declare class ClientRegistry {
54
55
  /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */
55
56
  private static deriver;
56
57
  /**
57
- * The process's ONE {@link ErrorTranslator}. NEVER undefined — it starts life holding
58
- * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, and {@link ClientRegistry.setErrorTranslator}
59
- * REPLACES it. Process-global, set once at startup on the SERVER and in the BROWSER — the same
60
- * no-DI pattern as {@link ClientRegistry.mappings} above.
58
+ * The {@link ErrorTranslator} per {@link CallScope} — method -> API -> ALL, and below every rung
59
+ * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, so {@link ClientRegistry.getErrorTranslator} is
60
+ * never undefined. Process-global, set once at startup on the SERVER and in the BROWSER — the
61
+ * same no-DI pattern as {@link ClientRegistry.mappings} above.
61
62
  *
62
- * There is no "did anyone register one" question anywhere in the framework because this field
63
- * cannot be empty: every caller makes ONE unconditional call, and an app translator declines an
64
- * error by DELEGATING to {@link WebpiecesDefaultErrorTranslator}.
65
- *
66
- * ONE, not a list, on purpose: an app that has several layers of error policy composes them
67
- * INSIDE its own `toWire`, where the precedence is written down, instead of leaving it implicit
68
- * in the order two unrelated startup paths happened to register.
69
- */
70
- private static errorTranslator;
71
- /**
72
- * The app/company DEFAULT {@link ErrorClassifier} — ONE per process, reads {@link ApiMethodInfo.side}
73
- * so a single classifier covers the server router AND all internal clients. NEVER undefined: it
74
- * starts life holding the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}) and
75
- * {@link ClientRegistry.setDefaultErrorClassifier} replaces it. Populated once at startup on the
76
- * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.
63
+ * ONE translator per scope, not a list, on purpose: an app that has several layers of error
64
+ * policy for one scope composes them INSIDE its own `toWire`, where the precedence is written
65
+ * down. The ladder exists because vendors put their errors on the wire in their OWN formats — a
66
+ * per-API translator reads one vendor's 4xx body (`fromWire`), or writes the error body an
67
+ * inbound vendor expects back (`toWire`), while every other contract keeps the app's ALL one.
77
68
  */
78
- private static appDefaultErrorClassifier;
69
+ private static readonly errorTranslators;
79
70
  /**
80
- * Per-EXTERNAL-client {@link ErrorClassifier}s, keyed by {@link ApiMethodInfo.apiClass}
81
- * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external
82
- * client overrides the default for its own apiClass only. Internal clients / the server register
83
- * nothing here and fall through to the default. See {@link ClientRegistry.classify}.
71
+ * The {@link ErrorClassifier} per {@link CallScope} — method -> API -> ALL, and below every rung
72
+ * {@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}. A classifier that DEFERS (`undefined`) falls through
73
+ * to the next rung. The ALL classifier reads {@link ApiMethodInfo.side}, so one classifier covers
74
+ * the server router AND every internal client; an SDK-wrapping api-client registers its own at
75
+ * its abstract `XxxApi` class. See {@link ClientRegistry.classify}.
84
76
  */
85
- private static readonly errorClassifiersByApiClass;
77
+ private static readonly errorClassifiers;
86
78
  /** Map a service name to `http://localhost:<port>`. */
87
79
  static addMapping(svcName: string, port: number): void;
88
80
  /**
@@ -120,42 +112,46 @@ export declare class ClientRegistry {
120
112
  */
121
113
  static resolve(svcName: string): Promise<string>;
122
114
  /**
123
- * Install the app's {@link ErrorTranslator} — the ONE symmetric owner of error translation for
124
- * this process, server side AND every client side. Consulted BEFORE webpieces' built-in mapping
125
- * in both directions. Call ONCE at startup, on the server AND in the browser, mirroring
126
- * {@link ClientRegistry.addMapping}.
115
+ * Install an {@link ErrorTranslator} for one {@link CallScope}: `'ALL'` (the app's translator,
116
+ * server side AND every client side), one contract CLASS, or one method of one contract. The
117
+ * scope is REQUIRED — there is no unscoped spelling. Call at startup, on the server AND in the
118
+ * browser, mirroring {@link ClientRegistry.addMapping}.
127
119
  *
128
- * `set`, not `add`: see the field doc above for why a registry LIST is not wanted.
129
- */
130
- static setErrorTranslator(translator: ErrorTranslator): void;
131
- /**
132
- * THE {@link ErrorTranslator} for this process — never `undefined`, so a caller never branches.
120
+ * ```ts
121
+ * ClientRegistry.setErrorTranslator(new AppErrorTranslator(), 'ALL');
122
+ * ClientRegistry.setErrorTranslator(new VendorErrorTranslator(), VendorApi); // one contract
123
+ * ClientRegistry.setErrorTranslator(new LegacyErrorTranslator(), VendorApi, 'sync'); // one method
124
+ * ```
133
125
  *
134
- * Until an app calls {@link ClientRegistry.setErrorTranslator} this is
135
- * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}. A registered translator answers EVERY error and
136
- * EVERY response, declining by delegating to the webpieces default (see {@link ErrorTranslator}),
137
- * so every call site in the framework is one unconditional line.
126
+ * `set`, not `add`: see the field doc above for why a per-scope LIST is not wanted. A translator
127
+ * declines an error by DELEGATING to {@link WebpiecesDefaultErrorTranslator} — not to the next
128
+ * rung — exactly as before.
138
129
  */
139
- static getErrorTranslator(): ErrorTranslator;
130
+ static setErrorTranslator(translator: ErrorTranslator, ...scope: CallScope): void;
140
131
  /**
141
- * Set the app/company DEFAULT error classifier — ONE per process, covering the server router and
142
- * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the
143
- * server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier
144
- * claims. See {@link ErrorClassifier}.
132
+ * THE {@link ErrorTranslator} for one call — method -> API -> ALL ->
133
+ * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, never `undefined`, so a caller never branches.
134
+ *
135
+ * Both arguments are required so every call site states what it knows: the client and the server
136
+ * pass the route's contract class and method; a failure with no route (an unmatched request, a
137
+ * failure before routing) passes `undefined, undefined` and reaches the ALL rung alone.
145
138
  */
146
- static setDefaultErrorClassifier(classifier: ErrorClassifier): void;
139
+ static getErrorTranslator(api: Function | undefined, methodName: string | undefined): ErrorTranslator;
147
140
  /**
148
- * Register a per-EXTERNAL-client error classifier, keyed by its
149
- * {@link KeyedErrorClassifier.apiClass} (e.g. 'FirestoreAdminClient'). Consulted BEFORE the
150
- * default tier for calls with that apiClass. Call ONCE at startup. See {@link ErrorClassifier}.
141
+ * Install an {@link ErrorClassifier} for one {@link CallScope}: `'ALL'` (the app/company
142
+ * classifier — it reads {@link ApiMethodInfo.side}, so it covers the server router and every
143
+ * internal client), one contract CLASS (an SDK-wrapping api-client keys by its abstract `XxxApi`),
144
+ * or one method of one contract. The scope is REQUIRED. Call at startup, on the server AND in the
145
+ * browser. See {@link ErrorClassifier}.
151
146
  */
152
- static addErrorClassifier(keyed: KeyedErrorClassifier): void;
147
+ static setErrorClassifier(classifier: ErrorClassifier, ...scope: CallScope): void;
153
148
  /**
154
- * The {@link CallOutcome} of a thrown API-call error. Resolved most-specific-first, and ALWAYS
155
- * definitive:
156
- * 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;
157
- * 2. else the app default classifier — unless it defers;
158
- * 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.
149
+ * The {@link CallOutcome} of a thrown API-call error. Resolved on the {@link CallScope} ladder of
150
+ * `methodInfo.contract` / `methodInfo.methodName`, most specific first, and ALWAYS definitive:
151
+ * 1. the method classifier — unless it defers;
152
+ * 2. else the API classifier — unless it defers;
153
+ * 3. else the ALL classifier — unless it defers;
154
+ * 4. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.
159
155
  *
160
156
  * The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `CallRegistry`'s per-attempt
161
157
  * log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.
@@ -165,10 +161,10 @@ export declare class ClientRegistry {
165
161
  * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec
166
162
  * cannot leak its registrations into the next.
167
163
  *
168
- * It RESTORES the webpieces defaults rather than emptying the pluggable slots, which is the whole
169
- * reason it is not called `clear()` any more: `getErrorTranslator()` and the default error
170
- * classifier are non-optional now, so "cleared" is not a state they have. A spec that wants
171
- * webpieces' own behaviour back asks for exactly that.
164
+ * It empties every translator and classifier rung, which RESTORES the webpieces defaults:
165
+ * `getErrorTranslator(...)` and `classify(...)` fall through to them below every rung, so
166
+ * "cleared" is not a state they have. A spec that wants webpieces' own behaviour back asks for
167
+ * exactly that.
172
168
  */
173
169
  static resetForTests(): void;
174
170
  }
@@ -1,8 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ClientRegistry = void 0;
4
+ const CallScope_1 = require("./CallScope");
4
5
  const WebpiecesDefaultErrorClassifier_1 = require("./WebpiecesDefaultErrorClassifier");
5
6
  const WebpiecesDefaultErrorTranslator_1 = require("./WebpiecesDefaultErrorTranslator");
7
+ /** One {@link CallScope} rung of the error-translator ladder. Data only, so a class. */
8
+ class TranslatorSlot {
9
+ translator;
10
+ }
11
+ /** One {@link CallScope} rung of the error-classifier ladder. Data only, so a class. */
12
+ class ClassifierSlot {
13
+ classifier;
14
+ }
6
15
  /**
7
16
  * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every
8
17
  * environment. A client is built once but a URL is per-environment, so the URL belongs here rather
@@ -43,35 +52,26 @@ class ClientRegistry {
43
52
  /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */
44
53
  static deriver;
45
54
  /**
46
- * The process's ONE {@link ErrorTranslator}. NEVER undefined — it starts life holding
47
- * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, and {@link ClientRegistry.setErrorTranslator}
48
- * REPLACES it. Process-global, set once at startup on the SERVER and in the BROWSER — the same
49
- * no-DI pattern as {@link ClientRegistry.mappings} above.
50
- *
51
- * There is no "did anyone register one" question anywhere in the framework because this field
52
- * cannot be empty: every caller makes ONE unconditional call, and an app translator declines an
53
- * error by DELEGATING to {@link WebpiecesDefaultErrorTranslator}.
55
+ * The {@link ErrorTranslator} per {@link CallScope} — method -> API -> ALL, and below every rung
56
+ * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, so {@link ClientRegistry.getErrorTranslator} is
57
+ * never undefined. Process-global, set once at startup on the SERVER and in the BROWSER — the
58
+ * same no-DI pattern as {@link ClientRegistry.mappings} above.
54
59
  *
55
- * ONE, not a list, on purpose: an app that has several layers of error policy composes them
56
- * INSIDE its own `toWire`, where the precedence is written down, instead of leaving it implicit
57
- * in the order two unrelated startup paths happened to register.
60
+ * ONE translator per scope, not a list, on purpose: an app that has several layers of error
61
+ * policy for one scope composes them INSIDE its own `toWire`, where the precedence is written
62
+ * down. The ladder exists because vendors put their errors on the wire in their OWN formats — a
63
+ * per-API translator reads one vendor's 4xx body (`fromWire`), or writes the error body an
64
+ * inbound vendor expects back (`toWire`), while every other contract keeps the app's ALL one.
58
65
  */
59
- static errorTranslator = WebpiecesDefaultErrorTranslator_1.WEBPIECES_DEFAULT_ERROR_TRANSLATOR;
66
+ static errorTranslators = new CallScope_1.CallScopeLadder(() => new TranslatorSlot());
60
67
  /**
61
- * The app/company DEFAULT {@link ErrorClassifier} — ONE per process, reads {@link ApiMethodInfo.side}
62
- * so a single classifier covers the server router AND all internal clients. NEVER undefined: it
63
- * starts life holding the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}) and
64
- * {@link ClientRegistry.setDefaultErrorClassifier} replaces it. Populated once at startup on the
65
- * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.
68
+ * The {@link ErrorClassifier} per {@link CallScope} — method -> API -> ALL, and below every rung
69
+ * {@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}. A classifier that DEFERS (`undefined`) falls through
70
+ * to the next rung. The ALL classifier reads {@link ApiMethodInfo.side}, so one classifier covers
71
+ * the server router AND every internal client; an SDK-wrapping api-client registers its own at
72
+ * its abstract `XxxApi` class. See {@link ClientRegistry.classify}.
66
73
  */
67
- static appDefaultErrorClassifier = WebpiecesDefaultErrorClassifier_1.WEBPIECES_DEFAULT_ERROR_CLASSIFIER;
68
- /**
69
- * Per-EXTERNAL-client {@link ErrorClassifier}s, keyed by {@link ApiMethodInfo.apiClass}
70
- * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external
71
- * client overrides the default for its own apiClass only. Internal clients / the server register
72
- * nothing here and fall through to the default. See {@link ClientRegistry.classify}.
73
- */
74
- static errorClassifiersByApiClass = new Map();
74
+ static errorClassifiers = new CallScope_1.CallScopeLadder(() => new ClassifierSlot());
75
75
  /** Map a service name to `http://localhost:<port>`. */
76
76
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
77
77
  static addMapping(svcName, port) {
@@ -153,89 +153,88 @@ class ClientRegistry {
153
153
  return url;
154
154
  }
155
155
  /**
156
- * Install the app's {@link ErrorTranslator} — the ONE symmetric owner of error translation for
157
- * this process, server side AND every client side. Consulted BEFORE webpieces' built-in mapping
158
- * in both directions. Call ONCE at startup, on the server AND in the browser, mirroring
159
- * {@link ClientRegistry.addMapping}.
156
+ * Install an {@link ErrorTranslator} for one {@link CallScope}: `'ALL'` (the app's translator,
157
+ * server side AND every client side), one contract CLASS, or one method of one contract. The
158
+ * scope is REQUIRED — there is no unscoped spelling. Call at startup, on the server AND in the
159
+ * browser, mirroring {@link ClientRegistry.addMapping}.
160
160
  *
161
- * `set`, not `add`: see the field doc above for why a registry LIST is not wanted.
162
- */
163
- // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
164
- static setErrorTranslator(translator) {
165
- ClientRegistry.errorTranslator = translator;
166
- }
167
- /**
168
- * THE {@link ErrorTranslator} for this process — never `undefined`, so a caller never branches.
161
+ * ```ts
162
+ * ClientRegistry.setErrorTranslator(new AppErrorTranslator(), 'ALL');
163
+ * ClientRegistry.setErrorTranslator(new VendorErrorTranslator(), VendorApi); // one contract
164
+ * ClientRegistry.setErrorTranslator(new LegacyErrorTranslator(), VendorApi, 'sync'); // one method
165
+ * ```
169
166
  *
170
- * Until an app calls {@link ClientRegistry.setErrorTranslator} this is
171
- * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}. A registered translator answers EVERY error and
172
- * EVERY response, declining by delegating to the webpieces default (see {@link ErrorTranslator}),
173
- * so every call site in the framework is one unconditional line.
167
+ * `set`, not `add`: see the field doc above for why a per-scope LIST is not wanted. A translator
168
+ * declines an error by DELEGATING to {@link WebpiecesDefaultErrorTranslator} — not to the next
169
+ * rung — exactly as before.
174
170
  */
175
171
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
176
- static getErrorTranslator() {
177
- return ClientRegistry.errorTranslator;
172
+ static setErrorTranslator(translator, ...scope) {
173
+ ClientRegistry.errorTranslators.at(...scope).translator = translator;
178
174
  }
179
175
  /**
180
- * Set the app/company DEFAULT error classifier — ONE per process, covering the server router and
181
- * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the
182
- * server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier
183
- * claims. See {@link ErrorClassifier}.
176
+ * THE {@link ErrorTranslator} for one call — method -> API -> ALL ->
177
+ * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, never `undefined`, so a caller never branches.
178
+ *
179
+ * Both arguments are required so every call site states what it knows: the client and the server
180
+ * pass the route's contract class and method; a failure with no route (an unmatched request, a
181
+ * failure before routing) passes `undefined, undefined` and reaches the ALL rung alone.
184
182
  */
185
183
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
186
- static setDefaultErrorClassifier(classifier) {
187
- ClientRegistry.appDefaultErrorClassifier = classifier;
184
+ static getErrorTranslator(api, methodName) {
185
+ const registered = ClientRegistry.errorTranslators
186
+ .rungs(api, methodName)
187
+ .find((slot) => slot.translator !== undefined)?.translator;
188
+ return registered ?? WebpiecesDefaultErrorTranslator_1.WEBPIECES_DEFAULT_ERROR_TRANSLATOR;
188
189
  }
189
190
  /**
190
- * Register a per-EXTERNAL-client error classifier, keyed by its
191
- * {@link KeyedErrorClassifier.apiClass} (e.g. 'FirestoreAdminClient'). Consulted BEFORE the
192
- * default tier for calls with that apiClass. Call ONCE at startup. See {@link ErrorClassifier}.
191
+ * Install an {@link ErrorClassifier} for one {@link CallScope}: `'ALL'` (the app/company
192
+ * classifier — it reads {@link ApiMethodInfo.side}, so it covers the server router and every
193
+ * internal client), one contract CLASS (an SDK-wrapping api-client keys by its abstract `XxxApi`),
194
+ * or one method of one contract. The scope is REQUIRED. Call at startup, on the server AND in the
195
+ * browser. See {@link ErrorClassifier}.
193
196
  */
194
197
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
195
- static addErrorClassifier(keyed) {
196
- ClientRegistry.errorClassifiersByApiClass.set(keyed.apiClass, keyed.classifier);
198
+ static setErrorClassifier(classifier, ...scope) {
199
+ ClientRegistry.errorClassifiers.at(...scope).classifier = classifier;
197
200
  }
198
201
  /**
199
- * The {@link CallOutcome} of a thrown API-call error. Resolved most-specific-first, and ALWAYS
200
- * definitive:
201
- * 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;
202
- * 2. else the app default classifier — unless it defers;
203
- * 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.
202
+ * The {@link CallOutcome} of a thrown API-call error. Resolved on the {@link CallScope} ladder of
203
+ * `methodInfo.contract` / `methodInfo.methodName`, most specific first, and ALWAYS definitive:
204
+ * 1. the method classifier — unless it defers;
205
+ * 2. else the API classifier — unless it defers;
206
+ * 3. else the ALL classifier — unless it defers;
207
+ * 4. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.
204
208
  *
205
209
  * The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `CallRegistry`'s per-attempt
206
210
  * log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.
207
211
  */
208
212
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
209
213
  static classify(error, methodInfo) {
210
- const perClient = ClientRegistry.errorClassifiersByApiClass.get(methodInfo.apiClass);
211
- if (perClient !== undefined) {
212
- const verdict = perClient.classify(error, methodInfo);
214
+ const rungs = ClientRegistry.errorClassifiers.rungs(methodInfo.contract, methodInfo.methodName);
215
+ for (const slot of rungs) {
216
+ const verdict = slot.classifier?.classify(error, methodInfo);
213
217
  if (verdict !== undefined) {
214
218
  return verdict;
215
219
  }
216
220
  }
217
- const verdict = ClientRegistry.appDefaultErrorClassifier.classify(error, methodInfo);
218
- if (verdict !== undefined) {
219
- return verdict;
220
- }
221
221
  return WebpiecesDefaultErrorClassifier_1.WEBPIECES_DEFAULT_ERROR_CLASSIFIER.classify(error, methodInfo);
222
222
  }
223
223
  /**
224
224
  * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec
225
225
  * cannot leak its registrations into the next.
226
226
  *
227
- * It RESTORES the webpieces defaults rather than emptying the pluggable slots, which is the whole
228
- * reason it is not called `clear()` any more: `getErrorTranslator()` and the default error
229
- * classifier are non-optional now, so "cleared" is not a state they have. A spec that wants
230
- * webpieces' own behaviour back asks for exactly that.
227
+ * It empties every translator and classifier rung, which RESTORES the webpieces defaults:
228
+ * `getErrorTranslator(...)` and `classify(...)` fall through to them below every rung, so
229
+ * "cleared" is not a state they have. A spec that wants webpieces' own behaviour back asks for
230
+ * exactly that.
231
231
  */
232
232
  // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected
233
233
  static resetForTests() {
234
234
  ClientRegistry.mappings.clear();
235
235
  ClientRegistry.deriver = undefined;
236
- ClientRegistry.errorTranslator = WebpiecesDefaultErrorTranslator_1.WEBPIECES_DEFAULT_ERROR_TRANSLATOR;
237
- ClientRegistry.appDefaultErrorClassifier = WebpiecesDefaultErrorClassifier_1.WEBPIECES_DEFAULT_ERROR_CLASSIFIER;
238
- ClientRegistry.errorClassifiersByApiClass.clear();
236
+ ClientRegistry.errorTranslators.clear();
237
+ ClientRegistry.errorClassifiers.clear();
239
238
  }
240
239
  }
241
240
  exports.ClientRegistry = ClientRegistry;
@@ -1 +1 @@
1
- {"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { ErrorTranslator } from './ErrorTranslator';\nimport { CallOutcome, ErrorClassifier, KeyedErrorClassifier } from './ErrorClassifier';\nimport { WEBPIECES_DEFAULT_ERROR_CLASSIFIER } from './WebpiecesDefaultErrorClassifier';\nimport { WEBPIECES_DEFAULT_ERROR_TRANSLATOR } from './WebpiecesDefaultErrorTranslator';\nimport { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * Derives a base URL for a service name that has NO registered mapping — the pluggable half of\n * {@link ClientRegistry} resolution. One per environment, installed once at startup:\n *\n * - `gcpCloudRunDeriver()` (@webpieces/gcp-identity) — the Cloud Run formula, read from the metadata\n * server. For code running ON GCP.\n * - `gcpCloudRunDeriver(new GcpCloudRunTarget(projectNumber, region))` — the SAME formula with the\n * values supplied from config, for code running OFF GCP that still calls Cloud Run (a CLI, CI).\n * - {@link templateDeriver} (this package, browser-safe) — pure string substitution, for AWS or\n * anything else with predictable DNS.\n * - none at all — a browser goes same-origin, and localhost/tests hand-register their mappings.\n */\nexport type ServiceUrlDeriver = (svcName: string) => Promise<string>;\n\n/**\n * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every\n * environment. A client is built once but a URL is per-environment, so the URL belongs here rather\n * than on the client: clients carry ONLY a svcName.\n *\n * Resolution is one precedence chain, identical in the browser, in node, and in Cloud Tasks:\n *\n * 1. a registered mapping wins — the localhost port table, AWS, an external API, another region\n * or project, a host that is not Cloud Run at all. Populated at startup from per-env config.\n * 2. else the installed {@link ServiceUrlDeriver}, if any — GCP's built-in, a DNS template, or\n * your own. OPTIONAL on purpose: localhost is inherently a TABLE (helper-fsdb -> :8401,\n * helper-portal -> :8201 have per-service ports), so explicit mappings must stay sufficient on\n * their own.\n * 3. else the caller's fallback: the BROWSER goes relative (same origin — see\n * {@link ClientRegistry.tryResolve}), while node THROWS (see {@link ClientRegistry.resolve}),\n * because a server has no \"own origin\" and an unresolvable peer is a setup bug.\n *\n * Configured like {@link HeaderRegistry} / LogManager — populated once at startup, then globally\n * accessible with NO DI wiring. It is browser-safe (no `process.env`, no node-only deps), which is\n * why it lives in core-util rather than gcp-identity.\n *\n * A `svcName` should be the nx MODULE name of the service you are calling — that is the one name\n * `architecture:validate-runtime-architecture` can verify actually exists, so a typo or a rename\n * fails the build instead of failing in production. When the DEPLOYED name differs (an environment\n * prefix like `tf-`, or a Cloud Run service named unlike its module), translate it in ONE place —\n * the deriver below, or a `serviceName` alias in the module's project.json — never at the call site.\n *\n * ```ts\n * // startup, from the current environment's config:\n * ClientRegistry.addMapping('server2', 8202); // -> http://localhost:8202\n * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');\n * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP\n * ```\n */\nexport class ClientRegistry {\n /** svcName -> resolved base URL. Process-global; populated at startup per environment. */\n private static readonly mappings = new Map<string, string>();\n\n /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */\n private static deriver: ServiceUrlDeriver | undefined;\n\n /**\n * The process's ONE {@link ErrorTranslator}. NEVER undefined — it starts life holding\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, and {@link ClientRegistry.setErrorTranslator}\n * REPLACES it. Process-global, set once at startup on the SERVER and in the BROWSER — the same\n * no-DI pattern as {@link ClientRegistry.mappings} above.\n *\n * There is no \"did anyone register one\" question anywhere in the framework because this field\n * cannot be empty: every caller makes ONE unconditional call, and an app translator declines an\n * error by DELEGATING to {@link WebpiecesDefaultErrorTranslator}.\n *\n * ONE, not a list, on purpose: an app that has several layers of error policy composes them\n * INSIDE its own `toWire`, where the precedence is written down, instead of leaving it implicit\n * in the order two unrelated startup paths happened to register.\n */\n private static errorTranslator: ErrorTranslator = WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n\n /**\n * The app/company DEFAULT {@link ErrorClassifier} — ONE per process, reads {@link ApiMethodInfo.side}\n * so a single classifier covers the server router AND all internal clients. NEVER undefined: it\n * starts life holding the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}) and\n * {@link ClientRegistry.setDefaultErrorClassifier} replaces it. Populated once at startup on the\n * SERVER and in the BROWSER — same no-DI pattern as {@link ClientRegistry.mappings}.\n */\n private static appDefaultErrorClassifier: ErrorClassifier = WEBPIECES_DEFAULT_ERROR_CLASSIFIER;\n\n /**\n * Per-EXTERNAL-client {@link ErrorClassifier}s, keyed by {@link ApiMethodInfo.apiClass}\n * ('FirestoreAdminClient', 'ClaudeApi', ...). Consulted BEFORE the default tier, so an external\n * client overrides the default for its own apiClass only. Internal clients / the server register\n * nothing here and fall through to the default. See {@link ClientRegistry.classify}.\n */\n private static readonly errorClassifiersByApiClass = new Map<string, ErrorClassifier>();\n\n /** Map a service name to `http://localhost:<port>`. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addMapping(svcName: string, port: number): void {\n ClientRegistry.mappings.set(svcName, `http://localhost:${port}`);\n }\n\n /**\n * Map a service name to an explicit base URL (any host / any environment).\n *\n * The EMPTY STRING is a legal, meaningful mapping: it makes the service relative, i.e.\n * same-origin, because the client builds its URL as `${baseUrl}${route.path}`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addUrlMapping(svcName: string, url: string): void {\n ClientRegistry.mappings.set(svcName, url);\n }\n\n /**\n * Install the environment's {@link ServiceUrlDeriver} — how to resolve a svcName that has NO\n * mapping. Optional: an environment that registers every svcName it calls needs none.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDeriver(fn: ServiceUrlDeriver): void {\n ClientRegistry.deriver = fn;\n }\n\n /** The registered override for `svcName`, or undefined if none. Registry only — no derivation. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryLookup(svcName: string): string | undefined {\n return ClientRegistry.mappings.get(svcName);\n }\n\n /**\n * Resolve `svcName` to its registered base URL. THROWS if the service was never registered.\n * Registry only — it does NOT consult the deriver; prefer {@link ClientRegistry.resolve}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static lookup(svcName: string): string {\n const url = ClientRegistry.tryLookup(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL registered for service \"${svcName}\". Register it at startup: ` +\n `ClientRegistry.addMapping(svcName, port) for a localhost port, or ` +\n `ClientRegistry.addUrlMapping(svcName, url) for an explicit URL.`,\n );\n }\n return url;\n }\n\n /**\n * Steps 1 + 2 of the chain: the registered mapping, else the installed deriver, else undefined.\n * Non-throwing — the BROWSER uses this and treats undefined as \"\" (relative → same origin),\n * which is what a browser app calling the backend it was served from wants by default.\n *\n * Note the `!== undefined` guard: an empty-string mapping is a legal answer (\"this service is\n * same-origin\"), so it must NOT fall through to derivation.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryResolve(svcName: string): Promise<string | undefined> {\n const override = ClientRegistry.tryLookup(svcName);\n if (override !== undefined) {\n return Promise.resolve(override);\n }\n if (!ClientRegistry.deriver) {\n return Promise.resolve(undefined);\n }\n return ClientRegistry.deriver(svcName);\n }\n\n /**\n * The full chain, with node's fallback: mapping, else deriver, else THROW. A server has no \"own\n * origin\" to go relative to, so an unresolvable peer is a setup bug and must fail loudly — and\n * say how to fix it.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static async resolve(svcName: string): Promise<string> {\n const url = await ClientRegistry.tryResolve(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL for service \"${svcName}\".\\n` +\n ` - localhost/AWS: ClientRegistry.addMapping('${svcName}', 8401)\\n` +\n ` or ClientRegistry.addUrlMapping('${svcName}', 'https://...')\\n` +\n ` - GCP: install a deriver — ClientRegistry.setDeriver(gcpCloudRunDeriver())\\n` +\n ` - deployed name differs from the module name (e.g. a 'tf-' prefix)? Translate it ONCE\\n` +\n ` in the deriver — setDeriver(s => gcpCloudRunDeriver()('tf-' + s)) — so every call\\n` +\n ` site keeps naming the MODULE, which architecture:validate-runtime-architecture checks`,\n );\n }\n return url;\n }\n\n /**\n * Install the app's {@link ErrorTranslator} — the ONE symmetric owner of error translation for\n * this process, server side AND every client side. Consulted BEFORE webpieces' built-in mapping\n * in both directions. Call ONCE at startup, on the server AND in the browser, mirroring\n * {@link ClientRegistry.addMapping}.\n *\n * `set`, not `add`: see the field doc above for why a registry LIST is not wanted.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setErrorTranslator(translator: ErrorTranslator): void {\n ClientRegistry.errorTranslator = translator;\n }\n\n /**\n * THE {@link ErrorTranslator} for this process — never `undefined`, so a caller never branches.\n *\n * Until an app calls {@link ClientRegistry.setErrorTranslator} this is\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}. A registered translator answers EVERY error and\n * EVERY response, declining by delegating to the webpieces default (see {@link ErrorTranslator}),\n * so every call site in the framework is one unconditional line.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static getErrorTranslator(): ErrorTranslator {\n return ClientRegistry.errorTranslator;\n }\n\n /**\n * Set the app/company DEFAULT error classifier — ONE per process, covering the server router and\n * all internal clients (it branches on {@link ApiMethodInfo.side}). Call ONCE at startup, on the\n * server AND in the browser. Overrides the webpieces built-in for calls no per-apiClass classifier\n * claims. See {@link ErrorClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDefaultErrorClassifier(classifier: ErrorClassifier): void {\n ClientRegistry.appDefaultErrorClassifier = classifier;\n }\n\n /**\n * Register a per-EXTERNAL-client error classifier, keyed by its\n * {@link KeyedErrorClassifier.apiClass} (e.g. 'FirestoreAdminClient'). Consulted BEFORE the\n * default tier for calls with that apiClass. Call ONCE at startup. See {@link ErrorClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addErrorClassifier(keyed: KeyedErrorClassifier): void {\n ClientRegistry.errorClassifiersByApiClass.set(keyed.apiClass, keyed.classifier);\n }\n\n /**\n * The {@link CallOutcome} of a thrown API-call error. Resolved most-specific-first, and ALWAYS\n * definitive:\n * 1. the per-apiClass classifier, if one is registered for `methodInfo.apiClass` — unless it defers;\n * 2. else the app default classifier — unless it defers;\n * 3. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.\n *\n * The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `CallRegistry`'s per-attempt\n * log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome {\n const perClient = ClientRegistry.errorClassifiersByApiClass.get(methodInfo.apiClass);\n if (perClient !== undefined) {\n const verdict = perClient.classify(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n const verdict = ClientRegistry.appDefaultErrorClassifier.classify(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n return WEBPIECES_DEFAULT_ERROR_CLASSIFIER.classify(error, methodInfo);\n }\n\n /**\n * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec\n * cannot leak its registrations into the next.\n *\n * It RESTORES the webpieces defaults rather than emptying the pluggable slots, which is the whole\n * reason it is not called `clear()` any more: `getErrorTranslator()` and the default error\n * classifier are non-optional now, so \"cleared\" is not a state they have. A spec that wants\n * webpieces' own behaviour back asks for exactly that.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static resetForTests(): void {\n ClientRegistry.mappings.clear();\n ClientRegistry.deriver = undefined;\n ClientRegistry.errorTranslator = WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n ClientRegistry.appDefaultErrorClassifier = WEBPIECES_DEFAULT_ERROR_CLASSIFIER;\n ClientRegistry.errorClassifiersByApiClass.clear();\n }\n}\n"]}
1
+ {"version":3,"file":"ClientRegistry.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ClientRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import { ErrorTranslator } from './ErrorTranslator';\nimport { CallOutcome, ErrorClassifier } from './ErrorClassifier';\nimport { CallScope, CallScopeLadder } from './CallScope';\nimport { WEBPIECES_DEFAULT_ERROR_CLASSIFIER } from './WebpiecesDefaultErrorClassifier';\nimport { WEBPIECES_DEFAULT_ERROR_TRANSLATOR } from './WebpiecesDefaultErrorTranslator';\nimport { ApiMethodInfo } from './ApiMethodInfo';\n\n/** One {@link CallScope} rung of the error-translator ladder. Data only, so a class. */\nclass TranslatorSlot {\n translator?: ErrorTranslator;\n}\n\n/** One {@link CallScope} rung of the error-classifier ladder. Data only, so a class. */\nclass ClassifierSlot {\n classifier?: ErrorClassifier;\n}\n\n/**\n * Derives a base URL for a service name that has NO registered mapping — the pluggable half of\n * {@link ClientRegistry} resolution. One per environment, installed once at startup:\n *\n * - `gcpCloudRunDeriver()` (@webpieces/gcp-identity) — the Cloud Run formula, read from the metadata\n * server. For code running ON GCP.\n * - `gcpCloudRunDeriver(new GcpCloudRunTarget(projectNumber, region))` — the SAME formula with the\n * values supplied from config, for code running OFF GCP that still calls Cloud Run (a CLI, CI).\n * - {@link templateDeriver} (this package, browser-safe) — pure string substitution, for AWS or\n * anything else with predictable DNS.\n * - none at all — a browser goes same-origin, and localhost/tests hand-register their mappings.\n */\nexport type ServiceUrlDeriver = (svcName: string) => Promise<string>;\n\n/**\n * ClientRegistry - the ONE place a `svcName` becomes a base URL, for every outbound client in every\n * environment. A client is built once but a URL is per-environment, so the URL belongs here rather\n * than on the client: clients carry ONLY a svcName.\n *\n * Resolution is one precedence chain, identical in the browser, in node, and in Cloud Tasks:\n *\n * 1. a registered mapping wins — the localhost port table, AWS, an external API, another region\n * or project, a host that is not Cloud Run at all. Populated at startup from per-env config.\n * 2. else the installed {@link ServiceUrlDeriver}, if any — GCP's built-in, a DNS template, or\n * your own. OPTIONAL on purpose: localhost is inherently a TABLE (helper-fsdb -> :8401,\n * helper-portal -> :8201 have per-service ports), so explicit mappings must stay sufficient on\n * their own.\n * 3. else the caller's fallback: the BROWSER goes relative (same origin — see\n * {@link ClientRegistry.tryResolve}), while node THROWS (see {@link ClientRegistry.resolve}),\n * because a server has no \"own origin\" and an unresolvable peer is a setup bug.\n *\n * Configured like {@link HeaderRegistry} / LogManager — populated once at startup, then globally\n * accessible with NO DI wiring. It is browser-safe (no `process.env`, no node-only deps), which is\n * why it lives in core-util rather than gcp-identity.\n *\n * A `svcName` should be the nx MODULE name of the service you are calling — that is the one name\n * `architecture:validate-runtime-architecture` can verify actually exists, so a typo or a rename\n * fails the build instead of failing in production. When the DEPLOYED name differs (an environment\n * prefix like `tf-`, or a Cloud Run service named unlike its module), translate it in ONE place —\n * the deriver below, or a `serviceName` alias in the module's project.json — never at the call site.\n *\n * ```ts\n * // startup, from the current environment's config:\n * ClientRegistry.addMapping('server2', 8202); // -> http://localhost:8202\n * ClientRegistry.addUrlMapping('email', 'https://email.other-region.example');\n * ClientRegistry.setDeriver(gcpCloudRunDeriver()); // everything else, on GCP\n * ```\n */\nexport class ClientRegistry {\n /** svcName -> resolved base URL. Process-global; populated at startup per environment. */\n private static readonly mappings = new Map<string, string>();\n\n /** The fallback for svcNames with no mapping. Undefined = no derivation in this environment. */\n private static deriver: ServiceUrlDeriver | undefined;\n\n /**\n * The {@link ErrorTranslator} per {@link CallScope} — method -> API -> ALL, and below every rung\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, so {@link ClientRegistry.getErrorTranslator} is\n * never undefined. Process-global, set once at startup on the SERVER and in the BROWSER — the\n * same no-DI pattern as {@link ClientRegistry.mappings} above.\n *\n * ONE translator per scope, not a list, on purpose: an app that has several layers of error\n * policy for one scope composes them INSIDE its own `toWire`, where the precedence is written\n * down. The ladder exists because vendors put their errors on the wire in their OWN formats — a\n * per-API translator reads one vendor's 4xx body (`fromWire`), or writes the error body an\n * inbound vendor expects back (`toWire`), while every other contract keeps the app's ALL one.\n */\n private static readonly errorTranslators = new CallScopeLadder<TranslatorSlot>(\n () => new TranslatorSlot(),\n );\n\n /**\n * The {@link ErrorClassifier} per {@link CallScope} — method -> API -> ALL, and below every rung\n * {@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}. A classifier that DEFERS (`undefined`) falls through\n * to the next rung. The ALL classifier reads {@link ApiMethodInfo.side}, so one classifier covers\n * the server router AND every internal client; an SDK-wrapping api-client registers its own at\n * its abstract `XxxApi` class. See {@link ClientRegistry.classify}.\n */\n private static readonly errorClassifiers = new CallScopeLadder<ClassifierSlot>(\n () => new ClassifierSlot(),\n );\n\n /** Map a service name to `http://localhost:<port>`. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addMapping(svcName: string, port: number): void {\n ClientRegistry.mappings.set(svcName, `http://localhost:${port}`);\n }\n\n /**\n * Map a service name to an explicit base URL (any host / any environment).\n *\n * The EMPTY STRING is a legal, meaningful mapping: it makes the service relative, i.e.\n * same-origin, because the client builds its URL as `${baseUrl}${route.path}`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static addUrlMapping(svcName: string, url: string): void {\n ClientRegistry.mappings.set(svcName, url);\n }\n\n /**\n * Install the environment's {@link ServiceUrlDeriver} — how to resolve a svcName that has NO\n * mapping. Optional: an environment that registers every svcName it calls needs none.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setDeriver(fn: ServiceUrlDeriver): void {\n ClientRegistry.deriver = fn;\n }\n\n /** The registered override for `svcName`, or undefined if none. Registry only — no derivation. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryLookup(svcName: string): string | undefined {\n return ClientRegistry.mappings.get(svcName);\n }\n\n /**\n * Resolve `svcName` to its registered base URL. THROWS if the service was never registered.\n * Registry only — it does NOT consult the deriver; prefer {@link ClientRegistry.resolve}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static lookup(svcName: string): string {\n const url = ClientRegistry.tryLookup(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL registered for service \"${svcName}\". Register it at startup: ` +\n `ClientRegistry.addMapping(svcName, port) for a localhost port, or ` +\n `ClientRegistry.addUrlMapping(svcName, url) for an explicit URL.`,\n );\n }\n return url;\n }\n\n /**\n * Steps 1 + 2 of the chain: the registered mapping, else the installed deriver, else undefined.\n * Non-throwing — the BROWSER uses this and treats undefined as \"\" (relative → same origin),\n * which is what a browser app calling the backend it was served from wants by default.\n *\n * Note the `!== undefined` guard: an empty-string mapping is a legal answer (\"this service is\n * same-origin\"), so it must NOT fall through to derivation.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static tryResolve(svcName: string): Promise<string | undefined> {\n const override = ClientRegistry.tryLookup(svcName);\n if (override !== undefined) {\n return Promise.resolve(override);\n }\n if (!ClientRegistry.deriver) {\n return Promise.resolve(undefined);\n }\n return ClientRegistry.deriver(svcName);\n }\n\n /**\n * The full chain, with node's fallback: mapping, else deriver, else THROW. A server has no \"own\n * origin\" to go relative to, so an unresolvable peer is a setup bug and must fail loudly — and\n * say how to fix it.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static async resolve(svcName: string): Promise<string> {\n const url = await ClientRegistry.tryResolve(svcName);\n if (url === undefined) {\n throw new Error(\n `No URL for service \"${svcName}\".\\n` +\n ` - localhost/AWS: ClientRegistry.addMapping('${svcName}', 8401)\\n` +\n ` or ClientRegistry.addUrlMapping('${svcName}', 'https://...')\\n` +\n ` - GCP: install a deriver — ClientRegistry.setDeriver(gcpCloudRunDeriver())\\n` +\n ` - deployed name differs from the module name (e.g. a 'tf-' prefix)? Translate it ONCE\\n` +\n ` in the deriver — setDeriver(s => gcpCloudRunDeriver()('tf-' + s)) — so every call\\n` +\n ` site keeps naming the MODULE, which architecture:validate-runtime-architecture checks`,\n );\n }\n return url;\n }\n\n /**\n * Install an {@link ErrorTranslator} for one {@link CallScope}: `'ALL'` (the app's translator,\n * server side AND every client side), one contract CLASS, or one method of one contract. The\n * scope is REQUIRED — there is no unscoped spelling. Call at startup, on the server AND in the\n * browser, mirroring {@link ClientRegistry.addMapping}.\n *\n * ```ts\n * ClientRegistry.setErrorTranslator(new AppErrorTranslator(), 'ALL');\n * ClientRegistry.setErrorTranslator(new VendorErrorTranslator(), VendorApi); // one contract\n * ClientRegistry.setErrorTranslator(new LegacyErrorTranslator(), VendorApi, 'sync'); // one method\n * ```\n *\n * `set`, not `add`: see the field doc above for why a per-scope LIST is not wanted. A translator\n * declines an error by DELEGATING to {@link WebpiecesDefaultErrorTranslator} — not to the next\n * rung — exactly as before.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setErrorTranslator(translator: ErrorTranslator, ...scope: CallScope): void {\n ClientRegistry.errorTranslators.at(...scope).translator = translator;\n }\n\n /**\n * THE {@link ErrorTranslator} for one call — method -> API -> ALL ->\n * {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR}, never `undefined`, so a caller never branches.\n *\n * Both arguments are required so every call site states what it knows: the client and the server\n * pass the route's contract class and method; a failure with no route (an unmatched request, a\n * failure before routing) passes `undefined, undefined` and reaches the ALL rung alone.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static getErrorTranslator(api: Function | undefined, methodName: string | undefined): ErrorTranslator {\n const registered = ClientRegistry.errorTranslators\n .rungs(api, methodName)\n .find((slot: TranslatorSlot) => slot.translator !== undefined)?.translator;\n return registered ?? WEBPIECES_DEFAULT_ERROR_TRANSLATOR;\n }\n\n /**\n * Install an {@link ErrorClassifier} for one {@link CallScope}: `'ALL'` (the app/company\n * classifier — it reads {@link ApiMethodInfo.side}, so it covers the server router and every\n * internal client), one contract CLASS (an SDK-wrapping api-client keys by its abstract `XxxApi`),\n * or one method of one contract. The scope is REQUIRED. Call at startup, on the server AND in the\n * browser. See {@link ErrorClassifier}.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static setErrorClassifier(classifier: ErrorClassifier, ...scope: CallScope): void {\n ClientRegistry.errorClassifiers.at(...scope).classifier = classifier;\n }\n\n /**\n * The {@link CallOutcome} of a thrown API-call error. Resolved on the {@link CallScope} ladder of\n * `methodInfo.contract` / `methodInfo.methodName`, most specific first, and ALWAYS definitive:\n * 1. the method classifier — unless it defers;\n * 2. else the API classifier — unless it defers;\n * 3. else the ALL classifier — unless it defers;\n * 4. else the webpieces built-in ({@link WEBPIECES_DEFAULT_ERROR_CLASSIFIER}), which never defers.\n *\n * The ONE verdict read by `RetryStrategy` (retry only TRANSIENT), `CallRegistry`'s per-attempt\n * log line, `LogApiCall` (EXPECTED → OTHER, else FAIL) and the MCP renderer's retry advice.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome {\n const rungs = ClientRegistry.errorClassifiers.rungs(methodInfo.contract, methodInfo.methodName);\n for (const slot of rungs) {\n const verdict = slot.classifier?.classify(error, methodInfo);\n if (verdict !== undefined) {\n return verdict;\n }\n }\n return WEBPIECES_DEFAULT_ERROR_CLASSIFIER.classify(error, methodInfo);\n }\n\n /**\n * Put the process-globals back to the state a fresh process starts in. For TESTS, so one spec\n * cannot leak its registrations into the next.\n *\n * It empties every translator and classifier rung, which RESTORES the webpieces defaults:\n * `getErrorTranslator(...)` and `classify(...)` fall through to them below every rung, so\n * \"cleared\" is not a state they have. A spec that wants webpieces' own behaviour back asks for\n * exactly that.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/LogManager); populated once at startup, never DI-injected\n static resetForTests(): void {\n ClientRegistry.mappings.clear();\n ClientRegistry.deriver = undefined;\n ClientRegistry.errorTranslators.clear();\n ClientRegistry.errorClassifiers.clear();\n }\n}\n"]}
@@ -26,39 +26,32 @@ export declare enum CallOutcome {
26
26
  * rather than a bare function — so "find usages" in an IDE lands on every implementation.
27
27
  *
28
28
  * Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once
29
- * singleton that owns URL mappings and the {@link ErrorTranslator}. Resolved most-specific first by
30
- * {@link ClientRegistry.classify}:
29
+ * singleton that owns URL mappings and the {@link ErrorTranslator} — with
30
+ * `ClientRegistry.setErrorClassifier(c, ...scope)`, on the same `CallScope` ladder as
31
+ * `CallRegistry`'s strategy and timeout. {@link ClientRegistry.classify} resolves it most specific
32
+ * first:
31
33
  *
32
- * 1. `ClientRegistry.addErrorClassifier(new KeyedErrorClassifier(apiClass, c))` — per EXTERNAL
33
- * client, keyed by {@link ApiMethodInfo.apiClass} ('FirestoreAdminClient', 'ClaudeApi',
34
- * 'TwilioApi'). Each external API qualifies its errors differently (a Firestore 404 miss is
35
- * EXPECTED; a Twilio 429 is TRANSIENT), so it overrides the tiers below for that apiClass only.
36
- * 2. `ClientRegistry.setDefaultErrorClassifier(c)` — ONE per app/company. It reads
37
- * {@link ApiMethodInfo.side}, so a single classifier covers the SERVER router AND every INTERNAL
38
- * client (webpieces http client, cloud tasks).
39
- * 3. {@link WebpiecesDefaultErrorClassifier} — the terminal tier, which never defers.
34
+ * 1. `setErrorClassifier(c, XxxApi, 'method')` — one method of one contract.
35
+ * 2. `setErrorClassifier(c, XxxApi)` — one contract, keyed by its CLASS (never a name). An
36
+ * SDK-wrapping api-client keys by its abstract `XxxApi`: each external API qualifies its errors
37
+ * differently (a document store's 404 miss is EXPECTED; a messaging vendor's 429 is TRANSIENT),
38
+ * so it overrides the rungs below for that contract only.
39
+ * 3. `setErrorClassifier(c, 'ALL')` — ONE per app/company. It reads {@link ApiMethodInfo.side}, so a
40
+ * single classifier covers the SERVER router AND every INTERNAL client (webpieces http client,
41
+ * cloud tasks).
42
+ * 4. {@link WebpiecesDefaultErrorClassifier} — the terminal rung, which never defers.
40
43
  *
41
- * Internal clients and the server register NOTHING — the ABSENCE of an apiClass entry is the signal.
42
- * An external client that forgets to register is FAIL-SAFE: it falls to the tiers below, where every
43
- * client-side error except a 266 is a failure.
44
+ * Internal clients and the server register NOTHING per contract — the ABSENCE of a contract rung is
45
+ * the signal. An external client that forgets to register is FAIL-SAFE: it falls to the rungs below,
46
+ * where every client-side error except a 266 is a failure.
44
47
  */
45
48
  export interface ErrorClassifier {
46
49
  /**
47
50
  * @param error - the already-normalized thrown error (callers pass `toError(err)`)
48
- * @param methodInfo - the call identity; `side` distinguishes server/client, `apiClass` the client
49
- * @returns the outcome, or `undefined` to DEFER to the next tier (a per-apiClass classifier defers
50
- * to the app default; the app default defers to the webpieces built-in). Deferring is what
51
- * makes classifiers additive AND override-capable.
51
+ * @param methodInfo - the call identity; `side` distinguishes server/client, `contract` the API
52
+ * @returns the outcome, or `undefined` to DEFER to the next rung (method defers to API, API to
53
+ * ALL, ALL to the webpieces built-in). Deferring is what makes classifiers additive AND
54
+ * override-capable.
52
55
  */
53
56
  classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome | undefined;
54
57
  }
55
- /**
56
- * A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the
57
- * {@link ErrorClassifier} to apply for it. Data only, so it is a class (not an inline object literal),
58
- * per CLAUDE.md — handed to {@link ClientRegistry.addErrorClassifier}.
59
- */
60
- export declare class KeyedErrorClassifier {
61
- readonly apiClass: string;
62
- readonly classifier: ErrorClassifier;
63
- constructor(apiClass: string, classifier: ErrorClassifier);
64
- }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.KeyedErrorClassifier = exports.CallOutcome = void 0;
3
+ exports.CallOutcome = void 0;
4
4
  /**
5
5
  * The ONE verdict on a thrown API-call error. Three readers share it, so there is exactly one table
6
6
  * answering "is this error expected, and is it worth repeating?":
@@ -22,18 +22,4 @@ var CallOutcome;
22
22
  /** Broken, and repeating will not help: a RECEIVED 4xx (our bug), implementation, 501, retries exhausted. */
23
23
  CallOutcome["FAILURE"] = "failure";
24
24
  })(CallOutcome || (exports.CallOutcome = CallOutcome = {}));
25
- /**
26
- * A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the
27
- * {@link ErrorClassifier} to apply for it. Data only, so it is a class (not an inline object literal),
28
- * per CLAUDE.md — handed to {@link ClientRegistry.addErrorClassifier}.
29
- */
30
- class KeyedErrorClassifier {
31
- apiClass;
32
- classifier;
33
- constructor(apiClass, classifier) {
34
- this.apiClass = apiClass;
35
- this.classifier = classifier;
36
- }
37
- }
38
- exports.KeyedErrorClassifier = KeyedErrorClassifier;
39
25
  //# sourceMappingURL=ErrorClassifier.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ErrorClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorClassifier.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;GAWG;AACH,IAAY,WAOX;AAPD,WAAY,WAAW;IACnB,qGAAqG;IACrG,oCAAqB,CAAA;IACrB,qGAAqG;IACrG,8CAA+B,CAAA;IAC/B,6GAA6G;IAC7G,kCAAmB,CAAA;AACvB,CAAC,EAPW,WAAW,2BAAX,WAAW,QAOtB;AAoCD;;;;GAIG;AACH,MAAa,oBAAoB;IAET;IACA;IAFpB,YACoB,QAAgB,EAChB,UAA2B;QAD3B,aAAQ,GAAR,QAAQ,CAAQ;QAChB,eAAU,GAAV,UAAU,CAAiB;IAC5C,CAAC;CACP;AALD,oDAKC","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * The ONE verdict on a thrown API-call error. Three readers share it, so there is exactly one table\n * answering \"is this error expected, and is it worth repeating?\":\n *\n * - `RetryStrategy` retries only {@link CallOutcome.TRANSIENT_FAILURE} (and only on a `READ` /\n * `WRITE_IDEMPOTENT` endpoint — the ENDPOINT's safety is `CallContext.operation`'s question, never\n * the classifier's);\n * - `LogApiCall` logs {@link CallOutcome.EXPECTED} as `[API-*-resp-OTHER]` (`result='success'`) and\n * both failure outcomes as `[API-*-resp-FAIL]` (`result='failure'`);\n * - the MCP tool-call renderer derives its model-visible `retry` advice from the outcome plus the\n * endpoint's operation.\n */\nexport enum CallOutcome {\n /** It threw, but things are WORKING: a 266, a server-side 4xx, an external client's \"404 = miss\". */\n EXPECTED = 'expected',\n /** Broken NOW, may work if repeated: unavailable, connection, timeouts, 429/backoff, bad-gateway. */\n TRANSIENT_FAILURE = 'transient',\n /** Broken, and repeating will not help: a RECEIVED 4xx (our bug), implementation, 501, retries exhausted. */\n FAILURE = 'failure',\n}\n\n/**\n * Classifies a thrown API-call error into a {@link CallOutcome}.\n *\n * This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method\n * rather than a bare function — so \"find usages\" in an IDE lands on every implementation.\n *\n * Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once\n * singleton that owns URL mappings and the {@link ErrorTranslator}. Resolved most-specific first by\n * {@link ClientRegistry.classify}:\n *\n * 1. `ClientRegistry.addErrorClassifier(new KeyedErrorClassifier(apiClass, c))` — per EXTERNAL\n * client, keyed by {@link ApiMethodInfo.apiClass} ('FirestoreAdminClient', 'ClaudeApi',\n * 'TwilioApi'). Each external API qualifies its errors differently (a Firestore 404 miss is\n * EXPECTED; a Twilio 429 is TRANSIENT), so it overrides the tiers below for that apiClass only.\n * 2. `ClientRegistry.setDefaultErrorClassifier(c)` — ONE per app/company. It reads\n * {@link ApiMethodInfo.side}, so a single classifier covers the SERVER router AND every INTERNAL\n * client (webpieces http client, cloud tasks).\n * 3. {@link WebpiecesDefaultErrorClassifier} — the terminal tier, which never defers.\n *\n * Internal clients and the server register NOTHING — the ABSENCE of an apiClass entry is the signal.\n * An external client that forgets to register is FAIL-SAFE: it falls to the tiers below, where every\n * client-side error except a 266 is a failure.\n */\nexport interface ErrorClassifier {\n /**\n * @param error - the already-normalized thrown error (callers pass `toError(err)`)\n * @param methodInfo - the call identity; `side` distinguishes server/client, `apiClass` the client\n * @returns the outcome, or `undefined` to DEFER to the next tier (a per-apiClass classifier defers\n * to the app default; the app default defers to the webpieces built-in). Deferring is what\n * makes classifiers additive AND override-capable.\n */\n classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome | undefined;\n}\n\n/**\n * A per-external-client registration pair: the {@link ApiMethodInfo.apiClass} to key on, and the\n * {@link ErrorClassifier} to apply for it. Data only, so it is a class (not an inline object literal),\n * per CLAUDE.md — handed to {@link ClientRegistry.addErrorClassifier}.\n */\nexport class KeyedErrorClassifier {\n constructor(\n public readonly apiClass: string,\n public readonly classifier: ErrorClassifier,\n ) {}\n}\n"]}
1
+ {"version":3,"file":"ErrorClassifier.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/ErrorClassifier.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;GAWG;AACH,IAAY,WAOX;AAPD,WAAY,WAAW;IACnB,qGAAqG;IACrG,oCAAqB,CAAA;IACrB,qGAAqG;IACrG,8CAA+B,CAAA;IAC/B,6GAA6G;IAC7G,kCAAmB,CAAA;AACvB,CAAC,EAPW,WAAW,2BAAX,WAAW,QAOtB","sourcesContent":["import { ApiMethodInfo } from './ApiMethodInfo';\n\n/**\n * The ONE verdict on a thrown API-call error. Three readers share it, so there is exactly one table\n * answering \"is this error expected, and is it worth repeating?\":\n *\n * - `RetryStrategy` retries only {@link CallOutcome.TRANSIENT_FAILURE} (and only on a `READ` /\n * `WRITE_IDEMPOTENT` endpoint — the ENDPOINT's safety is `CallContext.operation`'s question, never\n * the classifier's);\n * - `LogApiCall` logs {@link CallOutcome.EXPECTED} as `[API-*-resp-OTHER]` (`result='success'`) and\n * both failure outcomes as `[API-*-resp-FAIL]` (`result='failure'`);\n * - the MCP tool-call renderer derives its model-visible `retry` advice from the outcome plus the\n * endpoint's operation.\n */\nexport enum CallOutcome {\n /** It threw, but things are WORKING: a 266, a server-side 4xx, an external client's \"404 = miss\". */\n EXPECTED = 'expected',\n /** Broken NOW, may work if repeated: unavailable, connection, timeouts, 429/backoff, bad-gateway. */\n TRANSIENT_FAILURE = 'transient',\n /** Broken, and repeating will not help: a RECEIVED 4xx (our bug), implementation, 501, retries exhausted. */\n FAILURE = 'failure',\n}\n\n/**\n * Classifies a thrown API-call error into a {@link CallOutcome}.\n *\n * This is BEHAVIOR, so it is an interface (per CLAUDE.md), passed as an object with a NAMED method\n * rather than a bare function — so \"find usages\" in an IDE lands on every implementation.\n *\n * Registered on {@link ClientRegistry} at startup — the same browser-safe, no-DI, populated-once\n * singleton that owns URL mappings and the {@link ErrorTranslator} — with\n * `ClientRegistry.setErrorClassifier(c, ...scope)`, on the same `CallScope` ladder as\n * `CallRegistry`'s strategy and timeout. {@link ClientRegistry.classify} resolves it most specific\n * first:\n *\n * 1. `setErrorClassifier(c, XxxApi, 'method')` — one method of one contract.\n * 2. `setErrorClassifier(c, XxxApi)` — one contract, keyed by its CLASS (never a name). An\n * SDK-wrapping api-client keys by its abstract `XxxApi`: each external API qualifies its errors\n * differently (a document store's 404 miss is EXPECTED; a messaging vendor's 429 is TRANSIENT),\n * so it overrides the rungs below for that contract only.\n * 3. `setErrorClassifier(c, 'ALL')` — ONE per app/company. It reads {@link ApiMethodInfo.side}, so a\n * single classifier covers the SERVER router AND every INTERNAL client (webpieces http client,\n * cloud tasks).\n * 4. {@link WebpiecesDefaultErrorClassifier} — the terminal rung, which never defers.\n *\n * Internal clients and the server register NOTHING per contract — the ABSENCE of a contract rung is\n * the signal. An external client that forgets to register is FAIL-SAFE: it falls to the rungs below,\n * where every client-side error except a 266 is a failure.\n */\nexport interface ErrorClassifier {\n /**\n * @param error - the already-normalized thrown error (callers pass `toError(err)`)\n * @param methodInfo - the call identity; `side` distinguishes server/client, `contract` the API\n * @returns the outcome, or `undefined` to DEFER to the next rung (method defers to API, API to\n * ALL, ALL to the webpieces built-in). Deferring is what makes classifiers additive AND\n * override-capable.\n */\n classify(error: Error, methodInfo: ApiMethodInfo): CallOutcome | undefined;\n}\n\n"]}
@@ -0,0 +1 @@
1
+ export {};