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.
- package/README.md +124 -0
- package/TESTING.md +39 -1
- package/dist/{chunk-BE7LLFFE.js → chunk-376SABPG.js} +208 -2
- package/dist/index.d.ts +89 -3
- package/dist/index.js +89 -1
- package/dist/testing/index.d.ts +111 -2
- package/dist/testing/index.js +937 -192
- package/dist/{types-BeRUxGKN.d.ts → types-BGLXh4JQ.d.ts} +164 -2
- package/package.json +3 -3
package/dist/testing/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
mindooDBAppComponentFieldsMatch,
|
|
5
5
|
pageUsers,
|
|
6
6
|
releaseMindooDBAppBridgeSessions
|
|
7
|
-
} from "../chunk-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
1604
|
-
if (
|
|
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
|
|
2087
|
-
setValueAtPath(next, richTextPatch.path, structuredClone(
|
|
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
|
|
2099
|
-
setValueAtPath(target,
|
|
2420
|
+
for (const operation2 of patch.set ?? []) {
|
|
2421
|
+
setValueAtPath(target, operation2.path, readMockTypedValues(operation2.value));
|
|
2100
2422
|
}
|
|
2101
|
-
for (const
|
|
2102
|
-
unsetValueAtPath(target,
|
|
2423
|
+
for (const operation2 of patch.unset ?? []) {
|
|
2424
|
+
unsetValueAtPath(target, operation2.path);
|
|
2103
2425
|
}
|
|
2104
|
-
for (const
|
|
2105
|
-
const list = readListAtPath(target,
|
|
2106
|
-
list.splice(
|
|
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
|
|
2109
|
-
const parent = ensureParentAtPath(target,
|
|
2110
|
-
const leaf =
|
|
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,
|
|
2115
|
-
list.splice(
|
|
2436
|
+
const list = readListAtPath(target, operation2.path);
|
|
2437
|
+
list.splice(operation2.index, 0, ...readMockTypedValues(operation2.values));
|
|
2116
2438
|
}
|
|
2117
|
-
for (const
|
|
2118
|
-
applyMockTextPatch(target,
|
|
2119
|
-
index:
|
|
2120
|
-
deleteCount:
|
|
2121
|
-
...
|
|
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
|
|
2125
|
-
applyMockTextMarks(target,
|
|
2446
|
+
for (const operation2 of patch.textMark ?? []) {
|
|
2447
|
+
applyMockTextMarks(target, operation2.path, operation2.index, operation2.length, operation2.marks, false);
|
|
2126
2448
|
}
|
|
2127
|
-
for (const
|
|
2128
|
-
applyMockTextMarks(target,
|
|
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
|
|
2131
|
-
assertMockCounterAmount(
|
|
2132
|
-
const parent = ensureParentAtPath(target,
|
|
2133
|
-
const leaf =
|
|
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] =
|
|
2458
|
+
parent[leaf] = operation2.delta;
|
|
2137
2459
|
} else if (typeof current === "number") {
|
|
2138
|
-
parent[leaf] = current +
|
|
2460
|
+
parent[leaf] = current + operation2.delta;
|
|
2139
2461
|
} else {
|
|
2140
2462
|
throw new Error(
|
|
2141
|
-
`Cannot increment non-counter value at ${
|
|
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
|
|
2646
|
-
validateMockJsonPath(
|
|
2647
|
-
hydrateMockAutomergeValues(
|
|
2967
|
+
for (const operation2 of patch.set ?? []) {
|
|
2968
|
+
validateMockJsonPath(operation2.path, "JSON set");
|
|
2969
|
+
hydrateMockAutomergeValues(operation2.value);
|
|
2648
2970
|
}
|
|
2649
|
-
for (const
|
|
2650
|
-
validateMockJsonPath(
|
|
2971
|
+
for (const operation2 of patch.unset ?? []) {
|
|
2972
|
+
validateMockJsonPath(operation2.path, "JSON unset");
|
|
2651
2973
|
}
|
|
2652
|
-
for (const
|
|
2653
|
-
validateMockJsonPath(
|
|
2654
|
-
assertMockNonNegativeInteger(
|
|
2655
|
-
assertMockNonNegativeInteger(
|
|
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
|
|
2658
|
-
validateMockJsonPath(
|
|
2659
|
-
assertMockNonNegativeInteger(
|
|
2660
|
-
if (!Array.isArray(
|
|
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(
|
|
2985
|
+
hydrateMockAutomergeValues(operation2.values);
|
|
2664
2986
|
}
|
|
2665
|
-
for (const
|
|
2666
|
-
validateMockJsonPath(
|
|
2667
|
-
assertMockNonNegativeInteger(
|
|
2668
|
-
assertMockNonNegativeInteger(
|
|
2669
|
-
if (
|
|
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
|
|
2674
|
-
validateMockJsonPath(
|
|
2675
|
-
assertMockNonNegativeInteger(
|
|
2676
|
-
if (!Number.isInteger(
|
|
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
|
|
2681
|
-
validateMockJsonPath(
|
|
2682
|
-
assertMockCounterAmount(
|
|
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
|
|
2785
|
-
setMockAutomergeValue(target,
|
|
3106
|
+
for (const operation2 of patch.set ?? []) {
|
|
3107
|
+
setMockAutomergeValue(target, operation2.path, hydrateMockAutomergeValues(operation2.value));
|
|
2786
3108
|
}
|
|
2787
|
-
for (const
|
|
2788
|
-
unsetMockAutomergeValue(target,
|
|
3109
|
+
for (const operation2 of patch.unset ?? []) {
|
|
3110
|
+
unsetMockAutomergeValue(target, operation2.path);
|
|
2789
3111
|
}
|
|
2790
|
-
for (const
|
|
2791
|
-
readMockAutomergeList(target,
|
|
3112
|
+
for (const operation2 of patch.listDelete ?? []) {
|
|
3113
|
+
readMockAutomergeList(target, operation2.path).splice(operation2.index, operation2.deleteCount);
|
|
2792
3114
|
}
|
|
2793
|
-
for (const
|
|
2794
|
-
ensureMockAutomergeList(target,
|
|
2795
|
-
|
|
3115
|
+
for (const operation2 of patch.listInsert ?? []) {
|
|
3116
|
+
ensureMockAutomergeList(target, operation2.path).splice(
|
|
3117
|
+
operation2.index,
|
|
2796
3118
|
0,
|
|
2797
|
-
...hydrateMockAutomergeValues(
|
|
3119
|
+
...hydrateMockAutomergeValues(operation2.values)
|
|
2798
3120
|
);
|
|
2799
3121
|
}
|
|
2800
|
-
for (const
|
|
2801
|
-
ensureMockAutomergeText(target,
|
|
3122
|
+
for (const operation2 of patch.textSplice ?? []) {
|
|
3123
|
+
ensureMockAutomergeText(target, operation2.path, "JSON textSplice");
|
|
2802
3124
|
spliceMockAutomergeText(
|
|
2803
3125
|
target,
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
3126
|
+
operation2.path,
|
|
3127
|
+
operation2.index,
|
|
3128
|
+
operation2.deleteCount,
|
|
3129
|
+
operation2.insert ?? ""
|
|
2808
3130
|
);
|
|
2809
3131
|
}
|
|
2810
|
-
for (const
|
|
2811
|
-
for (const [name, value] of Object.entries(
|
|
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
|
-
|
|
2815
|
-
{ start:
|
|
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
|
|
2822
|
-
for (const name of
|
|
3143
|
+
for (const operation2 of patch.textUnmark ?? []) {
|
|
3144
|
+
for (const name of operation2.names) {
|
|
2823
3145
|
Automerge.unmark(
|
|
2824
3146
|
target,
|
|
2825
|
-
|
|
2826
|
-
{ start:
|
|
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
|
|
2832
|
-
const parent = ensureMockAutomergeParent(target,
|
|
2833
|
-
const leaf =
|
|
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(
|
|
3158
|
+
parent[leaf] = new Automerge.Counter(operation2.delta);
|
|
2837
3159
|
} else if (Automerge.isCounter(existing)) {
|
|
2838
|
-
existing.increment(
|
|
3160
|
+
existing.increment(operation2.delta);
|
|
2839
3161
|
} else {
|
|
2840
3162
|
throw new Error(
|
|
2841
|
-
`Cannot increment non-counter value at '${
|
|
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
|
-
|
|
2887
|
-
const
|
|
2888
|
-
|
|
2889
|
-
|
|
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
|
-
|
|
3454
|
+
validateMockRichTextStepPatch(stepPatch);
|
|
2894
3455
|
next = changeMockAutomergeDocument(next, stepPatch.baseHeads, (target) => {
|
|
2895
|
-
|
|
2896
|
-
|
|
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
|
-
|
|
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,
|
|
2929
|
-
return
|
|
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
|
-
([
|
|
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((
|
|
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((
|
|
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
|
|
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,
|
|
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((
|
|
3971
|
-
|
|
3972
|
-
getFieldValue2(document2.data,
|
|
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
|
-
|
|
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,
|
|
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(
|
|
4355
|
-
|
|
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
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
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
|
-
|
|
4368
|
-
|
|
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,
|
|
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(
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
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(
|
|
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
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
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
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
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,
|
|
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
|
|
4838
|
-
if (
|
|
4839
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|