@relayfile/adapter-core 0.3.36 → 0.3.38
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/dist/src/alias-lifecycle.d.ts +73 -0
- package/dist/src/alias-lifecycle.d.ts.map +1 -0
- package/dist/src/alias-lifecycle.js +90 -0
- package/dist/src/alias-lifecycle.js.map +1 -0
- package/dist/src/http/index.d.ts +3 -0
- package/dist/src/http/index.d.ts.map +1 -0
- package/dist/src/http/index.js +2 -0
- package/dist/src/http/index.js.map +1 -0
- package/dist/src/http/retry.d.ts +165 -0
- package/dist/src/http/retry.d.ts.map +1 -0
- package/dist/src/http/retry.js +330 -0
- package/dist/src/http/retry.js.map +1 -0
- package/dist/src/http/retry.test.d.ts +2 -0
- package/dist/src/http/retry.test.d.ts.map +1 -0
- package/dist/src/http/retry.test.js +382 -0
- package/dist/src/http/retry.test.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/ingest/shared.d.ts.map +1 -1
- package/dist/src/ingest/shared.js +2 -1
- package/dist/src/ingest/shared.js.map +1 -1
- package/dist/src/runtime/file-native-router.test.js +15 -0
- package/dist/src/runtime/file-native-router.test.js.map +1 -1
- package/dist/src/runtime/schema-adapter.d.ts.map +1 -1
- package/dist/src/runtime/schema-adapter.js +3 -2
- package/dist/src/runtime/schema-adapter.js.map +1 -1
- package/dist/src/writeback-paths/catalog.generated.d.ts +4 -0
- package/dist/src/writeback-paths/catalog.generated.d.ts.map +1 -1
- package/dist/src/writeback-paths/catalog.generated.js +6 -0
- package/dist/src/writeback-paths/catalog.generated.js.map +1 -1
- package/dist/tests/alias-lifecycle/alias-lifecycle.test.d.ts +2 -0
- package/dist/tests/alias-lifecycle/alias-lifecycle.test.d.ts.map +1 -0
- package/dist/tests/alias-lifecycle/alias-lifecycle.test.js +104 -0
- package/dist/tests/alias-lifecycle/alias-lifecycle.test.js.map +1 -0
- package/package.json +6 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stale alias lifecycle cleanup (issue #106).
|
|
3
|
+
*
|
|
4
|
+
* Adapters mirror provider records to a canonical path plus lookup aliases
|
|
5
|
+
* (`by-title/…`, `by-name/…`). Aliases duplicate the canonical bytes
|
|
6
|
+
* verbatim, and the title-independent `by-id` alias is rewritten on every
|
|
7
|
+
* ingest — so the bytes sitting at the `by-id` alias path *before* a
|
|
8
|
+
* re-ingest are a faithful snapshot of the previous record version. That
|
|
9
|
+
* snapshot is the prior state consumed here: callers read it before
|
|
10
|
+
* overwriting, derive the candidate alias paths the previous version may
|
|
11
|
+
* occupy (base + collision variants), and this helper deletes every
|
|
12
|
+
* candidate that still holds the previous record bytes.
|
|
13
|
+
*
|
|
14
|
+
* Content equality is the ownership proof: a candidate is deleted only when
|
|
15
|
+
* its bytes match `previousContent`. This guarantees we never delete
|
|
16
|
+
* (a) an alias owned by a different record that happens to share a slug
|
|
17
|
+
* (collision case), (b) the freshly written alias for the current version
|
|
18
|
+
* (its bytes are the new content), or (c) anything when the previous
|
|
19
|
+
* version was never mirrored.
|
|
20
|
+
*
|
|
21
|
+
* Relayfile contract: a title change is an alias cleanup, never a record
|
|
22
|
+
* deletion. Callers must only pass alias paths as candidates — canonical
|
|
23
|
+
* record paths are never deleted by this helper's callers.
|
|
24
|
+
*
|
|
25
|
+
* See also `PriorAliasReader` in `emit-auxiliary` — the same "by-id alias
|
|
26
|
+
* as prior-state anchor" convention for adapters built on the auxiliary
|
|
27
|
+
* emitter client. This module is the IO-agnostic deletion side, usable
|
|
28
|
+
* with both `RelayFileClientLike` and duck-typed `VfsLike` backends.
|
|
29
|
+
*/
|
|
30
|
+
export interface StaleAliasCleanupIo {
|
|
31
|
+
/** Returns file content, or `undefined` when missing/unreadable. */
|
|
32
|
+
readFile(path: string): Promise<string | undefined> | string | undefined;
|
|
33
|
+
/** Deletes the file at `path`. */
|
|
34
|
+
deleteFile(path: string): Promise<unknown> | unknown;
|
|
35
|
+
}
|
|
36
|
+
export interface StaleAliasCleanupInput {
|
|
37
|
+
/**
|
|
38
|
+
* Bytes of the record as previously mirrored — read from a
|
|
39
|
+
* title-independent alias (e.g. `by-id/…`) before it was overwritten.
|
|
40
|
+
*/
|
|
41
|
+
previousContent: string;
|
|
42
|
+
/**
|
|
43
|
+
* Alias paths the previous record version may occupy. Derive from the
|
|
44
|
+
* previous title/name (base + collision variants).
|
|
45
|
+
*/
|
|
46
|
+
candidatePaths: readonly string[];
|
|
47
|
+
/**
|
|
48
|
+
* Alias paths belonging to the current record version. Never deleted,
|
|
49
|
+
* even when a candidate path matches.
|
|
50
|
+
*/
|
|
51
|
+
keepPaths?: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
export interface StaleAliasCleanupResult {
|
|
54
|
+
deletedPaths: string[];
|
|
55
|
+
errors: Array<{
|
|
56
|
+
path: string;
|
|
57
|
+
error: string;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Deletes stale alias files left behind when a record's alias-relevant
|
|
62
|
+
* field (title/name) changed between ingests. Failures are captured per
|
|
63
|
+
* path and never thrown — a leftover stale alias is benign, while failing
|
|
64
|
+
* the surrounding ingest is not.
|
|
65
|
+
*/
|
|
66
|
+
export declare function cleanupStaleAliases(io: StaleAliasCleanupIo, input: StaleAliasCleanupInput): Promise<StaleAliasCleanupResult>;
|
|
67
|
+
/**
|
|
68
|
+
* Extracts a top-level string field from mirrored JSON record content.
|
|
69
|
+
* Returns `undefined` when the content is not parseable JSON or the field
|
|
70
|
+
* is missing/non-string — callers treat that as "no prior alias to clean".
|
|
71
|
+
*/
|
|
72
|
+
export declare function readAliasKeyFromContent(content: string, ...fieldPath: readonly string[]): string | undefined;
|
|
73
|
+
//# sourceMappingURL=alias-lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alias-lifecycle.d.ts","sourceRoot":"","sources":["../../src/alias-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,SAAS,CAAC;IACzE,kCAAkC;IAClC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CACtD;AAED,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,mBAAmB,EACvB,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,uBAAuB,CAAC,CAgClC;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAgB5G"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stale alias lifecycle cleanup (issue #106).
|
|
3
|
+
*
|
|
4
|
+
* Adapters mirror provider records to a canonical path plus lookup aliases
|
|
5
|
+
* (`by-title/…`, `by-name/…`). Aliases duplicate the canonical bytes
|
|
6
|
+
* verbatim, and the title-independent `by-id` alias is rewritten on every
|
|
7
|
+
* ingest — so the bytes sitting at the `by-id` alias path *before* a
|
|
8
|
+
* re-ingest are a faithful snapshot of the previous record version. That
|
|
9
|
+
* snapshot is the prior state consumed here: callers read it before
|
|
10
|
+
* overwriting, derive the candidate alias paths the previous version may
|
|
11
|
+
* occupy (base + collision variants), and this helper deletes every
|
|
12
|
+
* candidate that still holds the previous record bytes.
|
|
13
|
+
*
|
|
14
|
+
* Content equality is the ownership proof: a candidate is deleted only when
|
|
15
|
+
* its bytes match `previousContent`. This guarantees we never delete
|
|
16
|
+
* (a) an alias owned by a different record that happens to share a slug
|
|
17
|
+
* (collision case), (b) the freshly written alias for the current version
|
|
18
|
+
* (its bytes are the new content), or (c) anything when the previous
|
|
19
|
+
* version was never mirrored.
|
|
20
|
+
*
|
|
21
|
+
* Relayfile contract: a title change is an alias cleanup, never a record
|
|
22
|
+
* deletion. Callers must only pass alias paths as candidates — canonical
|
|
23
|
+
* record paths are never deleted by this helper's callers.
|
|
24
|
+
*
|
|
25
|
+
* See also `PriorAliasReader` in `emit-auxiliary` — the same "by-id alias
|
|
26
|
+
* as prior-state anchor" convention for adapters built on the auxiliary
|
|
27
|
+
* emitter client. This module is the IO-agnostic deletion side, usable
|
|
28
|
+
* with both `RelayFileClientLike` and duck-typed `VfsLike` backends.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Deletes stale alias files left behind when a record's alias-relevant
|
|
32
|
+
* field (title/name) changed between ingests. Failures are captured per
|
|
33
|
+
* path and never thrown — a leftover stale alias is benign, while failing
|
|
34
|
+
* the surrounding ingest is not.
|
|
35
|
+
*/
|
|
36
|
+
export async function cleanupStaleAliases(io, input) {
|
|
37
|
+
const result = { deletedPaths: [], errors: [] };
|
|
38
|
+
const keep = new Set(input.keepPaths ?? []);
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
for (const candidate of input.candidatePaths) {
|
|
41
|
+
if (!candidate || keep.has(candidate) || seen.has(candidate)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
seen.add(candidate);
|
|
45
|
+
let existing;
|
|
46
|
+
try {
|
|
47
|
+
existing = await io.readFile(candidate);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
result.errors.push({ path: candidate, error: formatError(error) });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (existing === undefined || existing !== input.previousContent) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await io.deleteFile(candidate);
|
|
58
|
+
result.deletedPaths.push(candidate);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
result.errors.push({ path: candidate, error: formatError(error) });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extracts a top-level string field from mirrored JSON record content.
|
|
68
|
+
* Returns `undefined` when the content is not parseable JSON or the field
|
|
69
|
+
* is missing/non-string — callers treat that as "no prior alias to clean".
|
|
70
|
+
*/
|
|
71
|
+
export function readAliasKeyFromContent(content, ...fieldPath) {
|
|
72
|
+
let value;
|
|
73
|
+
try {
|
|
74
|
+
value = JSON.parse(content);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
for (const field of fieldPath) {
|
|
80
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
value = value[field];
|
|
84
|
+
}
|
|
85
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
86
|
+
}
|
|
87
|
+
function formatError(error) {
|
|
88
|
+
return error instanceof Error ? error.message : String(error);
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=alias-lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alias-lifecycle.js","sourceRoot":"","sources":["../../src/alias-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAgCH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,EAAuB,EACvB,KAA6B;IAE7B,MAAM,MAAM,GAA4B,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACzE,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IAC5C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE/B,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;QAC7C,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7D,SAAS;QACX,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAEpB,IAAI,QAA4B,CAAC;QACjC,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QAC1C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACnE,SAAS;QACX,CAAC;QAED,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,KAAK,CAAC,eAAe,EAAE,CAAC;YACjE,SAAS;QACX,CAAC;QAED,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAC/B,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAe,EAAE,GAAG,SAA4B;IACtF,IAAI,KAAc,CAAC;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;QAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACxE,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,KAAK,GAAI,KAAiC,CAAC,KAAK,CAAC,CAAC;IACpD,CAAC;IAED,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3E,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { RetryExhaustedError, executeWithRetry, fetchWithRetry, isTransientNetworkError, parseRetryAfterMs, withProxyRetry, } from './retry.js';
|
|
2
|
+
export type { FetchRetryOptions, ProxyCapable, ProxyResponseLike, RetryOptions, RetryRequestInit, RetryResponseLike, } from './retry.js';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/http/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,uBAAuB,EACvB,iBAAiB,EACjB,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/http/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,uBAAuB,EACvB,iBAAiB,EACjB,cAAc,GACf,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP rate-limit / retry helpers.
|
|
3
|
+
*
|
|
4
|
+
* Adapters in this repo talk to providers through one of two transports:
|
|
5
|
+
*
|
|
6
|
+
* 1. A `fetch`-shaped client (Notion's raw token path, the X search client).
|
|
7
|
+
* Wrap those call sites with {@link fetchWithRetry}.
|
|
8
|
+
* 2. A relayfile `ConnectionProvider.proxy()` call. Wrap the provider with
|
|
9
|
+
* {@link withProxyRetry} at the call site:
|
|
10
|
+
*
|
|
11
|
+
* const response = await withProxyRetry(provider).proxy({ ... });
|
|
12
|
+
*
|
|
13
|
+
* Both honor `Retry-After` (delta-seconds and HTTP-date forms), retry on 429,
|
|
14
|
+
* 5xx, and transient network errors with exponential backoff plus jitter, and
|
|
15
|
+
* never retry non-idempotent requests (POST/PATCH) unless the caller opts in
|
|
16
|
+
* via `retryNonIdempotent: true`.
|
|
17
|
+
*
|
|
18
|
+
* Exhaustion semantics:
|
|
19
|
+
* - If the final attempt produced an HTTP response, that response is returned
|
|
20
|
+
* so existing status handling in adapters keeps working unchanged. Callers
|
|
21
|
+
* that want a typed failure instead can pass
|
|
22
|
+
* `throwOnExhaustedRetryableStatus: true` to receive a
|
|
23
|
+
* {@link RetryExhaustedError}.
|
|
24
|
+
* - If the final attempt failed with a transient network error, a
|
|
25
|
+
* {@link RetryExhaustedError} is thrown with the underlying error as
|
|
26
|
+
* `cause`.
|
|
27
|
+
*/
|
|
28
|
+
export interface RetryOptions {
|
|
29
|
+
/** Total attempts, including the first one. Default 3. */
|
|
30
|
+
maxAttempts?: number;
|
|
31
|
+
/** Give up once this much wall-clock time has been spent. Default 30s. */
|
|
32
|
+
maxElapsedMs?: number;
|
|
33
|
+
/** Base delay before the first retry. Default 250ms. */
|
|
34
|
+
initialDelayMs?: number;
|
|
35
|
+
/** Upper bound for a single computed backoff delay. Default 10s. */
|
|
36
|
+
maxDelayMs?: number;
|
|
37
|
+
/** Exponential growth factor. Default 2. */
|
|
38
|
+
backoffFactor?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Retry POST/PATCH requests too. Default false: only idempotent methods
|
|
41
|
+
* (GET, HEAD, OPTIONS, PUT, DELETE, TRACE) are retried.
|
|
42
|
+
*/
|
|
43
|
+
retryNonIdempotent?: boolean;
|
|
44
|
+
/** Status classifier. Default: 429 or any 5xx is retryable. */
|
|
45
|
+
isRetryableStatus?: (status: number) => boolean;
|
|
46
|
+
/** Error classifier. Default: {@link isTransientNetworkError}. */
|
|
47
|
+
isRetryableError?: (error: unknown) => boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Throw a {@link RetryExhaustedError} instead of returning the final
|
|
50
|
+
* response when attempts run out while the response is still retryable
|
|
51
|
+
* (e.g. a persistent 429). Default false.
|
|
52
|
+
*/
|
|
53
|
+
throwOnExhaustedRetryableStatus?: boolean;
|
|
54
|
+
/** Abort retries (and in-flight sleeps) when this signal fires. */
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
/** Injectable sleep, for tests. */
|
|
57
|
+
sleep?: (ms: number) => Promise<void>;
|
|
58
|
+
/** Injectable jitter source, for tests. Must return [0, 1). */
|
|
59
|
+
random?: () => number;
|
|
60
|
+
/** Injectable clock, for tests. Returns epoch milliseconds. */
|
|
61
|
+
now?: () => number;
|
|
62
|
+
}
|
|
63
|
+
/** Typed error surfaced when retries are exhausted. */
|
|
64
|
+
export declare class RetryExhaustedError extends Error {
|
|
65
|
+
readonly code = "RETRY_EXHAUSTED";
|
|
66
|
+
/** Number of attempts that were made. */
|
|
67
|
+
readonly attempts: number;
|
|
68
|
+
/** Wall-clock time spent across all attempts, in milliseconds. */
|
|
69
|
+
readonly elapsedMs: number;
|
|
70
|
+
/** Status of the final response, when the final attempt got one. */
|
|
71
|
+
readonly lastStatus?: number;
|
|
72
|
+
constructor(message: string, details: {
|
|
73
|
+
attempts: number;
|
|
74
|
+
elapsedMs: number;
|
|
75
|
+
lastStatus?: number;
|
|
76
|
+
cause?: unknown;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** True for errors that look like transient network failures worth retrying. */
|
|
80
|
+
export declare function isTransientNetworkError(error: unknown): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Parse a `Retry-After` header value: either delta-seconds or an HTTP-date.
|
|
83
|
+
* Returns the wait in milliseconds, or `undefined` when unparseable.
|
|
84
|
+
*/
|
|
85
|
+
export declare function parseRetryAfterMs(value: string | null | undefined, nowMs: number): number | undefined;
|
|
86
|
+
type HeadersLike = {
|
|
87
|
+
get(name: string): string | null;
|
|
88
|
+
} | Record<string, string | string[] | undefined>;
|
|
89
|
+
interface AttemptInspection {
|
|
90
|
+
retryable: boolean;
|
|
91
|
+
status: number;
|
|
92
|
+
retryAfterMs?: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Core retry loop shared by {@link fetchWithRetry} and {@link withProxyRetry}.
|
|
96
|
+
*
|
|
97
|
+
* `attempt` performs one request; `inspect` classifies its result. Thrown
|
|
98
|
+
* errors are retried only when `isRetryableError` says so and the request is
|
|
99
|
+
* idempotent (or `retryNonIdempotent` is set).
|
|
100
|
+
*/
|
|
101
|
+
export declare function executeWithRetry<R>(args: {
|
|
102
|
+
attempt: () => Promise<R>;
|
|
103
|
+
inspect: (result: R) => AttemptInspection;
|
|
104
|
+
idempotent: boolean;
|
|
105
|
+
describe: string;
|
|
106
|
+
options: RetryOptions;
|
|
107
|
+
}): Promise<R>;
|
|
108
|
+
/** Minimal response surface required by {@link fetchWithRetry}. */
|
|
109
|
+
export interface RetryResponseLike {
|
|
110
|
+
status: number;
|
|
111
|
+
headers?: HeadersLike;
|
|
112
|
+
}
|
|
113
|
+
/** Permissive `RequestInit` so any fetch implementation can be wrapped. */
|
|
114
|
+
export interface RetryRequestInit {
|
|
115
|
+
method?: string;
|
|
116
|
+
signal?: AbortSignal | null;
|
|
117
|
+
[key: string]: unknown;
|
|
118
|
+
}
|
|
119
|
+
export interface FetchRetryOptions<R extends RetryResponseLike> extends RetryOptions {
|
|
120
|
+
/** Fetch implementation. Defaults to `globalThis.fetch`. */
|
|
121
|
+
fetch?: (input: string | URL, init?: RetryRequestInit) => Promise<R>;
|
|
122
|
+
/**
|
|
123
|
+
* Force the idempotency classification instead of deriving it from
|
|
124
|
+
* `init.method`. Useful for POST endpoints that are semantically reads.
|
|
125
|
+
*/
|
|
126
|
+
idempotent?: boolean;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* `fetch` with rate-limit aware retries.
|
|
130
|
+
*
|
|
131
|
+
* Retries 429/5xx responses (honoring `Retry-After`) and transient network
|
|
132
|
+
* errors with exponential backoff plus jitter. Non-idempotent methods
|
|
133
|
+
* (POST/PATCH) are never retried unless the caller opts in via
|
|
134
|
+
* `retryNonIdempotent` or `idempotent: true`.
|
|
135
|
+
*
|
|
136
|
+
* Returns the final response even when it is still an error status, unless
|
|
137
|
+
* `throwOnExhaustedRetryableStatus` is set; network-error exhaustion throws a
|
|
138
|
+
* {@link RetryExhaustedError}.
|
|
139
|
+
*/
|
|
140
|
+
export declare function fetchWithRetry<R extends RetryResponseLike = Response>(input: string | URL, init?: RetryRequestInit, options?: FetchRetryOptions<R>): Promise<R>;
|
|
141
|
+
/** Minimal proxy response surface required by {@link withProxyRetry}. */
|
|
142
|
+
export interface ProxyResponseLike {
|
|
143
|
+
status: number;
|
|
144
|
+
headers?: Record<string, string>;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Minimal provider surface required by {@link withProxyRetry}. The `never`
|
|
148
|
+
* parameter type makes any concrete `proxy(request)` method assignable here
|
|
149
|
+
* (method parameters are checked bivariantly).
|
|
150
|
+
*/
|
|
151
|
+
export interface ProxyCapable {
|
|
152
|
+
proxy(request: never): Promise<ProxyResponseLike>;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Wrap a `ConnectionProvider`-shaped object so `proxy()` retries 429/5xx
|
|
156
|
+
* responses (honoring `Retry-After`) and transient network errors. The
|
|
157
|
+
* wrapper preserves the provider's full type, so call sites change from
|
|
158
|
+
* `provider.proxy({...})` to `withProxyRetry(provider).proxy({...})`.
|
|
159
|
+
*
|
|
160
|
+
* POST/PATCH requests pass through without retries unless
|
|
161
|
+
* `retryNonIdempotent` is set.
|
|
162
|
+
*/
|
|
163
|
+
export declare function withProxyRetry<P extends ProxyCapable>(provider: P, options?: RetryOptions): P;
|
|
164
|
+
export {};
|
|
165
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../../src/http/retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAChD,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAC/C;;;;OAIG;IACH,+BAA+B,CAAC,EAAE,OAAO,CAAC;IAC1C,mEAAmE;IACnE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,mCAAmC;IACnC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAyBD,uDAAuD;AACvD,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,qBAAqB;IAClC,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;gBAG3B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAQzF;AAED,gFAAgF;AAChF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAc/D;AAkBD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUrG;AAED,KAAK,WAAW,GACZ;IAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,GACpC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;AAgFlD,UAAU,iBAAiB;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,IAAI,EAAE;IAC9C,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1B,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,iBAAiB,CAAC;IAC1C,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,YAAY,CAAC;CACvB,GAAG,OAAO,CAAC,CAAC,CAAC,CA8Cb;AA4BD,mEAAmE;AACnE,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,iBAAiB,CAAE,SAAQ,YAAY;IAClF,4DAA4D;IAC5D,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,gBAAgB,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACrE;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,CAAC,SAAS,iBAAiB,GAAG,QAAQ,EACzE,KAAK,EAAE,MAAM,GAAG,GAAG,EACnB,IAAI,GAAE,gBAAqB,EAC3B,OAAO,GAAE,iBAAiB,CAAC,CAAC,CAAM,GACjC,OAAO,CAAC,CAAC,CAAC,CA4BZ;AAED,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,OAAO,EAAE,KAAK,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACnD;AASD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,YAAY,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,CAAC,CAyC7F"}
|