Skip to content

Commit

Permalink
feat(extensions): started action extension
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewcourtice committed Jun 16, 2021
1 parent ce94b35 commit 9874a8a
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 9 deletions.
36 changes: 30 additions & 6 deletions core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type {

import type {
EventPayload,
ExtendedStore,
Extension,
HarlemPlugin,
InternalStores,
MutationEventData,
Expand Down Expand Up @@ -60,6 +62,23 @@ function emitCreated(store: InternalStore, state: any): void {
eventEmitter.once(EVENTS.core.installed, created);
}

function getExtendedStore<TExtensions extends Extension[]>(store: InternalStore, extensions: TExtensions): ReturnType<Extension> {
return extensions.reduce((output, extension) => {
let result = {};

try {
result = extension(store) || {};
} catch {
result = {};
}

return {
...output,
...result
};
}, {});
}

function installPlugin(plugin: HarlemPlugin, app: App): void {
if (!plugin || typeof plugin.install !== 'function') {
return;
Expand All @@ -86,22 +105,24 @@ function installPlugin(plugin: HarlemPlugin, app: App): void {
export const on = eventEmitter.on.bind(eventEmitter);
export const once = eventEmitter.once.bind(eventEmitter);

export function createStore<T extends object = any>(name: string, data: T, options?: Partial<StoreOptions>): Store<T> {
export function createStore<TState extends object, TExtensions extends Extension[]>(name: string, state: TState, options?: Partial<StoreOptions<TExtensions>>): Store<TState> & ExtendedStore<TExtensions> {
const {
allowOverwrite
allowOverwrite,
extensions
} = {
allowOverwrite: true,
extensions: [store => ({})] as TExtensions,
...options
};

validateStoreCreation(name);

const store = new InternalStore(name, data, {
const store = new InternalStore(name, state, {
allowOverwrite
});

const destroy = () => {
store.emit(EVENTS.store.destroyed, SENDER, data);
store.emit(EVENTS.store.destroyed, SENDER, state);
stores.delete(name);
};

Expand All @@ -119,8 +140,10 @@ export function createStore<T extends object = any>(name: string, data: T, optio
const onAfterMutation = getMutationHook(EVENTS.mutation.after);
const onMutationError = getMutationHook(EVENTS.mutation.error);

const extendedStore = getExtendedStore(store, extensions);

stores.set(name, store);
emitCreated(store, data);
emitCreated(store, state);

return {
destroy,
Expand All @@ -132,7 +155,8 @@ export function createStore<T extends object = any>(name: string, data: T, optio
mutation: store.mutation.bind(store),
on: store.on.bind(store),
once: store.once.bind(store),
};
...extendedStore
} as any;
}

export default {
Expand Down
10 changes: 7 additions & 3 deletions core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import type {
DeepReadonly
} from 'vue';

type UnionToIntersection<U> = (U extends any ? (arg: U) => any : never) extends ((arg: infer I) => void) ? I : never;

export type ReadState<TState> = DeepReadonly<TState>;
export type WriteState<TState> = TState;
export type Getter<TState, TResult> = (state: ReadState<TState>) => TResult;
export type Mutator<TState, TPayload, TResult = void> = (state: WriteState<TState>, payload: TPayload) => TResult;
export type Mutation<TPayload, TResult = void> = undefined extends TPayload ? (payload?: TPayload) => TResult : (payload: TPayload) => TResult;
export type InternalStores = Map<string, InternalStore<any>>;
export type EventHandler<TData = any> = (payload?: EventPayload<TData>) => void;
export type MutationHookHandler<TPayload, TResult> = (data: MutationEventData<TPayload, TResult>) => void
export type MutationHookHandler<TPayload, TResult> = (data: MutationEventData<TPayload, TResult>) => void;
export type Extension = (store: InternalStore) => Record<string, any>;
export type ExtendedStore<TExtensions extends Extension[]> = UnionToIntersection<ReturnType<TExtensions[number]>>;

export interface Emittable {
on(event: string, handler: EventHandler): EventListener;
Expand Down Expand Up @@ -56,8 +60,8 @@ export interface InternalStoreOptions {
allowOverwrite: boolean;
}

export interface StoreOptions extends InternalStoreOptions {
// extensions: (() => Record<string, any>)[]
export interface StoreOptions<TExtensions extends Extension[]> extends InternalStoreOptions {
extensions?: TExtensions;
}

export interface Store<TState> extends StoreBase<TState> {
Expand Down
44 changes: 44 additions & 0 deletions extensions/actions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@harlem/extension-actions",
"amdName": "harlemActions",
"version": "1.3.1",
"license": "MIT",
"author": "Andrew Courtice <[email protected]>",
"description": "The official actions extension for Harlem",
"homepage": "https://harlemjs.com",
"main": "dist/harlem-actions.cjs.js",
"module": "dist/harlem-actions.bundler.esm.js",
"unpkg": "dist/harlem-actions.min.js",
"types": "dist/index.d.ts",
"source": "src/index.ts",
"keywords": [
"vue",
"state",
"harlem",
"extension",
"actions"
],
"repository": {
"type": "git",
"url": "https://github.com/andrewcourtice/harlem.git",
"directory": "extensions/actions"
},
"bugs": {
"url": "https://github.com/andrewcourtice/harlem/issues"
},
"scripts": {
"dev": "microbundle watch --raw",
"build": "node build.js",
"prepublish": "yarn build"
},
"peerDependencies": {
"@harlem/core": "^1.1.2",
"vue": "^3.0.0"
},
"devDependencies": {
"@harlem/build": "^1.3.1",
"@harlem/core": "^1.3.1",
"@harlem/testing": "^1.3.1",
"vue": "^3.0.0"
}
}
110 changes: 110 additions & 0 deletions extensions/actions/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
computed
} from 'vue';

import {
Extension,
InternalStore,
createStore,
Mutator
} from '@harlem/core';

import type {
Action,
ActionBody,
ActionPredicate,
ActionStoreState,
AddActionInstancePayload,
ComposedAction,
RemoveActionInstancePayload
} from './types';

export default ((store: InternalStore<ActionStoreState>) => {

store.write('$add-actions', '$actions-extension', state => {
state.$actions = {};
});

const addActionInstance = store.mutation<AddActionInstancePayload>('$add-action-instance', (state, payload) => {
const {
actionName,
instanceId,
instancePayload
} = payload;

if (!state.$actions[actionName]) {
state.$actions[actionName] = new Map<symbol, any>();
}

state.$actions[actionName].set(instanceId, instancePayload);
});

const removeActionInstance = store.mutation<RemoveActionInstancePayload>('$remove-action-instance', (state, payload) => {
const {
actionName,
instanceId,
} = payload;

if (!state.$actions[actionName]) {
return;
}

state.$actions[actionName].delete(instanceId);
});

function action<TPayload, TResult = void>(name: string, body: ActionBody<TPayload, TResult>): Action<TPayload, TResult> {
const mutate = (mutator: Mutator<ActionStoreState, undefined, void>) => store.write(name, '$actions-extension', mutator);

const _action = async (payload: TPayload) => {
const id = Symbol(name);

addActionInstance({
actionName: name,
instanceId: id,
instancePayload: payload
});

let result: TResult;

try {
result = await body(payload);
} finally {
removeActionInstance({
actionName: name,
instanceId: id 
});
}

return result;
};

_action.name = name;

return _action as Action<TPayload, TResult>;
}

function isActionRunnng<TPayload = any>(name: string, instancePredicate?: ActionPredicate<TPayload>) {
const predicate = instancePredicate || (() => true);
const instances = store.state.$actions[name];

return !!instances && Array
.from(instances.values())
.some(payload => predicate(payload));
}

function useAction<TPayload, TResult>(action: Action<TPayload, TResult>): ComposedAction<TPayload, TResult> {
const isRunning = (predicate?: ActionPredicate<TPayload>) => isActionRunnng(action.name, predicate);

return [
action,
isRunning
];
}

return {
action,
isActionRunnng,
useAction
};

});
26 changes: 26 additions & 0 deletions extensions/actions/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export type ActionBody<TPayload, TResult = void> = undefined extends TPayload ? (payload?: TPayload) => Promise<TResult> : (payload: TPayload) => Promise<TResult>;
//export type Action<TPayload, TResult = void> = undefined extends TPayload ? (payload?: TPayload) => Promise<TResult> : (payload: TPayload) => Promise<TResult>;
export type ActionPredicate<TPayload = any> = (payload?: TPayload) => boolean;
export type ComposedAction<TPayload, TResult = void> = [Action<TPayload, TResult>, (predicate: ActionPredicate<TPayload>) => boolean];

export interface Action<TPayload, TResult = void> {
(payload?: TPayload): Promise<TResult>;
name: string
}

export interface ActionStoreState {
$actions: {
[key: string]: Map<symbol, any>;
}
}

export interface AddActionInstancePayload {
actionName: string;
instanceId: symbol;
instancePayload?: any;
}

export interface RemoveActionInstancePayload {
actionName: string;
instanceId: symbol;
}
7 changes: 7 additions & 0 deletions extensions/actions/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"include": [
"src/**/*.ts",
"../../global.d.ts"
]
}
1 change: 1 addition & 0 deletions lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"packages": [
"core",
"packages/*",
"extensions/*",
"plugins/*"
]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"core",
"docs",
"packages/*",
"extensions/*",
"plugins/*"
],
"scripts": {
Expand Down

0 comments on commit 9874a8a

Please sign in to comment.