├── .babelrc ├── .gitignore ├── README.md ├── __mocks__ └── react-native-firebase.js ├── __tests__ ├── actions.js ├── reducer.js └── sagas.js ├── actions.js ├── createReducer.js ├── firebase.js ├── package.json ├── patterns ├── listener_actions.js ├── listener_reducer.js ├── listener_sagas.js ├── listener_wrapper_functions.js ├── remove_actions.js ├── remove_reducer.js ├── remove_sagas.js ├── remove_wrapper_function.js ├── sagas_registration.js ├── update_action_test.js ├── update_actions.js ├── update_reducer.js ├── update_sagas.js ├── update_sagas_test.js └── update_wrapper_function.js ├── reducer.js ├── sagas.js ├── types.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"], 3 | "plugins": [ 4 | "transform-decorators-legacy" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Some code patterns for working with React Native, Redux, Redux-Saga and Firebase 2 | 3 | To run the tests 4 | ``` 5 | git clone git@github.com:rmrs/react-native-redux-saga-firebase-patterns.git 6 | 7 | yarn install 8 | 9 | npm run coverage 10 | ``` 11 | -------------------------------------------------------------------------------- /__mocks__/react-native-firebase.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | export class Database { 4 | ref = path => { 5 | if (!this[path]) { 6 | this[path] = new Reference(path) 7 | } 8 | return this[path] 9 | } 10 | } 11 | 12 | export class Reference { 13 | constructor(path) { 14 | this.path = path 15 | this.snap = { val: () => this._val() } 16 | this.data = null 17 | } 18 | 19 | _val = jest.fn(() => { 20 | return this.data 21 | }) 22 | 23 | once = jest.fn((param, callback) => { 24 | const promise = new Promise((resolve, reject) => { 25 | if (callback) { 26 | callback(this.snap) 27 | resolve() 28 | } else { 29 | resolve(this.snap) 30 | } 31 | }) 32 | RNFirebase.promises.push(promise) 33 | return promise 34 | }) 35 | 36 | on = jest.fn((param, callback) => { 37 | const promise = new Promise((resolve, reject) => { 38 | if (callback) { 39 | callback(this.snap) 40 | resolve() 41 | } else { 42 | resolve(this.snap) 43 | } 44 | }) 45 | RNFirebase.promises.push(promise) 46 | return promise 47 | }) 48 | 49 | off = jest.fn((param, callback) => { 50 | const promise = Promise.resolve() 51 | RNFirebase.promises.push(promise) 52 | return promise 53 | }) 54 | 55 | update = jest.fn(data => { 56 | const promise = Promise.resolve() 57 | RNFirebase.promises.push(promise) 58 | return promise 59 | }) 60 | 61 | remove = jest.fn(() => { 62 | const promise = Promise.resolve() 63 | RNFirebase.promises.push(promise) 64 | return promise 65 | }) 66 | } 67 | 68 | export class MockFirebase { 69 | constructor() { 70 | this.database = () => { 71 | if (!this.databaseInstance) { 72 | this.databaseInstance = new Database() 73 | } 74 | return this.databaseInstance 75 | } 76 | } 77 | } 78 | 79 | export default class RNFirebase { 80 | static initializeApp() { 81 | RNFirebase.firebase = new MockFirebase() 82 | RNFirebase.promises = [] 83 | return RNFirebase.firebase 84 | } 85 | 86 | static reset() { 87 | RNFirebase.promises = [] 88 | RNFirebase.firebase.databaseInstance = null 89 | } 90 | 91 | static waitForPromises() { 92 | return Promise.all(RNFirebase.promises) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /__tests__/actions.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions' 2 | import * as types from '../types' 3 | import firebase from '../firebase' 4 | import { metaTypes } from '../types' 5 | 6 | describe('firebase actions', () => { 7 | test(types.firebase.FIREBASE_LISTEN_REQUESTED, () => { 8 | const ref = firebase.database().ref('someRef') 9 | const expectedAction = { 10 | type: types.firebase.FIREBASE_LISTEN_REQUESTED, 11 | payload: ref, 12 | meta: { type: metaTypes.userContacts }, 13 | } 14 | expect( 15 | actions.firebaseListenRequested(ref, metaTypes.userContacts) 16 | ).toEqual(expectedAction) 17 | }) 18 | 19 | test(types.firebase.FIREBASE_LISTEN_REJECTED, () => { 20 | const error = new Error('Error!') 21 | const expectedAction = { 22 | type: types.firebase.FIREBASE_LISTEN_REJECTED, 23 | payload: { error }, 24 | meta: { type: metaTypes.userContacts }, 25 | } 26 | expect( 27 | actions.firebaseListenRejected(error, metaTypes.userContacts) 28 | ).toEqual(expectedAction) 29 | }) 30 | 31 | test(types.firebase.FIREBASE_LISTEN_FULFILLED, () => { 32 | const items = { item1: 1, item2: 2 } 33 | const expectedAction = { 34 | type: types.firebase.FIREBASE_LISTEN_FULFILLED, 35 | payload: { items }, 36 | meta: { type: metaTypes.userContacts }, 37 | } 38 | expect( 39 | actions.firebaseListenFulfilled(items, metaTypes.userContacts) 40 | ).toEqual(expectedAction) 41 | }) 42 | 43 | test(types.firebase.FIREBASE_LISTEN_CHILD_ADDED, () => { 44 | const id = '1' 45 | const value = 'a' 46 | const expectedAction = { 47 | type: types.firebase.FIREBASE_LISTEN_CHILD_ADDED, 48 | payload: { id, value }, 49 | meta: { type: metaTypes.userContacts }, 50 | } 51 | expect( 52 | actions.firebaseListenChildAdded(id, value, metaTypes.userContacts) 53 | ).toEqual(expectedAction) 54 | }) 55 | 56 | test(types.firebase.FIREBASE_LISTEN_CHILD_CHANGED, () => { 57 | const id = '1' 58 | const value = 'a' 59 | const expectedAction = { 60 | type: types.firebase.FIREBASE_LISTEN_CHILD_CHANGED, 61 | payload: { id, value }, 62 | meta: { type: metaTypes.userContacts }, 63 | } 64 | expect( 65 | actions.firebaseListenChildChanged(id, value, metaTypes.userContacts) 66 | ).toEqual(expectedAction) 67 | }) 68 | 69 | test(types.firebase.FIREBASE_LISTEN_CHILD_REMOVED, () => { 70 | const id = '1' 71 | const expectedAction = { 72 | type: types.firebase.FIREBASE_LISTEN_CHILD_REMOVED, 73 | payload: { id }, 74 | meta: { type: metaTypes.userContacts }, 75 | } 76 | expect( 77 | actions.firebaseListenChildRemoved(id, metaTypes.userContacts) 78 | ).toEqual(expectedAction) 79 | }) 80 | 81 | test(types.firebase.FIREBASE_UPDATE_REQUESTED, () => { 82 | const uid = '1' 83 | const expectedAction = { 84 | type: types.firebase.FIREBASE_UPDATE_REQUESTED, 85 | payload: uid, 86 | meta: { type: metaTypes.userContacts }, 87 | } 88 | expect( 89 | actions.firebaseUpdateRequested(uid, metaTypes.userContacts) 90 | ).toEqual(expectedAction) 91 | }) 92 | 93 | test(types.firebase.FIREBASE_UPDATE_FULFILLED, () => { 94 | const error = 'error' 95 | const expectedAction = { 96 | type: types.firebase.FIREBASE_UPDATE_FULFILLED, 97 | payload: {}, 98 | meta: { type: metaTypes.userContacts }, 99 | } 100 | 101 | expect(actions.firebaseUpdateFulfilled(metaTypes.userContacts)).toEqual( 102 | expectedAction 103 | ) 104 | }) 105 | 106 | test(types.firebase.FIREBASE_UPDATE_REJECTED, () => { 107 | const error = new Error('Error!') 108 | const expectedAction = { 109 | type: types.firebase.FIREBASE_UPDATE_REJECTED, 110 | payload: { error }, 111 | meta: { type: metaTypes.userContacts }, 112 | } 113 | 114 | expect( 115 | actions.firebaseUpdateRejected(error, metaTypes.userContacts) 116 | ).toEqual(expectedAction) 117 | }) 118 | 119 | test(types.firebase.FIREBASE_REMOVE_REQUESTED, () => { 120 | const uid = '1' 121 | const expectedAction = { 122 | type: types.firebase.FIREBASE_REMOVE_REQUESTED, 123 | payload: uid, 124 | meta: { type: metaTypes.userContacts }, 125 | } 126 | expect( 127 | actions.firebaseRemoveRequested(uid, metaTypes.userContacts) 128 | ).toEqual(expectedAction) 129 | }) 130 | 131 | test(types.firebase.FIREBASE_REMOVE_FULFILLED, () => { 132 | const error = 'error' 133 | const expectedAction = { 134 | type: types.firebase.FIREBASE_REMOVE_FULFILLED, 135 | payload: {}, 136 | meta: { type: metaTypes.userContacts }, 137 | } 138 | 139 | expect(actions.firebaseRemoveFulfilled(metaTypes.userContacts)).toEqual( 140 | expectedAction 141 | ) 142 | }) 143 | 144 | test(types.firebase.FIREBASE_REMOVE_REJECTED, () => { 145 | const error = new Error('Error!') 146 | const expectedAction = { 147 | type: types.firebase.FIREBASE_REMOVE_REJECTED, 148 | payload: { error }, 149 | meta: { type: metaTypes.userContacts }, 150 | } 151 | 152 | expect( 153 | actions.firebaseRemoveRejected(error, metaTypes.userContacts) 154 | ).toEqual(expectedAction) 155 | }) 156 | 157 | test(types.firebase.FIREBASE_LISTEN_REMOVED, () => { 158 | const expectedAction = { 159 | type: types.firebase.FIREBASE_LISTEN_REMOVED, 160 | payload: { clearItems: true }, 161 | meta: { type: metaTypes.userContacts }, 162 | } 163 | 164 | expect(actions.firebaseListenRemoved(true, metaTypes.userContacts)).toEqual( 165 | expectedAction 166 | ) 167 | }) 168 | 169 | test(types.firebase.FIREBASE_REMOVE_LISTENER_REQUESTED, () => { 170 | const expectedAction = { 171 | type: types.firebase.FIREBASE_REMOVE_LISTENER_REQUESTED, 172 | payload: { clearItems: true }, 173 | meta: { type: metaTypes.userContacts }, 174 | } 175 | 176 | expect( 177 | actions.firebaseRemoveListenerRequested(true, metaTypes.userContacts) 178 | ).toEqual(expectedAction) 179 | }) 180 | 181 | test(types.firebase.FIREBASE_REMOVE_ALL_LISTENERS_REQUESTED, () => { 182 | const expectedAction = { 183 | type: types.firebase.FIREBASE_REMOVE_ALL_LISTENERS_REQUESTED, 184 | payload: { clearItems: true }, 185 | } 186 | 187 | expect(actions.firebaseRemoveAllListenersRequested()).toEqual( 188 | expectedAction 189 | ) 190 | }) 191 | 192 | test('listenToMessages', () => { 193 | const ref = firebase.database().ref('messages') 194 | const expectedAction = { 195 | type: types.firebase.FIREBASE_LISTEN_REQUESTED, 196 | payload: ref, 197 | meta: { type: metaTypes.messages }, 198 | } 199 | expect(actions.listenToMessages()).toEqual(expectedAction) 200 | }) 201 | 202 | test('listenToUserContacts', () => { 203 | const uid = '123' 204 | const ref = firebase.database().ref(`users/${uid}/contacts`) 205 | const expectedAction = { 206 | type: types.firebase.FIREBASE_LISTEN_REQUESTED, 207 | payload: ref, 208 | meta: { type: metaTypes.userContacts }, 209 | } 210 | expect(actions.listenToUserContacts(uid)).toEqual(expectedAction) 211 | }) 212 | 213 | test('removeMessagesListenerRequested', () => { 214 | const expectedAction = { 215 | type: types.firebase.FIREBASE_REMOVE_LISTENER_REQUESTED, 216 | payload: { clearItems: false }, 217 | meta: { type: metaTypes.messages }, 218 | } 219 | expect(actions.removeMessagesListenerRequested()).toEqual(expectedAction) 220 | }) 221 | 222 | test('removeUserContactsListenerRequested', () => { 223 | const expectedAction = { 224 | type: types.firebase.FIREBASE_REMOVE_LISTENER_REQUESTED, 225 | payload: { clearItems: false }, 226 | meta: { type: metaTypes.userContacts }, 227 | } 228 | expect(actions.removeUserContactsListenerRequested()).toEqual( 229 | expectedAction 230 | ) 231 | }) 232 | 233 | test('updateUserContactsRequested', () => { 234 | const uid = '1' 235 | const contactId = '123' 236 | const name = 'John Doe' 237 | const phone = '123456789' 238 | const expectedAction = { 239 | type: types.firebase.FIREBASE_UPDATE_REQUESTED, 240 | payload: { uid, contactId, name, phone }, 241 | meta: { type: metaTypes.userContacts }, 242 | } 243 | expect( 244 | actions.updateUserContactsRequested(uid, contactId, name, phone) 245 | ).toEqual(expectedAction) 246 | }) 247 | 248 | test('removeUserContactsRequested', () => { 249 | const uid = '1' 250 | const contactId = '123' 251 | const expectedAction = { 252 | type: types.firebase.FIREBASE_REMOVE_REQUESTED, 253 | payload: { uid, contactId }, 254 | meta: { type: metaTypes.userContacts }, 255 | } 256 | expect(actions.removeUserContactsRequested(uid, contactId)).toEqual( 257 | expectedAction 258 | ) 259 | }) 260 | }) 261 | -------------------------------------------------------------------------------- /__tests__/reducer.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions' 2 | import * as types from '../types' 3 | import { getInitialState, firebaseReducer } from '../reducer' 4 | 5 | describe('firebaseReducer reducer', () => { 6 | test(types.firebase.FIREBASE_UPDATE_REQUESTED, () => { 7 | const initialState = { 8 | [types.metaTypes.updateMessage]: { inProgress: false, error: '' }, 9 | } 10 | const action = actions.firebaseUpdateRequested( 11 | types.metaTypes.updateMessage 12 | ) 13 | const expectedState = { 14 | [types.metaTypes.updateMessage]: { inProgress: true, error: '' }, 15 | } 16 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 17 | }) 18 | 19 | test(types.firebase.FIREBASE_UPDATE_REJECTED, () => { 20 | const initialState = { 21 | [types.metaTypes.updateMessage]: { inProgress: true, error: '' }, 22 | } 23 | const error = 'error' 24 | const action = actions.firebaseUpdateRejected( 25 | error, 26 | types.metaTypes.updateMessage 27 | ) 28 | const expectedState = { 29 | [types.metaTypes.updateMessage]: { inProgress: false, error }, 30 | } 31 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 32 | }) 33 | 34 | test(types.firebase.FIREBASE_UPDATE_FULFILLED, () => { 35 | const initialState = { 36 | [types.metaTypes.updateMessage]: { inProgress: true, error: '' }, 37 | } 38 | const action = actions.firebaseUpdateFulfilled( 39 | types.metaTypes.updateMessage 40 | ) 41 | const expectedState = { 42 | [types.metaTypes.updateMessage]: { inProgress: false, error: '' }, 43 | } 44 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 45 | }) 46 | 47 | test(types.firebase.FIREBASE_REMOVE_REQUESTED, () => { 48 | const initialState = { 49 | [types.metaTypes.removeMessage]: { inProgress: false, error: '' }, 50 | } 51 | const action = actions.firebaseRemoveRequested( 52 | types.metaTypes.removeMessage 53 | ) 54 | const expectedState = { 55 | [types.metaTypes.removeMessage]: { inProgress: true, error: '' }, 56 | } 57 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 58 | }) 59 | 60 | test(types.firebase.FIREBASE_REMOVE_REJECTED, () => { 61 | const initialState = { 62 | [types.metaTypes.removeMessage]: { inProgress: true, error: '' }, 63 | } 64 | const error = 'error' 65 | const action = actions.firebaseRemoveRejected( 66 | error, 67 | types.metaTypes.removeMessage 68 | ) 69 | const expectedState = { 70 | [types.metaTypes.removeMessage]: { inProgress: false, error }, 71 | } 72 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 73 | }) 74 | 75 | test(types.firebase.FIREBASE_REMOVE_FULFILLED, () => { 76 | const initialState = { 77 | [types.metaTypes.removeMessage]: { inProgress: true, error: '' }, 78 | } 79 | const action = actions.firebaseRemoveFulfilled( 80 | types.metaTypes.removeMessage 81 | ) 82 | const expectedState = { 83 | [types.metaTypes.removeMessage]: { inProgress: false, error: '' }, 84 | } 85 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 86 | }) 87 | 88 | test(types.firebase.FIREBASE_LISTEN_REQUESTED, () => { 89 | const initialState = { 90 | [types.metaTypes.messages]: { 91 | inProgress: false, 92 | error: '', 93 | items: {}, 94 | }, 95 | } 96 | const ref = new Object() 97 | const action = actions.firebaseListenRequested( 98 | ref, 99 | types.metaTypes.messages 100 | ) 101 | const expectedState = { 102 | [types.metaTypes.messages]: { 103 | inProgress: true, 104 | error: '', 105 | items: {}, 106 | }, 107 | } 108 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 109 | }) 110 | 111 | test(types.firebase.FIREBASE_LISTEN_REJECTED, () => { 112 | const initialState = { 113 | [types.metaTypes.messages]: { inProgress: true, error: '', items: {} }, 114 | } 115 | const error = 'error' 116 | const action = actions.firebaseListenRejected( 117 | error, 118 | types.metaTypes.messages 119 | ) 120 | const expectedState = { 121 | [types.metaTypes.messages]: { inProgress: false, error, items: {} }, 122 | } 123 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 124 | }) 125 | 126 | test(types.firebase.FIREBASE_LISTEN_FULFILLED, () => { 127 | const initialState = { 128 | [types.metaTypes.messages]: { inProgress: true, error: '', items: {} }, 129 | } 130 | const items = { '1': { text: 'hello' }, '2': { text: 'world' } } 131 | const action = actions.firebaseListenFulfilled( 132 | items, 133 | types.metaTypes.messages 134 | ) 135 | const expectedState = { 136 | [types.metaTypes.messages]: { inProgress: false, error: '', items }, 137 | } 138 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 139 | }) 140 | 141 | test(types.firebase.FIREBASE_LISTEN_CHILD_ADDED, () => { 142 | const initialState = { 143 | [types.metaTypes.messages]: { 144 | inProgress: false, 145 | error: '', 146 | items: { '1': { text: 'hello' }, '2': { text: 'world' } }, 147 | }, 148 | } 149 | const child_id = '3' 150 | const child = { text: 'goodbye' } 151 | const action = actions.firebaseListenChildAdded( 152 | child_id, 153 | child, 154 | types.metaTypes.messages 155 | ) 156 | const expectedState = { 157 | [types.metaTypes.messages]: { 158 | inProgress: false, 159 | error: '', 160 | items: { 161 | '1': { text: 'hello' }, 162 | '2': { text: 'world' }, 163 | '3': { text: 'goodbye' }, 164 | }, 165 | }, 166 | } 167 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 168 | }) 169 | 170 | test(types.firebase.FIREBASE_LISTEN_CHILD_CHANGED, () => { 171 | const initialState = { 172 | [types.metaTypes.messages]: { 173 | inProgress: false, 174 | error: '', 175 | items: { 176 | '1': { text: 'hello' }, 177 | '2': { text: 'world' }, 178 | '3': { text: 'goodbye' }, 179 | }, 180 | }, 181 | } 182 | const child_id = '3' 183 | const child = { text: 'ciao' } 184 | const action = actions.firebaseListenChildChanged( 185 | child_id, 186 | child, 187 | types.metaTypes.messages 188 | ) 189 | const expectedState = { 190 | [types.metaTypes.messages]: { 191 | inProgress: false, 192 | error: '', 193 | items: { 194 | '1': { text: 'hello' }, 195 | '2': { text: 'world' }, 196 | '3': { text: 'ciao' }, 197 | }, 198 | }, 199 | } 200 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 201 | }) 202 | 203 | test(types.firebase.FIREBASE_LISTEN_CHILD_REMOVED, () => { 204 | const initialState = { 205 | [types.metaTypes.messages]: { 206 | inProgress: false, 207 | error: '', 208 | items: { '1': { text: 'hello' }, '2': { text: 'world' } }, 209 | }, 210 | } 211 | const child_id = '2' 212 | const action = actions.firebaseListenChildRemoved( 213 | child_id, 214 | types.metaTypes.messages 215 | ) 216 | const expectedState = { 217 | [types.metaTypes.messages]: { 218 | inProgress: false, 219 | error: '', 220 | items: { '1': { text: 'hello' } }, 221 | }, 222 | } 223 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 224 | }) 225 | 226 | test(types.firebase.FIREBASE_LISTEN_REMOVED + ' clear items false', () => { 227 | const initialState = { 228 | [types.metaTypes.messages]: { 229 | inProgress: false, 230 | error: '', 231 | items: { '1': { text: 'hello' }, '2': { text: 'world' } }, 232 | }, 233 | } 234 | const action = actions.firebaseListenRemoved( 235 | false, 236 | types.metaTypes.messages 237 | ) 238 | const expectedState = { 239 | [types.metaTypes.messages]: { 240 | inProgress: false, 241 | error: '', 242 | items: { '1': { text: 'hello' }, '2': { text: 'world' } }, 243 | }, 244 | } 245 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 246 | }) 247 | 248 | test(types.firebase.FIREBASE_LISTEN_REMOVED + ' clear items true', () => { 249 | const initialState = { 250 | [types.metaTypes.messages]: { 251 | inProgress: false, 252 | error: '', 253 | items: { '1': { text: 'hello' }, '2': { text: 'world' } }, 254 | }, 255 | } 256 | const action = actions.firebaseListenRemoved(true, types.metaTypes.messages) 257 | const expectedState = { 258 | [types.metaTypes.messages]: { 259 | inProgress: false, 260 | error: '', 261 | items: {}, 262 | }, 263 | } 264 | expect(firebaseReducer(initialState, action)).toEqual(expectedState) 265 | }) 266 | 267 | test('bogus action does nothing', () => { 268 | const initialState = { 269 | [types.metaTypes.messages]: { 270 | inProgress: false, 271 | error: '', 272 | items: { '1': { text: 'hello' }, '2': { text: 'world' } }, 273 | }, 274 | } 275 | const action = { 276 | type: 'DO_NOT_TOUCH_STATE_ACTION', 277 | } 278 | expect(firebaseReducer(initialState, action)).toBe(initialState) 279 | }) 280 | 281 | test('no initial state', () => { 282 | const action = { 283 | type: 'DO_NOT_TOUCH_STATE_ACTION', 284 | } 285 | expect(firebaseReducer(undefined, action)).toEqual(getInitialState()) 286 | }) 287 | }) 288 | -------------------------------------------------------------------------------- /__tests__/sagas.js: -------------------------------------------------------------------------------- 1 | import * as sagas from '../sagas' 2 | import * as types from '../types' 3 | import firebase from '../firebase' 4 | import * as actions from '../actions' 5 | import { metaTypes, eventTypes } from '../types' 6 | import { cloneableGenerator, createMockTask } from 'redux-saga/utils' 7 | import { put, take, call, fork, cancel, flush } from 'redux-saga/effects' 8 | 9 | describe('database saga', () => { 10 | test(`watchUpdateRequested ${metaTypes.userContacts}`, () => { 11 | const generator = sagas.watchUpdateRequested() 12 | const updates = { updates: { a: '1', b: '2' } } 13 | const action = actions.updateUserContactsRequested() 14 | const selector = sagas.getUserContactsUpdates 15 | 16 | expect(generator.next().value).toEqual( 17 | take(types.firebase.FIREBASE_UPDATE_REQUESTED) 18 | ) 19 | expect(generator.next(action).value).toEqual(call(selector, action.payload)) 20 | expect(generator.next(updates).value).toEqual( 21 | fork(sagas.updateItems, updates, action.meta.type) 22 | ) 23 | }) 24 | 25 | test('watchUpdateRequested unknownType', () => { 26 | const generator = sagas.watchUpdateRequested() 27 | 28 | //test non function case 29 | expect(generator.next().value).toEqual( 30 | take(types.firebase.FIREBASE_UPDATE_REQUESTED) 31 | ) 32 | expect(generator.next({ meta: { type: 'unknownType' } }).value).toEqual( 33 | take(types.firebase.FIREBASE_UPDATE_REQUESTED) 34 | ) 35 | }) 36 | 37 | test(`watchRemoveRequested ${metaTypes.userContacts}`, () => { 38 | const generator = sagas.watchRemoveRequested() 39 | const path = 'a/b/c' 40 | 41 | const action = actions.removeUserContactsRequested() 42 | const selector = sagas.getUserContactsPath 43 | 44 | expect(generator.next().value).toEqual( 45 | take(types.firebase.FIREBASE_REMOVE_REQUESTED) 46 | ) 47 | expect(generator.next(action).value).toEqual(call(selector, action.payload)) 48 | expect(generator.next(path).value).toEqual( 49 | fork(sagas.removeItem, path, action.meta.type) 50 | ) 51 | expect(generator.next({ meta: { type: 'unknownType' } }).value).toEqual( 52 | take(types.firebase.FIREBASE_REMOVE_REQUESTED) 53 | ) 54 | }) 55 | 56 | test('watchRemoveRequested unknownType', () => { 57 | const generator = sagas.watchRemoveRequested() 58 | 59 | //test non function case 60 | expect(generator.next().value).toEqual( 61 | take(types.firebase.FIREBASE_REMOVE_REQUESTED) 62 | ) 63 | expect(generator.next({ meta: { type: 'unknownType' } }).value).toEqual( 64 | take(types.firebase.FIREBASE_REMOVE_REQUESTED) 65 | ) 66 | }) 67 | 68 | test('watchListener', () => { 69 | const checkedMetaType = metaTypes.messages 70 | const unwantedMetaType = metaTypes.userContacts 71 | 72 | const generator = cloneableGenerator(sagas.watchListener)(checkedMetaType) 73 | 74 | expect(generator.next().value).toEqual( 75 | take(types.firebase.FIREBASE_LISTEN_REQUESTED) 76 | ) 77 | 78 | const regularGenerator = generator.clone() 79 | const checkedListenRequestAction = actions.listenToMessages() 80 | const checkedListenRemoveAction = actions.removeMessagesListenerRequested() 81 | const unwantedListenRequestAction = actions.listenToUserContacts() 82 | const unwantedListenRemoveAction = actions.removeUserContactsListenerRequested() 83 | const ref = checkedListenRequestAction.payload.ref 84 | const mockTask = createMockTask() 85 | 86 | //regular flow 87 | expect(regularGenerator.next(checkedListenRequestAction).value).toEqual( 88 | fork( 89 | sagas.getDataAndListenToChannel, 90 | ref, 91 | checkedListenRequestAction.meta.type 92 | ) 93 | ) 94 | 95 | expect(regularGenerator.next(mockTask).value).toEqual( 96 | take([ 97 | types.firebase.FIREBASE_REMOVE_LISTENER_REQUESTED, 98 | types.firebase.FIREBASE_LISTEN_REQUESTED, 99 | types.firebase.FIREBASE_REMOVE_ALL_LISTENERS_REQUESTED, 100 | ]) 101 | ) 102 | 103 | const regularWithUnwantedRemoveMetaType = regularGenerator.clone() 104 | const regularWithListenActionGenerator = regularGenerator.clone() 105 | 106 | expect(regularGenerator.next(checkedListenRemoveAction).value).toEqual( 107 | cancel(mockTask) 108 | ) 109 | 110 | expect(regularGenerator.next().value).toEqual( 111 | put( 112 | actions.firebaseListenRemoved( 113 | checkedListenRemoveAction.payload.clearItems, 114 | checkedMetaType 115 | ) 116 | ) 117 | ) 118 | 119 | expect(regularGenerator.next().value).toEqual( 120 | take(types.firebase.FIREBASE_LISTEN_REQUESTED) 121 | ) //back to start 122 | 123 | //unwanted listen request flow 124 | const unwantedListenRequestActionGenerator = generator.clone() 125 | expect( 126 | unwantedListenRequestActionGenerator.next(unwantedListenRequestAction) 127 | .value 128 | ).toEqual(take(types.firebase.FIREBASE_LISTEN_REQUESTED)) // unwatned action - go to start 129 | 130 | //unwanted remove request while waiting to specifig cancel request 131 | expect( 132 | regularWithUnwantedRemoveMetaType.next(unwantedListenRemoveAction).value 133 | ).toEqual( 134 | take([ 135 | types.firebase.FIREBASE_REMOVE_LISTENER_REQUESTED, 136 | types.firebase.FIREBASE_LISTEN_REQUESTED, 137 | types.firebase.FIREBASE_REMOVE_ALL_LISTENERS_REQUESTED, 138 | ]) 139 | ) //contintue to wait 140 | 141 | //regualr with listen aciton 142 | expect( 143 | regularWithListenActionGenerator.next(checkedListenRequestAction).value 144 | ).toEqual(cancel(mockTask)) 145 | expect(regularWithListenActionGenerator.next().value).toEqual( 146 | put(actions.firebaseListenRemoved(false, checkedMetaType)) 147 | ) 148 | expect(regularWithListenActionGenerator.next().value).toEqual( 149 | fork( 150 | sagas.getDataAndListenToChannel, 151 | checkedListenRemoveAction.payload.ref, 152 | checkedMetaType 153 | ) 154 | ) 155 | 156 | expect(regularWithListenActionGenerator.next().value).toEqual( 157 | take([ 158 | types.firebase.FIREBASE_REMOVE_LISTENER_REQUESTED, 159 | types.firebase.FIREBASE_LISTEN_REQUESTED, 160 | types.firebase.FIREBASE_REMOVE_ALL_LISTENERS_REQUESTED, 161 | ]) 162 | ) //contintue to wait 163 | }) 164 | 165 | test('getDataAndListenToChannel', () => { 166 | const ref = firebase.database().ref() 167 | const chan = sagas.createEventChannel(ref) 168 | const metaType = metaTypes.offeringsCategories 169 | const value = 'Data from database' 170 | const snap = { val: () => value } 171 | 172 | const generator = cloneableGenerator(sagas.getDataAndListenToChannel)( 173 | ref, 174 | metaType 175 | ) 176 | expect(generator.next().value).toEqual(call(sagas.createEventChannel, ref)) 177 | expect(generator.next(chan).value).toEqual(call([ref, ref.once], 'value')) 178 | 179 | const failureGenerator = generator.clone() 180 | 181 | //regular flow 182 | expect(generator.next(snap).value).toEqual(flush(chan)) 183 | expect(generator.next().value).toEqual( 184 | put(actions.firebaseListenFulfilled(value, metaType)) 185 | ) 186 | expect(generator.next().value).toEqual(take(chan)) 187 | const data = { 188 | eventType: eventTypes.CHILD_ADDED, 189 | key: '1', 190 | value: 'Data from channel', 191 | } 192 | expect(generator.next(data).value).toEqual( 193 | put(sagas.getUpdateAction(data, metaType)) 194 | ) 195 | expect(generator.next().value).toEqual(take(chan)) //return to listen to the channel 196 | generator.return().value //simulate cancellation 197 | 198 | //failure flow 199 | const error = new Error('An error occured') 200 | expect(failureGenerator.throw(error).value).toEqual( 201 | put(actions.firebaseListenRejected(error, metaType)) 202 | ) 203 | expect(failureGenerator.next().value).toEqual(take(chan)) //listen to the channel 204 | }) 205 | 206 | test('getDataAndListenToChannel null value', () => { 207 | const ref = firebase.database().ref() 208 | const chan = sagas.createEventChannel(ref) 209 | const metaType = metaTypes.offeringsCategories 210 | const snap = { val: () => null } 211 | 212 | const generator = cloneableGenerator(sagas.getDataAndListenToChannel)( 213 | ref, 214 | metaType 215 | ) 216 | expect(generator.next().value).toEqual(call(sagas.createEventChannel, ref)) 217 | expect(generator.next(chan).value).toEqual(call([ref, ref.once], 'value')) 218 | 219 | //regular flow 220 | expect(generator.next(snap).value).toEqual(flush(chan)) 221 | expect(generator.next().value).toEqual( 222 | put(actions.firebaseListenFulfilled({}, metaType)) 223 | ) 224 | expect(generator.next().value).toEqual(take(chan)) 225 | const data = { 226 | eventType: eventTypes.CHILD_ADDED, 227 | key: '1', 228 | value: 'Data from channel', 229 | } 230 | expect(generator.next(data).value).toEqual( 231 | put(sagas.getUpdateAction(data, metaType)) 232 | ) 233 | expect(generator.next().value).toEqual(take(chan)) //return to listen to the channel 234 | generator.return().value //simulate cancellation 235 | }) 236 | 237 | test('getUpdateAction CHILD_ADDED', () => { 238 | const metaType = metaTypes.offeringsCategories 239 | const childAddedData = { 240 | eventType: eventTypes.CHILD_ADDED, 241 | key: '1', 242 | value: 'Data from channel', 243 | } 244 | 245 | expect(sagas.getUpdateAction(childAddedData, metaType)).toEqual( 246 | actions.firebaseListenChildAdded( 247 | childAddedData.key, 248 | childAddedData.value, 249 | metaType 250 | ) 251 | ) 252 | }) 253 | 254 | test('getUpdateAction CHILD_CHANGED', () => { 255 | const metaType = metaTypes.offeringsCategories 256 | const childChangedData = { 257 | eventType: eventTypes.CHILD_CHANGED, 258 | key: '1', 259 | value: 'Data from channel', 260 | } 261 | 262 | expect(sagas.getUpdateAction(childChangedData, metaType)).toEqual( 263 | actions.firebaseListenChildChanged( 264 | childChangedData.key, 265 | childChangedData.value, 266 | metaType 267 | ) 268 | ) 269 | }) 270 | 271 | test('getUpdateAction CHILD_REMOVED', () => { 272 | const metaType = metaTypes.offeringsCategories 273 | const childRemovedData = { 274 | eventType: eventTypes.CHILD_REMOVED, 275 | key: '1', 276 | } 277 | 278 | expect(sagas.getUpdateAction(childRemovedData, metaType)).toEqual( 279 | actions.firebaseListenChildRemoved(childRemovedData.key, metaType) 280 | ) 281 | }) 282 | 283 | test('updateItems - regular stream - success and failure', () => { 284 | const updates = { x: true } 285 | const metaType = 'someType' 286 | const ref = firebase.database().ref() 287 | const generator = cloneableGenerator(sagas.updateItems)(updates, metaType) 288 | expect(generator.next().value).toEqual(call([ref, ref.update], updates)) 289 | 290 | const successGenerator = generator.clone() 291 | expect(successGenerator.next().value).toEqual( 292 | put(actions.firebaseUpdateFulfilled(metaType)) 293 | ) 294 | expect(successGenerator.next().done).toEqual(true) 295 | 296 | const failGenerator = generator.clone() 297 | const error = new Error('An error occured') 298 | expect(failGenerator.throw(error).value).toEqual( 299 | put(actions.firebaseUpdateRejected(error, metaType)) 300 | ) 301 | expect(failGenerator.next().done).toEqual(true) 302 | }) 303 | 304 | test('removeItem - regular stream - success and failure', () => { 305 | const path = 'a/b/c' 306 | const metaType = 'someType' 307 | const ref = firebase.database().ref(path) 308 | const generator = cloneableGenerator(sagas.removeItem)(path, metaType) 309 | expect(generator.next().value).toEqual(call([ref, ref.remove])) 310 | 311 | const successGenerator = generator.clone() 312 | expect(successGenerator.next().value).toEqual( 313 | put(actions.firebaseRemoveFulfilled(metaType)) 314 | ) 315 | expect(successGenerator.next().done).toEqual(true) 316 | 317 | const failGenerator = generator.clone() 318 | const error = new Error('An error occured') 319 | expect(failGenerator.throw(error).value).toEqual( 320 | put(actions.firebaseRemoveRejected(error, metaType)) 321 | ) 322 | expect(failGenerator.next().done).toEqual(true) 323 | }) 324 | 325 | test('getUpdateOfferingUpdates', () => { 326 | const uid = '1' 327 | const contactId = '123' 328 | const name = 'John Doe' 329 | const phone = '123456789' 330 | 331 | const updates = { 332 | [`users/${uid}/contacts/${contactId}/name`]: name, 333 | [`users/${uid}/contacts/${contactId}/phone`]: phone, 334 | } 335 | 336 | expect( 337 | sagas.getUserContactsUpdates({ uid, contactId, name, phone }) 338 | ).toEqual(updates) 339 | }) 340 | 341 | test('getUserContactsPath', () => { 342 | const uid = '1' 343 | const contactId = '123' 344 | const path = `users/${uid}/contacts/${contactId}` 345 | expect(sagas.getUserContactsPath({ uid, contactId })).toEqual(path) 346 | }) 347 | }) 348 | -------------------------------------------------------------------------------- /actions.js: -------------------------------------------------------------------------------- 1 | import firebase from './firebase' 2 | import * as types from './types' 3 | import { metaTypes } from './types' 4 | 5 | export function firebaseListenRequested(ref, metaType) { 6 | return { 7 | type: types.firebase.FIREBASE_LISTEN_REQUESTED, 8 | payload: ref, 9 | meta: { type: metaType }, 10 | } 11 | } 12 | 13 | export function firebaseListenRejected(error, metaType) { 14 | return { 15 | type: types.firebase.FIREBASE_LISTEN_REJECTED, 16 | payload: { error }, 17 | meta: { type: metaType }, 18 | } 19 | } 20 | 21 | export function firebaseListenFulfilled(items, metaType) { 22 | return { 23 | type: types.firebase.FIREBASE_LISTEN_FULFILLED, 24 | payload: { items }, 25 | meta: { type: metaType }, 26 | } 27 | } 28 | 29 | export function firebaseListenChildAdded(id, value, metaType) { 30 | return { 31 | type: types.firebase.FIREBASE_LISTEN_CHILD_ADDED, 32 | payload: { id, value }, 33 | meta: { type: metaType }, 34 | } 35 | } 36 | 37 | export function firebaseListenChildChanged(id, value, metaType) { 38 | return { 39 | type: types.firebase.FIREBASE_LISTEN_CHILD_CHANGED, 40 | payload: { id, value }, 41 | meta: { type: metaType }, 42 | } 43 | } 44 | 45 | export function firebaseListenChildRemoved(id, metaType) { 46 | return { 47 | type: types.firebase.FIREBASE_LISTEN_CHILD_REMOVED, 48 | payload: { id }, 49 | meta: { type: metaType }, 50 | } 51 | } 52 | 53 | export function firebaseUpdateRequested(payload, metaType) { 54 | return { 55 | type: types.firebase.FIREBASE_UPDATE_REQUESTED, 56 | payload, 57 | meta: { type: metaType }, 58 | } 59 | } 60 | 61 | export function firebaseUpdateRejected(error, metaType) { 62 | return { 63 | type: types.firebase.FIREBASE_UPDATE_REJECTED, 64 | payload: { error }, 65 | meta: { type: metaType }, 66 | } 67 | } 68 | 69 | export function firebaseUpdateFulfilled(metaType) { 70 | return { 71 | type: types.firebase.FIREBASE_UPDATE_FULFILLED, 72 | payload: {}, 73 | meta: { type: metaType }, 74 | } 75 | } 76 | 77 | export function firebaseRemoveRequested(payload, metaType) { 78 | return { 79 | type: types.firebase.FIREBASE_REMOVE_REQUESTED, 80 | payload, 81 | meta: { type: metaType }, 82 | } 83 | } 84 | 85 | export function firebaseRemoveRejected(error, metaType) { 86 | return { 87 | type: types.firebase.FIREBASE_REMOVE_REJECTED, 88 | payload: { error }, 89 | meta: { type: metaType }, 90 | } 91 | } 92 | 93 | export function firebaseRemoveFulfilled(metaType) { 94 | return { 95 | type: types.firebase.FIREBASE_REMOVE_FULFILLED, 96 | payload: {}, 97 | meta: { type: metaType }, 98 | } 99 | } 100 | 101 | export function firebaseListenRemoved(clearItems, metaType) { 102 | return { 103 | type: types.firebase.FIREBASE_LISTEN_REMOVED, 104 | payload: { clearItems }, 105 | meta: { type: metaType }, 106 | } 107 | } 108 | 109 | export function firebaseRemoveListenerRequested(clearItems, metaType) { 110 | return { 111 | type: types.firebase.FIREBASE_REMOVE_LISTENER_REQUESTED, 112 | payload: { clearItems }, 113 | meta: { type: metaType }, 114 | } 115 | } 116 | 117 | export function firebaseRemoveAllListenersRequested() { 118 | return { 119 | type: types.firebase.FIREBASE_REMOVE_ALL_LISTENERS_REQUESTED, 120 | payload: { clearItems: true }, 121 | } 122 | } 123 | 124 | export function listenToMessages() { 125 | const ref = firebase.database().ref('messages') 126 | return firebaseListenRequested(ref, metaTypes.messages) 127 | } 128 | 129 | export function listenToUserContacts(uid) { 130 | const ref = firebase.database().ref(`users/${uid}/contacts`) 131 | return firebaseListenRequested(ref, metaTypes.userContacts) 132 | } 133 | 134 | export function removeMessagesListenerRequested() { 135 | return firebaseRemoveListenerRequested(false, metaTypes.messages) 136 | } 137 | 138 | export function removeUserContactsListenerRequested() { 139 | return firebaseRemoveListenerRequested(false, metaTypes.userContacts) 140 | } 141 | 142 | export function updateUserContactsRequested(uid, contactId, name, phone) { 143 | return firebaseUpdateRequested( 144 | { uid, contactId, name, phone }, 145 | metaTypes.userContacts 146 | ) 147 | } 148 | 149 | export function removeUserContactsRequested(uid, contactId) { 150 | return firebaseRemoveRequested({ uid, contactId }, metaTypes.userContacts) 151 | } 152 | -------------------------------------------------------------------------------- /createReducer.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | export default function createReducer(initialState: Object, handlers: Object) { 4 | return function reducer( 5 | state: Object = initialState, 6 | action: { type: string } 7 | ) { 8 | if (handlers.hasOwnProperty(action.type)) { 9 | return handlers[action.type](state, action) 10 | } else { 11 | return state 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /firebase.js: -------------------------------------------------------------------------------- 1 | import RNFirebase from 'react-native-firebase' 2 | 3 | const configurationOptions = { 4 | debug: false, 5 | } 6 | 7 | firebase = RNFirebase.initializeApp(configurationOptions) 8 | 9 | export default firebase 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "jest", 4 | "test:watch": "jest --watch", 5 | "coverage": "jest --coverage", 6 | "coverage:watch": "jest --coverage --watch" 7 | }, 8 | "devDependencies": { 9 | "babel-cli": "^6.24.1", 10 | "babel-jest": "^20.0.3", 11 | "babel-preset-flow": "^6.23.0", 12 | "jest": "^20.0.4", 13 | "jest-cli": "^20.0.4", 14 | "redux-mock-store": "^1.2.3" 15 | }, 16 | "dependencies": { 17 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 18 | "babel-preset-react-native": "^1.9.2", 19 | "react-native-firebase": "^1.1.0", 20 | "redux": "^3.6.0", 21 | "redux-saga": "^0.15.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /patterns/listener_actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './types' 2 | import { metaTypes } from './types' 3 | 4 | export function firebaseListenRequested(ref, metaType) { 5 | return { 6 | type: types.firebase.FIREBASE_LISTEN_REQUESTED, 7 | payload: ref, 8 | meta: { type: metaType }, 9 | } 10 | } 11 | 12 | export function firebaseListenRejected(error, metaType) { 13 | return { 14 | type: types.firebase.FIREBASE_LISTEN_REJECTED, 15 | payload: { error }, 16 | meta: { type: metaType }, 17 | } 18 | } 19 | 20 | export function firebaseListenFulfilled(items, metaType) { 21 | return { 22 | type: types.firebase.FIREBASE_LISTEN_FULFILLED, 23 | payload: { items }, 24 | meta: { type: metaType }, 25 | } 26 | } 27 | 28 | export function firebaseListenChildAdded(id, value, metaType) { 29 | return { 30 | type: types.firebase.FIREBASE_LISTEN_CHILD_ADDED, 31 | payload: { id, value }, 32 | meta: { type: metaType }, 33 | } 34 | } 35 | 36 | export function firebaseListenRemoved(clearItems, metaType) { 37 | return { 38 | type: types.firebase.FIREBASE_LISTEN_REMOVED, 39 | payload: { clearItems }, 40 | meta: { type: metaType }, 41 | } 42 | } 43 | 44 | export function firebaseRemoveListenerRequested(clearItems, metaType) { 45 | return { 46 | type: types.firebase.FIREBASE_REMOVE_LISTENER_REQUESTED, 47 | payload: { clearItems }, 48 | meta: { type: metaType }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /patterns/listener_reducer.js: -------------------------------------------------------------------------------- 1 | import * as types from './types' 2 | import { metaTypes } from './types' 3 | //... 4 | 5 | [types.firebase.FIREBASE_LISTEN_REQUESTED](state, action) { 6 | const property = action.meta.type 7 | const propertyState = state[property] 8 | 9 | let newState = { 10 | ...state, 11 | [property]: { ...propertyState, inProgress: true, error: '' }, 12 | } 13 | return newState 14 | }, 15 | [types.firebase.FIREBASE_LISTEN_FULFILLED](state, action) { 16 | const items = action.payload.items 17 | const property = action.meta.type 18 | const propertyState = state[property] 19 | 20 | let newState = { 21 | ...state, 22 | [property]: { ...propertyState, inProgress: false, error: '', items }, 23 | } 24 | return newState 25 | }, 26 | [types.firebase.FIREBASE_LISTEN_REJECTED](state, action) { 27 | const property = action.meta.type 28 | const propertyState = state[property] 29 | const error = action.payload.error 30 | 31 | let newState = { 32 | ...state, 33 | [property]: { ...propertyState, inProgress: false, error }, 34 | } 35 | return newState 36 | }, 37 | //notice child added and changed are the same at the moment 38 | [types.firebase.FIREBASE_LISTEN_CHILD_ADDED](state, action) { 39 | const property = action.meta.type 40 | const propertyState = state[property] 41 | const items = { 42 | ...propertyState.items, 43 | [action.payload.id]: action.payload.value, 44 | } 45 | 46 | let newState = { 47 | ...state, 48 | [property]: { ...propertyState, inProgress: false, error: '', items }, 49 | } 50 | return newState 51 | }, 52 | [types.firebase.FIREBASE_LISTEN_REMOVED](state, action) { 53 | const property = action.meta.type 54 | const propertyState = state[property] 55 | const items = action.payload.clearItems ? {} : propertyState.items 56 | 57 | let newState = { 58 | ...state, 59 | [property]: { ...propertyState, inProgress: false, error: '', items }, 60 | } 61 | return newState 62 | }, 63 | -------------------------------------------------------------------------------- /patterns/listener_sagas.js: -------------------------------------------------------------------------------- 1 | import * as types from './types' 2 | import { metaTypes, eventTypes } from './types' 3 | import * as actions from './actions' 4 | import firebase from './firebase' 5 | import { eventChannel, buffers } from 'redux-saga' 6 | import { put, take, call, fork, cancel, flush } from 'redux-saga/effects' 7 | 8 | export function* watchListener(metaType) { 9 | while (true) { 10 | const listenRequestAction = yield take( 11 | types.firebase.FIREBASE_LISTEN_REQUESTED 12 | ) 13 | if (listenRequestAction.meta.type === metaType) { 14 | let task = yield fork( 15 | getDataAndListenToChannel, 16 | listenRequestAction.payload.ref, 17 | metaType 18 | ) 19 | while (true) { 20 | const action = yield take([ 21 | types.firebase.FIREBASE_REMOVE_LISTENER_REQUESTED, 22 | types.firebase.FIREBASE_LISTEN_REQUESTED, 23 | ]) 24 | 25 | if (action.meta.type === metaType) { 26 | yield cancel(task) 27 | yield put( 28 | actions.firebaseListenRemoved(!!action.payload.clearItems, metaType) 29 | ) 30 | 31 | if (action.type === types.firebase.FIREBASE_LISTEN_REQUESTED) { 32 | task = yield fork( 33 | getDataAndListenToChannel, 34 | action.payload.ref, 35 | metaType 36 | ) 37 | } else { 38 | break 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | export function createEventChannel(ref) { 47 | const listener = eventChannel(emit => { 48 | ref.on('child_added', snap => { 49 | emit({ 50 | eventType: eventTypes.CHILD_ADDED, 51 | key: snap.key, 52 | value: snap.val(), 53 | }) 54 | }) 55 | return () => { 56 | ref.off() 57 | } 58 | }, buffers.expanding(1)) 59 | return listener 60 | } 61 | 62 | export function* getDataAndListenToChannel(ref, metaType) { 63 | const chan = yield call(createEventChannel, ref) 64 | try { 65 | try { 66 | const snap = yield call([ref, ref.once], 'value') 67 | yield flush(chan) 68 | const val = snap.val() 69 | const value = val ? val : {} 70 | yield put(actions.firebaseListenFulfilled(value, metaType)) 71 | } catch (error) { 72 | yield put(actions.firebaseListenRejected(error, metaType)) 73 | } 74 | while (true) { 75 | const data = yield take(chan) 76 | yield put( 77 | actions.firebaseListenChildAdded(data.key, data.value, metaType) 78 | ) 79 | } 80 | } finally { 81 | chan.close() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /patterns/listener_wrapper_functions.js: -------------------------------------------------------------------------------- 1 | export function listenToUserContacts(uid) { 2 | const ref = firebase.database().ref(`users/${uid}/contacts`) 3 | return firebaseListenRequested(ref, metaTypes.userContacts) 4 | } 5 | 6 | export function removeUserContactsListenerRequested() { 7 | return firebaseRemoveListenerRequested(false, metaTypes.userContacts) 8 | } 9 | -------------------------------------------------------------------------------- /patterns/remove_actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './types' 2 | import { metaTypes } from './types' 3 | 4 | export function firebaseRemoveRequested(payload, metaType) { 5 | return { 6 | type: types.firebase.FIREBASE_REMOVE_REQUESTED, 7 | payload, 8 | meta: { type: metaType }, 9 | } 10 | } 11 | 12 | export function firebaseRemoveRejected(error, metaType) { 13 | return { 14 | type: types.firebase.FIREBASE_REMOVE_REJECTED, 15 | payload: { error }, 16 | meta: { type: metaType }, 17 | } 18 | } 19 | 20 | export function firebaseRemoveFulfilled(metaType) { 21 | return { 22 | type: types.firebase.FIREBASE_REMOVE_FULFILLED, 23 | payload: {}, 24 | meta: { type: metaType }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /patterns/remove_reducer.js: -------------------------------------------------------------------------------- 1 | import * as types from './types' 2 | import { metaTypes } from './types' 3 | 4 | //... 5 | 6 | [types.firebase.FIREBASE_REMOVE_REQUESTED](state, action) { 7 | const property = action.meta.type 8 | let newState = { ...state, [property]: { inProgress: true, error: '' } } 9 | return newState 10 | }, 11 | [types.firebase.FIREBASE_REMOVE_FULFILLED](state, action) { 12 | const property = action.meta.type 13 | let newState = { ...state, [property]: { inProgress: false, error: '' } } 14 | return newState 15 | }, 16 | [types.firebase.FIREBASE_REMOVE_REJECTED](state, action) { 17 | const property = action.meta.type 18 | const error = action.payload.error 19 | let newState = { ...state, [property]: { inProgress: false, error } } 20 | return newState 21 | }, 22 | -------------------------------------------------------------------------------- /patterns/remove_sagas.js: -------------------------------------------------------------------------------- 1 | import * as types from './types' 2 | import { metaTypes } from './types' 3 | import * as actions from './actions' 4 | import firebase from './firebase' 5 | import { put, take, call, fork } from 'redux-saga/effects' 6 | 7 | export function* watchRemoveRequested() { 8 | while (true) { 9 | const action = yield take(types.firebase.FIREBASE_REMOVE_REQUESTED) 10 | let getPath = null 11 | switch (action.meta.type) { 12 | case metaTypes.userContacts: 13 | getPath = getUserContactsPath 14 | break 15 | } 16 | 17 | if (typeof getPath === 'function') { 18 | const path = yield call(getPath, action.payload) 19 | yield fork(removeItem, path, action.meta.type) 20 | } 21 | } 22 | } 23 | 24 | export function getUserContactsPath({ uid, contactId }) { 25 | return `users/${uid}/contacts/${contactId}` 26 | } 27 | 28 | export function* removeItem(path, metaType) { 29 | try { 30 | const ref = firebase.database().ref(path) 31 | yield call([ref, ref.remove]) 32 | yield put(actions.firebaseRemoveFulfilled(metaType)) 33 | } catch (error) { 34 | yield put(actions.firebaseRemoveRejected(error, metaType)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /patterns/remove_wrapper_function.js: -------------------------------------------------------------------------------- 1 | export function removeUserContactsRequested(uid, contactId) { 2 | return firebaseRemoveRequested({ uid, contactId }, metaTypes.userContacts) 3 | } 4 | -------------------------------------------------------------------------------- /patterns/sagas_registration.js: -------------------------------------------------------------------------------- 1 | import metaTypes from './types' 2 | import sagas from './sagas' 3 | import { all } from 'redux-saga/effects' 4 | 5 | export default function* rootSaga() { 6 | yield all([ 7 | sagas.watchListener(metaTypes.userContacts), 8 | sagas.watchUpdateRequested(), 9 | ]) 10 | } 11 | -------------------------------------------------------------------------------- /patterns/update_action_test.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions' 2 | import * as types from '../types' 3 | import { metaTypes } from '../types' 4 | 5 | test('updateUserContactsRequested', () => { 6 | const uid = '1' 7 | const contactId = '123' 8 | const name = 'John Doe' 9 | const phone = '123456789' 10 | const expectedAction = { 11 | type: types.firebase.FIREBASE_UPDATE_REQUESTED, 12 | payload: { uid, contactId, name, phone }, 13 | meta: { type: metaTypes.userContacts }, 14 | } 15 | expect( 16 | actions.updateUserContactsRequested(uid, contactId, name, phone) 17 | ).toEqual(expectedAction) 18 | }) 19 | -------------------------------------------------------------------------------- /patterns/update_actions.js: -------------------------------------------------------------------------------- 1 | import * as types from './types' 2 | import { metaTypes } from './types' 3 | 4 | export function firebaseUpdateRequested(payload, metaType) { 5 | return { 6 | type: types.firebase.FIREBASE_UPDATE_REQUESTED, 7 | payload, 8 | meta: { type: metaType }, 9 | } 10 | } 11 | 12 | export function firebaseUpdateRejected(error, metaType) { 13 | return { 14 | type: types.firebase.FIREBASE_UPDATE_REJECTED, 15 | payload: { error }, 16 | meta: { type: metaType }, 17 | } 18 | } 19 | 20 | export function firebaseUpdateFulfilled(metaType) { 21 | return { 22 | type: types.firebase.FIREBASE_UPDATE_FULFILLED, 23 | payload: {}, 24 | meta: { type: metaType }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /patterns/update_reducer.js: -------------------------------------------------------------------------------- 1 | import * as types from './types' 2 | import { metaTypes } from './types' 3 | 4 | //... 5 | 6 | [types.firebase.FIREBASE_UPDATE_REQUESTED](state, action) { 7 | const property = action.meta.type 8 | let newState = { ...state, [property]: { inProgress: true, error: '' } } 9 | return newState 10 | }, 11 | [types.firebase.FIREBASE_UPDATE_FULFILLED](state, action) { 12 | const property = action.meta.type 13 | let newState = { ...state, [property]: { inProgress: false, error: '' } } 14 | return newState 15 | }, 16 | [types.firebase.FIREBASE_UPDATE_REJECTED](state, action) { 17 | const property = action.meta.type 18 | const error = action.payload.error 19 | let newState = { ...state, [property]: { inProgress: false, error } } 20 | return newState 21 | } 22 | -------------------------------------------------------------------------------- /patterns/update_sagas.js: -------------------------------------------------------------------------------- 1 | import * as types from './types' 2 | import { metaTypes } from './types' 3 | import * as actions from './actions' 4 | import firebase from './firebase' 5 | import { put, take, call, fork } from 'redux-saga/effects' 6 | 7 | export function* watchUpdateRequested() { 8 | while (true) { 9 | const action = yield take(types.firebase.FIREBASE_UPDATE_REQUESTED) 10 | let getUpdates = null 11 | switch (action.meta.type) { 12 | case metaTypes.userContacts: 13 | getUpdates = getUserContactsUpdates 14 | break 15 | } 16 | if (typeof getUpdates === 'function') { 17 | const updates = yield call(getUpdates, action.payload) 18 | yield fork(updateItems, updates, action.meta.type) 19 | } 20 | } 21 | } 22 | 23 | export function* updateItems(updates, metaType) { 24 | try { 25 | const ref = firebase.database().ref() 26 | yield call([ref, ref.update], updates) 27 | yield put(actions.firebaseUpdateFulfilled(metaType)) 28 | } catch (error) { 29 | yield put(actions.firebaseUpdateRejected(error, metaType)) 30 | } 31 | } 32 | 33 | export function getUserContactsUpdates({ uid, contactId, name, phone }) { 34 | return { 35 | [`users/${uid}/contacts/${contactId}/name`]: name, 36 | [`users/${uid}/contacts/${contactId}/phone`]: phone, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /patterns/update_sagas_test.js: -------------------------------------------------------------------------------- 1 | import * as sagas from '../sagas' 2 | import * as types from '../types' 3 | import * as actions from '../actions' 4 | import firebase from '../firebase' 5 | import { put, take, call, fork, cancel, flush } from 'redux-saga/effects' 6 | 7 | import { cloneableGenerator } from 'redux-saga/utils' 8 | import { metaTypes } from '../types' 9 | 10 | test(`watchUpdateRequested ${metaTypes.userContacts}`, () => { 11 | const generator = sagas.watchUpdateRequested() 12 | const updates = { updates: { a: '1', b: '2' } } 13 | const action = actions.updateUserContactsRequested() 14 | const selector = sagas.getUserContactsUpdates 15 | 16 | expect(generator.next().value).toEqual( 17 | take(types.firebase.FIREBASE_UPDATE_REQUESTED) 18 | ) 19 | expect(generator.next(action).value).toEqual(call(selector, action.payload)) 20 | expect(generator.next(updates).value).toEqual( 21 | fork(sagas.updateItems, updates, action.meta.type) 22 | ) 23 | }) 24 | 25 | test('watchUpdateRequested unknownType', () => { 26 | const generator = sagas.watchUpdateRequested() 27 | 28 | //test non function case 29 | expect(generator.next().value).toEqual( 30 | take(types.firebase.FIREBASE_UPDATE_REQUESTED) 31 | ) 32 | expect(generator.next({ meta: { type: 'unknownType' } }).value).toEqual( 33 | take(types.firebase.FIREBASE_UPDATE_REQUESTED) 34 | ) 35 | }) 36 | 37 | test('updateItems - regular stream - success and failure', () => { 38 | const updates = { x: true } 39 | const metaType = 'someType' 40 | const ref = firebase.database().ref() 41 | const generator = cloneableGenerator(sagas.updateItems)(updates, metaType) 42 | expect(generator.next().value).toEqual(call([ref, ref.update], updates)) 43 | 44 | const successGenerator = generator.clone() 45 | expect(successGenerator.next().value).toEqual( 46 | put(actions.firebaseUpdateFulfilled(metaType)) 47 | ) 48 | expect(successGenerator.next().done).toEqual(true) 49 | 50 | const failGenerator = generator.clone() 51 | const error = new Error('An error occured') 52 | expect(failGenerator.throw(error).value).toEqual( 53 | put(actions.firebaseUpdateRejected(error, metaType)) 54 | ) 55 | expect(failGenerator.next().done).toEqual(true) 56 | }) 57 | 58 | test('getUpdateOfferingUpdates', () => { 59 | const uid = '1' 60 | const contactId = '123' 61 | const name = 'John Doe' 62 | const phone = '123456789' 63 | 64 | const updates = { 65 | [`users/${uid}/contacts/${contactId}/name`]: name, 66 | [`users/${uid}/contacts/${contactId}/phone`]: phone, 67 | } 68 | 69 | expect(sagas.getUserContactsUpdates({ uid, contactId, name, phone })).toEqual( 70 | updates 71 | ) 72 | }) 73 | -------------------------------------------------------------------------------- /patterns/update_wrapper_function.js: -------------------------------------------------------------------------------- 1 | export function updateUserContactsRequested(uid, contactId, name, phone) { 2 | return firebaseUpdateRequested( 3 | { uid, contactId, name, phone }, 4 | metaTypes.userContacts 5 | ) 6 | } 7 | -------------------------------------------------------------------------------- /reducer.js: -------------------------------------------------------------------------------- 1 | import createReducer from './createReducer' 2 | import * as types from './types' 3 | import { metaTypes } from './types' 4 | 5 | export function getInitialState() { 6 | let state = {} 7 | Object.keys(metaTypes).forEach(key => { 8 | const subState = { inProgress: false, error: '', items: {} } 9 | state[key] = subState 10 | }) 11 | return state 12 | } 13 | 14 | const initialState = getInitialState() 15 | 16 | export const firebaseReducer = createReducer(initialState, { 17 | [types.firebase.FIREBASE_UPDATE_REQUESTED](state, action) { 18 | const property = action.meta.type 19 | let newState = { ...state, [property]: { inProgress: true, error: '' } } 20 | return newState 21 | }, 22 | [types.firebase.FIREBASE_UPDATE_FULFILLED](state, action) { 23 | const property = action.meta.type 24 | let newState = { ...state, [property]: { inProgress: false, error: '' } } 25 | return newState 26 | }, 27 | [types.firebase.FIREBASE_UPDATE_REJECTED](state, action) { 28 | const property = action.meta.type 29 | const error = action.payload.error 30 | let newState = { ...state, [property]: { inProgress: false, error } } 31 | return newState 32 | }, 33 | [types.firebase.FIREBASE_REMOVE_REQUESTED](state, action) { 34 | const property = action.meta.type 35 | let newState = { ...state, [property]: { inProgress: true, error: '' } } 36 | return newState 37 | }, 38 | [types.firebase.FIREBASE_REMOVE_FULFILLED](state, action) { 39 | const property = action.meta.type 40 | let newState = { ...state, [property]: { inProgress: false, error: '' } } 41 | return newState 42 | }, 43 | [types.firebase.FIREBASE_REMOVE_REJECTED](state, action) { 44 | const property = action.meta.type 45 | const error = action.payload.error 46 | let newState = { ...state, [property]: { inProgress: false, error } } 47 | return newState 48 | }, 49 | [types.firebase.FIREBASE_LISTEN_REQUESTED](state, action) { 50 | const property = action.meta.type 51 | const propertyState = state[property] 52 | 53 | let newState = { 54 | ...state, 55 | [property]: { ...propertyState, inProgress: true, error: '' }, 56 | } 57 | return newState 58 | }, 59 | [types.firebase.FIREBASE_LISTEN_FULFILLED](state, action) { 60 | const items = action.payload.items 61 | const property = action.meta.type 62 | const propertyState = state[property] 63 | 64 | let newState = { 65 | ...state, 66 | [property]: { ...propertyState, inProgress: false, error: '', items }, 67 | } 68 | return newState 69 | }, 70 | [types.firebase.FIREBASE_LISTEN_REJECTED](state, action) { 71 | const property = action.meta.type 72 | const propertyState = state[property] 73 | const error = action.payload.error 74 | 75 | let newState = { 76 | ...state, 77 | [property]: { ...propertyState, inProgress: false, error }, 78 | } 79 | return newState 80 | }, 81 | //notice child added and changed are the same at the moment 82 | [types.firebase.FIREBASE_LISTEN_CHILD_ADDED](state, action) { 83 | const property = action.meta.type 84 | const propertyState = state[property] 85 | const items = { 86 | ...propertyState.items, 87 | [action.payload.id]: action.payload.value, 88 | } 89 | 90 | let newState = { 91 | ...state, 92 | [property]: { ...propertyState, inProgress: false, error: '', items }, 93 | } 94 | return newState 95 | }, 96 | [types.firebase.FIREBASE_LISTEN_CHILD_CHANGED](state, action) { 97 | const property = action.meta.type 98 | const propertyState = state[property] 99 | const items = { 100 | ...propertyState.items, 101 | [action.payload.id]: action.payload.value, 102 | } 103 | 104 | let newState = { 105 | ...state, 106 | [property]: { ...propertyState, inProgress: false, error: '', items }, 107 | } 108 | return newState 109 | }, 110 | [types.firebase.FIREBASE_LISTEN_CHILD_REMOVED](state, action) { 111 | const property = action.meta.type 112 | const propertyState = state[property] 113 | const items = { ...propertyState.items } 114 | delete items[action.payload.id] 115 | 116 | let newState = { 117 | ...state, 118 | [property]: { ...propertyState, inProgress: false, error: '', items }, 119 | } 120 | return newState 121 | }, 122 | [types.firebase.FIREBASE_LISTEN_REMOVED](state, action) { 123 | const property = action.meta.type 124 | const propertyState = state[property] 125 | const items = action.payload.clearItems ? {} : propertyState.items 126 | 127 | let newState = { 128 | ...state, 129 | [property]: { ...propertyState, inProgress: false, error: '', items }, 130 | } 131 | return newState 132 | }, 133 | }) 134 | -------------------------------------------------------------------------------- /sagas.js: -------------------------------------------------------------------------------- 1 | import * as types from './types' 2 | import { metaTypes, eventTypes } from './types' 3 | import * as actions from './actions' 4 | import firebase from './firebase' 5 | import { eventChannel, buffers } from 'redux-saga' 6 | import { put, take, call, fork, cancel, flush } from 'redux-saga/effects' 7 | 8 | export function* watchUpdateRequested() { 9 | while (true) { 10 | const action = yield take(types.firebase.FIREBASE_UPDATE_REQUESTED) 11 | let getUpdates = null 12 | switch (action.meta.type) { 13 | case metaTypes.userContacts: 14 | getUpdates = getUserContactsUpdates 15 | break 16 | } 17 | if (typeof getUpdates === 'function') { 18 | const updates = yield call(getUpdates, action.payload) 19 | yield fork(updateItems, updates, action.meta.type) 20 | } 21 | } 22 | } 23 | 24 | export function* watchRemoveRequested() { 25 | while (true) { 26 | const action = yield take(types.firebase.FIREBASE_REMOVE_REQUESTED) 27 | let getPath = null 28 | switch (action.meta.type) { 29 | case metaTypes.userContacts: 30 | getPath = getUserContactsPath 31 | break 32 | } 33 | 34 | if (typeof getPath === 'function') { 35 | const path = yield call(getPath, action.payload) 36 | yield fork(removeItem, path, action.meta.type) 37 | } 38 | } 39 | } 40 | 41 | export function getUserContactsPath({ uid, contactId }) { 42 | return `users/${uid}/contacts/${contactId}` 43 | } 44 | 45 | export function getUserContactsUpdates({ uid, contactId, name, phone }) { 46 | return { 47 | [`users/${uid}/contacts/${contactId}/name`]: name, 48 | [`users/${uid}/contacts/${contactId}/phone`]: phone, 49 | } 50 | } 51 | 52 | export function* updateItems(updates, metaType) { 53 | try { 54 | const ref = firebase.database().ref() 55 | yield call([ref, ref.update], updates) 56 | yield put(actions.firebaseUpdateFulfilled(metaType)) 57 | } catch (error) { 58 | yield put(actions.firebaseUpdateRejected(error, metaType)) 59 | } 60 | } 61 | 62 | export function* removeItem(path, metaType) { 63 | try { 64 | const ref = firebase.database().ref(path) 65 | yield call([ref, ref.remove]) 66 | yield put(actions.firebaseRemoveFulfilled(metaType)) 67 | } catch (error) { 68 | yield put(actions.firebaseRemoveRejected(error, metaType)) 69 | } 70 | } 71 | 72 | export function createEventChannel(ref) { 73 | const listener = eventChannel(emit => { 74 | ref.on('child_added', snap => { 75 | emit({ 76 | eventType: eventTypes.CHILD_ADDED, 77 | key: snap.key, 78 | value: snap.val(), 79 | }) 80 | }) 81 | 82 | ref.on('child_changed', snap => { 83 | const val = snap.val() 84 | emit({ 85 | eventType: eventTypes.CHILD_CHANGED, 86 | key: snap.key, 87 | value: snap.val(), 88 | }) 89 | }) 90 | 91 | ref.on('child_removed', snap => { 92 | emit({ eventType: eventTypes.CHILD_REMOVED, key: snap.key }) 93 | }) 94 | return () => { 95 | ref.off() 96 | } 97 | }, buffers.expanding(1)) 98 | return listener 99 | } 100 | 101 | export function* getDataAndListenToChannel(ref, metaType) { 102 | const chan = yield call(createEventChannel, ref) 103 | try { 104 | try { 105 | const snap = yield call([ref, ref.once], 'value') 106 | yield flush(chan) 107 | const val = snap.val() 108 | const value = val ? val : {} 109 | yield put(actions.firebaseListenFulfilled(value, metaType)) 110 | } catch (error) { 111 | yield put(actions.firebaseListenRejected(error, metaType)) 112 | } 113 | while (true) { 114 | const data = yield take(chan) 115 | yield put(getUpdateAction(data, metaType)) 116 | } 117 | } finally { 118 | chan.close() 119 | } 120 | } 121 | 122 | export function getUpdateAction(data, metaType) { 123 | switch (data.eventType) { 124 | case eventTypes.CHILD_ADDED: 125 | return actions.firebaseListenChildAdded(data.key, data.value, metaType) 126 | case eventTypes.CHILD_CHANGED: 127 | return actions.firebaseListenChildChanged(data.key, data.value, metaType) 128 | case eventTypes.CHILD_REMOVED: 129 | return actions.firebaseListenChildRemoved(data.key, metaType) 130 | } 131 | } 132 | 133 | export function* watchListener(metaType) { 134 | while (true) { 135 | const listenRequestAction = yield take( 136 | types.firebase.FIREBASE_LISTEN_REQUESTED 137 | ) 138 | if (listenRequestAction.meta.type === metaType) { 139 | let task = yield fork( 140 | getDataAndListenToChannel, 141 | listenRequestAction.payload.ref, 142 | metaType 143 | ) 144 | while (true) { 145 | const action = yield take([ 146 | types.firebase.FIREBASE_REMOVE_LISTENER_REQUESTED, 147 | types.firebase.FIREBASE_LISTEN_REQUESTED, 148 | types.firebase.FIREBASE_REMOVE_ALL_LISTENERS_REQUESTED, 149 | ]) 150 | 151 | if ( 152 | action.type === 153 | types.firebase.FIREBASE_REMOVE_ALL_LISTENERS_REQUESTED || 154 | action.meta.type === metaType 155 | ) { 156 | yield cancel(task) 157 | yield put( 158 | actions.firebaseListenRemoved(!!action.payload.clearItems, metaType) 159 | ) 160 | 161 | if (action.type === types.firebase.FIREBASE_LISTEN_REQUESTED) { 162 | task = yield fork( 163 | getDataAndListenToChannel, 164 | action.payload.ref, 165 | metaType 166 | ) 167 | } else { 168 | break 169 | } 170 | } 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /types.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | 3 | export const firebase = { 4 | FIREBASE_UPDATE_REQUESTED: 'FIREBASE_UPDATE_REQUESTED', 5 | FIREBASE_UPDATE_FULFILLED: 'FIREBASE_UPDATE_FULFILLED', 6 | FIREBASE_UPDATE_REJECTED: 'FIREBASE_UPDATE_REJECTED', 7 | 8 | FIREBASE_REMOVE_REQUESTED: 'FIREBASE_REMOVE_REQUESTED', 9 | FIREBASE_REMOVE_FULFILLED: 'FIREBASE_REMOVE_FULFILLED', 10 | FIREBASE_REMOVE_REJECTED: 'FIREBASE_REMOVE_REJECTED', 11 | 12 | FIREBASE_LISTEN_REQUESTED: 'FIREBASE_LISTEN_REQUESTED', 13 | FIREBASE_LISTEN_FULFILLED: 'FIREBASE_LISTEN_FULFILLED', 14 | FIREBASE_LISTEN_REJECTED: 'FIREBASE_LISTEN_REJECTED', 15 | FIREBASE_LISTEN_CHILD_ADDED: 'FIREBASE_LISTEN_CHILD_ADDED', 16 | FIREBASE_LISTEN_CHILD_CHANGED: 'FIREBASE_LISTEN_CHILD_CHANGED', 17 | FIREBASE_LISTEN_CHILD_REMOVED: 'FIREBASE_LISTEN_CHILD_REMOVED', 18 | FIREBASE_LISTEN_REMOVED: 'FIREBASE_LISTEN_REMOVED', 19 | 20 | FIREBASE_REMOVE_LISTENER_REQUESTED: 'FIREBASE_REMOVE_LISTENER_REQUESTED', 21 | FIREBASE_REMOVE_ALL_LISTENERS_REQUESTED: 22 | 'FIREBASE_REMOVE_ALL_LISTENERS_REQUESTED', 23 | } 24 | 25 | export const metaTypes = { 26 | messages: 'messages', 27 | userContacts: 'userContacts', 28 | } 29 | 30 | export const eventTypes = { 31 | CHILD_ADDED: 'CHILD_ADDED', 32 | CHILD_REMOVED: 'CHILD_REMOVED', 33 | CHILD_CHANGED: 'CHILD_CHANGED', 34 | } 35 | --------------------------------------------------------------------------------