-
Notifications
You must be signed in to change notification settings - Fork 83
feat: Added Offline storage support to Event processor for React Native Apps #517
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
Conversation
export { LogTierV1EventProcessor } from './v1/v1EventProcessor' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why change the exports like this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because i exported both RN and normal event processor from the single file. i will probably separate them out.
export type EventDispatcherResponse = { | ||
statusCode: number | ||
} | { | ||
status: number | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Node event dispatcher calls the ED callback with an object like this ({ statusCode: 200 }
). Is that a problem? Why change this to accept a number?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think i changed it initially to try something quickly and then forgot to revert back. i will reverts it
// This results in race condition when two items are added to the map or array in parallel. | ||
// for ex. Req 1 gets the map. Req 2 gets the map. Req 1 sets the map. Req 2 sets the map. The map now loses item from Req 1. | ||
// This synchronizer makes sure the operations are atomic using promises. | ||
class Synchronizer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This Synchronizer
is similar to the RequestTracker
. Can we use RequestTracker
instead of adding this? We can consider renaming RequestTracker
to something more generic.
// This stores individual events generated from the SDK till they are part of the pending buffer. | ||
// The store is cleared right before the event is formatted to be dispatched. | ||
// This is to make sure that individual events are not lost when app closes before the buffer was flushed. | ||
export class ReactNativeEventBufferStore { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need two different classes ReactNativePendingEventStore
and ReactNativeEventBufferStore
? They seem very similar. Also I think they should both have a limit on how much they will store.
@@ -27,3 +28,13 @@ export class LogTierV1EventProcessor extends AbstractEventProcessor { | |||
} | |||
} | |||
} | |||
|
|||
export class LogTierV1ReactNativeEventProcessor extends AbstractReactNativeEventProcessor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we can put this here. This causes the RN code to be included in the main bundle.
await this.pendingEventsStore.set(cacheKey, formattedEvent) | ||
|
||
// Clear buffer because the buffer has become a formatted event and is already stored in pending cache. | ||
this.eventBufferStore.clear() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to await
the completion of this clear?
private isProcessingPendingEvents: boolean = false | ||
private pendingEventsPromise: Promise<void> | null = null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need isProcessingPendingEvents
? Can we say if pendingEventsPromise
is not null
, then it is processing pending events?
this.pendingEventsPromise = new Promise(async (resolvePendingEventPromise) => { | ||
this.isProcessingPendingEvents = true | ||
const formattedEvents: {[key: string]: any} = await this.pendingEventsStore.getEventsMap() | ||
const eventKeys = Object.keys(formattedEvents) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest: use utils.objectEntries
for iteration here rather than for
loop.
if (this.isProcessingPendingEvents && this.pendingEventsPromise) { | ||
return this.pendingEventsPromise | ||
} | ||
this.pendingEventsPromise = new Promise(async (resolvePendingEventPromise) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's hard to follow what's going on with two nested Promise constructors, event dispatcher callbacks, the loop continue
, etc. I would try refactoring this to use async
/await
as much as possible. To deal with the callback-based event dispatcher, create a helper async
function that hides the complexity away from the main logic of processPendingEvents
.
const eventKeys = Object.keys(formattedEvents) | ||
for (let i = 0; i < eventKeys.length; i++) { | ||
const eventKey = eventKeys[i] | ||
if (this.eventsInProgress[eventKey]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this check? How does the same event get dispatched twice?
2. moved connectivity listener to start
…ent processor implementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I added a few suggestions which you can consider (non-blocking). Please have @jaeopt approve also.
/** | ||
* React Native Events Processor with Caching support for events when app is offline. | ||
*/ | ||
export abstract class LogTierV1EventProcessor implements EventProcessor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we name it ReactNativeEventProcessor
? Or something with React Native in the name to differentiate from the standard one.
packages/event-processor/src/v1/v1EventProcessor.react_native.ts
Outdated
Show resolved
Hide resolved
|
||
process(event: ProcessableEvent): void { | ||
// Adding events to buffer store. If app closes before dispatch, we can reprocess next time the app initializes | ||
this.eventBufferStore.set(generateUUID(), event) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is awkward that we have to generate the UUID to use as a key here, but we don't retrieve it using this key, we just get all of them by calling getEventsList
. Consider changing the interface so you can set/append without passing a key.
Co-authored-by: Matt Carroll <mjc1283@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about the implications, but I see a few places of out-of-order event dispatch. Check it out.
A couple of other suggestions too.
await this.synchronizer.getLock() | ||
const eventsMap: {[key: string]: T} = await this.cache.get(this.storeKey) || {} | ||
this.synchronizer.releaseLock() | ||
return objectValues(eventsMap) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this list guarantee FIFO ordering from map?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Looping through map always iterates in the order of insertion.
|
||
async drainQueue(buffer: ProcessableEvent[]): Promise<void> { | ||
// Retry pending failed events while draining queue | ||
await this.processPendingEvents() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about the implication of this out-of-order dispatching, but if "processPendingEvents" fail to flush all pending events, the new dispatch below (line 147) can be fired before previous buffered events. Check it out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed. skipping subsequent events on failure now.
for (const [eventKey, event] of eventEntries) { | ||
await this.dispatchEvent(eventKey, event) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another case of out-of-order delivery. If some events failed with errors, they stay in the buffer for retry later (out-of-order) since other events buffered later can be dispatched ok.
constructor({ | ||
dispatcher, | ||
flushInterval = 30000, | ||
batchSize = 3000, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check out this huge batch size default value. I understand we have max batched event size. 3000 events will be batched to be > 3MB. Not sure if this fits in the restrictions.
I think default batch size can be much smaller like 10-100.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I actually borrowed the value from the existing event processor. @mjc1283 ! Do you suggest to change it to something else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually @jaeopt you are correct. this particular default never gets used by optimizely SDK, the sdk package already sets default of 10
if the user does not provide a value. This default will only take effect if someone uses event processor package directly. I will change it to 10
to make it consistent with SDK default.
…omplete. might add more tests to the list
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Summary
Added Offline storage support to Event Processor for React Native Apps.
eventMaxQueueSize
config option which represents number of offline events SDK will store at a time. Deault is 10k for now.Follow-up changes expected in
optimizely-sdk
package.EventProcessor
constructor to representmaxQueueSize
inReact Native
entry point.DefaultEventDispacher
for browser entry point which is also used for React Native entry point.AsyncStorage
module configuration for umd bundles inrollup
config.Test plan