@basaltkit/cache 1.0.0 → 1.1.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Machize Contributors
3
+ Copyright (c) 2026 Basalt Contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _basaltkit_core from '@basaltkit/core';
2
- import { DurationInput } from '@basaltkit/core';
2
+ import { DurationInput, BasaltError } from '@basaltkit/core';
3
3
  import { Redis } from 'ioredis';
4
4
 
5
5
  /** Cache driver contract. Every driver passes the same conformance suite. */
@@ -37,19 +37,32 @@ declare class RedisCacheDriver implements CacheDriver {
37
37
  disconnect(): Promise<void>;
38
38
  }
39
39
 
40
+ declare class MissingCacheScopeError extends BasaltError {
41
+ constructor(op: string);
42
+ }
40
43
  interface CacheOptions {
41
44
  /** Root prefix for all keys. Default: 'basalt' */
42
45
  prefix?: string;
43
46
  /**
44
47
  * Dynamic segment of the prefix, resolved on every operation. The default reads
45
- * `ctx().tenant.id` — automatic per-tenant isolation. Pass `null` to disable.
48
+ * `ctx().tenant.id` — automatic per-tenant isolation. Pass `null` to disable
49
+ * (a deliberate global cache).
46
50
  */
47
51
  scope?: (() => string | undefined) | null;
52
+ /**
53
+ * What to do when the scope function resolves nothing (no tenant in context):
54
+ * `'global'` (default) shares one namespace — convenient but a per-tenant value
55
+ * cached without a tenant leaks to others; `'error'` fails closed (throws
56
+ * {@link MissingCacheScopeError}) on read/write. `flush()` ALWAYS fails closed
57
+ * regardless, so a mis-scoped call can't wipe every tenant's cache.
58
+ */
59
+ onMissingScope?: 'global' | 'error';
48
60
  }
49
61
  declare class Cache {
50
62
  private readonly driver;
51
63
  private readonly prefix;
52
64
  private readonly scope;
65
+ private readonly onMissingScope;
53
66
  /** dedupe of in-flight factories — per-process stampede protection */
54
67
  private readonly pending;
55
68
  constructor(driver: CacheDriver, options?: CacheOptions);
@@ -83,4 +96,4 @@ interface CachePluginOptions extends CacheOptions {
83
96
  }
84
97
  declare function cachePlugin(options?: CachePluginOptions): _basaltkit_core.BasaltPlugin<unknown>;
85
98
 
86
- export { CACHE, Cache, type CacheDriver, type CacheOptions, type CachePluginOptions, MemoryCacheDriver, RedisCacheDriver, cachePlugin };
99
+ export { CACHE, Cache, type CacheDriver, type CacheOptions, type CachePluginOptions, MemoryCacheDriver, MissingCacheScopeError, RedisCacheDriver, cachePlugin };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/index.ts
2
2
  import {
3
+ BasaltError,
3
4
  createToken,
4
5
  definePlugin,
5
6
  parseDuration,
@@ -94,6 +95,14 @@ var RedisCacheDriver = class _RedisCacheDriver {
94
95
  };
95
96
 
96
97
  // src/index.ts
98
+ var MissingCacheScopeError = class extends BasaltError {
99
+ constructor(op) {
100
+ super(
101
+ "CACHE_SCOPE_MISSING",
102
+ `Refusing cache ${op}: a tenant-scoped cache resolved no tenant (ran without a tenant context). Establish a tenant, or use scope:null for a deliberate global cache.`
103
+ );
104
+ }
105
+ };
97
106
  var defaultScope = () => {
98
107
  const tenant = tryCtx()?.["tenant"];
99
108
  return tenant?.id ? `tenant:${tenant.id}` : void 0;
@@ -103,10 +112,12 @@ var Cache = class {
103
112
  this.driver = driver;
104
113
  this.prefix = options.prefix ?? "basalt";
105
114
  this.scope = options.scope === void 0 ? defaultScope : options.scope;
115
+ this.onMissingScope = options.onMissingScope ?? "global";
106
116
  }
107
117
  driver;
108
118
  prefix;
109
119
  scope;
120
+ onMissingScope;
110
121
  /** dedupe of in-flight factories — per-process stampede protection */
111
122
  pending = /* @__PURE__ */ new Map();
112
123
  async get(key, fallback) {
@@ -132,6 +143,7 @@ var Cache = class {
132
143
  }
133
144
  /** Clears only the keys under this prefix/scope — never the entire Redis. */
134
145
  async flush() {
146
+ if (this.scope !== null && this.scope() === void 0) throw new MissingCacheScopeError("flush");
135
147
  await this.driver.flushPrefix(this.root());
136
148
  }
137
149
  /** Tag-scoped operations: `cache.tags('plans').flush()` invalidates the group. */
@@ -171,7 +183,9 @@ var Cache = class {
171
183
  return computation;
172
184
  }
173
185
  root() {
174
- const scope = this.scope?.();
186
+ if (this.scope === null) return `${this.prefix}:`;
187
+ const scope = this.scope();
188
+ if (scope === void 0 && this.onMissingScope === "error") throw new MissingCacheScopeError("operation");
175
189
  return scope ? `${this.prefix}:${scope}:` : `${this.prefix}:`;
176
190
  }
177
191
  key(key) {
@@ -198,6 +212,7 @@ export {
198
212
  CACHE,
199
213
  Cache,
200
214
  MemoryCacheDriver,
215
+ MissingCacheScopeError,
201
216
  RedisCacheDriver,
202
217
  cachePlugin
203
218
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/cache",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Basalt cache layer: Redis and Memory drivers, tags, TTL, stampede protection and automatic per-tenant isolation.",
5
5
  "license": "MIT",
6
6
  "type": "module",