@apifuse/provider-sdk 2.1.0-beta.4 → 2.1.0-beta.6

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 (42) hide show
  1. package/AUTHORING.md +24 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +23 -2
  4. package/SUBMISSION.md +2 -1
  5. package/bin/apifuse-check.ts +60 -6
  6. package/bin/apifuse-dev.ts +48 -5
  7. package/bin/apifuse-perf.ts +106 -26
  8. package/bin/apifuse-record.ts +142 -52
  9. package/bin/apifuse-submit-check.ts +1489 -3
  10. package/package.json +107 -92
  11. package/src/ceremonies/index.ts +8 -2
  12. package/src/choice-token.ts +1 -0
  13. package/src/cli/commands.ts +10 -8
  14. package/src/cli/create.ts +49 -1
  15. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  16. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  17. package/src/cli/templates/provider/README.md.tpl +18 -0
  18. package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
  19. package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
  20. package/src/config/loader.ts +19 -1
  21. package/src/contract-json.ts +75 -0
  22. package/src/contract-serialization.ts +89 -0
  23. package/src/contract-types.ts +52 -0
  24. package/src/contract.ts +215 -0
  25. package/src/define.ts +40 -5
  26. package/src/errors.ts +15 -0
  27. package/src/i18n/catalog.ts +156 -0
  28. package/src/index.ts +22 -1
  29. package/src/lint.ts +265 -46
  30. package/src/provider.ts +45 -2
  31. package/src/runtime/browser.ts +685 -30
  32. package/src/runtime/cache.ts +35 -89
  33. package/src/runtime/choice.ts +760 -0
  34. package/src/runtime/executor.ts +19 -2
  35. package/src/runtime/redis.ts +116 -0
  36. package/src/runtime/state.ts +487 -0
  37. package/src/runtime/stealth.ts +8 -1
  38. package/src/runtime/trace.ts +1 -1
  39. package/src/server/serve.ts +361 -46
  40. package/src/server/types.ts +2 -0
  41. package/src/testing/run.ts +16 -3
  42. package/src/types.ts +225 -18
@@ -1,7 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
- import Redis from "ioredis";
4
-
3
+ import { providerCacheRedisUrlFromEnv } from "../config/loader";
5
4
  import type {
6
5
  ProviderCache,
7
6
  ProviderCacheGetOrSetOptions,
@@ -10,6 +9,12 @@ import type {
10
9
  ProviderCacheResponseMeta,
11
10
  ProviderCacheResult,
12
11
  } from "../types";
12
+ import {
13
+ createProviderRedisClient,
14
+ ensureRedisReady,
15
+ type ProviderRedisClient,
16
+ withRedisTimeout,
17
+ } from "./redis";
13
18
 
14
19
  type CacheSource = ProviderCacheLookupMeta["source"];
15
20
 
@@ -26,7 +31,7 @@ type MemoryEntry = CacheEnvelope & {
26
31
  };
27
32
 
28
33
  type SharedCacheBackend = {
29
- redis?: Redis;
34
+ redis?: ProviderRedisClient;
30
35
  memory: Map<string, MemoryEntry>;
31
36
  inflight: Map<string, Promise<ProviderCacheResult<unknown>>>;
32
37
  };
@@ -57,14 +62,6 @@ const SECRET_FIELD_NAMES = new Set([
57
62
 
58
63
  const sharedBackends = new Map<string, SharedCacheBackend>();
59
64
 
60
- function redisUrlFromEnv(): string | undefined {
61
- return (
62
- process.env.APIFUSE__PROVIDER__CACHE_REDIS_URL?.trim() ||
63
- process.env.APIFUSE__REDIS__URL?.trim() ||
64
- undefined
65
- );
66
- }
67
-
68
65
  function backendKey(redisUrl: string | undefined): string {
69
66
  return redisUrl ?? "memory";
70
67
  }
@@ -80,15 +77,12 @@ function getSharedBackend(redisUrl: string | undefined): SharedCacheBackend {
80
77
  };
81
78
 
82
79
  if (redisUrl) {
83
- const redis = new Redis(redisUrl, {
84
- connectTimeout: DEFAULT_REDIS_TIMEOUT_MS,
85
- enableOfflineQueue: false,
86
- lazyConnect: true,
87
- maxRetriesPerRequest: 0,
88
- retryStrategy: () => null,
89
- });
90
- redis.on("error", () => {
91
- // Fail-open: cache connectivity must never fail provider execution.
80
+ const redis = createProviderRedisClient({
81
+ redisUrl,
82
+ timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
83
+ onError: () => {
84
+ // Fail-open: cache connectivity must never fail provider execution.
85
+ },
92
86
  });
93
87
  backend.redis = redis;
94
88
  }
@@ -202,77 +196,20 @@ function sourceSummary(
202
196
  return "mixed";
203
197
  }
204
198
 
205
- async function withRedisTimeout<T>(
199
+ async function withRedisFallback<T>(
206
200
  operation: () => Promise<T>,
207
201
  ): Promise<T | undefined> {
208
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
209
- try {
210
- const timeout = new Promise<undefined>((resolve) => {
211
- timeoutId = setTimeout(
212
- () => resolve(undefined),
213
- DEFAULT_REDIS_TIMEOUT_MS,
214
- );
215
- });
216
- return await Promise.race([operation().catch(() => undefined), timeout]);
217
- } finally {
218
- if (timeoutId) clearTimeout(timeoutId);
219
- }
220
- }
221
-
222
- function redisStatus(redis: Redis): string {
223
- return redis.status;
224
- }
225
-
226
- async function waitForRedisReady(redis: Redis): Promise<boolean> {
227
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
228
- let settled = false;
229
-
230
- return await new Promise<boolean>((resolve) => {
231
- const cleanup = () => {
232
- if (timeoutId) clearTimeout(timeoutId);
233
- redis.off("ready", onReady);
234
- redis.off("close", onUnavailable);
235
- redis.off("end", onUnavailable);
236
- redis.off("error", onUnavailable);
237
- };
238
- const finish = (ready: boolean) => {
239
- if (settled) return;
240
- settled = true;
241
- cleanup();
242
- resolve(ready);
243
- };
244
- const onReady = () => finish(true);
245
- const onUnavailable = () => finish(false);
246
-
247
- timeoutId = setTimeout(
248
- () => finish(redisStatus(redis) === "ready"),
249
- DEFAULT_REDIS_TIMEOUT_MS,
250
- );
251
- redis.once("ready", onReady);
252
- redis.once("close", onUnavailable);
253
- redis.once("end", onUnavailable);
254
- redis.once("error", onUnavailable);
202
+ return await withRedisTimeout(operation, {
203
+ timeoutMs: DEFAULT_REDIS_TIMEOUT_MS,
204
+ onTimeout: () => undefined,
205
+ onError: () => undefined,
255
206
  });
256
207
  }
257
208
 
258
- async function ensureRedisReady(redis: Redis): Promise<boolean> {
259
- if (redisStatus(redis) === "ready") return true;
260
-
261
- if (redisStatus(redis) === "wait" || redisStatus(redis) === "end") {
262
- const connected = await withRedisTimeout(async () => {
263
- await redis.connect();
264
- return true;
265
- });
266
- return connected === true && redisStatus(redis) === "ready";
267
- }
268
-
269
- return await waitForRedisReady(redis);
270
- }
271
-
272
209
  export function createProviderCache(
273
210
  options: ProviderCacheOptions,
274
211
  ): ProviderCache {
275
- const redisUrl = options.redisUrl ?? redisUrlFromEnv();
212
+ const redisUrl = options.redisUrl ?? providerCacheRedisUrlFromEnv();
276
213
  const backend = getSharedBackend(redisUrl);
277
214
  const memoryMaxEntries = Math.max(
278
215
  1,
@@ -333,9 +270,11 @@ export function createProviderCache(
333
270
  result: ProviderCacheResult<T>;
334
271
  } | null> {
335
272
  const redis = backend.redis;
336
- if (!redis || !(await ensureRedisReady(redis))) return null;
273
+ if (!redis || !(await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS))) {
274
+ return null;
275
+ }
337
276
 
338
- const raw = await withRedisTimeout(async () => {
277
+ const raw = await withRedisFallback(async () => {
339
278
  return await redis.get(key);
340
279
  });
341
280
  if (typeof raw !== "string" && raw !== null) return null;
@@ -410,8 +349,10 @@ export function createProviderCache(
410
349
  rememberEnvelope(key, envelope, currentTime);
411
350
 
412
351
  const redis = backend.redis;
413
- if (!redis || !(await ensureRedisReady(redis))) return;
414
- await withRedisTimeout(() =>
352
+ if (!redis || !(await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS))) {
353
+ return;
354
+ }
355
+ await withRedisFallback(() =>
415
356
  redis.set(key, JSON.stringify(envelope), "PX", staleTtlMs),
416
357
  );
417
358
  }
@@ -464,8 +405,13 @@ export function createProviderCache(
464
405
  async delete(key: string): Promise<void> {
465
406
  backend.memory.delete(key);
466
407
  const redis = backend.redis;
467
- if (!redis || !(await ensureRedisReady(redis))) return;
468
- await withRedisTimeout(() => redis.del(key));
408
+ if (
409
+ !redis ||
410
+ !(await ensureRedisReady(redis, DEFAULT_REDIS_TIMEOUT_MS))
411
+ ) {
412
+ return;
413
+ }
414
+ await withRedisFallback(() => redis.del(key));
469
415
  },
470
416
 
471
417
  async getOrSet<T = unknown>(