Skip to content
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

Client side pagination gap detection #41962

Merged
merged 130 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
130 commits
Select commit Hold shift + click to select a range
c88356f
Client previousReportActionID proof of concept
janicduplessis May 10, 2024
8f39e39
New implementation
janicduplessis May 24, 2024
273ec0f
wip
janicduplessis May 24, 2024
068f53e
Consolidate appversion in enhanceparameters
roryabraham May 24, 2024
49a1034
wip
janicduplessis May 24, 2024
e4be92c
DRY up applyOptimisticOnyxData
roryabraham May 24, 2024
d59db4b
Add API.paginate
roryabraham May 24, 2024
bf0d282
Handle delete / reordered actions, add more tests
janicduplessis May 25, 2024
450189f
Set up bulk of generalized Middleware
roryabraham May 24, 2024
867abfc
Implement PaginationUtils.mergeContinuousPages
roryabraham May 25, 2024
780f631
Implement API.paginate for GetOlderActions and GetNewerActions
roryabraham May 27, 2024
23558ae
Make API.pagination work for non-read request types
roryabraham May 27, 2024
1200cea
Rename CommandForRequestType
roryabraham May 27, 2024
b0cf310
Update existing usages of API.paginate
roryabraham May 27, 2024
8ca521e
Add API.paginate to openReport
roryabraham May 27, 2024
8eb364d
Merge branch 'main' into Rory-PaginationNation
roryabraham May 28, 2024
962d006
Upgrade typescript for ConditionalKeys fix
roryabraham May 28, 2024
1b70ee9
Remove duplicate comment
roryabraham May 28, 2024
4afecc6
Fix CommandOfType utility
roryabraham May 28, 2024
f317885
Fix generic type default in PaginatedRequest
roryabraham May 28, 2024
90aecb3
Move pusherSocketID from enhanceParameters back to API/index.js
roryabraham May 28, 2024
7fa3b2c
Consolidate PaginationConfig type
roryabraham May 28, 2024
3326fae
Misc cleanup
roryabraham May 28, 2024
c8b2f50
Add getContinuousChain in PaginationUtils
roryabraham May 28, 2024
4561beb
Save draft state - moving getContinuousChain to PaginationUtils
roryabraham May 29, 2024
f785b7e
Add a comment explaining the Pagination middleware
roryabraham May 29, 2024
5a8c40a
Merge branch 'main' into Rory-PaginationNation
roryabraham May 30, 2024
c20540b
fix package-lock.json
roryabraham May 30, 2024
e5038c1
Use new PaginationUtils.getContinuousChain
janicduplessis May 30, 2024
27cf878
Handle no cache entries
janicduplessis May 30, 2024
3de5595
Merge remote-tracking branch 'upstream/main' into @janic/client-actio…
janicduplessis Jun 6, 2024
2bc68fd
Fixes after update main
janicduplessis Jun 6, 2024
5428d94
Back in working state
janicduplessis Jun 6, 2024
77eff74
Ts fixes and minor api changes
janicduplessis Jun 6, 2024
8dd4dfe
Fix most tests
janicduplessis Jun 7, 2024
e908d64
Fix
janicduplessis Jun 7, 2024
1a4ec80
Fix scrolling from linked message
janicduplessis Jun 7, 2024
ba6358e
Move config outside of request object
janicduplessis Jun 7, 2024
2fa4d41
UI tests wip
janicduplessis Jun 8, 2024
fed68e1
Merge branch 'main' into @janic/client-action-chain
roryabraham Jun 10, 2024
84c1c6c
Bump type-fest
roryabraham Jun 10, 2024
4161096
Use Onyx.connect instead of OnyxCache
janicduplessis Jun 11, 2024
70aef20
Fixes
janicduplessis Jun 11, 2024
159f0a1
Simplify OnyxPagesKey type
roryabraham Jun 12, 2024
61f249c
Fix API.paginate type overloads
roryabraham Jun 12, 2024
43caf1e
Use a switch statement in API.paginate
roryabraham Jun 12, 2024
ff1883f
Remove outdated TODO
roryabraham Jun 12, 2024
9b33c67
Fix typo
roryabraham Jun 12, 2024
c3200a2
Add comments to explain local objects in Pagination actions file
roryabraham Jun 12, 2024
db30f6b
Extract ItemWithIndex type in PaginationUtils
roryabraham Jun 12, 2024
e8d5b41
Add comments to PaginationConfig type
roryabraham Jun 12, 2024
a527300
Remove type casts from Onyx.connect
roryabraham Jun 12, 2024
64c8161
remove TODO: TODONOTHING
roryabraham Jun 12, 2024
9155177
Merge branch 'main' into @janic/client-action-chain
roryabraham Jun 12, 2024
f9f4491
Document Pages shape
roryabraham Jun 12, 2024
c0fcffb
Remove duplicate ReportActionsPages type
roryabraham Jun 12, 2024
035e81c
Suppress lint for global fetch any type
roryabraham Jun 12, 2024
c67511c
Revert TS bump
roryabraham Jun 12, 2024
91025c6
Merge branch 'main' into @janic/client-action-chain
roryabraham Jun 12, 2024
bfa6dbd
Remove unnecessary optimistic previousReportActionID
roryabraham Jun 12, 2024
fa30c87
Remove unnecessary effect from ReportActionsView
roryabraham Jun 12, 2024
63e3a74
Fix some type errors
roryabraham Jun 12, 2024
20f5bd6
Fix mockFetch types
roryabraham Jun 12, 2024
f7671bf
Use regular for loop to iterate backwards without copy
roryabraham Jun 12, 2024
f8dc223
Update TS again
roryabraham Jun 12, 2024
cb796cd
Fix mock
janicduplessis Jun 13, 2024
6cb459c
Work on ui tests
janicduplessis Jun 14, 2024
fa845ca
Fix findLastItem
janicduplessis Jun 14, 2024
7c5bc5b
Merge branch 'main' into @janic/client-action-chain
roryabraham Jun 19, 2024
ad72970
Remove empty file
roryabraham Jun 19, 2024
40a3a6a
Make react-is a dev dependency
roryabraham Jun 19, 2024
bf3ef1d
Improve test mocks and assertions
janicduplessis Jun 19, 2024
3401769
Fix ts errors
roryabraham Jun 19, 2024
2bb077b
Add end marker
janicduplessis Jun 19, 2024
093c15f
Remove console.debug mock
janicduplessis Jun 19, 2024
5c521ac
make storybook plugins dev dependencies
roryabraham Jun 19, 2024
60f3798
Fix storybook
roryabraham Jun 19, 2024
e1b0b94
Merge branch 'main' into @janic/client-action-chain
roryabraham Jun 19, 2024
19c97f5
Update Podfile.lock
roryabraham Jun 19, 2024
2f3a152
DRY up listener mock
roryabraham Jun 19, 2024
1444238
Fix tsc and lint
roryabraham Jun 20, 2024
6e8b047
Merge branch 'main' into @janic/client-action-chain
roryabraham Jun 20, 2024
4a9b730
Merge branch 'main' into @janic/client-action-chain
roryabraham Jun 20, 2024
9e768af
Fix test ID (updated on main)
roryabraham Jun 20, 2024
97a39d9
Merge branch 'main' into @janic/client-action-chain
roryabraham Jun 20, 2024
c8e3dbe
WTF - why does this fix my test?
roryabraham Jun 20, 2024
29fcb8b
Use correct type in processRequest for side effect requests
roryabraham Jun 20, 2024
428ed08
Fix and add tests
janicduplessis Jun 20, 2024
ef6ba10
Finalize GetNewerActions test assertions
janicduplessis Jun 20, 2024
0fe243d
Add more mergeContinuousPages tests
janicduplessis Jun 21, 2024
ea552f5
Merge remote-tracking branch 'upstream/main' into @janic/client-actio…
janicduplessis Jun 24, 2024
829bc9d
Fix enhanceParametersTest
roryabraham Jun 24, 2024
54c2f91
Add back testID
roryabraham Jun 24, 2024
b69ee4c
Refactor and fixed tests#
roryabraham Jun 24, 2024
fd8b278
Fix empty reportID
janicduplessis Jun 25, 2024
72a3bf1
Merge remote-tracking branch 'upstream/main' into @janic/client-actio…
janicduplessis Jun 25, 2024
4e735de
Fix package lock after main merge
janicduplessis Jun 25, 2024
c51c369
Remove useless null coalescing
janicduplessis Jun 25, 2024
14610af
Remove unused import
roryabraham Jun 25, 2024
5a03bd9
Fix TestHelper type
roryabraham Jun 25, 2024
0d92f2d
Ignore lint warnings in test debug util
roryabraham Jun 25, 2024
9103a51
Merge branch 'main' into @janic/client-action-chain
roryabraham Jun 25, 2024
73249e8
Remove unrelated storybook diff
roryabraham Jun 25, 2024
367a0c1
Remove extra successData from bad merge
roryabraham Jun 25, 2024
c673561
Remove unrelated keyboard type change
roryabraham Jun 25, 2024
5905054
Fix perf test
janicduplessis Jun 25, 2024
df7dd20
Merge branch 'main' into @janic/client-action-chain
roryabraham Jun 25, 2024
86d4534
Merge branch 'main' into @janic/client-action-chain
roryabraham Jun 26, 2024
504d687
Fix comment
janicduplessis Jun 27, 2024
66bd418
Merge remote-tracking branch 'upstream/main' into @janic/client-actio…
janicduplessis Jun 27, 2024
93e8185
Merge remote-tracking branch 'upstream/main' into @janic/client-actio…
janicduplessis Jun 28, 2024
9f616be
Merge remote-tracking branch 'upstream/main' into @janic/client-actio…
janicduplessis Jun 28, 2024
abb0d3a
Revert change in CONFIG.ts
janicduplessis Jun 28, 2024
4eb350f
Remove it.only
janicduplessis Jun 28, 2024
2f76d05
Address review comments in Pagination.ts
janicduplessis Jun 28, 2024
c841bc4
Update PaginationConfig type
janicduplessis Jun 28, 2024
70ac619
Remove extracted debug util
janicduplessis Jun 28, 2024
815a63e
Cleanup package-lock
janicduplessis Jun 28, 2024
dec7e9a
Add comment to getPagesWithIndexes
janicduplessis Jun 28, 2024
74375cf
Rename to mergeAndSortContinuousPages
janicduplessis Jun 28, 2024
8fe8324
Fix test names
janicduplessis Jun 28, 2024
c74a87f
Fix ts error
janicduplessis Jun 28, 2024
6024176
Merge branch 'main' into @janic/client-action-chain
roryabraham Jun 28, 2024
80d4d7b
Rename validateReadyToRead to waitForWrites
roryabraham Jul 1, 2024
5b5aa7e
Apply clientUpdateID to all requests
roryabraham Jul 1, 2024
eaa7b19
Merge remote-tracking branch 'upstream/main' into @janic/client-actio…
janicduplessis Jul 4, 2024
a3d792f
Fixes
janicduplessis Jul 4, 2024
ceb584f
Fix tests
janicduplessis Jul 4, 2024
e592214
Merge remote-tracking branch 'upstream/main' into @janic/client-actio…
janicduplessis Jul 4, 2024
1fded14
Add comment
janicduplessis Jul 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion __mocks__/react-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ jest.doMock('react-native', () => {
// so it seems easier to just run the callback immediately in tests.
InteractionManager: {
...ReactNative.InteractionManager,
runAfterInteractions: (callback: () => void) => callback(),
runAfterInteractions: (callback: () => void) => {
callback();
return {cancel: () => {}};
},
},
},
ReactNative,
Expand Down
14 changes: 8 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,8 @@
"time-analytics-webpack-plugin": "^0.1.17",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"type-fest": "^4.10.2",
"typescript": "^5.3.2",
"type-fest": "4.20.0",
"typescript": "^5.4.5",
"wait-port": "^0.2.9",
"webpack": "^5.76.0",
"webpack-bundle-analyzer": "^4.5.0",
Expand Down
3 changes: 3 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4814,6 +4814,9 @@ const CONST = {
DESC: 'desc',
},

PAGINATION_START_ID: '-1',
PAGINATION_END_ID: '-2',
janicduplessis marked this conversation as resolved.
Show resolved Hide resolved

SUBSCRIPTION_SIZE_LIMIT: 20000,
SUBSCRIPTION_POSSIBLE_COST_SAVINGS: {
COLLECT_PLAN: 10,
Expand Down
7 changes: 5 additions & 2 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {ValueOf} from 'type-fest';
import type {ConditionalKeys, ValueOf} from 'type-fest';
import type CONST from './CONST';
import type * as FormTypes from './types/form';
import type * as OnyxTypes from './types/onyx';
Expand Down Expand Up @@ -353,6 +353,7 @@ const ONYXKEYS = {
REPORT_METADATA: 'reportMetadata_',
REPORT_ACTIONS: 'reportActions_',
REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_',
REPORT_ACTIONS_PAGES: 'reportActionsPages_',
REPORT_ACTIONS_REACTIONS: 'reportActionsReactions_',
REPORT_DRAFT_COMMENT: 'reportDraftComment_',
REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_',
Expand Down Expand Up @@ -569,6 +570,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata;
[ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions;
[ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts;
[ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES]: OnyxTypes.Pages;
[ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions;
[ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string;
[ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean;
Expand Down Expand Up @@ -709,11 +711,12 @@ type OnyxFormDraftKey = keyof OnyxFormDraftValuesMapping;
type OnyxValueKey = keyof OnyxValuesMapping;

type OnyxKey = OnyxValueKey | OnyxCollectionKey | OnyxFormKey | OnyxFormDraftKey;
type OnyxPagesKey = ConditionalKeys<OnyxValues, OnyxTypes.Pages>;

roryabraham marked this conversation as resolved.
Show resolved Hide resolved
type MissingOnyxKeysError = `Error: Types don't match, OnyxKey type is missing: ${Exclude<AllOnyxKeys, OnyxKey>}`;
/** If this type errors, it means that the `OnyxKey` type is missing some keys. */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type AssertOnyxKeys = AssertTypesEqual<AllOnyxKeys, OnyxKey, MissingOnyxKeysError>;

export default ONYXKEYS;
export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxValueKey, OnyxValues};
export type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxFormDraftKey, OnyxFormKey, OnyxFormValuesMapping, OnyxKey, OnyxPagesKey, OnyxValueKey, OnyxValues};
188 changes: 123 additions & 65 deletions src/libs/API/index.ts
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {SetRequired} from 'type-fest';
import Log from '@libs/Log';
import * as Middleware from '@libs/Middleware';
import * as SequentialQueue from '@libs/Network/SequentialQueue';
Expand All @@ -8,9 +9,9 @@ import * as Request from '@libs/Request';
import * as PersistedRequests from '@userActions/PersistedRequests';
import CONST from '@src/CONST';
import type OnyxRequest from '@src/types/onyx/Request';
import type {PaginatedRequest, PaginationConfig} from '@src/types/onyx/Request';
import type Response from '@src/types/onyx/Response';
import pkg from '../../../package.json';
import type {ApiRequest, ApiRequestCommandParameters, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types';
import type {ApiCommand, ApiRequestCommandParameters, ApiRequestType, CommandOfType, ReadCommand, SideEffectRequestCommand, WriteCommand} from './types';

// Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next).
// Note: The ordering here is intentional as we want to Log, Recheck Connection, Reauthenticate, and Save the Response in Onyx. Errors thrown in one middleware will bubble to the next.
Expand All @@ -28,6 +29,8 @@ Request.use(Middleware.Reauthentication);
// If an optimistic ID is not used by the server, this will update the remaining serialized requests using that optimistic ID to use the correct ID instead.
Request.use(Middleware.HandleUnusedOptimisticID);

Request.use(Middleware.Pagination);

// SaveResponseInOnyx - Merges either the successData or failureData (or finallyData, if included in place of the former two values) into Onyx depending on if the call was successful or not. This needs to be the LAST middleware we use, don't add any
// middlewares after this, because the SequentialQueue depends on the result of this middleware to pause the queue (if needed) to bring the app to an up-to-date state.
Request.use(Middleware.SaveResponseInOnyx);
Expand All @@ -40,54 +43,83 @@ type OnyxData = {
};

/**
* All calls to API.write() will be persisted to disk as JSON with the params, successData, and failureData (or finallyData, if included in place of the former two values).
* This is so that if the network is unavailable or the app is closed, we can send the WRITE request later.
*
* @param command - Name of API command to call.
* @param apiCommandParameters - Parameters to send to the API.
* @param onyxData - Object containing errors, loading states, and optimistic UI data that will be merged
* into Onyx before and after a request is made. Each nested object will be formatted in
* the same way as an API response.
* @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
* @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
* @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
* @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
* Prepare the request to be sent. Bind data together with request metadata and apply optimistic Onyx data.
*/
function write<TCommand extends WriteCommand>(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}) {
Log.info('Called API write', false, {command, ...apiCommandParameters});
const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData;
function prepareRequest<TCommand extends ApiCommand>(command: TCommand, type: ApiRequestType, params: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): OnyxRequest {
Log.info('[API] Preparing request', false, {command, type});

// Optimistically update Onyx
const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData;
if (optimisticData) {
Log.info('[API] Applying optimistic data', false, {command, type});
Onyx.update(optimisticData);
}

// Assemble the data we'll send to the API
const isWriteRequest = type === CONST.API_REQUEST_TYPE.WRITE;

// Prepare the data we'll send to the API
const data = {
...apiCommandParameters,
appversion: pkg.version,
apiRequestType: CONST.API_REQUEST_TYPE.WRITE,
...params,
apiRequestType: type,

// We send the pusherSocketID with all write requests so that the api can include it in push events to prevent Pusher from sending the events to the requesting client. The push event
// is sent back to the requesting client in the response data instead, which prevents a replay effect in the UI. See https://github.com/Expensify/App/issues/12775.
pusherSocketID: Pusher.getPusherSocketID(),
pusherSocketID: isWriteRequest ? Pusher.getPusherSocketID() : undefined,
};

// Assemble all the request data we'll be storing in the queue
const request: OnyxRequest = {
// Assemble all request metadata (used by middlewares, and for persisted requests stored in Onyx)
const request: SetRequired<OnyxRequest, 'data'> = {
command,
data: {
...data,

// This should be removed once we are no longer using deprecatedAPI https://github.com/Expensify/Expensify/issues/215650
shouldRetry: true,
canCancel: true,
},
data,
...onyxDataWithoutOptimisticData,
};

// This should be removed once we are no longer using deprecatedAPI https://github.com/Expensify/Expensify/issues/215650
if (isWriteRequest) {
request.data.shouldRetry = true;
request.data.canCancel = true;
}

return request;
}

/**
* Process a prepared request according to its type.
*/
function processRequest(request: OnyxRequest, type: ApiRequestType): Promise<void | Response> {
// Write commands can be saved and retried, so push it to the SequentialQueue
SequentialQueue.push(request);
if (type === CONST.API_REQUEST_TYPE.WRITE) {
SequentialQueue.push(request);
return Promise.resolve();
}

// Read requests are processed right away, but don't return the response to the caller
if (type === CONST.API_REQUEST_TYPE.READ) {
Request.processWithMiddleware(request);
return Promise.resolve();
}

// Requests with side effects process right away, and return the response to the caller
return Request.processWithMiddleware(request);
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* All calls to API.write() will be persisted to disk as JSON with the params, successData, and failureData (or finallyData, if included in place of the former two values).
* This is so that if the network is unavailable or the app is closed, we can send the WRITE request later.
*
* @param command - Name of API command to call.
* @param apiCommandParameters - Parameters to send to the API.
* @param onyxData - Object containing errors, loading states, and optimistic UI data that will be merged
* into Onyx before and after a request is made. Each nested object will be formatted in
* the same way as an API response.
* @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
* @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
* @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
* @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
*/
function write<TCommand extends WriteCommand>(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void {
Log.info('[API] Called API write', false, {command, ...apiCommandParameters});
const request = prepareRequest(command, CONST.API_REQUEST_TYPE.WRITE, apiCommandParameters, onyxData);
processRequest(request, CONST.API_REQUEST_TYPE.WRITE);
}

/**
Expand All @@ -111,36 +143,28 @@ function write<TCommand extends WriteCommand>(command: TCommand, apiCommandParam
* response back to the caller or to trigger reconnection callbacks when re-authentication is required.
* @returns
*/
function makeRequestWithSideEffects<TCommand extends SideEffectRequestCommand | WriteCommand | ReadCommand>(
function makeRequestWithSideEffects<TCommand extends SideEffectRequestCommand>(
command: TCommand,
apiCommandParameters: ApiRequestCommandParameters[TCommand],
onyxData: OnyxData = {},
apiRequestType: ApiRequest = CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS,
): Promise<void | Response> {
Log.info('Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters});
const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData;

// Optimistically update Onyx
if (optimisticData) {
Onyx.update(optimisticData);
}

// Assemble the data we'll send to the API
const data = {
...apiCommandParameters,
appversion: pkg.version,
apiRequestType,
};

// Assemble all the request data we'll be storing
const request: OnyxRequest = {
command,
data,
...onyxDataWithoutOptimisticData,
};
Log.info('[API] Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters});
const request = prepareRequest(command, CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS, apiCommandParameters, onyxData);

// Return a promise containing the response from HTTPS
return Request.processWithMiddleware(request);
return processRequest(request, CONST.API_REQUEST_TYPE.WRITE);
}

/**
* Ensure all write requests on the sequential queue have finished responding before running read requests.
* Responses from read requests can overwrite the optimistic data inserted by
* write requests that use the same Onyx keys and haven't responded yet.
*/
function validateReadyToRead<TCommand extends ReadCommand>(command: TCommand) {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
if (PersistedRequests.getLength() > 0) {
Log.info(`[API] '${command}' is waiting on ${PersistedRequests.getLength()} write commands`);
}
return SequentialQueue.waitForIdle();
}

/**
Expand All @@ -156,14 +180,48 @@ function makeRequestWithSideEffects<TCommand extends SideEffectRequestCommand |
* @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
* @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200.
*/
function read<TCommand extends ReadCommand>(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}) {
// Ensure all write requests on the sequential queue have finished responding before running read requests.
// Responses from read requests can overwrite the optimistic data inserted by
// write requests that use the same Onyx keys and haven't responded yet.
if (PersistedRequests.getLength() > 0) {
Log.info(`[API] '${command}' is waiting on ${PersistedRequests.getLength()} write commands`);
function read<TCommand extends ReadCommand>(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void {
Log.info('[API] Called API.read', false, {command, ...apiCommandParameters});

validateReadyToRead(command).then(() => {
const request = prepareRequest(command, CONST.API_REQUEST_TYPE.READ, apiCommandParameters, onyxData);
processRequest(request, CONST.API_REQUEST_TYPE.READ);
});
}

function paginate<TRequestType extends ApiRequestType, TCommand extends CommandOfType<TRequestType>>(
type: TRequestType,
command: TCommand,
apiCommandParameters: ApiRequestCommandParameters[TCommand],
onyxData: OnyxData,
config: PaginationConfig,
): TRequestType extends typeof CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS ? Promise<Response | void> : void;
function paginate<TRequestType extends ApiRequestType, TCommand extends CommandOfType<TRequestType>>(
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
type: TRequestType,
command: TCommand,
apiCommandParameters: ApiRequestCommandParameters[TCommand],
onyxData: OnyxData,
config: PaginationConfig,
): Promise<Response | void> | void {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
Log.info('[API] Called API.paginate', false, {command, ...apiCommandParameters});
const request: PaginatedRequest = {
...prepareRequest(command, type, apiCommandParameters, onyxData),
...config,
...{
isPaginated: true,
},
};

if (type === CONST.API_REQUEST_TYPE.WRITE) {
processRequest(request, type);
return;
}
SequentialQueue.waitForIdle().then(() => makeRequestWithSideEffects(command, apiCommandParameters, onyxData, CONST.API_REQUEST_TYPE.READ));

if (type === CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS) {
return processRequest(request, type);
}

validateReadyToRead(command as ReadCommand).then(() => processRequest(request, type));
}

export {write, makeRequestWithSideEffects, read};
export {write, makeRequestWithSideEffects, read, paginate};
11 changes: 9 additions & 2 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type * as Parameters from './parameters';
import type SignInUserParams from './parameters/SignInUserParams';
import type UpdateBeneficialOwnersForBankAccountParams from './parameters/UpdateBeneficialOwnersForBankAccountParams';

type ApiRequest = ValueOf<typeof CONST.API_REQUEST_TYPE>;
type ApiRequestType = ValueOf<typeof CONST.API_REQUEST_TYPE>;
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved

const WRITE_COMMANDS = {
SET_WORKSPACE_AUTO_REPORTING: 'SetWorkspaceAutoReporting',
Expand Down Expand Up @@ -566,4 +566,11 @@ type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameter

export {WRITE_COMMANDS, READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS};

export type {ApiRequest, ApiRequestCommandParameters, WriteCommand, ReadCommand, SideEffectRequestCommand};
type ApiCommand = WriteCommand | ReadCommand | SideEffectRequestCommand;
type CommandOfType<TRequestType extends ApiRequestType> = TRequestType extends typeof CONST.API_REQUEST_TYPE.WRITE
? WriteCommand
: TRequestType extends typeof CONST.API_REQUEST_TYPE.READ
? ReadCommand
: SideEffectRequestCommand;

export type {ApiCommand, ApiRequestType, ApiRequestCommandParameters, CommandOfType, WriteCommand, ReadCommand, SideEffectRequestCommand};
Loading
Loading