Skip to content

Commit

Permalink
feat(core): rework event source
Browse files Browse the repository at this point in the history
  • Loading branch information
Julien Capellari committed Jul 4, 2022
1 parent 1a32156 commit 6c63cd1
Show file tree
Hide file tree
Showing 11 changed files with 69 additions and 79 deletions.
6 changes: 0 additions & 6 deletions .idea/jsLinters/eslint.xml

This file was deleted.

10 changes: 5 additions & 5 deletions packages/core/src/entities/entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EventListener, EventListenerOptions, EventUnsubscribe } from '../events';
import { AegisQuery } from '../protocols';
import { AegisStore, StoreEventMap } from '../stores';
import { PartialKey, StringKey } from '../utils';
import { PartialKey } from '../utils';

import { AegisItem } from './item';
import { AegisList } from './list';
Expand All @@ -26,14 +26,14 @@ export class AegisEntity<D> {
// Methods
// - events
subscribe(
key: StringKey<PartialKey<['update', string]>>,
listener: EventListener<StoreEventMap<D>, 'update'>,
key: PartialKey<`update.${string}`>,
listener: EventListener<StoreEventMap<D>, `update.${string}.${string}`>,
opts?: EventListenerOptions
): EventUnsubscribe {
const [type, ...filters] = key.split('.');

return this.store.subscribe<'update'>(
[type, this.name, ...filters].join('.') as StringKey<PartialKey<['update', string, string]>>,
return this.store.subscribe(
[type, this.name, ...filters].join('.') as PartialKey<`update.${string}.${string}`>,
listener,
opts
);
Expand Down
20 changes: 10 additions & 10 deletions packages/core/src/entities/item.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EventKey, EventListener, EventListenerOptions, EventUnsubscribe } from '../events';
import { EventType, EventListener, EventListenerOptions, EventUnsubscribe } from '../events';
import { AegisQuery, QueryManager, QueryManagerEventMap, RefreshStrategy } from '../protocols';
import { StoreEventMap } from '../stores';
import { PartialKey, StringKey } from '../utils';
import { ExtractKey, PartialKey } from '../utils';

import { AegisEntity } from './entity';

Expand All @@ -26,24 +26,24 @@ export class AegisItem<D> {
// Methods
subscribe(
key: 'update',
listener: EventListener<StoreEventMap<D>, 'update'>,
listener: EventListener<StoreEventMap<D>, `update.${string}.${string}`>,
opts?: EventListenerOptions
): EventUnsubscribe;
subscribe(
key: StringKey<PartialKey<EventKey<QueryManagerEventMap<D>, 'query'>>>,
listener: EventListener<QueryManagerEventMap<D>, 'query'>,
subscribe<T extends PartialKey<EventType<QueryManagerEventMap<D>>>>(
type: T,
listener: EventListener<QueryManagerEventMap<D>, ExtractKey<EventType<QueryManagerEventMap<D>>, T>>,
opts?: EventListenerOptions
): EventUnsubscribe;
subscribe(
key: 'update' | StringKey<PartialKey<EventKey<QueryManagerEventMap<D>, 'query'>>>,
listener: EventListener<StoreEventMap<D>, 'update'> | EventListener<QueryManagerEventMap<D>, 'query'>,
key: 'update' | PartialKey<EventType<QueryManagerEventMap<D>>>,
listener: EventListener<StoreEventMap<D>, `update.${string}.${string}`> | EventListener<QueryManagerEventMap<D>, ExtractKey<EventType<QueryManagerEventMap<D>>, 'query'>>,
opts?: EventListenerOptions
): EventUnsubscribe {
if (key === 'update') {
return this.entity.subscribe(`update.${this.id}`, listener as EventListener<StoreEventMap<D>, 'update'>, opts);
return this.entity.subscribe(`update.${this.id}`, listener as EventListener<StoreEventMap<D>, `update.${string}.${string}`>, opts);
}

return this._manager.subscribe(key, listener as EventListener<QueryManagerEventMap<D>, 'query'>, opts);
return this._manager.subscribe(key, listener as EventListener<QueryManagerEventMap<D>, ExtractKey<EventType<QueryManagerEventMap<D>>, 'query'>>, opts);
}

refresh(fetcher: () => AegisQuery<D>, strategy: RefreshStrategy): AegisQuery<D> {
Expand Down
18 changes: 9 additions & 9 deletions packages/core/src/entities/list.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { EventKey, EventListener, EventListenerOptions, EventSource, EventUnsubscribe } from '../events';
import { EventType, EventListener, EventListenerOptions, EventSource, EventUnsubscribe } from '../events';
import { AegisQuery, QueryManager, QueryManagerEventMap, RefreshStrategy } from '../protocols';
import { PartialKey, StringKey } from '../utils';
import { ExtractKey, PartialKey } from '../utils';

import { AegisEntity } from './entity';

// Types
export type ListEventMap<D> = {
update: { data: D[], filters: [] },
update: D[],
}

// Class
Expand Down Expand Up @@ -63,21 +63,21 @@ export class AegisList<D> extends EventSource<ListEventMap<D>> {
listener: EventListener<ListEventMap<D>, 'update'>,
opts?: EventListenerOptions
): EventUnsubscribe;
subscribe(
key: StringKey<PartialKey<EventKey<QueryManagerEventMap<D>, 'query'>>>,
listener: EventListener<QueryManagerEventMap<D[]>, 'query'>,
subscribe<T extends PartialKey<EventType<QueryManagerEventMap<D>>>>(
type: T,
listener: EventListener<QueryManagerEventMap<D[]>, ExtractKey<EventType<QueryManagerEventMap<D[]>>, T>>,
opts?: EventListenerOptions
): EventUnsubscribe;
subscribe(
key: 'update' | StringKey<PartialKey<EventKey<QueryManagerEventMap<D>, 'query'>>>,
listener: EventListener<ListEventMap<D>, 'update'> | EventListener<QueryManagerEventMap<D[]>, 'query'>,
key: 'update' | PartialKey<EventType<QueryManagerEventMap<D[]>>>,
listener: EventListener<ListEventMap<D>, 'update'> | EventListener<QueryManagerEventMap<D[]>, ExtractKey<EventType<QueryManagerEventMap<D>>, 'query'>>,
opts?: EventListenerOptions
): EventUnsubscribe {
if (key === 'update') {
return super.subscribe('update', listener as EventListener<ListEventMap<D>, 'update'>, opts);
}

return this._manager.subscribe(key, listener as EventListener<QueryManagerEventMap<D[]>, 'query'>, opts);
return this._manager.subscribe(key, listener as EventListener<QueryManagerEventMap<D[]>, ExtractKey<EventType<QueryManagerEventMap<D[]>>, 'query'>>, opts);
}


Expand Down
21 changes: 9 additions & 12 deletions packages/core/src/events/event-source.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { KeyTree, PartialKey, StringKey } from '../utils';
import { ExtractKey, KeyTree, PartialKey } from '../utils';

import { EventData, EventEmitter, EventKey, EventListener, EventListenerOptions, EventMap, EventOptions, EventUnsubscribe } from './types';
import { EventData, EventEmitter, EventType, EventListener, EventListenerOptions, EventMap, EventOptions, EventUnsubscribe } from './types';

// Class
export class EventSource<M extends EventMap> implements EventEmitter<M> {
Expand All @@ -9,23 +9,20 @@ export class EventSource<M extends EventMap> implements EventEmitter<M> {
private readonly _listeners = new KeyTree();

// Emit
emit<T extends keyof M & string>(key: StringKey<EventKey<M, T>>, data: EventData<M, T>, opts?: EventOptions): void {
const _key = key.split('.') as EventKey<M, T>;
const [type, ...filters] = _key;

for (const listener of this._listeners.searchWithParent(_key)) {
(listener as EventListener<M, T>)(data, { type, filters, source: opts?.source ?? this as EventEmitter });
emit<T extends EventType<M>>(type: T, data: EventData<M, T>, opts?: EventOptions): void {
for (const listener of this._listeners.searchWithParent(type)) {
(listener as EventListener<M, T>)(data, { type, source: opts?.source ?? this });
}
}

subscribe<T extends keyof M & string>(
key: StringKey<PartialKey<EventKey<M, T>>>,
listener: EventListener<M, T>,
subscribe<T extends PartialKey<EventType<M>>>(
type: T,
listener: EventListener<M, ExtractKey<EventType<M>, T>>,
opts: EventListenerOptions = {},
): EventUnsubscribe {
opts.signal ??= this.controller?.signal;

this._listeners.insert(key.split('.'), listener);
this._listeners.insert(type, listener);

if (opts.signal) {
opts.signal.addEventListener('abort', () => this._listeners.remove(listener), { once: true });
Expand Down
36 changes: 14 additions & 22 deletions packages/core/src/events/types.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
import { PartialKey, StringKey } from '../utils';
import { ExtractKey, PartialKey } from '../utils';

// Types
export type EventMap = Record<string, { data: unknown, filters: (string | number)[] }>;
export type EventMap = Record<string, unknown>;

/**
* Extract data for given event type
*/
export type EventData<M extends EventMap, T extends keyof M & string> = M[T]['data'];

/**
* Extract filters for given event type
* Build key type for given event type
*/
export type EventFilters<M extends EventMap, T extends keyof M & string> = M[T]['filters'];
export type EventType<M extends EventMap> = keyof M & string;

/**
* Build key type for given event type
* Extract data for given event type
*/
export type EventKey<M extends EventMap, T extends keyof M & string> = {
[K in keyof M & string]: [K, ...EventFilters<M, K>]
}[T];
export type EventData<M extends EventMap, T extends EventType<M> = EventType<M>> = M[T];

/**
* Event emit options
Expand All @@ -37,31 +30,30 @@ export interface EventListenerOptions {
/**
* Event metadata
*/
export interface EventMetadata<M extends EventMap, T extends keyof M & string> {
export interface EventMetadata<M extends EventMap, T extends EventType<M> = EventType<M>> {
type: T;
filters: EventFilters<M, T>;
source: unknown;
}

/**
* Event listener
*/
export type EventListener<M extends EventMap, T extends keyof M & string> =
export type EventListener<M extends EventMap, T extends EventType<M> = EventType<M>> =
(data: EventData<M, T>, metadata: EventMetadata<M, T>) => void;

export type EventUnsubscribe = () => void;

export interface EventEmitter<M extends EventMap = EventMap> {
// Emit
emit<T extends keyof M & string>(
key: StringKey<EventKey<M, T>>,
emit<T extends EventType<M>>(
type: T,
data: EventData<M, T>,
opts?: EventOptions
): void;

subscribe<T extends keyof M & string>(
key: StringKey<PartialKey<EventKey<M, T>>>,
listener: EventListener<M, T>,
opts?: EventListenerOptions
subscribe<T extends PartialKey<EventType<M>>>(
type: T,
listener: EventListener<M, ExtractKey<EventType<M>, T>>,
opts?: EventListenerOptions,
): EventUnsubscribe;
}
8 changes: 5 additions & 3 deletions packages/core/src/protocols/query-manager.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { EventSource } from '../events';

import { AegisQuery, QueryState, QueryStatus } from './query';
import { AegisQuery, QueryStateCompleted, QueryStateFailed, QueryStatePending } from './query';

// Types
export type RefreshStrategy = 'keep' | 'replace';

export type QueryManagerEventMap<D> = {
query: { data: Readonly<QueryState<D>>, filters: [QueryStatus] },
'query.pending': QueryStatePending,
'query.completed': QueryStateCompleted<D>,
'query.failed': QueryStateFailed,
}

// Class
Expand Down Expand Up @@ -40,7 +42,7 @@ export class QueryManager<D> extends EventSource<QueryManagerEventMap<D>> {
this.emit(`query.${state.status}`, state, { source: metadata.source });
});

this.emit('query.pending', this._query.state);
this.emit(`query.${this._query.state.status}`, this._query.state);

return this._query;
}
Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/protocols/query.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { EventSource } from '../events';

// Types
interface QueryStatePending {
export interface QueryStatePending {
readonly status: 'pending';
}

interface QueryStateCompleted<D> {
export interface QueryStateCompleted<D> {
readonly status: 'completed';
readonly result: D;
}

interface QueryStateFailed {
export interface QueryStateFailed {
readonly status: 'failed';
readonly error: Error;
}
Expand All @@ -19,7 +19,8 @@ export type QueryState<D> = QueryStatePending | QueryStateCompleted<D> | QuerySt
export type QueryStatus = QueryState<unknown>['status'];

export type QueryEventMap<D> = {
update: { data: Readonly<QueryState<D>>, filters: [Exclude<QueryStatus, 'pending'>] },
'update.completed': QueryStateCompleted<D>,
'update.failed': QueryStateFailed,
}

// Query
Expand Down
4 changes: 1 addition & 3 deletions packages/core/src/stores/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ export interface StoreUpdateEvent<D = unknown> {
new: Readonly<D>;
}

export type StoreEventMap<D = unknown> = {
update: { data: StoreUpdateEvent<D>, filters: [string, string] }
}
export type StoreEventMap<D = unknown> = Record<`update.${string}.${string}`, StoreUpdateEvent<D>>;

// Store
export abstract class AegisStore extends EventSource<StoreEventMap> {
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export type RestOfKey<K extends Key> =
? R
: '';

/**
* Extract keys by beginning
*/
export type ExtractKey<K extends Key, S extends Key> =
K extends `${S}.${string}`
? K
: Extract<K, S>;

/**
* Partial key
*/
Expand Down
8 changes: 3 additions & 5 deletions packages/core/tests/events/event-source.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { EventSource } from '../../src';

// Types
type TestEventMap = {
test: { data: null, filters: [number] },
};
type TestEventMap = Record<`test.${number}`, null>;

// Tests
describe('EventSource', () => {
Expand All @@ -18,8 +16,8 @@ describe('EventSource', () => {

src.emit('test.1', null);

expect(typeListener).toHaveBeenCalledWith(null, { type: 'test', filters: ['1'], source: src });
expect(targetListener).toHaveBeenCalledWith(null, { type: 'test', filters: ['1'], source: src });
expect(typeListener).toHaveBeenCalledWith(null, { type: 'test.1', source: src });
expect(targetListener).toHaveBeenCalledWith(null, { type: 'test.1', source: src });
});

it('should not call unsubscribed listeners', () => {
Expand Down

0 comments on commit 6c63cd1

Please sign in to comment.