From 85a5dde6d6deb2426a0a9db1559666d14a2b06f5 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Wed, 27 Nov 2019 00:03:40 +0100 Subject: [PATCH] refactor: apply code from readme --- __tests__/getters.spec.ts | 24 ++-- __tests__/pinia/stores/cart.ts | 14 +-- __tests__/pinia/stores/user.ts | 8 +- __tests__/ssr/app/main.ts | 4 +- __tests__/ssr/app/store.ts | 4 +- __tests__/store.patch.spec.ts | 26 ++--- __tests__/store.spec.ts | 26 ++--- __tests__/tds/store.test-d.ts | 4 +- rollup.config.js | 5 +- src/index.ts | 201 +-------------------------------- src/store.ts | 168 +++++++++++++++++++++++++++ 11 files changed, 227 insertions(+), 257 deletions(-) create mode 100644 src/store.ts diff --git a/__tests__/getters.spec.ts b/__tests__/getters.spec.ts index cdab1b7d..61b585e5 100644 --- a/__tests__/getters.spec.ts +++ b/__tests__/getters.spec.ts @@ -1,27 +1,25 @@ import { createStore } from '../src' describe('Store', () => { - function buildStore() { - return createStore( - 'main', - () => ({ - name: 'Eduardo', - }), - { - upperCaseName: ({ name }) => name.toUpperCase(), - } - ) - } + const useStore = createStore( + 'main', + () => ({ + name: 'Eduardo', + }), + { + upperCaseName: ({ name }) => name.toUpperCase(), + } + ).bind(null, true) it('adds getters to the store', () => { - const store = buildStore() + const store = useStore() expect(store.upperCaseName.value).toBe('EDUARDO') store.state.name = 'Ed' expect(store.upperCaseName.value).toBe('ED') }) it('updates the value', () => { - const store = buildStore() + const store = useStore() store.state.name = 'Ed' expect(store.upperCaseName.value).toBe('ED') }) diff --git a/__tests__/pinia/stores/cart.ts b/__tests__/pinia/stores/cart.ts index 4a09328e..1f93406b 100644 --- a/__tests__/pinia/stores/cart.ts +++ b/__tests__/pinia/stores/cart.ts @@ -1,7 +1,7 @@ -import { makeStore } from '../../../src' -import { userStore } from './user' +import { createStore } from '../../../src' +import { useUserStore } from './user' -export const cartStore = makeStore( +export const useCartStore = createStore( 'cart', () => ({ rawItems: [] as string[], @@ -23,19 +23,19 @@ export const cartStore = makeStore( ) export function addItem(name: string) { - const store = cartStore.useStore() + const store = useCartStore() store.state.rawItems.push(name) } export function removeItem(name: string) { - const store = cartStore.useStore() + const store = useCartStore() const i = store.state.rawItems.indexOf(name) if (i > -1) store.state.rawItems.splice(i, 1) } export async function purchaseItems() { - const cart = cartStore.useStore() - const user = userStore.useStore() + const cart = useCartStore() + const user = useUserStore() if (!user.state.name) return console.log('Purchasing', cart.items.value) diff --git a/__tests__/pinia/stores/user.ts b/__tests__/pinia/stores/user.ts index 1bd282b5..d30467b8 100644 --- a/__tests__/pinia/stores/user.ts +++ b/__tests__/pinia/stores/user.ts @@ -1,6 +1,6 @@ -import { makeStore } from '../../../src' +import { createStore } from '../../../src' -export const userStore = makeStore( +export const useUserStore = createStore( 'user', () => ({ name: 'Eduardo', @@ -10,7 +10,7 @@ export const userStore = makeStore( ) export function logout() { - const store = userStore.useStore() + const store = useUserStore() store.patch({ name: '', @@ -26,7 +26,7 @@ function apiLogin(a: string, p: string) { } export async function login(user: string, password: string) { - const store = userStore.useStore() + const store = useUserStore() const userData = await apiLogin(user, password) store.patch({ diff --git a/__tests__/ssr/app/main.ts b/__tests__/ssr/app/main.ts index fabda861..548cfb17 100644 --- a/__tests__/ssr/app/main.ts +++ b/__tests__/ssr/app/main.ts @@ -1,14 +1,14 @@ import Vue from 'vue' // import VueCompositionApi from '@vue/composition-api' import App from './App' -import { useStore, clear } from './store' +import { useStore } from './store' // Done in setup.ts // Vue.use(VueCompositionApi) export function createApp() { // create router and store instances - const store = useStore() + const store = useStore(true) store.reset() store.state.counter++ diff --git a/__tests__/ssr/app/store.ts b/__tests__/ssr/app/store.ts index 55d09aae..ca1cbf3b 100644 --- a/__tests__/ssr/app/store.ts +++ b/__tests__/ssr/app/store.ts @@ -1,6 +1,6 @@ -import { makeStore } from '../../../src' +import { createStore } from '../../../src' -export const { useStore, clear } = makeStore('main', () => ({ +export const useStore = createStore('main', () => ({ counter: 0, name: 'anon', })) diff --git a/__tests__/store.patch.spec.ts b/__tests__/store.patch.spec.ts index bbdde42b..d8e74a64 100644 --- a/__tests__/store.patch.spec.ts +++ b/__tests__/store.patch.spec.ts @@ -1,20 +1,18 @@ import { createStore } from '../src' describe('store.patch', () => { - function buildStore() { - return createStore('main', () => ({ - // TODO: the boolean cas shouldn't be necessary - // https://www.typescriptlang.org/play/#code/MYewdgzgLgBCMF4YG8CwAoGWYEMBcMUATgK4CmGAvhhiAHQ6IwBmOANhBehqJLMETI4oZJgAoAlIgB8MMclwFi5GJQk10vaDGBMBQkZI3AGTVhzJA - a: true as boolean, - nested: { - foo: 'foo', - a: { b: 'string' }, - }, - })) - } + const useStore = createStore('main', () => ({ + // TODO: the boolean cas shouldn't be necessary + // https://www.typescriptlang.org/play/#code/MYewdgzgLgBCMF4YG8CwAoGWYEMBcMUATgK4CmGAvhhiAHQ6IwBmOANhBehqJLMETI4oZJgAoAlIgB8MMclwFi5GJQk10vaDGBMBQkZI3AGTVhzJA + a: true as boolean, + nested: { + foo: 'foo', + a: { b: 'string' }, + }, + })).bind(null, true) // force always a fresh instance it('patches a property without touching the rest', () => { - const store = buildStore() + const store = useStore() store.patch({ a: false }) expect(store.state).toEqual({ a: false, @@ -26,7 +24,7 @@ describe('store.patch', () => { }) it('patches a nested property without touching the rest', () => { - const store = buildStore() + const store = useStore() store.patch({ nested: { foo: 'bar' } }) expect(store.state).toEqual({ a: true, @@ -46,7 +44,7 @@ describe('store.patch', () => { }) it('patches multiple properties at the same time', () => { - const store = buildStore() + const store = useStore() store.patch({ a: false, nested: { foo: 'hello' } }) expect(store.state).toEqual({ a: false, diff --git a/__tests__/store.spec.ts b/__tests__/store.spec.ts index c042a6ca..39ac7d29 100644 --- a/__tests__/store.spec.ts +++ b/__tests__/store.spec.ts @@ -1,18 +1,18 @@ import { createStore } from '../src' describe('Store', () => { - function buildStore() { - return createStore('main', () => ({ - a: true as boolean, - nested: { - foo: 'foo', - a: { b: 'string' }, - }, - })) - } + const useStore = createStore('main', () => ({ + // TODO: the boolean cas shouldn't be necessary + // https://www.typescriptlang.org/play/#code/MYewdgzgLgBCMF4YG8CwAoGWYEMBcMUATgK4CmGAvhhiAHQ6IwBmOANhBehqJLMETI4oZJgAoAlIgB8MMclwFi5GJQk10vaDGBMBQkZI3AGTVhzJA + a: true as boolean, + nested: { + foo: 'foo', + a: { b: 'string' }, + }, + })).bind(null, true) // force always a fresh instance it('sets the initial state', () => { - const store = buildStore() + const store = useStore() expect(store.state).toEqual({ a: true, nested: { @@ -23,7 +23,7 @@ describe('Store', () => { }) it('can replace its state', () => { - const store = buildStore() + const store = useStore() store.state = { a: false, nested: { @@ -43,8 +43,8 @@ describe('Store', () => { }) it('do not share the state between same id store', () => { - const store = buildStore() - const store2 = buildStore() + const store = useStore() + const store2 = useStore() expect(store.state).not.toBe(store2.state) store.state.nested.a.b = 'hey' expect(store2.state.nested.a.b).toBe('string') diff --git a/__tests__/tds/store.test-d.ts b/__tests__/tds/store.test-d.ts index a401b359..b018fb45 100644 --- a/__tests__/tds/store.test-d.ts +++ b/__tests__/tds/store.test-d.ts @@ -1,10 +1,12 @@ import { createStore } from '../../src' import { expectType, expectError } from 'tsd' -const store = createStore('name', () => ({ a: 'on' as 'on' | 'off' }), { +const useStore = createStore('name', () => ({ a: 'on' as 'on' | 'off' }), { upper: state => state.a.toUpperCase(), }) +const store = useStore() + expectType<{ a: 'on' | 'off' }>(store.state) expectError(() => store.nonExistant) diff --git a/rollup.config.js b/rollup.config.js index 4f8b7811..199db11e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -91,7 +91,10 @@ function createEntry({ terser({ module: format === 'es', output: { - preamble: banner, + // comments: false, + // already added by rollup + // only necessary if removing others + // preamble: banner, }, }) ) diff --git a/src/index.ts b/src/index.ts index f92d7f00..97b1efdc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,200 +1 @@ -import { ref, watch, computed } from '@vue/composition-api' -import { Ref } from '@vue/composition-api/dist/reactivity' -import { - StateTree, - Store, - SubscriptionCallback, - DeepPartial, - isPlainObject, - StoreGetters, - StoreGetter, -} from './types' -import { devtoolPlugin } from './devtools' - -function innerPatch( - target: T, - patchToApply: DeepPartial -): T { - // TODO: get all keys - for (const key in patchToApply) { - const subPatch = patchToApply[key] - const targetValue = target[key] - if (isPlainObject(targetValue) && isPlainObject(subPatch)) { - target[key] = innerPatch(targetValue, subPatch) - } else { - // @ts-ignore - target[key] = subPatch - } - } - - return target -} - -/** - * NOTE: by allowing users to name stores correctly, they can nest them the way - * they want, no? like user/cart - */ - -type CombinedStore< - Id extends string, - S extends StateTree, - G extends Record> -> = Store & StoreGetters - -/** - * Creates a store instance - * @param id unique identifier of the store, like a name. eg: main, cart, user - * @param initialState initial state applied to the store, Must be correctly typed to infer typings - */ - -export function createStore< - Id extends string, - S extends StateTree, - G extends Record> ->( - id: Id, - buildState: () => S, - getters: G = {} as G - // methods: Record -): CombinedStore { - const state: Ref = ref(buildState()) - - let isListening = true - let subscriptions: SubscriptionCallback[] = [] - - watch( - () => state.value, - state => { - if (isListening) { - subscriptions.forEach(callback => { - callback({ storeName: id, type: '🧩 in place', payload: {} }, state) - }) - } - }, - { - deep: true, - flush: 'sync', - } - ) - - function patch(partialState: DeepPartial): void { - isListening = false - innerPatch(state.value, partialState) - isListening = true - subscriptions.forEach(callback => { - callback( - { storeName: id, type: '⤵️ patch', payload: partialState }, - state.value - ) - }) - } - - function subscribe(callback: SubscriptionCallback): void { - subscriptions.push(callback) - // TODO: return function to remove subscription - } - - function reset() { - subscriptions = [] - state.value = buildState() - } - - const storeWithState: Store = { - id, - // it is replaced below by a getter - state: state.value, - - patch, - subscribe, - reset, - } - - // @ts-ignore we have to build it - const computedGetters: StoreGetters = {} - for (const getterName in getters) { - const method = getters[getterName] - // @ts-ignore - computedGetters[getterName] = computed>(() => - getters[getterName](state.value) - ) - } - - const store = { - ...storeWithState, - ...computedGetters, - } - - // make state access invisible - Object.defineProperty(store, 'state', { - get: () => state.value, - set: (newState: S) => { - isListening = false - state.value = newState - isListening = true - }, - }) - - // Devtools injection hue hue - devtoolPlugin(store) - - return store -} - -/** - * The api needs more work we must be able to use the store easily in any - * function by calling `useStore` to get the store Instance and we also need to - * be able to reset the store instance between requests on the server - */ - -/** - * - * @param id id of the store we are creating - * @param buildState function that returns a state - */ - -export function makeStore< - Id extends string, - S extends StateTree, - G extends Record> ->(id: Id, buildState: () => S, getters: G = {} as G) { - let store: CombinedStore | undefined - - function useStore(): CombinedStore { - if (!store) store = createStore(id, buildState, getters) - - return store - } - - function clear(): void { - store = undefined - } - - return { - useStore, - clear, - } -} - -// export const store = createStore('main', initialState) - -// type StateI = ReturnType -// const buildState = () => ({ -// items: ['thing 1'], -// }) -// export const cartStore = createStore('cart', buildState, { -// amount: state => state.items.length, -// }) - -// cartStore.nonueo -// cartStore.amount.value * 2 - -// store.patch({ -// toggle: 'off', -// nested: { -// a: { -// b: { -// c: 'one', -// }, -// }, -// }, -// }) +export { createStore } from './store' diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 00000000..be2b5581 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,168 @@ +import { ref, watch, computed } from '@vue/composition-api' +import { Ref } from '@vue/composition-api/dist/reactivity' +import { + StateTree, + Store, + SubscriptionCallback, + DeepPartial, + isPlainObject, + StoreGetters, + StoreGetter, +} from './types' +import { devtoolPlugin } from './devtools' + +function innerPatch( + target: T, + patchToApply: DeepPartial +): T { + // TODO: get all keys like symbols as well + for (const key in patchToApply) { + const subPatch = patchToApply[key] + const targetValue = target[key] + if (isPlainObject(targetValue) && isPlainObject(subPatch)) { + target[key] = innerPatch(targetValue, subPatch) + } else { + // @ts-ignore + target[key] = subPatch + } + } + + return target +} + +/** + * NOTE: by allowing users to name stores correctly, they can nest them the way + * they want, no? like user/cart + */ + +type CombinedStore< + Id extends string, + S extends StateTree, + G extends Record> +> = Store & StoreGetters + +/** + * Creates a store instance + * @param id unique identifier of the store, like a name. eg: main, cart, user + * @param initialState initial state applied to the store, Must be correctly typed to infer typings + */ + +export function buildStore< + Id extends string, + S extends StateTree, + G extends Record> +>( + id: Id, + buildState: () => S, + getters: G = {} as G + // methods: Record +): CombinedStore { + const state: Ref = ref(buildState()) + + let isListening = true + let subscriptions: SubscriptionCallback[] = [] + + watch( + () => state.value, + state => { + if (isListening) { + subscriptions.forEach(callback => { + callback({ storeName: id, type: '🧩 in place', payload: {} }, state) + }) + } + }, + { + deep: true, + flush: 'sync', + } + ) + + function patch(partialState: DeepPartial): void { + isListening = false + innerPatch(state.value, partialState) + isListening = true + subscriptions.forEach(callback => { + callback( + { storeName: id, type: '⤵️ patch', payload: partialState }, + state.value + ) + }) + } + + function subscribe(callback: SubscriptionCallback): void { + subscriptions.push(callback) + // TODO: return function to remove subscription + } + + function reset() { + subscriptions = [] + state.value = buildState() + } + + const storeWithState: Store = { + id, + // it is replaced below by a getter + state: state.value, + + patch, + subscribe, + reset, + } + + // @ts-ignore we have to build it + const computedGetters: StoreGetters = {} + for (const getterName in getters) { + const method = getters[getterName] + // @ts-ignore + computedGetters[getterName] = computed>(() => + getters[getterName](state.value) + ) + } + + const store = { + ...storeWithState, + ...computedGetters, + } + + // make state access invisible + Object.defineProperty(store, 'state', { + get: () => state.value, + set: (newState: S) => { + isListening = false + state.value = newState + isListening = true + }, + }) + + // Devtools injection hue hue + devtoolPlugin(store) + + return store +} + +/** + * The api needs more work we must be able to use the store easily in any + * function by calling `useStore` to get the store Instance and we also need to + * be able to reset the store instance between requests on the server + */ + +/** + * Creates a `useStore` function that retrieves the store instance + * @param id id of the store we are creating + * @param buildState function that returns a state + * @param getters optional object of getters + */ + +export function createStore< + Id extends string, + S extends StateTree, + G extends Record> +>(id: Id, buildState: () => S, getters: G = {} as G) { + let store: CombinedStore | undefined + + return function useStore(forceNewStore = false): CombinedStore { + if (!store || forceNewStore) store = buildStore(id, buildState, getters) + + return store + } +} -- 2.47.2