@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 +5 -0
- package/README.md +186 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +295 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
- package/src/index.ts +338 -0
package/CHANGELOG.md
ADDED
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
|
package/dist/index.d.ts
ADDED
|
@@ -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":"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"}
|
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
|
+
});
|