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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # deepbase-redis
2
2
 
3
- Redis Stack driver for DeepBase.
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 Stack (includes RedisJSON module):
13
+ Requires standard Redis (no modules needed):
14
14
 
15
15
  ```bash
16
- docker run -d -p 6379:6379 --name redis redis/redis-stack-server:latest
16
+ docker run -d -p 6379:6379 --name redis redis:latest
17
17
  ```
18
18
 
19
- **Note:** Standard Redis won't work - you need Redis Stack for JSON support.
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 the RedisJSON module. Perfect for:
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
- - ✅ Fast reads and writes
29
- - ✅ In-memory speed with persistence
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 Redis JSON keys:
61
+ Data is stored as JSON strings in Redis:
62
62
 
63
63
  ```javascript
64
64
  // Keys created
65
- myapp:users -> { alice: {...}, bob: {...} }
66
- myapp:config -> { theme: "dark", lang: "en" }
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
- ### RedisJSON Support
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
- Uses Redis JSON module for native JSON operations:
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
- // Stored at JSON path: $.alice.address
100
+ // Reads full object, modifies, and writes back
78
101
  ```
79
102
 
80
- ### Atomic Increment
103
+ ### Increment/Decrement
81
104
 
82
- Uses Redis `JSON.NUMINCRBY` for atomic operations:
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
- - **Atomic operations**: Lock-free increments and updates
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. **Set appropriate TTLs** - Expire old data
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 Stack supports:
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/redis-stack-server:latest \
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.0",
4
- "description": "⚡ DeepBase Redis Stack - driver",
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.0"
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
- "redis-stack",
34
- "redis-json",
33
+ "vanilla",
35
34
  "driver",
36
35
  "database",
37
36
  "persist",
@@ -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 path = args.length == 0 ? "." : this._pathToKey(args);
34
+ const redisKey = this.name + ":" + key;
34
35
 
35
36
  try {
36
- return await this.client.json.get(this.name + ":" + key, { path });
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 keyPath = keys.length == 0 ? "." : this._pathToKey(keys);
50
- await this._set(key, keyPath, value);
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
- try {
61
- await this.client.json.numIncrBy(this.name + ":" + key, path, i);
62
- return [key, ...args];
63
- } catch (error) {
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 path = args.length == 0 ? "." : this._pathToKey(args);
85
- await this.client.json.del(this.name + ":" + key, path);
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');