zudo-text

検索したい単語を入力

いつでも検索バーを開ける

Explicit cloud Save and Load contract

Scope and capture

Save confirms one capture inside one absolute deadline. deadline is Date.now() + (timeoutMs ?? 30_000) before flush, allocation, reconciliation, or either queue wait. Neither queue receives a fresh 30s budget. Save does not call awaitFlush and does not recapture work until typing stops.

At entry it calls the optional synchronous captureCreationIntents once. Those ids are new-note operations that already exist. Ids created later are outside this Save. saveWindowToCloud passes the current workspace's pending notes that are not kept and not failed, operation ids only.

flushLocal is raced against the deadline. If the deadline wins, the outcome is timed-out. The in-flight local seal is not cancelled. Writer failures or a throw while the scope is still valid produce local-save-failed. A change of workspace, window, arm, keys, or either queue instance produces invalidated. The bridge has no account id; the renderer coordinator still invalidates on account identity.

After a successful flush, Save waits — still inside the same deadline — for the canonical journal to open and for the adoption already in flight. It then snapshots canonical operation ids. The captured set is that snapshot union the t0 creation-intent ids. A later operation keeps its own id and predecessor and cannot satisfy a captured id.

The legacy outbox still snapshots path identities after the flush, inside the remaining time. saveTicket(0) means zero, not the default 30s. Restored legacy entries participate without ChangeTracker generations.

Acknowledgement and coalescing

A captured canonical operation is confirmed only when queue.receipt(operationId) exists after a successful journal commit and that receipt's operation id, note id, and path match the operation. A move or restore matches the destination path of that operation. Queue removal, a numbered tab, a local seal, matching content, or an unrelated acknowledgement is not success. A creation intent that has not been enqueued stays unresolved until its own receipt; absence is timed-out or partial, never confirmed. A held captured operation finishes as held-conflicts and is not released. A retryable send, including a receipt seal that fails after the server responds, stays open until a matching receipt commits or the original deadline is reached. Auth, offline, and holds may finish early, matching the legacy ticket. While the ticket is still open, a matching receipt replaces that earlier failure. At the deadline a still-retrying operation is partial. A pre-send failure is still retried; if its delay elapses during the same flush, that pass schedules another delay when it ends. Timeout and invalidation only remove listeners. A server response whose receipt commit fails stays queued.

Legacy entries still wait for an actual uploader acknowledgement. A tracker acknowledgement, queue removal, or skipped upload cannot establish confirmation. PUT requires a server acknowledgement; DELETE requires a successful response (or the server's already-deleted response). Held entries report held-conflicts.

A PUT response with a missing, invalid or mismatched acknowledgement does not populate the already-uploaded-content cache. The bridge makes one read of that path's version history. It recovers confirmation only when the live head has the exact ciphertext hash and size sent by this attempt and a valid server version. For an ordinary conditional PUT, that version must be the supplied base plus one; an explicit move retains the moved identity's version contract. History failure, a tombstone, an unknown deletion flag, or a different hash/size/version leaves the encrypted queue intact. A later retry must not silently skip that unconfirmed write. Workspace disarm aborts the readback and cannot confirm another arm.

Keep Mine records the reviewed choice and expedites its conditional upload; it does not itself acknowledge cloud persistence. A newer different cloud revision still requires fresh review. An invalid request version is rejected before sending and is never repaired by dropping the version precondition or guessing a version.

A newer queued entry on the same path supersedes the captured entry: its content contains the earlier edit, so the ticket waits for the replacement's acknowledgement. newerEdits remains true for edits after capture, including coalesced replacements; the result certifies only the original captured scope. New paths are outside the ticket. pendingPaths describes outstanding queued work at completion, and consumers must also preserve renderer-held pending work.

Outcomes

The typed outcomes are confirmed, held-conflicts, partial, timed-out, offline, auth, local-save-failed, and invalidated. When both queues have a result the precedence is invalidated, then local-save-failed, auth, offline, timed-out, partial, held-conflicts, and confirmed. Confirmed requires every captured canonical operation to be receipt-confirmed, the legacy ticket to be confirmed, and no unresolved captured intent. An empty captured set plus a successful flush is confirmed. An empty legacy queue does not confirm canonical work. A failed local seal, a blocked canonical journal, or a lost acknowledgement cannot become confirmed.

capturedPaths is the captured scope. heldPaths lists captured holds. pendingPaths lists unconfirmed captured work and known newer canonical or legacy work. newerEdits is true when an edit after the capture boundary exists. The renderer also unions its own dirty paths.

Timeout leaves durable work queued for automatic retry. A stale completion cannot report success for a replacement workspace or write that workspace's timestamps. REST and Tauri use the same coordinator. The mock calls captureCreationIntents once and will not confirm, or clear simulated pending paths, while one of those intents is still unacknowledged. It makes no server durability claim beyond its simulated backend.

When the outcome is confirmed and newer edits or pending paths remain, the panel says the captured Save was confirmed and newer edits are still pending. The unqualified "this window's changes were confirmed" sentence is only for a confirmed Save with nothing newer left.

Fresh Load and window boundary

SyncProvider serializes explicit operations against the active account, workspace, and encryption arm. Save and Load can queue one opposite action; duplicate actions are disabled. A scope change invalidates outstanding operations. Popout windows do not claim this main-window confirmation.

Load runs a fresh reconciliation before WebSocket replay resumes. Remote application checks local edit generations again at apply time, including renderer-held typing that began after the request. Unsent edits are protected by conflict-aware adoption; Load is neither a reload nor an unconditional snapshot replacement. Held conflicts, partial application, offline and auth outcomes cannot report loaded success.

UI evidence and conflict review

The wide Cloud sync arrows open a popover. Compact More tools retains its identity, 44px touch floor, drag handlers and accessible name; only its glyph rotates while busy. App → Cloud sync shares CloudSyncPanelContent and useCloudSyncPresentation with the wide control. The conflict snapshot's supplied reviewNeeded, including false, is authoritative over historical operation results. Review and sign-in close their owner surface without restoring editor focus.

Details distinguish local durability, cloud acknowledgement and successful catch-up. Timestamps are recorded workspace events in the current session, not durable history or proof that every note is current. No unsupported device attribution is inferred. Idle and a connected WebSocket do not prove freshness. Automatic success stays quiet; only explicit completion briefly shows success.

Automatic conflict discovery sets attention without opening resolution while typing. Explicit Review authorizes resolution. Changed remote revisions invalidate old review and require fresh Review. Before Load Incoming replaces local text, sealed workspace, account and window scoped storage preserves it; its completion notice offers Copy preserved local text. Preservation failure blocks replacement. Keep both is omitted: no second identity is created by this contract.

Pending items

CloudSyncMetadata.pendingItems names every outstanding row in pendingPaths, in the same order, without content (#7589). Legacy rows stay one per path. Canonical rows stay one per operation, in journal order, even when two operations share a path; operationId distinguishes them. SyncProvider does not collapse those rows. It stable-sorts by path and still adds renderer-only dirty paths as unsaved.

SyncOutbox.peekEntryStates() and NoteOutbox.peekOperationStates() derive each stage from queue state. Canonical attempt counts and lastFailure live only in memory:

StageSourceMeaning
unsavedrenderer boundary registryEditor text not yet written on this device (added by SyncProvider)
queuedqueued entry, no attempt yet, and not the operation inside upload()Waiting for its debounced or expedited send. A canonical successor behind an in-flight predecessor stays queued
uploadingthe exact entry or canonical operation on the wireOnly that one operation
retryingattempts > 0 and not the one on the wireThe last send failed and it backs off. lastFailure keeps its kind (offline, auth for 401, access for 403, error) and a bounded message: HTTP <status> plus an allowlisted error code, or at most 120 characters. It never holds the raw response body or note content. Attempts increment only when a send is attempted. Reconciliation and pre-send validation (missing device, unknown base, missing wire) are not attempts. HTTP outcomes, including 409/412/426 and network failures, are attempts. After a reload the counts are unknown and are not invented
heldthe journal or legacy holdNot retried until its owner settles it. upgrade-required is refused; a stored unknown-base reason stays unknown-base; other conflict holds are conflict

A held item's holdReason is conflict (a sealed server version this edit was not based on, for an upsert or a delete), unknown-base (restored text with no known server base, waiting for explicit recovery), or refused (parked by the uploader without either). recovery marks an explicit recovery upload. unsavedEdits marks a queued path that also has newer editor text. Upload start, upload failure, release() and the end of the persisted-queue load notify the metadata listener at once. Acknowledgements and new holds surface through the persist() call at the end of the flush pass.

lastLocalDurableWriteAt advances when a legacy durable write commits and when this arm's canonical enqueue seal commits. Holds, adoption, receipts, identity memory, release, and settlement do not move it. lastCloudAcknowledgmentAt advances when a legacy upload is acknowledged and when this arm's matching canonical receipt is durably committed. onChanged is not a timestamp source. Both remain current-session workspace events.

pendingLoaded is false until both the legacy queue and the canonical queue have opened, while no outbox is armed, while a canonical journal is blocked or corrupt, while orphan adoption is in flight or an orphan journal cannot be read, and in SyncProvider until this workspace's bridge metadata arrives. A blocked or unreadable journal is unknown, not an empty queue. An empty list is then unknown, not empty. The control shows Checking ("The pending count is not known yet"), and the panel marks the list as still loading. It never shows No pending changes or Up to date in that window.

Cloud sync and Settings → Sync render the list. A held conflict gets a per-row Review link only while the panel's own Review is offered (reviewNeeded). Any held item moves the control to Changes held with attention, never Changes to save. Save cannot release a hold. Its description also counts the other pending changes. Up to date still requires an empty list, so timestamps and Sync History rows (completed passes) never stand in for outstanding work. The list exposes state that already exists. It does not release holds, force retries, resolve conflicts or relax any version check.

Mock journey boundary

main-mock.tsx alone installs the mock explicit-sync controls. The real App, SyncProvider, encryption/access gates, writer flushing and conflict owner still run. Remote rows are sampled at Load entry and the simulated result waits for the owner's application or held-conflict signal. Missing completion reports partial failure. These journeys prove renderer integration in browser emulation, not actual server durability or physical device/native behavior; the latter is deferred under #7010.

Recovering a held local edit with an unknown base

Save does not guess a missing edit base or overwrite a held cloud conflict. Its result is shown next to the Save button, including the held count and the next available actions. A held upsert with an unknown base offers Keep local version, with an explicit explanation that the local text wins even if the cloud copy differs.

That action first flushes this window's editors. Only a confirmed or held-only flush may continue. keepUnknownBaseLocal checks the workspace/arm and local edit generation, probes the retained cloud head, validates its version and live/deleted discriminator, then durably rebases the queued text through the existing Keep Mine path. A missing path uses conditional creation (base zero); a retained tombstone uses its actual version. This is a newly authorized local choice, never an inferred original baseline. No plaintext is replaced locally. The subsequent conditional PUT still rejects a concurrent cloud update. Dirty editors, changed queued text, failed persistence, invalid replies and failed probes leave the local change pending. The UI requests Save again after the choice and reports that operation's result; recording a choice alone is not a cloud acknowledgment.