@baeta/cache 0.0.0 → 2.0.0-next.12

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 ADDED
@@ -0,0 +1,7 @@
1
+ # @baeta/cache
2
+
3
+ ## 2.0.0-next.12
4
+
5
+ ### Minor Changes
6
+
7
+ - [#275](https://github.com/andreisergiu98/baeta/pull/275) [`54f4361`](https://github.com/andreisergiu98/baeta/commit/54f4361c857f2a3d32a29dd898b2be648ac0bb2c) Thanks [@andreisergiu98](https://github.com/andreisergiu98)! - Decouple cache from graphql
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Pampu Andrei
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,164 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/andreisergiu98/baeta/refs/heads/main/website/static/img/logo-baeta.svg" alt="Baeta Logo" width="150"/>
3
+ </p>
4
+
5
+ <div align="center">
6
+ <h1>Baeta</h1>
7
+ <a href="https://www.npmjs.com/package/@baeta/cli"><img src="https://img.shields.io/npm/v/@baeta/cli.svg?style=flat" /></a>
8
+ <a href="https://github.com/andreisergiu98/baeta/actions/workflows/checks.yml"><img src="https://img.shields.io/github/actions/workflow/status/andreisergiu98/baeta/checks.yml" /></a>
9
+ <a href="https://github.com/andreisergiu98/baeta/pulls"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" /></a>
10
+ <a href="https://github.com/andreisergiu98/baeta/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" /></a>
11
+ <br />
12
+ <br />
13
+ <a href="https://baeta.io/docs/getting-started/installation">Getting Started</a>
14
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
15
+ <a href="https://www.baeta.io/">Website</a>
16
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
17
+ <a href="https://baeta.io/docs/intro">Docs</a>
18
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
19
+ <a href="https://github.com/andreisergiu98/baeta/tree/main/examples">Examples</a>
20
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
21
+ <a href="https://discord.gg/BHFXHvyj">Discord</a>
22
+ <br />
23
+ <hr />
24
+ </div>
25
+
26
+ # What is Baeta?
27
+
28
+ Building GraphQL APIs shouldn't be complicated. **Baeta** is a modern, modular, open-source GraphQL framework designed with flexibility in mind. It follows a granular approach where you only add what you need, helping developers focus on what matters most - creating powerful, scalable APIs without the boilerplate.
29
+
30
+ ### Key Features
31
+
32
+ - **Modular Architecture**: Organize your API into manageable modules
33
+ - **Schema-First Development**: Define your API contract upfront
34
+ - **Type Safety**: Automatic code generation for type-safe development
35
+ - **Middleware & Directives**: Easy integration of custom behaviors
36
+ - **High Performance**: Built for scalability and efficiency
37
+
38
+ #### And optional extensions and plugins
39
+
40
+ - **@baeta/extension-auth**: Add powerful scope-based authorization
41
+ - **@baeta/extension-cache**: Implement automatic caching with simple update patterns
42
+ - ... and more!
43
+
44
+ ## Why use Baeta?
45
+
46
+ Baeta makes it easy to build better GraphQL APIs while staying flexible. Here's how:
47
+
48
+ **Granular and Progressive:** Start small and add features as you need them. Whether you're building a simple API or a complex system, Baeta scales with your needs.
49
+
50
+ **Modular architecture:** Baeta's modular design allows you to organize your GraphQL API into smaller, more manageable modules that can be added or removed as needed. This makes it easier to maintain and scale your API over time.
51
+
52
+ **Schema-first approach:** With Baeta, you define your schema first, and then logic and resolvers. This approach ensures a consistent and well-defined API for your clients and reduces boilerplate code.
53
+
54
+ ## How it Works
55
+
56
+ #### 1. Define your schema
57
+
58
+ ```graphql
59
+ type User {
60
+ id: ID!
61
+ name: String!
62
+ email: String!
63
+ age: Int
64
+ }
65
+
66
+ input UserWhereUnique {
67
+ id: ID
68
+ email: String
69
+ }
70
+
71
+ type Query {
72
+ user(where: UserWhereUnique!): User!
73
+ users: [User!]!
74
+ }
75
+ ```
76
+
77
+ #### 2. Implement your resolvers
78
+
79
+ ```typescript
80
+ import { UserModule } from "./typedef.ts";
81
+
82
+ const { Query } = UserModule;
83
+
84
+ const userQuery = Query.user.resolve(({ args }) => {
85
+ return dataSource.user.find(args.where);
86
+ });
87
+
88
+ const usersQuery = Query.users.resolve(() => {
89
+ return dataSource.user.findMany();
90
+ });
91
+
92
+ Query.$fields({
93
+ user: userQuery,
94
+ users: usersQuery,
95
+ });
96
+ ```
97
+
98
+ #### 3. Add authorization
99
+
100
+ ```typescript
101
+ import { UserModule } from "./typedef.ts";
102
+
103
+ const { Query } = UserModule;
104
+
105
+ const userQuery = Query.user
106
+ .$auth({
107
+ $or: {
108
+ isPublic: true,
109
+ isLoggedIn: true,
110
+ },
111
+ })
112
+ .resolve(async ({ args }) => {
113
+ // ...
114
+ });
115
+ ```
116
+
117
+ #### 4. Add caching
118
+
119
+ ```typescript
120
+ const { Query, Mutation, User } = UserModule;
121
+
122
+ export const userCache = User.$createCache();
123
+
124
+ const userQuery = Query.user
125
+ .$auth({
126
+ // ...
127
+ })
128
+ .$useCache(userCache)
129
+ .resolve(async ({ args }) => {
130
+ // ...
131
+ });
132
+
133
+ const updateUserMutation = Mutation.updateUser
134
+ .$use(async (next) => {
135
+ const user = await next();
136
+ await userCache.save(user);
137
+ return user;
138
+ })
139
+ .resolve(async ({ args }) => {
140
+ // ...
141
+ });
142
+ ```
143
+
144
+ ## Compatibility
145
+
146
+ Baeta is compatible with all GraphQL servers, which makes it easy to integrate with your existing stack. It works seamlessly with popular GraphQL server libraries such as **Graphql Yoga** and **Apollo Server**, as well as other popular tools like **Prisma**, **Drizzle** and **Kysely**.
147
+
148
+ Baeta's development tools are built for Node.js, but the runtime code is environment-agnostic. This means your Baeta applications can run anywhere JavaScript runs, including:
149
+
150
+ - Deno
151
+ - Cloudflare Workers
152
+ - AWS Lambda
153
+ - Vercel Edge Functions
154
+ - Bun
155
+ - Node.js
156
+ - Any other JavaScript runtime
157
+
158
+ ## Credits
159
+
160
+ Baeta was inspired by several amazing projects and people in the GraphQL ecosystem. Check out our [Credits page](https://baeta.io/docs/credits) to learn more about the individuals and projects that influenced Baeta's development.
161
+
162
+ ## License
163
+
164
+ Baeta is licensed under the [MIT License](./LICENSE).
@@ -0,0 +1,281 @@
1
+ import DataLoader from "dataloader";
2
+
3
+ //#region lib/item.d.ts
4
+ /**
5
+ * Reference type for cached items
6
+ */
7
+ type ItemRef = string | number | bigint;
8
+ /**
9
+ * Type constraint for objects that are compatible with default cache ref
10
+ */
11
+ type RefCompatibleItem = {
12
+ id: string | number | bigint;
13
+ };
14
+ //#endregion
15
+ //#region lib/query.d.ts
16
+ type QueryIndex = [string, QueryIndexValue];
17
+ type QueryIndexValue = string | number | boolean | null;
18
+ type QueryArgs = Record<string, string | number | boolean | null | Array<string | number | boolean | null> | Record<string, string | number | boolean | null>>;
19
+ interface QueryTag {
20
+ name: string;
21
+ revision: number | undefined;
22
+ }
23
+ interface QueryTagWithIndexes extends QueryTag {
24
+ indexes: QueryIndex[];
25
+ }
26
+ interface QueryTagWithArgs extends QueryTag {
27
+ args: QueryArgs;
28
+ }
29
+ type QueryArgsIndexes<Args extends QueryArgs, T = Record<string, never>> = { [K in keyof Args]?: Args[K] extends QueryIndexValue ? true : never } & Record<Exclude<keyof T, keyof Args>, never>;
30
+ type IndexedQueryArgs<Args extends QueryArgs, Indexes extends QueryArgsIndexes<Args>> = Pick<Args, keyof Indexes & keyof Args>;
31
+ interface QueryTagWithData<T> extends QueryTagWithArgs {
32
+ indexes: QueryIndex[];
33
+ data: null | T | Array<T | null>;
34
+ }
35
+ type QueryHelpersWithoutIndexes = {
36
+ invalidateAll: () => Promise<void>;
37
+ };
38
+ type QueryHelpersWithIndexes<Args extends QueryArgs, Indexes extends QueryArgsIndexes<Args>> = {
39
+ invalidateAll: () => Promise<void>;
40
+ invalidateByArgs: (args: Partial<IndexedQueryArgs<Args, Indexes>> | Partial<IndexedQueryArgs<Args, Indexes>>[]) => Promise<void>;
41
+ };
42
+ type QueryHelpers<Args extends QueryArgs, Indexes extends QueryArgsIndexes<Args>> = Indexes extends Record<string, never> ? QueryHelpersWithoutIndexes : QueryHelpersWithIndexes<Args, Indexes>;
43
+ type QueryResult<Item> = null | Item | Array<null | Item>;
44
+ type ItemFromQueryResult<R> = R extends Array<infer I | null> ? NonNullable<I> : R extends null ? never : R;
45
+ type QueryOptions<Result extends QueryResult<Item>, Args extends QueryArgs, Indexes extends QueryArgsIndexes<Args, Indexes>, Item = ItemFromQueryResult<Result>> = {
46
+ resolve: (args: Args) => Result | Promise<Result>;
47
+ indexArgsBy?: Indexes;
48
+ ttlMs?: number;
49
+ revision?: number;
50
+ onUpdate?: (pairs: Array<{
51
+ next: Item;
52
+ previous: Item | null;
53
+ }>, helpers: QueryHelpers<Args, Indexes>) => Promise<void> | void;
54
+ onInsert?: (items: Item[], helpers: QueryHelpers<Args, Indexes>) => Promise<void> | void;
55
+ onDelete?: (pairs: Array<{
56
+ ref: ItemRef;
57
+ previous: Item | null;
58
+ }>, helpers: QueryHelpers<Args, Indexes>) => Promise<void> | void;
59
+ };
60
+ type QueryOnUpdateHook<Item> = (pairs: Array<{
61
+ next: Item;
62
+ previous: Item | null;
63
+ }>) => Promise<void>;
64
+ type QueryOnInsertHook<Item> = (items: Item[]) => Promise<void>;
65
+ type QueryOnDeleteHook<Item> = (pairs: Array<{
66
+ ref: ItemRef;
67
+ previous: Item | null;
68
+ }>) => Promise<void>;
69
+ type QueryHandle<Result extends QueryResult<Item>, Args extends QueryArgs, Indexes extends QueryArgsIndexes<Args, Indexes>, Item = ItemFromQueryResult<Result>> = {
70
+ call: (args: Args) => Promise<Result>;
71
+ tag: QueryTag;
72
+ helpers: QueryHelpersWithIndexes<Args, Indexes>;
73
+ hooks: {
74
+ onUpdate?: QueryOnUpdateHook<Item>;
75
+ onInsert?: QueryOnInsertHook<Item>;
76
+ onDelete?: QueryOnDeleteHook<Item>;
77
+ };
78
+ };
79
+ //#endregion
80
+ //#region lib/key.d.ts
81
+ type QueryCacheKey = `${QueryCacheKeyPrefix}:${QueryCacheKeyId}`;
82
+ type QueryCacheIndexKey = `${QueryCacheKeyPrefix}:${QueryCacheIndexKeyId}`;
83
+ type QueryCacheKeyId = `id:${string}`;
84
+ type QueryCacheIndexKeyId = `idx:${string}`;
85
+ type QueryCacheKeyPrefix = `${string}:${string}:query:${string}:${CacheRevisionId}`;
86
+ type ItemCacheKey = `${ItemCacheKeyPrefix}:${ItemCacheKeyId}`;
87
+ type ItemCacheKeyId = `id:${string}`;
88
+ type ItemCacheKeyPrefix = `${string}:${string}:item:${CacheRevisionId}`;
89
+ type CacheRevisionId = `rev_${string}`;
90
+ //#endregion
91
+ //#region lib/client.d.ts
92
+ interface CacheClientArgs<Item> {
93
+ ttlMs: number;
94
+ parse: (value: string) => Item | null;
95
+ serialize: (item: Item) => string;
96
+ }
97
+ interface CacheClientOptions {
98
+ /**
99
+ * Revision number for cache invalidation.
100
+ * Incrementing this number will invalidate all existing cache entries for this store.
101
+ * @defaultValue "default"
102
+ */
103
+ revision?: number;
104
+ /**
105
+ * Time-to-live in milliseconds
106
+ * @defaultValue 3_600_000 (1 hour)
107
+ */
108
+ ttlMs?: number;
109
+ /**
110
+ * Optional namespace to prefix all cache keys
111
+ * @defaultValue "baeta"
112
+ */
113
+ namespace?: string;
114
+ }
115
+ declare abstract class CacheClient {
116
+ options?: CacheClientOptions;
117
+ constructor(options?: CacheClientOptions);
118
+ abstract getPartialItems<Item>(keys: ItemCacheKey[], options: CacheClientArgs<Item>): Promise<Array<Item | null>>;
119
+ abstract saveItems<Item>(items: Array<[ItemCacheKey, Item]>, options: CacheClientArgs<Item>): Promise<void>;
120
+ abstract saveItemsWithDiff<Item>(items: Array<[ItemCacheKey, Item]>, options: CacheClientArgs<Item>): Promise<Array<Item | null>>;
121
+ abstract deleteItems<Item>(keys: ItemCacheKey[], options: CacheClientArgs<Item>): Promise<void>;
122
+ abstract deleteItemsWithDiff<Item>(keys: ItemCacheKey[], options: CacheClientArgs<Item>): Promise<Array<Item | null>>;
123
+ abstract getQuery<QueryMetadata>(key: QueryCacheKey, options: CacheClientArgs<QueryMetadata>): Promise<QueryMetadata | null>;
124
+ abstract saveQuery<QueryMetadata>(key: QueryCacheKey, indexes: QueryCacheIndexKey[], metadata: QueryMetadata, options: CacheClientArgs<QueryMetadata>): Promise<void>;
125
+ abstract deleteQueries<QueryMetadata>(indexes: QueryCacheIndexKey[], options: CacheClientArgs<QueryMetadata>): Promise<void>;
126
+ }
127
+ //#endregion
128
+ //#region lib/store.d.ts
129
+ interface CacheStoreOptions<Item> {
130
+ name: string;
131
+ client: CacheClient;
132
+ namespace?: string;
133
+ revision?: string | number;
134
+ ttlMs?: number;
135
+ parse: (value: string) => Item;
136
+ serialize: (item: Item) => string;
137
+ getRef?: (source: Item) => ItemRef;
138
+ }
139
+ type QueryMetadata = {
140
+ refs: Array<null | string>;
141
+ isList: boolean;
142
+ };
143
+ declare class CacheStore<Item> {
144
+ protected readonly name: string;
145
+ protected readonly namespace: string;
146
+ protected readonly revision: string;
147
+ protected readonly client: CacheClient;
148
+ protected readonly itemCacheArgs: CacheClientArgs<Item>;
149
+ protected readonly queryCacheArgs: CacheClientArgs<QueryMetadata>;
150
+ protected readonly getRef?: (source: Item) => ItemRef;
151
+ constructor({
152
+ name,
153
+ client,
154
+ namespace,
155
+ revision,
156
+ ttlMs,
157
+ parse,
158
+ serialize,
159
+ getRef
160
+ }: CacheStoreOptions<Item>);
161
+ protected loadAndParse: (refs: ReadonlyArray<ItemRef>) => Promise<Array<Item | null>>;
162
+ protected loader: DataLoader<ItemRef, Item | null, ItemRef>;
163
+ get(ref: ItemRef): Promise<Item | null>;
164
+ getMany(refs: ItemRef[]): Promise<Item[] | null>;
165
+ getPartial(refs: ItemRef[]): Promise<Array<Item | null>>;
166
+ save(item: Item | Item[]): Promise<void>;
167
+ saveWithDiff(item: Item | Item[]): Promise<Array<Item | null>>;
168
+ delete(ref: ItemRef | ItemRef[]): Promise<void>;
169
+ deleteWithDiff(ref: ItemRef | ItemRef[]): Promise<Array<Item | null>>;
170
+ getQuery(query: QueryTagWithArgs): Promise<{
171
+ query: unknown;
172
+ } | null>;
173
+ saveQuery(query: QueryTagWithData<Item>): Promise<void>;
174
+ deleteQueries(query: QueryTagWithIndexes): Promise<void>;
175
+ getRefWithFallback(root: Item): ItemRef;
176
+ getItemKey(item: Item): ItemCacheKey;
177
+ getItemKeyByRef(ref: ItemRef): ItemCacheKey;
178
+ getQueryKey(query: QueryTagWithArgs): QueryCacheKey;
179
+ getQueryIndexes(query: QueryTagWithIndexes, ensureCatchAll?: boolean): QueryCacheIndexKey[];
180
+ getQueryKeyPrefix(query: QueryTag): QueryCacheKeyPrefix;
181
+ getQueryKeyRevision(query: QueryTag): CacheRevisionId;
182
+ }
183
+ //#endregion
184
+ //#region lib/define-query.d.ts
185
+ type QueryFactory<Result extends QueryResult<Item>, Args extends QueryArgs, Indexes extends QueryArgsIndexes<Args, Indexes>, Item = ItemFromQueryResult<Result>> = (store: CacheStore<Item>, name: string) => QueryHandle<Result, Args, Indexes, Item>;
186
+ type QueryMethod<T extends QueryFactory<any, any, any>> = T extends QueryFactory<infer Result, infer Args, any, any> ? (args: Args) => Promise<Result> : never;
187
+ type QueryDefinitionMap<Item> = Record<string, QueryFactory<QueryResult<Item>, any, any, Item>>;
188
+ type QueryMethodMap<Definitions extends QueryDefinitionMap<any>> = { [K in keyof Definitions]: QueryMethod<Definitions[K]> };
189
+ declare function defineQuery<Result extends QueryResult<Item>, Args extends QueryArgs = Record<string, never>, Indexes extends QueryArgsIndexes<Args, Indexes> = Record<string, never>, Item = ItemFromQueryResult<Result>>(options: QueryOptions<Result, Args, Indexes, Item>): QueryFactory<Result, Args, Indexes, Item>;
190
+ //#endregion
191
+ //#region lib/options.d.ts
192
+ /**
193
+ * Configuration options for cache stores
194
+ */
195
+ type CacheOptions<Item> = {
196
+ /**
197
+ * Unique name for the cache store. Used as a prefix for cache keys to avoid collisions.
198
+ */
199
+ name: string;
200
+ /**
201
+ * Revision number for cache invalidation.
202
+ * Incrementing this number will invalidate all existing cache entries for this store.
203
+ * @defaultValue CacheClient options or "default"
204
+ */
205
+ revision?: number;
206
+ /**
207
+ * Time-to-live in milliseconds
208
+ * @defaultValue CacheClient options or 3_600_000 (1 hour)
209
+ */
210
+ ttlMs?: number;
211
+ /**
212
+ * Optional namespace to prefix all cache keys
213
+ * @defaultValue CacheClient options or "baeta"
214
+ */
215
+ namespace?: string;
216
+ /**
217
+ * Function to parse stored strings back into source objects.
218
+ * If it throws an error, the cache will be treated as a miss and the value will be deleted.
219
+ */
220
+ parse: (value: string) => Item;
221
+ /**
222
+ * Function to serialize source objects into strings for cache storage.
223
+ */
224
+ serialize: (value: Item) => string;
225
+ /**
226
+ * Hook that gets called with the updated items after an update operation.
227
+ */
228
+ onUpdate?: (item: Item[]) => void | Promise<void>;
229
+ /**
230
+ * Hook that gets called with the inserted items after an insert operation.
231
+ */
232
+ onInsert?: (item: Item[]) => void | Promise<void>;
233
+ /**
234
+ * Hook that gets called with the refs of deleted items after a delete operation.
235
+ */
236
+ onDelete?: (ref: ItemRef[]) => void | Promise<void>;
237
+ };
238
+ type OptionalGetRef<Item> = {
239
+ getRef?: (source: Item) => ItemRef;
240
+ };
241
+ type RequiredGetRef<Item> = {
242
+ getRef: (source: Item) => ItemRef;
243
+ };
244
+ //#endregion
245
+ //#region lib/create-cache.d.ts
246
+ type CacheHooksOptions = {
247
+ /**
248
+ * If true, hooks will be awaited. If false, hooks will be fired asynchronously and will not block cache methods.
249
+ * @defaultValue false
250
+ */
251
+ waitForHooks?: boolean;
252
+ };
253
+ type Cache<Item> = {
254
+ get(ref: ItemRef): Promise<Item | null>;
255
+ getMany(refs: ItemRef[]): Promise<Item[] | null>;
256
+ getPartial(refs: ItemRef[]): Promise<Array<Item | null>>;
257
+ update(item: Item | Item[]): Promise<void>;
258
+ insert(item: Item | Item[]): Promise<void>;
259
+ delete(ref: ItemRef | ItemRef[]): Promise<void>;
260
+ };
261
+ type CacheWithQueries<Item, QueryDefinitions extends QueryDefinitionMap<Item>> = {
262
+ get(ref: ItemRef): Promise<Item | null>;
263
+ getMany(refs: ItemRef[]): Promise<Item[] | null>;
264
+ getPartial(refs: ItemRef[]): Promise<Array<Item | null>>;
265
+ update(item: Item | Item[], options?: CacheHooksOptions): Promise<void>;
266
+ insert(item: Item | Item[], options?: CacheHooksOptions): Promise<void>;
267
+ delete(ref: ItemRef | ItemRef[], options?: CacheHooksOptions): Promise<void>;
268
+ deleteQueries(): Promise<void>;
269
+ queries: QueryMethodMap<QueryDefinitions>;
270
+ };
271
+ type CreateCacheFactory<Item> = {
272
+ build: () => Cache<Item>;
273
+ withQueries: <QueryDefinitions extends QueryDefinitionMap<Item>>(definitions: QueryDefinitions) => {
274
+ build: () => CacheWithQueries<Item, QueryDefinitions>;
275
+ };
276
+ };
277
+ declare function createCache<Item extends RefCompatibleItem>(client: CacheClient, options: CacheOptions<Item> & OptionalGetRef<Item>): CreateCacheFactory<Item>;
278
+ declare function createCache<Item>(client: CacheClient, options: CacheOptions<Item> & RequiredGetRef<Item>): CreateCacheFactory<Item>;
279
+ //#endregion
280
+ export { type Cache, CacheClient, type CacheClientArgs, type CacheClientOptions, type CacheHooksOptions, type CacheOptions, type CacheWithQueries, type CreateCacheFactory, type ItemCacheKey, type ItemRef, type OptionalGetRef, type QueryArgs, type QueryArgsIndexes, type QueryCacheIndexKey, type QueryCacheKey, type QueryIndexValue, type QueryOptions, type RequiredGetRef, createCache, defineQuery };
281
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,403 @@
1
+ import { log } from "@baeta/util-log";
2
+ import DataLoader from "dataloader";
3
+ import { encodeBase64 } from "@baeta/util-encoding";
4
+ import { hash } from "ohash";
5
+ //#region lib/client.ts
6
+ var CacheClient = class {
7
+ options;
8
+ constructor(options) {
9
+ this.options = options;
10
+ }
11
+ };
12
+ //#endregion
13
+ //#region lib/item.ts
14
+ /**
15
+ * Asserts that a given reference is of a valid type (string, number, or bigint) and that it can be safely serialized.
16
+ */
17
+ function assertValidRefType(ref) {
18
+ if (typeof ref !== "string" && typeof ref !== "number" && typeof ref !== "bigint") throw new TypeError("Reference must be string, number or bigint. Define getRef function in cache options");
19
+ }
20
+ //#endregion
21
+ //#region lib/key.ts
22
+ function buildCacheRevisionId(revision) {
23
+ return `rev_${revision}`;
24
+ }
25
+ function buildItemCacheKeyId(id) {
26
+ return `id:${id}`;
27
+ }
28
+ function buildQueryCacheKeyId(args) {
29
+ return `id:${hash(args)}`;
30
+ }
31
+ function buildQueryCacheIndexKeyId(index) {
32
+ if (index == null) return "idx:";
33
+ const key = index[0];
34
+ const value = index[1];
35
+ return `idx:${hash(`${key}=${value === null ? "null" : `_${encodeBase64(value.toString())}`}`)}`;
36
+ }
37
+ function buildQueryCacheIndexKeysIds(indexes) {
38
+ if (indexes.length === 0) return ["idx:"];
39
+ return indexes.map((idx) => buildQueryCacheIndexKeyId(idx));
40
+ }
41
+ //#endregion
42
+ //#region lib/utils.ts
43
+ function arrayIsComplete(items) {
44
+ if (items == null) return false;
45
+ return items.every((item) => item != null);
46
+ }
47
+ function alignItemsWithRefs(partialRefs, items) {
48
+ if (partialRefs.length === items.length) return items;
49
+ let itemIndex = 0;
50
+ const aligned = [];
51
+ for (const ref of partialRefs) if (ref == null) aligned.push(null);
52
+ else aligned.push(items[itemIndex++]);
53
+ return aligned;
54
+ }
55
+ function toArray(item) {
56
+ return Array.isArray(item) ? item : [item];
57
+ }
58
+ function zip(array1, array2, fn) {
59
+ if (array1.length !== array2.length) throw new Error("Arrays must have the same length to be zipped");
60
+ const result = [];
61
+ for (let i = 0; i < array1.length; i++) result.push(fn(array1[i], array2[i]));
62
+ return result;
63
+ }
64
+ //#endregion
65
+ //#region lib/store.ts
66
+ var CacheStore = class {
67
+ name;
68
+ namespace;
69
+ revision;
70
+ client;
71
+ itemCacheArgs;
72
+ queryCacheArgs;
73
+ getRef;
74
+ constructor({ name, client, namespace, revision, ttlMs, parse, serialize, getRef }) {
75
+ this.name = name;
76
+ this.namespace = namespace ?? "baeta";
77
+ this.revision = revision?.toString() ?? "default";
78
+ this.client = client;
79
+ this.getRef = getRef;
80
+ this.itemCacheArgs = {
81
+ ttlMs: ttlMs ?? 36e5,
82
+ parse: (value) => parseItemSafely(value, parse),
83
+ serialize: (item) => serialize(item)
84
+ };
85
+ this.queryCacheArgs = {
86
+ ttlMs: ttlMs ?? 36e5,
87
+ parse: (value) => JSON.parse(value),
88
+ serialize: (meta) => JSON.stringify(meta)
89
+ };
90
+ }
91
+ loadAndParse = async (refs) => {
92
+ const keys = refs.map((ref) => this.getItemKeyByRef(ref));
93
+ return await this.client.getPartialItems(keys, this.itemCacheArgs);
94
+ };
95
+ loader = new DataLoader(this.loadAndParse, { cache: false });
96
+ async get(ref) {
97
+ return await this.loader.load(ref);
98
+ }
99
+ async getMany(refs) {
100
+ const results = await Promise.all(refs.map((r) => this.get(r)));
101
+ if (!arrayIsComplete(results)) return null;
102
+ return results;
103
+ }
104
+ async getPartial(refs) {
105
+ return await Promise.all(refs.map((ref) => this.get(ref)));
106
+ }
107
+ async save(item) {
108
+ const entries = toArray(item).map((item) => [this.getItemKey(item), item]);
109
+ return this.client.saveItems(entries, this.itemCacheArgs);
110
+ }
111
+ async saveWithDiff(item) {
112
+ const entries = toArray(item).map((item) => [this.getItemKey(item), item]);
113
+ return await this.client.saveItemsWithDiff(entries, this.itemCacheArgs);
114
+ }
115
+ async delete(ref) {
116
+ const keys = toArray(ref).map((r) => this.getItemKeyByRef(r));
117
+ await this.client.deleteItems(keys, this.itemCacheArgs);
118
+ }
119
+ async deleteWithDiff(ref) {
120
+ const keys = toArray(ref).map((r) => this.getItemKeyByRef(r));
121
+ return await this.client.deleteItemsWithDiff(keys, this.itemCacheArgs);
122
+ }
123
+ async getQuery(query) {
124
+ const queryKey = this.getQueryKey(query);
125
+ const meta = await this.client.getQuery(queryKey, this.queryCacheArgs);
126
+ if (meta == null) return null;
127
+ const isList = meta.isList;
128
+ const nullableRefs = meta.refs;
129
+ const refs = nullableRefs.filter((ref) => ref != null);
130
+ const items = await this.getMany(refs);
131
+ if (items == null) return null;
132
+ const aligned = alignItemsWithRefs(nullableRefs, items);
133
+ return { query: isList ? aligned : aligned.at(0) ?? null };
134
+ }
135
+ async saveQuery(query) {
136
+ const data = query.data;
137
+ const queryKey = this.getQueryKey(query);
138
+ const queryIndexes = this.getQueryIndexes(query, true);
139
+ const isList = Array.isArray(data);
140
+ const nullableItems = isList ? data : [data];
141
+ const items = nullableItems.filter((item) => item != null);
142
+ if (items.length > 0) await this.save(items);
143
+ const metadata = {
144
+ refs: nullableItems.map((item) => {
145
+ if (item == null) return null;
146
+ return this.getRefWithFallback(item).toString();
147
+ }),
148
+ isList
149
+ };
150
+ return this.client.saveQuery(queryKey, queryIndexes, metadata, this.queryCacheArgs);
151
+ }
152
+ deleteQueries(query) {
153
+ const queryIndexes = this.getQueryIndexes(query);
154
+ return this.client.deleteQueries(queryIndexes, this.queryCacheArgs);
155
+ }
156
+ getRefWithFallback(root) {
157
+ if (this.getRef) return this.getRef(root);
158
+ if (root == null) throw new Error("Object is null or undefined, cannot get ref");
159
+ if (typeof root === "object" && "id" in root) {
160
+ assertValidRefType(root.id);
161
+ return root.id.toString();
162
+ }
163
+ throw new Error("Object does not have id. Define \"getRef\" function in cache options");
164
+ }
165
+ getItemKey(item) {
166
+ const ref = this.getRefWithFallback(item);
167
+ return this.getItemKeyByRef(ref);
168
+ }
169
+ getItemKeyByRef(ref) {
170
+ return `${`${this.namespace}:{${this.name}:item:${buildCacheRevisionId(this.revision)}}`}:${buildItemCacheKeyId(ref.toString())}`;
171
+ }
172
+ getQueryKey(query) {
173
+ return `${this.getQueryKeyPrefix(query)}:${buildQueryCacheKeyId(query.args)}`;
174
+ }
175
+ getQueryIndexes(query, ensureCatchAll = false) {
176
+ const prefix = this.getQueryKeyPrefix(query);
177
+ const keyIndexes = buildQueryCacheIndexKeysIds(query.indexes);
178
+ if (ensureCatchAll && query.indexes.length > 0) keyIndexes.push(buildQueryCacheIndexKeyId());
179
+ return keyIndexes.map((keyIndex) => `${prefix}:${keyIndex}`);
180
+ }
181
+ getQueryKeyPrefix(query) {
182
+ return `${this.namespace}:{${this.name}:query:${query.name}:${this.getQueryKeyRevision(query)}}`;
183
+ }
184
+ getQueryKeyRevision(query) {
185
+ return buildCacheRevisionId(`${this.revision}_${query.revision?.toString() ?? "default"}`);
186
+ }
187
+ };
188
+ function parseItemSafely(value, parseItem) {
189
+ try {
190
+ return parseItem(value);
191
+ } catch (err) {
192
+ log.warn(err, "Failed to parse item, returning null", { value });
193
+ return null;
194
+ }
195
+ }
196
+ //#endregion
197
+ //#region lib/create-cache.ts
198
+ function createCache(client, options) {
199
+ const storeClient = new CacheStore({
200
+ client,
201
+ name: options.name,
202
+ parse: options.parse,
203
+ serialize: options.serialize,
204
+ getRef: options.getRef,
205
+ ttlMs: options.ttlMs ?? client.options?.ttlMs,
206
+ namespace: options.namespace ?? client.options?.namespace,
207
+ revision: options.revision ?? client.options?.revision
208
+ });
209
+ return {
210
+ build: () => createCacheMethods(storeClient, {
211
+ onUpdateHook: options.onUpdate,
212
+ onInsertHook: options.onInsert,
213
+ onDeleteHook: options.onDelete
214
+ }, {
215
+ onUpdateHooks: [],
216
+ onInsertHooks: [],
217
+ onDeleteHooks: []
218
+ }),
219
+ withQueries: (definitions) => {
220
+ return { build: () => createCacheMethodsWithQueries(storeClient, options, definitions) };
221
+ }
222
+ };
223
+ }
224
+ function createCacheMethods(storeClient, itemHooks, queryHooks) {
225
+ return {
226
+ get: (ref) => storeClient.get(ref),
227
+ getMany: (refs) => storeClient.getMany(refs),
228
+ getPartial: (refs) => storeClient.getPartial(refs),
229
+ update: async (item, options) => {
230
+ if (queryHooks.onUpdateHooks.length === 0) {
231
+ await storeClient.save(item);
232
+ await fireHook(itemHooks.onUpdateHook, toArray(item), options);
233
+ } else {
234
+ const items = toArray(item);
235
+ const pairs = zip(items, await storeClient.saveWithDiff(items), (next, previous) => ({
236
+ next,
237
+ previous
238
+ }));
239
+ await Promise.all([fireHook(itemHooks.onUpdateHook, items, options), fireHooks(queryHooks.onUpdateHooks, pairs, options)]);
240
+ }
241
+ },
242
+ insert: async (item, options) => {
243
+ await storeClient.save(item);
244
+ if (queryHooks.onInsertHooks.length === 0) await fireHook(itemHooks.onInsertHook, toArray(item), options);
245
+ else {
246
+ const items = toArray(item);
247
+ await Promise.all([fireHook(itemHooks.onInsertHook, items, options), fireHooks(queryHooks.onInsertHooks, items, options)]);
248
+ }
249
+ },
250
+ delete: async (ref, options) => {
251
+ if (queryHooks.onDeleteHooks.length === 0) {
252
+ await storeClient.delete(ref);
253
+ await fireHook(itemHooks.onDeleteHook, toArray(ref), options);
254
+ } else {
255
+ const refs = toArray(ref);
256
+ const previous = await storeClient.deleteWithDiff(refs);
257
+ await storeClient.delete(refs);
258
+ const pairs = zip(refs, previous, (ref, previous) => ({
259
+ ref,
260
+ previous
261
+ }));
262
+ await Promise.all([fireHook(itemHooks.onDeleteHook, refs, options), fireHooks(queryHooks.onDeleteHooks, pairs, options)]);
263
+ }
264
+ }
265
+ };
266
+ }
267
+ function createCacheMethodsWithQueries(storeClient, options, definitions) {
268
+ const { queries, queryTags, onUpdateHooks, onInsertHooks, onDeleteHooks } = createQueriesAndHooks(definitions, storeClient);
269
+ return {
270
+ ...createCacheMethods(storeClient, {
271
+ onUpdateHook: options.onUpdate,
272
+ onInsertHook: options.onInsert,
273
+ onDeleteHook: options.onDelete
274
+ }, {
275
+ onUpdateHooks,
276
+ onInsertHooks,
277
+ onDeleteHooks
278
+ }),
279
+ deleteQueries: async () => {
280
+ const promises = queryTags.map((tag) => storeClient.deleteQueries({
281
+ ...tag,
282
+ indexes: []
283
+ }));
284
+ await Promise.all(promises);
285
+ },
286
+ queries
287
+ };
288
+ }
289
+ function createQueriesAndHooks(definitions, store) {
290
+ const queries = {};
291
+ const queryTags = [];
292
+ const onUpdateHooks = [];
293
+ const onInsertHooks = [];
294
+ const onDeleteHooks = [];
295
+ for (const [queryName, buildQuery] of Object.entries(definitions ?? {})) {
296
+ const queryHandler = buildQuery(store, queryName);
297
+ queries[queryName] = queryHandler.call;
298
+ queryTags.push(queryHandler.tag);
299
+ if (queryHandler.hooks.onUpdate) onUpdateHooks.push(queryHandler.hooks.onUpdate);
300
+ if (queryHandler.hooks.onDelete) onDeleteHooks.push(queryHandler.hooks.onDelete);
301
+ if (queryHandler.hooks.onInsert) onInsertHooks.push(queryHandler.hooks.onInsert);
302
+ }
303
+ return {
304
+ queries,
305
+ queryTags,
306
+ onUpdateHooks,
307
+ onInsertHooks,
308
+ onDeleteHooks
309
+ };
310
+ }
311
+ async function fireHook(hook, value, options) {
312
+ if (hook == null) return;
313
+ if (options?.waitForHooks) await hook(value);
314
+ else Promise.resolve().then(() => hook(value)).catch((err) => {
315
+ log.error(err, "Error in cache hook");
316
+ });
317
+ }
318
+ async function fireHooks(hooks, value, options) {
319
+ await Promise.all(hooks.map((hook) => fireHook(hook, value, options)));
320
+ }
321
+ //#endregion
322
+ //#region lib/define-query.ts
323
+ function defineQuery(options) {
324
+ return (store, name) => {
325
+ const tag = {
326
+ name,
327
+ revision: options.revision
328
+ };
329
+ const resolve = async (args) => {
330
+ const fromCache = await store.getQuery({
331
+ ...tag,
332
+ args
333
+ }).catch((err) => {
334
+ log.error(err, `Failed to get query result for ${name}. Proceeding without caching.`);
335
+ return null;
336
+ });
337
+ if (fromCache != null) return fromCache.query;
338
+ const data = await options.resolve(args);
339
+ await store.saveQuery({
340
+ ...tag,
341
+ args,
342
+ data,
343
+ indexes: buildIndexes(args, options.indexArgsBy)
344
+ }).catch((err) => {
345
+ log.error(err, `Failed to save query result for ${name}. Proceeding without caching.`);
346
+ });
347
+ return data;
348
+ };
349
+ const invalidateAll = async () => {
350
+ await store.deleteQueries({
351
+ ...tag,
352
+ indexes: []
353
+ });
354
+ };
355
+ const invalidateByArgs = async (args) => {
356
+ const deletePromises = (Array.isArray(args) ? args : [args]).map((args) => store.deleteQueries({
357
+ ...tag,
358
+ indexes: buildIndexes(args, options.indexArgsBy)
359
+ }));
360
+ await Promise.all(deletePromises);
361
+ };
362
+ const helpers = {
363
+ invalidateAll,
364
+ invalidateByArgs
365
+ };
366
+ const helpersCast = helpers;
367
+ const { onUpdate, onDelete, onInsert } = options;
368
+ return {
369
+ call: resolve,
370
+ tag,
371
+ helpers,
372
+ hooks: {
373
+ onUpdate: hookWithHelpers(helpersCast, onUpdate),
374
+ onInsert: hookWithHelpers(helpersCast, onInsert),
375
+ onDelete: hookWithHelpers(helpersCast, onDelete)
376
+ }
377
+ };
378
+ };
379
+ }
380
+ function hookWithHelpers(helpers, hook) {
381
+ if (!hook) return;
382
+ const hookWithHelpers = async (args) => {
383
+ await hook(args, helpers);
384
+ };
385
+ return hookWithHelpers;
386
+ }
387
+ function buildIndexes(args, indexArgsBy) {
388
+ if (!indexArgsBy) return [];
389
+ const indexes = [];
390
+ for (const key in indexArgsBy) {
391
+ if (!Object.hasOwn(args, key)) continue;
392
+ const value = args[key];
393
+ if (value === void 0) continue;
394
+ if (Array.isArray(value)) continue;
395
+ if (typeof value === "object" && value !== null) continue;
396
+ indexes.push([key, value]);
397
+ }
398
+ return indexes;
399
+ }
400
+ //#endregion
401
+ export { CacheClient, createCache, defineQuery };
402
+
403
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../lib/client.ts","../lib/item.ts","../lib/key.ts","../lib/options.ts","../lib/utils.ts","../lib/store.ts","../lib/create-cache.ts","../lib/define-query.ts"],"sourcesContent":["import type { ItemCacheKey, QueryCacheIndexKey, QueryCacheKey } from './key.ts';\n\nexport interface CacheClientArgs<Item> {\n\tttlMs: number;\n\tparse: (value: string) => Item | null;\n\tserialize: (item: Item) => string;\n}\n\nexport interface CacheClientOptions {\n\t/**\n\t * Revision number for cache invalidation.\n\t * Incrementing this number will invalidate all existing cache entries for this store.\n\t * @defaultValue \"default\"\n\t */\n\trevision?: number;\n\t/**\n\t * Time-to-live in milliseconds\n\t * @defaultValue 3_600_000 (1 hour)\n\t */\n\tttlMs?: number;\n\t/**\n\t * Optional namespace to prefix all cache keys\n\t * @defaultValue \"baeta\"\n\t */\n\tnamespace?: string;\n}\n\nexport abstract class CacheClient {\n\toptions?: CacheClientOptions;\n\n\tconstructor(options?: CacheClientOptions) {\n\t\tthis.options = options;\n\t}\n\n\tabstract getPartialItems<Item>(\n\t\tkeys: ItemCacheKey[],\n\t\toptions: CacheClientArgs<Item>,\n\t): Promise<Array<Item | null>>;\n\n\tabstract saveItems<Item>(\n\t\titems: Array<[ItemCacheKey, Item]>,\n\t\toptions: CacheClientArgs<Item>,\n\t): Promise<void>;\n\n\tabstract saveItemsWithDiff<Item>(\n\t\titems: Array<[ItemCacheKey, Item]>,\n\t\toptions: CacheClientArgs<Item>,\n\t): Promise<Array<Item | null>>;\n\n\tabstract deleteItems<Item>(keys: ItemCacheKey[], options: CacheClientArgs<Item>): Promise<void>;\n\n\tabstract deleteItemsWithDiff<Item>(\n\t\tkeys: ItemCacheKey[],\n\t\toptions: CacheClientArgs<Item>,\n\t): Promise<Array<Item | null>>;\n\n\tabstract getQuery<QueryMetadata>(\n\t\tkey: QueryCacheKey,\n\t\toptions: CacheClientArgs<QueryMetadata>,\n\t): Promise<QueryMetadata | null>;\n\n\tabstract saveQuery<QueryMetadata>(\n\t\tkey: QueryCacheKey,\n\t\tindexes: QueryCacheIndexKey[],\n\t\tmetadata: QueryMetadata,\n\t\toptions: CacheClientArgs<QueryMetadata>,\n\t): Promise<void>;\n\n\tabstract deleteQueries<QueryMetadata>(\n\t\tindexes: QueryCacheIndexKey[],\n\t\toptions: CacheClientArgs<QueryMetadata>,\n\t): Promise<void>;\n}\n","/**\n * Reference type for cached items\n */\nexport type ItemRef = string | number | bigint;\n\n/**\n * Type constraint for objects that are compatible with default cache ref\n */\nexport type RefCompatibleItem = { id: string | number | bigint };\n\n/**\n * Asserts that a given reference is of a valid type (string, number, or bigint) and that it can be safely serialized.\n */\nexport function assertValidRefType(ref: unknown): asserts ref is string | number | bigint {\n\tif (typeof ref !== 'string' && typeof ref !== 'number' && typeof ref !== 'bigint') {\n\t\tthrow new TypeError(\n\t\t\t'Reference must be string, number or bigint. Define getRef function in cache options',\n\t\t);\n\t}\n}\n","import { encodeBase64 } from '@baeta/util-encoding';\nimport { hash } from 'ohash';\nimport type { QueryArgs, QueryIndex } from './query.ts';\n\nexport type QueryCacheKey = `${QueryCacheKeyPrefix}:${QueryCacheKeyId}`;\nexport type QueryCacheIndexKey = `${QueryCacheKeyPrefix}:${QueryCacheIndexKeyId}`;\nexport type QueryCacheKeyId = `id:${string}`;\nexport type QueryCacheIndexKeyId = `idx:${string}`;\nexport type QueryCacheKeyPrefix = `${string}:${string}:query:${string}:${CacheRevisionId}`; // $namespace:$storeName:query:$queryName:rev_$value\n\nexport type ItemCacheKey = `${ItemCacheKeyPrefix}:${ItemCacheKeyId}`;\nexport type ItemCacheKeyId = `id:${string}`;\nexport type ItemCacheKeyPrefix = `${string}:${string}:item:${CacheRevisionId}`; // $namespace:$storeName:item:rev_$value\n\nexport type CacheRevisionId = `rev_${string}`;\n\nexport function buildCacheRevisionId(revision: string): CacheRevisionId {\n\treturn `rev_${revision}`;\n}\n\nexport function buildItemCacheKeyId(id: string): ItemCacheKeyId {\n\treturn `id:${id}`;\n}\n\nexport function buildQueryCacheKeyId(args: QueryArgs): QueryCacheKeyId {\n\treturn `id:${hash(args)}`;\n}\n\nexport function buildQueryCacheIndexKeyId(index?: QueryIndex | undefined): QueryCacheIndexKeyId {\n\tif (index == null) {\n\t\treturn 'idx:';\n\t}\n\tconst key = index[0];\n\tconst value = index[1];\n\tconst safeValue = value === null ? 'null' : `_${encodeBase64(value.toString())}`;\n\treturn `idx:${hash(`${key}=${safeValue}`)}`;\n}\n\nexport function buildQueryCacheIndexKeysIds(indexes: QueryIndex[]): QueryCacheIndexKeyId[] {\n\tif (indexes.length === 0) {\n\t\treturn ['idx:'];\n\t}\n\treturn indexes.map((idx) => buildQueryCacheIndexKeyId(idx));\n}\n","import type { ItemRef } from './item.ts';\n\nexport const DEFAULT_REVISION = 'default';\nexport const DEFAULT_NAMESPACE = 'baeta';\nexport const DEFAULT_TTL_MS = 3_600_000; // 1 hour\n\n/**\n * Configuration options for cache stores\n */\nexport type CacheOptions<Item> = {\n\t/**\n\t * Unique name for the cache store. Used as a prefix for cache keys to avoid collisions.\n\t */\n\tname: string;\n\t/**\n\t * Revision number for cache invalidation.\n\t * Incrementing this number will invalidate all existing cache entries for this store.\n\t * @defaultValue CacheClient options or \"default\"\n\t */\n\trevision?: number;\n\t/**\n\t * Time-to-live in milliseconds\n\t * @defaultValue CacheClient options or 3_600_000 (1 hour)\n\t */\n\tttlMs?: number;\n\t/**\n\t * Optional namespace to prefix all cache keys\n\t * @defaultValue CacheClient options or \"baeta\"\n\t */\n\tnamespace?: string;\n\t/**\n\t * Function to parse stored strings back into source objects.\n\t * If it throws an error, the cache will be treated as a miss and the value will be deleted.\n\t */\n\tparse: (value: string) => Item;\n\t/**\n\t * Function to serialize source objects into strings for cache storage.\n\t */\n\tserialize: (value: Item) => string;\n\t/**\n\t * Hook that gets called with the updated items after an update operation.\n\t */\n\tonUpdate?: (item: Item[]) => void | Promise<void>;\n\t/**\n\t * Hook that gets called with the inserted items after an insert operation.\n\t */\n\tonInsert?: (item: Item[]) => void | Promise<void>;\n\t/**\n\t * Hook that gets called with the refs of deleted items after a delete operation.\n\t */\n\tonDelete?: (ref: ItemRef[]) => void | Promise<void>;\n};\n\nexport type OptionalGetRef<Item> = {\n\t/*\n\t * Optional function to extract a string reference from source objects.\n\t * Will use \"id\" property by default.\n\t */\n\tgetRef?: (source: Item) => ItemRef;\n};\n\nexport type RequiredGetRef<Item> = {\n\t/*\n\t * Function to extract a string reference from source objects.\n\t */\n\tgetRef: (source: Item) => ItemRef;\n};\n","import type { ItemRef } from './item.ts';\n\nexport function arrayIsComplete<T>(items: Array<T | null> | null): items is T[] {\n\tif (items == null) {\n\t\treturn false;\n\t}\n\treturn items.every((item) => item != null);\n}\n\nexport function alignItemsWithRefs<T>(partialRefs: Array<ItemRef | null>, items: T[]) {\n\tif (partialRefs.length === items.length) {\n\t\treturn items;\n\t}\n\tlet itemIndex = 0;\n\tconst aligned: Array<T | null> = [];\n\tfor (const ref of partialRefs) {\n\t\tif (ref == null) {\n\t\t\taligned.push(null);\n\t\t} else {\n\t\t\taligned.push(items[itemIndex++]);\n\t\t}\n\t}\n\treturn aligned;\n}\n\nexport function fillNullItemsWithFallback<T>(items: Array<T | null>, fallbacks: T[]) {\n\tlet fallbackIndex = 0;\n\tconst completeItems: T[] = [];\n\tfor (const item of items) {\n\t\tif (item != null) {\n\t\t\tcompleteItems.push(item);\n\t\t} else if (fallbackIndex < fallbacks.length) {\n\t\t\tcompleteItems.push(fallbacks[fallbackIndex++]);\n\t\t}\n\t}\n\treturn {\n\t\titems: completeItems,\n\t\tmissing: items.length - completeItems.length,\n\t\textra: fallbacks.length - fallbackIndex,\n\t};\n}\n\nexport function toArray<T>(item: T | T[]): T[] {\n\treturn Array.isArray(item) ? item : [item];\n}\n\nexport function zip<T, U, R>(array1: T[], array2: U[], fn: (a: T, b: U) => R): R[] {\n\tif (array1.length !== array2.length) {\n\t\tthrow new Error('Arrays must have the same length to be zipped');\n\t}\n\tconst result: R[] = [];\n\tfor (let i = 0; i < array1.length; i++) {\n\t\tresult.push(fn(array1[i], array2[i]));\n\t}\n\treturn result;\n}\n","import { log } from '@baeta/util-log';\nimport DataLoader from 'dataloader';\nimport type { CacheClient, CacheClientArgs } from './client.ts';\nimport { assertValidRefType, type ItemRef } from './item.ts';\nimport {\n\tbuildCacheRevisionId,\n\tbuildItemCacheKeyId,\n\tbuildQueryCacheIndexKeyId,\n\tbuildQueryCacheIndexKeysIds,\n\tbuildQueryCacheKeyId,\n\ttype CacheRevisionId,\n\ttype ItemCacheKey,\n\ttype ItemCacheKeyPrefix,\n\ttype QueryCacheIndexKey,\n\ttype QueryCacheKey,\n\ttype QueryCacheKeyPrefix,\n} from './key.ts';\nimport { DEFAULT_NAMESPACE, DEFAULT_REVISION, DEFAULT_TTL_MS } from './options.ts';\nimport type { QueryTag, QueryTagWithArgs, QueryTagWithData, QueryTagWithIndexes } from './query.ts';\nimport { alignItemsWithRefs, arrayIsComplete, toArray } from './utils.ts';\n\nexport interface CacheStoreOptions<Item> {\n\tname: string;\n\tclient: CacheClient;\n\tnamespace?: string;\n\trevision?: string | number;\n\tttlMs?: number;\n\tparse: (value: string) => Item;\n\tserialize: (item: Item) => string;\n\tgetRef?: (source: Item) => ItemRef;\n}\n\ntype QueryMetadata = { refs: Array<null | string>; isList: boolean };\n\nexport class CacheStore<Item> {\n\tprotected readonly name: string;\n\tprotected readonly namespace: string;\n\tprotected readonly revision: string;\n\n\tprotected readonly client: CacheClient;\n\tprotected readonly itemCacheArgs: CacheClientArgs<Item>;\n\tprotected readonly queryCacheArgs: CacheClientArgs<QueryMetadata>;\n\n\tprotected readonly getRef?: (source: Item) => ItemRef;\n\n\tconstructor({\n\t\tname,\n\t\tclient,\n\t\tnamespace,\n\t\trevision,\n\t\tttlMs,\n\t\tparse,\n\t\tserialize,\n\t\tgetRef,\n\t}: CacheStoreOptions<Item>) {\n\t\tthis.name = name;\n\t\tthis.namespace = namespace ?? DEFAULT_NAMESPACE;\n\t\tthis.revision = revision?.toString() ?? DEFAULT_REVISION;\n\t\tthis.client = client;\n\t\tthis.getRef = getRef;\n\t\tthis.itemCacheArgs = {\n\t\t\tttlMs: ttlMs ?? DEFAULT_TTL_MS,\n\t\t\tparse: (value: string) => parseItemSafely(value, parse),\n\t\t\tserialize: (item: Item) => serialize(item),\n\t\t};\n\t\tthis.queryCacheArgs = {\n\t\t\tttlMs: ttlMs ?? DEFAULT_TTL_MS,\n\t\t\tparse: (value: string) => JSON.parse(value) as QueryMetadata,\n\t\t\tserialize: (meta: QueryMetadata) => JSON.stringify(meta),\n\t\t};\n\t}\n\n\tprotected loadAndParse = async (refs: ReadonlyArray<ItemRef>): Promise<Array<Item | null>> => {\n\t\tconst keys = refs.map((ref) => this.getItemKeyByRef(ref));\n\t\treturn await this.client.getPartialItems(keys, this.itemCacheArgs);\n\t};\n\n\tprotected loader = new DataLoader<ItemRef, Item | null>(this.loadAndParse, {\n\t\tcache: false,\n\t});\n\n\tasync get(ref: ItemRef): Promise<Item | null> {\n\t\treturn await this.loader.load(ref);\n\t}\n\n\tasync getMany(refs: ItemRef[]): Promise<Item[] | null> {\n\t\tconst results = await Promise.all(refs.map((r) => this.get(r)));\n\t\tif (!arrayIsComplete(results)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn results;\n\t}\n\n\tasync getPartial(refs: ItemRef[]): Promise<Array<Item | null>> {\n\t\treturn await Promise.all(refs.map((ref) => this.get(ref)));\n\t}\n\n\tasync save(item: Item | Item[]) {\n\t\tconst values = toArray(item);\n\t\tconst entries: Array<[ItemCacheKey, Item]> = values.map((item) => [\n\t\t\tthis.getItemKey(item),\n\t\t\titem,\n\t\t]);\n\t\treturn this.client.saveItems(entries, this.itemCacheArgs);\n\t}\n\n\tasync saveWithDiff(item: Item | Item[]): Promise<Array<Item | null>> {\n\t\tconst values = toArray(item);\n\t\tconst entries: Array<[ItemCacheKey, Item]> = values.map((item) => [\n\t\t\tthis.getItemKey(item),\n\t\t\titem,\n\t\t]);\n\t\treturn await this.client.saveItemsWithDiff(entries, this.itemCacheArgs);\n\t}\n\n\tasync delete(ref: ItemRef | ItemRef[]) {\n\t\tconst refs = toArray(ref);\n\t\tconst keys = refs.map((r) => this.getItemKeyByRef(r));\n\t\tawait this.client.deleteItems(keys, this.itemCacheArgs);\n\t}\n\n\tasync deleteWithDiff(ref: ItemRef | ItemRef[]): Promise<Array<Item | null>> {\n\t\tconst refs = toArray(ref);\n\t\tconst keys = refs.map((r) => this.getItemKeyByRef(r));\n\t\treturn await this.client.deleteItemsWithDiff(keys, this.itemCacheArgs);\n\t}\n\n\tasync getQuery(query: QueryTagWithArgs): Promise<{ query: unknown } | null> {\n\t\tconst queryKey = this.getQueryKey(query);\n\t\tconst meta = await this.client.getQuery(queryKey, this.queryCacheArgs);\n\n\t\tif (meta == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst isList = meta.isList;\n\t\tconst nullableRefs = meta.refs;\n\t\tconst refs = nullableRefs.filter((ref) => ref != null);\n\t\tconst items = await this.getMany(refs);\n\n\t\tif (items == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst aligned = alignItemsWithRefs(nullableRefs, items);\n\t\tconst result = isList ? aligned : (aligned.at(0) ?? null);\n\n\t\treturn { query: result };\n\t}\n\n\tasync saveQuery(query: QueryTagWithData<Item>): Promise<void> {\n\t\tconst data = query.data;\n\t\tconst queryKey = this.getQueryKey(query);\n\t\tconst queryIndexes = this.getQueryIndexes(query, true);\n\n\t\tconst isList = Array.isArray(data);\n\t\tconst nullableItems = isList ? data : [data];\n\t\tconst items = nullableItems.filter((item) => item != null);\n\n\t\tif (items.length > 0) {\n\t\t\tawait this.save(items);\n\t\t}\n\n\t\tconst encodedRefs = nullableItems.map((item) => {\n\t\t\tif (item == null) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn this.getRefWithFallback(item).toString();\n\t\t});\n\n\t\tconst metadata = {\n\t\t\trefs: encodedRefs,\n\t\t\tisList,\n\t\t};\n\n\t\treturn this.client.saveQuery(queryKey, queryIndexes, metadata, this.queryCacheArgs);\n\t}\n\n\tdeleteQueries(query: QueryTagWithIndexes): Promise<void> {\n\t\tconst queryIndexes = this.getQueryIndexes(query);\n\t\treturn this.client.deleteQueries(queryIndexes, this.queryCacheArgs);\n\t}\n\n\tgetRefWithFallback(root: Item): ItemRef {\n\t\tif (this.getRef) {\n\t\t\treturn this.getRef(root);\n\t\t}\n\t\tif (root == null) {\n\t\t\tthrow new Error('Object is null or undefined, cannot get ref');\n\t\t}\n\t\tif (typeof root === 'object' && 'id' in root) {\n\t\t\tassertValidRefType(root.id);\n\t\t\treturn root.id.toString();\n\t\t}\n\t\tthrow new Error('Object does not have id. Define \"getRef\" function in cache options');\n\t}\n\n\tgetItemKey(item: Item): ItemCacheKey {\n\t\tconst ref = this.getRefWithFallback(item);\n\t\treturn this.getItemKeyByRef(ref);\n\t}\n\n\tgetItemKeyByRef(ref: ItemRef): ItemCacheKey {\n\t\tconst prefix: ItemCacheKeyPrefix = `${this.namespace}:{${this.name}:item:${buildCacheRevisionId(this.revision)}}`;\n\t\tconst keyId = buildItemCacheKeyId(ref.toString());\n\t\treturn `${prefix}:${keyId}`;\n\t}\n\n\tgetQueryKey(query: QueryTagWithArgs): QueryCacheKey {\n\t\tconst prefix = this.getQueryKeyPrefix(query);\n\t\treturn `${prefix}:${buildQueryCacheKeyId(query.args)}`;\n\t}\n\n\tgetQueryIndexes(query: QueryTagWithIndexes, ensureCatchAll = false): QueryCacheIndexKey[] {\n\t\tconst prefix = this.getQueryKeyPrefix(query);\n\t\tconst keyIndexes = buildQueryCacheIndexKeysIds(query.indexes);\n\t\tif (ensureCatchAll && query.indexes.length > 0) {\n\t\t\tkeyIndexes.push(buildQueryCacheIndexKeyId());\n\t\t}\n\t\treturn keyIndexes.map((keyIndex) => `${prefix}:${keyIndex}` as const);\n\t}\n\n\tgetQueryKeyPrefix(query: QueryTag): QueryCacheKeyPrefix {\n\t\treturn `${this.namespace}:{${this.name}:query:${query.name}:${this.getQueryKeyRevision(query)}}`;\n\t}\n\n\tgetQueryKeyRevision(query: QueryTag): CacheRevisionId {\n\t\tconst combinedRevision = `${this.revision}_${query.revision?.toString() ?? DEFAULT_REVISION}`;\n\t\treturn buildCacheRevisionId(combinedRevision);\n\t}\n}\n\nfunction parseItemSafely<T>(value: string, parseItem: (value: string) => T): T | null {\n\ttry {\n\t\treturn parseItem(value);\n\t} catch (err) {\n\t\tlog.warn(err, 'Failed to parse item, returning null', {\n\t\t\tvalue,\n\t\t});\n\t\treturn null;\n\t}\n}\n","import { log } from '@baeta/util-log';\nimport type { CacheClient } from './client.ts';\nimport type { QueryDefinitionMap, QueryMethod, QueryMethodMap } from './define-query.ts';\nimport type { ItemRef, RefCompatibleItem } from './item.ts';\nimport type { CacheOptions, OptionalGetRef, RequiredGetRef } from './options.ts';\nimport type { QueryOnDeleteHook, QueryOnInsertHook, QueryOnUpdateHook, QueryTag } from './query.ts';\nimport { CacheStore } from './store.ts';\nimport { toArray, zip } from './utils.ts';\n\nexport type CacheHooksOptions = {\n\t/**\n\t * If true, hooks will be awaited. If false, hooks will be fired asynchronously and will not block cache methods.\n\t * @defaultValue false\n\t */\n\twaitForHooks?: boolean;\n};\n\nexport type Cache<Item> = {\n\tget(ref: ItemRef): Promise<Item | null>;\n\tgetMany(refs: ItemRef[]): Promise<Item[] | null>;\n\tgetPartial(refs: ItemRef[]): Promise<Array<Item | null>>;\n\tupdate(item: Item | Item[]): Promise<void>;\n\tinsert(item: Item | Item[]): Promise<void>;\n\tdelete(ref: ItemRef | ItemRef[]): Promise<void>;\n};\n\nexport type CacheWithQueries<Item, QueryDefinitions extends QueryDefinitionMap<Item>> = {\n\tget(ref: ItemRef): Promise<Item | null>;\n\tgetMany(refs: ItemRef[]): Promise<Item[] | null>;\n\tgetPartial(refs: ItemRef[]): Promise<Array<Item | null>>;\n\tupdate(item: Item | Item[], options?: CacheHooksOptions): Promise<void>;\n\tinsert(item: Item | Item[], options?: CacheHooksOptions): Promise<void>;\n\tdelete(ref: ItemRef | ItemRef[], options?: CacheHooksOptions): Promise<void>;\n\tdeleteQueries(): Promise<void>;\n\tqueries: QueryMethodMap<QueryDefinitions>;\n};\n\nexport type CreateCacheFactory<Item> = {\n\tbuild: () => Cache<Item>;\n\twithQueries: <QueryDefinitions extends QueryDefinitionMap<Item>>(\n\t\tdefinitions: QueryDefinitions,\n\t) => {\n\t\tbuild: () => CacheWithQueries<Item, QueryDefinitions>;\n\t};\n};\n\nexport function createCache<Item extends RefCompatibleItem>(\n\tclient: CacheClient,\n\toptions: CacheOptions<Item> & OptionalGetRef<Item>,\n): CreateCacheFactory<Item>;\nexport function createCache<Item>(\n\tclient: CacheClient,\n\toptions: CacheOptions<Item> & RequiredGetRef<Item>,\n): CreateCacheFactory<Item>;\nexport function createCache<Item>(\n\tclient: CacheClient,\n\toptions: CacheOptions<Item> & OptionalGetRef<Item>,\n): CreateCacheFactory<Item> {\n\tconst storeClient = new CacheStore<Item>({\n\t\tclient,\n\t\tname: options.name,\n\t\tparse: options.parse,\n\t\tserialize: options.serialize,\n\t\tgetRef: options.getRef,\n\t\tttlMs: options.ttlMs ?? client.options?.ttlMs,\n\t\tnamespace: options.namespace ?? client.options?.namespace,\n\t\trevision: options.revision ?? client.options?.revision,\n\t});\n\treturn {\n\t\tbuild: () =>\n\t\t\tcreateCacheMethods(\n\t\t\t\tstoreClient,\n\t\t\t\t{\n\t\t\t\t\tonUpdateHook: options.onUpdate,\n\t\t\t\t\tonInsertHook: options.onInsert,\n\t\t\t\t\tonDeleteHook: options.onDelete,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tonUpdateHooks: [],\n\t\t\t\t\tonInsertHooks: [],\n\t\t\t\t\tonDeleteHooks: [],\n\t\t\t\t},\n\t\t\t),\n\t\twithQueries: (definitions) => {\n\t\t\treturn {\n\t\t\t\tbuild: () => createCacheMethodsWithQueries(storeClient, options, definitions),\n\t\t\t};\n\t\t},\n\t};\n}\n\nfunction createCacheMethods<Item>(\n\tstoreClient: CacheStore<Item>,\n\titemHooks: {\n\t\tonUpdateHook?: (item: Item[]) => void | Promise<void>;\n\t\tonInsertHook?: (item: Item[]) => void | Promise<void>;\n\t\tonDeleteHook?: (ref: ItemRef[]) => void | Promise<void>;\n\t},\n\tqueryHooks: {\n\t\tonUpdateHooks: Array<QueryOnUpdateHook<Item>>;\n\t\tonInsertHooks: Array<QueryOnInsertHook<Item>>;\n\t\tonDeleteHooks: Array<QueryOnDeleteHook<Item>>;\n\t},\n): Cache<Item> | Omit<CacheWithQueries<Item, any>, 'queries' | 'deleteQueries'> {\n\treturn {\n\t\tget: (ref) => storeClient.get(ref),\n\t\tgetMany: (refs) => storeClient.getMany(refs),\n\t\tgetPartial: (refs) => storeClient.getPartial(refs),\n\t\tupdate: async (item, options) => {\n\t\t\tif (queryHooks.onUpdateHooks.length === 0) {\n\t\t\t\tawait storeClient.save(item);\n\t\t\t\tawait fireHook(itemHooks.onUpdateHook, toArray(item), options);\n\t\t\t} else {\n\t\t\t\tconst items = toArray(item);\n\t\t\t\tconst previous = await storeClient.saveWithDiff(items);\n\t\t\t\tconst pairs = zip(items, previous, (next, previous) => ({ next, previous }));\n\t\t\t\tawait Promise.all([\n\t\t\t\t\tfireHook(itemHooks.onUpdateHook, items, options),\n\t\t\t\t\tfireHooks(queryHooks.onUpdateHooks, pairs, options),\n\t\t\t\t]);\n\t\t\t}\n\t\t},\n\t\tinsert: async (item, options) => {\n\t\t\tawait storeClient.save(item);\n\t\t\tif (queryHooks.onInsertHooks.length === 0) {\n\t\t\t\tawait fireHook(itemHooks.onInsertHook, toArray(item), options);\n\t\t\t} else {\n\t\t\t\tconst items = toArray(item);\n\t\t\t\tawait Promise.all([\n\t\t\t\t\tfireHook(itemHooks.onInsertHook, items, options),\n\t\t\t\t\tfireHooks(queryHooks.onInsertHooks, items, options),\n\t\t\t\t]);\n\t\t\t}\n\t\t},\n\t\tdelete: async (ref, options) => {\n\t\t\tif (queryHooks.onDeleteHooks.length === 0) {\n\t\t\t\tawait storeClient.delete(ref);\n\t\t\t\tawait fireHook(itemHooks.onDeleteHook, toArray(ref), options);\n\t\t\t} else {\n\t\t\t\tconst refs = toArray(ref);\n\t\t\t\tconst previous = await storeClient.deleteWithDiff(refs);\n\t\t\t\tawait storeClient.delete(refs);\n\t\t\t\tconst pairs = zip(refs, previous, (ref, previous) => ({ ref, previous }));\n\t\t\t\tawait Promise.all([\n\t\t\t\t\tfireHook(itemHooks.onDeleteHook, refs, options),\n\t\t\t\t\tfireHooks(queryHooks.onDeleteHooks, pairs, options),\n\t\t\t\t]);\n\t\t\t}\n\t\t},\n\t};\n}\n\nfunction createCacheMethodsWithQueries<Item, QueryDefinitions extends QueryDefinitionMap<Item>>(\n\tstoreClient: CacheStore<Item>,\n\toptions: CacheOptions<Item> & OptionalGetRef<Item>,\n\tdefinitions: QueryDefinitions,\n): CacheWithQueries<Item, QueryDefinitions> {\n\tconst { queries, queryTags, onUpdateHooks, onInsertHooks, onDeleteHooks } = createQueriesAndHooks<\n\t\tItem,\n\t\tQueryDefinitions\n\t>(definitions, storeClient);\n\treturn {\n\t\t...createCacheMethods(\n\t\t\tstoreClient,\n\t\t\t{\n\t\t\t\tonUpdateHook: options.onUpdate,\n\t\t\t\tonInsertHook: options.onInsert,\n\t\t\t\tonDeleteHook: options.onDelete,\n\t\t\t},\n\t\t\t{ onUpdateHooks, onInsertHooks, onDeleteHooks },\n\t\t),\n\t\tdeleteQueries: async () => {\n\t\t\tconst promises = queryTags.map((tag) =>\n\t\t\t\tstoreClient.deleteQueries({\n\t\t\t\t\t...tag,\n\t\t\t\t\tindexes: [],\n\t\t\t\t}),\n\t\t\t);\n\t\t\tawait Promise.all(promises);\n\t\t},\n\t\tqueries,\n\t};\n}\n\nfunction createQueriesAndHooks<Item, QueryDefinitions extends QueryDefinitionMap<Item>>(\n\tdefinitions: QueryDefinitions | undefined,\n\tstore: CacheStore<Item>,\n) {\n\tconst queries: Partial<QueryMethodMap<QueryDefinitions>> = {};\n\tconst queryTags: QueryTag[] = [];\n\tconst onUpdateHooks: Array<QueryOnUpdateHook<Item>> = [];\n\tconst onInsertHooks: Array<QueryOnInsertHook<Item>> = [];\n\tconst onDeleteHooks: Array<QueryOnDeleteHook<Item>> = [];\n\tfor (const [queryName, buildQuery] of Object.entries(definitions ?? {})) {\n\t\tconst queryHandler = buildQuery(store, queryName);\n\t\tqueries[queryName as keyof QueryDefinitions] = queryHandler.call as QueryMethod<\n\t\t\tQueryDefinitions[keyof QueryDefinitions]\n\t\t>;\n\t\tqueryTags.push(queryHandler.tag);\n\t\tif (queryHandler.hooks.onUpdate) {\n\t\t\tonUpdateHooks.push(queryHandler.hooks.onUpdate);\n\t\t}\n\t\tif (queryHandler.hooks.onDelete) {\n\t\t\tonDeleteHooks.push(queryHandler.hooks.onDelete);\n\t\t}\n\t\tif (queryHandler.hooks.onInsert) {\n\t\t\tonInsertHooks.push(queryHandler.hooks.onInsert);\n\t\t}\n\t}\n\treturn {\n\t\tqueries: queries as QueryMethodMap<QueryDefinitions>,\n\t\tqueryTags,\n\t\tonUpdateHooks,\n\t\tonInsertHooks,\n\t\tonDeleteHooks,\n\t};\n}\n\nasync function fireHook<T>(\n\thook: ((v: T) => void | Promise<void>) | undefined,\n\tvalue: T,\n\toptions?: CacheHooksOptions,\n) {\n\tif (hook == null) {\n\t\treturn;\n\t}\n\tif (options?.waitForHooks) {\n\t\tawait hook(value);\n\t} else {\n\t\tPromise.resolve()\n\t\t\t.then(() => hook(value))\n\t\t\t.catch((err) => {\n\t\t\t\tlog.error(err, 'Error in cache hook');\n\t\t\t});\n\t}\n}\n\nasync function fireHooks<T>(\n\thooks: Array<(v: T) => Promise<void>>,\n\tvalue: T,\n\toptions?: CacheHooksOptions,\n) {\n\tawait Promise.all(hooks.map((hook) => fireHook(hook, value, options)));\n}\n","import { log } from '@baeta/util-log';\nimport type {\n\tIndexedQueryArgs,\n\tItemFromQueryResult,\n\tQueryArgs,\n\tQueryArgsIndexes,\n\tQueryHandle,\n\tQueryHelpers,\n\tQueryHelpersWithIndexes,\n\tQueryIndex,\n\tQueryOptions,\n\tQueryResult,\n\tQueryTag,\n} from './query.ts';\nimport type { CacheStore } from './store.ts';\n\nexport type QueryFactory<\n\tResult extends QueryResult<Item>,\n\tArgs extends QueryArgs,\n\tIndexes extends QueryArgsIndexes<Args, Indexes>,\n\tItem = ItemFromQueryResult<Result>,\n> = (store: CacheStore<Item>, name: string) => QueryHandle<Result, Args, Indexes, Item>;\n\nexport type QueryMethod<T extends QueryFactory<any, any, any>> =\n\tT extends QueryFactory<infer Result, infer Args, any, any>\n\t\t? (args: Args) => Promise<Result>\n\t\t: never;\n\nexport type QueryDefinitionMap<Item> = Record<\n\tstring,\n\tQueryFactory<QueryResult<Item>, any, any, Item>\n>;\n\nexport type QueryMethodMap<Definitions extends QueryDefinitionMap<any>> = {\n\t[K in keyof Definitions]: QueryMethod<Definitions[K]>;\n};\n\nexport function defineQuery<\n\tResult extends QueryResult<Item>,\n\tArgs extends QueryArgs = Record<string, never>,\n\tIndexes extends QueryArgsIndexes<Args, Indexes> = Record<string, never>,\n\tItem = ItemFromQueryResult<Result>,\n>(options: QueryOptions<Result, Args, Indexes, Item>): QueryFactory<Result, Args, Indexes, Item> {\n\treturn (store: CacheStore<Item>, name: string) => {\n\t\tconst tag: QueryTag = {\n\t\t\tname,\n\t\t\trevision: options.revision,\n\t\t};\n\n\t\tconst resolve = async (args: Args): Promise<Result> => {\n\t\t\tconst fromCache = await store\n\t\t\t\t.getQuery({\n\t\t\t\t\t...tag,\n\t\t\t\t\targs,\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tlog.error(err, `Failed to get query result for ${name}. Proceeding without caching.`);\n\t\t\t\t\treturn null;\n\t\t\t\t});\n\n\t\t\tif (fromCache != null) {\n\t\t\t\treturn fromCache.query as Result;\n\t\t\t}\n\n\t\t\tconst data = await options.resolve(args);\n\n\t\t\tawait store\n\t\t\t\t.saveQuery({\n\t\t\t\t\t...tag,\n\t\t\t\t\targs,\n\t\t\t\t\tdata,\n\t\t\t\t\tindexes: buildIndexes(args, options.indexArgsBy),\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tlog.error(err, `Failed to save query result for ${name}. Proceeding without caching.`);\n\t\t\t\t});\n\n\t\t\treturn data;\n\t\t};\n\n\t\tconst invalidateAll = async () => {\n\t\t\tawait store.deleteQueries({\n\t\t\t\t...tag,\n\t\t\t\tindexes: [],\n\t\t\t});\n\t\t};\n\n\t\tconst invalidateByArgs = async (\n\t\t\targs: Partial<IndexedQueryArgs<Args, Indexes>> | Partial<IndexedQueryArgs<Args, Indexes>>[],\n\t\t) => {\n\t\t\tconst argsArray = Array.isArray(args) ? args : [args];\n\t\t\tconst deletePromises = argsArray.map((args) =>\n\t\t\t\tstore.deleteQueries({\n\t\t\t\t\t...tag,\n\t\t\t\t\tindexes: buildIndexes(args, options.indexArgsBy),\n\t\t\t\t}),\n\t\t\t);\n\t\t\tawait Promise.all(deletePromises);\n\t\t};\n\n\t\tconst helpers: QueryHelpersWithIndexes<Args, Indexes> = {\n\t\t\tinvalidateAll,\n\t\t\tinvalidateByArgs,\n\t\t};\n\t\tconst helpersCast: QueryHelpers<Args, Indexes> = helpers as QueryHelpers<Args, Indexes>;\n\n\t\tconst { onUpdate, onDelete, onInsert } = options;\n\n\t\tconst hooks = {\n\t\t\tonUpdate: hookWithHelpers(helpersCast, onUpdate),\n\t\t\tonInsert: hookWithHelpers(helpersCast, onInsert),\n\t\t\tonDelete: hookWithHelpers(helpersCast, onDelete),\n\t\t};\n\n\t\treturn {\n\t\t\tcall: resolve,\n\t\t\ttag,\n\t\t\thelpers,\n\t\t\thooks,\n\t\t};\n\t};\n}\n\nfunction hookWithHelpers<Helpers, Args>(\n\thelpers: Helpers,\n\thook?: (args: Args, helpers: Helpers) => void | Promise<void>,\n) {\n\tif (!hook) {\n\t\treturn undefined;\n\t}\n\tconst hookWithHelpers = async (args: Args) => {\n\t\tawait hook(args, helpers);\n\t};\n\treturn hookWithHelpers;\n}\n\nfunction buildIndexes<Args extends QueryArgs, Indexes extends QueryArgsIndexes<Args, Indexes>>(\n\targs: Partial<Args>,\n\tindexArgsBy?: Indexes,\n): QueryIndex[] {\n\tif (!indexArgsBy) {\n\t\treturn [];\n\t}\n\tconst indexes: QueryIndex[] = [];\n\tfor (const key in indexArgsBy) {\n\t\tif (!Object.hasOwn(args, key)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst value = args[key];\n\t\tif (value === undefined) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (Array.isArray(value)) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof value === 'object' && value !== null) {\n\t\t\tcontinue;\n\t\t}\n\t\tindexes.push([key, value]);\n\t}\n\treturn indexes;\n}\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ //#region sdk/batch.d.ts
2
+ declare function doBatched<T>(items: T[], batchSize: number, fn: (batch: T[]) => Promise<void>): Promise<void>;
3
+ //#endregion
4
+ export { doBatched };
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,8 @@
1
+ //#region sdk/batch.ts
2
+ async function doBatched(items, batchSize, fn) {
3
+ for (let i = 0; i < items.length; i += batchSize) await fn(items.slice(i, i + batchSize));
4
+ }
5
+ //#endregion
6
+ export { doBatched };
7
+
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../sdk/batch.ts"],"sourcesContent":["export async function doBatched<T>(\n\titems: T[],\n\tbatchSize: number,\n\tfn: (batch: T[]) => Promise<void>,\n): Promise<void> {\n\tfor (let i = 0; i < items.length; i += batchSize) {\n\t\tconst batch = items.slice(i, i + batchSize);\n\t\tawait fn(batch);\n\t}\n}\n"],"mappings":";AAAA,eAAsB,UACrB,OACA,WACA,IACgB;AAChB,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,UAEtC,OAAM,GADQ,MAAM,MAAM,GAAG,IAAI,UAAU,CAC5B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baeta/cache",
3
- "version": "0.0.0",
3
+ "version": "2.0.0-next.12",
4
4
  "keywords": [
5
5
  "baeta",
6
6
  "graphql",
@@ -25,13 +25,75 @@
25
25
  "url": "https://github.com/andreisergiu98"
26
26
  },
27
27
  "type": "module",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./sdk": {
34
+ "types": "./dist/sdk/index.d.ts",
35
+ "default": "./dist/sdk/index.js"
36
+ }
37
+ },
38
+ "types": "dist/index.d.ts",
28
39
  "files": [
40
+ "dist",
29
41
  "package.json"
30
42
  ],
43
+ "scripts": {
44
+ "build": "builder build",
45
+ "check:deps": "builder check-deps",
46
+ "prepack": "builder prepare",
47
+ "postpack": "builder prepare --restore",
48
+ "test": "builder test --skip-coverage",
49
+ "test:circular": "builder test-circular",
50
+ "types": "tsc --noEmit"
51
+ },
52
+ "ava": {
53
+ "extensions": {
54
+ "ts": "module"
55
+ }
56
+ },
57
+ "dependencies": {
58
+ "@baeta/util-encoding": "^2.0.0-next.3",
59
+ "@baeta/util-log": "^2.0.0-next.3",
60
+ "dataloader": "^2.2.3",
61
+ "ohash": "^2.0.11"
62
+ },
63
+ "devDependencies": {
64
+ "@baeta/builder": "^0.0.0",
65
+ "@baeta/testing": "^0.0.0",
66
+ "@baeta/tsconfig": "^0.0.0",
67
+ "typescript": "^5.9.3"
68
+ },
31
69
  "engines": {
32
70
  "node": ">=22.20.0"
33
71
  },
34
72
  "publishConfig": {
35
- "access": "public"
73
+ "access": "public",
74
+ "exports": {
75
+ ".": {
76
+ "types": "./dist/index.d.ts",
77
+ "default": "./dist/index.js"
78
+ },
79
+ "./sdk": {
80
+ "types": "./dist/sdk/index.d.ts",
81
+ "default": "./dist/sdk/index.js"
82
+ }
83
+ }
84
+ },
85
+ "typedocOptions": {
86
+ "entryPoints": [
87
+ "./index.ts",
88
+ "./sdk/index.ts"
89
+ ],
90
+ "readme": "none",
91
+ "tsconfig": "./tsconfig.json",
92
+ "sort": [
93
+ "kind",
94
+ "instance-first",
95
+ "required-first",
96
+ "alphabetical-ignoring-documents"
97
+ ]
36
98
  }
37
- }
99
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@baeta/cache/sdk",
3
+ "types": "../dist/sdk/index.d.ts",
4
+ "type": "module"
5
+ }