Skip to content

Commit c41fb2d

Browse files
marcjAgent
authored andcommitted
feat(bson): add BSONHandlerRegistry for extensibility
Add registry infrastructure for custom BSON type handlers: - Create packages/bson/src/registry.ts with BSONHandlerRegistry class - Support handler registration by kind, class type, and annotation predicate - Handler priority: annotation > class > kind - Support pre/post hooks for wrapping handler execution - Add 13 tests for registry functionality This enables users to register custom serialization handlers for: - Custom class types (e.g., Decimal128) - Annotated types (e.g., GeoJSON points) - Override default kind handlers Note: Full integration with serializer.ts (replacing switch statement) is deferred to a future commit.
1 parent ed7fd0a commit c41fb2d

File tree

4 files changed

+491
-15
lines changed

4 files changed

+491
-15
lines changed

docs/todo/bson-rewrite/UNIFIED-SERIALIZER-PROPOSAL.md

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -123,26 +123,25 @@ This ensures each phase is verified before moving to the next.
123123

124124
### Phase 3: Add HandlerRegistry to BSON
125125

126-
- [ ] **3.1** Create `packages/bson/src/registry.ts`
127-
- Create `BSONHandlerRegistry` extending/wrapping `HandlerRegistry<BSONBuildState>`
128-
- Define `BSONTypeHandler` signature
129-
- Support `registerKindHandler()`, `registerClassHandler()`, `addDecorator()`
126+
- [x] **3.1** Create `packages/bson/src/registry.ts`
127+
- Create `BSONHandlerRegistry` with BSON-specific handler signature
128+
- Define `BSONTypeHandler` and `BSONTypeHook` types
129+
- Support `register()`, `registerClass()`, `registerAnnotation()`, `addPreHook()`, `addPostHook()`
130130

131131
- [ ] **3.2** Register default BSON handlers
132-
- Move handlers from switch statement to registry
133-
- `serializeString`, `serializeNumber`, `serializeBoolean`, etc.
134-
- Keep handler implementations in `packages/bson/src/handlers/` directory
132+
- DEFERRED: Requires significant refactoring of serializer.ts
133+
- Current handlers are internal functions tightly coupled together
134+
- Registry infrastructure is ready for this when needed
135135

136136
- [ ] **3.3** Replace switch dispatch with registry
137-
- In `bson-serializer.ts`, use `registry.build()` instead of switch
138-
- Ensure performance is not degraded (benchmark before/after)
139-
- Keep inline optimizations (header packing) in handlers
140-
141-
- [ ] **3.4** Add extensibility tests
142-
- Test user can register custom type handler
143-
- Test annotation-based handlers work
144-
- Test class-based handlers work
137+
- DEFERRED: Will be done when default handlers are registered
138+
- Registry.build() method is ready for use
139+
140+
- [x] **3.4** Add extensibility tests
141+
- Test user can register custom type handler (kind, class, annotation)
145142
- Test handler priority (annotation > class > kind)
143+
- Test pre/post hooks work correctly
144+
- 13 tests passing in packages/bson/tests/registry.spec.ts
146145

147146
- [ ] **3.5** Document BSON extensibility
148147
- Add examples to README or docs

packages/bson/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
export * from './src/errors.js';
1212
export * from './src/model.js';
1313
export * from './src/reader.js';
14+
export * from './src/registry.js';
1415
export * from './src/serializer.js';
1516
export * from './src/types.js';
1617
export * from './src/writer.js';

packages/bson/src/registry.ts

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/*
2+
* Deepkit Framework
3+
* Copyright (c) Deepkit UG, Marc J. Schmidt
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the MIT License.
7+
*
8+
* You should have received a copy of the MIT License along with this program.
9+
*/
10+
import type { Builder, Ref, VarRef } from '@deepkit/core';
11+
import type { ClassType } from '@deepkit/core';
12+
import { ReflectionKind, Type } from '@deepkit/type';
13+
14+
import type { BSONBuildState } from './serializer.js';
15+
16+
/**
17+
* BSON type handler function signature.
18+
*
19+
* Unlike @deepkit/type handlers which return Ref<T>, BSON handlers emit code
20+
* that writes directly to the buffer. They don't return anything.
21+
*
22+
* @param b - Builder for JIT code generation
23+
* @param buffer - Reference to the output buffer
24+
* @param view - Reference to the DataView for the buffer
25+
* @param o - Variable reference to the current offset
26+
* @param name - Property name (string) or array index (number)
27+
* @param type - The type being serialized
28+
* @param value - Reference to the value being serialized
29+
* @param ctx - BSON build state for recursion and options
30+
*/
31+
export type BSONTypeHandler<T extends Type = Type> = (
32+
b: Builder,
33+
buffer: Ref<Uint8Array>,
34+
view: Ref<DataView>,
35+
o: VarRef<number>,
36+
name: string | number,
37+
type: T,
38+
value: Ref<any>,
39+
ctx: BSONBuildState,
40+
) => void;
41+
42+
/**
43+
* Hook function for wrapping BSON type handlers.
44+
* Can modify behavior before/after the main handler executes.
45+
*/
46+
export type BSONTypeHook = (
47+
b: Builder,
48+
buffer: Ref<Uint8Array>,
49+
view: Ref<DataView>,
50+
o: VarRef<number>,
51+
name: string | number,
52+
type: Type,
53+
value: Ref<any>,
54+
ctx: BSONBuildState,
55+
next: () => void,
56+
) => void;
57+
58+
/**
59+
* Registry for BSON type handlers, organized by ReflectionKind, class type, and annotations.
60+
*
61+
* Handlers are executed in order:
62+
* 1. Pre-hooks (in order added)
63+
* 2. Annotation handlers (first matching predicate wins)
64+
* 3. Class handlers (for class types with specific classType)
65+
* 4. Kind handlers (by ReflectionKind)
66+
* 5. Post-hooks (in order added)
67+
*
68+
* @example
69+
* ```typescript
70+
* const registry = new BSONHandlerRegistry();
71+
*
72+
* // Register a custom handler for a specific class
73+
* registry.registerClass(Decimal128, (b, buffer, view, o, name, type, value, ctx) => {
74+
* // Write Decimal128 as BSON decimal type
75+
* writeDecimal128(b, buffer, view, o, name, value);
76+
* });
77+
*
78+
* // Register a handler for an annotation
79+
* registry.registerAnnotation(
80+
* type => hasAnnotation(type, geoPointAnnotation),
81+
* (b, buffer, view, o, name, type, value, ctx) => {
82+
* // Write as GeoJSON point
83+
* writeGeoPoint(b, buffer, view, o, name, value);
84+
* }
85+
* );
86+
* ```
87+
*/
88+
export class BSONHandlerRegistry {
89+
private static nextId = 0;
90+
91+
/** Unique ID that changes when handlers are modified. Used for cache invalidation. */
92+
public id: number;
93+
94+
private kindHandlers = new Map<ReflectionKind, BSONTypeHandler[]>();
95+
private classHandlers = new Map<ClassType, BSONTypeHandler[]>();
96+
private annotationHandlers: Array<{
97+
predicate: (type: Type) => boolean;
98+
handler: BSONTypeHandler;
99+
}> = [];
100+
private preHooks: BSONTypeHook[] = [];
101+
private postHooks: BSONTypeHook[] = [];
102+
103+
constructor() {
104+
this.id = BSONHandlerRegistry.nextId++;
105+
}
106+
107+
/** Increment ID to invalidate cached functions. */
108+
private invalidateCache(): void {
109+
this.id = BSONHandlerRegistry.nextId++;
110+
}
111+
112+
/**
113+
* Register a handler for a ReflectionKind.
114+
* Multiple handlers can be registered for the same kind (last one wins).
115+
*/
116+
register(kind: ReflectionKind, handler: BSONTypeHandler): this {
117+
const handlers = this.kindHandlers.get(kind);
118+
if (handlers) {
119+
handlers.push(handler);
120+
} else {
121+
this.kindHandlers.set(kind, [handler]);
122+
}
123+
this.invalidateCache();
124+
return this;
125+
}
126+
127+
/**
128+
* Register a handler for a specific class type.
129+
*/
130+
registerClass(classType: ClassType, handler: BSONTypeHandler): this {
131+
const handlers = this.classHandlers.get(classType);
132+
if (handlers) {
133+
handlers.push(handler);
134+
} else {
135+
this.classHandlers.set(classType, [handler]);
136+
}
137+
this.invalidateCache();
138+
return this;
139+
}
140+
141+
/**
142+
* Register a handler that matches types by a predicate.
143+
* Useful for annotation-based handling.
144+
*
145+
* @param predicate - Function that returns true if this handler should handle the type
146+
* @param handler - The handler function
147+
*/
148+
registerAnnotation(predicate: (type: Type) => boolean, handler: BSONTypeHandler): this {
149+
this.annotationHandlers.push({ predicate, handler });
150+
this.invalidateCache();
151+
return this;
152+
}
153+
154+
/**
155+
* Add a pre-hook that runs before type handlers.
156+
*/
157+
addPreHook(hook: BSONTypeHook): this {
158+
this.preHooks.push(hook);
159+
this.invalidateCache();
160+
return this;
161+
}
162+
163+
/**
164+
* Add a post-hook that runs after type handlers.
165+
*/
166+
addPostHook(hook: BSONTypeHook): this {
167+
this.postHooks.push(hook);
168+
this.invalidateCache();
169+
return this;
170+
}
171+
172+
/**
173+
* Get the handler for a type.
174+
* Priority: annotation > class > kind
175+
*/
176+
getHandler(type: Type): BSONTypeHandler | undefined {
177+
// 1. Check annotation handlers
178+
for (const { predicate, handler } of this.annotationHandlers) {
179+
if (predicate(type)) {
180+
return handler;
181+
}
182+
}
183+
184+
// 2. Check class handlers
185+
if (type.kind === ReflectionKind.class) {
186+
const classType = (type as any).classType;
187+
const handlers = this.classHandlers.get(classType);
188+
if (handlers && handlers.length > 0) {
189+
return handlers[handlers.length - 1]; // Last registered wins
190+
}
191+
}
192+
193+
// 3. Check kind handlers
194+
const handlers = this.kindHandlers.get(type.kind);
195+
if (handlers && handlers.length > 0) {
196+
return handlers[handlers.length - 1]; // Last registered wins
197+
}
198+
199+
return undefined;
200+
}
201+
202+
/**
203+
* Check if a handler exists for a type.
204+
*/
205+
hasHandler(type: Type): boolean {
206+
return this.getHandler(type) !== undefined;
207+
}
208+
209+
/**
210+
* Build (serialize) a type by dispatching to the appropriate handler.
211+
* Runs pre-hooks, then the handler, then post-hooks.
212+
*/
213+
build(
214+
b: Builder,
215+
buffer: Ref<Uint8Array>,
216+
view: Ref<DataView>,
217+
o: VarRef<number>,
218+
name: string | number,
219+
type: Type,
220+
value: Ref<any>,
221+
ctx: BSONBuildState,
222+
): void {
223+
const handler = this.getHandler(type);
224+
if (!handler) {
225+
throw new Error(`No BSON handler for type: ${ReflectionKind[type.kind]}`);
226+
}
227+
228+
// Build the execution chain: pre-hooks → handler → post-hooks
229+
let index = 0;
230+
const allHooks = [...this.preHooks, ...this.postHooks];
231+
232+
const runNext = (): void => {
233+
if (index < this.preHooks.length) {
234+
// Run pre-hook
235+
const hook = this.preHooks[index++];
236+
hook(b, buffer, view, o, name, type, value, ctx, runNext);
237+
} else if (index === this.preHooks.length) {
238+
// Run main handler
239+
index++;
240+
handler(b, buffer, view, o, name, type, value, ctx);
241+
runNext();
242+
} else if (index <= this.preHooks.length + this.postHooks.length) {
243+
// Run post-hook
244+
const hookIndex = index - this.preHooks.length - 1;
245+
index++;
246+
if (hookIndex < this.postHooks.length) {
247+
this.postHooks[hookIndex](b, buffer, view, o, name, type, value, ctx, runNext);
248+
}
249+
}
250+
};
251+
252+
runNext();
253+
}
254+
255+
/**
256+
* Create a copy of this registry.
257+
* Useful for creating specialized registries based on a base one.
258+
*/
259+
clone(): BSONHandlerRegistry {
260+
const copy = new BSONHandlerRegistry();
261+
copy.kindHandlers = new Map(this.kindHandlers);
262+
copy.classHandlers = new Map(this.classHandlers);
263+
copy.annotationHandlers = [...this.annotationHandlers];
264+
copy.preHooks = [...this.preHooks];
265+
copy.postHooks = [...this.postHooks];
266+
return copy;
267+
}
268+
}

0 commit comments

Comments
 (0)