Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework event types #59

Merged
merged 4 commits into from
Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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