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 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
- - Blocking commands (BLPOP, etc.)
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
- An external CLI imports data from a running Redis instance into a RESPlite SQLite DB. Supports strings, hashes, sets, lists, zsets, and TTL. Requires a local or remote Redis and the `redis` npm package (dev dependency).
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
- Then start RESPLite with the migrated DB: `RESPLITE_DB=./migrated.db npm start`.
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` | Import from Redis into a SQLite DB |
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.2",
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",