@windrun-huaiin/backend-core 15.1.0 → 17.0.0

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 (66) hide show
  1. package/LICENSE +1 -1
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +44 -0
  5. package/dist/index.mjs +8 -1
  6. package/dist/lib/index.js +19 -0
  7. package/dist/lib/index.mjs +1 -1
  8. package/dist/lib/upstash/qstash.d.ts +20 -7
  9. package/dist/lib/upstash/qstash.d.ts.map +1 -1
  10. package/dist/lib/upstash/qstash.js +33 -7
  11. package/dist/lib/upstash/qstash.mjs +33 -7
  12. package/dist/lib/upstash/redis-structures.d.ts +83 -0
  13. package/dist/lib/upstash/redis-structures.d.ts.map +1 -1
  14. package/dist/lib/upstash/redis-structures.js +220 -0
  15. package/dist/lib/upstash/redis-structures.mjs +202 -1
  16. package/dist/lib/upstash-config.d.ts.map +1 -1
  17. package/dist/lib/upstash-config.js +76 -4
  18. package/dist/lib/upstash-config.mjs +76 -4
  19. package/dist/services/ai/abort.d.ts +2 -0
  20. package/dist/services/ai/abort.d.ts.map +1 -0
  21. package/dist/services/ai/abort.js +24 -0
  22. package/dist/services/ai/abort.mjs +22 -0
  23. package/dist/services/ai/env.d.ts +21 -0
  24. package/dist/services/ai/env.d.ts.map +1 -0
  25. package/dist/services/ai/env.js +85 -0
  26. package/dist/services/ai/env.mjs +80 -0
  27. package/dist/services/ai/error.d.ts +3 -0
  28. package/dist/services/ai/error.d.ts.map +1 -0
  29. package/dist/services/ai/error.js +54 -0
  30. package/dist/services/ai/error.mjs +52 -0
  31. package/dist/services/ai/index.d.ts +9 -0
  32. package/dist/services/ai/index.d.ts.map +1 -0
  33. package/dist/services/ai/index.js +30 -0
  34. package/dist/services/ai/index.mjs +7 -0
  35. package/dist/services/ai/message-builder.d.ts +4 -0
  36. package/dist/services/ai/message-builder.d.ts.map +1 -0
  37. package/dist/services/ai/message-builder.js +15 -0
  38. package/dist/services/ai/message-builder.mjs +13 -0
  39. package/dist/services/ai/mock.d.ts +30 -0
  40. package/dist/services/ai/mock.d.ts.map +1 -0
  41. package/dist/services/ai/mock.js +314 -0
  42. package/dist/services/ai/mock.mjs +308 -0
  43. package/dist/services/ai/openrouter-client.d.ts +12 -0
  44. package/dist/services/ai/openrouter-client.d.ts.map +1 -0
  45. package/dist/services/ai/openrouter-client.js +81 -0
  46. package/dist/services/ai/openrouter-client.mjs +78 -0
  47. package/dist/services/ai/route.d.ts +6 -0
  48. package/dist/services/ai/route.d.ts.map +1 -0
  49. package/dist/services/ai/route.js +178 -0
  50. package/dist/services/ai/route.mjs +173 -0
  51. package/dist/services/ai/types.d.ts +98 -0
  52. package/dist/services/ai/types.d.ts.map +1 -0
  53. package/package.json +11 -4
  54. package/src/index.ts +1 -0
  55. package/src/lib/upstash/qstash.ts +55 -15
  56. package/src/lib/upstash/redis-structures.ts +248 -0
  57. package/src/lib/upstash-config.ts +106 -4
  58. package/src/services/ai/abort.ts +26 -0
  59. package/src/services/ai/env.ts +120 -0
  60. package/src/services/ai/error.ts +64 -0
  61. package/src/services/ai/index.ts +8 -0
  62. package/src/services/ai/message-builder.ts +17 -0
  63. package/src/services/ai/mock.ts +378 -0
  64. package/src/services/ai/openrouter-client.ts +94 -0
  65. package/src/services/ai/route.ts +218 -0
  66. package/src/services/ai/types.ts +131 -0
package/src/index.ts CHANGED
@@ -3,4 +3,5 @@ export * from './services/database';
3
3
  export * from './services/aggregate';
4
4
  export * from './services/context';
5
5
  export * from './services/stripe';
6
+ export * from './services/ai';
6
7
  export * from './lib';
@@ -46,63 +46,103 @@ const getReceiver = (): Receiver | null => {
46
46
 
47
47
  export type PublishBody = Record<string, unknown> | string | number | boolean | null;
48
48
 
49
- export interface PublishMessageOptions {
49
+ export interface QstashEnvelope<TBody extends PublishBody = PublishBody> {
50
+ source_msg_id: string;
51
+ payload: TBody;
52
+ }
53
+
54
+ export interface PublishMessageOptions<TBody extends PublishBody = PublishBody> {
50
55
  url: string;
51
- body: PublishBody;
56
+ body: TBody;
52
57
  }
53
58
 
59
+ const generateSourceMessageId = (): string => {
60
+ try {
61
+ return crypto.randomUUID();
62
+ } catch {
63
+ return `${Date.now()}_${Math.random().toString(16).slice(2)}`;
64
+ }
65
+ };
66
+
67
+ const createEnvelope = <TBody extends PublishBody>(body: TBody): QstashEnvelope<TBody> => {
68
+ return {
69
+ source_msg_id: generateSourceMessageId(),
70
+ payload: body,
71
+ };
72
+ };
73
+
54
74
  /**
55
75
  * Publish a message. Returns message id or null if QStash is unavailable.
56
76
  */
57
- export const publishMessage = async (options: PublishMessageOptions): Promise<string | null> => {
77
+ export const publishMessage = async <TBody extends PublishBody>(
78
+ options: PublishMessageOptions<TBody>
79
+ ): Promise<{ messageId: string | null; message: QstashEnvelope<TBody> } | null> => {
80
+ const message = createEnvelope(options.body);
81
+
58
82
  return withQstash(async (client) => {
59
83
  const result = await (client as any).publishJSON({
60
84
  url: options.url,
61
- body: options.body,
85
+ body: message,
62
86
  });
63
- return typeof result === 'string' ? result : result?.messageId ?? null;
87
+ return {
88
+ messageId: typeof result === 'string' ? result : result?.messageId ?? null,
89
+ message,
90
+ };
64
91
  });
65
92
  };
66
93
 
67
94
  /**
68
95
  * Publish a delayed message. Returns message id or null if QStash is unavailable.
69
96
  */
70
- export const publishDelayedMessage = async (
71
- options: PublishMessageOptions & { delaySec: number }
72
- ): Promise<string | null> => {
97
+ export const publishDelayedMessage = async <TBody extends PublishBody>(
98
+ options: PublishMessageOptions<TBody> & { delaySec: number }
99
+ ): Promise<{ messageId: string | null; message: QstashEnvelope<TBody> } | null> => {
100
+ const message = createEnvelope(options.body);
101
+
73
102
  return withQstash(async (client) => {
74
103
  const result = await (client as any).publishJSON({
75
104
  url: options.url,
76
- body: options.body,
105
+ body: message,
77
106
  delay: options.delaySec,
78
107
  });
79
- return typeof result === 'string' ? result : result?.messageId ?? null;
108
+ return {
109
+ messageId: typeof result === 'string' ? result : result?.messageId ?? null,
110
+ message,
111
+ };
80
112
  });
81
113
  };
82
114
 
83
- export interface ScheduleMessageOptions extends PublishMessageOptions {
115
+ export interface ScheduleMessageOptions<TBody extends PublishBody = PublishBody>
116
+ extends PublishMessageOptions<TBody> {
84
117
  cron: string;
85
118
  }
86
119
 
87
120
  /**
88
121
  * Schedule a recurring message. Returns schedule id or null if QStash is unavailable.
89
122
  */
90
- export const scheduleMessage = async (options: ScheduleMessageOptions): Promise<string | null> => {
123
+ export const scheduleMessage = async <TBody extends PublishBody>(
124
+ options: ScheduleMessageOptions<TBody>
125
+ ): Promise<{ scheduleId: string | null; message: QstashEnvelope<TBody> } | null> => {
126
+ const message = createEnvelope(options.body);
127
+
91
128
  return withQstash(async (client) => {
92
129
  const anyClient = client as any;
93
130
  const result =
94
131
  (await anyClient.schedules?.create?.({
95
132
  url: options.url,
96
- body: options.body,
133
+ body: message,
97
134
  cron: options.cron,
98
135
  })) ??
99
136
  (await anyClient.publishJSON?.({
100
137
  url: options.url,
101
- body: options.body,
138
+ body: message,
102
139
  cron: options.cron,
103
140
  }));
104
141
 
105
- return typeof result === 'string' ? result : result?.scheduleId ?? result?.id ?? null;
142
+ return {
143
+ scheduleId: typeof result === 'string' ? result : result?.scheduleId ?? result?.id ?? null,
144
+ message,
145
+ };
106
146
  });
107
147
  };
108
148
 
@@ -1,5 +1,14 @@
1
+ import type { Redis } from '@upstash/redis';
2
+
1
3
  import { withRedis } from '../upstash-config';
2
4
 
5
+ export type RedisStringKeyValue = Record<string, string>;
6
+ export type RedisJsonKeyValue<T> = Record<string, T>;
7
+ export type RedisHashStringValue = Record<string, string>;
8
+ export type RedisPipelineBuilder<TResult> = (pipeline: ReturnType<Redis['pipeline']>) => {
9
+ exec: () => Promise<TResult>;
10
+ };
11
+
3
12
  /**
4
13
  * Set a plain string value with optional TTL (seconds).
5
14
  */
@@ -26,6 +35,34 @@ export const getString = async (key: string): Promise<string | null> => {
26
35
  return withRedis((redis) => redis.get<string>(key));
27
36
  };
28
37
 
38
+ /**
39
+ * MGET plain string values. Missing keys are returned as null.
40
+ */
41
+ export const mget = async (keys: string[]): Promise<(string | null)[] | null> => {
42
+ return withRedis((redis) => {
43
+ if (keys.length === 0) {
44
+ return [];
45
+ }
46
+
47
+ return redis.mget<(string | null)[]>(...keys);
48
+ });
49
+ };
50
+
51
+ /**
52
+ * MSET plain string values.
53
+ */
54
+ export const mset = async (entries: RedisStringKeyValue): Promise<boolean> => {
55
+ const keys = Object.keys(entries);
56
+ if (keys.length === 0) {
57
+ return true;
58
+ }
59
+
60
+ return withRedis(async (redis) => {
61
+ await redis.mset(entries);
62
+ return true;
63
+ }).then((result) => result ?? false);
64
+ };
65
+
29
66
  /**
30
67
  * Store an object as JSON string with optional TTL (seconds).
31
68
  */
@@ -64,6 +101,49 @@ export const getJson = async <T>(key: string): Promise<T | null> => {
64
101
  });
65
102
  };
66
103
 
104
+ /**
105
+ * MGET JSON values stored as strings. Missing or invalid values are returned as null.
106
+ */
107
+ export const mgetJson = async <T>(keys: string[]): Promise<(T | null)[] | null> => {
108
+ return withRedis(async (redis) => {
109
+ if (keys.length === 0) {
110
+ return [];
111
+ }
112
+
113
+ const payloads = await redis.mget<(string | null)[]>(...keys);
114
+ return payloads.map((payload) => {
115
+ if (!payload) {
116
+ return null;
117
+ }
118
+
119
+ try {
120
+ return JSON.parse(payload) as T;
121
+ } catch {
122
+ return null;
123
+ }
124
+ });
125
+ });
126
+ };
127
+
128
+ /**
129
+ * MSET JSON values as strings.
130
+ */
131
+ export const msetJson = async <T>(entries: RedisJsonKeyValue<T>): Promise<boolean> => {
132
+ const keys = Object.keys(entries);
133
+ if (keys.length === 0) {
134
+ return true;
135
+ }
136
+
137
+ const payloads = Object.fromEntries(
138
+ Object.entries(entries).map(([key, value]) => [key, JSON.stringify(value)])
139
+ ) as RedisStringKeyValue;
140
+
141
+ return withRedis(async (redis) => {
142
+ await redis.mset(payloads);
143
+ return true;
144
+ }).then((result) => result ?? false);
145
+ };
146
+
67
147
  /**
68
148
  * Delete a key. Returns false if Redis is unavailable.
69
149
  */
@@ -75,6 +155,52 @@ export const deleteKey = async (key: string): Promise<boolean> => {
75
155
  return result ?? false;
76
156
  };
77
157
 
158
+ /**
159
+ * DEL multiple keys. Returns deleted count, or null if Redis is unavailable.
160
+ */
161
+ export const del = async (keys: string[]): Promise<number | null> => {
162
+ return withRedis((redis) => {
163
+ if (keys.length === 0) {
164
+ return 0;
165
+ }
166
+
167
+ return redis.del(...keys);
168
+ });
169
+ };
170
+
171
+ /**
172
+ * EXISTS a key.
173
+ */
174
+ export const exists = async (key: string): Promise<boolean | null> => {
175
+ return withRedis(async (redis) => {
176
+ const count = await redis.exists(key);
177
+ return count > 0;
178
+ });
179
+ };
180
+
181
+ /**
182
+ * EXPIRE a key in seconds.
183
+ */
184
+ export const expire = async (key: string, ttlSec: number): Promise<boolean> => {
185
+ if (ttlSec <= 0) {
186
+ return false;
187
+ }
188
+
189
+ const result = await withRedis(async (redis) => {
190
+ const changed = await redis.expire(key, ttlSec);
191
+ return changed > 0;
192
+ });
193
+
194
+ return result ?? false;
195
+ };
196
+
197
+ /**
198
+ * TTL for a key in seconds. Returns null if Redis is unavailable.
199
+ */
200
+ export const ttl = async (key: string): Promise<number | null> => {
201
+ return withRedis((redis) => redis.ttl(key));
202
+ };
203
+
78
204
  /**
79
205
  * Set a hash field value.
80
206
  */
@@ -93,6 +219,42 @@ export const getHashField = async (key: string, field: string): Promise<string |
93
219
  return withRedis((redis) => redis.hget<string>(key, field));
94
220
  };
95
221
 
222
+ /**
223
+ * HMSET hash fields.
224
+ */
225
+ export const hmset = async (key: string, values: RedisHashStringValue): Promise<boolean> => {
226
+ const fields = Object.keys(values);
227
+ if (fields.length === 0) {
228
+ return true;
229
+ }
230
+
231
+ const result = await withRedis(async (redis) => {
232
+ await redis.hset(key, values);
233
+ return true;
234
+ });
235
+ return result ?? false;
236
+ };
237
+
238
+ /**
239
+ * HMGET hash fields.
240
+ */
241
+ export const hmget = async (
242
+ key: string,
243
+ fields: string[]
244
+ ): Promise<Record<string, string | null> | null> => {
245
+ return withRedis(async (redis) => {
246
+ if (fields.length === 0) {
247
+ return {};
248
+ }
249
+
250
+ const result = await redis.hmget<Record<string, string | null>>(key, ...fields);
251
+ if (!result) {
252
+ return Object.fromEntries(fields.map((field) => [field, null]));
253
+ }
254
+ return result;
255
+ });
256
+ };
257
+
96
258
  /**
97
259
  * Store a hash field as JSON string.
98
260
  */
@@ -137,6 +299,30 @@ export const getHashAll = async (key: string): Promise<Record<string, string> |
137
299
  });
138
300
  };
139
301
 
302
+ /**
303
+ * HEXISTS a hash field.
304
+ */
305
+ export const hexists = async (key: string, field: string): Promise<boolean | null> => {
306
+ return withRedis(async (redis) => {
307
+ const exists = await redis.hexists(key, field);
308
+ return exists > 0;
309
+ });
310
+ };
311
+
312
+ /**
313
+ * HKEYS for a hash.
314
+ */
315
+ export const hkeys = async (key: string): Promise<string[] | null> => {
316
+ return withRedis((redis) => redis.hkeys(key));
317
+ };
318
+
319
+ /**
320
+ * HLEN for a hash.
321
+ */
322
+ export const hlen = async (key: string): Promise<number | null> => {
323
+ return withRedis((redis) => redis.hlen(key));
324
+ };
325
+
140
326
  /**
141
327
  * Remove a hash field.
142
328
  */
@@ -148,6 +334,56 @@ export const deleteHashField = async (key: string, field: string): Promise<boole
148
334
  return result ?? false;
149
335
  };
150
336
 
337
+ /**
338
+ * SADD members to a set. Returns count of newly added members, or null if Redis is unavailable.
339
+ */
340
+ export const sadd = async (key: string, members: string[]): Promise<number | null> => {
341
+ return withRedis((redis) => {
342
+ if (members.length === 0) {
343
+ return 0;
344
+ }
345
+
346
+ return redis.sadd(key, members[0], ...members.slice(1));
347
+ });
348
+ };
349
+
350
+ /**
351
+ * SREM members from a set. Returns count of removed members, or null if Redis is unavailable.
352
+ */
353
+ export const srem = async (key: string, members: string[]): Promise<number | null> => {
354
+ return withRedis((redis) => {
355
+ if (members.length === 0) {
356
+ return 0;
357
+ }
358
+
359
+ return redis.srem(key, ...members);
360
+ });
361
+ };
362
+
363
+ /**
364
+ * SISMEMBER for a set member.
365
+ */
366
+ export const sismember = async (key: string, member: string): Promise<boolean | null> => {
367
+ return withRedis(async (redis) => {
368
+ const exists = await redis.sismember(key, member);
369
+ return exists > 0;
370
+ });
371
+ };
372
+
373
+ /**
374
+ * SMEMBERS for a set.
375
+ */
376
+ export const smembers = async (key: string): Promise<string[] | null> => {
377
+ return withRedis((redis) => redis.smembers<string[]>(key));
378
+ };
379
+
380
+ /**
381
+ * SCARD for a set.
382
+ */
383
+ export const scard = async (key: string): Promise<number | null> => {
384
+ return withRedis((redis) => redis.scard(key));
385
+ };
386
+
151
387
  type ListDirection = 'left' | 'right';
152
388
 
153
389
  /**
@@ -198,3 +434,15 @@ export const rangeList = async (
198
434
  export const listLength = async (key: string): Promise<number | null> => {
199
435
  return withRedis((redis) => redis.llen(key));
200
436
  };
437
+
438
+ /**
439
+ * Execute a Redis pipeline and return the result array from exec().
440
+ */
441
+ export const pipeline = async <TResult>(
442
+ build: RedisPipelineBuilder<TResult>
443
+ ): Promise<TResult | null> => {
444
+ return withRedis(async (redis) => {
445
+ const pipeline = redis.pipeline();
446
+ return build(pipeline).exec();
447
+ });
448
+ };
@@ -19,6 +19,7 @@ let qstashWarnedHealthSchedule = false;
19
19
 
20
20
  let redisHealthTimer: ReturnType<typeof setTimeout> | null = null;
21
21
  let qstashHealthTimer: ReturnType<typeof setTimeout> | null = null;
22
+ let cachedRedisPrefixed: Redis | null = null;
22
23
 
23
24
  const isNonEmpty = (value: string | undefined): value is string =>
24
25
  typeof value === 'string' && value.trim().length > 0;
@@ -32,6 +33,105 @@ const isValidUrl = (value: string): boolean => {
32
33
  }
33
34
  };
34
35
 
36
+ const getRequiredRedisAppName = (): string => {
37
+ const appName = process.env.NEXT_PUBLIC_APP_NAME;
38
+ if (!isNonEmpty(appName)) {
39
+ throw new Error(
40
+ '[Upstash Config] NEXT_PUBLIC_APP_NAME is required for Redis key prefixing and must not be empty'
41
+ );
42
+ }
43
+
44
+ const normalized = appName.replace(/\s+/g, '').toLowerCase();
45
+ if (!normalized) {
46
+ throw new Error(
47
+ '[Upstash Config] NEXT_PUBLIC_APP_NAME must contain non-whitespace characters for Redis key prefixing'
48
+ );
49
+ }
50
+
51
+ return normalized;
52
+ };
53
+
54
+ const getRedisKeyPrefix = (): string => {
55
+ const envSuffix = process.env.NODE_ENV === 'production' ? 'live' : 'test';
56
+ return `${getRequiredRedisAppName()}_${envSuffix}`;
57
+ };
58
+
59
+ const prefixRedisKey = (prefix: string, key: string): string => `${prefix}:${key}`;
60
+
61
+ const prefixRedisKeys = (prefix: string, keys: string[]): string[] =>
62
+ keys.map((key) => prefixRedisKey(prefix, key));
63
+
64
+ const prefixFirstStringArg = (args: unknown[], prefix: string): unknown[] => {
65
+ if (typeof args[0] !== 'string') {
66
+ return args;
67
+ }
68
+
69
+ const nextArgs = [...args];
70
+ nextArgs[0] = prefixRedisKey(prefix, args[0]);
71
+ return nextArgs;
72
+ };
73
+
74
+ const prefixAllStringArgs = (args: unknown[], prefix: string): unknown[] => {
75
+ return args.map((arg) => (typeof arg === 'string' ? prefixRedisKey(prefix, arg) : arg));
76
+ };
77
+
78
+ const keyArrayCommands = new Set(['mget', 'del']);
79
+ const allStringKeyCommands = new Set(['exists']);
80
+
81
+ const createPrefixedPipeline = <T extends object>(target: T, prefix: string): T => {
82
+ return new Proxy(target, {
83
+ get(obj, prop, receiver) {
84
+ const value = Reflect.get(obj, prop, receiver);
85
+ if (typeof value !== 'function') {
86
+ return value;
87
+ }
88
+
89
+ return (...args: unknown[]) => {
90
+ if (prop === 'eval' || prop === 'evalsha' || prop === 'evalro' || prop === 'evalshaRo') {
91
+ const [script, keys, argv] = args as [string, string[], unknown[]];
92
+ return (value as (...innerArgs: unknown[]) => unknown).call(
93
+ obj,
94
+ script,
95
+ prefixRedisKeys(prefix, keys),
96
+ argv
97
+ );
98
+ }
99
+
100
+ if (prop === 'pipeline' || prop === 'multi') {
101
+ const nested = (value as (...innerArgs: unknown[]) => unknown).call(obj);
102
+ return createPrefixedPipeline(nested as T, prefix);
103
+ }
104
+
105
+ if (typeof prop === 'string' && keyArrayCommands.has(prop)) {
106
+ const nextArgs = prefixAllStringArgs(args, prefix);
107
+ return (value as (...innerArgs: unknown[]) => unknown).apply(obj, nextArgs);
108
+ }
109
+
110
+ if (typeof prop === 'string' && allStringKeyCommands.has(prop)) {
111
+ const nextArgs = prefixAllStringArgs(args, prefix);
112
+ return (value as (...innerArgs: unknown[]) => unknown).apply(obj, nextArgs);
113
+ }
114
+
115
+ if (prop === 'mset') {
116
+ const [entries] = args as [Record<string, unknown>];
117
+ const prefixedEntries = Object.fromEntries(
118
+ Object.entries(entries).map(([key, entryValue]) => [prefixRedisKey(prefix, key), entryValue])
119
+ );
120
+ return (value as (...innerArgs: unknown[]) => unknown).call(obj, prefixedEntries);
121
+ }
122
+
123
+ if (prop === 'hmget') {
124
+ const nextArgs = prefixFirstStringArg(args, prefix);
125
+ return (value as (...innerArgs: unknown[]) => unknown).apply(obj, nextArgs);
126
+ }
127
+
128
+ const nextArgs = prefixFirstStringArg(args, prefix);
129
+ return (value as (...innerArgs: unknown[]) => unknown).apply(obj, nextArgs);
130
+ };
131
+ },
132
+ });
133
+ };
134
+
35
135
  const parseMinutes = (value: string | undefined, fallback: number): number => {
36
136
  if (!isNonEmpty(value)) {
37
137
  return fallback;
@@ -138,12 +238,12 @@ const scheduleQstashHealthCheck = (token: string): void => {
138
238
  * - read-through cached instance only
139
239
  */
140
240
  export const getRedis = (): Redis | null => {
141
- return cachedRedis;
241
+ return cachedRedisPrefixed;
142
242
  };
143
243
 
144
244
  const ensureRedis = async (): Promise<Redis | null> => {
145
- if (cachedRedis) {
146
- return cachedRedis;
245
+ if (cachedRedisPrefixed) {
246
+ return cachedRedisPrefixed;
147
247
  }
148
248
  if (redisInitPromise) {
149
249
  return redisInitPromise;
@@ -170,14 +270,16 @@ const ensureRedis = async (): Promise<Redis | null> => {
170
270
  }
171
271
 
172
272
  try {
273
+ const keyPrefix = getRedisKeyPrefix();
173
274
  const client = new Redis({
174
275
  url: UPSTASH_REDIS_REST_URL,
175
276
  token: UPSTASH_REDIS_REST_TOKEN,
176
277
  });
177
278
  await client.ping();
178
279
  cachedRedis = client;
280
+ cachedRedisPrefixed = createPrefixedPipeline(client, keyPrefix) as Redis;
179
281
  scheduleRedisHealthCheck();
180
- return cachedRedis;
282
+ return cachedRedisPrefixed;
181
283
  } catch (error) {
182
284
  if (!redisWarnedInitError) {
183
285
  redisWarnedInitError = true;
@@ -0,0 +1,26 @@
1
+ export function createUpstreamAbortSignal(requestSignal: AbortSignal, timeoutMs: number) {
2
+ const controller = new AbortController();
3
+ const timeoutId = setTimeout(() => controller.abort('timeout'), timeoutMs);
4
+
5
+ const forwardAbort = () => {
6
+ clearTimeout(timeoutId);
7
+ controller.abort(requestSignal.reason ?? 'request_aborted');
8
+ };
9
+
10
+ if (requestSignal.aborted) {
11
+ forwardAbort();
12
+ } else {
13
+ requestSignal.addEventListener('abort', forwardAbort, { once: true });
14
+ }
15
+
16
+ controller.signal.addEventListener(
17
+ 'abort',
18
+ () => {
19
+ clearTimeout(timeoutId);
20
+ requestSignal.removeEventListener('abort', forwardAbort);
21
+ },
22
+ { once: true },
23
+ );
24
+
25
+ return controller.signal;
26
+ }