Skip to content

Commit

Permalink
Merge pull request #76 from Jujulego/feat/string-keys
Browse files Browse the repository at this point in the history
Use string keys
  • Loading branch information
Jujulego authored Jul 4, 2022
2 parents 2138204 + 608cea3 commit 0ac4c88
Show file tree
Hide file tree
Showing 19 changed files with 133 additions and 168 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
24 changes: 11 additions & 13 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 @@ -17,33 +17,31 @@ export class AegisItem<D> {
) {
// Subscribe to manager events
this._manager.subscribe('query.completed', (data) => {
if (data.status === 'completed') {
this.entity.setItem(this.id, data.result);
}
this.entity.setItem(this.id, data.result);
});
}

// 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
26 changes: 12 additions & 14 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 All @@ -27,11 +27,9 @@ export class AegisList<D> extends EventSource<ListEventMap<D>> {

// Subscribe to manager events
this._manager.subscribe('query.completed', (data) => {
if (data.status === 'completed') {
this._ids = data.result.map(item => this.entity.storeItem(item));
this._cache = new WeakRef(data.result);
this._markDirty();
}
this._ids = data.result.map(item => this.entity.storeItem(item));
this._cache = new WeakRef(data.result);
this._markDirty();
});

// Subscribe to entity update events
Expand Down Expand Up @@ -63,21 +61,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
28 changes: 22 additions & 6 deletions packages/core/src/utils/key-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ export class KeyTree<T, K extends Key> {
private readonly _children = new Map<FirstOfKey<K>, KeyTree<T, RestOfKey<K>>>();

// Methods
private _splitKey(key: PartialKey<K>): [FirstOfKey<K>, PartialKey<RestOfKey<K>>] {
const idx = key.indexOf('.');

if (idx === -1) {
return [
key as string as FirstOfKey<K>,
'' as PartialKey<RestOfKey<K>>
];
}

return [
key.substring(0, idx) as FirstOfKey<K>,
key.substring(idx + 1) as PartialKey<RestOfKey<K>>
];
}

private _getChild(part: FirstOfKey<K>): KeyTree<T, RestOfKey<K>> {
let child = this._children.get(part);

Expand All @@ -23,22 +39,22 @@ export class KeyTree<T, K extends Key> {
yield elem;
}

const [part, ...rest] = key;
const child = this._children.get(part as FirstOfKey<K>);
const [part, rest] = this._splitKey(key);
const child = this._children.get(part);

if (child) {
yield* child.searchWithParent(rest as PartialKey<RestOfKey<K>>);
yield* child.searchWithParent(rest);
}
}

insert(key: PartialKey<K>, elem: T): void {
if (key.length === 0) {
this._elements.add(elem);
} else {
const [part, ...rest] = key;
const [part, rest] = this._splitKey(key);

const child = this._getChild(part as FirstOfKey<K>);
child.insert(rest as PartialKey<RestOfKey<K>>, elem);
const child = this._getChild(part);
child.insert(rest, elem);
}
}

Expand Down
Loading

0 comments on commit 0ac4c88

Please sign in to comment.