resplite 1.2.6 → 1.2.8
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 +168 -275
- package/package.json +1 -6
- package/scripts/create-interface-smoke.js +32 -0
- package/skills/README.md +22 -0
- package/skills/resplite-command-vertical-slice/SKILL.md +134 -0
- package/skills/resplite-ft-search-workbench/SKILL.md +138 -0
- package/skills/resplite-migration-cutover-assistant/SKILL.md +138 -0
- package/spec/00-INDEX.md +37 -0
- package/spec/01-overview-and-goals.md +125 -0
- package/spec/02-protocol-and-commands.md +174 -0
- package/spec/03-data-model-ttl-transactions.md +157 -0
- package/spec/04-cache-architecture.md +171 -0
- package/spec/05-scan-admin-implementation.md +379 -0
- package/spec/06-migration-strategy-core.md +79 -0
- package/spec/07-type-lists.md +202 -0
- package/spec/08-type-sorted-sets.md +220 -0
- package/spec/{SPEC_D.md → 09-search-ft-commands.md} +3 -1
- package/spec/{SPEC_E.md → 10-blocking-commands.md} +3 -1
- package/spec/{SPEC_F.md → 11-migration-dirty-registry.md} +61 -147
- package/src/commands/object.js +17 -0
- package/src/commands/registry.js +2 -0
- package/src/engine/engine.js +11 -0
- package/src/migration/apply-dirty.js +8 -1
- package/src/migration/index.js +5 -4
- package/src/migration/migrate-search.js +25 -6
- package/test/integration/object-idletime.test.js +51 -0
- package/test/unit/migrate-search.test.js +50 -2
- package/spec/SPEC_A.md +0 -1171
- package/spec/SPEC_B.md +0 -426
- package/src/cli/import-from-redis.js +0 -194
- package/src/cli/resplite-dirty-tracker.js +0 -92
- package/src/cli/resplite-import.js +0 -296
- package/test/contract/import-from-redis.test.js +0 -83
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* resplite-import CLI (SPEC_F §F.9): preflight, bulk, status, apply-dirty, verify.
|
|
4
|
-
* Usage: resplite-import <preflight|bulk|status|apply-dirty|verify> [options]
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { createClient } from 'redis';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { openDb } from '../storage/sqlite/db.js';
|
|
10
|
-
import { runPreflight } from '../migration/preflight.js';
|
|
11
|
-
import { runBulkImport } from '../migration/bulk.js';
|
|
12
|
-
import { runApplyDirty } from '../migration/apply-dirty.js';
|
|
13
|
-
import { runVerify } from '../migration/verify.js';
|
|
14
|
-
import { runMigrateSearch } from '../migration/migrate-search.js';
|
|
15
|
-
import { getRun, getDirtyCounts } from '../migration/registry.js';
|
|
16
|
-
|
|
17
|
-
const SUBCOMMANDS = ['preflight', 'bulk', 'status', 'apply-dirty', 'verify', 'migrate-search'];
|
|
18
|
-
|
|
19
|
-
function parseArgs(argv = process.argv.slice(2)) {
|
|
20
|
-
const args = { _: [] };
|
|
21
|
-
for (let i = 0; i < argv.length; i++) {
|
|
22
|
-
const arg = argv[i];
|
|
23
|
-
if (arg === '--from' && argv[i + 1]) {
|
|
24
|
-
args.from = argv[++i];
|
|
25
|
-
} else if (arg === '--to' && argv[i + 1]) {
|
|
26
|
-
args.to = argv[++i];
|
|
27
|
-
} else if (arg === '--run-id' && argv[i + 1]) {
|
|
28
|
-
args.runId = argv[++i];
|
|
29
|
-
} else if (arg === '--scan-count' && argv[i + 1]) {
|
|
30
|
-
args.scanCount = parseInt(argv[++i], 10);
|
|
31
|
-
} else if (arg === '--max-concurrency' && argv[i + 1]) {
|
|
32
|
-
args.maxConcurrency = parseInt(argv[++i], 10);
|
|
33
|
-
} else if (arg === '--max-rps' && argv[i + 1]) {
|
|
34
|
-
args.maxRps = parseInt(argv[++i], 10);
|
|
35
|
-
} else if (arg === '--batch-keys' && argv[i + 1]) {
|
|
36
|
-
args.batchKeys = parseInt(argv[++i], 10);
|
|
37
|
-
} else if (arg === '--batch-bytes' && argv[i + 1]) {
|
|
38
|
-
const v = argv[++i];
|
|
39
|
-
const match = v.match(/^(\d+)(MB|KB|GB)?$/i);
|
|
40
|
-
if (match) {
|
|
41
|
-
let n = parseInt(match[1], 10);
|
|
42
|
-
if (match[2]) {
|
|
43
|
-
if (match[2].toUpperCase() === 'KB') n *= 1024;
|
|
44
|
-
else if (match[2].toUpperCase() === 'MB') n *= 1024 * 1024;
|
|
45
|
-
else if (match[2].toUpperCase() === 'GB') n *= 1024 * 1024 * 1024;
|
|
46
|
-
}
|
|
47
|
-
args.batchBytes = n;
|
|
48
|
-
}
|
|
49
|
-
} else if (arg === '--resume') {
|
|
50
|
-
args.resume = true;
|
|
51
|
-
} else if (arg === '--no-resume') {
|
|
52
|
-
args.resume = false;
|
|
53
|
-
} else if (arg === '--pragma-template' && argv[i + 1]) {
|
|
54
|
-
args.pragmaTemplate = argv[++i];
|
|
55
|
-
} else if (arg === '--sample' && argv[i + 1]) {
|
|
56
|
-
args.sample = argv[++i];
|
|
57
|
-
} else if (arg === '--index' && argv[i + 1]) {
|
|
58
|
-
if (!args.index) args.index = [];
|
|
59
|
-
args.index.push(argv[++i]);
|
|
60
|
-
} else if (arg === '--batch-docs' && argv[i + 1]) {
|
|
61
|
-
args.batchDocs = parseInt(argv[++i], 10);
|
|
62
|
-
} else if (arg === '--max-suggestions' && argv[i + 1]) {
|
|
63
|
-
args.maxSuggestions = parseInt(argv[++i], 10);
|
|
64
|
-
} else if (arg === '--no-skip') {
|
|
65
|
-
args.noSkip = true;
|
|
66
|
-
} else if (arg === '--no-suggestions') {
|
|
67
|
-
args.noSuggestions = true;
|
|
68
|
-
} else if (arg.startsWith('--')) {
|
|
69
|
-
args[arg.slice(2).replace(/-/g, '')] = argv[i + 1] ?? true;
|
|
70
|
-
if (argv[i + 1] && !argv[i + 1].startsWith('--')) i++;
|
|
71
|
-
} else {
|
|
72
|
-
args._.push(arg);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return args;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function getRedisUrl(args) {
|
|
79
|
-
return args.from || process.env.RESPLITE_IMPORT_FROM || 'redis://127.0.0.1:6379';
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function getDbPath(args) {
|
|
83
|
-
if (!args.to) {
|
|
84
|
-
console.error('Missing --to <db-path>');
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
return args.to;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function getRunId(args, required = false) {
|
|
91
|
-
const id = args.runId || process.env.RESPLITE_RUN_ID;
|
|
92
|
-
if (required && !id) {
|
|
93
|
-
console.error('Missing --run-id <id>');
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
return id;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function cmdPreflight(args) {
|
|
100
|
-
const redisUrl = getRedisUrl(args);
|
|
101
|
-
const client = createClient({ url: redisUrl });
|
|
102
|
-
client.on('error', (e) => console.error('Redis:', e.message));
|
|
103
|
-
await client.connect();
|
|
104
|
-
try {
|
|
105
|
-
const result = await runPreflight(client);
|
|
106
|
-
console.log('Preflight:');
|
|
107
|
-
console.log(' Key count (estimate):', result.keyCountEstimate);
|
|
108
|
-
console.log(' Type distribution (sample):', result.typeDistribution);
|
|
109
|
-
console.log(' notify-keyspace-events:', result.notifyKeyspaceEvents ?? '(not set or not readable)');
|
|
110
|
-
if (!result.notifyKeyspaceEvents || result.notifyKeyspaceEvents === '') {
|
|
111
|
-
console.warn(' WARNING: Keyspace notifications disabled. Enable for dirty-key tracker (e.g. "Kgxe").');
|
|
112
|
-
}
|
|
113
|
-
console.log(' Recommended:');
|
|
114
|
-
console.log(' --scan-count', result.recommended.scan_count);
|
|
115
|
-
console.log(' --max-concurrency', result.recommended.max_concurrency);
|
|
116
|
-
console.log(' --max-rps', result.recommended.max_rps);
|
|
117
|
-
} finally {
|
|
118
|
-
await client.quit();
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function cmdBulk(args) {
|
|
123
|
-
const redisUrl = getRedisUrl(args);
|
|
124
|
-
const dbPath = getDbPath(args);
|
|
125
|
-
const runId = getRunId(args, true);
|
|
126
|
-
const client = createClient({ url: redisUrl });
|
|
127
|
-
client.on('error', (e) => console.error('Redis:', e.message));
|
|
128
|
-
await client.connect();
|
|
129
|
-
try {
|
|
130
|
-
const run = await runBulkImport(client, dbPath, runId, {
|
|
131
|
-
sourceUri: redisUrl,
|
|
132
|
-
pragmaTemplate: args.pragmaTemplate || 'default',
|
|
133
|
-
scan_count: args.scanCount || 1000,
|
|
134
|
-
max_rps: args.maxRps || 0,
|
|
135
|
-
batch_keys: args.batchKeys || 200,
|
|
136
|
-
batch_bytes: args.batchBytes || 64 * 1024 * 1024,
|
|
137
|
-
resume: args.resume !== false, // default true: start from 0 or continue from checkpoint
|
|
138
|
-
onProgress: (r) => {
|
|
139
|
-
console.log(` scanned=${r.scanned_keys} migrated=${r.migrated_keys} skipped=${r.skipped_keys} errors=${r.error_keys} cursor=${r.scan_cursor}`);
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
console.log('Bulk complete:', run);
|
|
143
|
-
} finally {
|
|
144
|
-
await client.quit();
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function cmdStatus(args) {
|
|
149
|
-
const dbPath = getDbPath(args);
|
|
150
|
-
const runId = getRunId(args, true);
|
|
151
|
-
const db = openDb(dbPath, { pragmaTemplate: args.pragmaTemplate || 'default' });
|
|
152
|
-
const run = getRun(db, runId);
|
|
153
|
-
if (!run) {
|
|
154
|
-
console.error('Run not found:', runId);
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
const dirty = getDirtyCounts(db, runId);
|
|
158
|
-
console.log('Run:', runId);
|
|
159
|
-
console.log(' status:', run.status);
|
|
160
|
-
console.log(' source:', run.source_uri);
|
|
161
|
-
console.log(' scan_cursor:', run.scan_cursor);
|
|
162
|
-
console.log(' scanned_keys:', run.scanned_keys, 'migrated_keys:', run.migrated_keys, 'skipped_keys:', run.skipped_keys, 'error_keys:', run.error_keys);
|
|
163
|
-
console.log(' migrated_bytes:', run.migrated_bytes);
|
|
164
|
-
console.log(' dirty_keys: seen=', run.dirty_keys_seen, 'applied=', run.dirty_keys_applied, 'deleted=', run.dirty_keys_deleted);
|
|
165
|
-
console.log(' dirty by state:', dirty);
|
|
166
|
-
if (run.last_error) console.log(' last_error:', run.last_error);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function cmdApplyDirty(args) {
|
|
170
|
-
const redisUrl = getRedisUrl(args);
|
|
171
|
-
const dbPath = getDbPath(args);
|
|
172
|
-
const runId = getRunId(args, true);
|
|
173
|
-
const client = createClient({ url: redisUrl });
|
|
174
|
-
client.on('error', (e) => console.error('Redis:', e.message));
|
|
175
|
-
await client.connect();
|
|
176
|
-
try {
|
|
177
|
-
const run = await runApplyDirty(client, dbPath, runId, {
|
|
178
|
-
pragmaTemplate: args.pragmaTemplate || 'default',
|
|
179
|
-
batch_keys: args.batchKeys || 200,
|
|
180
|
-
max_rps: args.maxRps || 0,
|
|
181
|
-
});
|
|
182
|
-
console.log('Apply-dirty complete:', run);
|
|
183
|
-
} finally {
|
|
184
|
-
await client.quit();
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function cmdVerify(args) {
|
|
189
|
-
const redisUrl = getRedisUrl(args);
|
|
190
|
-
const dbPath = getDbPath(args);
|
|
191
|
-
const client = createClient({ url: redisUrl });
|
|
192
|
-
client.on('error', (e) => console.error('Redis:', e.message));
|
|
193
|
-
await client.connect();
|
|
194
|
-
try {
|
|
195
|
-
let samplePct = 0.5;
|
|
196
|
-
if (args.sample) {
|
|
197
|
-
const m = args.sample.match(/^(\d*\.?\d+)\s*%?$/);
|
|
198
|
-
if (m) samplePct = parseFloat(m[1]);
|
|
199
|
-
}
|
|
200
|
-
const result = await runVerify(client, dbPath, {
|
|
201
|
-
pragmaTemplate: args.pragmaTemplate || 'default',
|
|
202
|
-
samplePct,
|
|
203
|
-
maxSample: 10000,
|
|
204
|
-
});
|
|
205
|
-
console.log('Verify: sampled=', result.sampled, 'matched=', result.matched, 'mismatches=', result.mismatches.length);
|
|
206
|
-
if (result.mismatches.length) {
|
|
207
|
-
result.mismatches.slice(0, 20).forEach((m) => console.log(' ', m.key, m.reason));
|
|
208
|
-
if (result.mismatches.length > 20) console.log(' ... and', result.mismatches.length - 20, 'more');
|
|
209
|
-
}
|
|
210
|
-
} finally {
|
|
211
|
-
await client.quit();
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async function cmdMigrateSearch(args) {
|
|
216
|
-
const redisUrl = getRedisUrl(args);
|
|
217
|
-
const dbPath = getDbPath(args);
|
|
218
|
-
const client = createClient({ url: redisUrl });
|
|
219
|
-
client.on('error', (e) => console.error('Redis:', e.message));
|
|
220
|
-
await client.connect();
|
|
221
|
-
try {
|
|
222
|
-
const onlyIndices = args.index
|
|
223
|
-
? (Array.isArray(args.index) ? args.index : [args.index])
|
|
224
|
-
: null;
|
|
225
|
-
|
|
226
|
-
const result = await runMigrateSearch(client, dbPath, {
|
|
227
|
-
pragmaTemplate: args.pragmaTemplate || 'default',
|
|
228
|
-
onlyIndices,
|
|
229
|
-
scanCount: args.scanCount || 500,
|
|
230
|
-
maxRps: args.maxRps || 0,
|
|
231
|
-
batchDocs: args.batchDocs || 200,
|
|
232
|
-
maxSuggestions: args.maxSuggestions || 10000,
|
|
233
|
-
skipExisting: args.noSkip ? false : true,
|
|
234
|
-
withSuggestions: args.noSuggestions ? false : true,
|
|
235
|
-
onProgress: (r) => {
|
|
236
|
-
const status = r.error ? `ERROR: ${r.error}` : (r.skipped ? 'skipped (already exists)' : 'created');
|
|
237
|
-
console.log(` [${r.name}] ${status} — docs=${r.docsImported} skipped=${r.docsSkipped} errors=${r.docErrors} sugs=${r.sugsImported}`);
|
|
238
|
-
if (r.warnings?.length) r.warnings.forEach((w) => console.log(` WARN: ${w}`));
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
if (result.aborted) console.log('Migration aborted by signal.');
|
|
243
|
-
console.log(`Done. Indices processed: ${result.indices.length}`);
|
|
244
|
-
const errors = result.indices.filter((i) => i.error);
|
|
245
|
-
if (errors.length) {
|
|
246
|
-
console.error(` ${errors.length} index(es) failed:`);
|
|
247
|
-
errors.forEach((i) => console.error(` - ${i.name}: ${i.error}`));
|
|
248
|
-
}
|
|
249
|
-
} finally {
|
|
250
|
-
await client.quit();
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function main() {
|
|
255
|
-
const args = parseArgs();
|
|
256
|
-
const sub = args._[0];
|
|
257
|
-
if (!SUBCOMMANDS.includes(sub)) {
|
|
258
|
-
console.error('Usage: resplite-import <preflight|bulk|status|apply-dirty|verify|migrate-search> [options]');
|
|
259
|
-
console.error(' --from <redis-url> (default: redis://127.0.0.1:6379)');
|
|
260
|
-
console.error(' --to <db-path> (required for bulk, status, apply-dirty, verify, migrate-search)');
|
|
261
|
-
console.error(' --run-id <id> (required for bulk, status, apply-dirty)');
|
|
262
|
-
console.error(' --scan-count N (bulk / migrate-search, default 1000 / 500)');
|
|
263
|
-
console.error(' --max-rps N (bulk, apply-dirty, migrate-search)');
|
|
264
|
-
console.error(' --batch-keys N (default 200)');
|
|
265
|
-
console.error(' --batch-bytes N[MB|KB|GB](default 64MB)');
|
|
266
|
-
console.error(' --resume / --no-resume (bulk: default resume=on)');
|
|
267
|
-
console.error(' --sample 0.5% (verify, default 0.5%)');
|
|
268
|
-
console.error(' --index <name> (migrate-search: repeat for multiple; omit for all indices)');
|
|
269
|
-
console.error(' --batch-docs N (migrate-search: docs per SQLite tx, default 200)');
|
|
270
|
-
console.error(' --max-suggestions N (migrate-search: cap for FT.SUGGET, default 10000)');
|
|
271
|
-
console.error(' --no-skip (migrate-search: overwrite if index exists)');
|
|
272
|
-
console.error(' --no-suggestions (migrate-search: skip suggestion import)');
|
|
273
|
-
process.exit(1);
|
|
274
|
-
}
|
|
275
|
-
try {
|
|
276
|
-
if (sub === 'preflight') await cmdPreflight(args);
|
|
277
|
-
else if (sub === 'bulk') await cmdBulk(args);
|
|
278
|
-
else if (sub === 'status') await cmdStatus(args);
|
|
279
|
-
else if (sub === 'apply-dirty')await cmdApplyDirty(args);
|
|
280
|
-
else if (sub === 'verify') await cmdVerify(args);
|
|
281
|
-
else if (sub === 'migrate-search') await cmdMigrateSearch(args);
|
|
282
|
-
} catch (err) {
|
|
283
|
-
console.error(err);
|
|
284
|
-
process.exit(1);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const isMain = process.argv[1] === fileURLToPath(import.meta.url) || process.argv[1]?.endsWith('resplite-import.js');
|
|
289
|
-
if (isMain) {
|
|
290
|
-
main().catch((e) => {
|
|
291
|
-
console.error(e);
|
|
292
|
-
process.exit(1);
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export { parseArgs, cmdPreflight, cmdBulk, cmdStatus, cmdApplyDirty, cmdVerify, cmdMigrateSearch };
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Contract test for Redis import CLI (SPEC §26).
|
|
3
|
-
* Requires a local Redis on 127.0.0.1:6379; skips if unavailable.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, before, after } from 'node:test';
|
|
7
|
-
import assert from 'node:assert/strict';
|
|
8
|
-
import { createClient } from 'redis';
|
|
9
|
-
import { importFromRedis } from '../../src/cli/import-from-redis.js';
|
|
10
|
-
import { createTestServer } from '../helpers/server.js';
|
|
11
|
-
import { tmpDbPath } from '../helpers/tmp.js';
|
|
12
|
-
|
|
13
|
-
const PREFIX = 'resplite:import:';
|
|
14
|
-
const REDIS_URL = 'redis://127.0.0.1:6379';
|
|
15
|
-
|
|
16
|
-
describe('import-from-redis', () => {
|
|
17
|
-
let redisClient;
|
|
18
|
-
let redisAvailable = false;
|
|
19
|
-
|
|
20
|
-
before(async () => {
|
|
21
|
-
try {
|
|
22
|
-
redisClient = createClient({ url: REDIS_URL });
|
|
23
|
-
await redisClient.connect();
|
|
24
|
-
await redisClient.ping();
|
|
25
|
-
redisAvailable = true;
|
|
26
|
-
} catch (_) {
|
|
27
|
-
redisAvailable = false;
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
after(async () => {
|
|
32
|
-
if (redisClient) {
|
|
33
|
-
try {
|
|
34
|
-
const keys = await redisClient.keys(PREFIX + '*');
|
|
35
|
-
if (keys.length) await redisClient.del(keys);
|
|
36
|
-
await redisClient.quit();
|
|
37
|
-
} catch (_) {}
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('skips when Redis is not available', { skip: redisAvailable }, () => {});
|
|
42
|
-
|
|
43
|
-
it('imports strings, hashes, sets and TTL from Redis into SQLite', { skip: !redisAvailable }, async () => {
|
|
44
|
-
const k1 = PREFIX + 's1';
|
|
45
|
-
const k2 = PREFIX + 'h1';
|
|
46
|
-
const k3 = PREFIX + 'set1';
|
|
47
|
-
const k4 = PREFIX + 'ttl1';
|
|
48
|
-
|
|
49
|
-
await redisClient.set(k1, 'hello');
|
|
50
|
-
await redisClient.hSet(k2, { a: '1', b: '2' });
|
|
51
|
-
await redisClient.sAdd(k3, ['x', 'y', 'z']);
|
|
52
|
-
await redisClient.set(k4, 'expires');
|
|
53
|
-
await redisClient.pExpire(k4, 60_000);
|
|
54
|
-
|
|
55
|
-
const dbPath = tmpDbPath();
|
|
56
|
-
const { stats } = await importFromRedis(redisClient, dbPath);
|
|
57
|
-
|
|
58
|
-
assert.ok(stats.string >= 1, 'at least one string imported');
|
|
59
|
-
assert.ok(stats.hash >= 1, 'at least one hash imported');
|
|
60
|
-
assert.ok(stats.set >= 1, 'at least one set imported');
|
|
61
|
-
|
|
62
|
-
const server = await createTestServer({ dbPath });
|
|
63
|
-
const client = createClient({ socket: { port: server.port, host: '127.0.0.1' } });
|
|
64
|
-
await client.connect();
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
assert.equal(await client.get(k1), 'hello');
|
|
68
|
-
const h = await client.hGetAll(k2);
|
|
69
|
-
assert.equal(h?.a, '1');
|
|
70
|
-
assert.equal(h?.b, '2');
|
|
71
|
-
const members = await client.sMembers(k3);
|
|
72
|
-
assert.ok(members.includes('x'));
|
|
73
|
-
assert.ok(members.includes('y'));
|
|
74
|
-
assert.ok(members.includes('z'));
|
|
75
|
-
assert.equal(await client.get(k4), 'expires');
|
|
76
|
-
const ttl = await client.ttl(k4);
|
|
77
|
-
assert.ok(ttl > 0 && ttl <= 60, 'TTL preserved in ms range');
|
|
78
|
-
} finally {
|
|
79
|
-
await client.quit();
|
|
80
|
-
await server.closeAsync();
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
});
|