@iskra-bun/kv-kit 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/CHANGELOG.md +75 -0
- package/README.md +9 -4
- package/dist/index.d.ts +62 -5
- package/dist/index.js +368 -30
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
- package/src/adapters/memory.ts +111 -10
- package/src/adapters/redis.ts +270 -20
- package/src/manager.ts +153 -9
- package/src/ttl.ts +12 -0
- package/src/types.ts +30 -2
package/dist/index.js
CHANGED
|
@@ -1,67 +1,322 @@
|
|
|
1
|
+
// src/manager.ts
|
|
2
|
+
import { isProductionEnv } from "@iskra-bun/core";
|
|
3
|
+
|
|
4
|
+
// src/ttl.ts
|
|
5
|
+
function checkTtl(ttl) {
|
|
6
|
+
if (ttl === void 0 || ttl === null || ttl === 0) return void 0;
|
|
7
|
+
if (typeof ttl !== "number" || !Number.isFinite(ttl) || ttl < 0) {
|
|
8
|
+
throw new RangeError(`Invalid TTL ${String(ttl)}: expected a positive number of seconds`);
|
|
9
|
+
}
|
|
10
|
+
return ttl;
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
// src/adapters/memory.ts
|
|
14
|
+
var MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
2
15
|
var MemoryAdapter = class {
|
|
3
16
|
id = "memory";
|
|
4
17
|
store = /* @__PURE__ */ new Map();
|
|
18
|
+
sets = /* @__PURE__ */ new Map();
|
|
19
|
+
timers = /* @__PURE__ */ new Map();
|
|
5
20
|
connect() {
|
|
6
21
|
}
|
|
7
22
|
disconnect() {
|
|
23
|
+
for (const timer of this.timers.values()) {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
}
|
|
26
|
+
this.timers.clear();
|
|
8
27
|
this.store.clear();
|
|
28
|
+
this.sets.clear();
|
|
9
29
|
}
|
|
30
|
+
// Values are copied in and out, as Redis does: a stored object returned by
|
|
31
|
+
// reference let one request's mutation show up in every other one.
|
|
10
32
|
async get(key) {
|
|
11
|
-
return this.store.get(key);
|
|
33
|
+
return structuredClone(this.store.get(key));
|
|
12
34
|
}
|
|
13
35
|
async set(key, value, ttl) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
const seconds = checkTtl(ttl);
|
|
37
|
+
const copy = structuredClone(value);
|
|
38
|
+
this.clearTimer(key);
|
|
39
|
+
this.sets.delete(key);
|
|
40
|
+
this.store.set(key, copy);
|
|
41
|
+
if (seconds !== void 0) this.expireIn(key, seconds * 1e3);
|
|
42
|
+
}
|
|
43
|
+
/** Arms the expiry timer, in steps when the delay exceeds setTimeout's limit. */
|
|
44
|
+
expireIn(key, ms) {
|
|
45
|
+
const step = Math.min(ms, MAX_TIMEOUT_MS);
|
|
46
|
+
const timer = setTimeout(() => {
|
|
47
|
+
if (ms > step) {
|
|
48
|
+
this.expireIn(key, ms - step);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.store.delete(key);
|
|
52
|
+
this.sets.delete(key);
|
|
53
|
+
this.timers.delete(key);
|
|
54
|
+
}, step);
|
|
55
|
+
timer.unref?.();
|
|
56
|
+
this.timers.set(key, timer);
|
|
57
|
+
}
|
|
58
|
+
clearTimer(key) {
|
|
59
|
+
const timer = this.timers.get(key);
|
|
60
|
+
if (timer !== void 0) {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
this.timers.delete(key);
|
|
17
63
|
}
|
|
18
64
|
}
|
|
19
65
|
async del(key) {
|
|
66
|
+
this.clearTimer(key);
|
|
20
67
|
this.store.delete(key);
|
|
68
|
+
this.sets.delete(key);
|
|
21
69
|
}
|
|
22
70
|
async has(key) {
|
|
23
|
-
return this.store.has(key);
|
|
71
|
+
return this.store.has(key) || this.sets.has(key);
|
|
72
|
+
}
|
|
73
|
+
async clear(prefix = "") {
|
|
74
|
+
for (const key of [...this.store.keys(), ...this.sets.keys()]) {
|
|
75
|
+
if (key.startsWith(prefix)) await this.del(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async sadd(key, member, ttl) {
|
|
79
|
+
const seconds = checkTtl(ttl);
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const expiresAt = seconds === void 0 ? Infinity : now + seconds * 1e3;
|
|
82
|
+
let set = this.sets.get(key);
|
|
83
|
+
if (!set) {
|
|
84
|
+
this.store.delete(key);
|
|
85
|
+
set = { members: /* @__PURE__ */ new Map(), expiresAt: 0, pruneAt: 64 };
|
|
86
|
+
this.sets.set(key, set);
|
|
87
|
+
}
|
|
88
|
+
set.members.set(member, Math.max(set.members.get(member) ?? 0, expiresAt));
|
|
89
|
+
if (set.members.size >= set.pruneAt) {
|
|
90
|
+
for (const [name, at] of set.members) if (at <= now) set.members.delete(name);
|
|
91
|
+
set.pruneAt = Math.max(64, set.members.size * 2);
|
|
92
|
+
}
|
|
93
|
+
if (expiresAt > set.expiresAt) {
|
|
94
|
+
set.expiresAt = expiresAt;
|
|
95
|
+
this.clearTimer(key);
|
|
96
|
+
if (expiresAt !== Infinity) this.expireIn(key, expiresAt - now);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async sdrain(key) {
|
|
100
|
+
const set = this.sets.get(key);
|
|
101
|
+
if (!set) return [];
|
|
102
|
+
this.sets.delete(key);
|
|
103
|
+
this.clearTimer(key);
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
return [...set.members].filter(([, at]) => at > now).map(([name]) => name);
|
|
24
106
|
}
|
|
25
107
|
};
|
|
26
108
|
|
|
27
109
|
// src/adapters/redis.ts
|
|
28
110
|
import Redis from "ioredis";
|
|
111
|
+
function encode(value) {
|
|
112
|
+
if (value === void 0) return "null";
|
|
113
|
+
return JSON.stringify(value);
|
|
114
|
+
}
|
|
115
|
+
function decode(raw) {
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(raw);
|
|
118
|
+
return parsed === null ? void 0 : parsed;
|
|
119
|
+
} catch {
|
|
120
|
+
return raw;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function ttlArgs(ttl) {
|
|
124
|
+
return Number.isInteger(ttl) ? ["EX", ttl] : ["PX", Math.max(1, Math.round(ttl * 1e3))];
|
|
125
|
+
}
|
|
126
|
+
var escapeGlob = (prefix) => prefix.replace(/[*?[\]\\]/g, "\\$&");
|
|
127
|
+
var CLEAR_BATCH = 1e3;
|
|
128
|
+
var SADD_SCRIPT = `
|
|
129
|
+
if redis.replicate_commands then redis.replicate_commands() end
|
|
130
|
+
local forever = 9007199254740991
|
|
131
|
+
local t = redis.call('TIME')
|
|
132
|
+
local now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)
|
|
133
|
+
local expires = forever
|
|
134
|
+
if ARGV[2] ~= '' then expires = now + tonumber(ARGV[2]) end
|
|
135
|
+
local current = tonumber(redis.call('ZSCORE', KEYS[1], ARGV[1]) or '0')
|
|
136
|
+
if expires > current then redis.call('ZADD', KEYS[1], expires, ARGV[1]) end
|
|
137
|
+
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', '(' .. now)
|
|
138
|
+
local last = redis.call('ZRANGE', KEYS[1], -1, -1, 'WITHSCORES')
|
|
139
|
+
if tonumber(last[2]) >= forever then
|
|
140
|
+
redis.call('PERSIST', KEYS[1])
|
|
141
|
+
else
|
|
142
|
+
redis.call('PEXPIREAT', KEYS[1], last[2])
|
|
143
|
+
end
|
|
144
|
+
`;
|
|
145
|
+
var SDRAIN_SCRIPT = `
|
|
146
|
+
if redis.replicate_commands then redis.replicate_commands() end
|
|
147
|
+
local t = redis.call('TIME')
|
|
148
|
+
local now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)
|
|
149
|
+
local members = redis.call('ZRANGEBYSCORE', KEYS[1], '(' .. now, '+inf')
|
|
150
|
+
redis.call('DEL', KEYS[1])
|
|
151
|
+
return members
|
|
152
|
+
`;
|
|
153
|
+
function withoutCommandArgs(error) {
|
|
154
|
+
const command = error?.command;
|
|
155
|
+
if (command && typeof command === "object" && "args" in command) {
|
|
156
|
+
error.command = { name: command.name };
|
|
157
|
+
}
|
|
158
|
+
return error;
|
|
159
|
+
}
|
|
29
160
|
var RedisAdapter = class {
|
|
30
161
|
id = "redis";
|
|
31
162
|
client = null;
|
|
32
163
|
options;
|
|
33
|
-
|
|
164
|
+
hooks;
|
|
165
|
+
constructor(options, hooks = {}) {
|
|
34
166
|
this.options = options;
|
|
167
|
+
this.hooks = hooks;
|
|
35
168
|
}
|
|
36
|
-
|
|
37
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Connects and waits for Redis to answer: an unreachable server or a wrong
|
|
171
|
+
* password fails here (and so the app's start) instead of on the first
|
|
172
|
+
* command. Once connected, a dropped connection is retried by ioredis.
|
|
173
|
+
*/
|
|
174
|
+
async connect() {
|
|
175
|
+
let client;
|
|
176
|
+
if (typeof this.options === "string") {
|
|
177
|
+
client = new Redis(this.options, { lazyConnect: true });
|
|
178
|
+
} else if (this.options.url) {
|
|
179
|
+
const { url, ...rest } = this.options;
|
|
180
|
+
client = new Redis(url, { ...rest, lazyConnect: true });
|
|
181
|
+
} else {
|
|
182
|
+
client = new Redis({ ...this.options, lazyConnect: true });
|
|
183
|
+
}
|
|
184
|
+
client.on("error", (error) => this.hooks.onError?.(withoutCommandArgs(error)));
|
|
185
|
+
try {
|
|
186
|
+
await client.connect();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
client.disconnect();
|
|
189
|
+
throw new Error(`Could not connect to Redis: ${error instanceof Error ? error.message : String(error)}`, {
|
|
190
|
+
cause: withoutCommandArgs(error)
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
this.client = client;
|
|
38
194
|
}
|
|
39
|
-
|
|
40
|
-
|
|
195
|
+
/** Longest wait for QUIT before the connection is closed anyway. */
|
|
196
|
+
quitTimeoutMs = 2e3;
|
|
197
|
+
/**
|
|
198
|
+
* Closes the connection. When connected, QUIT lets pending replies arrive
|
|
199
|
+
* (in-flight writes are not dropped), for at most `quitTimeoutMs`. When the
|
|
200
|
+
* connection is down, ioredis would queue QUIT behind the offline queue and
|
|
201
|
+
* resolve only after its reconnect attempts run out (about 10 s by default,
|
|
202
|
+
* never with `maxRetriesPerRequest: null`), eating the app's shutdown
|
|
203
|
+
* timeout: the client is closed at once instead.
|
|
204
|
+
*/
|
|
205
|
+
async disconnect() {
|
|
206
|
+
const client = this.client;
|
|
207
|
+
this.client = null;
|
|
208
|
+
if (!client) return;
|
|
209
|
+
if (typeof client.quit !== "function" || client.status !== "ready") {
|
|
210
|
+
client.disconnect();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let timer;
|
|
214
|
+
const timedOut = new Promise((resolve) => {
|
|
215
|
+
timer = setTimeout(() => resolve("timeout"), this.quitTimeoutMs);
|
|
216
|
+
});
|
|
217
|
+
const outcome = await Promise.race([
|
|
218
|
+
client.quit().then(
|
|
219
|
+
() => "quit",
|
|
220
|
+
() => "error"
|
|
221
|
+
),
|
|
222
|
+
timedOut
|
|
223
|
+
]);
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
if (outcome !== "quit") client.disconnect();
|
|
226
|
+
}
|
|
227
|
+
/** The ioredis client once connected (null before connect() and after disconnect()). */
|
|
228
|
+
get nativeClient() {
|
|
229
|
+
return this.client;
|
|
230
|
+
}
|
|
231
|
+
/** The live client; operations before connect() fail instead of silently doing nothing. */
|
|
232
|
+
get redis() {
|
|
233
|
+
if (!this.client) throw new Error("RedisAdapter is not connected; call connect() first");
|
|
234
|
+
return this.client;
|
|
41
235
|
}
|
|
42
236
|
async get(key) {
|
|
43
|
-
const val = await this.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} catch {
|
|
47
|
-
return val;
|
|
48
|
-
}
|
|
237
|
+
const val = await this.redis.get(key);
|
|
238
|
+
if (val === null || val === void 0) return void 0;
|
|
239
|
+
return decode(val);
|
|
49
240
|
}
|
|
50
241
|
async set(key, value, ttl) {
|
|
51
|
-
const val =
|
|
52
|
-
|
|
53
|
-
|
|
242
|
+
const val = encode(value);
|
|
243
|
+
const seconds = checkTtl(ttl);
|
|
244
|
+
if (seconds !== void 0) {
|
|
245
|
+
await this.redis.set(key, val, ...ttlArgs(seconds));
|
|
54
246
|
} else {
|
|
55
|
-
await this.
|
|
247
|
+
await this.redis.set(key, val);
|
|
56
248
|
}
|
|
57
249
|
}
|
|
58
250
|
async del(key) {
|
|
59
|
-
await this.
|
|
251
|
+
await this.redis.del(key);
|
|
60
252
|
}
|
|
61
253
|
async has(key) {
|
|
62
|
-
const exists = await this.
|
|
254
|
+
const exists = await this.redis.exists(key);
|
|
63
255
|
return exists === 1;
|
|
64
256
|
}
|
|
257
|
+
// Native batch operations. A single MGET / pipelined MSET / variadic DEL
|
|
258
|
+
// replaces the manager's per-key fan-out (avoids the N+1 round-trips).
|
|
259
|
+
async mget(keys) {
|
|
260
|
+
if (keys.length === 0) return [];
|
|
261
|
+
const raws = await this.redis.mget(...keys) ?? [];
|
|
262
|
+
return keys.map((_, i) => {
|
|
263
|
+
const raw = raws[i];
|
|
264
|
+
return raw === null || raw === void 0 ? void 0 : decode(raw);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async mset(entries, ttl) {
|
|
268
|
+
const seconds = checkTtl(ttl);
|
|
269
|
+
if (entries.length === 0) return;
|
|
270
|
+
const pipeline = this.redis.pipeline();
|
|
271
|
+
for (const [key, value] of entries) {
|
|
272
|
+
const val = encode(value);
|
|
273
|
+
if (seconds !== void 0) {
|
|
274
|
+
pipeline.set(key, val, ...ttlArgs(seconds));
|
|
275
|
+
} else {
|
|
276
|
+
pipeline.set(key, val);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const results = await pipeline.exec();
|
|
280
|
+
const failed = results?.find(([err]) => err);
|
|
281
|
+
if (failed) throw failed[0];
|
|
282
|
+
}
|
|
283
|
+
async mdel(keys) {
|
|
284
|
+
if (keys.length === 0) return;
|
|
285
|
+
await this.redis.del(...keys);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Deletes the keys under `prefix` (after ioredis' own `keyPrefix`, if set)
|
|
289
|
+
* with SCAN and DEL. With no prefix at all this would be every key in the
|
|
290
|
+
* database, which only `flushDb: true` allows (as a FLUSHDB).
|
|
291
|
+
*/
|
|
292
|
+
async clear(prefix = "") {
|
|
293
|
+
const redis = this.redis;
|
|
294
|
+
const keyPrefix = redis.options.keyPrefix ?? "";
|
|
295
|
+
if (!keyPrefix && !prefix) {
|
|
296
|
+
if (!this.hooks.flushDb) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
"RedisAdapter.clear() without a key prefix would empty the whole Redis database: give the KVManager a namespace, or pass flushDb: true if the database belongs to this app alone"
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
await redis.flushdb();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const match = `${escapeGlob(keyPrefix + prefix)}*`;
|
|
305
|
+
let cursor = "0";
|
|
306
|
+
do {
|
|
307
|
+
const [next, keys] = await redis.scan(cursor, "MATCH", match, "COUNT", CLEAR_BATCH);
|
|
308
|
+
cursor = next;
|
|
309
|
+
if (keys.length > 0) await redis.del(...keys.map((key) => key.slice(keyPrefix.length)));
|
|
310
|
+
} while (cursor !== "0");
|
|
311
|
+
}
|
|
312
|
+
async sadd(key, member, ttl) {
|
|
313
|
+
const seconds = checkTtl(ttl);
|
|
314
|
+
const ms = seconds === void 0 ? "" : String(Math.max(1, Math.ceil(seconds * 1e3)));
|
|
315
|
+
await this.redis.eval(SADD_SCRIPT, 1, key, member, ms);
|
|
316
|
+
}
|
|
317
|
+
async sdrain(key) {
|
|
318
|
+
return await this.redis.eval(SDRAIN_SCRIPT, 1, key);
|
|
319
|
+
}
|
|
65
320
|
};
|
|
66
321
|
|
|
67
322
|
// src/manager.ts
|
|
@@ -70,18 +325,39 @@ var KVManager = class {
|
|
|
70
325
|
id = "manager";
|
|
71
326
|
app = null;
|
|
72
327
|
adapter;
|
|
73
|
-
|
|
328
|
+
prefix;
|
|
329
|
+
flushDb;
|
|
330
|
+
constructor(options = {}) {
|
|
331
|
+
const misplaced = ["adapter", "driver", "connection"].filter((key) => key in options);
|
|
332
|
+
if (misplaced.length > 0) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`KVManager does not take ${misplaced.map((k) => `"${k}"`).join(", ")}: choose the store in the App config, e.g. new App({ name, kv: { driver: 'redis', connection: process.env.REDIS_URL } })`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
74
337
|
this.adapter = new MemoryAdapter();
|
|
338
|
+
this.prefix = options.namespace ? `${options.namespace}:` : "";
|
|
339
|
+
this.flushDb = options.flushDb ?? false;
|
|
75
340
|
}
|
|
76
341
|
init(app) {
|
|
77
342
|
this.app = app;
|
|
343
|
+
app.context.set("kv", this);
|
|
78
344
|
const config = app.config.kv;
|
|
79
345
|
if (config?.driver === "redis") {
|
|
80
346
|
app.logger.info("Initializing KV with Redis");
|
|
81
|
-
this.adapter = new RedisAdapter(config.connection
|
|
82
|
-
|
|
347
|
+
this.adapter = new RedisAdapter(config.connection ?? {}, {
|
|
348
|
+
onError: (err) => app.logger.warn({ err }, "KV Redis connection error"),
|
|
349
|
+
flushDb: this.flushDb
|
|
350
|
+
});
|
|
351
|
+
} else if (!config?.driver || config.driver === "memory") {
|
|
352
|
+
if (!config?.driver && isProductionEnv()) {
|
|
353
|
+
app.logger.warn(
|
|
354
|
+
"KV has no driver configured: using the in-memory store, which each process keeps on its own and loses on restart. Set kv: { driver: 'redis', connection } in the App config, or kv: { driver: 'memory' } to keep it."
|
|
355
|
+
);
|
|
356
|
+
}
|
|
83
357
|
app.logger.info("Initializing KV with Memory");
|
|
84
358
|
this.adapter = new MemoryAdapter();
|
|
359
|
+
} else {
|
|
360
|
+
throw new Error(`Unsupported KV driver "${config.driver}" (supported: "memory", "redis")`);
|
|
85
361
|
}
|
|
86
362
|
}
|
|
87
363
|
async connect() {
|
|
@@ -98,20 +374,82 @@ var KVManager = class {
|
|
|
98
374
|
async stop() {
|
|
99
375
|
await this.disconnect();
|
|
100
376
|
}
|
|
101
|
-
|
|
377
|
+
/**
|
|
378
|
+
* The underlying ioredis client with the "redis" driver, once started: for
|
|
379
|
+
* commands the KV API does not cover (sets, sorted sets, pipelines). It
|
|
380
|
+
* bypasses `namespace` and the JSON codec. `undefined` with the memory
|
|
381
|
+
* driver or before start().
|
|
382
|
+
*/
|
|
383
|
+
get client() {
|
|
384
|
+
return this.adapter instanceof RedisAdapter ? this.adapter.nativeClient ?? void 0 : void 0;
|
|
385
|
+
}
|
|
386
|
+
prefixed(key) {
|
|
387
|
+
return `${this.prefix}${key}`;
|
|
388
|
+
}
|
|
389
|
+
// Proxy methods (namespace-aware)
|
|
102
390
|
get(key) {
|
|
103
|
-
return this.adapter.get(key);
|
|
391
|
+
return this.adapter.get(this.prefixed(key));
|
|
104
392
|
}
|
|
105
393
|
set(key, value, ttl) {
|
|
106
|
-
return this.adapter.set(key, value, ttl);
|
|
394
|
+
return this.adapter.set(this.prefixed(key), value, ttl);
|
|
107
395
|
}
|
|
108
396
|
del(key) {
|
|
109
|
-
return this.adapter.del(key);
|
|
397
|
+
return this.adapter.del(this.prefixed(key));
|
|
110
398
|
}
|
|
111
399
|
has(key) {
|
|
112
|
-
return this.adapter.has(key);
|
|
400
|
+
return this.adapter.has(this.prefixed(key));
|
|
401
|
+
}
|
|
402
|
+
// Batch operations. When the underlying adapter exposes a native batch
|
|
403
|
+
// method, the manager delegates to it (a single round-trip) after applying
|
|
404
|
+
// the namespace prefix; otherwise it falls back to a per-key loop. This
|
|
405
|
+
// keeps the optional adapter methods out of the hot path for adapters that
|
|
406
|
+
// do not implement them while avoiding the N+1 fan-out for those that do.
|
|
407
|
+
async mget(keys) {
|
|
408
|
+
const prefixed = keys.map((k) => this.prefixed(k));
|
|
409
|
+
if (this.adapter.mget) {
|
|
410
|
+
return this.adapter.mget(prefixed);
|
|
411
|
+
}
|
|
412
|
+
return Promise.all(prefixed.map((k) => this.adapter.get(k)));
|
|
413
|
+
}
|
|
414
|
+
async mset(entries, ttl) {
|
|
415
|
+
const pairs = Array.isArray(entries) ? entries : Object.entries(entries);
|
|
416
|
+
const prefixed = pairs.map(([k, v]) => [this.prefixed(k), v]);
|
|
417
|
+
if (this.adapter.mset) {
|
|
418
|
+
await this.adapter.mset(prefixed, ttl);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
await Promise.all(prefixed.map(([k, v]) => this.adapter.set(k, v, ttl)));
|
|
422
|
+
}
|
|
423
|
+
async mdel(keys) {
|
|
424
|
+
const prefixed = keys.map((k) => this.prefixed(k));
|
|
425
|
+
if (this.adapter.mdel) {
|
|
426
|
+
await this.adapter.mdel(prefixed);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
await Promise.all(prefixed.map((k) => this.adapter.del(k)));
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Deletes every key under `prefix` in this manager's namespace (the whole
|
|
433
|
+
* namespace without one). With the redis driver and no namespace, only
|
|
434
|
+
* `flushDb: true` lets it empty the database.
|
|
435
|
+
*/
|
|
436
|
+
async clear(prefix = "") {
|
|
437
|
+
if (!this.adapter.clear) throw unsupported(this.adapter, "clear");
|
|
438
|
+
await this.adapter.clear(this.prefixed(prefix));
|
|
439
|
+
}
|
|
440
|
+
/** See {@link KVAdapter.sadd}: the member is kept as is, only `key` is namespaced. */
|
|
441
|
+
async sadd(key, member, ttl) {
|
|
442
|
+
if (!this.adapter.sadd) throw unsupported(this.adapter, "sadd");
|
|
443
|
+
await this.adapter.sadd(this.prefixed(key), member, ttl);
|
|
444
|
+
}
|
|
445
|
+
async sdrain(key) {
|
|
446
|
+
if (!this.adapter.sdrain) throw unsupported(this.adapter, "sdrain");
|
|
447
|
+
return this.adapter.sdrain(this.prefixed(key));
|
|
113
448
|
}
|
|
114
449
|
};
|
|
450
|
+
function unsupported(adapter, method) {
|
|
451
|
+
return new Error(`The "${adapter.id}" KV adapter does not support ${method}()`);
|
|
452
|
+
}
|
|
115
453
|
export {
|
|
116
454
|
KVManager
|
|
117
455
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapters/memory.ts","../src/adapters/redis.ts","../src/manager.ts"],"sourcesContent":["import type { KVAdapter } from '../types';\n\nexport class MemoryAdapter implements KVAdapter {\n id = 'memory';\n private store = new Map<string, any>();\n\n connect() {\n // No-op\n }\n disconnect() {\n this.store.clear();\n }\n\n async get(key: string) {\n return this.store.get(key);\n }\n\n async set(key: string, value: any, ttl?: number) {\n this.store.set(key, value);\n if (ttl) {\n setTimeout(() => this.store.delete(key), ttl * 1000);\n }\n }\n\n async del(key: string) {\n this.store.delete(key);\n }\n\n async has(key: string) {\n return this.store.has(key);\n }\n}\n","import type { KVAdapter } from '../types';\nimport Redis from 'ioredis';\n\nexport class RedisAdapter implements KVAdapter {\n id = 'redis';\n private client: Redis | null = null;\n private options: any;\n\n constructor(options: any) {\n this.options = options;\n }\n\n connect() {\n this.client = new Redis(this.options);\n }\n\n disconnect() {\n this.client?.disconnect();\n }\n\n async get(key: string) {\n const val = await this.client?.get(key);\n try {\n return val ? JSON.parse(val) : null;\n } catch {\n return val;\n }\n }\n\n async set(key: string, value: any, ttl?: number) {\n const val = typeof value === 'object' ? JSON.stringify(value) : value;\n if (ttl) {\n await this.client?.set(key, val, 'EX', ttl);\n } else {\n await this.client?.set(key, val);\n }\n }\n\n async del(key: string) {\n await this.client?.del(key);\n }\n\n async has(key: string) {\n const exists = await this.client?.exists(key);\n return exists === 1;\n }\n}\n","import type { App, Driver } from '@iskra-bun/core';\nimport type { KVAdapter } from './types';\nimport { MemoryAdapter } from './adapters/memory';\nimport { RedisAdapter } from './adapters/redis';\n\nexport class KVManager implements Driver, KVAdapter {\n name = 'KVManager';\n id = 'manager';\n private app: App | null = null;\n private adapter: KVAdapter;\n\n constructor() {\n // Default to memory until configured\n this.adapter = new MemoryAdapter();\n }\n\n init(app: App) {\n this.app = app;\n const config = app.config.kv;\n\n if (config?.driver === 'redis') {\n app.logger.info('Initializing KV with Redis');\n this.adapter = new RedisAdapter(config.connection);\n } else {\n app.logger.info('Initializing KV with Memory');\n this.adapter = new MemoryAdapter();\n }\n }\n\n async connect() {\n await this.adapter.connect();\n this.app?.logger.info('KV Store connected');\n }\n\n async disconnect() {\n await this.adapter.disconnect();\n }\n\n // Driver Interface\n async start() {\n await this.connect();\n }\n\n async stop() {\n await this.disconnect();\n }\n\n // Proxy methods\n get(key: string) { return this.adapter.get(key); }\n set(key: string, value: any, ttl?: number) { return this.adapter.set(key, value, ttl); }\n del(key: string) { return this.adapter.del(key); }\n has(key: string) { return this.adapter.has(key); }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/manager.ts","../src/ttl.ts","../src/adapters/memory.ts","../src/adapters/redis.ts"],"sourcesContent":["import { isProductionEnv, type App, type Driver } from '@iskra-bun/core';\nimport type { Redis } from 'ioredis';\nimport type { KVAdapter } from './types';\nimport { MemoryAdapter } from './adapters/memory';\nimport { RedisAdapter } from './adapters/redis';\n\nexport interface KVManagerOptions {\n /**\n * Optional namespace prepended to every key as `\"<namespace>:<key>\"`.\n * Defaults to `\"\"` (no prefix) to preserve existing behavior.\n */\n namespace?: string;\n /**\n * Lets `clear()` without a `namespace` empty the whole Redis database\n * (FLUSHDB). Only for a database no other app or service writes to.\n */\n flushDb?: boolean;\n}\n\nexport class KVManager implements Driver, KVAdapter {\n name = 'KVManager';\n id = 'manager';\n private app: App | null = null;\n private adapter: KVAdapter;\n private readonly prefix: string;\n private readonly flushDb: boolean;\n\n constructor(options: KVManagerOptions = {}) {\n // The store is chosen by the App's config (`kv.driver`), not here: an\n // `adapter: 'redis'` copied from an old README was ignored, and the app\n // silently ran on per-process memory.\n const misplaced = ['adapter', 'driver', 'connection'].filter((key) => key in options);\n if (misplaced.length > 0) {\n throw new Error(\n `KVManager does not take ${misplaced.map((k) => `\"${k}\"`).join(', ')}: choose the store in the App config, ` +\n \"e.g. new App({ name, kv: { driver: 'redis', connection: process.env.REDIS_URL } })\",\n );\n }\n // Default to memory until configured\n this.adapter = new MemoryAdapter();\n this.prefix = options.namespace ? `${options.namespace}:` : '';\n this.flushDb = options.flushDb ?? false;\n }\n\n init(app: App) {\n this.app = app;\n // Like db-kit's 'db': services look the store up as app.context.get('kv').\n app.context.set('kv', this);\n const config = app.config.kv;\n\n if (config?.driver === 'redis') {\n app.logger.info('Initializing KV with Redis');\n this.adapter = new RedisAdapter(config.connection ?? {}, {\n onError: (err) => app.logger.warn({ err }, 'KV Redis connection error'),\n flushDb: this.flushDb,\n });\n } else if (!config?.driver || config.driver === 'memory') {\n if (!config?.driver && isProductionEnv()) {\n app.logger.warn(\n 'KV has no driver configured: using the in-memory store, which each process keeps on its own ' +\n \"and loses on restart. Set kv: { driver: 'redis', connection } in the App config, or \" +\n \"kv: { driver: 'memory' } to keep it.\",\n );\n }\n app.logger.info('Initializing KV with Memory');\n this.adapter = new MemoryAdapter();\n } else {\n // Unknown drivers (e.g. 'libsql') used to fall back to memory\n // silently, losing every value on restart.\n throw new Error(`Unsupported KV driver \"${config.driver}\" (supported: \"memory\", \"redis\")`);\n }\n }\n\n async connect() {\n await this.adapter.connect();\n this.app?.logger.info('KV Store connected');\n }\n\n async disconnect() {\n await this.adapter.disconnect();\n }\n\n // Driver Interface\n async start() {\n await this.connect();\n }\n\n async stop() {\n await this.disconnect();\n }\n\n /**\n * The underlying ioredis client with the \"redis\" driver, once started: for\n * commands the KV API does not cover (sets, sorted sets, pipelines). It\n * bypasses `namespace` and the JSON codec. `undefined` with the memory\n * driver or before start().\n */\n get client(): Redis | undefined {\n return this.adapter instanceof RedisAdapter ? (this.adapter.nativeClient ?? undefined) : undefined;\n }\n\n private prefixed(key: string): string {\n return `${this.prefix}${key}`;\n }\n\n // Proxy methods (namespace-aware)\n get<T = unknown>(key: string): Promise<T | undefined> {\n return this.adapter.get<T>(this.prefixed(key));\n }\n\n set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {\n return this.adapter.set<T>(this.prefixed(key), value, ttl);\n }\n\n del(key: string): Promise<void> {\n return this.adapter.del(this.prefixed(key));\n }\n\n has(key: string): Promise<boolean> {\n return this.adapter.has(this.prefixed(key));\n }\n\n // Batch operations. When the underlying adapter exposes a native batch\n // method, the manager delegates to it (a single round-trip) after applying\n // the namespace prefix; otherwise it falls back to a per-key loop. This\n // keeps the optional adapter methods out of the hot path for adapters that\n // do not implement them while avoiding the N+1 fan-out for those that do.\n\n async mget<T = unknown>(keys: string[]): Promise<(T | undefined)[]> {\n const prefixed = keys.map((k) => this.prefixed(k));\n\n if (this.adapter.mget) {\n return this.adapter.mget<T>(prefixed);\n }\n\n return Promise.all(prefixed.map((k) => this.adapter.get<T>(k)));\n }\n\n async mset<T = unknown>(entries: Array<[string, T]> | Record<string, T>, ttl?: number): Promise<void> {\n const pairs: Array<[string, T]> = Array.isArray(entries)\n ? entries\n : (Object.entries(entries) as Array<[string, T]>);\n\n const prefixed: Array<[string, T]> = pairs.map(([k, v]) => [this.prefixed(k), v] as [string, T]);\n\n if (this.adapter.mset) {\n await this.adapter.mset<T>(prefixed, ttl);\n return;\n }\n\n await Promise.all(prefixed.map(([k, v]) => this.adapter.set<T>(k, v, ttl)));\n }\n\n async mdel(keys: string[]): Promise<void> {\n const prefixed = keys.map((k) => this.prefixed(k));\n\n if (this.adapter.mdel) {\n await this.adapter.mdel(prefixed);\n return;\n }\n\n await Promise.all(prefixed.map((k) => this.adapter.del(k)));\n }\n\n /**\n * Deletes every key under `prefix` in this manager's namespace (the whole\n * namespace without one). With the redis driver and no namespace, only\n * `flushDb: true` lets it empty the database.\n */\n async clear(prefix = ''): Promise<void> {\n if (!this.adapter.clear) throw unsupported(this.adapter, 'clear');\n await this.adapter.clear(this.prefixed(prefix));\n }\n\n /** See {@link KVAdapter.sadd}: the member is kept as is, only `key` is namespaced. */\n async sadd(key: string, member: string, ttl?: number): Promise<void> {\n if (!this.adapter.sadd) throw unsupported(this.adapter, 'sadd');\n await this.adapter.sadd(this.prefixed(key), member, ttl);\n }\n\n async sdrain(key: string): Promise<string[]> {\n if (!this.adapter.sdrain) throw unsupported(this.adapter, 'sdrain');\n return this.adapter.sdrain(this.prefixed(key));\n }\n}\n\n/** Both built-in adapters have the optional methods; an injected one may not. */\nfunction unsupported(adapter: KVAdapter, method: string): Error {\n return new Error(`The \"${adapter.id}\" KV adapter does not support ${method}()`);\n}\n\n// `app.context.get('kv')` is the KVManager registered on the app.\ndeclare module '@iskra-bun/core' {\n interface AppContextRegistry {\n kv: KVManager;\n }\n}\n","/**\n * A TTL in seconds: `undefined`, `null` or `0` mean \"no expiry\". Anything else\n * must be a positive finite number: a negative TTL used to delete the key at\n * once with the memory adapter and fail with Redis.\n */\nexport function checkTtl(ttl: number | null | undefined): number | undefined {\n if (ttl === undefined || ttl === null || ttl === 0) return undefined;\n if (typeof ttl !== 'number' || !Number.isFinite(ttl) || ttl < 0) {\n throw new RangeError(`Invalid TTL ${String(ttl)}: expected a positive number of seconds`);\n }\n return ttl;\n}\n","import type { KVAdapter } from '../types';\nimport { checkTtl } from '../ttl';\n\n/** setTimeout's limit: a longer delay (a TTL over ~24.8 days) fires at once. */\nconst MAX_TIMEOUT_MS = 2 ** 31 - 1;\n\n/** An expiring set: each member's expiry (ms since the epoch, Infinity for none). */\ninterface ExpiringSet {\n members: Map<string, number>;\n expiresAt: number;\n /** Size at which expired members are next dropped. */\n pruneAt: number;\n}\n\nexport class MemoryAdapter implements KVAdapter {\n id = 'memory';\n private store = new Map<string, unknown>();\n private sets = new Map<string, ExpiringSet>();\n private timers = new Map<string, ReturnType<typeof setTimeout>>();\n\n connect() {\n // No-op\n }\n\n disconnect() {\n for (const timer of this.timers.values()) {\n clearTimeout(timer);\n }\n this.timers.clear();\n this.store.clear();\n this.sets.clear();\n }\n\n // Values are copied in and out, as Redis does: a stored object returned by\n // reference let one request's mutation show up in every other one.\n async get<T = unknown>(key: string): Promise<T | undefined> {\n return structuredClone(this.store.get(key)) as T | undefined;\n }\n\n async set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {\n const seconds = checkTtl(ttl);\n const copy = structuredClone(value);\n // Note: storing `null`/`undefined` is undefined behavior across adapters.\n // This in-memory adapter stores the value verbatim (so `get` returns it\n // as-is), whereas the RedisAdapter normalizes both to \"absent\" because\n // they have no faithful JSON round-trip. Callers should not depend on\n // either form being preserved.\n //\n // Clear any existing expiry timer for this key before setting a new one\n this.clearTimer(key);\n this.sets.delete(key);\n this.store.set(key, copy);\n\n if (seconds !== undefined) this.expireIn(key, seconds * 1000);\n }\n\n /** Arms the expiry timer, in steps when the delay exceeds setTimeout's limit. */\n private expireIn(key: string, ms: number) {\n const step = Math.min(ms, MAX_TIMEOUT_MS);\n const timer = setTimeout(() => {\n if (ms > step) {\n this.expireIn(key, ms - step);\n return;\n }\n this.store.delete(key);\n this.sets.delete(key);\n this.timers.delete(key);\n }, step);\n\n // Avoid keeping the process alive just for expiry timers\n timer.unref?.();\n\n this.timers.set(key, timer);\n }\n\n private clearTimer(key: string) {\n const timer = this.timers.get(key);\n if (timer !== undefined) {\n clearTimeout(timer);\n this.timers.delete(key);\n }\n }\n\n async del(key: string): Promise<void> {\n this.clearTimer(key);\n this.store.delete(key);\n this.sets.delete(key);\n }\n\n async has(key: string): Promise<boolean> {\n return this.store.has(key) || this.sets.has(key);\n }\n\n async clear(prefix = ''): Promise<void> {\n for (const key of [...this.store.keys(), ...this.sets.keys()]) {\n if (key.startsWith(prefix)) await this.del(key);\n }\n }\n\n async sadd(key: string, member: string, ttl?: number): Promise<void> {\n const seconds = checkTtl(ttl);\n const now = Date.now();\n const expiresAt = seconds === undefined ? Infinity : now + seconds * 1000;\n let set = this.sets.get(key);\n if (!set) {\n this.store.delete(key);\n set = { members: new Map(), expiresAt: 0, pruneAt: 64 };\n this.sets.set(key, set);\n }\n // A member keeps its latest expiry: dropped earlier, an entry written\n // with a longer TTL would survive an invalidation of its tag.\n set.members.set(member, Math.max(set.members.get(member) ?? 0, expiresAt));\n // Expired members go each time the set doubles: O(1) per add on average.\n if (set.members.size >= set.pruneAt) {\n for (const [name, at] of set.members) if (at <= now) set.members.delete(name);\n set.pruneAt = Math.max(64, set.members.size * 2);\n }\n if (expiresAt > set.expiresAt) {\n set.expiresAt = expiresAt;\n this.clearTimer(key);\n if (expiresAt !== Infinity) this.expireIn(key, expiresAt - now);\n }\n }\n\n async sdrain(key: string): Promise<string[]> {\n const set = this.sets.get(key);\n if (!set) return [];\n this.sets.delete(key);\n this.clearTimer(key);\n const now = Date.now();\n return [...set.members].filter(([, at]) => at > now).map(([name]) => name);\n }\n}\n","import type { KVAdapter } from '../types';\nimport { checkTtl } from '../ttl';\nimport Redis from 'ioredis';\nimport type { RedisOptions } from 'ioredis';\n\n/**\n * Single consistent codec shared by every write/read path.\n *\n * `undefined` is guarded explicitly: there is no JSON representation for it,\n * so it is stored as the JSON `null` literal and decoded back to `undefined`\n * on read. Every other value is `JSON.stringify`'d on write and `JSON.parse`'d\n * on read, so the string \"123\" round-trips as the string \"123\" (not the number\n * 123) and \"{}\" round-trips as the string \"{}\" (not an empty object).\n */\nfunction encode<T>(value: T): string {\n if (value === undefined) return 'null';\n return JSON.stringify(value);\n}\n\nfunction decode<T>(raw: string): T | undefined {\n try {\n const parsed = JSON.parse(raw) as T | null;\n return parsed === null ? undefined : (parsed as T);\n } catch {\n // Tolerate values written outside the adapter that are not valid JSON.\n return raw as unknown as T;\n }\n}\n\n/**\n * Connection settings: a `redis://` / `rediss://` URL, ioredis options, or\n * ioredis options with a `url` (the form used in app.config and the docs).\n */\nexport type RedisAdapterOptions = string | (RedisOptions & { url?: string });\n\nexport interface RedisAdapterHooks {\n /**\n * Receives the client's `error` events (a dropped connection, each failed\n * reconnect). Without it they are ignored; ioredis printed them to the\n * console, outside the app's logger.\n */\n onError?: (error: Error) => void;\n /**\n * Lets `clear()` with no key prefix at all run FLUSHDB, which empties the\n * whole Redis database, other apps' keys included.\n */\n flushDb?: boolean;\n}\n\n/** `EX` for whole seconds; `PX` for fractional TTLs, which Redis `EX` rejects. */\nfunction ttlArgs(ttl: number): ['EX', number] | ['PX', number] {\n return Number.isInteger(ttl) ? ['EX', ttl] : ['PX', Math.max(1, Math.round(ttl * 1000))];\n}\n\n/** `prefix` as a literal in a SCAN MATCH pattern. */\nconst escapeGlob = (prefix: string): string => prefix.replace(/[*?[\\]\\\\]/g, '\\\\$&');\n\n/** Keys deleted per DEL while clearing. */\nconst CLEAR_BATCH = 1000;\n\n/**\n * An expiring set is a sorted set scored by each member's expiry (Redis time,\n * in ms; FOREVER for none). Adding a member drops the expired ones and keeps\n * the key alive until its last member expires, in one atomic step.\n * KEYS[1]: the set; ARGV[1]: the member; ARGV[2]: its TTL in ms, '' for none.\n */\nconst SADD_SCRIPT = `\nif redis.replicate_commands then redis.replicate_commands() end\nlocal forever = 9007199254740991\nlocal t = redis.call('TIME')\nlocal now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)\nlocal expires = forever\nif ARGV[2] ~= '' then expires = now + tonumber(ARGV[2]) end\nlocal current = tonumber(redis.call('ZSCORE', KEYS[1], ARGV[1]) or '0')\nif expires > current then redis.call('ZADD', KEYS[1], expires, ARGV[1]) end\nredis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', '(' .. now)\nlocal last = redis.call('ZRANGE', KEYS[1], -1, -1, 'WITHSCORES')\nif tonumber(last[2]) >= forever then\n redis.call('PERSIST', KEYS[1])\nelse\n redis.call('PEXPIREAT', KEYS[1], last[2])\nend\n`;\n\n/**\n * Returns the unexpired members of an expiring set (see SADD_SCRIPT) and\n * deletes it, in one atomic step: members past their expiry are left out, as\n * the memory adapter does. KEYS[1]: the set.\n */\nconst SDRAIN_SCRIPT = `\nif redis.replicate_commands then redis.replicate_commands() end\nlocal t = redis.call('TIME')\nlocal now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)\nlocal members = redis.call('ZRANGEBYSCORE', KEYS[1], '(' .. now, '+inf')\nredis.call('DEL', KEYS[1])\nreturn members\n`;\n\n/**\n * ioredis errors carry the command they answer with its arguments: AUTH's\n * password on a refused login, which the app then logged. Only the command\n * name is kept.\n */\nfunction withoutCommandArgs<E>(error: E): E {\n const command = (error as { command?: { name?: unknown; args?: unknown } } | null)?.command;\n if (command && typeof command === 'object' && 'args' in command) {\n (error as { command: unknown }).command = { name: command.name };\n }\n return error;\n}\n\nexport class RedisAdapter implements KVAdapter {\n id = 'redis';\n private client: Redis | null = null;\n private readonly options: RedisAdapterOptions;\n private readonly hooks: RedisAdapterHooks;\n\n constructor(options: RedisAdapterOptions, hooks: RedisAdapterHooks = {}) {\n this.options = options;\n this.hooks = hooks;\n }\n\n /**\n * Connects and waits for Redis to answer: an unreachable server or a wrong\n * password fails here (and so the app's start) instead of on the first\n * command. Once connected, a dropped connection is retried by ioredis.\n */\n async connect() {\n // ioredis only parses a URL passed as its own argument: an object with\n // a `url` key was ignored and it silently connected to localhost:6379.\n let client: Redis;\n if (typeof this.options === 'string') {\n client = new Redis(this.options, { lazyConnect: true });\n } else if (this.options.url) {\n const { url, ...rest } = this.options;\n client = new Redis(url, { ...rest, lazyConnect: true });\n } else {\n client = new Redis({ ...this.options, lazyConnect: true });\n }\n client.on('error', (error: Error) => this.hooks.onError?.(withoutCommandArgs(error)));\n try {\n await client.connect();\n } catch (error) {\n client.disconnect();\n // The message, not the URL: it may carry the password.\n throw new Error(`Could not connect to Redis: ${error instanceof Error ? error.message : String(error)}`, {\n cause: withoutCommandArgs(error),\n });\n }\n this.client = client;\n }\n\n /** Longest wait for QUIT before the connection is closed anyway. */\n protected quitTimeoutMs = 2000;\n\n /**\n * Closes the connection. When connected, QUIT lets pending replies arrive\n * (in-flight writes are not dropped), for at most `quitTimeoutMs`. When the\n * connection is down, ioredis would queue QUIT behind the offline queue and\n * resolve only after its reconnect attempts run out (about 10 s by default,\n * never with `maxRetriesPerRequest: null`), eating the app's shutdown\n * timeout: the client is closed at once instead.\n */\n async disconnect() {\n const client = this.client;\n this.client = null;\n if (!client) return;\n if (typeof client.quit !== 'function' || client.status !== 'ready') {\n client.disconnect();\n return;\n }\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<'timeout'>((resolve) => {\n timer = setTimeout(() => resolve('timeout'), this.quitTimeoutMs);\n });\n const outcome = await Promise.race([\n client.quit().then(\n () => 'quit' as const,\n () => 'error' as const,\n ),\n timedOut,\n ]);\n clearTimeout(timer);\n if (outcome !== 'quit') client.disconnect();\n }\n\n /** The ioredis client once connected (null before connect() and after disconnect()). */\n get nativeClient(): Redis | null {\n return this.client;\n }\n\n /** The live client; operations before connect() fail instead of silently doing nothing. */\n private get redis(): Redis {\n if (!this.client) throw new Error('RedisAdapter is not connected; call connect() first');\n return this.client;\n }\n\n async get<T = unknown>(key: string): Promise<T | undefined> {\n const val = await this.redis.get(key);\n if (val === null || val === undefined) return undefined;\n return decode<T>(val);\n }\n\n async set<T = unknown>(key: string, value: T, ttl?: number): Promise<void> {\n const val = encode(value);\n const seconds = checkTtl(ttl);\n if (seconds !== undefined) {\n await this.redis.set(key, val, ...(ttlArgs(seconds) as ['EX', number]));\n } else {\n await this.redis.set(key, val);\n }\n }\n\n async del(key: string) {\n await this.redis.del(key);\n }\n\n async has(key: string) {\n const exists = await this.redis.exists(key);\n return exists === 1;\n }\n\n // Native batch operations. A single MGET / pipelined MSET / variadic DEL\n // replaces the manager's per-key fan-out (avoids the N+1 round-trips).\n\n async mget<T = unknown>(keys: string[]): Promise<(T | undefined)[]> {\n if (keys.length === 0) return [];\n const raws = (await this.redis.mget(...keys)) ?? [];\n return keys.map((_, i) => {\n const raw = raws[i];\n return raw === null || raw === undefined ? undefined : decode<T>(raw);\n });\n }\n\n async mset<T = unknown>(entries: Array<[string, T]>, ttl?: number): Promise<void> {\n const seconds = checkTtl(ttl);\n if (entries.length === 0) return;\n const pipeline = this.redis.pipeline();\n\n for (const [key, value] of entries) {\n const val = encode(value);\n if (seconds !== undefined) {\n pipeline.set(key, val, ...(ttlArgs(seconds) as ['EX', number]));\n } else {\n pipeline.set(key, val);\n }\n }\n\n // exec() resolves even when individual commands fail; surface them.\n const results = await pipeline.exec();\n const failed = results?.find(([err]) => err);\n if (failed) throw failed[0];\n }\n\n async mdel(keys: string[]): Promise<void> {\n if (keys.length === 0) return;\n await this.redis.del(...keys);\n }\n\n /**\n * Deletes the keys under `prefix` (after ioredis' own `keyPrefix`, if set)\n * with SCAN and DEL. With no prefix at all this would be every key in the\n * database, which only `flushDb: true` allows (as a FLUSHDB).\n */\n async clear(prefix = ''): Promise<void> {\n const redis = this.redis;\n const keyPrefix = redis.options.keyPrefix ?? '';\n if (!keyPrefix && !prefix) {\n if (!this.hooks.flushDb) {\n throw new Error(\n 'RedisAdapter.clear() without a key prefix would empty the whole Redis database: ' +\n 'give the KVManager a namespace, or pass flushDb: true if the database belongs to this app alone',\n );\n }\n await redis.flushdb();\n return;\n }\n const match = `${escapeGlob(keyPrefix + prefix)}*`;\n let cursor = '0';\n do {\n const [next, keys] = await redis.scan(cursor, 'MATCH', match, 'COUNT', CLEAR_BATCH);\n cursor = next;\n // SCAN returns whole keys, and ioredis prepends keyPrefix to DEL's.\n if (keys.length > 0) await redis.del(...keys.map((key) => key.slice(keyPrefix.length)));\n } while (cursor !== '0');\n }\n\n async sadd(key: string, member: string, ttl?: number): Promise<void> {\n const seconds = checkTtl(ttl);\n const ms = seconds === undefined ? '' : String(Math.max(1, Math.ceil(seconds * 1000)));\n await this.redis.eval(SADD_SCRIPT, 1, key, member, ms);\n }\n\n async sdrain(key: string): Promise<string[]> {\n return (await this.redis.eval(SDRAIN_SCRIPT, 1, key)) as string[];\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iskra-bun/kv-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Key-Value store de Iskra con adaptadores de Redis y memoria.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iskra",
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
},
|
|
21
21
|
"homepage": "https://github.com/fearful/iskra/tree/main/packages/kv-kit#readme",
|
|
22
22
|
"bugs": "https://github.com/fearful/iskra/issues",
|
|
23
|
+
"engines": {
|
|
24
|
+
"bun": ">=1.3.0"
|
|
25
|
+
},
|
|
23
26
|
"type": "module",
|
|
24
27
|
"main": "./dist/index.js",
|
|
25
28
|
"module": "./dist/index.js",
|
|
@@ -47,7 +50,7 @@
|
|
|
47
50
|
"build": "tsup --config ../../tsup.config.ts"
|
|
48
51
|
},
|
|
49
52
|
"dependencies": {
|
|
50
|
-
"@iskra-bun/core": "0.
|
|
53
|
+
"@iskra-bun/core": "^0.2.0",
|
|
51
54
|
"ioredis": "^5.4.1"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|