@naturalcycles/redis-lib 4.5.0 → 4.6.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.
@@ -53,8 +53,28 @@ export declare class RedisClient implements CommonClient {
53
53
  hsetWithTTL(_key: string, _value: AnyObject, _expireAt: UnixTimestamp): Promise<void>;
54
54
  mset(obj: Record<string, string | number>): Promise<void>;
55
55
  msetBuffer(obj: Record<string, Buffer>): Promise<void>;
56
+ /**
57
+ * For counters that are supposed to expire, use {@link incrWithTTL} instead.
58
+ */
56
59
  incr(key: string, by?: number): Promise<number>;
60
+ /**
61
+ * Increments the key and guarantees it has an expiry, in a single transaction.
62
+ *
63
+ * Returns the new value.
64
+ *
65
+ * Requires Redis 7.0+
66
+ */
67
+ incrWithTTL(key: string, expireAt: UnixTimestamp, by?: number): Promise<number>;
68
+ /**
69
+ * For counters that are supposed to expire, use {@link incrBatchWithTTL} instead.
70
+ */
57
71
  incrBatch(incrementTuples: [string, number][]): Promise<[string, number][]>;
72
+ /**
73
+ * Batch version of {@link incrWithTTL}, all increments and expiries in a single transaction.
74
+ *
75
+ * Requires Redis 7.0+
76
+ */
77
+ incrBatchWithTTL(incrementTuples: [string, number][], expireAt: UnixTimestamp): Promise<[string, number][]>;
58
78
  ttl(key: string): Promise<number>;
59
79
  dropTable(table: string): Promise<void>;
60
80
  clearAll(): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { _assert } from '@naturalcycles/js-lib/error';
1
2
  import { _stringMapEntries } from '@naturalcycles/js-lib/types';
2
3
  import { Pipeline } from '@naturalcycles/nodejs-lib/stream';
3
4
  /**
@@ -149,10 +150,37 @@ export class RedisClient {
149
150
  const redis = await this.redis();
150
151
  await redis.mset(obj);
151
152
  }
153
+ /**
154
+ * For counters that are supposed to expire, use {@link incrWithTTL} instead.
155
+ */
152
156
  async incr(key, by = 1) {
153
157
  const redis = await this.redis();
154
158
  return await redis.incrby(key, by);
155
159
  }
160
+ /**
161
+ * Increments the key and guarantees it has an expiry, in a single transaction.
162
+ *
163
+ * Returns the new value.
164
+ *
165
+ * Requires Redis 7.0+
166
+ */
167
+ async incrWithTTL(key, expireAt, by = 1) {
168
+ const redis = await this.redis();
169
+ const results = await redis.multi().incrby(key, by).expireat(key, expireAt, 'NX').exec();
170
+ const result = results?.[0];
171
+ _assert(result, `redis: incrWithTTL transaction returned no result, key: ${key}`);
172
+ // Redis does not roll back, so a failed EXPIREAT leaves the key incremented and without an
173
+ // expiry. It self-heals on the next successful call, as EXPIREAT NX applies to a TTL-less key.
174
+ for (const [err] of results) {
175
+ if (err)
176
+ throw err;
177
+ }
178
+ const [, value] = result;
179
+ return value;
180
+ }
181
+ /**
182
+ * For counters that are supposed to expire, use {@link incrBatchWithTTL} instead.
183
+ */
156
184
  async incrBatch(incrementTuples) {
157
185
  const results = {};
158
186
  await this.withPipeline(async (pipeline) => {
@@ -165,6 +193,31 @@ export class RedisClient {
165
193
  const validResults = _stringMapEntries(results).filter(([_, v]) => v !== undefined);
166
194
  return validResults;
167
195
  }
196
+ /**
197
+ * Batch version of {@link incrWithTTL}, all increments and expiries in a single transaction.
198
+ *
199
+ * Requires Redis 7.0+
200
+ */
201
+ async incrBatchWithTTL(incrementTuples, expireAt) {
202
+ const redis = await this.redis();
203
+ const multi = redis.multi();
204
+ for (const [key, increment] of incrementTuples) {
205
+ multi.incrby(key, increment);
206
+ multi.expireat(key, expireAt, 'NX');
207
+ }
208
+ // 2 commands are queued per key, so the increments sit at the even indexes
209
+ const results = await multi.exec();
210
+ const expectedLength = incrementTuples.length * 2;
211
+ _assert(results?.length === expectedLength, `redis: incrBatchWithTTL expected ${expectedLength} results, got ${results?.length}`);
212
+ for (const [err] of results) {
213
+ if (err)
214
+ throw err;
215
+ }
216
+ return incrementTuples.map(([key], i) => {
217
+ const [, newValue] = results[i * 2];
218
+ return [key, newValue];
219
+ });
220
+ }
168
221
  async ttl(key) {
169
222
  const redis = await this.redis();
170
223
  return await redis.ttl(key);
package/package.json CHANGED
@@ -9,9 +9,9 @@
9
9
  "tslib": "^2"
10
10
  },
11
11
  "devDependencies": {
12
+ "@naturalcycles/dev-lib": "0.0.0",
12
13
  "@types/node": "^26",
13
- "typescript": "^7",
14
- "@naturalcycles/dev-lib": "0.0.0"
14
+ "typescript": "^7"
15
15
  },
16
16
  "exports": {
17
17
  ".": "./dist/index.js"
@@ -38,7 +38,7 @@
38
38
  "engines": {
39
39
  "node": ">=24.10.0"
40
40
  },
41
- "version": "4.5.0",
41
+ "version": "4.6.0",
42
42
  "description": "Redis implementation of CommonKeyValueDB interface",
43
43
  "author": "Natural Cycles Team",
44
44
  "license": "MIT",
@@ -1,3 +1,4 @@
1
+ import { _assert } from '@naturalcycles/js-lib/error'
1
2
  import type { CommonLogger } from '@naturalcycles/js-lib/log'
2
3
  import type {
3
4
  AnyObject,
@@ -221,11 +222,40 @@ export class RedisClient implements CommonClient {
221
222
  await redis.mset(obj)
222
223
  }
223
224
 
225
+ /**
226
+ * For counters that are supposed to expire, use {@link incrWithTTL} instead.
227
+ */
224
228
  async incr(key: string, by = 1): Promise<number> {
225
229
  const redis = await this.redis()
226
230
  return await redis.incrby(key, by)
227
231
  }
228
232
 
233
+ /**
234
+ * Increments the key and guarantees it has an expiry, in a single transaction.
235
+ *
236
+ * Returns the new value.
237
+ *
238
+ * Requires Redis 7.0+
239
+ */
240
+ async incrWithTTL(key: string, expireAt: UnixTimestamp, by = 1): Promise<number> {
241
+ const redis = await this.redis()
242
+ const results = await redis.multi().incrby(key, by).expireat(key, expireAt, 'NX').exec()
243
+ const result = results?.[0]
244
+ _assert(result, `redis: incrWithTTL transaction returned no result, key: ${key}`)
245
+
246
+ // Redis does not roll back, so a failed EXPIREAT leaves the key incremented and without an
247
+ // expiry. It self-heals on the next successful call, as EXPIREAT NX applies to a TTL-less key.
248
+ for (const [err] of results) {
249
+ if (err) throw err
250
+ }
251
+
252
+ const [, value] = result
253
+ return value as number
254
+ }
255
+
256
+ /**
257
+ * For counters that are supposed to expire, use {@link incrBatchWithTTL} instead.
258
+ */
229
259
  async incrBatch(incrementTuples: [string, number][]): Promise<[string, number][]> {
230
260
  const results: StringMap<number | undefined> = {}
231
261
 
@@ -245,6 +275,41 @@ export class RedisClient implements CommonClient {
245
275
  return validResults
246
276
  }
247
277
 
278
+ /**
279
+ * Batch version of {@link incrWithTTL}, all increments and expiries in a single transaction.
280
+ *
281
+ * Requires Redis 7.0+
282
+ */
283
+ async incrBatchWithTTL(
284
+ incrementTuples: [string, number][],
285
+ expireAt: UnixTimestamp,
286
+ ): Promise<[string, number][]> {
287
+ const redis = await this.redis()
288
+ const multi = redis.multi()
289
+
290
+ for (const [key, increment] of incrementTuples) {
291
+ multi.incrby(key, increment)
292
+ multi.expireat(key, expireAt, 'NX')
293
+ }
294
+
295
+ // 2 commands are queued per key, so the increments sit at the even indexes
296
+ const results = await multi.exec()
297
+ const expectedLength = incrementTuples.length * 2
298
+ _assert(
299
+ results?.length === expectedLength,
300
+ `redis: incrBatchWithTTL expected ${expectedLength} results, got ${results?.length}`,
301
+ )
302
+
303
+ for (const [err] of results) {
304
+ if (err) throw err
305
+ }
306
+
307
+ return incrementTuples.map(([key], i) => {
308
+ const [, newValue] = results[i * 2]!
309
+ return [key, newValue as number]
310
+ })
311
+ }
312
+
248
313
  async ttl(key: string): Promise<number> {
249
314
  const redis = await this.redis()
250
315
  return await redis.ttl(key)