Skip to content

Commit

Permalink
docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Alorel committed Sep 25, 2020
1 parent d7444c4 commit f82c5bd
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 5 deletions.
110 changes: 107 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ A set of utilities for running Redux in a web worker.
- [Basic usage](#basic-usage)
- [Sending default state from the main thread](#sending-default-state-from-the-main-thread)
- [Adding Redux devtools support](#adding-redux-devtools-support)
- [API](#api)
- [Worker](#worker)
- [Main thread](#main-thread)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand Down Expand Up @@ -49,11 +52,17 @@ The main thread should be used by the UI, not state management. This set of util
a web worker so your main thread doesn't slow down the UI! The general process is as follows:

1. Initialise your store with all its reducers on a web worker, apply the off main thread middleware.
2. Initialise a Worker wrapper on the main thread - this has the same API as a regular store, but with a few notable differences:
1. Initialise a Worker wrapper on the main thread - this has the same API as a regular store, but with a few notable differences:
- It does not have any reducers, `replaceReducer` throws an error
- It does not have `[Symbo.observable]`
- It does not have `[Symbol.observable]`
- Actions do not synchronously update the state anymore, therefore the `subscribe()` function may not behave as expected
3. Use the store as usual.
1. Use the store as usual: dispatch an action.
1. The action is serialised and sent to the web worker.
1. The worker passes it on to the real redux store - reducers are triggered at this point.
1. A diff of the state change is produced - this is where the `fast-json-patch` dependency comes in - and sent to the main thread along with the action that triggered it.
- It would be much simpler to just overwrite the entire state object, but that would kill all the old object references and could potentially have a terrible effect on app performance as well as introducing bugs
1. The main thread's store wrapper clones only the paths that changed and applies the diff to the new state object.
1. A change is emitted.

# Usage

Expand Down Expand Up @@ -154,3 +163,98 @@ const store = createWrappedStore({
worker
});
```

# API

Typescript definitions are provided for clarity

## Worker

```typescript
import {Middleware} from 'redux';


/** Create a redux-off-main-thread middleware instance. This should be run on the worker thread. */
export declare function createReduxOMTMiddleware(): Middleware;


/**
* Resolves with the initial state when the worker receives an initial state message.
* Rejects when called outside a worker thread.
*/
export declare function onReduxWorkerThreadInitialStateReceived(): Promise<any>;


/**
* Resolves when the worker receives a ready event, indicating that the main thread has finished setting up
* event listeners. Should be instant unless you've created some weird environment e.g. during CI.
* Rejects when called outside a worker thread.
*/
export declare function onReduxWorkerThreadReady(): Promise<void>;
```

## Main thread

```typescript
import type {Action, AnyAction, Store} from 'redux';
import type {EnhancerOptions} from 'redux-devtools-extension';


export declare type WorkerPartial = Pick<Worker, 'addEventListener' | 'postMessage'>;


/** A Redux store wrapped to run off the main thread */
export type WrappedStore<S, A extends Action = AnyAction> = Store<S, A> & {

/**
* Actions no longer mutate the state synchronously, therefore the store no longer behaves exactly as a regular
* Redux store:
* <code>
* const oldState = store.getState();
* store.dispatch({type: 'some-valid-action-that-should-mutate-the-state''});
* // True on an off-main-thread store, false on a regular store
* console.log(oldState === store.getState());
* </code>
* This method can be used to react to when the store off the main thread
*/
onChange(listener: (action: A, newState: S, oldState: S) => void): () => void;
}


/** {@link createWrappedStore} initialisation config */
export interface CreateWrappedStoreInit<S> {
/**
* Options for enabling devtools support. Can be either an {@link EnhancerOptions} object or true,
* which is equivalent to passing {}
* @default false
*/
devtoolsInit?: boolean | EnhancerOptions;
/** Initial store state */
initialState: S;
/**
* Having this as false requires the main thread and worker thread to set the same initial state from an object
* somewhere in your codebase (and bundled by your build system) and is suitable for the
* {@link https://github.com/Alorel/redux-off-main-thread/tree/master#basic-usage Basic usage} use case. You may
* instead opt to only set this to true and send the initial state as a message to the worker; this is outlined in the
* {@link https://github.com/Alorel/redux-off-main-thread/tree/master#sending-default-state-from-the-main-thread Sending default state from the main thread}
* example.
* @default false
*/
syncInitialState?: boolean;
/** The worker instance Redux is running on */
worker: WorkerPartial;
}


/**
* Create a wrapped store with the same API as a regular Redux store bar several differences:
* <ul>
* <li>It does not have any reducers, replaceReducer throws an error</li>
* <li>It does not have a Symbol.observable</li>
* <li>Actions do not synchronously update the state anymore, therefore the subscribe() function may not behave as expected</li>
* <li>It has an extra onChange() method</li>
* </ul>
* @param init
*/
export declare function createWrappedStore<S, A extends Action = AnyAction>(init: CreateWrappedStoreInit<S>): WrappedStore<S, A>;
```
8 changes: 7 additions & 1 deletion projects/core/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/** @internal */
export * from './ActionDispatchedEvent';

/** @internal */
export * from './ActionProcessedEvent';

/** @internal */
export * from './InitialStateEvent';

/** @internal */
export * from './ReadyEvent';
export * from './ReduxOMTEvent';
28 changes: 28 additions & 0 deletions projects/core/main-thread/lib/createWrappedStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,31 @@ import {WrappedStore} from './wrapped-store/WrappedStore';
/** @internal */
declare const __REDUX_DEVTOOLS_EXTENSION__: DevtoolsExtensionFactory;

/** {@link createWrappedStore} initialisation config */
export interface CreateWrappedStoreInit<S> {

/**
* Options for enabling devtools support. Can be either an {@link EnhancerOptions} object or true,
* which is equivalent to passing {}
* @default false
*/
devtoolsInit?: boolean | EnhancerOptions;

/** Initial store state */
initialState: S;

/**
* Having this as false requires the main thread and worker thread to set the same initial state from an object
* somewhere in your codebase (and bundled by your build system) and is suitable for the
* {@link https://github.com/Alorel/redux-off-main-thread/tree/master#basic-usage Basic usage} use case. You may
* instead opt to only set this to true and send the initial state as a message to the worker; this is outlined in the
* {@link https://github.com/Alorel/redux-off-main-thread/tree/master#sending-default-state-from-the-main-thread Sending default state from the main thread}
* example.
* @default false
*/
syncInitialState?: boolean;

/** The worker instance Redux is running on */
worker: WorkerPartial;
}

Expand Down Expand Up @@ -85,6 +103,16 @@ function create<S, A extends Action>(
} as Omit<WrappedStore<S, A>, (typeof Symbol)['observable']> as WrappedStore<S, A>;
}

/**
* Create a wrapped store with the same API as a regular Redux store bar several differences:
* <ul>
* <li>It does not have any reducers, replaceReducer throws an error</li>
* <li>It does not have a Symbol.observable</li>
* <li>Actions do not synchronously update the state anymore, therefore the subscribe() function may not behave as expected</li>
* <li>It has an extra onChange() method</li>
* </ul>
* @param init
*/
export function createWrappedStore<S, A extends Action = AnyAction>(
init: CreateWrappedStoreInit<S>
): WrappedStore<S, A> {
Expand Down
13 changes: 13 additions & 0 deletions projects/core/main-thread/lib/wrapped-store/WrappedStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import {Action, AnyAction, Store} from 'redux';

/** A Redux store wrapped to run off the main thread */
export type WrappedStore<S, A extends Action = AnyAction> = Store<S, A> & {

/**
* Actions no longer mutate the state synchronously, therefore the store no longer behaves exactly as a regular
* Redux store:
* <code>
* const oldState = store.getState();
* store.dispatch({type: 'some-valid-action-that-should-mutate-the-state''});
* // True on an off-main-thread store, false on a regular store
* console.log(oldState === store.getState());
* </code>
* This method can be used to react to when the store off the main thread
*/
onChange(listener: (action: A, newState: S, oldState: S) => void): () => void;
}
1 change: 1 addition & 0 deletions projects/core/worker/lib/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const middleware: Middleware = (store: MiddlewareAPI) => {
/* eslint-enable implicit-arrow-linebreak */
};

/** Create a redux-off-main-thread middleware instance. This should be run on the worker thread. */
export function createReduxOMTMiddleware(): Middleware {
if (!IS_ON_WORKER) {
throw new Error('Not running in a worker context');
Expand Down
4 changes: 4 additions & 0 deletions projects/core/worker/lib/onInitialStateReceved.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ const INITIAL_STATE$: Promise<any> = IS_ON_WORKER ?
timeoutListener(ReduxOMTEvent.INITIAL_STATE, isInitialStateEvent, e => e.state) :
null as any;

/**
* Resolves with the initial state when the worker receives an initial state message.
* Rejects when called outside a worker thread.
*/
export function onReduxWorkerThreadInitialStateReceived(): Promise<any> {
return INITIAL_STATE$ || Promise.reject(new Error('Not on worker thread'));
}
5 changes: 5 additions & 0 deletions projects/core/worker/lib/ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ const READY$: Promise<void> = IS_ON_WORKER ?
timeoutListener(ReduxOMTEvent.READY, isReduxOMTReadyEvent) :
null as any;

/**
* Resolves when the worker receives a ready event, indicating that the main thread has finished setting up
* event listeners. Should be instant unless you've created some weird environment e.g. during CI.
* Rejects when called outside a worker thread.
*/
export function onReduxWorkerThreadReady(): Promise<void> {
return READY$ || Promise.reject(new Error('Not on worker thread'));
}
Expand Down
1 change: 0 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ function createConfig(rollupConfig) {
external: _buildBaseExternals,
input: [
join(projectDir, 'index.ts'),
join(projectDir, 'common', 'index.ts'),
join(projectDir, 'main-thread', 'index.ts'),
join(projectDir, 'worker', 'index.ts')
],
Expand Down

0 comments on commit f82c5bd

Please sign in to comment.