@warlock.js/cache 5.15.0 → 5.17.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 CHANGED
@@ -4,6 +4,18 @@ 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.17.0 (Unreleased)
8
+
9
+ ### Changed
10
+
11
+ - Cache misses and expiries log at `info` instead of `warn`. A cache miss or expiry is normal behaviour, not a warning.
12
+
13
+ ### Fixed
14
+
15
+ - `cache.tags([...]).invalidate()` now deletes the tagged entries when a `globalPrefix` is configured, whether static (`"store"`) or a function. Before, the tag index stored each key with the prefix already applied, and invalidation passed that key back through `remove()`, which applied the prefix a second time. So it dropped the tag index but deleted none of the tagged entries, and reads stayed stale until TTL. Every scaffolded app sets a `globalPrefix`. The tag index now stores the un-prefixed key in all of these paths: `tags().set()`, inline `set(key, value, { tags })`, `tags().remove()`, the scoped `cache.namespace(...).tags(...)` handle (including `setNX`), and the `similar()` tag filter. `remove()` applies the prefix exactly once, on every driver. With a function prefix, invalidation uses the prefix that is current when it runs, which is the same prefix the tag index itself is read under. The two therefore agree as long as the prefix is stable for a given app or tenant.
16
+ - `MemoryCacheDriver.similar()` returned no results whenever a `globalPrefix` was set, because it read each stored, already-prefixed key back through `get()`, which prefixed it again. It now reads entries by their stored key.
17
+ - **Upgrade note:** entries tagged before this upgrade are indexed under the old, already-prefixed form, so invalidation still can't reach them. Their tag index is dropped on the first `invalidate()`, which leaves those entries orphaned. They expire by their TTL, or `flush()` clears them right away.
18
+
7
19
  ## 5.15.0 - 2026-09-18
8
20
 
9
21
  ### Fixed
package/cjs/index.cjs CHANGED
@@ -627,8 +627,7 @@ var TaggedScopedCache = class {
627
627
  const allTags = mergeTagSets(this.scope.defaults.tags, this.handleTags);
628
628
  if (!allTags || allTags.length === 0) return true;
629
629
  const scopedKey = this.buildScopedKey(key);
630
- const parsedKey = this.scope.source.parseKey(scopedKey);
631
- await this.scope.source.tags(allTags).storeTagRelationship(parsedKey);
630
+ await this.scope.source.tags(allTags).storeTagRelationship(scopedKey);
632
631
  return true;
633
632
  }
634
633
  /**
@@ -1662,20 +1661,36 @@ var TaggedCache = class {
1662
1661
  * Public alias of the tag-index writer. Called by `BaseCacheDriver.applyTags`
1663
1662
  * when tags are passed inline through `CacheSetOptions.tags`.
1664
1663
  *
1664
+ * Takes the caller's key, never the driver-parsed one — see {@link indexedKey}.
1665
+ *
1665
1666
  * @internal — public for cross-class use within this package; not part of the
1666
1667
  * stable consumer API.
1667
1668
  */
1668
- async storeTagRelationship(parsedKey) {
1669
+ async storeTagRelationship(key) {
1670
+ const indexedKey = this.indexedKey(key);
1669
1671
  for (const tag of this.cacheTags) {
1670
1672
  const tagKey = this.tagKey(tag);
1671
1673
  const keys = await this.driver.get(tagKey) || [];
1672
- if (!keys.includes(parsedKey)) {
1673
- keys.push(parsedKey);
1674
+ if (!keys.includes(indexedKey)) {
1675
+ keys.push(indexedKey);
1674
1676
  await this.driver.set(tagKey, keys, Infinity);
1675
1677
  }
1676
1678
  }
1677
1679
  }
1678
1680
  /**
1681
+ * The form a key takes inside a tag index: normalized but un-prefixed.
1682
+ *
1683
+ * The index is read back and deleted through `driver.remove(key)`, which
1684
+ * applies `globalPrefix` itself — so the index must hold the key *before*
1685
+ * the prefix, or invalidation would prefix it twice and delete nothing.
1686
+ * With a function prefix, the current prefix is re-applied at invalidation
1687
+ * time; the tag index key is resolved under that same prefix, so the two
1688
+ * agree as long as the prefix is stable for a given request/tenant.
1689
+ */
1690
+ indexedKey(key) {
1691
+ return parseCacheKey(key);
1692
+ }
1693
+ /**
1679
1694
  * Get all keys associated with tags
1680
1695
  */
1681
1696
  async getTaggedKeys() {
@@ -1691,9 +1706,8 @@ var TaggedCache = class {
1691
1706
  * {@inheritdoc}
1692
1707
  */
1693
1708
  async set(key, value, ttlOrOptions) {
1694
- const parsedKey = this.driver.parseKey(key);
1695
1709
  await this.driver.set(key, value, ttlOrOptions);
1696
- await this.storeTaggedKey(parsedKey);
1710
+ await this.storeTaggedKey(key);
1697
1711
  return value;
1698
1712
  }
1699
1713
  /**
@@ -1706,11 +1720,11 @@ var TaggedCache = class {
1706
1720
  * {@inheritdoc}
1707
1721
  */
1708
1722
  async remove(key) {
1709
- const parsedKey = this.driver.parseKey(key);
1723
+ const indexedKey = this.indexedKey(key);
1710
1724
  await this.driver.remove(key);
1711
1725
  for (const tag of this.cacheTags) {
1712
1726
  const tagKey = this.tagKey(tag);
1713
- const updatedKeys = (await this.driver.get(tagKey) || []).filter((k) => k !== parsedKey);
1727
+ const updatedKeys = (await this.driver.get(tagKey) || []).filter((k) => k !== indexedKey);
1714
1728
  await this.driver.set(tagKey, updatedKeys, Infinity);
1715
1729
  }
1716
1730
  }
@@ -1905,6 +1919,8 @@ var BaseCacheDriver = class {
1905
1919
  * Used by `similar()` to narrow the candidate pool before similarity ranking.
1906
1920
  *
1907
1921
  * Returns `null` when no tags are passed (callers should treat that as "no filter").
1922
+ * Keys come back parsed (prefixed) — the index stores them un-prefixed, but
1923
+ * callers match them against their own parsed storage keys.
1908
1924
  */
1909
1925
  async getKeysForTags(tags) {
1910
1926
  if (!tags || tags.length === 0) return null;
@@ -1912,17 +1928,20 @@ var BaseCacheDriver = class {
1912
1928
  for (const tag of tags) {
1913
1929
  const tagKey = `cache:tags:${tag}`;
1914
1930
  const keys = await this.get(tagKey) || [];
1915
- for (const k of keys) allKeys.add(k);
1931
+ for (const k of keys) allKeys.add(this.parseKey(k));
1916
1932
  }
1917
1933
  return allKeys;
1918
1934
  }
1919
1935
  /**
1920
1936
  * Apply tag relationships after a successful write. Called by drivers once
1921
1937
  * the value is in storage.
1938
+ *
1939
+ * Pass the caller's key, not the parsed one: the tag index stores keys
1940
+ * un-prefixed so `invalidate()` can delete them through `remove(key)`.
1922
1941
  */
1923
- async applyTags(parsedKey, tags) {
1942
+ async applyTags(key, tags) {
1924
1943
  if (tags.length === 0) return;
1925
- await this.tags(tags).storeTagRelationship(parsedKey);
1944
+ await this.tags(tags).storeTagRelationship(key);
1926
1945
  }
1927
1946
  /**
1928
1947
  * {@inheritdoc}
@@ -2113,7 +2132,7 @@ var BaseCacheDriver = class {
2113
2132
  if (!this.shouldLog) return;
2114
2133
  if (typeof key === "string") key = key.replace(/\//g, ".");
2115
2134
  if (operation === "connectionFailed") _warlock_js_logger.log.fatal(`cache.${this.name}`, operation, key);
2116
- if (operation == "notFound" || operation == "expired") return _warlock_js_logger.log.warn("cache." + this.name, operation, (key ? key + " " : "") + messages[operation]);
2135
+ if (operation == "notFound" || operation == "expired") return _warlock_js_logger.log.info("cache." + this.name, operation, (key ? key + " " : "") + messages[operation]);
2117
2136
  if (operation.endsWith("ed")) return _warlock_js_logger.log.success("cache." + this.name, operation, (key ? key + " " : "") + messages[operation]);
2118
2137
  _warlock_js_logger.log.info("cache." + this.name, operation, (key ? key + " " : "") + messages[operation]);
2119
2138
  }
@@ -2422,7 +2441,7 @@ var FileCacheDriver = class extends BaseCacheDriver {
2422
2441
  const fileDirectory = this.keyDirectory(parsedKey);
2423
2442
  await (0, _warlock_js_fs.ensureDirectoryAsync)(fileDirectory);
2424
2443
  await (0, _warlock_js_fs.putJsonFileAsync)(path.default.resolve(fileDirectory, this.fileName), data);
2425
- if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
2444
+ if (tags && tags.length > 0) await this.applyTags(key, tags);
2426
2445
  this.log("cached", parsedKey);
2427
2446
  await this.emit("set", {
2428
2447
  key: parsedKey,
@@ -2652,7 +2671,7 @@ var LRUMemoryCacheDriver = class extends BaseCacheDriver {
2652
2671
  this.addNode(newNode);
2653
2672
  if (this.cache.size > this.capacity) this.removeTail();
2654
2673
  }
2655
- if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
2674
+ if (tags && tags.length > 0) await this.applyTags(key, tags);
2656
2675
  this.log("cached", parsedKey);
2657
2676
  await this.emit("set", {
2658
2677
  key: parsedKey,
@@ -2906,7 +2925,7 @@ var MemoryCacheDriver = class extends BaseCacheDriver {
2906
2925
  (0, _mongez_reinforcements.set)(this.data, parsedKey, data);
2907
2926
  this.trackAccess(parsedKey);
2908
2927
  if (!exists && this.options.maxSize) await this.enforceMaxSize();
2909
- if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
2928
+ if (tags && tags.length > 0) await this.applyTags(key, tags);
2910
2929
  if (vector) this.vectorIndex.set(parsedKey, vector.slice());
2911
2930
  this.log("cached", parsedKey);
2912
2931
  await this.emit("set", {
@@ -3047,8 +3066,14 @@ var MemoryCacheDriver = class extends BaseCacheDriver {
3047
3066
  const hits = [];
3048
3067
  for (const [parsedKey, stored] of this.vectorIndex) {
3049
3068
  if (tagFilter && !tagFilter.has(parsedKey)) continue;
3050
- const value = await this.get(parsedKey);
3051
- if (value === null) continue;
3069
+ const entry = (0, _mongez_reinforcements.get)(this.data, parsedKey);
3070
+ if (!entry) continue;
3071
+ if (entry.expiresAt !== void 0 && entry.expiresAt <= Date.now()) continue;
3072
+ let value = entry.data;
3073
+ if (value !== null && value !== void 0) {
3074
+ const type = typeof value;
3075
+ if (type !== "string" && type !== "number" && type !== "boolean") value = structuredClone(value);
3076
+ }
3052
3077
  const score = cosineSimilarity(vector, stored);
3053
3078
  if (options.threshold !== void 0 && score < options.threshold) continue;
3054
3079
  hits.push({
@@ -3200,7 +3225,7 @@ var MockCacheDriver = class extends BaseCacheDriver {
3200
3225
  };
3201
3226
  const data = this.prepareDataForStorage(value, ttl, staleAt);
3202
3227
  this.storage.set(parsedKey, data);
3203
- if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
3228
+ if (tags && tags.length > 0) await this.applyTags(key, tags);
3204
3229
  this.log("cached", parsedKey);
3205
3230
  await this.emit("set", {
3206
3231
  key: parsedKey,
@@ -3625,7 +3650,7 @@ var PgCacheDriver = class extends BaseCacheDriver {
3625
3650
  wasSet: false,
3626
3651
  existing: await this.get(key)
3627
3652
  };
3628
- if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
3653
+ if (tags && tags.length > 0) await this.applyTags(key, tags);
3629
3654
  this.log("cached", parsedKey);
3630
3655
  await this.emit("set", {
3631
3656
  key: parsedKey,
@@ -3646,7 +3671,7 @@ var PgCacheDriver = class extends BaseCacheDriver {
3646
3671
  wasSet: false,
3647
3672
  existing: null
3648
3673
  };
3649
- if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
3674
+ if (tags && tags.length > 0) await this.applyTags(key, tags);
3650
3675
  this.log("cached", parsedKey);
3651
3676
  await this.emit("set", {
3652
3677
  key: parsedKey,
@@ -3662,7 +3687,7 @@ var PgCacheDriver = class extends BaseCacheDriver {
3662
3687
  VALUES (${valList})
3663
3688
  ON CONFLICT (key) DO UPDATE
3664
3689
  SET ${setClause}`, params);
3665
- if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
3690
+ if (tags && tags.length > 0) await this.applyTags(key, tags);
3666
3691
  this.log("cached", parsedKey);
3667
3692
  await this.emit("set", {
3668
3693
  key: parsedKey,
@@ -3932,7 +3957,7 @@ var RedisCacheDriver = class extends BaseCacheDriver {
3932
3957
  wasSet: false,
3933
3958
  existing: onConflict === "create" ? await this.get(key) : null
3934
3959
  };
3935
- if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
3960
+ if (tags && tags.length > 0) await this.applyTags(key, tags);
3936
3961
  if (staleAt !== void 0) {
3937
3962
  const sidecarOptions = {};
3938
3963
  if (hasExpiry) sidecarOptions.EX = ttl;