refactor: improve type checking for DispatchingStore dispatch payloads

This commit is contained in:
Andrew Bastin
2023-02-14 14:09:32 +05:30
parent cb5fff0310
commit b27fe871c4
11 changed files with 85 additions and 57 deletions

View File

@@ -1,15 +1,15 @@
import { Ref } from "vue" import { Ref } from "vue"
import { settingsStore, SettingsType } from "~/newstore/settings" import { settingsStore, SettingsDef } from "~/newstore/settings"
import { pluck, distinctUntilChanged } from "rxjs/operators" import { pluck, distinctUntilChanged } from "rxjs/operators"
import { useStream, useStreamStatic } from "./stream" import { useStream, useStreamStatic } from "./stream"
export function useSetting<K extends keyof SettingsType>( export function useSetting<K extends keyof SettingsDef>(
settingKey: K settingKey: K
): Ref<SettingsType[K]> { ): Ref<SettingsDef[K]> {
return useStream( return useStream(
settingsStore.subject$.pipe(pluck(settingKey), distinctUntilChanged()), settingsStore.subject$.pipe(pluck(settingKey), distinctUntilChanged()),
settingsStore.value[settingKey], settingsStore.value[settingKey],
(value: SettingsType[K]) => { (value: SettingsDef[K]) => {
settingsStore.dispatch({ settingsStore.dispatch({
dispatcher: "applySetting", dispatcher: "applySetting",
payload: { payload: {
@@ -25,13 +25,13 @@ export function useSetting<K extends keyof SettingsType>(
* A static version (does not require component setup) * A static version (does not require component setup)
* of `useSetting` * of `useSetting`
*/ */
export function useSettingStatic<K extends keyof SettingsType>( export function useSettingStatic<K extends keyof SettingsDef>(
settingKey: K settingKey: K
): [Ref<SettingsType[K]>, () => void] { ): [Ref<SettingsDef[K]>, () => void] {
return useStreamStatic( return useStreamStatic(
settingsStore.subject$.pipe(pluck(settingKey), distinctUntilChanged()), settingsStore.subject$.pipe(pluck(settingKey), distinctUntilChanged()),
settingsStore.value[settingKey], settingsStore.value[settingKey],
(value: SettingsType[K]) => { (value: SettingsDef[K]) => {
settingsStore.dispatch({ settingsStore.dispatch({
dispatcher: "applySetting", dispatcher: "applySetting",
payload: { payload: {

View File

@@ -6,7 +6,7 @@ import {
setDoc, setDoc,
} from "firebase/firestore" } from "firebase/firestore"
import { platform } from "~/platform" import { platform } from "~/platform"
import { applySetting, settingsStore, SettingsType } from "~/newstore/settings" import { applySetting, settingsStore, SettingsDef } from "~/newstore/settings"
/** /**
* Used locally to prevent infinite loop when settings sync update * Used locally to prevent infinite loop when settings sync update
@@ -59,7 +59,7 @@ export function initSettings() {
} else { } else {
writeSettings( writeSettings(
dispatch.payload.settingKey, dispatch.payload.settingKey,
settingsStore.value[dispatch.payload.settingKey as keyof SettingsType] settingsStore.value[dispatch.payload.settingKey as keyof SettingsDef]
) )
} }
} }

View File

@@ -2,9 +2,9 @@ import { Subject, BehaviorSubject } from "rxjs"
import { map } from "rxjs/operators" import { map } from "rxjs/operators"
import { assign, clone } from "lodash-es" import { assign, clone } from "lodash-es"
type dispatcherFunc<StoreType> = ( type DispatcherFunc<StoreType, PayloadType> = (
currentVal: StoreType, currentVal: StoreType,
payload: any payload: PayloadType
) => Partial<StoreType> ) => Partial<StoreType>
/** /**
@@ -13,26 +13,32 @@ type dispatcherFunc<StoreType> = (
* This function exists to provide better typing for dispatch function. * This function exists to provide better typing for dispatch function.
* As you can see, its pretty much an identity function. * As you can see, its pretty much an identity function.
*/ */
export const defineDispatchers = <StoreType, T>( export const defineDispatchers = <
StoreType,
T extends { [x: string]: DispatcherFunc<StoreType, any> }
>(
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
dispatchers: { [_ in keyof T]: dispatcherFunc<StoreType> } dispatchers: T
) => dispatchers ) => dispatchers
type Dispatch< type Dispatch<
StoreType, StoreType,
DispatchersType extends Record<string, dispatcherFunc<StoreType>> DispatchersType extends { [x: string]: DispatcherFunc<StoreType, any> },
Dispatcher extends keyof DispatchersType
> = { > = {
dispatcher: keyof DispatchersType dispatcher: Dispatcher
payload: any payload: Parameters<DispatchersType[Dispatcher]>[1]
} }
export default class DispatchingStore< export default class DispatchingStore<
StoreType, StoreType,
DispatchersType extends Record<string, dispatcherFunc<StoreType>> DispatchersType extends { [x: string]: DispatcherFunc<StoreType, any> }
> { > {
#state$: BehaviorSubject<StoreType> #state$: BehaviorSubject<StoreType>
#dispatchers: DispatchersType #dispatchers: DispatchersType
#dispatches$: Subject<Dispatch<StoreType, DispatchersType>> = new Subject() #dispatches$: Subject<
Dispatch<StoreType, DispatchersType, keyof DispatchersType>
> = new Subject()
constructor(initialValue: StoreType, dispatchers: DispatchersType) { constructor(initialValue: StoreType, dispatchers: DispatchersType) {
this.#state$ = new BehaviorSubject(initialValue) this.#state$ = new BehaviorSubject(initialValue)
@@ -64,9 +70,12 @@ export default class DispatchingStore<
return this.#dispatches$ return this.#dispatches$
} }
dispatch({ dispatcher, payload }: Dispatch<StoreType, DispatchersType>) { dispatch<Dispatcher extends keyof DispatchersType>({
dispatcher,
payload,
}: Dispatch<StoreType, DispatchersType, Dispatcher>) {
if (!this.#dispatchers[dispatcher]) if (!this.#dispatchers[dispatcher])
throw new Error(`Undefined dispatch type '${dispatcher}'`) throw new Error(`Undefined dispatch type '${String(dispatcher)}'`)
this.#dispatches$.next({ dispatcher, payload }) this.#dispatches$.next({ dispatcher, payload })
} }

View File

@@ -114,7 +114,7 @@ const dispatchers = defineDispatchers({
}, },
} }
}, },
deleteAllParams(curr: RESTSession) { deleteAllParams(curr: RESTSession, {}) {
return { return {
request: { request: {
...curr.request, ...curr.request,
@@ -168,7 +168,7 @@ const dispatchers = defineDispatchers({
}, },
} }
}, },
deleteAllHeaders(curr: RESTSession) { deleteAllHeaders(curr: RESTSession, {}) {
return { return {
request: { request: {
...curr.request, ...curr.request,
@@ -257,7 +257,7 @@ const dispatchers = defineDispatchers({
}, },
} }
}, },
deleteAllFormDataEntries(curr: RESTSession) { deleteAllFormDataEntries(curr: RESTSession, {}) {
// Only perform update if the current content-type is formdata // Only perform update if the current content-type is formdata
if (curr.request.body.contentType !== "multipart/form-data") return {} if (curr.request.body.contentType !== "multipart/form-data") return {}
@@ -288,7 +288,7 @@ const dispatchers = defineDispatchers({
} }
}, },
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
clearResponse(_curr: RESTSession) { clearResponse(_curr: RESTSession, {}) {
return { return {
response: null, response: null,
} }
@@ -490,7 +490,7 @@ export function setRESTTestScript(newScript: string) {
}) })
} }
export function setRESTReqBody(newBody: HoppRESTReqBody | null) { export function setRESTReqBody(newBody: HoppRESTReqBody) {
restSessionStore.dispatch({ restSessionStore.dispatch({
dispatcher: "setRequestBody", dispatcher: "setRequestBody",
payload: { payload: {

View File

@@ -109,7 +109,7 @@ export function setSIOEndpoint(newEndpoint: string) {
}) })
} }
export function setSIOVersion(newVersion: string) { export function setSIOVersion(newVersion: SIOClientVersion) {
SIOSessionStore.dispatch({ SIOSessionStore.dispatch({
dispatcher: "setVersion", dispatcher: "setVersion",
payload: { payload: {

View File

@@ -74,7 +74,7 @@ const dispatchers = defineDispatchers({
}, },
} }
}, },
deleteAllProtocols(curr: HoppWSSession) { deleteAllProtocols(curr: HoppWSSession, {}) {
return { return {
request: { request: {
endpoint: curr.request.endpoint, endpoint: curr.request.endpoint,

View File

@@ -127,7 +127,13 @@ const restCollectionDispatchers = defineDispatchers({
editFolder( editFolder(
{ state }: RESTCollectionStoreType, { state }: RESTCollectionStoreType,
{ path, folder }: { path: string; folder: string } {
path,
folder,
}: {
path: string
folder: HoppCollection<HoppRESTRequest>
}
) { ) {
const newState = state const newState = state
@@ -393,7 +399,7 @@ const gqlCollectionDispatchers = defineDispatchers({
editFolder( editFolder(
{ state }: GraphqlCollectionStoreType, { state }: GraphqlCollectionStoreType,
{ path, folder }: { path: string; folder: string } { path, folder }: { path: string; folder: HoppCollection<HoppGQLRequest> }
) { ) {
const newState = state const newState = state

View File

@@ -236,7 +236,8 @@ const dispatchers = defineDispatchers({
globals: entries, globals: entries,
} }
}, },
clearGlobalVariables() { // eslint-disable-next-line @typescript-eslint/no-unused-vars
clearGlobalVariables(_store, {}) {
return { return {
globals: [], globals: [],
} }

View File

@@ -141,7 +141,8 @@ const RESTHistoryDispatchers = defineDispatchers({
state: currentVal.state.filter((e) => !isEqual(e, entry)), state: currentVal.state.filter((e) => !isEqual(e, entry)),
} }
}, },
clearHistory() { // eslint-disable-next-line @typescript-eslint/no-unused-vars
clearHistory(_, {}) {
return { return {
state: [], state: [],
} }
@@ -189,7 +190,8 @@ const GQLHistoryDispatchers = defineDispatchers({
state: currentVal.state.filter((e) => !isEqual(e, entry)), state: currentVal.state.filter((e) => !isEqual(e, entry)),
} }
}, },
clearHistory() { // eslint-disable-next-line @typescript-eslint/no-unused-vars
clearHistory(_, {}) {
return { return {
state: [], state: [],
} }

View File

@@ -11,14 +11,18 @@ const defaultLocalState: LocalState = {
REMEMBERED_TEAM_ID: undefined, REMEMBERED_TEAM_ID: undefined,
} }
type ApplyLocalState = {
[K in keyof LocalState]: {
key: K
value: LocalState[K]
}
}[keyof LocalState]
const dispatchers = defineDispatchers({ const dispatchers = defineDispatchers({
bulkApplyState(_currentState: LocalState, payload: Partial<LocalState>) { bulkApplyState(_currentState: LocalState, payload: Partial<LocalState>) {
return payload return payload
}, },
applyState<K extends keyof LocalState>( applyState(_currentState: LocalState, { key, value }: ApplyLocalState) {
_currentState: LocalState,
{ key, value }: { key: K; value: LocalState[K] }
) {
const result: Partial<LocalState> = { const result: Partial<LocalState> = {
[key]: value, [key]: value,
} }

View File

@@ -27,7 +27,7 @@ export const HoppFontSizes = ["small", "medium", "large"] as const
export type HoppFontSize = (typeof HoppFontSizes)[number] export type HoppFontSize = (typeof HoppFontSizes)[number]
export type SettingsType = { export type SettingsDef = {
syncCollections: boolean syncCollections: boolean
syncHistory: boolean syncHistory: boolean
syncEnvironments: boolean syncEnvironments: boolean
@@ -53,7 +53,7 @@ export type SettingsType = {
COLUMN_LAYOUT: boolean COLUMN_LAYOUT: boolean
} }
export const defaultSettings: SettingsType = { export const defaultSettings: SettingsDef = {
syncCollections: true, syncCollections: true,
syncHistory: true, syncHistory: true,
syncEnvironments: true, syncEnvironments: true,
@@ -79,18 +79,22 @@ export const defaultSettings: SettingsType = {
COLUMN_LAYOUT: true, COLUMN_LAYOUT: true,
} }
type ApplySettingPayload = {
[K in keyof SettingsDef]: {
settingKey: K
value: SettingsDef[K]
}
}[keyof SettingsDef]
const validKeys = Object.keys(defaultSettings) const validKeys = Object.keys(defaultSettings)
const dispatchers = defineDispatchers({ const dispatchers = defineDispatchers({
bulkApplySettings( bulkApplySettings(_currentState: SettingsDef, payload: Partial<SettingsDef>) {
_currentState: SettingsType,
payload: Partial<SettingsType>
) {
return payload return payload
}, },
toggleSetting( toggleSetting(
currentState: SettingsType, currentState: SettingsDef,
{ settingKey }: { settingKey: KeysMatching<SettingsType, boolean> } { settingKey }: { settingKey: KeysMatching<SettingsDef, boolean> }
) { ) {
if (!has(currentState, settingKey)) { if (!has(currentState, settingKey)) {
// console.log( // console.log(
@@ -99,14 +103,14 @@ const dispatchers = defineDispatchers({
return {} return {}
} }
const result: Partial<SettingsType> = {} const result: Partial<SettingsDef> = {}
result[settingKey] = !currentState[settingKey] result[settingKey] = !currentState[settingKey]
return result return result
}, },
applySetting<K extends keyof SettingsType>( applySetting(
_currentState: SettingsType, _currentState: SettingsDef,
{ settingKey, value }: { settingKey: K; value: SettingsType[K] } { settingKey, value }: ApplySettingPayload
) { ) {
if (!validKeys.includes(settingKey)) { if (!validKeys.includes(settingKey)) {
// console.log( // console.log(
@@ -115,8 +119,9 @@ const dispatchers = defineDispatchers({
return {} return {}
} }
const result: Partial<SettingsType> = {} const result: Partial<SettingsDef> = {
result[settingKey] = value [settingKey]: value,
}
return result return result
}, },
@@ -129,20 +134,20 @@ export const settingsStore = new DispatchingStore(defaultSettings, dispatchers)
*/ */
export const settings$ = settingsStore.subject$.asObservable() export const settings$ = settingsStore.subject$.asObservable()
export function getSettingSubject<K extends keyof SettingsType>( export function getSettingSubject<K extends keyof SettingsDef>(
settingKey: K settingKey: K
): Observable<SettingsType[K]> { ): Observable<SettingsDef[K]> {
return settingsStore.subject$.pipe(pluck(settingKey), distinctUntilChanged()) return settingsStore.subject$.pipe(pluck(settingKey), distinctUntilChanged())
} }
export function bulkApplySettings(settingsObj: Partial<SettingsType>) { export function bulkApplySettings(settingsObj: Partial<SettingsDef>) {
settingsStore.dispatch({ settingsStore.dispatch({
dispatcher: "bulkApplySettings", dispatcher: "bulkApplySettings",
payload: settingsObj, payload: settingsObj,
}) })
} }
export function toggleSetting(settingKey: KeysMatching<SettingsType, boolean>) { export function toggleSetting(settingKey: KeysMatching<SettingsDef, boolean>) {
settingsStore.dispatch({ settingsStore.dispatch({
dispatcher: "toggleSetting", dispatcher: "toggleSetting",
payload: { payload: {
@@ -151,12 +156,13 @@ export function toggleSetting(settingKey: KeysMatching<SettingsType, boolean>) {
}) })
} }
export function applySetting<K extends keyof SettingsType>( export function applySetting<K extends ApplySettingPayload>(
settingKey: K, settingKey: K["settingKey"],
value: SettingsType[K] value: K["value"]
) { ) {
settingsStore.dispatch({ settingsStore.dispatch({
dispatcher: "applySetting", dispatcher: "applySetting",
// @ts-expect-error TS is not able to understand the type semantics here
payload: { payload: {
settingKey, settingKey,
value, value,