Skip to content

feature/72373 Add offline editing via IndexedDB; drop local document fallback#22125

Open
akabiru wants to merge 33 commits intodevfrom
implementation/72665-remove-test-mode-for-local-document-fallback-require-hocuspocus-provider
Open

feature/72373 Add offline editing via IndexedDB; drop local document fallback#22125
akabiru wants to merge 33 commits intodevfrom
implementation/72665-remove-test-mode-for-local-document-fallback-require-hocuspocus-provider

Conversation

@akabiru
Copy link
Member

@akabiru akabiru commented Feb 27, 2026

Ticket

What are you trying to accomplish?

Continues:

Adds two main changes:

Screenshots

bn-offline-mode.mp4

Note

This is not the final design- updates will be done separately

What approach did you choose and why?

As we now have hocuspocus and indexeddb providers available in test environment, we can remove the "hacky" yDoc setup in test and also add more tests! 👍🏾

Merge checklist

  • Added/updated tests
  • Added/updated documentation in Lookbook (patterns, previews, etc)
  • Tested major browsers (Chrome, Firefox, Edge, ...)

…ment-editing

[72373]: Add offline editing with IndexedDB sync support
Since HocuspocusProvider is now available in test environments (backed by
IndexedDB and hocuspocus), the separate test-mode code path that created a
local Y.Doc is no longer needed. OpBlockNoteContainer now always expects a
HocuspocusProvider, removing the optional provider type, the isTest check,
and the conditional doc-creation logic.

- block-note-element: skip initialization when collaboration is disabled;
  provider parameter is now required (non-optional type)
- OpBlockNoteContainer: remove isTest check, local-doc fallback,
  inputField/inputText props, and the optional HocuspocusProvider type
- useCollaboration: remove optional provider type and doc parameter; always
  derive doc from provider.document
- block_note_editor.rb: drive collaboration_enabled from the real setting
  instead of hardcoding true
- block_note_editor_spec.rb: remove the any_instance stub that forced
  collaboration off; add a proper spec for the disabled-collaboration case
- init-yjs-provider.controller.ts: fix no-misused-promises by extracting the
  async body of connect() into a private setupProvider() method so the
  Stimulus lifecycle hook returns void as its type requires; fix
  no-floating-promises on indexeddbPersistence.destroy() with void operator
@akabiru akabiru changed the title Ihordubas99/feature/{72373,72665} Add offline editing with IndexedDB sync support and remove test-mode for local document fallback feature/72373 Add offline editing with IndexedDB sync support and remove test-mode for local document fallback Feb 27, 2026
@akabiru akabiru self-assigned this Feb 27, 2026
…pecs

Add test_selector attributes to the connection error and collaboration
disabled Banner components so they can be targeted by feature specs.

Update the offline mode test to access the notice via editor.shadow_root
rather than page-level have_test_selector, which cannot pierce the Shadow
DOM boundary of op-block-note.

Also update the connection_error_notice i18n description to reflect
offline mode messaging (was: "server is unreachable").

- connection_error_notice_component: add test_selector "connection-error-notice"
- collaboration_disabled_notice_component: add test_selector "collaboration-disabled-notice"
- block_note_editor_spec: assert collaboration-disabled-notice banner text;
  use editor.shadow_root + have_css to access connection-error-notice
- real_time_collaboration_spec: gate with real_time_text_collaboration_enabled
  setting; nest hocuspocus tests under dedicated context; move offline mode
  test here where no hocuspocus server is started

Co-authored-by: Ihor Dubas <dubasihor25@gmail.com>
@akabiru akabiru force-pushed the implementation/72665-remove-test-mode-for-local-document-fallback-require-hocuspocus-provider branch from 24aff7d to 89b12aa Compare February 27, 2026 13:22
@akabiru akabiru marked this pull request as ready for review February 27, 2026 13:44
@akabiru akabiru requested a review from a team February 27, 2026 13:44
@github-actions
Copy link

github-actions bot commented Feb 27, 2026

Deploying openproject with PullPreview

Field Value
Latest commit 76191dc
Job deploy
Status ✅ Deploy successful
Preview URL https://pr-22125-72665-remove-test-m-ip-49-12-2-188.my.opf.run:443

View logs

@akabiru
Copy link
Member Author

akabiru commented Feb 27, 2026

@claude review this pr

@akabiru akabiru requested a review from Copilot February 27, 2026 13:46
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • frontend/package-lock.json: Language not supported

Remove the await on waitForIndexedDBSync so the HocuspocusProvider is
created immediately rather than waiting up to 10 seconds for IndexedDB
to sync. IndexedDB now runs in the background with a .catch() handler.

This is safe because:
- Y.js CRDTs handle out-of-order merges: updates applied in any order
  converge to the same result
- The Hocuspocus server's onLoadDocument always pushes the authoritative
  document state to the client on connect (via Y.applyUpdate), so any
  offline edits IndexedDB applies afterward are forwarded to the server
  automatically by the provider

Also removes the isConnected guard and makes setupProvider synchronous
since there are no longer any await points in the function.
- Convert useConnectionTimeout to accept an onTimeout callback instead
  of returning a boolean state, removing the redundant intermediate
  useEffect and establishing a single source of truth for offline state
- Remove duplicate setHasTimedOut(false) call that was unreachable after
  the initial reset at the top of the same effect
- Register synced/disconnect listeners before checking provider.synced
  to prevent a race where a sync event fires between the guard check
  and listener registration
Previously the timeout callback relied on reading provider.synced at
fire time to avoid calling onTimeout() after a successful sync. This
left the timer running past the point it was needed and made
correctness dependent on the provider's live state at an arbitrary
future moment.

Subscribe to the 'synced' event inside useConnectionTimeout so the
timer is cancelled immediately when the provider syncs, and clean up
the listener in the effect's teardown.
- Extract INDEXEDDB_SYNC_TIMEOUT_MS to a module-level constant
- Fix double-space in IndexedDB sync debug log message
- Remove unnecessary fragment wrapper in OpBlockNoteContainer
Document the purpose, behaviour, and state transitions of
useConnectionTimeout, useCollaborationProvider, and useCollaboration
to make the non-obvious design decisions (register-before-check,
proactive cancel on sync, offline-mode semantics) easy to follow.
Move the inline useEffect for the document-level auth error event into
its own useProviderAuthError hook, consistent with the callback-based
convention used by useConnectionTimeout and useCollaborationProvider.

useCollaboration is now a pure orchestrator with no bare effects.
@akabiru akabiru changed the title feature/72373 Add offline editing with IndexedDB sync support and remove test-mode for local document fallback feature/72373 Add offline editing via IndexedDB; drop local document fallback Feb 27, 2026
Copy link
Contributor

@brunopagno brunopagno left a comment

Choose a reason for hiding this comment

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

I did not test, but the code looks nice. I'm really glad we're doing this 👏

But just not to leave unsaid, hope you can sanity check if you haven't already a few things:

  • How does the indexdb sync with remote state? Is it somewhat stable?
  • How about local and remote state sync during active collaboration sessions?
  • What happens with readonly when using offline mode? And what happens when the user reconnects?
  • Is there a chance that a user might be using offline mode and not notice? I'm afraid it could cause false positives when an admin is setting up hocuspocus, they could think that things are working but it's just offline mode

Still, this is a beautiful job. Thanks for making it 🙇

@akabiru
Copy link
Member Author

akabiru commented Mar 2, 2026

Thanks for the review @brunopagno

  • How does the indexdb sync with remote state? Is it somewhat stable?

Seems fairly stable, but will of course need some real world use. IndexedDB is a local cache only; it doesn't sync with remote directly. Instead, both IndexedDB and Hocuspocus will write into the same shared Y.Doc. On hocuspocus connection Y.js should automatically merge any local changes without conflict.

We also set up non-blocking providers- IndexedDB initialization runs in the background with a 10s timeout guard (important because y-indexeddb has no built-in error events), and if it fails the hocuspocus provider continues without offline persistence.

It's fairly small in footprint: See: https://github.com/yjs/y-indexeddb/blob/master/src/y-indexeddb.js

  • How about local and remote state sync during active collaboration sessions?

Hocuspocus handles all real-time, multi-client sync; IndexedDB is purely a local cache layer that mirrors the Y.Doc on-device. If the connection to sync server (HP) drops, changes are buffered in the Y.Doc locally (including in IndexedDB) and Hocuspocus forwards them automatically on reconnect.

  • What happens with readonly when using offline mode? And what happens when the user reconnects?

Nice catch, seems we didn't account for this! y-indexeddb does not have have readonly awareness afaict- so we need to rely on the editor. Will cover this shortly!

  • Is there a chance that a user might be using offline mode and not notice? I'm afraid it could cause false positives when an admin is setting up hocuspocus, they could think that things are working but it's just offline mode

No. The connection-error banner will be displayed. See current version below: however, the designs will soon change to this.

Screenshot 2026-03-02 at 3 58 58 PM

@akabiru akabiru marked this pull request as draft March 2, 2026 16:30
@akabiru akabiru marked this pull request as draft March 2, 2026 16:30
@akabiru
Copy link
Member Author

akabiru commented Mar 3, 2026

@brunopagno @ihordubas99 as it turns out we needed to revert the non-blocking IndexedDB <-> Hocuspocus provider setup and add local cache presence detection to close two data-loss paths:

🟠 Race condition (users with a local cache)
Making waitForIndexedDBSync non-blocking meant HocuspocusProvider could connect with an empty Y.Doc before the local cache had loaded. Awaiting IDB first ensures the provider's initial state vector already includes cached content, so no late updates are sent.

🔴 No-cache + server offline = block editor
A user opening a document for the first time while Hocuspocus is unreachable has no verified state at all. Previously they could type freely into an empty Y.Doc; on reconnect onStoreDocument would overwrite the real document for everyone. We now check hasLocalCache after waitForIndexedDBSync: if the connection times out and there is no local cache, the editor is blocked with a "server unavailable" message instead of allowing edits that could poison server state on reconnect.

Screenshot 2026-03-02 at 10 56 05 PM

Add a Phase 1 to each soft-offline test that types content and waits
for Hocuspocus to persist it (content_binary present) before switching
to the unreachable URL. An empty document never triggers a Y.Doc write,
so IndexedDB stayed empty and Phase 2 fell into blocking-offline mode
instead of soft-offline mode.

For the readonly soft-offline test, Phase 1 now logs in as admin to
type the seed content (readonly users cannot write), then switches to
the readonly user for the offline visit.

Also add blocking: param support to ConnectionErrorNoticeComponent and
documents controller to display a distinct "server unreachable" message
when no local IndexedDB cache exists.
@akabiru akabiru force-pushed the implementation/72665-remove-test-mode-for-local-document-fallback-require-hocuspocus-provider branch from 06292ce to a3eaf76 Compare March 3, 2026 09:13
@akabiru akabiru requested a review from brunopagno March 3, 2026 09:45
@akabiru akabiru marked this pull request as ready for review March 3, 2026 09:46
@akabiru akabiru requested a review from Copilot March 3, 2026 09:50
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • frontend/package-lock.json: Language not supported

Copy link
Contributor

Copilot AI commented Mar 3, 2026

@akabiru I've opened a new pull request, #22168, to work on those changes. Once the pull request is ready, I'll request review from you.

Store the setTimeout ID and call clearTimeout() in the whenSynced
.then() callback so the stale timer is cancelled when sync completes
before the 10-second deadline, preventing a no-op reject() call.
@akabiru akabiru requested review from a team and removed request for brunopagno March 3, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

5 participants