diff --git a/src/core/middleware/store.ts b/src/core/middleware/store.ts new file mode 100644 index 000000000..7c1799e39 --- /dev/null +++ b/src/core/middleware/store.ts @@ -0,0 +1,54 @@ +import { destroy, invalidator, create } from '../vdom'; +import injector from '../middleware/injector'; +import Store, { StatePaths, Path } from '../../stores/Store'; +import { Process } from '../../stores/process'; + +const factory = create({ destroy, invalidator, injector }); + +export const createStoreMiddleware = (initial?: (store: Store) => void) => { + let store = new Store(); + let initialized = false; + initial && initial(store); + const storeMiddleware = factory(({ middleware: { destroy, invalidator, injector } }) => { + const handles: any[] = []; + destroy(() => { + let handle: any; + while ((handle = handles.pop())) { + handle(); + } + }); + if (!initialized) { + const injectedStore = injector.get>('state'); + if (injectedStore) { + store = injectedStore; + } + initialized = true; + } + const registeredPaths: string[] = []; + const path: StatePaths = (path: any, ...segments: any) => { + return (store as any).path(path, ...segments); + }; + return { + get(path: Path): U { + if (registeredPaths.indexOf(path.path) === -1) { + const handle = store.onChange(path, () => { + invalidator(); + }); + handles.push(() => handle.remove()); + registeredPaths.push(path.path); + } + return store.get(path); + }, + path, + at(path: Path, index: number) { + return store.at(path, index); + }, + executor>(process: T): ReturnType { + return process(store) as any; + } + }; + }); + return storeMiddleware; +}; + +export default createStoreMiddleware; diff --git a/tests/core/unit/middleware/all.ts b/tests/core/unit/middleware/all.ts index b787ca123..b0a0d9ba2 100644 --- a/tests/core/unit/middleware/all.ts +++ b/tests/core/unit/middleware/all.ts @@ -6,4 +6,5 @@ import './icache'; import './injector'; import './intersection'; import './resize'; +import './store'; import './theme'; diff --git a/tests/core/unit/middleware/store.ts b/tests/core/unit/middleware/store.ts new file mode 100644 index 000000000..c066fecda --- /dev/null +++ b/tests/core/unit/middleware/store.ts @@ -0,0 +1,158 @@ +const { it, describe, afterEach, beforeEach } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); +import { sandbox } from 'sinon'; + +import { createProcess } from '../../../../src/stores/process'; +import { replace } from '../../../../src/stores/state/operations'; +import Store from '../../../../src/stores/Store'; +import createStoreMiddleware from '../../../../src/core/middleware/store'; + +const sb = sandbox.create(); +const destroyStub = sb.stub(); +const invalidatorStub = sb.stub(); +const injectorStub = { + get: sb.stub(), + subscribe: sb.stub() +}; +let storeMiddleware = createStoreMiddleware(); + +describe('store middleware', () => { + beforeEach(() => { + storeMiddleware = createStoreMiddleware(); + }); + + afterEach(() => { + sb.resetHistory(); + }); + + it('Should return data from store and subscribe to data changes for the path', () => { + const { callback } = storeMiddleware(); + const store = callback({ + id: 'test', + middleware: { + destroy: destroyStub, + invalidator: invalidatorStub, + injector: injectorStub + }, + properties: {} + }); + let result = store.get(store.path('my-state')); + assert.isUndefined(result); + const testProcess = createProcess('test', [ + ({ path }) => { + return [replace(path('my-state'), 'test-data')]; + } + ]); + store.executor(testProcess)({}); + assert.isTrue(invalidatorStub.calledOnce); + result = store.get(store.path('my-state')); + assert.strictEqual(result, 'test-data'); + }); + + it('Should be able to work with arrays in the store', () => { + const { callback } = storeMiddleware(); + const store = callback({ + id: 'test', + middleware: { + destroy: destroyStub, + invalidator: invalidatorStub, + injector: injectorStub + }, + properties: {} + }); + let result = store.get(store.path('my-state')); + assert.isUndefined(result); + let testProcess = createProcess('test', [ + ({ path }) => { + return [replace(path('my-state'), [1])]; + } + ]); + store.executor(testProcess)({}); + assert.isTrue(invalidatorStub.calledOnce); + result = store.get(store.at(store.path('my-state'), 0)); + assert.deepEqual(result, 1); + testProcess = createProcess('test', [ + ({ path, at }) => { + return [replace(at(path('my-state'), 1), 2)]; + } + ]); + store.executor(testProcess)({}); + assert.isTrue(invalidatorStub.calledTwice); + result = store.get(store.at(store.path('my-state'), 1)); + assert.deepEqual(result, 2); + }); + + it('Should remove subscription handles when destroyed', () => { + const { callback } = storeMiddleware(); + const store = callback({ + id: 'test', + middleware: { + destroy: destroyStub, + invalidator: invalidatorStub, + injector: injectorStub + }, + properties: {} + }); + let result = store.get(store.path('my-state')); + assert.isUndefined(result); + let testProcess = createProcess('test', [ + ({ path }) => { + return [replace(path('my-state'), 'test-data')]; + } + ]); + store.executor(testProcess)({}); + assert.isTrue(invalidatorStub.calledOnce); + result = store.get(store.path('my-state')); + assert.strictEqual(result, 'test-data'); + destroyStub.getCall(0).callArg(0); + testProcess = createProcess('test', [ + ({ path }) => { + return [replace(path('my-state'), 'test')]; + } + ]); + store.executor(testProcess)({}); + assert.isTrue(invalidatorStub.calledOnce); + result = store.get(store.path('my-state')); + assert.strictEqual(result, 'test'); + }); + + it('Should use an injected store if available', () => { + const { callback } = storeMiddleware(); + const injectedStore = new Store(); + let testProcess = createProcess('test', [ + ({ path }) => { + return [replace(path('my', 'nested', 'state'), 'existing-data')]; + } + ]); + testProcess(injectedStore)({}); + injectorStub.get.returns(injectedStore); + const store = callback({ + id: 'test', + middleware: { + destroy: destroyStub, + invalidator: invalidatorStub, + injector: injectorStub + }, + properties: {} + }); + const result = store.get(store.path('my', 'nested', 'state')); + assert.strictEqual(result, 'existing-data'); + const otherStore = callback({ + id: 'test', + middleware: { + destroy: destroyStub, + invalidator: invalidatorStub, + injector: injectorStub + }, + properties: {} + }); + const resultTwo = otherStore.get(store.path('my', 'nested', 'state')); + assert.strictEqual(resultTwo, 'existing-data'); + }); + + it('Should run initialize function on creation', () => { + const init = sb.stub(); + storeMiddleware = createStoreMiddleware(init); + assert.isTrue(init.calledOnce); + }); +});