@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/lib/walker.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { BaseSchema, ObjectSchema, VariantSchema } from '@atcute/lexicons/validations';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isArraySchema,
|
|
5
|
+
isNullableSchema,
|
|
6
|
+
isObjectSchema,
|
|
7
|
+
isOptionalSchema,
|
|
8
|
+
isVariantSchema,
|
|
9
|
+
} from './predicates.js';
|
|
10
|
+
import type { EntityTypeId } from './types.js';
|
|
11
|
+
import { getTypeIdFromSchema } from './types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* compiled walk function for a schema
|
|
15
|
+
* @param data input data to walk
|
|
16
|
+
* @returns walked data with entities swapped in
|
|
17
|
+
*/
|
|
18
|
+
export type WalkFn = (data: unknown) => unknown;
|
|
19
|
+
|
|
20
|
+
/** identity walker - returns data unchanged */
|
|
21
|
+
const identity: WalkFn = (data) => data;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* context for building and executing schema walkers
|
|
25
|
+
*/
|
|
26
|
+
export interface WalkerContext {
|
|
27
|
+
/** check if a type ID is a registered entity */
|
|
28
|
+
isEntityType: (typeId: EntityTypeId) => boolean;
|
|
29
|
+
/** upsert entity into cache, returns the cached entity */
|
|
30
|
+
upsertEntity: (typeId: EntityTypeId, incoming: object) => object;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* walker cache that tracks schema -> compiled walker mappings
|
|
35
|
+
* invalidated when entity definitions change
|
|
36
|
+
*/
|
|
37
|
+
export class WalkerCache {
|
|
38
|
+
#ctx: WalkerContext;
|
|
39
|
+
#cache = new WeakMap<BaseSchema, WalkFn>();
|
|
40
|
+
|
|
41
|
+
constructor(ctx: WalkerContext) {
|
|
42
|
+
this.#ctx = ctx;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** clear all cached walkers */
|
|
46
|
+
invalidate(): void {
|
|
47
|
+
this.#cache = new WeakMap<BaseSchema, WalkFn>();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* get or build a walker for a schema
|
|
52
|
+
* @param schema schema to get walker for
|
|
53
|
+
* @returns walk function
|
|
54
|
+
*/
|
|
55
|
+
getWalker(schema: BaseSchema): WalkFn {
|
|
56
|
+
return this.#build(schema);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#build(schema: BaseSchema): WalkFn {
|
|
60
|
+
const cached = this.#cache.get(schema);
|
|
61
|
+
if (cached !== undefined) {
|
|
62
|
+
return cached;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// set thunk for cycle detection - will be replaced with actual walker
|
|
66
|
+
this.#cache.set(schema, (data) => this.#cache.get(schema)!(data));
|
|
67
|
+
|
|
68
|
+
const walker = this.#buildForSchema(schema);
|
|
69
|
+
|
|
70
|
+
this.#cache.set(schema, walker);
|
|
71
|
+
|
|
72
|
+
return walker;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#buildForSchema(schema: BaseSchema): WalkFn {
|
|
76
|
+
if (isObjectSchema(schema)) {
|
|
77
|
+
return this.#buildObjectWalker(schema);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isArraySchema(schema)) {
|
|
81
|
+
return this.#buildArrayWalker(schema);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isVariantSchema(schema)) {
|
|
85
|
+
return this.#buildVariantWalker(schema);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isOptionalSchema(schema)) {
|
|
89
|
+
const innerWalker = this.#build(schema.wrapped);
|
|
90
|
+
if (innerWalker === identity) {
|
|
91
|
+
return identity;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return createOptionalWalker(innerWalker);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isNullableSchema(schema)) {
|
|
98
|
+
const innerWalker = this.#build(schema.wrapped);
|
|
99
|
+
if (innerWalker === identity) {
|
|
100
|
+
return identity;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return createNullableWalker(innerWalker);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// primitive types - no walking needed
|
|
107
|
+
return identity;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#buildObjectWalker(schema: ObjectSchema): WalkFn {
|
|
111
|
+
const ctx = this.#ctx;
|
|
112
|
+
|
|
113
|
+
// check if this is a registered entity type
|
|
114
|
+
const typeId = getTypeIdFromSchema(schema);
|
|
115
|
+
const entityTypeId = typeId !== undefined && ctx.isEntityType(typeId) ? typeId : undefined;
|
|
116
|
+
|
|
117
|
+
// build walkers for properties that need walking
|
|
118
|
+
const shape = schema.shape;
|
|
119
|
+
let propWalkers: [string, WalkFn][] | undefined = [];
|
|
120
|
+
|
|
121
|
+
for (const propName in shape) {
|
|
122
|
+
const propSchema = shape[propName];
|
|
123
|
+
const propWalker = this.#build(propSchema);
|
|
124
|
+
|
|
125
|
+
if (propWalker !== identity) {
|
|
126
|
+
propWalkers.push([propName, propWalker]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (propWalkers.length === 0) {
|
|
131
|
+
propWalkers = undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// nothing to do
|
|
135
|
+
if (entityTypeId === undefined && propWalkers === undefined) {
|
|
136
|
+
return identity;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return createObjectWalker(ctx, entityTypeId, propWalkers);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#buildArrayWalker(schema: { item: BaseSchema }): WalkFn {
|
|
143
|
+
const itemWalker = this.#build(schema.item);
|
|
144
|
+
|
|
145
|
+
if (itemWalker === identity) {
|
|
146
|
+
return identity;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return createArrayWalker(itemWalker);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#buildVariantWalker(schema: VariantSchema): WalkFn {
|
|
153
|
+
// build a map of type ID -> walker for members that need walking
|
|
154
|
+
const walkerMap: Record<string, WalkFn> = Object.create(null);
|
|
155
|
+
let hasWalkers = false;
|
|
156
|
+
|
|
157
|
+
for (const member of schema.members) {
|
|
158
|
+
const memberSchema = member as ObjectSchema;
|
|
159
|
+
const memberTypeId = getTypeIdFromSchema(memberSchema);
|
|
160
|
+
|
|
161
|
+
if (memberTypeId !== undefined) {
|
|
162
|
+
const memberWalker = this.#build(memberSchema);
|
|
163
|
+
|
|
164
|
+
if (memberWalker !== identity) {
|
|
165
|
+
walkerMap[memberTypeId] = memberWalker;
|
|
166
|
+
hasWalkers = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!hasWalkers) {
|
|
172
|
+
return identity;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return createVariantWalker(walkerMap);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** creates an object walker with minimal closure scope */
|
|
180
|
+
const createObjectWalker = (
|
|
181
|
+
ctx: WalkerContext,
|
|
182
|
+
entityTypeId: EntityTypeId | undefined,
|
|
183
|
+
propWalkers: [string, WalkFn][] | undefined,
|
|
184
|
+
): WalkFn => {
|
|
185
|
+
return (data) => {
|
|
186
|
+
let entity = data as Record<string, unknown>;
|
|
187
|
+
let cloned = entityTypeId !== undefined;
|
|
188
|
+
|
|
189
|
+
if (entityTypeId !== undefined) {
|
|
190
|
+
entity = ctx.upsertEntity(entityTypeId, entity) as Record<string, unknown>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (propWalkers !== undefined) {
|
|
194
|
+
for (const [name, walk] of propWalkers) {
|
|
195
|
+
const propValue = entity[name];
|
|
196
|
+
const walked = walk(propValue);
|
|
197
|
+
|
|
198
|
+
if (walked !== propValue) {
|
|
199
|
+
if (entityTypeId === undefined && !cloned) {
|
|
200
|
+
entity = { ...entity };
|
|
201
|
+
cloned = true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
entity[name] = walked;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return entity;
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** creates an array walker with minimal closure scope */
|
|
214
|
+
const createArrayWalker = (itemWalker: WalkFn): WalkFn => {
|
|
215
|
+
return (data) => {
|
|
216
|
+
const prev = data as unknown[];
|
|
217
|
+
let next: unknown[] | undefined;
|
|
218
|
+
|
|
219
|
+
for (let i = 0; i < prev.length; i++) {
|
|
220
|
+
const item = prev[i];
|
|
221
|
+
const walked = itemWalker(item);
|
|
222
|
+
|
|
223
|
+
if (walked !== item) {
|
|
224
|
+
if (next === undefined) {
|
|
225
|
+
next = prev.slice();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
next[i] = walked;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return next ?? prev;
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** creates a variant walker with minimal closure scope */
|
|
237
|
+
const createVariantWalker = (walkerMap: Record<string, WalkFn>): WalkFn => {
|
|
238
|
+
return (data) => {
|
|
239
|
+
const obj = data as Record<string, unknown>;
|
|
240
|
+
const type = obj.$type as string | undefined;
|
|
241
|
+
|
|
242
|
+
if (type === undefined) {
|
|
243
|
+
return data;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const memberWalker = walkerMap[type];
|
|
247
|
+
return memberWalker ? memberWalker(data) : data;
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/** creates an optional walker with minimal closure scope */
|
|
252
|
+
const createOptionalWalker = (innerWalker: WalkFn): WalkFn => {
|
|
253
|
+
return (data) => (data === undefined ? data : innerWalker(data));
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/** creates a nullable walker with minimal closure scope */
|
|
257
|
+
const createNullableWalker = (innerWalker: WalkFn): WalkFn => {
|
|
258
|
+
return (data) => (data === null ? data : innerWalker(data));
|
|
259
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@atcute/cache",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "normalized cache store for AT Protocol clients",
|
|
6
|
+
"license": "0BSD",
|
|
7
|
+
"repository": {
|
|
8
|
+
"url": "https://github.com/mary-ext/atcute",
|
|
9
|
+
"directory": "packages/clients/cache"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/",
|
|
13
|
+
"lib/",
|
|
14
|
+
"!lib/**/*.bench.ts",
|
|
15
|
+
"!lib/**/*.test.ts"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@atcute/lexicons": "^1.2.5"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"vitest": "^4.0.14",
|
|
25
|
+
"@atcute/bluesky": "^3.2.12"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsgo --project tsconfig.build.json",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"prepublish": "rm -rf dist; pnpm run build"
|
|
31
|
+
}
|
|
32
|
+
}
|