mindoodb-app-sdk 0.0.19 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # mindoodb-app-sdk
2
2
 
3
- Build apps that run inside **MindooDB Haven** -- the browser-based workspace for end-to-end encrypted, offline-first data.
3
+ Build apps that run inside **MindooDB Haven** -- the browser-based workspace for end-to-end encrypted, local-first data.
4
4
 
5
5
  `mindoodb-app-sdk` is the TypeScript SDK that connects your web application to Haven through a secure message bridge. Your app runs in a sandboxed iframe (or a separate browser window) and gains access to databases, documents, attachments, virtual views, and live host events -- all without ever touching encryption keys or raw sync state.
6
6
 
7
7
  ## What is a MindooDB App?
8
8
 
9
- [MindooDB](https://mindoodb.com) is an **end-to-end encrypted, offline-first sync database**. Data is encrypted on the client before it ever leaves the device. Under the hood it uses [Automerge](https://automerge.org/) CRDTs so multiple users can edit concurrently and conflicts resolve automatically.
9
+ [MindooDB](https://mindoodb.com) is an **end-to-end encrypted, local-first sync database**. Data is encrypted on the client before it ever leaves the device. Under the hood it uses [Automerge](https://automerge.org/) CRDTs so multiple users can edit concurrently and conflicts resolve automatically.
10
10
 
11
11
  **Haven** is the browser-based workspace that sits on top of MindooDB. Users organize databases, applications, notes, and media on a visual grid -- think of it as a customizable home screen for encrypted data that works offline and syncs in the background.
12
12
 
@@ -772,11 +772,227 @@ Like the text buffer, the rich-text handle always sends the original `baseHeads`
772
772
 
773
773
  #### End-to-end sample
774
774
 
775
- The Word/.docx mode in [`mindoodb-app-teamedit`](https://github.com/klehmann/mindoodb-app-teamedit) wires `MindooDBRichTextHandle` to an `@eigenpal/docx-editor-vue` editor and is the recommended starting point for building your own collaborative rich-text app.
775
+ The Word/.docx mode in [`mindoodb-app-teamedit`](https://github.com/klehmann/mindoodb-app-teamedit) is the **only supported in-repo use case** for the Automerge binary sync API below — it needs a local replica and binary flush because `set`/`unset` and the JSON patch APIs are not sufficient for that editor. For span-only rich-text apps, use `createMindooDBRichTextHandle` instead.
776
+
777
+ ### Automerge binary sync (advanced)
778
+
779
+ > **Avoid these APIs unless you have no alternative.** For normal app editing, use `documents.update({ set })`, `documents.update({ unset })`, and the JSON patch operations (`text`, `richText`, `json`) documented above. `getAutomergeSnapshot` and `applyAutomergeChanges` were added as an **escape hatch** for the one case where those APIs are not enough: TeamEdit's Word/.docx mode, which hosts a local `@automerge/automerge` replica and flushes raw change bytes on save. Do not adopt binary sync for new features unless you have the same constraint — you take on an Automerge dependency, manual replica lifecycle, and a much harder integration path.
780
+
781
+ If you must use binary sync, Haven decrypts the canonical document, merges incoming change batches with `Automerge.applyChanges`, and persists the result. Your app never sends encrypted blobs or sync metadata; it only sends the same binary change format Automerge produces locally.
782
+
783
+ Typical flow:
784
+
785
+ ```
786
+ ┌────────────────────────────────────────────────────┐
787
+ open ──► │ getAutomergeSnapshot → load local replica │
788
+ └─────────────────────────┬──────────────────────────┘
789
+ │
790
+ edit ──► │ Local Automerge doc (or span edits applied locally) │
791
+ └─────────────────────────┬──────────────────────────┘
792
+ │ getChangesSince(baseHeads)
793
+ ▼
794
+ ┌────────────────────────────────────────────────────┐
795
+ save ──► │ applyAutomergeChanges({ baseHeads, replicaHeads, │
796
+ │ changes }) │
797
+ └─────────────────────────┬──────────────────────────┘
798
+ │ Haven merges + persists
799
+ ▼
800
+ ┌────────────────────────────────────────────────────┐
801
+ │ Apply result.changesSince locally (or skip if empty) │
802
+ └────────────────────────────────────────────────────┘
803
+ ```
804
+
805
+ Bridge RPCs: `documents.automerge.getSnapshot` and `documents.automerge.applyChanges`.
806
+
807
+ #### `getAutomergeSnapshot`
808
+
809
+ Export the full internal Automerge document as a binary snapshot plus the current document heads:
810
+
811
+ ```ts
812
+ const snapshot = await db.documents.getAutomergeSnapshot(docId);
813
+ // snapshot = { binary: Uint8Array, heads: string[] }
814
+
815
+ const localDoc = Automerge.load(snapshot.binary);
816
+ const baseHeads = [...snapshot.heads];
817
+ ```
818
+
819
+ Options:
820
+
821
+ ```ts
822
+ interface MindooDBAppAutomergeGetOptions {
823
+ revisionId?: MindooDBAppDocumentRevisionId; // historical read via documents.listHistory()
824
+ }
825
+ ```
826
+
827
+ Only call this when binary sync is unavoidable — e.g. opening a local replica, refreshing after `applyAutomergeChanges`, or read-only historical preview at a `revisionId`. For normal reads, use `getRichText` or `documents.get`.
828
+
829
+ #### `applyAutomergeChanges`
830
+
831
+ Merge a batch of raw Automerge change bytes into the canonical document on Haven:
832
+
833
+ ```ts
834
+ const changes = Automerge.getChangesSince(localDoc, baseHeads as Automerge.Heads);
835
+ const replicaHeads = Automerge.getHeads(localDoc);
836
+
837
+ const result = await db.documents.applyAutomergeChanges(docId, {
838
+ baseHeads, // optional metadata: heads when the local batch was authored
839
+ replicaHeads, // local heads after authoring; enables incremental catch-up in the response
840
+ changes: changes.map((change) => new Uint8Array(change)),
841
+ });
842
+
843
+ // result.document — updated MindooDBAppDocument snapshot (includes merged data + heads)
844
+ // result.heads — new Automerge heads after the merge
845
+ // result.changesSince — incremental bytes to reconcile the local replica (when replicaHeads was sent)
846
+ if (result.changesSince?.changes.length) {
847
+ [localDoc] = Automerge.applyChanges(
848
+ localDoc,
849
+ result.changesSince.changes.map((change) => new Uint8Array(change)),
850
+ );
851
+ }
852
+ baseHeads = [...result.heads];
853
+ ```
854
+
855
+ Patch and result types:
856
+
857
+ ```ts
858
+ interface MindooDBAppAutomergeChangesPatch {
859
+ baseHeads?: string[]; // correlation only; merge is not rejected when Haven moved on
860
+ replicaHeads?: string[]; // local heads after authoring `changes`; enables `changesSince` in the response
861
+ changes: Uint8Array[]; // from Automerge.getChangesSince (or equivalent)
862
+ }
863
+
864
+ interface MindooDBAppAutomergeChangesSince {
865
+ sinceHeads: string[]; // echo of `replicaHeads`
866
+ changes: Uint8Array[]; // apply locally with Automerge.applyChanges
867
+ }
868
+
869
+ interface MindooDBAppAutomergePatchResult {
870
+ document: MindooDBAppDocument;
871
+ heads: string[];
872
+ changesSince?: MindooDBAppAutomergeChangesSince;
873
+ }
874
+ ```
875
+
876
+ Haven applies `changes` with `Automerge.applyChanges` against its **current** document, so concurrent edits from other clients are merged by Automerge's CRDT logic rather than rejected. `baseHeads` is optional logging metadata — it does not gate the merge.
877
+
878
+ When you include `replicaHeads` (the local replica's heads after authoring your batch), the response includes `changesSince`: the incremental change bytes your replica still needs after the merge. Apply them locally with `Automerge.applyChanges` and set `baseHeads = result.heads`. When there was no concurrent edit, `changesSince.changes` is empty and your local doc is already up to date — no full snapshot download needed. Omit `replicaHeads` only if you plan to reload via `getAutomergeSnapshot` instead.
879
+
880
+ #### When to use binary sync vs JSON patches
881
+
882
+ | Approach | App sends | When to use |
883
+ | -------- | --------- | ----------- |
884
+ | `documents.update({ set })` / `{ unset }` | Field paths | **Default** for structured document data |
885
+ | `documents.update({ richText })` | JSON span snapshots | **Default** for rich-text fields |
886
+ | `documents.update({ text })` | Text splices | Plain markdown / string fields |
887
+ | `documents.update({ json })` | JSON Patch (RFC 6902) | Arbitrary nested JSON paths |
888
+ | `applyAutomergeChanges` | Binary change bytes | **Last resort only** — TeamEdit Word/.docx with a local Automerge replica |
889
+
890
+ Requires the `update` capability (same as other write operations). For historical reads, prefer `getRichText(..., { revisionId })`; only use `getAutomergeSnapshot(..., { revisionId })` when you truly need the full binary at a revision.
891
+
892
+ #### Reference implementation (not a template)
893
+
894
+ TeamEdit's Word/.docx mode is why these RPCs exist — see `wordAutomergeHandle.ts` in [`mindoodb-app-teamedit`](https://github.com/klehmann/mindoodb-app-teamedit). Treat it as a reference for an unsupported edge case, not as a pattern to copy into new apps.
895
+
896
+ ### Queries & Live Queries
897
+
898
+ `documents.query()` runs ad-hoc queries against the database's **document summary buffer** on the host: declarative filtering, dynamic sorting, paging, and field projection — without materializing documents. This is the fastest way to find and order documents from an app and the right default for lists, search results, and dashboards.
899
+
900
+ ```ts
901
+ import { createViewLanguage } from "mindoodb-app-sdk";
902
+
903
+ const v = createViewLanguage<{ type: string; total: number }>();
904
+
905
+ const result = await db.documents.query({
906
+ filter: v.and(
907
+ v.eq(v.field("type"), v.string("invoice")),
908
+ v.gt(v.toNumber(v.field("total")), v.number(100)),
909
+ ),
910
+ sortBy: [{ field: "total", direction: "descending" }],
911
+ limit: 50,
912
+ offset: 0,
913
+ fields: ["total", "customer"],
914
+ });
915
+
916
+ console.log(result.total); // matching docs before paging
917
+ console.log(result.coverage); // "full" | "rebuilding" | "full-scan"
918
+ for (const row of result.rows) {
919
+ console.log(row.docId, row.fields, row.lastModified);
920
+ }
921
+ ```
922
+
923
+ The `filter` also accepts formula source text, which the SDK parses locally before it crosses the bridge (only the JSON expression AST travels):
924
+
925
+ ```ts
926
+ const invoices = await db.documents.query({
927
+ filter: 'v.eq(v.field("type"), "invoice")',
928
+ });
929
+ ```
930
+
931
+ Notes:
932
+
933
+ - The host caps `limit` (default 200, maximum 1000) so result payloads stay bounded. Page with `offset` for more rows.
934
+ - `coverage: "rebuilding"` means the summary buffer is still backfilling — results may be incomplete and will improve on their own.
935
+ - Queries that reference encrypted fields or fields outside the summary configuration are rejected with a `query-not-supported` bridge error. Use a view with `useFullDocuments` (see below) or `documents.list()` for those cases.
936
+ - Queries require the `read` capability and are not available in time-travel launches (the summary buffer reflects the live state only).
937
+
938
+ **Full-text search** — a query can additionally carry a `text` clause, evaluated against the database's client-side full-text index on the host and combined with `filter` as a logical AND:
939
+
940
+ ```ts
941
+ const result = await db.documents.query({
942
+ text: { query: "solar panels" }, // full-text match + relevance score
943
+ filter: 'v.eq(v.field("type"), "article")', // optional structured filter
944
+ });
945
+
946
+ for (const row of result.rows) {
947
+ console.log(row.docId, row.textScore); // higher score = better match
948
+ }
949
+ ```
950
+
951
+ - Options: `fields` (restrict matching to specific index fields), `prefix` (default `true` — `"sol"` matches `"solar"`), `fuzzy` (edit-distance tolerance, default off), `combineWith` (`"AND"` default | `"OR"`).
952
+ - Rows gain a `textScore`; without an explicit `sortBy`, results come best score first. Sorting can reference the score explicitly with `sortBy: [{ special: "textScore", direction: "descending" }]`.
953
+ - Full-text indexing is **opt-in per database** (`fulltextSetup` in the `dbsetup` document). A `text` clause against a database without it is rejected with a `fulltext-not-enabled` bridge error — catch it and hide/disable the search UI.
954
+ - `liveQuery` supports `text` clauses unchanged; the host rounds scores in its result fingerprint so minor relevance drift does not spam your callback.
955
+
956
+ **Enabling full-text indexing** — apps turn indexing on themselves through the database handle. The configuration is stored in the synced `dbsetup` document, so it applies on every replica (each device builds its own local, encrypted index):
957
+
958
+ ```ts
959
+ // Idempotent — safe to call on every app start (no write when unchanged).
960
+ // Requires the `update` capability.
961
+ await db.setFulltextSetup({
962
+ enabled: true,
963
+ attachments: true, // index text extracted from attachments (PDF, Office, …)
964
+ language: "de", // BCP-47 tag steering tokenization; omit for language-neutral
965
+ });
966
+
967
+ const setup = await db.getFulltextSetup(); // null when never configured
968
+ ```
969
+
970
+ - Without `include`, every non-underscore top-level field with extractable text is indexed (auto mode). Pass `include: ["subject", "body"]` to pin the indexed fields, including nested/long-text fields outside the summary buffer.
971
+ - `attachments: true` indexes attachment text under the synthetic `_attachments` field using the host's extractors (Haven ships PDF, Office, and plain-text extractors). Restrict a query to attachment content with `text: { query, fields: ["_attachments"] }`.
972
+ - Changing the configuration (including `language`) rebuilds the index in the background; queries during the rebuild report `coverage: "rebuilding"`.
973
+
974
+ **Live queries** keep a query result up to date. The callback receives the initial result and runs again whenever the result actually changed — the host coalesces bursts of writes and fingerprints results, so you only hear about real changes:
975
+
976
+ ```ts
977
+ const subscription = await db.documents.liveQuery(
978
+ { filter: 'v.eq(v.field("status"), "open")', limit: 100 },
979
+ (result) => {
980
+ renderList(result.rows);
981
+ },
982
+ );
983
+
984
+ // Force a re-evaluation (delivers a result even when nothing changed):
985
+ await subscription.refresh();
986
+
987
+ // Always dispose when the consuming UI goes away:
988
+ await subscription.dispose();
989
+ ```
990
+
991
+ Live query results are pushed from the host over the bridge port — no polling. Subscriptions are cleaned up automatically when the app disconnects, but dispose them explicitly when the UI that consumes them unmounts.
776
992
 
777
993
  ### Incremental sync
778
994
 
779
- The changefeed-backed `documents.list()` API is also the right primitive for app-side indexes, search, and other derived caches.
995
+ The changefeed-backed `documents.list()` API is also the right primitive for app-side indexes, search, and other derived caches. For plain "show me matching documents" use cases, prefer [`documents.query()`](#queries--live-queries); the changefeed shines when you need deletions and a durable resume checkpoint for derived state.
780
996
 
781
997
  **Initial scan from the beginning:**
782
998
 
@@ -1106,13 +1322,28 @@ await navigator.setExpansionState(expansion);
1106
1322
  await navigator.setSelectionState(selection);
1107
1323
  ```
1108
1324
 
1325
+ **Live updates** -- the host keeps the view bound to its source databases' change feeds. Subscribe with `onDidUpdate` and re-read the visible page when it fires; there is no need to poll or call `refresh()`:
1326
+
1327
+ ```ts
1328
+ const unsubscribe = navigator.onDidUpdate(({ addedCount, removedCount }) => {
1329
+ console.log(`view changed: +${addedCount} / -${removedCount}`);
1330
+ void rerenderVisiblePage();
1331
+ });
1332
+
1333
+ // later, when the consuming UI unmounts:
1334
+ unsubscribe();
1335
+ ```
1336
+
1109
1337
  The expression language is fully declarative: apps define filter and column logic through a typed builder API that compiles to JSON-safe expression objects. No app-provided JavaScript runs inside the bridge host.
1110
1338
 
1111
1339
  #### Performance notes
1112
1340
 
1341
+ - Navigator views are built **summary-first**: whenever the view definition (filter + column expressions) can be answered from the database's document summary buffer, the host feeds the view from the summary without materializing a single document. Views fall back to the (slower) document path automatically when they need decrypted fields, JS value functions, or fields outside the summary configuration.
1342
+ - Pass `useFullDocuments: true` in `createViewNavigator` input to force the document path explicitly — the escape hatch for views that deliberately need full document data.
1113
1343
  - `goto*()` and `getCurrentEntry()` are lightweight stateful cursor operations. They may still produce many bridge round-trips if you call them for every rendered row.
1114
1344
  - `entriesForward()` and `entriesBackward()` exist specifically to reduce those round-trips for UI rendering.
1115
- - `refresh()` rebuilds or invalidates the host-side navigator state and should be treated as an expensive operation compared to simple cursor movement.
1345
+ - `refresh()` performs an incremental catch-up on the host-side view (no pipeline rebuild). With `onDidUpdate` in place you rarely need it — the host pushes updates on its own.
1346
+ - `session.listDocumentsSinceViewCursor(cursor)` remains available as a **legacy polling/delta-sync path**; prefer `onDidUpdate` pushes and `documents.liveQuery()` for keeping UI current.
1116
1347
  - Child/key/range helpers operate on the same host-side navigator session. They do not create a separate query engine.
1117
1348
 
1118
1349
  Available expression helpers:
@@ -1124,7 +1355,7 @@ Available expression helpers:
1124
1355
  - **String helpers:** `concat()`, `lower()`, `upper()`, `trim()`, `contains()`, `startsWith()`, `endsWith()`
1125
1356
  - **Null/existence:** `coalesce()`, `exists()`, `notExists()`
1126
1357
  - **Date/path:** `datePart()`, `pathJoin()`
1127
- - **Control flow:** `ifElse()`, `let()`
1358
+ - **Control flow:** `ifElse()`, `let()` — `ifElse()` accepts multiple condition/value pairs followed by a default, like Domino's `@If`: `v.ifElse(cond1, val1, cond2, val2, default)`
1128
1359
 
1129
1360
  The full language guide lives in the `mindoodb-view-language` package. The SDK re-exports `createViewLanguage()` for convenience.
1130
1361
 
@@ -1195,19 +1426,25 @@ Connect options: `launchId?`, `targetOrigin?`, `connectTimeoutMs?`.
1195
1426
 
1196
1427
  ### MindooDBAppDatabase
1197
1428
 
1198
- | Property / Method | Returns |
1199
- | ----------------- | ---------------------------------- |
1200
- | `info()` | `Promise<MindooDBAppDatabaseInfo>` |
1201
- | `documents` | `MindooDBAppDocumentApi` |
1202
- | `attachments` | `MindooDBAppAttachmentApi` |
1429
+ | Property / Method | Returns |
1430
+ | ---------------------------- | ---------------------------------------------- |
1431
+ | `info()` | `Promise<MindooDBAppDatabaseInfo>` |
1432
+ | `documents` | `MindooDBAppDocumentApi` |
1433
+ | `attachments` | `MindooDBAppAttachmentApi` |
1434
+ | `getFulltextSetup()` | `Promise<MindooDBAppFulltextSetup \| null>` |
1435
+ | `setFulltextSetup(config)` | `Promise<void>` |
1203
1436
 
1204
1437
  ### MindooDBAppDocumentApi
1205
1438
 
1206
1439
  | Method | Returns |
1207
1440
  | ---------------------------------- | -------------------------------------------- |
1441
+ | `query(query?)` | `Promise<MindooDBAppQueryResult>` |
1442
+ | `liveQuery(query, onResult)` | `Promise<MindooDBAppLiveQuerySubscription>` |
1208
1443
  | `list(query?)` | `Promise<MindooDBAppDocumentListResult>` |
1209
1444
  | `get(docId)` | `Promise<MindooDBAppDocument \| null>` |
1210
1445
  | `getRichText(docId, path, opts?)` | `Promise<MindooDBAppRichTextSnapshot>` |
1446
+ | `getAutomergeSnapshot(docId, opts?)` | `Promise<MindooDBAppAutomergeSnapshot>` |
1447
+ | `applyAutomergeChanges(docId, patch)` | `Promise<MindooDBAppAutomergePatchResult>` |
1211
1448
  | `create(input)` | `Promise<MindooDBAppDocument>` |
1212
1449
  | `update(docId, patch)` | `Promise<MindooDBAppDocument>` |
1213
1450
  | `delete(docId)` | `Promise<{ ok: true }>` |
@@ -1216,6 +1453,8 @@ Connect options: `launchId?`, `targetOrigin?`, `connectTimeoutMs?`.
1216
1453
  | `getAtTimestamp(docId, timestamp)` | `Promise<MindooDBAppHistoricalDocument>` |
1217
1454
  | `getAtRevision(docId, revisionId)` | `Promise<MindooDBAppHistoricalDocument>` |
1218
1455
 
1456
+ `getAutomergeSnapshot` and `applyAutomergeChanges` are **discouraged** escape-hatch APIs for binary Automerge sync. Prefer `set`/`unset` and JSON patches (`text`, `richText`, `json`) instead. See [Automerge binary sync (advanced)](#automerge-binary-sync-advanced).
1457
+
1219
1458
  `list(query?)` accepts the changefeed query options documented above. The `cursor` value is an opaque checkpoint string managed by Haven and should be stored and passed back unchanged.
1220
1459
 
1221
1460
  For `create(input)`, use `MindooDBAppCreateDocumentInput` with shape `{ set: Record<string, unknown>; decryptionKeyId?: string; id?: string }`. The optional `id` lets the app pick the document id instead of letting MindooDB generate a UUID7; when provided it must match `/^[A-Za-z][A-Za-z0-9_]*$/` and existing live documents with that id are returned as-is (idempotent create-if-missing). If an existing custom-id document is deleted, `create({ id })` undeletes it and preserves the previous document body. Documents created with the same custom id on different replicas converge correctly when synced.
@@ -1255,7 +1494,9 @@ interface MindooDBAppUpdateDocumentInput {
1255
1494
  | Method | Returns |
1256
1495
  | ---------------------------------------------------------- | ------------------------------------------------- |
1257
1496
  | `getDefinition()` | `Promise<MindooDBAppViewDefinition>` |
1258
- | `refresh()` | `Promise<void>` |
1497
+ | `refresh()` | `Promise<string \| null>` (new view cursor) |
1498
+ | `onDidUpdate(listener)` | `() => void` (unsubscribe) |
1499
+ | `getViewCursor()` | `Promise<string \| null>` |
1259
1500
  | `getCurrentEntry()` | `Promise<MindooDBAppViewEntry \| null>` |
1260
1501
  | `gotoFirst()` / `gotoLast()` | `Promise<boolean>` |
1261
1502
  | `gotoNext()` / `gotoPrev()` | `Promise<boolean>` |
package/TESTING.md CHANGED
@@ -12,6 +12,15 @@ import {
12
12
  } from "mindoodb-app-sdk/testing";
13
13
  ```
14
14
 
15
+ The testing entrypoint emulates document storage with real Automerge documents,
16
+ so `@automerge/automerge` is declared as an optional peer dependency. Add it to
17
+ your app's `devDependencies` when you import `mindoodb-app-sdk/testing`
18
+ (the runtime SDK entrypoint does not need it):
19
+
20
+ ```bash
21
+ pnpm add -D @automerge/automerge
22
+ ```
23
+
15
24
  ## Pick a level
16
25
 
17
26
  ### Level 1: simple app tests
@@ -93,6 +102,30 @@ Each database entry can provide:
93
102
  - `methods.documents`
94
103
  - `methods.views` for session-level `createView()` and `openView()` calls
95
104
  - `methods.attachments`
105
+ - `fulltextSetup` — the initial config returned by `db.getFulltextSetup()`; `db.setFulltextSetup()` overwrites it for the lifetime of the handle. Use this to test your app's full-text bootstrap logic. Note the mock evaluates `text` query clauses regardless of this config.
106
+
107
+ The default in-memory document store also implements `documents.query()` and `documents.liveQuery()`: expression and formula-string filters are evaluated against the seeded documents, `sortBy`/`limit`/`offset` work as documented, and live query callbacks fire automatically after `create`/`update`/`delete`/`undelete` mutations. Full-text `text` clauses are supported with a deterministic mock implementation (token matching with prefix/AND semantics and an occurrence-count `textScore`) — assert on membership and relative ordering, not absolute scores, since real hosts use a BM25-style engine. That means app code built on queries and live queries is testable at Level 1 without any extra setup:
108
+
109
+ ```ts
110
+ const session = await mock.bridge.connect();
111
+ const db = await session.openDatabase("main");
112
+
113
+ await db.documents.create({ set: { type: "invoice", total: 120 } });
114
+
115
+ const result = await db.documents.query({
116
+ filter: 'v.eq(v.field("type"), "invoice")',
117
+ sortBy: [{ field: "total", direction: "descending" }],
118
+ });
119
+ expect(result.rows).toHaveLength(1);
120
+
121
+ const updates: number[] = [];
122
+ const sub = await db.documents.liveQuery(
123
+ { filter: 'v.eq(v.field("type"), "invoice")' },
124
+ (r) => updates.push(r.total),
125
+ );
126
+ await db.documents.create({ set: { type: "invoice", total: 50 } });
127
+ await sub.dispose();
128
+ ```
96
129
 
97
130
  ## Level 2 example
98
131
 
@@ -161,10 +194,14 @@ Useful Level 2 methods:
161
194
  - `dispose()`
162
195
  - `emitThemeChange()`
163
196
  - `emitViewportChange()`
197
+ - `emitQueryResult()` — push a `query-result` message to live query subscribers
198
+ - `emitViewChanged()` — push a `view-changed` message to navigator `onDidUpdate` listeners
164
199
  - `setRequestHandler()`
165
200
  - `clearRequestHandler()`
166
201
  - `postPortMessage()`
167
202
 
203
+ The built-in request handling also covers `documents.query` and the `documents.liveQuery.*` RPCs against the seeded documents, so `db.documents.query()` / `db.documents.liveQuery()` and `navigator.onDidUpdate()` work end-to-end over the real bridge transport in Level 2 tests.
204
+
168
205
  ## When to use which level
169
206
 
170
207
  Choose Level 1 when:
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
- import { M as MindooDBAppAttachmentPreviewMode, a as MindooDBAppBridge, b as MindooDBAppDatabase, c as MindooDBAppDocument, d as MindooDBAppRichTextSpan, e as MindooDBAppRichTextSnapshot } from './types-nFt-aryL.js';
2
- export { f as MindooDBAppAttachmentApi, g as MindooDBAppAttachmentChunk, h as MindooDBAppAttachmentInfo, i as MindooDBAppAttachmentPreviewOptions, j as MindooDBAppAttachmentPreviewSession, k as MindooDBAppBridgeConnectMessage, l as MindooDBAppBridgeConnectOptions, m as MindooDBAppBridgeConnectedMessage, n as MindooDBAppBridgeErrorPayload, o as MindooDBAppBridgeHandshakeErrorMessage, p as MindooDBAppBridgePortMessage, q as MindooDBAppBridgeRpcError, r as MindooDBAppBridgeRpcMessage, s as MindooDBAppBridgeRpcRequest, t as MindooDBAppBridgeRpcSuccess, u as MindooDBAppBridgeStreamAbortRequest, v as MindooDBAppBridgeStreamAck, w as MindooDBAppBridgeStreamCloseRequest, x as MindooDBAppBridgeStreamError, y as MindooDBAppBridgeStreamMessage, z as MindooDBAppBridgeStreamOpenResult, A as MindooDBAppBridgeStreamReadRequest, B as MindooDBAppBridgeStreamWriteRequest, C as MindooDBAppBridgeThemeChangedMessage, D as MindooDBAppBridgeUiPreferencesChangedMessage, E as MindooDBAppBridgeViewportChangedMessage, F as MindooDBAppCapability, G as MindooDBAppConfiguredViewCategorizationStyle, H as MindooDBAppConfiguredViewColumn, I as MindooDBAppConfiguredViewColumnRole, J as MindooDBAppConfiguredViewExpressionMode, K as MindooDBAppConfiguredViewFieldExpression, L as MindooDBAppConfiguredViewFilterDefinition, N as MindooDBAppConfiguredViewFilterMode, O as MindooDBAppConfiguredViewFilterRule, P as MindooDBAppConfiguredViewFormulaExpression, Q as MindooDBAppConfiguredViewFormulaFilterDefinition, R as MindooDBAppConfiguredViewPreviewMode, S as MindooDBAppConfiguredViewRuleFilterDefinition, T as MindooDBAppConfiguredViewRuleMatchMode, U as MindooDBAppConfiguredViewRuleOperator, V as MindooDBAppConfiguredViewValueExpression, W as MindooDBAppCreateDocumentInput, X as MindooDBAppCreateViewInput, Y as MindooDBAppCreateViewNavigatorInput, Z as MindooDBAppDatabaseInfo, _ as MindooDBAppDocumentApi, $ as MindooDBAppDocumentHeadCursorResult, a0 as MindooDBAppDocumentHistoryEntry, a1 as MindooDBAppDocumentListQuery, a2 as MindooDBAppDocumentListResult, a3 as MindooDBAppDocumentRevisionId, a4 as MindooDBAppDocumentScanPreset, a5 as MindooDBAppDocumentSummary, a6 as MindooDBAppHistoricalDocument, a7 as MindooDBAppHistoryQuery, a8 as MindooDBAppHostTheme, a9 as MindooDBAppJsonListDeletePatch, aa as MindooDBAppJsonListInsertPatch, ab as MindooDBAppJsonPatch, ac as MindooDBAppJsonSetPatch, ad as MindooDBAppJsonUnsetPatch, ae as MindooDBAppLaunchContext, af as MindooDBAppMenuAnchor, ag as MindooDBAppMenuApi, ah as MindooDBAppMenuCommandItem, ai as MindooDBAppMenuDismissReason, aj as MindooDBAppMenuDismissedResult, ak as MindooDBAppMenuItem, al as MindooDBAppMenuKind, am as MindooDBAppMenuPlacement, an as MindooDBAppMenuPointAnchor, ao as MindooDBAppMenuRect, ap as MindooDBAppMenuRectAnchor, aq as MindooDBAppMenuSelectionResult, ar as MindooDBAppMenuSeparatorItem, as as MindooDBAppReadableAttachmentStream, at as MindooDBAppResolvedViewDefinition, au as MindooDBAppResolvedViewSource, av as MindooDBAppRichTextBlockSpan, aw as MindooDBAppRichTextGetOptions, ax as MindooDBAppRichTextImmutableString, ay as MindooDBAppRichTextMaterializeValue, az as MindooDBAppRichTextPatch, aA as MindooDBAppRichTextScalar, aB as MindooDBAppRichTextTextSpan, aC as MindooDBAppRuntime, aD as MindooDBAppScanAttachmentOptions, aE as MindooDBAppScanAttachmentResult, aF as MindooDBAppScopedDocId, aG as MindooDBAppScopedDocumentSummary, aH as MindooDBAppSession, aI as MindooDBAppShowMenuInput, aJ as MindooDBAppShowMenuResult, aK as MindooDBAppTextEdit, aL as MindooDBAppTextPatch, aM as MindooDBAppThemeMode, aN as MindooDBAppUiPreferences, aO as MindooDBAppUpdateDocumentInput, aP as MindooDBAppViewCursorDocumentListResult, aQ as MindooDBAppViewCursorResult, aR as MindooDBAppViewEntry, aS as MindooDBAppViewEntryKind, aT as MindooDBAppViewNavigator, aU as MindooDBAppViewNavigatorExpansionState, aV as MindooDBAppViewNavigatorOpenOptions, aW as MindooDBAppViewNavigatorPageOptions, aX as MindooDBAppViewNavigatorPageResult, aY as MindooDBAppViewNavigatorRangeQuery, aZ as MindooDBAppViewNavigatorSelectionState, a_ as MindooDBAppViewport, a$ as MindooDBAppWritableAttachmentStream } from './types-nFt-aryL.js';
1
+ import { M as MindooDBAppAttachmentPreviewMode, a as MindooDBAppBridge, b as MindooDBAppDatabase, c as MindooDBAppDocument, d as MindooDBAppRichTextSpan, e as MindooDBAppRichTextSnapshot } from './types-B6vEy1VC.js';
2
+ export { f as MindooDBAppAccessDecision, g as MindooDBAppAccessTier, h as MindooDBAppAttachmentApi, i as MindooDBAppAttachmentChunk, j as MindooDBAppAttachmentInfo, k as MindooDBAppAttachmentPreviewOptions, l as MindooDBAppAttachmentPreviewSession, m as MindooDBAppAutomergeChangesPatch, n as MindooDBAppAutomergeChangesSince, o as MindooDBAppAutomergeGetOptions, p as MindooDBAppAutomergePatchResult, q as MindooDBAppAutomergeSnapshot, r as MindooDBAppBridgeConnectMessage, s as MindooDBAppBridgeConnectOptions, t as MindooDBAppBridgeConnectedMessage, u as MindooDBAppBridgeErrorPayload, v as MindooDBAppBridgeHandshakeErrorMessage, w as MindooDBAppBridgeLocaleChangedMessage, x as MindooDBAppBridgePortMessage, y as MindooDBAppBridgeQueryResultMessage, z as MindooDBAppBridgeRpcError, A as MindooDBAppBridgeRpcMessage, B as MindooDBAppBridgeRpcRequest, C as MindooDBAppBridgeRpcSuccess, D as MindooDBAppBridgeStreamAbortRequest, E as MindooDBAppBridgeStreamAck, F as MindooDBAppBridgeStreamCloseRequest, G as MindooDBAppBridgeStreamError, H as MindooDBAppBridgeStreamMessage, I as MindooDBAppBridgeStreamOpenResult, J as MindooDBAppBridgeStreamReadRequest, K as MindooDBAppBridgeStreamWriteRequest, L as MindooDBAppBridgeThemeChangedMessage, N as MindooDBAppBridgeUiPreferencesChangedMessage, O as MindooDBAppBridgeViewChangedMessage, P as MindooDBAppBridgeViewportChangedMessage, Q as MindooDBAppCapability, R as MindooDBAppConfiguredViewCategorizationStyle, S as MindooDBAppConfiguredViewColumn, T as MindooDBAppConfiguredViewColumnRole, U as MindooDBAppConfiguredViewExpressionMode, V as MindooDBAppConfiguredViewFieldExpression, W as MindooDBAppConfiguredViewFilterDefinition, X as MindooDBAppConfiguredViewFilterMode, Y as MindooDBAppConfiguredViewFilterRule, Z as MindooDBAppConfiguredViewFormulaExpression, _ as MindooDBAppConfiguredViewFormulaFilterDefinition, $ as MindooDBAppConfiguredViewPreviewMode, a0 as MindooDBAppConfiguredViewRuleFilterDefinition, a1 as MindooDBAppConfiguredViewRuleMatchMode, a2 as MindooDBAppConfiguredViewRuleOperator, a3 as MindooDBAppConfiguredViewValueExpression, a4 as MindooDBAppCreateDocumentInput, a5 as MindooDBAppCreateViewInput, a6 as MindooDBAppCreateViewNavigatorInput, a7 as MindooDBAppDatabaseInfo, a8 as MindooDBAppDocumentApi, a9 as MindooDBAppDocumentHeadCursorResult, aa as MindooDBAppDocumentHistoryEntry, ab as MindooDBAppDocumentListQuery, ac as MindooDBAppDocumentListResult, ad as MindooDBAppDocumentQuery, ae as MindooDBAppDocumentRevisionId, af as MindooDBAppDocumentScanPreset, ag as MindooDBAppDocumentSummary, ah as MindooDBAppExtractionSetup, ai as MindooDBAppFulltextSetup, aj as MindooDBAppHistoricalDocument, ak as MindooDBAppHistoryQuery, al as MindooDBAppHostTheme, am as MindooDBAppJsonListDeletePatch, an as MindooDBAppJsonListInsertPatch, ao as MindooDBAppJsonPatch, ap as MindooDBAppJsonSetPatch, aq as MindooDBAppJsonTextMarkPatch, ar as MindooDBAppJsonTextSplicePatch, as as MindooDBAppJsonTextUnmarkPatch, at as MindooDBAppJsonUnsetPatch, au as MindooDBAppLaunchContext, av as MindooDBAppLiveQuerySubscription, aw as MindooDBAppMenuAnchor, ax as MindooDBAppMenuApi, ay as MindooDBAppMenuCommandItem, az as MindooDBAppMenuDismissReason, aA as MindooDBAppMenuDismissedResult, aB as MindooDBAppMenuItem, aC as MindooDBAppMenuKind, aD as MindooDBAppMenuPlacement, aE as MindooDBAppMenuPointAnchor, aF as MindooDBAppMenuRect, aG as MindooDBAppMenuRectAnchor, aH as MindooDBAppMenuSelectionResult, aI as MindooDBAppMenuSeparatorItem, aJ as MindooDBAppQueryCoverage, aK as MindooDBAppQueryResult, aL as MindooDBAppQueryRow, aM as MindooDBAppQuerySortKey, aN as MindooDBAppQueryTextClause, aO as MindooDBAppReadableAttachmentStream, aP as MindooDBAppResolvedViewDefinition, aQ as MindooDBAppResolvedViewSource, aR as MindooDBAppRichTextBlockSpan, aS as MindooDBAppRichTextGetOptions, aT as MindooDBAppRichTextImmutableString, aU as MindooDBAppRichTextMarkRange, aV as MindooDBAppRichTextMaterializeValue, aW as MindooDBAppRichTextPatch, aX as MindooDBAppRichTextScalar, aY as MindooDBAppRichTextSpliceStep, aZ as MindooDBAppRichTextStep, a_ as MindooDBAppRichTextStepPatch, a$ as MindooDBAppRichTextTextSpan, b0 as MindooDBAppRuntime, b1 as MindooDBAppScanAttachmentOptions, b2 as MindooDBAppScanAttachmentResult, b3 as MindooDBAppScopedDocId, b4 as MindooDBAppScopedDocumentSummary, b5 as MindooDBAppSession, b6 as MindooDBAppShowMenuInput, b7 as MindooDBAppShowMenuResult, b8 as MindooDBAppTextEdit, b9 as MindooDBAppTextPatch, ba as MindooDBAppThemeMode, bb as MindooDBAppUiPreferences, bc as MindooDBAppUpdateDocumentInput, bd as MindooDBAppViewCursorDocumentListResult, be as MindooDBAppViewCursorResult, bf as MindooDBAppViewEntry, bg as MindooDBAppViewEntryKind, bh as MindooDBAppViewNavigator, bi as MindooDBAppViewNavigatorExpansionState, bj as MindooDBAppViewNavigatorOpenOptions, bk as MindooDBAppViewNavigatorPageOptions, bl as MindooDBAppViewNavigatorPageResult, bm as MindooDBAppViewNavigatorRangeQuery, bn as MindooDBAppViewNavigatorSelectionState, bo as MindooDBAppViewUpdateStats, bp as MindooDBAppViewport, bq as MindooDBAppWritableAttachmentStream } from './types-B6vEy1VC.js';
3
+ import { MindooDBAppBooleanExpression } from 'mindoodb-view-language';
3
4
  export { MindooDBAppBooleanExpression, MindooDBAppExpression, MindooDBAppFilterDefinition, MindooDBAppViewCategoryChildrenPageRequest, MindooDBAppViewColumn, MindooDBAppViewColumnRole, MindooDBAppViewDefinition, MindooDBAppViewExpansionState, MindooDBAppViewExpressionBase, MindooDBAppViewExpressionDatePart, MindooDBAppViewExpressionOperation, MindooDBAppViewFieldExpression, MindooDBAppViewIfExpression, MindooDBAppViewLetExpression, MindooDBAppViewLiteralExpression, MindooDBAppViewLookupByPath, MindooDBAppViewOperationExpression, MindooDBAppViewOriginExpression, MindooDBAppViewPageRequest, MindooDBAppViewPageResult, MindooDBAppViewRow, MindooDBAppViewSortDirection, MindooDBAppViewTotalMode, MindooDBAppViewValueExpressionRef, MindooDBAppViewVariableExpression, createViewLanguage } from 'mindoodb-view-language';
5
+ import { MindooDB, MindooQuerySortKey, MindooQueryOptions, MindooQueryResult } from 'mindoodb';
4
6
 
5
7
  /**
6
8
  * Resolves whether Haven can render a built-in preview for an attachment.
@@ -53,13 +55,15 @@ type MindooDBRichTextHandleChangeListener = (snapshot: MindooDBAppRichTextSnapsh
53
55
  /**
54
56
  * Local rich-text span buffer for SDK apps.
55
57
  *
56
- * The helper deliberately mirrors the causal shape of `MindooDBTextBuffer`:
57
- * local editor code replaces the current span snapshot, then `flush()` sends a
58
- * separate `richText` patch with the document heads the snapshot was based on.
58
+ * Mirrors `MindooDBTextBuffer`: track edits against a base snapshot, then flush
59
+ * with causal `baseHeads`. Text edits prefer positional `richTextSteps` so
60
+ * concurrent saves merge like plain-text splices; formatting-only or structural
61
+ * changes fall back to full `richText` span snapshots.
59
62
  */
60
63
  declare class MindooDBRichTextHandle {
61
64
  private readonly options;
62
65
  private spansInternal;
66
+ private baseMaterializedText;
63
67
  private heads;
64
68
  private dirtyInternal;
65
69
  private listeners;
@@ -77,6 +81,7 @@ declare class MindooDBRichTextHandle {
77
81
  refresh(): Promise<MindooDBAppRichTextSnapshot>;
78
82
  pollOnce(): Promise<boolean>;
79
83
  flush(updateSpansConfig?: Record<string, unknown>): Promise<MindooDBRichTextHandleFlushResult>;
84
+ private flushTextSteps;
80
85
  private emitChange;
81
86
  }
82
87
  declare function createMindooDBRichTextHandle(options: CreateMindooDBRichTextHandleOptions): MindooDBRichTextHandle;
@@ -174,4 +179,11 @@ declare class MindooDBTextBuffer {
174
179
  /** Create a local text buffer for one string field in a document. */
175
180
  declare function createMindooDBTextBuffer(options: CreateMindooDBTextBufferOptions): MindooDBTextBuffer;
176
181
 
177
- export { type CreateMindooDBRichTextHandleOptions, type CreateMindooDBTextBufferOptions, MindooDBAppAttachmentPreviewMode, MindooDBAppBridge, MindooDBAppDatabase, MindooDBAppDocument, MindooDBAppRichTextSnapshot, MindooDBAppRichTextSpan, MindooDBRichTextHandle, type MindooDBRichTextHandleChangeListener, type MindooDBRichTextHandleFlushResult, MindooDBTextBuffer, type MindooDBTextBufferFlushResult, abbreviateCanonicalName, canPreviewAttachment, createMindooDBAppBridge, createMindooDBRichTextHandle, createMindooDBTextBuffer, expandAbbreviatedName };
182
+ /**
183
+ * Thin convenience wrapper around `db.query()`: accepts the filter either
184
+ * as a prebuilt expression (from `createViewLanguage()`) or as formula
185
+ * source text, which is parsed with the shared formula parser.
186
+ */
187
+ declare function queryDocuments(db: Pick<MindooDB, "query">, filter: MindooDBAppBooleanExpression | string, sortBy?: MindooQuerySortKey[], options?: MindooQueryOptions): Promise<MindooQueryResult>;
188
+
189
+ export { type CreateMindooDBRichTextHandleOptions, type CreateMindooDBTextBufferOptions, MindooDBAppAttachmentPreviewMode, MindooDBAppBridge, MindooDBAppDatabase, MindooDBAppDocument, MindooDBAppRichTextSnapshot, MindooDBAppRichTextSpan, MindooDBRichTextHandle, type MindooDBRichTextHandleChangeListener, type MindooDBRichTextHandleFlushResult, MindooDBTextBuffer, type MindooDBTextBufferFlushResult, abbreviateCanonicalName, canPreviewAttachment, createMindooDBAppBridge, createMindooDBRichTextHandle, createMindooDBTextBuffer, expandAbbreviatedName, queryDocuments };