Skip to content

Commit d2208ff

Browse files
authored
feat: Added Offline storage support to Event processor for React Native Apps (#517)
Summary: Added Offline storage support to Event Processor for React Native Apps. Stores events when device is offline and then redispatches when the device is online again, new event is ready to be dispatched or SDK is re-initialized. Adds a new eventMaxQueueSize config option which represents number of offline events SDK will store at a time. Deault is 10k for now. Adds a connectivity listener to redispatch events when device is back online. Follow-up changes expected in optimizely-sdk package. Add a new option to EventProcessor constructor to represent maxQueueSize in React Native entry point. Change callback parameter to http status in DefaultEventDispacher for browser entry point which is also used for React Native entry point. Add AsyncStorage module configuration for umd bundles in rollup config. Test plan: Wrote new unit tests Tested offline with FSC suite to make sure it does not break existing event processor functionality. Tested sequence of redispatching events using a local mock server and timestamps.
1 parent 751adac commit d2208ff

18 files changed

+3511
-2035
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Copyright 2020, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
let items: {[key: string]: string} = {}
17+
18+
export default class AsyncStorage {
19+
20+
static getItem(key: string, callback?: (error?: Error, result?: string) => void): Promise<string | null> {
21+
return new Promise(resolve => {
22+
setTimeout(() => resolve(items[key] || null), 1)
23+
})
24+
}
25+
26+
static setItem(key: string, value: string, callback?: (error?: Error) => void): Promise<void> {
27+
return new Promise((resolve) => {
28+
setTimeout(() => {
29+
items[key] = value
30+
resolve()
31+
}, 1)
32+
})
33+
}
34+
35+
static removeItem(key: string, callback?: (error?: Error, result?: string) => void): Promise<string | null> {
36+
return new Promise(resolve => {
37+
setTimeout(() => {
38+
items[key] && delete items[key]
39+
resolve()
40+
}, 1)
41+
})
42+
}
43+
44+
static dumpItems(): {[key: string]: string} {
45+
return items
46+
}
47+
48+
static clearStore(): void {
49+
items = {}
50+
}
51+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright 2020, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
let localCallback: any
17+
18+
export function addEventListener(callback: any) {
19+
localCallback = callback
20+
}
21+
22+
export function triggerInternetState(isInternetReachable: boolean) {
23+
localCallback({ isInternetReachable })
24+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/**
2+
* Copyright 2020, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
/// <reference types="jest" />
17+
18+
import { ReactNativeEventsStore } from '../src/reactNativeEventsStore'
19+
import AsyncStorage from '../__mocks__/@react-native-community/async-storage'
20+
21+
const STORE_KEY = 'test-store'
22+
23+
describe('ReactNativeEventsStore', () => {
24+
let store: ReactNativeEventsStore<any>
25+
26+
beforeEach(() => {
27+
store = new ReactNativeEventsStore(5, STORE_KEY)
28+
})
29+
30+
describe('set', () => {
31+
it('should store all the events correctly in the store', async () => {
32+
await store.set('event1', {'name': 'event1'})
33+
await store.set('event2', {'name': 'event2'})
34+
await store.set('event3', {'name': 'event3'})
35+
await store.set('event4', {'name': 'event4'})
36+
const storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY])
37+
expect(storedPendingEvents).toEqual({
38+
"event1": { "name": "event1" },
39+
"event2": { "name": "event2" },
40+
"event3": { "name": "event3" },
41+
"event4": { "name": "event4" },
42+
})
43+
})
44+
45+
it('should store all the events when set asynchronously', async (done) => {
46+
const promises = []
47+
promises.push(store.set('event1', {'name': 'event1'}))
48+
promises.push(store.set('event2', {'name': 'event2'}))
49+
promises.push(store.set('event3', {'name': 'event3'}))
50+
promises.push(store.set('event4', {'name': 'event4'}))
51+
Promise.all(promises).then(() => {
52+
const storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY])
53+
expect(storedPendingEvents).toEqual({
54+
"event1": { "name": "event1" },
55+
"event2": { "name": "event2" },
56+
"event3": { "name": "event3" },
57+
"event4": { "name": "event4" },
58+
})
59+
done()
60+
})
61+
})
62+
})
63+
64+
describe('get', () => {
65+
it('should correctly get items', async () => {
66+
await store.set('event1', {'name': 'event1'})
67+
await store.set('event2', {'name': 'event2'})
68+
await store.set('event3', {'name': 'event3'})
69+
await store.set('event4', {'name': 'event4'})
70+
expect(await store.get('event1')).toEqual({'name': 'event1'})
71+
expect(await store.get('event2')).toEqual({'name': 'event2'})
72+
expect(await store.get('event3')).toEqual({'name': 'event3'})
73+
expect(await store.get('event4')).toEqual({'name': 'event4'})
74+
})
75+
})
76+
77+
describe('getEventsMap', () => {
78+
it('should get the whole map correctly', async () => {
79+
await store.set('event1', {'name': 'event1'})
80+
await store.set('event2', {'name': 'event2'})
81+
await store.set('event3', {'name': 'event3'})
82+
await store.set('event4', {'name': 'event4'})
83+
const mapResult = await store.getEventsMap()
84+
expect(mapResult).toEqual({
85+
"event1": { "name": "event1" },
86+
"event2": { "name": "event2" },
87+
"event3": { "name": "event3" },
88+
"event4": { "name": "event4" },
89+
})
90+
})
91+
})
92+
93+
describe('getEventsList', () => {
94+
it('should get all the events as a list', async () => {
95+
await store.set('event1', {'name': 'event1'})
96+
await store.set('event2', {'name': 'event2'})
97+
await store.set('event3', {'name': 'event3'})
98+
await store.set('event4', {'name': 'event4'})
99+
const listResult = await store.getEventsList()
100+
expect(listResult).toEqual([
101+
{ "name": "event1" },
102+
{ "name": "event2" },
103+
{ "name": "event3" },
104+
{ "name": "event4" },
105+
])
106+
})
107+
})
108+
109+
describe('remove', () => {
110+
it('should correctly remove items from the store', async () => {
111+
await store.set('event1', {'name': 'event1'})
112+
await store.set('event2', {'name': 'event2'})
113+
await store.set('event3', {'name': 'event3'})
114+
await store.set('event4', {'name': 'event4'})
115+
let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY])
116+
expect(storedPendingEvents).toEqual({
117+
"event1": { "name": "event1" },
118+
"event2": { "name": "event2" },
119+
"event3": { "name": "event3" },
120+
"event4": { "name": "event4" },
121+
})
122+
123+
await store.remove('event1')
124+
storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY])
125+
expect(storedPendingEvents).toEqual({
126+
"event2": { "name": "event2" },
127+
"event3": { "name": "event3" },
128+
"event4": { "name": "event4" },
129+
})
130+
131+
await store.remove('event2')
132+
storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY])
133+
expect(storedPendingEvents).toEqual({
134+
"event3": { "name": "event3" },
135+
"event4": { "name": "event4" },
136+
})
137+
})
138+
139+
it('should correctly remove items from the store when removed asynchronously', async (done) => {
140+
await store.set('event1', {'name': 'event1'})
141+
await store.set('event2', {'name': 'event2'})
142+
await store.set('event3', {'name': 'event3'})
143+
await store.set('event4', {'name': 'event4'})
144+
let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY])
145+
expect(storedPendingEvents).toEqual({
146+
"event1": { "name": "event1" },
147+
"event2": { "name": "event2" },
148+
"event3": { "name": "event3" },
149+
"event4": { "name": "event4" },
150+
})
151+
152+
const promises = []
153+
promises.push(store.remove('event1'))
154+
promises.push(store.remove('event2'))
155+
promises.push(store.remove('event3'))
156+
Promise.all(promises).then(() => {
157+
let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY])
158+
expect(storedPendingEvents).toEqual({ "event4": { "name": "event4" }})
159+
done()
160+
})
161+
})
162+
})
163+
164+
describe('clear', () => {
165+
it('should clear the whole store',async () => {
166+
await store.set('event1', {'name': 'event1'})
167+
await store.set('event2', {'name': 'event2'})
168+
await store.set('event3', {'name': 'event3'})
169+
await store.set('event4', {'name': 'event4'})
170+
let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY])
171+
expect(storedPendingEvents).toEqual({
172+
"event1": { "name": "event1" },
173+
"event2": { "name": "event2" },
174+
"event3": { "name": "event3" },
175+
"event4": { "name": "event4" },
176+
})
177+
await store.clear()
178+
storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY] || '{}')
179+
expect(storedPendingEvents).toEqual({})
180+
})
181+
})
182+
183+
describe('maxSize', () => {
184+
it('should not add anymore events if the store if full', async () => {
185+
await store.set('event1', {'name': 'event1'})
186+
await store.set('event2', {'name': 'event2'})
187+
await store.set('event3', {'name': 'event3'})
188+
await store.set('event4', {'name': 'event4'})
189+
190+
let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY])
191+
expect(storedPendingEvents).toEqual({
192+
"event1": { "name": "event1" },
193+
"event2": { "name": "event2" },
194+
"event3": { "name": "event3" },
195+
"event4": { "name": "event4" },
196+
})
197+
await store.set('event5', {'name': 'event5'})
198+
199+
storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY])
200+
expect(storedPendingEvents).toEqual({
201+
"event1": { "name": "event1" },
202+
"event2": { "name": "event2" },
203+
"event3": { "name": "event3" },
204+
"event4": { "name": "event4" },
205+
"event5": { "name": "event5" },
206+
})
207+
208+
await store.set('event6', {'name': 'event6'})
209+
storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY])
210+
expect(storedPendingEvents).toEqual({
211+
"event1": { "name": "event1" },
212+
"event2": { "name": "event2" },
213+
"event3": { "name": "event3" },
214+
"event4": { "name": "event4" },
215+
"event5": { "name": "event5" },
216+
})
217+
})
218+
})
219+
})

0 commit comments

Comments
 (0)