@apifuse/provider-sdk 2.1.0-beta.5 → 2.1.0-beta.8
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 +16 -0
- package/README.md +2 -2
- package/SUBMISSION.md +2 -1
- package/bin/apifuse-check.ts +60 -6
- package/bin/apifuse-dev.ts +48 -5
- package/bin/apifuse-perf.ts +50 -11
- package/bin/apifuse-record.ts +35 -11
- package/bin/apifuse-submit-check.ts +1425 -3
- package/dist/ceremonies/index.d.ts +41 -0
- package/dist/ceremonies/index.js +490 -0
- package/dist/choice-token.d.ts +24 -0
- package/dist/choice-token.js +74 -0
- package/dist/cli/commands.d.ts +10 -0
- package/dist/cli/commands.js +80 -0
- package/dist/cli/create.d.ts +47 -0
- package/dist/cli/create.js +762 -0
- package/dist/config/loader.d.ts +107 -0
- package/dist/config/loader.js +935 -0
- package/dist/contract-json.d.ts +9 -0
- package/dist/contract-json.js +51 -0
- package/dist/contract-serialization.d.ts +4 -0
- package/dist/contract-serialization.js +78 -0
- package/dist/contract-types.d.ts +49 -0
- package/dist/contract-types.js +1 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +155 -0
- package/dist/define.d.ts +97 -0
- package/dist/define.js +1320 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.js +15 -0
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +97 -0
- package/dist/i18n/catalog.d.ts +29 -0
- package/dist/i18n/catalog.js +159 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/keys.d.ts +10 -0
- package/dist/i18n/keys.js +34 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +37 -0
- package/dist/lint.d.ts +73 -0
- package/dist/lint.js +702 -0
- package/dist/observability.d.ts +5 -0
- package/dist/observability.js +39 -0
- package/dist/provider.d.ts +9 -0
- package/dist/provider.js +8 -0
- package/dist/public-schema-field-lint.d.ts +2 -0
- package/dist/public-schema-field-lint.js +158 -0
- package/dist/recipes/gov-api.d.ts +19 -0
- package/dist/recipes/gov-api.js +72 -0
- package/dist/recipes/rest-api.d.ts +21 -0
- package/dist/recipes/rest-api.js +115 -0
- package/dist/runtime/auth-flow.d.ts +14 -0
- package/dist/runtime/auth-flow.js +44 -0
- package/dist/runtime/browser.d.ts +25 -0
- package/dist/runtime/browser.js +1034 -0
- package/dist/runtime/cache.d.ts +10 -0
- package/dist/runtime/cache.js +372 -0
- package/dist/runtime/choice.d.ts +15 -0
- package/dist/runtime/choice.js +435 -0
- package/dist/runtime/credential.d.ts +8 -0
- package/dist/runtime/credential.js +61 -0
- package/dist/runtime/env.d.ts +2 -0
- package/dist/runtime/env.js +10 -0
- package/dist/runtime/executor.d.ts +16 -0
- package/dist/runtime/executor.js +51 -0
- package/dist/runtime/http.d.ts +8 -0
- package/dist/runtime/http.js +706 -0
- package/dist/runtime/insights.d.ts +9 -0
- package/dist/runtime/insights.js +324 -0
- package/dist/runtime/instrumentation.d.ts +8 -0
- package/dist/runtime/instrumentation.js +269 -0
- package/dist/runtime/key-derivation.d.ts +24 -0
- package/dist/runtime/key-derivation.js +73 -0
- package/dist/runtime/keyring.d.ts +25 -0
- package/dist/runtime/keyring.js +93 -0
- package/dist/runtime/namespace.d.ts +9 -0
- package/dist/runtime/namespace.js +19 -0
- package/dist/runtime/otlp.d.ts +39 -0
- package/dist/runtime/otlp.js +103 -0
- package/dist/runtime/perf.d.ts +12 -0
- package/dist/runtime/perf.js +52 -0
- package/dist/runtime/prevalidate.d.ts +12 -0
- package/dist/runtime/prevalidate.js +173 -0
- package/dist/runtime/provider.d.ts +2 -0
- package/dist/runtime/provider.js +11 -0
- package/dist/runtime/proxy-errors.d.ts +21 -0
- package/dist/runtime/proxy-errors.js +83 -0
- package/dist/runtime/proxy-telemetry.d.ts +8 -0
- package/dist/runtime/proxy-telemetry.js +174 -0
- package/dist/runtime/redis.d.ts +17 -0
- package/dist/runtime/redis.js +82 -0
- package/dist/runtime/request-options.d.ts +3 -0
- package/dist/runtime/request-options.js +42 -0
- package/dist/runtime/state.d.ts +17 -0
- package/dist/runtime/state.js +344 -0
- package/dist/runtime/stealth.d.ts +18 -0
- package/dist/runtime/stealth.js +827 -0
- package/dist/runtime/stt.d.ts +22 -0
- package/dist/runtime/stt.js +480 -0
- package/dist/runtime/trace.d.ts +26 -0
- package/dist/runtime/trace.js +142 -0
- package/dist/runtime/waterfall.d.ts +12 -0
- package/dist/runtime/waterfall.js +147 -0
- package/dist/schema.d.ts +74 -0
- package/dist/schema.js +243 -0
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/serve.d.ts +64 -0
- package/dist/server/serve.js +1110 -0
- package/dist/server/types.d.ts +136 -0
- package/dist/server/types.js +86 -0
- package/dist/stealth/profiles.d.ts +4 -0
- package/dist/stealth/profiles.js +259 -0
- package/dist/stream.d.ts +44 -0
- package/dist/stream.js +151 -0
- package/dist/testing/helpers.d.ts +23 -0
- package/dist/testing/helpers.js +95 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/run.d.ts +34 -0
- package/dist/testing/run.js +303 -0
- package/dist/types.d.ts +1324 -0
- package/dist/types.js +61 -0
- package/dist/utils/date.d.ts +6 -0
- package/dist/utils/date.js +101 -0
- package/dist/utils/parse.d.ts +16 -0
- package/dist/utils/parse.js +51 -0
- package/dist/utils/text.d.ts +4 -0
- package/dist/utils/text.js +14 -0
- package/dist/utils/transform.d.ts +8 -0
- package/dist/utils/transform.js +48 -0
- package/package.json +42 -25
- package/src/ceremonies/index.ts +8 -2
- package/src/choice-token.ts +1 -0
- package/src/cli/commands.ts +8 -5
- package/src/cli/create.ts +28 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
- package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
- package/src/config/loader.ts +19 -1
- package/src/contract-json.ts +75 -0
- package/src/contract-serialization.ts +89 -0
- package/src/contract-types.ts +52 -0
- package/src/contract.ts +215 -0
- package/src/define.ts +37 -2
- package/src/errors.ts +15 -0
- package/src/i18n/catalog.ts +156 -0
- package/src/index.ts +22 -1
- package/src/lint.ts +256 -37
- package/src/provider.ts +45 -2
- package/src/runtime/browser.ts +685 -30
- package/src/runtime/cache.ts +35 -89
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +19 -2
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/state.ts +487 -0
- package/src/runtime/stealth.ts +8 -1
- package/src/server/serve.ts +361 -46
- package/src/server/types.ts +2 -0
- package/src/testing/run.ts +16 -3
- package/src/types.ts +209 -6
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProviderCache } from "../types";
|
|
2
|
+
export type ProviderCacheOptions = {
|
|
3
|
+
providerId: string;
|
|
4
|
+
redisUrl?: string;
|
|
5
|
+
memoryMaxEntries?: number;
|
|
6
|
+
now?: () => number;
|
|
7
|
+
};
|
|
8
|
+
export declare function createProviderCache(options: ProviderCacheOptions): ProviderCache;
|
|
9
|
+
export declare function createBypassProviderCache(options: Pick<ProviderCacheOptions, "providerId">): ProviderCache;
|
|
10
|
+
export declare function resetProviderCacheForTests(): void;
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { providerCacheRedisUrlFromEnv } from "../config/loader";
|
|
3
|
+
import { createProviderRedisClient, ensureRedisReady, withRedisTimeout, } from "./redis";
|
|
4
|
+
const DEFAULT_PREFIX = "apifuse:provider-cache:v1";
|
|
5
|
+
const DEFAULT_MEMORY_MAX_ENTRIES = 1_000;
|
|
6
|
+
const DEFAULT_REDIS_TIMEOUT_MS = 150;
|
|
7
|
+
const SECRET_FIELD_NAMES = new Set([
|
|
8
|
+
"authorization",
|
|
9
|
+
"cookie",
|
|
10
|
+
"password",
|
|
11
|
+
"secret",
|
|
12
|
+
"servicekey",
|
|
13
|
+
"service_key",
|
|
14
|
+
"token",
|
|
15
|
+
"apikey",
|
|
16
|
+
"api_key",
|
|
17
|
+
"access_token",
|
|
18
|
+
"refresh_token",
|
|
19
|
+
]);
|
|
20
|
+
const sharedBackends = new Map();
|
|
21
|
+
function backendKey(redisUrl) {
|
|
22
|
+
return redisUrl ?? "memory";
|
|
23
|
+
}
|
|
24
|
+
function getSharedBackend(redisUrl) {
|
|
25
|
+
const key = backendKey(redisUrl);
|
|
26
|
+
const existing = sharedBackends.get(key);
|
|
27
|
+
if (existing)
|
|
28
|
+
return existing;
|
|
29
|
+
const backend = {
|
|
30
|
+
memory: new Map(),
|
|
31
|
+
inflight: new Map(),
|
|
32
|
+
};
|
|
33
|
+
if (redisUrl) {
|
|
34
|
+
const redis = createProviderRedisClient({
|
|
35
|
+
redisUrl,
|
|
36
|
+
timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
|
|
37
|
+
onError: () => {
|
|
38
|
+
// Fail-open: cache connectivity must never fail provider execution.
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
backend.redis = redis;
|
|
42
|
+
}
|
|
43
|
+
sharedBackends.set(key, backend);
|
|
44
|
+
return backend;
|
|
45
|
+
}
|
|
46
|
+
function isRecord(value) {
|
|
47
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
function shouldRedactField(name, extra) {
|
|
50
|
+
const normalized = name.toLowerCase();
|
|
51
|
+
return (SECRET_FIELD_NAMES.has(normalized) ||
|
|
52
|
+
extra.has(normalized) ||
|
|
53
|
+
normalized.includes("authorization") ||
|
|
54
|
+
normalized.includes("cookie") ||
|
|
55
|
+
normalized.includes("password") ||
|
|
56
|
+
normalized.includes("secret"));
|
|
57
|
+
}
|
|
58
|
+
function normalizeKeyPart(value, extra) {
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
return value.map((entry) => normalizeKeyPart(entry, extra));
|
|
61
|
+
}
|
|
62
|
+
if (isRecord(value)) {
|
|
63
|
+
const normalized = {};
|
|
64
|
+
for (const key of Object.keys(value).sort()) {
|
|
65
|
+
if (shouldRedactField(key, extra))
|
|
66
|
+
continue;
|
|
67
|
+
normalized[key] = normalizeKeyPart(value[key], extra);
|
|
68
|
+
}
|
|
69
|
+
return normalized;
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function stableHash(value) {
|
|
74
|
+
return createHash("sha256")
|
|
75
|
+
.update(JSON.stringify(value))
|
|
76
|
+
.digest("hex")
|
|
77
|
+
.slice(0, 32);
|
|
78
|
+
}
|
|
79
|
+
function jitteredTtlMs(ttlMs, jitterPct) {
|
|
80
|
+
if (!jitterPct || jitterPct <= 0)
|
|
81
|
+
return ttlMs;
|
|
82
|
+
const bounded = Math.min(jitterPct, 0.5);
|
|
83
|
+
const delta = ttlMs * bounded;
|
|
84
|
+
const multiplier = 1 - bounded + (Math.random() * delta * 2) / ttlMs;
|
|
85
|
+
return Math.max(1, Math.round(ttlMs * multiplier));
|
|
86
|
+
}
|
|
87
|
+
function safeParseEnvelope(raw) {
|
|
88
|
+
if (!raw)
|
|
89
|
+
return null;
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(raw);
|
|
92
|
+
if (!isRecord(parsed) ||
|
|
93
|
+
typeof parsed.writtenAt !== "number" ||
|
|
94
|
+
typeof parsed.freshUntil !== "number" ||
|
|
95
|
+
typeof parsed.staleUntil !== "number" ||
|
|
96
|
+
!("value" in parsed)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
value: parsed.value,
|
|
101
|
+
writtenAt: parsed.writtenAt,
|
|
102
|
+
freshUntil: parsed.freshUntil,
|
|
103
|
+
staleUntil: parsed.staleUntil,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function resultWithValue(value, meta) {
|
|
111
|
+
return {
|
|
112
|
+
value: value,
|
|
113
|
+
meta,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function resultFromEnvelope(key, envelope, now, source) {
|
|
117
|
+
if (now > envelope.staleUntil)
|
|
118
|
+
return null;
|
|
119
|
+
return resultWithValue(envelope.value, {
|
|
120
|
+
key,
|
|
121
|
+
hit: true,
|
|
122
|
+
stale: now > envelope.freshUntil,
|
|
123
|
+
ageMs: Math.max(0, now - envelope.writtenAt),
|
|
124
|
+
source,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function sourceSummary(events) {
|
|
128
|
+
const sources = new Set(events.map((event) => event.source));
|
|
129
|
+
if (sources.size === 0)
|
|
130
|
+
return undefined;
|
|
131
|
+
if (sources.size === 1)
|
|
132
|
+
return events[0]?.source;
|
|
133
|
+
return "mixed";
|
|
134
|
+
}
|
|
135
|
+
async function withRedisFallback(operation) {
|
|
136
|
+
return await withRedisTimeout(operation, {
|
|
137
|
+
timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
|
|
138
|
+
onTimeout: () => undefined,
|
|
139
|
+
onError: () => undefined,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export function createProviderCache(options) {
|
|
143
|
+
const redisUrl = options.redisUrl ?? providerCacheRedisUrlFromEnv();
|
|
144
|
+
const backend = getSharedBackend(redisUrl);
|
|
145
|
+
const memoryMaxEntries = Math.max(1, options.memoryMaxEntries ?? DEFAULT_MEMORY_MAX_ENTRIES);
|
|
146
|
+
const now = options.now ?? Date.now;
|
|
147
|
+
const events = [];
|
|
148
|
+
function record(meta) {
|
|
149
|
+
events.push(meta);
|
|
150
|
+
}
|
|
151
|
+
function sweepMemory(currentTime) {
|
|
152
|
+
for (const [entryKey, entry] of backend.memory) {
|
|
153
|
+
if (entry.expiresAt <= currentTime) {
|
|
154
|
+
backend.memory.delete(entryKey);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function enforceMemoryLimit() {
|
|
159
|
+
while (backend.memory.size > memoryMaxEntries) {
|
|
160
|
+
const oldestKey = backend.memory.keys().next().value;
|
|
161
|
+
if (typeof oldestKey !== "string")
|
|
162
|
+
return;
|
|
163
|
+
backend.memory.delete(oldestKey);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function rememberEnvelope(key, envelope, currentTime) {
|
|
167
|
+
sweepMemory(currentTime);
|
|
168
|
+
backend.memory.delete(key);
|
|
169
|
+
backend.memory.set(key, {
|
|
170
|
+
...envelope,
|
|
171
|
+
expiresAt: envelope.staleUntil,
|
|
172
|
+
lastAccessedAt: currentTime,
|
|
173
|
+
});
|
|
174
|
+
enforceMemoryLimit();
|
|
175
|
+
}
|
|
176
|
+
function touchMemory(key, entry, currentTime) {
|
|
177
|
+
backend.memory.delete(key);
|
|
178
|
+
backend.memory.set(key, { ...entry, lastAccessedAt: currentTime });
|
|
179
|
+
}
|
|
180
|
+
async function readRedis(key, currentTime) {
|
|
181
|
+
const redis = backend.redis;
|
|
182
|
+
if (!redis || !(await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS))) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const raw = await withRedisFallback(async () => {
|
|
186
|
+
return await redis.get(key);
|
|
187
|
+
});
|
|
188
|
+
if (typeof raw !== "string" && raw !== null)
|
|
189
|
+
return null;
|
|
190
|
+
const envelope = safeParseEnvelope(raw);
|
|
191
|
+
if (!envelope)
|
|
192
|
+
return null;
|
|
193
|
+
const result = resultFromEnvelope(key, envelope, currentTime, "redis");
|
|
194
|
+
if (!result)
|
|
195
|
+
return null;
|
|
196
|
+
rememberEnvelope(key, envelope, currentTime);
|
|
197
|
+
return { envelope, result };
|
|
198
|
+
}
|
|
199
|
+
async function read(key) {
|
|
200
|
+
const currentTime = now();
|
|
201
|
+
const memoryEntry = backend.memory.get(key);
|
|
202
|
+
let staleMemoryResult = null;
|
|
203
|
+
let staleMemoryWrittenAt;
|
|
204
|
+
if (memoryEntry) {
|
|
205
|
+
if (memoryEntry.expiresAt <= currentTime) {
|
|
206
|
+
backend.memory.delete(key);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const memoryResult = resultFromEnvelope(key, memoryEntry, currentTime, "memory");
|
|
210
|
+
if (memoryResult && !memoryResult.meta.stale) {
|
|
211
|
+
touchMemory(key, memoryEntry, currentTime);
|
|
212
|
+
return memoryResult;
|
|
213
|
+
}
|
|
214
|
+
staleMemoryResult = memoryResult;
|
|
215
|
+
staleMemoryWrittenAt = memoryEntry.writtenAt;
|
|
216
|
+
touchMemory(key, memoryEntry, currentTime);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const redisResult = await readRedis(key, currentTime);
|
|
220
|
+
if (redisResult) {
|
|
221
|
+
if (!staleMemoryResult)
|
|
222
|
+
return redisResult.result;
|
|
223
|
+
if (!redisResult.result.meta.stale ||
|
|
224
|
+
redisResult.envelope.writtenAt >= (staleMemoryWrittenAt ?? 0)) {
|
|
225
|
+
return redisResult.result;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return staleMemoryResult;
|
|
229
|
+
}
|
|
230
|
+
async function write(key, value, cacheOptions) {
|
|
231
|
+
const currentTime = now();
|
|
232
|
+
const freshTtlMs = jitteredTtlMs(cacheOptions.ttlMs, cacheOptions.jitterPct);
|
|
233
|
+
const staleIfErrorMs = cacheOptions.staleIfErrorMs ?? 0;
|
|
234
|
+
const staleTtlMs = freshTtlMs + staleIfErrorMs;
|
|
235
|
+
const envelope = {
|
|
236
|
+
value,
|
|
237
|
+
writtenAt: currentTime,
|
|
238
|
+
freshUntil: currentTime + freshTtlMs,
|
|
239
|
+
staleUntil: currentTime + staleTtlMs,
|
|
240
|
+
};
|
|
241
|
+
rememberEnvelope(key, envelope, currentTime);
|
|
242
|
+
const redis = backend.redis;
|
|
243
|
+
if (!redis || !(await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS))) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
await withRedisFallback(() => redis.set(key, JSON.stringify(envelope), "PX", staleTtlMs));
|
|
247
|
+
}
|
|
248
|
+
async function loadAndStore(key, loader, cacheOptions, staleCandidate) {
|
|
249
|
+
try {
|
|
250
|
+
const value = await loader();
|
|
251
|
+
await write(key, value, cacheOptions);
|
|
252
|
+
return {
|
|
253
|
+
value,
|
|
254
|
+
meta: {
|
|
255
|
+
key,
|
|
256
|
+
hit: false,
|
|
257
|
+
stale: false,
|
|
258
|
+
source: "loader",
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (staleCandidate?.meta.stale) {
|
|
264
|
+
return staleCandidate;
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
key(namespace, parts, keyOptions) {
|
|
271
|
+
const extra = new Set((keyOptions?.redactFields ?? []).map((field) => field.toLowerCase()));
|
|
272
|
+
const normalized = normalizeKeyPart(parts, extra);
|
|
273
|
+
return `${DEFAULT_PREFIX}:${options.providerId}:${namespace}:${stableHash(normalized)}`;
|
|
274
|
+
},
|
|
275
|
+
async get(key) {
|
|
276
|
+
const result = await read(key);
|
|
277
|
+
if (result)
|
|
278
|
+
record(result.meta);
|
|
279
|
+
return result;
|
|
280
|
+
},
|
|
281
|
+
set: write,
|
|
282
|
+
async delete(key) {
|
|
283
|
+
backend.memory.delete(key);
|
|
284
|
+
const redis = backend.redis;
|
|
285
|
+
if (!redis ||
|
|
286
|
+
!(await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS))) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
await withRedisFallback(() => redis.del(key));
|
|
290
|
+
},
|
|
291
|
+
async getOrSet(key, loader, cacheOptions) {
|
|
292
|
+
const existing = await read(key);
|
|
293
|
+
if (existing && !existing.meta.stale) {
|
|
294
|
+
record(existing.meta);
|
|
295
|
+
return existing;
|
|
296
|
+
}
|
|
297
|
+
const existingInflight = backend.inflight.get(key);
|
|
298
|
+
if (existingInflight) {
|
|
299
|
+
const inflightResult = await existingInflight;
|
|
300
|
+
const result = resultWithValue(inflightResult.value, inflightResult.meta);
|
|
301
|
+
record(result.meta);
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
const promise = loadAndStore(key, loader, cacheOptions, existing).finally(() => {
|
|
305
|
+
backend.inflight.delete(key);
|
|
306
|
+
});
|
|
307
|
+
backend.inflight.set(key, promise);
|
|
308
|
+
const loaded = await promise;
|
|
309
|
+
const result = resultWithValue(loaded.value, loaded.meta);
|
|
310
|
+
record(result.meta);
|
|
311
|
+
return result;
|
|
312
|
+
},
|
|
313
|
+
responseMeta() {
|
|
314
|
+
if (events.length === 0)
|
|
315
|
+
return undefined;
|
|
316
|
+
return {
|
|
317
|
+
hit: events.some((event) => event.hit),
|
|
318
|
+
stale: events.some((event) => event.stale),
|
|
319
|
+
keys: Array.from(new Set(events.map((event) => event.key))),
|
|
320
|
+
source: sourceSummary(events),
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
export function createBypassProviderCache(options) {
|
|
326
|
+
const events = [];
|
|
327
|
+
return {
|
|
328
|
+
key(namespace, parts, keyOptions) {
|
|
329
|
+
const extra = new Set((keyOptions?.redactFields ?? []).map((field) => field.toLowerCase()));
|
|
330
|
+
const normalized = normalizeKeyPart(parts, extra);
|
|
331
|
+
return `${DEFAULT_PREFIX}:${options.providerId}:${namespace}:${stableHash(normalized)}`;
|
|
332
|
+
},
|
|
333
|
+
async get(_key) {
|
|
334
|
+
return null;
|
|
335
|
+
},
|
|
336
|
+
async set() {
|
|
337
|
+
// Intentionally disabled for SDK tools that must hit upstream directly.
|
|
338
|
+
},
|
|
339
|
+
async delete() {
|
|
340
|
+
// Intentionally disabled for SDK tools that must hit upstream directly.
|
|
341
|
+
},
|
|
342
|
+
async getOrSet(key, loader) {
|
|
343
|
+
const value = await loader();
|
|
344
|
+
const meta = {
|
|
345
|
+
key,
|
|
346
|
+
hit: false,
|
|
347
|
+
stale: false,
|
|
348
|
+
source: "loader",
|
|
349
|
+
};
|
|
350
|
+
events.push(meta);
|
|
351
|
+
return { value, meta };
|
|
352
|
+
},
|
|
353
|
+
responseMeta() {
|
|
354
|
+
if (events.length === 0)
|
|
355
|
+
return undefined;
|
|
356
|
+
return {
|
|
357
|
+
hit: false,
|
|
358
|
+
stale: false,
|
|
359
|
+
keys: Array.from(new Set(events.map((event) => event.key))),
|
|
360
|
+
source: sourceSummary(events),
|
|
361
|
+
};
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
export function resetProviderCacheForTests() {
|
|
366
|
+
for (const backend of sharedBackends.values()) {
|
|
367
|
+
backend.memory.clear();
|
|
368
|
+
backend.inflight.clear();
|
|
369
|
+
backend.redis?.disconnect();
|
|
370
|
+
}
|
|
371
|
+
sharedBackends.clear();
|
|
372
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CredentialContext, EnvContext, ProviderChoiceContext, ProviderRequestContext, ProviderRuntimeState } from "../types";
|
|
2
|
+
export declare const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
|
|
3
|
+
export type CreateProviderChoiceContextOptions = {
|
|
4
|
+
readonly providerId: string;
|
|
5
|
+
readonly env?: EnvContext;
|
|
6
|
+
readonly request?: ProviderRequestContext;
|
|
7
|
+
readonly credential?: CredentialContext;
|
|
8
|
+
readonly state?: ProviderRuntimeState;
|
|
9
|
+
readonly masterSecret?: string;
|
|
10
|
+
readonly kid?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function createProviderChoiceContext(options: CreateProviderChoiceContextOptions): ProviderChoiceContext;
|
|
13
|
+
export declare function createTestProviderChoiceContext(options: Omit<CreateProviderChoiceContextOptions, "masterSecret"> & {
|
|
14
|
+
readonly masterSecret?: string;
|
|
15
|
+
}): ProviderChoiceContext;
|