diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9cfa41..f4a32ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,6 +78,13 @@ jobs: - name: Install website dependencies run: pnpm install --frozen-lockfile + - name: Check the Worklog example and the generated API reference + run: | + npm ci --prefix examples/worklog + npm run check --prefix examples/worklog + npm run build --prefix examples/worklog + pnpm check:sdk-api + - name: Build contract catalog working-directory: .sources/mdbase-contracts run: npm ci && npm run build diff --git a/README.md b/README.md index 27f5c30..48e0191 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,25 @@ remain traceable to the release artifacts they document. The first-party contract catalog is built from the commit pinned in `site-sources.json`, then published as static files under `/contracts/`. +## SDK guide example and API reference + +`examples/worklog` is the app the SDK tutorial builds. The tutorial and the +other SDK pages import their listings from its files, so a listing cannot drift +from code that compiles. CI installs, type-checks and builds it. + +```sh +cd examples/worklog +npm ci +npm run dev # http://localhost:5173, against production mdbase +npm run build # WORKLOG_ORIGIN=https://… sets the deployed origin +``` + +`/sdk/api/` is rendered from `src/data/sdk-api.json`, which +`pnpm build:sdk-api` reads out of the type declarations of the +`@mdbase-dev/sdk` version the example installs. After changing that version, +run `npm install` in `examples/worklog`, then `pnpm build:sdk-api`, and commit +both. CI fails (`pnpm check:sdk-api`) when the committed file is stale. + ## Connect release updates A published Connect desktop release dispatches its immutable tag to diff --git a/examples/worklog/.gitignore b/examples/worklog/.gitignore new file mode 100644 index 0000000..1cfc60a --- /dev/null +++ b/examples/worklog/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +src/mdbase-app.json diff --git a/examples/worklog/index.html b/examples/worklog/index.html new file mode 100644 index 0000000..088938a --- /dev/null +++ b/examples/worklog/index.html @@ -0,0 +1,12 @@ + + +
+ + +{problem}
: null} +{part} : part));
+ return inline ? run : {run}
; + }) +} diff --git a/src/data/docs-sections.ts b/src/data/docs-sections.ts index c79f672..d4fdc9e 100644 --- a/src/data/docs-sections.ts +++ b/src/data/docs-sections.ts @@ -23,24 +23,35 @@ export const docsSections: Record- Building an app? Connect a browser - app walks through sign-in, choosing a collection and working with + Building an app? Build an app + {" "}walks through sign-in, choosing a collection and working with records using the SDK.
diff --git a/src/pages/sdk/api/index.astro b/src/pages/sdk/api/index.astro index cb62e78..d154c46 100644 --- a/src/pages/sdk/api/index.astro +++ b/src/pages/sdk/api/index.astro @@ -1,222 +1,177 @@ --- import DocsLayout from "../../../components/DocsLayout.astro"; import CodeBlock from "../../../components/CodeBlock.astro"; +import DocText from "../../../components/DocText.astro"; +import api from "../../../data/sdk-api.json"; -const connectSignature = `connect(options: ClientOptions): Promise
- This page lists the main public API. The package’s TypeScript declarations
- are the full reference.
+ This page is generated from the declarations published in
+ {" "}{api.package} {api.version}, so every
+ signature and description is the one your editor shows. The main types are
+ listed in full under each entry point, followed by an index of its other
+ exports. The guide pages explain how the parts fit together; start with
+ {" "}Build an app.
- Connectors: relayConnector and
- {" "}nextRelayConnector (remote, through the relay or to the
- hosted replica), inProcessConnector,
- {" "}localLinkConnector, and from
- {" "}@mdbase-dev/sdk/node, ipcConnector and
- {" "}connectDaemonLocalhost. In tests,
- {" "}new MemoryReplica().connector().
-
| Method | Returns |
|---|---|
get(ref, include?, signal?) | RecordView; throws not_found |
find(ref, include?, signal?) | RecordView | null |
readMany(refs, …) | Several records in one call |
query(query, include?, signal?) | One QueryResult page; cursor when there is more |
pages(query, include?, signal?, { onReset }) | An async iterator over every page |
live(query, include?) | A LiveQuery: records, stale, complete, ready, subscribe(fn), setQuery(q), close() |
changes(cursor?, limit?) | { changes, cursor, reset } |
watchChanges(cursor, onChanges) | A stop function with ready and state |
backlinks(…) | Records that link to a record |
describe() | Types, contracts and settings of the collection |
queryContract({ id, version }, options?) | Records through a contract’s field names |
validate(refs?) | Validation issues per record |
views.list(), views.execute(…) | Saved views |
- A RecordRef is a record ID, { path },
- or a RecordView. Include is
- {" "}{ body, effective, document, links, diagnostics },
- each a boolean.
-
| Method | Notes |
|---|---|
create({ id?, type?, path?, frontmatter?, body?, document? }, options?) | The ID is minted locally and usable at once |
update(ref, { patch, unset, add, remove, body, bodyEdits, ifRevision }, options?) | Pass a RecordView to merge field by field |
replaceDocument(ref, source, options?) | Replace the whole Markdown source |
rename(view, to, { updateRefs, ifRevision }) | updateRefs defaults to true |
delete(ref, { ifRevision }) | |
preflightRename(view, to), preflightDelete(ref) | What the change would touch |
receipt(mutationId), awaitReceipt(mutationId, timeoutMs?) | The current receipt; or wait until it leaves pending |
pendingWrites() | Writes still waiting |
records.open(ref, { autosave, timeoutMs, signal, recovery }) | A record session lease; see Editing records |
- Every write resolves to a Write:
- {" "}state, receipt, records (the
- optimistic view), mutationId, onReceipt(fn),
- {" "}confirmed and published (promises).
- {" "}confirmed rejects with an MdbaseError if the
- write is rejected or its outcome is unknown. published
- {" "}settles when a file-backed replica has written the change to disk,
- which can be before confirmation.
-
| Member | Notes |
|---|---|
getStatus(), onStatus(fn), status | SyncStatus: mode, confirmedThrough, headKnown, pending, holds, unresolved, connection, installing |
summarizeStatus(status) | { kind, text, pending, holds, conflicts, confirmedThrough } |
link, onLink(fn) | "open", "reconnecting" or "closed" |
hello | What the replica reported at connect, including the grant’s role and capabilities |
close() | Close the client and its subscriptions |
| Member | Notes |
|---|---|
listHolds(), onHolds(fn) | Hold[]: id, path, reason, mine, theirs |
resolveHold(id, how, use?) | how: keep_mine, take_theirs, use, delete, keep_both |
describeHold(hold, { collectionId }), holdNotice(holds) | Plain-language text and the actions that apply |
listConflicts(recordId?), onConflicts(fn) | ConflictEntry[] |
resolveConflict(entry, field, value), dismissConflict(entry) | Choose a value, or keep what the record has |
Member of db.files | Notes |
|---|---|
list({ folder, media }), listPage(…), get(ref) | FileView: id, path, size, digest, media, state |
upload(path, source, { onProgress, transferId, fileId, ifRevision, signal }) | source: Uint8Array, ArrayBuffer or Blob |
download(ref), downloadStream(ref, { range }) | download is capped at 64 MiB |
move(view, to), delete(ref) | Both take ifRevision |
- createMdbase(declaration) is the entry point for web apps. Its
- snapshot has a phase that says what to show; when
- {" "}phase.kind is "ready",
- {" "}phase.connection is the open connection. Its actions
- include start, signIn, select,
- {" "}forget, signOut and close.
-
- new MdbaseConnect(declaration) is the same entry under its
- previous name. Its application() returns an object with
- {" "}getSnapshot, subscribe, start,
- {" "}authorize, select, assess,
- {" "}configure, clearSelection,
- {" "}forget, setForeground and
- {" "}close. connection(collectionId) returns a
- connection whose records is an MdbaseClient.
- See Connect a browser app for its
- current status.
-
toPlain, toValue, valueEquals, uuidv7: values and IDs.loadOrCreateClientKey, indexedDbKeyStorage, memoryKeyStorage: your app’s session key.isConfirmed(receipt): whether a receipt is confirmed.TimersApi: see Notifications and timers.@mdbase-dev/sdk/testing: MemoryReplica, createApplicationFixture.
+ import { … } from "{entry.specifier}"
+
{item.name} {item.kind}
+
+ Deprecated{typeof item.deprecated === "string" ? <>:
{member.signature}
+
+ Deprecated{typeof member.deprecated === "string" ? <>:
| Name | +Kind | +Description | +
|---|---|---|
+ {item.name}
+ |
+ {item.kind} | +
+ {item.deprecated ? "Deprecated. " : ""}
+ |
+
- Failures throw MdbaseError with code,
- {" "}recovery, and often reason,
+ Failures throw MdbaseError with a code, a
+ {" "}recovery, and often a reason,
{" "}details, issues or
- {" "}retryAfterMs. Use isMdbaseError(e, code?).
- Branch on code and reason; message
- {" "}is for developers. Each code has one recovery.
+ {" "}retryAfterMs. Check them with
+ {" "}isMdbaseError(error, code?). Branch on code
+ {" "}and reason; message is for developers. A
+ write the collection refuses does not throw: its Write has
+ {" "}state: "rejected" and the same fields under
+ {" "}receipt.problem. To show a failure to a person, pass it
+ to describeError().
| Code | Recovery | When |
|---|---|---|
| Code | +Recovery | +When | +
invalid_request | fix_request | Malformed parameters, unsafe paths, an expired query cursor |
invalid_record | fix_request | The write fails validation; see issues |
not_found | refresh | No such record, file, hold or transfer |
conflict | resolve_conflict | The current state doesn’t allow this write: revision, path_taken, duplicate_value, … |
unauthenticated | reauthorize | No session, an expired one, or a revoked grant |
forbidden | reauthorize | The grant lacks the capability, or the role doesn’t allow it |
collection_invalid | repair_collection | The collection’s config or type files can’t be loaded |
unavailable | retry | The replica can’t be reached, for example no device online |
rate_limited | retry | Too many requests; see retryAfterMs |
quota_exceeded | free_space | The collection is over its storage quota |
too_large | fix_request | A request or file over its limit |
upgrade_required | upgrade | This SDK version is too old for the replica or collection |
outcome_unknown | resolve_outcome | A write’s outcome can’t be determined; read again and decide |
cancelled | none | You cancelled the request |
internal | contact_support | A bug; the request may be retried once |
{error.code} |
+ {error.recovery} |
+ {when[error.code]} | +
+ A build-time package. Nothing from it runs in your app; install it as a + development dependency. +
+ +
+ validate-manifest runs the parser mdbase uses when your app
+ registers. It exits non-zero with a list of problems, each with the path
+ of the field at fault.
+
+ An HTTP homepage is refused unless you pass --allow-local.
+ Use the flag for a development
+ manifest and leave it off for the one you publish, so a localhost
+ address cannot reach production by mistake.
+
+ The SDK also validates the manifest when the app starts. Checking in the + build moves the failure from your users’ browsers to your own terminal. +
+ +
+ A type pack carries the type
+ and contract files your app adds to a collection. Inside a manifest every
+ file is pinned by a SHA-256 digest. defineTypePack() reads
+ the documents, computes the digests and returns the object to place under
+ {" "}provisions.type_packs:
+
| Resource field | +Meaning | +
|---|---|
kind | "type", "contract" or "schema" |
mode | "seed" or "managed"; see below |
source | The file’s name inside the pack |
target | Where it goes in the collection, such as _types/worklog-entry.md. Defaults to source. |
document | The file’s exact text |
upgradeFrom | Seed types only: earlier versions you shipped, oldest first, that a collection may be upgraded from |
+ Keep the documents as real files in your repository and read them in the + script. They are then reviewable in a pull request, and the manifest is a + build output. +
+ +| Mode | +Who owns the file after setup | +Use it for | +
|---|---|---|
seed |
+ The user. Your app writes it once; later edits by the user stay. | +A type people may want to extend with fields of their own | +
managed |
+ Your app. A new version of your pack updates it. | +A contract, or a type whose exact shape your app depends on | +
+ Adding files to a collection needs the
+ {" "}definitions.manage capability in your manifest, and the
+ user sees and accepts the change first. See
+ {" "}Setup.
+
+ defineDataContract() validates a
+ {" "}contract document as you write it, and
+ {" "}dataContractDigest() computes the digest a manifest
+ names it by.
+
+ validate-contract on the command line checks a contract file
+ the same way.
+
+ The Worklog example writes its development and deployed manifests from one + script, validating each: +
+
+ The package also exports createSandbox(). It is for the
+ older @mdbase-dev/connect client. With
+ {" "}@mdbase-dev/sdk, test against
+ {" "}MemoryReplica and
+ {" "}createApplicationFixture(); see
+ {" "}Testing.
+
+ Worklog is a one-screen app: a form that adds an entry, and a list of + entries that stays current. Each entry is a Markdown file in the user’s + collection, so the user can also open it in the + {" "}mdbase apps, in Obsidian, or in any text editor. +
+
+ The finished app is about 150 lines. mdbase supplies sign-in, the
+ collection picker, storage, sync and conflict handling. The complete
+ project is in
+ {" "}examples/worklog;
+ every listing on this page is a file from it.
+
- You can write and test the whole flow on this page against
- {" "}createApplicationFixture() before
- signing in to a real collection.
+ Node 20 or later. You need no account to start: the app first runs in
+ a collection kept in your browser. Signing in to an mdbase account
+ comes at step 7. Developing on http://localhost needs an
+ {" "}@mdbase-dev/sdk release after
+ {" "}0.1.0-beta.168.
+ @mdbase-dev/sdk is the client.
+ {" "}@mdbase-dev/ui has the sign-in and collection screens;
+ {" "}@mdbase-dev/i18n and the two Lingui packages are its
+ peer dependencies. @mdbase-dev/connect-dev checks the
+ manifest at build time. The packages are in beta, so the versions are
+ pinned exactly.
+
+ Delete the template’s src/App.tsx, src/App.css
+ {" "}and src/index.css; the steps below replace them. Then fix
+ the development port, because the manifest names the exact origin the app
+ is served from:
+
+ Every record in a collection has a type, and a type is itself a Markdown
+ file in the collection’s _types/ folder. Worklog needs one.
+ Write the copy your app will offer to a collection that lacks it:
+
+ The schema is JSON Schema. mdbase checks every write against it, so an
+ entry with no summary is refused before it is saved.
+ {" "}additionalProperties: true lets the user and other apps
+ keep fields of their own on the same records.
+
- The manifest says who your app is and what it needs. Ask only for the - capabilities you use; see Authorization. + The manifest tells mdbase who your app is and what it needs. It names + the origin the app is served from, so development and the deployed site + each need their own. A short script writes either one: +
+id is a lowercase reverse-domain name with
+ at least three parts. Use a domain you control.
+ homepage and redirect_uris
+ {" "}are on the app’s origin. A browser app can list its homepage as
+ the one redirect URI.
+ capabilities and files are
+ what the user is asked to approve. Ask for what you use;
+ {" "}Authorization lists them.
+ {" "}definitions.manage is there so the app can add its
+ type to a collection the user already has.
+ provisions.type_packs carries the type
+ document from step 2. See
+ {" "}Provisioning.
+ Run the script before the development server and before a build:
+
+ Add src/mdbase-app.json to .gitignore: it is
+ generated.
+ Create one entry for the whole app. Creating it does no work; the SDK + starts on the first call. It registers the manifest with mdbase when the + app starts, so there is no application ID to request first. +
+origin must be the exact origin the page is served from.
+ Reading it from the manifest keeps the two in step.
+ environment is "production" on localhost too.
+ Your development build talks to the same mdbase service your users do.
+ unsynced: true lets people use the app before they sign in.
+ Their work is kept in the browser until they connect a collection. See
+ {" "}Work before connecting.
+
+ Between opening the app and seeing their entries, a user may need to sign
+ in, pick or create a collection, approve your app’s access, and let it add
+ its type. CollectionGate from @mdbase-dev/ui
+ {" "}shows each of those screens and renders your app once a collection is
+ open.
+
- The SDK is in beta; pin an exact version in an app you ship. See the
- {" "}quickstart.
+ The gate hands your app a connection. Its
+ {" "}records property is an MdbaseClient for the
+ open collection. CollectionSwitcher is the control for
+ changing collection, adding one and signing out. Keying the screen on
+ {" "}connection.id resets it when the user switches.
+
The entry point loads the gate’s stylesheet:
++ UI components covers restyling and the other + components. To draw these screens yourself, see + {" "}Sign-in and collections.
-db.live() sends
+ the current entries and then every change, including ones made in
+ another app or on another device. Nothing in this file refetches.
+ db.create()
+ {" "}resolves when the entry is saved on this device, and the live query
+ already shows it. Sync continues in the background, online or offline.
+ path, or give the type a path pattern.
+ Write has state: "rejected" and
+ {" "}receipt.problem says why. describeError()
+ {" "}turns that, or a thrown error, into a sentence you can show.
+
- Create one createMdbase() entry for your app.
- {" "}origin must be the exact origin your app is served from.
- The SDK registers your manifest with mdbase when the app starts, so you
- don’t need an ID from mdbase first. MdbaseConnect is the same
- entry under its previous name.
+ Records and operations has the rest:
+ queries, updates, renames, attachments.
- Render from the snapshot’s phase. It tells you where the user
- is: signed out, signing in, choosing a collection, or ready. Switch on
- {" "}phase.kind rather than building your own ready check.
- When a collection needs set-up first, the phase is
- {" "}needs_setup;
- {" "}assess() says why and configure() applies
- what your manifest declares.
+ Open http://localhost:5173. The app opens straight away in
+ its unsynced collection. Add an entry: a line appears saying the work is
+ saved only in this browser. Reload, and the entry is still there.
- forget(id) removes a collection from your app’s list on this
- device. It does not revoke access or delete anything.
+ Choose Connect to sync your work to sign in. mdbase opens
+ in a window and asks you to approve the app. Because the app is on
+ {" "}localhost, that screen calls it “an app running on this
+ computer” and says its identity is unverified. Then:
+
worklog-entry type automatically.
+
+ Open the same collection in the
+ {" "}Editor: your entries are
+ files under worklog/. Change one there and the list in your
+ app updates.
+
+ Develop on localhost explains what + is different about a development build.
-
- connection.records is an ordinary MdbaseClient.
- Everything in Records and operations applies.
+ The build is a folder of static files. Host it anywhere that serves HTTPS
+ at the origin you built for. There is no server component, no API key and
+ no secret: the app holds nothing until a user signs in and approves it.
homepage origin and the site’s origin must
+ match exactly, including www and any port.
+ id from
+ {" "}dev.example.worklog to a name under your own domain.
+
+ Only App.tsx and the hooks in Worklog.tsx are
+ React. The entry is a plain object with a snapshot you subscribe to, and
+ a phase that says where the user is:
+
- origin is normally your manifest’s HTTPS
- {" "}homepage origin. A Capacitor shell
- ({" "}capacitor://) or Chrome extension
- ({" "}chrome-extension://) origin must also be listed under the
- manifest’s installation_origins. Firefox extension origins are
- not accepted. Browser redirect_uris must be HTTPS on the
- homepage origin; a native app can use a private-use scheme based on its
- manifest ID.
+ You can draw each phase yourself, or mount the shared screens into an
+ element with mountCollectionUi(); see
+ {" "}UI components.
- Web apps, Node scripts and editor plugins use it to read, query, watch - and change records and files in one collection. + An mdbase collection is a folder of Markdown files with typed + frontmatter, synced across a person’s devices. An app built with the SDK + reads and writes the records in a collection its user chooses.
+
- @mdbase-dev/sdk is in beta on
- {" "}npm. New
- betas are published as Connect changes, and the API can change between
- them, so pin an exact version in an app you ship.
-
@@ -119,7 +157,7 @@ export async function listOpenTasks(db: MdbaseClient) {
createMdbase() signs the user in and hands you a
connection. Its client is an ordinary MdbaseClient.
- See Connect a browser app.
+ See Build an app.
| Package | +What it is for | +
|---|---|
@mdbase-dev/sdk |
+
+ The client. createMdbase for apps,
+ {" "}connect and MdbaseClient for
+ everything that reads and writes records.
+ {" "}API reference.
+ |
+
@mdbase-dev/ui |
+ + The sign-in, collection picker, approval and setup screens as React + components. UI components. + | +
@mdbase-dev/connect-dev |
+ + Build-time tools: the manifest checker and the type pack builder. + {" "}Developer kit. + | +
The SDK has several entry points:
@mdbase-dev/sdk |
- connect, MdbaseClient,
- {" "}createMdbase, errors, status helpers and connectors
+ createMdbase, connect,
+ {" "}MdbaseClient, errors and status helpers
|
||
@mdbase-dev/sdk/node |
- Connectors to the desktop app over local IPC or localhost | +@mdbase-dev/sdk/describe |
+ describeError: a title, message and action for any failure |
@mdbase-dev/sdk/testing |
MemoryReplica and test fixtures |
||
@mdbase-dev/sdk/node |
+ Connectors to the desktop app over local IPC or localhost | +||
@mdbase-dev/sdk/browser |
@@ -182,17 +258,70 @@ export async function listOpenTasks(db: MdbaseClient) { | @mdbase-dev/sdk/account |
Encryption password and recovery key set-up for Private collections |
@mdbase-dev/sdk/app-host, @mdbase-dev/sdk/app-storage |
- - The parts that let an app run its own replica in the browser. These - are for mdbase’s own apps at launch. - | -
- The package needs Node 20 or later outside the browser. It is what apps
- use instead of the @mdbase-dev/connect beta.
+ The SDK needs Node 20 or later outside the browser. It is what apps use
+ instead of the @mdbase-dev/connect beta.
+
+ There is no review, registration form or API key. The SDK registers your + manifest with mdbase the first time your app starts, and each user + approves the app for their own collection. To publish, deploy the built + files to an HTTPS origin; the + {" "}tutorial ends with + that step. +
++ The approval screen shows your app’s name and the site it is served + from. mdbase does not verify who publishes an app, and an application ID + is a label, so serve your app from an origin your users recognise. +
++ The apps page lists the apps mdbase makes. There is + no directory of other apps yet. If you build one, write to + {" "}callum@mdbase.dev. +
+ +
+ @mdbase-dev/sdk, @mdbase-dev/ui and
+ {" "}@mdbase-dev/connect-dev are in beta on npm.
update_app.
+
+ createMdbase() returns one object for your whole app. It
+ holds a snapshot of where the user is, and the actions that move them on.
+ {" "}CollectionGate draws every screen
+ on this page for you; read on if you draw them yourself, or want to know
+ what the gate is doing.
+
+ mdbase.getSnapshot() returns the current state, and
+ {" "}mdbase.subscribe(listener) calls you when it changes.
+ The snapshot object is replaced on every change, so it works with
+ {" "}useSyncExternalStore and with
+ plain equality checks.
+
| Field | +Meaning | +
|---|---|
phase | Where the user is. Render from this. |
collections | The collections this app may open, for a picker. |
selectedCollectionId | The open collection, or null. |
sync | Sync state of the open collection, as summarizeStatus() returns it. |
writes | Unconfirmed and refused writes of the open collection. See Pending writes and sync status. |
links | account and manageAccess: mdbase pages to link to for the account and for who can reach the collection. |
unsyncedWork | Set when work saved before connecting has not been brought in. See Work before connecting. |
+ snapshot.phase.kind is always exactly one of these. Switch on
+ it; do not build a ready check from other fields.
+
| Phase | +The user is | +What moves them on | +
|---|---|---|
starting |
+ Waiting for the SDK to restore a signed-in device | +Nothing; it settles by itself | +
signed_out |
+ Not signed in. notice says why when sign-in just ended. |
+ signIn() |
+
authorizing |
+ In the mdbase window. purpose is sign_in, add or approve. |
+ The user finishes or closes the window | +
choosing |
+ Signed in with nothing open | +select(id), addCollection() |
+
opening |
+ Waiting for a collection to open. slow turns true after 15 seconds. |
+ Nothing; chooseAnother() to leave |
+
needs_approval |
+ Looking at a collection this app is not approved for, usually from a shared link | +approve() |
+
needs_setup |
+ In a collection that lacks something the manifest provides, such as your types | +setup.plan() then setup.apply(plan) |
+
ready |
+ In an open collection. Use phase.connection. |
+ + |
unsynced |
+ Working before connecting. Use phase.connection as when ready. |
+ signIn() to connect |
+
problem |
+ Stopped. phase.problem has the wording and one action. |
+ The action in problem.action |
+
unavailable |
+ Somewhere the SDK cannot run: server rendering, or an unsupported browser | ++ |
closed |
+ After close() |
+ + |
+ Once a collection is ready it stays ready through
+ re-checks and passing problems; phase.rechecking and
+ {" "}phase.problem report them for a status line. Your app
+ screen is not torn down while the network drops and returns.
+
+ phase.connection has records (the
+ {" "}MdbaseClient),
+ {" "}files, people and, when the manifest asks
+ for them, timers.
+
+ Sign-in and approval happen on mdbase’s own pages, in a window the SDK + opens. Browsers only allow that during a click or key press. +
+| Call | +What the user does in the window | +
|---|---|
signIn() | Signs in, then picks or creates a collection and approves your app for it |
addCollection() | Picks another collection or creates one. Nothing is preselected. |
approve(id?) | Approves one named collection; by default, the one the URL asks for |
reauthorize() | Approves the open collection again, after access was lost or the app’s manifest asks for more |
requestAccess() | Allows an optional file action. See below. |
+ Each call resolves with the new snapshot when the window closes. In the
+ {" "}choosing phase, phase.lastApproval.outcome
+ {" "}says how the last one ended: added,
+ {" "}unchanged, declined or closed.
+
refreshCollectionList() to re-read it at another
+ moment, such as when a menu opens.
+ deleteCollection(id, { confirmName }) deletes a
+ collection for everyone, on every device. Only its owner can, and
+ {" "}confirmName must be the collection’s name as the user
+ typed it.
+ kind: "private". It opens the same way; see
+ {" "}Private collections.
+
+ The SDK keeps the open collection in the page URL as
+ {" "}?collection=<id> and follows Back and Forward. A
+ link with that parameter opens that collection for anyone the app is
+ approved for, and takes others to needs_approval. The rest of
+ the URL, path and hash included, is yours.
+
+ A collection is in needs_setup when it lacks something your
+ manifest’s provisions supply.
+ Setup shows the user what would be added and applies it only when they
+ accept.
+
plan.summary and each change.message are
+ written to be shown as they are. plan.staged has the
+ documents themselves, for a details view.
+ definitions.manage capability. An app that
+ lacks it gets a plan with refusal.code
+ {" "}"setup_forbidden".
+ status: "conflict". Conflicts listed in
+ {" "}plan.adoptable can be resolved by taking your version:
+ pass them back as setup.plan({ adopt }).
+ setup.plan({ records }) adds starter records,
+ such as a welcome note, written with the setup when their path is free.
+ configure() plans and applies in one call, for an app with
+ nothing to review.
+ + A collection the user creates while signing in to your app is set up + during sign-in, so new users do not see this screen. +
+ +
+ File actions listed as optional in the manifest are asked for
+ at the moment the user first needs them.
+
+ The result is granted, declined or
+ {" "}cancelled. An action that is already allowed resolves
+ {" "}granted at once and opens nothing. The reason is one
+ line of at most 140 characters, shown to the user. The collection stays
+ open throughout.
+
+ A user can remove your app’s access on their account page at any time.
+ The collection then stops with a problem whose action is
+ {" "}approve; offer a button that calls
+ {" "}reauthorize(). Link to
+ {" "}snapshot.links.manageAccess wherever your app says who
+ can reach the collection; that page is where access is managed.
+
+ Signing out ends the session and removes this browser’s copy of the + account’s collections and keys. It is refused while changes are still + waiting to sync, so that work is not thrown away by accident. +
+ +
+ describeError() accepts a thrown error, a refused write’s
+ {" "}receipt.problem, or a phase’s problem, and returns a
+ title, a message and one action. It returns null when there
+ is nothing to show, such as a request your own code aborted.
+
action |
+ What to offer | +
|---|---|
sign_in | A button that calls signIn() |
approve | A button that calls approve() or reauthorize() |
choose_collection | chooseAnother() |
set_up | Your setup review |
retry | start(), or the call that failed |
wait | Nothing to press; retryAfterMs may say how long |
reload | Reload the page |
update_app | A link to updateUrl |
free_space, check_connection, resolve_outcome, contact_support, none | The message says what to do; there is nothing to call |
+ actionLabel is the button’s wording, and
+ {" "}transient is true for problems the SDK clears by itself,
+ which can be shown quietly.
+
+ A native shell or a browser extension cannot open a popup. Pass a
+ {" "}signIn opener and the SDK gives it the URL to show:
+
+ returnUrl must be one of the manifest’s
+ {" "}redirect_uris. mdbase sends the browser there when
+ sign-in ends. It carries no credential; the SDK learns the result itself.
+
+ You develop against the same mdbase service your users use. There is no + development mode, sandbox key or allowlist to apply for. +
+
+ Use an @mdbase-dev/sdk release after
+ {" "}0.1.0-beta.168. Earlier releases refuse an
+ {" "}http://localhost origin and fail to load under
+ {" "}vite dev.
+
+ Your app’s origin may be plain HTTP when it is on this computer: +
+http://localhost:<port>http://127.0.0.1:<port>http://[::1]:<port>+ Any port works. Every other app origin must be HTTPS, including a + development machine reached by its LAN address or a tunnel hostname. +
+ +
+ A manifest names the origin its app is served from, so keep two: the one
+ you publish, with your HTTPS homepage, and one for development whose
+ {" "}homepage and redirect_uris use the
+ development server’s origin.
+
+ Everything else can be the same. You can keep two files, or write both + from one script as the Worklog tutorial + {" "}does. +
+
+ The manifest checker refuses an HTTP homepage by default, so that a
+ development manifest is caught before it ships. Pass
+ {" "}--allow-local for the development one:
+
+ The published
+ {" "}JSON Schema
+ {" "}describes the deployed form, so an editor that validates against it
+ marks an HTTP homepage. The checker with --allow-local is the
+ authority for a development manifest.
+
+ environment stays "production". It names the
+ mdbase service your app talks to, and that is the same one in development.
+ Only origin and the manifest change between development and
+ your deployed site.
+
+ mdbase identifies an app by its manifest and binds what a user approves to + the exact origin, port included. Your localhost build is therefore its own + application: +
+localhost:5173 are approved for
+ that origin only. Your deployed app asks its users separately.
+ name, such as
+ “Worklog (development)”, so the two are easy to tell apart on your
+ account page.
+ + The records are real. A development build that opens your own collection + reads and writes your own files, so use a collection made for testing. +
+ ++ When a localhost app asks for access, the approval screen calls it “an + app running on this computer”, says its identity is unverified, and shows + no icon. A page on the internet cannot give itself a localhost origin, so + the only program that can be there is one already running on the person’s + machine. The screen says that plainly instead of presenting the manifest’s + name as a brand. +
++ Account sign-in, the collection choice and the list of capabilities are + the same as for a deployed app. +
+ +
+ redirect_uris must list at least one URL on the app’s origin.
+ A browser app needs no page there. Sign-in happens in a window that the
+ SDK opens, and the SDK learns the result itself; that window never
+ navigates to your redirect URI and no token is delivered to it. Listing
+ the homepage is enough.
+
+ The redirect URI is used when you pass your own sign-in opener, as a
+ native shell or browser extension does. mdbase then sends the person’s
+ browser to returnUrl when sign-in ends, to bring them back to
+ your app. It carries no credential, so any page or deep link of yours can
+ receive it. See Sign-in outside a popup.
+
+ The SDK runs its engine in a Web Worker and loads WebAssembly files that
+ ship inside the package. With Vite, vite dev and
+ {" "}vite build both work with the default configuration.
+
+ Fix the port, since the manifest names it: +
+
+ With another bundler, the requirement is that the files under the
+ package’s dist/ are served from your own origin, beside the
+ SDK’s modules. For a page with no bundler, use the
+ {" "}browser bundle.
+
+ Much of an app can be built before you sign in at all: +
+createMdbase(declaration, { unsynced: true })
+ {" "}opens a real collection kept in the browser. See
+ {" "}Work before connecting.
+ MemoryReplica and createApplicationFixture()
+ {" "}run the same client in tests. See Testing.
+ | What you see | +Cause | +
|---|---|
| “The app couldn’t load” as soon as the page opens | ++ The SDK release predates localhost support, or the SDK’s files are + not served from your origin. + | +
| The entry refuses to start with an origin error | +
+ origin differs from the page’s origin, or from the
+ manifest’s homepage origin. localhost and
+ {" "}127.0.0.1 are different origins.
+ |
+
| Signed out after restarting the development server | +The server picked another port. Set strictPort. |
+
The checker reports /homepage |
+
+ An HTTP homepage needs --allow-local, or
+ {" "}allowLocal: true when you call
+ {" "}validateAppManifest().
+ |
+
| The sign-in window does not open | +
+ signIn() was called after an await. Call
+ it directly in the click handler.
+ |
+
- A web app declares an HTTPS homepage and up to ten
- {" "}redirect_uris on the homepage’s origin.
+ A web app declares an HTTPS homepage and between one and
+ ten redirect_uris on the homepage’s origin.
{" "}distribution may be left out; it defaults to
{" "}"web".
+ For development, the homepage and redirect URIs may instead be on
+ {" "}http://localhost, http://127.0.0.1 or
+ {" "}http://[::1], with any port. That manifest is a separate
+ application from your published one; see
+ {" "}Develop on localhost.
+
+ A browser app that signs in through the SDK’s own window can list its + homepage as its one redirect URI. See + {" "}Redirect URIs. +
+
+ You do not have to write the manifest by hand.
+ {" "}@mdbase-dev/connect-dev
+ {" "}checks it and builds its type packs.
+
| Prop | +Meaning | +
|---|---|
mdbase | The entry from createMdbase(). |
app | name, and optionally mark: an element shown as your app’s icon. |
children | A function of the open connection. Called in the ready and unsynced phases. |
layout | "page" (default) fills the viewport. "panel" sits inside your own shell or dialog. |
unsynced | Show the work before connecting screens. Pass true, or { storage, map }. |
extraActions | Your own links or buttons on the gate’s screens, such as “Try a sample”. |
onBeforeLeave | Called before anything that leaves the open collection. Return false, or a promise of it, to stay. |
autoStart | Call mdbase.start() on mount. Default true. |
copy | Replacement wording. See below. |
+ While a collection is open, the gate shows a banner above your app for a + live problem, such as access that was removed. +
+ +
+ The control inside your app for the open collection. It lists the
+ approved collections, and has “Add a collection”, “Manage access” and the
+ account with sign-out. It takes the same mdbase,
+ {" "}app, unsynced and
+ {" "}onBeforeLeave props as the gate.
+
| Export | +Use | +
|---|---|
CollectionPanel | The gate’s screens and the switcher’s list in one panel, for a settings dialog or an app that opens on its own screen first. |
CollectionProblem | One problem with its fix, as a block or a banner. |
CollectionSetupReview | The setup review on its own. |
CollectionUnsyncedLine, CollectionBringIn | The “saved only in this browser” line and the offer to bring that work in. |
CollectionSaveWarning | A warning when unsaved changes sit in storage the browser may clear. |
useCollectionUi() | The state and actions behind the components, for a layout of your own. |
+ All of them are exported from @mdbase-dev/ui/collections.
+
+ The stylesheet is self-contained. It follows the system’s light or dark
+ setting; put data-theme="light" or
+ {" "}data-theme="dark" on the root element to fix one. Set
+ {" "}--mdbase-collections-* custom properties on any
+ ancestor to match your app:
+
| Properties | +Control | +
|---|---|
-accent, -on-accent | Primary buttons and focus |
-canvas, -surface, -border, -current-bg | Backgrounds and lines |
-text, -text-soft, -danger, -warning | Text and status colours |
-font, -font-size, -font-size-caption, -font-size-section, -font-size-heading, -font-weight-strong | Type |
-radius-sm, -radius, -radius-lg, -control-height | Shape and size |
-elevation-low, -elevation-high, -scrim | Shadows and the dialog backdrop |
+ The components are in English by default and need no provider. Pass
+ {" "}copy to replace individual strings;
+ {" "}defaultCollectionCopy lists every key.
+
+ The package also ships its strings in French, German, Spanish, Italian,
+ Portuguese, Russian, Japanese, Korean and Chinese.
+ {" "}@mdbase-dev/ui/i18n exports them as
+ {" "}uiCatalogues, for an app that sets up
+ {" "}@mdbase-dev/i18n with its own Lingui catalogue.
+
+ mountCollectionUi() renders a component into an element you
+ give it. Your app needs no React code of its own, though the
+ {" "}react and react-dom packages must be
+ installed.
+
+ part is "panel", "switcher" or
+ {" "}"problem". To draw everything yourself and keep only the
+ logic, collectionView() turns a snapshot into the screen to
+ show:
+
+ A collection your app uses can also be opened in mdbase’s
+ {" "}Editor, Reader and Writer.
+ {" "}mdbaseAppHref() builds a link to one of them that opens
+ the collection named in the current page’s URL:
+
+ The AppSwitcher component in the same package is the menu
+ mdbase’s own apps show under their name. It lists only those three apps,
+ so it is not a place to add yours.
+
+ With the unsynced option, a signed-out visitor gets a
+ working app straight away. The SDK opens an unsynced
+ collection: a real collection, with your types installed, stored
+ in this browser and nowhere else. Your code reads and writes it through
+ the same MdbaseClient as a synced one.
+
+ When the person later signs in and opens a collection, the work they did + can be brought into it. +
+
+ The shared UI handles all of this when you pass
+ {" "}unsynced to CollectionGate and
+ {" "}CollectionSwitcher. The rest of this page is for apps
+ that draw it themselves.
+
+ With the option set, start() opens the unsynced collection
+ whenever the person is signed out and the URL names no collection. The
+ phase is unsynced. The types in your manifest’s
+ {" "}provisions are installed
+ in it automatically.
+
unsynced like ready:
+ {" "}phase.connection is the same kind of connection.
+ snapshot.unsyncedWork is set; show a line such as
+ “Saved only in this browser” with a way to connect.
+ snapshot.collections.
+ choosing phase as usual.
+ {" "}mdbase.unsynced.open() opens the unsynced collection
+ from there, or from a sign-in screen that offers “Continue without
+ connecting”.
+
+ After the person connects, snapshot.unsyncedWork stays set
+ while the collection they opened does not have the work yet. Plan the
+ transfer, show what it would do when there is something to decide, then
+ run it.
+
| Item outcome | +Meaning | +
|---|---|
create | The record will be created, with the same ID. |
present | An earlier transfer already brought it in. |
skipped | Your map left it out. |
changed | It was brought in earlier and has changed here since. The newer version is created beside the first as a copy. |
renamedFrom has the path
+ you asked for.
+ addTypes, types it lacks are added from the unsynced
+ collection.
+ result.retired is
+ true and the unsynced collection starts again empty.
+
+ By default each record keeps its path and its whole Markdown source. Pass
+ {" "}map to change where records go, or to leave some out:
+
+ The function returns a CreateInput, or null to
+ leave the record out. A record always keeps its ID; returning a different
+ {" "}id is refused.
+
+ Discarding erases the unsynced collection from this browser. Ask first. +
+ +