@warlock.js/cache 5.30.0 → 5.31.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +20 -8
  2. package/README.md +12 -21
  3. package/cjs/index.cjs +46 -27
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/cache-manager.d.mts +3 -1
  6. package/esm/cache-manager.d.mts.map +1 -1
  7. package/esm/cache-manager.mjs +2 -2
  8. package/esm/cache-manager.mjs.map +1 -1
  9. package/esm/drivers/base-cache-driver.d.mts +3 -1
  10. package/esm/drivers/base-cache-driver.d.mts.map +1 -1
  11. package/esm/drivers/base-cache-driver.mjs +4 -3
  12. package/esm/drivers/base-cache-driver.mjs.map +1 -1
  13. package/esm/drivers/file-cache-driver.d.mts.map +1 -1
  14. package/esm/drivers/file-cache-driver.mjs.map +1 -1
  15. package/esm/drivers/lru-memory-cache-driver.d.mts +3 -1
  16. package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
  17. package/esm/drivers/lru-memory-cache-driver.mjs +6 -4
  18. package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
  19. package/esm/drivers/memory-cache-driver.d.mts +3 -1
  20. package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
  21. package/esm/drivers/memory-cache-driver.mjs +9 -3
  22. package/esm/drivers/memory-cache-driver.mjs.map +1 -1
  23. package/esm/drivers/mock-cache-driver.d.mts +3 -1
  24. package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
  25. package/esm/drivers/mock-cache-driver.mjs +11 -4
  26. package/esm/drivers/mock-cache-driver.mjs.map +1 -1
  27. package/esm/drivers/pg-cache-driver.d.mts +3 -1
  28. package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
  29. package/esm/drivers/pg-cache-driver.mjs +6 -3
  30. package/esm/drivers/pg-cache-driver.mjs.map +1 -1
  31. package/esm/drivers/redis-cache-driver.d.mts +4 -2
  32. package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
  33. package/esm/drivers/redis-cache-driver.mjs +4 -3
  34. package/esm/drivers/redis-cache-driver.mjs.map +1 -1
  35. package/esm/list/memory-cache-list.mjs.map +1 -1
  36. package/esm/metrics.d.mts.map +1 -1
  37. package/esm/metrics.mjs.map +1 -1
  38. package/esm/scoped-cache.d.mts +3 -1
  39. package/esm/scoped-cache.d.mts.map +1 -1
  40. package/esm/scoped-cache.mjs +2 -2
  41. package/esm/scoped-cache.mjs.map +1 -1
  42. package/esm/tagged-cache.d.mts +3 -1
  43. package/esm/tagged-cache.d.mts.map +1 -1
  44. package/esm/tagged-cache.mjs +2 -2
  45. package/esm/tagged-cache.mjs.map +1 -1
  46. package/esm/tagged-scoped-cache.d.mts +3 -1
  47. package/esm/tagged-scoped-cache.d.mts.map +1 -1
  48. package/esm/tagged-scoped-cache.mjs +2 -2
  49. package/esm/tagged-scoped-cache.mjs.map +1 -1
  50. package/esm/types.d.mts +16 -5
  51. package/esm/types.d.mts.map +1 -1
  52. package/esm/types.mjs.map +1 -1
  53. package/esm/utils.d.mts.map +1 -1
  54. package/esm/utils.mjs.map +1 -1
  55. package/llms-full.txt +229 -224
  56. package/llms.txt +1 -1
  57. package/package.json +3 -3
  58. package/skills/apply-cache-patterns/SKILL.md +3 -2
  59. package/skills/cache-basics/SKILL.md +24 -24
  60. package/skills/configure-pg-cache/SKILL.md +2 -2
  61. package/skills/configure-set-options/SKILL.md +14 -14
  62. package/skills/handle-cache-errors/SKILL.md +8 -8
  63. package/skills/index.md +4 -1
  64. package/skills/observe-cache/SKILL.md +8 -8
  65. package/skills/pick-cache-driver/SKILL.md +24 -23
  66. package/skills/test-cache-code/SKILL.md +15 -10
  67. package/skills/use-cache-atomic/SKILL.md +17 -11
  68. package/skills/use-cache-bulk/SKILL.md +8 -5
  69. package/skills/use-cache-list/SKILL.md +13 -13
  70. package/skills/use-cache-lock/SKILL.md +11 -17
  71. package/skills/use-cache-namespace/SKILL.md +11 -13
  72. package/skills/use-cache-similarity/SKILL.md +9 -9
  73. package/skills/use-cache-tags/SKILL.md +13 -13
  74. package/skills/use-cache-update-merge/SKILL.md +10 -10
  75. package/skills/use-cache-utils/SKILL.md +17 -17
  76. package/skills/use-cached-hof/SKILL.md +8 -7
  77. package/skills/use-swr/SKILL.md +14 -18
package/CHANGELOG.md CHANGED
@@ -4,11 +4,21 @@ All notable changes to `@warlock.js/cache` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 5.31.0 - 2026-10-18
8
+
9
+ ### Added
10
+
11
+ - Real Redis integration coverage for atomic counters and their expiry behavior, including concurrent requests, preserved deadlines and counters recreated after a seeded key expires. Local runs opt in with `WARLOCK_REDIS_INTEGRATION=1`; CI requires a live Redis service. Tests use UUID prefixes and clean only their own keys.
12
+
13
+ ### Fixed
14
+
15
+ - `cache.increment(key, amount, { ttl })` supplies a TTL when an atomic increment creates or recreates a counter, through direct, tagged and scoped calls. Existing expiry deadlines are preserved; an explicit TTL also repairs a live permanent counter across drivers. Redis applies the count and TTL together, so an expired seed cannot leave a permanent counter even when the driver has no default TTL.
16
+
7
17
  ## 5.30.0 - 2026-10-16
8
18
 
9
19
  ### Changed
10
20
 
11
- - Email addresses inside logged cache keys are masked (`user.member@example.com` logs as `u***@example.com`), in the base driver's operation log, in error lines (written even with logging off: SWR refresh failures, lock release, clone failures), in the "Cannot increment non-numeric value" error, and in the null driver (FW-04).
21
+ - Email addresses inside logged cache keys are masked (`user.member@example.com` logs as `u***@example.com`), in the base driver's operation log, in error lines (written even with logging off: SWR refresh failures, lock release, clone failures), in the "Cannot increment non-numeric value" error, and in the null driver.
12
22
 
13
23
  ### Fixed
14
24
 
@@ -87,6 +97,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
87
97
  ### Changed
88
98
 
89
99
  - Lockstep release maintenance and dependency refresh.
100
+
90
101
  ## 5.20.0 - 2026-09-24
91
102
 
92
103
  ### Breaking
@@ -130,14 +141,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
130
141
 
131
142
  - **Atomic `increment`/`decrement`, `pull` and `update`**, with a per-driver guarantee:
132
143
 
133
- | Driver | `increment` / `decrement` | `pull` | `update` |
134
- | --- | --- | --- | --- |
135
- | `redis` | cross-server (`INCRBY`, keeps TTL) | cross-server (`GETDEL`, Lua fallback) | cross-server (Lua compare-and-set, bounded retries, `KEEPTTL`) |
136
- | `pg` | cross-server (one upsert, keeps `expires_at`) | cross-server (`DELETE … RETURNING`) | cross-server (compare-and-set, bounded retries) |
137
- | `memory` / `memoryExtended` / `lru` / `mock` | in-process, synchronous | in-process, synchronous | serialized per key, in-process only |
138
- | `file` | serialized per key, in-process only | serialized per key, in-process only | not supported (throws) |
144
+ | Driver | `increment` / `decrement` | `pull` | `update` |
145
+ | -------------------------------------------- | --------------------------------------------- | ------------------------------------- | -------------------------------------------------------------- |
146
+ | `redis` | cross-server (`INCRBY`, keeps TTL) | cross-server (`GETDEL`, Lua fallback) | cross-server (Lua compare-and-set, bounded retries, `KEEPTTL`) |
147
+ | `pg` | cross-server (one upsert, keeps `expires_at`) | cross-server (`DELETE … RETURNING`) | cross-server (compare-and-set, bounded retries) |
148
+ | `memory` / `memoryExtended` / `lru` / `mock` | in-process, synchronous | in-process, synchronous | serialized per key, in-process only |
149
+ | `file` | serialized per key, in-process only | serialized per key, in-process only | not supported (throws) |
139
150
 
140
151
  `update()` callbacks on Redis and Postgres may run more than once under contention, so keep them side-effect free. To coordinate more than one server, use `redis` or `pg`; `memory` is per-process.
152
+
141
153
  - **Tag index primitives** on the driver contract: `tagAdd`, `tagMembers`, `tagRemove`. Redis uses a native SET inside the prefix (a legacy JSON index is upgraded on first touch), Postgres a one-statement JSON-array merge on the index row, and the in-memory drivers a separate store that eviction and expiry never touch. `file` and `null` keep the serialized fallback. `invalidate()` deletes exactly the members it read, writes prune up to 20 dead members, `remove()`/`pull()` detach the key, and tagged `increment`/`pull` use the atomic ops.
142
154
  - **`PgCacheDriver.prune(limit = 1000)`** deletes expired rows in batches and returns the count. `set()` also runs it in the background on about 1 in 200 writes, so unique-key workloads no longer grow the table forever.
143
155
  - **`lock()` successor-safe release** (see Fixed). There is no renewal, so the TTL must exceed the worst-case duration of the locked work.
@@ -242,7 +254,7 @@ changelogs.
242
254
  - Removed the raw `console.log(value)` dump of the full cached payload when `structuredClone` fails in `parseCachedData` — cached values (potentially PII/tokens) no longer leak to stdout; the structured error log with the value's type is kept.
243
255
  - **Credential leak via error logging (Medium):** `logError()` and the Redis driver's `connect()` failure path printed the raw `Error` object straight to stdout (`console.log`) or to `log.fatal`, which could include the connection URL — and password — that some Redis/Node client errors echo back in `error.message`/`cause` on connection failure. Both call sites now go through a new `safeErrorInfo()` helper that logs only a redacted `{ message, code }` shape (never the raw error object), with any `scheme://user:pass@` credentials in the message masked to `scheme://[REDACTED]@`. The bare `console.log(error)`/`console.log("Err", error)` calls are gone entirely.
244
256
  - **Redis `removeNamespace` blocking `KEYS` scan (Medium):** replaced the blocking `KEYS` command with a non-blocking `SCAN` cursor loop (`client.scanIterator`), so clearing a namespace on a large keyspace no longer stalls the single-threaded Redis event loop for every other tenant/consumer. The existing glob-escaping fix (above) is unchanged.
245
- - **File driver `removeNamespace` dotted-key gap (Medium):** dotted keys (`ns.a`) are stored as *sibling* directories under the cache root (see the path-traversal fix above), so removing namespace `ns` — which only ever deleted a directory literally named `ns` — silently left every `ns.*` key on disk. `removeNamespace` now lists the cache root's immediate children, decodes each back to its logical key, and removes every directory whose logical key equals the namespace or starts with `<namespace>.`, matching the boundary semantics the `pg` driver already used for the same contract. Honors `globalPrefix` (previously ignored, so a global flush could wipe the whole cache root instead of scoping to the tenant) and preserves the existing path-containment guard.
257
+ - **File driver `removeNamespace` dotted-key gap (Medium):** dotted keys (`ns.a`) are stored as _sibling_ directories under the cache root (see the path-traversal fix above), so removing namespace `ns` — which only ever deleted a directory literally named `ns` — silently left every `ns.*` key on disk. `removeNamespace` now lists the cache root's immediate children, decodes each back to its logical key, and removes every directory whose logical key equals the namespace or starts with `<namespace>.`, matching the boundary semantics the `pg` driver already used for the same contract. Honors `globalPrefix` (previously ignored, so a global flush could wipe the whole cache root instead of scoping to the tenant) and preserves the existing path-containment guard.
246
258
 
247
259
  ### Dependencies
248
260
 
package/README.md CHANGED
@@ -33,11 +33,7 @@ Or
33
33
  First step, we need to define the cache configurations, in your configuration directory create a file to define the cache configurations.
34
34
 
35
35
  ```ts
36
- import {
37
- cache,
38
- type CacheConfigurations,
39
- MemoryCacheDriver,
40
- } from "@mongez/warlock.cache";
36
+ import { cache, type CacheConfigurations, MemoryCacheDriver } from "@mongez/warlock.cache";
41
37
 
42
38
  const configurations: CacheConfigurations = {
43
39
  drivers: {
@@ -102,11 +98,11 @@ await cache.set("report", report, "7d");
102
98
 
103
99
  // 3. Rich options object
104
100
  await cache.set("user:1", user, {
105
- ttl: "1h", // or number of seconds — mutually exclusive with expiresAt
106
- expiresAt: new Date("..."), // absolute deadline (Date or epoch ms)
107
- tags: ["users", "active"], // inline tags
108
- onConflict: "create", // "create" | "update" | "upsert" (default)
109
- driver: "redis", // route this single call to a non-default driver
101
+ ttl: "1h", // or number of seconds — mutually exclusive with expiresAt
102
+ expiresAt: new Date("..."), // absolute deadline (Date or epoch ms)
103
+ tags: ["users", "active"], // inline tags
104
+ onConflict: "create", // "create" | "update" | "upsert" (default)
105
+ driver: "redis", // route this single call to a non-default driver
110
106
  });
111
107
  ```
112
108
 
@@ -117,8 +113,8 @@ cache.setCacheConfigurations({
117
113
  default: "redis",
118
114
  drivers: { redis: RedisCacheDriver, memory: MemoryCacheDriver },
119
115
  options: {
120
- redis: { url: "...", ttl: "7d" }, // string OK
121
- memory: { ttl: 3600 }, // number OK
116
+ redis: { url: "...", ttl: "7d" }, // string OK
117
+ memory: { ttl: 3600 }, // number OK
122
118
  },
123
119
  });
124
120
  ```
@@ -142,7 +138,7 @@ Redis users who know the shorthand: `"create"` maps to `NX`, `"update"` maps to
142
138
  ```ts
143
139
  // Read → transform → write under a per-key in-process lock.
144
140
  await cache.update<User>("user:1", (current) => {
145
- if (!current) return null; // returning null removes the entry
141
+ if (!current) return null; // returning null removes the entry
146
142
  return { ...current, lastSeen: Date.now() };
147
143
  });
148
144
 
@@ -166,7 +162,7 @@ const head = await recent.shift();
166
162
  const firstTen = await recent.slice(0, 10);
167
163
  const count = await recent.length();
168
164
 
169
- await recent.trim(0, 99); // keep only indices 0–99
165
+ await recent.trim(0, 99); // keep only indices 0–99
170
166
  await recent.clear();
171
167
  ```
172
168
 
@@ -231,11 +227,7 @@ It could be useful when working with multi tenant applications, we can use the t
231
227
  Every driver's options have by default `globalPrefix` async function, this function is called each time an operation is performed, and it returns a string, this string is used as a prefix for the key, let's add it to our memory driver.
232
228
 
233
229
  ```ts
234
- import {
235
- cache,
236
- type CacheConfigurations,
237
- MemoryCacheDriver,
238
- } from "@mongez/warlock.cache";
230
+ import { cache, type CacheConfigurations, MemoryCacheDriver } from "@mongez/warlock.cache";
239
231
 
240
232
  const configurations: CacheConfigurations = {
241
233
  drivers: {
@@ -416,10 +408,9 @@ async function main() {
416
408
  }
417
409
  ```
418
410
 
419
-
420
411
  ### LRU Cache Driver
421
412
 
422
- [LRU Cache](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)) is a cache driver that caches the data in the memory with LRU algorithm.
413
+ [LRU Cache](<https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)>) is a cache driver that caches the data in the memory with LRU algorithm.
423
414
 
424
415
  So how this works? The LRU cache driver will store the data in the memory, and when the memory is full, it will remove the least recently used item.
425
416
 
package/cjs/index.cjs CHANGED
@@ -666,8 +666,8 @@ var TaggedScopedCache = class {
666
666
  * Atomic counter increment on the scoped key. Tags aren't applied to
667
667
  * subsequent increments — they're attached at first-write time.
668
668
  */
669
- increment(key, value) {
670
- return this.scope.increment(key, value);
669
+ increment(key, value, options) {
670
+ return this.scope.increment(key, value, options);
671
671
  }
672
672
  /**
673
673
  * Atomic counter decrement on the scoped key. See {@link increment}.
@@ -860,8 +860,8 @@ var ScopedCache = class ScopedCache {
860
860
  * Atomic counter increment on the scoped key. TTL is preserved by the
861
861
  * underlying driver — scope ttl is only applied on first write via `set`.
862
862
  */
863
- increment(key, value) {
864
- return this.source.increment(this.scopedKey(key), value);
863
+ increment(key, value, options) {
864
+ return this.source.increment(this.scopedKey(key), value, { ttl: options?.ttl ?? this.defaults.ttl });
865
865
  }
866
866
  /**
867
867
  * Atomic counter decrement on the scoped key. See {@link increment} for
@@ -1321,9 +1321,9 @@ var CacheManager = class {
1321
1321
  /**
1322
1322
  * {@inheritdoc}
1323
1323
  */
1324
- async increment(key, value) {
1324
+ async increment(key, value, options) {
1325
1325
  this.ensureDriverInitialized();
1326
- return this.currentDriver.increment(key, value);
1326
+ return this.currentDriver.increment(key, value, options);
1327
1327
  }
1328
1328
  /**
1329
1329
  * {@inheritdoc}
@@ -1902,8 +1902,8 @@ var TaggedCache = class TaggedCache {
1902
1902
  /**
1903
1903
  * {@inheritdoc}
1904
1904
  */
1905
- async increment(key, value = 1) {
1906
- const newValue = await this.driver.increment(key, value);
1905
+ async increment(key, value = 1, options) {
1906
+ const newValue = await this.driver.increment(key, value, options);
1907
1907
  await this.storeTaggedKey(key);
1908
1908
  return newValue;
1909
1909
  }
@@ -2319,7 +2319,8 @@ var BaseCacheDriver = class {
2319
2319
  * The remaining TTL is rounded down, so repeated increments can never push
2320
2320
  * a window's deadline later.
2321
2321
  */
2322
- async increment(key, value = 1) {
2322
+ async increment(key, value = 1, options = {}) {
2323
+ const ttl = options.ttl === void 0 ? this.ttl : parseTtl(options.ttl);
2323
2324
  const parsedKey = this.parseKey(key);
2324
2325
  return this.runSerialized(parsedKey, async () => {
2325
2326
  const entry = await this.getEntry(key);
@@ -2330,8 +2331,8 @@ var BaseCacheDriver = class {
2330
2331
  const base = current === null || current === void 0 ? 0 : current;
2331
2332
  if (typeof base !== "number") throw new Error(`Cannot increment non-numeric value for key: ${(0, _warlock_js_logger.maskEmailsInText)(parsedKey)}`);
2332
2333
  const newValue = base + value;
2333
- if (!live) await this.set(key, newValue);
2334
- else if (expiresAt == null || expiresAt === Infinity) await this.set(key, newValue, { ttl: Infinity });
2334
+ if (!live) await this.set(key, newValue, { ttl });
2335
+ else if (expiresAt == null || expiresAt === Infinity) await this.set(key, newValue, { ttl: options.ttl === void 0 ? Infinity : ttl });
2335
2336
  else await this.set(key, newValue, { ttl: Math.max(1, Math.floor((expiresAt - now) / 1e3)) });
2336
2337
  return newValue;
2337
2338
  });
@@ -3132,7 +3133,8 @@ var LRUMemoryCacheDriver = class extends BaseCacheDriver {
3132
3133
  * concurrent increments in this process never lose an update. An existing
3133
3134
  * node keeps its expiry; a missing key starts from 0 with the default TTL.
3134
3135
  */
3135
- async increment(key, value = 1) {
3136
+ async increment(key, value = 1, options = {}) {
3137
+ const ttl = options.ttl === void 0 ? this.ttl : parseTtl(options.ttl);
3136
3138
  const parsedKey = this.parseKey(key);
3137
3139
  const node = this.liveNode(parsedKey);
3138
3140
  const current = node ? node.value : 0;
@@ -3140,9 +3142,10 @@ var LRUMemoryCacheDriver = class extends BaseCacheDriver {
3140
3142
  const newValue = current + value;
3141
3143
  if (node) {
3142
3144
  node.value = newValue;
3145
+ if (options.ttl !== void 0 && !Number.isFinite(node.expiresAt) && Number.isFinite(ttl) && ttl > 0) node.expiresAt = Date.now() + ttl * 1e3;
3143
3146
  this.moveHead(node);
3144
3147
  } else {
3145
- const newNode = new CacheNode(parsedKey, newValue, this.ttl);
3148
+ const newNode = new CacheNode(parsedKey, newValue, ttl);
3146
3149
  this.cache.set(parsedKey, newNode);
3147
3150
  this.addNode(newNode);
3148
3151
  if (this.cache.size > this.capacity) this.removeTail();
@@ -3150,7 +3153,7 @@ var LRUMemoryCacheDriver = class extends BaseCacheDriver {
3150
3153
  await this.emit("set", {
3151
3154
  key: parsedKey,
3152
3155
  value: newValue,
3153
- ttl: this.ttl
3156
+ ttl
3154
3157
  });
3155
3158
  return newValue;
3156
3159
  }
@@ -3503,7 +3506,8 @@ var MemoryCacheDriver = class extends BaseCacheDriver {
3503
3506
  * concurrent increments in this process never lose an update. An existing
3504
3507
  * entry keeps its expiry; a missing key starts from 0 with the default TTL.
3505
3508
  */
3506
- async increment(key, value = 1) {
3509
+ async increment(key, value = 1, options = {}) {
3510
+ const ttl = options.ttl === void 0 ? this.ttl : parseTtl(options.ttl);
3507
3511
  const parsedKey = this.parseKey(key);
3508
3512
  const entry = this.readLive(parsedKey);
3509
3513
  const current = entry ? entry.data : 0;
@@ -3511,9 +3515,14 @@ var MemoryCacheDriver = class extends BaseCacheDriver {
3511
3515
  const newValue = current + value;
3512
3516
  if (entry) {
3513
3517
  entry.data = newValue;
3518
+ if (options.ttl !== void 0 && !Number.isFinite(entry.expiresAt) && Number.isFinite(ttl) && ttl > 0) {
3519
+ entry.ttl = ttl;
3520
+ entry.expiresAt = Date.now() + ttl * 1e3;
3521
+ this.expiry.set(parsedKey, entry.expiresAt);
3522
+ }
3514
3523
  this.touch(parsedKey, entry);
3515
3524
  } else {
3516
- const data = this.prepareDataForStorage(newValue, this.ttl);
3525
+ const data = this.prepareDataForStorage(newValue, ttl);
3517
3526
  this.entries.set(parsedKey, data);
3518
3527
  if (data.expiresAt !== void 0 && Number.isFinite(data.expiresAt)) this.expiry.set(parsedKey, data.expiresAt);
3519
3528
  this.enforceMaxSize(parsedKey);
@@ -3521,7 +3530,7 @@ var MemoryCacheDriver = class extends BaseCacheDriver {
3521
3530
  await this.emit("set", {
3522
3531
  key: parsedKey,
3523
3532
  value: newValue,
3524
- ttl: entry?.ttl ?? this.ttl
3533
+ ttl: entry?.ttl ?? ttl
3525
3534
  });
3526
3535
  return newValue;
3527
3536
  }
@@ -3873,19 +3882,25 @@ var MockCacheDriver = class extends BaseCacheDriver {
3873
3882
  * Synchronous read-add-write (atomic within the process). Keeps an existing
3874
3883
  * entry's expiry; a missing key starts from 0 with the default TTL.
3875
3884
  */
3876
- async increment(key, value = 1) {
3885
+ async increment(key, value = 1, options = {}) {
3886
+ const ttl = options.ttl === void 0 ? this.ttl : parseTtl(options.ttl);
3877
3887
  const parsedKey = this.parseKey(key);
3878
3888
  this.recordCall("increment", parsedKey, [value]);
3879
3889
  const entry = this.liveEntry(parsedKey);
3880
3890
  const current = entry ? entry.data : 0;
3881
3891
  if (typeof current !== "number") throw new Error(`Cannot increment non-numeric value for key: ${(0, _warlock_js_logger.maskEmailsInText)(parsedKey)}`);
3882
3892
  const newValue = current + value;
3883
- if (entry) entry.data = newValue;
3884
- else this.storage.set(parsedKey, this.prepareDataForStorage(newValue, this.ttl));
3893
+ if (entry) {
3894
+ entry.data = newValue;
3895
+ if (options.ttl !== void 0 && !Number.isFinite(entry.expiresAt) && Number.isFinite(ttl) && ttl > 0) {
3896
+ entry.ttl = ttl;
3897
+ entry.expiresAt = Date.now() + ttl * 1e3;
3898
+ }
3899
+ } else this.storage.set(parsedKey, this.prepareDataForStorage(newValue, ttl));
3885
3900
  await this.emit("set", {
3886
3901
  key: parsedKey,
3887
3902
  value: newValue,
3888
- ttl: this.ttl
3903
+ ttl
3889
3904
  });
3890
3905
  return newValue;
3891
3906
  }
@@ -4562,7 +4577,7 @@ var PgCacheDriver = class extends BaseCacheDriver {
4562
4577
  * keeps its `expires_at`; a missing or expired row restarts from `value`
4563
4578
  * with the driver default TTL. A non-numeric live value is rejected.
4564
4579
  */
4565
- async increment(key, value = 1) {
4580
+ async increment(key, value = 1, options = {}) {
4566
4581
  const parsedKey = this.parseKey(key);
4567
4582
  const t = this.table;
4568
4583
  const expired = `${t}.expires_at IS NOT NULL AND ${t}.expires_at <= now()`;
@@ -4571,13 +4586,16 @@ var PgCacheDriver = class extends BaseCacheDriver {
4571
4586
  ON CONFLICT (key) DO UPDATE
4572
4587
  SET value = CASE WHEN ${expired} THEN EXCLUDED.value
4573
4588
  ELSE to_jsonb((${t}.value #>> '{}')::numeric + $2::numeric) END,
4574
- expires_at = CASE WHEN ${expired} THEN EXCLUDED.expires_at ELSE ${t}.expires_at END,
4589
+ expires_at = CASE WHEN ${expired} THEN EXCLUDED.expires_at
4590
+ WHEN $4::boolean AND ${t}.expires_at IS NULL THEN EXCLUDED.expires_at
4591
+ ELSE ${t}.expires_at END,
4575
4592
  stale_at = CASE WHEN ${expired} THEN NULL ELSE ${t}.stale_at END
4576
4593
  WHERE (${expired}) OR jsonb_typeof(${t}.value) = 'number'
4577
4594
  RETURNING value`, [
4578
4595
  parsedKey,
4579
4596
  value,
4580
- this.ttlToSeconds(this.ttl)
4597
+ this.ttlToSeconds(options.ttl === void 0 ? this.ttl : parseTtl(options.ttl)),
4598
+ options.ttl !== void 0
4581
4599
  ]);
4582
4600
  if (rows.length === 0) throw new Error(`Cannot increment non-numeric value for key: ${(0, _warlock_js_logger.maskEmailsInText)(parsedKey)}`);
4583
4601
  const newValue = Number(this.decodeJson(rows[0].value));
@@ -4845,7 +4863,7 @@ const REDIS_GET_DEL_SCRIPT = `local v = redis.call('GET', KEYS[1])
4845
4863
  if v then redis.call('DEL', KEYS[1]) end
4846
4864
  return v`;
4847
4865
  /**
4848
- * Increment that gives a counter without an expiry the driver's default TTL,
4866
+ * Increment that gives a counter without an expiry its per-call or default TTL,
4849
4867
  * in one round trip.
4850
4868
  *
4851
4869
  * KEYS[1]=key; ARGV[1]=amount; ARGV[2]=ttl seconds ('0' = none).
@@ -5289,10 +5307,11 @@ var RedisCacheDriver = class extends BaseCacheDriver {
5289
5307
  * Atomic increment using Redis native INCRBY command
5290
5308
  * {@inheritdoc}
5291
5309
  */
5292
- async increment(key, value = 1) {
5310
+ async increment(key, value = 1, options = {}) {
5293
5311
  const parsedKey = this.parseKey(key);
5294
5312
  this.log("caching", parsedKey);
5295
- const defaultTtl = Number.isFinite(this.ttl) && this.ttl > 0 ? Math.ceil(this.ttl) : 0;
5313
+ const ttl = options.ttl === void 0 ? this.ttl : parseTtl(options.ttl);
5314
+ const defaultTtl = Number.isFinite(ttl) && ttl > 0 ? Math.ceil(ttl) : 0;
5296
5315
  const result = defaultTtl > 0 ? Number(await this.client?.eval(REDIS_INCREMENT_SCRIPT, {
5297
5316
  keys: [parsedKey],
5298
5317
  arguments: [String(value), String(defaultTtl)]