super-productivity/packages/plugin-dev/document-mode
Johannes Millan 6d81dde043
feat(plugins): adopt PERSISTED_DATA_CHANGED in document-mode (#7752) (#7812)
* feat(plugins): adopt PERSISTED_DATA_CHANGED in document-mode (#7752)

Document-mode's iframe editor went stale when another device edited the
same context's doc — the editor kept typing on in-memory `storedState`
and the next throttled save merged-on-stale-base, partially clobbering
the remote edit. Background's `enabledIds` set had the same gap for
remote toggles.

Adopts the host-side `PERSISTED_DATA_CHANGED` hook (shipped in #7805,
made multi-handler-safe in #7811) in both surfaces:

- `background.ts` — on fire: re-read `enabledIds`, diff, and call
  `showInWorkContext`/`closeWorkContextView` only when the active
  context's membership flipped. Idempotent on no-op re-fires; the
  hook also fires for `doc:` writes which background ignores via the
  set-unchanged short-circuit.

- `ui/editor.ts` — on fire: load the current ctx's raw `doc:` bytes via
  the new `loadContextDoc` `{ raw, parsed }` shape, byte-compare to
  `lastSeenRemoteData`. Equal → noop (self-echo or another key's fire).
  Different → show a one-button "reload" banner with a distinct
  `doc-banner--remote-update` modifier class. Clicking Reload re-runs
  `setActiveContext` which already re-reads + setContent's.
  `isDocCorrupt === true` short-circuits to a direct reload (the
  corruption banner already promises saved data is untouched).

The load-bearing piece is `lastSeenRemoteData` — captured as the raw
`loadSyncedData()` string on both load (`setActiveContext`) and write
(`flushSave` / `flushSaveSync`). The editor's own `JSON.stringify(getJSON())`
is NOT byte-stable across the load path because `prepareStoredDoc`
reshapes chip content against the local task cache, so comparing
against editor output would mis-flag every load as remote. Raw-against-raw
keeps the host's deterministic encoding the only thing that matters.

Acceptance criteria from #7752: all met except the E2E append, which
needs iframe-level state injection that the existing spec helpers
don't cover; covered by manual verification + unit specs instead.

What this doesn't fix:
- No cross-context notification (editor catches up silently on switch).
- Same-context concurrent edits still resolve whole-doc LWW.
- No silent swap or selection preservation.
- No "Keep mine" force-flush — dismiss-and-keep-typing already wins LWW.

Tests: 84/84 plugin specs pass (+6 background hook membership branches,
+1 corrupt-entry raw-bytes case, +1 saveContextDoc-returns-raw case).
Plan v6 changelog documents the v5/v6 multi-review iterations and the
cuts (pre-reload backup, pure-fn file, coalesce timer, "Keep mine").

* refactor(plugins): apply doc-mode review findings (#7752)

Multi-review of the prior commit surfaced two real and one likely-real
bugs in the reconciler and a few cleanup wins. Apply all that survived
verification.

W1 — wrap getActiveWorkContext in try/catch. The prior handler only
caught loadEnabledCtxIds rejections; a getActiveWorkContext throw
would escape via the void onPersistedDataChanged() registration as an
unhandled rejection and leave enabledIds stale across every subsequent
fire.

W2 — snapshot enabledIds at entry. The prior handler read the closure
variable across multiple awaits before assigning at the end; two
interleaved fires could mis-compute wasActiveEnabled and drop a
close/show call. Pass-by-parameter to reconcileEnabledIds eliminates
the in-function race; concurrent fires now race only on the final
caller assignment, which is at least eventual-consistent.

W3 — extract reconcileEnabledIds to its own file and import the real
function from the spec (no more local copy). reconcile-enabled.ts
sidesteps the testability problem at its root: background.ts has
top-level PluginAPI side-effects (registerWorkContextHeaderButton,
registerHook) that crash in node, so the spec can't import from there.
The new file holds only the pure reconciler + try/catch hardening.

W4 — extract serializeContextDoc(doc) helper. flushSave and
flushSaveSync now produce byte-identical output via the same function,
so a future encoding change can't silently desync the sync path from
the async one.

W5 — fix the inline comment on showRemoteUpdateBanner re-entrancy to
match what the code actually does (early-return because text is
invariant, not "replace text in place" as the v6 plan implied).

S1 — rename lastSeenRemoteData → lastSeenDocBytes. The variable also
holds local-write bytes, not only "remote" ones; the new name follows
the existing lastSeenTaskIds convention.

S2 — drop the try/catch around loadSyncedData in
onRemotePersistedDataChanged. The hook dispatcher already catches +
logs handler rejections (PluginHooksService._invokeWithTimeout), and
loadContextDoc elsewhere in this file follows the no-wrap convention.

S3 — add primitive-JSON case to persistence.spec.ts. loadContextDoc's
new {raw, parsed} shape silently forwards parsed=123/null/"hi" to
callers; the editor's truthy guard is the safety net, but the
persistence contract is now locked.

S5 — explain why isDocCorrupt short-circuits to a direct reload
(auto-recovery without user click; alternative would leave the user
stuck on the corruption banner even when the remote already fixed
the entry).

Tests: 86/86 plugin specs pass (+7 reconciler error/membership cases,
+1 persistence primitive-JSON). Bundle redeployed.

* docs(plugins): update stale lastSeenRemoteData refs in JSDoc/comments

Two comments referenced the pre-rename variable name.
2026-05-27 11:46:50 +02:00
..
scripts feat(document-mode): add TipTap-based document-mode plugin 2026-05-22 17:33:22 +02:00
src feat(plugins): adopt PERSISTED_DATA_CHANGED in document-mode (#7752) (#7812) 2026-05-27 11:46:50 +02:00
.gitignore feat(document-mode): add TipTap-based document-mode plugin 2026-05-22 17:33:22 +02:00
package-lock.json feat(document-mode): add TipTap-based document-mode plugin 2026-05-22 17:33:22 +02:00
package.json feat(document-mode): add TipTap-based document-mode plugin 2026-05-22 17:33:22 +02:00
tsconfig.json feat(document-mode): add TipTap-based document-mode plugin 2026-05-22 17:33:22 +02:00