diff --git a/core/src/index.ts b/core/src/index.ts index b8eb1e04..27db4d34 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -19,6 +19,8 @@ import type { import type { EventPayload, + ExtendedStore, + Extension, HarlemPlugin, InternalStores, MutationEventData, @@ -60,6 +62,23 @@ function emitCreated(store: InternalStore, state: any): void { eventEmitter.once(EVENTS.core.installed, created); } +function getExtendedStore(store: InternalStore, extensions: TExtensions): ReturnType { + 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; @@ -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(name: string, data: T, options?: Partial): Store { +export function createStore(name: string, state: TState, options?: Partial>): Store & ExtendedStore { 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); }; @@ -119,8 +140,10 @@ export function createStore(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, @@ -132,7 +155,8 @@ export function createStore(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 { diff --git a/core/src/types.ts b/core/src/types.ts index 26fb39cd..7e293387 100644 --- a/core/src/types.ts +++ b/core/src/types.ts @@ -4,6 +4,8 @@ import type { DeepReadonly } from 'vue'; +type UnionToIntersection = (U extends any ? (arg: U) => any : never) extends ((arg: infer I) => void) ? I : never; + export type ReadState = DeepReadonly; export type WriteState = TState; export type Getter = (state: ReadState) => TResult; @@ -11,7 +13,9 @@ export type Mutator = (state: WriteState = undefined extends TPayload ? (payload?: TPayload) => TResult : (payload: TPayload) => TResult; export type InternalStores = Map>; export type EventHandler = (payload?: EventPayload) => void; -export type MutationHookHandler = (data: MutationEventData) => void +export type MutationHookHandler = (data: MutationEventData) => void; +export type Extension = (store: InternalStore) => Record; +export type ExtendedStore = UnionToIntersection>; export interface Emittable { on(event: string, handler: EventHandler): EventListener; @@ -56,8 +60,8 @@ export interface InternalStoreOptions { allowOverwrite: boolean; } -export interface StoreOptions extends InternalStoreOptions { - // extensions: (() => Record)[] +export interface StoreOptions extends InternalStoreOptions { + extensions?: TExtensions; } export interface Store extends StoreBase { diff --git a/extensions/actions/package.json b/extensions/actions/package.json new file mode 100644 index 00000000..4f24363e --- /dev/null +++ b/extensions/actions/package.json @@ -0,0 +1,44 @@ +{ + "name": "@harlem/extension-actions", + "amdName": "harlemActions", + "version": "1.3.1", + "license": "MIT", + "author": "Andrew Courtice ", + "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" + } +} diff --git a/extensions/actions/src/index.ts b/extensions/actions/src/index.ts new file mode 100644 index 00000000..8da9799a --- /dev/null +++ b/extensions/actions/src/index.ts @@ -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) => { + + store.write('$add-actions', '$actions-extension', state => { + state.$actions = {}; + }); + + const addActionInstance = store.mutation('$add-action-instance', (state, payload) => { + const { + actionName, + instanceId, + instancePayload + } = payload; + + if (!state.$actions[actionName]) { + state.$actions[actionName] = new Map(); + } + + state.$actions[actionName].set(instanceId, instancePayload); + }); + + const removeActionInstance = store.mutation('$remove-action-instance', (state, payload) => { + const { + actionName, + instanceId, + } = payload; + + if (!state.$actions[actionName]) { + return; + } + + state.$actions[actionName].delete(instanceId); + }); +   + function action(name: string, body: ActionBody): Action { + const mutate = (mutator: Mutator) => 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; + } + + function isActionRunnng(name: string, instancePredicate?: ActionPredicate) { + const predicate = instancePredicate || (() => true); + const instances = store.state.$actions[name]; + + return !!instances && Array + .from(instances.values()) + .some(payload => predicate(payload)); + } + + function useAction(action: Action): ComposedAction { + const isRunning = (predicate?: ActionPredicate) => isActionRunnng(action.name, predicate); + + return [ + action, + isRunning + ]; + } + + return { + action, + isActionRunnng, + useAction + }; + +}); \ No newline at end of file diff --git a/extensions/actions/src/types.ts b/extensions/actions/src/types.ts new file mode 100644 index 00000000..e0b6605c --- /dev/null +++ b/extensions/actions/src/types.ts @@ -0,0 +1,26 @@ +export type ActionBody = undefined extends TPayload ? (payload?: TPayload) => Promise : (payload: TPayload) => Promise; +//export type Action = undefined extends TPayload ? (payload?: TPayload) => Promise : (payload: TPayload) => Promise; +export type ActionPredicate = (payload?: TPayload) => boolean; +export type ComposedAction = [Action, (predicate: ActionPredicate) => boolean]; + +export interface Action { + (payload?: TPayload): Promise; + name: string +} + +export interface ActionStoreState { + $actions: { + [key: string]: Map; + } +} + +export interface AddActionInstancePayload { + actionName: string; + instanceId: symbol; + instancePayload?: any; +} + +export interface RemoveActionInstancePayload { + actionName: string; + instanceId: symbol; +} \ No newline at end of file diff --git a/extensions/actions/tsconfig.json b/extensions/actions/tsconfig.json new file mode 100644 index 00000000..7d5ec66e --- /dev/null +++ b/extensions/actions/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src/**/*.ts", + "../../global.d.ts" + ] +} \ No newline at end of file diff --git a/lerna.json b/lerna.json index 7dcaa4b3..646913a5 100644 --- a/lerna.json +++ b/lerna.json @@ -5,6 +5,7 @@ "packages": [ "core", "packages/*", + "extensions/*", "plugins/*" ] } diff --git a/package.json b/package.json index 189cf67d..a9c36333 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "core", "docs", "packages/*", + "extensions/*", "plugins/*" ], "scripts": {