@beignet/provider-redis 0.0.1

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 ADDED
@@ -0,0 +1,5 @@
1
+ # @beignet/provider-redis
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial Beignet release under the `@beignet` npm scope.
package/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # @beignet/provider-redis
2
+
3
+ Redis-backed `CachePort` provider for Beignet applications.
4
+
5
+ The provider installs `ctx.ports.cache` using
6
+ [ioredis](https://github.com/redis/ioredis) and exposes the Redis client only
7
+ as an escape hatch for Redis-specific features.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ bun add @beignet/provider-redis ioredis
13
+ ```
14
+
15
+ ## Setup
16
+
17
+ ```typescript
18
+ import { createNextServer } from "@beignet/next";
19
+ import { definePorts } from "@beignet/core/ports";
20
+ import { redisProvider } from "@beignet/provider-redis";
21
+ import { routes } from "@/server/routes";
22
+
23
+ // Set environment variables:
24
+ // REDIS_URL=redis://localhost:6379
25
+ // REDIS_DB=0 (optional)
26
+
27
+ const appPorts = definePorts({});
28
+
29
+ export const server = await createNextServer({
30
+ ports: appPorts,
31
+ providers: [redisProvider],
32
+ createContext: ({ ports }) => ({
33
+ ports,
34
+ }),
35
+ routes,
36
+ });
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ Once the provider is registered, your ports will include a `cache` property:
42
+
43
+ ```typescript
44
+ // In your use case
45
+ async function getUserProfile(ctx: AppCtx) {
46
+ const userId = ctx.actor.type === "user" ? ctx.actor.id : undefined;
47
+ if (!userId) throw new Error("User actor required.");
48
+
49
+ const cacheKey = `user:${userId}:profile`;
50
+
51
+ // Try to get from cache
52
+ const cached = await ctx.ports.cache.get(cacheKey);
53
+ if (cached) {
54
+ return JSON.parse(cached);
55
+ }
56
+
57
+ // Fetch from database
58
+ const profile = await ctx.ports.db.users.findById(userId);
59
+
60
+ // Store in cache for 1 hour
61
+ await ctx.ports.cache.set(
62
+ cacheKey,
63
+ JSON.stringify(profile),
64
+ { ttlSeconds: 3600 }
65
+ );
66
+
67
+ return profile;
68
+ }
69
+ ```
70
+
71
+ ## Configuration
72
+
73
+ The Redis provider reads configuration from environment variables with the `REDIS_` prefix:
74
+
75
+ | Variable | Required | Description | Example |
76
+ |----------|----------|-------------|---------|
77
+ | `REDIS_URL` | Yes | Redis connection URL | `redis://localhost:6379` |
78
+ | `REDIS_DB` | No | Redis database number (default: 0) | `0` |
79
+
80
+ ## Cache port API
81
+
82
+ The provider extends your ports with the following cache interface:
83
+
84
+ ### `get(key: string): Promise<string | null>`
85
+
86
+ Get a value from the cache.
87
+
88
+ ```typescript
89
+ const value = await ctx.ports.cache.get("my-key");
90
+ ```
91
+
92
+ ### `set(key: string, value: string, options?: { ttlSeconds?: number }): Promise<void>`
93
+
94
+ Set a value in the cache with optional TTL (time-to-live) in seconds.
95
+
96
+ ```typescript
97
+ // Without TTL (persists forever)
98
+ await ctx.ports.cache.set("key", "value");
99
+
100
+ // With TTL (expires after 1 hour)
101
+ await ctx.ports.cache.set("key", "value", { ttlSeconds: 3600 });
102
+ ```
103
+
104
+ ### `delete(key: string): Promise<boolean>`
105
+
106
+ Delete a key from the cache. Returns `true` when a key was deleted.
107
+
108
+ ```typescript
109
+ const deleted = await ctx.ports.cache.delete("my-key");
110
+ ```
111
+
112
+ ### `has(key: string): Promise<boolean>`
113
+
114
+ Check if a key exists in the cache.
115
+
116
+ ```typescript
117
+ const exists = await ctx.ports.cache.has("my-key");
118
+ ```
119
+
120
+ ### `remember(key: string, factory: () => Promise<string>, options?: { ttlSeconds?: number }): Promise<string>`
121
+
122
+ Return the cached value when present. On a miss, compute, store, and return the
123
+ factory value.
124
+
125
+ ```typescript
126
+ const value = await ctx.ports.cache.remember(
127
+ "my-key",
128
+ async () => JSON.stringify(await loadExpensiveData()),
129
+ { ttlSeconds: 300 },
130
+ );
131
+ ```
132
+
133
+ ### `client: Redis`
134
+
135
+ Access the underlying ioredis client for advanced operations.
136
+
137
+ ```typescript
138
+ // Use ioredis methods directly
139
+ await ctx.ports.cache.client.expire("key", 300);
140
+ await ctx.ports.cache.client.incr("counter");
141
+ ```
142
+
143
+ ## Devtools
144
+
145
+ When `@beignet/devtools` is installed before this provider, Redis cache
146
+ operations appear under the dashboard's Cache watcher.
147
+
148
+ The provider records `cache.get`, `cache.set`, `cache.delete`, `cache.has`, and
149
+ `cache.remember` events with the cache key, hit/miss or deleted status, TTL, and
150
+ duration. Cached values are not recorded.
151
+
152
+ ## TypeScript support
153
+
154
+ To get proper type inference for the cache port, extend your ports type:
155
+
156
+ ```typescript
157
+ import type { RedisCachePort } from "@beignet/provider-redis";
158
+
159
+ // Your base ports, if any
160
+ const basePorts = definePorts({});
161
+
162
+ // After using redisProvider, your ports will have this shape:
163
+ type AppPorts = typeof basePorts & {
164
+ cache: RedisCachePort;
165
+ };
166
+ ```
167
+
168
+ ## Lifecycle
169
+
170
+ The Redis provider:
171
+
172
+ 1. **During `setup`**: Connects to Redis and returns the `cache` port
173
+ 2. **During `stop`**: Gracefully closes the Redis connection
174
+
175
+ ## Error handling
176
+
177
+ The provider will throw errors in these cases:
178
+
179
+ - Missing `REDIS_URL` environment variable
180
+ - Failed connection to Redis server
181
+
182
+ Make sure to handle these during application startup.
183
+
184
+ ## License
185
+
186
+ MIT
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @beignet/provider-redis
3
+ *
4
+ * Redis provider that extends ports with cache capabilities using ioredis.
5
+ *
6
+ * Configuration:
7
+ * - REDIS_URL: Redis connection URL (required)
8
+ * - REDIS_DB: Redis database number (optional, default: 0)
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createNextServer } from "@beignet/next";
13
+ * import { redisProvider } from "@beignet/provider-redis";
14
+ *
15
+ * const server = await createNextServer({
16
+ * ports: basePorts,
17
+ * providers: [redisProvider],
18
+ * // ...
19
+ * });
20
+ *
21
+ * // In your use cases:
22
+ * async function myUseCase(ctx: AppCtx) {
23
+ * const cached = await ctx.ports.cache.get("key");
24
+ * if (!cached) {
25
+ * const value = await fetchData();
26
+ * await ctx.ports.cache.set("key", value, { ttlSeconds: 3600 });
27
+ * }
28
+ * }
29
+ * ```
30
+ */
31
+ import type { CachePort } from "@beignet/core/ports";
32
+ import Redis from "ioredis";
33
+ import { z } from "zod";
34
+ /**
35
+ * Configuration schema for the Redis provider.
36
+ * Validates environment variables with REDIS_ prefix.
37
+ */
38
+ declare const RedisConfigSchema: z.ZodObject<{
39
+ URL: z.ZodString;
40
+ DB: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
41
+ }, z.core.$strip>;
42
+ /**
43
+ * Inferred configuration type for Redis provider.
44
+ */
45
+ export type RedisConfig = z.infer<typeof RedisConfigSchema>;
46
+ /**
47
+ * Extended cache port interface that includes the Redis client.
48
+ * The Redis provider adds the underlying client for advanced operations.
49
+ */
50
+ export interface RedisCachePort extends CachePort {
51
+ /**
52
+ * The underlying Redis client instance.
53
+ * Use this for advanced operations not covered by the cache interface.
54
+ */
55
+ client: Redis;
56
+ }
57
+ /**
58
+ * Redis provider that extends ports with cache capabilities.
59
+ *
60
+ * Configuration via environment variables:
61
+ * - REDIS_URL: Redis connection URL (required)
62
+ * - REDIS_DB: Redis database number (optional)
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * const server = await createNextServer({
67
+ * ports: basePorts,
68
+ * providers: [redisProvider],
69
+ * // ...
70
+ * });
71
+ * ```
72
+ */
73
+ export declare const redisProvider: import("@beignet/core/providers").ServiceProvider<unknown, z.ZodObject<{
74
+ URL: z.ZodString;
75
+ DB: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
76
+ }, z.core.$strip>, {
77
+ cache: RedisCachePort;
78
+ }>;
79
+ export {};
80
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAKrD,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,QAAA,MAAM,iBAAiB;;;iBAYrB,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;GAGG;AACH,MAAM,WAAW,cAAe,SAAQ,SAAS;IAC/C;;;OAGG;IACH,MAAM,EAAE,KAAK,CAAC;CACf;AAMD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa;;;;;EAmPxB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,295 @@
1
+ /**
2
+ * @beignet/provider-redis
3
+ *
4
+ * Redis provider that extends ports with cache capabilities using ioredis.
5
+ *
6
+ * Configuration:
7
+ * - REDIS_URL: Redis connection URL (required)
8
+ * - REDIS_DB: Redis database number (optional, default: 0)
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createNextServer } from "@beignet/next";
13
+ * import { redisProvider } from "@beignet/provider-redis";
14
+ *
15
+ * const server = await createNextServer({
16
+ * ports: basePorts,
17
+ * providers: [redisProvider],
18
+ * // ...
19
+ * });
20
+ *
21
+ * // In your use cases:
22
+ * async function myUseCase(ctx: AppCtx) {
23
+ * const cached = await ctx.ports.cache.get("key");
24
+ * if (!cached) {
25
+ * const value = await fetchData();
26
+ * await ctx.ports.cache.set("key", value, { ttlSeconds: 3600 });
27
+ * }
28
+ * }
29
+ * ```
30
+ */
31
+ import { createProvider, createProviderInstrumentation, } from "@beignet/core/providers";
32
+ import Redis from "ioredis";
33
+ import { z } from "zod";
34
+ /**
35
+ * Configuration schema for the Redis provider.
36
+ * Validates environment variables with REDIS_ prefix.
37
+ */
38
+ const RedisConfigSchema = z.object({
39
+ /**
40
+ * Redis connection URL.
41
+ * Example: "redis://localhost:6379"
42
+ */
43
+ URL: z.string().url(),
44
+ /**
45
+ * Redis database number (optional).
46
+ * Defaults to 0 if not specified.
47
+ */
48
+ DB: z.coerce.number().int().min(0).optional(),
49
+ });
50
+ function errorMessage(error) {
51
+ return error instanceof Error ? error.message : String(error);
52
+ }
53
+ /**
54
+ * Redis provider that extends ports with cache capabilities.
55
+ *
56
+ * Configuration via environment variables:
57
+ * - REDIS_URL: Redis connection URL (required)
58
+ * - REDIS_DB: Redis database number (optional)
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const server = await createNextServer({
63
+ * ports: basePorts,
64
+ * providers: [redisProvider],
65
+ * // ...
66
+ * });
67
+ * ```
68
+ */
69
+ export const redisProvider = createProvider({
70
+ name: "redis",
71
+ config: {
72
+ schema: RedisConfigSchema,
73
+ envPrefix: "REDIS_",
74
+ },
75
+ async setup({ ports, config }) {
76
+ if (!config) {
77
+ throw new Error("[redisProvider] Missing Redis config. " +
78
+ "Please set REDIS_URL environment variable.");
79
+ }
80
+ // Create Redis client
81
+ const client = new Redis(config.URL, {
82
+ db: config.DB ?? 0,
83
+ // Prevent ioredis from logging connection errors to console
84
+ // (we'll handle errors in the application)
85
+ lazyConnect: true,
86
+ });
87
+ // Test connection
88
+ try {
89
+ await client.connect();
90
+ }
91
+ catch (error) {
92
+ throw new Error(`[redisProvider] Failed to connect to Redis at ${config.URL}: ${error instanceof Error ? error.message : String(error)}`);
93
+ }
94
+ const instrumentation = createProviderInstrumentation(ports, {
95
+ providerName: "redis",
96
+ watcher: "cache",
97
+ });
98
+ const recordCacheEvent = (event) => {
99
+ instrumentation.custom({
100
+ name: `cache.${event.operation}`,
101
+ label: `Cache ${event.operation}`,
102
+ summary: event.summary,
103
+ details: {
104
+ operation: event.operation,
105
+ key: event.key,
106
+ ...event.details,
107
+ },
108
+ });
109
+ };
110
+ // Extend ports with cache API
111
+ const cache = {
112
+ get: async (key) => {
113
+ const startedAt = Date.now();
114
+ try {
115
+ const value = await client.get(key);
116
+ recordCacheEvent({
117
+ operation: "get",
118
+ key,
119
+ summary: value == null ? "Cache miss" : "Cache hit",
120
+ details: {
121
+ hit: value != null,
122
+ durationMs: Date.now() - startedAt,
123
+ },
124
+ });
125
+ return value;
126
+ }
127
+ catch (error) {
128
+ recordCacheEvent({
129
+ operation: "get.failed",
130
+ key,
131
+ summary: "Cache get failed",
132
+ details: {
133
+ durationMs: Date.now() - startedAt,
134
+ error: errorMessage(error),
135
+ },
136
+ });
137
+ throw error;
138
+ }
139
+ },
140
+ set: async (key, value, options) => {
141
+ const startedAt = Date.now();
142
+ try {
143
+ if (options?.ttlSeconds != null && options.ttlSeconds > 0) {
144
+ await client.set(key, value, "EX", options.ttlSeconds);
145
+ }
146
+ else {
147
+ await client.set(key, value);
148
+ }
149
+ recordCacheEvent({
150
+ operation: "set",
151
+ key,
152
+ summary: "Cache set",
153
+ details: {
154
+ ttlSeconds: options?.ttlSeconds ?? null,
155
+ durationMs: Date.now() - startedAt,
156
+ },
157
+ });
158
+ }
159
+ catch (error) {
160
+ recordCacheEvent({
161
+ operation: "set.failed",
162
+ key,
163
+ summary: "Cache set failed",
164
+ details: {
165
+ ttlSeconds: options?.ttlSeconds ?? null,
166
+ durationMs: Date.now() - startedAt,
167
+ error: errorMessage(error),
168
+ },
169
+ });
170
+ throw error;
171
+ }
172
+ },
173
+ delete: async (key) => {
174
+ const startedAt = Date.now();
175
+ try {
176
+ const deleted = (await client.del(key)) > 0;
177
+ recordCacheEvent({
178
+ operation: "delete",
179
+ key,
180
+ summary: deleted ? "Cache key deleted" : "Cache key missing",
181
+ details: {
182
+ deleted,
183
+ durationMs: Date.now() - startedAt,
184
+ },
185
+ });
186
+ return deleted;
187
+ }
188
+ catch (error) {
189
+ recordCacheEvent({
190
+ operation: "delete.failed",
191
+ key,
192
+ summary: "Cache delete failed",
193
+ details: {
194
+ durationMs: Date.now() - startedAt,
195
+ error: errorMessage(error),
196
+ },
197
+ });
198
+ throw error;
199
+ }
200
+ },
201
+ has: async (key) => {
202
+ const startedAt = Date.now();
203
+ try {
204
+ const result = await client.exists(key);
205
+ const exists = result === 1;
206
+ recordCacheEvent({
207
+ operation: "has",
208
+ key,
209
+ summary: exists ? "Cache key exists" : "Cache key missing",
210
+ details: {
211
+ exists,
212
+ durationMs: Date.now() - startedAt,
213
+ },
214
+ });
215
+ return exists;
216
+ }
217
+ catch (error) {
218
+ recordCacheEvent({
219
+ operation: "has.failed",
220
+ key,
221
+ summary: "Cache has failed",
222
+ details: {
223
+ durationMs: Date.now() - startedAt,
224
+ error: errorMessage(error),
225
+ },
226
+ });
227
+ throw error;
228
+ }
229
+ },
230
+ remember: async (key, factory, options) => {
231
+ const startedAt = Date.now();
232
+ try {
233
+ const cached = await client.get(key);
234
+ if (cached != null) {
235
+ recordCacheEvent({
236
+ operation: "remember",
237
+ key,
238
+ summary: "Cache remember hit",
239
+ details: {
240
+ hit: true,
241
+ ttlSeconds: options?.ttlSeconds ?? null,
242
+ durationMs: Date.now() - startedAt,
243
+ },
244
+ });
245
+ return cached;
246
+ }
247
+ const value = await factory();
248
+ if (options?.ttlSeconds != null && options.ttlSeconds > 0) {
249
+ await client.set(key, value, "EX", options.ttlSeconds);
250
+ }
251
+ else {
252
+ await client.set(key, value);
253
+ }
254
+ recordCacheEvent({
255
+ operation: "remember",
256
+ key,
257
+ summary: "Cache remember miss",
258
+ details: {
259
+ hit: false,
260
+ ttlSeconds: options?.ttlSeconds ?? null,
261
+ durationMs: Date.now() - startedAt,
262
+ },
263
+ });
264
+ return value;
265
+ }
266
+ catch (error) {
267
+ recordCacheEvent({
268
+ operation: "remember.failed",
269
+ key,
270
+ summary: "Cache remember failed",
271
+ details: {
272
+ ttlSeconds: options?.ttlSeconds ?? null,
273
+ durationMs: Date.now() - startedAt,
274
+ error: errorMessage(error),
275
+ },
276
+ });
277
+ throw error;
278
+ }
279
+ },
280
+ client,
281
+ };
282
+ return {
283
+ ports: { cache },
284
+ async stop() {
285
+ try {
286
+ await client.quit();
287
+ }
288
+ catch {
289
+ // Silently ignore errors during shutdown
290
+ }
291
+ },
292
+ };
293
+ },
294
+ });
295
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,EACL,cAAc,EACd,6BAA6B,GAC9B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACjC;;;OAGG;IACH,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAErB;;;OAGG;IACH,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAC9C,CAAC,CAAC;AAmBH,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,cAAc,CAAC;IAC1C,IAAI,EAAE,OAAO;IAEb,MAAM,EAAE;QACN,MAAM,EAAE,iBAAiB;QACzB,SAAS,EAAE,QAAQ;KACpB;IAED,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;QAC3B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CACb,wCAAwC;gBACtC,4CAA4C,CAC/C,CAAC;QACJ,CAAC;QAED,sBAAsB;QACtB,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE;YACnC,EAAE,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC;YAClB,4DAA4D;YAC5D,2CAA2C;YAC3C,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;QAEH,kBAAkB;QAClB,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;QACzB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,iDAAiD,MAAM,CAAC,GAAG,KACzD,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,EAAE,CACH,CAAC;QACJ,CAAC;QAED,MAAM,eAAe,GAAG,6BAA6B,CAAC,KAAK,EAAE;YAC3D,YAAY,EAAE,OAAO;YACrB,OAAO,EAAE,OAAO;SACjB,CAAC,CAAC;QAEH,MAAM,gBAAgB,GAAG,CAAC,KAKzB,EAAE,EAAE;YACH,eAAe,CAAC,MAAM,CAAC;gBACrB,IAAI,EAAE,SAAS,KAAK,CAAC,SAAS,EAAE;gBAChC,KAAK,EAAE,SAAS,KAAK,CAAC,SAAS,EAAE;gBACjC,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,OAAO,EAAE;oBACP,SAAS,EAAE,KAAK,CAAC,SAAS;oBAC1B,GAAG,EAAE,KAAK,CAAC,GAAG;oBACd,GAAG,KAAK,CAAC,OAAO;iBACjB;aACF,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,8BAA8B;QAC9B,MAAM,KAAK,GAAmB;YAC5B,GAAG,EAAE,KAAK,EAAE,GAAW,EAAE,EAAE;gBACzB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC7B,IAAI,CAAC;oBACH,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;oBACpC,gBAAgB,CAAC;wBACf,SAAS,EAAE,KAAK;wBAChB,GAAG;wBACH,OAAO,EAAE,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW;wBACnD,OAAO,EAAE;4BACP,GAAG,EAAE,KAAK,IAAI,IAAI;4BAClB,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;yBACnC;qBACF,CAAC,CAAC;oBACH,OAAO,KAAK,CAAC;gBACf,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,gBAAgB,CAAC;wBACf,SAAS,EAAE,YAAY;wBACvB,GAAG;wBACH,OAAO,EAAE,kBAAkB;wBAC3B,OAAO,EAAE;4BACP,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;4BAClC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC;yBAC3B;qBACF,CAAC,CAAC;oBACH,MAAM,KAAK,CAAC;gBACd,CAAC;YACH,CAAC;YAED,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;gBACjC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC7B,IAAI,CAAC;oBACH,IAAI,OAAO,EAAE,UAAU,IAAI,IAAI,IAAI,OAAO,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;wBAC1D,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;oBACzD,CAAC;yBAAM,CAAC;wBACN,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;oBAC/B,CAAC;oBACD,gBAAgB,CAAC;wBACf,SAAS,EAAE,KAAK;wBAChB,GAAG;wBACH,OAAO,EAAE,WAAW;wBACpB,OAAO,EAAE;4BACP,UAAU,EAAE,OAAO,EAAE,UAAU,IAAI,IAAI;4BACvC,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;yBACnC;qBACF,CAAC,CAAC;gBACL,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,gBAAgB,CAAC;wBACf,SAAS,EAAE,YAAY;wBACvB,GAAG;wBACH,OAAO,EAAE,kBAAkB;wBAC3B,OAAO,EAAE;4BACP,UAAU,EAAE,OAAO,EAAE,UAAU,IAAI,IAAI;4BACvC,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;4BAClC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC;yBAC3B;qBACF,CAAC,CAAC;oBACH,MAAM,KAAK,CAAC;gBACd,CAAC;YACH,CAAC;YAED,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;gBACpB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC7B,IAAI,CAAC;oBACH,MAAM,OAAO,GAAG,CAAC,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;oBAC5C,gBAAgB,CAAC;wBACf,SAAS,EAAE,QAAQ;wBACnB,GAAG;wBACH,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,mBAAmB;wBAC5D,OAAO,EAAE;4BACP,OAAO;4BACP,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;yBACnC;qBACF,CAAC,CAAC;oBACH,OAAO,OAAO,CAAC;gBACjB,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,gBAAgB,CAAC;wBACf,SAAS,EAAE,eAAe;wBAC1B,GAAG;wBACH,OAAO,EAAE,qBAAqB;wBAC9B,OAAO,EAAE;4BACP,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;4BAClC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC;yBAC3B;qBACF,CAAC,CAAC;oBACH,MAAM,KAAK,CAAC;gBACd,CAAC;YACH,CAAC;YAED,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;gBACjB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC7B,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;oBACxC,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,CAAC;oBAC5B,gBAAgB,CAAC;wBACf,SAAS,EAAE,KAAK;wBAChB,GAAG;wBACH,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,mBAAmB;wBAC1D,OAAO,EAAE;4BACP,MAAM;4BACN,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;yBACnC;qBACF,CAAC,CAAC;oBACH,OAAO,MAAM,CAAC;gBAChB,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,gBAAgB,CAAC;wBACf,SAAS,EAAE,YAAY;wBACvB,GAAG;wBACH,OAAO,EAAE,kBAAkB;wBAC3B,OAAO,EAAE;4BACP,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;4BAClC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC;yBAC3B;qBACF,CAAC,CAAC;oBACH,MAAM,KAAK,CAAC;gBACd,CAAC;YACH,CAAC;YAED,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;gBACxC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC7B,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;oBACrC,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;wBACnB,gBAAgB,CAAC;4BACf,SAAS,EAAE,UAAU;4BACrB,GAAG;4BACH,OAAO,EAAE,oBAAoB;4BAC7B,OAAO,EAAE;gCACP,GAAG,EAAE,IAAI;gCACT,UAAU,EAAE,OAAO,EAAE,UAAU,IAAI,IAAI;gCACvC,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;6BACnC;yBACF,CAAC,CAAC;wBACH,OAAO,MAAM,CAAC;oBAChB,CAAC;oBAED,MAAM,KAAK,GAAG,MAAM,OAAO,EAAE,CAAC;oBAC9B,IAAI,OAAO,EAAE,UAAU,IAAI,IAAI,IAAI,OAAO,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;wBAC1D,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;oBACzD,CAAC;yBAAM,CAAC;wBACN,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;oBAC/B,CAAC;oBAED,gBAAgB,CAAC;wBACf,SAAS,EAAE,UAAU;wBACrB,GAAG;wBACH,OAAO,EAAE,qBAAqB;wBAC9B,OAAO,EAAE;4BACP,GAAG,EAAE,KAAK;4BACV,UAAU,EAAE,OAAO,EAAE,UAAU,IAAI,IAAI;4BACvC,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;yBACnC;qBACF,CAAC,CAAC;oBAEH,OAAO,KAAK,CAAC;gBACf,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,gBAAgB,CAAC;wBACf,SAAS,EAAE,iBAAiB;wBAC5B,GAAG;wBACH,OAAO,EAAE,uBAAuB;wBAChC,OAAO,EAAE;4BACP,UAAU,EAAE,OAAO,EAAE,UAAU,IAAI,IAAI;4BACvC,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS;4BAClC,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC;yBAC3B;qBACF,CAAC,CAAC;oBACH,MAAM,KAAK,CAAC;gBACd,CAAC;YACH,CAAC;YAED,MAAM;SACP,CAAC;QAEF,OAAO;YACL,KAAK,EAAE,EAAE,KAAK,EAAE;YAChB,KAAK,CAAC,IAAI;gBACR,IAAI,CAAC;oBACH,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;gBACtB,CAAC;gBAAC,MAAM,CAAC;oBACP,yCAAyC;gBAC3C,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@beignet/provider-redis",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Redis provider for Beignet - adds cache port using ioredis",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "!src/**/*.test.ts",
18
+ "!src/**/*.test.tsx",
19
+ "!src/**/*.test-d.ts",
20
+ "README.md",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "dev": "tsc --watch",
26
+ "clean": "rm -rf dist coverage .turbo",
27
+ "test": "bun test",
28
+ "test:coverage": "bun test --coverage",
29
+ "lint": "biome check ."
30
+ },
31
+ "keywords": [
32
+ "beignet",
33
+ "redis",
34
+ "provider",
35
+ "cache",
36
+ "ports"
37
+ ],
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/taylorbryant/beignet.git",
42
+ "directory": "packages/provider-redis"
43
+ },
44
+ "author": "Taylor Bryant",
45
+ "homepage": "https://github.com/taylorbryant/beignet#readme",
46
+ "bugs": "https://github.com/taylorbryant/beignet/issues",
47
+ "sideEffects": false,
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "engines": {
52
+ "node": ">=18.0.0"
53
+ },
54
+ "peerDependencies": {
55
+ "ioredis": "^5.0.0"
56
+ },
57
+ "dependencies": {
58
+ "zod": "^4.0.0",
59
+ "@beignet/core": "*"
60
+ },
61
+ "devDependencies": {
62
+ "@beignet/devtools": "*",
63
+ "@types/bun": "^1.3.13",
64
+ "@types/node": "^20.10.0",
65
+ "ioredis": "^5.0.0",
66
+ "typescript": "^5.3.0"
67
+ }
68
+ }
package/src/index.ts ADDED
@@ -0,0 +1,338 @@
1
+ /**
2
+ * @beignet/provider-redis
3
+ *
4
+ * Redis provider that extends ports with cache capabilities using ioredis.
5
+ *
6
+ * Configuration:
7
+ * - REDIS_URL: Redis connection URL (required)
8
+ * - REDIS_DB: Redis database number (optional, default: 0)
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createNextServer } from "@beignet/next";
13
+ * import { redisProvider } from "@beignet/provider-redis";
14
+ *
15
+ * const server = await createNextServer({
16
+ * ports: basePorts,
17
+ * providers: [redisProvider],
18
+ * // ...
19
+ * });
20
+ *
21
+ * // In your use cases:
22
+ * async function myUseCase(ctx: AppCtx) {
23
+ * const cached = await ctx.ports.cache.get("key");
24
+ * if (!cached) {
25
+ * const value = await fetchData();
26
+ * await ctx.ports.cache.set("key", value, { ttlSeconds: 3600 });
27
+ * }
28
+ * }
29
+ * ```
30
+ */
31
+
32
+ import type { CachePort } from "@beignet/core/ports";
33
+ import {
34
+ createProvider,
35
+ createProviderInstrumentation,
36
+ } from "@beignet/core/providers";
37
+ import Redis from "ioredis";
38
+ import { z } from "zod";
39
+
40
+ /**
41
+ * Configuration schema for the Redis provider.
42
+ * Validates environment variables with REDIS_ prefix.
43
+ */
44
+ const RedisConfigSchema = z.object({
45
+ /**
46
+ * Redis connection URL.
47
+ * Example: "redis://localhost:6379"
48
+ */
49
+ URL: z.string().url(),
50
+
51
+ /**
52
+ * Redis database number (optional).
53
+ * Defaults to 0 if not specified.
54
+ */
55
+ DB: z.coerce.number().int().min(0).optional(),
56
+ });
57
+
58
+ /**
59
+ * Inferred configuration type for Redis provider.
60
+ */
61
+ export type RedisConfig = z.infer<typeof RedisConfigSchema>;
62
+
63
+ /**
64
+ * Extended cache port interface that includes the Redis client.
65
+ * The Redis provider adds the underlying client for advanced operations.
66
+ */
67
+ export interface RedisCachePort extends CachePort {
68
+ /**
69
+ * The underlying Redis client instance.
70
+ * Use this for advanced operations not covered by the cache interface.
71
+ */
72
+ client: Redis;
73
+ }
74
+
75
+ function errorMessage(error: unknown): string {
76
+ return error instanceof Error ? error.message : String(error);
77
+ }
78
+
79
+ /**
80
+ * Redis provider that extends ports with cache capabilities.
81
+ *
82
+ * Configuration via environment variables:
83
+ * - REDIS_URL: Redis connection URL (required)
84
+ * - REDIS_DB: Redis database number (optional)
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * const server = await createNextServer({
89
+ * ports: basePorts,
90
+ * providers: [redisProvider],
91
+ * // ...
92
+ * });
93
+ * ```
94
+ */
95
+ export const redisProvider = createProvider({
96
+ name: "redis",
97
+
98
+ config: {
99
+ schema: RedisConfigSchema,
100
+ envPrefix: "REDIS_",
101
+ },
102
+
103
+ async setup({ ports, config }) {
104
+ if (!config) {
105
+ throw new Error(
106
+ "[redisProvider] Missing Redis config. " +
107
+ "Please set REDIS_URL environment variable.",
108
+ );
109
+ }
110
+
111
+ // Create Redis client
112
+ const client = new Redis(config.URL, {
113
+ db: config.DB ?? 0,
114
+ // Prevent ioredis from logging connection errors to console
115
+ // (we'll handle errors in the application)
116
+ lazyConnect: true,
117
+ });
118
+
119
+ // Test connection
120
+ try {
121
+ await client.connect();
122
+ } catch (error) {
123
+ throw new Error(
124
+ `[redisProvider] Failed to connect to Redis at ${config.URL}: ${
125
+ error instanceof Error ? error.message : String(error)
126
+ }`,
127
+ );
128
+ }
129
+
130
+ const instrumentation = createProviderInstrumentation(ports, {
131
+ providerName: "redis",
132
+ watcher: "cache",
133
+ });
134
+
135
+ const recordCacheEvent = (event: {
136
+ operation: string;
137
+ key: string;
138
+ summary: string;
139
+ details?: Record<string, unknown>;
140
+ }) => {
141
+ instrumentation.custom({
142
+ name: `cache.${event.operation}`,
143
+ label: `Cache ${event.operation}`,
144
+ summary: event.summary,
145
+ details: {
146
+ operation: event.operation,
147
+ key: event.key,
148
+ ...event.details,
149
+ },
150
+ });
151
+ };
152
+
153
+ // Extend ports with cache API
154
+ const cache: RedisCachePort = {
155
+ get: async (key: string) => {
156
+ const startedAt = Date.now();
157
+ try {
158
+ const value = await client.get(key);
159
+ recordCacheEvent({
160
+ operation: "get",
161
+ key,
162
+ summary: value == null ? "Cache miss" : "Cache hit",
163
+ details: {
164
+ hit: value != null,
165
+ durationMs: Date.now() - startedAt,
166
+ },
167
+ });
168
+ return value;
169
+ } catch (error) {
170
+ recordCacheEvent({
171
+ operation: "get.failed",
172
+ key,
173
+ summary: "Cache get failed",
174
+ details: {
175
+ durationMs: Date.now() - startedAt,
176
+ error: errorMessage(error),
177
+ },
178
+ });
179
+ throw error;
180
+ }
181
+ },
182
+
183
+ set: async (key, value, options) => {
184
+ const startedAt = Date.now();
185
+ try {
186
+ if (options?.ttlSeconds != null && options.ttlSeconds > 0) {
187
+ await client.set(key, value, "EX", options.ttlSeconds);
188
+ } else {
189
+ await client.set(key, value);
190
+ }
191
+ recordCacheEvent({
192
+ operation: "set",
193
+ key,
194
+ summary: "Cache set",
195
+ details: {
196
+ ttlSeconds: options?.ttlSeconds ?? null,
197
+ durationMs: Date.now() - startedAt,
198
+ },
199
+ });
200
+ } catch (error) {
201
+ recordCacheEvent({
202
+ operation: "set.failed",
203
+ key,
204
+ summary: "Cache set failed",
205
+ details: {
206
+ ttlSeconds: options?.ttlSeconds ?? null,
207
+ durationMs: Date.now() - startedAt,
208
+ error: errorMessage(error),
209
+ },
210
+ });
211
+ throw error;
212
+ }
213
+ },
214
+
215
+ delete: async (key) => {
216
+ const startedAt = Date.now();
217
+ try {
218
+ const deleted = (await client.del(key)) > 0;
219
+ recordCacheEvent({
220
+ operation: "delete",
221
+ key,
222
+ summary: deleted ? "Cache key deleted" : "Cache key missing",
223
+ details: {
224
+ deleted,
225
+ durationMs: Date.now() - startedAt,
226
+ },
227
+ });
228
+ return deleted;
229
+ } catch (error) {
230
+ recordCacheEvent({
231
+ operation: "delete.failed",
232
+ key,
233
+ summary: "Cache delete failed",
234
+ details: {
235
+ durationMs: Date.now() - startedAt,
236
+ error: errorMessage(error),
237
+ },
238
+ });
239
+ throw error;
240
+ }
241
+ },
242
+
243
+ has: async (key) => {
244
+ const startedAt = Date.now();
245
+ try {
246
+ const result = await client.exists(key);
247
+ const exists = result === 1;
248
+ recordCacheEvent({
249
+ operation: "has",
250
+ key,
251
+ summary: exists ? "Cache key exists" : "Cache key missing",
252
+ details: {
253
+ exists,
254
+ durationMs: Date.now() - startedAt,
255
+ },
256
+ });
257
+ return exists;
258
+ } catch (error) {
259
+ recordCacheEvent({
260
+ operation: "has.failed",
261
+ key,
262
+ summary: "Cache has failed",
263
+ details: {
264
+ durationMs: Date.now() - startedAt,
265
+ error: errorMessage(error),
266
+ },
267
+ });
268
+ throw error;
269
+ }
270
+ },
271
+
272
+ remember: async (key, factory, options) => {
273
+ const startedAt = Date.now();
274
+ try {
275
+ const cached = await client.get(key);
276
+ if (cached != null) {
277
+ recordCacheEvent({
278
+ operation: "remember",
279
+ key,
280
+ summary: "Cache remember hit",
281
+ details: {
282
+ hit: true,
283
+ ttlSeconds: options?.ttlSeconds ?? null,
284
+ durationMs: Date.now() - startedAt,
285
+ },
286
+ });
287
+ return cached;
288
+ }
289
+
290
+ const value = await factory();
291
+ if (options?.ttlSeconds != null && options.ttlSeconds > 0) {
292
+ await client.set(key, value, "EX", options.ttlSeconds);
293
+ } else {
294
+ await client.set(key, value);
295
+ }
296
+
297
+ recordCacheEvent({
298
+ operation: "remember",
299
+ key,
300
+ summary: "Cache remember miss",
301
+ details: {
302
+ hit: false,
303
+ ttlSeconds: options?.ttlSeconds ?? null,
304
+ durationMs: Date.now() - startedAt,
305
+ },
306
+ });
307
+
308
+ return value;
309
+ } catch (error) {
310
+ recordCacheEvent({
311
+ operation: "remember.failed",
312
+ key,
313
+ summary: "Cache remember failed",
314
+ details: {
315
+ ttlSeconds: options?.ttlSeconds ?? null,
316
+ durationMs: Date.now() - startedAt,
317
+ error: errorMessage(error),
318
+ },
319
+ });
320
+ throw error;
321
+ }
322
+ },
323
+
324
+ client,
325
+ };
326
+
327
+ return {
328
+ ports: { cache },
329
+ async stop() {
330
+ try {
331
+ await client.quit();
332
+ } catch {
333
+ // Silently ignore errors during shutdown
334
+ }
335
+ },
336
+ };
337
+ },
338
+ });