mindoodb-app-sdk 0.0.64 → 0.0.66

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.
@@ -4,7 +4,7 @@ import {
4
4
  mindooDBAppComponentFieldsMatch,
5
5
  pageUsers,
6
6
  releaseMindooDBAppBridgeSessions
7
- } from "../chunk-BE7LLFFE.js";
7
+ } from "../chunk-B3K3I2S2.js";
8
8
 
9
9
  // src/testing/index.ts
10
10
  import * as Automerge from "@automerge/automerge";
@@ -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
 
@@ -464,6 +464,8 @@ function createMockEmbedHost(options, getDatabase) {
464
464
  const handles = /* @__PURE__ */ new Map();
465
465
  const closedListeners = /* @__PURE__ */ new Set();
466
466
  const dirtyListeners = /* @__PURE__ */ new Set();
467
+ const toolListeners = /* @__PURE__ */ new Set();
468
+ const toolNames = /* @__PURE__ */ new Map();
467
469
  let nextSaveError = null;
468
470
  let embeddingDirty;
469
471
  let saveRequestHandler = null;
@@ -504,11 +506,49 @@ function createMockEmbedHost(options, getDatabase) {
504
506
  docId: input.docId,
505
507
  intent,
506
508
  rect: mockRect(input),
507
- visible: input.visible !== false
509
+ visible: input.visible !== false,
510
+ active: false,
511
+ agentTools: null
508
512
  });
509
- notifyChange();
513
+ setActive(embedId);
510
514
  return embedId;
511
515
  }
516
+ function toolPrefix(component) {
517
+ return component.appId.replace(/^mindoodb-app-/, "").replace(/[^a-z0-9_]/g, "_") || "app";
518
+ }
519
+ function publishTools(entry) {
520
+ const declared = toolNames.get(entry.embedId);
521
+ const prefix = toolPrefix(entry.component);
522
+ const next = declared?.names.length ? { prefix, names: declared.names.map((name) => `${prefix}_${name}`), active: entry.active, enabled: declared.enabled } : null;
523
+ if (JSON.stringify(next) === JSON.stringify(entry.agentTools)) {
524
+ return;
525
+ }
526
+ entry.agentTools = next;
527
+ handles.get(entry.embedId)?.setAgentToolsState(next);
528
+ toolListeners.forEach((listener) => listener({ embedId: entry.embedId, agentTools: next }));
529
+ }
530
+ function setActive(embedId) {
531
+ const target = embeds.get(embedId);
532
+ if (!target) {
533
+ return;
534
+ }
535
+ for (const entry of embeds.values()) {
536
+ if (entry.component.appId === target.component.appId) {
537
+ entry.active = entry.embedId === embedId;
538
+ publishTools(entry);
539
+ }
540
+ }
541
+ notifyChange();
542
+ }
543
+ function setEmbedAgentTools(embedId, names, toolOptions = {}) {
544
+ const entry = embeds.get(embedId);
545
+ if (!entry) {
546
+ return;
547
+ }
548
+ toolNames.set(embedId, { names: [...names ?? []], enabled: toolOptions.enabled !== false });
549
+ publishTools(entry);
550
+ notifyChange();
551
+ }
512
552
  function closeEmbed(embedId, reason = "closed", result) {
513
553
  if (!embeds.delete(embedId)) {
514
554
  return;
@@ -520,6 +560,7 @@ function createMockEmbedHost(options, getDatabase) {
520
560
  };
521
561
  handles.get(embedId)?.settle(event);
522
562
  handles.delete(embedId);
563
+ toolNames.delete(embedId);
523
564
  closedListeners.forEach((listener) => listener(event));
524
565
  notifyChange();
525
566
  }
@@ -558,6 +599,39 @@ function createMockEmbedHost(options, getDatabase) {
558
599
  async save() {
559
600
  await saveEmbed(this.embedId);
560
601
  }
602
+ agentTools = null;
603
+ toolsListeners = /* @__PURE__ */ new Set();
604
+ async setActive() {
605
+ setActive(this.embedId);
606
+ }
607
+ onAgentToolsChange(listener) {
608
+ this.toolsListeners.add(listener);
609
+ return () => {
610
+ this.toolsListeners.delete(listener);
611
+ };
612
+ }
613
+ setAgentToolsState(tools) {
614
+ this.agentTools = tools;
615
+ this.toolsListeners.forEach((listener) => listener(tools));
616
+ }
617
+ waitForAgentTools(timeoutMs = 1e4) {
618
+ if (this.agentTools?.names.length) {
619
+ return Promise.resolve(this.agentTools);
620
+ }
621
+ return new Promise((resolve) => {
622
+ const timer = setTimeout(() => {
623
+ stop();
624
+ resolve(null);
625
+ }, timeoutMs);
626
+ const stop = this.onAgentToolsChange((tools) => {
627
+ if (tools?.names.length) {
628
+ clearTimeout(timer);
629
+ stop();
630
+ resolve(tools);
631
+ }
632
+ });
633
+ });
634
+ }
561
635
  resolve;
562
636
  listeners = /* @__PURE__ */ new Set();
563
637
  settled = null;
@@ -652,6 +726,9 @@ function createMockEmbedHost(options, getDatabase) {
652
726
  async setDirty(dirty) {
653
727
  embeddingDirty = dirty === true;
654
728
  },
729
+ async lookup(field2, lookupOptions) {
730
+ return structuredClone(await (options.embeddingLookup?.(field2, { ...lookupOptions }) ?? []));
731
+ },
655
732
  onSaveRequest(handler) {
656
733
  saveRequestHandler = handler;
657
734
  return () => {
@@ -684,6 +761,14 @@ function createMockEmbedHost(options, getDatabase) {
684
761
  };
685
762
  },
686
763
  setEmbedDirty,
764
+ setActive,
765
+ setEmbedAgentTools,
766
+ onAgentTools(listener) {
767
+ toolListeners.add(listener);
768
+ return () => {
769
+ toolListeners.delete(listener);
770
+ };
771
+ },
687
772
  onDirty(listener) {
688
773
  dirtyListeners.add(listener);
689
774
  return () => {
@@ -828,6 +913,44 @@ function guardDatabase(database, currentInfo, enforce) {
828
913
  };
829
914
  }
830
915
 
916
+ // src/testing/configuredViews.ts
917
+ var field = (path) => ({ kind: "field", path });
918
+ var literal = (value) => ({ kind: "literal", value });
919
+ var operation = (op, args) => ({ kind: "operation", op, args });
920
+ function ruleExpression(rule) {
921
+ const target = field(rule.field);
922
+ switch (rule.operator) {
923
+ case "notContains":
924
+ return operation("not", [operation("contains", [target, literal(rule.value ?? "")])]);
925
+ case "exists":
926
+ case "notExists":
927
+ return operation(rule.operator, [target]);
928
+ default:
929
+ return operation(rule.operator, [target, literal(rule.value ?? "")]);
930
+ }
931
+ }
932
+ function configuredViewDefinition(view2) {
933
+ const filter = view2.filter.mode === "formula" ? { mode: "expression", expression: structuredClone(view2.filter.expression) } : view2.filter.rules.length ? {
934
+ mode: "expression",
935
+ expression: view2.filter.rules.length === 1 ? ruleExpression(view2.filter.rules[0]) : operation(view2.filter.match === "all" ? "and" : "or", view2.filter.rules.map(ruleExpression))
936
+ } : void 0;
937
+ return {
938
+ id: view2.id,
939
+ title: view2.description?.trim() || view2.id,
940
+ ...filter ? { filter } : {},
941
+ defaultExpand: "collapsed",
942
+ columns: view2.columns.map((column) => ({
943
+ name: column.name,
944
+ title: column.title,
945
+ role: column.role === "sort" ? "display" : column.role,
946
+ expression: column.expression.mode === "field" ? field(column.expression.field) : structuredClone(column.expression.expression),
947
+ sorting: column.sorting,
948
+ hidden: column.hidden,
949
+ totalMode: column.totalMode
950
+ }))
951
+ };
952
+ }
953
+
831
954
  // src/testing/mockAgentTools.ts
832
955
  var TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]{0,47}$/;
833
956
  var PREFIX_PATTERN = /^[a-z][a-z0-9_]{0,15}$/;
@@ -877,11 +1000,15 @@ function validateMockAgentTools(raw) {
877
1000
  throw invalid(`Agent tool "${tool.name}" has invalid annotations.`);
878
1001
  }
879
1002
  }
1003
+ if (tool.scope !== void 0 && tool.scope !== "app" && tool.scope !== "document") {
1004
+ throw invalid(`Agent tool "${tool.name}" has an invalid scope; use "app" or "document".`);
1005
+ }
880
1006
  return {
881
1007
  name: tool.name,
882
1008
  description: tool.description,
883
1009
  inputSchema: schema,
884
- ...tool.annotations ? { annotations: { ...tool.annotations } } : {}
1010
+ ...tool.annotations ? { annotations: { ...tool.annotations } } : {},
1011
+ ...tool.scope ? { scope: tool.scope } : {}
885
1012
  };
886
1013
  });
887
1014
  }
@@ -934,7 +1061,7 @@ function createMockAgentHost(options) {
934
1061
  async function dispatch(method, params) {
935
1062
  const input = params ?? {};
936
1063
  if (method === "agent.registerTools") {
937
- tools = validateMockAgentTools(input.tools);
1064
+ tools = validateMockAgentTools(input.tools).filter((tool) => !options.embedded?.() || tool.scope === "document");
938
1065
  changed();
939
1066
  return { result: registration() };
940
1067
  }
@@ -1078,6 +1205,10 @@ function concat(chunks) {
1078
1205
  }
1079
1206
  return bytes;
1080
1207
  }
1208
+ var MEMORY_ATTACHMENTS = /* @__PURE__ */ Symbol.for("mindoodb-app-sdk.memoryAttachments");
1209
+ function isMemoryAttachments(value) {
1210
+ return !!value && typeof value === "object" && MEMORY_ATTACHMENTS in value;
1211
+ }
1081
1212
  function createMemoryAttachments() {
1082
1213
  const files = /* @__PURE__ */ new Map();
1083
1214
  const forDoc = (docId) => {
@@ -1085,7 +1216,7 @@ function createMemoryAttachments() {
1085
1216
  if (!map) files.set(docId, map = /* @__PURE__ */ new Map());
1086
1217
  return map;
1087
1218
  };
1088
- return {
1219
+ const api = {
1089
1220
  async list(docId) {
1090
1221
  return [...forDoc(docId)].map(
1091
1222
  ([fileName, file]) => ({
@@ -1132,6 +1263,131 @@ function createMemoryAttachments() {
1132
1263
  };
1133
1264
  }
1134
1265
  };
1266
+ return Object.assign(api, { [MEMORY_ATTACHMENTS]: true });
1267
+ }
1268
+
1269
+ // src/testing/twoUserTestHost.ts
1270
+ var DEFAULT_TEST_HOST_USERS = [
1271
+ { name: "Anna", username: "CN=Anna Test/O=Test" },
1272
+ { name: "Ben", username: "CN=Ben Test/O=Test" }
1273
+ ];
1274
+ var STYLE = `
1275
+ .htu{display:grid;grid-template-rows:auto 1fr;height:100vh;margin:0;font:13px/1.45 system-ui,sans-serif;background:#eef1f7;color:#172033}
1276
+ .htu *{box-sizing:border-box}
1277
+ .htu__bar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:8px 12px;border-bottom:1px solid #d7dce8;background:#f8f9fc}
1278
+ .htu__badge{padding:2px 8px;border-radius:999px;background:#fde68a;color:#713f12;font-weight:600;font-size:11px;letter-spacing:.04em}
1279
+ .htu button{font:inherit;padding:5px 12px;border-radius:7px;border:1px solid #c9d0e0;background:#fff;cursor:pointer}
1280
+ .htu__status{font-weight:600}
1281
+ .htu .htest__mode{display:flex;gap:8px;align-items:center;padding:0 8px;border-left:1px solid #d7dce8;border-right:1px solid #d7dce8}
1282
+ .htu__panes{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:#d7dce8;min-height:0}
1283
+ .htu__pane{display:grid;grid-template-rows:auto 1fr;min-height:0;background:#fff}
1284
+ .htu__pane h2{margin:0;padding:4px 10px;font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#5b6478;background:#f8f9fc;border-bottom:1px solid #e4e8f1}
1285
+ .htu iframe{width:100%;height:100%;border:0}
1286
+ @media (max-width:900px){.htu__panes{grid-template-columns:1fr;grid-template-rows:1fr 1fr}}
1287
+ `;
1288
+ function replicaOf(databases) {
1289
+ return databases.map((database) => ({
1290
+ ...database,
1291
+ automerge: true,
1292
+ info: structuredClone(database.info),
1293
+ documents: database.documents ? structuredClone(database.documents) : void 0,
1294
+ ...database.methods ? {
1295
+ methods: {
1296
+ ...database.methods,
1297
+ ...isMemoryAttachments(database.methods.attachments) ? { attachments: createMemoryAttachments() } : {}
1298
+ }
1299
+ } : {}
1300
+ }));
1301
+ }
1302
+ function mountTwoUserTestHost(options, createHost) {
1303
+ const { container = document.body, title = "Haven test host", users = DEFAULT_TEST_HOST_USERS, ...hostOptions } = options;
1304
+ const style = document.createElement("style");
1305
+ style.textContent = STYLE;
1306
+ document.head.append(style);
1307
+ if (container === document.body) document.body.style.margin = "0";
1308
+ const root = document.createElement("div");
1309
+ root.className = "htu";
1310
+ const bar = document.createElement("div");
1311
+ bar.className = "htu__bar";
1312
+ const badge = document.createElement("span");
1313
+ badge.className = "htu__badge";
1314
+ badge.textContent = `TEST HOST \xB7 TWO USERS \xB7 ${title}`;
1315
+ const syncButton = document.createElement("button");
1316
+ syncButton.type = "button";
1317
+ syncButton.textContent = "Sync";
1318
+ syncButton.dataset.testid = "haven-two-users-sync";
1319
+ const auto = document.createElement("label");
1320
+ const autoBox = document.createElement("input");
1321
+ autoBox.type = "checkbox";
1322
+ autoBox.checked = true;
1323
+ autoBox.dataset.testid = "haven-two-users-autosync";
1324
+ auto.append(autoBox, " Auto-sync every 2 s");
1325
+ const status = document.createElement("span");
1326
+ status.className = "htu__status";
1327
+ status.dataset.testid = "haven-two-users-status";
1328
+ bar.append(badge, modeSwitch("two"), syncButton, auto, status);
1329
+ const panes = document.createElement("div");
1330
+ panes.className = "htu__panes";
1331
+ root.append(bar, panes);
1332
+ container.replaceChildren(root);
1333
+ const databasesA = (hostOptions.databases ?? []).map((database) => ({ ...database, automerge: true }));
1334
+ const hosts = users.map((user, index) => {
1335
+ const pane = document.createElement("section");
1336
+ pane.className = "htu__pane";
1337
+ const heading = document.createElement("h2");
1338
+ heading.textContent = `${user.name} \u2014 replica ${index + 1}`;
1339
+ const frame = document.createElement("iframe");
1340
+ frame.title = `${title} as ${user.name}`;
1341
+ frame.dataset.testid = `haven-two-users-frame-${index + 1}`;
1342
+ pane.append(heading, frame);
1343
+ panes.append(pane);
1344
+ const launchContext = {
1345
+ ...hostOptions.launchContext,
1346
+ user: { id: `test-user-${index + 1}`, username: user.username }
1347
+ };
1348
+ return createHost({
1349
+ ...hostOptions,
1350
+ frame,
1351
+ launchContext,
1352
+ databases: index === 0 ? databasesA : replicaOf(databasesA),
1353
+ // distinct Automerge actors: the two replicas write concurrently
1354
+ automergeActor: index === 0 ? "a11ce000000000000000000000000001" : "b0b00000000000000000000000000002"
1355
+ });
1356
+ });
1357
+ let lastSync = null;
1358
+ const describe = () => {
1359
+ const open = mockReplicaDifferences(hosts[0].host, hosts[1].host);
1360
+ status.textContent = `${open ? `\u26A0 ${open} document(s) not synced` : "\u2713 in sync"} \xB7 last sync ${lastSync ?? "never"}`;
1361
+ status.style.color = open ? "#b45309" : "#15803d";
1362
+ };
1363
+ let syncing = null;
1364
+ const sync = () => {
1365
+ syncing ??= syncMockReplicas(hosts[0].host, hosts[1].host).finally(() => {
1366
+ syncing = null;
1367
+ lastSync = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1368
+ describe();
1369
+ });
1370
+ return syncing;
1371
+ };
1372
+ syncButton.addEventListener("click", () => void sync());
1373
+ let timer;
1374
+ const setAutoSync = (on) => {
1375
+ autoBox.checked = on;
1376
+ if (timer) clearInterval(timer);
1377
+ timer = on ? setInterval(() => void sync(), 2e3) : void 0;
1378
+ };
1379
+ autoBox.addEventListener("change", () => setAutoSync(autoBox.checked));
1380
+ setAutoSync(true);
1381
+ setInterval(describe, 500);
1382
+ describe();
1383
+ const handle = {
1384
+ hosts,
1385
+ sync,
1386
+ setAutoSync,
1387
+ differences: () => mockReplicaDifferences(hosts[0].host, hosts[1].host)
1388
+ };
1389
+ window.__havenTwoUsers = handle;
1390
+ return handle;
1135
1391
  }
1136
1392
 
1137
1393
  // src/testing/browserTestHost.ts
@@ -1170,6 +1426,7 @@ function mockDatabasesFromDefinition(definition, seed = {}, options = {}) {
1170
1426
  capabilities: override?.capabilities ? [...new Set(override.capabilities)] : capabilitiesForDefinitionPermissions(database.permissions)
1171
1427
  },
1172
1428
  documents: seed[database.logicalDatabaseId],
1429
+ ...options.automerge ? { automerge: true } : {},
1173
1430
  ...options.attachments === "none" ? {} : { methods: { attachments: createMemoryAttachments() } }
1174
1431
  };
1175
1432
  });
@@ -1190,6 +1447,8 @@ function readTestHostUrlSettings(search) {
1190
1447
  }
1191
1448
  const enforce = params.get("enforce");
1192
1449
  if (enforce !== null) settings.enforceCapabilities = enforce === "1" || enforce === "true";
1450
+ const automerge = params.get("automerge");
1451
+ if (automerge !== null) settings.automerge = automerge === "1" || automerge === "true";
1193
1452
  const users = Number.parseInt(params.get("users") ?? "", 10);
1194
1453
  if (Number.isFinite(users) && users >= 0) settings.directoryUserCount = users;
1195
1454
  const locale = params.get("locale");
@@ -1387,6 +1646,11 @@ function createBrowserTestHost(options) {
1387
1646
  host.setDirectoryUsers(users);
1388
1647
  onChange?.();
1389
1648
  },
1649
+ async applyRemoteUpdate(databaseId, docId, input, updateOptions) {
1650
+ const document2 = await host.applyRemoteUpdate(databaseId, docId, input, updateOptions);
1651
+ onChange?.();
1652
+ return document2;
1653
+ },
1390
1654
  reloadApp,
1391
1655
  dispose() {
1392
1656
  window.removeEventListener("message", onMessage);
@@ -1418,7 +1682,7 @@ var TEST_HOST_LOCALES = [
1418
1682
  { tag: "he-IL", label: "\u05E2\u05D1\u05E8\u05D9\u05EA" }
1419
1683
  ];
1420
1684
  var DIRECTORY_SIZES = [0, 5, 25, 120, 500];
1421
- var STYLE = `
1685
+ var STYLE2 = `
1422
1686
  .htest{display:grid;grid-template-columns:minmax(0,1fr) 320px;height:100vh;margin:0;font:13px/1.45 system-ui,sans-serif;background:#eef1f7;color:#172033}
1423
1687
  .htest *{box-sizing:border-box}
1424
1688
  .htest__frame{width:100%;height:100%;border:0;background:#fff}
@@ -1486,26 +1750,63 @@ function item(text) {
1486
1750
  li.textContent = text;
1487
1751
  return li;
1488
1752
  }
1753
+ function modeSwitch(current) {
1754
+ const row = document.createElement("div");
1755
+ row.className = "htest__row htest__mode";
1756
+ row.dataset.testid = "haven-test-mode";
1757
+ for (const [mode, label] of [
1758
+ ["single", "Single user"],
1759
+ ["two", "Two users"]
1760
+ ]) {
1761
+ const url = new URL(window.location.href);
1762
+ if (mode === "two") url.searchParams.set("twoUsers", "1");
1763
+ else url.searchParams.delete("twoUsers");
1764
+ if (mode === current) {
1765
+ const active = document.createElement("strong");
1766
+ active.textContent = label;
1767
+ active.setAttribute("aria-current", "page");
1768
+ row.append(active);
1769
+ } else {
1770
+ const link = document.createElement("a");
1771
+ link.href = url.toString();
1772
+ link.textContent = label;
1773
+ link.dataset.testid = `haven-test-mode-${mode}`;
1774
+ row.append(link);
1775
+ }
1776
+ }
1777
+ return row;
1778
+ }
1489
1779
  function mountHavenTestHost(options) {
1490
1780
  const {
1491
1781
  container = document.body,
1492
1782
  title = "Haven test host",
1493
1783
  locales = TEST_HOST_LOCALES,
1494
1784
  urlSettings: useUrl = true,
1785
+ twoUsers = false,
1786
+ users,
1495
1787
  ...hostOptions
1496
1788
  } = options;
1497
1789
  const url = useUrl ? readTestHostUrlSettings(window.location.search) : { overrides: {} };
1498
1790
  const databases = (hostOptions.databases ?? []).filter((database) => url.overrides[database.info.id]?.mapped !== false).map((database) => {
1499
1791
  const capabilities = url.overrides[database.info.id]?.capabilities;
1500
1792
  return capabilities ? { ...database, info: { ...database.info, capabilities } } : database;
1501
- });
1793
+ }).map((database) => url.automerge === void 0 ? database : { ...database, automerge: url.automerge });
1502
1794
  const launchContext = {
1503
1795
  ...hostOptions.launchContext,
1504
1796
  ...url.locale ? { locale: url.locale } : {},
1505
1797
  ...url.theme ? { theme: { mode: url.theme, preset: "mindoo" } } : {}
1506
1798
  };
1799
+ const twoUsersFromUrl = useUrl && new URLSearchParams(window.location.search).get("twoUsers") === "1";
1800
+ if (twoUsers || twoUsersFromUrl) {
1801
+ const handle = mountTwoUserTestHost(
1802
+ { ...hostOptions, databases, launchContext, container, title, ...users ? { users } : {} },
1803
+ createBrowserTestHost
1804
+ );
1805
+ window.__havenTestHost = handle.hosts[0];
1806
+ return handle.hosts[0];
1807
+ }
1507
1808
  const style = document.createElement("style");
1508
- style.textContent = STYLE;
1809
+ style.textContent = STYLE2;
1509
1810
  document.head.append(style);
1510
1811
  const root = document.createElement("div");
1511
1812
  root.className = "htest";
@@ -1592,8 +1893,8 @@ function mountHavenTestHost(options) {
1592
1893
  if (override.mapped === false) params.append("db", `${id}:none`);
1593
1894
  }
1594
1895
  if (testHost.host.getEnforceCapabilities()) params.set("enforce", "1");
1595
- const users = testHost.host.getDirectoryUsers().length;
1596
- if (users) params.set("users", String(users));
1896
+ const users2 = testHost.host.getDirectoryUsers().length;
1897
+ if (users2) params.set("users", String(users2));
1597
1898
  if (currentLocale) params.set("locale", currentLocale);
1598
1899
  if (initialDark === void 0 || dark !== initialDark) params.set("theme", dark ? "dark" : "light");
1599
1900
  const query = params.toString();
@@ -1723,6 +2024,7 @@ Allow?`),
1723
2024
  }),
1724
2025
  button("Reload app", () => testHost.reloadApp())
1725
2026
  );
2027
+ section("Mode", modeSwitch("single"));
1726
2028
  section("Host", hostRow, focusState);
1727
2029
  const language = document.createElement("select");
1728
2030
  language.dataset.testid = "haven-test-language";
@@ -2075,8 +2377,8 @@ function applyDocumentUpdatePatch(current, patch) {
2075
2377
  applyMockTextPatch(next, textPatch.path, textPatch.edits);
2076
2378
  }
2077
2379
  for (const richTextPatch of patch.richText ?? []) {
2078
- const spans = richTextPatch.spansSequence?.at(-1) ?? richTextPatch.spans ?? [];
2079
- setValueAtPath(next, richTextPatch.path, structuredClone(spans));
2380
+ const spans2 = richTextPatch.spansSequence?.at(-1) ?? richTextPatch.spans ?? [];
2381
+ setValueAtPath(next, richTextPatch.path, structuredClone(spans2));
2080
2382
  }
2081
2383
  for (const richTextStepPatch of patch.richTextSteps ?? []) {
2082
2384
  applyMockRichTextSteps(next, richTextStepPatch.path, richTextStepPatch.steps);
@@ -2087,50 +2389,50 @@ function applyMockJsonPatch(target, patch) {
2087
2389
  if (!patch) {
2088
2390
  return;
2089
2391
  }
2090
- for (const operation of patch.set ?? []) {
2091
- setValueAtPath(target, operation.path, readMockTypedValues(operation.value));
2392
+ for (const operation2 of patch.set ?? []) {
2393
+ setValueAtPath(target, operation2.path, readMockTypedValues(operation2.value));
2092
2394
  }
2093
- for (const operation of patch.unset ?? []) {
2094
- unsetValueAtPath(target, operation.path);
2395
+ for (const operation2 of patch.unset ?? []) {
2396
+ unsetValueAtPath(target, operation2.path);
2095
2397
  }
2096
- for (const operation of patch.listDelete ?? []) {
2097
- const list = readListAtPath(target, operation.path);
2098
- list.splice(operation.index, operation.deleteCount);
2398
+ for (const operation2 of patch.listDelete ?? []) {
2399
+ const list = readListAtPath(target, operation2.path);
2400
+ list.splice(operation2.index, operation2.deleteCount);
2099
2401
  }
2100
- for (const operation of patch.listInsert ?? []) {
2101
- const parent = ensureParentAtPath(target, operation.path);
2102
- const leaf = operation.path[operation.path.length - 1];
2402
+ for (const operation2 of patch.listInsert ?? []) {
2403
+ const parent = ensureParentAtPath(target, operation2.path);
2404
+ const leaf = operation2.path[operation2.path.length - 1];
2103
2405
  if (parent[leaf] == null) {
2104
2406
  parent[leaf] = [];
2105
2407
  }
2106
- const list = readListAtPath(target, operation.path);
2107
- list.splice(operation.index, 0, ...readMockTypedValues(operation.values));
2408
+ const list = readListAtPath(target, operation2.path);
2409
+ list.splice(operation2.index, 0, ...readMockTypedValues(operation2.values));
2108
2410
  }
2109
- for (const operation of patch.textSplice ?? []) {
2110
- applyMockTextPatch(target, operation.path, [{
2111
- index: operation.index,
2112
- deleteCount: operation.deleteCount,
2113
- ...operation.insert !== void 0 ? { insert: operation.insert } : {}
2411
+ for (const operation2 of patch.textSplice ?? []) {
2412
+ applyMockTextPatch(target, operation2.path, [{
2413
+ index: operation2.index,
2414
+ deleteCount: operation2.deleteCount,
2415
+ ...operation2.insert !== void 0 ? { insert: operation2.insert } : {}
2114
2416
  }]);
2115
2417
  }
2116
- for (const operation of patch.textMark ?? []) {
2117
- applyMockTextMarks(target, operation.path, operation.index, operation.length, operation.marks, false);
2418
+ for (const operation2 of patch.textMark ?? []) {
2419
+ applyMockTextMarks(target, operation2.path, operation2.index, operation2.length, operation2.marks, false);
2118
2420
  }
2119
- for (const operation of patch.textUnmark ?? []) {
2120
- applyMockTextMarks(target, operation.path, operation.index, operation.length, Object.fromEntries(operation.names.map((name) => [name, null])), true);
2421
+ for (const operation2 of patch.textUnmark ?? []) {
2422
+ applyMockTextMarks(target, operation2.path, operation2.index, operation2.length, Object.fromEntries(operation2.names.map((name) => [name, null])), true);
2121
2423
  }
2122
- for (const operation of patch.counterIncrement ?? []) {
2123
- assertMockCounterAmount(operation.delta, "JSON counterIncrement delta");
2124
- const parent = ensureParentAtPath(target, operation.path);
2125
- const leaf = operation.path[operation.path.length - 1];
2424
+ for (const operation2 of patch.counterIncrement ?? []) {
2425
+ assertMockCounterAmount(operation2.delta, "JSON counterIncrement delta");
2426
+ const parent = ensureParentAtPath(target, operation2.path);
2427
+ const leaf = operation2.path[operation2.path.length - 1];
2126
2428
  const current = parent[leaf];
2127
2429
  if (current == null) {
2128
- parent[leaf] = operation.delta;
2430
+ parent[leaf] = operation2.delta;
2129
2431
  } else if (typeof current === "number") {
2130
- parent[leaf] = current + operation.delta;
2432
+ parent[leaf] = current + operation2.delta;
2131
2433
  } else {
2132
2434
  throw new Error(
2133
- `Cannot increment non-counter value at ${operation.path.map(String).join(".")}`
2435
+ `Cannot increment non-counter value at ${operation2.path.map(String).join(".")}`
2134
2436
  );
2135
2437
  }
2136
2438
  }
@@ -2343,23 +2645,23 @@ function mergeLaunchContext(current, patch) {
2343
2645
  ...database,
2344
2646
  capabilities: [...database.capabilities]
2345
2647
  })),
2346
- views: current.views.map((view) => ({
2347
- ...view,
2348
- sources: view.sources.map((source) => ({ ...source })),
2349
- columns: view.columns.map((column) => ({
2648
+ views: current.views.map((view2) => ({
2649
+ ...view2,
2650
+ sources: view2.sources.map((source) => ({ ...source })),
2651
+ columns: view2.columns.map((column) => ({
2350
2652
  ...column,
2351
2653
  expression: column.expression.mode === "field" ? { ...column.expression } : {
2352
2654
  mode: "formula",
2353
2655
  expression: structuredClone(column.expression.expression)
2354
2656
  }
2355
2657
  })),
2356
- filter: view.filter.mode === "rules" ? {
2658
+ filter: view2.filter.mode === "rules" ? {
2357
2659
  mode: "rules",
2358
- match: view.filter.match,
2359
- rules: view.filter.rules.map((rule) => ({ ...rule }))
2660
+ match: view2.filter.match,
2661
+ rules: view2.filter.rules.map((rule) => ({ ...rule }))
2360
2662
  } : {
2361
2663
  mode: "formula",
2362
- expression: structuredClone(view.filter.expression)
2664
+ expression: structuredClone(view2.filter.expression)
2363
2665
  }
2364
2666
  }))
2365
2667
  };
@@ -2381,41 +2683,41 @@ function mergeLaunchContext(current, patch) {
2381
2683
  ...database,
2382
2684
  capabilities: [...database.capabilities]
2383
2685
  })),
2384
- views: patch.views ? patch.views.map((view) => ({
2385
- ...view,
2386
- sources: view.sources.map((source) => ({ ...source })),
2387
- columns: view.columns.map((column) => ({
2686
+ views: patch.views ? patch.views.map((view2) => ({
2687
+ ...view2,
2688
+ sources: view2.sources.map((source) => ({ ...source })),
2689
+ columns: view2.columns.map((column) => ({
2388
2690
  ...column,
2389
2691
  expression: column.expression.mode === "field" ? { ...column.expression } : {
2390
2692
  mode: "formula",
2391
2693
  expression: structuredClone(column.expression.expression)
2392
2694
  }
2393
2695
  })),
2394
- filter: view.filter.mode === "rules" ? {
2696
+ filter: view2.filter.mode === "rules" ? {
2395
2697
  mode: "rules",
2396
- match: view.filter.match,
2397
- rules: view.filter.rules.map((rule) => ({ ...rule }))
2698
+ match: view2.filter.match,
2699
+ rules: view2.filter.rules.map((rule) => ({ ...rule }))
2398
2700
  } : {
2399
2701
  mode: "formula",
2400
- expression: structuredClone(view.filter.expression)
2702
+ expression: structuredClone(view2.filter.expression)
2401
2703
  }
2402
- })) : current.views.map((view) => ({
2403
- ...view,
2404
- sources: view.sources.map((source) => ({ ...source })),
2405
- columns: view.columns.map((column) => ({
2704
+ })) : current.views.map((view2) => ({
2705
+ ...view2,
2706
+ sources: view2.sources.map((source) => ({ ...source })),
2707
+ columns: view2.columns.map((column) => ({
2406
2708
  ...column,
2407
2709
  expression: column.expression.mode === "field" ? { ...column.expression } : {
2408
2710
  mode: "formula",
2409
2711
  expression: structuredClone(column.expression.expression)
2410
2712
  }
2411
2713
  })),
2412
- filter: view.filter.mode === "rules" ? {
2714
+ filter: view2.filter.mode === "rules" ? {
2413
2715
  mode: "rules",
2414
- match: view.filter.match,
2415
- rules: view.filter.rules.map((rule) => ({ ...rule }))
2716
+ match: view2.filter.match,
2717
+ rules: view2.filter.rules.map((rule) => ({ ...rule }))
2416
2718
  } : {
2417
2719
  mode: "formula",
2418
- expression: structuredClone(view.filter.expression)
2720
+ expression: structuredClone(view2.filter.expression)
2419
2721
  }
2420
2722
  }))
2421
2723
  };
@@ -2506,9 +2808,12 @@ function mockDecryptionKeyId(document2) {
2506
2808
  }
2507
2809
  return { decryptionKeyId: document2.decryptionKeyId };
2508
2810
  }
2509
- function loadMockAutomergeDocument(document2) {
2811
+ function loadMockAutomergeDocument(document2, actor) {
2510
2812
  if (document2.automergeBinary) {
2511
- return Automerge.load(document2.automergeBinary);
2813
+ return Automerge.load(
2814
+ document2.automergeBinary,
2815
+ actor ? { actor } : void 0
2816
+ );
2512
2817
  }
2513
2818
  const initialDoc = Automerge.from({
2514
2819
  ...structuredClone(document2.data),
@@ -2519,11 +2824,646 @@ function loadMockAutomergeDocument(document2) {
2519
2824
  }
2520
2825
  function persistMockAutomergeDocument(document2, automergeDoc) {
2521
2826
  document2.automergeBinary = Automerge.save(automergeDoc);
2522
- document2.data = structuredClone(automergeDoc);
2827
+ document2.data = projectMockAutomergeValue(
2828
+ Automerge.load(document2.automergeBinary)
2829
+ );
2523
2830
  document2.heads = Automerge.getHeads(automergeDoc);
2524
2831
  }
2525
- function getFieldValue2(source, field) {
2526
- return field.split(".").reduce((current, part) => {
2832
+ var MOCK_AUTOMERGE_SEED_ACTOR = "5eed0000000000000000000000000000";
2833
+ var MOCK_AUTOMERGE_LOCAL_ACTOR = "10ca1000000000000000000000000000";
2834
+ var MOCK_AUTOMERGE_REMOTE_ACTOR = "2e307e00000000000000000000000000";
2835
+ function projectMockAutomergeValue(value, depth = 0) {
2836
+ if (value === null || typeof value !== "object") {
2837
+ return value;
2838
+ }
2839
+ if (Automerge.isCounter(value)) {
2840
+ return value.value;
2841
+ }
2842
+ if (Automerge.isImmutableString(value)) {
2843
+ return String(value);
2844
+ }
2845
+ if (value instanceof Date) {
2846
+ return value.toISOString();
2847
+ }
2848
+ if (value instanceof Uint8Array) {
2849
+ return value.slice();
2850
+ }
2851
+ if (depth > 100) {
2852
+ throw new Error("Document value is nested too deeply");
2853
+ }
2854
+ if (Array.isArray(value)) {
2855
+ return value.map((entry) => projectMockAutomergeValue(entry, depth + 1));
2856
+ }
2857
+ return Object.fromEntries(
2858
+ Object.entries(value).map(([key, entry]) => [
2859
+ key,
2860
+ projectMockAutomergeValue(entry, depth + 1)
2861
+ ])
2862
+ );
2863
+ }
2864
+ function hydrateMockAutomergeValues(value, depth = 0) {
2865
+ if (value === null || typeof value !== "object") {
2866
+ return value;
2867
+ }
2868
+ if (depth > 100) {
2869
+ throw new Error("Document value is nested too deeply");
2870
+ }
2871
+ if (value instanceof Uint8Array || value instanceof Date || Automerge.isCounter(value) || Automerge.isImmutableString(value)) {
2872
+ return value;
2873
+ }
2874
+ if (Array.isArray(value)) {
2875
+ return value.map(
2876
+ (entry) => entry === void 0 ? null : hydrateMockAutomergeValues(entry, depth + 1)
2877
+ );
2878
+ }
2879
+ const record = value;
2880
+ if (Object.prototype.hasOwnProperty.call(record, MINDOODB_APP_VALUE_TAG)) {
2881
+ switch (record[MINDOODB_APP_VALUE_TAG]) {
2882
+ case "atomic":
2883
+ if (typeof record.value !== "string") {
2884
+ throw new Error('Typed value "atomic" needs a string value');
2885
+ }
2886
+ return new Automerge.ImmutableString(record.value);
2887
+ case "counter":
2888
+ assertMockCounterAmount(record.value, 'Typed value "counter"');
2889
+ return new Automerge.Counter(record.value);
2890
+ case "timestamp":
2891
+ return new Date(toMockTimestamp(record.value));
2892
+ default:
2893
+ throw new Error(
2894
+ `Unknown typed value ${JSON.stringify(record[MINDOODB_APP_VALUE_TAG])}; the "${MINDOODB_APP_VALUE_TAG}" key is reserved`
2895
+ );
2896
+ }
2897
+ }
2898
+ const result = {};
2899
+ for (const [key, entry] of Object.entries(record)) {
2900
+ if (entry !== void 0) {
2901
+ result[key] = hydrateMockAutomergeValues(entry, depth + 1);
2902
+ }
2903
+ }
2904
+ return result;
2905
+ }
2906
+ function createMockAutomergeDocument(values, actor, time) {
2907
+ const entries = Object.entries(values).filter(([, value]) => value !== void 0);
2908
+ const initial = Automerge.init({ actor });
2909
+ const options = time === void 0 ? {} : { time };
2910
+ if (entries.length === 0) {
2911
+ return Automerge.emptyChange(initial, options);
2912
+ }
2913
+ return Automerge.change(initial, options, (doc) => {
2914
+ for (const [key, value] of entries) {
2915
+ doc[key] = hydrateMockAutomergeValues(value);
2916
+ }
2917
+ });
2918
+ }
2919
+ function validateMockJsonPath(path, label) {
2920
+ if (!Array.isArray(path) || path.length === 0) {
2921
+ throw new Error(`${label} path must contain at least one segment`);
2922
+ }
2923
+ for (const segment of path) {
2924
+ if (typeof segment !== "string" && typeof segment !== "number") {
2925
+ throw new Error(`${label} path segments must be strings or numbers`);
2926
+ }
2927
+ }
2928
+ }
2929
+ function assertMockNonNegativeInteger(value, label) {
2930
+ if (!Number.isInteger(value) || value < 0) {
2931
+ throw new Error(`${label} must be a non-negative integer`);
2932
+ }
2933
+ }
2934
+ function validateMockJsonPatch(patch) {
2935
+ 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);
2936
+ if (operationCount === 0) {
2937
+ throw new Error("JSON patch must include at least one operation");
2938
+ }
2939
+ for (const operation2 of patch.set ?? []) {
2940
+ validateMockJsonPath(operation2.path, "JSON set");
2941
+ hydrateMockAutomergeValues(operation2.value);
2942
+ }
2943
+ for (const operation2 of patch.unset ?? []) {
2944
+ validateMockJsonPath(operation2.path, "JSON unset");
2945
+ }
2946
+ for (const operation2 of patch.listDelete ?? []) {
2947
+ validateMockJsonPath(operation2.path, "JSON listDelete");
2948
+ assertMockNonNegativeInteger(operation2.index, "JSON listDelete index");
2949
+ assertMockNonNegativeInteger(operation2.deleteCount, "JSON listDelete deleteCount");
2950
+ }
2951
+ for (const operation2 of patch.listInsert ?? []) {
2952
+ validateMockJsonPath(operation2.path, "JSON listInsert");
2953
+ assertMockNonNegativeInteger(operation2.index, "JSON listInsert index");
2954
+ if (!Array.isArray(operation2.values)) {
2955
+ throw new Error("JSON listInsert values must be an array");
2956
+ }
2957
+ hydrateMockAutomergeValues(operation2.values);
2958
+ }
2959
+ for (const operation2 of patch.textSplice ?? []) {
2960
+ validateMockJsonPath(operation2.path, "JSON textSplice");
2961
+ assertMockNonNegativeInteger(operation2.index, "JSON textSplice index");
2962
+ assertMockNonNegativeInteger(operation2.deleteCount, "JSON textSplice deleteCount");
2963
+ if (operation2.insert !== void 0 && typeof operation2.insert !== "string") {
2964
+ throw new Error("JSON textSplice insert must be a string");
2965
+ }
2966
+ }
2967
+ for (const operation2 of [...patch.textMark ?? [], ...patch.textUnmark ?? []]) {
2968
+ validateMockJsonPath(operation2.path, "JSON text mark");
2969
+ assertMockNonNegativeInteger(operation2.index, "JSON text mark index");
2970
+ if (!Number.isInteger(operation2.length) || operation2.length <= 0) {
2971
+ throw new Error("JSON text mark length must be a positive integer");
2972
+ }
2973
+ }
2974
+ for (const operation2 of patch.counterIncrement ?? []) {
2975
+ validateMockJsonPath(operation2.path, "JSON counterIncrement");
2976
+ assertMockCounterAmount(operation2.delta, "JSON counterIncrement delta");
2977
+ }
2978
+ }
2979
+ function ensureMockAutomergeParent(target, path) {
2980
+ let parent = target;
2981
+ for (let index = 0; index < path.length - 1; index += 1) {
2982
+ const segment = path[index];
2983
+ const nextSegment = path[index + 1];
2984
+ if (parent[segment] === void 0 || parent[segment] === null) {
2985
+ parent[segment] = typeof nextSegment === "number" ? [] : {};
2986
+ }
2987
+ parent = parent[segment];
2988
+ if (parent === null || typeof parent !== "object") {
2989
+ throw new Error(
2990
+ `Cannot apply JSON patch through non-object path segment '${String(segment)}'`
2991
+ );
2992
+ }
2993
+ }
2994
+ return parent;
2995
+ }
2996
+ function readMockAutomergeValue(target, path) {
2997
+ let value = target;
2998
+ for (const segment of path) {
2999
+ value = value?.[segment];
3000
+ }
3001
+ return value;
3002
+ }
3003
+ function setMockAutomergeValue(target, path, value) {
3004
+ const parent = ensureMockAutomergeParent(target, path);
3005
+ parent[path[path.length - 1]] = value;
3006
+ }
3007
+ function unsetMockAutomergeValue(target, path) {
3008
+ let parent = target;
3009
+ for (let index = 0; index < path.length - 1; index += 1) {
3010
+ parent = parent?.[path[index]];
3011
+ if (parent === null || typeof parent !== "object") {
3012
+ return;
3013
+ }
3014
+ }
3015
+ delete parent[path[path.length - 1]];
3016
+ }
3017
+ function readMockAutomergeList(target, path) {
3018
+ const value = readMockAutomergeValue(target, path);
3019
+ if (!Array.isArray(value)) {
3020
+ throw new Error(
3021
+ `Cannot apply JSON list operation to non-array value at '${path.map(String).join(".")}'`
3022
+ );
3023
+ }
3024
+ return value;
3025
+ }
3026
+ function ensureMockAutomergeList(target, path) {
3027
+ const parent = ensureMockAutomergeParent(target, path);
3028
+ const leaf = path[path.length - 1];
3029
+ if (parent[leaf] === void 0 || parent[leaf] === null) {
3030
+ parent[leaf] = [];
3031
+ }
3032
+ return readMockAutomergeList(target, path);
3033
+ }
3034
+ function clampMockIndex(index, length) {
3035
+ if (!Number.isFinite(index)) {
3036
+ return length;
3037
+ }
3038
+ return Math.max(0, Math.min(Math.trunc(index), length));
3039
+ }
3040
+ function ensureMockAutomergeText(target, path, label) {
3041
+ const value = readMockAutomergeValue(target, path);
3042
+ if (value === void 0 || value === null) {
3043
+ setMockAutomergeValue(target, path, "");
3044
+ } else if (typeof value !== "string") {
3045
+ throw new Error(
3046
+ `Cannot apply ${label} to non-string value at '${path.map(String).join(".")}'`
3047
+ );
3048
+ }
3049
+ }
3050
+ function spliceMockAutomergeText(target, path, index, deleteCount, insert) {
3051
+ const text = readMockAutomergeValue(target, path);
3052
+ const length = typeof text === "string" ? text.length : 0;
3053
+ const start = clampMockIndex(index, length);
3054
+ Automerge.splice(
3055
+ target,
3056
+ path,
3057
+ start,
3058
+ Math.max(0, Math.min(deleteCount, length - start)),
3059
+ insert
3060
+ );
3061
+ }
3062
+ function reviveMockRichTextValue(value) {
3063
+ if (Array.isArray(value)) {
3064
+ return value.map((entry) => reviveMockRichTextValue(entry));
3065
+ }
3066
+ if (!value || typeof value !== "object") {
3067
+ return value;
3068
+ }
3069
+ const record = value;
3070
+ if (record.type === "immutableString" && typeof record.value === "string") {
3071
+ return new Automerge.ImmutableString(record.value);
3072
+ }
3073
+ return Object.fromEntries(
3074
+ Object.entries(record).map(([key, entry]) => [key, reviveMockRichTextValue(entry)])
3075
+ );
3076
+ }
3077
+ function applyMockAutomergeJsonOperations(target, patch) {
3078
+ for (const operation2 of patch.set ?? []) {
3079
+ setMockAutomergeValue(target, operation2.path, hydrateMockAutomergeValues(operation2.value));
3080
+ }
3081
+ for (const operation2 of patch.unset ?? []) {
3082
+ unsetMockAutomergeValue(target, operation2.path);
3083
+ }
3084
+ for (const operation2 of patch.listDelete ?? []) {
3085
+ readMockAutomergeList(target, operation2.path).splice(operation2.index, operation2.deleteCount);
3086
+ }
3087
+ for (const operation2 of patch.listInsert ?? []) {
3088
+ ensureMockAutomergeList(target, operation2.path).splice(
3089
+ operation2.index,
3090
+ 0,
3091
+ ...hydrateMockAutomergeValues(operation2.values)
3092
+ );
3093
+ }
3094
+ for (const operation2 of patch.textSplice ?? []) {
3095
+ ensureMockAutomergeText(target, operation2.path, "JSON textSplice");
3096
+ spliceMockAutomergeText(
3097
+ target,
3098
+ operation2.path,
3099
+ operation2.index,
3100
+ operation2.deleteCount,
3101
+ operation2.insert ?? ""
3102
+ );
3103
+ }
3104
+ for (const operation2 of patch.textMark ?? []) {
3105
+ for (const [name, value] of Object.entries(operation2.marks)) {
3106
+ Automerge.mark(
3107
+ target,
3108
+ operation2.path,
3109
+ { start: operation2.index, end: operation2.index + operation2.length, expand: "none" },
3110
+ name,
3111
+ reviveMockRichTextValue(value)
3112
+ );
3113
+ }
3114
+ }
3115
+ for (const operation2 of patch.textUnmark ?? []) {
3116
+ for (const name of operation2.names) {
3117
+ Automerge.unmark(
3118
+ target,
3119
+ operation2.path,
3120
+ { start: operation2.index, end: operation2.index + operation2.length, expand: "none" },
3121
+ name
3122
+ );
3123
+ }
3124
+ }
3125
+ for (const operation2 of patch.counterIncrement ?? []) {
3126
+ const parent = ensureMockAutomergeParent(target, operation2.path);
3127
+ const leaf = operation2.path[operation2.path.length - 1];
3128
+ const existing = parent[leaf];
3129
+ if (existing === void 0 || existing === null) {
3130
+ parent[leaf] = new Automerge.Counter(operation2.delta);
3131
+ } else if (Automerge.isCounter(existing)) {
3132
+ existing.increment(operation2.delta);
3133
+ } else {
3134
+ throw new Error(
3135
+ `Cannot increment non-counter value at '${operation2.path.map(String).join(".")}'; create it with MindooValue.counter() first`
3136
+ );
3137
+ }
3138
+ }
3139
+ }
3140
+ function changeMockAutomergeDocument(doc, baseHeads, apply) {
3141
+ if (baseHeads && baseHeads.length > 0) {
3142
+ const { newDoc } = Automerge.changeAt(doc, baseHeads, apply);
3143
+ return Automerge.load(Automerge.save(newDoc), { actor: Automerge.getActorId(doc) });
3144
+ }
3145
+ return Automerge.change(doc, apply);
3146
+ }
3147
+ function reviveMockRichTextSpans(spans2) {
3148
+ return spans2.map(
3149
+ (span) => span.type === "text" ? {
3150
+ type: "text",
3151
+ value: span.value,
3152
+ marks: span.marks ? reviveMockRichTextValue(span.marks) : void 0
3153
+ } : { type: "block", value: reviveMockRichTextValue(span.value) }
3154
+ );
3155
+ }
3156
+ function dehydrateMockRichTextValue(value) {
3157
+ if (Automerge.isImmutableString(value)) {
3158
+ return { type: "immutableString", value: String(value) };
3159
+ }
3160
+ if (Array.isArray(value)) {
3161
+ return value.map((entry) => dehydrateMockRichTextValue(entry));
3162
+ }
3163
+ if (!value || typeof value !== "object" || value instanceof Date || value instanceof Uint8Array) {
3164
+ return value;
3165
+ }
3166
+ return Object.fromEntries(
3167
+ Object.entries(value).map(([key, entry]) => [key, dehydrateMockRichTextValue(entry)])
3168
+ );
3169
+ }
3170
+ function validateMockRichTextSpans(spans2) {
3171
+ if (!Array.isArray(spans2)) {
3172
+ throw new Error("Rich-text patch spans must be an array");
3173
+ }
3174
+ for (const span of spans2) {
3175
+ if (!span || typeof span !== "object") {
3176
+ throw new Error("Rich-text span must be an object");
3177
+ }
3178
+ if (span.type === "text") {
3179
+ if (typeof span.value !== "string") {
3180
+ throw new Error("Rich-text text span value must be a string");
3181
+ }
3182
+ continue;
3183
+ }
3184
+ if (span.type === "block") {
3185
+ if (!span.value || typeof span.value !== "object" || Array.isArray(span.value)) {
3186
+ throw new Error("Rich-text block span value must be an object");
3187
+ }
3188
+ continue;
3189
+ }
3190
+ throw new Error("Rich-text span type must be 'text' or 'block'");
3191
+ }
3192
+ }
3193
+ function validateMockRichTextPatch(patch) {
3194
+ validateMockJsonPath(patch.path, "Rich-text patch");
3195
+ const hasSpans = patch.spans !== void 0;
3196
+ const hasSequence = patch.spansSequence !== void 0;
3197
+ if (hasSpans === hasSequence) {
3198
+ throw new Error("Rich-text patch must include exactly one of spans or spansSequence");
3199
+ }
3200
+ if (hasSpans) {
3201
+ validateMockRichTextSpans(patch.spans);
3202
+ return;
3203
+ }
3204
+ if (!Array.isArray(patch.spansSequence) || patch.spansSequence.length === 0) {
3205
+ throw new Error("Rich-text patch spansSequence must be a non-empty array");
3206
+ }
3207
+ for (const spans2 of patch.spansSequence) {
3208
+ validateMockRichTextSpans(spans2);
3209
+ }
3210
+ }
3211
+ function validateMockRichTextStepPatch(patch) {
3212
+ validateMockJsonPath(patch.path, "Rich-text steps patch");
3213
+ if (!Array.isArray(patch.steps) || patch.steps.length === 0) {
3214
+ throw new Error("Rich-text steps patch steps must be a non-empty array");
3215
+ }
3216
+ for (const step of patch.steps) {
3217
+ if (!step || typeof step !== "object") {
3218
+ throw new Error("Rich-text step must be an object");
3219
+ }
3220
+ if (step.type !== "splice") {
3221
+ throw new Error("Rich-text step type must be 'splice'");
3222
+ }
3223
+ if (!Number.isInteger(step.index) || step.index < 0) {
3224
+ throw new Error("Rich-text splice step index must be a non-negative integer");
3225
+ }
3226
+ if (!Number.isInteger(step.deleteCount) || step.deleteCount < 0) {
3227
+ throw new Error("Rich-text splice step deleteCount must be a non-negative integer");
3228
+ }
3229
+ if (step.insert !== void 0 && typeof step.insert !== "string") {
3230
+ throw new Error("Rich-text splice step insert must be a string");
3231
+ }
3232
+ for (const range of step.marks ?? []) {
3233
+ if (!Number.isInteger(range.index) || range.index < 0) {
3234
+ throw new Error("Rich-text mark range index must be a non-negative integer");
3235
+ }
3236
+ if (!Number.isInteger(range.length) || range.length <= 0) {
3237
+ throw new Error("Rich-text mark range length must be a positive integer");
3238
+ }
3239
+ if (!range.marks || typeof range.marks !== "object" || Array.isArray(range.marks)) {
3240
+ throw new Error("Rich-text mark range marks must be an object");
3241
+ }
3242
+ }
3243
+ }
3244
+ }
3245
+ function ensureMockRichTextPath(target, path) {
3246
+ let node = target;
3247
+ for (let index = 0; index < path.length - 1; index += 1) {
3248
+ const segment = path[index];
3249
+ if (node[segment] === void 0 || node[segment] === null) {
3250
+ node[segment] = typeof path[index + 1] === "number" ? [] : {};
3251
+ }
3252
+ node = node[segment];
3253
+ if (node === null || typeof node !== "object") {
3254
+ throw new Error(`Cannot apply rich-text patch through non-object path segment '${String(segment)}'`);
3255
+ }
3256
+ }
3257
+ const leaf = path[path.length - 1];
3258
+ const current = node[leaf];
3259
+ if (current === void 0 || current === null) {
3260
+ node[leaf] = "";
3261
+ return;
3262
+ }
3263
+ if (typeof current !== "string") {
3264
+ throw new Error(`Cannot apply rich-text patch to non-string value at '${path.map(String).join(".")}'`);
3265
+ }
3266
+ }
3267
+ function changeMockAutomergeDocumentOnFork(doc, baseHeads, apply) {
3268
+ const current = Automerge.getHeads(doc);
3269
+ if (!baseHeads || baseHeads.length === 0 || current.length === baseHeads.length && current.every((head) => baseHeads.includes(head))) {
3270
+ return Automerge.change(doc, apply);
3271
+ }
3272
+ const base = Automerge.view(doc, baseHeads);
3273
+ const actor = Automerge.getActorId(doc);
3274
+ const ownChangeSince = Automerge.getChanges(base, doc).some(
3275
+ (change2) => Automerge.decodeChange(change2).actor === actor
3276
+ );
3277
+ const fork = Automerge.clone(base, ownChangeSince ? void 0 : { actor });
3278
+ return Automerge.merge(doc, Automerge.change(fork, apply));
3279
+ }
3280
+ function isMockUpdateSpansBoundsError(error) {
3281
+ const message = error instanceof Error ? error.message : String(error);
3282
+ return /Cannot updateSpans|out of bounds/i.test(message);
3283
+ }
3284
+ function readMockAutomergeRichText(document2, path) {
3285
+ validateMockJsonPath(path, "Rich-text snapshot");
3286
+ const doc = loadMockAutomergeDocument(document2);
3287
+ return Automerge.spans(doc, path).map((span) => {
3288
+ const record = span;
3289
+ return record.type === "text" ? {
3290
+ type: "text",
3291
+ value: typeof record.value === "string" ? record.value : "",
3292
+ marks: record.marks ? dehydrateMockRichTextValue(record.marks) : void 0
3293
+ } : {
3294
+ type: "block",
3295
+ value: dehydrateMockRichTextValue(record.value)
3296
+ };
3297
+ });
3298
+ }
3299
+ function commonMockBaseHeads(patch) {
3300
+ const all = [
3301
+ patch.json?.baseHeads,
3302
+ ...(patch.text ?? []).map((p) => p.baseHeads),
3303
+ ...(patch.richText ?? []).map((p) => p.baseHeads),
3304
+ ...(patch.richTextSteps ?? []).map((p) => p.baseHeads)
3305
+ ].filter((heads) => Array.isArray(heads) && heads.length > 0);
3306
+ if (all.length === 0) return void 0;
3307
+ const first = [...all[0]].sort().join(",");
3308
+ return all.every((heads) => [...heads].sort().join(",") === first) ? all[0] : null;
3309
+ }
3310
+ function applyMockRichTextSpansInChange(target, richTextPatch) {
3311
+ const sequence = richTextPatch.spansSequence ?? (richTextPatch.spans ? [richTextPatch.spans] : []);
3312
+ const config = richTextPatch.updateSpansConfig;
3313
+ ensureMockRichTextPath(target, richTextPatch.path);
3314
+ const path = richTextPatch.path;
3315
+ for (const spans2 of sequence) {
3316
+ const revived = reviveMockRichTextSpans(spans2);
3317
+ try {
3318
+ Automerge.updateSpans(target, path, revived, config);
3319
+ } catch (error) {
3320
+ if (!isMockUpdateSpansBoundsError(error)) {
3321
+ throw error;
3322
+ }
3323
+ setMockAutomergeValue(target, richTextPatch.path, "");
3324
+ Automerge.updateSpans(target, path, revived, config);
3325
+ }
3326
+ }
3327
+ }
3328
+ function applyMockRichTextStepsInChange(target, stepPatch) {
3329
+ ensureMockRichTextPath(target, stepPatch.path);
3330
+ const path = stepPatch.path;
3331
+ for (const step of stepPatch.steps) {
3332
+ Automerge.splice(target, path, step.index, step.deleteCount, step.insert ?? "");
3333
+ for (const range of step.marks ?? []) {
3334
+ for (const [name, value] of Object.entries(range.marks)) {
3335
+ Automerge.mark(
3336
+ target,
3337
+ path,
3338
+ { start: range.index, end: range.index + range.length, expand: "none" },
3339
+ name,
3340
+ reviveMockRichTextValue(value)
3341
+ );
3342
+ }
3343
+ }
3344
+ }
3345
+ }
3346
+ function applyMockAutomergeUpdate(doc, patch) {
3347
+ const common = commonMockBaseHeads(patch);
3348
+ if (common !== null) {
3349
+ if (patch.json) validateMockJsonPatch(patch.json);
3350
+ for (const textPatch of patch.text ?? []) validateMockJsonPath(textPatch.path, "Text patch");
3351
+ for (const richTextPatch of patch.richText ?? []) validateMockRichTextPatch(richTextPatch);
3352
+ for (const stepPatch of patch.richTextSteps ?? []) validateMockRichTextStepPatch(stepPatch);
3353
+ return changeMockAutomergeDocumentOnFork(doc, common, (target) => {
3354
+ for (const [key, value] of Object.entries(patch.set ?? {})) {
3355
+ if (value !== void 0) {
3356
+ target[key] = hydrateMockAutomergeValues(value);
3357
+ }
3358
+ }
3359
+ for (const key of patch.unset ?? []) {
3360
+ delete target[key];
3361
+ }
3362
+ if (patch.json) applyMockAutomergeJsonOperations(target, patch.json);
3363
+ for (const textPatch of patch.text ?? []) {
3364
+ ensureMockAutomergeText(target, textPatch.path, "text patch");
3365
+ for (const edit of textPatch.edits) {
3366
+ spliceMockAutomergeText(target, textPatch.path, edit.index, edit.deleteCount, edit.insert ?? "");
3367
+ }
3368
+ }
3369
+ for (const richTextPatch of patch.richText ?? []) applyMockRichTextSpansInChange(target, richTextPatch);
3370
+ for (const stepPatch of patch.richTextSteps ?? []) applyMockRichTextStepsInChange(target, stepPatch);
3371
+ });
3372
+ }
3373
+ let next = doc;
3374
+ if (patch.set || (patch.unset?.length ?? 0) > 0) {
3375
+ next = Automerge.change(next, (target) => {
3376
+ for (const [key, value] of Object.entries(patch.set ?? {})) {
3377
+ if (value !== void 0) {
3378
+ target[key] = hydrateMockAutomergeValues(value);
3379
+ }
3380
+ }
3381
+ for (const key of patch.unset ?? []) {
3382
+ delete target[key];
3383
+ }
3384
+ });
3385
+ }
3386
+ if (patch.json) {
3387
+ const json = patch.json;
3388
+ validateMockJsonPatch(json);
3389
+ next = changeMockAutomergeDocument(
3390
+ next,
3391
+ json.baseHeads,
3392
+ (target) => applyMockAutomergeJsonOperations(target, json)
3393
+ );
3394
+ }
3395
+ for (const textPatch of patch.text ?? []) {
3396
+ validateMockJsonPath(textPatch.path, "Text patch");
3397
+ next = changeMockAutomergeDocument(next, textPatch.baseHeads, (target) => {
3398
+ ensureMockAutomergeText(target, textPatch.path, "text patch");
3399
+ for (const edit of textPatch.edits) {
3400
+ spliceMockAutomergeText(target, textPatch.path, edit.index, edit.deleteCount, edit.insert ?? "");
3401
+ }
3402
+ });
3403
+ }
3404
+ for (const richTextPatch of patch.richText ?? []) {
3405
+ validateMockRichTextPatch(richTextPatch);
3406
+ const sequence = richTextPatch.spansSequence ?? (richTextPatch.spans ? [richTextPatch.spans] : []);
3407
+ const config = richTextPatch.updateSpansConfig;
3408
+ next = changeMockAutomergeDocumentOnFork(next, richTextPatch.baseHeads, (target) => {
3409
+ ensureMockRichTextPath(target, richTextPatch.path);
3410
+ const path = richTextPatch.path;
3411
+ for (const spans2 of sequence) {
3412
+ const revived = reviveMockRichTextSpans(spans2);
3413
+ try {
3414
+ Automerge.updateSpans(target, path, revived, config);
3415
+ } catch (error) {
3416
+ if (!isMockUpdateSpansBoundsError(error)) {
3417
+ throw error;
3418
+ }
3419
+ setMockAutomergeValue(target, richTextPatch.path, "");
3420
+ Automerge.updateSpans(target, path, revived, config);
3421
+ }
3422
+ }
3423
+ });
3424
+ }
3425
+ for (const stepPatch of patch.richTextSteps ?? []) {
3426
+ validateMockRichTextStepPatch(stepPatch);
3427
+ next = changeMockAutomergeDocument(next, stepPatch.baseHeads, (target) => {
3428
+ ensureMockRichTextPath(target, stepPatch.path);
3429
+ const path = stepPatch.path;
3430
+ for (const step of stepPatch.steps) {
3431
+ Automerge.splice(target, path, step.index, step.deleteCount, step.insert ?? "");
3432
+ for (const range of step.marks ?? []) {
3433
+ for (const [name, value] of Object.entries(range.marks)) {
3434
+ Automerge.mark(
3435
+ target,
3436
+ path,
3437
+ { start: range.index, end: range.index + range.length, expand: "none" },
3438
+ name,
3439
+ reviveMockRichTextValue(value)
3440
+ );
3441
+ }
3442
+ }
3443
+ }
3444
+ });
3445
+ }
3446
+ return next;
3447
+ }
3448
+ function forkMockAutomergeDocument(doc, baseHeads, actor) {
3449
+ if (!baseHeads || baseHeads.length === 0) {
3450
+ return Automerge.clone(doc, { actor });
3451
+ }
3452
+ const heads = new Set(baseHeads);
3453
+ let ownLatest;
3454
+ for (const change2 of Automerge.getAllChanges(doc)) {
3455
+ const decoded = Automerge.decodeChange(change2);
3456
+ if (decoded.actor === actor) {
3457
+ ownLatest = decoded.hash;
3458
+ }
3459
+ }
3460
+ if (ownLatest) {
3461
+ heads.add(ownLatest);
3462
+ }
3463
+ return Automerge.clone(Automerge.view(doc, [...heads]), { actor });
3464
+ }
3465
+ function getFieldValue2(source, field2) {
3466
+ return field2.split(".").reduce((current, part) => {
2527
3467
  if (current && typeof current === "object" && part in current) {
2528
3468
  return current[part];
2529
3469
  }
@@ -2535,7 +3475,7 @@ function matchesDocumentFilter(document2, filter) {
2535
3475
  return true;
2536
3476
  }
2537
3477
  return Object.entries(filter).every(
2538
- ([field, expected]) => getFieldValue2(document2.data, field) === expected
3478
+ ([field2, expected]) => getFieldValue2(document2.data, field2) === expected
2539
3479
  );
2540
3480
  }
2541
3481
  function withValidatedCreateIds(documents) {
@@ -2612,7 +3552,7 @@ function computeMockTextScore(document2, clause) {
2612
3552
  if (terms.length === 0) {
2613
3553
  return void 0;
2614
3554
  }
2615
- const haystack = clause.fields ? clause.fields.map((field) => collectMockText(getFieldValue2(document2.data, field))).join(" ") : collectMockText(
3555
+ const haystack = clause.fields ? clause.fields.map((field2) => collectMockText(getFieldValue2(document2.data, field2))).join(" ") : collectMockText(
2616
3556
  Object.fromEntries(
2617
3557
  Object.entries(document2.data).filter(([key]) => !key.startsWith("_"))
2618
3558
  )
@@ -2842,7 +3782,7 @@ function createLiveMockViewNavigator(initial, rebuild, subscribe) {
2842
3782
  });
2843
3783
  }
2844
3784
  function projectMockFields(data, fields) {
2845
- return fields ? Object.fromEntries(fields.map((field) => [field, getFieldValue2(data, field)])) : structuredClone(data);
3785
+ return fields ? Object.fromEntries(fields.map((field2) => [field2, getFieldValue2(data, field2)])) : structuredClone(data);
2846
3786
  }
2847
3787
  function mockLastModified(document2) {
2848
3788
  return document2.updatedAt ? Date.parse(document2.updatedAt) || 0 : 0;
@@ -3370,13 +4310,22 @@ async function mockSha256Hex(value) {
3370
4310
  const digest = await subtle.digest("SHA-256", new TextEncoder().encode(value));
3371
4311
  return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
3372
4312
  }
3373
- function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
4313
+ function createDatabaseHandle(definition, peers, directoryUsers = () => [], localActor = MOCK_AUTOMERGE_LOCAL_ACTOR) {
3374
4314
  let createCounter = 0;
3375
4315
  let changeCounter = 0;
4316
+ const replicaTag = localActor === MOCK_AUTOMERGE_LOCAL_ACTOR ? "" : `-${localActor.slice(0, 6)}`;
3376
4317
  const defaultViewFactory = async () => createDefaultViewNavigator();
3377
- const storedDocuments = /* @__PURE__ */ new Map();
4318
+ const automergeMode = definition.automerge === true;
4319
+ let changeSequence = 0;
4320
+ const changedAt = /* @__PURE__ */ new Map();
4321
+ const storedDocuments = new class extends Map {
4322
+ set(id, document2) {
4323
+ changedAt.set(id, ++changeSequence);
4324
+ return super.set(id, document2);
4325
+ }
4326
+ }();
3378
4327
  for (const seed of definition.documents ?? []) {
3379
- storedDocuments.set(seed.id, {
4328
+ const stored = {
3380
4329
  id: seed.id,
3381
4330
  data: structuredClone(seed.data),
3382
4331
  heads: seed.heads ? [...seed.heads] : void 0,
@@ -3387,7 +4336,14 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3387
4336
  decryptionKeyId: seed.decryptionKeyId,
3388
4337
  authorLabel: seed.authorLabel,
3389
4338
  createdAt: seed.createdAt
3390
- });
4339
+ };
4340
+ if (automergeMode) {
4341
+ persistMockAutomergeDocument(
4342
+ stored,
4343
+ createMockAutomergeDocument(seed.data, MOCK_AUTOMERGE_SEED_ACTOR, 0)
4344
+ );
4345
+ }
4346
+ storedDocuments.set(seed.id, stored);
3391
4347
  }
3392
4348
  const liveQuerySubscriptions = /* @__PURE__ */ new Set();
3393
4349
  const refreshLiveQueries = () => {
@@ -3430,6 +4386,15 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3430
4386
  throw new Error("Recipient mutators require a document created with `recipients`");
3431
4387
  }
3432
4388
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4389
+ if (automergeMode) {
4390
+ return writeAutomergeDocument(
4391
+ docId,
4392
+ existing,
4393
+ Automerge.change(loadMockAutomergeDocument(existing, localActor), (target) => {
4394
+ target._encryptFor = hydrateMockAutomergeValues(nextEncryptFor);
4395
+ })
4396
+ );
4397
+ }
3433
4398
  const updated = {
3434
4399
  id: docId,
3435
4400
  data: { ...existing.data, _encryptFor: nextEncryptFor },
@@ -3448,6 +4413,28 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3448
4413
  notifyLiveQueries();
3449
4414
  return updated;
3450
4415
  }
4416
+ function writeAutomergeDocument(docId, existing, automergeDoc) {
4417
+ const stored = {
4418
+ ...existing ?? {},
4419
+ id: docId,
4420
+ data: {},
4421
+ attachments: existing?.attachments ? structuredClone(existing.attachments) : [],
4422
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4423
+ isDeleted: false,
4424
+ decryptionKeyId: existing?.decryptionKeyId ?? "default"
4425
+ };
4426
+ persistMockAutomergeDocument(stored, automergeDoc);
4427
+ storedDocuments.set(docId, stored);
4428
+ notifyLiveQueries();
4429
+ return {
4430
+ id: docId,
4431
+ data: structuredClone(stored.data),
4432
+ heads: [...stored.heads ?? []],
4433
+ attachments: structuredClone(stored.attachments ?? []),
4434
+ updatedAt: stored.updatedAt,
4435
+ ...mockDecryptionKeyId(stored)
4436
+ };
4437
+ }
3451
4438
  const defaultDocuments = {
3452
4439
  async query(query) {
3453
4440
  return runMockDocumentQuery(
@@ -3500,12 +4487,17 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3500
4487
  const offset = decodeMockListCursor(query?.cursor) + skip;
3501
4488
  const metadataOnly = query?.metadataOnly ?? false;
3502
4489
  const idPrefix = query?.idPrefix;
3503
- const items = Array.from(storedDocuments.values()).filter((document2) => !document2.inaccessible).filter(
4490
+ const after = automergeMode ? decodeMockListCursor(query?.cursor) : 0;
4491
+ const items = Array.from(storedDocuments.values()).filter((document2) => !automergeMode || (changedAt.get(document2.id) ?? 0) > after).filter((document2) => !document2.inaccessible).filter(
3504
4492
  (document2) => status === "all" ? true : status === "deleted" ? document2.isDeleted : !document2.isDeleted
3505
4493
  ).filter((document2) => matchesMockIdPrefix(document2.id, idPrefix)).filter(
3506
4494
  (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) => {
4495
+ ).sort(
4496
+ (left, right) => automergeMode ? (changedAt.get(left.id) ?? 0) - (changedAt.get(right.id) ?? 0) : left.id.localeCompare(right.id)
4497
+ );
4498
+ const pageStart = automergeMode ? skip : offset;
4499
+ const pageDocuments = items.slice(pageStart, pageStart + limit);
4500
+ const page = pageDocuments.map((document2) => {
3509
4501
  if (metadataOnly) {
3510
4502
  return {
3511
4503
  id: document2.id,
@@ -3513,9 +4505,9 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3513
4505
  };
3514
4506
  }
3515
4507
  const projectedData = query?.fields ? Object.fromEntries(
3516
- query.fields.map((field) => [
3517
- field,
3518
- getFieldValue2(document2.data, field)
4508
+ query.fields.map((field2) => [
4509
+ field2,
4510
+ getFieldValue2(document2.data, field2)
3519
4511
  ])
3520
4512
  ) : document2.data;
3521
4513
  return {
@@ -3526,6 +4518,13 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3526
4518
  isDeleted: status !== "existing" ? document2.isDeleted : void 0
3527
4519
  };
3528
4520
  });
4521
+ if (automergeMode) {
4522
+ const last = pageDocuments[pageDocuments.length - 1];
4523
+ return {
4524
+ items: page,
4525
+ nextCursor: last ? String(changedAt.get(last.id) ?? after) : null
4526
+ };
4527
+ }
3529
4528
  const nextCursor = offset + page.length < items.length ? String(offset + page.length) : null;
3530
4529
  return {
3531
4530
  items: page,
@@ -3534,7 +4533,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3534
4533
  },
3535
4534
  async getHeadCursor() {
3536
4535
  return {
3537
- cursor: String(storedDocuments.size)
4536
+ cursor: String(automergeMode ? changeSequence : storedDocuments.size)
3538
4537
  };
3539
4538
  },
3540
4539
  async listInaccessible(query) {
@@ -3569,7 +4568,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3569
4568
  return {
3570
4569
  path: [...path],
3571
4570
  heads: document2.heads ? [...document2.heads] : void 0,
3572
- spans: structuredClone(readRichTextSpansAtPath(document2.data, path))
4571
+ spans: automergeMode ? readMockAutomergeRichText(document2, path) : structuredClone(readRichTextSpansAtPath(document2.data, path))
3573
4572
  };
3574
4573
  },
3575
4574
  // Mock cursors store the index they were created at and do not follow
@@ -3630,7 +4629,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3630
4629
  const currentDoc = loadMockAutomergeDocument(document2);
3631
4630
  const [mergedDoc] = Automerge.applyChanges(
3632
4631
  currentDoc,
3633
- patch.changes.map((change) => new Uint8Array(change))
4632
+ patch.changes.map((change2) => new Uint8Array(change2))
3634
4633
  );
3635
4634
  persistMockAutomergeDocument(document2, mergedDoc);
3636
4635
  document2.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -3643,7 +4642,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3643
4642
  );
3644
4643
  changesSince = {
3645
4644
  sinceHeads: [...patch.replicaHeads],
3646
- changes: missingChanges.map((change) => cloneBytes(new Uint8Array(change)))
4645
+ changes: missingChanges.map((change2) => cloneBytes(new Uint8Array(change2)))
3647
4646
  };
3648
4647
  }
3649
4648
  return {
@@ -3680,7 +4679,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3680
4679
  if (existing.isDeleted) {
3681
4680
  existing.isDeleted = false;
3682
4681
  existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3683
- existing.heads = [`mock-head-${++changeCounter}`];
4682
+ if (!automergeMode) {
4683
+ existing.heads = [`mock-head-${++changeCounter}`];
4684
+ }
4685
+ storedDocuments.set(callerId, existing);
3684
4686
  notifyLiveQueries();
3685
4687
  }
3686
4688
  return {
@@ -3698,10 +4700,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3698
4700
  id = callerId;
3699
4701
  } else if (typeof input.idPrefix === "string" && input.idPrefix.length > 0) {
3700
4702
  createCounter += 1;
3701
- id = `${input.idPrefix}_${String(createCounter).padStart(22, "0")}`;
4703
+ id = `${input.idPrefix}_${String(createCounter).padStart(22, "0")}${replicaTag}`;
3702
4704
  } else {
3703
4705
  createCounter += 1;
3704
- id = `doc-${createCounter}`;
4706
+ id = `doc-${createCounter}${replicaTag}`;
3705
4707
  }
3706
4708
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
3707
4709
  if (Array.isArray(input.recipients) && input.recipientOptions?.includeSelf === false && input.recipients.every((name) => name.trim().length === 0)) {
@@ -3709,11 +4711,39 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3709
4711
  "recipients: [] with includeSelf: false would produce a document nobody can read"
3710
4712
  );
3711
4713
  }
4714
+ const encryptFor = Array.isArray(input.recipients) ? { _encryptFor: mockEncryptForMap(input.recipients) } : {};
4715
+ if (automergeMode) {
4716
+ const stored2 = {
4717
+ id,
4718
+ data: {},
4719
+ attachments: [],
4720
+ updatedAt: createdAt,
4721
+ isDeleted: false,
4722
+ decryptionKeyId: input.decryptionKeyId ?? "default"
4723
+ };
4724
+ persistMockAutomergeDocument(
4725
+ stored2,
4726
+ createMockAutomergeDocument(
4727
+ { ...input.set ?? {}, ...encryptFor },
4728
+ localActor
4729
+ )
4730
+ );
4731
+ storedDocuments.set(id, stored2);
4732
+ notifyLiveQueries();
4733
+ return {
4734
+ id,
4735
+ data: structuredClone(stored2.data),
4736
+ heads: [...stored2.heads ?? []],
4737
+ attachments: [],
4738
+ updatedAt: createdAt,
4739
+ ...mockDecryptionKeyId(stored2)
4740
+ };
4741
+ }
3712
4742
  const created = {
3713
4743
  id,
3714
4744
  data: {
3715
4745
  ...readMockTypedValues(input.set ?? {}),
3716
- ...Array.isArray(input.recipients) ? { _encryptFor: mockEncryptForMap(input.recipients) } : {}
4746
+ ...encryptFor
3717
4747
  },
3718
4748
  heads: [`mock-head-${++changeCounter}`],
3719
4749
  attachments: [],
@@ -3744,6 +4774,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3744
4774
  async update(docId, patch) {
3745
4775
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3746
4776
  const existing = storedDocuments.get(docId);
4777
+ if (automergeMode) {
4778
+ const current = existing?.automergeBinary ? loadMockAutomergeDocument(existing, localActor) : createMockAutomergeDocument(existing?.data ?? {}, localActor);
4779
+ return writeAutomergeDocument(docId, existing, applyMockAutomergeUpdate(current, patch));
4780
+ }
3747
4781
  const updated = {
3748
4782
  id: docId,
3749
4783
  data: applyDocumentUpdatePatch(existing?.data ?? {}, patch),
@@ -3830,7 +4864,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3830
4864
  storedDocuments.set(_docId, {
3831
4865
  ...existing,
3832
4866
  isDeleted: false,
3833
- heads: [`mock-head-${++changeCounter}`],
4867
+ heads: automergeMode ? existing.heads : [`mock-head-${++changeCounter}`],
3834
4868
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3835
4869
  });
3836
4870
  notifyLiveQueries();
@@ -4054,7 +5088,26 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
4054
5088
  createdAt: document2.updatedAt ?? null
4055
5089
  })),
4056
5090
  documents: storedDocuments,
4057
- notifyLiveQueries: refreshLiveQueries
5091
+ notifyLiveQueries: refreshLiveQueries,
5092
+ async applyRemoteUpdate(docId, input, options) {
5093
+ const existing = storedDocuments.get(docId);
5094
+ if (!existing || existing.isDeleted) {
5095
+ throw new Error(`Document ${docId} was not found.`);
5096
+ }
5097
+ if (!automergeMode || !existing.automergeBinary) {
5098
+ return await defaultDocuments.update(docId, input);
5099
+ }
5100
+ const current = loadMockAutomergeDocument(existing, localActor);
5101
+ const remote = applyMockAutomergeUpdate(
5102
+ forkMockAutomergeDocument(
5103
+ current,
5104
+ input.json?.baseHeads,
5105
+ options?.actor ?? MOCK_AUTOMERGE_REMOTE_ACTOR
5106
+ ),
5107
+ input.json ? { ...input, json: { ...input.json, baseHeads: void 0 } } : input
5108
+ );
5109
+ return writeAutomergeDocument(docId, existing, Automerge.merge(current, remote));
5110
+ }
4058
5111
  };
4059
5112
  }
4060
5113
  function createMockSessionState(options = {}) {
@@ -4080,6 +5133,7 @@ function createMockSessionState(options = {}) {
4080
5133
  const databaseViewDocumentLists = /* @__PURE__ */ new Map();
4081
5134
  const databaseDocumentStores = /* @__PURE__ */ new Map();
4082
5135
  const databaseLiveQueryRefreshers = /* @__PURE__ */ new Map();
5136
+ const databaseRemoteUpdaters = /* @__PURE__ */ new Map();
4083
5137
  const databasePeers = {
4084
5138
  getDocuments: (databaseId) => databaseDocumentStores.get(databaseId),
4085
5139
  refreshAllLiveQueries: () => {
@@ -4170,12 +5224,18 @@ function createMockSessionState(options = {}) {
4170
5224
  databaseViewApis.clear();
4171
5225
  databaseDocumentStores.clear();
4172
5226
  databaseLiveQueryRefreshers.clear();
5227
+ databaseRemoteUpdaters.clear();
4173
5228
  databaseInfos = definitions.map((definition) => ({
4174
5229
  ...definition.info,
4175
5230
  capabilities: [...definition.info.capabilities]
4176
5231
  }));
4177
5232
  for (const definition of definitions) {
4178
- const created = createDatabaseHandle(definition, databasePeers, () => directoryUsers);
5233
+ const created = createDatabaseHandle(
5234
+ definition,
5235
+ databasePeers,
5236
+ () => directoryUsers,
5237
+ options.automergeActor ?? MOCK_AUTOMERGE_LOCAL_ACTOR
5238
+ );
4179
5239
  const databaseId = definition.info.id;
4180
5240
  databaseHandles.set(
4181
5241
  databaseId,
@@ -4188,35 +5248,49 @@ function createMockSessionState(options = {}) {
4188
5248
  databaseViewDocumentLists.set(definition.info.id, created.listViewDocuments);
4189
5249
  databaseDocumentStores.set(definition.info.id, created.documents);
4190
5250
  databaseLiveQueryRefreshers.set(definition.info.id, created.notifyLiveQueries);
5251
+ databaseRemoteUpdaters.set(definition.info.id, created.applyRemoteUpdate);
5252
+ const createView = async (input) => {
5253
+ const build = () => createEvaluatingViewNavigator(
5254
+ input,
5255
+ input.databaseIds.flatMap((databaseId2) => {
5256
+ const list = databaseViewDocumentLists.get(databaseId2);
5257
+ return list ? list() : [];
5258
+ })
5259
+ );
5260
+ const subscribe = (onChange) => {
5261
+ const listener = { databaseIds: [...input.databaseIds], onChange };
5262
+ viewChangeListeners.add(listener);
5263
+ return () => viewChangeListeners.delete(listener);
5264
+ };
5265
+ const evaluating = await build();
5266
+ if (evaluating) {
5267
+ return createLiveMockViewNavigator(evaluating, build, subscribe);
5268
+ }
5269
+ console.warn(
5270
+ "[mindoodb-app-sdk/testing] mindoodb peer not available; using empty view navigator. Install mindoodb as a devDependency for evaluating VirtualViews."
5271
+ );
5272
+ return createLiveMockViewNavigator(
5273
+ await createDefaultViewNavigator(),
5274
+ async () => null,
5275
+ subscribe
5276
+ );
5277
+ };
4191
5278
  databaseViewApis.set(definition.info.id, {
4192
- async create(input) {
4193
- const build = () => createEvaluatingViewNavigator(
4194
- input,
4195
- input.databaseIds.flatMap((databaseId2) => {
4196
- const list = databaseViewDocumentLists.get(databaseId2);
4197
- return list ? list() : [];
4198
- })
4199
- );
4200
- const subscribe = (onChange) => {
4201
- const listener = { databaseIds: [...input.databaseIds], onChange };
4202
- viewChangeListeners.add(listener);
4203
- return () => viewChangeListeners.delete(listener);
4204
- };
4205
- const evaluating = await build();
4206
- if (evaluating) {
4207
- return createLiveMockViewNavigator(evaluating, build, subscribe);
5279
+ create: createView,
5280
+ // A view configured in the launch context, evaluated over the mock
5281
+ // documents of its sources: a fresh navigator per open, with the
5282
+ // open options applied, as Haven builds one.
5283
+ async open(viewId, openOptions) {
5284
+ const configured = launchContext.views.find((view2) => view2.id === viewId);
5285
+ if (!configured) {
5286
+ return await createDefaultViewNavigator();
4208
5287
  }
4209
- console.warn(
4210
- "[mindoodb-app-sdk/testing] mindoodb peer not available; using empty view navigator. Install mindoodb as a devDependency for evaluating VirtualViews."
4211
- );
4212
- return createLiveMockViewNavigator(
4213
- await createDefaultViewNavigator(),
4214
- async () => null,
4215
- subscribe
4216
- );
4217
- },
4218
- async open(_viewId) {
4219
- return await createDefaultViewNavigator();
5288
+ return await createView({
5289
+ databaseIds: configured.sources.map((source) => source.databaseId),
5290
+ definition: configuredViewDefinition(configured),
5291
+ categorizationStyle: configured.categorizationStyle,
5292
+ ...openOptions ? { options: openOptions } : {}
5293
+ });
4220
5294
  },
4221
5295
  ...definition.methods?.views ?? {}
4222
5296
  });
@@ -4310,15 +5384,20 @@ function createMockSessionState(options = {}) {
4310
5384
  `Unknown test database for view creation: ${firstDatabaseId}`
4311
5385
  );
4312
5386
  }
4313
- const view = await api.create(input);
5387
+ const view2 = await api.create(input);
4314
5388
  const viewId = input.definition.id || crypto.randomUUID();
4315
- sessionViews.set(viewId, view);
4316
- return view;
5389
+ sessionViews.set(viewId, input);
5390
+ return view2;
4317
5391
  },
5392
+ // Every open is a navigator of its own with its own options, as in
5393
+ // Haven: disposing one leaves the others and later opens working.
4318
5394
  async openViewNavigator(viewId, options2) {
4319
- const existing = sessionViews.get(viewId);
4320
- if (existing) {
4321
- return existing;
5395
+ const created = sessionViews.get(viewId);
5396
+ if (created) {
5397
+ const api2 = databaseViewApis.get(created.databaseIds[0]);
5398
+ if (api2) {
5399
+ return await api2.create({ ...created, ...options2 ? { options: options2 } : {} });
5400
+ }
4322
5401
  }
4323
5402
  const sourceDatabaseId = launchContext.views.find(
4324
5403
  (view2) => view2.id === viewId
@@ -4332,9 +5411,7 @@ function createMockSessionState(options = {}) {
4332
5411
  `Unknown test database for view ${viewId}: ${sourceDatabaseId}`
4333
5412
  );
4334
5413
  }
4335
- const view = await api.open(viewId, options2);
4336
- sessionViews.set(viewId, view);
4337
- return view;
5414
+ return await api.open(viewId, options2);
4338
5415
  },
4339
5416
  menus,
4340
5417
  drag,
@@ -4463,6 +5540,8 @@ function createMockSessionState(options = {}) {
4463
5540
  directoryUsers = [...users];
4464
5541
  },
4465
5542
  getDirectoryUsers: () => [...directoryUsers],
5543
+ replicaStores: () => databaseDocumentStores,
5544
+ notifyReplicaChange: (databaseId) => databaseLiveQueryRefreshers.get(databaseId)?.(),
4466
5545
  embedHost,
4467
5546
  getDatabase(databaseId) {
4468
5547
  const database = databaseHandles.get(databaseId);
@@ -4471,6 +5550,13 @@ function createMockSessionState(options = {}) {
4471
5550
  }
4472
5551
  return database;
4473
5552
  },
5553
+ async applyRemoteUpdate(databaseId, docId, input, updateOptions) {
5554
+ const applyRemoteUpdate = databaseRemoteUpdaters.get(databaseId);
5555
+ if (!applyRemoteUpdate) {
5556
+ throw new Error(`Unknown test database: ${databaseId}`);
5557
+ }
5558
+ return await applyRemoteUpdate(docId, input, updateOptions);
5559
+ },
4474
5560
  getLicensedProducts() {
4475
5561
  return [...launchContext.licensedProducts ?? []];
4476
5562
  },
@@ -4573,9 +5659,92 @@ function buildMockIncomingContent(input, deliveryId) {
4573
5659
  })
4574
5660
  };
4575
5661
  }
5662
+ var replicaStates = /* @__PURE__ */ new WeakMap();
5663
+ function sameMockHeads(a, b) {
5664
+ const x = a?.heads ?? [];
5665
+ const y = b?.heads ?? [];
5666
+ return x.length === y.length && x.every((head) => y.includes(head));
5667
+ }
5668
+ function mockReplicaDifferences(a, b) {
5669
+ const left = replicaStates.get(a)?.replicaStores();
5670
+ const right = replicaStates.get(b)?.replicaStores();
5671
+ if (!left || !right) throw new Error("syncMockReplicas: not a mock controller");
5672
+ let count = 0;
5673
+ for (const [databaseId, docsA] of left) {
5674
+ const docsB = right.get(databaseId);
5675
+ if (!docsB) continue;
5676
+ for (const id of /* @__PURE__ */ new Set([...docsA.keys(), ...docsB.keys()])) {
5677
+ const x = docsA.get(id);
5678
+ const y = docsB.get(id);
5679
+ if (!x || !y || x.isDeleted !== y.isDeleted || !sameMockHeads(x, y)) count += 1;
5680
+ }
5681
+ }
5682
+ return count;
5683
+ }
5684
+ async function syncMockReplicas(a, b) {
5685
+ const stateA = replicaStates.get(a);
5686
+ const stateB = replicaStates.get(b);
5687
+ if (!stateA || !stateB) throw new Error("syncMockReplicas: not a mock controller");
5688
+ let changed = 0;
5689
+ for (const [databaseId, docsA] of stateA.replicaStores()) {
5690
+ const docsB = stateB.replicaStores().get(databaseId);
5691
+ if (!docsB) continue;
5692
+ const touchedA = /* @__PURE__ */ new Set();
5693
+ const touchedB = /* @__PURE__ */ new Set();
5694
+ for (const id of /* @__PURE__ */ new Set([...docsA.keys(), ...docsB.keys()])) {
5695
+ const x = docsA.get(id);
5696
+ const y = docsB.get(id);
5697
+ if (x && y && x.isDeleted === y.isDeleted && sameMockHeads(x, y)) continue;
5698
+ changed += 1;
5699
+ let merged;
5700
+ if (x?.automergeBinary && y?.automergeBinary) {
5701
+ const doc = Automerge.merge(Automerge.load(x.automergeBinary), Automerge.load(y.automergeBinary));
5702
+ merged = { ...x, ...y, attachments: structuredClone(x.attachments ?? y.attachments ?? []) };
5703
+ persistMockAutomergeDocument(merged, doc);
5704
+ } else if (x && y) {
5705
+ merged = structuredClone((x.updatedAt ?? "") >= (y.updatedAt ?? "") ? x : y);
5706
+ } else {
5707
+ merged = structuredClone(x ?? y);
5708
+ }
5709
+ merged.isDeleted = Boolean(x?.isDeleted || y?.isDeleted);
5710
+ merged.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
5711
+ if (!x || !sameMockHeads(x, merged) || x.isDeleted !== merged.isDeleted) {
5712
+ docsA.set(id, structuredClone(merged));
5713
+ touchedA.add(id);
5714
+ }
5715
+ if (!y || !sameMockHeads(y, merged) || y.isDeleted !== merged.isDeleted) {
5716
+ docsB.set(id, structuredClone(merged));
5717
+ touchedB.add(id);
5718
+ }
5719
+ }
5720
+ const dbA = stateA.getDatabase(databaseId);
5721
+ const dbB = stateB.getDatabase(databaseId);
5722
+ for (const id of /* @__PURE__ */ new Set([...docsA.keys()])) {
5723
+ if (docsA.get(id)?.isDeleted) continue;
5724
+ for (const [from2, to, touched] of [
5725
+ [dbA, dbB, touchedB],
5726
+ [dbB, dbA, touchedA]
5727
+ ]) {
5728
+ const have = new Set((await to.attachments.list(id).catch(() => [])).map((f) => f.fileName));
5729
+ for (const file of await from2.attachments.list(id).catch(() => [])) {
5730
+ if (have.has(file.fileName)) continue;
5731
+ const reader = await from2.attachments.openReadStream(id, file.fileName);
5732
+ const writer = await to.attachments.openWriteStream(id, file.fileName, file.mimeType);
5733
+ for (let chunk = await reader.read(); chunk !== null; chunk = await reader.read()) await writer.write(chunk);
5734
+ await reader.close();
5735
+ await writer.close();
5736
+ touched.add(id);
5737
+ }
5738
+ }
5739
+ }
5740
+ if (touchedA.size) stateA.notifyReplicaChange(databaseId);
5741
+ if (touchedB.size) stateB.notifyReplicaChange(databaseId);
5742
+ }
5743
+ return changed;
5744
+ }
4576
5745
  function createMockMindooDBAppSession(options = {}) {
4577
5746
  const state = createMockSessionState(options);
4578
- return {
5747
+ const controller = {
4579
5748
  bridge: state.bridge,
4580
5749
  session: state.session,
4581
5750
  getLaunchContext: state.getLaunchContext,
@@ -4587,6 +5756,7 @@ function createMockMindooDBAppSession(options = {}) {
4587
5756
  setCapabilities: state.setCapabilities,
4588
5757
  setEnforceCapabilities: state.setEnforceCapabilities,
4589
5758
  setDirectoryUsers: state.setDirectoryUsers,
5759
+ applyRemoteUpdate: state.applyRemoteUpdate,
4590
5760
  emitThemeChange: state.emitThemeChange,
4591
5761
  emitViewportChange: state.emitViewportChange,
4592
5762
  emitUiPreferencesChange: state.emitUiPreferencesChange,
@@ -4603,10 +5773,13 @@ function createMockMindooDBAppSession(options = {}) {
4603
5773
  setComponents: (components) => state.embedHost.setComponents(components),
4604
5774
  getEmbeddingFinish: () => state.embedHost.getEmbeddingFinish(),
4605
5775
  setEmbedDirty: (embedId, dirty) => state.embedHost.setEmbedDirty(embedId, dirty),
5776
+ setEmbedAgentTools: (embedId, names, toolOptions) => state.embedHost.setEmbedAgentTools(embedId, names, toolOptions),
4606
5777
  failNextEmbedSave: (message) => state.embedHost.failNextSave(message),
4607
5778
  getEmbeddingDirty: () => state.embedHost.getEmbeddingDirty(),
4608
5779
  requestEmbeddingSave: () => state.embedHost.requestEmbeddingSave()
4609
5780
  };
5781
+ replicaStates.set(controller, state);
5782
+ return controller;
4610
5783
  }
4611
5784
  function createMockMindooDBAppBridge(options = {}) {
4612
5785
  return createMockMindooDBAppSession(options);
@@ -4625,6 +5798,7 @@ function createFakeBridgeHost(options = {}) {
4625
5798
  ...options.agentConsent !== void 0 ? { agentConsent: options.agentConsent } : {},
4626
5799
  ...options.agentInvokeTimeoutMs !== void 0 ? { agentInvokeTimeoutMs: options.agentInvokeTimeoutMs } : {},
4627
5800
  appId: () => state.getLaunchContext().appId,
5801
+ embedded: () => Boolean(state.getLaunchContext().embed),
4628
5802
  post(message) {
4629
5803
  const port = [...connectedPorts].pop();
4630
5804
  if (!port) return false;
@@ -4643,6 +5817,11 @@ function createFakeBridgeHost(options = {}) {
4643
5817
  (port) => port.postMessage({ protocol: PROTOCOL, kind: "embed-event", event: { type: "dirty", ...event } })
4644
5818
  );
4645
5819
  });
5820
+ state.embedHost.onAgentTools((event) => {
5821
+ connectedPorts.forEach(
5822
+ (port) => port.postMessage({ protocol: PROTOCOL, kind: "embed-event", event: { type: "agentTools", ...event } })
5823
+ );
5824
+ });
4646
5825
  const embeddingRequests = /* @__PURE__ */ new Map();
4647
5826
  let embeddingRequestCounter = 0;
4648
5827
  const beforeCloseWaiters = /* @__PURE__ */ new Map();
@@ -4895,9 +6074,18 @@ function createFakeBridgeHost(options = {}) {
4895
6074
  case "embeds.save":
4896
6075
  await state.embedHost.saveEmbed(String(params.embedId));
4897
6076
  return { ok: true };
6077
+ case "embeds.setActive":
6078
+ state.embedHost.setActive(String(params.embedId));
6079
+ return { ok: true };
4898
6080
  case "embedding.setDirty":
4899
6081
  state.embedHost.setEmbeddingDirty(params.dirty === true);
4900
6082
  return { ok: true };
6083
+ case "embedding.lookup":
6084
+ return await state.embedHost.embedding.lookup(String(params.field), {
6085
+ ...typeof params.query === "string" ? { query: params.query } : {},
6086
+ ...Array.isArray(params.docIds) ? { docIds: params.docIds.map(String) } : {},
6087
+ ...typeof params.limit === "number" ? { limit: params.limit } : {}
6088
+ });
4901
6089
  case "embedding.respond": {
4902
6090
  const pending = embeddingRequests.get(String(params.requestId));
4903
6091
  embeddingRequests.delete(String(params.requestId));
@@ -5529,10 +6717,12 @@ function createFakeBridgeHost(options = {}) {
5529
6717
  getEnforceCapabilities: state.getEnforceCapabilities,
5530
6718
  setDirectoryUsers: state.setDirectoryUsers,
5531
6719
  getDirectoryUsers: state.getDirectoryUsers,
6720
+ applyRemoteUpdate: state.applyRemoteUpdate,
5532
6721
  listEmbeds: () => state.embedHost.listEmbeds(),
5533
6722
  closeEmbed: (embedId, reason, result) => state.embedHost.closeEmbed(embedId, reason, result),
5534
6723
  getEmbeddingFinish: () => state.embedHost.getEmbeddingFinish(),
5535
6724
  setEmbedDirty: (embedId, dirty) => state.embedHost.setEmbedDirty(embedId, dirty),
6725
+ setEmbedAgentTools: (embedId, names, toolOptions) => state.embedHost.setEmbedAgentTools(embedId, names, toolOptions),
5536
6726
  failNextEmbedSave: (message) => state.embedHost.failNextSave(message),
5537
6727
  getEmbeddingDirty: () => state.embedHost.getEmbeddingDirty(),
5538
6728
  requestEmbeddingSave() {
@@ -5658,10 +6848,12 @@ function createFakeBridgeHost(options = {}) {
5658
6848
  customRequestHandlers.delete(method);
5659
6849
  }
5660
6850
  };
6851
+ replicaStates.set(controller, state);
5661
6852
  return controller;
5662
6853
  }
5663
6854
  export {
5664
6855
  ALL_CAPABILITIES,
6856
+ DEFAULT_TEST_HOST_USERS,
5665
6857
  MockForbiddenError,
5666
6858
  TEST_HOST_LOCALES,
5667
6859
  capabilitiesForDefinitionPermissions,
@@ -5676,8 +6868,12 @@ export {
5676
6868
  loadMindoodbForTesting,
5677
6869
  mockAgentToolPrefix,
5678
6870
  mockDatabasesFromDefinition,
6871
+ mockReplicaDifferences,
6872
+ modeSwitch,
5679
6873
  mountHavenTestHost,
6874
+ mountTwoUserTestHost,
5680
6875
  previewJson,
5681
6876
  readTestHostUrlSettings,
6877
+ syncMockReplicas,
5682
6878
  validateMockAgentTools
5683
6879
  };