Skip to content

Commit 271df24

Browse files
authored
Local Storage Middleware (#65)
* Local Storage Middleware * Cannot set local storage on browsers, so stub if exists * fix tests * skip safari tests
1 parent f9e6933 commit 271df24

File tree

4 files changed

+164
-13
lines changed

4 files changed

+164
-13
lines changed

src/stores/README.md

+41-13
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ An application store designed to complement @dojo/widgets and @dojo/widget-core
2626
- [Transforming Executor Arguments](#transforming-executor-arguments)
2727
- [Optimistic Update Pattern](#optimistic-update-pattern)
2828
- [Executing Concurrent Commands](#executing-concurrent-commands)
29-
- [Decorating Processes](#decorating-processes)
30-
- [Decorating Multiple Processes](#decorating-multiple-processes)
29+
- [Middleware](#middleware)
30+
- [Applying Middleware to Multiple Processes](#applying-middleware-to-multiple-processes)
31+
- [Local Storage Middleware](#local-storage-middleware)
3132

3233
-----
3334

@@ -524,21 +525,23 @@ In this example, `commandOne` is executed, then both `concurrentCommandOne` and
524525

525526
**Note:** Concurrent commands are always assumed to be asynchronous and resolved using `Promise.all`.
526527

527-
### Decorating Processes
528+
## Middleware
528529

529-
The `Process` callback provides a hook to apply generic/global functionality across multiple or all processes used within an application. This is done using higher order functions that wrap the process' local `callback` using the error and result payload to decorate or perform an action for all processes it is used for.
530+
Middleware provides a hook to apply generic/global functionality across multiple or all processes used within an application. Middleware is a function that receives the error and the result of a process to perform a specific action, before calling the next middleware if provided.
531+
532+
This is done using higher order functions that wrap the process' local `callback` using the error and result payload to decorate or perform an action for all processes it is used for.
530533

531534
`callback` decorators can be composed together to combine multiple units of functionality, such that in the example below `myProcess` would run the `error` and `result` through the `collector`, `logger` and then `snapshot` callbacks.
532535

533536
```ts
534537
const myProcess = createProcess('my-process', [ commandOne, commandTwo ], collector(logger(snapshot())));
535538
```
536539

537-
#### Decorating Multiple Processes
540+
### Applying Middleware to Multiple Processes
538541

539-
Specifying a `callback` decorator on an individual process explicitly works for targeted behavior but can become cumbersome when the decorator needs to be applied to multiple processes throughout the application.
542+
Specifying a middleware on an individual process explicitly works for targeted behavior but can become cumbersome when the middleware needs to be applied to multiple processes throughout the application.
540543

541-
The `createProcessWith` higher order function can be used to specify `callback` decorators that need to be applied across multiple `processes`. The function accepts an array of `callback` decorators and returns a new `createProcess` factory function that will automatically apply the decorators to any process that it creates.
544+
The `createProcessWith` higher order function can be used to specify middlewares that need to be applied across multiple `processes`. The function accepts an array of middleware and returns a new `createProcess` factory function that will automatically apply the middleware to any process that it creates.
542545

543546
```ts
544547
const customCreateProcess = createProcessWith([ logger ]);
@@ -547,18 +550,43 @@ const customCreateProcess = createProcessWith([ logger ]);
547550
const myProcess = customCreateProcess('my-process', [ commandOne, commandTwo ]);
548551
```
549552

550-
An additional helper function `createCallbackDecorator` can be used to turn a simple `ProcessCallback` into a decorator that ensures the passed callback is executed after the decorating `callback` has been run.
553+
An additional helper function `createCallbackDecorator` can be used to ensure that a middleware function calls the next middleware after it has finished executing.
551554

552555
```ts
553-
const myCallback = (error: ProcessError, result: ProcessResult) => {
556+
const myMiddleware = (error: ProcessError, result: ProcessResult) => {
554557
// do things with the outcome of the process
555558
};
556559

557-
// turns the callback into a callback decorator
558-
const myCallbackDecorator = createCallbackDecorator(myCallback);
560+
// ensures the middleware will call the next middleware in the stack
561+
const myMiddlewareDecorator = createCallbackDecorator(myMiddleware);
562+
563+
// use the middleware decorator as normal
564+
const myProcess = createProcess('my-process', [ commandOne ], myMiddlewareDecorator());
565+
```
566+
567+
### Local Storage Middleware
568+
569+
Middleware that provides a `collector` that saves state to `LocalStorage` and a `load` function to hydrate a store from `LocalStorage`.
570+
571+
```ts
572+
export const myProcess = createProcess(
573+
'my-process',
574+
[ command ],
575+
collector('my-process', (path) => {
576+
return [
577+
path('state', 'to', 'save'),
578+
path('other', 'state', 'to', 'save')
579+
];
580+
})
581+
);
582+
```
583+
584+
```ts
585+
import { load } from '@dojo/framework/stores/middleware/localStorage';
586+
import { Store } from '@dojo/framework/stores/Store';
559587

560-
// use the callback decorator as normal
561-
const myProcess = createProcess('my-process', [ commandOne ], myCallbackDecorator());
588+
const store = new Store();
589+
load('my-process', store);
562590
```
563591

564592
<!-- doc-viewer-config

src/stores/middleware/localStorage.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import global from '../../shim/global';
2+
import { ProcessError, ProcessResult, ProcessCallback, processExecutor } from '../process';
3+
import { Store } from '../Store';
4+
import { GetPaths } from '../StoreInjector';
5+
import { add } from '../state/operations';
6+
7+
export function collector<T = any>(id: string, getPaths: GetPaths<T>, callback?: ProcessCallback): ProcessCallback {
8+
return (error: ProcessError | null, result: ProcessResult): void => {
9+
const paths = getPaths(result.store.path);
10+
const data = paths.map((path) => {
11+
const state = result.get(path);
12+
return { meta: { path: path.path }, state };
13+
});
14+
global.localStorage.setItem(id, JSON.stringify(data));
15+
callback && callback(error, result);
16+
};
17+
}
18+
19+
export function load<T>(id: string, store: Store<T>) {
20+
let data = global.localStorage.getItem(id);
21+
if (data) {
22+
try {
23+
const parsedData: any[] = JSON.parse(data);
24+
const operations = parsedData.map((item) => {
25+
return add(store.path(item.meta.path), item.state);
26+
});
27+
processExecutor('local-storage-load', [() => operations], store, undefined, undefined)({});
28+
} catch {
29+
// do nothing?
30+
}
31+
}
32+
}

tests/stores/unit/all.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import './middleware/HistoryManager';
2+
import './middleware/localStorage';
23
import './Store';
34
import './process';
45
import './state/all';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import global from '../../../../src/shim/global';
2+
const { describe, it, beforeEach, before } = intern.getInterface('bdd');
3+
const { assert } = intern.getPlugin('chai');
4+
5+
import { collector, load } from './../../../../src/stores/middleware/localStorage';
6+
import { Store } from './../../../../src/stores/Store';
7+
import { CommandRequest, createProcess } from '../../../../src/stores/process';
8+
import { PatchOperation, OperationType } from '../../../../src/stores/state/Patch';
9+
import { Pointer } from '../../../../src/stores/state/Pointer';
10+
11+
function incrementCounter({ get, path }: CommandRequest<{ counter: number }>): PatchOperation[] {
12+
let counter = get(path('counter')) || 0;
13+
return [{ op: OperationType.REPLACE, path: new Pointer('/counter'), value: ++counter }];
14+
}
15+
16+
const LOCAL_STORAGE_TEST_ID = 'local-storage-id';
17+
18+
if (!global.localStorage) {
19+
global.localStorage = {
20+
storage: {},
21+
getItem(this: any, key: string) {
22+
return this.storage[key];
23+
},
24+
setItem(this: any, key: string, item: string) {
25+
this.storage[key] = item;
26+
},
27+
removeItem(this: any, key: string) {
28+
delete this.storage[key];
29+
}
30+
};
31+
}
32+
33+
let store: Store;
34+
35+
describe('middleware - local storage', (suite) => {
36+
before(() => {
37+
try {
38+
global.localStorage.setItem('test', '');
39+
} catch {
40+
suite.skip('Local Storage is not accessible on private mode pre version 11.');
41+
}
42+
});
43+
44+
beforeEach(() => {
45+
global.localStorage.removeItem(LOCAL_STORAGE_TEST_ID);
46+
store = new Store();
47+
});
48+
49+
it('Should save state to local storage', () => {
50+
const incrementCounterProcess = createProcess(
51+
'increment',
52+
[incrementCounter],
53+
collector(LOCAL_STORAGE_TEST_ID, (path) => [path('counter')])
54+
);
55+
incrementCounterProcess(store)({});
56+
assert.deepEqual(
57+
global.localStorage.getItem(LOCAL_STORAGE_TEST_ID),
58+
'[{"meta":{"path":"/counter"},"state":1}]'
59+
);
60+
});
61+
62+
it('Should call next middleware', () => {
63+
let composedMiddlewareCalled = false;
64+
const incrementCounterProcess = createProcess(
65+
'increment',
66+
[incrementCounter],
67+
collector(
68+
LOCAL_STORAGE_TEST_ID,
69+
(path) => [path('counter')],
70+
(error, result) => {
71+
composedMiddlewareCalled = true;
72+
}
73+
)
74+
);
75+
incrementCounterProcess(store)({});
76+
assert.isTrue(composedMiddlewareCalled);
77+
});
78+
79+
it('should load from local storage', () => {
80+
global.localStorage.setItem(LOCAL_STORAGE_TEST_ID, '[{"meta":{"path":"/counter"},"state":1}]');
81+
load(LOCAL_STORAGE_TEST_ID, store);
82+
assert.deepEqual((store as any)._state, { counter: 1 });
83+
});
84+
85+
it('should not load anything or throw an error if data does exist', () => {
86+
global.localStorage.setItem('other-storage-id', '[{"meta":{"path":"/counter"},"state":1}]');
87+
load(LOCAL_STORAGE_TEST_ID, store);
88+
assert.deepEqual((store as any)._state, {});
89+
});
90+
});

0 commit comments

Comments
 (0)