Skip to content

Commit

Permalink
Refactor model events
Browse files Browse the repository at this point in the history
  • Loading branch information
NoelDeMartin committed Sep 12, 2024
1 parent 376c89f commit ba95358
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 41 deletions.
45 changes: 4 additions & 41 deletions src/models/Model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
arrayRemove,
arrayUnique,
deepEquals,
fail,
Expand Down Expand Up @@ -28,19 +27,17 @@ import { FieldType, expandFieldDefinition } from './fields';
import { removeUndefinedAttributes, validateAttributes, validateRequiredAttributes } from './attributes';
import { TIMESTAMP_FIELDS, TimestampField } from './timestamps';
import { withEngineImpl } from './utils';
import { emitModelEvent, registerModelListener } from './listeners';
import type { Attributes } from './attributes';
import type { BootedFieldDefinition, BootedFieldsDefinition, FieldsDefinition } from './fields';
import type { ModelConstructor } from './inference';
import type { Relation } from './relations/Relation';
import type { TimestampFieldValue, TimestampsDefinition } from './timestamps';
import type { ModelEmitArgs, ModelEvents, ModelListener } from './listeners';

const modelsWithMintedCollections = new WeakSet();

export type Key = string | number | Record<string, string | number>;
export type ModelListener<
TModel extends Model = Model,
TEvent extends keyof ModelEvents = keyof ModelEvents
> = (...args: ModelListenerArgs<TModel, TEvent>) => unknown;

export type ModelCloneOptions = Partial<{
clean: boolean;
Expand All @@ -54,24 +51,6 @@ export type ModelCastAttributeOptions = {
malformedAttributes?: Record<string, string[]>;
};

export interface ModelEvents {
created: void;
deleted: void;
updated: void;
modified: string;
'relation-loaded': Relation;
}

export type ModelEmitArgs<T extends keyof ModelEvents> =
ModelEvents[T] extends void
? [event: T]
: [event: T, payload: ModelEvents[T]];

export type ModelListenerArgs<TModel extends Model, TEvent extends keyof ModelEvents> =
ModelEvents[TEvent] extends void
? [model: TModel]
: [model: TModel, payload: ModelEvents[TEvent]];

export class Model {

public static collection: string;
Expand All @@ -88,7 +67,6 @@ export class Model {
private static pureInstances: WeakMap<typeof Model, Model> = new WeakMap;
private static bootedModels: WeakMap<typeof Model, true> = new WeakMap;
private static engines: WeakMap<typeof Model, Engine> = new WeakMap;
private static listeners: WeakMap<typeof Model, Record<string, ModelListener[]>> = new WeakMap;
private static ignoringTimestamps: WeakMap<Model | typeof Model, true> = new WeakMap;

public static boot<T extends Model>(this: ModelConstructor<T>, name?: string): void {
Expand Down Expand Up @@ -273,15 +251,7 @@ export class Model {
event: TEvent,
listener: ModelListener<TModel, TEvent>,
): () => void {
if (!this.listeners.has(this))
this.listeners.set(this, {});

const modelListeners = this.listeners.get(this) as Record<string, ModelListener<TModel, TEvent>[]>;
const listeners = modelListeners[event] ??= [];

listeners.push(listener);

return () => arrayRemove(listeners, listener);
return registerModelListener(this, event, listener);
}

public static instance<T extends Model>(this: ModelConstructor<T>): T {
Expand Down Expand Up @@ -1045,14 +1015,7 @@ export class Model {
}

protected async emit<T extends keyof ModelEvents>(...args: ModelEmitArgs<T>): Promise<void> {
const event = args[0];
const payload = args[1];
const listeners = this.static().listeners.get(this.static())?.[event];

if (!listeners)
return;

await Promise.all(listeners.map(listener => listener(this, payload)));
return emitModelEvent<T>(this, ...args);
}

protected async syncDirty(): Promise<string> {
Expand Down
1 change: 1 addition & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './deprecated';
export * from './inference';
export * from './Model';
export * from './relations/index';
export * from './listeners';
export * from './schema';

export { TimestampField } from './timestamps';
Expand Down
66 changes: 66 additions & 0 deletions src/models/listeners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { arrayRemove } from '@noeldemartin/utils';

import type { Model } from './Model';
import type { ModelConstructor } from './inference';
import type { Relation } from './relations/Relation';

let listeners: WeakMap<typeof Model, Record<string, ModelListener[]>> = new WeakMap;

export type ModelListener<
TModel extends Model = Model,
TEvent extends keyof ModelEvents = keyof ModelEvents
> = (...args: ModelListenerArgs<TModel, TEvent>) => unknown;

export interface ModelEvents {
created: void;
deleted: void;
updated: void;
modified: string;
'relation-loaded': Relation;
}

export type ModelEmitArgs<T extends keyof ModelEvents> =
ModelEvents[T] extends void
? [event: T]
: [event: T, payload: ModelEvents[T]];

export type ModelListenerArgs<TModel extends Model, TEvent extends keyof ModelEvents> =
ModelEvents[TEvent] extends void
? [model: TModel]
: [model: TModel, payload: ModelEvents[TEvent]];

export function resetModelListeners(): void {
listeners = new WeakMap();
}

export function registerModelListener<TModel extends Model, TEvent extends keyof ModelEvents>(
modelClass: ModelConstructor<TModel>,
event: TEvent,
listener: ModelListener<TModel, TEvent>,
): () => void {
if (!listeners.has(modelClass)) {
listeners.set(modelClass, {});
}

const modelListeners = listeners.get(modelClass) as Record<string, ModelListener<TModel, TEvent>[]>;
const eventListeners = modelListeners[event] ??= [];

eventListeners.push(listener);

return () => arrayRemove(eventListeners, listener);
}

export async function emitModelEvent<T extends keyof ModelEvents>(
model: Model,
...args: ModelEmitArgs<T>
): Promise<void> {
const event = args[0];
const payload = args[1];
const modelListeners = listeners.get(model.static())?.[event];

if (!modelListeners) {
return;
}

await Promise.all(modelListeners.map(listener => listener(model, payload)));
}

0 comments on commit ba95358

Please sign in to comment.