From f10723227576f8a6e9eede5d4cab615f240407fc Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Fri, 4 Jan 2019 17:30:54 -0500 Subject: [PATCH 01/13] WIP prototype of immutablejs state impl --- intern.json | 3 +- package-lock.json | 5 ++ package.json | 1 + src/stores/Store.ts | 99 +++++++++++++++------ src/stores/state/ImmutableState.ts | 136 +++++++++++++++++++++++++++++ tests/stores/unit/process.ts | 103 ++++++++++++++++++++++ 6 files changed, 317 insertions(+), 30 deletions(-) create mode 100644 src/stores/state/ImmutableState.ts diff --git a/intern.json b/intern.json index 9b53948a7..805bae306 100644 --- a/intern.json +++ b/intern.json @@ -84,5 +84,6 @@ "!./dist/dev/tests/widget-core/unit/meta/WebAnimation.js", "!./dist/dev/tests/widget-core/unit/meta/Intersection.js" ] - } + }, + "benchmark": true } diff --git a/package-lock.json b/package-lock.json index 09e361fd4..7bb7a900b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3511,6 +3511,11 @@ "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", "dev": true }, + "immutable": { + "version": "4.0.0-rc.12", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0-rc.12.tgz", + "integrity": "sha512-0M2XxkZLx/mi3t8NVwIm1g8nHoEmM9p9UBl/G9k4+hm0kBgOVdMV/B3CY5dQ8qG8qc80NN4gDV4HQv6FTJ5q7A==" + }, "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", diff --git a/package.json b/package.json index 638b386bb..ffa3cdddb 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "css-select-umd": "1.3.0-rc0", "diff": "3.5.0", "globalize": "1.4.0", + "immutable": "^4.0.0-rc.12", "intersection-observer": "0.4.2", "pepjs": "0.4.2", "resize-observer-polyfill": "1.5.0", diff --git a/src/stores/Store.ts b/src/stores/Store.ts index e2162c28e..c60f68a17 100644 --- a/src/stores/Store.ts +++ b/src/stores/Store.ts @@ -128,19 +128,19 @@ function isString(segment?: string): segment is string { return typeof segment === 'string'; } -/** - * Application state store - */ -export class Store extends Evented implements State { +export interface MutableState extends State { + /** + * Applies store operations to state and returns the undo operations + */ + apply(operations: PatchOperation[]): PatchOperation[]; +} + +export class DefaultState implements MutableState { /** * The private state object */ private _state = {} as T; - private _changePaths = new Map(); - - private _callbackId = 0; - /** * Returns the state at a specific pointer path location. */ @@ -151,13 +151,10 @@ export class Store extends Evented implements State { /** * Applies store operations to state and returns the undo operations */ - public apply = (operations: PatchOperation[], invalidate: boolean = false): PatchOperation[] => { + public apply = (operations: PatchOperation[]): PatchOperation[] => { const patch = new Patch(operations); const patchResult = patch.apply(this._state); this._state = patchResult.object; - if (invalidate) { - this.invalidate(); - } return patchResult.undoOperations; }; @@ -172,6 +169,66 @@ export class Store extends Evented implements State { }; }; + public path: State['path'] = (path: string | Path, ...segments: (string | undefined)[]) => { + if (typeof path === 'string') { + segments = [path, ...segments]; + } else { + segments = [...new Pointer(path.path).segments, ...segments]; + } + + const stringSegments = segments.filter(isString); + const hasMultipleSegments = stringSegments.length > 1; + const pointer = new Pointer(hasMultipleSegments ? stringSegments : stringSegments[0] || ''); + + return { + path: pointer.path, + state: this._state, + value: pointer.get(this._state) + }; + }; +} + +/** + * Application state store + */ +export class Store extends Evented implements MutableState { + private _state: MutableState = new DefaultState(); + + private _changePaths = new Map(); + + private _callbackId = 0; + + /** + * Returns the state at a specific pointer path location. + */ + public get = (path: Path): U => { + return this._state.get(path); + }; + + constructor(options?: { state?: MutableState }) { + super(); + if (options && options.state) { + this._state = options.state; + } + } + + /** + * Applies store operations to state and returns the undo operations + */ + public apply = (operations: PatchOperation[], invalidate: boolean = false): PatchOperation[] => { + const result = this._state.apply(operations); + + if (invalidate) { + this.invalidate(); + } + + return result; + }; + + public at = (path: Path>, index: number): Path => { + return this._state.at(path, index); + }; + public onChange = (paths: Path | Path[], callback: () => void) => { const callbackId = this._callbackId; if (!Array.isArray(paths)) { @@ -228,23 +285,7 @@ export class Store extends Evented implements State { this.emit({ type: 'invalidate' }); } - public path: State['path'] = (path: string | Path, ...segments: (string | undefined)[]) => { - if (typeof path === 'string') { - segments = [path, ...segments]; - } else { - segments = [...new Pointer(path.path).segments, ...segments]; - } - - const stringSegments = segments.filter(isString); - const hasMultipleSegments = stringSegments.length > 1; - const pointer = new Pointer(hasMultipleSegments ? stringSegments : stringSegments[0] || ''); - - return { - path: pointer.path, - state: this._state, - value: pointer.get(this._state) - }; - }; + public path: State['path'] = this._state.path.bind(this._state); } export default Store; diff --git a/src/stores/state/ImmutableState.ts b/src/stores/state/ImmutableState.ts new file mode 100644 index 000000000..5e91958d1 --- /dev/null +++ b/src/stores/state/ImmutableState.ts @@ -0,0 +1,136 @@ +import { + PatchOperation, + OperationType, + RemovePatchOperation, + ReplacePatchOperation, + TestPatchOperation +} from './Patch'; +import { Pointer } from './Pointer'; +import { MutableState, Path, State } from '../Store'; +import { Map, List } from 'immutable'; + +function isString(segment?: string): segment is string { + return typeof segment === 'string'; +} + +function inverse(operation: PatchOperation, state: Map): PatchOperation[] { + if (operation.op === OperationType.ADD) { + const op: RemovePatchOperation = { + op: OperationType.REMOVE, + path: operation.path + }; + const test: TestPatchOperation = { + op: OperationType.TEST, + path: operation.path, + value: operation.value + }; + return [test, op]; + } else if (operation.op === OperationType.REPLACE) { + const value = state.getIn(operation.path.segments); + let op: RemovePatchOperation | ReplacePatchOperation; + if (value === undefined) { + op = { + op: OperationType.REMOVE, + path: operation.path + }; + } else { + op = { + op: OperationType.REPLACE, + path: operation.path, + value: state.getIn(operation.path.segments) + }; + } + const test: TestPatchOperation = { + op: OperationType.TEST, + path: operation.path, + value: operation.value + }; + return [test, op]; + } else { + return [ + { + op: OperationType.ADD, + path: operation.path, + value: state.getIn(operation.path.segments) + } + ]; + } +} + +export class ImmutableState implements MutableState { + private _state = Map(); + + /** + * Returns the state at a specific pointer path location. + */ + public get = (path: Path): U => { + return path.value; + }; + + public at = (path: Path>, index: number): Path => { + const array = this.get(path); + const value = array && array[index]; + + return { + path: `${path.path}/${index}`, + state: path.state, + value + }; + }; + + public path: State['path'] = (path: string | Path, ...segments: (string | undefined)[]) => { + if (typeof path === 'string') { + segments = [path, ...segments]; + } else { + segments = [...new Pointer(path.path).segments, ...segments]; + } + + const stringSegments = segments.filter(isString); + const hasMultipleSegments = stringSegments.length > 1; + const pointer = new Pointer(hasMultipleSegments ? stringSegments : stringSegments[0] || ''); + + return { + path: pointer.path, + state: this._state as any, + value: pointer.get(this._state) + }; + }; + + public apply(operations: PatchOperation[]): PatchOperation[] { + let undoOperations: PatchOperation[] = []; + + const newState = operations.reduce((state, next) => { + switch (next.op) { + case OperationType.ADD: + const segments = next.path.segments.slice(); + const lastSegment = segments.pop(); + const parent = state.getIn(segments); + if (parent instanceof List) { + state = state.setIn(segments, parent.insert(lastSegment, next.value)); + } else { + state = state.setIn([...segments, lastSegment], next.value); + } + break; + case OperationType.REPLACE: + state = state.setIn(next.path.segments, next.value); + break; + case OperationType.REMOVE: + state = state.removeIn(next.path.segments); + break; + case OperationType.TEST: + const current = state.getIn(next.path.segments); + if (current === next.value || (current && current.equals && current.equals(next.value))) { + const location = next.path.path; + throw new Error(`Test operation failure at "${location}".`); + } + return state; + default: + throw new Error('Unknown operation'); + } + undoOperations = [...inverse(next, state), ...undoOperations]; + return state; + }, this._state); + this._state = newState; + return undoOperations; + } +} diff --git a/tests/stores/unit/process.ts b/tests/stores/unit/process.ts index 651186db2..361f565e4 100644 --- a/tests/stores/unit/process.ts +++ b/tests/stores/unit/process.ts @@ -1,5 +1,7 @@ const { beforeEach, describe, it } = intern.getInterface('bdd'); const { assert } = intern.getPlugin('chai'); +const { registerSuite } = intern.getPlugin('interface.benchmark'); +// import Test from 'intern/lib/Test'; import { uuid } from '../../../src/core/util'; import { Pointer } from './../../../src/stores/state/Pointer'; @@ -786,3 +788,104 @@ describe('process', () => { return processExecutor({}); }); }); + +const performanceTestStore = new Store(); +const operations: PatchOperation[] = []; +for (let i = 0; i < 100; i++) { + operations.push({ op: OperationType.ADD, path: new Pointer(`/${i}`), value: {} }); + for (let j = 0; j < 50; j++) { + operations.push({ op: OperationType.ADD, path: new Pointer(`/${i}/${j}`), value: {} }); + for (let k = 0; k < 10; k++) { + operations.push({ op: OperationType.ADD, path: new Pointer(`/${i}/${j}/${k}`), value: k }); + } + } +} +console.time('buildstore'); +performanceTestStore.apply(operations); +console.timeEnd('buildstore'); +registerSuite('Normal performance', { + 'update values'() { + const process = createProcess('test', [testCommandFactory('foo'), testCommandFactory('foo/bar')]); + const processExecutor = process(performanceTestStore); + processExecutor({}); + } +}); + +// registerSuite('Proxy performance', { +// beforeEach() { +// store = new Store(); +// }, +// +// tests: { +// 'update values'() { +// const process = createProcess('test', [ +// testProxyCommandFactory('foo'), +// testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo', 'bar'), +// // testProxyCommandFactory('foo', 'bar'), +// ]); +// const processExecutor = process(store); +// processExecutor({}); +// } +// } +// }); From 055ddf2a98a01e523aceb914707f6ac84a47f943 Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Mon, 21 Jan 2019 09:08:12 -0500 Subject: [PATCH 02/13] All tests passing --- intern.json | 7 +- src/stores/Store.ts | 6 +- src/stores/state/ImmutableState.ts | 81 ++++++++++++++++---- tests/stores/unit/middleware/localStorage.ts | 4 +- tests/stores/unit/process.ts | 50 ++++++------ 5 files changed, 106 insertions(+), 42 deletions(-) diff --git a/intern.json b/intern.json index 805bae306..167a1488e 100644 --- a/intern.json +++ b/intern.json @@ -46,7 +46,12 @@ "shimPath": "./dist/dev/src/shim/util/amd.js", "packages": [ { "name": "src", "location": "dist/dev/src" }, - { "name": "tests", "location": "dist/dev/tests" } + { "name": "tests", "location": "dist/dev/tests" }, + { + "name": "immutable", + "location": "./node_modules/immutable/dist", + "main": "immutable.js" + } ], "map": { "*": { diff --git a/src/stores/Store.ts b/src/stores/Store.ts index c60f68a17..2cd79a1f0 100644 --- a/src/stores/Store.ts +++ b/src/stores/Store.ts @@ -209,6 +209,7 @@ export class Store extends Evented implements MutableState { super(); if (options && options.state) { this._state = options.state; + this.path = this._state.path.bind(this._state); } } @@ -263,7 +264,10 @@ export class Store extends Evented implements MutableState { const callbackIdsCalled: number[] = []; this._changePaths.forEach((value: OnChangeValue, path: string) => { const { previousValue, callbacks } = value; - const newValue = new Pointer(path).get(this._state); + const pointer = new Pointer(path); + const newValue = pointer.segments.length + ? this._state.path(pointer.segments[0] as keyof T, ...pointer.segments.slice(1)).value + : undefined; if (previousValue !== newValue) { this._changePaths.set(path, { callbacks, previousValue: newValue }); callbacks.forEach((callbackItem) => { diff --git a/src/stores/state/ImmutableState.ts b/src/stores/state/ImmutableState.ts index 5e91958d1..0fb9f5dee 100644 --- a/src/stores/state/ImmutableState.ts +++ b/src/stores/state/ImmutableState.ts @@ -88,38 +88,37 @@ export class ImmutableState implements MutableState { const stringSegments = segments.filter(isString); const hasMultipleSegments = stringSegments.length > 1; const pointer = new Pointer(hasMultipleSegments ? stringSegments : stringSegments[0] || ''); + let value = this._state.getIn(pointer.segments); + + if (value instanceof List || value instanceof Map) { + value = value.toJS(); + } return { path: pointer.path, state: this._state as any, - value: pointer.get(this._state) + value }; }; public apply(operations: PatchOperation[]): PatchOperation[] { let undoOperations: PatchOperation[] = []; - const newState = operations.reduce((state, next) => { + const patchedState = operations.reduce((state, next) => { + let patchedState; switch (next.op) { case OperationType.ADD: - const segments = next.path.segments.slice(); - const lastSegment = segments.pop(); - const parent = state.getIn(segments); - if (parent instanceof List) { - state = state.setIn(segments, parent.insert(lastSegment, next.value)); - } else { - state = state.setIn([...segments, lastSegment], next.value); - } + patchedState = this.setIn(next.path.segments, next.value, state, true); break; case OperationType.REPLACE: - state = state.setIn(next.path.segments, next.value); + patchedState = this.setIn(next.path.segments, next.value, state); break; case OperationType.REMOVE: - state = state.removeIn(next.path.segments); + patchedState = state.removeIn(next.path.segments); break; case OperationType.TEST: const current = state.getIn(next.path.segments); - if (current === next.value || (current && current.equals && current.equals(next.value))) { + if (!current === next.value && !(current && current.equals && current.equals(next.value))) { const location = next.path.path; throw new Error(`Test operation failure at "${location}".`); } @@ -128,9 +127,61 @@ export class ImmutableState implements MutableState { throw new Error('Unknown operation'); } undoOperations = [...inverse(next, state), ...undoOperations]; - return state; + return patchedState; }, this._state); - this._state = newState; + this._state = patchedState; return undoOperations; } + + private setIn(segments: string[], value: any, state: Map, add = false) { + const updated = this.set(segments, value, state, add); + if (updated) { + return updated; + } + + state = state.withMutations((map) => { + segments.slice(0, segments.length - 1).forEach((segment, index) => { + let nextSegment = ''; + if (index + 1 < segments.length) { + nextSegment = segments[index + 1]; + } + const value = state.getIn([...segments.slice(0, index), segment]); + if (!value || !(value instanceof List || value instanceof Map)) { + if (!isNaN(parseInt(nextSegment, 10))) { + map = map.setIn([...segments.slice(0, index), segment], List()); + } else { + map = map.setIn([...segments.slice(0, index), segment], Map()); + } + } + }); + }); + + return this.set(segments, value, state, add) || state; + } + + private set(segments: string[], value: any, state: Map, add = false) { + if (typeof value === 'object' && value != null) { + if (Array.isArray(value)) { + value = List(value); + } else { + value = Map(value); + } + } + segments = segments.slice(); + const allSegments = segments.slice(); + const lastSegment = segments.pop(); + const parent = state.getIn(segments); + + if (parent instanceof List && add) { + state = state.setIn(segments, parent.insert(lastSegment, value)); + + return state; + } else if (parent instanceof Map || parent instanceof List) { + state = state.setIn(allSegments, value); + + return state; + } + + return false; + } } diff --git a/tests/stores/unit/middleware/localStorage.ts b/tests/stores/unit/middleware/localStorage.ts index 947d45135..a46a3e0a6 100644 --- a/tests/stores/unit/middleware/localStorage.ts +++ b/tests/stores/unit/middleware/localStorage.ts @@ -62,12 +62,12 @@ describe('middleware - local storage', (suite) => { it('should load from local storage', () => { global.localStorage.setItem(LOCAL_STORAGE_TEST_ID, '[{"meta":{"path":"/counter"},"state":1}]'); load(LOCAL_STORAGE_TEST_ID, store); - assert.deepEqual((store as any)._state, { counter: 1 }); + assert.deepEqual((store as any)._state._state, { counter: 1 }); }); it('should not load anything or throw an error if data does exist', () => { global.localStorage.setItem('other-storage-id', '[{"meta":{"path":"/counter"},"state":1}]'); load(LOCAL_STORAGE_TEST_ID, store); - assert.deepEqual((store as any)._state, {}); + assert.deepEqual((store as any)._state._state, {}); }); }); diff --git a/tests/stores/unit/process.ts b/tests/stores/unit/process.ts index 361f565e4..dce1f915f 100644 --- a/tests/stores/unit/process.ts +++ b/tests/stores/unit/process.ts @@ -1,6 +1,8 @@ +import { ImmutableState } from '../../../src/stores/state/ImmutableState'; + const { beforeEach, describe, it } = intern.getInterface('bdd'); const { assert } = intern.getPlugin('chai'); -const { registerSuite } = intern.getPlugin('interface.benchmark'); +// const { registerSuite } = intern.getPlugin('interface.benchmark'); // import Test from 'intern/lib/Test'; import { uuid } from '../../../src/core/util'; @@ -159,7 +161,7 @@ async function assertProxyError(test: () => void) { describe('process', () => { beforeEach(() => { - store = new Store(); + store = new Store({ state: new ImmutableState() }); promises = []; promiseResolvers = []; }); @@ -789,27 +791,29 @@ describe('process', () => { }); }); -const performanceTestStore = new Store(); -const operations: PatchOperation[] = []; -for (let i = 0; i < 100; i++) { - operations.push({ op: OperationType.ADD, path: new Pointer(`/${i}`), value: {} }); - for (let j = 0; j < 50; j++) { - operations.push({ op: OperationType.ADD, path: new Pointer(`/${i}/${j}`), value: {} }); - for (let k = 0; k < 10; k++) { - operations.push({ op: OperationType.ADD, path: new Pointer(`/${i}/${j}/${k}`), value: k }); - } - } -} -console.time('buildstore'); -performanceTestStore.apply(operations); -console.timeEnd('buildstore'); -registerSuite('Normal performance', { - 'update values'() { - const process = createProcess('test', [testCommandFactory('foo'), testCommandFactory('foo/bar')]); - const processExecutor = process(performanceTestStore); - processExecutor({}); - } -}); +// const performanceTestStore = new Store({ +// state: new ImmutableState() +// }); +// const operations: PatchOperation[] = []; +// for (let i = 0; i < 100; i++) { +// operations.push({ op: OperationType.ADD, path: new Pointer(`/${i}`), value: {} }); +// for (let j = 0; j < 50; j++) { +// operations.push({ op: OperationType.ADD, path: new Pointer(`/${i}/${j}`), value: {} }); +// for (let k = 0; k < 10; k++) { +// operations.push({ op: OperationType.ADD, path: new Pointer(`/${i}/${j}/${k}`), value: k }); +// } +// } +// } +// console.time('buildstore'); +// performanceTestStore.apply(operations); +// console.timeEnd('buildstore'); +// registerSuite('Normal performance', { +// 'update values'() { +// const process = createProcess('test', [testCommandFactory('foo'), testCommandFactory('foo/bar')]); +// const processExecutor = process(performanceTestStore); +// processExecutor({}); +// } +// }); // registerSuite('Proxy performance', { // beforeEach() { From 23a0e872fa4a80e609451879f198cdb3c8f3b7d8 Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Tue, 5 Feb 2019 13:26:58 -0500 Subject: [PATCH 03/13] Add immutable state tests --- tests/stores/unit/state/ImmutableState.ts | 226 ++++++++++++++++++++++ tests/stores/unit/state/all.ts | 1 + 2 files changed, 227 insertions(+) create mode 100644 tests/stores/unit/state/ImmutableState.ts diff --git a/tests/stores/unit/state/ImmutableState.ts b/tests/stores/unit/state/ImmutableState.ts new file mode 100644 index 000000000..c6defcb97 --- /dev/null +++ b/tests/stores/unit/state/ImmutableState.ts @@ -0,0 +1,226 @@ +const { describe, it } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); + +import { ImmutableState } from '../../../../src/stores/state/ImmutableState'; +import { Pointer } from '../../../../src/stores/state/Pointer'; +import { OperationType } from '../../../../src/stores/state/Patch'; +import * as ops from './../../../../src/stores/state/operations'; + +describe('state/Patch', () => { + describe('add', () => { + it('value to new path', () => { + const state = new ImmutableState(); + const result = state.apply([ops.add({ path: 'test', state: null, value: null }, 'test')]); + assert.equal(state.path('/test').value, 'test'); + assert.deepEqual(result, [ + { op: OperationType.TEST, path: new Pointer('/test'), value: 'test' }, + { op: OperationType.REMOVE, path: new Pointer('/test') } + ]); + }); + + it('value to new nested path', () => { + const state = new ImmutableState(); + const result = state.apply([ops.add({ path: '/foo/bar/qux', state: null, value: null }, 'test')]); + assert.deepEqual(state.path('/foo').value, { bar: { qux: 'test' } }); + assert.deepEqual(result, [ + { op: OperationType.TEST, path: new Pointer('/foo/bar/qux'), value: 'test' }, + { op: OperationType.REMOVE, path: new Pointer('/foo/bar/qux') } + ]); + }); + + it('value to existing path', () => { + const state = new ImmutableState(); + state.apply([ops.add({ path: '/test', state: null, value: null }, true)]); + const result = state.apply([ops.add({ path: '/test', state: null, value: null }, 'test')]); + assert.deepEqual(state.path('/test').value, 'test'); + assert.deepEqual(result, [ + { op: OperationType.TEST, path: new Pointer('/test'), value: 'test' }, + { op: OperationType.REMOVE, path: new Pointer('/test') } + ]); + }); + + it('value to array index path', () => { + const state = new ImmutableState(); + const result = state.apply([ops.add({ path: '/test/0', state: null, value: null }, 'test')]); + assert.deepEqual(state.path('/test').value, ['test']); + assert.deepEqual(result, [ + { op: OperationType.TEST, path: new Pointer('/test/0'), value: 'test' }, + { op: OperationType.REMOVE, path: new Pointer('/test/0') } + ]); + }); + }); + + describe('replace', () => { + it('new path', () => { + const state = new ImmutableState(); + const result = state.apply([ops.replace({ path: '/test', state: null, value: null }, 'test')]); + assert.deepEqual(state.path('/test').value, 'test'); + assert.deepEqual(result, [ + { op: OperationType.TEST, path: new Pointer('/test'), value: 'test' }, + { op: OperationType.REMOVE, path: new Pointer('/test') } + ]); + }); + + it('value to new nested path', () => { + const state = new ImmutableState(); + const result = state.apply([ops.replace({ path: '/foo/bar/qux', state: null, value: null }, 'test')]); + assert.deepEqual(state.path('/foo').value, { bar: { qux: 'test' } }); + assert.deepEqual(result, [ + { op: OperationType.TEST, path: new Pointer('/foo/bar/qux'), value: 'test' }, + { op: OperationType.REMOVE, path: new Pointer('/foo/bar/qux') } + ]); + }); + + it('existing path', () => { + const state = new ImmutableState(); + state.apply([ops.add({ path: '/test', state: null, value: null }, true)]); + const result = state.apply([ops.replace({ path: '/test', state: null, value: null }, 'test')]); + assert.deepEqual(state.path('/test').value, 'test'); + assert.deepEqual(result, [ + { op: OperationType.TEST, path: new Pointer('/test'), value: 'test' }, + { op: OperationType.REPLACE, path: new Pointer('/test'), value: true } + ]); + }); + + it('array index path', () => { + const state = new ImmutableState(); + state.apply([ops.add({ path: '/test', value: null, state: null }, ['test', 'foo'])]); + const result = state.apply([ops.replace({ path: '/test/1', state: null, value: null }, 'test')]); + assert.deepEqual(state.path('/test').value, ['test', 'test']); + assert.deepEqual(result, [ + { op: OperationType.TEST, path: new Pointer('/test/1'), value: 'test' }, + { op: OperationType.REPLACE, path: new Pointer('/test/1'), value: 'foo' } + ]); + }); + }); + + describe('remove', () => { + it('new path', () => { + const state = new ImmutableState(); + state.apply([ops.add({ path: '/other', state: null, value: null }, true)]); + const result = state.apply([ops.remove({ path: '/test', state: null, value: null })]); + assert.deepEqual(state.path('/other').value, true); + assert.deepEqual(result, [{ op: OperationType.ADD, path: new Pointer('/test'), value: undefined }]); + }); + + it('existing path', () => { + const state = new ImmutableState(); + state.apply([ops.add({ path: '/test', state: null, value: null }, true)]); + const result = state.apply([ops.remove({ path: '/test', state: null, value: null })]); + assert.deepEqual(state.path('/test'), undefined); + assert.deepEqual(result, [{ op: OperationType.ADD, path: new Pointer('/test'), value: true }]); + }); + + it('array index path', () => { + const state = new ImmutableState(); + state.apply([ops.add({ path: '/test', state: null, value: null }, ['test', 'foo'])]); + const result = state.apply([ops.remove({ path: '/test/1', state: null, value: null })]); + assert.deepEqual(state.path('test').value, ['test']); + assert.deepEqual(result, [{ op: OperationType.ADD, path: new Pointer('/test/1'), value: 'foo' }]); + }); + }); + + describe('test', () => { + it('success', () => { + const state = new ImmutableState(); + state.apply([ops.add({ path: '/test', state: null, value: null }, 'test')]); + const result = state.apply([ops.test({ path: '/test', state: null, value: null }, 'test')]); + assert.deepEqual(result, []); + assert.equal(state.path('test').value, 'test'); + }); + + function assertTestFailure(initial: any, value: any, errorMessage: string) { + const state = new ImmutableState(); + state.apply([ops.add({ path: '/test', state: null, value: null }, value)]); + assert.throws( + () => { + state.apply([ops.test({ path: '/test', state: null, value: null }, initial)]); + }, + Error, + errorMessage + ); + } + + it('failure', () => { + assertTestFailure( + true, + 'true', + 'Test operation failure at "/test". Expected boolean "true" but got string "true" instead.' + ); + assertTestFailure( + {}, + [], + 'Test operation failure at "/test". Expected object "[object Object]" but got array "" instead.' + ); + }); + + it('nested path failure', () => { + const state = new ImmutableState(); + state.apply([ops.add({ path: '/foo/0/bar/baz', state: null, value: null }, { thing: 'one' })]); + assert.throws( + () => { + state.apply([ops.test({ path: '/foo/0/bar/baz', state: null, value: null }, { thing: 'one' })]); + }, + Error, + 'Test operation failure at "/foo/0/bar/baz". Expected "one" for object property thing but got "two" instead.' + ); + }); + + it('nested path', () => { + const state = new ImmutableState(); + state.apply([ops.add({ path: '/foo/0/bar/baz/0/qux', state: null, value: null }, true)]); + const result = state.apply([ops.test({ path: '/foo/0/bar/baz/0/qux', state: null, value: null }, true)]); + assert.deepEqual(result, []); + assert.deepEqual(state.path('/foo').value, [{ bar: { baz: [{ qux: true }] } }]); + }); + + it('complex value', () => { + const state = new ImmutableState(); + state.apply([ops.add({ path: '/foo/0/bar/baz/0/qux', state: null, value: null }, true)]); + const result = state.apply([ + ops.test({ path: '/foo', state: null, value: null }, [ + { + bar: { + baz: [ + { + qux: true + } + ] + } + } + ]) + ]); + assert.deepEqual(result, []); + assert.deepEqual(state.path('/foo').value, [ + { + bar: { + baz: [ + { + qux: true + } + ] + } + } + ]); + }); + + it('no value', () => { + const state = new ImmutableState(); + state.apply([ops.add({ path: '/test', state: null, value: null }, 'test')]); + + const result = state.apply([ops.test({ path: '/test', state: null, value: null }, 'test')]); + assert.deepEqual(result, []); + assert.equal(state.path('/test').value, 'test'); + }); + }); + + it('unknown', () => { + assert.throws( + () => { + new ImmutableState().apply([{} as any]); + }, + Error, + 'Unknown operation' + ); + }); +}); diff --git a/tests/stores/unit/state/all.ts b/tests/stores/unit/state/all.ts index 81e10d42e..4e9e4a400 100644 --- a/tests/stores/unit/state/all.ts +++ b/tests/stores/unit/state/all.ts @@ -1,4 +1,5 @@ import './compare'; +import './ImmutableState'; import './operations'; import './Patch'; import './Pointer'; From 18b0755032b38566dce0b9a9ea9bc2e4aed5b314 Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Tue, 5 Feb 2019 16:26:07 -0500 Subject: [PATCH 04/13] Fix test function and flesh out/cleanup immutableJS tests --- src/stores/README.md | 2 +- src/stores/state/ImmutableState.ts | 11 +- tests/stores/unit/process.ts | 1211 ++++++++++----------- tests/stores/unit/state/ImmutableState.ts | 6 +- 4 files changed, 572 insertions(+), 658 deletions(-) diff --git a/src/stores/README.md b/src/stores/README.md index b41bb6aa7..ec5d01c7f 100644 --- a/src/stores/README.md +++ b/src/stores/README.md @@ -220,7 +220,7 @@ and will immediately throw an error. function calculateCountsCommand = createCommand(({ state }) => { const todos = state.todos; const completedTodos = todos.filter((todo: any) => todo.completed); - + state.activeCount = todos.length - completedTodos.length; state.completedCount = completedTodos.length; }); diff --git a/src/stores/state/ImmutableState.ts b/src/stores/state/ImmutableState.ts index 0fb9f5dee..11a51b18b 100644 --- a/src/stores/state/ImmutableState.ts +++ b/src/stores/state/ImmutableState.ts @@ -8,6 +8,7 @@ import { import { Pointer } from './Pointer'; import { MutableState, Path, State } from '../Store'; import { Map, List } from 'immutable'; +import { getFriendlyDifferenceMessage, isEqual } from './compare'; function isString(segment?: string): segment is string { return typeof segment === 'string'; @@ -118,9 +119,15 @@ export class ImmutableState implements MutableState { break; case OperationType.TEST: const current = state.getIn(next.path.segments); - if (!current === next.value && !(current && current.equals && current.equals(next.value))) { + const currentValue = current && current.toJS ? current.toJS() : current; + if (!isEqual(currentValue, next.value)) { const location = next.path.path; - throw new Error(`Test operation failure at "${location}".`); + throw new Error( + `Test operation failure at "${location}". ${getFriendlyDifferenceMessage( + next.value, + currentValue + )}.` + ); } return state; default: diff --git a/tests/stores/unit/process.ts b/tests/stores/unit/process.ts index dce1f915f..2df3740fd 100644 --- a/tests/stores/unit/process.ts +++ b/tests/stores/unit/process.ts @@ -2,8 +2,6 @@ import { ImmutableState } from '../../../src/stores/state/ImmutableState'; const { beforeEach, describe, it } = intern.getInterface('bdd'); const { assert } = intern.getPlugin('chai'); -// const { registerSuite } = intern.getPlugin('interface.benchmark'); -// import Test from 'intern/lib/Test'; import { uuid } from '../../../src/core/util'; import { Pointer } from './../../../src/stores/state/Pointer'; @@ -19,7 +17,7 @@ import { ProcessCallbackAfter, createCallbackDecorator } from '../../../src/stores/process'; -import { Store } from '../../../src/stores/Store'; +import { Store, MutableState } from '../../../src/stores/Store'; import { replace, add } from '../../../src/stores/state/operations'; let store: Store; @@ -159,737 +157,646 @@ async function assertProxyError(test: () => void) { } } -describe('process', () => { - beforeEach(() => { - store = new Store({ state: new ImmutableState() }); - promises = []; - promiseResolvers = []; - }); +const tests = (stateType: string, state?: () => MutableState) => { + describe(`process - ${stateType}`, () => { + beforeEach(() => { + store = new Store({ state: state ? state() : undefined }); + promises = []; + promiseResolvers = []; + }); - it('with synchronous commands running in order', () => { - const process = createProcess('test', [[testCommandFactory('foo')], testCommandFactory('foo/bar')]); - const processExecutor = process(store); - processExecutor({}); - const foo = store.get(store.path('foo')); - const foobar = store.get(store.path('foo', 'bar')); - assert.deepEqual(foo, { bar: 'foo/bar' }); - assert.strictEqual(foobar, 'foo/bar'); - }); + it('with synchronous commands running in order', () => { + const process = createProcess('test', [[testCommandFactory('foo')], testCommandFactory('foo/bar')]); + const processExecutor = process(store); + processExecutor({}); + const foo = store.get(store.path('foo')); + const foobar = store.get(store.path('foo', 'bar')); + assert.deepEqual(foo, { bar: 'foo/bar' }); + assert.strictEqual(foobar, 'foo/bar'); + }); - it('should handle optional properties for updates', () => { - type StateType = { a?: { b?: string }; foo?: number; bar: string }; - const createCommand = createCommandFactory(); + it('should handle optional properties for updates', () => { + type StateType = { a?: { b?: string }; foo?: number; bar: string }; + const createCommand = createCommandFactory(); + + createProcess('test', [ + createCommand(({ path }) => [ + { + op: OperationType.ADD, + path: new Pointer(path('foo').path), + value: 3 + } + ]), + createCommand(({ path }) => [ + { + op: OperationType.ADD, + path: new Pointer(path('a', 'b').path), + value: 'foo' + } + ]) + ])(store)({}); + + assert.equal(store.get(store.path('a', 'b')), 'foo'); + assert.equal(store.get(store.path('foo')), 3); + }); - createProcess('test', [ - createCommand(({ path }) => [ - { - op: OperationType.ADD, - path: new Pointer(path('foo').path), - value: 3 - } - ]), - createCommand(({ path }) => [ - { - op: OperationType.ADD, - path: new Pointer(path('a', 'b').path), - value: 'foo' + it('handles commands modifying the state proxy directly', async () => { + await assertProxyError(async () => { + const process = createProcess('test', [ + [testProxyCommandFactory('foo')], + testProxyCommandFactory('foo', 'bar') + ]); + const processExecutor = process(store); + const promise = processExecutor({}); + + if (typeof Proxy !== 'undefined') { + const foo = store.get(store.path('foo')); + const foobar = store.get(store.path('foo', 'bar')); + assert.deepEqual(foo, { bar: 'foo/bar' }); + assert.strictEqual(foobar, 'foo/bar'); + } else { + await promise; } - ]) - ])(store)({}); + }); + }); - assert.equal(store.get(store.path('a', 'b')), 'foo'); - assert.equal(store.get(store.path('foo')), 3); - }); + it('Can set a proxied property to itself', async () => { + await assertProxyError(async () => { + const process = createProcess('test', [ + testSetCurrentTodo, + testSelfAssignment, + testUpdateTodoCountsCommand, + testUpdateCompletedFlagCommand + ]); + const promises = [process(store)({ label: 'label-1' }), process(store)({ label: 'label-2' })]; + if (typeof Proxy !== 'undefined') { + const todos = store.get(store.path('todos')); + const todoCount = store.get(store.path('todoCount')); + const completedCount = store.get(store.path('completedCount')); + assert.strictEqual(Object.keys(todos).length, 2); + assert.strictEqual(todoCount, 2); + assert.strictEqual(completedCount, 0); + } else { + await Promise.all(promises); + } + }); + }); - it('handles commands modifying the state proxy directly', async () => { - await assertProxyError(async () => { + it('processes wait for asynchronous commands to complete before continuing', () => { const process = createProcess('test', [ - [testProxyCommandFactory('foo')], - testProxyCommandFactory('foo', 'bar') + testCommandFactory('foo'), + testAsyncCommandFactory('bar'), + testCommandFactory('foo/bar') ]); const processExecutor = process(store); const promise = processExecutor({}); - - if (typeof Proxy !== 'undefined') { + const foo = store.get(store.path('foo')); + const bar = store.get(store.path('bar')); + assert.strictEqual(foo, 'foo'); + assert.isUndefined(bar); + promiseResolver(); + return promise.then(() => { const foo = store.get(store.path('foo')); + const bar = store.get(store.path('bar')); const foobar = store.get(store.path('foo', 'bar')); assert.deepEqual(foo, { bar: 'foo/bar' }); + assert.strictEqual(bar, 'bar'); assert.strictEqual(foobar, 'foo/bar'); - } else { - await promise; - } + }); }); - }); - it('Can set a proxied property to itself', async () => { - await assertProxyError(async () => { - const process = createProcess('test', [ - testSetCurrentTodo, - testSelfAssignment, - testUpdateTodoCountsCommand, - testUpdateCompletedFlagCommand - ]); - const promises = [process(store)({ label: 'label-1' }), process(store)({ label: 'label-2' })]; - if (typeof Proxy !== 'undefined') { - const todos = store.get(store.path('todos')); - const todoCount = store.get(store.path('todoCount')); - const completedCount = store.get(store.path('completedCount')); - assert.strictEqual(Object.keys(todos).length, 2); - assert.strictEqual(todoCount, 2); - assert.strictEqual(completedCount, 0); - } else { - await Promise.all(promises); - } - }); - }); + it('does not make updates till async processes that modify the proxy resolve', async () => { + await assertProxyError(async () => { + const process = createProcess('test', [testAsyncProxyCommandFactory('foo')]); + const processExecutor = process(store); + const promise = processExecutor({}); - it('processes wait for asynchronous commands to complete before continuing', () => { - const process = createProcess('test', [ - testCommandFactory('foo'), - testAsyncCommandFactory('bar'), - testCommandFactory('foo/bar') - ]); - const processExecutor = process(store); - const promise = processExecutor({}); - const foo = store.get(store.path('foo')); - const bar = store.get(store.path('bar')); - assert.strictEqual(foo, 'foo'); - assert.isUndefined(bar); - promiseResolver(); - return promise.then(() => { - const foo = store.get(store.path('foo')); - const bar = store.get(store.path('bar')); - const foobar = store.get(store.path('foo', 'bar')); - assert.deepEqual(foo, { bar: 'foo/bar' }); - assert.strictEqual(bar, 'bar'); - assert.strictEqual(foobar, 'foo/bar'); + let foo = store.get(store.path('foo')); + assert.isUndefined(foo); + + promiseResolver(); + await promise; + foo = store.get(store.path('foo')); + assert.equal(foo, 'foo'); + }); }); - }); - it('does not make updates till async processes that modify the proxy resolve', async () => { - await assertProxyError(async () => { - const process = createProcess('test', [testAsyncProxyCommandFactory('foo')]); - const processExecutor = process(store); - const promise = processExecutor({}); + it('waits for asynchronous commands to complete before continuing with proxy updates', async () => { + await assertProxyError(async () => { + const process = createProcess('test', [ + testProxyCommandFactory('foo'), + testAsyncProxyCommandFactory('bar'), + testProxyCommandFactory('foo', 'bar') + ]); + const processExecutor = process(store); + const promise = processExecutor({}); - let foo = store.get(store.path('foo')); - assert.isUndefined(foo); + let foo; + let bar; - promiseResolver(); - await promise; - foo = store.get(store.path('foo')); - assert.equal(foo, 'foo'); + if (typeof Proxy !== 'undefined') { + foo = store.get(store.path('foo')); + bar = store.get(store.path('bar')); + assert.strictEqual(foo, 'foo'); + assert.isUndefined(bar); + } + + promiseResolver(); + await promise; + foo = store.get(store.path('foo')); + bar = store.get(store.path('bar')); + const foobar = store.get(store.path('foo', 'bar')); + assert.deepEqual(foo, { bar: 'foo/bar' }); + assert.strictEqual(bar, 'bar'); + assert.strictEqual(foobar, 'foo/bar'); + }); + }); + + it('updates the proxy as it is being modified', async () => { + await assertProxyError(async () => { + const process = createProcess('test', [testIterativeProxyCommand]); + const processExecutor = process(store); + const promise = processExecutor({}); + + if (typeof Proxy !== 'undefined') { + const itemCount = store.get(store.path('itemCount')); + assert.equal(itemCount, 10); + const finalCount = store.get(store.path('finalCount')); + assert.equal(finalCount, 9); + const items = store.get(store.path('items')); + assert.deepEqual(items, [0, 1, 2, 3, 4, { foo: { bar: 'baz' } }, { bar: 'baz' }, 8, 9]); + const temp = store.get(store.path('temp')); + assert.isUndefined(temp); + } + + await promise; + }); }); - }); - it('waits for asynchronous commands to complete before continuing with proxy updates', async () => { - await assertProxyError(async () => { + it('support concurrent commands executed synchronously', () => { const process = createProcess('test', [ - testProxyCommandFactory('foo'), - testAsyncProxyCommandFactory('bar'), - testProxyCommandFactory('foo', 'bar') + testCommandFactory('foo'), + [testAsyncCommandFactory('bar'), testAsyncCommandFactory('baz')], + testCommandFactory('foo/bar') ]); const processExecutor = process(store); const promise = processExecutor({}); - - let foo; - let bar; - - if (typeof Proxy !== 'undefined') { - foo = store.get(store.path('foo')); - bar = store.get(store.path('bar')); - assert.strictEqual(foo, 'foo'); + promiseResolvers[0](); + return promises[0].then(() => { + const bar = store.get(store.path('bar')); + const baz = store.get(store.path('baz')); assert.isUndefined(bar); - } - - promiseResolver(); - await promise; - foo = store.get(store.path('foo')); - bar = store.get(store.path('bar')); - const foobar = store.get(store.path('foo', 'bar')); - assert.deepEqual(foo, { bar: 'foo/bar' }); - assert.strictEqual(bar, 'bar'); - assert.strictEqual(foobar, 'foo/bar'); + assert.isUndefined(baz); + promiseResolver(); + return promise.then(() => { + const bar = store.get(store.path('bar')); + const baz = store.get(store.path('baz')); + assert.strictEqual(bar, 'bar'); + assert.strictEqual(baz, 'baz'); + }); + }); }); - }); - it('updates the proxy as it is being modified', async () => { - await assertProxyError(async () => { - const process = createProcess('test', [testIterativeProxyCommand]); + it('passes the payload to each command', () => { + const process = createProcess('test', [ + testCommandFactory('foo'), + testCommandFactory('bar'), + testCommandFactory('baz') + ]); const processExecutor = process(store); - const promise = processExecutor({}); + processExecutor({ payload: 'payload' }); + const foo = store.get(store.path('foo')); + const bar = store.get(store.path('bar')); + const baz = store.get(store.path('baz')); + assert.deepEqual(foo, { payload: 'payload' }); + assert.deepEqual(bar, { payload: 'payload' }); + assert.deepEqual(baz, { payload: 'payload' }); + }); - if (typeof Proxy !== 'undefined') { - const itemCount = store.get(store.path('itemCount')); - assert.equal(itemCount, 10); - const finalCount = store.get(store.path('finalCount')); - assert.equal(finalCount, 9); - const items = store.get(store.path('items')); - assert.deepEqual(items, [0, 1, 2, 3, 4, { foo: { bar: 'baz' } }, { bar: 'baz' }, 8, 9]); - const temp = store.get(store.path('temp')); - assert.isUndefined(temp); - } + it('can use a transformer for the arguments passed to the process executor', () => { + const process = createProcess('test', [ + testCommandFactory('foo'), + testCommandFactory('bar'), + testCommandFactory('baz') + ]); + const processExecutorOne = process(store, (payload: { foo: number }) => { + return { foo: 'changed' }; + }); - await promise; - }); - }); + const processExecutorTwo = process(store); + + processExecutorTwo({ foo: '' }); + processExecutorOne({ foo: 1 }); + // processExecutorOne({ foo: '' }); // doesn't compile - it('support concurrent commands executed synchronously', () => { - const process = createProcess('test', [ - testCommandFactory('foo'), - [testAsyncCommandFactory('bar'), testAsyncCommandFactory('baz')], - testCommandFactory('foo/bar') - ]); - const processExecutor = process(store); - const promise = processExecutor({}); - promiseResolvers[0](); - return promises[0].then(() => { + const foo = store.get(store.path('foo')); const bar = store.get(store.path('bar')); const baz = store.get(store.path('baz')); - assert.isUndefined(bar); - assert.isUndefined(baz); - promiseResolver(); - return promise.then(() => { - const bar = store.get(store.path('bar')); - const baz = store.get(store.path('baz')); - assert.strictEqual(bar, 'bar'); - assert.strictEqual(baz, 'baz'); - }); + assert.deepEqual(foo, { foo: 'changed' }); + assert.deepEqual(bar, { foo: 'changed' }); + assert.deepEqual(baz, { foo: 'changed' }); }); - }); - it('passes the payload to each command', () => { - const process = createProcess('test', [ - testCommandFactory('foo'), - testCommandFactory('bar'), - testCommandFactory('baz') - ]); - const processExecutor = process(store); - processExecutor({ payload: 'payload' }); - const foo = store.get(store.path('foo')); - const bar = store.get(store.path('bar')); - const baz = store.get(store.path('baz')); - assert.deepEqual(foo, { payload: 'payload' }); - assert.deepEqual(bar, { payload: 'payload' }); - assert.deepEqual(baz, { payload: 'payload' }); - }); + it('provides a command factory', () => { + const createCommand = createCommandFactory<{ foo: string }, { foo: string }>(); - it('can use a transformer for the arguments passed to the process executor', () => { - const process = createProcess('test', [ - testCommandFactory('foo'), - testCommandFactory('bar'), - testCommandFactory('baz') - ]); - const processExecutorOne = process(store, (payload: { foo: number }) => { - return { foo: 'changed' }; + const command = createCommand(({ get, path, payload }) => { + // get(path('bar')); // shouldn't compile + payload.foo; + // payload.bar; // shouldn't compile + get(path('foo')); + return []; + }); + + assert.equal(typeof command, 'function'); }); - const processExecutorTwo = process(store); + it('should add object by integer like index key', () => { + const id = '3fe3c6d3-15e1-4d77-886f-daeb0ed63458'; + const createCommand = createCommandFactory(); + const command = createCommand(({ get, path, payload }) => { + return [add(path('test', id), { foo: 'bar' })]; + }); + const process = createProcess('test', [command]); + const executor = process(store); + executor({}); + assert.deepEqual(store.get(store.path('test')), { [id]: { foo: 'bar' } }); + }); - processExecutorTwo({ foo: '' }); - processExecutorOne({ foo: 1 }); - // processExecutorOne({ foo: '' }); // doesn't compile + it('can type payload that extends an object', () => { + const createCommandOne = createCommandFactory(); + const createCommandTwo = createCommandFactory(); + const createCommandThree = createCommandFactory(); + const commandOne = createCommandOne(({ get, path, payload }) => []); + const commandTwo = createCommandTwo(({ get, path, payload }) => []); + const commandThree = createCommandThree(({ get, path, payload }) => []); + const processOne = createProcess('test', [commandOne, commandTwo]); + // createProcess('test', [commandOne, commandTwo]); // shouldn't compile + // createProcess([commandOne]); // shouldn't compile + const processTwo = createProcess('test', [commandTwo]); + const processThree = createProcess('test', [commandThree]); + const executorOne = processOne(store); + const executorTwo = processTwo(store); + const executorThree = processThree(store); + + // executorOne({}); // shouldn't compile + // executorOne({ foo: 1 }); // shouldn't compile + executorOne({ foo: 'bar', bar: 'string' }); + executorTwo({ bar: 'bar' }); + // executorTwo({}); // shouldn't compile; + // executorTwo(1); // shouldn't compile + // executorTwo(''); // shouldn't compile + // executorThree(); // shouldn't compile + executorThree({}); + }); - const foo = store.get(store.path('foo')); - const bar = store.get(store.path('bar')); - const baz = store.get(store.path('baz')); - assert.deepEqual(foo, { foo: 'changed' }); - assert.deepEqual(bar, { foo: 'changed' }); - assert.deepEqual(baz, { foo: 'changed' }); - }); + it('if a transformer is provided it determines the payload type', () => { + const createCommandOne = createCommandFactory(); + const createCommandTwo = createCommandFactory(); + const commandOne = createCommandOne(({ get, path, payload }) => []); + const commandTwo = createCommandTwo(({ get, path, payload }) => []); + const transformerOne = (payload: { foo: string }): { bar: number } => { + return { + bar: 1 + }; + }; + const transformerTwo = (payload: { foo: number }): { bar: number; foo: number } => { + return { + bar: 1, + foo: 2 + }; + }; - it('provides a command factory', () => { - const createCommand = createCommandFactory<{ foo: string }, { foo: string }>(); + const processOne = createProcess('test', [commandOne]); + const processTwo = createProcess('test', [commandOne, commandTwo]); + const processOneResult = processOne(store, transformerOne)({ foo: '' }); + // processTwo(store, transformerOne); // compile error + const processTwoResult = processTwo(store, transformerTwo)({ foo: 3 }); + // processTwo(store)({ foo: 3 }); // compile error + processOneResult.then((result) => { + result.payload.bar.toPrecision(); + result.executor(processTwo, { foo: 3, bar: 1 }); + // result.executor(processTwo, { foo: 3, bar: '' }); // compile error + result.executor(processTwo, { foo: 1 }, transformerTwo); + // result.executor(processTwo, { foo: '' }, transformerTwo); // compile error + // result.payload.bar.toUpperCase(); // compile error + // result.payload.foo; // compile error + }); + processTwoResult.then((result) => { + result.payload.bar.toPrecision(); + result.payload.foo.toPrecision(); + // result.payload.bar.toUpperCase(); // compile error + // result.payload.foo.toUpperCase(); // compile error + }); + }); - const command = createCommand(({ get, path, payload }) => { - // get(path('bar')); // shouldn't compile - payload.foo; - // payload.bar; // shouldn't compile - get(path('foo')); - return []; + it('can provide a callback that gets called on process completion', () => { + let callbackCalled = false; + const process = createProcess('test', [testCommandFactory('foo')], () => ({ + after: () => { + callbackCalled = true; + } + })); + const processExecutor = process(store); + processExecutor({}); + assert.isTrue(callbackCalled); }); - assert.equal(typeof command, 'function'); - }); + it('when a command errors, the error and command is returned in the error argument of the callback', async () => { + const process = createProcess('test', [testCommandFactory('foo'), testErrorCommand], () => ({ + after: (error) => { + assert.isNotNull(error); + assert.equal(error && error.error && error.error.message, 'Command Failed'); + assert.strictEqual(error && error.command, testErrorCommand); + } + })); + const processExecutor = process(store); + await processExecutor({}); + }); - it('should add object by integer like index key', () => { - const id = '3fe3c6d3-15e1-4d77-886f-daeb0ed63458'; - const createCommand = createCommandFactory(); - const command = createCommand(({ get, path, payload }) => { - return [add(path('test', id), { foo: 'bar' })]; + it('a command that does not return results does not break other commands', () => { + const process = createProcess('test', [testCommandFactory('foo'), testNoop], () => ({ + after: (error) => { + assert.isNull(error); + assert.strictEqual(store.get(store.path('foo')), 'foo'); + } + })); + const processExecutor = process(store); + processExecutor({}); }); - const process = createProcess('test', [command]); - const executor = process(store); - executor({}); - assert.deepEqual(store.get(store.path('test')), { [id]: { foo: 'bar' } }); - }); - it('can type payload that extends an object', () => { - const createCommandOne = createCommandFactory(); - const createCommandTwo = createCommandFactory(); - const createCommandThree = createCommandFactory(); - const commandOne = createCommandOne(({ get, path, payload }) => []); - const commandTwo = createCommandTwo(({ get, path, payload }) => []); - const commandThree = createCommandThree(({ get, path, payload }) => []); - const processOne = createProcess('test', [commandOne, commandTwo]); - // createProcess('test', [commandOne, commandTwo]); // shouldn't compile - // createProcess([commandOne]); // shouldn't compile - const processTwo = createProcess('test', [commandTwo]); - const processThree = createProcess('test', [commandThree]); - const executorOne = processOne(store); - const executorTwo = processTwo(store); - const executorThree = processThree(store); - - // executorOne({}); // shouldn't compile - // executorOne({ foo: 1 }); // shouldn't compile - executorOne({ foo: 'bar', bar: 'string' }); - executorTwo({ bar: 'bar' }); - // executorTwo({}); // shouldn't compile; - // executorTwo(1); // shouldn't compile - // executorTwo(''); // shouldn't compile - // executorThree(); // shouldn't compile - executorThree({}); - }); + it('executor can be used to programmatically run additional processes', () => { + const extraProcess = createProcess('test', [testCommandFactory('bar')]); + const process = createProcess('test', [testCommandFactory('foo')], () => ({ + after: (error, result) => { + assert.isNull(error); + let bar = store.get(store.path('bar')); + assert.isUndefined(bar); + result.executor(extraProcess, {}); + bar = store.get(store.path('bar')); + assert.strictEqual(bar, 'bar'); + } + })); + const processExecutor = process(store); + processExecutor({}); + }); - it('if a transformer is provided it determines the payload type', () => { - const createCommandOne = createCommandFactory(); - const createCommandTwo = createCommandFactory(); - const commandOne = createCommandOne(({ get, path, payload }) => []); - const commandTwo = createCommandTwo(({ get, path, payload }) => []); - const transformerOne = (payload: { foo: string }): { bar: number } => { - return { - bar: 1 + it('process decorator should convert callbacks to after callback', () => { + let callbackArray: string[] = []; + const legacyCallbackOne: ProcessCallbackAfter = (error, result) => { + callbackArray.push('one'); }; - }; - const transformerTwo = (payload: { foo: number }): { bar: number; foo: number } => { - return { - bar: 1, - foo: 2 + const legacyCallbackTwo: ProcessCallbackAfter = (error, result) => { + callbackArray.push('two'); }; - }; - - const processOne = createProcess('test', [commandOne]); - const processTwo = createProcess('test', [commandOne, commandTwo]); - const processOneResult = processOne(store, transformerOne)({ foo: '' }); - // processTwo(store, transformerOne); // compile error - const processTwoResult = processTwo(store, transformerTwo)({ foo: 3 }); - // processTwo(store)({ foo: 3 }); // compile error - processOneResult.then((result) => { - result.payload.bar.toPrecision(); - result.executor(processTwo, { foo: 3, bar: 1 }); - // result.executor(processTwo, { foo: 3, bar: '' }); // compile error - result.executor(processTwo, { foo: 1 }, transformerTwo); - // result.executor(processTwo, { foo: '' }, transformerTwo); // compile error - // result.payload.bar.toUpperCase(); // compile error - // result.payload.foo; // compile error - }); - processTwoResult.then((result) => { - result.payload.bar.toPrecision(); - result.payload.foo.toPrecision(); - // result.payload.bar.toUpperCase(); // compile error - // result.payload.foo.toUpperCase(); // compile error - }); - }); - - it('can provide a callback that gets called on process completion', () => { - let callbackCalled = false; - const process = createProcess('test', [testCommandFactory('foo')], () => ({ - after: () => { - callbackCalled = true; - } - })); - const processExecutor = process(store); - processExecutor({}); - assert.isTrue(callbackCalled); - }); - it('when a command errors, the error and command is returned in the error argument of the callback', async () => { - const process = createProcess('test', [testCommandFactory('foo'), testErrorCommand], () => ({ - after: (error) => { - assert.isNotNull(error); - assert.equal(error && error.error && error.error.message, 'Command Failed'); - assert.strictEqual(error && error.command, testErrorCommand); - } - })); - const processExecutor = process(store); - await processExecutor({}); - }); + const decoratorOne = createCallbackDecorator(legacyCallbackOne); + const decoratorTwo = createCallbackDecorator(legacyCallbackTwo); - it('a command that does not return results does not break other commands', () => { - const process = createProcess('test', [testCommandFactory('foo'), testNoop], () => ({ - after: (error) => { - assert.isNull(error); - assert.strictEqual(store.get(store.path('foo')), 'foo'); - } - })); - const processExecutor = process(store); - processExecutor({}); - }); + const callbackOne: ProcessCallback = () => ({ + after: (error, result) => { + callbackArray.push('one'); + } + }); + const callbackTwo: ProcessCallback = () => ({ + after: (error, result) => { + callbackArray.push('two'); + } + }); - it('executor can be used to programmatically run additional processes', () => { - const extraProcess = createProcess('test', [testCommandFactory('bar')]); - const process = createProcess('test', [testCommandFactory('foo')], () => ({ - after: (error, result) => { - assert.isNull(error); - let bar = store.get(store.path('bar')); - assert.isUndefined(bar); - result.executor(extraProcess, {}); - bar = store.get(store.path('bar')); - assert.strictEqual(bar, 'bar'); - } - })); - const processExecutor = process(store); - processExecutor({}); - }); + const process = createProcess('decorator-test', [], [callbackOne, callbackTwo]); + const processWithLegacyMiddleware = createProcess( + 'createCallbackDecorator-test', + [], + decoratorOne(decoratorTwo()) + ); + const processExecutor = process(store); + const legacyMiddlewareProcessExecutor = processWithLegacyMiddleware(store); + processExecutor({}); + assert.deepEqual(callbackArray, ['one', 'two']); + callbackArray = []; + legacyMiddlewareProcessExecutor({}); + assert.deepEqual(callbackArray, ['one', 'two']); + }); - it('process decorator should convert callbacks to after callback', () => { - let callbackArray: string[] = []; - const legacyCallbackOne: ProcessCallbackAfter = (error, result) => { - callbackArray.push('one'); - }; - const legacyCallbackTwo: ProcessCallbackAfter = (error, result) => { - callbackArray.push('two'); - }; + it('Creating a process returned automatically decorates all process callbacks', () => { + let results: string[] = []; - const decoratorOne = createCallbackDecorator(legacyCallbackOne); - const decoratorTwo = createCallbackDecorator(legacyCallbackTwo); + const callback: ProcessCallback = () => ({ + after: (error, result): void => { + results.push('callback one'); + } + }); - const callbackOne: ProcessCallback = () => ({ - after: (error, result) => { - callbackArray.push('one'); - } - }); - const callbackTwo: ProcessCallback = () => ({ - after: (error, result) => { - callbackArray.push('two'); - } - }); + const callbackTwo = () => ({ + after: (error: ProcessError | null, result: ProcessResult): void => { + results.push('callback two'); + result.payload; + } + }); - const process = createProcess('decorator-test', [], [callbackOne, callbackTwo]); - const processWithLegacyMiddleware = createProcess( - 'createCallbackDecorator-test', - [], - decoratorOne(decoratorTwo()) - ); - const processExecutor = process(store); - const legacyMiddlewareProcessExecutor = processWithLegacyMiddleware(store); - processExecutor({}); - assert.deepEqual(callbackArray, ['one', 'two']); - callbackArray = []; - legacyMiddlewareProcessExecutor({}); - assert.deepEqual(callbackArray, ['one', 'two']); - }); + const logPointerCallback = () => ({ + after: (error: ProcessError | null, result: ProcessResult<{ logs: string[][] }>): void => { + const paths = result.operations.map((operation) => operation.path.path); + const logs = result.get(store.path('logs')) || []; - it('Creating a process returned automatically decorates all process callbacks', () => { - let results: string[] = []; + result.apply([{ op: OperationType.ADD, path: new Pointer(`/logs/${logs.length}`), value: paths }]); + } + }); - const callback: ProcessCallback = () => ({ - after: (error, result): void => { - results.push('callback one'); - } + const createProcess = createProcessFactoryWith([callback, callbackTwo, logPointerCallback]); + + const process = createProcess('test', [testCommandFactory('foo'), testCommandFactory('bar')]); + const executor = process(store); + executor({}); + assert.lengthOf(results, 2); + assert.strictEqual(results[0], 'callback one'); + assert.strictEqual(results[1], 'callback two'); + assert.deepEqual(store.get(store.path('logs')), [['/foo', '/bar']]); + executor({}); + assert.lengthOf(results, 4); + assert.strictEqual(results[0], 'callback one'); + assert.strictEqual(results[1], 'callback two'); + assert.strictEqual(results[2], 'callback one'); + assert.strictEqual(results[3], 'callback two'); + assert.deepEqual(store.get(store.path('logs')), [['/foo', '/bar'], ['/foo', '/bar']]); }); - const callbackTwo = () => ({ - after: (error: ProcessError | null, result: ProcessResult): void => { - results.push('callback two'); - result.payload; - } - }); + it('Creating a process automatically decorates all process initializers', async () => { + let initialization: string[] = []; + let firstCall = true; + + const initializer: ProcessCallback = () => ({ + before: async (payload, store) => { + initialization.push('initializer one'); + const initLog = store.get(store.path('initLogs')) || []; + store.apply([ + { + op: OperationType.ADD, + path: new Pointer(`/initLogs/${initLog.length}`), + value: 'initial value' + } + ]); + } + }); - const logPointerCallback = () => ({ - after: (error: ProcessError | null, result: ProcessResult<{ logs: string[][] }>): void => { - const paths = result.operations.map((operation) => operation.path.path); - const logs = result.get(store.path('logs')) || []; + const initializerTwo = () => ({ + before: async (payload: any) => { + initialization.push('initializer two'); + payload.foo; + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } + }); - result.apply([{ op: OperationType.ADD, path: new Pointer(`/logs/${logs.length}`), value: paths }]); - } - }); + const initializerThree = () => ({ + before: () => { + initialization.push('initializer three'); + } + }); - const createProcess = createProcessFactoryWith([callback, callbackTwo, logPointerCallback]); - - const process = createProcess('test', [testCommandFactory('foo'), testCommandFactory('bar')]); - const executor = process(store); - executor({}); - assert.lengthOf(results, 2); - assert.strictEqual(results[0], 'callback one'); - assert.strictEqual(results[1], 'callback two'); - assert.deepEqual(store.get(store.path('logs')), [['/foo', '/bar']]); - executor({}); - assert.lengthOf(results, 4); - assert.strictEqual(results[0], 'callback one'); - assert.strictEqual(results[1], 'callback two'); - assert.strictEqual(results[2], 'callback one'); - assert.strictEqual(results[3], 'callback two'); - assert.deepEqual(store.get(store.path('logs')), [['/foo', '/bar'], ['/foo', '/bar']]); - }); + const logPointerCallback = () => ({ + after: (error: ProcessError | null, result: ProcessResult<{ logs: string[][] }>): void => { + assert.lengthOf(initialization, firstCall ? 3 : 6); + firstCall = false; + const paths = result.operations.map((operation) => operation.path.path); + const logs = result.get(store.path('logs')) || []; - it('Creating a process automatically decorates all process initializers', async () => { - let initialization: string[] = []; - let firstCall = true; + result.apply([{ op: OperationType.ADD, path: new Pointer(`/logs/${logs.length}`), value: paths }]); + } + }); - const initializer: ProcessCallback = () => ({ - before: async (payload, store) => { - initialization.push('initializer one'); - const initLog = store.get(store.path('initLogs')) || []; - store.apply([ - { op: OperationType.ADD, path: new Pointer(`/initLogs/${initLog.length}`), value: 'initial value' } - ]); - } + const createProcess = createProcessFactoryWith([initializer, initializerTwo, initializerThree]); + + const process = createProcess( + 'test', + [ + (): PatchOperation[] => { + assert.lengthOf(initialization, firstCall ? 3 : 6); + return []; + }, + testCommandFactory('foo'), + testCommandFactory('bar') + ], + logPointerCallback + ); + const executor = process(store); + await executor({}); + assert.lengthOf(initialization, 3); + assert.strictEqual(initialization[0], 'initializer one'); + assert.strictEqual(initialization[1], 'initializer two'); + assert.strictEqual(initialization[2], 'initializer three'); + assert.deepEqual(store.get(store.path('logs')), [['/foo', '/bar']]); + assert.deepEqual(store.get(store.path('initLogs')), ['initial value']); + await executor({}); + assert.lengthOf(initialization, 6); + assert.strictEqual(initialization[0], 'initializer one'); + assert.strictEqual(initialization[1], 'initializer two'); + assert.strictEqual(initialization[2], 'initializer three'); + assert.strictEqual(initialization[3], 'initializer one'); + assert.strictEqual(initialization[4], 'initializer two'); + assert.strictEqual(initialization[5], 'initializer three'); + assert.deepEqual(store.get(store.path('logs')), [['/foo', '/bar'], ['/foo', '/bar']]); + assert.deepEqual(store.get(store.path('initLogs')), ['initial value', 'initial value']); }); - const initializerTwo = () => ({ - before: async (payload: any) => { - initialization.push('initializer two'); - payload.foo; - await new Promise((resolve) => { - setTimeout(resolve, 100); - }); - } - }); + it('Should work with a single initializer', async () => { + let initialization: string[] = []; - const initializerThree = () => ({ - before: () => { - initialization.push('initializer three'); - } - }); + const initializer = async () => { + initialization.push('initializer'); + }; - const logPointerCallback = () => ({ - after: (error: ProcessError | null, result: ProcessResult<{ logs: string[][] }>): void => { - assert.lengthOf(initialization, firstCall ? 3 : 6); - firstCall = false; + const logPointerCallback = ( + error: ProcessError | null, + result: ProcessResult<{ logs: string[][] }> + ): void => { const paths = result.operations.map((operation) => operation.path.path); const logs = result.get(store.path('logs')) || []; result.apply([{ op: OperationType.ADD, path: new Pointer(`/logs/${logs.length}`), value: paths }]); - } - }); + }; - const createProcess = createProcessFactoryWith([initializer, initializerTwo, initializerThree]); + const process = createProcess('test', [testCommandFactory('foo'), testCommandFactory('bar')], () => ({ + before: initializer, + after: logPointerCallback + })); + const executor = process(store); + await executor({}); + assert.lengthOf(initialization, 1); + assert.strictEqual(initialization[0], 'initializer'); + assert.deepEqual(store.get(store.path('logs')), [['/foo', '/bar']]); + await executor({}); + assert.lengthOf(initialization, 2); + assert.strictEqual(initialization[0], 'initializer'); + assert.strictEqual(initialization[1], 'initializer'); + assert.deepEqual(store.get(store.path('logs')), [['/foo', '/bar'], ['/foo', '/bar']]); + }); - const process = createProcess( - 'test', - [ - (): PatchOperation[] => { - assert.lengthOf(initialization, firstCall ? 3 : 6); - return []; - }, - testCommandFactory('foo'), - testCommandFactory('bar') - ], - logPointerCallback - ); - const executor = process(store); - await executor({}); - assert.lengthOf(initialization, 3); - assert.strictEqual(initialization[0], 'initializer one'); - assert.strictEqual(initialization[1], 'initializer two'); - assert.strictEqual(initialization[2], 'initializer three'); - assert.deepEqual(store.get(store.path('logs')), [['/foo', '/bar']]); - assert.deepEqual(store.get(store.path('initLogs')), ['initial value']); - await executor({}); - assert.lengthOf(initialization, 6); - assert.strictEqual(initialization[0], 'initializer one'); - assert.strictEqual(initialization[1], 'initializer two'); - assert.strictEqual(initialization[2], 'initializer three'); - assert.strictEqual(initialization[3], 'initializer one'); - assert.strictEqual(initialization[4], 'initializer two'); - assert.strictEqual(initialization[5], 'initializer three'); - assert.deepEqual(store.get(store.path('logs')), [['/foo', '/bar'], ['/foo', '/bar']]); - assert.deepEqual(store.get(store.path('initLogs')), ['initial value', 'initial value']); - }); + it('process can be undone using the undo function provided via the callback', () => { + const command = ({ payload }: CommandRequest): PatchOperation[] => { + return [{ op: OperationType.REPLACE, path: new Pointer('/foo'), value: 'bar' }]; + }; + const process = createProcess('foo', [testCommandFactory('foo'), command], () => ({ + after: (error, result) => { + let foo = store.get(result.store.path('foo')); + assert.strictEqual(foo, 'bar'); + store.apply(result.undoOperations); + foo = store.get(result.store.path('foo')); + assert.isUndefined(foo); + } + })); + const processExecutor = process(store); + return processExecutor({}); + }); - it('Should work with a single initializer', async () => { - let initialization: string[] = []; - - const initializer = async () => { - initialization.push('initializer'); - }; - - const logPointerCallback = (error: ProcessError | null, result: ProcessResult<{ logs: string[][] }>): void => { - const paths = result.operations.map((operation) => operation.path.path); - const logs = result.get(store.path('logs')) || []; - - result.apply([{ op: OperationType.ADD, path: new Pointer(`/logs/${logs.length}`), value: paths }]); - }; - - const process = createProcess('test', [testCommandFactory('foo'), testCommandFactory('bar')], () => ({ - before: initializer, - after: logPointerCallback - })); - const executor = process(store); - await executor({}); - assert.lengthOf(initialization, 1); - assert.strictEqual(initialization[0], 'initializer'); - assert.deepEqual(store.get(store.path('logs')), [['/foo', '/bar']]); - await executor({}); - assert.lengthOf(initialization, 2); - assert.strictEqual(initialization[0], 'initializer'); - assert.strictEqual(initialization[1], 'initializer'); - assert.deepEqual(store.get(store.path('logs')), [['/foo', '/bar'], ['/foo', '/bar']]); - }); + it('Can undo operations for commands that modify the same section of state', () => { + const commandOne = (): PatchOperation[] => { + return [{ op: OperationType.REPLACE, path: new Pointer('/state'), value: { b: 'b' } }]; + }; + const commandTwo = (): PatchOperation[] => { + return [{ op: OperationType.REPLACE, path: new Pointer('/state/a'), value: 'a' }]; + }; - it('process can be undone using the undo function provided via the callback', () => { - const command = ({ payload }: CommandRequest): PatchOperation[] => { - return [{ op: OperationType.REPLACE, path: new Pointer('/foo'), value: 'bar' }]; - }; - const process = createProcess('foo', [testCommandFactory('foo'), command], () => ({ - after: (error, result) => { - let foo = store.get(result.store.path('foo')); - assert.strictEqual(foo, 'bar'); - store.apply(result.undoOperations); - foo = store.get(result.store.path('foo')); - assert.isUndefined(foo); - } - })); - const processExecutor = process(store); - return processExecutor({}); - }); + const process = createProcess('foo', [commandOne, commandTwo], () => ({ + after: (error, result) => { + let state = store.get(result.store.path('state')); + assert.deepEqual(state, { a: 'a', b: 'b' }); + store.apply(result.undoOperations); + state = store.get(result.store.path('state')); + assert.isUndefined(state); + } + })); + const processExecutor = process(store); + return processExecutor({}); + }); - it('Can undo operations for commands that modify the same section of state', () => { - const commandOne = (): PatchOperation[] => { - return [{ op: OperationType.REPLACE, path: new Pointer('/state'), value: { b: 'b' } }]; - }; - const commandTwo = (): PatchOperation[] => { - return [{ op: OperationType.REPLACE, path: new Pointer('/state/a'), value: 'a' }]; - }; - - const process = createProcess('foo', [commandOne, commandTwo], () => ({ - after: (error, result) => { - let state = store.get(result.store.path('state')); - assert.deepEqual(state, { a: 'a', b: 'b' }); - store.apply(result.undoOperations); - state = store.get(result.store.path('state')); - assert.isUndefined(state); - } - })); - const processExecutor = process(store); - return processExecutor({}); - }); + it('Can undo operations for commands that return multiple operations', () => { + const commandFactory = createCommandFactory(); + const commandOne = commandFactory(({ path }) => { + return [ + replace(path('state'), {}), + replace(path('state', 'foo'), 'foo'), + replace(path('state', 'bar'), 'bar'), + replace(path('state', 'baz'), 'baz') + ]; + }); - it('Can undo operations for commands that return multiple operations', () => { - const commandFactory = createCommandFactory(); - const commandOne = commandFactory(({ path }) => { - return [ - replace(path('state'), {}), - replace(path('state', 'foo'), 'foo'), - replace(path('state', 'bar'), 'bar'), - replace(path('state', 'baz'), 'baz') - ]; + const process = createProcess('foo', [commandOne], () => ({ + after: (error, result) => { + let state = store.get(result.store.path('state')); + assert.deepEqual(state, { foo: 'foo', bar: 'bar', baz: 'baz' }); + store.apply(result.undoOperations); + state = store.get(result.store.path('state')); + assert.isUndefined(state); + } + })); + const processExecutor = process(store); + return processExecutor({}); }); - - const process = createProcess('foo', [commandOne], () => ({ - after: (error, result) => { - let state = store.get(result.store.path('state')); - assert.deepEqual(state, { foo: 'foo', bar: 'bar', baz: 'baz' }); - store.apply(result.undoOperations); - state = store.get(result.store.path('state')); - assert.isUndefined(state); - } - })); - const processExecutor = process(store); - return processExecutor({}); }); -}); - -// const performanceTestStore = new Store({ -// state: new ImmutableState() -// }); -// const operations: PatchOperation[] = []; -// for (let i = 0; i < 100; i++) { -// operations.push({ op: OperationType.ADD, path: new Pointer(`/${i}`), value: {} }); -// for (let j = 0; j < 50; j++) { -// operations.push({ op: OperationType.ADD, path: new Pointer(`/${i}/${j}`), value: {} }); -// for (let k = 0; k < 10; k++) { -// operations.push({ op: OperationType.ADD, path: new Pointer(`/${i}/${j}/${k}`), value: k }); -// } -// } -// } -// console.time('buildstore'); -// performanceTestStore.apply(operations); -// console.timeEnd('buildstore'); -// registerSuite('Normal performance', { -// 'update values'() { -// const process = createProcess('test', [testCommandFactory('foo'), testCommandFactory('foo/bar')]); -// const processExecutor = process(performanceTestStore); -// processExecutor({}); -// } -// }); - -// registerSuite('Proxy performance', { -// beforeEach() { -// store = new Store(); -// }, -// -// tests: { -// 'update values'() { -// const process = createProcess('test', [ -// testProxyCommandFactory('foo'), -// testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo', 'bar'), -// // testProxyCommandFactory('foo', 'bar'), -// ]); -// const processExecutor = process(store); -// processExecutor({}); -// } -// } -// }); +}; + +tests('default'); +tests('immutableJS', () => new ImmutableState()); diff --git a/tests/stores/unit/state/ImmutableState.ts b/tests/stores/unit/state/ImmutableState.ts index c6defcb97..f20e2ae86 100644 --- a/tests/stores/unit/state/ImmutableState.ts +++ b/tests/stores/unit/state/ImmutableState.ts @@ -6,7 +6,7 @@ import { Pointer } from '../../../../src/stores/state/Pointer'; import { OperationType } from '../../../../src/stores/state/Patch'; import * as ops from './../../../../src/stores/state/operations'; -describe('state/Patch', () => { +describe('state/ImmutableState', () => { describe('add', () => { it('value to new path', () => { const state = new ImmutableState(); @@ -107,7 +107,7 @@ describe('state/Patch', () => { const state = new ImmutableState(); state.apply([ops.add({ path: '/test', state: null, value: null }, true)]); const result = state.apply([ops.remove({ path: '/test', state: null, value: null })]); - assert.deepEqual(state.path('/test'), undefined); + assert.deepEqual(state.path('/test').value, undefined); assert.deepEqual(result, [{ op: OperationType.ADD, path: new Pointer('/test'), value: true }]); }); @@ -156,7 +156,7 @@ describe('state/Patch', () => { it('nested path failure', () => { const state = new ImmutableState(); - state.apply([ops.add({ path: '/foo/0/bar/baz', state: null, value: null }, { thing: 'one' })]); + state.apply([ops.add({ path: '/foo/0/bar/baz', state: null, value: null }, { thing: 'two' })]); assert.throws( () => { state.apply([ops.test({ path: '/foo/0/bar/baz', state: null, value: null }, { thing: 'one' })]); From 5bece9e4b6bcffa70ea5cb353fefaf8949383f26 Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Mon, 11 Feb 2019 18:17:16 -0500 Subject: [PATCH 05/13] Use a working version of immutable --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ffa3cdddb..c861753c1 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "css-select-umd": "1.3.0-rc0", "diff": "3.5.0", "globalize": "1.4.0", - "immutable": "^4.0.0-rc.12", + "immutable": "3.8.2", "intersection-observer": "0.4.2", "pepjs": "0.4.2", "resize-observer-polyfill": "1.5.0", From b0c7b23b27323b5c719ab62ea7c27bf6d616a8fc Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Tue, 26 Feb 2019 10:38:09 -0500 Subject: [PATCH 06/13] Make immutable optional and fixed index-like key handling --- package.json | 5 ++++- src/stores/state/ImmutableState.ts | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index c861753c1..36cf8cf4d 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "css-select-umd": "1.3.0-rc0", "diff": "3.5.0", "globalize": "1.4.0", - "immutable": "3.8.2", "intersection-observer": "0.4.2", "pepjs": "0.4.2", "resize-observer-polyfill": "1.5.0", @@ -67,6 +66,7 @@ "@types/selenium-webdriver": "^3.0.8", "@types/sinon": "~4.1.2", "benchmark": "^1.0.0", + "immutable": "3.8.2", "bootstrap": "^3.3.7", "chromedriver": "2.40.0", "cldr-data": "32.0.1", @@ -84,6 +84,9 @@ "sinon": "~4.1.3", "typescript": "~2.6.2" }, + "optionalDependencies": { + "immutable": "3.8.2" + }, "lint-staged": { "*.{ts,tsx}": [ "prettier --write", diff --git a/src/stores/state/ImmutableState.ts b/src/stores/state/ImmutableState.ts index 11a51b18b..889971151 100644 --- a/src/stores/state/ImmutableState.ts +++ b/src/stores/state/ImmutableState.ts @@ -7,14 +7,22 @@ import { } from './Patch'; import { Pointer } from './Pointer'; import { MutableState, Path, State } from '../Store'; -import { Map, List } from 'immutable'; +import { Map as _Map, List as _List } from 'immutable'; +let Map: typeof _Map; +let List: typeof _List; +try { + const immutable = require('immutable'); + Map = immutable.Map; + List = immutable.List; +} catch (ex) {} + import { getFriendlyDifferenceMessage, isEqual } from './compare'; function isString(segment?: string): segment is string { return typeof segment === 'string'; } -function inverse(operation: PatchOperation, state: Map): PatchOperation[] { +function inverse(operation: PatchOperation, state: _Map): PatchOperation[] { if (operation.op === OperationType.ADD) { const op: RemovePatchOperation = { op: OperationType.REMOVE, @@ -59,7 +67,7 @@ function inverse(operation: PatchOperation, state: Map): PatchOperatio } export class ImmutableState implements MutableState { - private _state = Map(); + private _state: _Map = Map(); /** * Returns the state at a specific pointer path location. @@ -140,7 +148,7 @@ export class ImmutableState implements MutableState { return undoOperations; } - private setIn(segments: string[], value: any, state: Map, add = false) { + private setIn(segments: string[], value: any, state: _Map, add = false) { const updated = this.set(segments, value, state, add); if (updated) { return updated; @@ -148,13 +156,13 @@ export class ImmutableState implements MutableState { state = state.withMutations((map) => { segments.slice(0, segments.length - 1).forEach((segment, index) => { - let nextSegment = ''; + let nextSegment: any = ''; if (index + 1 < segments.length) { nextSegment = segments[index + 1]; } const value = state.getIn([...segments.slice(0, index), segment]); if (!value || !(value instanceof List || value instanceof Map)) { - if (!isNaN(parseInt(nextSegment, 10))) { + if (!isNaN(nextSegment) && !isNaN(parseInt(nextSegment, 0))) { map = map.setIn([...segments.slice(0, index), segment], List()); } else { map = map.setIn([...segments.slice(0, index), segment], Map()); @@ -166,7 +174,7 @@ export class ImmutableState implements MutableState { return this.set(segments, value, state, add) || state; } - private set(segments: string[], value: any, state: Map, add = false) { + private set(segments: string[], value: any, state: _Map, add = false) { if (typeof value === 'object' && value != null) { if (Array.isArray(value)) { value = List(value); From a7b182e655e274d6a2c7028f24a9a4838284b8c5 Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Tue, 26 Feb 2019 11:08:28 -0500 Subject: [PATCH 07/13] Update README --- src/stores/README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/stores/README.md b/src/stores/README.md index ec5d01c7f..0781e2db8 100644 --- a/src/stores/README.md +++ b/src/stores/README.md @@ -698,3 +698,27 @@ import { Store } from '@dojo/framework/stores/Store'; const store = new Store(); load('my-process', store); ``` + +## Providing an alternative `State` implementation + +Processing operations and updating the store state is handled by an implementation of the `MutableState` interface +defined in `Store.ts`. This interface defines four methods necessary to properly apply operations to the state. + +- `get(path: Path): S`: Takes a `Path` object and returns the value in the current state that that path points to +- `at>>(path: S, index: number): Path`: Returns a `Path` object that +points to the provided `index` in the array at the provided `path` +- `path: StatePaths`: A typesafe way to generate a `Path` object for a given path in the state +- `apply(operations: PatchOperation[]): PatchOperation[]`: Apply the provided operations to the current state + +The default state implementation is reasonably optimized and in most circumstances will be sufficient. +If a particular use case merits an alternative implementation it can be provided to the store constructor: + +```ts +const store = new Store({ state: myStateImpl }); +``` + +### ImmutableState + +An implementation of the `MutableState` interface that leverages [ImmutableJS](https://github.com/immutable-js/immutable-js) under the hood is provided as +an example. This implementation may provide better performance if there are frequent, deep updates to the store's state, but this should be tested and verified +before switching to this implementation. From b4e453214cc229f89f5f28dcd91dd621f4c158cc Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Tue, 26 Feb 2019 11:11:29 -0500 Subject: [PATCH 08/13] Reorganize README updates --- src/stores/README.md | 50 +++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/src/stores/README.md b/src/stores/README.md index 0781e2db8..c38f334b8 100644 --- a/src/stores/README.md +++ b/src/stores/README.md @@ -27,6 +27,7 @@ An application store for dojo. - [Transforming Executor Arguments](#transforming-executor-arguments) - [Optimistic Update Pattern](#optimistic-update-pattern) - [Executing Concurrent Commands](#executing-concurrent-commands) + - [Providing An Alternative State Implementation](#providing-an-alternative-state-implementation) - [Middleware](#middleware) - [After Middleware](#after-middleware) - [Before Middleware](#before-middleware) @@ -567,6 +568,31 @@ In this example, `commandOne` is executed, then both `concurrentCommandOne` and **Note:** Concurrent commands are always assumed to be asynchronous and resolved using `Promise.all`. + +### Providing an alternative State implementation + +Processing operations and updating the store state is handled by an implementation of the `MutableState` interface +defined in `Store.ts`. This interface defines four methods necessary to properly apply operations to the state. + +- `get(path: Path): S`: Takes a `Path` object and returns the value in the current state that that path points to +- `at>>(path: S, index: number): Path`: Returns a `Path` object that +points to the provided `index` in the array at the provided `path` +- `path: StatePaths`: A typesafe way to generate a `Path` object for a given path in the state +- `apply(operations: PatchOperation[]): PatchOperation[]`: Apply the provided operations to the current state + +The default state implementation is reasonably optimized and in most circumstances will be sufficient. +If a particular use case merits an alternative implementation it can be provided to the store constructor: + +```ts +const store = new Store({ state: myStateImpl }); +``` + +#### ImmutableState + +An implementation of the `MutableState` interface that leverages [ImmutableJS](https://github.com/immutable-js/immutable-js) under the hood is provided as +an example. This implementation may provide better performance if there are frequent, deep updates to the store's state, but this should be tested and verified +before switching to this implementation. + ## Middleware Middleware provides a hook to apply generic/global functionality across multiple or all processes used within an application. Middleware is a function that returns an object with optional `before` and `after` callback functions. @@ -698,27 +724,3 @@ import { Store } from '@dojo/framework/stores/Store'; const store = new Store(); load('my-process', store); ``` - -## Providing an alternative `State` implementation - -Processing operations and updating the store state is handled by an implementation of the `MutableState` interface -defined in `Store.ts`. This interface defines four methods necessary to properly apply operations to the state. - -- `get(path: Path): S`: Takes a `Path` object and returns the value in the current state that that path points to -- `at>>(path: S, index: number): Path`: Returns a `Path` object that -points to the provided `index` in the array at the provided `path` -- `path: StatePaths`: A typesafe way to generate a `Path` object for a given path in the state -- `apply(operations: PatchOperation[]): PatchOperation[]`: Apply the provided operations to the current state - -The default state implementation is reasonably optimized and in most circumstances will be sufficient. -If a particular use case merits an alternative implementation it can be provided to the store constructor: - -```ts -const store = new Store({ state: myStateImpl }); -``` - -### ImmutableState - -An implementation of the `MutableState` interface that leverages [ImmutableJS](https://github.com/immutable-js/immutable-js) under the hood is provided as -an example. This implementation may provide better performance if there are frequent, deep updates to the store's state, but this should be tested and verified -before switching to this implementation. From 280446723e6e34935c6adf94a550c73e953edfb1 Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Tue, 26 Feb 2019 11:13:52 -0500 Subject: [PATCH 09/13] README cleanup --- src/stores/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/stores/README.md b/src/stores/README.md index c38f334b8..4e563c5f0 100644 --- a/src/stores/README.md +++ b/src/stores/README.md @@ -574,14 +574,14 @@ In this example, `commandOne` is executed, then both `concurrentCommandOne` and Processing operations and updating the store state is handled by an implementation of the `MutableState` interface defined in `Store.ts`. This interface defines four methods necessary to properly apply operations to the state. -- `get(path: Path): S`: Takes a `Path` object and returns the value in the current state that that path points to -- `at>>(path: S, index: number): Path`: Returns a `Path` object that +- `get(path: Path): S` Takes a `Path` object and returns the value in the current state that that path points to +- `at>>(path: S, index: number): Path` Returns a `Path` object that points to the provided `index` in the array at the provided `path` -- `path: StatePaths`: A typesafe way to generate a `Path` object for a given path in the state -- `apply(operations: PatchOperation[]): PatchOperation[]`: Apply the provided operations to the current state +- `path: StatePaths` A typesafe way to generate a `Path` object for a given path in the state +- `apply(operations: PatchOperation[]): PatchOperation[]` Apply the provided operations to the current state The default state implementation is reasonably optimized and in most circumstances will be sufficient. -If a particular use case merits an alternative implementation it can be provided to the store constructor: +If a particular use case merits an alternative implementation it can be provided to the store constructor ```ts const store = new Store({ state: myStateImpl }); From c0b14deabb2d663f0b1ac0e214907446a5f3c838 Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Tue, 26 Feb 2019 11:15:51 -0500 Subject: [PATCH 10/13] Cleanup --- src/stores/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/README.md b/src/stores/README.md index 4e563c5f0..1026091d3 100644 --- a/src/stores/README.md +++ b/src/stores/README.md @@ -589,9 +589,9 @@ const store = new Store({ state: myStateImpl }); #### ImmutableState -An implementation of the `MutableState` interface that leverages [ImmutableJS](https://github.com/immutable-js/immutable-js) under the hood is provided as -an example. This implementation may provide better performance if there are frequent, deep updates to the store's state, but this should be tested and verified -before switching to this implementation. +An implementation of the `MutableState` interface that leverages [Immutable](https://github.com/immutable-js/immutable-js) under the hood is provided as +an example. This implementation may provide better performance if there are frequent, deep updates to the store's state, but performance should be tested and verified +for your app before switching to this implementation. ## Middleware From 98b61c20ff71794e91791eb337d506f21c0f9179 Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Tue, 26 Feb 2019 11:18:52 -0500 Subject: [PATCH 11/13] Remove benchmark flag --- intern.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/intern.json b/intern.json index 167a1488e..26a41dc74 100644 --- a/intern.json +++ b/intern.json @@ -89,6 +89,5 @@ "!./dist/dev/tests/widget-core/unit/meta/WebAnimation.js", "!./dist/dev/tests/widget-core/unit/meta/Intersection.js" ] - }, - "benchmark": true + } } From 8952c71f93abb8df775b70025303b164f56e00e6 Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Tue, 26 Feb 2019 11:46:42 -0500 Subject: [PATCH 12/13] Fix import, address feedback --- src/stores/README.md | 4 +--- src/stores/Store.ts | 16 ++++++++-------- src/stores/state/ImmutableState.ts | 5 ++--- tests/stores/unit/middleware/localStorage.ts | 4 ++-- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/stores/README.md b/src/stores/README.md index 1026091d3..897f93822 100644 --- a/src/stores/README.md +++ b/src/stores/README.md @@ -589,9 +589,7 @@ const store = new Store({ state: myStateImpl }); #### ImmutableState -An implementation of the `MutableState` interface that leverages [Immutable](https://github.com/immutable-js/immutable-js) under the hood is provided as -an example. This implementation may provide better performance if there are frequent, deep updates to the store's state, but performance should be tested and verified -for your app before switching to this implementation. +An implementation of the `MutableState` interface that leverages [Immutable](https://github.com/immutable-js/immutable-js) under the hood is provided as an example. This implementation may provide better performance if there are frequent, deep updates to the store's state, but performance should be tested and verified for your app before switching to this implementation. ## Middleware diff --git a/src/stores/Store.ts b/src/stores/Store.ts index 2cd79a1f0..563b25da3 100644 --- a/src/stores/Store.ts +++ b/src/stores/Store.ts @@ -192,7 +192,7 @@ export class DefaultState implements MutableState { * Application state store */ export class Store extends Evented implements MutableState { - private _state: MutableState = new DefaultState(); + private _adapter: MutableState = new DefaultState(); private _changePaths = new Map(); @@ -202,14 +202,14 @@ export class Store extends Evented implements MutableState { * Returns the state at a specific pointer path location. */ public get = (path: Path): U => { - return this._state.get(path); + return this._adapter.get(path); }; constructor(options?: { state?: MutableState }) { super(); if (options && options.state) { - this._state = options.state; - this.path = this._state.path.bind(this._state); + this._adapter = options.state; + this.path = this._adapter.path.bind(this._adapter); } } @@ -217,7 +217,7 @@ export class Store extends Evented implements MutableState { * Applies store operations to state and returns the undo operations */ public apply = (operations: PatchOperation[], invalidate: boolean = false): PatchOperation[] => { - const result = this._state.apply(operations); + const result = this._adapter.apply(operations); if (invalidate) { this.invalidate(); @@ -227,7 +227,7 @@ export class Store extends Evented implements MutableState { }; public at = (path: Path>, index: number): Path => { - return this._state.at(path, index); + return this._adapter.at(path, index); }; public onChange = (paths: Path | Path[], callback: () => void) => { @@ -266,7 +266,7 @@ export class Store extends Evented implements MutableState { const { previousValue, callbacks } = value; const pointer = new Pointer(path); const newValue = pointer.segments.length - ? this._state.path(pointer.segments[0] as keyof T, ...pointer.segments.slice(1)).value + ? this._adapter.path(pointer.segments[0] as keyof T, ...pointer.segments.slice(1)).value : undefined; if (previousValue !== newValue) { this._changePaths.set(path, { callbacks, previousValue: newValue }); @@ -289,7 +289,7 @@ export class Store extends Evented implements MutableState { this.emit({ type: 'invalidate' }); } - public path: State['path'] = this._state.path.bind(this._state); + public path: State['path'] = this._adapter.path.bind(this._adapter); } export default Store; diff --git a/src/stores/state/ImmutableState.ts b/src/stores/state/ImmutableState.ts index 889971151..91da81081 100644 --- a/src/stores/state/ImmutableState.ts +++ b/src/stores/state/ImmutableState.ts @@ -10,11 +10,10 @@ import { MutableState, Path, State } from '../Store'; import { Map as _Map, List as _List } from 'immutable'; let Map: typeof _Map; let List: typeof _List; -try { - const immutable = require('immutable'); +import('immutable').then((immutable) => { Map = immutable.Map; List = immutable.List; -} catch (ex) {} +}); import { getFriendlyDifferenceMessage, isEqual } from './compare'; diff --git a/tests/stores/unit/middleware/localStorage.ts b/tests/stores/unit/middleware/localStorage.ts index a46a3e0a6..c54d361c6 100644 --- a/tests/stores/unit/middleware/localStorage.ts +++ b/tests/stores/unit/middleware/localStorage.ts @@ -62,12 +62,12 @@ describe('middleware - local storage', (suite) => { it('should load from local storage', () => { global.localStorage.setItem(LOCAL_STORAGE_TEST_ID, '[{"meta":{"path":"/counter"},"state":1}]'); load(LOCAL_STORAGE_TEST_ID, store); - assert.deepEqual((store as any)._state._state, { counter: 1 }); + assert.deepEqual((store as any)._adapter._state, { counter: 1 }); }); it('should not load anything or throw an error if data does exist', () => { global.localStorage.setItem('other-storage-id', '[{"meta":{"path":"/counter"},"state":1}]'); load(LOCAL_STORAGE_TEST_ID, store); - assert.deepEqual((store as any)._state._state, {}); + assert.deepEqual((store as any)._adapter._state, {}); }); }); From 1aa332ebfd2b94a80edb5fc363aca4cf89353006 Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Tue, 26 Feb 2019 11:48:32 -0500 Subject: [PATCH 13/13] Undo dep changes --- package.json | 5 +---- src/stores/README.md | 5 ++--- src/stores/state/ImmutableState.ts | 16 +++++----------- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 36cf8cf4d..c861753c1 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "css-select-umd": "1.3.0-rc0", "diff": "3.5.0", "globalize": "1.4.0", + "immutable": "3.8.2", "intersection-observer": "0.4.2", "pepjs": "0.4.2", "resize-observer-polyfill": "1.5.0", @@ -66,7 +67,6 @@ "@types/selenium-webdriver": "^3.0.8", "@types/sinon": "~4.1.2", "benchmark": "^1.0.0", - "immutable": "3.8.2", "bootstrap": "^3.3.7", "chromedriver": "2.40.0", "cldr-data": "32.0.1", @@ -84,9 +84,6 @@ "sinon": "~4.1.3", "typescript": "~2.6.2" }, - "optionalDependencies": { - "immutable": "3.8.2" - }, "lint-staged": { "*.{ts,tsx}": [ "prettier --write", diff --git a/src/stores/README.md b/src/stores/README.md index 897f93822..559ff1bc8 100644 --- a/src/stores/README.md +++ b/src/stores/README.md @@ -568,15 +568,14 @@ In this example, `commandOne` is executed, then both `concurrentCommandOne` and **Note:** Concurrent commands are always assumed to be asynchronous and resolved using `Promise.all`. - ### Providing an alternative State implementation Processing operations and updating the store state is handled by an implementation of the `MutableState` interface defined in `Store.ts`. This interface defines four methods necessary to properly apply operations to the state. - `get(path: Path): S` Takes a `Path` object and returns the value in the current state that that path points to -- `at>>(path: S, index: number): Path` Returns a `Path` object that -points to the provided `index` in the array at the provided `path` +- `at>>(path: S, index: number): Path` Returns a `Path` object that + points to the provided `index` in the array at the provided `path` - `path: StatePaths` A typesafe way to generate a `Path` object for a given path in the state - `apply(operations: PatchOperation[]): PatchOperation[]` Apply the provided operations to the current state diff --git a/src/stores/state/ImmutableState.ts b/src/stores/state/ImmutableState.ts index 91da81081..33b63328c 100644 --- a/src/stores/state/ImmutableState.ts +++ b/src/stores/state/ImmutableState.ts @@ -7,13 +7,7 @@ import { } from './Patch'; import { Pointer } from './Pointer'; import { MutableState, Path, State } from '../Store'; -import { Map as _Map, List as _List } from 'immutable'; -let Map: typeof _Map; -let List: typeof _List; -import('immutable').then((immutable) => { - Map = immutable.Map; - List = immutable.List; -}); +import { Map, List } from 'immutable'; import { getFriendlyDifferenceMessage, isEqual } from './compare'; @@ -21,7 +15,7 @@ function isString(segment?: string): segment is string { return typeof segment === 'string'; } -function inverse(operation: PatchOperation, state: _Map): PatchOperation[] { +function inverse(operation: PatchOperation, state: Map): PatchOperation[] { if (operation.op === OperationType.ADD) { const op: RemovePatchOperation = { op: OperationType.REMOVE, @@ -66,7 +60,7 @@ function inverse(operation: PatchOperation, state: _Map): PatchOperati } export class ImmutableState implements MutableState { - private _state: _Map = Map(); + private _state: Map = Map(); /** * Returns the state at a specific pointer path location. @@ -147,7 +141,7 @@ export class ImmutableState implements MutableState { return undoOperations; } - private setIn(segments: string[], value: any, state: _Map, add = false) { + private setIn(segments: string[], value: any, state: Map, add = false) { const updated = this.set(segments, value, state, add); if (updated) { return updated; @@ -173,7 +167,7 @@ export class ImmutableState implements MutableState { return this.set(segments, value, state, add) || state; } - private set(segments: string[], value: any, state: _Map, add = false) { + private set(segments: string[], value: any, state: Map, add = false) { if (typeof value === 'object' && value != null) { if (Array.isArray(value)) { value = List(value);