mindoodb-app-sdk 0.0.64 → 0.0.65

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/TESTING.md CHANGED
@@ -108,6 +108,24 @@ Each database entry can provide:
108
108
  - `methods.attachments`
109
109
  - `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.
110
110
  - `summarySetup` — the initial config returned by `db.getSummarySetup()`; `db.setSummarySetup()` overwrites it for the lifetime of the handle. The mock's `query()` reads seeded documents directly and ignores this config; use it to test your app's summary bootstrap logic.
111
+ - `automerge: true` — back every document with a real Automerge document, as Haven does (off by default: plain JSON with `mock-head-N` heads). `create`/`update` then return real Automerge heads, `update` applies `set`/`unset`, `json` (with `Automerge.changeAt(json.baseHeads)` when given) and `text` patches as Automerge changes with Haven's rules (missing parents and lists are created, `unset` is idempotent, `counterIncrement` needs a counter, an empty `json` patch is rejected), and `get`/`list`/`query` return the plain projection (atomic strings as strings, counters as numbers, timestamps as ISO strings). `richText` snapshots replace the value at their path and `richTextSteps` are spliced into the text without marks. Seeds are built with a fixed actor, so two mocks from the same seed have identical heads. `mockDatabasesFromDefinition(definition, seed, { automerge: true })` and the test host URL setting `automerge=1` turn it on, too.
112
+
113
+ #### Concurrent edits from another device
114
+
115
+ `applyRemoteUpdate(databaseId, docId, input, { actor? })` on the controller (`createMockMindooDBAppSession`, `createFakeBridgeHost`, the browser test host and `window.__havenTestHost`) writes `input` as another device and syncs it in; live queries update as after any write. With `automerge: true` the device holds the history up to `input.json.baseHeads` (plus its own earlier changes) and writes as its own Automerge actor, so its edit truly merges with an app save at the same heads:
116
+
117
+ ```ts
118
+ const doc = await database.documents.get("sheet-1"); // heads H
119
+ await mock.applyRemoteUpdate("main", "sheet-1", { // device B, at H
120
+ json: { baseHeads: doc!.heads, listInsert: [{ path: ["rows"], index: 1, values: ["remote"] }] },
121
+ });
122
+ const saved = await database.documents.update("sheet-1", { // the app, also at H
123
+ json: { baseHeads: doc!.heads, listInsert: [{ path: ["rows"], index: 1, values: ["local"] }] },
124
+ });
125
+ // saved.data.rows keeps both entries; saved.heads has two heads
126
+ ```
127
+
128
+ The relative order of concurrent inserts at the same index follows Automerge's op ids, as in Haven. Without `automerge`, `applyRemoteUpdate` is a plain update.
111
129
 
112
130
  ### Evaluating VirtualViews
113
131
 
@@ -364,7 +382,7 @@ mountHavenTestHost({
364
382
  });
365
383
  ```
366
384
 
367
- The panel keeps its settings in the page URL, so a scenario can be linked or opened by Playwright directly: `/__haven-test/?db=main:read,attachments&db=archive:none&enforce=1&users=120&locale=de-DE&theme=dark` (`db=<id>:none` unmaps a database; URL settings win over the options). The same controls are scriptable as `window.__havenTestHost`:
385
+ The panel keeps its settings in the page URL, so a scenario can be linked or opened by Playwright directly: `/__haven-test/?db=main:read,attachments&db=archive:none&enforce=1&users=120&locale=de-DE&theme=dark` (`db=<id>:none` unmaps a database; `automerge=1` backs all databases with Automerge documents; URL settings win over the options). The same controls are scriptable as `window.__havenTestHost`:
368
386
 
369
387
  ```ts
370
388
  await page.goto("/__haven-test/?enforce=1");
@@ -376,6 +394,9 @@ await page.evaluate(() => window.__havenTestHost!.setLocale("de-DE"));
376
394
  await page.evaluate(() => window.__havenTestHost!.setNextScan({ fileName: "receipt.pdf", mimeType: "application/pdf", size: 1024 }));
377
395
  await app.getByRole("button", { name: "Scan" }).click();
378
396
  expect(await page.evaluate(() => window.__havenTestHost!.log.scans)).toHaveLength(1);
397
+
398
+ // with ?automerge=1: another device edits the document the app has open
399
+ await page.evaluate(() => window.__havenTestHost!.applyRemoteUpdate("main", "sheet-1", { set: { title: "Renamed elsewhere" } }));
379
400
  ```
380
401
 
381
402
  Agent tools are scriptable through `window.__havenTestHost.agent`, the same controller the Vitest helpers expose as `createFakeBridgeHost().agent`:
@@ -1,4 +1,4 @@
1
- import { ay as MindooDBAppComponentInfo, bq as MindooDBAppEmbedRect, I as MindooDBAppAgentToolDescriptor, f as MindooDBAppAgentToolErrorCode, V as MindooDBAppBridgeAgentResultMessage, U as MindooDBAppBridgeAgentInvokeMessage, aU as MindooDBAppCreateViewNavigatorInput, de as MindooDBAppViewNavigator, ad as MindooDBAppBridgeRpcRequest, bC as MindooDBAppHostTheme, av as MindooDBAppCapability, j as MindooDBAppDefinition, J as MindooDBAppAttachmentApi, e as MindooDBAppBridge, cQ as MindooDBAppSession, cg as MindooDBAppProposeAppInput, dm as MindooDBAppViewport, d8 as MindooDBAppUiPreferences, cm as MindooDBAppQueryResult, dl as MindooDBAppViewUpdateStats, a9 as MindooDBAppBridgePortMessage, aV as MindooDBAppDatabaseInfo, bl as MindooDBAppEmbedCloseReason, a4 as MindooDBAppBridgeErrorPayload, bX as MindooDBAppLaunchContext, b2 as MindooDBAppDocumentApi, dg as MindooDBAppViewNavigatorOpenOptions, bD as MindooDBAppIdentityApi, d4 as MindooDBAppTimestampApi, b0 as MindooDBAppDirectoryApi, l as MindooDBAppDocument, bz as MindooDBAppFulltextSetup, by as MindooDBAppExtractionSetup, cY as MindooDBAppSummarySetup, ch as MindooDBAppProposeAppResult, bN as MindooDBAppIncomingSource, c as MindooDBAppDragStartResult, bj as MindooDBAppDragProfile, bM as MindooDBAppIncomingResult } from '../types-BeRUxGKN.js';
1
+ import { ay as MindooDBAppComponentInfo, bq as MindooDBAppEmbedRect, I as MindooDBAppAgentToolDescriptor, f as MindooDBAppAgentToolErrorCode, V as MindooDBAppBridgeAgentResultMessage, U as MindooDBAppBridgeAgentInvokeMessage, aU as MindooDBAppCreateViewNavigatorInput, de as MindooDBAppViewNavigator, ad as MindooDBAppBridgeRpcRequest, bC as MindooDBAppHostTheme, av as MindooDBAppCapability, d9 as MindooDBAppUpdateDocumentInput, l as MindooDBAppDocument, j as MindooDBAppDefinition, J as MindooDBAppAttachmentApi, e as MindooDBAppBridge, cQ as MindooDBAppSession, cg as MindooDBAppProposeAppInput, dm as MindooDBAppViewport, d8 as MindooDBAppUiPreferences, cm as MindooDBAppQueryResult, dl as MindooDBAppViewUpdateStats, a9 as MindooDBAppBridgePortMessage, aV as MindooDBAppDatabaseInfo, bl as MindooDBAppEmbedCloseReason, a4 as MindooDBAppBridgeErrorPayload, bX as MindooDBAppLaunchContext, b2 as MindooDBAppDocumentApi, dg as MindooDBAppViewNavigatorOpenOptions, bD as MindooDBAppIdentityApi, d4 as MindooDBAppTimestampApi, b0 as MindooDBAppDirectoryApi, bz as MindooDBAppFulltextSetup, by as MindooDBAppExtractionSetup, cY as MindooDBAppSummarySetup, ch as MindooDBAppProposeAppResult, bN as MindooDBAppIncomingSource, c as MindooDBAppDragStartResult, bj as MindooDBAppDragProfile, bM as MindooDBAppIncomingResult } from '../types-BeRUxGKN.js';
2
2
  import * as mindoodb from 'mindoodb';
3
3
  import 'mindoodb-view-language';
4
4
 
@@ -244,6 +244,14 @@ interface BrowserTestHost {
244
244
  setEnforceCapabilities(enforce: boolean): void;
245
245
  /** Replaces the usernames of the tenant directory (`directory.listUsers()`). */
246
246
  setDirectoryUsers(users: readonly string[]): void;
247
+ /**
248
+ * Another device edits a document and the change syncs in: the app's live queries
249
+ * update. With Automerge-backed databases (`automerge=1` / `automerge: true`) the
250
+ * edit is made at `input.json.baseHeads` by another actor and merged, so it is
251
+ * concurrent to app saves at the same heads. See
252
+ * `MockMindooDBAppSessionController.applyRemoteUpdate`.
253
+ */
254
+ applyRemoteUpdate(databaseId: string, docId: string, input: MindooDBAppUpdateDocumentInput, options?: MockRemoteUpdateOptions): Promise<MindooDBAppDocument>;
247
255
  /** Reloads the app frame. The mock data survives; the app's in-memory state does not. */
248
256
  reloadApp(): void;
249
257
  dispose(): void;
@@ -271,6 +279,8 @@ interface MockDatabasesFromDefinitionOptions {
271
279
  * `"none"`: the mock's stateless default.
272
280
  */
273
281
  attachments?: "memory" | "none";
282
+ /** Back documents with real Automerge documents (`MockMindooDBAppDatabaseDefinition.automerge`). */
283
+ automerge?: boolean;
274
284
  }
275
285
  /**
276
286
  * Mock databases for every database a `haven-app.json` declares, granted as requested
@@ -284,10 +294,13 @@ declare function mockDatabasesFromDefinition(definition: Pick<MindooDBAppDefinit
284
294
  * - `enforce=1` turns the capability checks on
285
295
  * - `users=<count>` fills the directory with generated users
286
296
  * - `locale=<tag>`, `theme=dark|light`
297
+ * - `automerge=1` backs every database with real Automerge documents (concurrent
298
+ * `baseHeads` merges, real heads); `automerge=0` turns it off
287
299
  */
288
300
  interface TestHostUrlSettings {
289
301
  overrides: Record<string, MockDatabaseOverride>;
290
302
  enforceCapabilities?: boolean;
303
+ automerge?: boolean;
291
304
  directoryUserCount?: number;
292
305
  locale?: string;
293
306
  theme?: "dark" | "light";
@@ -305,7 +318,7 @@ interface MountHavenTestHostOptions extends Omit<CreateBrowserTestHostOptions, "
305
318
  label: string;
306
319
  }>;
307
320
  /**
308
- * Read `db=`, `enforce=`, `users=`, `locale=` and `theme=` from the page URL (see
321
+ * Read `db=`, `enforce=`, `users=`, `locale=`, `theme=` and `automerge=` from the page URL (see
309
322
  * {@link readTestHostUrlSettings}) and keep the URL in sync with the panel. Default
310
323
  * true. URL settings win over the options passed here.
311
324
  */
@@ -413,6 +426,22 @@ interface MockMindooDBAppDatabaseDefinition {
413
426
  * results the same way they would in Haven.
414
427
  */
415
428
  summarySetup?: MindooDBAppSummarySetup | null;
429
+ /**
430
+ * Back every document with a real Automerge document, like Haven: `update`
431
+ * applies `set`/`unset`, `json` (at `json.baseHeads`) and `text` patches as
432
+ * Automerge changes, heads are real Automerge heads, and `applyRemoteUpdate`
433
+ * on the controller writes as another device so concurrent edits truly
434
+ * merge. Off by default (plain JSON with `mock-head-N` heads).
435
+ */
436
+ automerge?: boolean;
437
+ }
438
+ /** Options of `applyRemoteUpdate`. */
439
+ interface MockRemoteUpdateOptions {
440
+ /**
441
+ * Automerge actor id (hex) of the simulated device. Defaults to one fixed
442
+ * "remote" actor; pass different ids to simulate several devices.
443
+ */
444
+ actor?: string;
416
445
  }
417
446
  /**
418
447
  * Stands in for the host-owned sealed channel.
@@ -497,6 +526,15 @@ interface MockMindooDBAppSessionController {
497
526
  setEnforceCapabilities(enforce: boolean): void;
498
527
  /** Replaces the directory's usernames (see `directoryUsers`). */
499
528
  setDirectoryUsers(users: readonly string[]): void;
529
+ /**
530
+ * Writes `input` to a document as another device would, then syncs it in:
531
+ * live queries update like after any write. With `automerge: true` on the
532
+ * database the edit is made by a different Automerge actor on the history
533
+ * up to `input.json.baseHeads` (the current state when omitted) and merged,
534
+ * so it is truly concurrent to app writes at the same heads. Without it, it
535
+ * is a plain update. Bypasses capability checks (it is not the app writing).
536
+ */
537
+ applyRemoteUpdate(databaseId: string, docId: string, input: MindooDBAppUpdateDocumentInput, options?: MockRemoteUpdateOptions): Promise<MindooDBAppDocument>;
500
538
  emitThemeChange(theme: MindooDBAppHostTheme): void;
501
539
  emitViewportChange(viewport: MindooDBAppViewport): void;
502
540
  emitUiPreferencesChange(uiPreferences: MindooDBAppUiPreferences): void;
@@ -622,6 +660,8 @@ interface FakeBridgeHostController {
622
660
  /** See {@link CreateMockMindooDBAppSessionOptions.directoryUsers}. */
623
661
  setDirectoryUsers(users: readonly string[]): void;
624
662
  getDirectoryUsers(): string[];
663
+ /** See {@link MockMindooDBAppSessionController.applyRemoteUpdate}. */
664
+ applyRemoteUpdate(databaseId: string, docId: string, input: MindooDBAppUpdateDocumentInput, options?: MockRemoteUpdateOptions): Promise<MindooDBAppDocument>;
625
665
  /** Embeds the app has open (`session.embeds.open`). */
626
666
  listEmbeds(): MockEmbedState[];
627
667
  /** Closes an embed and pushes the `embed-event` to the app. */
@@ -647,4 +687,4 @@ interface FakeBridgeHostController {
647
687
  }
648
688
  declare function createFakeBridgeHost(options?: CreateFakeBridgeHostOptions): FakeBridgeHostController;
649
689
 
650
- export { ALL_CAPABILITIES, type BrowserTestHost, type BrowserTestHostLog, type BrowserTestHostScan, type BrowserTestHostScanMode, type CreateBrowserTestHostOptions, type CreateFakeBridgeHostOptions, type CreateMockMindooDBAppSessionOptions, type EvaluatingViewDocument, type FakeBridgeHostController, type FakeBridgePortMessageHandler, type FakeBridgeRequestHandler, type FakeBridgeRpcResponse, type MockAgentCall, type MockAgentCallResult, type MockAgentConsent, type MockAgentFile, type MockAgentHost, type MockAgentHostController, type MockAgentHostOptions, type MockDatabaseOverride, type MockDatabasesFromDefinitionOptions, type MockEmbedState, type MockEmbeddingFinish, MockForbiddenError, type MockIncomingContentInput, type MockMindooDBAppDatabaseDefinition, type MockMindooDBAppSessionController, type MockSealedChannelOptions, type MockSeedDocument, type MountHavenTestHostOptions, TEST_HOST_LOCALES, type TestHostUrlSettings, capabilitiesForDefinitionPermissions, createBrowserTestHost, createEvaluatingViewNavigator, createFakeBridgeHost, createMemoryAttachments, createMockAgentHost, createMockMindooDBAppBridge, createMockMindooDBAppSession, generateDirectoryUsers, loadMindoodbForTesting, mockAgentToolPrefix, mockDatabasesFromDefinition, mountHavenTestHost, previewJson, readTestHostUrlSettings, validateMockAgentTools };
690
+ export { ALL_CAPABILITIES, type BrowserTestHost, type BrowserTestHostLog, type BrowserTestHostScan, type BrowserTestHostScanMode, type CreateBrowserTestHostOptions, type CreateFakeBridgeHostOptions, type CreateMockMindooDBAppSessionOptions, type EvaluatingViewDocument, type FakeBridgeHostController, type FakeBridgePortMessageHandler, type FakeBridgeRequestHandler, type FakeBridgeRpcResponse, type MockAgentCall, type MockAgentCallResult, type MockAgentConsent, type MockAgentFile, type MockAgentHost, type MockAgentHostController, type MockAgentHostOptions, type MockDatabaseOverride, type MockDatabasesFromDefinitionOptions, type MockEmbedState, type MockEmbeddingFinish, MockForbiddenError, type MockIncomingContentInput, type MockMindooDBAppDatabaseDefinition, type MockMindooDBAppSessionController, type MockRemoteUpdateOptions, type MockSealedChannelOptions, type MockSeedDocument, type MountHavenTestHostOptions, TEST_HOST_LOCALES, type TestHostUrlSettings, capabilitiesForDefinitionPermissions, createBrowserTestHost, createEvaluatingViewNavigator, createFakeBridgeHost, createMemoryAttachments, createMockAgentHost, createMockMindooDBAppBridge, createMockMindooDBAppSession, generateDirectoryUsers, loadMindoodbForTesting, mockAgentToolPrefix, mockDatabasesFromDefinition, mountHavenTestHost, previewJson, readTestHostUrlSettings, validateMockAgentTools };
@@ -173,24 +173,24 @@ async function buildVirtualView(mindoodb, definition, documents, categorizationS
173
173
  break;
174
174
  }
175
175
  }
176
- const view = await builder.build();
176
+ const view2 = await builder.build();
177
177
  const changes = /* @__PURE__ */ new Map();
178
178
  for (const document2 of documents) {
179
179
  if (!matchesFilter(mindoodb, definition, document2)) {
180
180
  continue;
181
181
  }
182
- const change = changes.get(document2.origin) ?? new mindoodb.VirtualViewDataChange(document2.origin);
183
- change.addEntry(document2.docId, computeColumnValues(definition, document2));
184
- changes.set(document2.origin, change);
182
+ const change2 = changes.get(document2.origin) ?? new mindoodb.VirtualViewDataChange(document2.origin);
183
+ change2.addEntry(document2.docId, computeColumnValues(definition, document2));
184
+ changes.set(document2.origin, change2);
185
185
  }
186
- for (const change of changes.values()) {
187
- if (change.hasChanges()) view.applyChanges(change);
186
+ for (const change2 of changes.values()) {
187
+ if (change2.hasChanges()) view2.applyChanges(change2);
188
188
  }
189
- view.getRoot();
190
- return view;
189
+ view2.getRoot();
190
+ return view2;
191
191
  }
192
- function createCoreNavigator(mindoodb, view, options) {
193
- const builder = mindoodb.VirtualViewFactory.createNavigator(view);
192
+ function createCoreNavigator(mindoodb, view2, options) {
193
+ const builder = mindoodb.VirtualViewFactory.createNavigator(view2);
194
194
  const includeCategories = options?.includeCategories ?? true;
195
195
  const includeDocuments = options?.includeDocuments ?? true;
196
196
  if (!includeCategories && includeDocuments) builder.documentsOnly();
@@ -199,7 +199,7 @@ function createCoreNavigator(mindoodb, view, options) {
199
199
  if (Array.isArray(options?.rootCategoryPath) && options.rootCategoryPath.length > 0) {
200
200
  builder.fromCategory(options.rootCategoryPath.map((part) => String(part)).join("\\"));
201
201
  } else if (options?.rootEntryKey) {
202
- const temp = mindoodb.VirtualViewFactory.createSimpleNavigator(view);
202
+ const temp = mindoodb.VirtualViewFactory.createSimpleNavigator(view2);
203
203
  const rootEntry = temp.getPos(options.rootEntryKey);
204
204
  if (rootEntry) builder.fromEntry(rootEntry);
205
205
  }
@@ -447,13 +447,13 @@ function collectSortedDocIds(entries) {
447
447
  async function createEvaluatingViewNavigator(input, documents) {
448
448
  const mindoodb = await loadMindoodbForTesting();
449
449
  if (!mindoodb) return null;
450
- const view = await buildVirtualView(
450
+ const view2 = await buildVirtualView(
451
451
  mindoodb,
452
452
  input.definition,
453
453
  documents,
454
454
  input.categorizationStyle
455
455
  );
456
- const navigator2 = createCoreNavigator(mindoodb, view, input.options);
456
+ const navigator2 = createCoreNavigator(mindoodb, view2, input.options);
457
457
  return wrapNavigator(input.definition, navigator2);
458
458
  }
459
459
 
@@ -1170,6 +1170,7 @@ function mockDatabasesFromDefinition(definition, seed = {}, options = {}) {
1170
1170
  capabilities: override?.capabilities ? [...new Set(override.capabilities)] : capabilitiesForDefinitionPermissions(database.permissions)
1171
1171
  },
1172
1172
  documents: seed[database.logicalDatabaseId],
1173
+ ...options.automerge ? { automerge: true } : {},
1173
1174
  ...options.attachments === "none" ? {} : { methods: { attachments: createMemoryAttachments() } }
1174
1175
  };
1175
1176
  });
@@ -1190,6 +1191,8 @@ function readTestHostUrlSettings(search) {
1190
1191
  }
1191
1192
  const enforce = params.get("enforce");
1192
1193
  if (enforce !== null) settings.enforceCapabilities = enforce === "1" || enforce === "true";
1194
+ const automerge = params.get("automerge");
1195
+ if (automerge !== null) settings.automerge = automerge === "1" || automerge === "true";
1193
1196
  const users = Number.parseInt(params.get("users") ?? "", 10);
1194
1197
  if (Number.isFinite(users) && users >= 0) settings.directoryUserCount = users;
1195
1198
  const locale = params.get("locale");
@@ -1387,6 +1390,11 @@ function createBrowserTestHost(options) {
1387
1390
  host.setDirectoryUsers(users);
1388
1391
  onChange?.();
1389
1392
  },
1393
+ async applyRemoteUpdate(databaseId, docId, input, updateOptions) {
1394
+ const document2 = await host.applyRemoteUpdate(databaseId, docId, input, updateOptions);
1395
+ onChange?.();
1396
+ return document2;
1397
+ },
1390
1398
  reloadApp,
1391
1399
  dispose() {
1392
1400
  window.removeEventListener("message", onMessage);
@@ -1498,7 +1506,7 @@ function mountHavenTestHost(options) {
1498
1506
  const databases = (hostOptions.databases ?? []).filter((database) => url.overrides[database.info.id]?.mapped !== false).map((database) => {
1499
1507
  const capabilities = url.overrides[database.info.id]?.capabilities;
1500
1508
  return capabilities ? { ...database, info: { ...database.info, capabilities } } : database;
1501
- });
1509
+ }).map((database) => url.automerge === void 0 ? database : { ...database, automerge: url.automerge });
1502
1510
  const launchContext = {
1503
1511
  ...hostOptions.launchContext,
1504
1512
  ...url.locale ? { locale: url.locale } : {},
@@ -2343,23 +2351,23 @@ function mergeLaunchContext(current, patch) {
2343
2351
  ...database,
2344
2352
  capabilities: [...database.capabilities]
2345
2353
  })),
2346
- views: current.views.map((view) => ({
2347
- ...view,
2348
- sources: view.sources.map((source) => ({ ...source })),
2349
- columns: view.columns.map((column) => ({
2354
+ views: current.views.map((view2) => ({
2355
+ ...view2,
2356
+ sources: view2.sources.map((source) => ({ ...source })),
2357
+ columns: view2.columns.map((column) => ({
2350
2358
  ...column,
2351
2359
  expression: column.expression.mode === "field" ? { ...column.expression } : {
2352
2360
  mode: "formula",
2353
2361
  expression: structuredClone(column.expression.expression)
2354
2362
  }
2355
2363
  })),
2356
- filter: view.filter.mode === "rules" ? {
2364
+ filter: view2.filter.mode === "rules" ? {
2357
2365
  mode: "rules",
2358
- match: view.filter.match,
2359
- rules: view.filter.rules.map((rule) => ({ ...rule }))
2366
+ match: view2.filter.match,
2367
+ rules: view2.filter.rules.map((rule) => ({ ...rule }))
2360
2368
  } : {
2361
2369
  mode: "formula",
2362
- expression: structuredClone(view.filter.expression)
2370
+ expression: structuredClone(view2.filter.expression)
2363
2371
  }
2364
2372
  }))
2365
2373
  };
@@ -2381,41 +2389,41 @@ function mergeLaunchContext(current, patch) {
2381
2389
  ...database,
2382
2390
  capabilities: [...database.capabilities]
2383
2391
  })),
2384
- views: patch.views ? patch.views.map((view) => ({
2385
- ...view,
2386
- sources: view.sources.map((source) => ({ ...source })),
2387
- columns: view.columns.map((column) => ({
2392
+ views: patch.views ? patch.views.map((view2) => ({
2393
+ ...view2,
2394
+ sources: view2.sources.map((source) => ({ ...source })),
2395
+ columns: view2.columns.map((column) => ({
2388
2396
  ...column,
2389
2397
  expression: column.expression.mode === "field" ? { ...column.expression } : {
2390
2398
  mode: "formula",
2391
2399
  expression: structuredClone(column.expression.expression)
2392
2400
  }
2393
2401
  })),
2394
- filter: view.filter.mode === "rules" ? {
2402
+ filter: view2.filter.mode === "rules" ? {
2395
2403
  mode: "rules",
2396
- match: view.filter.match,
2397
- rules: view.filter.rules.map((rule) => ({ ...rule }))
2404
+ match: view2.filter.match,
2405
+ rules: view2.filter.rules.map((rule) => ({ ...rule }))
2398
2406
  } : {
2399
2407
  mode: "formula",
2400
- expression: structuredClone(view.filter.expression)
2408
+ expression: structuredClone(view2.filter.expression)
2401
2409
  }
2402
- })) : current.views.map((view) => ({
2403
- ...view,
2404
- sources: view.sources.map((source) => ({ ...source })),
2405
- columns: view.columns.map((column) => ({
2410
+ })) : current.views.map((view2) => ({
2411
+ ...view2,
2412
+ sources: view2.sources.map((source) => ({ ...source })),
2413
+ columns: view2.columns.map((column) => ({
2406
2414
  ...column,
2407
2415
  expression: column.expression.mode === "field" ? { ...column.expression } : {
2408
2416
  mode: "formula",
2409
2417
  expression: structuredClone(column.expression.expression)
2410
2418
  }
2411
2419
  })),
2412
- filter: view.filter.mode === "rules" ? {
2420
+ filter: view2.filter.mode === "rules" ? {
2413
2421
  mode: "rules",
2414
- match: view.filter.match,
2415
- rules: view.filter.rules.map((rule) => ({ ...rule }))
2422
+ match: view2.filter.match,
2423
+ rules: view2.filter.rules.map((rule) => ({ ...rule }))
2416
2424
  } : {
2417
2425
  mode: "formula",
2418
- expression: structuredClone(view.filter.expression)
2426
+ expression: structuredClone(view2.filter.expression)
2419
2427
  }
2420
2428
  }))
2421
2429
  };
@@ -2506,9 +2514,12 @@ function mockDecryptionKeyId(document2) {
2506
2514
  }
2507
2515
  return { decryptionKeyId: document2.decryptionKeyId };
2508
2516
  }
2509
- function loadMockAutomergeDocument(document2) {
2517
+ function loadMockAutomergeDocument(document2, actor) {
2510
2518
  if (document2.automergeBinary) {
2511
- return Automerge.load(document2.automergeBinary);
2519
+ return Automerge.load(
2520
+ document2.automergeBinary,
2521
+ actor ? { actor } : void 0
2522
+ );
2512
2523
  }
2513
2524
  const initialDoc = Automerge.from({
2514
2525
  ...structuredClone(document2.data),
@@ -2519,9 +2530,401 @@ function loadMockAutomergeDocument(document2) {
2519
2530
  }
2520
2531
  function persistMockAutomergeDocument(document2, automergeDoc) {
2521
2532
  document2.automergeBinary = Automerge.save(automergeDoc);
2522
- document2.data = structuredClone(automergeDoc);
2533
+ document2.data = projectMockAutomergeValue(
2534
+ Automerge.load(document2.automergeBinary)
2535
+ );
2523
2536
  document2.heads = Automerge.getHeads(automergeDoc);
2524
2537
  }
2538
+ var MOCK_AUTOMERGE_SEED_ACTOR = "5eed0000000000000000000000000000";
2539
+ var MOCK_AUTOMERGE_LOCAL_ACTOR = "10ca1000000000000000000000000000";
2540
+ var MOCK_AUTOMERGE_REMOTE_ACTOR = "2e307e00000000000000000000000000";
2541
+ function projectMockAutomergeValue(value, depth = 0) {
2542
+ if (value === null || typeof value !== "object") {
2543
+ return value;
2544
+ }
2545
+ if (Automerge.isCounter(value)) {
2546
+ return value.value;
2547
+ }
2548
+ if (Automerge.isImmutableString(value)) {
2549
+ return String(value);
2550
+ }
2551
+ if (value instanceof Date) {
2552
+ return value.toISOString();
2553
+ }
2554
+ if (value instanceof Uint8Array) {
2555
+ return value.slice();
2556
+ }
2557
+ if (depth > 100) {
2558
+ throw new Error("Document value is nested too deeply");
2559
+ }
2560
+ if (Array.isArray(value)) {
2561
+ return value.map((entry) => projectMockAutomergeValue(entry, depth + 1));
2562
+ }
2563
+ return Object.fromEntries(
2564
+ Object.entries(value).map(([key, entry]) => [
2565
+ key,
2566
+ projectMockAutomergeValue(entry, depth + 1)
2567
+ ])
2568
+ );
2569
+ }
2570
+ function hydrateMockAutomergeValues(value, depth = 0) {
2571
+ if (value === null || typeof value !== "object") {
2572
+ return value;
2573
+ }
2574
+ if (depth > 100) {
2575
+ throw new Error("Document value is nested too deeply");
2576
+ }
2577
+ if (value instanceof Uint8Array || value instanceof Date || Automerge.isCounter(value) || Automerge.isImmutableString(value)) {
2578
+ return value;
2579
+ }
2580
+ if (Array.isArray(value)) {
2581
+ return value.map(
2582
+ (entry) => entry === void 0 ? null : hydrateMockAutomergeValues(entry, depth + 1)
2583
+ );
2584
+ }
2585
+ const record = value;
2586
+ if (Object.prototype.hasOwnProperty.call(record, MINDOODB_APP_VALUE_TAG)) {
2587
+ switch (record[MINDOODB_APP_VALUE_TAG]) {
2588
+ case "atomic":
2589
+ if (typeof record.value !== "string") {
2590
+ throw new Error('Typed value "atomic" needs a string value');
2591
+ }
2592
+ return new Automerge.ImmutableString(record.value);
2593
+ case "counter":
2594
+ assertMockCounterAmount(record.value, 'Typed value "counter"');
2595
+ return new Automerge.Counter(record.value);
2596
+ case "timestamp":
2597
+ return new Date(toMockTimestamp(record.value));
2598
+ default:
2599
+ throw new Error(
2600
+ `Unknown typed value ${JSON.stringify(record[MINDOODB_APP_VALUE_TAG])}; the "${MINDOODB_APP_VALUE_TAG}" key is reserved`
2601
+ );
2602
+ }
2603
+ }
2604
+ const result = {};
2605
+ for (const [key, entry] of Object.entries(record)) {
2606
+ if (entry !== void 0) {
2607
+ result[key] = hydrateMockAutomergeValues(entry, depth + 1);
2608
+ }
2609
+ }
2610
+ return result;
2611
+ }
2612
+ function createMockAutomergeDocument(values, actor, time) {
2613
+ const entries = Object.entries(values).filter(([, value]) => value !== void 0);
2614
+ const initial = Automerge.init({ actor });
2615
+ const options = time === void 0 ? {} : { time };
2616
+ if (entries.length === 0) {
2617
+ return Automerge.emptyChange(initial, options);
2618
+ }
2619
+ return Automerge.change(initial, options, (doc) => {
2620
+ for (const [key, value] of entries) {
2621
+ doc[key] = hydrateMockAutomergeValues(value);
2622
+ }
2623
+ });
2624
+ }
2625
+ function validateMockJsonPath(path, label) {
2626
+ if (!Array.isArray(path) || path.length === 0) {
2627
+ throw new Error(`${label} path must contain at least one segment`);
2628
+ }
2629
+ for (const segment of path) {
2630
+ if (typeof segment !== "string" && typeof segment !== "number") {
2631
+ throw new Error(`${label} path segments must be strings or numbers`);
2632
+ }
2633
+ }
2634
+ }
2635
+ function assertMockNonNegativeInteger(value, label) {
2636
+ if (!Number.isInteger(value) || value < 0) {
2637
+ throw new Error(`${label} must be a non-negative integer`);
2638
+ }
2639
+ }
2640
+ function validateMockJsonPatch(patch) {
2641
+ const operationCount = (patch.set?.length ?? 0) + (patch.unset?.length ?? 0) + (patch.listDelete?.length ?? 0) + (patch.listInsert?.length ?? 0) + (patch.textSplice?.length ?? 0) + (patch.textMark?.length ?? 0) + (patch.textUnmark?.length ?? 0) + (patch.counterIncrement?.length ?? 0);
2642
+ if (operationCount === 0) {
2643
+ throw new Error("JSON patch must include at least one operation");
2644
+ }
2645
+ for (const operation of patch.set ?? []) {
2646
+ validateMockJsonPath(operation.path, "JSON set");
2647
+ hydrateMockAutomergeValues(operation.value);
2648
+ }
2649
+ for (const operation of patch.unset ?? []) {
2650
+ validateMockJsonPath(operation.path, "JSON unset");
2651
+ }
2652
+ for (const operation of patch.listDelete ?? []) {
2653
+ validateMockJsonPath(operation.path, "JSON listDelete");
2654
+ assertMockNonNegativeInteger(operation.index, "JSON listDelete index");
2655
+ assertMockNonNegativeInteger(operation.deleteCount, "JSON listDelete deleteCount");
2656
+ }
2657
+ for (const operation of patch.listInsert ?? []) {
2658
+ validateMockJsonPath(operation.path, "JSON listInsert");
2659
+ assertMockNonNegativeInteger(operation.index, "JSON listInsert index");
2660
+ if (!Array.isArray(operation.values)) {
2661
+ throw new Error("JSON listInsert values must be an array");
2662
+ }
2663
+ hydrateMockAutomergeValues(operation.values);
2664
+ }
2665
+ for (const operation of patch.textSplice ?? []) {
2666
+ validateMockJsonPath(operation.path, "JSON textSplice");
2667
+ assertMockNonNegativeInteger(operation.index, "JSON textSplice index");
2668
+ assertMockNonNegativeInteger(operation.deleteCount, "JSON textSplice deleteCount");
2669
+ if (operation.insert !== void 0 && typeof operation.insert !== "string") {
2670
+ throw new Error("JSON textSplice insert must be a string");
2671
+ }
2672
+ }
2673
+ for (const operation of [...patch.textMark ?? [], ...patch.textUnmark ?? []]) {
2674
+ validateMockJsonPath(operation.path, "JSON text mark");
2675
+ assertMockNonNegativeInteger(operation.index, "JSON text mark index");
2676
+ if (!Number.isInteger(operation.length) || operation.length <= 0) {
2677
+ throw new Error("JSON text mark length must be a positive integer");
2678
+ }
2679
+ }
2680
+ for (const operation of patch.counterIncrement ?? []) {
2681
+ validateMockJsonPath(operation.path, "JSON counterIncrement");
2682
+ assertMockCounterAmount(operation.delta, "JSON counterIncrement delta");
2683
+ }
2684
+ }
2685
+ function ensureMockAutomergeParent(target, path) {
2686
+ let parent = target;
2687
+ for (let index = 0; index < path.length - 1; index += 1) {
2688
+ const segment = path[index];
2689
+ const nextSegment = path[index + 1];
2690
+ if (parent[segment] === void 0 || parent[segment] === null) {
2691
+ parent[segment] = typeof nextSegment === "number" ? [] : {};
2692
+ }
2693
+ parent = parent[segment];
2694
+ if (parent === null || typeof parent !== "object") {
2695
+ throw new Error(
2696
+ `Cannot apply JSON patch through non-object path segment '${String(segment)}'`
2697
+ );
2698
+ }
2699
+ }
2700
+ return parent;
2701
+ }
2702
+ function readMockAutomergeValue(target, path) {
2703
+ let value = target;
2704
+ for (const segment of path) {
2705
+ value = value?.[segment];
2706
+ }
2707
+ return value;
2708
+ }
2709
+ function setMockAutomergeValue(target, path, value) {
2710
+ const parent = ensureMockAutomergeParent(target, path);
2711
+ parent[path[path.length - 1]] = value;
2712
+ }
2713
+ function unsetMockAutomergeValue(target, path) {
2714
+ let parent = target;
2715
+ for (let index = 0; index < path.length - 1; index += 1) {
2716
+ parent = parent?.[path[index]];
2717
+ if (parent === null || typeof parent !== "object") {
2718
+ return;
2719
+ }
2720
+ }
2721
+ delete parent[path[path.length - 1]];
2722
+ }
2723
+ function readMockAutomergeList(target, path) {
2724
+ const value = readMockAutomergeValue(target, path);
2725
+ if (!Array.isArray(value)) {
2726
+ throw new Error(
2727
+ `Cannot apply JSON list operation to non-array value at '${path.map(String).join(".")}'`
2728
+ );
2729
+ }
2730
+ return value;
2731
+ }
2732
+ function ensureMockAutomergeList(target, path) {
2733
+ const parent = ensureMockAutomergeParent(target, path);
2734
+ const leaf = path[path.length - 1];
2735
+ if (parent[leaf] === void 0 || parent[leaf] === null) {
2736
+ parent[leaf] = [];
2737
+ }
2738
+ return readMockAutomergeList(target, path);
2739
+ }
2740
+ function clampMockIndex(index, length) {
2741
+ if (!Number.isFinite(index)) {
2742
+ return length;
2743
+ }
2744
+ return Math.max(0, Math.min(Math.trunc(index), length));
2745
+ }
2746
+ function ensureMockAutomergeText(target, path, label) {
2747
+ const value = readMockAutomergeValue(target, path);
2748
+ if (value === void 0 || value === null) {
2749
+ setMockAutomergeValue(target, path, "");
2750
+ } else if (typeof value !== "string") {
2751
+ throw new Error(
2752
+ `Cannot apply ${label} to non-string value at '${path.map(String).join(".")}'`
2753
+ );
2754
+ }
2755
+ }
2756
+ function spliceMockAutomergeText(target, path, index, deleteCount, insert) {
2757
+ const text = readMockAutomergeValue(target, path);
2758
+ const length = typeof text === "string" ? text.length : 0;
2759
+ const start = clampMockIndex(index, length);
2760
+ Automerge.splice(
2761
+ target,
2762
+ path,
2763
+ start,
2764
+ Math.max(0, Math.min(deleteCount, length - start)),
2765
+ insert
2766
+ );
2767
+ }
2768
+ function reviveMockRichTextValue(value) {
2769
+ if (Array.isArray(value)) {
2770
+ return value.map((entry) => reviveMockRichTextValue(entry));
2771
+ }
2772
+ if (!value || typeof value !== "object") {
2773
+ return value;
2774
+ }
2775
+ const record = value;
2776
+ if (record.type === "immutableString" && typeof record.value === "string") {
2777
+ return new Automerge.ImmutableString(record.value);
2778
+ }
2779
+ return Object.fromEntries(
2780
+ Object.entries(record).map(([key, entry]) => [key, reviveMockRichTextValue(entry)])
2781
+ );
2782
+ }
2783
+ function applyMockAutomergeJsonOperations(target, patch) {
2784
+ for (const operation of patch.set ?? []) {
2785
+ setMockAutomergeValue(target, operation.path, hydrateMockAutomergeValues(operation.value));
2786
+ }
2787
+ for (const operation of patch.unset ?? []) {
2788
+ unsetMockAutomergeValue(target, operation.path);
2789
+ }
2790
+ for (const operation of patch.listDelete ?? []) {
2791
+ readMockAutomergeList(target, operation.path).splice(operation.index, operation.deleteCount);
2792
+ }
2793
+ for (const operation of patch.listInsert ?? []) {
2794
+ ensureMockAutomergeList(target, operation.path).splice(
2795
+ operation.index,
2796
+ 0,
2797
+ ...hydrateMockAutomergeValues(operation.values)
2798
+ );
2799
+ }
2800
+ for (const operation of patch.textSplice ?? []) {
2801
+ ensureMockAutomergeText(target, operation.path, "JSON textSplice");
2802
+ spliceMockAutomergeText(
2803
+ target,
2804
+ operation.path,
2805
+ operation.index,
2806
+ operation.deleteCount,
2807
+ operation.insert ?? ""
2808
+ );
2809
+ }
2810
+ for (const operation of patch.textMark ?? []) {
2811
+ for (const [name, value] of Object.entries(operation.marks)) {
2812
+ Automerge.mark(
2813
+ target,
2814
+ operation.path,
2815
+ { start: operation.index, end: operation.index + operation.length, expand: "none" },
2816
+ name,
2817
+ reviveMockRichTextValue(value)
2818
+ );
2819
+ }
2820
+ }
2821
+ for (const operation of patch.textUnmark ?? []) {
2822
+ for (const name of operation.names) {
2823
+ Automerge.unmark(
2824
+ target,
2825
+ operation.path,
2826
+ { start: operation.index, end: operation.index + operation.length, expand: "none" },
2827
+ name
2828
+ );
2829
+ }
2830
+ }
2831
+ for (const operation of patch.counterIncrement ?? []) {
2832
+ const parent = ensureMockAutomergeParent(target, operation.path);
2833
+ const leaf = operation.path[operation.path.length - 1];
2834
+ const existing = parent[leaf];
2835
+ if (existing === void 0 || existing === null) {
2836
+ parent[leaf] = new Automerge.Counter(operation.delta);
2837
+ } else if (Automerge.isCounter(existing)) {
2838
+ existing.increment(operation.delta);
2839
+ } else {
2840
+ throw new Error(
2841
+ `Cannot increment non-counter value at '${operation.path.map(String).join(".")}'; create it with MindooValue.counter() first`
2842
+ );
2843
+ }
2844
+ }
2845
+ }
2846
+ function changeMockAutomergeDocument(doc, baseHeads, apply) {
2847
+ if (baseHeads && baseHeads.length > 0) {
2848
+ const { newDoc } = Automerge.changeAt(doc, baseHeads, apply);
2849
+ return Automerge.load(Automerge.save(newDoc), { actor: Automerge.getActorId(doc) });
2850
+ }
2851
+ return Automerge.change(doc, apply);
2852
+ }
2853
+ function applyMockAutomergeUpdate(doc, patch) {
2854
+ let next = doc;
2855
+ if (patch.set || (patch.unset?.length ?? 0) > 0) {
2856
+ next = Automerge.change(next, (target) => {
2857
+ for (const [key, value] of Object.entries(patch.set ?? {})) {
2858
+ if (value !== void 0) {
2859
+ target[key] = hydrateMockAutomergeValues(value);
2860
+ }
2861
+ }
2862
+ for (const key of patch.unset ?? []) {
2863
+ delete target[key];
2864
+ }
2865
+ });
2866
+ }
2867
+ if (patch.json) {
2868
+ const json = patch.json;
2869
+ validateMockJsonPatch(json);
2870
+ next = changeMockAutomergeDocument(
2871
+ next,
2872
+ json.baseHeads,
2873
+ (target) => applyMockAutomergeJsonOperations(target, json)
2874
+ );
2875
+ }
2876
+ for (const textPatch of patch.text ?? []) {
2877
+ validateMockJsonPath(textPatch.path, "Text patch");
2878
+ next = changeMockAutomergeDocument(next, textPatch.baseHeads, (target) => {
2879
+ ensureMockAutomergeText(target, textPatch.path, "text patch");
2880
+ for (const edit of textPatch.edits) {
2881
+ spliceMockAutomergeText(target, textPatch.path, edit.index, edit.deleteCount, edit.insert ?? "");
2882
+ }
2883
+ });
2884
+ }
2885
+ for (const richTextPatch of patch.richText ?? []) {
2886
+ validateMockJsonPath(richTextPatch.path, "Rich-text patch");
2887
+ const spans = richTextPatch.spansSequence?.at(-1) ?? richTextPatch.spans ?? [];
2888
+ next = Automerge.change(next, (target) => {
2889
+ setMockAutomergeValue(target, richTextPatch.path, hydrateMockAutomergeValues(structuredClone(spans)));
2890
+ });
2891
+ }
2892
+ for (const stepPatch of patch.richTextSteps ?? []) {
2893
+ validateMockJsonPath(stepPatch.path, "Rich-text steps patch");
2894
+ next = changeMockAutomergeDocument(next, stepPatch.baseHeads, (target) => {
2895
+ const current = readMockAutomergeValue(target, stepPatch.path);
2896
+ if (current !== void 0 && current !== null && typeof current !== "string") {
2897
+ setMockAutomergeValue(
2898
+ target,
2899
+ stepPatch.path,
2900
+ materializeMockRichTextValue(projectMockAutomergeValue(current))
2901
+ );
2902
+ }
2903
+ ensureMockAutomergeText(target, stepPatch.path, "rich-text steps");
2904
+ for (const step of stepPatch.steps) {
2905
+ spliceMockAutomergeText(target, stepPatch.path, step.index, step.deleteCount, step.insert ?? "");
2906
+ }
2907
+ });
2908
+ }
2909
+ return next;
2910
+ }
2911
+ function forkMockAutomergeDocument(doc, baseHeads, actor) {
2912
+ if (!baseHeads || baseHeads.length === 0) {
2913
+ return Automerge.clone(doc, { actor });
2914
+ }
2915
+ const heads = new Set(baseHeads);
2916
+ let ownLatest;
2917
+ for (const change2 of Automerge.getAllChanges(doc)) {
2918
+ const decoded = Automerge.decodeChange(change2);
2919
+ if (decoded.actor === actor) {
2920
+ ownLatest = decoded.hash;
2921
+ }
2922
+ }
2923
+ if (ownLatest) {
2924
+ heads.add(ownLatest);
2925
+ }
2926
+ return Automerge.clone(Automerge.view(doc, [...heads]), { actor });
2927
+ }
2525
2928
  function getFieldValue2(source, field) {
2526
2929
  return field.split(".").reduce((current, part) => {
2527
2930
  if (current && typeof current === "object" && part in current) {
@@ -3374,9 +3777,17 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3374
3777
  let createCounter = 0;
3375
3778
  let changeCounter = 0;
3376
3779
  const defaultViewFactory = async () => createDefaultViewNavigator();
3377
- const storedDocuments = /* @__PURE__ */ new Map();
3780
+ const automergeMode = definition.automerge === true;
3781
+ let changeSequence = 0;
3782
+ const changedAt = /* @__PURE__ */ new Map();
3783
+ const storedDocuments = new class extends Map {
3784
+ set(id, document2) {
3785
+ changedAt.set(id, ++changeSequence);
3786
+ return super.set(id, document2);
3787
+ }
3788
+ }();
3378
3789
  for (const seed of definition.documents ?? []) {
3379
- storedDocuments.set(seed.id, {
3790
+ const stored = {
3380
3791
  id: seed.id,
3381
3792
  data: structuredClone(seed.data),
3382
3793
  heads: seed.heads ? [...seed.heads] : void 0,
@@ -3387,7 +3798,14 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3387
3798
  decryptionKeyId: seed.decryptionKeyId,
3388
3799
  authorLabel: seed.authorLabel,
3389
3800
  createdAt: seed.createdAt
3390
- });
3801
+ };
3802
+ if (automergeMode) {
3803
+ persistMockAutomergeDocument(
3804
+ stored,
3805
+ createMockAutomergeDocument(seed.data, MOCK_AUTOMERGE_SEED_ACTOR, 0)
3806
+ );
3807
+ }
3808
+ storedDocuments.set(seed.id, stored);
3391
3809
  }
3392
3810
  const liveQuerySubscriptions = /* @__PURE__ */ new Set();
3393
3811
  const refreshLiveQueries = () => {
@@ -3430,6 +3848,15 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3430
3848
  throw new Error("Recipient mutators require a document created with `recipients`");
3431
3849
  }
3432
3850
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3851
+ if (automergeMode) {
3852
+ return writeAutomergeDocument(
3853
+ docId,
3854
+ existing,
3855
+ Automerge.change(loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR), (target) => {
3856
+ target._encryptFor = hydrateMockAutomergeValues(nextEncryptFor);
3857
+ })
3858
+ );
3859
+ }
3433
3860
  const updated = {
3434
3861
  id: docId,
3435
3862
  data: { ...existing.data, _encryptFor: nextEncryptFor },
@@ -3448,6 +3875,28 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3448
3875
  notifyLiveQueries();
3449
3876
  return updated;
3450
3877
  }
3878
+ function writeAutomergeDocument(docId, existing, automergeDoc) {
3879
+ const stored = {
3880
+ ...existing ?? {},
3881
+ id: docId,
3882
+ data: {},
3883
+ attachments: existing?.attachments ? structuredClone(existing.attachments) : [],
3884
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3885
+ isDeleted: false,
3886
+ decryptionKeyId: existing?.decryptionKeyId ?? "default"
3887
+ };
3888
+ persistMockAutomergeDocument(stored, automergeDoc);
3889
+ storedDocuments.set(docId, stored);
3890
+ notifyLiveQueries();
3891
+ return {
3892
+ id: docId,
3893
+ data: structuredClone(stored.data),
3894
+ heads: [...stored.heads ?? []],
3895
+ attachments: structuredClone(stored.attachments ?? []),
3896
+ updatedAt: stored.updatedAt,
3897
+ ...mockDecryptionKeyId(stored)
3898
+ };
3899
+ }
3451
3900
  const defaultDocuments = {
3452
3901
  async query(query) {
3453
3902
  return runMockDocumentQuery(
@@ -3500,12 +3949,17 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3500
3949
  const offset = decodeMockListCursor(query?.cursor) + skip;
3501
3950
  const metadataOnly = query?.metadataOnly ?? false;
3502
3951
  const idPrefix = query?.idPrefix;
3503
- const items = Array.from(storedDocuments.values()).filter((document2) => !document2.inaccessible).filter(
3952
+ const after = automergeMode ? decodeMockListCursor(query?.cursor) : 0;
3953
+ const items = Array.from(storedDocuments.values()).filter((document2) => !automergeMode || (changedAt.get(document2.id) ?? 0) > after).filter((document2) => !document2.inaccessible).filter(
3504
3954
  (document2) => status === "all" ? true : status === "deleted" ? document2.isDeleted : !document2.isDeleted
3505
3955
  ).filter((document2) => matchesMockIdPrefix(document2.id, idPrefix)).filter(
3506
3956
  (document2) => metadataOnly ? true : matchesDocumentFilter(document2, query?.filter)
3507
- ).sort((left, right) => left.id.localeCompare(right.id));
3508
- const page = items.slice(offset, offset + limit).map((document2) => {
3957
+ ).sort(
3958
+ (left, right) => automergeMode ? (changedAt.get(left.id) ?? 0) - (changedAt.get(right.id) ?? 0) : left.id.localeCompare(right.id)
3959
+ );
3960
+ const pageStart = automergeMode ? skip : offset;
3961
+ const pageDocuments = items.slice(pageStart, pageStart + limit);
3962
+ const page = pageDocuments.map((document2) => {
3509
3963
  if (metadataOnly) {
3510
3964
  return {
3511
3965
  id: document2.id,
@@ -3526,6 +3980,13 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3526
3980
  isDeleted: status !== "existing" ? document2.isDeleted : void 0
3527
3981
  };
3528
3982
  });
3983
+ if (automergeMode) {
3984
+ const last = pageDocuments[pageDocuments.length - 1];
3985
+ return {
3986
+ items: page,
3987
+ nextCursor: last ? String(changedAt.get(last.id) ?? after) : null
3988
+ };
3989
+ }
3529
3990
  const nextCursor = offset + page.length < items.length ? String(offset + page.length) : null;
3530
3991
  return {
3531
3992
  items: page,
@@ -3534,7 +3995,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3534
3995
  },
3535
3996
  async getHeadCursor() {
3536
3997
  return {
3537
- cursor: String(storedDocuments.size)
3998
+ cursor: String(automergeMode ? changeSequence : storedDocuments.size)
3538
3999
  };
3539
4000
  },
3540
4001
  async listInaccessible(query) {
@@ -3630,7 +4091,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3630
4091
  const currentDoc = loadMockAutomergeDocument(document2);
3631
4092
  const [mergedDoc] = Automerge.applyChanges(
3632
4093
  currentDoc,
3633
- patch.changes.map((change) => new Uint8Array(change))
4094
+ patch.changes.map((change2) => new Uint8Array(change2))
3634
4095
  );
3635
4096
  persistMockAutomergeDocument(document2, mergedDoc);
3636
4097
  document2.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -3643,7 +4104,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3643
4104
  );
3644
4105
  changesSince = {
3645
4106
  sinceHeads: [...patch.replicaHeads],
3646
- changes: missingChanges.map((change) => cloneBytes(new Uint8Array(change)))
4107
+ changes: missingChanges.map((change2) => cloneBytes(new Uint8Array(change2)))
3647
4108
  };
3648
4109
  }
3649
4110
  return {
@@ -3680,7 +4141,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3680
4141
  if (existing.isDeleted) {
3681
4142
  existing.isDeleted = false;
3682
4143
  existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3683
- existing.heads = [`mock-head-${++changeCounter}`];
4144
+ if (!automergeMode) {
4145
+ existing.heads = [`mock-head-${++changeCounter}`];
4146
+ }
4147
+ storedDocuments.set(callerId, existing);
3684
4148
  notifyLiveQueries();
3685
4149
  }
3686
4150
  return {
@@ -3709,11 +4173,39 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3709
4173
  "recipients: [] with includeSelf: false would produce a document nobody can read"
3710
4174
  );
3711
4175
  }
4176
+ const encryptFor = Array.isArray(input.recipients) ? { _encryptFor: mockEncryptForMap(input.recipients) } : {};
4177
+ if (automergeMode) {
4178
+ const stored2 = {
4179
+ id,
4180
+ data: {},
4181
+ attachments: [],
4182
+ updatedAt: createdAt,
4183
+ isDeleted: false,
4184
+ decryptionKeyId: input.decryptionKeyId ?? "default"
4185
+ };
4186
+ persistMockAutomergeDocument(
4187
+ stored2,
4188
+ createMockAutomergeDocument(
4189
+ { ...input.set ?? {}, ...encryptFor },
4190
+ MOCK_AUTOMERGE_LOCAL_ACTOR
4191
+ )
4192
+ );
4193
+ storedDocuments.set(id, stored2);
4194
+ notifyLiveQueries();
4195
+ return {
4196
+ id,
4197
+ data: structuredClone(stored2.data),
4198
+ heads: [...stored2.heads ?? []],
4199
+ attachments: [],
4200
+ updatedAt: createdAt,
4201
+ ...mockDecryptionKeyId(stored2)
4202
+ };
4203
+ }
3712
4204
  const created = {
3713
4205
  id,
3714
4206
  data: {
3715
4207
  ...readMockTypedValues(input.set ?? {}),
3716
- ...Array.isArray(input.recipients) ? { _encryptFor: mockEncryptForMap(input.recipients) } : {}
4208
+ ...encryptFor
3717
4209
  },
3718
4210
  heads: [`mock-head-${++changeCounter}`],
3719
4211
  attachments: [],
@@ -3744,6 +4236,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3744
4236
  async update(docId, patch) {
3745
4237
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3746
4238
  const existing = storedDocuments.get(docId);
4239
+ if (automergeMode) {
4240
+ const current = existing?.automergeBinary ? loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR) : createMockAutomergeDocument(existing?.data ?? {}, MOCK_AUTOMERGE_LOCAL_ACTOR);
4241
+ return writeAutomergeDocument(docId, existing, applyMockAutomergeUpdate(current, patch));
4242
+ }
3747
4243
  const updated = {
3748
4244
  id: docId,
3749
4245
  data: applyDocumentUpdatePatch(existing?.data ?? {}, patch),
@@ -3830,7 +4326,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3830
4326
  storedDocuments.set(_docId, {
3831
4327
  ...existing,
3832
4328
  isDeleted: false,
3833
- heads: [`mock-head-${++changeCounter}`],
4329
+ heads: automergeMode ? existing.heads : [`mock-head-${++changeCounter}`],
3834
4330
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3835
4331
  });
3836
4332
  notifyLiveQueries();
@@ -4054,7 +4550,26 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
4054
4550
  createdAt: document2.updatedAt ?? null
4055
4551
  })),
4056
4552
  documents: storedDocuments,
4057
- notifyLiveQueries: refreshLiveQueries
4553
+ notifyLiveQueries: refreshLiveQueries,
4554
+ async applyRemoteUpdate(docId, input, options) {
4555
+ const existing = storedDocuments.get(docId);
4556
+ if (!existing || existing.isDeleted) {
4557
+ throw new Error(`Document ${docId} was not found.`);
4558
+ }
4559
+ if (!automergeMode || !existing.automergeBinary) {
4560
+ return await defaultDocuments.update(docId, input);
4561
+ }
4562
+ const current = loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR);
4563
+ const remote = applyMockAutomergeUpdate(
4564
+ forkMockAutomergeDocument(
4565
+ current,
4566
+ input.json?.baseHeads,
4567
+ options?.actor ?? MOCK_AUTOMERGE_REMOTE_ACTOR
4568
+ ),
4569
+ input.json ? { ...input, json: { ...input.json, baseHeads: void 0 } } : input
4570
+ );
4571
+ return writeAutomergeDocument(docId, existing, Automerge.merge(current, remote));
4572
+ }
4058
4573
  };
4059
4574
  }
4060
4575
  function createMockSessionState(options = {}) {
@@ -4080,6 +4595,7 @@ function createMockSessionState(options = {}) {
4080
4595
  const databaseViewDocumentLists = /* @__PURE__ */ new Map();
4081
4596
  const databaseDocumentStores = /* @__PURE__ */ new Map();
4082
4597
  const databaseLiveQueryRefreshers = /* @__PURE__ */ new Map();
4598
+ const databaseRemoteUpdaters = /* @__PURE__ */ new Map();
4083
4599
  const databasePeers = {
4084
4600
  getDocuments: (databaseId) => databaseDocumentStores.get(databaseId),
4085
4601
  refreshAllLiveQueries: () => {
@@ -4170,6 +4686,7 @@ function createMockSessionState(options = {}) {
4170
4686
  databaseViewApis.clear();
4171
4687
  databaseDocumentStores.clear();
4172
4688
  databaseLiveQueryRefreshers.clear();
4689
+ databaseRemoteUpdaters.clear();
4173
4690
  databaseInfos = definitions.map((definition) => ({
4174
4691
  ...definition.info,
4175
4692
  capabilities: [...definition.info.capabilities]
@@ -4188,6 +4705,7 @@ function createMockSessionState(options = {}) {
4188
4705
  databaseViewDocumentLists.set(definition.info.id, created.listViewDocuments);
4189
4706
  databaseDocumentStores.set(definition.info.id, created.documents);
4190
4707
  databaseLiveQueryRefreshers.set(definition.info.id, created.notifyLiveQueries);
4708
+ databaseRemoteUpdaters.set(definition.info.id, created.applyRemoteUpdate);
4191
4709
  databaseViewApis.set(definition.info.id, {
4192
4710
  async create(input) {
4193
4711
  const build = () => createEvaluatingViewNavigator(
@@ -4310,10 +4828,10 @@ function createMockSessionState(options = {}) {
4310
4828
  `Unknown test database for view creation: ${firstDatabaseId}`
4311
4829
  );
4312
4830
  }
4313
- const view = await api.create(input);
4831
+ const view2 = await api.create(input);
4314
4832
  const viewId = input.definition.id || crypto.randomUUID();
4315
- sessionViews.set(viewId, view);
4316
- return view;
4833
+ sessionViews.set(viewId, view2);
4834
+ return view2;
4317
4835
  },
4318
4836
  async openViewNavigator(viewId, options2) {
4319
4837
  const existing = sessionViews.get(viewId);
@@ -4321,7 +4839,7 @@ function createMockSessionState(options = {}) {
4321
4839
  return existing;
4322
4840
  }
4323
4841
  const sourceDatabaseId = launchContext.views.find(
4324
- (view2) => view2.id === viewId
4842
+ (view3) => view3.id === viewId
4325
4843
  )?.sources[0]?.databaseId;
4326
4844
  if (!sourceDatabaseId) {
4327
4845
  throw new Error(`Unknown test view: ${viewId}`);
@@ -4332,9 +4850,9 @@ function createMockSessionState(options = {}) {
4332
4850
  `Unknown test database for view ${viewId}: ${sourceDatabaseId}`
4333
4851
  );
4334
4852
  }
4335
- const view = await api.open(viewId, options2);
4336
- sessionViews.set(viewId, view);
4337
- return view;
4853
+ const view2 = await api.open(viewId, options2);
4854
+ sessionViews.set(viewId, view2);
4855
+ return view2;
4338
4856
  },
4339
4857
  menus,
4340
4858
  drag,
@@ -4471,6 +4989,13 @@ function createMockSessionState(options = {}) {
4471
4989
  }
4472
4990
  return database;
4473
4991
  },
4992
+ async applyRemoteUpdate(databaseId, docId, input, updateOptions) {
4993
+ const applyRemoteUpdate = databaseRemoteUpdaters.get(databaseId);
4994
+ if (!applyRemoteUpdate) {
4995
+ throw new Error(`Unknown test database: ${databaseId}`);
4996
+ }
4997
+ return await applyRemoteUpdate(docId, input, updateOptions);
4998
+ },
4474
4999
  getLicensedProducts() {
4475
5000
  return [...launchContext.licensedProducts ?? []];
4476
5001
  },
@@ -4587,6 +5112,7 @@ function createMockMindooDBAppSession(options = {}) {
4587
5112
  setCapabilities: state.setCapabilities,
4588
5113
  setEnforceCapabilities: state.setEnforceCapabilities,
4589
5114
  setDirectoryUsers: state.setDirectoryUsers,
5115
+ applyRemoteUpdate: state.applyRemoteUpdate,
4590
5116
  emitThemeChange: state.emitThemeChange,
4591
5117
  emitViewportChange: state.emitViewportChange,
4592
5118
  emitUiPreferencesChange: state.emitUiPreferencesChange,
@@ -5529,6 +6055,7 @@ function createFakeBridgeHost(options = {}) {
5529
6055
  getEnforceCapabilities: state.getEnforceCapabilities,
5530
6056
  setDirectoryUsers: state.setDirectoryUsers,
5531
6057
  getDirectoryUsers: state.getDirectoryUsers,
6058
+ applyRemoteUpdate: state.applyRemoteUpdate,
5532
6059
  listEmbeds: () => state.embedHost.listEmbeds(),
5533
6060
  closeEmbed: (embedId, reason, result) => state.embedHost.closeEmbed(embedId, reason, result),
5534
6061
  getEmbeddingFinish: () => state.embedHost.getEmbeddingFinish(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindoodb-app-sdk",
3
- "version": "0.0.64",
3
+ "version": "0.0.65",
4
4
  "engines": {
5
5
  "node": ">=22.13.0"
6
6
  },