resplite 1.0.4 → 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 |
@@ -296,9 +314,43 @@ npm run import-from-redis -- --db ./migrated.db --host 127.0.0.1 --port 6379
296
314
  npm run import-from-redis -- --db ./migrated.db --pragma-template performance
297
315
  ```
298
316
 
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
+
299
337
  ### Minimal-downtime migration (SPEC_F)
300
338
 
301
- 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:
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.)
302
354
 
303
355
  1. **Preflight** – Check Redis, key count, type distribution, and that keyspace notifications are enabled:
304
356
  ```bash
@@ -338,6 +390,68 @@ For large datasets (~30 GB), use the Dirty Key Registry flow so the bulk of the
338
390
 
339
391
  Then start RespLite with the migrated DB: `RESPLITE_DB=./resplite.db npm start`.
340
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
+ ```
454
+
341
455
  ## Benchmark (Redis vs RESPLite)
342
456
 
343
457
  Compare throughput of local Redis and RESPLite with the same workload (PING, SET/GET, hashes, sets, lists, zsets, etc.):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resplite",
3
- "version": "1.0.4",
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",
@@ -10,7 +10,8 @@
10
10
  },
11
11
  "exports": {
12
12
  ".": "./src/index.js",
13
- "./embed": "./src/embed.js"
13
+ "./embed": "./src/embed.js",
14
+ "./migration": "./src/migration/index.js"
14
15
  },
15
16
  "scripts": {
16
17
  "start": "node src/index.js",
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Programmatic migration API (SPEC_F §F.9).
3
+ *
4
+ * Usage:
5
+ * import { createMigration } from 'resplite/migration';
6
+ *
7
+ * const m = createMigration({
8
+ * from: 'redis://127.0.0.1:6379',
9
+ * to: './data.db',
10
+ * runId: 'my-migration-1',
11
+ * });
12
+ *
13
+ * const info = await m.preflight();
14
+ * await m.bulk({ onProgress: console.log });
15
+ * const status = m.status();
16
+ * await m.applyDirty();
17
+ * const result = await m.verify();
18
+ * await m.close();
19
+ */
20
+
21
+ import { createClient } from 'redis';
22
+ import { openDb } from '../storage/sqlite/db.js';
23
+ import { runPreflight } from './preflight.js';
24
+ import { runBulkImport } from './bulk.js';
25
+ import { runApplyDirty } from './apply-dirty.js';
26
+ import { runVerify } from './verify.js';
27
+ import { getRun, getDirtyCounts } from './registry.js';
28
+
29
+ /**
30
+ * @typedef {object} MigrationOptions
31
+ * @property {string} [from='redis://127.0.0.1:6379'] - Source Redis URL.
32
+ * @property {string} to - Destination SQLite DB path.
33
+ * @property {string} [runId] - Unique run identifier (required for bulk/status/applyDirty).
34
+ * @property {string} [pragmaTemplate='default'] - PRAGMA preset.
35
+ * @property {number} [scanCount=1000]
36
+ * @property {number} [maxRps=0] - Max requests/s (0 = unlimited).
37
+ * @property {number} [batchKeys=200]
38
+ * @property {number} [batchBytes=67108864] - 64 MB default.
39
+ */
40
+
41
+ /**
42
+ * Create a migration controller bound to a source Redis and destination DB.
43
+ *
44
+ * @param {MigrationOptions} options
45
+ * @returns {{
46
+ * preflight(): Promise<import('./preflight.js').PreflightResult>,
47
+ * bulk(opts?: { resume?: boolean, onProgress?: function }): Promise<object>,
48
+ * status(): { run: object, dirty: object } | null,
49
+ * applyDirty(opts?: { batchKeys?: number, maxRps?: number }): Promise<object>,
50
+ * verify(opts?: { samplePct?: number, maxSample?: number }): Promise<object>,
51
+ * close(): Promise<void>,
52
+ * }}
53
+ */
54
+ export function createMigration({
55
+ from = 'redis://127.0.0.1:6379',
56
+ to,
57
+ runId,
58
+ pragmaTemplate = 'default',
59
+ scanCount = 1000,
60
+ maxRps = 0,
61
+ batchKeys = 200,
62
+ batchBytes = 64 * 1024 * 1024,
63
+ } = {}) {
64
+ if (!to) throw new Error('createMigration: "to" (db path) is required');
65
+
66
+ let _client = null;
67
+
68
+ async function getClient() {
69
+ if (_client) return _client;
70
+ _client = createClient({ url: from });
71
+ _client.on('error', (err) => {
72
+ /* non-fatal connection errors; callers surface them on next await */
73
+ void err;
74
+ });
75
+ await _client.connect();
76
+ return _client;
77
+ }
78
+
79
+ function requireRunId() {
80
+ if (!runId) throw new Error('createMigration: "runId" is required for this operation');
81
+ return runId;
82
+ }
83
+
84
+ return {
85
+ /**
86
+ * Step 0 — Preflight: inspect the source Redis instance.
87
+ * Returns key count estimate, type distribution, keyspace-events config,
88
+ * and recommended import parameters.
89
+ */
90
+ async preflight() {
91
+ const client = await getClient();
92
+ return runPreflight(client);
93
+ },
94
+
95
+ /**
96
+ * Step 1 — Bulk import: SCAN all keys from Redis into the destination DB.
97
+ * Supports resume (checkpoint-based) and optional progress callback.
98
+ *
99
+ * @param {{ resume?: boolean, onProgress?: (run: object) => void }} [opts]
100
+ */
101
+ async bulk({ resume = false, onProgress } = {}) {
102
+ const id = requireRunId();
103
+ const client = await getClient();
104
+ return runBulkImport(client, to, id, {
105
+ sourceUri: from,
106
+ pragmaTemplate,
107
+ scan_count: scanCount,
108
+ max_rps: maxRps,
109
+ batch_keys: batchKeys,
110
+ batch_bytes: batchBytes,
111
+ resume,
112
+ onProgress,
113
+ });
114
+ },
115
+
116
+ /**
117
+ * Step 2 — Status: read run metadata and dirty-key counts from the DB.
118
+ * Synchronous — no Redis connection needed.
119
+ *
120
+ * @returns {{ run: object, dirty: object } | null}
121
+ */
122
+ status() {
123
+ const id = requireRunId();
124
+ const db = openDb(to, { pragmaTemplate });
125
+ const run = getRun(db, id);
126
+ if (!run) return null;
127
+ const dirty = getDirtyCounts(db, id);
128
+ return { run, dirty };
129
+ },
130
+
131
+ /**
132
+ * Step 3 — Apply dirty: reconcile keys that changed in Redis during bulk import.
133
+ *
134
+ * @param {{ batchKeys?: number, maxRps?: number }} [opts]
135
+ */
136
+ async applyDirty({ batchKeys: bk = batchKeys, maxRps: rps = maxRps } = {}) {
137
+ const id = requireRunId();
138
+ const client = await getClient();
139
+ return runApplyDirty(client, to, id, {
140
+ pragmaTemplate,
141
+ batch_keys: bk,
142
+ max_rps: rps,
143
+ });
144
+ },
145
+
146
+ /**
147
+ * Step 4 — Verify: sample keys from Redis and compare with the destination DB.
148
+ *
149
+ * @param {{ samplePct?: number, maxSample?: number }} [opts]
150
+ * @returns {Promise<{ sampled: number, matched: number, mismatches: Array<{ key: string, reason: string }> }>}
151
+ */
152
+ async verify({ samplePct = 0.5, maxSample = 10000 } = {}) {
153
+ const client = await getClient();
154
+ return runVerify(client, to, { pragmaTemplate, samplePct, maxSample });
155
+ },
156
+
157
+ /**
158
+ * Disconnect from Redis. Call when done with all migration operations.
159
+ */
160
+ async close() {
161
+ if (_client) {
162
+ await _client.quit().catch(() => {});
163
+ _client = null;
164
+ }
165
+ },
166
+ };
167
+ }
168
+
169
+ export { runPreflight, runBulkImport, runApplyDirty, runVerify };
170
+ export { getRun, getDirtyCounts, createRun, setRunStatus, logError } from './registry.js';