mindoodb-app-sdk 0.0.65 → 0.0.67

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-376SABPG.js";
8
8
 
9
9
  // src/testing/index.ts
10
10
  import * as Automerge from "@automerge/automerge";
@@ -458,17 +458,40 @@ async function createEvaluatingViewNavigator(input, documents) {
458
458
  }
459
459
 
460
460
  // src/testing/mockEmbeds.ts
461
- function createMockEmbedHost(options, getDatabase) {
461
+ function createMockEmbedHost(options, getDatabase, listDatabaseIds = () => []) {
462
462
  let components = [...options.components ?? []];
463
463
  const embeds = /* @__PURE__ */ new Map();
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;
470
472
  let embedCounter = 0;
471
473
  let embeddingFinish = null;
474
+ const accessRequests = [];
475
+ const grantedAccess = /* @__PURE__ */ new Set();
476
+ const pendingAccess = /* @__PURE__ */ new Set();
477
+ async function requestAccess(input) {
478
+ accessRequests.push(structuredClone(input));
479
+ const pairs = (input.components ?? []).flatMap(
480
+ (entry) => components.some((component) => component.key === entry.componentKey) ? (entry.databaseIds ?? listDatabaseIds()).filter((databaseId) => listDatabaseIds().includes(databaseId)).map((databaseId) => ({ componentKey: entry.componentKey, databaseId })) : []
481
+ );
482
+ const pairKey = (pair) => `${pair.componentKey}/${pair.databaseId}`;
483
+ const ask = pairs.filter((pair) => !grantedAccess.has(pairKey(pair)) && !pendingAccess.has(pairKey(pair)));
484
+ if (ask.length) {
485
+ const allowed = new Set((await (options.embedAccess?.(structuredClone(ask)) ?? ask)).map(pairKey));
486
+ for (const pair of ask) {
487
+ (allowed.has(pairKey(pair)) ? grantedAccess : pendingAccess).add(pairKey(pair));
488
+ }
489
+ }
490
+ return {
491
+ granted: pairs.filter((pair) => grantedAccess.has(pairKey(pair))),
492
+ pending: pairs.filter((pair) => !grantedAccess.has(pairKey(pair)))
493
+ };
494
+ }
472
495
  function notifyChange() {
473
496
  options.onEmbedChange?.([...embeds.values()].map((entry) => ({ ...entry, rect: { ...entry.rect } })));
474
497
  }
@@ -504,11 +527,49 @@ function createMockEmbedHost(options, getDatabase) {
504
527
  docId: input.docId,
505
528
  intent,
506
529
  rect: mockRect(input),
507
- visible: input.visible !== false
530
+ visible: input.visible !== false,
531
+ active: false,
532
+ agentTools: null
508
533
  });
509
- notifyChange();
534
+ setActive(embedId);
510
535
  return embedId;
511
536
  }
537
+ function toolPrefix(component) {
538
+ return component.appId.replace(/^mindoodb-app-/, "").replace(/[^a-z0-9_]/g, "_") || "app";
539
+ }
540
+ function publishTools(entry) {
541
+ const declared = toolNames.get(entry.embedId);
542
+ const prefix = toolPrefix(entry.component);
543
+ const next = declared?.names.length ? { prefix, names: declared.names.map((name) => `${prefix}_${name}`), active: entry.active, enabled: declared.enabled } : null;
544
+ if (JSON.stringify(next) === JSON.stringify(entry.agentTools)) {
545
+ return;
546
+ }
547
+ entry.agentTools = next;
548
+ handles.get(entry.embedId)?.setAgentToolsState(next);
549
+ toolListeners.forEach((listener) => listener({ embedId: entry.embedId, agentTools: next }));
550
+ }
551
+ function setActive(embedId) {
552
+ const target = embeds.get(embedId);
553
+ if (!target) {
554
+ return;
555
+ }
556
+ for (const entry of embeds.values()) {
557
+ if (entry.component.appId === target.component.appId) {
558
+ entry.active = entry.embedId === embedId;
559
+ publishTools(entry);
560
+ }
561
+ }
562
+ notifyChange();
563
+ }
564
+ function setEmbedAgentTools(embedId, names, toolOptions = {}) {
565
+ const entry = embeds.get(embedId);
566
+ if (!entry) {
567
+ return;
568
+ }
569
+ toolNames.set(embedId, { names: [...names ?? []], enabled: toolOptions.enabled !== false });
570
+ publishTools(entry);
571
+ notifyChange();
572
+ }
512
573
  function closeEmbed(embedId, reason = "closed", result) {
513
574
  if (!embeds.delete(embedId)) {
514
575
  return;
@@ -520,6 +581,7 @@ function createMockEmbedHost(options, getDatabase) {
520
581
  };
521
582
  handles.get(embedId)?.settle(event);
522
583
  handles.delete(embedId);
584
+ toolNames.delete(embedId);
523
585
  closedListeners.forEach((listener) => listener(event));
524
586
  notifyChange();
525
587
  }
@@ -558,6 +620,39 @@ function createMockEmbedHost(options, getDatabase) {
558
620
  async save() {
559
621
  await saveEmbed(this.embedId);
560
622
  }
623
+ agentTools = null;
624
+ toolsListeners = /* @__PURE__ */ new Set();
625
+ async setActive() {
626
+ setActive(this.embedId);
627
+ }
628
+ onAgentToolsChange(listener) {
629
+ this.toolsListeners.add(listener);
630
+ return () => {
631
+ this.toolsListeners.delete(listener);
632
+ };
633
+ }
634
+ setAgentToolsState(tools) {
635
+ this.agentTools = tools;
636
+ this.toolsListeners.forEach((listener) => listener(tools));
637
+ }
638
+ waitForAgentTools(timeoutMs = 1e4) {
639
+ if (this.agentTools?.names.length) {
640
+ return Promise.resolve(this.agentTools);
641
+ }
642
+ return new Promise((resolve) => {
643
+ const timer = setTimeout(() => {
644
+ stop();
645
+ resolve(null);
646
+ }, timeoutMs);
647
+ const stop = this.onAgentToolsChange((tools) => {
648
+ if (tools?.names.length) {
649
+ clearTimeout(timer);
650
+ stop();
651
+ resolve(tools);
652
+ }
653
+ });
654
+ });
655
+ }
561
656
  resolve;
562
657
  listeners = /* @__PURE__ */ new Set();
563
658
  settled = null;
@@ -637,7 +732,8 @@ function createMockEmbedHost(options, getDatabase) {
637
732
  const handle = new MockEmbed(embedId, input.componentKey, input.databaseId, input.docId);
638
733
  handles.set(embedId, handle);
639
734
  return handle;
640
- }
735
+ },
736
+ requestAccess
641
737
  },
642
738
  embedding: {
643
739
  async complete(result) {
@@ -652,6 +748,9 @@ function createMockEmbedHost(options, getDatabase) {
652
748
  async setDirty(dirty) {
653
749
  embeddingDirty = dirty === true;
654
750
  },
751
+ async lookup(field2, lookupOptions) {
752
+ return structuredClone(await (options.embeddingLookup?.(field2, { ...lookupOptions }) ?? []));
753
+ },
655
754
  onSaveRequest(handler) {
656
755
  saveRequestHandler = handler;
657
756
  return () => {
@@ -671,6 +770,12 @@ function createMockEmbedHost(options, getDatabase) {
671
770
  setComponents(next) {
672
771
  components = [...next];
673
772
  },
773
+ getAccessRequests() {
774
+ return structuredClone(accessRequests);
775
+ },
776
+ getGrantedAccess() {
777
+ return [...grantedAccess];
778
+ },
674
779
  getEmbeddingFinish() {
675
780
  return embeddingFinish;
676
781
  },
@@ -684,6 +789,14 @@ function createMockEmbedHost(options, getDatabase) {
684
789
  };
685
790
  },
686
791
  setEmbedDirty,
792
+ setActive,
793
+ setEmbedAgentTools,
794
+ onAgentTools(listener) {
795
+ toolListeners.add(listener);
796
+ return () => {
797
+ toolListeners.delete(listener);
798
+ };
799
+ },
687
800
  onDirty(listener) {
688
801
  dirtyListeners.add(listener);
689
802
  return () => {
@@ -828,6 +941,44 @@ function guardDatabase(database, currentInfo, enforce) {
828
941
  };
829
942
  }
830
943
 
944
+ // src/testing/configuredViews.ts
945
+ var field = (path) => ({ kind: "field", path });
946
+ var literal = (value) => ({ kind: "literal", value });
947
+ var operation = (op, args) => ({ kind: "operation", op, args });
948
+ function ruleExpression(rule) {
949
+ const target = field(rule.field);
950
+ switch (rule.operator) {
951
+ case "notContains":
952
+ return operation("not", [operation("contains", [target, literal(rule.value ?? "")])]);
953
+ case "exists":
954
+ case "notExists":
955
+ return operation(rule.operator, [target]);
956
+ default:
957
+ return operation(rule.operator, [target, literal(rule.value ?? "")]);
958
+ }
959
+ }
960
+ function configuredViewDefinition(view2) {
961
+ const filter = view2.filter.mode === "formula" ? { mode: "expression", expression: structuredClone(view2.filter.expression) } : view2.filter.rules.length ? {
962
+ mode: "expression",
963
+ expression: view2.filter.rules.length === 1 ? ruleExpression(view2.filter.rules[0]) : operation(view2.filter.match === "all" ? "and" : "or", view2.filter.rules.map(ruleExpression))
964
+ } : void 0;
965
+ return {
966
+ id: view2.id,
967
+ title: view2.description?.trim() || view2.id,
968
+ ...filter ? { filter } : {},
969
+ defaultExpand: "collapsed",
970
+ columns: view2.columns.map((column) => ({
971
+ name: column.name,
972
+ title: column.title,
973
+ role: column.role === "sort" ? "display" : column.role,
974
+ expression: column.expression.mode === "field" ? field(column.expression.field) : structuredClone(column.expression.expression),
975
+ sorting: column.sorting,
976
+ hidden: column.hidden,
977
+ totalMode: column.totalMode
978
+ }))
979
+ };
980
+ }
981
+
831
982
  // src/testing/mockAgentTools.ts
832
983
  var TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]{0,47}$/;
833
984
  var PREFIX_PATTERN = /^[a-z][a-z0-9_]{0,15}$/;
@@ -877,11 +1028,15 @@ function validateMockAgentTools(raw) {
877
1028
  throw invalid(`Agent tool "${tool.name}" has invalid annotations.`);
878
1029
  }
879
1030
  }
1031
+ if (tool.scope !== void 0 && tool.scope !== "app" && tool.scope !== "document") {
1032
+ throw invalid(`Agent tool "${tool.name}" has an invalid scope; use "app" or "document".`);
1033
+ }
880
1034
  return {
881
1035
  name: tool.name,
882
1036
  description: tool.description,
883
1037
  inputSchema: schema,
884
- ...tool.annotations ? { annotations: { ...tool.annotations } } : {}
1038
+ ...tool.annotations ? { annotations: { ...tool.annotations } } : {},
1039
+ ...tool.scope ? { scope: tool.scope } : {}
885
1040
  };
886
1041
  });
887
1042
  }
@@ -934,7 +1089,7 @@ function createMockAgentHost(options) {
934
1089
  async function dispatch(method, params) {
935
1090
  const input = params ?? {};
936
1091
  if (method === "agent.registerTools") {
937
- tools = validateMockAgentTools(input.tools);
1092
+ tools = validateMockAgentTools(input.tools).filter((tool) => !options.embedded?.() || tool.scope === "document");
938
1093
  changed();
939
1094
  return { result: registration() };
940
1095
  }
@@ -1078,6 +1233,10 @@ function concat(chunks) {
1078
1233
  }
1079
1234
  return bytes;
1080
1235
  }
1236
+ var MEMORY_ATTACHMENTS = /* @__PURE__ */ Symbol.for("mindoodb-app-sdk.memoryAttachments");
1237
+ function isMemoryAttachments(value) {
1238
+ return !!value && typeof value === "object" && MEMORY_ATTACHMENTS in value;
1239
+ }
1081
1240
  function createMemoryAttachments() {
1082
1241
  const files = /* @__PURE__ */ new Map();
1083
1242
  const forDoc = (docId) => {
@@ -1085,7 +1244,7 @@ function createMemoryAttachments() {
1085
1244
  if (!map) files.set(docId, map = /* @__PURE__ */ new Map());
1086
1245
  return map;
1087
1246
  };
1088
- return {
1247
+ const api = {
1089
1248
  async list(docId) {
1090
1249
  return [...forDoc(docId)].map(
1091
1250
  ([fileName, file]) => ({
@@ -1132,6 +1291,131 @@ function createMemoryAttachments() {
1132
1291
  };
1133
1292
  }
1134
1293
  };
1294
+ return Object.assign(api, { [MEMORY_ATTACHMENTS]: true });
1295
+ }
1296
+
1297
+ // src/testing/twoUserTestHost.ts
1298
+ var DEFAULT_TEST_HOST_USERS = [
1299
+ { name: "Anna", username: "CN=Anna Test/O=Test" },
1300
+ { name: "Ben", username: "CN=Ben Test/O=Test" }
1301
+ ];
1302
+ var STYLE = `
1303
+ .htu{display:grid;grid-template-rows:auto 1fr;height:100vh;margin:0;font:13px/1.45 system-ui,sans-serif;background:#eef1f7;color:#172033}
1304
+ .htu *{box-sizing:border-box}
1305
+ .htu__bar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:8px 12px;border-bottom:1px solid #d7dce8;background:#f8f9fc}
1306
+ .htu__badge{padding:2px 8px;border-radius:999px;background:#fde68a;color:#713f12;font-weight:600;font-size:11px;letter-spacing:.04em}
1307
+ .htu button{font:inherit;padding:5px 12px;border-radius:7px;border:1px solid #c9d0e0;background:#fff;cursor:pointer}
1308
+ .htu__status{font-weight:600}
1309
+ .htu .htest__mode{display:flex;gap:8px;align-items:center;padding:0 8px;border-left:1px solid #d7dce8;border-right:1px solid #d7dce8}
1310
+ .htu__panes{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:#d7dce8;min-height:0}
1311
+ .htu__pane{display:grid;grid-template-rows:auto 1fr;min-height:0;background:#fff}
1312
+ .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}
1313
+ .htu iframe{width:100%;height:100%;border:0}
1314
+ @media (max-width:900px){.htu__panes{grid-template-columns:1fr;grid-template-rows:1fr 1fr}}
1315
+ `;
1316
+ function replicaOf(databases) {
1317
+ return databases.map((database) => ({
1318
+ ...database,
1319
+ automerge: true,
1320
+ info: structuredClone(database.info),
1321
+ documents: database.documents ? structuredClone(database.documents) : void 0,
1322
+ ...database.methods ? {
1323
+ methods: {
1324
+ ...database.methods,
1325
+ ...isMemoryAttachments(database.methods.attachments) ? { attachments: createMemoryAttachments() } : {}
1326
+ }
1327
+ } : {}
1328
+ }));
1329
+ }
1330
+ function mountTwoUserTestHost(options, createHost) {
1331
+ const { container = document.body, title = "Haven test host", users = DEFAULT_TEST_HOST_USERS, ...hostOptions } = options;
1332
+ const style = document.createElement("style");
1333
+ style.textContent = STYLE;
1334
+ document.head.append(style);
1335
+ if (container === document.body) document.body.style.margin = "0";
1336
+ const root = document.createElement("div");
1337
+ root.className = "htu";
1338
+ const bar = document.createElement("div");
1339
+ bar.className = "htu__bar";
1340
+ const badge = document.createElement("span");
1341
+ badge.className = "htu__badge";
1342
+ badge.textContent = `TEST HOST \xB7 TWO USERS \xB7 ${title}`;
1343
+ const syncButton = document.createElement("button");
1344
+ syncButton.type = "button";
1345
+ syncButton.textContent = "Sync";
1346
+ syncButton.dataset.testid = "haven-two-users-sync";
1347
+ const auto = document.createElement("label");
1348
+ const autoBox = document.createElement("input");
1349
+ autoBox.type = "checkbox";
1350
+ autoBox.checked = true;
1351
+ autoBox.dataset.testid = "haven-two-users-autosync";
1352
+ auto.append(autoBox, " Auto-sync every 2 s");
1353
+ const status = document.createElement("span");
1354
+ status.className = "htu__status";
1355
+ status.dataset.testid = "haven-two-users-status";
1356
+ bar.append(badge, modeSwitch("two"), syncButton, auto, status);
1357
+ const panes = document.createElement("div");
1358
+ panes.className = "htu__panes";
1359
+ root.append(bar, panes);
1360
+ container.replaceChildren(root);
1361
+ const databasesA = (hostOptions.databases ?? []).map((database) => ({ ...database, automerge: true }));
1362
+ const hosts = users.map((user, index) => {
1363
+ const pane = document.createElement("section");
1364
+ pane.className = "htu__pane";
1365
+ const heading = document.createElement("h2");
1366
+ heading.textContent = `${user.name} \u2014 replica ${index + 1}`;
1367
+ const frame = document.createElement("iframe");
1368
+ frame.title = `${title} as ${user.name}`;
1369
+ frame.dataset.testid = `haven-two-users-frame-${index + 1}`;
1370
+ pane.append(heading, frame);
1371
+ panes.append(pane);
1372
+ const launchContext = {
1373
+ ...hostOptions.launchContext,
1374
+ user: { id: `test-user-${index + 1}`, username: user.username }
1375
+ };
1376
+ return createHost({
1377
+ ...hostOptions,
1378
+ frame,
1379
+ launchContext,
1380
+ databases: index === 0 ? databasesA : replicaOf(databasesA),
1381
+ // distinct Automerge actors: the two replicas write concurrently
1382
+ automergeActor: index === 0 ? "a11ce000000000000000000000000001" : "b0b00000000000000000000000000002"
1383
+ });
1384
+ });
1385
+ let lastSync = null;
1386
+ const describe = () => {
1387
+ const open = mockReplicaDifferences(hosts[0].host, hosts[1].host);
1388
+ status.textContent = `${open ? `\u26A0 ${open} document(s) not synced` : "\u2713 in sync"} \xB7 last sync ${lastSync ?? "never"}`;
1389
+ status.style.color = open ? "#b45309" : "#15803d";
1390
+ };
1391
+ let syncing = null;
1392
+ const sync = () => {
1393
+ syncing ??= syncMockReplicas(hosts[0].host, hosts[1].host).finally(() => {
1394
+ syncing = null;
1395
+ lastSync = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1396
+ describe();
1397
+ });
1398
+ return syncing;
1399
+ };
1400
+ syncButton.addEventListener("click", () => void sync());
1401
+ let timer;
1402
+ const setAutoSync = (on) => {
1403
+ autoBox.checked = on;
1404
+ if (timer) clearInterval(timer);
1405
+ timer = on ? setInterval(() => void sync(), 2e3) : void 0;
1406
+ };
1407
+ autoBox.addEventListener("change", () => setAutoSync(autoBox.checked));
1408
+ setAutoSync(true);
1409
+ setInterval(describe, 500);
1410
+ describe();
1411
+ const handle = {
1412
+ hosts,
1413
+ sync,
1414
+ setAutoSync,
1415
+ differences: () => mockReplicaDifferences(hosts[0].host, hosts[1].host)
1416
+ };
1417
+ window.__havenTwoUsers = handle;
1418
+ return handle;
1135
1419
  }
1136
1420
 
1137
1421
  // src/testing/browserTestHost.ts
@@ -1426,7 +1710,7 @@ var TEST_HOST_LOCALES = [
1426
1710
  { tag: "he-IL", label: "\u05E2\u05D1\u05E8\u05D9\u05EA" }
1427
1711
  ];
1428
1712
  var DIRECTORY_SIZES = [0, 5, 25, 120, 500];
1429
- var STYLE = `
1713
+ var STYLE2 = `
1430
1714
  .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}
1431
1715
  .htest *{box-sizing:border-box}
1432
1716
  .htest__frame{width:100%;height:100%;border:0;background:#fff}
@@ -1494,12 +1778,40 @@ function item(text) {
1494
1778
  li.textContent = text;
1495
1779
  return li;
1496
1780
  }
1781
+ function modeSwitch(current) {
1782
+ const row = document.createElement("div");
1783
+ row.className = "htest__row htest__mode";
1784
+ row.dataset.testid = "haven-test-mode";
1785
+ for (const [mode, label] of [
1786
+ ["single", "Single user"],
1787
+ ["two", "Two users"]
1788
+ ]) {
1789
+ const url = new URL(window.location.href);
1790
+ if (mode === "two") url.searchParams.set("twoUsers", "1");
1791
+ else url.searchParams.delete("twoUsers");
1792
+ if (mode === current) {
1793
+ const active = document.createElement("strong");
1794
+ active.textContent = label;
1795
+ active.setAttribute("aria-current", "page");
1796
+ row.append(active);
1797
+ } else {
1798
+ const link = document.createElement("a");
1799
+ link.href = url.toString();
1800
+ link.textContent = label;
1801
+ link.dataset.testid = `haven-test-mode-${mode}`;
1802
+ row.append(link);
1803
+ }
1804
+ }
1805
+ return row;
1806
+ }
1497
1807
  function mountHavenTestHost(options) {
1498
1808
  const {
1499
1809
  container = document.body,
1500
1810
  title = "Haven test host",
1501
1811
  locales = TEST_HOST_LOCALES,
1502
1812
  urlSettings: useUrl = true,
1813
+ twoUsers = false,
1814
+ users,
1503
1815
  ...hostOptions
1504
1816
  } = options;
1505
1817
  const url = useUrl ? readTestHostUrlSettings(window.location.search) : { overrides: {} };
@@ -1512,8 +1824,17 @@ function mountHavenTestHost(options) {
1512
1824
  ...url.locale ? { locale: url.locale } : {},
1513
1825
  ...url.theme ? { theme: { mode: url.theme, preset: "mindoo" } } : {}
1514
1826
  };
1827
+ const twoUsersFromUrl = useUrl && new URLSearchParams(window.location.search).get("twoUsers") === "1";
1828
+ if (twoUsers || twoUsersFromUrl) {
1829
+ const handle = mountTwoUserTestHost(
1830
+ { ...hostOptions, databases, launchContext, container, title, ...users ? { users } : {} },
1831
+ createBrowserTestHost
1832
+ );
1833
+ window.__havenTestHost = handle.hosts[0];
1834
+ return handle.hosts[0];
1835
+ }
1515
1836
  const style = document.createElement("style");
1516
- style.textContent = STYLE;
1837
+ style.textContent = STYLE2;
1517
1838
  document.head.append(style);
1518
1839
  const root = document.createElement("div");
1519
1840
  root.className = "htest";
@@ -1600,8 +1921,8 @@ function mountHavenTestHost(options) {
1600
1921
  if (override.mapped === false) params.append("db", `${id}:none`);
1601
1922
  }
1602
1923
  if (testHost.host.getEnforceCapabilities()) params.set("enforce", "1");
1603
- const users = testHost.host.getDirectoryUsers().length;
1604
- if (users) params.set("users", String(users));
1924
+ const users2 = testHost.host.getDirectoryUsers().length;
1925
+ if (users2) params.set("users", String(users2));
1605
1926
  if (currentLocale) params.set("locale", currentLocale);
1606
1927
  if (initialDark === void 0 || dark !== initialDark) params.set("theme", dark ? "dark" : "light");
1607
1928
  const query = params.toString();
@@ -1731,6 +2052,7 @@ Allow?`),
1731
2052
  }),
1732
2053
  button("Reload app", () => testHost.reloadApp())
1733
2054
  );
2055
+ section("Mode", modeSwitch("single"));
1734
2056
  section("Host", hostRow, focusState);
1735
2057
  const language = document.createElement("select");
1736
2058
  language.dataset.testid = "haven-test-language";
@@ -2083,8 +2405,8 @@ function applyDocumentUpdatePatch(current, patch) {
2083
2405
  applyMockTextPatch(next, textPatch.path, textPatch.edits);
2084
2406
  }
2085
2407
  for (const richTextPatch of patch.richText ?? []) {
2086
- const spans = richTextPatch.spansSequence?.at(-1) ?? richTextPatch.spans ?? [];
2087
- setValueAtPath(next, richTextPatch.path, structuredClone(spans));
2408
+ const spans2 = richTextPatch.spansSequence?.at(-1) ?? richTextPatch.spans ?? [];
2409
+ setValueAtPath(next, richTextPatch.path, structuredClone(spans2));
2088
2410
  }
2089
2411
  for (const richTextStepPatch of patch.richTextSteps ?? []) {
2090
2412
  applyMockRichTextSteps(next, richTextStepPatch.path, richTextStepPatch.steps);
@@ -2095,50 +2417,50 @@ function applyMockJsonPatch(target, patch) {
2095
2417
  if (!patch) {
2096
2418
  return;
2097
2419
  }
2098
- for (const operation of patch.set ?? []) {
2099
- setValueAtPath(target, operation.path, readMockTypedValues(operation.value));
2420
+ for (const operation2 of patch.set ?? []) {
2421
+ setValueAtPath(target, operation2.path, readMockTypedValues(operation2.value));
2100
2422
  }
2101
- for (const operation of patch.unset ?? []) {
2102
- unsetValueAtPath(target, operation.path);
2423
+ for (const operation2 of patch.unset ?? []) {
2424
+ unsetValueAtPath(target, operation2.path);
2103
2425
  }
2104
- for (const operation of patch.listDelete ?? []) {
2105
- const list = readListAtPath(target, operation.path);
2106
- list.splice(operation.index, operation.deleteCount);
2426
+ for (const operation2 of patch.listDelete ?? []) {
2427
+ const list = readListAtPath(target, operation2.path);
2428
+ list.splice(operation2.index, operation2.deleteCount);
2107
2429
  }
2108
- for (const operation of patch.listInsert ?? []) {
2109
- const parent = ensureParentAtPath(target, operation.path);
2110
- const leaf = operation.path[operation.path.length - 1];
2430
+ for (const operation2 of patch.listInsert ?? []) {
2431
+ const parent = ensureParentAtPath(target, operation2.path);
2432
+ const leaf = operation2.path[operation2.path.length - 1];
2111
2433
  if (parent[leaf] == null) {
2112
2434
  parent[leaf] = [];
2113
2435
  }
2114
- const list = readListAtPath(target, operation.path);
2115
- list.splice(operation.index, 0, ...readMockTypedValues(operation.values));
2436
+ const list = readListAtPath(target, operation2.path);
2437
+ list.splice(operation2.index, 0, ...readMockTypedValues(operation2.values));
2116
2438
  }
2117
- for (const operation of patch.textSplice ?? []) {
2118
- applyMockTextPatch(target, operation.path, [{
2119
- index: operation.index,
2120
- deleteCount: operation.deleteCount,
2121
- ...operation.insert !== void 0 ? { insert: operation.insert } : {}
2439
+ for (const operation2 of patch.textSplice ?? []) {
2440
+ applyMockTextPatch(target, operation2.path, [{
2441
+ index: operation2.index,
2442
+ deleteCount: operation2.deleteCount,
2443
+ ...operation2.insert !== void 0 ? { insert: operation2.insert } : {}
2122
2444
  }]);
2123
2445
  }
2124
- for (const operation of patch.textMark ?? []) {
2125
- applyMockTextMarks(target, operation.path, operation.index, operation.length, operation.marks, false);
2446
+ for (const operation2 of patch.textMark ?? []) {
2447
+ applyMockTextMarks(target, operation2.path, operation2.index, operation2.length, operation2.marks, false);
2126
2448
  }
2127
- for (const operation of patch.textUnmark ?? []) {
2128
- applyMockTextMarks(target, operation.path, operation.index, operation.length, Object.fromEntries(operation.names.map((name) => [name, null])), true);
2449
+ for (const operation2 of patch.textUnmark ?? []) {
2450
+ applyMockTextMarks(target, operation2.path, operation2.index, operation2.length, Object.fromEntries(operation2.names.map((name) => [name, null])), true);
2129
2451
  }
2130
- for (const operation of patch.counterIncrement ?? []) {
2131
- assertMockCounterAmount(operation.delta, "JSON counterIncrement delta");
2132
- const parent = ensureParentAtPath(target, operation.path);
2133
- const leaf = operation.path[operation.path.length - 1];
2452
+ for (const operation2 of patch.counterIncrement ?? []) {
2453
+ assertMockCounterAmount(operation2.delta, "JSON counterIncrement delta");
2454
+ const parent = ensureParentAtPath(target, operation2.path);
2455
+ const leaf = operation2.path[operation2.path.length - 1];
2134
2456
  const current = parent[leaf];
2135
2457
  if (current == null) {
2136
- parent[leaf] = operation.delta;
2458
+ parent[leaf] = operation2.delta;
2137
2459
  } else if (typeof current === "number") {
2138
- parent[leaf] = current + operation.delta;
2460
+ parent[leaf] = current + operation2.delta;
2139
2461
  } else {
2140
2462
  throw new Error(
2141
- `Cannot increment non-counter value at ${operation.path.map(String).join(".")}`
2463
+ `Cannot increment non-counter value at ${operation2.path.map(String).join(".")}`
2142
2464
  );
2143
2465
  }
2144
2466
  }
@@ -2642,44 +2964,44 @@ function validateMockJsonPatch(patch) {
2642
2964
  if (operationCount === 0) {
2643
2965
  throw new Error("JSON patch must include at least one operation");
2644
2966
  }
2645
- for (const operation of patch.set ?? []) {
2646
- validateMockJsonPath(operation.path, "JSON set");
2647
- hydrateMockAutomergeValues(operation.value);
2967
+ for (const operation2 of patch.set ?? []) {
2968
+ validateMockJsonPath(operation2.path, "JSON set");
2969
+ hydrateMockAutomergeValues(operation2.value);
2648
2970
  }
2649
- for (const operation of patch.unset ?? []) {
2650
- validateMockJsonPath(operation.path, "JSON unset");
2971
+ for (const operation2 of patch.unset ?? []) {
2972
+ validateMockJsonPath(operation2.path, "JSON unset");
2651
2973
  }
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");
2974
+ for (const operation2 of patch.listDelete ?? []) {
2975
+ validateMockJsonPath(operation2.path, "JSON listDelete");
2976
+ assertMockNonNegativeInteger(operation2.index, "JSON listDelete index");
2977
+ assertMockNonNegativeInteger(operation2.deleteCount, "JSON listDelete deleteCount");
2656
2978
  }
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)) {
2979
+ for (const operation2 of patch.listInsert ?? []) {
2980
+ validateMockJsonPath(operation2.path, "JSON listInsert");
2981
+ assertMockNonNegativeInteger(operation2.index, "JSON listInsert index");
2982
+ if (!Array.isArray(operation2.values)) {
2661
2983
  throw new Error("JSON listInsert values must be an array");
2662
2984
  }
2663
- hydrateMockAutomergeValues(operation.values);
2985
+ hydrateMockAutomergeValues(operation2.values);
2664
2986
  }
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") {
2987
+ for (const operation2 of patch.textSplice ?? []) {
2988
+ validateMockJsonPath(operation2.path, "JSON textSplice");
2989
+ assertMockNonNegativeInteger(operation2.index, "JSON textSplice index");
2990
+ assertMockNonNegativeInteger(operation2.deleteCount, "JSON textSplice deleteCount");
2991
+ if (operation2.insert !== void 0 && typeof operation2.insert !== "string") {
2670
2992
  throw new Error("JSON textSplice insert must be a string");
2671
2993
  }
2672
2994
  }
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) {
2995
+ for (const operation2 of [...patch.textMark ?? [], ...patch.textUnmark ?? []]) {
2996
+ validateMockJsonPath(operation2.path, "JSON text mark");
2997
+ assertMockNonNegativeInteger(operation2.index, "JSON text mark index");
2998
+ if (!Number.isInteger(operation2.length) || operation2.length <= 0) {
2677
2999
  throw new Error("JSON text mark length must be a positive integer");
2678
3000
  }
2679
3001
  }
2680
- for (const operation of patch.counterIncrement ?? []) {
2681
- validateMockJsonPath(operation.path, "JSON counterIncrement");
2682
- assertMockCounterAmount(operation.delta, "JSON counterIncrement delta");
3002
+ for (const operation2 of patch.counterIncrement ?? []) {
3003
+ validateMockJsonPath(operation2.path, "JSON counterIncrement");
3004
+ assertMockCounterAmount(operation2.delta, "JSON counterIncrement delta");
2683
3005
  }
2684
3006
  }
2685
3007
  function ensureMockAutomergeParent(target, path) {
@@ -2781,64 +3103,64 @@ function reviveMockRichTextValue(value) {
2781
3103
  );
2782
3104
  }
2783
3105
  function applyMockAutomergeJsonOperations(target, patch) {
2784
- for (const operation of patch.set ?? []) {
2785
- setMockAutomergeValue(target, operation.path, hydrateMockAutomergeValues(operation.value));
3106
+ for (const operation2 of patch.set ?? []) {
3107
+ setMockAutomergeValue(target, operation2.path, hydrateMockAutomergeValues(operation2.value));
2786
3108
  }
2787
- for (const operation of patch.unset ?? []) {
2788
- unsetMockAutomergeValue(target, operation.path);
3109
+ for (const operation2 of patch.unset ?? []) {
3110
+ unsetMockAutomergeValue(target, operation2.path);
2789
3111
  }
2790
- for (const operation of patch.listDelete ?? []) {
2791
- readMockAutomergeList(target, operation.path).splice(operation.index, operation.deleteCount);
3112
+ for (const operation2 of patch.listDelete ?? []) {
3113
+ readMockAutomergeList(target, operation2.path).splice(operation2.index, operation2.deleteCount);
2792
3114
  }
2793
- for (const operation of patch.listInsert ?? []) {
2794
- ensureMockAutomergeList(target, operation.path).splice(
2795
- operation.index,
3115
+ for (const operation2 of patch.listInsert ?? []) {
3116
+ ensureMockAutomergeList(target, operation2.path).splice(
3117
+ operation2.index,
2796
3118
  0,
2797
- ...hydrateMockAutomergeValues(operation.values)
3119
+ ...hydrateMockAutomergeValues(operation2.values)
2798
3120
  );
2799
3121
  }
2800
- for (const operation of patch.textSplice ?? []) {
2801
- ensureMockAutomergeText(target, operation.path, "JSON textSplice");
3122
+ for (const operation2 of patch.textSplice ?? []) {
3123
+ ensureMockAutomergeText(target, operation2.path, "JSON textSplice");
2802
3124
  spliceMockAutomergeText(
2803
3125
  target,
2804
- operation.path,
2805
- operation.index,
2806
- operation.deleteCount,
2807
- operation.insert ?? ""
3126
+ operation2.path,
3127
+ operation2.index,
3128
+ operation2.deleteCount,
3129
+ operation2.insert ?? ""
2808
3130
  );
2809
3131
  }
2810
- for (const operation of patch.textMark ?? []) {
2811
- for (const [name, value] of Object.entries(operation.marks)) {
3132
+ for (const operation2 of patch.textMark ?? []) {
3133
+ for (const [name, value] of Object.entries(operation2.marks)) {
2812
3134
  Automerge.mark(
2813
3135
  target,
2814
- operation.path,
2815
- { start: operation.index, end: operation.index + operation.length, expand: "none" },
3136
+ operation2.path,
3137
+ { start: operation2.index, end: operation2.index + operation2.length, expand: "none" },
2816
3138
  name,
2817
3139
  reviveMockRichTextValue(value)
2818
3140
  );
2819
3141
  }
2820
3142
  }
2821
- for (const operation of patch.textUnmark ?? []) {
2822
- for (const name of operation.names) {
3143
+ for (const operation2 of patch.textUnmark ?? []) {
3144
+ for (const name of operation2.names) {
2823
3145
  Automerge.unmark(
2824
3146
  target,
2825
- operation.path,
2826
- { start: operation.index, end: operation.index + operation.length, expand: "none" },
3147
+ operation2.path,
3148
+ { start: operation2.index, end: operation2.index + operation2.length, expand: "none" },
2827
3149
  name
2828
3150
  );
2829
3151
  }
2830
3152
  }
2831
- for (const operation of patch.counterIncrement ?? []) {
2832
- const parent = ensureMockAutomergeParent(target, operation.path);
2833
- const leaf = operation.path[operation.path.length - 1];
3153
+ for (const operation2 of patch.counterIncrement ?? []) {
3154
+ const parent = ensureMockAutomergeParent(target, operation2.path);
3155
+ const leaf = operation2.path[operation2.path.length - 1];
2834
3156
  const existing = parent[leaf];
2835
3157
  if (existing === void 0 || existing === null) {
2836
- parent[leaf] = new Automerge.Counter(operation.delta);
3158
+ parent[leaf] = new Automerge.Counter(operation2.delta);
2837
3159
  } else if (Automerge.isCounter(existing)) {
2838
- existing.increment(operation.delta);
3160
+ existing.increment(operation2.delta);
2839
3161
  } else {
2840
3162
  throw new Error(
2841
- `Cannot increment non-counter value at '${operation.path.map(String).join(".")}'; create it with MindooValue.counter() first`
3163
+ `Cannot increment non-counter value at '${operation2.path.map(String).join(".")}'; create it with MindooValue.counter() first`
2842
3164
  );
2843
3165
  }
2844
3166
  }
@@ -2850,7 +3172,232 @@ function changeMockAutomergeDocument(doc, baseHeads, apply) {
2850
3172
  }
2851
3173
  return Automerge.change(doc, apply);
2852
3174
  }
3175
+ function reviveMockRichTextSpans(spans2) {
3176
+ return spans2.map(
3177
+ (span) => span.type === "text" ? {
3178
+ type: "text",
3179
+ value: span.value,
3180
+ marks: span.marks ? reviveMockRichTextValue(span.marks) : void 0
3181
+ } : { type: "block", value: reviveMockRichTextValue(span.value) }
3182
+ );
3183
+ }
3184
+ function dehydrateMockRichTextValue(value) {
3185
+ if (Automerge.isImmutableString(value)) {
3186
+ return { type: "immutableString", value: String(value) };
3187
+ }
3188
+ if (Array.isArray(value)) {
3189
+ return value.map((entry) => dehydrateMockRichTextValue(entry));
3190
+ }
3191
+ if (!value || typeof value !== "object" || value instanceof Date || value instanceof Uint8Array) {
3192
+ return value;
3193
+ }
3194
+ return Object.fromEntries(
3195
+ Object.entries(value).map(([key, entry]) => [key, dehydrateMockRichTextValue(entry)])
3196
+ );
3197
+ }
3198
+ function validateMockRichTextSpans(spans2) {
3199
+ if (!Array.isArray(spans2)) {
3200
+ throw new Error("Rich-text patch spans must be an array");
3201
+ }
3202
+ for (const span of spans2) {
3203
+ if (!span || typeof span !== "object") {
3204
+ throw new Error("Rich-text span must be an object");
3205
+ }
3206
+ if (span.type === "text") {
3207
+ if (typeof span.value !== "string") {
3208
+ throw new Error("Rich-text text span value must be a string");
3209
+ }
3210
+ continue;
3211
+ }
3212
+ if (span.type === "block") {
3213
+ if (!span.value || typeof span.value !== "object" || Array.isArray(span.value)) {
3214
+ throw new Error("Rich-text block span value must be an object");
3215
+ }
3216
+ continue;
3217
+ }
3218
+ throw new Error("Rich-text span type must be 'text' or 'block'");
3219
+ }
3220
+ }
3221
+ function validateMockRichTextPatch(patch) {
3222
+ validateMockJsonPath(patch.path, "Rich-text patch");
3223
+ const hasSpans = patch.spans !== void 0;
3224
+ const hasSequence = patch.spansSequence !== void 0;
3225
+ if (hasSpans === hasSequence) {
3226
+ throw new Error("Rich-text patch must include exactly one of spans or spansSequence");
3227
+ }
3228
+ if (hasSpans) {
3229
+ validateMockRichTextSpans(patch.spans);
3230
+ return;
3231
+ }
3232
+ if (!Array.isArray(patch.spansSequence) || patch.spansSequence.length === 0) {
3233
+ throw new Error("Rich-text patch spansSequence must be a non-empty array");
3234
+ }
3235
+ for (const spans2 of patch.spansSequence) {
3236
+ validateMockRichTextSpans(spans2);
3237
+ }
3238
+ }
3239
+ function validateMockRichTextStepPatch(patch) {
3240
+ validateMockJsonPath(patch.path, "Rich-text steps patch");
3241
+ if (!Array.isArray(patch.steps) || patch.steps.length === 0) {
3242
+ throw new Error("Rich-text steps patch steps must be a non-empty array");
3243
+ }
3244
+ for (const step of patch.steps) {
3245
+ if (!step || typeof step !== "object") {
3246
+ throw new Error("Rich-text step must be an object");
3247
+ }
3248
+ if (step.type !== "splice") {
3249
+ throw new Error("Rich-text step type must be 'splice'");
3250
+ }
3251
+ if (!Number.isInteger(step.index) || step.index < 0) {
3252
+ throw new Error("Rich-text splice step index must be a non-negative integer");
3253
+ }
3254
+ if (!Number.isInteger(step.deleteCount) || step.deleteCount < 0) {
3255
+ throw new Error("Rich-text splice step deleteCount must be a non-negative integer");
3256
+ }
3257
+ if (step.insert !== void 0 && typeof step.insert !== "string") {
3258
+ throw new Error("Rich-text splice step insert must be a string");
3259
+ }
3260
+ for (const range of step.marks ?? []) {
3261
+ if (!Number.isInteger(range.index) || range.index < 0) {
3262
+ throw new Error("Rich-text mark range index must be a non-negative integer");
3263
+ }
3264
+ if (!Number.isInteger(range.length) || range.length <= 0) {
3265
+ throw new Error("Rich-text mark range length must be a positive integer");
3266
+ }
3267
+ if (!range.marks || typeof range.marks !== "object" || Array.isArray(range.marks)) {
3268
+ throw new Error("Rich-text mark range marks must be an object");
3269
+ }
3270
+ }
3271
+ }
3272
+ }
3273
+ function ensureMockRichTextPath(target, path) {
3274
+ let node = target;
3275
+ for (let index = 0; index < path.length - 1; index += 1) {
3276
+ const segment = path[index];
3277
+ if (node[segment] === void 0 || node[segment] === null) {
3278
+ node[segment] = typeof path[index + 1] === "number" ? [] : {};
3279
+ }
3280
+ node = node[segment];
3281
+ if (node === null || typeof node !== "object") {
3282
+ throw new Error(`Cannot apply rich-text patch through non-object path segment '${String(segment)}'`);
3283
+ }
3284
+ }
3285
+ const leaf = path[path.length - 1];
3286
+ const current = node[leaf];
3287
+ if (current === void 0 || current === null) {
3288
+ node[leaf] = "";
3289
+ return;
3290
+ }
3291
+ if (typeof current !== "string") {
3292
+ throw new Error(`Cannot apply rich-text patch to non-string value at '${path.map(String).join(".")}'`);
3293
+ }
3294
+ }
3295
+ function changeMockAutomergeDocumentOnFork(doc, baseHeads, apply) {
3296
+ const current = Automerge.getHeads(doc);
3297
+ if (!baseHeads || baseHeads.length === 0 || current.length === baseHeads.length && current.every((head) => baseHeads.includes(head))) {
3298
+ return Automerge.change(doc, apply);
3299
+ }
3300
+ const base = Automerge.view(doc, baseHeads);
3301
+ const actor = Automerge.getActorId(doc);
3302
+ const ownChangeSince = Automerge.getChanges(base, doc).some(
3303
+ (change2) => Automerge.decodeChange(change2).actor === actor
3304
+ );
3305
+ const fork = Automerge.clone(base, ownChangeSince ? void 0 : { actor });
3306
+ return Automerge.merge(doc, Automerge.change(fork, apply));
3307
+ }
3308
+ function isMockUpdateSpansBoundsError(error) {
3309
+ const message = error instanceof Error ? error.message : String(error);
3310
+ return /Cannot updateSpans|out of bounds/i.test(message);
3311
+ }
3312
+ function readMockAutomergeRichText(document2, path) {
3313
+ validateMockJsonPath(path, "Rich-text snapshot");
3314
+ const doc = loadMockAutomergeDocument(document2);
3315
+ return Automerge.spans(doc, path).map((span) => {
3316
+ const record = span;
3317
+ return record.type === "text" ? {
3318
+ type: "text",
3319
+ value: typeof record.value === "string" ? record.value : "",
3320
+ marks: record.marks ? dehydrateMockRichTextValue(record.marks) : void 0
3321
+ } : {
3322
+ type: "block",
3323
+ value: dehydrateMockRichTextValue(record.value)
3324
+ };
3325
+ });
3326
+ }
3327
+ function commonMockBaseHeads(patch) {
3328
+ const all = [
3329
+ patch.json?.baseHeads,
3330
+ ...(patch.text ?? []).map((p) => p.baseHeads),
3331
+ ...(patch.richText ?? []).map((p) => p.baseHeads),
3332
+ ...(patch.richTextSteps ?? []).map((p) => p.baseHeads)
3333
+ ].filter((heads) => Array.isArray(heads) && heads.length > 0);
3334
+ if (all.length === 0) return void 0;
3335
+ const first = [...all[0]].sort().join(",");
3336
+ return all.every((heads) => [...heads].sort().join(",") === first) ? all[0] : null;
3337
+ }
3338
+ function applyMockRichTextSpansInChange(target, richTextPatch) {
3339
+ const sequence = richTextPatch.spansSequence ?? (richTextPatch.spans ? [richTextPatch.spans] : []);
3340
+ const config = richTextPatch.updateSpansConfig;
3341
+ ensureMockRichTextPath(target, richTextPatch.path);
3342
+ const path = richTextPatch.path;
3343
+ for (const spans2 of sequence) {
3344
+ const revived = reviveMockRichTextSpans(spans2);
3345
+ try {
3346
+ Automerge.updateSpans(target, path, revived, config);
3347
+ } catch (error) {
3348
+ if (!isMockUpdateSpansBoundsError(error)) {
3349
+ throw error;
3350
+ }
3351
+ setMockAutomergeValue(target, richTextPatch.path, "");
3352
+ Automerge.updateSpans(target, path, revived, config);
3353
+ }
3354
+ }
3355
+ }
3356
+ function applyMockRichTextStepsInChange(target, stepPatch) {
3357
+ ensureMockRichTextPath(target, stepPatch.path);
3358
+ const path = stepPatch.path;
3359
+ for (const step of stepPatch.steps) {
3360
+ Automerge.splice(target, path, step.index, step.deleteCount, step.insert ?? "");
3361
+ for (const range of step.marks ?? []) {
3362
+ for (const [name, value] of Object.entries(range.marks)) {
3363
+ Automerge.mark(
3364
+ target,
3365
+ path,
3366
+ { start: range.index, end: range.index + range.length, expand: "none" },
3367
+ name,
3368
+ reviveMockRichTextValue(value)
3369
+ );
3370
+ }
3371
+ }
3372
+ }
3373
+ }
2853
3374
  function applyMockAutomergeUpdate(doc, patch) {
3375
+ const common = commonMockBaseHeads(patch);
3376
+ if (common !== null) {
3377
+ if (patch.json) validateMockJsonPatch(patch.json);
3378
+ for (const textPatch of patch.text ?? []) validateMockJsonPath(textPatch.path, "Text patch");
3379
+ for (const richTextPatch of patch.richText ?? []) validateMockRichTextPatch(richTextPatch);
3380
+ for (const stepPatch of patch.richTextSteps ?? []) validateMockRichTextStepPatch(stepPatch);
3381
+ return changeMockAutomergeDocumentOnFork(doc, common, (target) => {
3382
+ for (const [key, value] of Object.entries(patch.set ?? {})) {
3383
+ if (value !== void 0) {
3384
+ target[key] = hydrateMockAutomergeValues(value);
3385
+ }
3386
+ }
3387
+ for (const key of patch.unset ?? []) {
3388
+ delete target[key];
3389
+ }
3390
+ if (patch.json) applyMockAutomergeJsonOperations(target, patch.json);
3391
+ for (const textPatch of patch.text ?? []) {
3392
+ ensureMockAutomergeText(target, textPatch.path, "text patch");
3393
+ for (const edit of textPatch.edits) {
3394
+ spliceMockAutomergeText(target, textPatch.path, edit.index, edit.deleteCount, edit.insert ?? "");
3395
+ }
3396
+ }
3397
+ for (const richTextPatch of patch.richText ?? []) applyMockRichTextSpansInChange(target, richTextPatch);
3398
+ for (const stepPatch of patch.richTextSteps ?? []) applyMockRichTextStepsInChange(target, stepPatch);
3399
+ });
3400
+ }
2854
3401
  let next = doc;
2855
3402
  if (patch.set || (patch.unset?.length ?? 0) > 0) {
2856
3403
  next = Automerge.change(next, (target) => {
@@ -2883,26 +3430,44 @@ function applyMockAutomergeUpdate(doc, patch) {
2883
3430
  });
2884
3431
  }
2885
3432
  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)));
3433
+ validateMockRichTextPatch(richTextPatch);
3434
+ const sequence = richTextPatch.spansSequence ?? (richTextPatch.spans ? [richTextPatch.spans] : []);
3435
+ const config = richTextPatch.updateSpansConfig;
3436
+ next = changeMockAutomergeDocumentOnFork(next, richTextPatch.baseHeads, (target) => {
3437
+ ensureMockRichTextPath(target, richTextPatch.path);
3438
+ const path = richTextPatch.path;
3439
+ for (const spans2 of sequence) {
3440
+ const revived = reviveMockRichTextSpans(spans2);
3441
+ try {
3442
+ Automerge.updateSpans(target, path, revived, config);
3443
+ } catch (error) {
3444
+ if (!isMockUpdateSpansBoundsError(error)) {
3445
+ throw error;
3446
+ }
3447
+ setMockAutomergeValue(target, richTextPatch.path, "");
3448
+ Automerge.updateSpans(target, path, revived, config);
3449
+ }
3450
+ }
2890
3451
  });
2891
3452
  }
2892
3453
  for (const stepPatch of patch.richTextSteps ?? []) {
2893
- validateMockJsonPath(stepPatch.path, "Rich-text steps patch");
3454
+ validateMockRichTextStepPatch(stepPatch);
2894
3455
  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");
3456
+ ensureMockRichTextPath(target, stepPatch.path);
3457
+ const path = stepPatch.path;
2904
3458
  for (const step of stepPatch.steps) {
2905
- spliceMockAutomergeText(target, stepPatch.path, step.index, step.deleteCount, step.insert ?? "");
3459
+ Automerge.splice(target, path, step.index, step.deleteCount, step.insert ?? "");
3460
+ for (const range of step.marks ?? []) {
3461
+ for (const [name, value] of Object.entries(range.marks)) {
3462
+ Automerge.mark(
3463
+ target,
3464
+ path,
3465
+ { start: range.index, end: range.index + range.length, expand: "none" },
3466
+ name,
3467
+ reviveMockRichTextValue(value)
3468
+ );
3469
+ }
3470
+ }
2906
3471
  }
2907
3472
  });
2908
3473
  }
@@ -2925,8 +3490,8 @@ function forkMockAutomergeDocument(doc, baseHeads, actor) {
2925
3490
  }
2926
3491
  return Automerge.clone(Automerge.view(doc, [...heads]), { actor });
2927
3492
  }
2928
- function getFieldValue2(source, field) {
2929
- return field.split(".").reduce((current, part) => {
3493
+ function getFieldValue2(source, field2) {
3494
+ return field2.split(".").reduce((current, part) => {
2930
3495
  if (current && typeof current === "object" && part in current) {
2931
3496
  return current[part];
2932
3497
  }
@@ -2938,7 +3503,7 @@ function matchesDocumentFilter(document2, filter) {
2938
3503
  return true;
2939
3504
  }
2940
3505
  return Object.entries(filter).every(
2941
- ([field, expected]) => getFieldValue2(document2.data, field) === expected
3506
+ ([field2, expected]) => getFieldValue2(document2.data, field2) === expected
2942
3507
  );
2943
3508
  }
2944
3509
  function withValidatedCreateIds(documents) {
@@ -3015,7 +3580,7 @@ function computeMockTextScore(document2, clause) {
3015
3580
  if (terms.length === 0) {
3016
3581
  return void 0;
3017
3582
  }
3018
- const haystack = clause.fields ? clause.fields.map((field) => collectMockText(getFieldValue2(document2.data, field))).join(" ") : collectMockText(
3583
+ const haystack = clause.fields ? clause.fields.map((field2) => collectMockText(getFieldValue2(document2.data, field2))).join(" ") : collectMockText(
3019
3584
  Object.fromEntries(
3020
3585
  Object.entries(document2.data).filter(([key]) => !key.startsWith("_"))
3021
3586
  )
@@ -3245,7 +3810,7 @@ function createLiveMockViewNavigator(initial, rebuild, subscribe) {
3245
3810
  });
3246
3811
  }
3247
3812
  function projectMockFields(data, fields) {
3248
- return fields ? Object.fromEntries(fields.map((field) => [field, getFieldValue2(data, field)])) : structuredClone(data);
3813
+ return fields ? Object.fromEntries(fields.map((field2) => [field2, getFieldValue2(data, field2)])) : structuredClone(data);
3249
3814
  }
3250
3815
  function mockLastModified(document2) {
3251
3816
  return document2.updatedAt ? Date.parse(document2.updatedAt) || 0 : 0;
@@ -3773,19 +4338,56 @@ async function mockSha256Hex(value) {
3773
4338
  const digest = await subtle.digest("SHA-256", new TextEncoder().encode(value));
3774
4339
  return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
3775
4340
  }
3776
- function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
4341
+ function mockRevisionId(docId, index) {
4342
+ return `${docId}@${index}`;
4343
+ }
4344
+ function mockRevisionIndex(docId, revisionId) {
4345
+ const prefix = `${docId}@`;
4346
+ return revisionId.startsWith(prefix) ? Number(revisionId.slice(prefix.length)) : -1;
4347
+ }
4348
+ function createDatabaseHandle(definition, peers, directoryUsers = () => [], localActor = MOCK_AUTOMERGE_LOCAL_ACTOR) {
3777
4349
  let createCounter = 0;
3778
4350
  let changeCounter = 0;
4351
+ const replicaTag = localActor === MOCK_AUTOMERGE_LOCAL_ACTOR ? "" : `-${localActor.slice(0, 6)}`;
3779
4352
  const defaultViewFactory = async () => createDefaultViewNavigator();
3780
4353
  const automergeMode = definition.automerge === true;
3781
4354
  let changeSequence = 0;
3782
4355
  const changedAt = /* @__PURE__ */ new Map();
4356
+ const revisions = /* @__PURE__ */ new Map();
3783
4357
  const storedDocuments = new class extends Map {
3784
4358
  set(id, document2) {
3785
4359
  changedAt.set(id, ++changeSequence);
4360
+ const timeline = revisions.get(id) ?? [];
4361
+ const updatedAt = document2.updatedAt ? Date.parse(document2.updatedAt) : NaN;
4362
+ const previous = timeline[timeline.length - 1]?.timestamp ?? 0;
4363
+ timeline.push({
4364
+ // Strictly increasing, so a timestamp names one revision.
4365
+ timestamp: Math.max(Number.isFinite(updatedAt) ? updatedAt : Date.now(), previous + 1),
4366
+ data: structuredClone(document2.data),
4367
+ heads: document2.heads ? [...document2.heads] : void 0,
4368
+ isDeleted: document2.isDeleted,
4369
+ authorLabel: document2.authorLabel,
4370
+ attachments: structuredClone(document2.attachments ?? [])
4371
+ });
4372
+ revisions.set(id, timeline);
3786
4373
  return super.set(id, document2);
3787
4374
  }
3788
4375
  }();
4376
+ function historicalDocument(docId, index, timestamp, revisionId) {
4377
+ const revision = index >= 0 ? revisions.get(docId)?.[index] : void 0;
4378
+ if (!revision) {
4379
+ return { id: docId, timestamp, state: "missing", data: null, attachments: [], ...revisionId ? { revisionId } : {} };
4380
+ }
4381
+ return {
4382
+ id: docId,
4383
+ revisionId: revisionId ?? mockRevisionId(docId, index),
4384
+ timestamp: revision.timestamp,
4385
+ heads: revision.heads ? [...revision.heads] : [],
4386
+ state: revision.isDeleted ? "deleted" : "exists",
4387
+ data: revision.isDeleted ? null : structuredClone(revision.data),
4388
+ attachments: structuredClone(revision.attachments)
4389
+ };
4390
+ }
3789
4391
  for (const seed of definition.documents ?? []) {
3790
4392
  const stored = {
3791
4393
  id: seed.id,
@@ -3852,7 +4454,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3852
4454
  return writeAutomergeDocument(
3853
4455
  docId,
3854
4456
  existing,
3855
- Automerge.change(loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR), (target) => {
4457
+ Automerge.change(loadMockAutomergeDocument(existing, localActor), (target) => {
3856
4458
  target._encryptFor = hydrateMockAutomergeValues(nextEncryptFor);
3857
4459
  })
3858
4460
  );
@@ -3967,9 +4569,9 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
3967
4569
  };
3968
4570
  }
3969
4571
  const projectedData = query?.fields ? Object.fromEntries(
3970
- query.fields.map((field) => [
3971
- field,
3972
- getFieldValue2(document2.data, field)
4572
+ query.fields.map((field2) => [
4573
+ field2,
4574
+ getFieldValue2(document2.data, field2)
3973
4575
  ])
3974
4576
  ) : document2.data;
3975
4577
  return {
@@ -4030,7 +4632,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
4030
4632
  return {
4031
4633
  path: [...path],
4032
4634
  heads: document2.heads ? [...document2.heads] : void 0,
4033
- spans: structuredClone(readRichTextSpansAtPath(document2.data, path))
4635
+ spans: automergeMode ? readMockAutomergeRichText(document2, path) : structuredClone(readRichTextSpansAtPath(document2.data, path))
4034
4636
  };
4035
4637
  },
4036
4638
  // Mock cursors store the index they were created at and do not follow
@@ -4162,10 +4764,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
4162
4764
  id = callerId;
4163
4765
  } else if (typeof input.idPrefix === "string" && input.idPrefix.length > 0) {
4164
4766
  createCounter += 1;
4165
- id = `${input.idPrefix}_${String(createCounter).padStart(22, "0")}`;
4767
+ id = `${input.idPrefix}_${String(createCounter).padStart(22, "0")}${replicaTag}`;
4166
4768
  } else {
4167
4769
  createCounter += 1;
4168
- id = `doc-${createCounter}`;
4770
+ id = `doc-${createCounter}${replicaTag}`;
4169
4771
  }
4170
4772
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
4171
4773
  if (Array.isArray(input.recipients) && input.recipientOptions?.includeSelf === false && input.recipients.every((name) => name.trim().length === 0)) {
@@ -4187,7 +4789,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
4187
4789
  stored2,
4188
4790
  createMockAutomergeDocument(
4189
4791
  { ...input.set ?? {}, ...encryptFor },
4190
- MOCK_AUTOMERGE_LOCAL_ACTOR
4792
+ localActor
4191
4793
  )
4192
4794
  );
4193
4795
  storedDocuments.set(id, stored2);
@@ -4237,7 +4839,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
4237
4839
  const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4238
4840
  const existing = storedDocuments.get(docId);
4239
4841
  if (automergeMode) {
4240
- const current = existing?.automergeBinary ? loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR) : createMockAutomergeDocument(existing?.data ?? {}, MOCK_AUTOMERGE_LOCAL_ACTOR);
4842
+ const current = existing?.automergeBinary ? loadMockAutomergeDocument(existing, localActor) : createMockAutomergeDocument(existing?.data ?? {}, localActor);
4241
4843
  return writeAutomergeDocument(docId, existing, applyMockAutomergeUpdate(current, patch));
4242
4844
  }
4243
4845
  const updated = {
@@ -4351,28 +4953,30 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
4351
4953
  async listCreateKeys() {
4352
4954
  return [{ keyId: "default", isDefault: true }];
4353
4955
  },
4354
- async listHistory(_docId) {
4355
- return [];
4956
+ async listHistory(docId) {
4957
+ const timeline = revisions.get(docId) ?? [];
4958
+ return timeline.map((revision, index) => ({
4959
+ revisionId: mockRevisionId(docId, index),
4960
+ timestamp: revision.timestamp,
4961
+ ...revision.heads ? { heads: [...revision.heads] } : {},
4962
+ publicKey: revision.authorLabel ?? "mock-user",
4963
+ ...revision.authorLabel ? { identityLabel: revision.authorLabel } : {},
4964
+ isDeleted: revision.isDeleted,
4965
+ isCurrent: index === timeline.length - 1,
4966
+ ...index > 0 ? { dependencyIds: [mockRevisionId(docId, index - 1)] } : {}
4967
+ })).reverse();
4356
4968
  },
4357
4969
  async getAtTimestamp(docId, timestamp) {
4358
- return {
4359
- id: docId,
4360
- timestamp,
4361
- state: "missing",
4362
- data: null,
4363
- attachments: []
4364
- };
4970
+ const timeline = revisions.get(docId) ?? [];
4971
+ let index = -1;
4972
+ timeline.forEach((revision, position) => {
4973
+ if (revision.timestamp <= timestamp) index = position;
4974
+ });
4975
+ return historicalDocument(docId, index, timestamp);
4365
4976
  },
4366
- async getAtRevision(docId, revisionId) {
4367
- return {
4368
- id: docId,
4369
- revisionId,
4370
- timestamp: Date.now(),
4371
- heads: [],
4372
- state: "missing",
4373
- data: null,
4374
- attachments: []
4375
- };
4977
+ async getAtRevision(docId, revisionId, revisionOptions) {
4978
+ const index = mockRevisionIndex(docId, revisionId);
4979
+ return historicalDocument(docId, revisionOptions?.phase === "before" ? index - 1 : index, Date.now(), revisionId);
4376
4980
  },
4377
4981
  async getAtHeads(docId, _headIds) {
4378
4982
  return {
@@ -4559,7 +5163,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
4559
5163
  if (!automergeMode || !existing.automergeBinary) {
4560
5164
  return await defaultDocuments.update(docId, input);
4561
5165
  }
4562
- const current = loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR);
5166
+ const current = loadMockAutomergeDocument(existing, localActor);
4563
5167
  const remote = applyMockAutomergeUpdate(
4564
5168
  forkMockAutomergeDocument(
4565
5169
  current,
@@ -4574,13 +5178,17 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
4574
5178
  }
4575
5179
  function createMockSessionState(options = {}) {
4576
5180
  let launchContext = createDefaultLaunchContext(options.launchContext);
4577
- const embedHost = createMockEmbedHost(options, (databaseId) => {
4578
- const database = databaseHandles.get(databaseId);
4579
- if (!database) {
4580
- throw new Error(`Unknown test database: ${databaseId}`);
4581
- }
4582
- return database;
4583
- });
5181
+ const embedHost = createMockEmbedHost(
5182
+ options,
5183
+ (databaseId) => {
5184
+ const database = databaseHandles.get(databaseId);
5185
+ if (!database) {
5186
+ throw new Error(`Unknown test database: ${databaseId}`);
5187
+ }
5188
+ return database;
5189
+ },
5190
+ () => [...databaseHandles.keys()]
5191
+ );
4584
5192
  const themeListeners = /* @__PURE__ */ new Set();
4585
5193
  let incomingHandler = null;
4586
5194
  const pendingIncoming = [];
@@ -4692,7 +5300,12 @@ function createMockSessionState(options = {}) {
4692
5300
  capabilities: [...definition.info.capabilities]
4693
5301
  }));
4694
5302
  for (const definition of definitions) {
4695
- const created = createDatabaseHandle(definition, databasePeers, () => directoryUsers);
5303
+ const created = createDatabaseHandle(
5304
+ definition,
5305
+ databasePeers,
5306
+ () => directoryUsers,
5307
+ options.automergeActor ?? MOCK_AUTOMERGE_LOCAL_ACTOR
5308
+ );
4696
5309
  const databaseId = definition.info.id;
4697
5310
  databaseHandles.set(
4698
5311
  databaseId,
@@ -4706,35 +5319,48 @@ function createMockSessionState(options = {}) {
4706
5319
  databaseDocumentStores.set(definition.info.id, created.documents);
4707
5320
  databaseLiveQueryRefreshers.set(definition.info.id, created.notifyLiveQueries);
4708
5321
  databaseRemoteUpdaters.set(definition.info.id, created.applyRemoteUpdate);
5322
+ const createView = async (input) => {
5323
+ const build = () => createEvaluatingViewNavigator(
5324
+ input,
5325
+ input.databaseIds.flatMap((databaseId2) => {
5326
+ const list = databaseViewDocumentLists.get(databaseId2);
5327
+ return list ? list() : [];
5328
+ })
5329
+ );
5330
+ const subscribe = (onChange) => {
5331
+ const listener = { databaseIds: [...input.databaseIds], onChange };
5332
+ viewChangeListeners.add(listener);
5333
+ return () => viewChangeListeners.delete(listener);
5334
+ };
5335
+ const evaluating = await build();
5336
+ if (evaluating) {
5337
+ return createLiveMockViewNavigator(evaluating, build, subscribe);
5338
+ }
5339
+ console.warn(
5340
+ "[mindoodb-app-sdk/testing] mindoodb peer not available; using empty view navigator. Install mindoodb as a devDependency for evaluating VirtualViews."
5341
+ );
5342
+ return createLiveMockViewNavigator(
5343
+ await createDefaultViewNavigator(),
5344
+ async () => null,
5345
+ subscribe
5346
+ );
5347
+ };
4709
5348
  databaseViewApis.set(definition.info.id, {
4710
- async create(input) {
4711
- const build = () => createEvaluatingViewNavigator(
4712
- input,
4713
- input.databaseIds.flatMap((databaseId2) => {
4714
- const list = databaseViewDocumentLists.get(databaseId2);
4715
- return list ? list() : [];
4716
- })
4717
- );
4718
- const subscribe = (onChange) => {
4719
- const listener = { databaseIds: [...input.databaseIds], onChange };
4720
- viewChangeListeners.add(listener);
4721
- return () => viewChangeListeners.delete(listener);
4722
- };
4723
- const evaluating = await build();
4724
- if (evaluating) {
4725
- return createLiveMockViewNavigator(evaluating, build, subscribe);
5349
+ create: createView,
5350
+ // A view configured in the launch context, evaluated over the mock
5351
+ // documents of its sources: a fresh navigator per open, with the
5352
+ // open options applied, as Haven builds one.
5353
+ async open(viewId, openOptions) {
5354
+ const configured = launchContext.views.find((view2) => view2.id === viewId);
5355
+ if (!configured) {
5356
+ return await createDefaultViewNavigator();
4726
5357
  }
4727
- console.warn(
4728
- "[mindoodb-app-sdk/testing] mindoodb peer not available; using empty view navigator. Install mindoodb as a devDependency for evaluating VirtualViews."
4729
- );
4730
- return createLiveMockViewNavigator(
4731
- await createDefaultViewNavigator(),
4732
- async () => null,
4733
- subscribe
4734
- );
4735
- },
4736
- async open(_viewId) {
4737
- return await createDefaultViewNavigator();
5358
+ return await createView({
5359
+ databaseIds: configured.sources.map((source) => source.databaseId),
5360
+ definition: configuredViewDefinition(configured),
5361
+ categorizationStyle: configured.categorizationStyle,
5362
+ ...openOptions ? { options: openOptions } : {}
5363
+ });
4738
5364
  },
4739
5365
  ...definition.methods?.views ?? {}
4740
5366
  });
@@ -4830,16 +5456,21 @@ function createMockSessionState(options = {}) {
4830
5456
  }
4831
5457
  const view2 = await api.create(input);
4832
5458
  const viewId = input.definition.id || crypto.randomUUID();
4833
- sessionViews.set(viewId, view2);
5459
+ sessionViews.set(viewId, input);
4834
5460
  return view2;
4835
5461
  },
5462
+ // Every open is a navigator of its own with its own options, as in
5463
+ // Haven: disposing one leaves the others and later opens working.
4836
5464
  async openViewNavigator(viewId, options2) {
4837
- const existing = sessionViews.get(viewId);
4838
- if (existing) {
4839
- return existing;
5465
+ const created = sessionViews.get(viewId);
5466
+ if (created) {
5467
+ const api2 = databaseViewApis.get(created.databaseIds[0]);
5468
+ if (api2) {
5469
+ return await api2.create({ ...created, ...options2 ? { options: options2 } : {} });
5470
+ }
4840
5471
  }
4841
5472
  const sourceDatabaseId = launchContext.views.find(
4842
- (view3) => view3.id === viewId
5473
+ (view2) => view2.id === viewId
4843
5474
  )?.sources[0]?.databaseId;
4844
5475
  if (!sourceDatabaseId) {
4845
5476
  throw new Error(`Unknown test view: ${viewId}`);
@@ -4850,9 +5481,7 @@ function createMockSessionState(options = {}) {
4850
5481
  `Unknown test database for view ${viewId}: ${sourceDatabaseId}`
4851
5482
  );
4852
5483
  }
4853
- const view2 = await api.open(viewId, options2);
4854
- sessionViews.set(viewId, view2);
4855
- return view2;
5484
+ return await api.open(viewId, options2);
4856
5485
  },
4857
5486
  menus,
4858
5487
  drag,
@@ -4981,6 +5610,8 @@ function createMockSessionState(options = {}) {
4981
5610
  directoryUsers = [...users];
4982
5611
  },
4983
5612
  getDirectoryUsers: () => [...directoryUsers],
5613
+ replicaStores: () => databaseDocumentStores,
5614
+ notifyReplicaChange: (databaseId) => databaseLiveQueryRefreshers.get(databaseId)?.(),
4984
5615
  embedHost,
4985
5616
  getDatabase(databaseId) {
4986
5617
  const database = databaseHandles.get(databaseId);
@@ -5098,9 +5729,92 @@ function buildMockIncomingContent(input, deliveryId) {
5098
5729
  })
5099
5730
  };
5100
5731
  }
5732
+ var replicaStates = /* @__PURE__ */ new WeakMap();
5733
+ function sameMockHeads(a, b) {
5734
+ const x = a?.heads ?? [];
5735
+ const y = b?.heads ?? [];
5736
+ return x.length === y.length && x.every((head) => y.includes(head));
5737
+ }
5738
+ function mockReplicaDifferences(a, b) {
5739
+ const left = replicaStates.get(a)?.replicaStores();
5740
+ const right = replicaStates.get(b)?.replicaStores();
5741
+ if (!left || !right) throw new Error("syncMockReplicas: not a mock controller");
5742
+ let count = 0;
5743
+ for (const [databaseId, docsA] of left) {
5744
+ const docsB = right.get(databaseId);
5745
+ if (!docsB) continue;
5746
+ for (const id of /* @__PURE__ */ new Set([...docsA.keys(), ...docsB.keys()])) {
5747
+ const x = docsA.get(id);
5748
+ const y = docsB.get(id);
5749
+ if (!x || !y || x.isDeleted !== y.isDeleted || !sameMockHeads(x, y)) count += 1;
5750
+ }
5751
+ }
5752
+ return count;
5753
+ }
5754
+ async function syncMockReplicas(a, b) {
5755
+ const stateA = replicaStates.get(a);
5756
+ const stateB = replicaStates.get(b);
5757
+ if (!stateA || !stateB) throw new Error("syncMockReplicas: not a mock controller");
5758
+ let changed = 0;
5759
+ for (const [databaseId, docsA] of stateA.replicaStores()) {
5760
+ const docsB = stateB.replicaStores().get(databaseId);
5761
+ if (!docsB) continue;
5762
+ const touchedA = /* @__PURE__ */ new Set();
5763
+ const touchedB = /* @__PURE__ */ new Set();
5764
+ for (const id of /* @__PURE__ */ new Set([...docsA.keys(), ...docsB.keys()])) {
5765
+ const x = docsA.get(id);
5766
+ const y = docsB.get(id);
5767
+ if (x && y && x.isDeleted === y.isDeleted && sameMockHeads(x, y)) continue;
5768
+ changed += 1;
5769
+ let merged;
5770
+ if (x?.automergeBinary && y?.automergeBinary) {
5771
+ const doc = Automerge.merge(Automerge.load(x.automergeBinary), Automerge.load(y.automergeBinary));
5772
+ merged = { ...x, ...y, attachments: structuredClone(x.attachments ?? y.attachments ?? []) };
5773
+ persistMockAutomergeDocument(merged, doc);
5774
+ } else if (x && y) {
5775
+ merged = structuredClone((x.updatedAt ?? "") >= (y.updatedAt ?? "") ? x : y);
5776
+ } else {
5777
+ merged = structuredClone(x ?? y);
5778
+ }
5779
+ merged.isDeleted = Boolean(x?.isDeleted || y?.isDeleted);
5780
+ merged.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
5781
+ if (!x || !sameMockHeads(x, merged) || x.isDeleted !== merged.isDeleted) {
5782
+ docsA.set(id, structuredClone(merged));
5783
+ touchedA.add(id);
5784
+ }
5785
+ if (!y || !sameMockHeads(y, merged) || y.isDeleted !== merged.isDeleted) {
5786
+ docsB.set(id, structuredClone(merged));
5787
+ touchedB.add(id);
5788
+ }
5789
+ }
5790
+ const dbA = stateA.getDatabase(databaseId);
5791
+ const dbB = stateB.getDatabase(databaseId);
5792
+ for (const id of /* @__PURE__ */ new Set([...docsA.keys()])) {
5793
+ if (docsA.get(id)?.isDeleted) continue;
5794
+ for (const [from2, to, touched] of [
5795
+ [dbA, dbB, touchedB],
5796
+ [dbB, dbA, touchedA]
5797
+ ]) {
5798
+ const have = new Set((await to.attachments.list(id).catch(() => [])).map((f) => f.fileName));
5799
+ for (const file of await from2.attachments.list(id).catch(() => [])) {
5800
+ if (have.has(file.fileName)) continue;
5801
+ const reader = await from2.attachments.openReadStream(id, file.fileName);
5802
+ const writer = await to.attachments.openWriteStream(id, file.fileName, file.mimeType);
5803
+ for (let chunk = await reader.read(); chunk !== null; chunk = await reader.read()) await writer.write(chunk);
5804
+ await reader.close();
5805
+ await writer.close();
5806
+ touched.add(id);
5807
+ }
5808
+ }
5809
+ }
5810
+ if (touchedA.size) stateA.notifyReplicaChange(databaseId);
5811
+ if (touchedB.size) stateB.notifyReplicaChange(databaseId);
5812
+ }
5813
+ return changed;
5814
+ }
5101
5815
  function createMockMindooDBAppSession(options = {}) {
5102
5816
  const state = createMockSessionState(options);
5103
- return {
5817
+ const controller = {
5104
5818
  bridge: state.bridge,
5105
5819
  session: state.session,
5106
5820
  getLaunchContext: state.getLaunchContext,
@@ -5125,14 +5839,18 @@ function createMockMindooDBAppSession(options = {}) {
5125
5839
  getDragProfile: state.getDragProfile,
5126
5840
  emitIncomingContent: state.emitIncomingContent,
5127
5841
  listEmbeds: () => state.embedHost.listEmbeds(),
5842
+ getEmbedAccessRequests: () => state.embedHost.getAccessRequests(),
5128
5843
  closeEmbed: (embedId, reason, result) => state.embedHost.closeEmbed(embedId, reason, result),
5129
5844
  setComponents: (components) => state.embedHost.setComponents(components),
5130
5845
  getEmbeddingFinish: () => state.embedHost.getEmbeddingFinish(),
5131
5846
  setEmbedDirty: (embedId, dirty) => state.embedHost.setEmbedDirty(embedId, dirty),
5847
+ setEmbedAgentTools: (embedId, names, toolOptions) => state.embedHost.setEmbedAgentTools(embedId, names, toolOptions),
5132
5848
  failNextEmbedSave: (message) => state.embedHost.failNextSave(message),
5133
5849
  getEmbeddingDirty: () => state.embedHost.getEmbeddingDirty(),
5134
5850
  requestEmbeddingSave: () => state.embedHost.requestEmbeddingSave()
5135
5851
  };
5852
+ replicaStates.set(controller, state);
5853
+ return controller;
5136
5854
  }
5137
5855
  function createMockMindooDBAppBridge(options = {}) {
5138
5856
  return createMockMindooDBAppSession(options);
@@ -5151,6 +5869,7 @@ function createFakeBridgeHost(options = {}) {
5151
5869
  ...options.agentConsent !== void 0 ? { agentConsent: options.agentConsent } : {},
5152
5870
  ...options.agentInvokeTimeoutMs !== void 0 ? { agentInvokeTimeoutMs: options.agentInvokeTimeoutMs } : {},
5153
5871
  appId: () => state.getLaunchContext().appId,
5872
+ embedded: () => Boolean(state.getLaunchContext().embed),
5154
5873
  post(message) {
5155
5874
  const port = [...connectedPorts].pop();
5156
5875
  if (!port) return false;
@@ -5169,6 +5888,11 @@ function createFakeBridgeHost(options = {}) {
5169
5888
  (port) => port.postMessage({ protocol: PROTOCOL, kind: "embed-event", event: { type: "dirty", ...event } })
5170
5889
  );
5171
5890
  });
5891
+ state.embedHost.onAgentTools((event) => {
5892
+ connectedPorts.forEach(
5893
+ (port) => port.postMessage({ protocol: PROTOCOL, kind: "embed-event", event: { type: "agentTools", ...event } })
5894
+ );
5895
+ });
5172
5896
  const embeddingRequests = /* @__PURE__ */ new Map();
5173
5897
  let embeddingRequestCounter = 0;
5174
5898
  const beforeCloseWaiters = /* @__PURE__ */ new Map();
@@ -5409,6 +6133,10 @@ function createFakeBridgeHost(options = {}) {
5409
6133
  const embedId = await state.embedHost.openEmbed(input);
5410
6134
  return input.placement === "frame" && options.embedFrameUrl ? { embedId, frame: { url: options.embedFrameUrl } } : { embedId };
5411
6135
  }
6136
+ case "embeds.requestAccess":
6137
+ return await state.embedHost.embeds.requestAccess(
6138
+ params.input ?? { components: [] }
6139
+ );
5412
6140
  case "embeds.setRect":
5413
6141
  state.embedHost.setRect(String(params.embedId), params.rect);
5414
6142
  return { ok: true };
@@ -5421,9 +6149,18 @@ function createFakeBridgeHost(options = {}) {
5421
6149
  case "embeds.save":
5422
6150
  await state.embedHost.saveEmbed(String(params.embedId));
5423
6151
  return { ok: true };
6152
+ case "embeds.setActive":
6153
+ state.embedHost.setActive(String(params.embedId));
6154
+ return { ok: true };
5424
6155
  case "embedding.setDirty":
5425
6156
  state.embedHost.setEmbeddingDirty(params.dirty === true);
5426
6157
  return { ok: true };
6158
+ case "embedding.lookup":
6159
+ return await state.embedHost.embedding.lookup(String(params.field), {
6160
+ ...typeof params.query === "string" ? { query: params.query } : {},
6161
+ ...Array.isArray(params.docIds) ? { docIds: params.docIds.map(String) } : {},
6162
+ ...typeof params.limit === "number" ? { limit: params.limit } : {}
6163
+ });
5427
6164
  case "embedding.respond": {
5428
6165
  const pending = embeddingRequests.get(String(params.requestId));
5429
6166
  embeddingRequests.delete(String(params.requestId));
@@ -6057,9 +6794,11 @@ function createFakeBridgeHost(options = {}) {
6057
6794
  getDirectoryUsers: state.getDirectoryUsers,
6058
6795
  applyRemoteUpdate: state.applyRemoteUpdate,
6059
6796
  listEmbeds: () => state.embedHost.listEmbeds(),
6797
+ getEmbedAccessRequests: () => state.embedHost.getAccessRequests(),
6060
6798
  closeEmbed: (embedId, reason, result) => state.embedHost.closeEmbed(embedId, reason, result),
6061
6799
  getEmbeddingFinish: () => state.embedHost.getEmbeddingFinish(),
6062
6800
  setEmbedDirty: (embedId, dirty) => state.embedHost.setEmbedDirty(embedId, dirty),
6801
+ setEmbedAgentTools: (embedId, names, toolOptions) => state.embedHost.setEmbedAgentTools(embedId, names, toolOptions),
6063
6802
  failNextEmbedSave: (message) => state.embedHost.failNextSave(message),
6064
6803
  getEmbeddingDirty: () => state.embedHost.getEmbeddingDirty(),
6065
6804
  requestEmbeddingSave() {
@@ -6185,10 +6924,12 @@ function createFakeBridgeHost(options = {}) {
6185
6924
  customRequestHandlers.delete(method);
6186
6925
  }
6187
6926
  };
6927
+ replicaStates.set(controller, state);
6188
6928
  return controller;
6189
6929
  }
6190
6930
  export {
6191
6931
  ALL_CAPABILITIES,
6932
+ DEFAULT_TEST_HOST_USERS,
6192
6933
  MockForbiddenError,
6193
6934
  TEST_HOST_LOCALES,
6194
6935
  capabilitiesForDefinitionPermissions,
@@ -6203,8 +6944,12 @@ export {
6203
6944
  loadMindoodbForTesting,
6204
6945
  mockAgentToolPrefix,
6205
6946
  mockDatabasesFromDefinition,
6947
+ mockReplicaDifferences,
6948
+ modeSwitch,
6206
6949
  mountHavenTestHost,
6950
+ mountTwoUserTestHost,
6207
6951
  previewJson,
6208
6952
  readTestHostUrlSettings,
6953
+ syncMockReplicas,
6209
6954
  validateMockAgentTools
6210
6955
  };