@apifuse/provider-sdk 2.2.0-beta.61 → 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.
- package/CHANGELOG.md +4 -0
- package/bin/apifuse-pack-types.ts +16 -12
- package/dist/handle-meta.d.ts +18 -2
- package/dist/handle-meta.js +30 -2
- package/dist/handle.d.ts +33 -12
- package/dist/handle.js +69 -24
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lint.js +28 -8
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +2 -0
- package/dist/runtime/handle.js +15 -10
- package/dist/runtime/http-telemetry-binding.d.ts +10 -0
- package/dist/runtime/http-telemetry-binding.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +6 -0
- package/dist/runtime/http-telemetry-guard.js +150 -0
- package/dist/runtime/http-telemetry.d.ts +104 -0
- package/dist/runtime/http-telemetry.js +190 -0
- package/dist/runtime/http.d.ts +31 -0
- package/dist/runtime/http.js +222 -146
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/serve-implementation.d.ts +2 -0
- package/dist/server/serve-implementation.js +34 -35
- package/package.json +1 -1
- package/src/handle-meta.ts +39 -4
- package/src/handle.ts +121 -38
- package/src/index.ts +19 -0
- package/src/lint.ts +30 -7
- package/src/provider.ts +10 -0
- package/src/runtime/handle.ts +20 -10
- package/src/runtime/http-telemetry-binding.ts +162 -0
- package/src/runtime/http-telemetry-guard.ts +155 -0
- package/src/runtime/http-telemetry.ts +292 -0
- package/src/runtime/http.ts +295 -173
- package/src/server/index.ts +13 -0
- package/src/server/serve-implementation.ts +41 -58
package/dist/runtime/handle.js
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Storage layout: one state namespace per kind, `handle.<kind>`, scoped by the
|
|
5
5
|
* kind's access (bound → connection scope via `state.forConnection`, public →
|
|
6
|
-
* provider scope). The state key is the
|
|
6
|
+
* provider scope; both cursors and drafts may be either). The state key is the
|
|
7
|
+
* canonical handle string. Records hold
|
|
7
8
|
* the validated data plus status/timestamps; the handle string itself carries
|
|
8
9
|
* no payload and no signature.
|
|
9
10
|
*/
|
|
10
11
|
import { randomInt, randomUUID } from "node:crypto";
|
|
11
12
|
import { isProviderError } from "../errors.js";
|
|
12
13
|
import { HandleError, handleRecoverySentence, isDraftKind, } from "../handle.js";
|
|
14
|
+
import { formatHandleIssuers } from "../handle-meta.js";
|
|
13
15
|
import { BOUND_HANDLE_WORD_COUNT, findHandleWordWithinDistance, HIGH_PUBLIC_HANDLE_WORD_COUNT, ISSUABLE_HANDLE_WORDS, PUBLIC_HANDLE_WORD_COUNT, } from "./handle-wordlist.js";
|
|
14
16
|
import { createMemoryProviderRuntimeState } from "./state.js";
|
|
15
17
|
const RECORD_VERSION = 1;
|
|
@@ -151,14 +153,13 @@ export function formatNormalizationClasses(classes) {
|
|
|
151
153
|
// Errors (spec §5)
|
|
152
154
|
// ---------------------------------------------------------------------------
|
|
153
155
|
function publicCollapsedMessage(kind) {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
: "Request a new one.";
|
|
156
|
+
const issuers = formatHandleIssuers(kind.issuedBy);
|
|
157
|
+
const again = issuers ? `Call ${issuers} again to get a new one.` : "Request a new one.";
|
|
157
158
|
return `\`${kind.fieldName}\` is not valid or has expired. ${again}`;
|
|
158
159
|
}
|
|
159
160
|
function normalizationError(kind, failure) {
|
|
160
161
|
if (failure.reason === "kind_mismatch") {
|
|
161
|
-
return new HandleError("HANDLE_KIND_MISMATCH", `\`${kind.fieldName}\` must be a \`${kind.name}_...\` handle as returned by ${kind.issuedBy
|
|
162
|
+
return new HandleError("HANDLE_KIND_MISMATCH", `\`${kind.fieldName}\` must be a \`${kind.name}_...\` handle as returned by ${formatHandleIssuers(kind.issuedBy) ?? "the issuing operation"}. ${handleRecoverySentence(kind)}`, { kind: kind.name, details: { field: kind.fieldName } });
|
|
162
163
|
}
|
|
163
164
|
if (kind.access === "public") {
|
|
164
165
|
return new HandleError("HANDLE_INVALID", publicCollapsedMessage(kind), {
|
|
@@ -217,7 +218,7 @@ function committedError(kind) {
|
|
|
217
218
|
return new HandleError("HANDLE_COMMITTED", `\`${kind.fieldName}\` has already been committed and can no longer be changed. ${handleRecoverySentence(kind)}`, { kind: kind.name, details: { field: kind.fieldName } });
|
|
218
219
|
}
|
|
219
220
|
function connectionRequiredError(kind, operation) {
|
|
220
|
-
return new HandleError("HANDLE_CONNECTION_REQUIRED", `Handle kind "${kind.name}" is bound to a connection, but request.connectionId is missing for ${operation}. Bound handles require a connection-scoped request; use access: "public" for connection-less
|
|
221
|
+
return new HandleError("HANDLE_CONNECTION_REQUIRED", `Handle kind "${kind.name}" is bound to a connection, but request.connectionId is missing for ${operation}. Bound handles require a connection-scoped request; use access: "public" for connection-less kinds.`, { kind: kind.name });
|
|
221
222
|
}
|
|
222
223
|
function storageUnavailableError(kind, reason, cause) {
|
|
223
224
|
return new HandleError("HANDLE_STORAGE_UNAVAILABLE", `Handle storage for kind "${kind.name}" is not available: ${reason}.`, { kind: kind.name, cause, details: { reason } });
|
|
@@ -526,7 +527,7 @@ export function createHandleContext(options) {
|
|
|
526
527
|
record: value,
|
|
527
528
|
};
|
|
528
529
|
}
|
|
529
|
-
async function
|
|
530
|
+
async function createRecord(kind, data) {
|
|
530
531
|
return await instrumented(kind, "create", async (report) => {
|
|
531
532
|
const validated = validateData(kind, "create", data);
|
|
532
533
|
const namespace = resolveNamespace(kind, "create");
|
|
@@ -551,12 +552,16 @@ export function createHandleContext(options) {
|
|
|
551
552
|
for (let attempt = 0; attempt < CREATE_ATTEMPTS; attempt += 1) {
|
|
552
553
|
const canonical = `${kind.name}_${generateWords(kind.wordCount).join("-")}`;
|
|
553
554
|
const result = await storage(kind, () => namespace.compareAndSet(canonical, 0, record, { ttl: msDuration(writeTtlMs) }));
|
|
554
|
-
if (result.ok)
|
|
555
|
-
return { value: canonical, outcome: "success" };
|
|
555
|
+
if (result.ok) {
|
|
556
|
+
return { value: toHandleRecord(kind, canonical, record), outcome: "success" };
|
|
557
|
+
}
|
|
556
558
|
}
|
|
557
559
|
throw storageUnavailableError(kind, `could not allocate a unique handle after ${CREATE_ATTEMPTS} attempts`);
|
|
558
560
|
});
|
|
559
561
|
}
|
|
562
|
+
async function create(kind, data) {
|
|
563
|
+
return (await createRecord(kind, data)).handle;
|
|
564
|
+
}
|
|
560
565
|
async function read(kind, handle) {
|
|
561
566
|
return await instrumented(kind, "read", async (report) => {
|
|
562
567
|
const loaded = await load(kind, "read", handle, report);
|
|
@@ -876,7 +881,7 @@ export function createHandleContext(options) {
|
|
|
876
881
|
throw busyError(kind);
|
|
877
882
|
});
|
|
878
883
|
}
|
|
879
|
-
return { create, read, update, commit, discard };
|
|
884
|
+
return { create, createRecord, read, update, commit, discard };
|
|
880
885
|
}
|
|
881
886
|
/**
|
|
882
887
|
* Handle context for unit tests: memory state, a fixed connection, and an
|
|
@@ -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
|
+
}
|