Skip to content

Commit

Permalink
Merge pull request #59 from Jujulego/feat/event-types
Browse files Browse the repository at this point in the history
Rework event types
  • Loading branch information
Jujulego authored Jun 27, 2022
2 parents 2cf4c9f + 3970a7e commit b3f2792
Show file tree
Hide file tree
Showing 38 changed files with 505 additions and 481 deletions.
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jujulego/aegis-core",
"version": "1.0.0-alpha.19",
"version": "1.0.0-alpha.20",
"license": "MIT",
"author": "Julien Capellari <julien.capellari@google.com>",
"repository": {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/entities/entity.builder.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { AegisItem } from '../items';
import { AegisList } from '../lists';
import { AegisQuery } from '../protocols';
import { AegisStore } from '../stores';

import { AegisItem } from './item';
import { AegisList } from './list';

import { AegisEntity, EntityIdExtractor, EntityMerge } from './entity';

// Type
Expand Down
63 changes: 35 additions & 28 deletions packages/core/src/entities/entity.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,50 @@
import { EventEmitter, EventListener, EventListenerOptions, EventUnsubscribe } from '../events';
import { AegisItem } from '../items';
import { AegisList } from '../lists';
import { AegisStore, StoreUpdateEvent } from '../stores';
import { EventListener, EventListenerOptions, EventUnsubscribe } from '../events';
import { AegisQuery } from '../protocols';
import { AegisStore, StoreEventMap } from '../stores';
import { PartialKey, StringKey } from '../utils';

import { AegisItem } from './item';
import { AegisList } from './list';

// Types
export type EntityIdExtractor<T> = (entity: T) => string;
export type EntityMerge<T, R> = (stored: T, result: R) => T;
export type EntityIdExtractor<D> = (entity: D) => string;
export type EntityMerge<D, R> = (stored: D, result: R) => D;

// Class
export class AegisEntity<T> implements EventEmitter {
export class AegisEntity<D> {
// Attributes
private readonly _items = new Map<string, WeakRef<AegisItem<T>>>();
private readonly _lists = new Map<string, WeakRef<AegisList<T>>>();
private readonly _items = new Map<string, WeakRef<AegisItem<D>>>();
private readonly _lists = new Map<string, WeakRef<AegisList<D>>>();

// Constructor
constructor(
readonly name: string,
readonly store: AegisStore,
readonly extractor: EntityIdExtractor<T>
readonly extractor: EntityIdExtractor<D>
) {}

// Methods
// - events
subscribe(
type: 'update',
listener: EventListener<StoreUpdateEvent<T>>,
opts: Omit<EventListenerOptions<StoreUpdateEvent<T>>, 'key'> & { key?: [string] } = {}
key: StringKey<PartialKey<['update', string]>>,
listener: EventListener<StoreEventMap<D>, 'update'>,
opts?: EventListenerOptions
): EventUnsubscribe {
const { key = [] } = opts;
return this.store.subscribe('update', listener, { ...opts, key: [this.name, ...key] });
const [type, ...filters] = key.split('.');

return this.store.subscribe<'update'>(
[type, this.name, ...filters].join('.') as StringKey<PartialKey<['update', string, string]>>,
listener,
opts
);
}

// - query managers
/**
* Get item manager by id
* @param id
*/
item(id: string): AegisItem<T> {
item(id: string): AegisItem<D> {
let item = this._items.get(id)?.deref();

if (!item) {
Expand All @@ -52,7 +59,7 @@ export class AegisEntity<T> implements EventEmitter {
* Get list manager by key
* @param key
*/
list(key: string): AegisList<T> {
list(key: string): AegisList<D> {
let list = this._lists.get(key)?.deref();

if (!list) {
Expand All @@ -68,7 +75,7 @@ export class AegisEntity<T> implements EventEmitter {
* Will resolves to item manager for returned item
* @param query
*/
query(query: AegisQuery<T>): AegisQuery<AegisItem<T>> {
query(query: AegisQuery<D>): AegisQuery<AegisItem<D>> {
return query.then((item) => this.item(this.storeItem(item)));
}

Expand All @@ -77,7 +84,7 @@ export class AegisEntity<T> implements EventEmitter {
* @param id
* @param query
*/
mutation(id: string, query: AegisQuery<T>): AegisQuery<T>;
mutation(id: string, query: AegisQuery<D>): AegisQuery<D>;

/**
* Register a mutation. Query result will be merged with stored item.
Expand All @@ -86,9 +93,9 @@ export class AegisEntity<T> implements EventEmitter {
* @param query
* @param merge
*/
mutation<R>(id: string, query: AegisQuery<R>, merge: EntityMerge<T, R>): AegisQuery<T>;
mutation<R>(id: string, query: AegisQuery<R>, merge: EntityMerge<D, R>): AegisQuery<D>;

mutation(id: string, query: AegisQuery<unknown>, merge?: EntityMerge<T, unknown>): AegisQuery<T> {
mutation(id: string, query: AegisQuery<unknown>, merge?: EntityMerge<D, unknown>): AegisQuery<D> {
return query.then((result) => {
if (merge) {
const item = this.getItem(id);
Expand All @@ -102,9 +109,9 @@ export class AegisEntity<T> implements EventEmitter {

return updated;
} else {
this.setItem(id, result as T);
this.setItem(id, result as D);

return result as T;
return result as D;
}
});
}
Expand All @@ -115,7 +122,7 @@ export class AegisEntity<T> implements EventEmitter {
* @param id
* @param query
*/
deletion(id: string, query: AegisQuery<unknown>): AegisQuery<T | undefined> {
deletion(id: string, query: AegisQuery<unknown>): AegisQuery<D | undefined> {
return query.then(() => this.deleteItem(id));
}

Expand All @@ -124,7 +131,7 @@ export class AegisEntity<T> implements EventEmitter {
* Direct access to stored item, by id.
* @param id
*/
getItem(id: string): T | undefined {
getItem(id: string): D | undefined {
return this.store.get(this.name, id);
}

Expand All @@ -133,15 +140,15 @@ export class AegisEntity<T> implements EventEmitter {
* @param id
* @param value
*/
setItem(id: string, value: T): void {
setItem(id: string, value: D): void {
this.store.set(this.name, id, value);
}

/**
* Delete local item, by id.
* @param id
*/
deleteItem(id: string): T | undefined {
deleteItem(id: string): D | undefined {
return this.store.delete(this.name, id);
}

Expand All @@ -151,7 +158,7 @@ export class AegisEntity<T> implements EventEmitter {
*
* @param item
*/
storeItem(item: T): string {
storeItem(item: D): string {
const id = this.extractor(item);
this.setItem(id, item);

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './entity';
export * from './entity.builder';
export * from './item';
export * from './list';
90 changes: 90 additions & 0 deletions packages/core/src/entities/item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { EventKey, EventListener, EventListenerOptions, EventSource, EventUnsubscribe } from '../events';
import { AegisQuery, QueryStatus, QueryUpdateEvent } from '../protocols';
import { StoreEventMap } from '../stores';
import { PartialKey, StringKey } from '../utils';

import { AegisEntity } from './entity';

// Types
export type ItemEventMap<D> = {
query: { data: QueryUpdateEvent<D>, filters: ['pending' | 'completed' | 'error'] },
};

// Class
export class AegisItem<D> extends EventSource<ItemEventMap<D>> {
// Attributes
private _query?: AegisQuery<D>;

// Constructor
constructor(
readonly entity: AegisEntity<D>,
readonly id: string,
) {
super();
}

// Methods
subscribe(
key: 'update',
listener: EventListener<StoreEventMap<D>, 'update'>,
opts?: EventListenerOptions
): EventUnsubscribe;
subscribe<T extends keyof ItemEventMap<D>>(
key: StringKey<PartialKey<EventKey<ItemEventMap<D>, T>>>,
listener: EventListener<ItemEventMap<D>, T>,
opts?: EventListenerOptions
): EventUnsubscribe;
subscribe(
key: 'update' | StringKey<PartialKey<EventKey<ItemEventMap<D>, keyof ItemEventMap<D>>>>,
listener: EventListener<StoreEventMap<D>, 'update'> | EventListener<ItemEventMap<D>, keyof ItemEventMap<D>>,
opts?: EventListenerOptions
): EventUnsubscribe {
if (key === 'update') {
return this.entity.subscribe(`update.${this.id}`, listener as EventListener<StoreEventMap<D>, 'update'>, opts);
}

return super.subscribe(key, listener as EventListener<ItemEventMap<D>, keyof ItemEventMap<D>>, opts);
}

refresh(fetcher: () => AegisQuery<D>): AegisQuery<D> {
if (this._query?.status !== 'pending') {
// Register query
this._query = fetcher();

this._query.subscribe('update', (data, mtd) => {
if (this._query !== mtd.source) return;

this.emit(`query.${mtd.filters[0]}`, data, { source: mtd.source });

if (data.new.status === 'completed') {
this.entity.setItem(this.id, data.new.data);
}
});

this.emit('query.pending', { new: this._query.state }, { source: this._query });
}

return this._query;
}

// Properties
get status(): QueryStatus {
return this._query?.status ?? 'pending';
}

get query(): AegisQuery<D> | undefined {
return this._query;
}

get data(): D | undefined {
return this.entity.getItem(this.id);
}

set data(value: D | undefined) {
if (value === undefined) {
this.entity.deleteItem(this.id);
} else {
this.entity.setItem(this.id, value);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import { EventSource } from '../events';
import { AegisEntity } from '../entities';
import { AegisQuery, QueryStatus } from '../protocols';
import { AegisQuery, QueryStatus, QueryUpdateEvent } from '../protocols';

import { ListQueryEvent } from './list-query.event';
import { ListUpdateEvent } from './list-update.event';
import { AegisEntity } from './entity';

// Types
export type ListEventMap<D> = {
query: { data: QueryUpdateEvent<D[]>, filters: ['pending' | 'completed' | 'error'] },
update: { data: D[], filters: [] },
}

// Class
export class AegisList<T> extends EventSource<ListQueryEvent<T> | ListUpdateEvent<T>> {
export class AegisList<D> extends EventSource<ListEventMap<D>> {
// Attributes
private _ids: string[] = [];
private _cache?: WeakRef<T[]>;
private _query?: AegisQuery<T[]>;
private _cache?: WeakRef<D[]>;
private _query?: AegisQuery<D[]>;

private _pristine = true;

// Constructor
constructor(
readonly entity: AegisEntity<T>,
readonly entity: AegisEntity<D>,
readonly key: string,
) {
super();

// Subscribe to entity update events
this.entity.subscribe('update', (event) => {
if (this._ids.includes(event.data.id)) {
this.entity.subscribe('update', (data) => {
if (this._ids.includes(data.id)) {
this._cache = undefined;
this._markDirty();
}
Expand All @@ -45,27 +49,27 @@ export class AegisList<T> extends EventSource<ListQueryEvent<T> | ListUpdateEven
}
}

refresh(query: AegisQuery<T[]>): AegisQuery<T[]> {
refresh(query: AegisQuery<D[]>): AegisQuery<D[]> {
if (this._query?.status === 'pending') {
this._query.cancel();
}

// Register query
this._query = query;

this._query.subscribe('update', (event) => {
this._query.subscribe('update', (data, event) => {
if (this._query !== event.source) return;

this.emit('query', event.data, { source: event.source });
this.emit(`query.${event.filters[0]}`, data, { source: event.source });

if (event.data.new.status === 'completed') {
this._ids = event.data.new.data.map(item => this.entity.storeItem(item));
if (data.new.status === 'completed') {
this._ids = data.new.data.map(item => this.entity.storeItem(item));
this._cache = undefined;
this._markDirty();
}
});

this.emit('query', { new: this._query.state }, { key: ['pending'], source: this._query });
this.emit('query.pending', { new: this._query.state }, { source: this._query });

return this._query;
}
Expand All @@ -75,11 +79,11 @@ export class AegisList<T> extends EventSource<ListQueryEvent<T> | ListUpdateEven
return this._query?.status ?? 'pending';
}

get query(): AegisQuery<T[]> | undefined {
get query(): AegisQuery<D[]> | undefined {
return this._query;
}

get data(): T[] {
get data(): D[] {
// Use cache first
const cached = this._cache?.deref();

Expand All @@ -88,10 +92,10 @@ export class AegisList<T> extends EventSource<ListQueryEvent<T> | ListUpdateEven
}

// Read from store
const data: T[] = [];
const data: D[] = [];

for (const id of this._ids) {
const ent = this.entity.store.get<T>(this.entity.name, id);
const ent = this.entity.store.get<D>(this.entity.name, id);

if (ent) {
data.push(ent);
Expand All @@ -103,7 +107,7 @@ export class AegisList<T> extends EventSource<ListQueryEvent<T> | ListUpdateEven
return data;
}

set data(data: T[]) {
set data(data: D[]) {
this._ids = data.map(item => this.entity.storeItem(item));
this._cache = new WeakRef(data);

Expand Down
Loading

0 comments on commit b3f2792

Please sign in to comment.