deepbase-redis 3.1.0 → 3.1.4
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 +56 -46
- package/package.json +4 -5
- package/src/RedisDriver.js +109 -31
- package/test/test.js +60 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# deepbase-redis
|
|
2
2
|
|
|
3
|
-
Redis
|
|
3
|
+
Vanilla Redis driver for DeepBase (no modules required).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,23 +10,23 @@ npm install deepbase deepbase-redis
|
|
|
10
10
|
|
|
11
11
|
## Prerequisites
|
|
12
12
|
|
|
13
|
-
Requires Redis
|
|
13
|
+
Requires standard Redis (no modules needed):
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
docker run -d -p 6379:6379 --name redis redis
|
|
16
|
+
docker run -d -p 6379:6379 --name redis redis:latest
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
**Note:**
|
|
19
|
+
**Note:** This driver works with vanilla Redis. For RedisJSON support, use `deepbase-redis-json` instead.
|
|
20
20
|
|
|
21
21
|
## Description
|
|
22
22
|
|
|
23
|
-
Stores data in Redis using
|
|
23
|
+
Stores data in Redis using standard string operations with JSON serialization. Perfect for:
|
|
24
24
|
|
|
25
25
|
- ✅ High-performance caching
|
|
26
26
|
- ✅ Real-time applications
|
|
27
27
|
- ✅ Session storage
|
|
28
|
-
- ✅
|
|
29
|
-
- ✅
|
|
28
|
+
- ✅ Works with any Redis installation
|
|
29
|
+
- ✅ No modules required
|
|
30
30
|
|
|
31
31
|
## Usage
|
|
32
32
|
|
|
@@ -58,28 +58,51 @@ new RedisDriver({
|
|
|
58
58
|
|
|
59
59
|
## Data Structure
|
|
60
60
|
|
|
61
|
-
Data is stored as
|
|
61
|
+
Data is stored as JSON strings in Redis:
|
|
62
62
|
|
|
63
63
|
```javascript
|
|
64
64
|
// Keys created
|
|
65
|
-
myapp:users -> {
|
|
66
|
-
myapp:config -> {
|
|
65
|
+
myapp:users -> '{"alice": {...}, "bob": {...}}'
|
|
66
|
+
myapp:config -> '{"theme": "dark", "lang": "en"}'
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
## Differences from deepbase-redis-json
|
|
70
|
+
|
|
71
|
+
This vanilla driver:
|
|
72
|
+
- ✅ Works with any Redis installation
|
|
73
|
+
- ✅ No modules required
|
|
74
|
+
- ✅ Simpler deployment
|
|
75
|
+
- ❌ No atomic JSON path operations
|
|
76
|
+
- ❌ Entire values must be read/written
|
|
77
|
+
|
|
78
|
+
The RedisJSON driver (`deepbase-redis-json`):
|
|
79
|
+
- ✅ Atomic JSON path operations
|
|
80
|
+
- ✅ More efficient for large nested objects
|
|
81
|
+
- ❌ Requires Redis Stack or RedisJSON module
|
|
82
|
+
|
|
69
83
|
## Features
|
|
70
84
|
|
|
71
|
-
###
|
|
85
|
+
### JSON Serialization
|
|
86
|
+
|
|
87
|
+
Uses standard JSON serialization:
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
await db.set('users', 'alice', { name: 'Alice', age: 30 });
|
|
91
|
+
// Stored as: '{"alice": {"name": "Alice", "age": 30}}'
|
|
92
|
+
```
|
|
72
93
|
|
|
73
|
-
|
|
94
|
+
### Nested Operations
|
|
95
|
+
|
|
96
|
+
Supports nested path operations:
|
|
74
97
|
|
|
75
98
|
```javascript
|
|
76
99
|
await db.set('users', 'alice', 'address', { city: 'NYC' });
|
|
77
|
-
//
|
|
100
|
+
// Reads full object, modifies, and writes back
|
|
78
101
|
```
|
|
79
102
|
|
|
80
|
-
###
|
|
103
|
+
### Increment/Decrement
|
|
81
104
|
|
|
82
|
-
|
|
105
|
+
Basic increment operations:
|
|
83
106
|
|
|
84
107
|
```javascript
|
|
85
108
|
await db.inc('stats', 'views', 1);
|
|
@@ -95,30 +118,6 @@ const allUsers = await db.get('users');
|
|
|
95
118
|
// Scans all keys matching prefix
|
|
96
119
|
```
|
|
97
120
|
|
|
98
|
-
## Three-Tier Architecture
|
|
99
|
-
|
|
100
|
-
Use Redis as a cache layer:
|
|
101
|
-
|
|
102
|
-
```javascript
|
|
103
|
-
import DeepBase from '@deepbase/core';
|
|
104
|
-
import MongoDriver from 'deepbase-mongodb';
|
|
105
|
-
import { JsonDriver } from 'deepbase';
|
|
106
|
-
import RedisDriver from '@deepbase/redis';
|
|
107
|
-
|
|
108
|
-
const db = new DeepBase([
|
|
109
|
-
new MongoDriver({ url: 'mongodb://localhost:27017' }), // Primary
|
|
110
|
-
new JsonDriver({ path: './backup' }), // Backup
|
|
111
|
-
new RedisDriver({ url: 'redis://localhost:6379' }) // Cache
|
|
112
|
-
], {
|
|
113
|
-
writeAll: true, // Write to all three
|
|
114
|
-
readFirst: true, // Read from MongoDB first
|
|
115
|
-
failOnPrimaryError: false // Fallback through layers
|
|
116
|
-
});
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
**Read priority**: MongoDB → JSON → Redis
|
|
120
|
-
**Write replication**: All three updated simultaneously
|
|
121
|
-
|
|
122
121
|
## Connection String Formats
|
|
123
122
|
|
|
124
123
|
```javascript
|
|
@@ -145,7 +144,7 @@ Redis is extremely fast:
|
|
|
145
144
|
- **Reads**: Sub-millisecond response times
|
|
146
145
|
- **Writes**: Thousands of operations per second
|
|
147
146
|
- **In-memory**: Data stored in RAM with optional persistence
|
|
148
|
-
- **
|
|
147
|
+
- **Simple**: No complex JSON path operations
|
|
149
148
|
|
|
150
149
|
## Use Cases
|
|
151
150
|
|
|
@@ -159,7 +158,6 @@ await sessions.set(sessionId, 'user', userData);
|
|
|
159
158
|
```javascript
|
|
160
159
|
const stats = new DeepBase(new RedisDriver({ prefix: 'stats' }));
|
|
161
160
|
await stats.inc('page', 'views', 1);
|
|
162
|
-
await stats.inc('page', 'unique_visitors', 1);
|
|
163
161
|
```
|
|
164
162
|
|
|
165
163
|
### Cache Layer
|
|
@@ -168,21 +166,19 @@ const cache = new DeepBase([
|
|
|
168
166
|
new RedisDriver({ prefix: 'cache' }),
|
|
169
167
|
new MongoDriver({ url: '...' })
|
|
170
168
|
]);
|
|
171
|
-
|
|
172
|
-
// Fast reads from Redis, persistent in MongoDB
|
|
173
169
|
```
|
|
174
170
|
|
|
175
171
|
## Best Practices
|
|
176
172
|
|
|
177
173
|
1. **Use as cache layer** - Not as primary storage
|
|
178
|
-
2. **
|
|
174
|
+
2. **Small to medium objects** - Full objects are read/written
|
|
179
175
|
3. **Monitor memory usage** - Redis is in-memory
|
|
180
176
|
4. **Enable persistence** - RDB or AOF for durability
|
|
181
177
|
5. **Use with persistent drivers** - MongoDB or JSON backup
|
|
182
178
|
|
|
183
179
|
## Persistence Options
|
|
184
180
|
|
|
185
|
-
Redis
|
|
181
|
+
Redis supports:
|
|
186
182
|
- **RDB**: Periodic snapshots
|
|
187
183
|
- **AOF**: Append-only file for durability
|
|
188
184
|
|
|
@@ -190,7 +186,7 @@ Configure in Redis:
|
|
|
190
186
|
```bash
|
|
191
187
|
docker run -d -p 6379:6379 \
|
|
192
188
|
-v redis-data:/data \
|
|
193
|
-
redis
|
|
189
|
+
redis:latest \
|
|
194
190
|
--appendonly yes
|
|
195
191
|
```
|
|
196
192
|
|
|
@@ -205,6 +201,20 @@ try {
|
|
|
205
201
|
}
|
|
206
202
|
```
|
|
207
203
|
|
|
204
|
+
## When to Use
|
|
205
|
+
|
|
206
|
+
**Use `deepbase-redis` (this driver) when:**
|
|
207
|
+
- You have standard Redis
|
|
208
|
+
- You want simple deployment
|
|
209
|
+
- Your objects are small to medium sized
|
|
210
|
+
- You don't need atomic JSON operations
|
|
211
|
+
|
|
212
|
+
**Use `deepbase-redis-json` when:**
|
|
213
|
+
- You have Redis Stack
|
|
214
|
+
- You need atomic JSON path operations
|
|
215
|
+
- You work with large nested objects
|
|
216
|
+
- You want optimal performance for partial updates
|
|
217
|
+
|
|
208
218
|
## License
|
|
209
219
|
|
|
210
220
|
MIT - Copyright (c) Martin Clasen
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepbase-redis",
|
|
3
|
-
"version": "3.1.
|
|
4
|
-
"description": "⚡ DeepBase Redis
|
|
3
|
+
"version": "3.1.4",
|
|
4
|
+
"description": "⚡ DeepBase Redis (vanilla) - driver",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.cjs",
|
|
7
7
|
"module": "src/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"redis": "^4.7.0"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"deepbase": "^3.1.
|
|
18
|
+
"deepbase": "^3.1.4"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test": "mocha test/test.js"
|
|
@@ -30,8 +30,7 @@
|
|
|
30
30
|
"keywords": [
|
|
31
31
|
"deepbase",
|
|
32
32
|
"redis",
|
|
33
|
-
"
|
|
34
|
-
"redis-json",
|
|
33
|
+
"vanilla",
|
|
35
34
|
"driver",
|
|
36
35
|
"database",
|
|
37
36
|
"persist",
|
package/src/RedisDriver.js
CHANGED
|
@@ -9,6 +9,7 @@ export class RedisDriver extends DeepBaseDriver {
|
|
|
9
9
|
this.url = url || "redis://localhost:6379";
|
|
10
10
|
|
|
11
11
|
this.client = createClient({ url: this.url });
|
|
12
|
+
this._locks = new Map(); // Lock map for concurrent operations on same key
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
async connect() {
|
|
@@ -30,15 +31,45 @@ export class RedisDriver extends DeepBaseDriver {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const key = args.shift();
|
|
33
|
-
const
|
|
34
|
+
const redisKey = this.name + ":" + key;
|
|
34
35
|
|
|
35
36
|
try {
|
|
36
|
-
|
|
37
|
+
const value = await this.client.get(redisKey);
|
|
38
|
+
if (value === null) return null;
|
|
39
|
+
|
|
40
|
+
let data = JSON.parse(value);
|
|
41
|
+
|
|
42
|
+
// Navigate through nested path if provided
|
|
43
|
+
for (let pathKey of args) {
|
|
44
|
+
if (data === null || data === undefined) return null;
|
|
45
|
+
data = data[pathKey];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return data;
|
|
37
49
|
} catch (error) {
|
|
38
50
|
return null;
|
|
39
51
|
}
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
async _acquireLock(key) {
|
|
55
|
+
// Wait for any pending operation on this key to complete
|
|
56
|
+
while (this._locks.has(key)) {
|
|
57
|
+
await this._locks.get(key);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create a new lock for this operation
|
|
61
|
+
let releaseLock;
|
|
62
|
+
const lockPromise = new Promise(resolve => {
|
|
63
|
+
releaseLock = resolve;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this._locks.set(key, lockPromise);
|
|
67
|
+
return () => {
|
|
68
|
+
this._locks.delete(key);
|
|
69
|
+
releaseLock();
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
42
73
|
async set(...args) {
|
|
43
74
|
if (args.length < 2) return;
|
|
44
75
|
|
|
@@ -46,8 +77,49 @@ export class RedisDriver extends DeepBaseDriver {
|
|
|
46
77
|
const value = args[args.length - 1];
|
|
47
78
|
|
|
48
79
|
const key = keys.shift();
|
|
49
|
-
const
|
|
50
|
-
|
|
80
|
+
const redisKey = this.name + ":" + key;
|
|
81
|
+
|
|
82
|
+
if (keys.length === 0) {
|
|
83
|
+
// Direct set
|
|
84
|
+
if (value === undefined) {
|
|
85
|
+
await this.client.del(redisKey);
|
|
86
|
+
} else {
|
|
87
|
+
await this.client.set(redisKey, JSON.stringify(value));
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
// Nested set - serialize operations on same key to prevent race conditions
|
|
91
|
+
const releaseLock = await this._acquireLock(redisKey);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const current = await this.client.get(redisKey);
|
|
95
|
+
let data = current ? JSON.parse(current) : {};
|
|
96
|
+
|
|
97
|
+
if (typeof data !== 'object' || Array.isArray(data)) {
|
|
98
|
+
data = {};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Navigate and set nested value
|
|
102
|
+
let current_obj = data;
|
|
103
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
104
|
+
const pathKey = keys[i];
|
|
105
|
+
if (!current_obj[pathKey] || typeof current_obj[pathKey] !== 'object') {
|
|
106
|
+
current_obj[pathKey] = {};
|
|
107
|
+
}
|
|
108
|
+
current_obj = current_obj[pathKey];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const lastKey = keys[keys.length - 1];
|
|
112
|
+
if (value === undefined) {
|
|
113
|
+
delete current_obj[lastKey];
|
|
114
|
+
} else {
|
|
115
|
+
current_obj[lastKey] = value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await this.client.set(redisKey, JSON.stringify(data));
|
|
119
|
+
} finally {
|
|
120
|
+
releaseLock();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
51
123
|
|
|
52
124
|
return args.slice(0, -1);
|
|
53
125
|
}
|
|
@@ -55,14 +127,18 @@ export class RedisDriver extends DeepBaseDriver {
|
|
|
55
127
|
async inc(...args) {
|
|
56
128
|
const i = args.pop();
|
|
57
129
|
const key = args.shift();
|
|
58
|
-
const path = args.length == 0 ? "." : this._pathToKey(args);
|
|
59
130
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
131
|
+
if (args.length === 0) {
|
|
132
|
+
// Direct increment on root level
|
|
133
|
+
const redisKey = this.name + ":" + key;
|
|
134
|
+
const value = await this.client.get(redisKey);
|
|
135
|
+
const num = value ? JSON.parse(value) : 0;
|
|
136
|
+
await this.client.set(redisKey, JSON.stringify(num + i));
|
|
137
|
+
return [key];
|
|
138
|
+
} else {
|
|
139
|
+
// Nested increment
|
|
64
140
|
args.unshift(key);
|
|
65
|
-
return this.upd(...args, n => n + i);
|
|
141
|
+
return this.upd(...args, n => (n || 0) + i);
|
|
66
142
|
}
|
|
67
143
|
}
|
|
68
144
|
|
|
@@ -81,8 +157,29 @@ export class RedisDriver extends DeepBaseDriver {
|
|
|
81
157
|
}
|
|
82
158
|
|
|
83
159
|
const key = args.shift();
|
|
84
|
-
const
|
|
85
|
-
|
|
160
|
+
const redisKey = this.name + ":" + key;
|
|
161
|
+
|
|
162
|
+
if (args.length === 0) {
|
|
163
|
+
// Delete entire key
|
|
164
|
+
await this.client.del(redisKey);
|
|
165
|
+
} else {
|
|
166
|
+
// Delete nested path
|
|
167
|
+
let data = await this.get(key);
|
|
168
|
+
if (data === null) return [key, ...args];
|
|
169
|
+
|
|
170
|
+
// Navigate and delete nested value
|
|
171
|
+
let current = data;
|
|
172
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
173
|
+
const pathKey = args[i];
|
|
174
|
+
if (!current[pathKey]) return [key, ...args];
|
|
175
|
+
current = current[pathKey];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const lastKey = args[args.length - 1];
|
|
179
|
+
delete current[lastKey];
|
|
180
|
+
|
|
181
|
+
await this.client.set(redisKey, JSON.stringify(data));
|
|
182
|
+
}
|
|
86
183
|
|
|
87
184
|
return [key, ...args];
|
|
88
185
|
}
|
|
@@ -99,26 +196,8 @@ export class RedisDriver extends DeepBaseDriver {
|
|
|
99
196
|
return this.set(...args, func(await this.get(...args)));
|
|
100
197
|
}
|
|
101
198
|
|
|
102
|
-
async _set(key, path, value) {
|
|
103
|
-
if (value === undefined) {
|
|
104
|
-
await this.client.json.del(this.name + ":" + key, path);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
await this.client.json.set(this.name + ":" + key, path, value);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
const keys = this._keyToPath(path);
|
|
112
|
-
keys.pop();
|
|
113
|
-
const keyPath = keys.length == 0 ? "." : this._pathToKey(keys);
|
|
114
|
-
await this._set(key, keyPath, {});
|
|
115
|
-
await this._set(key, path, value);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
199
|
async _zeroKeys() {
|
|
120
200
|
const scan = {
|
|
121
|
-
TYPE: "ReJSON-RL",
|
|
122
201
|
MATCH: this.name + ":*",
|
|
123
202
|
COUNT: 1000,
|
|
124
203
|
};
|
|
@@ -133,4 +212,3 @@ export class RedisDriver extends DeepBaseDriver {
|
|
|
133
212
|
}
|
|
134
213
|
|
|
135
214
|
export default RedisDriver;
|
|
136
|
-
|
package/test/test.js
CHANGED
|
@@ -86,6 +86,66 @@ describe('RedisDriver', function() {
|
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
describe('Keys with Dots', function() {
|
|
90
|
+
it('should handle keys containing dots', async function() {
|
|
91
|
+
const setResult = await db.set('value', 'martin.clasen@gmail.com', 1);
|
|
92
|
+
|
|
93
|
+
assert.strictEqual(setResult.length, 2);
|
|
94
|
+
assert.strictEqual(setResult[0], 'value');
|
|
95
|
+
assert.strictEqual(setResult[1], 'martin.clasen@gmail.com');
|
|
96
|
+
|
|
97
|
+
const getValue = await db.get('value');
|
|
98
|
+
assert.strictEqual(getValue['martin.clasen@gmail.com'], 1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should handle keys with multiple dots', async function() {
|
|
102
|
+
await db.set('config', 'api.prod.endpoint', 'https://api.example.com');
|
|
103
|
+
const endpoint = await db.get('config', 'api.prod.endpoint');
|
|
104
|
+
assert.strictEqual(endpoint, 'https://api.example.com');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle nested paths with dots in keys', async function() {
|
|
108
|
+
await db.set('users', 'john.doe@example.com', 'name', 'John Doe');
|
|
109
|
+
await db.set('users', 'john.doe@example.com', 'age', 30);
|
|
110
|
+
|
|
111
|
+
const user = await db.get('users', 'john.doe@example.com');
|
|
112
|
+
assert.strictEqual(user.name, 'John Doe');
|
|
113
|
+
assert.strictEqual(user.age, 30);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should handle dots in first level keys', async function() {
|
|
117
|
+
await db.set('config.prod', 'value', 100);
|
|
118
|
+
const configProd = await db.get('config.prod');
|
|
119
|
+
assert.strictEqual(configProd.value, 100);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should distinguish between dots in keys and path separators', async function() {
|
|
123
|
+
// Set a regular nested path
|
|
124
|
+
await db.set('users', 'alice', 'email', 'alice@example.com');
|
|
125
|
+
|
|
126
|
+
// Set a key with dots at the same level
|
|
127
|
+
await db.set('users', 'john.doe@company.com', 'email', 'john@work.com');
|
|
128
|
+
|
|
129
|
+
const users = await db.get('users');
|
|
130
|
+
assert.strictEqual(users.alice.email, 'alice@example.com');
|
|
131
|
+
assert.strictEqual(users['john.doe@company.com'].email, 'john@work.com');
|
|
132
|
+
assert.strictEqual(Object.keys(users).length, 2);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should delete keys containing dots', async function() {
|
|
136
|
+
await db.set('emails', 'user.name@domain.com', 'verified', true);
|
|
137
|
+
await db.set('emails', 'other@email.com', 'verified', false);
|
|
138
|
+
|
|
139
|
+
await db.del('emails', 'user.name@domain.com');
|
|
140
|
+
|
|
141
|
+
const email1 = await db.get('emails', 'user.name@domain.com');
|
|
142
|
+
const email2 = await db.get('emails', 'other@email.com');
|
|
143
|
+
|
|
144
|
+
assert.strictEqual(email1, undefined);
|
|
145
|
+
assert.strictEqual(email2.verified, false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
89
149
|
describe('Delete Operations', function() {
|
|
90
150
|
it('should delete a key', async function() {
|
|
91
151
|
await db.set('temp', 'value');
|