@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 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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"}
@@ -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,2 @@
1
+ export { createAppCaches, DEFAULT_APP_CACHES_PROFILE, ISOLATE_SAFE_APP_CACHES_PROFILE, } from './appCaches.js';
2
+ //# sourceMappingURL=index.js.map
@@ -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
+ }