Skip to content

feat: dispatch shared-drive realtime events into the store - #3983

Draft
Crash-- wants to merge 27 commits into
masterfrom
feat/reactive-recents-shared-drives
Draft

Crash-- wants to merge 27 commits into
masterfrom
feat/reactive-recents-shared-drives

Conversation

@Crash--

@Crash-- Crash-- commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Update: reactivity now comes from the data-proxy push, no per-drive websocket

Instead of opening one CozyRealtime socket per recipient drive in every Drive tab, the shared worker now pushes shared-drive document changes to Drive. FilesRealTimeQueries listens for those pushes on window and feeds them into the same buffering and dispatch pipeline, so the store updates the same way, without any Drive-side websocket.

  • FilesRealTimeQueries no longer constructs CozyRealtime; it consumes { type: 'DATAPROXYMESSAGE', payload: { kind: 'realtime', event, doctype, driveId, doc } } messages (origin filtered to the data-proxy) and reuses the existing buffers, driveIdByFileId, path resolution and dispatch.
  • The worker side (relay on pouch onSync + forwarder) is in feat: relay shared-drive changes to drive via realtime push cozy/cozy-web-data-proxy#63, driven by the data-proxy periodic sync.
  • The own-files realtime path is unchanged.
  • Latency follows the worker periodic sync; a later change adds realtime inside the worker to bring it back to near-real-time through the same relay.

Original description below.


Summary

Goal: make the Recent view and the shared-drive folder listing update live (trash, rename, move, create) for files inside shared drives, the way they already do for a user's own files.

Approach: shared-drive files are replicated locally by the data-proxy worker under a per-drive doctype. We let any view query them as ordinary reactive io.cozy.files documents through cozy-client's store, by adding a driveId query option that reads the drive's database while keeping the documents typed io.cozy.files. Remote changes flow into the same store slice through one realtime socket per recipient drive.

The work is split across three repositories:

What this PR contains:

  • FilesRealTimeQueries opens one CozyRealtime per recipient shared drive (owner is false) and dispatches created, updated and deleted events into the store as io.cozy.files, reusing the existing buffering and dispatch pipeline.
  • Shared-drive file paths are resolved through the drive-scoped collection so the dispatched documents carry a correct path.
  • The own-files realtime path is unchanged; shared-drive support is additive.

Still in progress before this is ready for review:

  • Recipient-drive detection filters owner === false; confirm the sharings API always returns a boolean owner, or widen the filter so no recipient drive is silently skipped.
  • Code Health: the dispatch method and its tests carry added complexity and test duplication to revisit.
  • The end-to-end check needs the patched libraries linked into the worker build.
  • Follow-up PRs will move the Recent view and the shared-drive folder view onto this capability.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: db4938cc-97c2-4fef-a318-a24acfa7dc96

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/reactive-recents-shared-drives

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@Crash--

Crash-- commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Why the RecentScopeQuery component pattern

The reactive recents need one query for the user's own files plus one per recipient shared drive, and that count changes at runtime (drives are added or removed via the io.cozy.sharings realtime). The rules of hooks forbid calling useQuery in a loop or conditionally, so a variable number of queries cannot be expressed as direct hook calls.

The headless-component-per-scope pattern is the idiomatic React way to vary the number of live reactive queries: each RecentScopeQuery renders null, runs exactly one useQuery, and lifts its result to useRecentFiles via onResult. Mounting and unmounting a component is the legal way to change how many queries are active. From the outside useRecentFiles stays a hook returning { data, fetchStatus, error, scopeQueries }; the components are an internal detail the view renders invisibly.

Alternatives considered:

  • An imperative loop (client.query in an effect) would lose reactivity, which is exactly the dataProxy.recents() approach this PR replaces.
  • cozy-client's existing useQueries loops useQuery internally (with an eslint-disable), so it is only safe for a static set of queries, not a dynamic count.

A cleaner long-term fix, a dynamic-safe useQueries in cozy-client, is tracked in linagora/cozy-client#1692. Once available, useRecentFiles can drop this component pattern for a single useQueries(specs) call.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@Crash--
Crash-- force-pushed the feat/reactive-recents-shared-drives branch from c1f4c38 to 156d66d Compare July 2, 2026 10:49
codescene-delta-analysis[bot]

This comment was marked as outdated.

@Crash--
Crash-- force-pushed the feat/reactive-recents-shared-drives branch from 156d66d to b98950d Compare July 2, 2026 11:03
codescene-delta-analysis[bot]

This comment was marked as outdated.

@bundlemon

bundlemon Bot commented Jul 2, 2026

Copy link
Copy Markdown

BundleMon

Files updated (4)
Status Path Size Limits
static/js/main.(hash).js
56.2KB (+1.06KB +1.92%) -
static/js/public.(hash).js
26.39KB (+512B +1.93%) -
static/js/lib-polyfill.(hash).js
23.06KB (+309B +1.33%) -
static/js/(chunkId).(hash).js
59.96KB (+57B +0.09%) -
Unchanged files (16)
Status Path Size Limits
static/js/cozy.(hash).js
974.86KB -
static/resource/(hash).js
336.09KB -
services/qualificationMigration.js
283.4KB -
services/dacc.js
263.16KB -
static/js/lib-react.(hash).js
43.88KB -
static/css/cozy.(hash).css
29.5KB -
static/js/lib-router.(hash).js
21.86KB -
static/css/main.(hash).css
11.88KB -
static/js/async/(chunkId).(hash).js
7.5KB -
static/js/intents.(hash).js
6.98KB -
manifest.webapp
5.97KB -
index.html
772B -
public/index.html
704B -
intents/index.html
620B -
static/css/public.(hash).css
331B -
assets/manifest.json
185B -

Total files change +1.92KB +0.09%

Groups updated (1)
Status Path Size Limits
**/*.js
6.17MB (+1.37KB +0.02%) -
Unchanged groups (2)
Status Path Size Limits
**/*.{png,svg,ico}
2.16MB -
**/*.css
70.43KB -

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

codescene-delta-analysis[bot]

This comment was marked as outdated.

@rezk2ll rezk2ll 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.

other stuff i found, on recipient side:

  • The live view (store) can be correct while a refresh (which reads the recipient drive pouch) shows a just-added file missing or a just-deleted file still present. Root cause is on the worker and pouch-link side ( the other two PRs ), but flagging it here.

  • in targets/browser/setupAppContext.js: The link that serves every shared-drive query is only added when flag('dataproxy.queries.enabled') is truthy, but cozy-flags is not populated this early in setupApp, so on a cold load the link is missing and every drive query fails with No link could handle operation (the folder shows "Something went wrong")

Comment thread src/queries/index.ts Outdated
[sortAttribute]: { $gt: null }
})
.indexFields(['dir_id', 'driveId', sortAttribute])
.sortBy([{ dir_id: sortOrder }, { [sortAttribute]: sortOrder }])

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.

skipping driveId triggers the "sort order should be the same than the indexed fields" warning and an ad-hoc index. we should add { driveId: sortOrder } to the sortBy

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Something to add to my skill ^^

Comment thread src/components/FilesRealTimeQueries.jsx Outdated
? driveIdByFileId.get(filesInFolder[0]._id)
: undefined
const folder = await getParentFolder(client, folderId, driveId)
const files = filesInFolder.map(file => ensureFilePath(file, folder))

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.

we need to reapply driveId (already tracked in driveIdByFileId) onto each dispatched file.

Realtime docs arrive straight from the stack without the synthetic driveId (that field is added only by the data-proxy on replication). The dispatch reapplies path but not driveId, so cozy-client re-evaluates the shared-drive folder query (whose selector filters on driveId) against a doc that lacks it and drops the file from the live results. A refresh re-runs the pouch find and it reappears.

you can notice this when you rename a file in recipient side, it vanishes and reappear only on refresh. for owner it works fine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK for this one I think. Yes we alredy have the driveId we should reuse.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Crash-- added 8 commits July 6, 2026 19:41
…store

For each recipient shared drive (owner === false), open a CozyRealtime
socket scoped to that drive and feed created/updated/deleted io.cozy.files
events into the same buffer/dispatch pipeline as own-instance files, so
drive-backed useQuery results stay live. The global plugin subscription is
unchanged (no regression).
Carry the originating driveId for each buffered drive file via a
module-level driveIdByFileId map. In processEvents, pass that driveId to
getParentFolder, which now resolves the parent through a drive-scoped
client.collection('io.cozy.files', { driveId }).statById() call (returning
the folder from data, not included). Own-file resolution is unchanged.
- Export __resetDriveIdByFileId() from FilesRealTimeQueries and call it
  in beforeEach to prevent the module-level Map from leaking across tests
- Add mid-lifecycle test: asserts d1 stop() fires and CozyRealtime is
  constructed twice when the recipient-drive list changes on re-render
- Assert resolved path in the drive-dispatch test (regression guard for
  drive-scoped path resolution)
derive and return recipientDriveIds (owner !== true) from the hook so
components don't inline the recipient-detection logic. includes both
owner === false and owner === undefined drives; excludes only owner === true.
…e parent locally

- FilesRealTimeQueries now reads recipientDriveIds from useSharedDrives
  instead of filtering sharedDrives inline (owner === false removed)
- getParentFolder drive branch replaced network statById with a local-pouch
  fetchQueryAndGetFromState call using forceLink:'dataproxy' + driveId,
  keeping the realtime path fully reactive and off the network
- updated spec: useSharedDrives mocks include recipientDriveIds; drive
  path-resolution test asserts fetchQueryAndGetFromState options instead of statById
Crash-- and others added 19 commits July 6, 2026 19:41
…ggregate

Rewrites useRecentFiles to remove the imperative dataProxy.recents() snapshot
and useDataProxy dependency. Returns a scopeQueries array (one own-files scope +
one per recipient shared drive) that the Recent view mounts; results are merged,
deduped by _id, sorted by updated_at desc, filtered for trashed, and capped at
50. Degrades to own-files-only when no DataProxyLink is present in client.links.
Updates Recent view wiring and all three spec files.
…load

While `isSettingsLoaded` is false, the query is gated off and useQuery
returns `fetchStatus: 'pending'`. `getFolderViewState` does not treat
'pending' as loading, causing a brief empty-state flash. Normalise to
'loading' (with no lastUpdate) so the skeleton is shown instead.
When the user is viewing a shared-drive folder and loses access (drive
deleted or removed as recipient), useSharedDrives updates via realtime;
the new useEffect detects the missing driveId and navigates away with a
secondary alert instead of silently showing an empty folder.
…nRevokedDrive hook

moves the revocation-redirect useEffect out of SharedDriveFolderView into a
dedicated hook to reduce cyclomatic complexity (14→prior level) and make the
redirect logic independently testable
Extract a shared recents Mango definition reused by buildRecentQuery and
buildRecentsScopedQuery; extract a helper from processEvents; dedupe the
similar realtime, recents-query and shared-drive-folder tests. Default
recipientDriveIds to [] so mounting FilesRealTimeQueries with a partial
useSharedDrives mock does not throw.
…h smells

Extract actions setup into useSharedDriveFolderActions hook and render
body into SharedDriveFolderContent to bring cyclomatic complexity and
LoC of SharedDriveFolderView within CodeScene thresholds.
cozy-flags is not populated when setupApp runs, so the DataProxy link was
dropped and every shared-drive query failed with 'No link could handle
operation' until the flag happened to be cached from a prior session.
The sort skipped driveId while the index declared it, so pouchdb-find could
not use the declared index and fell back to building an ad hoc one.
Realtime docs arrive without the synthetic driveId, so the driveId-filtered
folder query dropped the file from its live results on every update until a
manual refresh.
@Crash--
Crash-- force-pushed the feat/reactive-recents-shared-drives branch from 8afa9e9 to ffbaa16 Compare July 6, 2026 17:45

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Health Improved (4 files improve in Code Health)

Our agent can fix these. Install it.

Gates Passed
3 Quality Gates Passed

View Improvements
File Code Health Impact Categories Improved
useSharedDriveFolder.spec.jsx 9.39 → 10.00 Code Duplication
useSharedDriveFolder.tsx 8.70 → 10.00 Complex Method, Complex Conditional
Empty.jsx 9.55 → 9.61 Complex Method
SharedDriveFolderView.jsx 9.03 → 10.00 Complex Method

Quality Gate Profile: The Bare Minimum
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

This branch has not been deployed

No deployments
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.

2 participants