Skip to content

Commit e84d121

Browse files
prescottprueScott Prue
authored and
Scott Prue
committed
v0.4.0
* feat(core): Queries for multiple documents now support single document state updates using `docChanges` (changes reference of document instead of collection) * fix(dataReducer): delete does not remove data from `state.data` - #45 * feat(query): nested subcollections - @thiagovice - #73 * fix(query): queryStr parsing of deep nested collections - @thiagovice * feat(query): `oneListenerPerPath` config option to only attach one callback per listener path - @compojoom
2 parents 4048924 + 538c208 commit e84d121

25 files changed

+579
-141
lines changed

.babelrc

+4-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
"test": {
3030
"plugins": [
3131
"transform-runtime",
32-
"transform-async-to-generator"
32+
"transform-async-to-generator",
33+
["module-resolver", {
34+
"root": ["./src"]
35+
}]
3336
]
3437
}
3538
}

.travis.yml

+10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ sudo: false
33
language: node_js
44

55
node_js:
6+
- 6.11.5
67
- 8
78
- 9 # LTS
89

@@ -20,6 +21,15 @@ branches:
2021
only:
2122
- master
2223

24+
deploy:
25+
skip_cleanup: true
26+
provider: npm
27+
email: $NPM_EMAIL
28+
api_key: $NPM_KEY
29+
on:
30+
node: '9'
31+
branch: 'master'
32+
2333
script:
2434
- npm run lint
2535
- npm run test:cov

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,11 @@ Default: `false`
418418

419419
Whether or not to allow multiple listeners to be attached for the same query. If a function is passed the arguments it receives are `listenerToAttach`, `currentListeners`, and the function should return a boolean.
420420

421+
### oneListenerPerPath
422+
Default: `false`
423+
424+
If set to true redux-firestore will attach a listener on the same path just once & will count how many the listener was set. When you try to unset the lisnter, it won't unset until you have less than 1 listeners on this path
425+
421426
#### preserveOnDelete
422427
Default: `null`
423428

package-lock.json

+47-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "redux-firestore",
3-
"version": "0.3.2",
3+
"version": "0.4.0",
44
"description": "Redux bindings for Firestore.",
55
"main": "lib/index.js",
66
"module": "es/index.js",
@@ -22,8 +22,9 @@
2222
"build:umd:min": "cross-env BABEL_ENV=commonjs NODE_ENV=production webpack",
2323
"build": "npm run build:commonjs && npm run build:es && npm run build:umd && npm run build:umd:min",
2424
"watch": "npm run build:es -- --watch",
25+
"watch:commonjs": "npm run build:commonjs -- --watch",
2526
"test": "mocha -R spec ./test/unit/**",
26-
"test:cov": "istanbul cover ./node_modules/mocha/bin/_mocha ./test/unit/**",
27+
"test:cov": "istanbul cover $(npm bin)/_mocha ./test/unit/**",
2728
"codecov": "cat coverage/lcov.info | codecov",
2829
"lint": "eslint .",
2930
"lint:fix": "npm run lint -- --fix",
@@ -42,6 +43,7 @@
4243
"babel-eslint": "^8.0.1",
4344
"babel-loader": "^7.1.2",
4445
"babel-plugin-lodash": "^3.2.11",
46+
"babel-plugin-module-resolver": "^3.1.1",
4547
"babel-plugin-transform-async-to-generator": "^6.24.1",
4648
"babel-plugin-transform-inline-environment-variables": "^0.2.0",
4749
"babel-plugin-transform-object-assign": "^6.22.0",

src/actions/firestore.js

+90-14
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import {
88
orderedFromSnap,
99
dataByIdSnapshot,
1010
getQueryConfig,
11+
getQueryName,
1112
firestoreRef,
1213
} from '../utils/query';
1314

15+
const pathListenerCounts = {};
16+
1417
/**
1518
* Add data to a collection or document on Cloud Firestore with the call to
1619
* the Firebase library being wrapped in action dispatches.
@@ -155,12 +158,35 @@ export function deleteRef(firebase, dispatch, queryOption) {
155158
type: actionTypes.DELETE_SUCCESS,
156159
preserve: firebase._.config.preserveOnDelete,
157160
},
158-
actionTypes.DELETE_SUCCESS,
159161
actionTypes.DELETE_FAILURE,
160162
],
161163
});
162164
}
163165

166+
const changeTypeToEventType = {
167+
added: actionTypes.DOCUMENT_ADDED,
168+
removed: actionTypes.DOCUMENT_REMOVED,
169+
modified: actionTypes.DOCUMENT_MODIFIED,
170+
};
171+
172+
/**
173+
* Action creator for document change event. Used to create action objects
174+
* to be passed to dispatch.
175+
* @param {Object} change - Document change object from Firebase callback
176+
* @param {Object} [originalMeta={}] - Original meta data of action
177+
* @return {Object} [description]
178+
*/
179+
function docChangeEvent(change, originalMeta = {}) {
180+
return {
181+
type: changeTypeToEventType[change.type] || actionTypes.DOCUMENT_MODIFIED,
182+
meta: { ...originalMeta, doc: change.doc.id },
183+
payload: {
184+
data: change.doc.data(),
185+
ordered: { oldIndex: change.oldIndex, newIndex: change.newIndex },
186+
},
187+
};
188+
}
189+
164190
/**
165191
* Set listener to Cloud Firestore with the call to the Firebase library
166192
* being wrapped in action dispatches.. Internall calls Firebase's onSnapshot()
@@ -186,18 +212,33 @@ export function setListener(firebase, dispatch, queryOpts, successCb, errorCb) {
186212
// Create listener
187213
const unsubscribe = firestoreRef(firebase, dispatch, meta).onSnapshot(
188214
docData => {
189-
dispatch({
190-
type: actionTypes.LISTENER_RESPONSE,
191-
meta,
192-
payload: {
193-
data: dataByIdSnapshot(docData),
194-
ordered: orderedFromSnap(docData),
195-
},
196-
merge: {
197-
docs: mergeOrdered && mergeOrderedDocUpdates,
198-
collections: mergeOrdered && mergeOrderedCollectionUpdates,
199-
},
200-
});
215+
// Dispatch different actions for doc changes (only update doc(s) by key)
216+
if (docData.docChanges && docData.docChanges.length < docData.size) {
217+
if (docData.docChanges.length === 1) {
218+
// Dispatch doc update if there is only one
219+
dispatch(docChangeEvent(docData.docChanges[0], meta));
220+
} else {
221+
// Loop to dispatch for each change if there are multiple
222+
// TODO: Option for dispatching multiple changes in single action
223+
docData.docChanges.forEach(change => {
224+
dispatch(docChangeEvent(change, meta));
225+
});
226+
}
227+
} else {
228+
// Dispatch action for whole collection change
229+
dispatch({
230+
type: actionTypes.LISTENER_RESPONSE,
231+
meta,
232+
payload: {
233+
data: dataByIdSnapshot(docData),
234+
ordered: orderedFromSnap(docData),
235+
},
236+
merge: {
237+
docs: mergeOrdered && mergeOrderedDocUpdates,
238+
collections: mergeOrdered && mergeOrderedCollectionUpdates,
239+
},
240+
});
241+
}
201242
// Invoke success callback if it exists
202243
if (successCb) successCb(docData);
203244
},
@@ -240,12 +281,31 @@ export function setListeners(firebase, dispatch, listeners) {
240281
);
241282
}
242283

284+
const { config } = firebase._;
285+
const oneListenerPerPath = !!config.oneListenerPerPath;
286+
287+
if (oneListenerPerPath) {
288+
return listeners.forEach(listener => {
289+
const path = getQueryName(listener);
290+
const oldListenerCount = pathListenerCounts[path] || 0;
291+
pathListenerCounts[path] = oldListenerCount + 1;
292+
293+
// If we already have an attached listener exit here
294+
if (oldListenerCount > 0) {
295+
return;
296+
}
297+
298+
setListener(firebase, dispatch, listener);
299+
});
300+
}
301+
243302
return listeners.forEach(listener => {
244303
// Config for supporting attaching of multiple listeners
245-
const { config } = firebase._;
304+
246305
const multipleListenersEnabled = isFunction(config.allowMultipleListeners)
247306
? config.allowMultipleListeners(listener, firebase._.listeners)
248307
: config.allowMultipleListeners;
308+
249309
// Only attach listener if it does not already exist or
250310
// if multiple listeners config is true or is a function which returns
251311
// truthy value
@@ -282,6 +342,22 @@ export function unsetListeners(firebase, dispatch, listeners) {
282342
'Listeners must be an Array of listener configs (Strings/Objects).',
283343
);
284344
}
345+
const { config } = firebase._;
346+
const oneListenerPerPath = !!config.oneListenerPerPath;
347+
348+
if (oneListenerPerPath) {
349+
listeners.forEach(listener => {
350+
const path = getQueryName(listener);
351+
pathListenerCounts[path] -= 1;
352+
353+
// If we aren't supposed to have listners for this path, then remove them
354+
if (pathListenerCounts[path] === 0) {
355+
unsetListener(firebase, dispatch, listener);
356+
}
357+
});
358+
359+
return;
360+
}
285361

286362
listeners.forEach(listener => {
287363
// Remove listener only if it exists

src/constants.js

+3
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ export const actionTypes = {
7272
ON_SNAPSHOT_REQUEST: `${actionsPrefix}/ON_SNAPSHOT_REQUEST`,
7373
ON_SNAPSHOT_SUCCESS: `${actionsPrefix}/ON_SNAPSHOT_SUCCESS`,
7474
ON_SNAPSHOT_FAILURE: `${actionsPrefix}/ON_SNAPSHOT_FAILURE`,
75+
DOCUMENT_ADDED: `${actionsPrefix}/DOCUMENT_ADDED`,
76+
DOCUMENT_MODIFIED: `${actionsPrefix}/DOCUMENT_MODIFIED`,
77+
DOCUMENT_REMOVED: `${actionsPrefix}/DOCUMENT_REMOVED`,
7578
};
7679

7780
/**

0 commit comments

Comments
 (0)