resplite 1.0.2 → 1.0.6
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 +182 -5
- package/package.json +7 -2
- package/spec/SPEC_F.md +505 -0
- package/src/blocking/manager.js +183 -0
- package/src/cli/resplite-dirty-tracker.js +124 -0
- package/src/cli/resplite-import.js +237 -0
- package/src/commands/blpop.js +50 -0
- package/src/commands/brpop.js +50 -0
- package/src/commands/registry.js +11 -5
- package/src/engine/engine.js +11 -3
- package/src/migration/apply-dirty.js +97 -0
- package/src/migration/bulk.js +181 -0
- package/src/migration/import-one.js +106 -0
- package/src/migration/index.js +170 -0
- package/src/migration/preflight.js +62 -0
- package/src/migration/registry.js +222 -0
- package/src/migration/verify.js +191 -0
- package/src/server/connection.js +55 -13
- package/src/storage/sqlite/db.js +2 -0
- package/src/storage/sqlite/migration-schema.js +66 -0
- package/tasks/todo.md +19 -0
- package/test/integration/blocking.test.js +107 -0
- package/test/unit/migration-registry.test.js +127 -0
package/README.md
CHANGED
|
@@ -35,6 +35,24 @@ OK
|
|
|
35
35
|
"bar"
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
### Standalone server script (fixed port)
|
|
39
|
+
|
|
40
|
+
Run this as a persistent background process (`node server.js`). RESPLite will listen on port 6380 and stay up until the process is killed.
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
// server.js
|
|
44
|
+
import { createRESPlite } from 'resplite/embed';
|
|
45
|
+
|
|
46
|
+
const srv = await createRESPlite({ port: 6380, db: './data.db' });
|
|
47
|
+
console.log(`RESPLite listening on ${srv.host}:${srv.port}`);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then connect from any other script or process:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
redis-cli -p 6380 PING
|
|
54
|
+
```
|
|
55
|
+
|
|
38
56
|
### Environment variables
|
|
39
57
|
|
|
40
58
|
| Variable | Default | Description |
|
|
@@ -141,6 +159,23 @@ console.log(await client.lPop('queue')); // → "a"
|
|
|
141
159
|
console.log(await client.rPop('queue')); // → "e"
|
|
142
160
|
```
|
|
143
161
|
|
|
162
|
+
### Blocking list commands (BLPOP / BRPOP)
|
|
163
|
+
|
|
164
|
+
`BLPOP` and `BRPOP` block until an element is available or a timeout (seconds) is reached. Use them for simple queues or coordination between producers and consumers.
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
// Consumer: block up to 10 seconds for an element from "tasks" or "fallback"
|
|
168
|
+
const result = await client.blPop(['tasks', 'fallback'], 10);
|
|
169
|
+
// result is { key: 'tasks', element: 'item1' } or null on timeout
|
|
170
|
+
|
|
171
|
+
// Producer (e.g. another client or process)
|
|
172
|
+
await client.rPush('tasks', 'item1');
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- **Timeout**: `0` = block indefinitely; `> 0` = block up to that many seconds.
|
|
176
|
+
- **Return**: `{ key, element }` on success, or `null` on timeout.
|
|
177
|
+
- **Multi-key**: Keys are checked in order; the first key that has an element wins. One push wakes at most one blocked client (FIFO per key).
|
|
178
|
+
|
|
144
179
|
### Sorted sets
|
|
145
180
|
|
|
146
181
|
```javascript
|
|
@@ -239,7 +274,7 @@ await srv2.close();
|
|
|
239
274
|
| **TTL** | EXPIRE, PEXPIRE, TTL, PTTL, PERSIST |
|
|
240
275
|
| **Hashes** | HSET, HGET, HMGET, HGETALL, HDEL, HEXISTS, HINCRBY |
|
|
241
276
|
| **Sets** | SADD, SREM, SMEMBERS, SISMEMBER, SCARD |
|
|
242
|
-
| **Lists** | LPUSH, RPUSH, LLEN, LRANGE, LINDEX, LPOP, RPOP |
|
|
277
|
+
| **Lists** | LPUSH, RPUSH, LLEN, LRANGE, LINDEX, LPOP, RPOP, BLPOP, BRPOP |
|
|
243
278
|
| **Sorted sets** | ZADD, ZREM, ZCARD, ZSCORE, ZRANGE, ZRANGEBYSCORE |
|
|
244
279
|
| **Search (FT.\*)** | FT.CREATE, FT.INFO, FT.ADD, FT.DEL, FT.SEARCH, FT.SUGADD, FT.SUGGET, FT.SUGDEL |
|
|
245
280
|
| **Introspection** | TYPE, SCAN |
|
|
@@ -252,14 +287,18 @@ await srv2.close();
|
|
|
252
287
|
- Streams (XADD, XRANGE, etc.)
|
|
253
288
|
- Lua (EVAL, EVALSHA)
|
|
254
289
|
- Transactions (MULTI, EXEC, WATCH)
|
|
255
|
-
-
|
|
290
|
+
- BRPOPLPUSH, BLMOVE (blocking list moves)
|
|
256
291
|
- SELECT (multiple logical DBs)
|
|
257
292
|
|
|
258
293
|
Unsupported commands return: `ERR command not supported yet`.
|
|
259
294
|
|
|
260
295
|
## Migration from Redis
|
|
261
296
|
|
|
262
|
-
|
|
297
|
+
Migration supports two modes:
|
|
298
|
+
|
|
299
|
+
### Simple one-shot import (legacy)
|
|
300
|
+
|
|
301
|
+
For small datasets or when downtime is acceptable:
|
|
263
302
|
|
|
264
303
|
```bash
|
|
265
304
|
# Default: redis://127.0.0.1:6379 → ./data.db
|
|
@@ -275,7 +314,143 @@ npm run import-from-redis -- --db ./migrated.db --host 127.0.0.1 --port 6379
|
|
|
275
314
|
npm run import-from-redis -- --db ./migrated.db --pragma-template performance
|
|
276
315
|
```
|
|
277
316
|
|
|
278
|
-
|
|
317
|
+
### Redis with authentication
|
|
318
|
+
|
|
319
|
+
Migration supports Redis instances protected by a password. Use a Redis URL that includes the password (or username and password for Redis 6+ ACL):
|
|
320
|
+
|
|
321
|
+
- **Password only:** `redis://:PASSWORD@host:port`
|
|
322
|
+
- **Username and password:** `redis://username:PASSWORD@host:port`
|
|
323
|
+
|
|
324
|
+
Examples:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
# One-shot import from authenticated Redis
|
|
328
|
+
npm run import-from-redis -- --db ./migrated.db --redis-url "redis://:mysecret@127.0.0.1:6379"
|
|
329
|
+
|
|
330
|
+
# SPEC_F flow: use --from with the full URL (or set RESPLITE_IMPORT_FROM)
|
|
331
|
+
npx resplite-import preflight --from "redis://:mysecret@10.0.0.10:6379" --to ./resplite.db
|
|
332
|
+
npx resplite-dirty-tracker start --run-id run_001 --from "redis://:mysecret@10.0.0.10:6379" --to ./resplite.db
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
For one-shot import, authentication is only available when using `--redis-url`; the `--host` / `--port` options do not support a password.
|
|
336
|
+
|
|
337
|
+
### Minimal-downtime migration (SPEC_F)
|
|
338
|
+
|
|
339
|
+
For large datasets (~30 GB), use the Dirty Key Registry flow so the bulk of the migration runs online and only a short cutover is needed.
|
|
340
|
+
|
|
341
|
+
**Enable keyspace notifications in Redis** (required for the dirty-key tracker). Either run at runtime:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
redis-cli CONFIG SET notify-keyspace-events Kgxe
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Or add to `redis.conf` and restart Redis:
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
notify-keyspace-events Kgxe
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
(`K` = keyspace, `g` = generic, `x` = expired; `e` = keyevent. This lets the tracker see key changes and expirations.)
|
|
354
|
+
|
|
355
|
+
1. **Preflight** – Check Redis, key count, type distribution, and that keyspace notifications are enabled:
|
|
356
|
+
```bash
|
|
357
|
+
npx resplite-import preflight --from redis://10.0.0.10:6379 --to ./resplite.db
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
2. **Start dirty-key tracker** – Captures keys modified during bulk (requires `notify-keyspace-events` in Redis):
|
|
361
|
+
```bash
|
|
362
|
+
npx resplite-dirty-tracker start --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
3. **Bulk import** – SCAN and copy all keys; progress is checkpointed and resumable:
|
|
366
|
+
```bash
|
|
367
|
+
npx resplite-import bulk --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db \
|
|
368
|
+
--scan-count 1000 --max-rps 2000 --batch-keys 200 --batch-bytes 64MB --resume
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
4. **Monitor** – Check run and dirty-key counts:
|
|
372
|
+
```bash
|
|
373
|
+
npx resplite-import status --run-id run_001 --to ./resplite.db
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
5. **Cutover** – Freeze app writes to Redis, then apply remaining dirty keys:
|
|
377
|
+
```bash
|
|
378
|
+
npx resplite-import apply-dirty --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
6. **Stop tracker and switch** – Stop the tracker and point clients to RespLite:
|
|
382
|
+
```bash
|
|
383
|
+
npx resplite-dirty-tracker stop --run-id run_001 --to ./resplite.db
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
7. **Verify** – Optional sampling check between Redis and destination:
|
|
387
|
+
```bash
|
|
388
|
+
npx resplite-import verify --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db --sample 0.5%
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Then start RespLite with the migrated DB: `RESPLITE_DB=./resplite.db npm start`.
|
|
392
|
+
|
|
393
|
+
### Programmatic migration API
|
|
394
|
+
|
|
395
|
+
As an alternative to the CLI, the full migration flow is available as a JavaScript API via `resplite/migration`. Useful for embedding the migration inside your own scripts or automation pipelines.
|
|
396
|
+
|
|
397
|
+
```javascript
|
|
398
|
+
import { createMigration } from 'resplite/migration';
|
|
399
|
+
|
|
400
|
+
const m = createMigration({
|
|
401
|
+
from: 'redis://127.0.0.1:6379', // source Redis URL (default)
|
|
402
|
+
to: './resplite.db', // destination SQLite DB path (required)
|
|
403
|
+
runId: 'my-migration-1', // unique run ID (required for bulk/status/applyDirty)
|
|
404
|
+
|
|
405
|
+
// optional — same defaults as the CLI:
|
|
406
|
+
scanCount: 1000,
|
|
407
|
+
batchKeys: 200,
|
|
408
|
+
batchBytes: 64 * 1024 * 1024, // 64 MB
|
|
409
|
+
maxRps: 0, // 0 = unlimited
|
|
410
|
+
pragmaTemplate: 'default',
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Step 0 — Preflight: inspect Redis before starting
|
|
414
|
+
const info = await m.preflight();
|
|
415
|
+
console.log('keys (estimate):', info.keyCountEstimate);
|
|
416
|
+
console.log('type distribution:', info.typeDistribution);
|
|
417
|
+
console.log('recommended params:', info.recommended);
|
|
418
|
+
|
|
419
|
+
// Step 1 — Bulk import (checkpointed, resumable)
|
|
420
|
+
await m.bulk({
|
|
421
|
+
resume: false, // true to resume a previous run
|
|
422
|
+
onProgress: (r) => console.log(
|
|
423
|
+
`scanned=${r.scanned_keys} migrated=${r.migrated_keys} errors=${r.error_keys}`
|
|
424
|
+
),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Check status at any point (synchronous, no Redis needed)
|
|
428
|
+
const { run, dirty } = m.status();
|
|
429
|
+
console.log('bulk status:', run.status, '— dirty counts:', dirty);
|
|
430
|
+
|
|
431
|
+
// Step 2 — Apply dirty keys that changed in Redis during bulk
|
|
432
|
+
await m.applyDirty();
|
|
433
|
+
|
|
434
|
+
// Step 3 — Verify a sample of keys match between Redis and the destination
|
|
435
|
+
const result = await m.verify({ samplePct: 0.5, maxSample: 10000 });
|
|
436
|
+
console.log(`verified ${result.sampled} keys — mismatches: ${result.mismatches.length}`);
|
|
437
|
+
|
|
438
|
+
// Disconnect Redis when done
|
|
439
|
+
await m.close();
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The dirty-key tracker (to capture writes during bulk) still runs as a separate process via `npx resplite-dirty-tracker`. The API above handles everything else in a single script.
|
|
443
|
+
|
|
444
|
+
#### Low-level re-exports
|
|
445
|
+
|
|
446
|
+
If you need more control, the individual functions and registry helpers are also exported:
|
|
447
|
+
|
|
448
|
+
```javascript
|
|
449
|
+
import {
|
|
450
|
+
runPreflight, runBulkImport, runApplyDirty, runVerify,
|
|
451
|
+
getRun, getDirtyCounts, createRun, setRunStatus, logError,
|
|
452
|
+
} from 'resplite/migration';
|
|
453
|
+
```
|
|
279
454
|
|
|
280
455
|
## Benchmark (Redis vs RESPLite)
|
|
281
456
|
|
|
@@ -303,7 +478,9 @@ npm run benchmark -- --iterations 10000 --redis-port 6379 --resplite-port 6380
|
|
|
303
478
|
| `npm run test:contract` | Contract tests (redis client) |
|
|
304
479
|
| `npm run test:stress` | Stress tests |
|
|
305
480
|
| `npm run benchmark` | Comparative benchmark Redis vs RESPLite |
|
|
306
|
-
| `npm run import-from-redis` |
|
|
481
|
+
| `npm run import-from-redis` | One-shot import from Redis into a SQLite DB |
|
|
482
|
+
| `npx resplite-import` (preflight, bulk, status, apply-dirty, verify) | Migration CLI (SPEC_F minimal-downtime flow) |
|
|
483
|
+
| `npx resplite-dirty-tracker <start\|stop>` | Dirty-key tracker for migration cutover |
|
|
307
484
|
|
|
308
485
|
## Specification
|
|
309
486
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "resplite",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "A RESP2 server with practical Redis compatibility, backed by SQLite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"resplite-import": "src/cli/resplite-import.js",
|
|
9
|
+
"resplite-dirty-tracker": "src/cli/resplite-dirty-tracker.js"
|
|
10
|
+
},
|
|
7
11
|
"exports": {
|
|
8
12
|
".": "./src/index.js",
|
|
9
|
-
"./embed": "./src/embed.js"
|
|
13
|
+
"./embed": "./src/embed.js",
|
|
14
|
+
"./migration": "./src/migration/index.js"
|
|
10
15
|
},
|
|
11
16
|
"scripts": {
|
|
12
17
|
"start": "node src/index.js",
|