@atcute/cache 0.1.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.
@@ -0,0 +1,34 @@
1
+ import type { BaseSchema } from '@atcute/lexicons/validations';
2
+ import type { EntityTypeId } from './types.js';
3
+ /**
4
+ * compiled walk function for a schema
5
+ * @param data input data to walk
6
+ * @returns walked data with entities swapped in
7
+ */
8
+ export type WalkFn = (data: unknown) => unknown;
9
+ /**
10
+ * context for building and executing schema walkers
11
+ */
12
+ export interface WalkerContext {
13
+ /** check if a type ID is a registered entity */
14
+ isEntityType: (typeId: EntityTypeId) => boolean;
15
+ /** upsert entity into cache, returns the cached entity */
16
+ upsertEntity: (typeId: EntityTypeId, incoming: object) => object;
17
+ }
18
+ /**
19
+ * walker cache that tracks schema -> compiled walker mappings
20
+ * invalidated when entity definitions change
21
+ */
22
+ export declare class WalkerCache {
23
+ #private;
24
+ constructor(ctx: WalkerContext);
25
+ /** clear all cached walkers */
26
+ invalidate(): void;
27
+ /**
28
+ * get or build a walker for a schema
29
+ * @param schema schema to get walker for
30
+ * @returns walk function
31
+ */
32
+ getWalker(schema: BaseSchema): WalkFn;
33
+ }
34
+ //# sourceMappingURL=walker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walker.d.ts","sourceRoot":"","sources":["../lib/walker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAA+B,MAAM,8BAA8B,CAAC;AAS5F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC;AAKhD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,gDAAgD;IAChD,YAAY,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,OAAO,CAAC;IAChD,0DAA0D;IAC1D,YAAY,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;CACjE;AAED;;;GAGG;AACH,qBAAa,WAAW;;IAIvB,YAAY,GAAG,EAAE,aAAa,EAE7B;IAED,+BAA+B;IAC/B,UAAU,IAAI,IAAI,CAEjB;IAED;;;;OAIG;IACH,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAEpC;CAwHD"}
package/dist/walker.js ADDED
@@ -0,0 +1,179 @@
1
+ import { isArraySchema, isNullableSchema, isObjectSchema, isOptionalSchema, isVariantSchema, } from './predicates.js';
2
+ import { getTypeIdFromSchema } from './types.js';
3
+ /** identity walker - returns data unchanged */
4
+ const identity = (data) => data;
5
+ /**
6
+ * walker cache that tracks schema -> compiled walker mappings
7
+ * invalidated when entity definitions change
8
+ */
9
+ export class WalkerCache {
10
+ #ctx;
11
+ #cache = new WeakMap();
12
+ constructor(ctx) {
13
+ this.#ctx = ctx;
14
+ }
15
+ /** clear all cached walkers */
16
+ invalidate() {
17
+ this.#cache = new WeakMap();
18
+ }
19
+ /**
20
+ * get or build a walker for a schema
21
+ * @param schema schema to get walker for
22
+ * @returns walk function
23
+ */
24
+ getWalker(schema) {
25
+ return this.#build(schema);
26
+ }
27
+ #build(schema) {
28
+ const cached = this.#cache.get(schema);
29
+ if (cached !== undefined) {
30
+ return cached;
31
+ }
32
+ // set thunk for cycle detection - will be replaced with actual walker
33
+ this.#cache.set(schema, (data) => this.#cache.get(schema)(data));
34
+ const walker = this.#buildForSchema(schema);
35
+ this.#cache.set(schema, walker);
36
+ return walker;
37
+ }
38
+ #buildForSchema(schema) {
39
+ if (isObjectSchema(schema)) {
40
+ return this.#buildObjectWalker(schema);
41
+ }
42
+ if (isArraySchema(schema)) {
43
+ return this.#buildArrayWalker(schema);
44
+ }
45
+ if (isVariantSchema(schema)) {
46
+ return this.#buildVariantWalker(schema);
47
+ }
48
+ if (isOptionalSchema(schema)) {
49
+ const innerWalker = this.#build(schema.wrapped);
50
+ if (innerWalker === identity) {
51
+ return identity;
52
+ }
53
+ return createOptionalWalker(innerWalker);
54
+ }
55
+ if (isNullableSchema(schema)) {
56
+ const innerWalker = this.#build(schema.wrapped);
57
+ if (innerWalker === identity) {
58
+ return identity;
59
+ }
60
+ return createNullableWalker(innerWalker);
61
+ }
62
+ // primitive types - no walking needed
63
+ return identity;
64
+ }
65
+ #buildObjectWalker(schema) {
66
+ const ctx = this.#ctx;
67
+ // check if this is a registered entity type
68
+ const typeId = getTypeIdFromSchema(schema);
69
+ const entityTypeId = typeId !== undefined && ctx.isEntityType(typeId) ? typeId : undefined;
70
+ // build walkers for properties that need walking
71
+ const shape = schema.shape;
72
+ let propWalkers = [];
73
+ for (const propName in shape) {
74
+ const propSchema = shape[propName];
75
+ const propWalker = this.#build(propSchema);
76
+ if (propWalker !== identity) {
77
+ propWalkers.push([propName, propWalker]);
78
+ }
79
+ }
80
+ if (propWalkers.length === 0) {
81
+ propWalkers = undefined;
82
+ }
83
+ // nothing to do
84
+ if (entityTypeId === undefined && propWalkers === undefined) {
85
+ return identity;
86
+ }
87
+ return createObjectWalker(ctx, entityTypeId, propWalkers);
88
+ }
89
+ #buildArrayWalker(schema) {
90
+ const itemWalker = this.#build(schema.item);
91
+ if (itemWalker === identity) {
92
+ return identity;
93
+ }
94
+ return createArrayWalker(itemWalker);
95
+ }
96
+ #buildVariantWalker(schema) {
97
+ // build a map of type ID -> walker for members that need walking
98
+ const walkerMap = Object.create(null);
99
+ let hasWalkers = false;
100
+ for (const member of schema.members) {
101
+ const memberSchema = member;
102
+ const memberTypeId = getTypeIdFromSchema(memberSchema);
103
+ if (memberTypeId !== undefined) {
104
+ const memberWalker = this.#build(memberSchema);
105
+ if (memberWalker !== identity) {
106
+ walkerMap[memberTypeId] = memberWalker;
107
+ hasWalkers = true;
108
+ }
109
+ }
110
+ }
111
+ if (!hasWalkers) {
112
+ return identity;
113
+ }
114
+ return createVariantWalker(walkerMap);
115
+ }
116
+ }
117
+ /** creates an object walker with minimal closure scope */
118
+ const createObjectWalker = (ctx, entityTypeId, propWalkers) => {
119
+ return (data) => {
120
+ let entity = data;
121
+ let cloned = entityTypeId !== undefined;
122
+ if (entityTypeId !== undefined) {
123
+ entity = ctx.upsertEntity(entityTypeId, entity);
124
+ }
125
+ if (propWalkers !== undefined) {
126
+ for (const [name, walk] of propWalkers) {
127
+ const propValue = entity[name];
128
+ const walked = walk(propValue);
129
+ if (walked !== propValue) {
130
+ if (entityTypeId === undefined && !cloned) {
131
+ entity = { ...entity };
132
+ cloned = true;
133
+ }
134
+ entity[name] = walked;
135
+ }
136
+ }
137
+ }
138
+ return entity;
139
+ };
140
+ };
141
+ /** creates an array walker with minimal closure scope */
142
+ const createArrayWalker = (itemWalker) => {
143
+ return (data) => {
144
+ const prev = data;
145
+ let next;
146
+ for (let i = 0; i < prev.length; i++) {
147
+ const item = prev[i];
148
+ const walked = itemWalker(item);
149
+ if (walked !== item) {
150
+ if (next === undefined) {
151
+ next = prev.slice();
152
+ }
153
+ next[i] = walked;
154
+ }
155
+ }
156
+ return next ?? prev;
157
+ };
158
+ };
159
+ /** creates a variant walker with minimal closure scope */
160
+ const createVariantWalker = (walkerMap) => {
161
+ return (data) => {
162
+ const obj = data;
163
+ const type = obj.$type;
164
+ if (type === undefined) {
165
+ return data;
166
+ }
167
+ const memberWalker = walkerMap[type];
168
+ return memberWalker ? memberWalker(data) : data;
169
+ };
170
+ };
171
+ /** creates an optional walker with minimal closure scope */
172
+ const createOptionalWalker = (innerWalker) => {
173
+ return (data) => (data === undefined ? data : innerWalker(data));
174
+ };
175
+ /** creates a nullable walker with minimal closure scope */
176
+ const createNullableWalker = (innerWalker) => {
177
+ return (data) => (data === null ? data : innerWalker(data));
178
+ };
179
+ //# sourceMappingURL=walker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walker.js","sourceRoot":"","sources":["../lib/walker.ts"],"names":[],"mappings":"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"}
package/lib/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { NormalizedCache } from './store.js';
2
+ export type { NormalizedCacheOptions } from './store.js';
3
+ export type { EntityDefinition, EntitySubscriber, EntityTypeId, TypeSubscriber } from './types.js';
@@ -0,0 +1,39 @@
1
+ import type {
2
+ ArraySchema,
3
+ BaseSchema,
4
+ LiteralSchema,
5
+ NullableSchema,
6
+ ObjectSchema,
7
+ OptionalSchema,
8
+ VariantSchema,
9
+ } from '@atcute/lexicons/validations';
10
+
11
+ /** check if schema is an object schema */
12
+ export const isObjectSchema = (schema: BaseSchema): schema is ObjectSchema => {
13
+ return schema.type === 'object';
14
+ };
15
+
16
+ /** check if schema is an array schema */
17
+ export const isArraySchema = (schema: BaseSchema): schema is ArraySchema => {
18
+ return schema.type === 'array';
19
+ };
20
+
21
+ /** check if schema is a variant schema */
22
+ export const isVariantSchema = (schema: BaseSchema): schema is VariantSchema => {
23
+ return schema.type === 'variant';
24
+ };
25
+
26
+ /** check if schema is an optional schema */
27
+ export const isOptionalSchema = (schema: BaseSchema): schema is OptionalSchema => {
28
+ return schema.type === 'optional';
29
+ };
30
+
31
+ /** check if schema is a nullable schema */
32
+ export const isNullableSchema = (schema: BaseSchema): schema is NullableSchema => {
33
+ return schema.type === 'nullable';
34
+ };
35
+
36
+ /** check if schema is a literal schema */
37
+ export const isLiteralSchema = (schema: BaseSchema): schema is LiteralSchema => {
38
+ return schema.type === 'literal';
39
+ };
package/lib/store.ts ADDED
@@ -0,0 +1,364 @@
1
+ import type { BaseSchema, InferOutput, ObjectSchema } from '@atcute/lexicons/validations';
2
+
3
+ import type { EntityDefinition, EntitySubscriber, EntityTypeId, TypeSubscriber } from './types.js';
4
+ import { getTypeIdFromSchema } from './types.js';
5
+ import { WalkerCache } from './walker.js';
6
+
7
+ type AnyEntityDefinition = EntityDefinition<ObjectSchema>;
8
+
9
+ interface EntityStoreEntry {
10
+ definition: AnyEntityDefinition;
11
+ entities: Map<string, WeakRef<object>>;
12
+ subscribers: Map<string, Set<EntitySubscriber<unknown>>>;
13
+ typeSubscribers: Set<TypeSubscriber<unknown>>;
14
+ }
15
+
16
+ export interface NormalizedCacheOptions {
17
+ wrapEntity?: (entity: unknown) => unknown;
18
+ }
19
+
20
+ /**
21
+ * normalized cache store for AT Protocol responses
22
+ */
23
+ export class NormalizedCache {
24
+ #schemaToTypeId = new Map<ObjectSchema, EntityTypeId>();
25
+
26
+ #stores = new Map<EntityTypeId, EntityStoreEntry>();
27
+ #wrapEntity: ((entity: unknown) => unknown) | undefined;
28
+
29
+ #walkerCache = new WalkerCache({
30
+ isEntityType: (typeId) => this.#stores.has(typeId),
31
+ upsertEntity: (typeId, incoming) => this.#upsertEntity(typeId, incoming),
32
+ });
33
+
34
+ #registry = new FinalizationRegistry<{ typeId: EntityTypeId; key: string }>((held) => {
35
+ const store = this.#stores.get(held.typeId);
36
+ if (store) {
37
+ const ref = store.entities.get(held.key);
38
+ // only delete if the ref is actually dead (not replaced with a new one)
39
+ if (ref !== undefined && ref.deref() === undefined) {
40
+ store.entities.delete(held.key);
41
+ }
42
+ }
43
+ });
44
+
45
+ constructor(options?: NormalizedCacheOptions) {
46
+ this.#wrapEntity = options?.wrapEntity;
47
+ }
48
+
49
+ #getTypeId(schema: ObjectSchema): EntityTypeId | undefined {
50
+ let typeId = this.#schemaToTypeId.get(schema);
51
+ if (typeId === undefined) {
52
+ typeId = getTypeIdFromSchema(schema);
53
+ if (typeId !== undefined) {
54
+ this.#schemaToTypeId.set(schema, typeId);
55
+ }
56
+ }
57
+ return typeId;
58
+ }
59
+
60
+ #getStore(schema: ObjectSchema): EntityStoreEntry | undefined {
61
+ const typeId = this.#getTypeId(schema);
62
+ return typeId ? this.#stores.get(typeId) : undefined;
63
+ }
64
+
65
+ #notifySubscribers(store: EntityStoreEntry, key: string, entity: object | undefined): void {
66
+ // notify entity-specific subscribers
67
+ const entitySubs = store.subscribers.get(key);
68
+ if (entitySubs) {
69
+ for (const cb of entitySubs) {
70
+ cb(entity);
71
+ }
72
+ }
73
+
74
+ // notify type subscribers
75
+ for (const cb of store.typeSubscribers) {
76
+ cb(key, entity);
77
+ }
78
+ }
79
+
80
+ #upsertEntity(typeId: EntityTypeId, incoming: object): object {
81
+ const store = this.#stores.get(typeId)!;
82
+ const key = store.definition.key(incoming);
83
+
84
+ const existingRef = store.entities.get(key);
85
+ const existing = existingRef?.deref();
86
+
87
+ if (existing !== undefined) {
88
+ // merge incoming into existing
89
+ const merge = store.definition.merge;
90
+ const merged = merge ? merge(existing, incoming) : incoming;
91
+ Object.assign(existing, merged);
92
+ this.#notifySubscribers(store, key, existing);
93
+ return existing;
94
+ }
95
+
96
+ // new entity - wrap and store it
97
+ const entity: any = this.#wrapEntity ? this.#wrapEntity(incoming) : incoming;
98
+ store.entities.set(key, new WeakRef(entity));
99
+ this.#registry.register(entity, { typeId, key });
100
+ this.#notifySubscribers(store, key, entity);
101
+ return entity;
102
+ }
103
+
104
+ /**
105
+ * register an entity type for normalization
106
+ * @param definition entity definition with schema, key extractor, and optional merge function
107
+ */
108
+ define<T extends ObjectSchema>(definition: EntityDefinition<T>): void {
109
+ const typeId = getTypeIdFromSchema(definition.schema);
110
+ if (typeId === undefined) {
111
+ throw new Error('schema must have a $type literal field');
112
+ }
113
+
114
+ if (this.#stores.has(typeId)) {
115
+ throw new Error(`entity type "${typeId}" is already defined`);
116
+ }
117
+
118
+ this.#stores.set(typeId, {
119
+ definition: definition as unknown as AnyEntityDefinition,
120
+ entities: new Map(),
121
+ subscribers: new Map(),
122
+ typeSubscribers: new Set(),
123
+ });
124
+
125
+ this.#schemaToTypeId.set(definition.schema, typeId);
126
+
127
+ // invalidate cached walkers since entity types changed
128
+ this.#walkerCache.invalidate();
129
+ }
130
+
131
+ /**
132
+ * walk response using schema, normalize and cache entities
133
+ * @param schema the response schema
134
+ * @param data the response data
135
+ * @returns response with cached entity refs swapped in
136
+ */
137
+ normalize<T extends BaseSchema>(schema: T, data: InferOutput<T>): InferOutput<T> {
138
+ return this.#walkerCache.getWalker(schema)(data) as InferOutput<T>;
139
+ }
140
+
141
+ /**
142
+ * create a reusable normalizer function for a schema
143
+ * @param schema the response schema
144
+ * @returns function that normalizes data according to schema
145
+ */
146
+ normalizer<T extends BaseSchema>(schema: T): (data: InferOutput<T>) => InferOutput<T> {
147
+ return (data) => this.normalize(schema, data);
148
+ }
149
+
150
+ /**
151
+ * get entity from cache by schema and key
152
+ * @param schema the entity schema
153
+ * @param key the entity key
154
+ * @returns the cached entity or undefined if not found/collected
155
+ */
156
+ get<T extends ObjectSchema>(schema: T, key: string): InferOutput<T> | undefined {
157
+ const store = this.#getStore(schema);
158
+ if (!store) {
159
+ return undefined;
160
+ }
161
+
162
+ const ref = store.entities.get(key);
163
+ return ref?.deref() as InferOutput<T> | undefined;
164
+ }
165
+
166
+ /**
167
+ * check if entity exists in cache
168
+ * @param schema the entity schema
169
+ * @param key the entity key
170
+ */
171
+ has(schema: ObjectSchema, key: string): boolean {
172
+ const store = this.#getStore(schema);
173
+ if (!store) {
174
+ return false;
175
+ }
176
+
177
+ const ref = store.entities.get(key);
178
+ return ref?.deref() !== undefined;
179
+ }
180
+
181
+ /**
182
+ * get all cached entities of a type
183
+ * @param schema the entity schema
184
+ * @returns map of key to entity (only includes live refs)
185
+ */
186
+ getAll<T extends ObjectSchema>(schema: T): Map<string, InferOutput<T>> {
187
+ const store = this.#getStore(schema);
188
+ const result = new Map<string, InferOutput<T>>();
189
+
190
+ if (!store) {
191
+ return result;
192
+ }
193
+
194
+ for (const [key, ref] of store.entities) {
195
+ const entity = ref.deref();
196
+ if (entity !== undefined) {
197
+ result.set(key, entity as InferOutput<T>);
198
+ }
199
+ }
200
+
201
+ return result;
202
+ }
203
+
204
+ /**
205
+ * set entity directly in cache
206
+ * @param schema the entity schema
207
+ * @param key the entity key
208
+ * @param entity the entity to cache
209
+ */
210
+ set<T extends ObjectSchema>(schema: T, key: string, entity: InferOutput<T>): void {
211
+ const typeId = this.#getTypeId(schema);
212
+ if (typeId === undefined || !this.#stores.has(typeId)) {
213
+ throw new Error('schema is not registered');
214
+ }
215
+
216
+ const store = this.#stores.get(typeId)!;
217
+ const existingRef = store.entities.get(key);
218
+ const existing = existingRef?.deref();
219
+
220
+ if (existing !== undefined) {
221
+ Object.assign(existing, entity);
222
+ this.#notifySubscribers(store, key, existing);
223
+ } else {
224
+ const wrapped: any = this.#wrapEntity ? this.#wrapEntity(entity) : entity;
225
+ store.entities.set(key, new WeakRef(wrapped));
226
+ this.#registry.register(wrapped, { typeId, key });
227
+ this.#notifySubscribers(store, key, wrapped);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * update entity with updater function
233
+ * @param schema the entity schema
234
+ * @param key the entity key
235
+ * @param updater function that returns updated entity
236
+ * @returns true if entity was found and updated
237
+ */
238
+ update<T extends ObjectSchema>(
239
+ schema: T,
240
+ key: string,
241
+ updater: (entity: InferOutput<T>) => InferOutput<T>,
242
+ ): boolean {
243
+ const store = this.#getStore(schema);
244
+ if (!store) {
245
+ return false;
246
+ }
247
+
248
+ const ref = store.entities.get(key);
249
+ const existing = ref?.deref() as InferOutput<T> | undefined;
250
+
251
+ if (existing === undefined) {
252
+ return false;
253
+ }
254
+
255
+ const updated = updater(existing);
256
+ Object.assign(existing, updated);
257
+ this.#notifySubscribers(store, key, existing);
258
+ return true;
259
+ }
260
+
261
+ /**
262
+ * delete entity from cache
263
+ * @param schema the entity schema
264
+ * @param key the entity key
265
+ * @returns true if entity was found and deleted
266
+ */
267
+ delete(schema: ObjectSchema, key: string): boolean {
268
+ const store = this.#getStore(schema);
269
+ if (!store) {
270
+ return false;
271
+ }
272
+
273
+ const existed = store.entities.has(key);
274
+ store.entities.delete(key);
275
+
276
+ if (existed) {
277
+ this.#notifySubscribers(store, key, undefined);
278
+ }
279
+
280
+ return existed;
281
+ }
282
+
283
+ /**
284
+ * delete all entities of a type
285
+ * @param schema the entity schema
286
+ */
287
+ deleteType(schema: ObjectSchema): void {
288
+ const store = this.#getStore(schema);
289
+ if (!store) {
290
+ return;
291
+ }
292
+
293
+ const keys = [...store.entities.keys()];
294
+ store.entities.clear();
295
+
296
+ for (const key of keys) {
297
+ this.#notifySubscribers(store, key, undefined);
298
+ }
299
+ }
300
+
301
+ /** clear entire cache */
302
+ clear(): void {
303
+ for (const [_typeId, store] of this.#stores) {
304
+ const keys = [...store.entities.keys()];
305
+ store.entities.clear();
306
+
307
+ for (const key of keys) {
308
+ this.#notifySubscribers(store, key, undefined);
309
+ }
310
+ }
311
+ }
312
+
313
+ /**
314
+ * subscribe to changes for a specific entity
315
+ * @param schema the entity schema
316
+ * @param key the entity key
317
+ * @param callback called when entity changes
318
+ * @returns unsubscribe function
319
+ */
320
+ subscribe<T extends ObjectSchema>(
321
+ schema: T,
322
+ key: string,
323
+ callback: EntitySubscriber<InferOutput<T>>,
324
+ ): () => void {
325
+ const store = this.#getStore(schema);
326
+ if (!store) {
327
+ throw new Error('schema is not registered');
328
+ }
329
+
330
+ let subs = store.subscribers.get(key);
331
+ if (!subs) {
332
+ subs = new Set();
333
+ store.subscribers.set(key, subs);
334
+ }
335
+
336
+ subs.add(callback as EntitySubscriber<unknown>);
337
+
338
+ return () => {
339
+ subs!.delete(callback as EntitySubscriber<unknown>);
340
+ if (subs!.size === 0) {
341
+ store.subscribers.delete(key);
342
+ }
343
+ };
344
+ }
345
+
346
+ /**
347
+ * subscribe to all changes for an entity type
348
+ * @param schema the entity schema
349
+ * @param callback called when any entity of this type changes
350
+ * @returns unsubscribe function
351
+ */
352
+ subscribeType<T extends ObjectSchema>(schema: T, callback: TypeSubscriber<InferOutput<T>>): () => void {
353
+ const store = this.#getStore(schema);
354
+ if (!store) {
355
+ throw new Error('schema is not registered');
356
+ }
357
+
358
+ store.typeSubscribers.add(callback as TypeSubscriber<unknown>);
359
+
360
+ return () => {
361
+ store.typeSubscribers.delete(callback as TypeSubscriber<unknown>);
362
+ };
363
+ }
364
+ }
package/lib/types.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { BaseSchema, InferOutput, ObjectSchema } from '@atcute/lexicons/validations';
2
+
3
+ import { isLiteralSchema, isOptionalSchema } from './predicates.js';
4
+
5
+ /** entity type identifier, extracted from schema's $type literal */
6
+ export type EntityTypeId = string;
7
+
8
+ /**
9
+ * definition for an entity type that can be normalized
10
+ * @template T the object schema type
11
+ */
12
+ export interface EntityDefinition<T extends ObjectSchema = ObjectSchema> {
13
+ /** the schema for this entity type */
14
+ schema: T;
15
+ /** extract cache key from entity instance */
16
+ key: (entity: InferOutput<T>) => string;
17
+ /**
18
+ * merge strategy when entity already exists in cache
19
+ * @param existing the currently cached entity
20
+ * @param incoming the new entity data
21
+ * @returns partial entity with fields to update
22
+ */
23
+ merge?: (existing: InferOutput<T>, incoming: InferOutput<T>) => Partial<InferOutput<T>>;
24
+ }
25
+
26
+ /** subscriber callback type */
27
+ export type EntitySubscriber<T> = (entity: T | undefined) => void;
28
+
29
+ /** type-level subscriber callback */
30
+ export type TypeSubscriber<T> = (key: string, entity: T | undefined) => void;
31
+
32
+ /**
33
+ * extract the $type literal value from an object schema
34
+ * @param schema object schema with $type field
35
+ * @returns the $type string value or undefined
36
+ */
37
+ export const getTypeIdFromSchema = (schema: ObjectSchema): EntityTypeId | undefined => {
38
+ const shape = schema.shape;
39
+ let typeField: BaseSchema | undefined = shape.$type;
40
+
41
+ if (typeField === undefined) {
42
+ return undefined;
43
+ }
44
+
45
+ // unwrap optional
46
+ if (isOptionalSchema(typeField)) {
47
+ typeField = typeField.wrapped;
48
+ }
49
+
50
+ if (isLiteralSchema(typeField) && typeof typeField.expected === 'string') {
51
+ return typeField.expected;
52
+ }
53
+
54
+ return undefined;
55
+ };