ts-server-lib 0.0.17

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/db/TSRedis.js ADDED
@@ -0,0 +1,602 @@
1
+ "use strict";
2
+ /**
3
+ * TSRedis – Single file for Redis: connection (standalone/Sentinel), events (pub/sub), operations.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TSRedis = void 0;
7
+ const events_1 = require("events");
8
+ const redis_1 = require("redis");
9
+ const TSRequest_1 = require("../utils/TSRequest");
10
+ // ═══════════════════════════════════════════════════════════════════
11
+ // Connection – module state
12
+ // ═══════════════════════════════════════════════════════════════════
13
+ const ERR_PREFIX = 'TSRedis:ERROR';
14
+ function errMsg(e) {
15
+ return e instanceof Error ? e.message : String(e);
16
+ }
17
+ let client = null;
18
+ let readClient = null;
19
+ /**
20
+ * Active cluster client. Set ONLY after `clusterClient.connect()` fully resolves —
21
+ * callers that check this are guaranteed to get a ready, slot-mapped client.
22
+ */
23
+ let clusterClient = null;
24
+ /** Dedup promise: concurrent calls to connectRedisImpl share one cluster-connect attempt. */
25
+ let _clusterConnectPromise = null;
26
+ const DEFAULT_CONFIG = {
27
+ connectTimeout: 5000,
28
+ socketTimeout: 0,
29
+ autoReconnect: true,
30
+ maxReconnectRetries: 10,
31
+ reconnectDelay: 1000,
32
+ disableOfflineQueue: false,
33
+ };
34
+ function parseRedisUrl(url, envPassword) {
35
+ const urlMatch = url.match(/^redis:\/\/:([^@]+)@(.+)$/);
36
+ if (urlMatch) {
37
+ return { url: `redis://${urlMatch[2]}`, password: urlMatch[1] };
38
+ }
39
+ return { url, password: envPassword };
40
+ }
41
+ function buildStandaloneClientOptions(cfg, redisUrl, password) {
42
+ const clientOptions = {
43
+ url: redisUrl,
44
+ socket: {
45
+ connectTimeout: cfg.connectTimeout,
46
+ socketTimeout: cfg.socketTimeout || undefined,
47
+ keepAlive: true,
48
+ keepAliveInitialDelay: 5000,
49
+ noDelay: true,
50
+ reconnectStrategy: cfg.autoReconnect
51
+ ? (retries) => {
52
+ if (retries > cfg.maxReconnectRetries) {
53
+ console.error(ERR_PREFIX, 'max reconnect attempts reached');
54
+ return new Error('Max reconnect attempts reached');
55
+ }
56
+ const jitter = Math.floor(Math.random() * 200);
57
+ const delay = Math.min(Math.pow(2, retries) * 50, cfg.reconnectDelay || 2000);
58
+ return delay + jitter;
59
+ }
60
+ : false,
61
+ },
62
+ disableOfflineQueue: cfg.disableOfflineQueue,
63
+ };
64
+ if (cfg.clientName)
65
+ clientOptions.name = cfg.clientName;
66
+ if (cfg.pingInterval)
67
+ clientOptions.pingInterval = cfg.pingInterval;
68
+ if (cfg.commandsQueueMaxLength)
69
+ clientOptions.commandsQueueMaxLength = cfg.commandsQueueMaxLength;
70
+ if (password)
71
+ clientOptions.password = password;
72
+ return clientOptions;
73
+ }
74
+ // ═══════════════════════════════════════════════════════════════════
75
+ // Connection – connect logic (standalone, Sentinel, read replicas)
76
+ // ═══════════════════════════════════════════════════════════════════
77
+ async function connectRedisSentinel(sentinel, cfg) {
78
+ const sentinelOptions = {
79
+ sentinels: sentinel.hosts,
80
+ name: sentinel.name,
81
+ socket: {
82
+ connectTimeout: cfg.connectTimeout,
83
+ reconnectStrategy: cfg.autoReconnect
84
+ ? (retries) => {
85
+ if (retries > cfg.maxReconnectRetries) {
86
+ console.error(ERR_PREFIX, 'Sentinel max reconnect attempts reached');
87
+ return new Error('Max reconnect attempts reached');
88
+ }
89
+ return cfg.reconnectDelay;
90
+ }
91
+ : false,
92
+ },
93
+ };
94
+ if (sentinel.password)
95
+ sentinelOptions.sentinelPassword = sentinel.password;
96
+ const urlMatch = cfg.url.match(/^redis:\/\/:([^@]+)@/);
97
+ if (urlMatch)
98
+ sentinelOptions.password = urlMatch[1];
99
+ else if (process.env.REDIS_PASSWORD)
100
+ sentinelOptions.password = process.env.REDIS_PASSWORD;
101
+ client = (0, redis_1.createClient)(sentinelOptions);
102
+ client.on('error', (err) => console.error(ERR_PREFIX, errMsg(err)));
103
+ await client.connect();
104
+ return client;
105
+ }
106
+ async function connectReadReplicas(replicaConfig, cfg) {
107
+ if (!replicaConfig.urls?.length)
108
+ return;
109
+ const { url, password } = parseRedisUrl(replicaConfig.urls[0]);
110
+ const options = {
111
+ url,
112
+ socket: {
113
+ connectTimeout: cfg.connectTimeout,
114
+ reconnectStrategy: cfg.autoReconnect
115
+ ? (retries) => {
116
+ if (retries > cfg.maxReconnectRetries)
117
+ return new Error('Max reconnect attempts');
118
+ return cfg.reconnectDelay;
119
+ }
120
+ : false,
121
+ },
122
+ readonly: true,
123
+ };
124
+ if (password)
125
+ options.password = password;
126
+ readClient = (0, redis_1.createClient)(options);
127
+ readClient.on('error', (err) => console.error(ERR_PREFIX, 'read replica', errMsg(err)));
128
+ await readClient.connect();
129
+ }
130
+ // ═══════════════════════════════════════════════════════════════════
131
+ // Connection – internal helpers (used by TSRedis static API)
132
+ // ═══════════════════════════════════════════════════════════════════
133
+ async function connectRedisCluster(clusterConfig, cfg) {
134
+ // Password resolution order: explicit clusterConfig.password → URL-embedded in cfg.url → env var.
135
+ let password = clusterConfig.password;
136
+ if (!password) {
137
+ const urlMatch = cfg.url.match(/^redis:\/\/:([^@]+)@/);
138
+ if (urlMatch)
139
+ password = urlMatch[1];
140
+ else if (process.env.REDIS_PASSWORD)
141
+ password = process.env.REDIS_PASSWORD;
142
+ }
143
+ const reconnectStrategy = cfg.autoReconnect
144
+ ? (retries) => {
145
+ if (retries > cfg.maxReconnectRetries) {
146
+ console.error(ERR_PREFIX, 'Cluster max reconnect attempts reached');
147
+ return new Error('Max reconnect attempts reached');
148
+ }
149
+ const jitter = Math.floor(Math.random() * 200);
150
+ const delay = Math.min(Math.pow(2, retries) * 50, cfg.reconnectDelay || 2000);
151
+ return delay + jitter;
152
+ }
153
+ : false;
154
+ // `nodeAddressMap` is passed through verbatim so callers (e.g. host-side validators) can
155
+ // rewrite cluster-discovered hostnames to client-reachable addresses without touching docker DNS.
156
+ // Build inline so node-redis's RedisClusterOptions generic stays satisfied (the type
157
+ // inference depends on the literal object shape — extracting to a typed variable widens it).
158
+ // Use a local variable: module-level `clusterClient` is set ONLY after connect() resolves
159
+ // so concurrent callers that check `clusterClient !== null` always get a ready client.
160
+ const cluster = (0, redis_1.createCluster)({
161
+ rootNodes: clusterConfig.nodes.map((n) => ({
162
+ socket: { host: n.host, port: n.port },
163
+ })),
164
+ useReplicas: clusterConfig.useReplicas ?? false,
165
+ defaults: {
166
+ socket: {
167
+ connectTimeout: cfg.connectTimeout,
168
+ keepAlive: true,
169
+ keepAliveInitialDelay: 5000,
170
+ noDelay: true,
171
+ reconnectStrategy,
172
+ },
173
+ ...(password ? { password } : {}),
174
+ },
175
+ ...(clusterConfig.nodeAddressMap !== undefined
176
+ ? { nodeAddressMap: clusterConfig.nodeAddressMap }
177
+ : {}),
178
+ });
179
+ cluster.on('error', (err) => console.error(ERR_PREFIX, 'cluster', errMsg(err)));
180
+ await cluster.connect();
181
+ clusterClient = cluster; // assign only after full connection + slot discovery
182
+ return clusterClient;
183
+ }
184
+ async function connectRedisImpl(urlOrConfig) {
185
+ if (client)
186
+ return client;
187
+ if (clusterClient) {
188
+ // Already connected in cluster mode. Be idempotent: callers like `configureRedisStrategy`
189
+ // re-invoke `connect()` defensively to ensure a client exists, and we shouldn't punish
190
+ // legitimate re-entry. The function signature promises `RedisClientType` but `RedisClusterType`
191
+ // is API-compatible for the basic command surface (set/get/publish/etc.) used by callers
192
+ // that don't need typed cluster access. Callers needing typed cluster operations should use
193
+ // `getCluster()` / `getActiveClient()`.
194
+ return clusterClient;
195
+ }
196
+ const config = typeof urlOrConfig === 'string' ? { url: urlOrConfig } : urlOrConfig;
197
+ const cfg = { ...DEFAULT_CONFIG, ...config };
198
+ if (config.cluster) {
199
+ // Dedup: concurrent callers share one connect attempt; clusterClient is only set after
200
+ // connect() fully resolves so nobody gets a not-yet-ready client.
201
+ if (!_clusterConnectPromise) {
202
+ _clusterConnectPromise = connectRedisCluster(config.cluster, cfg)
203
+ .finally(() => { _clusterConnectPromise = null; });
204
+ }
205
+ await _clusterConnectPromise;
206
+ return clusterClient;
207
+ }
208
+ if (config.sentinel) {
209
+ return connectRedisSentinel(config.sentinel, cfg);
210
+ }
211
+ const { url: redisUrl, password } = parseRedisUrl(cfg.url, process.env.REDIS_PASSWORD);
212
+ const clientOptions = buildStandaloneClientOptions(cfg, redisUrl, password);
213
+ client = (0, redis_1.createClient)(clientOptions);
214
+ client.on('error', (err) => console.error(ERR_PREFIX, errMsg(err)));
215
+ await client.connect();
216
+ if (config.readReplicas?.enabled && config.readReplicas.urls?.length) {
217
+ await connectReadReplicas(config.readReplicas, cfg);
218
+ }
219
+ return client;
220
+ }
221
+ function getRedisImpl() {
222
+ return client;
223
+ }
224
+ /**
225
+ /** Returns the active cluster client when `connectRedisImpl` was called with `config.cluster`, else null. */
226
+ function getRedisClusterImpl() {
227
+ return clusterClient;
228
+ }
229
+ /**
230
+ * Returns whichever client backs the active singleton: standalone, sentinel, or cluster.
231
+ * Use this when the code path is topology-agnostic (e.g. health checks, GET/SET on a single key).
232
+ * For multi-key ops you still need to know cluster mode to enforce hash-tag co-location.
233
+ */
234
+ function getActiveClientImpl() {
235
+ return clusterClient ?? client;
236
+ }
237
+ function getRedisForReadImpl() {
238
+ return readClient ?? client;
239
+ }
240
+ function hasReadReplicaImpl() {
241
+ return readClient !== null;
242
+ }
243
+ async function checkRedisHealthImpl() {
244
+ const active = clusterClient ?? client;
245
+ if (!active)
246
+ return { healthy: false, latencyMs: -1 };
247
+ const start = Date.now();
248
+ try {
249
+ // Both `RedisClientType.ping()` and `RedisClusterType.ping()` exist; cluster pings one node.
250
+ await active.ping();
251
+ return { healthy: true, latencyMs: Date.now() - start };
252
+ }
253
+ catch {
254
+ return { healthy: false, latencyMs: -1 };
255
+ }
256
+ }
257
+ // ═══════════════════════════════════════════════════════════════════
258
+ // Events – pub/sub (internal)
259
+ // ═══════════════════════════════════════════════════════════════════
260
+ async function publishImpl(channel, message) {
261
+ const active = clusterClient ?? client;
262
+ if (!active) {
263
+ console.error(ERR_PREFIX, 'not connected, message not published', channel);
264
+ return false;
265
+ }
266
+ try {
267
+ // Cluster pub/sub: classic PUBLISH broadcasts to every node (bandwidth amplification).
268
+ // For per-slot delivery use SPUBLISH/SSUBSCRIBE (Redis 7+, sharded pub/sub).
269
+ await active.publish(channel, message);
270
+ return true;
271
+ }
272
+ catch (error) {
273
+ console.error(ERR_PREFIX, 'publish failed', channel, errMsg(error));
274
+ return false;
275
+ }
276
+ }
277
+ async function subscribeImpl(channel, handler) {
278
+ const active = clusterClient ?? client;
279
+ if (!active)
280
+ throw new Error('Redis not connected');
281
+ if (clusterClient) {
282
+ // Cluster pub/sub: node-redis v4 cluster client manages its own internal subscriber
283
+ // pool per cluster node. Call subscribe() directly — no duplicate() needed.
284
+ // Classic PUBLISH broadcasts to all cluster nodes, so the subscriber is reachable
285
+ // regardless of which node it lands on.
286
+ await clusterClient.subscribe(channel, handler);
287
+ return async () => {
288
+ await clusterClient.unsubscribe(channel);
289
+ };
290
+ }
291
+ // Standalone / sentinel: dedicate a duplicate connection to avoid blocking the main client.
292
+ const subscriber = client.duplicate();
293
+ await subscriber.connect();
294
+ await subscriber.subscribe(channel, handler);
295
+ return async () => {
296
+ await subscriber.unsubscribe(channel);
297
+ await subscriber.quit();
298
+ };
299
+ }
300
+ // ═══════════════════════════════════════════════════════════════════
301
+ // Scan utilities (internal)
302
+ // ═══════════════════════════════════════════════════════════════════
303
+ /** Drain a single node's scanIterator into the generator, respecting maxKeys cap. */
304
+ async function* _yieldBatches(iterator, maxKeys, yielded) {
305
+ for await (const keysBatch of iterator) {
306
+ for (const key of keysBatch) {
307
+ if (maxKeys > 0 && yielded.count >= maxKeys)
308
+ return;
309
+ yield key;
310
+ yielded.count++;
311
+ }
312
+ }
313
+ }
314
+ async function* scanKeysIteratorImpl(options = {}) {
315
+ const { pattern = '*', maxKeys = 10000, batchSize = 100 } = options;
316
+ const yielded = { count: 0 };
317
+ if (clusterClient && !client) {
318
+ for (const master of clusterClient.masters) {
319
+ const nodeClient = master.client;
320
+ if (!nodeClient || !nodeClient.isReady)
321
+ continue;
322
+ try {
323
+ yield* _yieldBatches(nodeClient.scanIterator({ MATCH: pattern, COUNT: batchSize }), maxKeys, yielded);
324
+ }
325
+ catch {
326
+ // Shard unreachable — skip; L1 cleared, TTL handles L2 expiry.
327
+ }
328
+ if (maxKeys > 0 && yielded.count >= maxKeys)
329
+ return;
330
+ }
331
+ return;
332
+ }
333
+ // Standalone / sentinel: scanIterator yields string[] batches (one array per SCAN cursor step).
334
+ if (!client)
335
+ return;
336
+ yield* _yieldBatches(client.scanIterator({ MATCH: pattern, COUNT: batchSize }), maxKeys, yielded);
337
+ }
338
+ async function batchGetValuesImpl(keys) {
339
+ // Per-key GET so keys can span different cluster slots.
340
+ const redis = getActiveClientImpl();
341
+ if (!redis || keys.length === 0)
342
+ return {};
343
+ const values = await Promise.all(keys.map(k => redis.get(k)));
344
+ const result = {};
345
+ for (let i = 0; i < keys.length; i++)
346
+ result[keys[i]] = values[i] ?? null;
347
+ return result;
348
+ }
349
+ // ═══════════════════════════════════════════════════════════════════
350
+ // Lifecycle (internal)
351
+ // ═══════════════════════════════════════════════════════════════════
352
+ async function closeRedisImpl() {
353
+ if (readClient) {
354
+ try {
355
+ await readClient.quit();
356
+ readClient = null;
357
+ }
358
+ catch (error) {
359
+ console.error(ERR_PREFIX, 'close read replica', errMsg(error));
360
+ }
361
+ }
362
+ if (clusterClient) {
363
+ try {
364
+ await clusterClient.quit();
365
+ }
366
+ catch (error) {
367
+ console.error(ERR_PREFIX, 'close cluster', errMsg(error));
368
+ }
369
+ clusterClient = null;
370
+ }
371
+ if (client) {
372
+ await client.quit();
373
+ client = null;
374
+ }
375
+ }
376
+ function getRedisConnectionStatsImpl() {
377
+ if (clusterClient) {
378
+ // Cluster mode: replica routing is handled internally by node-redis when `useReplicas`
379
+ // is true; we surface `hasReadReplica = true` to keep observability uniform with the
380
+ // standalone+readClient story. Per-node connection counts would require introspecting
381
+ // the cluster's slot map and are intentionally left out of this lightweight snapshot.
382
+ return {
383
+ connected: true,
384
+ mode: 'cluster',
385
+ hasReadReplica: true,
386
+ master: { connected: true },
387
+ replica: { connected: true, count: 1 },
388
+ };
389
+ }
390
+ return {
391
+ connected: client !== null,
392
+ mode: 'standalone',
393
+ hasReadReplica: readClient !== null,
394
+ master: { connected: client !== null },
395
+ replica: { connected: readClient !== null, count: readClient ? 1 : 0 },
396
+ };
397
+ }
398
+ // ═══════════════════════════════════════════════════════════════════
399
+ // TSRedis – static API (connect, pub/sub, scan) + instance ops
400
+ // ═══════════════════════════════════════════════════════════════════
401
+ class TSRedis extends events_1.EventEmitter {
402
+ redis;
403
+ constructor(instance) {
404
+ super();
405
+ this.redis = instance;
406
+ }
407
+ /** Connect (standalone, Sentinel, Cluster, or with read replicas). Cluster is selected
408
+ * when the supplied `RedisConfig.cluster` is set; otherwise sentinel / standalone per
409
+ * the existing precedence rules. */
410
+ static connect = connectRedisImpl;
411
+ /** Standalone / sentinel client singleton. Returns null in cluster mode — use {@link getCluster}. */
412
+ static getClient = getRedisImpl;
413
+ /** Active cluster client. Returns null when not in cluster mode. */
414
+ static getCluster = getRedisClusterImpl;
415
+ /** Topology-agnostic accessor — returns the active client (standalone / sentinel / cluster). */
416
+ static getActiveClient = getActiveClientImpl;
417
+ static getClientForRead = getRedisForReadImpl;
418
+ static hasReadReplica = hasReadReplicaImpl;
419
+ static close = closeRedisImpl;
420
+ static checkHealth = checkRedisHealthImpl;
421
+ static getConnectionStats = getRedisConnectionStatsImpl;
422
+ static publish = publishImpl;
423
+ static subscribe = subscribeImpl;
424
+ static scanKeysIterator = scanKeysIteratorImpl;
425
+ static batchGetValues = batchGetValuesImpl;
426
+ async cachedRequest(options, defaultValue = {}, debug) {
427
+ if (!options.getData) {
428
+ options.getData = (response) => response;
429
+ }
430
+ const { url, key, ...requestOptions } = options;
431
+ const result = await this.redis.get(key);
432
+ if (result) {
433
+ return JSON.parse(result);
434
+ }
435
+ else {
436
+ const response = await TSRequest_1.TSRequest.json(url, { ...requestOptions, method: (options.method || 'post') }, debug);
437
+ const data = options.getData(response);
438
+ if (data) {
439
+ let value = this.pathToStore(options.pathValue || [], data);
440
+ value = options.format && typeof options.format === 'function'
441
+ ? options.format(value)
442
+ : value;
443
+ if (value) {
444
+ await this.redis.setEx(key, options.expire || 300, JSON.stringify(value));
445
+ return value;
446
+ }
447
+ else {
448
+ return defaultValue;
449
+ }
450
+ }
451
+ else {
452
+ console.error(response);
453
+ return defaultValue;
454
+ }
455
+ }
456
+ }
457
+ pathToStore(keys = [], object) {
458
+ let current = object;
459
+ let value = object;
460
+ for (const i in keys) {
461
+ if (Object.prototype.hasOwnProperty.call(keys, i) && current && typeof current === 'object' && current[keys[i]]) {
462
+ value = current[keys[i]];
463
+ current = value;
464
+ }
465
+ else {
466
+ value = undefined;
467
+ break;
468
+ }
469
+ }
470
+ return value;
471
+ }
472
+ cached = async (path, options) => {
473
+ const result = await this.redis.get(path);
474
+ if (result)
475
+ return result;
476
+ if (options?.value) {
477
+ await this.redis.setEx(path, options.expire || 3600, options.value);
478
+ return options.value;
479
+ }
480
+ };
481
+ load = async (entity) => {
482
+ const result = await this.redis.hGetAll(entity);
483
+ const mappedResult = [];
484
+ Object.keys(result || {}).map((k) => mappedResult.push(JSON.parse(result[k])));
485
+ return mappedResult;
486
+ };
487
+ get = async (key, cb) => {
488
+ const result = await this.redis.get(key);
489
+ return cb ? cb(result) : result;
490
+ };
491
+ set = async (key, data) => (await this.redis.set(key, String(data))) === 'OK';
492
+ hget = async (key, field) => {
493
+ const result = await this.redis.hGet(key, field);
494
+ return result ? JSON.parse(result) : null;
495
+ };
496
+ getM = async (entity, keys) => this.redis.hmGet(entity, keys);
497
+ hgetall = async (key, cb) => {
498
+ const result = await this.redis.hGetAll(key);
499
+ return cb ? cb(result) : result;
500
+ };
501
+ hset = async (key, field, data) => {
502
+ if (typeof data !== 'undefined' && data !== null && String(data).length > 0) {
503
+ return (await this.redis.hSet(key, field, JSON.stringify(data))) >= 0;
504
+ }
505
+ return false;
506
+ };
507
+ hdel = async (key, fields) => (await this.redis.hDel(key, fields)) > 0;
508
+ del = async (keys) => {
509
+ const ks = Array.isArray(keys) ? keys : [keys];
510
+ if (ks.length === 0)
511
+ return false;
512
+ // Per-key DEL: cluster mode rejects multi-key DEL across slots (CROSSSLOT).
513
+ const counts = await Promise.all(ks.map(k => this.redis.del(k)));
514
+ return counts.some(c => c > 0);
515
+ };
516
+ loadLists = async (pattern = '*') => {
517
+ const list = await this.scanr({ pattern });
518
+ const result = {};
519
+ if (list.length === 0)
520
+ return result;
521
+ // Per-key TYPE: cluster multi() across different slots causes CROSSSLOT.
522
+ const types = await Promise.all(list.map(k => this.redis.type(k).catch(() => 'none')));
523
+ const hashKeys = [];
524
+ const hashScopes = [];
525
+ for (let i = 0; i < list.length; i++) {
526
+ if (String(types[i]) === 'hash') {
527
+ hashKeys.push(list[i]);
528
+ hashScopes.push(list[i].split(':').pop());
529
+ }
530
+ }
531
+ if (hashKeys.length === 0)
532
+ return result;
533
+ // Per-key hGetAll: cluster multi() across slots causes CROSSSLOT.
534
+ const hashResults = await Promise.all(hashKeys.map(k => this.redis.hGetAll(k).catch(() => ({}))));
535
+ for (let j = 0; j < hashScopes.length; j++) {
536
+ const val = hashResults[j];
537
+ result[hashScopes[j]] = (val != null && typeof val === 'object') ? val : {};
538
+ }
539
+ return result;
540
+ };
541
+ hscanr = async (entity, { pattern, COUNT = 200 }, cb) => {
542
+ const MATCH = this.match(pattern ?? '*');
543
+ const results = {};
544
+ let cursor = '0';
545
+ let entries;
546
+ do {
547
+ ({ cursor, entries } = await this.redis.hScan(entity, cursor, { COUNT, MATCH }));
548
+ for (let i = 0; i < entries.length; i++) {
549
+ if (this.check(entries[i].value, pattern ?? '*')) {
550
+ if (cb)
551
+ cb(JSON.parse(entries[i].value), entries[i].field);
552
+ else
553
+ results[entries[i].field] = JSON.parse(entries[i].value);
554
+ }
555
+ }
556
+ } while (cursor !== '0');
557
+ return results;
558
+ };
559
+ zscanr = async (entity, { pattern, COUNT = 200 }, cb = (value, score) => ({ value, score })) => {
560
+ const MATCH = this.match(pattern ?? '*');
561
+ const results = [];
562
+ let cursor = '0';
563
+ let members;
564
+ do {
565
+ ({ cursor, members } = await this.redis.zScan(entity, cursor, { COUNT, MATCH }));
566
+ for (let i = 0; i < members.length; i++) {
567
+ if (this.check(members[i].value, pattern ?? '*')) {
568
+ const data = cb(members[i].value, String(members[i].score));
569
+ if (typeof data !== 'undefined')
570
+ results.push(data);
571
+ }
572
+ }
573
+ } while (cursor !== '0');
574
+ return results;
575
+ };
576
+ scanr = async ({ pattern, COUNT = 200 }, cb) => {
577
+ // Use the cluster-aware scanKeysIterator so all shards are covered.
578
+ const results = [];
579
+ for await (const key of scanKeysIteratorImpl({ pattern: this.match(pattern ?? '*'), maxKeys: COUNT * 500 })) {
580
+ if (!this.check(key, pattern ?? '*'))
581
+ continue;
582
+ if (cb)
583
+ cb([key]);
584
+ else
585
+ results.push(key);
586
+ }
587
+ return results;
588
+ };
589
+ static _regexCache = new Map();
590
+ check = (value, pattern) => {
591
+ if (typeof pattern !== 'object' || !pattern.text)
592
+ return true;
593
+ let re = TSRedis._regexCache.get(pattern.text);
594
+ if (!re) {
595
+ re = new RegExp(pattern.text, 'i');
596
+ TSRedis._regexCache.set(pattern.text, re);
597
+ }
598
+ return value.search(re) > -1;
599
+ };
600
+ match = (pattern) => (typeof pattern === 'string' ? pattern : (pattern.match ?? '*'));
601
+ }
602
+ exports.TSRedis = TSRedis;
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "ts-server-lib",
3
+ "version": "0.0.17",
4
+ "scripts": {
5
+ "prepublishOnly": "npm run build",
6
+ "postpublish": "npm run clean:artifacts",
7
+ "build": "tsc --declaration -p .",
8
+ "watch": "tsc -w --declaration -p .",
9
+ "clean:artifacts": "node -e \"const fs=require('fs'),path=require('path');const root=process.cwd();for(const rel of ['db','ussd','ussd/providers','utils','ws']){const dir=path.join(root,rel);let n;try{n=fs.readdirSync(dir)}catch{continue}for(const name of n){if(!name.endsWith('.d.ts')&&path.extname(name)!=='.js')continue;const fp=path.join(dir,name);try{if(fs.statSync(fp).isFile())fs.unlinkSync(fp)}catch{}}}\"",
10
+ "clean:deps": "node -e \"for(const p of ['coverage','node_modules','package-lock.json'])try{require('fs').rmSync(p,{recursive:true,force:true})}catch{}\"",
11
+ "clean": "npm run clean:artifacts && npm run clean:deps",
12
+ "lint": "eslint . --fix --ignore-pattern \"**/*.d.ts\"",
13
+ "test": "npm run build && vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest run --coverage"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/onalbi/ts-server-lib"
20
+ },
21
+ "author": {
22
+ "name": "Albion Liçi",
23
+ "email": "lici.albion@gmail.com"
24
+ },
25
+ "keywords": [
26
+ "typescript",
27
+ "server"
28
+ ],
29
+ "license": "MIT",
30
+ "bugs": {
31
+ "url": "https://github.com/onalbi/ts-server-lib/issues"
32
+ },
33
+ "files": [
34
+ "db/**/*.{js,d.ts}",
35
+ "utils/**/*.{js,d.ts}",
36
+ "ussd/**/*.{js,d.ts}",
37
+ "utils/mime.json"
38
+ ],
39
+ "dependencies": {
40
+ "cron": "^4.4.0",
41
+ "fast-xml-parser": "^5.8.0",
42
+ "i18n": "^0.15.3",
43
+ "mongodb": "^7.2.0",
44
+ "redis": "^6.0.0",
45
+ "ts-common-lib": "^0.0.6"
46
+ },
47
+ "engines": {
48
+ "node": ">=24.0.0"
49
+ },
50
+ "vitest": {
51
+ "globals": true,
52
+ "environment": "node",
53
+ "include": [
54
+ "test/**/*.spec.ts"
55
+ ],
56
+ "exclude": [
57
+ "**/*.spec.js",
58
+ "**/node_modules/**"
59
+ ],
60
+ "resolve": {
61
+ "extensions": [
62
+ ".ts",
63
+ ".tsx",
64
+ ".mts",
65
+ ".cts",
66
+ ".js",
67
+ ".mjs",
68
+ ".jsx",
69
+ ".json"
70
+ ]
71
+ },
72
+ "coverage": {
73
+ "provider": "v8",
74
+ "include": [
75
+ "db/**/*.ts",
76
+ "ussd/**/*.ts",
77
+ "utils/**/*.ts"
78
+ ],
79
+ "reporter": [
80
+ "text",
81
+ "lcov"
82
+ ]
83
+ }
84
+ }
85
+ }