-
Notifications
You must be signed in to change notification settings - Fork 4k
[No QA] Fix import/no-cycle - part 1 #99670
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
Merged
roryabraham
merged 8 commits into
Expensify:main
from
callstack-internal:fix/no-cycle-api-middleware-registration
Aug 31, 2026
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
73583fc
[No QA] Move API middleware registration out of makeRequest to break …
LukasMod a508ad8
Register API middlewares in SearchPageTest
LukasMod 7fe84e1
Register API middlewares in TransactionGroupListItemTest and fold the…
LukasMod 4733d21
Add middleware registration tests
LukasMod 2bc8146
clean up comments
LukasMod d377e0b
Describe the import cycle in prose instead of arrows
LukasMod bad6bf0
Merge upstream/main into fix/no-cycle-api-middleware-registration
LukasMod 333b2c6
Merge remote-tracking branch 'origin/main' into fix/no-cycle-api-midd…
LukasMod File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,29 @@ | ||
| import FailureTracking from './FailureTracking'; | ||
| import FraudMonitoring from './FraudMonitoring'; | ||
| import handleDeletedAccount from './HandleDeletedAccount'; | ||
| import HandleUnusedOptimisticID from './HandleUnusedOptimisticID'; | ||
| import LoadPostDataForOpenOrReconnect from './LoadPostDataForOpenOrReconnect'; | ||
| import LoadTest from './LoadTest'; | ||
| import Logging from './Logging'; | ||
| import {Pagination} from './Pagination'; | ||
| import Reauthentication from './Reauthentication'; | ||
| import RecordFullReconnectTime from './RecordFullReconnectTime'; | ||
| import SaveResponseInOnyx from './SaveResponseInOnyx'; | ||
| import SentryServerTiming from './SentryServerTiming'; | ||
| import SupportalPermission from './SupportalPermission'; | ||
|
|
||
| export {HandleUnusedOptimisticID, LoadTest, Logging, Reauthentication, RecordFullReconnectTime, FailureTracking, SaveResponseInOnyx, Pagination, handleDeletedAccount, SupportalPermission}; | ||
| export { | ||
| HandleUnusedOptimisticID, | ||
| LoadTest, | ||
| Logging, | ||
| Reauthentication, | ||
| RecordFullReconnectTime, | ||
| FailureTracking, | ||
| SaveResponseInOnyx, | ||
| Pagination, | ||
| handleDeletedAccount, | ||
| SupportalPermission, | ||
| FraudMonitoring, | ||
| LoadPostDataForOpenOrReconnect, | ||
| SentryServerTiming, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import {addMiddleware} from '@libs/Request'; | ||
|
|
||
| import { | ||
| FailureTracking, | ||
| FraudMonitoring, | ||
| handleDeletedAccount, | ||
| HandleUnusedOptimisticID, | ||
| LoadPostDataForOpenOrReconnect, | ||
| LoadTest, | ||
| Logging, | ||
| Pagination, | ||
| Reauthentication, | ||
| RecordFullReconnectTime, | ||
| SaveResponseInOnyx, | ||
| SentryServerTiming, | ||
| SupportalPermission, | ||
| } from './index'; | ||
|
|
||
| // 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. | ||
| // e.g. an error thrown in Logging or Reauthenticate logic will be caught by the next middleware or the SequentialQueue which retries failing requests. | ||
| // | ||
| // This lives here rather than in libs/API because six of the middlewares below import user actions, and every | ||
| // user action imports libs/API. Registering from inside libs/API therefore closes an import cycle: libs/API | ||
| // imports Middleware, Middleware imports an action, and that action imports libs/API again. Instead the | ||
| // composition root imports this module for its side effect, before anything can call processWithMiddleware: | ||
| // see src/setup/index.ts. | ||
|
|
||
| // Logging - Logs request details and errors. | ||
| addMiddleware(Logging); | ||
|
|
||
| // Duplicates API calls (tagged with mockRequest=true) when the server sends load-test parameters via the X-Load-Test response header. | ||
| addMiddleware(LoadTest); | ||
|
|
||
| // FailureTracking - Observes request outcomes and feeds them to FailureTracker for sustained failure detection. | ||
| addMiddleware(FailureTracking); | ||
|
|
||
| // Reauthentication - Handles jsonCode 407 which indicates an expired authToken. We need to reauthenticate and get a new authToken with our stored credentials. | ||
| addMiddleware(Reauthentication); | ||
|
|
||
| // Handles the case when the copilot has been deleted. The response contains jsonCode 408 and a message indicating account deletion | ||
| addMiddleware(handleDeletedAccount); | ||
|
|
||
| // Handle supportal permission denial centrally | ||
| addMiddleware(SupportalPermission); | ||
|
|
||
| // 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. | ||
| addMiddleware(HandleUnusedOptimisticID); | ||
|
|
||
| addMiddleware(Pagination); | ||
|
|
||
| // SentryServerTiming - Tracks server round-trip time for configured command groups via Sentry spans. | ||
| addMiddleware(SentryServerTiming); | ||
|
|
||
| // RecordFullReconnectTime - Records the full-reconnect time into an OpenApp/full-ReconnectApp response. Must run before SaveResponseInOnyx applies the response. | ||
| addMiddleware(RecordFullReconnectTime); | ||
|
|
||
| // LoadPostDataForOpenOrReconnect - Sends the reads that OpenApp/ReconnectApp does not return, once per response that reaches the server. | ||
| addMiddleware(LoadPostDataForOpenOrReconnect); | ||
|
|
||
| // 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 must be the last middleware that applies Onyx data | ||
| // (middlewares after it, like FraudMonitoring, must not write Onyx), 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. | ||
| addMiddleware(SaveResponseInOnyx); | ||
|
|
||
| // FraudMonitoring - Tags the request with the appropriate Fraud Protection event. | ||
| addMiddleware(FraudMonitoring); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import type * as RequestModule from '@libs/Request'; | ||
| import {addMiddleware} from '@libs/Request'; | ||
|
|
||
| jest.mock('@libs/Request', () => ({ | ||
| ...jest.requireActual<typeof RequestModule>('@libs/Request'), | ||
| addMiddleware: jest.fn(), | ||
| })); | ||
|
|
||
| describe('src/setup attaches the API middlewares', () => { | ||
| it('registers all 13 middlewares at module scope when the composition root loads', () => { | ||
| expect(jest.mocked(addMiddleware)).not.toHaveBeenCalled(); | ||
|
|
||
| require('@src/setup'); | ||
|
|
||
| expect(jest.mocked(addMiddleware)).toHaveBeenCalledTimes(13); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { | ||
| FailureTracking, | ||
| FraudMonitoring, | ||
| handleDeletedAccount, | ||
| HandleUnusedOptimisticID, | ||
| LoadPostDataForOpenOrReconnect, | ||
| LoadTest, | ||
| Logging, | ||
| Pagination, | ||
| Reauthentication, | ||
| RecordFullReconnectTime, | ||
| SaveResponseInOnyx, | ||
| SentryServerTiming, | ||
| SupportalPermission, | ||
| } from '@libs/Middleware'; | ||
| import type * as RequestModule from '@libs/Request'; | ||
| import {addMiddleware} from '@libs/Request'; | ||
|
|
||
| jest.mock('@libs/Request', () => ({ | ||
| ...jest.requireActual<typeof RequestModule>('@libs/Request'), | ||
| addMiddleware: jest.fn(), | ||
| })); | ||
|
|
||
| const EXPECTED_ORDER: RequestModule.Middleware[] = [ | ||
| Logging, | ||
| LoadTest, | ||
| FailureTracking, | ||
| Reauthentication, | ||
| handleDeletedAccount, | ||
| SupportalPermission, | ||
| HandleUnusedOptimisticID, | ||
| Pagination, | ||
| SentryServerTiming, | ||
| RecordFullReconnectTime, | ||
| LoadPostDataForOpenOrReconnect, | ||
| SaveResponseInOnyx, | ||
| FraudMonitoring, | ||
| ]; | ||
|
|
||
| describe('Middleware registration', () => { | ||
| let registered: RequestModule.Middleware[] = []; | ||
|
|
||
| beforeAll(() => { | ||
| // jest.isolateModules would give register.ts its own module registry, so the middlewares it resolves | ||
| // would be distinct function objects from the ones imported above and every identity check would fail. | ||
| require('@libs/Middleware/register'); | ||
| registered = jest.mocked(addMiddleware).mock.calls.map(([middleware]) => middleware); | ||
| }); | ||
|
|
||
| it('registers every middleware exactly once, in the documented order', () => { | ||
| expect(registered).toEqual(EXPECTED_ORDER); | ||
| }); | ||
|
|
||
| it('registers all 13 middlewares with no duplicates', () => { | ||
| expect(registered).toHaveLength(13); | ||
| expect(new Set(registered).size).toBe(13); | ||
| }); | ||
|
|
||
| it('keeps SaveResponseInOnyx after every other Onyx-writing middleware and before FraudMonitoring', () => { | ||
| const indexOf = (middleware: RequestModule.Middleware) => registered.indexOf(middleware); | ||
|
|
||
| expect(indexOf(SaveResponseInOnyx)).toBeGreaterThanOrEqual(0); | ||
| expect(indexOf(RecordFullReconnectTime)).toBeLessThan(indexOf(SaveResponseInOnyx)); | ||
| expect(indexOf(LoadPostDataForOpenOrReconnect)).toBeLessThan(indexOf(SaveResponseInOnyx)); | ||
| expect(indexOf(FraudMonitoring)).toBeGreaterThan(indexOf(SaveResponseInOnyx)); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
NAB/followup: over the years I've come to find that side-effect-only modules are a bit of a foot-gun (at least, it can make testing/mocking a bit more difficult). If I were writing this today, I'd wrap all the
addMiddlewarecalls in a singleregisterfunction.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 prepared small follow up #100009