···9090 expect(registry.isRegistered(symRegistered.id)).toBe(true);
9191 });
92929393+ it('indexes symbols and supports querying by meta', () => {
9494+ const registry = new SymbolRegistry();
9595+9696+ // register a couple of symbols with meta
9797+ const symA = registry.register({
9898+ meta: { bar: 'type', foo: { bar: true } },
9999+ name: 'A',
100100+ });
101101+ const symB = registry.register({
102102+ meta: { bar: 'value', foo: { bar: false } },
103103+ name: 'B',
104104+ });
105105+106106+ // query by top-level meta key
107107+ const types = registry.query({ bar: 'type' });
108108+ expect(types).toEqual([symA]);
109109+110110+ // query by nested meta key
111111+ const nestedTrue = registry.query({ foo: { bar: true } });
112112+ expect(nestedTrue).toEqual([symA]);
113113+114114+ const nestedFalse = registry.query({ foo: { bar: false } });
115115+ expect(nestedFalse).toEqual([symB]);
116116+ });
117117+118118+ it('replaces stubs after registering', () => {
119119+ const registry = new SymbolRegistry();
120120+121121+ const refA = registry.reference({ a: 0 });
122122+ const refAB = registry.reference({ a: 0, b: 0 });
123123+ const refB = registry.reference({ b: -1 });
124124+ const symC = registry.register({
125125+ meta: { a: 0, b: 0, c: 0 },
126126+ name: 'C',
127127+ });
128128+ const refAD = registry.reference({ a: 0, d: 0 });
129129+ const refAC = registry.reference({ a: 0, c: 0 });
130130+131131+ expect(symC).toEqual(refA);
132132+ expect(symC).toEqual(refAB);
133133+ expect(symC).toEqual(refAC);
134134+ expect(symC).not.toEqual(refAD);
135135+ expect(symC).not.toEqual(refB);
136136+ expect(symC.meta).toEqual({ a: 0, b: 0, c: 0 });
137137+ });
138138+93139 it('throws on invalid register or reference', () => {
94140 const registry = new SymbolRegistry();
95141 // Register with id that does not exist
···102148 expect(() => registry.register({ selector: ['missing'] })).toThrow(
103149 'Symbol with ID 42 not found. The selector ["missing"] matched an ID, but there was no result. This is likely an issue with the application logic.',
104150 );
151151+ });
152152+153153+ it('caches query results and invalidates on relevant updates', () => {
154154+ const registry = new SymbolRegistry();
155155+ const symA = registry.register({ meta: { foo: 'bar' }, name: 'A' });
156156+157157+ // first query populates cache
158158+ const result1 = registry.query({ foo: 'bar' });
159159+ expect(result1).toEqual([symA]);
160160+ expect(registry['queryCache'].size).toBe(1);
161161+162162+ // same query should hit cache, no change in cache size
163163+ const result2 = registry.query({ foo: 'bar' });
164164+ expect(result2).toEqual([symA]);
165165+ expect(registry['queryCache'].size).toBe(1);
166166+167167+ // register another symbol with matching key should invalidate cache
168168+ registry.register({ meta: { foo: 'bar' }, name: 'B' });
169169+ expect(registry['queryCache'].size).toBe(0);
170170+171171+ // new query repopulates cache
172172+ const result3 = registry.query({ foo: 'bar' });
173173+ expect(result3.map((r) => r.name).sort()).toEqual(['A', 'B']);
174174+ expect(registry['queryCache'].size).toBe(1);
175175+ });
176176+177177+ it('invalidates only affected cache entries', () => {
178178+ const registry = new SymbolRegistry();
179179+ const symA = registry.register({ meta: { foo: 'bar' }, name: 'A' });
180180+ const symX = registry.register({ meta: { x: 'y' }, name: 'X' });
181181+182182+ // Seed multiple cache entries
183183+ const resultFoo = registry.query({ foo: 'bar' });
184184+ const resultX = registry.query({ x: 'y' });
185185+ expect(resultFoo).toEqual([symA]);
186186+ expect(resultX).toEqual([symX]);
187187+ const initialCacheKeys = Array.from(registry['queryCache'].keys());
188188+ expect(initialCacheKeys.length).toBe(2);
189189+190190+ // Add new symbol that should only affect foo:bar queries
191191+ registry.register({ meta: { foo: 'bar' }, name: 'B' });
192192+193193+ // Cache entry for foo:bar should be invalidated, x:y should remain
194194+ const cacheKeysAfter = Array.from(registry['queryCache'].keys());
195195+ expect(cacheKeysAfter.length).toBe(1);
196196+ const remainingKey = cacheKeysAfter[0];
197197+ expect(remainingKey).toBe(
198198+ initialCacheKeys.find((k) => k.includes('x:"y"')),
199199+ );
200200+201201+ // Query foo:bar again to repopulate it
202202+ const resultFoo2 = registry.query({ foo: 'bar' });
203203+ expect(resultFoo2.map((r) => r.name).sort()).toEqual(['A', 'B']);
204204+ expect(registry['queryCache'].size).toBe(2);
105205 });
106206});
+7-7
packages/codegen-core/src/bindings/utils.ts
···1919 aliases: {},
2020 from: modulePath,
2121 };
2222- if (symbol.meta?.importKind) {
2323- if (symbol.meta.importKind === 'default') {
2222+ if (symbol.importKind) {
2323+ if (symbol.importKind === 'default') {
2424 binding.defaultBinding = symbol.placeholder;
2525- if (symbol.meta.kind === 'type') {
2525+ if (symbol.kind === 'type') {
2626 binding.typeDefaultBinding = true;
2727 }
2828- } else if (symbol.meta.importKind === 'namespace') {
2828+ } else if (symbol.importKind === 'namespace') {
2929 binding.namespaceBinding = symbol.placeholder;
3030- if (symbol.meta.kind === 'type') {
3030+ if (symbol.kind === 'type') {
3131 binding.typeNamespaceBinding = true;
3232 }
3333 }
3434 }
3535 // default to named binding
3636 if (
3737- symbol.meta?.importKind === 'named' ||
3737+ symbol.importKind === 'named' ||
3838 (!names.length && !binding.defaultBinding && !binding.namespaceBinding)
3939 ) {
4040 let name = symbol.placeholder;
···5252 }
5353 }
5454 names.push(name);
5555- if (symbol.meta?.kind === 'type') {
5555+ if (symbol.kind === 'type') {
5656 typeNames.push(name);
5757 }
5858 }
+1
packages/codegen-core/src/index.ts
···1414export type { ISelector as Selector } from './selectors/types';
1515export type {
1616 ISymbolOut as Symbol,
1717+ ISymbolIdentifier as SymbolIdentifier,
1718 ISymbolIn as SymbolIn,
1819} from './symbols/types';
+1
packages/codegen-core/src/selectors/types.d.ts
···22 * Selector array used to reference resources. We don't enforce
33 * uniqueness, but in practice it's desirable.
44 *
55+ * @deprecated
56 * @example ["zod", "#/components/schemas/Foo"]
67 */
78export type ISelector = ReadonlyArray<string>;
···11import type { ISymbolMeta } from '../extensions/types';
22import type { ISelector } from '../selectors/types';
3344+export type ISymbolIdentifier = number | ISymbolMeta | ISelector;
55+46export interface ISymbolIn {
57 /**
68 * Array of file names (without extensions) from which this symbol is re-exported.
···3234 */
3335 readonly id?: number;
3436 /**
3737+ * Kind of import if this symbol represents an import.
3838+ */
3939+ readonly importKind?: 'namespace' | 'default' | 'named';
4040+ /**
4141+ * Kind of symbol.
4242+ */
4343+ readonly kind?: 'type';
4444+ /**
3545 * Arbitrary metadata about the symbol.
3646 *
3747 * @default undefined
3848 */
3939- readonly meta?: ISymbolMeta & {
4040- /**
4141- * Kind of import if this symbol represents an import.
4242- */
4343- importKind?: 'namespace' | 'default' | 'named';
4444- /**
4545- * Kind of symbol.
4646- */
4747- kind?: 'type';
4848- };
4949+ readonly meta?: ISymbolMeta;
4950 /**
5051 * The desired name for the symbol within its file. If there are multiple symbols
5152 * with the same desired name, this might not end up being the actual name.
···6364 * Selector array used to select this symbol. It doesn't have to be
6465 * unique, but in practice it might be desirable.
6566 *
6767+ * @deprecated
6668 * @example ["zod", "#/components/schemas/Foo"]
6769 */
6870 readonly selector?: ISelector;
···87898890export interface ISymbolRegistry {
8991 /**
9090- * Get a symbol by its ID.
9292+ * Get a symbol.
9193 *
9292- * @param symbolIdOrSelector Symbol ID or selector to reference.
9494+ * @param identifier Symbol identifier to reference.
9395 * @returns The symbol, or undefined if not found.
9496 */
9595- get(symbolIdOrSelector: number | ISelector): ISymbolOut | undefined;
9797+ get(identifier: ISymbolIdentifier): ISymbolOut | undefined;
9698 /**
9799 * Returns the value associated with a symbol ID.
98100 *
···116118 /**
117119 * Returns whether a symbol is registered in the registry.
118120 *
119119- * @param symbolIdOrSelector Symbol ID or selector to check.
121121+ * @param identifier Symbol identifier to check.
120122 * @returns True if the symbol is registered, false otherwise.
121123 */
122122- isRegistered(symbolIdOrSelector: number | ISelector): boolean;
124124+ isRegistered(identifier: ISymbolIdentifier): boolean;
125125+ /**
126126+ * Queries symbols by metadata filter.
127127+ *
128128+ * @param filter Metadata filter to query symbols by.
129129+ * @returns Array of symbols matching the filter.
130130+ */
131131+ query(filter: ISymbolMeta): ReadonlyArray<ISymbolOut>;
123132 /**
124124- * Returns a symbol by ID or selector, registering it if it doesn't exist.
133133+ * Returns a symbol, registers it if it doesn't exist.
125134 *
126126- * @param symbolIdOrSelector Symbol ID or selector to reference.
135135+ * @param identifier Symbol identifier to reference.
127136 * @returns The referenced or newly registered symbol.
128137 */
129129- reference(symbolIdOrSelector: number | ISelector): ISymbolOut;
138138+ reference(identifier: ISymbolIdentifier): ISymbolOut;
130139 /**
131140 * Register a symbol globally.
132141 *