@apifuse/provider-sdk 2.2.0-beta.60 → 2.2.0-beta.62

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 (85) hide show
  1. package/AUTHORING.md +17 -16
  2. package/CHANGELOG.md +8 -0
  3. package/bin/apifuse-dev.ts +5 -7
  4. package/bin/apifuse-pack-types.ts +26 -0
  5. package/bin/apifuse-perf.ts +4 -6
  6. package/bin/apifuse-record.ts +4 -7
  7. package/bin/apifuse-submit-check.ts +2 -2
  8. package/dist/define.d.ts +3 -2
  9. package/dist/define.js +1 -1
  10. package/dist/dev.js +3 -1
  11. package/dist/engine.d.ts +7 -6
  12. package/dist/engine.js +3 -4
  13. package/dist/error-resolution.js +21 -4
  14. package/dist/errors.d.ts +3 -0
  15. package/dist/errors.js +9 -0
  16. package/dist/handle-meta.d.ts +55 -0
  17. package/dist/handle-meta.js +57 -0
  18. package/dist/handle.d.ts +208 -0
  19. package/dist/handle.js +299 -0
  20. package/dist/i18n/catalog.js +21 -1
  21. package/dist/index.d.ts +7 -4
  22. package/dist/index.js +6 -3
  23. package/dist/lint.d.ts +3 -0
  24. package/dist/lint.js +372 -47
  25. package/dist/provider.d.ts +5 -3
  26. package/dist/provider.js +4 -2
  27. package/dist/runtime/handle-wordlist.d.ts +43 -0
  28. package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
  29. package/dist/runtime/handle.d.ts +71 -0
  30. package/dist/runtime/handle.js +900 -0
  31. package/dist/runtime/http-telemetry-binding.d.ts +10 -0
  32. package/dist/runtime/http-telemetry-binding.js +152 -0
  33. package/dist/runtime/http-telemetry-guard.d.ts +6 -0
  34. package/dist/runtime/http-telemetry-guard.js +150 -0
  35. package/dist/runtime/http-telemetry.d.ts +104 -0
  36. package/dist/runtime/http-telemetry.js +190 -0
  37. package/dist/runtime/http.d.ts +31 -0
  38. package/dist/runtime/http.js +222 -146
  39. package/dist/runtime/native-network.d.ts +10 -1
  40. package/dist/runtime/native-network.js +230 -29
  41. package/dist/runtime/native-telemetry.d.ts +142 -0
  42. package/dist/runtime/native-telemetry.js +231 -0
  43. package/dist/runtime/state.d.ts +4 -1
  44. package/dist/runtime/state.js +32 -17
  45. package/dist/server/index.d.ts +3 -0
  46. package/dist/server/index.js +2 -0
  47. package/dist/server/sensitive-values.d.ts +1 -1
  48. package/dist/server/sensitive-values.js +0 -3
  49. package/dist/server/serve-implementation.d.ts +6 -11
  50. package/dist/server/serve-implementation.js +119 -52
  51. package/dist/testing/run.js +10 -7
  52. package/dist/types.d.ts +4 -107
  53. package/package.json +1 -1
  54. package/src/define.ts +4 -3
  55. package/src/dev.ts +3 -1
  56. package/src/engine.ts +5 -6
  57. package/src/error-resolution.ts +21 -4
  58. package/src/errors.ts +13 -0
  59. package/src/handle-meta.ts +93 -0
  60. package/src/handle.ts +612 -0
  61. package/src/i18n/catalog.ts +23 -1
  62. package/src/index.ts +45 -13
  63. package/src/lint.ts +437 -55
  64. package/src/provider.ts +19 -20
  65. package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
  66. package/src/runtime/handle.ts +1172 -0
  67. package/src/runtime/http-telemetry-binding.ts +162 -0
  68. package/src/runtime/http-telemetry-guard.ts +155 -0
  69. package/src/runtime/http-telemetry.ts +292 -0
  70. package/src/runtime/http.ts +295 -173
  71. package/src/runtime/native-network.ts +281 -33
  72. package/src/runtime/native-telemetry.ts +366 -0
  73. package/src/runtime/state.ts +44 -16
  74. package/src/server/index.ts +30 -0
  75. package/src/server/sensitive-values.ts +0 -3
  76. package/src/server/serve-implementation.ts +127 -89
  77. package/src/testing/run.ts +11 -7
  78. package/src/types.ts +4 -137
  79. package/dist/choice-token.d.ts +0 -24
  80. package/dist/choice-token.js +0 -74
  81. package/dist/runtime/choice-wordlist.d.ts +0 -9
  82. package/dist/runtime/choice.d.ts +0 -27
  83. package/dist/runtime/choice.js +0 -801
  84. package/src/choice-token.ts +0 -165
  85. package/src/runtime/choice.ts +0 -1197
@@ -0,0 +1,10 @@
1
+ import type { HttpClient } from "../types.js";
2
+ import { type HttpTelemetrySink } from "./http-telemetry.js";
3
+ /** Return whether a host binding has at least one callable declared HTTP method. */
4
+ export declare function isSupportedHttpBinding(value: unknown): value is HttpClient;
5
+ /** Emit the unsupported-shape warning once for the process. */
6
+ export declare function warnUnsupportedHttpBinding(): void;
7
+ /** Route SDK transports inside a host binding through the same attempt collector. */
8
+ export declare function scopedHttpTelemetry(existing: HttpTelemetrySink | undefined): HttpTelemetrySink | undefined;
9
+ /** Observe opaque host calls without changing their arguments, receiver or returned promise. */
10
+ export declare function bindHttpTelemetry(client: HttpClient, sink: HttpTelemetrySink): HttpClient;
@@ -0,0 +1,152 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { httpTelemetryErrorCode } from "./http-telemetry.js";
3
+ import { startHttpTelemetry } from "./http-telemetry-guard.js";
4
+ import { proxyTransportRetryErrorCode, proxyTransportRetryErrorStatus, } from "./proxy-retry-policy.js";
5
+ const scopes = new AsyncLocalStorage();
6
+ const methods = new Set(["request", "get", "post", "put", "delete", "stream", "sse"]);
7
+ const adapters = new WeakMap();
8
+ let unsupportedShapeWarned = false;
9
+ /** Return whether a host binding has at least one callable declared HTTP method. */
10
+ export function isSupportedHttpBinding(value) {
11
+ if ((typeof value !== "object" && typeof value !== "function") || value === null)
12
+ return false;
13
+ for (const method of methods) {
14
+ try {
15
+ if (typeof Reflect.get(value, method) === "function")
16
+ return true;
17
+ }
18
+ catch {
19
+ // A hostile property getter is not a usable binding.
20
+ }
21
+ }
22
+ return false;
23
+ }
24
+ /** Emit the unsupported-shape warning once for the process. */
25
+ export function warnUnsupportedHttpBinding() {
26
+ if (unsupportedShapeWarned)
27
+ return;
28
+ unsupportedShapeWarned = true;
29
+ try {
30
+ console.warn("[apifuse] http telemetry not attached; reason=unsupported_binding_shape");
31
+ }
32
+ catch {
33
+ // Warning output is diagnostic only and must not affect request handling.
34
+ }
35
+ }
36
+ /** Route SDK transports inside a host binding through the same attempt collector. */
37
+ export function scopedHttpTelemetry(existing) {
38
+ const scope = scopes.getStore();
39
+ if (!scope)
40
+ return existing;
41
+ scope.nativeObserved = true;
42
+ if (!existing || existing === scope.sink)
43
+ return scope.sink;
44
+ return {
45
+ markTelemetryFailed: () => scope.sink.markTelemetryFailed?.(),
46
+ startRequest(options) {
47
+ const request = startHttpTelemetry(scope.sink, options.retryPreset);
48
+ const observer = startHttpTelemetry({
49
+ startRequest: (options) => existing.startRequest(options),
50
+ markTelemetryFailed() {
51
+ scope.sink.markTelemetryFailed?.();
52
+ existing.markTelemetryFailed?.();
53
+ },
54
+ }, options.retryPreset);
55
+ return {
56
+ recordAttempt(event) {
57
+ request.recordAttempt(event);
58
+ observer.recordAttempt(event);
59
+ },
60
+ finish(ms) {
61
+ request.finish(ms);
62
+ observer.finish(ms);
63
+ },
64
+ toTenantRetryPayload() {
65
+ observer.toTenantRetryPayload();
66
+ return request.toTenantRetryPayload();
67
+ },
68
+ };
69
+ },
70
+ };
71
+ }
72
+ /** Observe opaque host calls without changing their arguments, receiver or returned promise. */
73
+ export function bindHttpTelemetry(client, sink) {
74
+ if ((typeof client === "object" && client !== null) || typeof client === "function") {
75
+ const existing = adapters.get(client);
76
+ if (existing) {
77
+ existing.sink = sink;
78
+ return existing.adapter;
79
+ }
80
+ }
81
+ const wrappers = new Map();
82
+ // An inheriting target also supports hosts whose own methods are frozen.
83
+ const adapter = new Proxy(Object.create(client), {
84
+ get(_target, property) {
85
+ const original = Reflect.get(client, property, client);
86
+ if (typeof property !== "string" || !methods.has(property) || typeof original !== "function")
87
+ return original;
88
+ const cached = wrappers.get(property);
89
+ if (cached?.original === original)
90
+ return cached.wrapped;
91
+ const wrapped = function (...args) {
92
+ // Direct calls keep the host receiver; explicit and unbound receivers pass through.
93
+ const receiver = this === adapter ? client : this;
94
+ const scope = { sink: state.sink, nativeObserved: false };
95
+ const started = performance.now();
96
+ const settled = (result, failed) => {
97
+ if (scope.nativeObserved)
98
+ return;
99
+ const request = startHttpTelemetry(state.sink, undefined);
100
+ request.observe(() => {
101
+ const status = failed
102
+ ? proxyTransportRetryErrorStatus(result)
103
+ : result &&
104
+ typeof result === "object" &&
105
+ "status" in result &&
106
+ typeof result.status === "number"
107
+ ? result.status
108
+ : undefined;
109
+ request.recordAttempt({
110
+ ms: performance.now() - started,
111
+ proxyUsed: false,
112
+ ...(status === undefined ? {} : { status }),
113
+ ...(failed
114
+ ? { e: httpTelemetryErrorCode(proxyTransportRetryErrorCode(result)) }
115
+ : {}),
116
+ });
117
+ });
118
+ request.finish(performance.now() - started);
119
+ };
120
+ return scopes.run(scope, () => {
121
+ let result;
122
+ try {
123
+ result = Reflect.apply(original, receiver, args);
124
+ }
125
+ catch (error) {
126
+ settled(error, true);
127
+ throw error;
128
+ }
129
+ try {
130
+ if (result instanceof Promise) {
131
+ void Promise.prototype.then.call(result, (value) => settled(value, false), (error) => settled(error, true));
132
+ }
133
+ else
134
+ settled(result, false);
135
+ }
136
+ catch (error) {
137
+ // Promise subclasses may throw while a telemetry continuation is attached.
138
+ startHttpTelemetry(state.sink, undefined).observe(() => {
139
+ throw error;
140
+ });
141
+ }
142
+ return result;
143
+ });
144
+ };
145
+ wrappers.set(property, { original, wrapped });
146
+ return wrapped;
147
+ },
148
+ });
149
+ const state = { sink, adapter };
150
+ adapters.set(adapter, state);
151
+ return adapter;
152
+ }
@@ -0,0 +1,6 @@
1
+ import type { HttpRetrySummary } from "../types.js";
2
+ import { type HttpTelemetryRequestSink, type HttpTelemetrySink } from "./http-telemetry.js";
3
+ /** Keep all public observers, including property access, outside transport failures. */
4
+ export declare function startHttpTelemetry(sink: HttpTelemetrySink | undefined, retryPreset: HttpRetrySummary["preset"]): HttpTelemetryRequestSink & {
5
+ observe(callback: () => void): void;
6
+ };
@@ -0,0 +1,150 @@
1
+ import { HttpTelemetryCollector, } from "./http-telemetry.js";
2
+ const intrinsicPromiseThen = Promise.prototype.then;
3
+ const ignoreObserverResult = () => { };
4
+ /** Attach handlers only through the Promise intrinsic; never invoke a host thenable. */
5
+ function absorbObserverPromise(value) {
6
+ let constructorDescriptor;
7
+ let shadowed = false;
8
+ try {
9
+ constructorDescriptor = Object.getOwnPropertyDescriptor(value, "constructor");
10
+ // Even the intrinsic `then` consults Symbol.species. An undefined constructor
11
+ // selects its intrinsic Promise without reading a subclass's species getter.
12
+ Object.defineProperty(value, "constructor", {
13
+ value: undefined,
14
+ configurable: constructorDescriptor?.configurable ?? true,
15
+ });
16
+ shadowed = true;
17
+ }
18
+ catch {
19
+ // Non-extensible ordinary promises can still accept intrinsic handlers.
20
+ }
21
+ try {
22
+ void intrinsicPromiseThen.call(value, ignoreObserverResult, ignoreObserverResult);
23
+ }
24
+ catch {
25
+ // A non-Promise thenable has no intrinsic rejection to absorb. Do not run it.
26
+ }
27
+ finally {
28
+ if (shadowed) {
29
+ try {
30
+ if (constructorDescriptor) {
31
+ Object.defineProperty(value, "constructor", constructorDescriptor);
32
+ }
33
+ else {
34
+ Reflect.deleteProperty(value, "constructor");
35
+ }
36
+ }
37
+ catch {
38
+ // Host object traps are also observer failures.
39
+ }
40
+ }
41
+ }
42
+ }
43
+ function isAsyncObserverReturn(value) {
44
+ if (!value || (typeof value !== "object" && typeof value !== "function"))
45
+ return false;
46
+ try {
47
+ if (!("then" in value) || typeof value.then !== "function")
48
+ return false;
49
+ }
50
+ catch {
51
+ // A rejected Promise can itself have a throwing `then` getter. Still absorb it.
52
+ }
53
+ absorbObserverPromise(value);
54
+ return true;
55
+ }
56
+ /** Keep all public observers, including property access, outside transport failures. */
57
+ export function startHttpTelemetry(sink, retryPreset) {
58
+ let failed = false;
59
+ const fail = () => {
60
+ if (failed)
61
+ return;
62
+ failed = true;
63
+ try {
64
+ isAsyncObserverReturn(sink?.markTelemetryFailed?.());
65
+ }
66
+ catch {
67
+ // The failure reporter is itself an observer. Never recurse.
68
+ }
69
+ };
70
+ const observe = (callback) => {
71
+ try {
72
+ const result = callback();
73
+ if (isAsyncObserverReturn(result)) {
74
+ // The observer API is synchronous. Absorb invalid asynchronous returns too.
75
+ fail();
76
+ return undefined;
77
+ }
78
+ return result;
79
+ }
80
+ catch {
81
+ fail();
82
+ return undefined;
83
+ }
84
+ };
85
+ const result = observe(() => (sink ?? new HttpTelemetryCollector()).startRequest({ retryPreset }));
86
+ const request = observe(() => {
87
+ if (!result ||
88
+ typeof result.recordAttempt !== "function" ||
89
+ typeof result.finish !== "function" ||
90
+ typeof result.toTenantRetryPayload !== "function") {
91
+ fail();
92
+ return undefined;
93
+ }
94
+ return result;
95
+ });
96
+ return {
97
+ observe,
98
+ recordAttempt: (event) => {
99
+ observe(() => request?.recordAttempt(event));
100
+ },
101
+ finish: (ms) => {
102
+ observe(() => request?.finish(ms));
103
+ },
104
+ toTenantRetryPayload: () => {
105
+ if (failed)
106
+ return undefined;
107
+ return observe(() => {
108
+ const value = observe(() => request?.toTenantRetryPayload());
109
+ if (value === undefined)
110
+ return undefined;
111
+ // Copy only validated primitive fields; never pass observer-owned objects on.
112
+ const { attempts, retries, preset, transport, lastErrorCode, lastStatus } = value;
113
+ if (!Number.isSafeInteger(attempts) ||
114
+ attempts < 2 ||
115
+ !Number.isSafeInteger(retries) ||
116
+ (retries !== attempts - 1 &&
117
+ !(attempts === Number.MAX_SAFE_INTEGER && retries === Number.MAX_SAFE_INTEGER)) ||
118
+ transport !== "native" ||
119
+ (preset !== undefined &&
120
+ ![
121
+ "off",
122
+ "transport_transient",
123
+ "safe_read",
124
+ "aggressive_read",
125
+ "rate_limit_aware",
126
+ ].includes(preset)) ||
127
+ (lastErrorCode !== undefined &&
128
+ ![
129
+ "transport_network_error",
130
+ "transport_timeout",
131
+ "transport_cancelled",
132
+ "upstream_http_error",
133
+ "other",
134
+ ].includes(lastErrorCode)) ||
135
+ (lastStatus !== undefined && (!Number.isSafeInteger(lastStatus) || lastStatus < 0))) {
136
+ fail();
137
+ return undefined;
138
+ }
139
+ return {
140
+ attempts,
141
+ retries,
142
+ ...(preset === undefined ? {} : { preset }),
143
+ transport,
144
+ ...(lastErrorCode === undefined ? {} : { lastErrorCode }),
145
+ ...(lastStatus === undefined ? {} : { lastStatus }),
146
+ };
147
+ });
148
+ },
149
+ };
150
+ }
@@ -0,0 +1,104 @@
1
+ import type { HttpRetrySummary } from "../types.js";
2
+ import { type ClosedEnum, type TelemetryContributor, type TenantNeutral } from "./request-telemetry.js";
3
+ /** The same closed vocabulary used by tenant-visible meta.retry. */
4
+ export type HttpTelemetryErrorCode = "transport_network_error" | "transport_timeout" | "transport_cancelled" | "upstream_http_error" | "other";
5
+ export declare function httpTelemetryErrorCode(code: string | undefined): HttpTelemetryErrorCode;
6
+ export type HttpTelemetryRetryPayload = {
7
+ attempts: number;
8
+ retries: number;
9
+ preset?: ClosedEnum<NonNullable<HttpRetrySummary["preset"]>>;
10
+ transport: ClosedEnum<HttpRetrySummary["transport"]>;
11
+ lastErrorCode?: ClosedEnum<HttpTelemetryErrorCode>;
12
+ /** Last failed attempt's status, preserving the existing meta.retry contract. */
13
+ lastStatus?: number;
14
+ };
15
+ export type HttpAttemptTelemetryEvent = {
16
+ ms: number;
17
+ proxyUsed: boolean;
18
+ status?: number;
19
+ e?: HttpTelemetryErrorCode;
20
+ /** A returned status-retry outcome, rather than a thrown transport error. */
21
+ statusRetry?: boolean;
22
+ /** Operator-only error text. Never projected into the header or tenant metadata. */
23
+ diagnostics?: {
24
+ name: string;
25
+ message: string;
26
+ cause?: {
27
+ name: string;
28
+ message: string;
29
+ };
30
+ };
31
+ };
32
+ export interface HttpTelemetryRequestSink {
33
+ recordAttempt(event: HttpAttemptTelemetryEvent): void;
34
+ /** Called once when the logical HTTP call settles; includes backoff time. */
35
+ finish(ms: number): void;
36
+ toTenantRetryPayload(): TenantNeutral<HttpTelemetryRetryPayload> | undefined;
37
+ }
38
+ export interface HttpTelemetrySink {
39
+ /** Optional, best-effort failure marker; called at most once per logical request. */
40
+ markTelemetryFailed?(): void;
41
+ /** Each call gets independent retry state, including concurrent calls. */
42
+ startRequest(options: {
43
+ retryPreset?: HttpRetrySummary["preset"];
44
+ }): HttpTelemetryRequestSink;
45
+ }
46
+ export type HttpAttemptSample = {
47
+ n: number;
48
+ ms: number;
49
+ status?: number;
50
+ e?: HttpTelemetryErrorCode;
51
+ diagnostics?: HttpAttemptTelemetryEvent["diagnostics"];
52
+ };
53
+ export type HttpTelemetryLogPayload = {
54
+ /** Observer failure; operator-only and never projected into the header. */
55
+ telemetryFailed?: true;
56
+ attempts: number;
57
+ retries: number;
58
+ timeouts: number;
59
+ proxyUsed: boolean;
60
+ /** Most recently completed attempt's status, including successful statuses. */
61
+ lastStatus?: number;
62
+ retryPreset?: HttpRetrySummary["preset"];
63
+ transport: HttpRetrySummary["transport"];
64
+ lastErrorCode?: HttpTelemetryErrorCode;
65
+ attemptSamples: HttpAttemptSample[];
66
+ dropped: number;
67
+ /** Sum of logical call durations, including retry delays. */
68
+ ms: number;
69
+ /** Most recent successful retried call; also the source of tenant meta.retry. */
70
+ retry?: HttpTelemetryRetryPayload;
71
+ };
72
+ export type HttpTelemetryHeaderPayload = {
73
+ attempts: number;
74
+ retries: number;
75
+ timeouts: number;
76
+ proxyUsed: boolean;
77
+ lastStatus?: number;
78
+ retryPreset?: ClosedEnum<NonNullable<HttpRetrySummary["preset"]>>;
79
+ transport: ClosedEnum<HttpRetrySummary["transport"]>;
80
+ lastErrorCode?: ClosedEnum<HttpTelemetryErrorCode>;
81
+ attemptSamples: {
82
+ n: number;
83
+ ms: number;
84
+ status?: number;
85
+ e?: ClosedEnum<HttpTelemetryErrorCode>;
86
+ }[];
87
+ dropped: number;
88
+ ms: number;
89
+ retry?: HttpTelemetryRetryPayload;
90
+ };
91
+ export declare class HttpTelemetryCollector implements HttpTelemetrySink, TelemetryContributor<HttpTelemetryLogPayload, HttpTelemetryHeaderPayload> {
92
+ #private;
93
+ readonly key: "http";
94
+ constructor(options?: {
95
+ redact?: (text: string) => string;
96
+ });
97
+ markTelemetryFailed(): void;
98
+ startRequest(options: {
99
+ retryPreset?: HttpRetrySummary["preset"];
100
+ }): HttpTelemetryRequestSink;
101
+ toTenantRetryPayload(): TenantNeutral<HttpTelemetryRetryPayload> | undefined;
102
+ toLogPayload(): HttpTelemetryLogPayload | undefined;
103
+ toHeaderPayload(log: HttpTelemetryLogPayload): HttpTelemetryHeaderPayload;
104
+ }
@@ -0,0 +1,190 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { closedEnum, } from "./request-telemetry.js";
3
+ export function httpTelemetryErrorCode(code) {
4
+ switch (code) {
5
+ case "transport_network_error":
6
+ case "transport_timeout":
7
+ case "transport_cancelled":
8
+ case "upstream_http_error":
9
+ return code;
10
+ default:
11
+ return "other";
12
+ }
13
+ }
14
+ const MAX_ATTEMPT_SAMPLES = 24;
15
+ const MAX_DIAGNOSTIC_TEXT = 300;
16
+ function integer(value) {
17
+ return Math.max(0, Math.floor(Number.isFinite(value) ? value : 0));
18
+ }
19
+ function saturatingAdd(a, b) {
20
+ if (!Number.isFinite(a) || !Number.isFinite(b))
21
+ return Number.MAX_SAFE_INTEGER;
22
+ return Math.min(Number.MAX_SAFE_INTEGER, integer(a) + integer(b));
23
+ }
24
+ function boundedText(value, redact) {
25
+ let result;
26
+ try {
27
+ result = redact?.(value) ?? value;
28
+ }
29
+ catch {
30
+ return "[REDACTION_FAILED]";
31
+ }
32
+ // Detach caller text through independent UTF-16 storage, preserving lone surrogates.
33
+ const length = Math.min(result.length, MAX_DIAGNOSTIC_TEXT);
34
+ const units = Buffer.alloc(length * 2);
35
+ for (let index = 0; index < length; index++)
36
+ units.writeUInt16LE(result.charCodeAt(index), index * 2);
37
+ return units.toString("utf16le");
38
+ }
39
+ export class HttpTelemetryCollector {
40
+ key = "http";
41
+ #redact;
42
+ #telemetryFailed = false;
43
+ #attempts = 0;
44
+ #retries = 0;
45
+ #timeouts = 0;
46
+ #proxyUsed = false;
47
+ #lastStatus;
48
+ #retryPreset;
49
+ #lastErrorCode;
50
+ #samples = [];
51
+ #dropped = 0;
52
+ #ms = 0;
53
+ #retry;
54
+ constructor(options = {}) {
55
+ this.#redact = options.redact;
56
+ }
57
+ markTelemetryFailed() {
58
+ this.#telemetryFailed = true;
59
+ }
60
+ startRequest(options) {
61
+ const preset = options.retryPreset;
62
+ let attempts = 0;
63
+ let retries = 0;
64
+ let lastErrorCode;
65
+ let lastStatus;
66
+ let retry;
67
+ let finished = false;
68
+ return {
69
+ recordAttempt: (event) => {
70
+ if (attempts > 0) {
71
+ retries = saturatingAdd(retries, 1);
72
+ this.#retries = saturatingAdd(this.#retries, 1);
73
+ }
74
+ attempts = saturatingAdd(attempts, 1);
75
+ this.#attempts = saturatingAdd(this.#attempts, 1);
76
+ if (event.e === "transport_timeout")
77
+ this.#timeouts = saturatingAdd(this.#timeouts, 1);
78
+ this.#proxyUsed ||= event.proxyUsed;
79
+ this.#retryPreset = preset;
80
+ if (event.status !== undefined)
81
+ this.#lastStatus = integer(event.status);
82
+ if (event.e)
83
+ this.#lastErrorCode = event.e;
84
+ if (this.#samples.length < MAX_ATTEMPT_SAMPLES) {
85
+ const text = (value) => boundedText(value, this.#redact);
86
+ this.#samples.push({
87
+ n: attempts,
88
+ ms: saturatingAdd(0, event.ms),
89
+ ...(event.status === undefined ? {} : { status: integer(event.status) }),
90
+ ...(event.e ? { e: event.e } : {}),
91
+ ...(event.diagnostics
92
+ ? {
93
+ diagnostics: {
94
+ name: text(event.diagnostics.name),
95
+ message: text(event.diagnostics.message),
96
+ ...(event.diagnostics.cause
97
+ ? {
98
+ cause: {
99
+ name: text(event.diagnostics.cause.name),
100
+ message: text(event.diagnostics.cause.message),
101
+ },
102
+ }
103
+ : {}),
104
+ },
105
+ }
106
+ : {}),
107
+ });
108
+ }
109
+ else
110
+ this.#dropped = saturatingAdd(this.#dropped, 1);
111
+ if (event.e) {
112
+ lastStatus = event.status;
113
+ // Historically status-retry outcomes set lastStatus only. Preserve those pins.
114
+ if (!event.statusRetry)
115
+ lastErrorCode = event.e;
116
+ }
117
+ else if (attempts > 1) {
118
+ retry = {
119
+ attempts,
120
+ retries,
121
+ ...(preset ? { preset: closedEnum(preset) } : {}),
122
+ transport: closedEnum("native"),
123
+ ...(lastErrorCode ? { lastErrorCode: closedEnum(lastErrorCode) } : {}),
124
+ ...(lastStatus ? { lastStatus: integer(lastStatus) } : {}),
125
+ };
126
+ this.#retry = retry;
127
+ }
128
+ },
129
+ finish: (ms) => {
130
+ if (!finished && attempts > 0)
131
+ this.#ms = saturatingAdd(this.#ms, ms);
132
+ finished = true;
133
+ },
134
+ toTenantRetryPayload: () => (!this.#telemetryFailed && retry ? { ...retry } : undefined),
135
+ };
136
+ }
137
+ toTenantRetryPayload() {
138
+ return !this.#telemetryFailed && this.#retry ? { ...this.#retry } : undefined;
139
+ }
140
+ toLogPayload() {
141
+ if (this.#attempts === 0 && !this.#telemetryFailed)
142
+ return undefined;
143
+ return {
144
+ ...(this.#telemetryFailed ? { telemetryFailed: true } : {}),
145
+ attempts: this.#attempts,
146
+ retries: this.#retries,
147
+ timeouts: this.#timeouts,
148
+ proxyUsed: this.#proxyUsed,
149
+ ...(this.#lastStatus === undefined ? {} : { lastStatus: this.#lastStatus }),
150
+ ...(this.#retryPreset ? { retryPreset: this.#retryPreset } : {}),
151
+ transport: "native",
152
+ ...(this.#lastErrorCode ? { lastErrorCode: this.#lastErrorCode } : {}),
153
+ attemptSamples: this.#samples.map((sample) => ({
154
+ ...sample,
155
+ ...(sample.diagnostics
156
+ ? {
157
+ diagnostics: {
158
+ ...sample.diagnostics,
159
+ ...(sample.diagnostics.cause ? { cause: { ...sample.diagnostics.cause } } : {}),
160
+ },
161
+ }
162
+ : {}),
163
+ })),
164
+ dropped: this.#dropped,
165
+ ms: this.#ms,
166
+ ...(this.#retry ? { retry: { ...this.#retry } } : {}),
167
+ };
168
+ }
169
+ toHeaderPayload(log) {
170
+ return {
171
+ attempts: log.attempts,
172
+ retries: log.retries,
173
+ timeouts: log.timeouts,
174
+ proxyUsed: log.proxyUsed,
175
+ ...(log.lastStatus === undefined ? {} : { lastStatus: log.lastStatus }),
176
+ ...(log.retryPreset ? { retryPreset: closedEnum(log.retryPreset) } : {}),
177
+ transport: closedEnum(log.transport),
178
+ ...(log.lastErrorCode ? { lastErrorCode: closedEnum(log.lastErrorCode) } : {}),
179
+ attemptSamples: log.attemptSamples.map(({ n, ms, status, e }) => ({
180
+ n,
181
+ ms,
182
+ ...(status === undefined ? {} : { status }),
183
+ ...(e ? { e: closedEnum(e) } : {}),
184
+ })),
185
+ dropped: log.dropped,
186
+ ms: log.ms,
187
+ ...(log.retry ? { retry: { ...log.retry } } : {}),
188
+ };
189
+ }
190
+ }
@@ -1,9 +1,40 @@
1
1
  import type { ProxyResolutionOptions } from "../config/loader.js";
2
2
  import type { HttpClient, HttpRetrySummary } from "../types.js";
3
+ import { type HttpTelemetrySink } from "./http-telemetry.js";
3
4
  export type HttpClientOptions = ProxyResolutionOptions & {
4
5
  warn?: (message: string) => void;
5
6
  userAgent?: string;
6
7
  onRetrySummary?: (summary: HttpRetrySummary) => void;
8
+ /**
9
+ * Synchronous observer for HTTP attempts, retries, timeouts, status, duration,
10
+ * and observed proxy use. Observer failures never change transport behavior.
11
+ * All hooks and their property reads are guarded, including `onRetrySummary`
12
+ * and the failure reporter. Invalid asynchronous results are ignored and never
13
+ * awaited. Observer failures set the log-only `telemetryFailed` marker; the
14
+ * marker and redacted error diagnostics are excluded from headers and tenant
15
+ * metadata. Tenant `meta.retry` retains its existing response shape.
16
+ *
17
+ * After `ProviderEngine.attach`, the server wraps supported host HTTP bindings
18
+ * with the request collector. The adapter preserves arguments, direct-call,
19
+ * explicit `.call`/`.apply`, and unbound receivers, plus the original returned
20
+ * promise, response, stream, and error. Streams and SSE are observed at headers
21
+ * without consuming their bodies. SDK clients report every observed attempt
22
+ * and keep any observer configured here; routing is scoped to each call.
23
+ *
24
+ * Binding an existing adapter reuses its identity and replaces its collector:
25
+ * the latest bind receives subsequent calls, the earlier collector receives no
26
+ * further calls, and in-flight calls keep the collector captured at invocation.
27
+ * An opaque host invocation is one observed attempt with zero observed retries;
28
+ * its undisclosed retries and proxy settings cannot be inferred, and tenant
29
+ * `meta.retry` is omitted. Status, duration, and error class remain available
30
+ * in logs and telemetry headers.
31
+ *
32
+ * Non-object bindings and objects without a callable `request`, `get`, `post`,
33
+ * `put`, `delete`, `stream`, or `sse` method are left untouched, with no `http`
34
+ * telemetry sibling and one process-wide warning for this reason:
35
+ * `[apifuse] http telemetry not attached; reason=unsupported_binding_shape`.
36
+ */
37
+ httpTelemetry?: HttpTelemetrySink;
7
38
  signal?: AbortSignal;
8
39
  };
9
40
  export declare function createHttpClient(baseUrl?: string, clientOptions?: HttpClientOptions): HttpClient;