@classytic/repo-core 0.26.1 → 0.27.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 +17 -0
- package/dist/cache/index.d.mts +2 -1
- package/dist/cache/index.mjs +2 -1
- package/dist/cache/memo.d.mts +30 -0
- package/dist/cache/memo.mjs +47 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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.27.0] - 2026-09-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`createTtlMemo(load, { ttlMs, keyOf?, maxEntries? })`** on `./cache` — the
|
|
12
|
+
one way to memoise a keyed async load for a while. It is `CacheEngine.prefetch`
|
|
13
|
+
over the memory adapter, so concurrent misses for one key share ONE load, a
|
|
14
|
+
thrown load caches nothing, and a `null` result is an answer.
|
|
15
|
+
|
|
16
|
+
Four packages had each hand-rolled `Map<key, { at, value }>` with a TTL, and
|
|
17
|
+
every one wrote its entry AFTER the await. Under parallel event dispatch N
|
|
18
|
+
callers reach the await together, all miss, and the cache only ever helps the
|
|
19
|
+
caller after them — a 30 s TTL that measured nine reads of one row per event.
|
|
20
|
+
`tests/unit/cache/ttl-memo.test.ts` pins single flight, expiry, invalidation,
|
|
21
|
+
`ttlMs: 0` = off, and refuses an object key without `keyOf` (an
|
|
22
|
+
identity-compared key never hits and reads as a caching bug).
|
|
23
|
+
|
|
7
24
|
## [0.26.1] - 2026-09-08
|
|
8
25
|
|
|
9
26
|
### Fixed
|
package/dist/cache/index.d.mts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { CacheOptions, CacheReadResult } from "./options.mjs";
|
|
2
2
|
import { CacheAdapter } from "./types.mjs";
|
|
3
3
|
import { CacheAdapterResolver, CacheAdapterSource, CacheEngine, CacheEngineOptions, SingleFlightClaim } from "./engine.mjs";
|
|
4
|
+
import { TtlMemo, TtlMemoOptions, createTtlMemo } from "./memo.mjs";
|
|
4
5
|
import { MemoryCacheAdapterOptions, createMemoryCacheAdapter } from "./memory-adapter.mjs";
|
|
5
6
|
import { DEFAULT_SHAPE_KEYS_BY_OP } from "./plugin/context.mjs";
|
|
6
7
|
import { DEFAULT_CACHEABLE_OPS, DEFAULT_INVALIDATING_OPS, LogCallbacks, RepositoryCacheHandle, RepositoryCachePluginOptions, cachePlugin } from "./plugin/index.mjs";
|
|
7
8
|
import { scheduleBackground } from "./runtime.mjs";
|
|
8
9
|
import { CacheTimeoutError, TimeoutAdapterOptions, withTimeout } from "./timeout-adapter.mjs";
|
|
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 };
|
|
10
|
+
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, type TtlMemo, type TtlMemoOptions, cachePlugin, createMemoryCacheAdapter, createTtlMemo, scheduleBackground, withTimeout };
|
package/dist/cache/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CacheEngine } from "./engine.mjs";
|
|
2
2
|
import { createMemoryCacheAdapter } from "./memory-adapter.mjs";
|
|
3
|
+
import { createTtlMemo } from "./memo.mjs";
|
|
3
4
|
import { DEFAULT_SHAPE_KEYS_BY_OP } from "./plugin/context.mjs";
|
|
4
5
|
import { scheduleBackground } from "./runtime.mjs";
|
|
5
6
|
import { DEFAULT_CACHEABLE_OPS, DEFAULT_INVALIDATING_OPS, cachePlugin } from "./plugin/index.mjs";
|
|
6
7
|
import { CacheTimeoutError, withTimeout } from "./timeout-adapter.mjs";
|
|
7
|
-
export { CacheEngine, CacheTimeoutError, DEFAULT_CACHEABLE_OPS, DEFAULT_INVALIDATING_OPS, DEFAULT_SHAPE_KEYS_BY_OP, cachePlugin, createMemoryCacheAdapter, scheduleBackground, withTimeout };
|
|
8
|
+
export { CacheEngine, CacheTimeoutError, DEFAULT_CACHEABLE_OPS, DEFAULT_INVALIDATING_OPS, DEFAULT_SHAPE_KEYS_BY_OP, cachePlugin, createMemoryCacheAdapter, createTtlMemo, scheduleBackground, withTimeout };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/cache/memo.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `createTtlMemo` — memoise a keyed async load for `ttlMs`, single-flight.
|
|
4
|
+
*
|
|
5
|
+
* Use this instead of a `Map<key, { at, value }>`. Every hand-rolled one wrote
|
|
6
|
+
* its entry AFTER the await, so N concurrent callers all missed and the cache
|
|
7
|
+
* only ever helped the caller after them. This is `CacheEngine.prefetch` over
|
|
8
|
+
* the memory adapter: concurrent misses share one load, a thrown load caches
|
|
9
|
+
* nothing, and a `null` result is an answer.
|
|
10
|
+
*/
|
|
11
|
+
interface TtlMemoOptions<K> {
|
|
12
|
+
/** Freshness window. `0` disables memoisation — every call loads. */
|
|
13
|
+
readonly ttlMs: number;
|
|
14
|
+
/**
|
|
15
|
+
* Key derivation. REQUIRED for non-primitive keys: an object key would be
|
|
16
|
+
* identity-compared, never hit, and read as a caching bug. Fold anything that
|
|
17
|
+
* changes the answer into it (a revision counter, a tenant).
|
|
18
|
+
*/
|
|
19
|
+
readonly keyOf?: (key: K) => string;
|
|
20
|
+
/** LRU ceiling. Default 10,000. */
|
|
21
|
+
readonly maxEntries?: number;
|
|
22
|
+
}
|
|
23
|
+
interface TtlMemo<K, V> {
|
|
24
|
+
get(key: K): Promise<V>;
|
|
25
|
+
/** Drop one key, or everything. */
|
|
26
|
+
invalidate(key?: K): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
declare function createTtlMemo<K, V>(load: (key: K) => Promise<V>, options: TtlMemoOptions<K>): TtlMemo<K, V>;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { TtlMemo, TtlMemoOptions, createTtlMemo };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CacheEngine } from "./engine.mjs";
|
|
2
|
+
import { createMemoryCacheAdapter } from "./memory-adapter.mjs";
|
|
3
|
+
//#region src/cache/memo.ts
|
|
4
|
+
/**
|
|
5
|
+
* `createTtlMemo` — memoise a keyed async load for `ttlMs`, single-flight.
|
|
6
|
+
*
|
|
7
|
+
* Use this instead of a `Map<key, { at, value }>`. Every hand-rolled one wrote
|
|
8
|
+
* its entry AFTER the await, so N concurrent callers all missed and the cache
|
|
9
|
+
* only ever helped the caller after them. This is `CacheEngine.prefetch` over
|
|
10
|
+
* the memory adapter: concurrent misses share one load, a thrown load caches
|
|
11
|
+
* nothing, and a `null` result is an answer.
|
|
12
|
+
*/
|
|
13
|
+
const PREFIX = "memo";
|
|
14
|
+
function createTtlMemo(load, options) {
|
|
15
|
+
const { ttlMs, keyOf, maxEntries } = options;
|
|
16
|
+
const adapter = createMemoryCacheAdapter({ maxEntries: maxEntries ?? 1e4 });
|
|
17
|
+
const engine = new CacheEngine(adapter, { prefix: PREFIX });
|
|
18
|
+
const resolved = {
|
|
19
|
+
staleTime: Math.max(0, ttlMs) / 1e3,
|
|
20
|
+
gcTime: 0,
|
|
21
|
+
tags: [],
|
|
22
|
+
bypass: false,
|
|
23
|
+
swr: false,
|
|
24
|
+
enabled: ttlMs > 0
|
|
25
|
+
};
|
|
26
|
+
const toKey = (key) => {
|
|
27
|
+
if (keyOf) return `${PREFIX}:${keyOf(key)}`;
|
|
28
|
+
const t = typeof key;
|
|
29
|
+
if (t === "string" || t === "number" || t === "boolean") return `${PREFIX}:${String(key)}`;
|
|
30
|
+
throw new TypeError(`createTtlMemo: a ${key === null ? "null" : t} key needs keyOf(...) — an identity-compared key never hits`);
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
get(key) {
|
|
34
|
+
const k = toKey(key);
|
|
35
|
+
return engine.prefetch(k, resolved, () => load(key));
|
|
36
|
+
},
|
|
37
|
+
async invalidate(key) {
|
|
38
|
+
if (key === void 0) {
|
|
39
|
+
await engine.clear();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await adapter.delete(toKey(key));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { createTtlMemo };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/repo-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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,
|