-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Update optimistic IDs in sequential queue #27996
Changes from all commits
2f29985
5a4676d
80102ca
0ebf3dc
833ba10
af7094b
0d5f4ac
f1441a8
09ba949
cb5888b
5f3110d
7f83936
bf153a5
15756ba
80af4cc
f0fbf0d
224a134
1d05d9b
5489195
06cc7bd
1aad488
e583ca4
0d69f32
ed415dc
e1f470f
d9863e1
e72dded
0d477ac
8b777ea
0094aab
38aa8ee
e059f57
f0b1a4e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import _ from 'lodash'; | ||
import ONYXKEYS from '../../ONYXKEYS'; | ||
import Report from '../../types/onyx/Report'; | ||
import {Middleware} from '../Request'; | ||
import * as PersistedRequests from '../actions/PersistedRequests'; | ||
import deepReplaceKeysAndValues from '../deepReplaceKeysAndValues'; | ||
|
||
const handleUnusedOptimisticID: Middleware = (requestResponse, request, isFromSequentialQueue) => | ||
requestResponse.then((response) => { | ||
const responseOnyxData = response?.onyxData ?? []; | ||
responseOnyxData.forEach((onyxData) => { | ||
const key = onyxData.key; | ||
if (!key.startsWith(ONYXKEYS.COLLECTION.REPORT)) { | ||
return; | ||
} | ||
|
||
if (!onyxData.value) { | ||
return; | ||
} | ||
|
||
const report: Report = onyxData.value as Report; | ||
const preexistingReportID = report.preexistingReportID; | ||
if (!preexistingReportID) { | ||
return; | ||
} | ||
const oldReportID = request.data?.reportID; | ||
const offset = isFromSequentialQueue ? 1 : 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would explain this with a code comment just a little bit. Something like:
|
||
PersistedRequests.getAll() | ||
.slice(offset) | ||
.forEach((persistedRequest, index) => { | ||
const persistedRequestClone = _.clone(persistedRequest); | ||
persistedRequestClone.data = deepReplaceKeysAndValues(persistedRequest.data, oldReportID as string, preexistingReportID); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Idea for future improvement: return a boolean if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think Onyx will also check this, but it incurs an extra object comparison so - maybe it can make some difference. |
||
PersistedRequests.update(index + offset, persistedRequestClone); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Idea for future improvement: don't run There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably depends on the number of pending requests. I would keep this how it is but keep an eye on it. Alternatively test it by going offline and triggering a bunch of different action. Then go online and observe how quickly the UI updates (non-scientific approach). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another school of thought is that this is gonna be pretty edge case so maybe we can get away with what we've got here.. |
||
}); | ||
}); | ||
return response; | ||
}); | ||
|
||
export default handleUnusedOptimisticID; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
import HandleUnusedOptimisticID from './HandleUnusedOptimisticID'; | ||
import Logging from './Logging'; | ||
import Reauthentication from './Reauthentication'; | ||
import RecheckConnection from './RecheckConnection'; | ||
import SaveResponseInOnyx from './SaveResponseInOnyx'; | ||
|
||
export {Logging, Reauthentication, RecheckConnection, SaveResponseInOnyx}; | ||
export {HandleUnusedOptimisticID, Logging, Reauthentication, RecheckConnection, SaveResponseInOnyx}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
type ReplaceableValue = Record<string, unknown> | unknown[] | string | number | boolean | undefined | null; | ||
|
||
/** | ||
* @param target the object or value to transform | ||
* @param oldVal the value to search for | ||
* @param newVal the replacement value | ||
*/ | ||
function deepReplaceKeysAndValues<T extends ReplaceableValue>(target: T, oldVal: string, newVal: string): T { | ||
if (!target) { | ||
return target; | ||
} | ||
|
||
if (typeof target === 'string') { | ||
return target.replace(oldVal, newVal) as T; | ||
} | ||
|
||
if (typeof target !== 'object') { | ||
return target; | ||
} | ||
|
||
if (Array.isArray(target)) { | ||
return target.map((item) => deepReplaceKeysAndValues(item as T, oldVal, newVal)) as T; | ||
} | ||
|
||
const newObj: Record<string, unknown> = {}; | ||
Object.entries(target).forEach(([key, val]) => { | ||
const newKey = key.replace(oldVal, newVal); | ||
|
||
if (typeof val === 'object') { | ||
newObj[newKey] = deepReplaceKeysAndValues(val as T, oldVal, newVal); | ||
return; | ||
} | ||
|
||
if (val === oldVal) { | ||
newObj[newKey] = newVal; | ||
return; | ||
} | ||
|
||
if (typeof val === 'string') { | ||
newObj[newKey] = val.replace(oldVal, newVal); | ||
return; | ||
} | ||
|
||
newObj[newKey] = val; | ||
}); | ||
|
||
return newObj as T; | ||
} | ||
|
||
export default deepReplaceKeysAndValues; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import Onyx from 'react-native-onyx'; | ||
import * as NetworkStore from '../../src/libs/Network/NetworkStore'; | ||
import * as Request from '../../src/libs/Request'; | ||
import * as SequentialQueue from '../../src/libs/Network/SequentialQueue'; | ||
import * as TestHelper from '../utils/TestHelper'; | ||
import waitForNetworkPromises from '../utils/waitForNetworkPromises'; | ||
import ONYXKEYS from '../../src/ONYXKEYS'; | ||
import * as MainQueue from '../../src/libs/Network/MainQueue'; | ||
import HttpUtils from '../../src/libs/HttpUtils'; | ||
|
||
Onyx.init({ | ||
keys: ONYXKEYS, | ||
}); | ||
|
||
beforeAll(() => { | ||
global.fetch = TestHelper.getGlobalFetchMock(); | ||
}); | ||
|
||
beforeEach(async () => { | ||
SequentialQueue.pause(); | ||
MainQueue.clear(); | ||
HttpUtils.cancelPendingRequests(); | ||
NetworkStore.checkRequiredData(); | ||
await waitForNetworkPromises(); | ||
jest.clearAllMocks(); | ||
Request.clearMiddlewares(); | ||
}); | ||
|
||
describe('Middleware', () => { | ||
describe('HandleUnusedOptimisticID', () => { | ||
test('Normal request', async () => { | ||
const actual = jest.requireActual('../../src/libs/Middleware/HandleUnusedOptimisticID'); | ||
const handleUnusedOptimisticID = jest.spyOn(actual, 'default'); | ||
Request.use(handleUnusedOptimisticID); | ||
const requests = [ | ||
{ | ||
command: 'OpenReport', | ||
data: {authToken: 'testToken', reportID: '1234'}, | ||
}, | ||
{ | ||
command: 'AddComment', | ||
data: {authToken: 'testToken', reportID: '1234', reportActionID: '5678', reportComment: 'foo'}, | ||
}, | ||
]; | ||
for (const request of requests) { | ||
SequentialQueue.push(request); | ||
} | ||
SequentialQueue.unpause(); | ||
await waitForNetworkPromises(); | ||
|
||
expect(global.fetch).toHaveBeenCalledTimes(2); | ||
expect(global.fetch).toHaveBeenLastCalledWith('https://www.expensify.com.dev/api?command=AddComment', expect.anything()); | ||
TestHelper.assertFormDataMatchesObject(global.fetch.mock.calls[1][1].body, {reportID: '1234', reportActionID: '5678', reportComment: 'foo'}); | ||
expect(global.fetch).toHaveBeenNthCalledWith(1, 'https://www.expensify.com.dev/api?command=OpenReport', expect.anything()); | ||
TestHelper.assertFormDataMatchesObject(global.fetch.mock.calls[0][1].body, {reportID: '1234'}); | ||
}); | ||
|
||
test('Request with preexistingReportID', async () => { | ||
const actual = jest.requireActual('../../src/libs/Middleware/HandleUnusedOptimisticID'); | ||
const handleUnusedOptimisticID = jest.spyOn(actual, 'default'); | ||
Request.use(handleUnusedOptimisticID); | ||
const requests = [ | ||
{ | ||
command: 'OpenReport', | ||
data: {authToken: 'testToken', reportID: '1234'}, | ||
}, | ||
{ | ||
command: 'AddComment', | ||
data: {authToken: 'testToken', reportID: '1234', reportActionID: '5678', reportComment: 'foo'}, | ||
}, | ||
]; | ||
for (const request of requests) { | ||
SequentialQueue.push(request); | ||
} | ||
|
||
global.fetch.mockImplementationOnce(async () => ({ | ||
ok: true, | ||
json: async () => ({ | ||
jsonCode: 200, | ||
onyxData: [ | ||
{ | ||
onyxMethod: Onyx.METHOD.MERGE, | ||
key: `${ONYXKEYS.COLLECTION.REPORT}1234`, | ||
value: { | ||
preexistingReportID: '5555', | ||
}, | ||
}, | ||
], | ||
}), | ||
})); | ||
|
||
SequentialQueue.unpause(); | ||
await waitForNetworkPromises(); | ||
|
||
expect(global.fetch).toHaveBeenCalledTimes(2); | ||
expect(global.fetch).toHaveBeenLastCalledWith('https://www.expensify.com.dev/api?command=AddComment', expect.anything()); | ||
TestHelper.assertFormDataMatchesObject(global.fetch.mock.calls[1][1].body, {reportID: '5555', reportActionID: '5678', reportComment: 'foo'}); | ||
expect(global.fetch).toHaveBeenNthCalledWith(1, 'https://www.expensify.com.dev/api?command=OpenReport', expect.anything()); | ||
TestHelper.assertFormDataMatchesObject(global.fetch.mock.calls[0][1].body, {reportID: '1234'}); | ||
}); | ||
}); | ||
}); |
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.
key
could beundefined
which caused this issue - #28748 (comment)