@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.
- package/LICENSE +14 -0
- package/README.md +105 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/predicates.d.ts +14 -0
- package/dist/predicates.d.ts.map +1 -0
- package/dist/predicates.js +25 -0
- package/dist/predicates.js.map +1 -0
- package/dist/store.d.ts +94 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +291 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -0
- package/dist/walker.d.ts +34 -0
- package/dist/walker.d.ts.map +1 -0
- package/dist/walker.js +179 -0
- package/dist/walker.js.map +1 -0
- package/lib/index.ts +3 -0
- package/lib/predicates.ts +39 -0
- package/lib/store.ts +364 -0
- package/lib/types.ts +55 -0
- package/lib/walker.ts +259 -0
- package/package.json +32 -0
package/dist/walker.d.ts
ADDED
|
@@ -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,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
|
+
};
|