Skip to content

fix: drop cached project instance on rejoin - #199

Merged
RangerMauve merged 5 commits into
mainfrom
fix/drop-cached-project-instance-on-rejoin
Aug 6, 2026
Merged

fix: drop cached project instance on rejoin#199
RangerMauve merged 5 commits into
mainfrom
fix/drop-cached-project-instance-on-rejoin

Conversation

@gmaclennan

Copy link
Copy Markdown
Member

useSingleProject caches the project client with staleTime/gcTime Infinity, but in @comapeo/ipc >=9 project instances have a lifecycle and can be closed. Without this fix, re-joining a project re-used a cached closed project instnace resulting in a ProjectClosed error.

This fix drops the cached project client in useAcceptInvite's onSuccess (removeQueries on the project-by-id key), so the next observer fetches a fresh instance. @comapeo/ipc >= 9.0.1 then validates the instance against the server on getProject, returning a working client for the re-opened project. The ipc devDependency is bumped to 9.0.1 accordingly.

Includes a regression test that runs two real MapeoManagers connected as local peers, driving the invitee through the hooks over real IPC: accept invite → use project → get removed → leave → re-accept → use project. It fails with ProjectClosed without the fix and passes with it. The test runs in the node environment because the jsdom environment replaces the Uint8Array global, which breaks @comapeo/core's timing-safe inviteId comparison and aborts the invite flow.

Verified end-to-end locally with this fix applied: the remove → leave → re-invite → accept cycle that previously crashed with ProjectClosed now completes and the re-joined project is fully usable.

Regression test for digidem/comapeo-mobile#2042 and #2041: accepting a
re-invite after leaving a project closes the stale project instance on
the manager, and with @comapeo/ipc 9.0.0 the client kept returning the
closed instance, so every project call rejected with ProjectClosed until
app restart. Fixed by @comapeo/ipc 9.0.1, which this test verifies
end-to-end through the hooks (accept invite, use project, get removed,
leave, re-accept, use project again).

Runs in the node environment because the jsdom environment replaces the
Uint8Array global, which breaks @comapeo/core's timing-safe inviteId
comparison (cross-realm instanceof) and aborts the invite flow.
Accepting an invite (re-)adds the project on the backend
(MapeoManager.addProject), which closes any project instance that was
open before the invite — e.g. after being removed from and leaving the
project — and opens a fresh one. useSingleProject caches the project
client with staleTime/gcTime Infinity, so every hook kept using the
closed instance and rejected with ProjectClosed until app restart.

Fixes digidem/comapeo-mobile#2042 and digidem/comapeo-mobile#2041
together with @comapeo/ipc >= 9.0.1, which re-validates the cached
instance against the server on getProject.
@awana-lockfile-bot

Copy link
Copy Markdown

package-lock.json changes

Click to toggle table visibility
Name Status Previous Current
@comapeo/ipc UPDATED 9.0.0 9.0.1

@socket-security

socket-security Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​comapeo/​ipc@​9.0.0 ⏵ 9.0.176 +1100100 +195 -1100

View full report

@achou11 achou11 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change seems solid based on added regression test. Would still like some clarification on the question I have about the cause of the issue though.

Comment thread src/hooks/invites.ts
Comment on lines +101 to +109
// Accepting an invite (re-)adds the project on the backend, which
// closes any project instance that was open before the invite (e.g.
// after leaving the project) and opens a fresh one. The project
// client is cached with staleTime/gcTime Infinity, so drop it here
// or every observer keeps using the closed instance.
queryClient.removeQueries({
queryKey: getProjectByIdQueryKey({ projectId }),
exact: true,
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, is the issue more specific to having gcTime: Inifinity? From my understanding of the docs here, the existing invalidation of all project queries just below should already account for queries using staleTime: Infinity due to inexact query matching:

set staleTime to Infinity to never trigger a refetch until the Query is invalidated manually.

'static' and Infinity both prevent staleness-based refetches, but 'static' is stricter: queryClient.invalidateQueries() can invalidate a query with staleTime: Infinity, but has no effect on staleTime: 'static'. refetchOnMount, refetchOnWindowFocus, and refetchOnReconnect set to "always" are also blocked by 'static'. Use 'static' for data that cannot change while the app is running: feature flags fetched at boot, user permissions loaded at login, static reference tables. Use Infinity when you still want manual invalidation to work.

Or is this tangential to what you're describing?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah this detail about the difference between removeQueries and invalidateQueries maybe helps a bit:

Unlike invalidateQueries or refetchQueries, removeQueries removes matching queries from the cache instead of refetching them.

So I guess the existing invalidation is triggering a refetch of the project instance, but maybe the refetch is returning a value that is referentially equivalent to the stale instance, and thus the observers are still using the stale one? Can't fully remember how IPC handles getProject calls for existing project instances.

Could be way off here...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the issue is that regardless of the gcTime and staleTime, react-query will serve the "stale" reference first, while it refetches the new reference, and that's what causes our errors. We could stop caching anything in react-query, but that would increase IPC traffic because getProject() does a round-trip check that the project is not closed for every call. However I think this is all getting rather messy to I'm considering dropping the changes to comapeo-ipc which made it "instance aware" at the expense of not being able to simply run the core e2e tests in core-react-native (the e2e tests as written require full lifecycle open -> close control)

Comment on lines +65 to +68
// (`MapeoManager.addProject`) and opens a fresh one. The project client
// wrapper is cached with `staleTime: Infinity`, so without invalidation the
// hooks keep using the closed instance and every project call rejects with
// ProjectClosed until app restart.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar question about precision of the root cause of the issue. My understanding is that it's more about the gcTime setting than the staleTime setting.

@achou11 achou11 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small thing that I found while re-reviewing

Comment thread test/hooks/invite-rejoin.test.ts
@RangerMauve
RangerMauve requested a review from achou11 August 5, 2026 19:47
@RangerMauve
RangerMauve merged commit 09698a6 into main Aug 6, 2026
8 checks passed
@RangerMauve
RangerMauve deleted the fix/drop-cached-project-instance-on-rejoin branch August 6, 2026 12:33
RangerMauve pushed a commit that referenced this pull request Aug 6, 2026
## Optic Release Automation

This **draft** PR is opened by Github action
[optic-release-automation-action](https://github.com/nearform-actions/optic-release-automation-action).

A new **draft** GitHub release
[v12.0.3](https://github.com/digidem/comapeo-core-react/releases/tag/untagged-39a2ab34637dfd20e41d)
has been created.

Release author: @RangerMauve

#### If you want to go ahead with the release, please merge this PR.
When you merge:

- The GitHub release will be published

- The npm package with tag latest will be published according to the
publishing rules you have configured



- No major or minor tags will be updated as configured


#### If you close the PR

- The new draft release will be deleted and nothing will change

## What's Changed
* fix: drop cached project instance on rejoin by @gmaclennan in
#199


**Full Changelog**:
v12.0.2...v12.0.3

<!--

<release-meta>{"id":366207480,"version":"v12.0.3","npmTag":"latest","opticUrl":"https://optic-zf3votdk5a-ew.a.run.app/api/generate/"}</release-meta>
-->

Co-authored-by: RangerMauve <actions@users.noreply.github.com>
gmaclennan added a commit that referenced this pull request Aug 20, 2026
Dropping the identity assertion left the regression without anything to
fail on. Assert instead what #199 was actually about: a call made
directly through the reference the hooks hand out after re-joining
reaches a live instance rather than the closed one that was cached
before the leave.

Asserted through the reference rather than only through the settings
query, and deliberately not through the pre-leave reference - that one
recovers under v10, where references are permanent, but stays closed
under v9, so it is not a property both majors share.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants