@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/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
+ }