snugkv-nest 0.1.0 → 0.3.0
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 +118 -2
- package/dist/core/bus.d.ts +4 -0
- package/dist/core/bus.js +7 -0
- package/dist/core/bus.js.map +1 -1
- package/dist/core/cache.d.ts +27 -0
- package/dist/core/cache.js +71 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/client.d.ts +115 -2
- package/dist/core/client.js +270 -1
- package/dist/core/client.js.map +1 -1
- package/dist/core/connection.d.ts +4 -0
- package/dist/core/connection.js +12 -2
- package/dist/core/connection.js.map +1 -1
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/lock.d.ts +38 -0
- package/dist/core/lock.js +68 -0
- package/dist/core/lock.js.map +1 -0
- package/dist/core/pubsub.d.ts +28 -0
- package/dist/core/pubsub.js +134 -0
- package/dist/core/pubsub.js.map +1 -0
- package/dist/core/queue/cron.d.ts +21 -0
- package/dist/core/queue/cron.js +144 -0
- package/dist/core/queue/cron.js.map +1 -0
- package/dist/core/queue/events.d.ts +32 -0
- package/dist/core/queue/events.js +98 -0
- package/dist/core/queue/events.js.map +1 -0
- package/dist/core/queue/flow.d.ts +35 -0
- package/dist/core/queue/flow.js +64 -0
- package/dist/core/queue/flow.js.map +1 -0
- package/dist/core/queue/index.d.ts +7 -0
- package/dist/core/queue/index.js +27 -0
- package/dist/core/queue/index.js.map +1 -0
- package/dist/core/queue/job.d.ts +68 -0
- package/dist/core/queue/job.js +221 -0
- package/dist/core/queue/job.js.map +1 -0
- package/dist/core/queue/queue.d.ts +152 -0
- package/dist/core/queue/queue.js +346 -0
- package/dist/core/queue/queue.js.map +1 -0
- package/dist/core/queue/types.d.ts +79 -0
- package/dist/core/queue/types.js +85 -0
- package/dist/core/queue/types.js.map +1 -0
- package/dist/core/queue/worker.d.ts +87 -0
- package/dist/core/queue/worker.js +307 -0
- package/dist/core/queue/worker.js.map +1 -0
- package/dist/core/ratelimit.d.ts +16 -0
- package/dist/core/ratelimit.js +30 -0
- package/dist/core/ratelimit.js.map +1 -0
- package/dist/core/tx.d.ts +24 -0
- package/dist/core/tx.js +52 -0
- package/dist/core/tx.js.map +1 -0
- package/dist/nest/explorer.d.ts +13 -0
- package/dist/nest/explorer.js +51 -0
- package/dist/nest/explorer.js.map +1 -1
- package/dist/nest/index.d.ts +3 -1
- package/dist/nest/index.js +4 -1
- package/dist/nest/index.js.map +1 -1
- package/dist/nest/module.d.ts +14 -1
- package/dist/nest/module.js +31 -4
- package/dist/nest/module.js.map +1 -1
- package/dist/nest/queue-decorators.d.ts +34 -0
- package/dist/nest/queue-decorators.js +56 -0
- package/dist/nest/queue-decorators.js.map +1 -0
- package/dist/nest/queue-module.d.ts +22 -0
- package/dist/nest/queue-module.js +41 -0
- package/dist/nest/queue-module.js.map +1 -0
- package/dist/nest/runtime.d.ts +1 -0
- package/dist/nest/runtime.js +15 -2
- package/dist/nest/runtime.js.map +1 -1
- package/package.json +49 -10
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# snugkv-nest
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The NestJS module and TypeScript client for [SnugKV](https://github.com/edijs1984/snugkv): strings, hashes, lists, sets, sorted sets, expiry, Lua, pub/sub and transactions, plus cache, lock and rate-limit helpers and a durable message bus on streams.
|
|
4
|
+
|
|
5
|
+
- [Using SnugKV as a data store](#using-snugkv-as-a-data-store)
|
|
6
|
+
- [Messaging (streams)](#messaging-streams)
|
|
4
7
|
|
|
5
8
|
- **Durable topics.** Messages are stream entries. They survive restarts and are replayable.
|
|
6
9
|
- **Consumer groups.** Instances of one service share the work; every different service gets every message.
|
|
@@ -18,6 +21,119 @@ npm install snugkv-nest
|
|
|
18
21
|
|
|
19
22
|
Needs Node 18+ and NestJS 10 or 11.
|
|
20
23
|
|
|
24
|
+
## Using SnugKV as a data store
|
|
25
|
+
|
|
26
|
+
Import the module once; everything below can be injected.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { Injectable } from '@nestjs/common';
|
|
30
|
+
import { SnugKvCache, SnugKvClient, SnugKvLocks, SnugKvModule, SnugKvRateLimiter, SnugKvSubscriber } from 'snugkv-nest';
|
|
31
|
+
|
|
32
|
+
// app.module.ts
|
|
33
|
+
@Module({ imports: [SnugKvModule.forRoot({ url: process.env.SNUGKV_URL, cache: { prefix: 'app:', ttlSeconds: 120 } })] })
|
|
34
|
+
export class AppModule {}
|
|
35
|
+
|
|
36
|
+
@Injectable()
|
|
37
|
+
export class UsersService {
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly kv: SnugKvClient,
|
|
40
|
+
private readonly cache: SnugKvCache,
|
|
41
|
+
private readonly locks: SnugKvLocks,
|
|
42
|
+
private readonly limiter: SnugKvRateLimiter,
|
|
43
|
+
private readonly pubsub: SnugKvSubscriber,
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
profile(id: string) {
|
|
47
|
+
return this.cache.wrap(`user:${id}`, () => this.loadFromDb(id)); // loader runs once, result cached 120 s
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async rebuild() {
|
|
51
|
+
const r = await this.locks.run('rebuild', () => this.doRebuild(), { ttlMs: 60_000 });
|
|
52
|
+
return r.acquired;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async allow(ip: string) {
|
|
56
|
+
return (await this.limiter.hit(ip, 100, 60_000)).allowed; // 100 per minute
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`service` is only needed for messaging; without it the module uses `app`. Outside Nest, use `new SnugKvClient({ url })` from `snugkv-nest/core`.
|
|
62
|
+
|
|
63
|
+
### Client
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
await kv.set('a', '1'); await kv.setOpts('a', '1', { nx: true, exSeconds: 30 });
|
|
67
|
+
await kv.incr('visits'); await kv.setJson('cfg', { a: 1 }); await kv.getJson('cfg');
|
|
68
|
+
await kv.hset('user:1', { name: 'Ann', age: 31 }); await kv.hgetall('user:1');
|
|
69
|
+
await kv.rpush('queue', 'a', 'b'); await kv.lrange('queue');
|
|
70
|
+
await kv.sadd('tags', 'x', 'y'); await kv.smembers('tags');
|
|
71
|
+
await kv.zadd('board', [{ member: 'ann', score: 10 }]); await kv.zrange('board', 0, 9, { reverse: true });
|
|
72
|
+
await kv.expire('a', 60); await kv.ttl('a');
|
|
73
|
+
for await (const key of kv.scanIterator({ match: 'user:*' })) { /* … */ }
|
|
74
|
+
await kv.eval("return redis.call('get', KEYS[1])", ['a']);
|
|
75
|
+
await kv.tx().set('a', '1').incr('n').hset('h', { f: 1 }).exec(); // MULTI/EXEC in one write
|
|
76
|
+
await kv.call('ANYTHING', 'else'); // raw command
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Pub/sub
|
|
80
|
+
|
|
81
|
+
Fire-and-forget broadcast. Use the streams below when a message must not be lost.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const off = await pubsub.subscribe('news', (message, channel) => console.log(channel, message));
|
|
85
|
+
await pubsub.psubscribe('events.*', (message, channel, pattern) => { /* … */ });
|
|
86
|
+
await kv.publish('news', 'hello');
|
|
87
|
+
await off();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Subscriptions are restored after a reconnect; messages sent while disconnected are not replayed.
|
|
91
|
+
|
|
92
|
+
### Cache, locks, rate limits
|
|
93
|
+
|
|
94
|
+
- `SnugKvCache`: `get`, `set`, `del`, `wrap(key, loader, ttl)` (concurrent callers share one load), `clear(pattern)`.
|
|
95
|
+
- `SnugKvLocks`: `acquire(name, { ttlMs, waitMs })`, `run(name, fn)`. A lock expires on its own, and only its holder can release or extend it. It protects against crashed holders, not against the server itself failing, so keep critical sections idempotent.
|
|
96
|
+
- `SnugKvRateLimiter`: `hit(key, limit, windowMs)` is a fixed window; it returns `{ allowed, count, remaining, resetMs }`.
|
|
97
|
+
|
|
98
|
+
## Job queues
|
|
99
|
+
|
|
100
|
+
BullMQ-style queues that run inside SnugKV (the `snugq` function library, loaded by the server by default). Needs a server version that includes it.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { Queue, Worker, QueueEvents, FlowProducer } from 'snugkv-nest';
|
|
104
|
+
|
|
105
|
+
const emails = new Queue('emails', { connection: { host: '127.0.0.1', port: 6379 } });
|
|
106
|
+
await emails.add('welcome', { to: 'a@b.c' }, { attempts: 3, backoff: { type: 'exponential', delay: 1000 }, removeOnComplete: 100 });
|
|
107
|
+
|
|
108
|
+
const worker = new Worker('emails', async (job) => {
|
|
109
|
+
await job.updateProgress(50);
|
|
110
|
+
return send(job.data);
|
|
111
|
+
}, { connection: { host: '127.0.0.1', port: 6379 }, concurrency: 5 });
|
|
112
|
+
worker.on('failed', (job, err) => console.error(job.id, err.message));
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
With Nest, register queues and mark processors; workers start and stop with the app:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
SnugKvQueueModule.registerQueue({ name: 'emails' })
|
|
119
|
+
|
|
120
|
+
@SnugProcessor('emails', { concurrency: 5 })
|
|
121
|
+
export class EmailProcessor extends WorkerHost {
|
|
122
|
+
async process(job: Job<{ to: string }>) { /* … */ }
|
|
123
|
+
|
|
124
|
+
@OnSnugWorkerEvent('failed')
|
|
125
|
+
onFailed(job: Job, err: Error) { /* … */ }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
constructor(@InjectSnugQueue('emails') private readonly emails: Queue) {}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Use `@SnugProcess('jobName')` on methods instead of `process()` to route by job name.
|
|
132
|
+
|
|
133
|
+
Supported: priorities, LIFO, delayed jobs, attempts with fixed, exponential or custom backoff (`UnrecoverableError` skips retries), locks with stalled-job recovery, pause/resume, global concurrency, queue rate limits (`limiter`, `queue.setRateLimit`, `worker.rateLimit` + `RateLimitError`), custom ids and deduplication, `removeOnComplete`/`removeOnFail`, progress and logs, `QueueEvents` and `job.waitUntilFinished`, `drain`/`clean`/`obliterate`/`retryJobs`/`promoteJobs`, repeatable jobs (`upsertJobScheduler` with `every` or a cron `pattern` and `tz`), and parent/child flows (`FlowProducer`, `job.getChildrenValues()`, `failParentOnFailure`, `ignoreDependencyOnFailure`). Job data must be JSON-serialisable. Cron patterns are computed by workers, so keep one running. See the server's `docs/QUEUES.md` for the storage layout.
|
|
134
|
+
|
|
135
|
+
## Messaging (streams)
|
|
136
|
+
|
|
21
137
|
## Quick start
|
|
22
138
|
|
|
23
139
|
```ts
|
|
@@ -188,4 +304,4 @@ Tests that need a server are skipped when `SNUGKV_BIN` is not set.
|
|
|
188
304
|
|
|
189
305
|
## License
|
|
190
306
|
|
|
191
|
-
MIT
|
|
307
|
+
MIT. This package is only a client. The SnugKV server it talks to is a separate project, licensed under AGPL-3.0 with a commercial licence option.
|
package/dist/core/bus.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SnugKvClient, SnugKvClientOptions } from './client';
|
|
2
2
|
import { MessageHandler, SubscribeOptions, ConsumerStats } from './consumer';
|
|
3
|
+
import { SnugKvSubscriber } from './pubsub';
|
|
3
4
|
import { PresenceOptions } from './presence';
|
|
4
5
|
import { BatchMessage, PublishOptions, Publisher, Serializer } from './publisher';
|
|
5
6
|
import { BusLogger } from './util';
|
|
@@ -39,6 +40,7 @@ export declare class SnugKvBus {
|
|
|
39
40
|
private readonly logger;
|
|
40
41
|
private readonly keyPrefix;
|
|
41
42
|
private closed;
|
|
43
|
+
private pubsub?;
|
|
42
44
|
constructor(options: SnugKvBusOptions);
|
|
43
45
|
/** Connects the main connection. Rejects if the server is not reachable in time. */
|
|
44
46
|
connect(): Promise<void>;
|
|
@@ -48,6 +50,8 @@ export declare class SnugKvBus {
|
|
|
48
50
|
}): Promise<string[]>;
|
|
49
51
|
/** Starts a consumer. Resolves when it is reading. */
|
|
50
52
|
subscribe<T = unknown>(options: SubscribeOptions, handler: MessageHandler<T>): Promise<Subscription>;
|
|
53
|
+
/** Shared pub/sub subscriber (own connection, opened on first use, closed with the bus). */
|
|
54
|
+
subscriber(): SnugKvSubscriber;
|
|
51
55
|
stats(): {
|
|
52
56
|
published: Record<string, number>;
|
|
53
57
|
consumers: ConsumerStats[];
|
package/dist/core/bus.js
CHANGED
|
@@ -20,6 +20,7 @@ class SnugKvBus {
|
|
|
20
20
|
logger;
|
|
21
21
|
keyPrefix;
|
|
22
22
|
closed = false;
|
|
23
|
+
pubsub;
|
|
23
24
|
constructor(options) {
|
|
24
25
|
this.options = options;
|
|
25
26
|
if (!options.service)
|
|
@@ -86,6 +87,11 @@ class SnugKvBus {
|
|
|
86
87
|
},
|
|
87
88
|
};
|
|
88
89
|
}
|
|
90
|
+
/** Shared pub/sub subscriber (own connection, opened on first use, closed with the bus). */
|
|
91
|
+
subscriber() {
|
|
92
|
+
this.pubsub ??= this.client.subscriber();
|
|
93
|
+
return this.pubsub;
|
|
94
|
+
}
|
|
89
95
|
stats() {
|
|
90
96
|
return { published: this.publisher.stats(), consumers: this.consumers.map((c) => c.stats()) };
|
|
91
97
|
}
|
|
@@ -96,6 +102,7 @@ class SnugKvBus {
|
|
|
96
102
|
this.closed = true;
|
|
97
103
|
await Promise.allSettled(this.consumers.splice(0).map((c) => c.stop()));
|
|
98
104
|
await this.presence?.stop();
|
|
105
|
+
this.pubsub?.close();
|
|
99
106
|
this.client.close();
|
|
100
107
|
}
|
|
101
108
|
}
|
package/dist/core/bus.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bus.js","sourceRoot":"","sources":["../../src/core/bus.ts"],"names":[],"mappings":";;;AAAA,qCAAmC;AACnC,qCAA6D;AAC7D,yCAAuF;
|
|
1
|
+
{"version":3,"file":"bus.js","sourceRoot":"","sources":["../../src/core/bus.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SnugKvClient } from './client';
|
|
2
|
+
export interface CacheOptions {
|
|
3
|
+
/** Prefix for every key. Default `cache:`. */
|
|
4
|
+
prefix?: string;
|
|
5
|
+
/** Default time to live in seconds. Default 60. */
|
|
6
|
+
ttlSeconds?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* JSON cache on top of strings with expiry. `wrap` runs the loader at most
|
|
10
|
+
* once at a time per key inside this process (concurrent callers share the
|
|
11
|
+
* result).
|
|
12
|
+
*/
|
|
13
|
+
export declare class SnugKvCache {
|
|
14
|
+
private readonly client;
|
|
15
|
+
private readonly prefix;
|
|
16
|
+
private readonly ttl;
|
|
17
|
+
private readonly inflight;
|
|
18
|
+
constructor(client: SnugKvClient, opts?: CacheOptions);
|
|
19
|
+
key(key: string): string;
|
|
20
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
21
|
+
set(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
|
|
22
|
+
del(...keys: string[]): Promise<number>;
|
|
23
|
+
/** Returns the cached value, or runs `load`, stores the result and returns it. `undefined` results are not cached. */
|
|
24
|
+
wrap<T>(key: string, load: () => Promise<T> | T, ttlSeconds?: number): Promise<T>;
|
|
25
|
+
/** Deletes every key under the cache prefix that matches `pattern` (default all). */
|
|
26
|
+
clear(pattern?: string): Promise<number>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SnugKvCache = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* JSON cache on top of strings with expiry. `wrap` runs the loader at most
|
|
6
|
+
* once at a time per key inside this process (concurrent callers share the
|
|
7
|
+
* result).
|
|
8
|
+
*/
|
|
9
|
+
class SnugKvCache {
|
|
10
|
+
client;
|
|
11
|
+
prefix;
|
|
12
|
+
ttl;
|
|
13
|
+
inflight = new Map();
|
|
14
|
+
constructor(client, opts = {}) {
|
|
15
|
+
this.client = client;
|
|
16
|
+
this.prefix = opts.prefix ?? 'cache:';
|
|
17
|
+
this.ttl = opts.ttlSeconds ?? 60;
|
|
18
|
+
}
|
|
19
|
+
key(key) {
|
|
20
|
+
return this.prefix + key;
|
|
21
|
+
}
|
|
22
|
+
async get(key) {
|
|
23
|
+
const raw = await this.client.get(this.key(key));
|
|
24
|
+
return raw === null ? undefined : JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
async set(key, value, ttlSeconds = this.ttl) {
|
|
27
|
+
await this.client.setJson(this.key(key), value, ttlSeconds);
|
|
28
|
+
}
|
|
29
|
+
async del(...keys) {
|
|
30
|
+
return keys.length ? this.client.del(...keys.map((k) => this.key(k))) : 0;
|
|
31
|
+
}
|
|
32
|
+
/** Returns the cached value, or runs `load`, stores the result and returns it. `undefined` results are not cached. */
|
|
33
|
+
async wrap(key, load, ttlSeconds = this.ttl) {
|
|
34
|
+
const hit = await this.get(key);
|
|
35
|
+
if (hit !== undefined)
|
|
36
|
+
return hit;
|
|
37
|
+
const running = this.inflight.get(key);
|
|
38
|
+
if (running)
|
|
39
|
+
return running;
|
|
40
|
+
const p = (async () => {
|
|
41
|
+
try {
|
|
42
|
+
const value = await load();
|
|
43
|
+
if (value !== undefined)
|
|
44
|
+
await this.set(key, value, ttlSeconds);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
this.inflight.delete(key);
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
this.inflight.set(key, p);
|
|
52
|
+
return p;
|
|
53
|
+
}
|
|
54
|
+
/** Deletes every key under the cache prefix that matches `pattern` (default all). */
|
|
55
|
+
async clear(pattern = '*') {
|
|
56
|
+
let n = 0;
|
|
57
|
+
let batch = [];
|
|
58
|
+
for await (const k of this.client.scanIterator({ match: this.prefix + pattern, count: 200 })) {
|
|
59
|
+
batch.push(k);
|
|
60
|
+
if (batch.length >= 200) {
|
|
61
|
+
n += await this.client.del(...batch);
|
|
62
|
+
batch = [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (batch.length)
|
|
66
|
+
n += await this.client.del(...batch);
|
|
67
|
+
return n;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.SnugKvCache = SnugKvCache;
|
|
71
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/core/cache.ts"],"names":[],"mappings":"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"}
|
package/dist/core/client.d.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import { ConnectionOptions } from './connection';
|
|
2
|
+
import { Connection, ConnectionOptions } from './connection';
|
|
3
3
|
import { Arg, Reply } from './resp';
|
|
4
|
+
import { SnugKvSubscriber } from './pubsub';
|
|
5
|
+
import { Transaction } from './tx';
|
|
4
6
|
export interface SnugKvClientOptions extends Partial<ConnectionOptions> {
|
|
5
7
|
/** `redis://user:pass@host:port`, `rediss://…`, `snugkv://…` or `snugkvs://…`. */
|
|
6
8
|
url?: string;
|
|
7
9
|
}
|
|
10
|
+
export interface SetOptions {
|
|
11
|
+
exSeconds?: number;
|
|
12
|
+
pxMs?: number;
|
|
13
|
+
/** Only set when the key does not exist. */
|
|
14
|
+
nx?: boolean;
|
|
15
|
+
/** Only set when the key already exists. */
|
|
16
|
+
xx?: boolean;
|
|
17
|
+
keepTtl?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function asArray(r: Reply): Reply[];
|
|
20
|
+
export declare function pairsToObject(flat: Reply[]): Record<string, string>;
|
|
8
21
|
export interface StreamEntry {
|
|
9
22
|
id: string;
|
|
10
23
|
fields: Record<string, string>;
|
|
@@ -36,7 +49,8 @@ export interface XReadGroupOptions {
|
|
|
36
49
|
}
|
|
37
50
|
export declare function toEntries(raw: Reply): StreamEntry[];
|
|
38
51
|
/**
|
|
39
|
-
* A
|
|
52
|
+
* A typed client for SnugKV: strings, hashes, lists, sets, sorted sets,
|
|
53
|
+
* expiry, scan, Lua, pub/sub, transactions and streams, plus a raw `call` for
|
|
40
54
|
* anything else. One client owns one connection; blocking reads belong on
|
|
41
55
|
* their own client (see {@link SnugKvClient.duplicate}).
|
|
42
56
|
*/
|
|
@@ -44,6 +58,8 @@ export declare class SnugKvClient extends EventEmitter {
|
|
|
44
58
|
private readonly conn;
|
|
45
59
|
private readonly options;
|
|
46
60
|
constructor(options?: SnugKvClientOptions);
|
|
61
|
+
/** @internal */
|
|
62
|
+
get connection(): Connection;
|
|
47
63
|
get isReady(): boolean;
|
|
48
64
|
connect(timeoutMs?: number): Promise<void>;
|
|
49
65
|
/** New client with its own connection and the same settings. */
|
|
@@ -59,6 +75,103 @@ export declare class SnugKvClient extends EventEmitter {
|
|
|
59
75
|
exSeconds?: number;
|
|
60
76
|
}): Promise<void>;
|
|
61
77
|
del(...keys: string[]): Promise<number>;
|
|
78
|
+
setOpts(key: string, value: string | number, opts?: SetOptions): Promise<boolean>;
|
|
79
|
+
mget(...keys: string[]): Promise<Array<string | null>>;
|
|
80
|
+
mset(values: Record<string, string | number>): Promise<void>;
|
|
81
|
+
incr(key: string, by?: number): Promise<number>;
|
|
82
|
+
decr(key: string, by?: number): Promise<number>;
|
|
83
|
+
incrByFloat(key: string, by: number): Promise<number>;
|
|
84
|
+
append(key: string, value: string): Promise<number>;
|
|
85
|
+
getDel(key: string): Promise<string | null>;
|
|
86
|
+
/** JSON convenience: stores `JSON.stringify(value)`; `ttlSeconds` optional. */
|
|
87
|
+
setJson(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
|
|
88
|
+
getJson<T = unknown>(key: string): Promise<T | null>;
|
|
89
|
+
exists(...keys: string[]): Promise<number>;
|
|
90
|
+
type(key: string): Promise<string>;
|
|
91
|
+
expire(key: string, seconds: number): Promise<boolean>;
|
|
92
|
+
pexpire(key: string, ms: number): Promise<boolean>;
|
|
93
|
+
persist(key: string): Promise<boolean>;
|
|
94
|
+
/** Seconds left; -1 = no expiry, -2 = no such key. */
|
|
95
|
+
ttl(key: string): Promise<number>;
|
|
96
|
+
pttl(key: string): Promise<number>;
|
|
97
|
+
rename(from: string, to: string): Promise<void>;
|
|
98
|
+
dbsize(): Promise<number>;
|
|
99
|
+
/** One SCAN page. */
|
|
100
|
+
scan(cursor: string, opts?: {
|
|
101
|
+
match?: string;
|
|
102
|
+
count?: number;
|
|
103
|
+
type?: string;
|
|
104
|
+
}): Promise<{
|
|
105
|
+
cursor: string;
|
|
106
|
+
keys: string[];
|
|
107
|
+
}>;
|
|
108
|
+
/** Iterates every matching key without loading them all at once. */
|
|
109
|
+
scanIterator(opts?: {
|
|
110
|
+
match?: string;
|
|
111
|
+
count?: number;
|
|
112
|
+
type?: string;
|
|
113
|
+
}): AsyncGenerator<string>;
|
|
114
|
+
/** Sets fields; returns how many were new. */
|
|
115
|
+
hset(key: string, fields: Record<string, string | number>): Promise<number>;
|
|
116
|
+
hget(key: string, field: string): Promise<string | null>;
|
|
117
|
+
hmget(key: string, ...fields: string[]): Promise<Array<string | null>>;
|
|
118
|
+
hgetall(key: string): Promise<Record<string, string>>;
|
|
119
|
+
hdel(key: string, ...fields: string[]): Promise<number>;
|
|
120
|
+
hexists(key: string, field: string): Promise<boolean>;
|
|
121
|
+
hlen(key: string): Promise<number>;
|
|
122
|
+
hkeys(key: string): Promise<string[]>;
|
|
123
|
+
hvals(key: string): Promise<string[]>;
|
|
124
|
+
hincrBy(key: string, field: string, by?: number): Promise<number>;
|
|
125
|
+
hincrByFloat(key: string, field: string, by: number): Promise<number>;
|
|
126
|
+
lpush(key: string, ...values: Array<string | number>): Promise<number>;
|
|
127
|
+
rpush(key: string, ...values: Array<string | number>): Promise<number>;
|
|
128
|
+
lpop(key: string): Promise<string | null>;
|
|
129
|
+
rpop(key: string): Promise<string | null>;
|
|
130
|
+
lrange(key: string, start?: number, stop?: number): Promise<string[]>;
|
|
131
|
+
llen(key: string): Promise<number>;
|
|
132
|
+
ltrim(key: string, start: number, stop: number): Promise<void>;
|
|
133
|
+
sadd(key: string, ...members: string[]): Promise<number>;
|
|
134
|
+
srem(key: string, ...members: string[]): Promise<number>;
|
|
135
|
+
smembers(key: string): Promise<string[]>;
|
|
136
|
+
sismember(key: string, member: string): Promise<boolean>;
|
|
137
|
+
scard(key: string): Promise<number>;
|
|
138
|
+
spop(key: string): Promise<string | null>;
|
|
139
|
+
/** Adds or updates members; returns how many were new. */
|
|
140
|
+
zadd(key: string, members: Array<{
|
|
141
|
+
member: string;
|
|
142
|
+
score: number;
|
|
143
|
+
}>): Promise<number>;
|
|
144
|
+
zincrBy(key: string, member: string, by?: number): Promise<number>;
|
|
145
|
+
zscore(key: string, member: string): Promise<number | null>;
|
|
146
|
+
zrank(key: string, member: string): Promise<number | null>;
|
|
147
|
+
zrem(key: string, ...members: string[]): Promise<number>;
|
|
148
|
+
zcard(key: string): Promise<number>;
|
|
149
|
+
/** Members by rank, lowest score first (or highest first with `reverse`). */
|
|
150
|
+
zrange(key: string, start?: number, stop?: number, opts?: {
|
|
151
|
+
reverse?: boolean;
|
|
152
|
+
}): Promise<Array<{
|
|
153
|
+
member: string;
|
|
154
|
+
score: number;
|
|
155
|
+
}>>;
|
|
156
|
+
/** Members with min <= score <= max. Use `-inf` / `+inf` or `(5` for exclusive bounds. */
|
|
157
|
+
zrangeByScore(key: string, min: number | string, max: number | string, opts?: {
|
|
158
|
+
offset?: number;
|
|
159
|
+
count?: number;
|
|
160
|
+
}): Promise<string[]>;
|
|
161
|
+
zpopMin(key: string): Promise<{
|
|
162
|
+
member: string;
|
|
163
|
+
score: number;
|
|
164
|
+
} | null>;
|
|
165
|
+
/** Runs a Lua script. */
|
|
166
|
+
eval(script: string, keys?: string[], args?: Array<string | number>): Promise<Reply>;
|
|
167
|
+
/** Returns the number of subscribers that received it. */
|
|
168
|
+
publish(channel: string, message: string): Promise<number>;
|
|
169
|
+
/** A subscriber with its own connection and the same settings. */
|
|
170
|
+
subscriber(overrides?: SnugKvClientOptions): SnugKvSubscriber;
|
|
171
|
+
/** Builds a MULTI/EXEC block that is sent in one write, so other callers cannot interleave. */
|
|
172
|
+
tx(): Transaction;
|
|
173
|
+
/** @internal */
|
|
174
|
+
execBlock(commands: Arg[][]): Promise<Reply[]>;
|
|
62
175
|
static xaddArgs(key: string, fields: Record<string, string>, opts?: XAddOptions): Arg[];
|
|
63
176
|
xadd(key: string, fields: Record<string, string>, opts?: XAddOptions): Promise<string>;
|
|
64
177
|
xlen(key: string): Promise<number>;
|