Skip to content

Commit

Permalink
refactor(app): restructure folder
Browse files Browse the repository at this point in the history
  • Loading branch information
exuanbo committed Dec 9, 2023
1 parent c69df67 commit 4dd9391
Show file tree
Hide file tree
Showing 42 changed files with 234 additions and 203 deletions.
2 changes: 0 additions & 2 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import Memory from '@/features/memory/Memory'

import ReloadPrompt from './ReloadPrompt'
import ResizablePanel from './ResizablePanel'
import { useStateSaver } from './stateSaver'

const App = (): JSX.Element => {
useStateSaver()
useGlobalExceptionHandler()

return (
Expand Down
2 changes: 1 addition & 1 deletion src/app/ResizablePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import BaseResizablePanel, {
} from '@/common/components/ResizablePanel'
import { selectIsRunning } from '@/features/controller/controllerSlice'

import { useSelector } from './selector'
import { useSelector } from './store'

const ResizablePanel = (props: ResizablePanelProps): JSX.Element => {
const isRunning = useSelector(selectIsRunning)
Expand Down
26 changes: 0 additions & 26 deletions src/app/localStorage.ts

This file was deleted.

31 changes: 0 additions & 31 deletions src/app/persist.ts

This file was deleted.

19 changes: 0 additions & 19 deletions src/app/stateSaver.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { StoreEnhancer } from '@reduxjs/toolkit'

export const extendStore =
export const injectExtension =
<Ext extends {}>(extension: Ext): StoreEnhancer<Ext> =>
(createStore) =>
(...args) => {
Expand Down
File renamed without changes.
28 changes: 13 additions & 15 deletions src/app/store.ts → src/app/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { combineSlices, configureStore } from '@reduxjs/toolkit'

import { merge } from '@/common/utils'
import { assemblerSlice } from '@/features/assembler/assemblerSlice'
import { controllerSlice } from '@/features/controller/controllerSlice'
import { cpuSlice } from '@/features/cpu/cpuSlice'
Expand All @@ -9,12 +8,14 @@ import { exceptionSlice } from '@/features/exception/exceptionSlice'
import { ioSlice } from '@/features/io/ioSlice'
import { memorySlice } from '@/features/memory/memorySlice'

import { createActionObserver } from './actionObserver'
import { loadState as loadStateFromLocalStorage } from './localStorage'
import { getInitialStateToPersist } from './persist'
import { createStateObserver } from './stateObserver'
import { subscribeChange } from './subscribeChange'
import { loadState as loadStateFromUrl } from './url'
import { subscribeChange } from './enhancers/subscribeChange'
import { createActionObserver } from './observers/actionObserver'
import { createStateObserver } from './observers/stateObserver'
import {
readStateFromPersistence,
selectStateToPersist,
writeStateToPersistence,
} from './persistence'

const rootReducer = combineSlices(
editorSlice,
Expand All @@ -28,13 +29,6 @@ const rootReducer = combineSlices(

export type RootState = ReturnType<typeof rootReducer>

const getPreloadedState = (): Partial<RootState> => {
const stateFromLocalStorage = loadStateFromLocalStorage()
const stateFromUrl = loadStateFromUrl()
// in case any future changes to the state structure
return merge(getInitialStateToPersist(), stateFromLocalStorage, stateFromUrl)
}

const stateObserver = createStateObserver<RootState>()
const actionObserver = createActionObserver()

Expand All @@ -45,11 +39,15 @@ export const store = configureStore({
return defaultMiddleware.prepend(stateObserver.middleware, actionObserver.middleware)
},
devTools: import.meta.env.DEV,
preloadedState: getPreloadedState(),
preloadedState: readStateFromPersistence(),
enhancers: (getDefaultEnhancers) => {
const defaultEnhancers = getDefaultEnhancers({ autoBatch: false })
return defaultEnhancers
.concat(subscribeChange)
.concat(stateObserver.enhancer, actionObserver.enhancer)
},
})

store.onState(selectStateToPersist).subscribe(writeStateToPersistence)

export * from './selector'
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@reduxjs/toolkit'
import { filter, map, type Observable, Subject } from 'rxjs'

import { extendStore } from './storeEnhancer'
import { injectExtension } from '../enhancers/injectExtension'
import { createWeakCache } from './weakCache'

type OnAction = <TPayload>(actionCreator: PayloadActionCreator<TPayload>) => Observable<TPayload>
Expand Down Expand Up @@ -44,6 +44,6 @@ export const createActionObserver = (): ActionObserver => {

return {
middleware,
enhancer: extendStore({ onAction }),
enhancer: injectExtension({ onAction }),
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Middleware, Selector, StoreEnhancer } from '@reduxjs/toolkit'
import { distinctUntilChanged, map, type Observable, ReplaySubject } from 'rxjs'

import { extendStore } from './storeEnhancer'
import { injectExtension } from '../enhancers/injectExtension'
import { createWeakCache } from './weakCache'

type OnState<TState> = <TSelected>(selector: Selector<TState, TSelected>) => Observable<TSelected>
Expand Down Expand Up @@ -40,6 +40,6 @@ export const createStateObserver = <TState>(): StateObserver<TState> => {

return {
middleware,
enhancer: extendStore({ onState }),
enhancer: injectExtension({ onState }),
}
}
File renamed without changes.
13 changes: 13 additions & 0 deletions src/app/store/persistence/combinedProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { merge, type PlainObject } from '@/common/utils'

import { localStorageProvider } from './providers/localStorage'
import { queryParamProvider } from './providers/queryParam'
import type { PersistenceProvider } from './types'

export const getCombinedProvider = <State extends PlainObject>(): PersistenceProvider<State> => {
const providers: PersistenceProvider<State>[] = [localStorageProvider, queryParamProvider]
return {
read: () => providers.reduce((result, provider) => merge(result, provider.read()), {}),
write: (state) => providers.forEach((provider) => provider.write(state)),
}
}
49 changes: 49 additions & 0 deletions src/app/store/persistence/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createSelector } from '@reduxjs/toolkit'
import type { Test as TypeTest } from 'ts-toolbelt'

import { merge } from '@/common/utils'
import { controllerSlice } from '@/features/controller/controllerSlice'
import { editorSlice } from '@/features/editor/editorSlice'

import { getCombinedProvider } from './combinedProvider'
import type { PersistenceProvider } from './types'

const provider = getCombinedProvider()

type PreloadedState = {
[editorSlice.reducerPath]: ReturnType<typeof editorSlice.getInitialState>
[controllerSlice.reducerPath]: ReturnType<typeof controllerSlice.getInitialState>
}

export const readStateFromPersistence = (): PreloadedState => {
const persistedState = provider.read()
// in case of future changes to the state shape
return merge(
{
[editorSlice.reducerPath]: editorSlice.getInitialState(),
[controllerSlice.reducerPath]: controllerSlice.getInitialState(),
},
persistedState,
)
}

export const selectStateToPersist = createSelector(
editorSlice.selectors.selectToPersist,
controllerSlice.selectors.selectToPersist,
(editorState, controllerState) => ({
[editorSlice.reducerPath]: editorState,
[controllerSlice.reducerPath]: controllerState,
}),
)

type StateToPersist = ReturnType<typeof selectStateToPersist>

export const writeStateToPersistence: PersistenceProvider<StateToPersist>['write'] = provider.write

if (import.meta.env.NEVER) {
const { checkType, checkTypes } = await import('@/common/utils')
checkTypes([
checkType<keyof PreloadedState, keyof StateToPersist, TypeTest.Pass>(),
checkType<keyof StateToPersist, keyof PreloadedState, TypeTest.Pass>(),
])
}
31 changes: 31 additions & 0 deletions src/app/store/persistence/providers/localStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { name } from '@/../package.json'
import { isPlainObject } from '@/common/utils'

import type { PersistenceProvider } from '../types'

const LOCAL_STORAGE_KEY = `persist:${name}`

export const localStorageProvider: PersistenceProvider = {
read: () => {
try {
const serializedState = localStorage.getItem(LOCAL_STORAGE_KEY)
if (serializedState !== null) {
const state: unknown = JSON.parse(serializedState)
if (isPlainObject(state)) {
return state
}
}
} catch {
// ignore error
}
return {}
},
write: (state) => {
try {
const serializedState = JSON.stringify(state)
localStorage.setItem(LOCAL_STORAGE_KEY, serializedState)
} catch {
// ignore write error
}
},
}
40 changes: 40 additions & 0 deletions src/app/store/persistence/providers/queryParam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as Base64 from 'js-base64'
import * as Pako from 'pako'

import { isPlainObject } from '@/common/utils'

import type { PersistenceProvider } from '../types'

const QUERY_PARAMETER_NAME = 'shareable'

const getShareUrl = (state: unknown) => {
const url = new URL(window.location.href)
const compressedData = Pako.deflate(JSON.stringify(state))
const encodedState = Base64.fromUint8Array(compressedData, /* urlsafe: */ true)
url.searchParams.set(QUERY_PARAMETER_NAME, encodedState)
return url
}

export const queryParamProvider: PersistenceProvider = {
read: () => {
const url = new URL(window.location.href)
const encodedState = url.searchParams.get(QUERY_PARAMETER_NAME)
if (encodedState !== null) {
try {
const compressedData = Base64.toUint8Array(encodedState)
const decodedState = Pako.inflate(compressedData, { to: 'string' })
const state: unknown = JSON.parse(decodedState)
if (isPlainObject(state)) {
return state
}
} catch {
// ignore error
}
}
return {}
},
write: (state) => {
const shareUrl = getShareUrl(state)
window.history.replaceState({}, '', shareUrl)
},
}
8 changes: 8 additions & 0 deletions src/app/store/persistence/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { PlainObject } from '@/common/utils'

type Persisted<State> = State | Record<string, never>

export interface PersistenceProvider<State extends PlainObject = PlainObject> {
read: () => Persisted<State>
write: (state: State) => void
}
2 changes: 1 addition & 1 deletion src/app/selector.ts → src/app/store/selector/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type { Selector } from '@reduxjs/toolkit'
import { useDebugValue } from 'react'

import { type RootState, store } from './store'
import { type RootState, store } from '..'
import { useSyncExternalStoreWithSelector } from './useSyncExternalStoreWithSelector'

type StateSelector<TSelected> = Selector<RootState, TSelected>
Expand Down
File renamed without changes.
34 changes: 0 additions & 34 deletions src/app/url.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/app/subscribe.ts → src/common/observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Observable, Observer, Subscription } from 'rxjs'

export type Unsubscribe = Subscription['unsubscribe']

export const subscribe = <T>(
export const observe = <T>(
observable: Observable<T>,
observerOrNext?: Partial<Observer<T>> | ((value: T) => void) | null,
): Unsubscribe => {
Expand Down
Loading

0 comments on commit 4dd9391

Please sign in to comment.