git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/gof-observer-pattern" ~/.claude/skills/intense-visions-harness-engineering-gof-observer-pattern-7013ea && rm -rf "$T"
agents/skills/codex/gof-observer-pattern/SKILL.mdGOF Observer Pattern
Implement push-based notification between Subject and Observer with typed subscriptions.
When to Use
- An object (Subject) needs to notify multiple dependents when its state changes
- You want loose coupling between the event source and its handlers
- You're implementing event systems, reactive data bindings, or pub/sub within a process
- You need typed, named events with specific payloads
Instructions
Typed Subject with generic observer interface:
type Observer<T> = (event: T) => void | Promise<void>; class Subject<T> { private observers = new Set<Observer<T>>(); subscribe(observer: Observer<T>): () => void { this.observers.add(observer); return () => this.observers.delete(observer); // unsubscribe function } async notify(event: T): Promise<void> { await Promise.all([...this.observers].map((obs) => obs(event))); } } // Typed usage interface PriceChanged { ticker: string; oldPrice: number; newPrice: number; timestamp: Date; } class StockTicker { private priceSubject = new Subject<PriceChanged>(); private prices = new Map<string, number>(); onPriceChange(observer: Observer<PriceChanged>): () => void { return this.priceSubject.subscribe(observer); } async updatePrice(ticker: string, newPrice: number): Promise<void> { const oldPrice = this.prices.get(ticker) ?? newPrice; this.prices.set(ticker, newPrice); if (oldPrice !== newPrice) { await this.priceSubject.notify({ ticker, oldPrice, newPrice, timestamp: new Date() }); } } } // Usage const ticker = new StockTicker(); const unsubDashboard = ticker.onPriceChange(async ({ ticker: t, newPrice }) => { console.log(`Dashboard: ${t} is now $${newPrice}`); }); ticker.onPriceChange(async ({ ticker: t, oldPrice, newPrice }) => { if (newPrice < oldPrice * 0.9) { console.log(`Alert: ${t} dropped more than 10%!`); } }); await ticker.updatePrice('AAPL', 185.5); await ticker.updatePrice('AAPL', 165.0); // triggers alert unsubDashboard(); // unsubscribe when done
Multi-event typed observer (event emitter pattern):
type EventMap = { [K: string]: unknown }; class TypedEventEmitter<Events extends EventMap> { private handlers = new Map<string, Set<(payload: unknown) => void>>(); on<K extends keyof Events & string>(event: K, handler: (payload: Events[K]) => void): () => void { if (!this.handlers.has(event)) this.handlers.set(event, new Set()); this.handlers.get(event)!.add(handler as (p: unknown) => void); return () => this.handlers.get(event)?.delete(handler as (p: unknown) => void); } emit<K extends keyof Events & string>(event: K, payload: Events[K]): void { this.handlers.get(event)?.forEach((h) => h(payload)); } } interface UserEvents { created: { id: string; email: string }; deleted: { id: string }; emailChanged: { id: string; oldEmail: string; newEmail: string }; } class UserService extends TypedEventEmitter<UserEvents> { async createUser(email: string): Promise<string> { const id = crypto.randomUUID(); // ... persist to db this.emit('created', { id, email }); return id; } }
WeakRef for automatic cleanup (memory-safe subscriptions):
class WeakSubject<T> { private observers = new Set<WeakRef<Observer<T>>>(); subscribe(observer: Observer<T>): void { this.observers.add(new WeakRef(observer)); } notify(event: T): void { for (const ref of this.observers) { const observer = ref.deref(); if (observer) { observer(event); } else { this.observers.delete(ref); // clean up dead references } } } }
Details
Memory leaks from forgotten subscriptions: The most common Observer bug in Node.js. Always:
- Return an unsubscribe function from
subscribe() - Call it in cleanup/teardown
- Or use WeakRef so GC handles it
Observer vs. Mediator: Observer: subject notifies observers directly; subject knows it has observers. Mediator: components notify the mediator which routes to others; components don't know who else is listening. Use Observer for 1:N relationships. Use Mediator for N:M relationships.
Synchronous vs. asynchronous notification:
- Synchronous: simpler, predictable, but blocks until all observers complete
- Async: use
for parallel execution, orPromise.all()
for sequentialfor...of await
Anti-patterns:
- Observers that modify the subject during notification — can cause infinite loops or inconsistent state
- Forgetting to unsubscribe on component unmount/service teardown
- Subject notifying during construction — no observers have subscribed yet
RxJS alternative: For complex reactive chains (debounce, filter, combine), RxJS Observables are a more powerful implementation of the Observer pattern. Use plain Observer for simple cases; RxJS for complex composition.
Source
refactoring.guru/design-patterns/observer
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.