@cat-factory/caching 0.4.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 +21 -0
- package/README.md +76 -0
- package/dist/appCaches.d.ts +85 -0
- package/dist/appCaches.d.ts.map +1 -0
- package/dist/appCaches.js +147 -0
- package/dist/appCaches.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Savin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @cat-factory/caching
|
|
2
|
+
|
|
3
|
+
The app-level caching seam (see `docs/initiatives/caching-layer.md` in the repo).
|
|
4
|
+
`createAppCaches(options)` builds the named, typed read-through caches the services
|
|
5
|
+
consume through the kernel `AppCaches` port, implemented on
|
|
6
|
+
[`layered-loader`](https://github.com/kibertoad/layered-loader).
|
|
7
|
+
|
|
8
|
+
## Design rules
|
|
9
|
+
|
|
10
|
+
- **In-memory only.** Each cache is a per-replica LRU (`layered-loader` `GroupLoader`
|
|
11
|
+
over its in-memory tier). A replica always repopulates from its own data source on a
|
|
12
|
+
miss. There is deliberately **no Redis (or any async) data tier**.
|
|
13
|
+
- **Redis is an invalidation bus, never a data tier.** In a multi-node Node deployment
|
|
14
|
+
the facade injects a `notificationPairFactory` (built from layered-loader's
|
|
15
|
+
`createGroupNotificationPair` over dedicated ioredis clients, gated on `REDIS_URL`);
|
|
16
|
+
a write on one node then broadcasts the invalidated key/group so every peer drops its
|
|
17
|
+
in-memory entry. Only keys/groups travel on the wire — never values. Absent the
|
|
18
|
+
factory (single replica, local mode, tests) the loaders are bare in-memory with zero
|
|
19
|
+
extra dependency.
|
|
20
|
+
- **Invalidate after commit, at every write site.** The consuming service calls
|
|
21
|
+
`invalidate`/`invalidateGroup` (or the coarse `invalidateAll` for rare wide-blast
|
|
22
|
+
writes) after the DB write commits; layered-loader publishes to peers automatically.
|
|
23
|
+
- **Staleness probes for git-backed caches.** A profile with `ttlLeftBeforeRefreshInMsecs`
|
|
24
|
+
turns on preemptive in-memory refresh (layered-loader ≥ 14.5.3): an entry hit inside the
|
|
25
|
+
window runs the caller's per-read `isStillCurrent` probe (a sha/hash compare, strictly
|
|
26
|
+
cheaper than the load) in the background — TTL bump when the source hasn't moved, full
|
|
27
|
+
background reload otherwise. DB-backed invalidation-driven caches leave the window unset:
|
|
28
|
+
a DB read as a probe saves nothing over the DB read as the load.
|
|
29
|
+
- **Deep imports keep ioredis out of every runtime but Node.** layered-loader's root
|
|
30
|
+
index eagerly loads its Redis modules (and `ioredis`), so this package deep-imports
|
|
31
|
+
only the in-memory machinery. The Redis notification classes are loaded dynamically
|
|
32
|
+
by the Node facade alone, behind `REDIS_URL`.
|
|
33
|
+
|
|
34
|
+
## The Cloudflare Worker profile (`ISOLATE_SAFE_APP_CACHES_PROFILE`)
|
|
35
|
+
|
|
36
|
+
A Worker isolate has no cross-isolate invalidation bus and no Redis, so a TTL'd
|
|
37
|
+
in-isolate cache over **mutable cross-instance state** would serve stale data after a
|
|
38
|
+
write processed by another isolate — a correctness bug, not an optimization. The
|
|
39
|
+
Worker therefore wires the isolate-safe profile: caches of mutable state are
|
|
40
|
+
configured **pass-through** (`enabled: false` — every read runs its load), and only
|
|
41
|
+
caches of immutable or self-verifying entries (sha-pinned repo reads, static
|
|
42
|
+
catalogs) get real TTLs. Distributed invalidation is a
|
|
43
|
+
genuine Node-only concern, not a facade-parity gap: the Worker's cross-instance state
|
|
44
|
+
already lives in globally-addressed Durable Objects / D1. Revisit only if a
|
|
45
|
+
per-isolate staleness bug actually surfaces.
|
|
46
|
+
|
|
47
|
+
`fragmentDocumentBody` is the first self-verifying cache that stays **enabled** on the
|
|
48
|
+
Worker: its entries are external Confluence/Notion/GitHub/… page content re-validated
|
|
49
|
+
by the source's cheap version probe (`ttlLeftBeforeRefreshInMsecs` + `isStillCurrent`),
|
|
50
|
+
so a peer isolate's cached body self-heals within the refresh window without an
|
|
51
|
+
invalidation bus — its staleness is bounded by the probe, exactly like a sha-pinned
|
|
52
|
+
read. Only `fragmentCatalog`, which mirrors our own mutable D1 rows, passes through.
|
|
53
|
+
|
|
54
|
+
## Named caches
|
|
55
|
+
|
|
56
|
+
| Cache | Value | Group / key | Profile |
|
|
57
|
+
| ---------------------- | ----------------------------------------------- | ------------------------------------------ | -------------------------------------------- |
|
|
58
|
+
| `fragmentCatalog` | merged per-workspace catalog | `workspaceId` / `workspaceId` | TTL + invalidation; pass-through on Worker |
|
|
59
|
+
| `fragmentDocumentBody` | a document-backed fragment's live external body | `viaWorkspaceId` / `<source>:<externalId>` | TTL + version probe; enabled on both facades |
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { createAppCaches } from '@cat-factory/caching'
|
|
65
|
+
|
|
66
|
+
// Node facade (multi-node): inject the Redis-backed notification pair factory.
|
|
67
|
+
const caches = createAppCaches({ notificationPairFactory, logger })
|
|
68
|
+
|
|
69
|
+
// Cloudflare Worker: the isolate-safe profile.
|
|
70
|
+
const caches = createAppCaches({ profile: ISOLATE_SAFE_APP_CACHES_PROFILE })
|
|
71
|
+
|
|
72
|
+
// A consuming service reads through its named handle…
|
|
73
|
+
const catalog = await caches.fragmentCatalog.get(key, workspaceId, () => loadCatalog())
|
|
74
|
+
// …and every write path invalidates after commit.
|
|
75
|
+
await caches.fragmentCatalog.invalidateGroup(workspaceId)
|
|
76
|
+
```
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { AppCaches } from '@cat-factory/kernel';
|
|
2
|
+
import type { AbstractNotificationConsumer } from 'layered-loader/dist/lib/notifications/AbstractNotificationConsumer.js';
|
|
3
|
+
import type { GroupNotificationPublisher } from 'layered-loader/dist/lib/notifications/GroupNotificationPublisher.js';
|
|
4
|
+
import type { InMemoryGroupCache } from 'layered-loader/dist/lib/memory/InMemoryGroupCache.js';
|
|
5
|
+
import type { Logger } from 'layered-loader/dist/lib/util/Logger.js';
|
|
6
|
+
/** Per-cache tuning knobs; a facade passes a profile so TTLs can differ per runtime. */
|
|
7
|
+
export interface GroupCacheProfile {
|
|
8
|
+
/**
|
|
9
|
+
* `false` ⇒ pass-through: no in-memory tier is built and every read runs its
|
|
10
|
+
* load. The Worker's isolate-safe stance for caches of MUTABLE cross-instance
|
|
11
|
+
* state — an isolate has no cross-isolate invalidation bus, so a TTL'd cache
|
|
12
|
+
* there would serve stale data after a write on another isolate.
|
|
13
|
+
*/
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
/** Entry freshness backstop; invalidation, not the TTL, is the coherence story. */
|
|
16
|
+
ttlInMsecs: number;
|
|
17
|
+
/** LRU bound on distinct groups (workspaces, typically). */
|
|
18
|
+
maxGroups: number;
|
|
19
|
+
/** LRU bound on entries within one group. */
|
|
20
|
+
maxItemsPerGroup: number;
|
|
21
|
+
/**
|
|
22
|
+
* Preemptive-refresh window for git-backed caches (layered-loader ≥ 14.5.3
|
|
23
|
+
* supports it in-memory-only): an entry hit with less than this much TTL left
|
|
24
|
+
* refreshes in the background — via the caller's cheap `isStillCurrent` probe
|
|
25
|
+
* (TTL bump when the source hasn't moved) when one is passed to `get`, else a
|
|
26
|
+
* full background reload. Unset ⇒ entries simply expire at `ttlInMsecs`
|
|
27
|
+
* (correct for the invalidation-driven DB-backed caches, where a probe would
|
|
28
|
+
* cost as much as the load).
|
|
29
|
+
*/
|
|
30
|
+
ttlLeftBeforeRefreshInMsecs?: number;
|
|
31
|
+
}
|
|
32
|
+
/** One profile entry per named cache in the kernel {@link AppCaches} bag. */
|
|
33
|
+
export interface AppCachesProfile {
|
|
34
|
+
fragmentCatalog: GroupCacheProfile;
|
|
35
|
+
fragmentDocumentBody: GroupCacheProfile;
|
|
36
|
+
repoProjection: GroupCacheProfile;
|
|
37
|
+
}
|
|
38
|
+
/** The default (Node/local/test) profile: caching on, modest bounds. */
|
|
39
|
+
export declare const DEFAULT_APP_CACHES_PROFILE: AppCachesProfile;
|
|
40
|
+
/**
|
|
41
|
+
* The Cloudflare Worker profile: every cache of mutable cross-instance state is
|
|
42
|
+
* pass-through, because a Worker isolate has no cross-isolate invalidation bus
|
|
43
|
+
* (and no Redis) — see the package README. Caches of immutable or self-verifying
|
|
44
|
+
* entries (sha-pinned reads, static catalogs) may enable real TTLs here.
|
|
45
|
+
*
|
|
46
|
+
* `fragmentDocumentBody` stays ENABLED here: its entries are external page content
|
|
47
|
+
* re-validated by a cheap version probe, so a peer isolate's cached body self-heals
|
|
48
|
+
* within the refresh window without an invalidation bus (the same reasoning that
|
|
49
|
+
* lets sha-pinned reads keep a TTL on the Worker) — its staleness is bounded by the
|
|
50
|
+
* probe, not indefinite. Only `fragmentCatalog`, which mirrors our own mutable D1
|
|
51
|
+
* state, must pass through.
|
|
52
|
+
*/
|
|
53
|
+
export declare const ISOLATE_SAFE_APP_CACHES_PROFILE: AppCachesProfile;
|
|
54
|
+
/**
|
|
55
|
+
* A per-cache invalidation-notification pair (layered-loader's group publisher +
|
|
56
|
+
* consumer). Produced by the facade's factory — Redis-backed in a multi-node Node
|
|
57
|
+
* deployment, a fake sharing an in-memory bus in tests.
|
|
58
|
+
*/
|
|
59
|
+
export interface GroupCacheNotifications<T> {
|
|
60
|
+
publisher: GroupNotificationPublisher<T>;
|
|
61
|
+
consumer: AbstractNotificationConsumer<T, InMemoryGroupCache<T>>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Builds the notification pair for one named cache (each cache gets its own
|
|
65
|
+
* channel, `<prefix>:<cacheName>`). Returning `undefined` leaves that cache bare
|
|
66
|
+
* in-memory. The factory is per-CACHE so a facade can wire dedicated clients per
|
|
67
|
+
* channel — layered-loader closes a pair's clients with its loader.
|
|
68
|
+
*/
|
|
69
|
+
export type GroupNotificationPairFactory = <T>(cacheName: string) => GroupCacheNotifications<T> | undefined;
|
|
70
|
+
export interface CreateAppCachesOptions {
|
|
71
|
+
/** Per-cache overrides merged over {@link DEFAULT_APP_CACHES_PROFILE}. */
|
|
72
|
+
profile?: Partial<AppCachesProfile>;
|
|
73
|
+
/** Absent ⇒ bare in-memory loaders (single replica, local mode, tests). */
|
|
74
|
+
notificationPairFactory?: GroupNotificationPairFactory;
|
|
75
|
+
/** Error sink for background cache/notification failures (a pino logger fits). */
|
|
76
|
+
logger?: Logger;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build the app-owned cache bag. Called once per process by a facade's
|
|
80
|
+
* composition root and threaded through the dependency bag as the kernel
|
|
81
|
+
* {@link AppCaches} port; `createCore` builds a bare default when a harness
|
|
82
|
+
* passes none.
|
|
83
|
+
*/
|
|
84
|
+
export declare function createAppCaches(options?: CreateAppCachesOptions): AppCaches;
|
|
85
|
+
//# sourceMappingURL=appCaches.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appCaches.d.ts","sourceRoot":"","sources":["../src/appCaches.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EAKV,MAAM,qBAAqB,CAAA;AAM5B,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,uEAAuE,CAAA;AACzH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qEAAqE,CAAA;AACrH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sDAAsD,CAAA;AAC9F,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wCAAwC,CAAA;AAUpE,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAA;IACjB,6CAA6C;IAC7C,gBAAgB,EAAE,MAAM,CAAA;IACxB;;;;;;;;OAQG;IACH,2BAA2B,CAAC,EAAE,MAAM,CAAA;CACrC;AAED,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,iBAAiB,CAAA;IAClC,oBAAoB,EAAE,iBAAiB,CAAA;IACvC,cAAc,EAAE,iBAAiB,CAAA;CAClC;AAED,wEAAwE;AACxE,eAAO,MAAM,0BAA0B,EAAE,gBAmBxC,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,+BAA+B,EAAE,gBAQ7C,CAAA;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB,CAAC,CAAC;IACxC,SAAS,EAAE,0BAA0B,CAAC,CAAC,CAAC,CAAA;IACxC,QAAQ,EAAE,4BAA4B,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAA;CACjE;AAED;;;;;GAKG;AACH,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,EAC3C,SAAS,EAAE,MAAM,KACd,uBAAuB,CAAC,CAAC,CAAC,GAAG,SAAS,CAAA;AAE3C,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,OAAO,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IACnC,2EAA2E;IAC3E,uBAAuB,CAAC,EAAE,4BAA4B,CAAA;IACtD,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAqGD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,SAAS,CA6B/E"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Deep imports on purpose: layered-loader's root index eagerly requires its Redis
|
|
2
|
+
// modules (and thereby `ioredis`), which must never load outside the Node facade's
|
|
3
|
+
// REDIS_URL-gated notification wiring — the Worker imports this package too. The
|
|
4
|
+
// non-Redis modules below pull in only in-memory machinery.
|
|
5
|
+
import { GroupLoader } from 'layered-loader/dist/lib/GroupLoader.js';
|
|
6
|
+
/** The default (Node/local/test) profile: caching on, modest bounds. */
|
|
7
|
+
export const DEFAULT_APP_CACHES_PROFILE = {
|
|
8
|
+
// One merged catalog per workspace; the key varies only when the workspace's
|
|
9
|
+
// account changes, so a small per-group bound is plenty.
|
|
10
|
+
fragmentCatalog: { enabled: true, ttlInMsecs: 5 * 60_000, maxGroups: 500, maxItemsPerGroup: 4 },
|
|
11
|
+
// The live external body of a document-backed fragment, grouped by workspace and
|
|
12
|
+
// keyed per document. Self-verifying: an entry entering the last minute of its TTL
|
|
13
|
+
// runs the source's cheap version probe (bump on unchanged, background reload on
|
|
14
|
+
// change) so a run never blocks on a live page fetch.
|
|
15
|
+
fragmentDocumentBody: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
ttlInMsecs: 5 * 60_000,
|
|
18
|
+
maxGroups: 500,
|
|
19
|
+
maxItemsPerGroup: 64,
|
|
20
|
+
ttlLeftBeforeRefreshInMsecs: 60_000,
|
|
21
|
+
},
|
|
22
|
+
// One repo-projection list per workspace, keyed by workspace id (so exactly one
|
|
23
|
+
// entry per group). Invalidation-driven — no version probe (a DB read as the probe
|
|
24
|
+
// would cost as much as the DB read as the load).
|
|
25
|
+
repoProjection: { enabled: true, ttlInMsecs: 5 * 60_000, maxGroups: 1000, maxItemsPerGroup: 1 },
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The Cloudflare Worker profile: every cache of mutable cross-instance state is
|
|
29
|
+
* pass-through, because a Worker isolate has no cross-isolate invalidation bus
|
|
30
|
+
* (and no Redis) — see the package README. Caches of immutable or self-verifying
|
|
31
|
+
* entries (sha-pinned reads, static catalogs) may enable real TTLs here.
|
|
32
|
+
*
|
|
33
|
+
* `fragmentDocumentBody` stays ENABLED here: its entries are external page content
|
|
34
|
+
* re-validated by a cheap version probe, so a peer isolate's cached body self-heals
|
|
35
|
+
* within the refresh window without an invalidation bus (the same reasoning that
|
|
36
|
+
* lets sha-pinned reads keep a TTL on the Worker) — its staleness is bounded by the
|
|
37
|
+
* probe, not indefinite. Only `fragmentCatalog`, which mirrors our own mutable D1
|
|
38
|
+
* state, must pass through.
|
|
39
|
+
*/
|
|
40
|
+
export const ISOLATE_SAFE_APP_CACHES_PROFILE = {
|
|
41
|
+
fragmentCatalog: { ...DEFAULT_APP_CACHES_PROFILE.fragmentCatalog, enabled: false },
|
|
42
|
+
fragmentDocumentBody: { ...DEFAULT_APP_CACHES_PROFILE.fragmentDocumentBody },
|
|
43
|
+
// Pass-through: the repo projection is our own mutable D1 state, and a Worker
|
|
44
|
+
// isolate has no cross-isolate invalidation bus (unlike `fragmentDocumentBody`,
|
|
45
|
+
// whose external entries self-verify via a version probe). So the Worker reads it
|
|
46
|
+
// live every time, exactly like `fragmentCatalog`.
|
|
47
|
+
repoProjection: { ...DEFAULT_APP_CACHES_PROFILE.repoProjection, enabled: false },
|
|
48
|
+
};
|
|
49
|
+
class LayeredGroupCacheHandle {
|
|
50
|
+
loader;
|
|
51
|
+
constructor(name, profile, notifications, logger) {
|
|
52
|
+
this.loader = new GroupLoader({
|
|
53
|
+
inMemoryCache: profile.enabled
|
|
54
|
+
? {
|
|
55
|
+
cacheId: name,
|
|
56
|
+
cacheType: 'lru-object',
|
|
57
|
+
groupCacheType: 'lru-object',
|
|
58
|
+
ttlInMsecs: profile.ttlInMsecs,
|
|
59
|
+
maxGroups: profile.maxGroups,
|
|
60
|
+
maxItemsPerGroup: profile.maxItemsPerGroup,
|
|
61
|
+
...(profile.ttlLeftBeforeRefreshInMsecs
|
|
62
|
+
? { ttlLeftBeforeRefreshInMsecs: profile.ttlLeftBeforeRefreshInMsecs }
|
|
63
|
+
: {}),
|
|
64
|
+
}
|
|
65
|
+
: false,
|
|
66
|
+
// The read-through source: each `get` carries its own load closure, so the
|
|
67
|
+
// owning service keeps its load logic and the loader keeps in-flight dedup.
|
|
68
|
+
dataSources: [
|
|
69
|
+
{
|
|
70
|
+
name: `${name}-load`,
|
|
71
|
+
getFromGroup: (params) => params.load(),
|
|
72
|
+
getManyFromGroup: () => Promise.reject(new Error(`cache '${name}' does not support getMany`)),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
cacheKeyFromLoadParamsResolver: (params) => params.key,
|
|
76
|
+
// The staleness probe rides the per-read load params, like the load itself.
|
|
77
|
+
// Wired only when the profile configures a refresh window (layered-loader
|
|
78
|
+
// rejects a probe with no window to fire in); a read that passed no probe
|
|
79
|
+
// reports stale, degrading to the default full background reload. A null
|
|
80
|
+
// cached value (resolved-but-empty) is re-loaded rather than probed.
|
|
81
|
+
...(profile.enabled && profile.ttlLeftBeforeRefreshInMsecs
|
|
82
|
+
? {
|
|
83
|
+
isEntryStillCurrentFn: (cached, params) => cached !== null && params.isStillCurrent
|
|
84
|
+
? params.isStillCurrent(cached)
|
|
85
|
+
: Promise.resolve(false),
|
|
86
|
+
}
|
|
87
|
+
: {}),
|
|
88
|
+
// A notification pair only makes sense with an in-memory tier to invalidate
|
|
89
|
+
// (layered-loader rejects the combination outright).
|
|
90
|
+
...(profile.enabled && notifications
|
|
91
|
+
? {
|
|
92
|
+
notificationConsumer: notifications.consumer,
|
|
93
|
+
notificationPublisher: notifications.publisher,
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
96
|
+
...(logger ? { logger } : {}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async get(key, group, load, isStillCurrent) {
|
|
100
|
+
// The data source always resolves to the load's result, and load errors
|
|
101
|
+
// propagate (throwIfLoadError defaults on) — so a non-value here is impossible
|
|
102
|
+
// unless T itself includes null.
|
|
103
|
+
return (await this.loader.get({ key, load, isStillCurrent }, group));
|
|
104
|
+
}
|
|
105
|
+
invalidate(key, group) {
|
|
106
|
+
return this.loader.invalidateCacheFor(key, group);
|
|
107
|
+
}
|
|
108
|
+
invalidateGroup(group) {
|
|
109
|
+
return this.loader.invalidateCacheForGroup(group);
|
|
110
|
+
}
|
|
111
|
+
invalidateAll() {
|
|
112
|
+
return this.loader.invalidateCache();
|
|
113
|
+
}
|
|
114
|
+
/** Releases the notification pair's resources along with the loader. */
|
|
115
|
+
close() {
|
|
116
|
+
return this.loader.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Build the app-owned cache bag. Called once per process by a facade's
|
|
121
|
+
* composition root and threaded through the dependency bag as the kernel
|
|
122
|
+
* {@link AppCaches} port; `createCore` builds a bare default when a harness
|
|
123
|
+
* passes none.
|
|
124
|
+
*/
|
|
125
|
+
export function createAppCaches(options = {}) {
|
|
126
|
+
const profile = { ...DEFAULT_APP_CACHES_PROFILE, ...options.profile };
|
|
127
|
+
const fragmentCatalog = buildGroupCache('fragment-catalog', profile.fragmentCatalog, options);
|
|
128
|
+
const fragmentDocumentBody = buildGroupCache('fragment-document-body', profile.fragmentDocumentBody, options);
|
|
129
|
+
const repoProjection = buildGroupCache('repo-projection', profile.repoProjection, options);
|
|
130
|
+
return {
|
|
131
|
+
fragmentCatalog,
|
|
132
|
+
fragmentDocumentBody,
|
|
133
|
+
repoProjection,
|
|
134
|
+
close: async () => {
|
|
135
|
+
await Promise.all([
|
|
136
|
+
fragmentCatalog.close(),
|
|
137
|
+
fragmentDocumentBody.close(),
|
|
138
|
+
repoProjection.close(),
|
|
139
|
+
]);
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function buildGroupCache(name, profile, options) {
|
|
144
|
+
const notifications = profile.enabled ? options.notificationPairFactory?.(name) : undefined;
|
|
145
|
+
return new LayeredGroupCacheHandle(name, profile, notifications, options.logger);
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=appCaches.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appCaches.js","sourceRoot":"","sources":["../src/appCaches.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createAppCaches, DEFAULT_APP_CACHES_PROFILE, ISOLATE_SAFE_APP_CACHES_PROFILE, } from './appCaches.js';
|
|
2
|
+
export type { AppCachesProfile, CreateAppCachesOptions, GroupCacheNotifications, GroupCacheProfile, GroupNotificationPairFactory, } from './appCaches.js';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,0BAA0B,EAC1B,+BAA+B,GAChC,MAAM,gBAAgB,CAAA;AACvB,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EACjB,4BAA4B,GAC7B,MAAM,gBAAgB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,0BAA0B,EAC1B,+BAA+B,GAChC,MAAM,gBAAgB,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cat-factory/caching",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "The app-level caching seam for the Agent Architecture Board (docs/initiatives/caching-layer.md): createAppCaches builds the named, typed in-memory read-through caches (layered-loader) the services consume via the kernel AppCaches port, with optional distributed invalidation over an injected Redis notification pair. Redis is only ever an invalidation bus, never a data tier.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/kibertoad/cat-factory.git",
|
|
8
|
+
"directory": "backend/packages/caching"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"layered-loader": "^14.5.3",
|
|
28
|
+
"@cat-factory/kernel": "0.85.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^26.0.1",
|
|
32
|
+
"typescript": "7.0.1-rc",
|
|
33
|
+
"vitest": "^4.1.9"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -b tsconfig.build.json",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
38
|
+
"test": "vitest",
|
|
39
|
+
"test:run": "vitest run"
|
|
40
|
+
}
|
|
41
|
+
}
|