Skip to content

connect: mdbase-next SDK backend for Reader (opt-in) - #60

Draft
callumalpass wants to merge 5 commits into
mainfrom
next/reader-sdk
Draft

callumalpass wants to merge 5 commits into
mainfrom
next/reader-sdk

Conversation

@callumalpass

Copy link
Copy Markdown
Contributor

Adds an opt-in backend for Reader on the mdbase-next TS SDK (@mdbase-dev/sdk). The current Connect SDK is still the default, and nothing changes unless the switch below is set.

Only packages/connect imports the new SDK. ESLint now also stops packages/core from importing it.

How to switch

  • Web app: add ?sdk=next to the URL, or build with VITE_MDBASE_SDK=next. These sit next to the existing ?server= and VITE_MDBASE_CONNECT_URL settings.
  • Extension: build with MDBASE_SDK=next, next to MDBASE_ENV.
  • Code: use @mdbase-reader/connect/next (ReaderNextApplicationSession, nextReaderClient, nextReaderFiles, nextReaderCollection).

SDK dependency (temporary)

The SDK is not published yet. The packed build is vendored at packages/connect/vendor/mdbase-dev-sdk-c573c96.tgz and referenced with a file: spec. This will switch to a published version once one exists.

What's ported

  • ReaderConnectClient over MdbaseClient (next/client.ts)
    • read maps to get.
    • readMany runs concurrent gets and returns revision-qualified rows, so read-many-documents-v1 is reported.
    • query and queryPages map to cursor-paged query. Page sizes are kept. Offsets are emulated for the library list.
    • create, update and delete map to field-level intents. The SDK fills in the base values and the body edits from the view that was just read.
  • CAS: ifRevision is passed through. A write that carries ifRevision waits for confirmed, so a refusal at head still reaches Reader. Other writes return the optimistic Write.records. A later rejection goes to onRejected.
  • Errors: the 15 codes map onto the existing ConnectRepositoryError and connectProblemMessage path, using Reader's own text for each code. A CAS conflict becomes concurrent_modification, so record sessions and editors behave as before. nextErrorOf(problem) returns the original MdbaseError.
  • Waiting for a device: unavailable with reason no_device_online shows "Waiting for one of your devices…". The session sets waitForDevice: true, the snapshot shows blocked with that message, and the session exposes waitingForDevice.
  • Files (next/files.ts): stat, list, upload and download over FilesApi. They feed the existing document, collection-file, asset and source-import repositories. Reader's retry keys are turned into stable transfer and mutation UUIDs, so retries are idempotent.
  • Content search: still one query with include: {body: true}, used only for that search.
  • Sessions:
    • ReaderNextApplicationSession implements a new ReaderSession interface, and also ReaderPortableSession for the extension.
    • Snapshots have the same shape as Connect's, so the screens are unchanged.
    • The client key comes from loadOrCreateClientKey, which keeps a non-extractable WebCrypto key in IndexedDB.
  • Record session: the source and annotation editors' MdbaseRecordSession adapters work unchanged, because they sit on the repositories.

What's stubbed (no equivalent in the client API yet)

  • Migration / Zotero import: this relied on the old pending-mutation journal. Now it refuses with unsupported_operation.
  • Delete preflight backlinks: the preflight is a real dryRun delete, but it reports no brokenLinks, and checkBacklinks is ignored.
  • Projections and select values: the annotation→source link resolution projection is dropped. Path-form source links that don't fall back to the legacy bare-ID form won't resolve. query-metadata-v1 is reported as unsupported.
  • Saved views: these call listViews/executeView. Because their payload shape isn't defined in the contract, they are read defensively. An unrecognised list is treated as empty, and an unrecognised execution is refused. Saving works through create/update.
  • Others:
    • interrupted-write recovery: pending() is always false, and receipts replace it;
    • direct (loopback) access: none;
    • applyCollectionSetup: a no-op, because type packs aren't provisioned;
    • modifiedAt on file descriptors: empty, because FileView has no mtime.
  • Contract selectors read Reader's own types (reader-source / reader-annotation), because the new API has no contract views.

Dependency on the control workstream

How the app obtains a grant (registering client_pk at consent) and a route is isolated behind ReaderNextControlPlane (next/control-plane.ts). ProposedHttpControlPlane currently does the following:

  • resolveRoute calls the proposed GET {serverUrl}/v1/next/collections/:id/route, which returns {targets: [{url, device, noise_pk}]}. A 404 or an empty targets means no device is online.
  • grants reads grants that were stored out of band (mdbase-reader:next-grants, via rememberGrant).
  • authorize makes no grant yet, so "Connect another collection" reports that the consent flow is unavailable.

The endpoint and the consent flow need to match whatever the control workstream ships.

Tests

  • New vitest suites run against MemoryReplica (26 tests). They cover:
    • read;
    • cursor paging and offset paging;
    • create and update going optimistic, then confirmed;
    • a CAS write waiting for confirmation;
    • a CAS conflict mapping to concurrent_modification / ConnectRepositoryError;
    • delete and its dry-run preflight;
    • the source repository and content search end to end;
    • file upload, list, stat and download, and the collection file repository;
    • the session in its ready, waiting-for-device and unselected states;
    • the backend switch, route parsing and the error mapping.
  • Checks run:
    • packages/connect: 157 tests pass;
    • apps/reader: 401 tests pass;
    • apps/extension: 169 tests pass;
    • typecheck passes for every workspace package;
    • eslint . --max-warnings 0, prettier --check . and the architecture check and tests all pass.
  • Not run: no browser or end-to-end run against a real relay or replica, because the control-plane route doesn't exist yet.

The SDK is not published yet. The packed build is vendored and referenced
with a file: spec until a published version replaces it.
nextReaderClient maps read to get, query/queryPages to cursor-paged query
(offsets emulated), and create/update/delete to field-level intents.
Writes with ifRevision wait for confirmation so a CAS refusal reaches
Reader as concurrent_modification; others return optimistically.
The 15 replica error codes map onto Connect problems, including a
waiting-for-device message for unavailable/no_device_online.

nextReaderFiles implements stat/list/upload/download over FilesApi.
Tests run against MemoryReplica.
ReaderNextApplicationSession produces the same snapshots as the Connect
session over a relay connection, keyed by loadOrCreateClientKey (a
non-extractable WebCrypto key in IndexedDB). A private collection with no
device online shows as blocked with a waiting-for-device message.

The control plane (grants registered at consent, routes) sits behind
ReaderNextControlPlane; ProposedHttpControlPlane targets the proposed
GET /v1/next/collections/:id/route endpoint.

nextReaderCollection assembles Reader's existing repositories on the new
seams. Migration, interrupted-write recovery, direct access and collection
setup are stubbed; saved views read listViews/executeView defensively.

ReaderSession and ReaderPortableSession describe what the apps need, and
the backend is exported as @mdbase-reader/connect/next. Core may not
import the new SDK.
The web app reads ?sdk=next or VITE_MDBASE_SDK=next, beside its existing
?server= and VITE_MDBASE_CONNECT_URL settings. The extension reads
MDBASE_SDK=next at build time, beside MDBASE_ENV. The Connect SDK stays
the default.
@callumalpass

Copy link
Copy Markdown
Contributor Author

D2 import stub replaced in this opt-in backend (commit a new head). NextMigrationTarget uses collection-scoped stable record/file/transfer IDs, immutable-payload mutation IDs and singleton submit allow_partial/mutation_ids via existing SDK; waits for confirmed writes/files; never updates existing records. Native legacy journal stays daemon-owned (migration confirmed browser must not read it).

Connect package typecheck, 163 tests, touched-file ESLint/Prettier, and architecture check pass. Six new tests cover pending-to-confirmed, lost confirmation response retry, reopened-session retry, normalized field order, changed payload/path collisions, duplicate identities, cancellation, and file retry. Full-repo build not run. No deploy/publish. Coordinator merges this port PR; security review requested on new adapter.

@callumalpass

Copy link
Copy Markdown
Contributor Author

security: no blocking findings — security-2, on head 6aaa6e5, for the three-file migration adapter delta from 150b185. Scoped deterministic identities and normalized-payload mutation IDs retain confirmed-only create semantics; collisions/changed records are never updated. Existing importer supplies a content-digest-qualified transfer key and checks the resulting file digest; files wait for confirmed. Duplicate record identities and malformed provenance stop import. Static delta/caller review; reported 163 tests (six new) were not rerun. This does not establish real relay/replica authorization conformance or remove earlier opt-in port integration gates.

@callumalpass
callumalpass marked this pull request as draft October 4, 2026 15:53

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.

1 participant