@classytic/repo-core 0.24.0 → 0.25.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 +22 -0
- package/dist/cache/engine.d.mts +31 -3
- package/dist/cache/engine.mjs +31 -9
- package/dist/cache/index.d.mts +2 -2
- package/dist/cache/plugin/index.d.mts +22 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to `@classytic/repo-core` are documented here.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.25.0] - 2026-08-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`CacheAdapterResolver = () => CacheAdapter | undefined`** — a function consulted per call that returns the store for the current scope, or `undefined` for "no cache right now". `undefined` is inert — identical to `enabled: false` — and never falls back to a shared store, because that fallback is the cross-scope leak the scoping exists to prevent.
|
|
12
|
+
- **`CacheAdapterSource = CacheAdapter | CacheAdapterResolver`** — `CacheEngine` and `cachePlugin` now accept either a fixed process-lifetime adapter (unchanged behaviour) or a resolver. A bare adapter is sugar for `() => adapter`; normalised at construction so every call site is on one path.
|
|
13
|
+
- **`CacheAdapterResolver` and `CacheAdapterSource` exported from `@classytic/repo-core/cache`** and re-exported from the plugin index.
|
|
14
|
+
|
|
15
|
+
## [0.24.0] - 2026-08-17
|
|
16
|
+
|
|
17
|
+
Version bump only — republished 0.23.1's contents under a minor so consumers pinning `>=0.24.0` get the tenant guard by a floor rather than a patch. No source changes over 0.23.1.
|
|
18
|
+
|
|
19
|
+
## [0.23.1] - 2026-08-17
|
|
20
|
+
|
|
21
|
+
### Added — `assertNoLegacyTenantKeys(config, pkg, extra?)` (`/tenant`)
|
|
22
|
+
|
|
23
|
+
Refuses a config still carrying a pre-consolidation tenant key (`multiTenant`, plus any package-specific retirees passed via `extra`) instead of ignoring it.
|
|
24
|
+
|
|
25
|
+
The rename onto `TenantConfig` under `tenant` is trivial; the failure mode when a CALLER misses it is not. Every kernel resolver reads `resolveTenantConfig(config.tenant ?? false)`, so an absent `tenant` resolves to `strategy: 'none'` — no tenant field, no tenant filter, every read spanning ALL tenants, no error, and figures that look plausible. A host that asked for tenancy silently gets none.
|
|
26
|
+
|
|
27
|
+
The guard lives beside the resolver every one of those packages already calls, rather than being hand-rolled nine times — nine chances to forget, and the one that forgets ships the leak. Additive and non-breaking: nothing calls it until a package renames.
|
|
28
|
+
|
|
7
29
|
## [0.23.0] - 2026-08-13
|
|
8
30
|
|
|
9
31
|
### Added — transactional-core contracts (Phase 1a + 1d) + fencing (Phase 2, first slice)
|
package/dist/cache/engine.d.mts
CHANGED
|
@@ -22,8 +22,36 @@ type SingleFlightClaim<T = unknown> = {
|
|
|
22
22
|
readonly status: 'wait';
|
|
23
23
|
readonly promise: Promise<T>;
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the adapter to use for THIS call, or `undefined` for "no cache
|
|
27
|
+
* right now".
|
|
28
|
+
*
|
|
29
|
+
* Exists so a cache can be scoped to something narrower than the process —
|
|
30
|
+
* a request, a job run, a unit of work — without this package learning what
|
|
31
|
+
* any of those are. The resolver is the whole seam: repo-core asks "is there
|
|
32
|
+
* a store for the current scope?", and whoever owns the lifecycle answers.
|
|
33
|
+
*
|
|
34
|
+
* **`undefined` must mean INERT, never "make one".** A resolver that returns
|
|
35
|
+
* nothing outside its scope is the correct, safe answer for cron jobs,
|
|
36
|
+
* scripts and tests — the alternative (falling back to a process-wide store)
|
|
37
|
+
* is exactly the cross-request leak the scoping exists to prevent, and it
|
|
38
|
+
* would be invisible.
|
|
39
|
+
*/
|
|
40
|
+
type CacheAdapterResolver = () => CacheAdapter | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Either a fixed adapter (process-lifetime, the original contract) or a
|
|
43
|
+
* resolver consulted per call. A bare adapter is sugar for `() => adapter`.
|
|
44
|
+
*/
|
|
45
|
+
type CacheAdapterSource = CacheAdapter | CacheAdapterResolver;
|
|
25
46
|
declare class CacheEngine {
|
|
26
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Consulted PER CALL, never cached in a field.
|
|
49
|
+
*
|
|
50
|
+
* Memoising the first resolution would defeat the entire purpose: the
|
|
51
|
+
* second request would be served the first request's store. The whole
|
|
52
|
+
* point of the indirection is that the answer changes.
|
|
53
|
+
*/
|
|
54
|
+
private readonly resolveAdapter;
|
|
27
55
|
private readonly prefix;
|
|
28
56
|
private readonly jitter;
|
|
29
57
|
/**
|
|
@@ -34,7 +62,7 @@ declare class CacheEngine {
|
|
|
34
62
|
* unbounded burst).
|
|
35
63
|
*/
|
|
36
64
|
private readonly pending;
|
|
37
|
-
constructor(adapter:
|
|
65
|
+
constructor(adapter: CacheAdapterSource, options?: CacheEngineOptions);
|
|
38
66
|
/**
|
|
39
67
|
* Read a cache entry under SWR + TTL semantics. Returns a
|
|
40
68
|
* structured `CacheReadResult` describing freshness state — the
|
|
@@ -123,4 +151,4 @@ declare class CacheEngine {
|
|
|
123
151
|
prefetch<TData>(key: string, opts: ResolvedCacheOptions, fetcher: () => Promise<TData>): Promise<TData>;
|
|
124
152
|
}
|
|
125
153
|
//#endregion
|
|
126
|
-
export { CacheEngine, CacheEngineOptions, SingleFlightClaim };
|
|
154
|
+
export { CacheAdapterResolver, CacheAdapterSource, CacheEngine, CacheEngineOptions, SingleFlightClaim };
|
package/dist/cache/engine.mjs
CHANGED
|
@@ -24,7 +24,14 @@ import { bumpModelVersion, getModelVersion } from "./version-store.mjs";
|
|
|
24
24
|
* call it directly when they need fine-grained control.
|
|
25
25
|
*/
|
|
26
26
|
var CacheEngine = class {
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Consulted PER CALL, never cached in a field.
|
|
29
|
+
*
|
|
30
|
+
* Memoising the first resolution would defeat the entire purpose: the
|
|
31
|
+
* second request would be served the first request's store. The whole
|
|
32
|
+
* point of the indirection is that the answer changes.
|
|
33
|
+
*/
|
|
34
|
+
resolveAdapter;
|
|
28
35
|
prefix;
|
|
29
36
|
jitter;
|
|
30
37
|
/**
|
|
@@ -36,7 +43,7 @@ var CacheEngine = class {
|
|
|
36
43
|
*/
|
|
37
44
|
pending = /* @__PURE__ */ new Map();
|
|
38
45
|
constructor(adapter, options = {}) {
|
|
39
|
-
this.
|
|
46
|
+
this.resolveAdapter = typeof adapter === "function" ? adapter : () => adapter;
|
|
40
47
|
this.prefix = options.prefix ?? "rc";
|
|
41
48
|
this.jitter = resolveJitter(options.jitter);
|
|
42
49
|
}
|
|
@@ -62,7 +69,12 @@ var CacheEngine = class {
|
|
|
62
69
|
status: "bypass",
|
|
63
70
|
data: void 0
|
|
64
71
|
};
|
|
65
|
-
const
|
|
72
|
+
const adapter = this.resolveAdapter();
|
|
73
|
+
if (!adapter) return {
|
|
74
|
+
status: "disabled",
|
|
75
|
+
data: void 0
|
|
76
|
+
};
|
|
77
|
+
const raw = await adapter.get(key);
|
|
66
78
|
const inspection = inspectEnvelope(raw);
|
|
67
79
|
if (inspection.state === "missing" || inspection.state === "expired") return {
|
|
68
80
|
status: "miss",
|
|
@@ -98,12 +110,14 @@ var CacheEngine = class {
|
|
|
98
110
|
*/
|
|
99
111
|
async set(key, value, opts) {
|
|
100
112
|
if (!opts.enabled) return;
|
|
113
|
+
const adapter = this.resolveAdapter();
|
|
114
|
+
if (!adapter) return;
|
|
101
115
|
const tags = opts.tags;
|
|
102
116
|
const envelope = buildEnvelope(value, opts.staleTime, opts.gcTime, tags);
|
|
103
117
|
const totalSeconds = opts.staleTime + opts.gcTime;
|
|
104
118
|
const ttl = this.jitter(totalSeconds);
|
|
105
|
-
await
|
|
106
|
-
if (tags.length > 0) await appendKeyToTags(
|
|
119
|
+
await adapter.set(key, envelope, ttl);
|
|
120
|
+
if (tags.length > 0) await appendKeyToTags(adapter, this.prefix, key, tags, ttl);
|
|
107
121
|
}
|
|
108
122
|
/**
|
|
109
123
|
* Look up an in-flight fetch for `key`. Returns the promise the
|
|
@@ -157,7 +171,9 @@ var CacheEngine = class {
|
|
|
157
171
|
* the index. Returns the count of entries removed.
|
|
158
172
|
*/
|
|
159
173
|
async invalidateByTags(tags) {
|
|
160
|
-
|
|
174
|
+
const adapter = this.resolveAdapter();
|
|
175
|
+
if (!adapter) return 0;
|
|
176
|
+
return invalidateByTags(adapter, this.prefix, tags);
|
|
161
177
|
}
|
|
162
178
|
/**
|
|
163
179
|
* Read a model's current version (optionally per-scope). Used by
|
|
@@ -165,7 +181,9 @@ var CacheEngine = class {
|
|
|
165
181
|
* version bump orphans the model's cache space.
|
|
166
182
|
*/
|
|
167
183
|
async getVersion(model, scopeKey) {
|
|
168
|
-
|
|
184
|
+
const adapter = this.resolveAdapter();
|
|
185
|
+
if (!adapter) return 0;
|
|
186
|
+
return getModelVersion(adapter, this.prefix, model, scopeKey);
|
|
169
187
|
}
|
|
170
188
|
/**
|
|
171
189
|
* Bump the model's version (per-scope when `scopeKey` is supplied)
|
|
@@ -174,11 +192,15 @@ var CacheEngine = class {
|
|
|
174
192
|
* invalidation.
|
|
175
193
|
*/
|
|
176
194
|
async bumpVersion(model, scopeKey) {
|
|
177
|
-
|
|
195
|
+
const adapter = this.resolveAdapter();
|
|
196
|
+
if (!adapter) return 0;
|
|
197
|
+
return bumpModelVersion(adapter, this.prefix, model, scopeKey);
|
|
178
198
|
}
|
|
179
199
|
/** Wipe the entire cache namespace (when the adapter supports `clear`). */
|
|
180
200
|
async clear() {
|
|
181
|
-
|
|
201
|
+
const adapter = this.resolveAdapter();
|
|
202
|
+
if (!adapter) return;
|
|
203
|
+
if (adapter.clear) await adapter.clear(`${this.prefix}:*`);
|
|
182
204
|
}
|
|
183
205
|
/** Expose the prefix so plugins building keys downstream stay aligned. */
|
|
184
206
|
get keyPrefix() {
|
package/dist/cache/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { CacheOptions, CacheReadResult } from "./options.mjs";
|
|
2
2
|
import { CacheAdapter } from "./types.mjs";
|
|
3
|
-
import { CacheEngine, CacheEngineOptions, SingleFlightClaim } from "./engine.mjs";
|
|
3
|
+
import { CacheAdapterResolver, CacheAdapterSource, CacheEngine, CacheEngineOptions, SingleFlightClaim } from "./engine.mjs";
|
|
4
4
|
import { MemoryCacheAdapterOptions, createMemoryCacheAdapter } from "./memory-adapter.mjs";
|
|
5
5
|
import { DEFAULT_SHAPE_KEYS_BY_OP } from "./plugin/context.mjs";
|
|
6
6
|
import { DEFAULT_CACHEABLE_OPS, DEFAULT_INVALIDATING_OPS, LogCallbacks, RepositoryCacheHandle, RepositoryCachePluginOptions, cachePlugin } from "./plugin/index.mjs";
|
|
7
7
|
import { scheduleBackground } from "./runtime.mjs";
|
|
8
8
|
import { CacheTimeoutError, TimeoutAdapterOptions, withTimeout } from "./timeout-adapter.mjs";
|
|
9
|
-
export { type CacheAdapter, CacheEngine, type CacheEngineOptions, type CacheOptions, type CacheReadResult, CacheTimeoutError, DEFAULT_CACHEABLE_OPS, DEFAULT_INVALIDATING_OPS, DEFAULT_SHAPE_KEYS_BY_OP, type LogCallbacks, type MemoryCacheAdapterOptions, type RepositoryCacheHandle, type RepositoryCachePluginOptions, type SingleFlightClaim, type TimeoutAdapterOptions, cachePlugin, createMemoryCacheAdapter, scheduleBackground, withTimeout };
|
|
9
|
+
export { type CacheAdapter, type CacheAdapterResolver, type CacheAdapterSource, CacheEngine, type CacheEngineOptions, type CacheOptions, type CacheReadResult, CacheTimeoutError, DEFAULT_CACHEABLE_OPS, DEFAULT_INVALIDATING_OPS, DEFAULT_SHAPE_KEYS_BY_OP, type LogCallbacks, type MemoryCacheAdapterOptions, type RepositoryCacheHandle, type RepositoryCachePluginOptions, type SingleFlightClaim, type TimeoutAdapterOptions, cachePlugin, createMemoryCacheAdapter, scheduleBackground, withTimeout };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Plugin } from "../../repository/plugin-types.mjs";
|
|
2
2
|
import { CacheOptions, CacheReadResult } from "../options.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { CacheEngine } from "../engine.mjs";
|
|
3
|
+
import { CacheAdapterSource, CacheEngine } from "../engine.mjs";
|
|
5
4
|
import { DEFAULT_SHAPE_KEYS_BY_OP } from "./context.mjs";
|
|
6
5
|
//#region src/cache/plugin/index.d.ts
|
|
7
6
|
/** Default read ops the plugin caches. Kits may override per resource. */
|
|
@@ -24,8 +23,27 @@ interface LogCallbacks {
|
|
|
24
23
|
onInvalidate?: (model: string, version: number, tagCount: number) => void;
|
|
25
24
|
}
|
|
26
25
|
interface RepositoryCachePluginOptions {
|
|
27
|
-
/**
|
|
28
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Concrete adapter — Redis, in-memory, custom KV — OR a resolver consulted
|
|
28
|
+
* per call that returns the store for the current scope.
|
|
29
|
+
*
|
|
30
|
+
* The resolver form is what makes a cache narrower than the process
|
|
31
|
+
* possible (per-request, per-job, per-unit-of-work) without this package
|
|
32
|
+
* knowing what those are. Returning `undefined` means "no cache right now"
|
|
33
|
+
* and the call behaves exactly as `enabled: false` — it does not fall back
|
|
34
|
+
* to a shared store, because that fallback IS the cross-scope leak the
|
|
35
|
+
* scoping exists to prevent.
|
|
36
|
+
*
|
|
37
|
+
* @example Process-lifetime (unchanged)
|
|
38
|
+
* ```ts
|
|
39
|
+
* cachePlugin({ adapter: createMemoryCacheAdapter() })
|
|
40
|
+
* ```
|
|
41
|
+
* @example Request-scoped — dies with the request, no TTL needed
|
|
42
|
+
* ```ts
|
|
43
|
+
* cachePlugin({ adapter: () => requestScopedCache() })
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
readonly adapter: CacheAdapterSource;
|
|
29
47
|
/** Read ops the plugin caches. Default: every op in `DEFAULT_CACHEABLE_OPS`. */
|
|
30
48
|
readonly enabled?: readonly string[];
|
|
31
49
|
/** Mutating ops that trigger invalidation. Default: every op in `DEFAULT_INVALIDATING_OPS`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/repo-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Driver-agnostic repository primitives: hooks, Filter IR, operations, pagination, cache contract. Foundation for mongokit, sqlitekit, pgkit, and prismakit. Lean by design — no plugins ship here; each kit owns its own.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|