react-native-onyx 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +187 -0
- package/LICENSE.md +21 -0
- package/README.md +258 -0
- package/dist/web.development.js +3289 -0
- package/dist/web.development.js.map +1 -0
- package/dist/web.min.js +2 -0
- package/dist/web.min.js.map +1 -0
- package/lib/Logger.js +35 -0
- package/lib/MDTable.js +69 -0
- package/lib/Onyx.js +901 -0
- package/lib/OnyxCache.js +195 -0
- package/lib/SyncQueue.js +51 -0
- package/lib/compose.js +29 -0
- package/lib/createDeferredTask.js +16 -0
- package/lib/index.js +5 -0
- package/lib/metrics/index.native.js +263 -0
- package/lib/metrics/index.web.js +13 -0
- package/lib/storage/NativeStorage.js +3 -0
- package/lib/storage/WebStorage.js +56 -0
- package/lib/storage/__mocks__/index.native.js +8 -0
- package/lib/storage/index.native.js +8 -0
- package/lib/storage/index.web.js +3 -0
- package/lib/storage/providers/AsyncStorage.js +83 -0
- package/lib/storage/providers/LocalForage.js +111 -0
- package/lib/withOnyx.js +200 -0
- package/native.js +11 -0
- package/package.json +109 -0
- package/web.js +12 -0
package/lib/OnyxCache.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import _ from 'underscore';
|
|
2
|
+
import lodashMerge from 'lodash/merge';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const isDefined = _.negate(_.isUndefined);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* In memory cache providing data by reference
|
|
9
|
+
* Encapsulates Onyx cache related functionality
|
|
10
|
+
*/
|
|
11
|
+
class OnyxCache {
|
|
12
|
+
constructor() {
|
|
13
|
+
/**
|
|
14
|
+
* @private
|
|
15
|
+
* Cache of all the storage keys available in persistent storage
|
|
16
|
+
* @type {Set<string>}
|
|
17
|
+
*/
|
|
18
|
+
this.storageKeys = new Set();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @private
|
|
22
|
+
* Unique list of keys maintained in access order (most recent at the end)
|
|
23
|
+
* @type {Set<string>}
|
|
24
|
+
*/
|
|
25
|
+
this.recentKeys = new Set();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @private
|
|
29
|
+
* A map of cached values
|
|
30
|
+
* @type {Record<string, *>}
|
|
31
|
+
*/
|
|
32
|
+
this.storageMap = {};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @private
|
|
36
|
+
* Captured pending tasks for already running storage methods
|
|
37
|
+
* @type {Record<string, Promise>}
|
|
38
|
+
*/
|
|
39
|
+
this.pendingPromises = {};
|
|
40
|
+
|
|
41
|
+
// bind all public methods to prevent problems with `this`
|
|
42
|
+
_.bindAll(
|
|
43
|
+
this,
|
|
44
|
+
'getAllKeys', 'getValue', 'hasCacheForKey', 'addKey', 'set', 'drop', 'merge',
|
|
45
|
+
'hasPendingTask', 'getTaskPromise', 'captureTask', 'removeLeastRecentlyUsedKeys',
|
|
46
|
+
'setRecentKeysLimit'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get all the storage keys
|
|
52
|
+
* @returns {string[]}
|
|
53
|
+
*/
|
|
54
|
+
getAllKeys() {
|
|
55
|
+
return Array.from(this.storageKeys);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get a cached value from storage
|
|
60
|
+
* @param {string} key
|
|
61
|
+
* @returns {*}
|
|
62
|
+
*/
|
|
63
|
+
getValue(key) {
|
|
64
|
+
this.addToAccessedKeys(key);
|
|
65
|
+
return this.storageMap[key];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check whether cache has data for the given key
|
|
70
|
+
* @param {string} key
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
hasCacheForKey(key) {
|
|
74
|
+
return isDefined(this.storageMap[key]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Saves a key in the storage keys list
|
|
79
|
+
* Serves to keep the result of `getAllKeys` up to date
|
|
80
|
+
* @param {string} key
|
|
81
|
+
*/
|
|
82
|
+
addKey(key) {
|
|
83
|
+
this.storageKeys.add(key);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Set's a key value in cache
|
|
88
|
+
* Adds the key to the storage keys list as well
|
|
89
|
+
* @param {string} key
|
|
90
|
+
* @param {*} value
|
|
91
|
+
* @returns {*} value - returns the cache value
|
|
92
|
+
*/
|
|
93
|
+
set(key, value) {
|
|
94
|
+
this.addKey(key);
|
|
95
|
+
this.addToAccessedKeys(key);
|
|
96
|
+
this.storageMap[key] = value;
|
|
97
|
+
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Forget the cached value for the given key
|
|
103
|
+
* @param {string} key
|
|
104
|
+
*/
|
|
105
|
+
drop(key) {
|
|
106
|
+
delete this.storageMap[key];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Deep merge data to cache, any non existing keys will be created
|
|
111
|
+
* @param {Record<string, *>} data - a map of (cache) key - values
|
|
112
|
+
*/
|
|
113
|
+
merge(data) {
|
|
114
|
+
this.storageMap = lodashMerge({}, this.storageMap, data);
|
|
115
|
+
|
|
116
|
+
const storageKeys = this.getAllKeys();
|
|
117
|
+
const mergedKeys = _.keys(data);
|
|
118
|
+
this.storageKeys = new Set([...storageKeys, ...mergedKeys]);
|
|
119
|
+
_.each(mergedKeys, key => this.addToAccessedKeys(key));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check whether the given task is already running
|
|
124
|
+
* @param {string} taskName - unique name given for the task
|
|
125
|
+
* @returns {*}
|
|
126
|
+
*/
|
|
127
|
+
hasPendingTask(taskName) {
|
|
128
|
+
return isDefined(this.pendingPromises[taskName]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Use this method to prevent concurrent calls for the same thing
|
|
133
|
+
* Instead of calling the same task again use the existing promise
|
|
134
|
+
* provided from this function
|
|
135
|
+
* @template T
|
|
136
|
+
* @param {string} taskName - unique name given for the task
|
|
137
|
+
* @returns {Promise<T>}
|
|
138
|
+
*/
|
|
139
|
+
getTaskPromise(taskName) {
|
|
140
|
+
return this.pendingPromises[taskName];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Capture a promise for a given task so other caller can
|
|
145
|
+
* hook up to the promise if it's still pending
|
|
146
|
+
* @template T
|
|
147
|
+
* @param {string} taskName - unique name for the task
|
|
148
|
+
* @param {Promise<T>} promise
|
|
149
|
+
* @returns {Promise<T>}
|
|
150
|
+
*/
|
|
151
|
+
captureTask(taskName, promise) {
|
|
152
|
+
this.pendingPromises[taskName] = promise.finally(() => {
|
|
153
|
+
delete this.pendingPromises[taskName];
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return this.pendingPromises[taskName];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @private
|
|
161
|
+
* Adds a key to the top of the recently accessed keys
|
|
162
|
+
* @param {string} key
|
|
163
|
+
*/
|
|
164
|
+
addToAccessedKeys(key) {
|
|
165
|
+
// Removing and re-adding a key ensures it's at the end of the list
|
|
166
|
+
this.recentKeys.delete(key);
|
|
167
|
+
this.recentKeys.add(key);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Remove keys that don't fall into the range of recently used keys
|
|
172
|
+
*/
|
|
173
|
+
removeLeastRecentlyUsedKeys() {
|
|
174
|
+
if (this.recentKeys.size > this.maxRecentKeysSize) {
|
|
175
|
+
// Get the last N keys by doing a negative slice
|
|
176
|
+
const recentlyAccessed = [...this.recentKeys].slice(-this.maxRecentKeysSize);
|
|
177
|
+
const storageKeys = _.keys(this.storageMap);
|
|
178
|
+
const keysToRemove = _.difference(storageKeys, recentlyAccessed);
|
|
179
|
+
|
|
180
|
+
_.each(keysToRemove, this.drop);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Set the recent keys list size
|
|
186
|
+
* @param {number} limit
|
|
187
|
+
*/
|
|
188
|
+
setRecentKeysLimit(limit) {
|
|
189
|
+
this.maxRecentKeysSize = limit;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const instance = new OnyxCache();
|
|
194
|
+
|
|
195
|
+
export default instance;
|
package/lib/SyncQueue.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous queue that can be used to ensure promise based tasks are run in sequence.
|
|
3
|
+
* Pass to the constructor a function that returns a promise to run the task then add data.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
*
|
|
7
|
+
* const queue = new SyncQueue(({key, val}) => {
|
|
8
|
+
* return someAsyncProcess(key, val);
|
|
9
|
+
* });
|
|
10
|
+
*
|
|
11
|
+
* queue.push({key: 1, val: '1'});
|
|
12
|
+
* queue.push({key: 2, val: '2'});
|
|
13
|
+
*/
|
|
14
|
+
export default class SyncQueue {
|
|
15
|
+
/**
|
|
16
|
+
* @param {Function} run - must return a promise
|
|
17
|
+
*/
|
|
18
|
+
constructor(run) {
|
|
19
|
+
this.queue = [];
|
|
20
|
+
this.isProcessing = false;
|
|
21
|
+
this.run = run;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
process() {
|
|
25
|
+
if (this.isProcessing || this.queue.length === 0) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.isProcessing = true;
|
|
30
|
+
|
|
31
|
+
const {data, resolve, reject} = this.queue.shift();
|
|
32
|
+
this.run(data)
|
|
33
|
+
.then(resolve)
|
|
34
|
+
.catch(reject)
|
|
35
|
+
.finally(() => {
|
|
36
|
+
this.isProcessing = false;
|
|
37
|
+
this.process();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {*} data
|
|
43
|
+
* @returns {Promise}
|
|
44
|
+
*/
|
|
45
|
+
push(data) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
this.queue.push({resolve, reject, data});
|
|
48
|
+
this.process();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
package/lib/compose.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is a utility function taken directly from Redux. (We don't want to add Redux as a dependency)
|
|
3
|
+
* It enables functional composition, useful for the chaining/composition of HOCs.
|
|
4
|
+
*
|
|
5
|
+
* For example, instead of:
|
|
6
|
+
*
|
|
7
|
+
* export default hoc1(config1, hoc2(config2, hoc3(config3)))(Component);
|
|
8
|
+
*
|
|
9
|
+
* Use this instead:
|
|
10
|
+
*
|
|
11
|
+
* export default compose(
|
|
12
|
+
* hoc1(config1),
|
|
13
|
+
* hoc2(config2),
|
|
14
|
+
* hoc3(config3),
|
|
15
|
+
* )(Component)
|
|
16
|
+
*
|
|
17
|
+
* @returns {Function}
|
|
18
|
+
*/
|
|
19
|
+
export default function compose(...funcs) {
|
|
20
|
+
if (funcs.length === 0) {
|
|
21
|
+
return arg => arg;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (funcs.length === 1) {
|
|
25
|
+
return funcs[0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return funcs.reduce((a, b) => (...args) => a(b(...args)));
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a deferred task that can be resolved when we call `resolve()`
|
|
3
|
+
* The returned promise will complete when we call `resolve`
|
|
4
|
+
* Useful when we want to wait for a tasks that is resolved from an external action
|
|
5
|
+
*
|
|
6
|
+
* @template T
|
|
7
|
+
* @returns {{ resolve: function(*), promise: Promise<T|void> }}
|
|
8
|
+
*/
|
|
9
|
+
export default function createDeferredTask() {
|
|
10
|
+
const deferred = {};
|
|
11
|
+
deferred.promise = new Promise((res) => {
|
|
12
|
+
deferred.resolve = res;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return deferred;
|
|
16
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import _ from 'underscore';
|
|
2
|
+
import performance from 'react-native-performance';
|
|
3
|
+
import MDTable from '../MDTable';
|
|
4
|
+
|
|
5
|
+
const decoratedAliases = new Set();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Capture a start mark to performance entries
|
|
9
|
+
* @param {string} alias
|
|
10
|
+
* @param {Array<*>} args
|
|
11
|
+
* @returns {{name: string, startTime:number, detail: {args: [], alias: string}}}
|
|
12
|
+
*/
|
|
13
|
+
function addMark(alias, args) {
|
|
14
|
+
return performance.mark(alias, {detail: {args, alias}});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Capture a measurement between the start mark and now
|
|
19
|
+
* @param {{name: string, startTime:number, detail: {args: []}}} startMark
|
|
20
|
+
* @param {*} detail
|
|
21
|
+
*/
|
|
22
|
+
function measureMarkToNow(startMark, detail) {
|
|
23
|
+
performance.measure(`${startMark.name} [${startMark.detail.args.toString()}]`, {
|
|
24
|
+
start: startMark.startTime,
|
|
25
|
+
end: performance.now(),
|
|
26
|
+
detail: {...startMark.detail, ...detail}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Wraps a function with metrics capturing logic
|
|
32
|
+
* @param {function} func
|
|
33
|
+
* @param {String} [alias]
|
|
34
|
+
* @returns {function} The wrapped function
|
|
35
|
+
*/
|
|
36
|
+
function decorateWithMetrics(func, alias = func.name) {
|
|
37
|
+
if (decoratedAliases.has(alias)) {
|
|
38
|
+
throw new Error(`"${alias}" is already decorated`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
decoratedAliases.add(alias);
|
|
42
|
+
|
|
43
|
+
function decorated(...args) {
|
|
44
|
+
const mark = addMark(alias, args);
|
|
45
|
+
|
|
46
|
+
const originalPromise = func.apply(this, args);
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* Then handlers added here are not affecting the original promise
|
|
50
|
+
* They create a separate chain that's not exposed (returned) to the original caller
|
|
51
|
+
* */
|
|
52
|
+
originalPromise
|
|
53
|
+
.then((result) => {
|
|
54
|
+
measureMarkToNow(mark, {result});
|
|
55
|
+
})
|
|
56
|
+
.catch((error) => {
|
|
57
|
+
measureMarkToNow(mark, {error});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return originalPromise;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return decorated;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Calculate the total sum of a given key in a list
|
|
68
|
+
* @param {Array<Record<prop, Number>>} list
|
|
69
|
+
* @param {string} prop
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
72
|
+
function sum(list, prop) {
|
|
73
|
+
return _.reduce(list, (memo, next) => memo + next[prop], 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Aggregates and returns benchmark information
|
|
78
|
+
* @returns {{summaries: Record<string, Object>, totalTime: number, lastCompleteCall: *}}
|
|
79
|
+
* An object with
|
|
80
|
+
* - `totalTime` - total time spent by decorated methods
|
|
81
|
+
* - `lastCompleteCall` - millisecond since launch the last call completed at
|
|
82
|
+
* - `summaries` - mapping of all captured stats: summaries.methodName -> method stats
|
|
83
|
+
*/
|
|
84
|
+
function getMetrics() {
|
|
85
|
+
const summaries = _.chain(performance.getEntriesByType('measure'))
|
|
86
|
+
.filter(entry => entry.detail && decoratedAliases.has(entry.detail.alias))
|
|
87
|
+
.groupBy(entry => entry.detail.alias)
|
|
88
|
+
.map((calls, methodName) => {
|
|
89
|
+
const total = sum(calls, 'duration');
|
|
90
|
+
const avg = (total / calls.length) || 0;
|
|
91
|
+
const max = _.max(calls, 'duration').duration || 0;
|
|
92
|
+
const min = _.min(calls, 'duration').duration || 0;
|
|
93
|
+
|
|
94
|
+
// Latest complete call (by end time) for all the calls made to the current method
|
|
95
|
+
const lastCall = _.max(calls, call => call.startTime + call.duration);
|
|
96
|
+
|
|
97
|
+
return [methodName, {
|
|
98
|
+
methodName,
|
|
99
|
+
total,
|
|
100
|
+
max,
|
|
101
|
+
min,
|
|
102
|
+
avg,
|
|
103
|
+
lastCall,
|
|
104
|
+
calls,
|
|
105
|
+
}];
|
|
106
|
+
})
|
|
107
|
+
.object() // Create a map like methodName -> StatSummary
|
|
108
|
+
.value();
|
|
109
|
+
|
|
110
|
+
const totalTime = sum(_.values(summaries), 'total');
|
|
111
|
+
|
|
112
|
+
// Latest complete call (by end time) of all methods up to this point
|
|
113
|
+
const lastCompleteCall = _.max(
|
|
114
|
+
_.values(summaries),
|
|
115
|
+
summary => summary.lastCall.startTime + summary.lastCall.duration,
|
|
116
|
+
).lastCall;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
totalTime,
|
|
120
|
+
summaries,
|
|
121
|
+
lastCompleteCall,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Convert milliseconds to human readable time
|
|
127
|
+
* @param {number} millis
|
|
128
|
+
* @param {boolean} [raw=false]
|
|
129
|
+
* @returns {string|number}
|
|
130
|
+
*/
|
|
131
|
+
function toDuration(millis, raw = false) {
|
|
132
|
+
if (raw) {
|
|
133
|
+
return millis;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const minute = 60 * 1000;
|
|
137
|
+
if (millis > minute) {
|
|
138
|
+
return `${(millis / minute).toFixed(1)}min`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const second = 1000;
|
|
142
|
+
if (millis > second) {
|
|
143
|
+
return `${(millis / second).toFixed(2)}sec`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `${millis.toFixed(3)}ms`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Print extensive information on the dev console
|
|
151
|
+
* max, min, average, total time for each method
|
|
152
|
+
* and a table of individual calls
|
|
153
|
+
*
|
|
154
|
+
* @param {Object} [options]
|
|
155
|
+
* @param {boolean} [options.raw=false] - setting this to true will print raw instead of human friendly times
|
|
156
|
+
* Useful when you copy the printed table to excel and let excel do the number formatting
|
|
157
|
+
* @param {'console'|'csv'|'json'|'string'} [options.format=console] The output format of this function
|
|
158
|
+
* `string` is useful when __DEV__ is set to `false` as writing to the console is disabled, but the result of this
|
|
159
|
+
* method would still get printed as output
|
|
160
|
+
* @param {string[]} [options.methods] Print stats only for these method names
|
|
161
|
+
* @returns {string|undefined}
|
|
162
|
+
*/
|
|
163
|
+
function printMetrics({raw = false, format = 'console', methods} = {}) {
|
|
164
|
+
const {totalTime, summaries, lastCompleteCall} = getMetrics();
|
|
165
|
+
|
|
166
|
+
const tableSummary = MDTable.factory({
|
|
167
|
+
heading: ['method', 'total time spent', 'max', 'min', 'avg', 'time last call completed', 'calls made'],
|
|
168
|
+
leftAlignedCols: [0],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/* Performance marks (startTimes) are relative to system uptime
|
|
172
|
+
* timeOrigin is the point at which the app started to init
|
|
173
|
+
* We use timeOrigin to display times relative to app launch time
|
|
174
|
+
* See: https://github.com/oblador/react-native-performance/issues/50 */
|
|
175
|
+
const timeOrigin = performance.timeOrigin;
|
|
176
|
+
const methodNames = _.isArray(methods) ? methods : _.keys(summaries);
|
|
177
|
+
|
|
178
|
+
const methodCallTables = _.chain(methodNames)
|
|
179
|
+
.filter(methodName => summaries[methodName] && summaries[methodName].avg > 0)
|
|
180
|
+
.map((methodName) => {
|
|
181
|
+
const {calls, ...methodStats} = summaries[methodName];
|
|
182
|
+
tableSummary.addRow(
|
|
183
|
+
methodName,
|
|
184
|
+
toDuration(methodStats.total, raw),
|
|
185
|
+
toDuration(methodStats.max, raw),
|
|
186
|
+
toDuration(methodStats.min, raw),
|
|
187
|
+
toDuration(methodStats.avg, raw),
|
|
188
|
+
toDuration((methodStats.lastCall.startTime + methodStats.lastCall.duration) - timeOrigin, raw),
|
|
189
|
+
calls.length,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return MDTable.factory({
|
|
193
|
+
title: methodName,
|
|
194
|
+
heading: ['start time', 'end time', 'duration', 'args'],
|
|
195
|
+
leftAlignedCols: [3],
|
|
196
|
+
rows: calls.map(call => ([
|
|
197
|
+
toDuration(call.startTime - performance.timeOrigin, raw),
|
|
198
|
+
toDuration((call.startTime + call.duration) - timeOrigin, raw),
|
|
199
|
+
toDuration(call.duration, raw),
|
|
200
|
+
call.detail.args.map(String).join(', ').slice(0, 60), // Restrict cell width to 60 chars max
|
|
201
|
+
]))
|
|
202
|
+
});
|
|
203
|
+
})
|
|
204
|
+
.value();
|
|
205
|
+
|
|
206
|
+
if (/csv|json|string/i.test(format)) {
|
|
207
|
+
const allTables = [tableSummary, ...methodCallTables];
|
|
208
|
+
|
|
209
|
+
return allTables.map((table) => {
|
|
210
|
+
switch (format.toLowerCase()) {
|
|
211
|
+
case 'csv':
|
|
212
|
+
return table.toCSV();
|
|
213
|
+
case 'json':
|
|
214
|
+
return table.toJSON();
|
|
215
|
+
default:
|
|
216
|
+
return table.toString();
|
|
217
|
+
}
|
|
218
|
+
}).join('\n\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const lastComplete = lastCompleteCall && toDuration(
|
|
222
|
+
(lastCompleteCall.startTime + lastCompleteCall.duration) - timeOrigin, raw
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const mainOutput = [
|
|
226
|
+
'### Onyx Benchmark',
|
|
227
|
+
` - Total: ${toDuration(totalTime, raw)}`,
|
|
228
|
+
` - Last call finished at: ${lastComplete || 'N/A'}`,
|
|
229
|
+
'',
|
|
230
|
+
tableSummary.toString()
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
/* eslint-disable no-console */
|
|
234
|
+
console.info(mainOutput.join('\n'));
|
|
235
|
+
methodCallTables.forEach((table) => {
|
|
236
|
+
console.groupCollapsed(table.getTitle());
|
|
237
|
+
console.info(table.toString());
|
|
238
|
+
console.groupEnd();
|
|
239
|
+
});
|
|
240
|
+
/* eslint-enable */
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Clears all collected metrics.
|
|
245
|
+
*/
|
|
246
|
+
function resetMetrics() {
|
|
247
|
+
const {summaries} = getMetrics();
|
|
248
|
+
|
|
249
|
+
_.chain(summaries)
|
|
250
|
+
.map(summary => summary.calls)
|
|
251
|
+
.flatten()
|
|
252
|
+
.each((measure) => {
|
|
253
|
+
performance.clearMarks(measure.detail.alias);
|
|
254
|
+
performance.clearMeasures(measure.name);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export {
|
|
259
|
+
decorateWithMetrics,
|
|
260
|
+
getMetrics,
|
|
261
|
+
resetMetrics,
|
|
262
|
+
printMetrics,
|
|
263
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// For web-only implementations of Onyx, this module will just be a no-op
|
|
2
|
+
|
|
3
|
+
function decorateWithMetrics() {}
|
|
4
|
+
function getMetrics() {}
|
|
5
|
+
function printMetrics() {}
|
|
6
|
+
function resetMetrics() {}
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
decorateWithMetrics,
|
|
10
|
+
getMetrics,
|
|
11
|
+
resetMetrics,
|
|
12
|
+
printMetrics,
|
|
13
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import _ from 'underscore';
|
|
2
|
+
import Storage from './providers/LocalForage';
|
|
3
|
+
|
|
4
|
+
const SYNC_ONYX = 'SYNC_ONYX';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Raise an event thorough `localStorage` to let other tabs know a value changed
|
|
8
|
+
* @param {String} onyxKey
|
|
9
|
+
*/
|
|
10
|
+
function raiseStorageSyncEvent(onyxKey) {
|
|
11
|
+
global.localStorage.setItem(SYNC_ONYX, onyxKey);
|
|
12
|
+
global.localStorage.removeItem(SYNC_ONYX, onyxKey);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const webStorage = {
|
|
16
|
+
...Storage,
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Contains keys for which we want to disable sync event across tabs.
|
|
20
|
+
* @param {String[]} keysToDisableSyncEvents
|
|
21
|
+
* Storage synchronization mechanism keeping all opened tabs in sync
|
|
22
|
+
* @param {function(key: String, data: *)} onStorageKeyChanged
|
|
23
|
+
*/
|
|
24
|
+
keepInstancesSync(keysToDisableSyncEvents, onStorageKeyChanged) {
|
|
25
|
+
// Override set, remove and clear to raise storage events that we intercept in other tabs
|
|
26
|
+
this.setItem = (key, value) => Storage.setItem(key, value)
|
|
27
|
+
.then(() => raiseStorageSyncEvent(key));
|
|
28
|
+
|
|
29
|
+
this.removeItem = key => Storage.removeItem(key)
|
|
30
|
+
.then(() => raiseStorageSyncEvent(key));
|
|
31
|
+
|
|
32
|
+
// If we just call Storage.clear other tabs will have no idea which keys were available previously
|
|
33
|
+
// so that they can call keysChanged for them. That's why we iterate and remove keys one by one
|
|
34
|
+
this.clear = () => Storage.getAllKeys()
|
|
35
|
+
.then(keys => _.map(keys, key => this.removeItem(key)))
|
|
36
|
+
.then(tasks => Promise.all(tasks));
|
|
37
|
+
|
|
38
|
+
// This listener will only be triggered by events coming from other tabs
|
|
39
|
+
global.addEventListener('storage', (event) => {
|
|
40
|
+
// Ignore events that don't originate from the SYNC_ONYX logic
|
|
41
|
+
if (event.key !== SYNC_ONYX || !event.newValue) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const onyxKey = event.newValue;
|
|
46
|
+
if (_.contains(keysToDisableSyncEvents, onyxKey)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Storage.getItem(onyxKey)
|
|
51
|
+
.then(value => onStorageKeyChanged(onyxKey, value));
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default webStorage;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Because we're using the `react-native` preset of jest this file extension
|
|
3
|
+
* is .native.js. Otherwise, since jest prefers index.native.js over index.js
|
|
4
|
+
* it'll skip loading the mock
|
|
5
|
+
*/
|
|
6
|
+
import WebStorage from '../WebStorage';
|
|
7
|
+
|
|
8
|
+
export default WebStorage;
|