Created
November 1, 2023 22:38
-
-
Save ralphschuler/2d7efff77342de5d7432ccc03032f1b2 to your computer and use it in GitHub Desktop.
Random Typescript ObjectObserver
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Custom error classes | |
class ObjectAlreadyObservedError extends Error { | |
constructor(identifier: Identifier) { | |
super(`Object with identifier ${identifier} is already being observed.`); | |
Object.setPrototypeOf(this, ObjectAlreadyObservedError.prototype); | |
} | |
} | |
class ObjectNotObservedError extends Error { | |
constructor(identifier: Identifier) { | |
super(`No object observed with identifier: ${identifier}`); | |
Object.setPrototypeOf(this, ObjectNotObservedError.prototype); | |
} | |
} | |
type Identifier = string; | |
type PropertyList<T> = keyof T[]; | |
type ObjectChangedCallback<T> = (identifier: Identifier, updatedObject: T, eventType: EventType, propertyName?: keyof T) => void; | |
type ObjectWithCallbacks<T> = { proxy: T, original: T, callbacks?: ObjectChangedCallback<T>[], observedProperties?: PropertyList<T> }; | |
enum EventType { | |
PropertyChanged = 'propertyChanged', | |
BatchUpdateCompleted = 'batchUpdateCompleted' | |
} | |
class ObjectObserver extends Map<Identifier, ObjectWithCallbacks<unknown>> { | |
private readonly pendingBatchUpdates = new Set<Identifier>(); | |
private notifyObservers<T>(identifier: Identifier, updatedObject: T, eventType: EventType, propertyName?: keyof T): void { | |
if (this.pendingBatchUpdates.has(identifier)) return; | |
this.get(identifier)?.callbacks?.forEach(callback => callback(identifier, updatedObject, eventType, propertyName)); | |
} | |
private beginBatchUpdate(identifier: Identifier): void { | |
this.pendingBatchUpdates.add(identifier); | |
} | |
private endBatchUpdate(identifier: Identifier): void { | |
this.pendingBatchUpdates.delete(identifier); | |
if (!this.get(identifier)?.original) return; | |
this.notifyObservers(identifier, this.get(identifier)!.original, EventType.BatchUpdateCompleted); | |
} | |
private createSetterLogic<T>(obj: T, prop: keyof T, value: T[keyof T], identifier: Identifier, propertiesToWatch?: PropertyList<T>): boolean { | |
if (!propertiesToWatch?.includes(prop) && propertiesToWatch) return true; | |
Reflect.set(obj, prop, value); | |
this.notifyObservers(identifier, obj, EventType.PropertyChanged, prop); | |
if (typeof value !== 'object' || Array.isArray(value)) return true; | |
Reflect.set(obj, prop, this.createDeepProxy(value, identifier, propertiesToWatch)); | |
return true; | |
} | |
private createDeepProxy<T>( | |
target: T, | |
identifier: Identifier, | |
propertiesToWatch?: PropertyList<T> | |
): T { | |
return new Proxy(target, { | |
set: (obj, prop, value) => this.createSetterLogic(obj, prop, value, identifier, propertiesToWatch), | |
}); | |
} | |
observe<T>( | |
target: T, | |
identifier: Identifier, | |
triggerInitialCallback = false, | |
propertiesToWatch?: PropertyList<T> | |
): T { | |
if (this.has(identifier)) throw new ObjectAlreadyObservedError(identifier); | |
const proxy = this.createDeepProxy(target, identifier, propertiesToWatch); | |
this.set(identifier, { proxy, original: target }); | |
if (triggerInitialCallback) { | |
this.notifyObservers(identifier, target, EventType.PropertyChanged); | |
} | |
return proxy; | |
} | |
addCallback<T>(identifier: Identifier, callback: ObjectChangedCallback<T>): void { | |
const objectInfo = this.get(identifier); | |
if (!objectInfo) throw new ObjectNotObservedError(identifier); | |
objectInfo.callbacks = objectInfo.callbacks ?? []; | |
objectInfo.callbacks.push(callback); | |
} | |
removeObservation(identifier: Identifier, property?: keyof any): void { | |
const objectInfo = this.get(identifier); | |
if (!objectInfo) return; | |
objectInfo.observedProperties = objectInfo.observedProperties ?? []; | |
objectInfo.observedProperties = objectInfo.observedProperties.filter(prop => prop !== property); | |
if (!property) { | |
this.delete(identifier); | |
} | |
} | |
batchUpdate<T>(identifier: Identifier, updates: Partial<T>): void { | |
const objectInfo = this.get(identifier); | |
if (!objectInfo) throw new ObjectNotObservedError(identifier); | |
this.beginBatchUpdate(identifier); | |
objectInfo.original = { ...objectInfo.original, ...updates }; | |
this.endBatchUpdate(identifier); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment