@memberjunction/redis-provider 0.0.1 → 5.9.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.
- package/README.md +307 -28
- package/dist/RedisLocalStorageProvider.d.ts +491 -0
- package/dist/RedisLocalStorageProvider.d.ts.map +1 -0
- package/dist/RedisLocalStorageProvider.js +740 -0
- package/dist/RedisLocalStorageProvider.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/package.json +30 -7
package/README.md
CHANGED
|
@@ -1,45 +1,324 @@
|
|
|
1
1
|
# @memberjunction/redis-provider
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Redis-backed implementation of MemberJunction's `ILocalStorageProvider` interface. Enables persistent, shared server-side caching via any Redis-compatible service — self-hosted Redis, Azure Managed Redis, AWS ElastiCache, Redis Cloud, Upstash, or any other Redis-protocol endpoint.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Why Redis?
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
MemberJunction's default server-side cache (`InMemoryLocalStorageProvider`) stores data in a plain `Map` inside the Node.js process. This works well for single-server development but has two limitations in production:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
| Limitation | Impact |
|
|
10
|
+
|-----------|--------|
|
|
11
|
+
| **Not shared** | Each MJAPI instance has its own cache — no benefit from horizontal scaling |
|
|
12
|
+
| **Not persistent** | Cache is lost on process restart — cold starts hit the database for everything |
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@memberjunction/redis-provider`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
14
|
+
A Redis-backed provider solves both problems while remaining a drop-in replacement — no changes to `LocalCacheManager`, `ProviderBase`, or any consumer code.
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Installation
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
```bash
|
|
19
|
+
# Add to the package that configures your data provider (typically MJAPI or your server bootstrap)
|
|
20
|
+
# Then run npm install at the repo root
|
|
21
|
+
npm install @memberjunction/redis-provider
|
|
22
|
+
```
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
> **Monorepo note:** In the MemberJunction monorepo, add the dependency to the relevant package's `package.json` and run `npm install` at the repo root.
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
## Quick Start
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
```typescript
|
|
29
|
+
import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
|
|
30
|
+
import { Metadata } from '@memberjunction/core';
|
|
31
|
+
import type { GenericDatabaseProvider } from '@memberjunction/generic-database-provider';
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
// 1. Create the Redis provider
|
|
34
|
+
const redisProvider = new RedisLocalStorageProvider({
|
|
35
|
+
url: 'redis://localhost:6379',
|
|
36
|
+
defaultTTLSeconds: 300, // 5-minute default TTL
|
|
37
|
+
});
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
39
|
+
// 2. Inject it into the data provider
|
|
40
|
+
const provider = Metadata.Provider as GenericDatabaseProvider;
|
|
41
|
+
provider.SetLocalStorageProvider(redisProvider);
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
// That's it! All MJ caching now flows through Redis.
|
|
44
|
+
```
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
46
|
+
## Configuration
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
The `RedisProviderConfig` object supports the following options:
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
| Option | Type | Default | Description |
|
|
51
|
+
|--------|------|---------|-------------|
|
|
52
|
+
| `url` | `string` | — | Redis connection URL (`redis://` or `rediss://` for TLS). Mutually exclusive with `options`. |
|
|
53
|
+
| `options` | `RedisOptions` | — | Full [`ioredis` options](https://github.com/redis/ioredis#connect-to-redis) object. Mutually exclusive with `url`. |
|
|
54
|
+
| `keyPrefix` | `string` | `'mj'` | Prefix for all Redis keys. Useful for isolating MJ data in a shared Redis instance. |
|
|
55
|
+
| `defaultTTLSeconds` | `number` | `undefined` | Default time-to-live for all cached entries. `undefined` means keys persist until explicitly removed. |
|
|
56
|
+
| `maxRetries` | `number` | `10` | Maximum reconnection attempts with exponential backoff before giving up. |
|
|
57
|
+
| `enableLogging` | `boolean` | `true` | Whether to log connection events via MJ's `LogStatus`/`LogError`. |
|
|
58
|
+
|
|
59
|
+
### Connection Examples
|
|
60
|
+
|
|
61
|
+
#### Local Development (Docker)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Start a local Redis container
|
|
65
|
+
docker run -d --name mj-redis -p 6379:6379 redis:7-alpine
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const provider = new RedisLocalStorageProvider({
|
|
70
|
+
url: 'redis://localhost:6379',
|
|
71
|
+
defaultTTLSeconds: 300,
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### Azure Managed Redis
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const provider = new RedisLocalStorageProvider({
|
|
79
|
+
url: `rediss://default:${process.env.AZURE_REDIS_KEY}@${process.env.AZURE_REDIS_HOST}:6380`,
|
|
80
|
+
defaultTTLSeconds: 600,
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or using the options object for more control:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const provider = new RedisLocalStorageProvider({
|
|
88
|
+
options: {
|
|
89
|
+
host: process.env.AZURE_REDIS_HOST,
|
|
90
|
+
port: 6380,
|
|
91
|
+
password: process.env.AZURE_REDIS_KEY,
|
|
92
|
+
tls: {}, // Required for Azure
|
|
93
|
+
db: 0,
|
|
94
|
+
},
|
|
95
|
+
defaultTTLSeconds: 600,
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### AWS ElastiCache
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const provider = new RedisLocalStorageProvider({
|
|
103
|
+
options: {
|
|
104
|
+
host: 'my-cluster.abc123.use1.cache.amazonaws.com',
|
|
105
|
+
port: 6379,
|
|
106
|
+
tls: {}, // Required for encryption in transit
|
|
107
|
+
},
|
|
108
|
+
defaultTTLSeconds: 600,
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### Redis Cloud / Upstash
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const provider = new RedisLocalStorageProvider({
|
|
116
|
+
url: process.env.REDIS_URL, // Provided by the service
|
|
117
|
+
defaultTTLSeconds: 600,
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Architecture
|
|
122
|
+
|
|
123
|
+
### How It Fits Into MemberJunction
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
127
|
+
│ Application Layer │
|
|
128
|
+
│ (MJAPI, Angular, React, Custom Apps) │
|
|
129
|
+
└────────────────────────┬────────────────────────────────────┘
|
|
130
|
+
│
|
|
131
|
+
▼
|
|
132
|
+
┌──────────────────────┐
|
|
133
|
+
│ LocalCacheManager │ Singleton — LRU eviction, TTL,
|
|
134
|
+
│ (MJCore) │ stats, category-based isolation
|
|
135
|
+
└──────────┬───────────┘
|
|
136
|
+
│
|
|
137
|
+
▼
|
|
138
|
+
┌──────────────────────┐
|
|
139
|
+
│ ILocalStorageProvider│ Abstract interface (MJCore)
|
|
140
|
+
└──────────┬───────────┘
|
|
141
|
+
│
|
|
142
|
+
┌────────────┼────────────────┐
|
|
143
|
+
│ │ │
|
|
144
|
+
▼ ▼ ▼
|
|
145
|
+
┌──────────────┐ ┌──────────┐ ┌────────────────┐
|
|
146
|
+
│ InMemory │ │ Browser │ │ Redis │
|
|
147
|
+
│ (default) │ │ (IDB/LS) │ │ (this package) │
|
|
148
|
+
└──────────────┘ └──────────┘ └────────┬───────┘
|
|
149
|
+
│
|
|
150
|
+
▼
|
|
151
|
+
┌─────────────┐
|
|
152
|
+
│ Redis Server │
|
|
153
|
+
│ (any host) │
|
|
154
|
+
└─────────────┘
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Key Structure
|
|
158
|
+
|
|
159
|
+
All keys follow the pattern: `{prefix}:{category}:{key}`
|
|
160
|
+
|
|
161
|
+
- **prefix** — Configurable (default `"mj"`), isolates MJ data in shared Redis instances
|
|
162
|
+
- **category** — Maps to MJ cache categories: `RunViewCache`, `Metadata`, `DatasetCache`, `RunQueryCache`, `default`
|
|
163
|
+
- **key** — The original key from the calling code
|
|
164
|
+
|
|
165
|
+
Example Redis keys:
|
|
166
|
+
```
|
|
167
|
+
mj:RunViewCache:Users|Active=1|Name ASC
|
|
168
|
+
mj:Metadata:___MJCore_Metadata_AllMetadata
|
|
169
|
+
mj:DatasetCache:MyDataset_items
|
|
170
|
+
mj:default:some-arbitrary-key
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Category Tracking
|
|
174
|
+
|
|
175
|
+
Each category has an associated Redis Set at `{prefix}:__categories__:{category}` that tracks all member keys. This enables efficient:
|
|
176
|
+
|
|
177
|
+
- **`ClearCategory()`** — Deletes all keys in a category in a single pipeline
|
|
178
|
+
- **`GetCategoryKeys()`** — Lists all keys without scanning the entire keyspace
|
|
179
|
+
|
|
180
|
+
### TTL (Time-to-Live)
|
|
181
|
+
|
|
182
|
+
Redis has native key expiration, so TTL is handled efficiently at the server level:
|
|
183
|
+
|
|
184
|
+
1. **Config default** — `defaultTTLSeconds` applies to every `SetItem()` call
|
|
185
|
+
2. **Per-call override** — `SetItem(key, value, category, ttlSeconds)` overrides the default
|
|
186
|
+
3. **No TTL** — If neither is set, keys persist until explicitly removed or `ClearCategory()` is called
|
|
187
|
+
|
|
188
|
+
### Error Handling
|
|
189
|
+
|
|
190
|
+
All Redis operations are wrapped in try/catch. On failure:
|
|
191
|
+
- **Reads** return `null` (cache miss, falls through to database)
|
|
192
|
+
- **Writes** are silently skipped (data stays in the database, just not cached)
|
|
193
|
+
- **Connection errors** are logged via `LogError()` but don't crash the app
|
|
194
|
+
- **Reconnection** is automatic via `ioredis` with configurable exponential backoff
|
|
195
|
+
|
|
196
|
+
This design ensures a Redis outage degrades performance (more database hits) but never causes application downtime.
|
|
197
|
+
|
|
198
|
+
## API Reference
|
|
199
|
+
|
|
200
|
+
### `RedisLocalStorageProvider`
|
|
201
|
+
|
|
202
|
+
#### Constructor
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
new RedisLocalStorageProvider(config?: RedisProviderConfig)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Creates a new provider and establishes a Redis connection. The connection is lazy — it happens on the first command, so construction itself does not block.
|
|
209
|
+
|
|
210
|
+
#### ILocalStorageProvider Methods
|
|
211
|
+
|
|
212
|
+
| Method | Description |
|
|
213
|
+
|--------|-------------|
|
|
214
|
+
| `GetItem(key, category?)` | Retrieves a cached value. Returns `null` on miss or error. |
|
|
215
|
+
| `SetItem(key, value, category?, ttlSeconds?)` | Stores a value with optional TTL. Uses pipeline for atomic set + category tracking. |
|
|
216
|
+
| `Remove(key, category?)` | Deletes a key and removes it from category tracking. |
|
|
217
|
+
| `ClearCategory(category)` | Deletes all keys in a category using the tracking Set. |
|
|
218
|
+
| `GetCategoryKeys(category)` | Returns all key names in a category. |
|
|
219
|
+
|
|
220
|
+
#### Additional Methods
|
|
221
|
+
|
|
222
|
+
| Method | Description |
|
|
223
|
+
|--------|-------------|
|
|
224
|
+
| `Exists(key, category?)` | Checks key existence without transferring the value (more efficient than `GetItem`). |
|
|
225
|
+
| `GetTTL(key, category?)` | Returns remaining TTL in seconds (`-1` = no expiry, `-2` = key doesn't exist). |
|
|
226
|
+
| `Ping()` | Health check — returns `true` if Redis responds with `PONG`. |
|
|
227
|
+
| `Disconnect()` | Graceful shutdown — sends `QUIT` and waits for pending replies. |
|
|
228
|
+
|
|
229
|
+
#### Properties
|
|
230
|
+
|
|
231
|
+
| Property | Type | Description |
|
|
232
|
+
|----------|------|-------------|
|
|
233
|
+
| `IsConnected` | `boolean` | Whether the client has an active connection (transient — auto-reconnects). |
|
|
234
|
+
| `Client` | `Redis` | The underlying `ioredis` client for advanced operations (pub/sub, streams, etc.). |
|
|
235
|
+
|
|
236
|
+
## Testing
|
|
237
|
+
|
|
238
|
+
### Unit Tests (No Redis Required)
|
|
239
|
+
|
|
240
|
+
The package includes comprehensive unit tests with mocked Redis:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
cd packages/RedisProvider
|
|
244
|
+
npm run test
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Integration Testing with Local Redis
|
|
248
|
+
|
|
249
|
+
For end-to-end testing with a real Redis instance:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# Start Redis
|
|
253
|
+
docker run -d --name mj-redis -p 6379:6379 redis:7-alpine
|
|
254
|
+
|
|
255
|
+
# Verify connectivity
|
|
256
|
+
docker exec mj-redis redis-cli ping
|
|
257
|
+
# → PONG
|
|
258
|
+
|
|
259
|
+
# Run your MJAPI with Redis configured
|
|
260
|
+
REDIS_URL=redis://localhost:6379 npm run start:api
|
|
261
|
+
|
|
262
|
+
# Monitor Redis activity in real time
|
|
263
|
+
docker exec mj-redis redis-cli monitor
|
|
264
|
+
|
|
265
|
+
# Clean up
|
|
266
|
+
docker stop mj-redis && docker rm mj-redis
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Monitoring Redis Usage
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# See all MJ keys
|
|
273
|
+
docker exec mj-redis redis-cli KEYS "mj:*"
|
|
274
|
+
|
|
275
|
+
# Check memory usage
|
|
276
|
+
docker exec mj-redis redis-cli INFO memory
|
|
277
|
+
|
|
278
|
+
# See cache hit/miss stats
|
|
279
|
+
docker exec mj-redis redis-cli INFO stats | grep keyspace
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Production Recommendations
|
|
283
|
+
|
|
284
|
+
### TTL Strategy
|
|
285
|
+
|
|
286
|
+
| Category | Recommended TTL | Rationale |
|
|
287
|
+
|----------|----------------|-----------|
|
|
288
|
+
| `Metadata` | 30–60 minutes | Entity schema changes infrequently |
|
|
289
|
+
| `RunViewCache` | 2–5 minutes | Balance freshness vs. database load |
|
|
290
|
+
| `RunQueryCache` | 2–5 minutes | Same as RunViewCache |
|
|
291
|
+
| `DatasetCache` | 5–10 minutes | Datasets are typically larger, change less often |
|
|
292
|
+
|
|
293
|
+
### Memory Management
|
|
294
|
+
|
|
295
|
+
- Set `maxmemory` and `maxmemory-policy allkeys-lru` in your Redis configuration so Redis automatically evicts least-recently-used keys when memory is full
|
|
296
|
+
- Monitor with `INFO memory` and set alerts on `used_memory_peak`
|
|
297
|
+
- Use `defaultTTLSeconds` to ensure keys don't accumulate indefinitely
|
|
298
|
+
|
|
299
|
+
### High Availability
|
|
300
|
+
|
|
301
|
+
- **Azure Managed Redis**: Use Standard or Premium tier for replication
|
|
302
|
+
- **AWS ElastiCache**: Enable Multi-AZ with automatic failover
|
|
303
|
+
- **Self-hosted**: Use Redis Sentinel or Redis Cluster for HA
|
|
304
|
+
|
|
305
|
+
### Security
|
|
306
|
+
|
|
307
|
+
- Always use TLS (`rediss://` URL scheme or `tls: {}` in options) for cloud-hosted Redis
|
|
308
|
+
- Use strong passwords and rotate them regularly
|
|
309
|
+
- Restrict network access to Redis (VNet/VPC peering, security groups)
|
|
310
|
+
- Never expose Redis ports to the public internet
|
|
311
|
+
|
|
312
|
+
## Dependencies
|
|
313
|
+
|
|
314
|
+
- [`ioredis`](https://github.com/redis/ioredis) — Feature-rich Redis client for Node.js
|
|
315
|
+
- `@memberjunction/core` — `ILocalStorageProvider` interface and logging utilities
|
|
316
|
+
- `@memberjunction/global` — Global object store utilities
|
|
317
|
+
|
|
318
|
+
## Related Packages
|
|
319
|
+
|
|
320
|
+
- [`@memberjunction/core`](../MJCore/) — Defines `ILocalStorageProvider`, `LocalCacheManager`, and `InMemoryLocalStorageProvider`
|
|
321
|
+
- [`@memberjunction/generic-database-provider`](../GenericDatabaseProvider/) — Where `LocalStorageProvider` is wired into the data provider chain
|
|
322
|
+
- [`@memberjunction/sqlserver-dataprovider`](../SQLServerDataProvider/) — SQL Server provider (inherits caching from GenericDatabaseProvider)
|
|
323
|
+
- [`@memberjunction/postgresql-dataprovider`](../PostgreSQLDataProvider/) — PostgreSQL provider (inherits caching from GenericDatabaseProvider)
|
|
324
|
+
- [**Caching & Pub/Sub Guide**](/guides/CACHING_AND_PUBSUB_GUIDE.md) — Comprehensive architecture guide covering Redis cross-server sync, GraphQL cache invalidation, deployment topologies, and troubleshooting
|