@basaltkit/cache 1.4.0 → 1.4.1
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/README.md +6 -0
- package/dist/index.d.ts +16 -3
- package/dist/index.js +20 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -244,6 +244,12 @@ closed turns it into a stack trace at the call site.
|
|
|
244
244
|
Single-tenant apps are untouched: no tenancy plugin, no marker, so the default stays `'global'`
|
|
245
245
|
and nothing changes.
|
|
246
246
|
|
|
247
|
+
`flush()` follows the same gate. It wipes a whole prefix, so in a multi-tenant app an unresolved
|
|
248
|
+
scope must fail closed — otherwise one mis-scoped call clears every tenant's cache. Without
|
|
249
|
+
tenancy, the prefix *is* this app's own cache and clearing it is exactly what `flush()` means, so
|
|
250
|
+
it proceeds. An explicit `onMissingScope: 'error'` keeps `flush()` fail-closed either way, and
|
|
251
|
+
`scope: null` (a deliberate global cache) may always flush its namespace.
|
|
252
|
+
|
|
247
253
|
```ts
|
|
248
254
|
// Multi-tenant: this now throws instead of poisoning the shared namespace.
|
|
249
255
|
cachePlugin({ driver: 'redis', url }) // + tenancyPlugin() registered → onMissingScope: 'error'
|
package/dist/index.d.ts
CHANGED
|
@@ -19,8 +19,9 @@ export interface CacheOptions {
|
|
|
19
19
|
* What to do when the scope function resolves nothing (no tenant in context):
|
|
20
20
|
* `'global'` (default) shares one namespace — convenient but a per-tenant value
|
|
21
21
|
* cached without a tenant leaks to others; `'error'` fails closed (throws
|
|
22
|
-
* {@link MissingCacheScopeError}) on read/write.
|
|
23
|
-
*
|
|
22
|
+
* {@link MissingCacheScopeError}) on read/write. In a multi-tenant app (or
|
|
23
|
+
* with an explicit `'error'`), `flush()` fails closed too, so a mis-scoped
|
|
24
|
+
* call can't wipe every tenant's cache.
|
|
24
25
|
*/
|
|
25
26
|
onMissingScope?: 'global' | 'error';
|
|
26
27
|
/** Injectable clock (ms) for stale-while-revalidate windows. Default: Date.now. */
|
|
@@ -39,13 +40,25 @@ export interface SwrOptions {
|
|
|
39
40
|
}
|
|
40
41
|
export declare class Cache {
|
|
41
42
|
private readonly driver;
|
|
43
|
+
/**
|
|
44
|
+
* Whether the host app registered `@basaltkit/tenancy`. `cachePlugin` wires
|
|
45
|
+
* this to the container's `'tenancy:active'` metadata marker — a signal,
|
|
46
|
+
* not an import. Defaults to `false` (single-tenant).
|
|
47
|
+
*/
|
|
48
|
+
private readonly tenancyActive;
|
|
42
49
|
private readonly prefix;
|
|
43
50
|
private readonly scope;
|
|
44
51
|
private readonly onMissingScope;
|
|
45
52
|
private readonly now;
|
|
46
53
|
/** dedupe of in-flight factories — per-process stampede protection (also dedupes SWR revalidation) */
|
|
47
54
|
private readonly pending;
|
|
48
|
-
constructor(driver: CacheDriver, options?: CacheOptions
|
|
55
|
+
constructor(driver: CacheDriver, options?: CacheOptions,
|
|
56
|
+
/**
|
|
57
|
+
* Whether the host app registered `@basaltkit/tenancy`. `cachePlugin` wires
|
|
58
|
+
* this to the container's `'tenancy:active'` metadata marker — a signal,
|
|
59
|
+
* not an import. Defaults to `false` (single-tenant).
|
|
60
|
+
*/
|
|
61
|
+
tenancyActive?: () => boolean);
|
|
49
62
|
get<T>(key: string): Promise<T | undefined>;
|
|
50
63
|
get<T>(key: string, fallback: T): Promise<T>;
|
|
51
64
|
put(key: string, value: unknown, ttl?: DurationInput): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -42,14 +42,22 @@ const defaultScope = () => {
|
|
|
42
42
|
};
|
|
43
43
|
export class Cache {
|
|
44
44
|
driver;
|
|
45
|
+
tenancyActive;
|
|
45
46
|
prefix;
|
|
46
47
|
scope;
|
|
47
48
|
onMissingScope;
|
|
48
49
|
now;
|
|
49
50
|
/** dedupe of in-flight factories — per-process stampede protection (also dedupes SWR revalidation) */
|
|
50
51
|
pending = new Map();
|
|
51
|
-
constructor(driver, options = {}
|
|
52
|
+
constructor(driver, options = {},
|
|
53
|
+
/**
|
|
54
|
+
* Whether the host app registered `@basaltkit/tenancy`. `cachePlugin` wires
|
|
55
|
+
* this to the container's `'tenancy:active'` metadata marker — a signal,
|
|
56
|
+
* not an import. Defaults to `false` (single-tenant).
|
|
57
|
+
*/
|
|
58
|
+
tenancyActive = () => false) {
|
|
52
59
|
this.driver = driver;
|
|
60
|
+
this.tenancyActive = tenancyActive;
|
|
53
61
|
this.prefix = options.prefix ?? 'basalt';
|
|
54
62
|
this.scope = options.scope === undefined ? defaultScope : options.scope;
|
|
55
63
|
this.onMissingScope = options.onMissingScope ?? 'global';
|
|
@@ -71,10 +79,15 @@ export class Cache {
|
|
|
71
79
|
}
|
|
72
80
|
/** Clears only the keys under this prefix/scope — never the entire Redis. */
|
|
73
81
|
async flush() {
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
82
|
+
// Fail closed where a wipe could cross a boundary: with tenancy registered
|
|
83
|
+
// (or an explicit onMissingScope:'error'), an unresolved scope would delete
|
|
84
|
+
// EVERY tenant's cache. In a single-tenant app the whole prefix IS this
|
|
85
|
+
// app's cache, which is exactly what flush() means — so it proceeds.
|
|
86
|
+
// `scope:null` (deliberate global) is fine either way.
|
|
87
|
+
const failClosed = this.tenancyActive() || this.onMissingScope === 'error';
|
|
88
|
+
if (failClosed && this.scope !== null && this.scope() === undefined) {
|
|
77
89
|
throw new MissingCacheScopeError('flush');
|
|
90
|
+
}
|
|
78
91
|
await this.driver.flushPrefix(this.root());
|
|
79
92
|
}
|
|
80
93
|
/** Tag-scoped operations: `cache.tags('plans').flush()` invalidates the group. */
|
|
@@ -198,11 +211,11 @@ export function cachePlugin(options = {}) {
|
|
|
198
211
|
// with no resolvable tenant scope throws instead of silently sharing
|
|
199
212
|
// one global namespace across tenants. Single-tenant apps (no tenancy)
|
|
200
213
|
// are untouched, and an explicit `onMissingScope`/custom `scope` wins.
|
|
201
|
-
const tenancyActive = ensureMetadata(container).get('tenancy:active').length > 0;
|
|
202
|
-
const resolved = options.onMissingScope === undefined && options.scope === undefined && tenancyActive
|
|
214
|
+
const tenancyActive = () => ensureMetadata(container).get('tenancy:active').length > 0;
|
|
215
|
+
const resolved = options.onMissingScope === undefined && options.scope === undefined && tenancyActive()
|
|
203
216
|
? { ...options, onMissingScope: 'error' }
|
|
204
217
|
: options;
|
|
205
|
-
return new Cache(driver, resolved);
|
|
218
|
+
return new Cache(driver, resolved, tenancyActive);
|
|
206
219
|
});
|
|
207
220
|
},
|
|
208
221
|
async shutdown() {
|