Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Optimistic UI #336

Merged
merged 28 commits into from
Jul 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d1abeb0
optimistic response
davidwoody Jun 29, 2016
8c677c7
update the change log with issue and pr numbers
davidwoody Jun 29, 2016
f4c3097
use the same logic for optimistic data cache
davidwoody Jun 29, 2016
e910ad0
fix linter / tests
davidwoody Jun 29, 2016
7af02bc
use merge instead of assign for cache data
davidwoody Jul 1, 2016
98b8cc3
Update typings, switch to merge package
Jul 1, 2016
6375d0c
Always read from optimistic response
Jul 1, 2016
0010001
Make function more descriptive
Jul 1, 2016
7864131
increase max gzip size from 34 to 36
davidwoody Jul 6, 2016
5e0f799
Move optimistic mutations logic into a separate file
Slava Jul 6, 2016
4561196
Use a stack of changes for optimistic updates
Slava Jul 6, 2016
9707d62
add a trailing comma
Slava Jul 7, 2016
19d4f89
only save patches of mutation results
davidwoody Jul 7, 2016
de55bd0
add tests for optimistic ui
Slava Jul 8, 2016
15e9e17
enable debugging with sourcemaps in VSCode
Slava Jul 8, 2016
b2d5344
apply resultBehaviors in optimistic updates. Use previous store
Slava Jul 8, 2016
1523edf
fix accidental mutation of the array
Slava Jul 8, 2016
a0884df
trailing comma
Slava Jul 8, 2016
f85714b
store optimistic copy of state, not patches
Slava Jul 8, 2016
548d25e
Use APOLLO_MUTATION_ERROR to handle errors from mutations in optimist…
Slava Jul 11, 2016
ea01cd1
Pass through optimisticResponse for the mutation error action
Slava Jul 11, 2016
6518dcc
test on two mutations with one failure
Slava Jul 11, 2016
c6b9098
fix stacking optimistic mutations
Slava Jul 11, 2016
06ee995
Add a test for 2 concurrent mutations with checks of every intermedia…
Slava Jul 12, 2016
ecb0daf
Rewrite the snapshots back to patches (fails the tests)
Slava Jul 12, 2016
020c976
fix bugs in the previous commit
Slava Jul 12, 2016
c16e400
move the changelog record about optimistic-updates to the top
Slava Jul 12, 2016
ea1b746
remove optimisticResponse property on mutation actions
Slava Jul 12, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"lib/test/tests.js"
],
"cwd": "${workspaceRoot}",
"runtimeExecutable": null
"runtimeExecutable": null,
"sourceMaps": true,
"outDir": "${workspaceRoot}/lib"
}
]
}
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Expect active development and potentially significant breaking changes in the `0

### vNEXT

- Allow `client.mutate` to accept an `optimisticResponse` argument to update the cache immediately, then after the server responds replace the `optimisticResponse` with the real response. [Issue #287](https://github.com/apollostack/apollo-client/issues/287) [PR #336](https://github.com/apollostack/apollo-client/pull/336)

### v0.4.0

This release has a minor version bump, which means npm will not automatically update to this version. Consider the list of breaking changes below, then upgrade and update your app correspondingly.
Expand Down
5 changes: 5 additions & 0 deletions ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ declare module 'lodash.assign' {
export = main.assign;
}

declare module 'lodash.merge' {
import main = require('~lodash/index');
export = main.merge;
}

declare module 'lodash.includes' {
import main = require('~lodash/index');
export = main.includes;
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"pretest": "npm run compile",
"test": "npm run testonly --",
"posttest": "npm run lint",
"filesize": "npm run compile:browser && ./scripts/filesize.js --file=./dist/index.min.js --maxGzip=34",
"filesize": "npm run compile:browser && ./scripts/filesize.js --file=./dist/index.min.js --maxGzip=36",
"compile": "tsc",
"compile:browser": "rm -rf ./dist && mkdir ./dist && browserify ./lib/src/index.js -o=./dist/index.js && npm run minify:browser",
"minify:browser": "uglifyjs --compress --mangle --screw-ie8 -o=./dist/index.min.js -- ./dist/index.js",
Expand Down Expand Up @@ -55,6 +55,8 @@
"lodash.isstring": "^4.0.1",
"lodash.isundefined": "^3.0.1",
"lodash.mapvalues": "^4.4.0",
"lodash.merge": "^4.4.0",
"lodash.pick": "^4.2.0",
"redux": "^3.3.1",
"symbol-observable": "^0.2.4",
"whatwg-fetch": "^1.0.0"
Expand Down
15 changes: 14 additions & 1 deletion src/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import isEqual = require('lodash.isequal');
import {
ApolloStore,
Store,
getDataWithOptimisticResults,
} from './store';

import {
Expand All @@ -32,6 +33,10 @@ import {
applyTransformerToOperation,
} from './queries/queryTransform';

import {
NormalizedCache,
} from './data/store';

import {
GraphQLResult,
Document,
Expand Down Expand Up @@ -218,11 +223,13 @@ export class QueryManager {
variables,
resultBehaviors,
fragments = [],
optimisticResponse,
}: {
mutation: Document,
variables?: Object,
resultBehaviors?: MutationBehavior[],
fragments?: FragmentDefinition[],
optimisticResponse?: Object,
}): Promise<ApolloQueryResult> {
const mutationId = this.generateQueryId();

Expand Down Expand Up @@ -256,6 +263,8 @@ export class QueryManager {
variables,
mutationId,
fragmentMap: queryFragmentMap,
optimisticResponse,
resultBehaviors,
});

return this.networkInterface.query(request)
Expand Down Expand Up @@ -312,7 +321,7 @@ export class QueryManager {
}
} else {
const resultFromStore = readSelectionSetFromStore({
store: this.getApolloState().data,
store: this.getDataWithOptimisticResults(),
rootId: queryStoreValue.query.id,
selectionSet: queryStoreValue.query.selectionSet,
variables: queryStoreValue.variables,
Expand Down Expand Up @@ -449,6 +458,10 @@ export class QueryManager {
return this.store.getState()[this.reduxRootKey];
}

public getDataWithOptimisticResults(): NormalizedCache {
return getDataWithOptimisticResults(this.getApolloState());
}

public addQueryListener(queryId: string, listener: QueryListener) {
this.queryListeners[queryId] = listener;
};
Expand Down
2 changes: 2 additions & 0 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export interface MutationInitAction {
variables: Object;
mutationId: string;
fragmentMap: FragmentMap;
optimisticResponse: Object;
resultBehaviors?: MutationBehavior[];
}

export function isMutationInitAction(action: ApolloAction): action is MutationInitAction {
Expand Down
8 changes: 4 additions & 4 deletions src/data/mutationResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ function mutationResultArrayInsertReducer(state: NormalizedCache, {
});

// Step 3: insert dataId reference into storePath array
const dataIdOfObj = storePath.shift();
const [dataIdOfObj, ...restStorePath] = storePath;
const clonedObj = cloneDeep(state[dataIdOfObj]);
const array = scopeJSONToResultPath({
json: clonedObj,
path: storePath,
path: restStorePath,
});

if (where === 'PREPEND') {
Expand Down Expand Up @@ -240,11 +240,11 @@ function mutationResultArrayDeleteReducer(state: NormalizedCache, {
storePath,
} = behavior as MutationArrayDeleteBehavior;

const dataIdOfObj = storePath.shift();
const [dataIdOfObj, ...restStorePath] = storePath;
const clonedObj = cloneDeep(state[dataIdOfObj]);
const array = scopeJSONToResultPath({
json: clonedObj,
path: storePath,
path: restStorePath,
});

array.splice(array.indexOf(dataId), 1);
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,10 @@ export default class ApolloClient {

public mutate = (options: {
mutation: Document,
resultBehaviors?: MutationBehavior[],
variables?: Object,
resultBehaviors?: MutationBehavior[],
fragments?: FragmentDefinition[],
optimisticResponse?: Object,
}): Promise<ApolloQueryResult> => {
this.initStore();
return this.queryManager.mutate(options);
Expand Down
75 changes: 75 additions & 0 deletions src/optimistic-data/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
ApolloAction,
isMutationInitAction,
isMutationResultAction,
isMutationErrorAction,
} from '../actions';

import {
data,
NormalizedCache,
} from '../data/store';

import {
getDataWithOptimisticResults,
Store,
} from '../store';

import assign = require('lodash.assign');
import pick = require('lodash.pick');

// a stack of patches of new or changed documents
export type OptimisticStore = {
mutationId: string,
data: NormalizedCache,
}[];

const optimisticDefaultState = [];

export function optimistic(
previousState = optimisticDefaultState,
action,
store,
config
): OptimisticStore {
if (isMutationInitAction(action) && action.optimisticResponse) {
const fakeMutationResultAction = {
type: 'APOLLO_MUTATION_RESULT',
result: { data: action.optimisticResponse },
mutationId: action.mutationId,
resultBehaviors: action.resultBehaviors,
} as ApolloAction;

const fakeStore = assign({}, store, { optimistic: previousState }) as Store;
const optimisticData = getDataWithOptimisticResults(fakeStore);
const fakeDataResultState = data(
optimisticData,
fakeMutationResultAction,
store.queries,
store.mutations,
config
);

const changedKeys = Object.keys(fakeDataResultState).filter(
key => optimisticData[key] !== fakeDataResultState[key]);
const patch = pick(fakeDataResultState, changedKeys);

const optimisticState = {
data: patch,
mutationId: action.mutationId,
};

const newState = [...previousState, optimisticState];

return newState;
} else if ((isMutationErrorAction(action) || isMutationResultAction(action))
&& previousState.some(change => change.mutationId === action.mutationId)) {
// throw away optimistic changes of that particular mutation
const newState = previousState.filter(
(change) => change.mutationId !== action.mutationId);

return newState;
}

return previousState;
}
25 changes: 25 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import {
MutationStore,
} from './mutations/store';

import {
optimistic,
OptimisticStore,
} from './optimistic-data/store';

import {
ApolloAction,
} from './actions';
Expand All @@ -32,10 +37,13 @@ import {
MutationBehaviorReducerMap,
} from './data/mutationResults';

import assign = require('lodash.assign');

export interface Store {
data: NormalizedCache;
queries: QueryStore;
mutations: MutationStore;
optimistic: OptimisticStore;
}

// This is our interface on top of Redux to get types in our actions
Expand Down Expand Up @@ -66,8 +74,20 @@ export function createApolloReducer(config: ApolloReducerConfig): Function {
// Note that we are passing the queries into this, because it reads them to associate
// the query ID in the result with the actual query
data: data(state.data, action, state.queries, state.mutations, config),
optimistic: [],
};

// Note, we need to have the results of the
// APOLLO_MUTATION_INIT action to simulate
// the APOLLO_MUTATION_RESULT action. That's
// why we pass in newState
newState.optimistic = optimistic(
state.optimistic,
action,
newState,
config
);

return newState;
};
}
Expand Down Expand Up @@ -112,3 +132,8 @@ export interface ApolloReducerConfig {
dataIdFromObject?: IdGetter;
mutationBehaviorReducers?: MutationBehaviorReducerMap;
}

export function getDataWithOptimisticResults(store: Store): NormalizedCache {
const patches = store.optimistic.map(opt => opt.data);
return assign({}, store.data, ...patches) as NormalizedCache;
}
1 change: 1 addition & 0 deletions test/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2195,6 +2195,7 @@ describe('QueryManager', () => {
data: {},
mutations: {},
queries: {},
optimistic: [],
};

assert.deepEqual(currentState, expectedState);
Expand Down
3 changes: 3 additions & 0 deletions test/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ describe('client', () => {
queries: {},
mutations: {},
data: {},
optimistic: [],
},
}
);
Expand All @@ -173,6 +174,7 @@ describe('client', () => {
queries: {},
mutations: {},
data: {},
optimistic: [],
},
}
);
Expand Down Expand Up @@ -359,6 +361,7 @@ describe('client', () => {
'allPeople({"first":1})': 'ROOT_QUERY.allPeople({"first":1})',
},
},
optimistic: [],
},
};

Expand Down
Loading