mindoodb-app-sdk 0.0.65 → 0.0.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/TESTING.md +38 -1
- package/dist/{chunk-BE7LLFFE.js → chunk-B3K3I2S2.js} +185 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -1
- package/dist/testing/index.d.ts +97 -2
- package/dist/testing/index.js +833 -164
- package/dist/{types-BeRUxGKN.d.ts → types-Cuw-0Iou.d.ts} +121 -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-B3K3I2S2.js";
|
|
8
8
|
|
|
9
9
|
// src/testing/index.ts
|
|
10
10
|
import * as Automerge from "@automerge/automerge";
|
|
@@ -464,6 +464,8 @@ function createMockEmbedHost(options, getDatabase) {
|
|
|
464
464
|
const handles = /* @__PURE__ */ new Map();
|
|
465
465
|
const closedListeners = /* @__PURE__ */ new Set();
|
|
466
466
|
const dirtyListeners = /* @__PURE__ */ new Set();
|
|
467
|
+
const toolListeners = /* @__PURE__ */ new Set();
|
|
468
|
+
const toolNames = /* @__PURE__ */ new Map();
|
|
467
469
|
let nextSaveError = null;
|
|
468
470
|
let embeddingDirty;
|
|
469
471
|
let saveRequestHandler = null;
|
|
@@ -504,11 +506,49 @@ function createMockEmbedHost(options, getDatabase) {
|
|
|
504
506
|
docId: input.docId,
|
|
505
507
|
intent,
|
|
506
508
|
rect: mockRect(input),
|
|
507
|
-
visible: input.visible !== false
|
|
509
|
+
visible: input.visible !== false,
|
|
510
|
+
active: false,
|
|
511
|
+
agentTools: null
|
|
508
512
|
});
|
|
509
|
-
|
|
513
|
+
setActive(embedId);
|
|
510
514
|
return embedId;
|
|
511
515
|
}
|
|
516
|
+
function toolPrefix(component) {
|
|
517
|
+
return component.appId.replace(/^mindoodb-app-/, "").replace(/[^a-z0-9_]/g, "_") || "app";
|
|
518
|
+
}
|
|
519
|
+
function publishTools(entry) {
|
|
520
|
+
const declared = toolNames.get(entry.embedId);
|
|
521
|
+
const prefix = toolPrefix(entry.component);
|
|
522
|
+
const next = declared?.names.length ? { prefix, names: declared.names.map((name) => `${prefix}_${name}`), active: entry.active, enabled: declared.enabled } : null;
|
|
523
|
+
if (JSON.stringify(next) === JSON.stringify(entry.agentTools)) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
entry.agentTools = next;
|
|
527
|
+
handles.get(entry.embedId)?.setAgentToolsState(next);
|
|
528
|
+
toolListeners.forEach((listener) => listener({ embedId: entry.embedId, agentTools: next }));
|
|
529
|
+
}
|
|
530
|
+
function setActive(embedId) {
|
|
531
|
+
const target = embeds.get(embedId);
|
|
532
|
+
if (!target) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
for (const entry of embeds.values()) {
|
|
536
|
+
if (entry.component.appId === target.component.appId) {
|
|
537
|
+
entry.active = entry.embedId === embedId;
|
|
538
|
+
publishTools(entry);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
notifyChange();
|
|
542
|
+
}
|
|
543
|
+
function setEmbedAgentTools(embedId, names, toolOptions = {}) {
|
|
544
|
+
const entry = embeds.get(embedId);
|
|
545
|
+
if (!entry) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
toolNames.set(embedId, { names: [...names ?? []], enabled: toolOptions.enabled !== false });
|
|
549
|
+
publishTools(entry);
|
|
550
|
+
notifyChange();
|
|
551
|
+
}
|
|
512
552
|
function closeEmbed(embedId, reason = "closed", result) {
|
|
513
553
|
if (!embeds.delete(embedId)) {
|
|
514
554
|
return;
|
|
@@ -520,6 +560,7 @@ function createMockEmbedHost(options, getDatabase) {
|
|
|
520
560
|
};
|
|
521
561
|
handles.get(embedId)?.settle(event);
|
|
522
562
|
handles.delete(embedId);
|
|
563
|
+
toolNames.delete(embedId);
|
|
523
564
|
closedListeners.forEach((listener) => listener(event));
|
|
524
565
|
notifyChange();
|
|
525
566
|
}
|
|
@@ -558,6 +599,39 @@ function createMockEmbedHost(options, getDatabase) {
|
|
|
558
599
|
async save() {
|
|
559
600
|
await saveEmbed(this.embedId);
|
|
560
601
|
}
|
|
602
|
+
agentTools = null;
|
|
603
|
+
toolsListeners = /* @__PURE__ */ new Set();
|
|
604
|
+
async setActive() {
|
|
605
|
+
setActive(this.embedId);
|
|
606
|
+
}
|
|
607
|
+
onAgentToolsChange(listener) {
|
|
608
|
+
this.toolsListeners.add(listener);
|
|
609
|
+
return () => {
|
|
610
|
+
this.toolsListeners.delete(listener);
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
setAgentToolsState(tools) {
|
|
614
|
+
this.agentTools = tools;
|
|
615
|
+
this.toolsListeners.forEach((listener) => listener(tools));
|
|
616
|
+
}
|
|
617
|
+
waitForAgentTools(timeoutMs = 1e4) {
|
|
618
|
+
if (this.agentTools?.names.length) {
|
|
619
|
+
return Promise.resolve(this.agentTools);
|
|
620
|
+
}
|
|
621
|
+
return new Promise((resolve) => {
|
|
622
|
+
const timer = setTimeout(() => {
|
|
623
|
+
stop();
|
|
624
|
+
resolve(null);
|
|
625
|
+
}, timeoutMs);
|
|
626
|
+
const stop = this.onAgentToolsChange((tools) => {
|
|
627
|
+
if (tools?.names.length) {
|
|
628
|
+
clearTimeout(timer);
|
|
629
|
+
stop();
|
|
630
|
+
resolve(tools);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
}
|
|
561
635
|
resolve;
|
|
562
636
|
listeners = /* @__PURE__ */ new Set();
|
|
563
637
|
settled = null;
|
|
@@ -652,6 +726,9 @@ function createMockEmbedHost(options, getDatabase) {
|
|
|
652
726
|
async setDirty(dirty) {
|
|
653
727
|
embeddingDirty = dirty === true;
|
|
654
728
|
},
|
|
729
|
+
async lookup(field2, lookupOptions) {
|
|
730
|
+
return structuredClone(await (options.embeddingLookup?.(field2, { ...lookupOptions }) ?? []));
|
|
731
|
+
},
|
|
655
732
|
onSaveRequest(handler) {
|
|
656
733
|
saveRequestHandler = handler;
|
|
657
734
|
return () => {
|
|
@@ -684,6 +761,14 @@ function createMockEmbedHost(options, getDatabase) {
|
|
|
684
761
|
};
|
|
685
762
|
},
|
|
686
763
|
setEmbedDirty,
|
|
764
|
+
setActive,
|
|
765
|
+
setEmbedAgentTools,
|
|
766
|
+
onAgentTools(listener) {
|
|
767
|
+
toolListeners.add(listener);
|
|
768
|
+
return () => {
|
|
769
|
+
toolListeners.delete(listener);
|
|
770
|
+
};
|
|
771
|
+
},
|
|
687
772
|
onDirty(listener) {
|
|
688
773
|
dirtyListeners.add(listener);
|
|
689
774
|
return () => {
|
|
@@ -828,6 +913,44 @@ function guardDatabase(database, currentInfo, enforce) {
|
|
|
828
913
|
};
|
|
829
914
|
}
|
|
830
915
|
|
|
916
|
+
// src/testing/configuredViews.ts
|
|
917
|
+
var field = (path) => ({ kind: "field", path });
|
|
918
|
+
var literal = (value) => ({ kind: "literal", value });
|
|
919
|
+
var operation = (op, args) => ({ kind: "operation", op, args });
|
|
920
|
+
function ruleExpression(rule) {
|
|
921
|
+
const target = field(rule.field);
|
|
922
|
+
switch (rule.operator) {
|
|
923
|
+
case "notContains":
|
|
924
|
+
return operation("not", [operation("contains", [target, literal(rule.value ?? "")])]);
|
|
925
|
+
case "exists":
|
|
926
|
+
case "notExists":
|
|
927
|
+
return operation(rule.operator, [target]);
|
|
928
|
+
default:
|
|
929
|
+
return operation(rule.operator, [target, literal(rule.value ?? "")]);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
function configuredViewDefinition(view2) {
|
|
933
|
+
const filter = view2.filter.mode === "formula" ? { mode: "expression", expression: structuredClone(view2.filter.expression) } : view2.filter.rules.length ? {
|
|
934
|
+
mode: "expression",
|
|
935
|
+
expression: view2.filter.rules.length === 1 ? ruleExpression(view2.filter.rules[0]) : operation(view2.filter.match === "all" ? "and" : "or", view2.filter.rules.map(ruleExpression))
|
|
936
|
+
} : void 0;
|
|
937
|
+
return {
|
|
938
|
+
id: view2.id,
|
|
939
|
+
title: view2.description?.trim() || view2.id,
|
|
940
|
+
...filter ? { filter } : {},
|
|
941
|
+
defaultExpand: "collapsed",
|
|
942
|
+
columns: view2.columns.map((column) => ({
|
|
943
|
+
name: column.name,
|
|
944
|
+
title: column.title,
|
|
945
|
+
role: column.role === "sort" ? "display" : column.role,
|
|
946
|
+
expression: column.expression.mode === "field" ? field(column.expression.field) : structuredClone(column.expression.expression),
|
|
947
|
+
sorting: column.sorting,
|
|
948
|
+
hidden: column.hidden,
|
|
949
|
+
totalMode: column.totalMode
|
|
950
|
+
}))
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
831
954
|
// src/testing/mockAgentTools.ts
|
|
832
955
|
var TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]{0,47}$/;
|
|
833
956
|
var PREFIX_PATTERN = /^[a-z][a-z0-9_]{0,15}$/;
|
|
@@ -877,11 +1000,15 @@ function validateMockAgentTools(raw) {
|
|
|
877
1000
|
throw invalid(`Agent tool "${tool.name}" has invalid annotations.`);
|
|
878
1001
|
}
|
|
879
1002
|
}
|
|
1003
|
+
if (tool.scope !== void 0 && tool.scope !== "app" && tool.scope !== "document") {
|
|
1004
|
+
throw invalid(`Agent tool "${tool.name}" has an invalid scope; use "app" or "document".`);
|
|
1005
|
+
}
|
|
880
1006
|
return {
|
|
881
1007
|
name: tool.name,
|
|
882
1008
|
description: tool.description,
|
|
883
1009
|
inputSchema: schema,
|
|
884
|
-
...tool.annotations ? { annotations: { ...tool.annotations } } : {}
|
|
1010
|
+
...tool.annotations ? { annotations: { ...tool.annotations } } : {},
|
|
1011
|
+
...tool.scope ? { scope: tool.scope } : {}
|
|
885
1012
|
};
|
|
886
1013
|
});
|
|
887
1014
|
}
|
|
@@ -934,7 +1061,7 @@ function createMockAgentHost(options) {
|
|
|
934
1061
|
async function dispatch(method, params) {
|
|
935
1062
|
const input = params ?? {};
|
|
936
1063
|
if (method === "agent.registerTools") {
|
|
937
|
-
tools = validateMockAgentTools(input.tools);
|
|
1064
|
+
tools = validateMockAgentTools(input.tools).filter((tool) => !options.embedded?.() || tool.scope === "document");
|
|
938
1065
|
changed();
|
|
939
1066
|
return { result: registration() };
|
|
940
1067
|
}
|
|
@@ -1078,6 +1205,10 @@ function concat(chunks) {
|
|
|
1078
1205
|
}
|
|
1079
1206
|
return bytes;
|
|
1080
1207
|
}
|
|
1208
|
+
var MEMORY_ATTACHMENTS = /* @__PURE__ */ Symbol.for("mindoodb-app-sdk.memoryAttachments");
|
|
1209
|
+
function isMemoryAttachments(value) {
|
|
1210
|
+
return !!value && typeof value === "object" && MEMORY_ATTACHMENTS in value;
|
|
1211
|
+
}
|
|
1081
1212
|
function createMemoryAttachments() {
|
|
1082
1213
|
const files = /* @__PURE__ */ new Map();
|
|
1083
1214
|
const forDoc = (docId) => {
|
|
@@ -1085,7 +1216,7 @@ function createMemoryAttachments() {
|
|
|
1085
1216
|
if (!map) files.set(docId, map = /* @__PURE__ */ new Map());
|
|
1086
1217
|
return map;
|
|
1087
1218
|
};
|
|
1088
|
-
|
|
1219
|
+
const api = {
|
|
1089
1220
|
async list(docId) {
|
|
1090
1221
|
return [...forDoc(docId)].map(
|
|
1091
1222
|
([fileName, file]) => ({
|
|
@@ -1132,6 +1263,131 @@ function createMemoryAttachments() {
|
|
|
1132
1263
|
};
|
|
1133
1264
|
}
|
|
1134
1265
|
};
|
|
1266
|
+
return Object.assign(api, { [MEMORY_ATTACHMENTS]: true });
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/testing/twoUserTestHost.ts
|
|
1270
|
+
var DEFAULT_TEST_HOST_USERS = [
|
|
1271
|
+
{ name: "Anna", username: "CN=Anna Test/O=Test" },
|
|
1272
|
+
{ name: "Ben", username: "CN=Ben Test/O=Test" }
|
|
1273
|
+
];
|
|
1274
|
+
var STYLE = `
|
|
1275
|
+
.htu{display:grid;grid-template-rows:auto 1fr;height:100vh;margin:0;font:13px/1.45 system-ui,sans-serif;background:#eef1f7;color:#172033}
|
|
1276
|
+
.htu *{box-sizing:border-box}
|
|
1277
|
+
.htu__bar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:8px 12px;border-bottom:1px solid #d7dce8;background:#f8f9fc}
|
|
1278
|
+
.htu__badge{padding:2px 8px;border-radius:999px;background:#fde68a;color:#713f12;font-weight:600;font-size:11px;letter-spacing:.04em}
|
|
1279
|
+
.htu button{font:inherit;padding:5px 12px;border-radius:7px;border:1px solid #c9d0e0;background:#fff;cursor:pointer}
|
|
1280
|
+
.htu__status{font-weight:600}
|
|
1281
|
+
.htu .htest__mode{display:flex;gap:8px;align-items:center;padding:0 8px;border-left:1px solid #d7dce8;border-right:1px solid #d7dce8}
|
|
1282
|
+
.htu__panes{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:#d7dce8;min-height:0}
|
|
1283
|
+
.htu__pane{display:grid;grid-template-rows:auto 1fr;min-height:0;background:#fff}
|
|
1284
|
+
.htu__pane h2{margin:0;padding:4px 10px;font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#5b6478;background:#f8f9fc;border-bottom:1px solid #e4e8f1}
|
|
1285
|
+
.htu iframe{width:100%;height:100%;border:0}
|
|
1286
|
+
@media (max-width:900px){.htu__panes{grid-template-columns:1fr;grid-template-rows:1fr 1fr}}
|
|
1287
|
+
`;
|
|
1288
|
+
function replicaOf(databases) {
|
|
1289
|
+
return databases.map((database) => ({
|
|
1290
|
+
...database,
|
|
1291
|
+
automerge: true,
|
|
1292
|
+
info: structuredClone(database.info),
|
|
1293
|
+
documents: database.documents ? structuredClone(database.documents) : void 0,
|
|
1294
|
+
...database.methods ? {
|
|
1295
|
+
methods: {
|
|
1296
|
+
...database.methods,
|
|
1297
|
+
...isMemoryAttachments(database.methods.attachments) ? { attachments: createMemoryAttachments() } : {}
|
|
1298
|
+
}
|
|
1299
|
+
} : {}
|
|
1300
|
+
}));
|
|
1301
|
+
}
|
|
1302
|
+
function mountTwoUserTestHost(options, createHost) {
|
|
1303
|
+
const { container = document.body, title = "Haven test host", users = DEFAULT_TEST_HOST_USERS, ...hostOptions } = options;
|
|
1304
|
+
const style = document.createElement("style");
|
|
1305
|
+
style.textContent = STYLE;
|
|
1306
|
+
document.head.append(style);
|
|
1307
|
+
if (container === document.body) document.body.style.margin = "0";
|
|
1308
|
+
const root = document.createElement("div");
|
|
1309
|
+
root.className = "htu";
|
|
1310
|
+
const bar = document.createElement("div");
|
|
1311
|
+
bar.className = "htu__bar";
|
|
1312
|
+
const badge = document.createElement("span");
|
|
1313
|
+
badge.className = "htu__badge";
|
|
1314
|
+
badge.textContent = `TEST HOST \xB7 TWO USERS \xB7 ${title}`;
|
|
1315
|
+
const syncButton = document.createElement("button");
|
|
1316
|
+
syncButton.type = "button";
|
|
1317
|
+
syncButton.textContent = "Sync";
|
|
1318
|
+
syncButton.dataset.testid = "haven-two-users-sync";
|
|
1319
|
+
const auto = document.createElement("label");
|
|
1320
|
+
const autoBox = document.createElement("input");
|
|
1321
|
+
autoBox.type = "checkbox";
|
|
1322
|
+
autoBox.checked = true;
|
|
1323
|
+
autoBox.dataset.testid = "haven-two-users-autosync";
|
|
1324
|
+
auto.append(autoBox, " Auto-sync every 2 s");
|
|
1325
|
+
const status = document.createElement("span");
|
|
1326
|
+
status.className = "htu__status";
|
|
1327
|
+
status.dataset.testid = "haven-two-users-status";
|
|
1328
|
+
bar.append(badge, modeSwitch("two"), syncButton, auto, status);
|
|
1329
|
+
const panes = document.createElement("div");
|
|
1330
|
+
panes.className = "htu__panes";
|
|
1331
|
+
root.append(bar, panes);
|
|
1332
|
+
container.replaceChildren(root);
|
|
1333
|
+
const databasesA = (hostOptions.databases ?? []).map((database) => ({ ...database, automerge: true }));
|
|
1334
|
+
const hosts = users.map((user, index) => {
|
|
1335
|
+
const pane = document.createElement("section");
|
|
1336
|
+
pane.className = "htu__pane";
|
|
1337
|
+
const heading = document.createElement("h2");
|
|
1338
|
+
heading.textContent = `${user.name} \u2014 replica ${index + 1}`;
|
|
1339
|
+
const frame = document.createElement("iframe");
|
|
1340
|
+
frame.title = `${title} as ${user.name}`;
|
|
1341
|
+
frame.dataset.testid = `haven-two-users-frame-${index + 1}`;
|
|
1342
|
+
pane.append(heading, frame);
|
|
1343
|
+
panes.append(pane);
|
|
1344
|
+
const launchContext = {
|
|
1345
|
+
...hostOptions.launchContext,
|
|
1346
|
+
user: { id: `test-user-${index + 1}`, username: user.username }
|
|
1347
|
+
};
|
|
1348
|
+
return createHost({
|
|
1349
|
+
...hostOptions,
|
|
1350
|
+
frame,
|
|
1351
|
+
launchContext,
|
|
1352
|
+
databases: index === 0 ? databasesA : replicaOf(databasesA),
|
|
1353
|
+
// distinct Automerge actors: the two replicas write concurrently
|
|
1354
|
+
automergeActor: index === 0 ? "a11ce000000000000000000000000001" : "b0b00000000000000000000000000002"
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
let lastSync = null;
|
|
1358
|
+
const describe = () => {
|
|
1359
|
+
const open = mockReplicaDifferences(hosts[0].host, hosts[1].host);
|
|
1360
|
+
status.textContent = `${open ? `\u26A0 ${open} document(s) not synced` : "\u2713 in sync"} \xB7 last sync ${lastSync ?? "never"}`;
|
|
1361
|
+
status.style.color = open ? "#b45309" : "#15803d";
|
|
1362
|
+
};
|
|
1363
|
+
let syncing = null;
|
|
1364
|
+
const sync = () => {
|
|
1365
|
+
syncing ??= syncMockReplicas(hosts[0].host, hosts[1].host).finally(() => {
|
|
1366
|
+
syncing = null;
|
|
1367
|
+
lastSync = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
1368
|
+
describe();
|
|
1369
|
+
});
|
|
1370
|
+
return syncing;
|
|
1371
|
+
};
|
|
1372
|
+
syncButton.addEventListener("click", () => void sync());
|
|
1373
|
+
let timer;
|
|
1374
|
+
const setAutoSync = (on) => {
|
|
1375
|
+
autoBox.checked = on;
|
|
1376
|
+
if (timer) clearInterval(timer);
|
|
1377
|
+
timer = on ? setInterval(() => void sync(), 2e3) : void 0;
|
|
1378
|
+
};
|
|
1379
|
+
autoBox.addEventListener("change", () => setAutoSync(autoBox.checked));
|
|
1380
|
+
setAutoSync(true);
|
|
1381
|
+
setInterval(describe, 500);
|
|
1382
|
+
describe();
|
|
1383
|
+
const handle = {
|
|
1384
|
+
hosts,
|
|
1385
|
+
sync,
|
|
1386
|
+
setAutoSync,
|
|
1387
|
+
differences: () => mockReplicaDifferences(hosts[0].host, hosts[1].host)
|
|
1388
|
+
};
|
|
1389
|
+
window.__havenTwoUsers = handle;
|
|
1390
|
+
return handle;
|
|
1135
1391
|
}
|
|
1136
1392
|
|
|
1137
1393
|
// src/testing/browserTestHost.ts
|
|
@@ -1426,7 +1682,7 @@ var TEST_HOST_LOCALES = [
|
|
|
1426
1682
|
{ tag: "he-IL", label: "\u05E2\u05D1\u05E8\u05D9\u05EA" }
|
|
1427
1683
|
];
|
|
1428
1684
|
var DIRECTORY_SIZES = [0, 5, 25, 120, 500];
|
|
1429
|
-
var
|
|
1685
|
+
var STYLE2 = `
|
|
1430
1686
|
.htest{display:grid;grid-template-columns:minmax(0,1fr) 320px;height:100vh;margin:0;font:13px/1.45 system-ui,sans-serif;background:#eef1f7;color:#172033}
|
|
1431
1687
|
.htest *{box-sizing:border-box}
|
|
1432
1688
|
.htest__frame{width:100%;height:100%;border:0;background:#fff}
|
|
@@ -1494,12 +1750,40 @@ function item(text) {
|
|
|
1494
1750
|
li.textContent = text;
|
|
1495
1751
|
return li;
|
|
1496
1752
|
}
|
|
1753
|
+
function modeSwitch(current) {
|
|
1754
|
+
const row = document.createElement("div");
|
|
1755
|
+
row.className = "htest__row htest__mode";
|
|
1756
|
+
row.dataset.testid = "haven-test-mode";
|
|
1757
|
+
for (const [mode, label] of [
|
|
1758
|
+
["single", "Single user"],
|
|
1759
|
+
["two", "Two users"]
|
|
1760
|
+
]) {
|
|
1761
|
+
const url = new URL(window.location.href);
|
|
1762
|
+
if (mode === "two") url.searchParams.set("twoUsers", "1");
|
|
1763
|
+
else url.searchParams.delete("twoUsers");
|
|
1764
|
+
if (mode === current) {
|
|
1765
|
+
const active = document.createElement("strong");
|
|
1766
|
+
active.textContent = label;
|
|
1767
|
+
active.setAttribute("aria-current", "page");
|
|
1768
|
+
row.append(active);
|
|
1769
|
+
} else {
|
|
1770
|
+
const link = document.createElement("a");
|
|
1771
|
+
link.href = url.toString();
|
|
1772
|
+
link.textContent = label;
|
|
1773
|
+
link.dataset.testid = `haven-test-mode-${mode}`;
|
|
1774
|
+
row.append(link);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return row;
|
|
1778
|
+
}
|
|
1497
1779
|
function mountHavenTestHost(options) {
|
|
1498
1780
|
const {
|
|
1499
1781
|
container = document.body,
|
|
1500
1782
|
title = "Haven test host",
|
|
1501
1783
|
locales = TEST_HOST_LOCALES,
|
|
1502
1784
|
urlSettings: useUrl = true,
|
|
1785
|
+
twoUsers = false,
|
|
1786
|
+
users,
|
|
1503
1787
|
...hostOptions
|
|
1504
1788
|
} = options;
|
|
1505
1789
|
const url = useUrl ? readTestHostUrlSettings(window.location.search) : { overrides: {} };
|
|
@@ -1512,8 +1796,17 @@ function mountHavenTestHost(options) {
|
|
|
1512
1796
|
...url.locale ? { locale: url.locale } : {},
|
|
1513
1797
|
...url.theme ? { theme: { mode: url.theme, preset: "mindoo" } } : {}
|
|
1514
1798
|
};
|
|
1799
|
+
const twoUsersFromUrl = useUrl && new URLSearchParams(window.location.search).get("twoUsers") === "1";
|
|
1800
|
+
if (twoUsers || twoUsersFromUrl) {
|
|
1801
|
+
const handle = mountTwoUserTestHost(
|
|
1802
|
+
{ ...hostOptions, databases, launchContext, container, title, ...users ? { users } : {} },
|
|
1803
|
+
createBrowserTestHost
|
|
1804
|
+
);
|
|
1805
|
+
window.__havenTestHost = handle.hosts[0];
|
|
1806
|
+
return handle.hosts[0];
|
|
1807
|
+
}
|
|
1515
1808
|
const style = document.createElement("style");
|
|
1516
|
-
style.textContent =
|
|
1809
|
+
style.textContent = STYLE2;
|
|
1517
1810
|
document.head.append(style);
|
|
1518
1811
|
const root = document.createElement("div");
|
|
1519
1812
|
root.className = "htest";
|
|
@@ -1600,8 +1893,8 @@ function mountHavenTestHost(options) {
|
|
|
1600
1893
|
if (override.mapped === false) params.append("db", `${id}:none`);
|
|
1601
1894
|
}
|
|
1602
1895
|
if (testHost.host.getEnforceCapabilities()) params.set("enforce", "1");
|
|
1603
|
-
const
|
|
1604
|
-
if (
|
|
1896
|
+
const users2 = testHost.host.getDirectoryUsers().length;
|
|
1897
|
+
if (users2) params.set("users", String(users2));
|
|
1605
1898
|
if (currentLocale) params.set("locale", currentLocale);
|
|
1606
1899
|
if (initialDark === void 0 || dark !== initialDark) params.set("theme", dark ? "dark" : "light");
|
|
1607
1900
|
const query = params.toString();
|
|
@@ -1731,6 +2024,7 @@ Allow?`),
|
|
|
1731
2024
|
}),
|
|
1732
2025
|
button("Reload app", () => testHost.reloadApp())
|
|
1733
2026
|
);
|
|
2027
|
+
section("Mode", modeSwitch("single"));
|
|
1734
2028
|
section("Host", hostRow, focusState);
|
|
1735
2029
|
const language = document.createElement("select");
|
|
1736
2030
|
language.dataset.testid = "haven-test-language";
|
|
@@ -2083,8 +2377,8 @@ function applyDocumentUpdatePatch(current, patch) {
|
|
|
2083
2377
|
applyMockTextPatch(next, textPatch.path, textPatch.edits);
|
|
2084
2378
|
}
|
|
2085
2379
|
for (const richTextPatch of patch.richText ?? []) {
|
|
2086
|
-
const
|
|
2087
|
-
setValueAtPath(next, richTextPatch.path, structuredClone(
|
|
2380
|
+
const spans2 = richTextPatch.spansSequence?.at(-1) ?? richTextPatch.spans ?? [];
|
|
2381
|
+
setValueAtPath(next, richTextPatch.path, structuredClone(spans2));
|
|
2088
2382
|
}
|
|
2089
2383
|
for (const richTextStepPatch of patch.richTextSteps ?? []) {
|
|
2090
2384
|
applyMockRichTextSteps(next, richTextStepPatch.path, richTextStepPatch.steps);
|
|
@@ -2095,50 +2389,50 @@ function applyMockJsonPatch(target, patch) {
|
|
|
2095
2389
|
if (!patch) {
|
|
2096
2390
|
return;
|
|
2097
2391
|
}
|
|
2098
|
-
for (const
|
|
2099
|
-
setValueAtPath(target,
|
|
2392
|
+
for (const operation2 of patch.set ?? []) {
|
|
2393
|
+
setValueAtPath(target, operation2.path, readMockTypedValues(operation2.value));
|
|
2100
2394
|
}
|
|
2101
|
-
for (const
|
|
2102
|
-
unsetValueAtPath(target,
|
|
2395
|
+
for (const operation2 of patch.unset ?? []) {
|
|
2396
|
+
unsetValueAtPath(target, operation2.path);
|
|
2103
2397
|
}
|
|
2104
|
-
for (const
|
|
2105
|
-
const list = readListAtPath(target,
|
|
2106
|
-
list.splice(
|
|
2398
|
+
for (const operation2 of patch.listDelete ?? []) {
|
|
2399
|
+
const list = readListAtPath(target, operation2.path);
|
|
2400
|
+
list.splice(operation2.index, operation2.deleteCount);
|
|
2107
2401
|
}
|
|
2108
|
-
for (const
|
|
2109
|
-
const parent = ensureParentAtPath(target,
|
|
2110
|
-
const leaf =
|
|
2402
|
+
for (const operation2 of patch.listInsert ?? []) {
|
|
2403
|
+
const parent = ensureParentAtPath(target, operation2.path);
|
|
2404
|
+
const leaf = operation2.path[operation2.path.length - 1];
|
|
2111
2405
|
if (parent[leaf] == null) {
|
|
2112
2406
|
parent[leaf] = [];
|
|
2113
2407
|
}
|
|
2114
|
-
const list = readListAtPath(target,
|
|
2115
|
-
list.splice(
|
|
2408
|
+
const list = readListAtPath(target, operation2.path);
|
|
2409
|
+
list.splice(operation2.index, 0, ...readMockTypedValues(operation2.values));
|
|
2116
2410
|
}
|
|
2117
|
-
for (const
|
|
2118
|
-
applyMockTextPatch(target,
|
|
2119
|
-
index:
|
|
2120
|
-
deleteCount:
|
|
2121
|
-
...
|
|
2411
|
+
for (const operation2 of patch.textSplice ?? []) {
|
|
2412
|
+
applyMockTextPatch(target, operation2.path, [{
|
|
2413
|
+
index: operation2.index,
|
|
2414
|
+
deleteCount: operation2.deleteCount,
|
|
2415
|
+
...operation2.insert !== void 0 ? { insert: operation2.insert } : {}
|
|
2122
2416
|
}]);
|
|
2123
2417
|
}
|
|
2124
|
-
for (const
|
|
2125
|
-
applyMockTextMarks(target,
|
|
2418
|
+
for (const operation2 of patch.textMark ?? []) {
|
|
2419
|
+
applyMockTextMarks(target, operation2.path, operation2.index, operation2.length, operation2.marks, false);
|
|
2126
2420
|
}
|
|
2127
|
-
for (const
|
|
2128
|
-
applyMockTextMarks(target,
|
|
2421
|
+
for (const operation2 of patch.textUnmark ?? []) {
|
|
2422
|
+
applyMockTextMarks(target, operation2.path, operation2.index, operation2.length, Object.fromEntries(operation2.names.map((name) => [name, null])), true);
|
|
2129
2423
|
}
|
|
2130
|
-
for (const
|
|
2131
|
-
assertMockCounterAmount(
|
|
2132
|
-
const parent = ensureParentAtPath(target,
|
|
2133
|
-
const leaf =
|
|
2424
|
+
for (const operation2 of patch.counterIncrement ?? []) {
|
|
2425
|
+
assertMockCounterAmount(operation2.delta, "JSON counterIncrement delta");
|
|
2426
|
+
const parent = ensureParentAtPath(target, operation2.path);
|
|
2427
|
+
const leaf = operation2.path[operation2.path.length - 1];
|
|
2134
2428
|
const current = parent[leaf];
|
|
2135
2429
|
if (current == null) {
|
|
2136
|
-
parent[leaf] =
|
|
2430
|
+
parent[leaf] = operation2.delta;
|
|
2137
2431
|
} else if (typeof current === "number") {
|
|
2138
|
-
parent[leaf] = current +
|
|
2432
|
+
parent[leaf] = current + operation2.delta;
|
|
2139
2433
|
} else {
|
|
2140
2434
|
throw new Error(
|
|
2141
|
-
`Cannot increment non-counter value at ${
|
|
2435
|
+
`Cannot increment non-counter value at ${operation2.path.map(String).join(".")}`
|
|
2142
2436
|
);
|
|
2143
2437
|
}
|
|
2144
2438
|
}
|
|
@@ -2642,44 +2936,44 @@ function validateMockJsonPatch(patch) {
|
|
|
2642
2936
|
if (operationCount === 0) {
|
|
2643
2937
|
throw new Error("JSON patch must include at least one operation");
|
|
2644
2938
|
}
|
|
2645
|
-
for (const
|
|
2646
|
-
validateMockJsonPath(
|
|
2647
|
-
hydrateMockAutomergeValues(
|
|
2939
|
+
for (const operation2 of patch.set ?? []) {
|
|
2940
|
+
validateMockJsonPath(operation2.path, "JSON set");
|
|
2941
|
+
hydrateMockAutomergeValues(operation2.value);
|
|
2648
2942
|
}
|
|
2649
|
-
for (const
|
|
2650
|
-
validateMockJsonPath(
|
|
2943
|
+
for (const operation2 of patch.unset ?? []) {
|
|
2944
|
+
validateMockJsonPath(operation2.path, "JSON unset");
|
|
2651
2945
|
}
|
|
2652
|
-
for (const
|
|
2653
|
-
validateMockJsonPath(
|
|
2654
|
-
assertMockNonNegativeInteger(
|
|
2655
|
-
assertMockNonNegativeInteger(
|
|
2946
|
+
for (const operation2 of patch.listDelete ?? []) {
|
|
2947
|
+
validateMockJsonPath(operation2.path, "JSON listDelete");
|
|
2948
|
+
assertMockNonNegativeInteger(operation2.index, "JSON listDelete index");
|
|
2949
|
+
assertMockNonNegativeInteger(operation2.deleteCount, "JSON listDelete deleteCount");
|
|
2656
2950
|
}
|
|
2657
|
-
for (const
|
|
2658
|
-
validateMockJsonPath(
|
|
2659
|
-
assertMockNonNegativeInteger(
|
|
2660
|
-
if (!Array.isArray(
|
|
2951
|
+
for (const operation2 of patch.listInsert ?? []) {
|
|
2952
|
+
validateMockJsonPath(operation2.path, "JSON listInsert");
|
|
2953
|
+
assertMockNonNegativeInteger(operation2.index, "JSON listInsert index");
|
|
2954
|
+
if (!Array.isArray(operation2.values)) {
|
|
2661
2955
|
throw new Error("JSON listInsert values must be an array");
|
|
2662
2956
|
}
|
|
2663
|
-
hydrateMockAutomergeValues(
|
|
2957
|
+
hydrateMockAutomergeValues(operation2.values);
|
|
2664
2958
|
}
|
|
2665
|
-
for (const
|
|
2666
|
-
validateMockJsonPath(
|
|
2667
|
-
assertMockNonNegativeInteger(
|
|
2668
|
-
assertMockNonNegativeInteger(
|
|
2669
|
-
if (
|
|
2959
|
+
for (const operation2 of patch.textSplice ?? []) {
|
|
2960
|
+
validateMockJsonPath(operation2.path, "JSON textSplice");
|
|
2961
|
+
assertMockNonNegativeInteger(operation2.index, "JSON textSplice index");
|
|
2962
|
+
assertMockNonNegativeInteger(operation2.deleteCount, "JSON textSplice deleteCount");
|
|
2963
|
+
if (operation2.insert !== void 0 && typeof operation2.insert !== "string") {
|
|
2670
2964
|
throw new Error("JSON textSplice insert must be a string");
|
|
2671
2965
|
}
|
|
2672
2966
|
}
|
|
2673
|
-
for (const
|
|
2674
|
-
validateMockJsonPath(
|
|
2675
|
-
assertMockNonNegativeInteger(
|
|
2676
|
-
if (!Number.isInteger(
|
|
2967
|
+
for (const operation2 of [...patch.textMark ?? [], ...patch.textUnmark ?? []]) {
|
|
2968
|
+
validateMockJsonPath(operation2.path, "JSON text mark");
|
|
2969
|
+
assertMockNonNegativeInteger(operation2.index, "JSON text mark index");
|
|
2970
|
+
if (!Number.isInteger(operation2.length) || operation2.length <= 0) {
|
|
2677
2971
|
throw new Error("JSON text mark length must be a positive integer");
|
|
2678
2972
|
}
|
|
2679
2973
|
}
|
|
2680
|
-
for (const
|
|
2681
|
-
validateMockJsonPath(
|
|
2682
|
-
assertMockCounterAmount(
|
|
2974
|
+
for (const operation2 of patch.counterIncrement ?? []) {
|
|
2975
|
+
validateMockJsonPath(operation2.path, "JSON counterIncrement");
|
|
2976
|
+
assertMockCounterAmount(operation2.delta, "JSON counterIncrement delta");
|
|
2683
2977
|
}
|
|
2684
2978
|
}
|
|
2685
2979
|
function ensureMockAutomergeParent(target, path) {
|
|
@@ -2781,64 +3075,64 @@ function reviveMockRichTextValue(value) {
|
|
|
2781
3075
|
);
|
|
2782
3076
|
}
|
|
2783
3077
|
function applyMockAutomergeJsonOperations(target, patch) {
|
|
2784
|
-
for (const
|
|
2785
|
-
setMockAutomergeValue(target,
|
|
3078
|
+
for (const operation2 of patch.set ?? []) {
|
|
3079
|
+
setMockAutomergeValue(target, operation2.path, hydrateMockAutomergeValues(operation2.value));
|
|
2786
3080
|
}
|
|
2787
|
-
for (const
|
|
2788
|
-
unsetMockAutomergeValue(target,
|
|
3081
|
+
for (const operation2 of patch.unset ?? []) {
|
|
3082
|
+
unsetMockAutomergeValue(target, operation2.path);
|
|
2789
3083
|
}
|
|
2790
|
-
for (const
|
|
2791
|
-
readMockAutomergeList(target,
|
|
3084
|
+
for (const operation2 of patch.listDelete ?? []) {
|
|
3085
|
+
readMockAutomergeList(target, operation2.path).splice(operation2.index, operation2.deleteCount);
|
|
2792
3086
|
}
|
|
2793
|
-
for (const
|
|
2794
|
-
ensureMockAutomergeList(target,
|
|
2795
|
-
|
|
3087
|
+
for (const operation2 of patch.listInsert ?? []) {
|
|
3088
|
+
ensureMockAutomergeList(target, operation2.path).splice(
|
|
3089
|
+
operation2.index,
|
|
2796
3090
|
0,
|
|
2797
|
-
...hydrateMockAutomergeValues(
|
|
3091
|
+
...hydrateMockAutomergeValues(operation2.values)
|
|
2798
3092
|
);
|
|
2799
3093
|
}
|
|
2800
|
-
for (const
|
|
2801
|
-
ensureMockAutomergeText(target,
|
|
3094
|
+
for (const operation2 of patch.textSplice ?? []) {
|
|
3095
|
+
ensureMockAutomergeText(target, operation2.path, "JSON textSplice");
|
|
2802
3096
|
spliceMockAutomergeText(
|
|
2803
3097
|
target,
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
3098
|
+
operation2.path,
|
|
3099
|
+
operation2.index,
|
|
3100
|
+
operation2.deleteCount,
|
|
3101
|
+
operation2.insert ?? ""
|
|
2808
3102
|
);
|
|
2809
3103
|
}
|
|
2810
|
-
for (const
|
|
2811
|
-
for (const [name, value] of Object.entries(
|
|
3104
|
+
for (const operation2 of patch.textMark ?? []) {
|
|
3105
|
+
for (const [name, value] of Object.entries(operation2.marks)) {
|
|
2812
3106
|
Automerge.mark(
|
|
2813
3107
|
target,
|
|
2814
|
-
|
|
2815
|
-
{ start:
|
|
3108
|
+
operation2.path,
|
|
3109
|
+
{ start: operation2.index, end: operation2.index + operation2.length, expand: "none" },
|
|
2816
3110
|
name,
|
|
2817
3111
|
reviveMockRichTextValue(value)
|
|
2818
3112
|
);
|
|
2819
3113
|
}
|
|
2820
3114
|
}
|
|
2821
|
-
for (const
|
|
2822
|
-
for (const name of
|
|
3115
|
+
for (const operation2 of patch.textUnmark ?? []) {
|
|
3116
|
+
for (const name of operation2.names) {
|
|
2823
3117
|
Automerge.unmark(
|
|
2824
3118
|
target,
|
|
2825
|
-
|
|
2826
|
-
{ start:
|
|
3119
|
+
operation2.path,
|
|
3120
|
+
{ start: operation2.index, end: operation2.index + operation2.length, expand: "none" },
|
|
2827
3121
|
name
|
|
2828
3122
|
);
|
|
2829
3123
|
}
|
|
2830
3124
|
}
|
|
2831
|
-
for (const
|
|
2832
|
-
const parent = ensureMockAutomergeParent(target,
|
|
2833
|
-
const leaf =
|
|
3125
|
+
for (const operation2 of patch.counterIncrement ?? []) {
|
|
3126
|
+
const parent = ensureMockAutomergeParent(target, operation2.path);
|
|
3127
|
+
const leaf = operation2.path[operation2.path.length - 1];
|
|
2834
3128
|
const existing = parent[leaf];
|
|
2835
3129
|
if (existing === void 0 || existing === null) {
|
|
2836
|
-
parent[leaf] = new Automerge.Counter(
|
|
3130
|
+
parent[leaf] = new Automerge.Counter(operation2.delta);
|
|
2837
3131
|
} else if (Automerge.isCounter(existing)) {
|
|
2838
|
-
existing.increment(
|
|
3132
|
+
existing.increment(operation2.delta);
|
|
2839
3133
|
} else {
|
|
2840
3134
|
throw new Error(
|
|
2841
|
-
`Cannot increment non-counter value at '${
|
|
3135
|
+
`Cannot increment non-counter value at '${operation2.path.map(String).join(".")}'; create it with MindooValue.counter() first`
|
|
2842
3136
|
);
|
|
2843
3137
|
}
|
|
2844
3138
|
}
|
|
@@ -2850,7 +3144,232 @@ function changeMockAutomergeDocument(doc, baseHeads, apply) {
|
|
|
2850
3144
|
}
|
|
2851
3145
|
return Automerge.change(doc, apply);
|
|
2852
3146
|
}
|
|
3147
|
+
function reviveMockRichTextSpans(spans2) {
|
|
3148
|
+
return spans2.map(
|
|
3149
|
+
(span) => span.type === "text" ? {
|
|
3150
|
+
type: "text",
|
|
3151
|
+
value: span.value,
|
|
3152
|
+
marks: span.marks ? reviveMockRichTextValue(span.marks) : void 0
|
|
3153
|
+
} : { type: "block", value: reviveMockRichTextValue(span.value) }
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
function dehydrateMockRichTextValue(value) {
|
|
3157
|
+
if (Automerge.isImmutableString(value)) {
|
|
3158
|
+
return { type: "immutableString", value: String(value) };
|
|
3159
|
+
}
|
|
3160
|
+
if (Array.isArray(value)) {
|
|
3161
|
+
return value.map((entry) => dehydrateMockRichTextValue(entry));
|
|
3162
|
+
}
|
|
3163
|
+
if (!value || typeof value !== "object" || value instanceof Date || value instanceof Uint8Array) {
|
|
3164
|
+
return value;
|
|
3165
|
+
}
|
|
3166
|
+
return Object.fromEntries(
|
|
3167
|
+
Object.entries(value).map(([key, entry]) => [key, dehydrateMockRichTextValue(entry)])
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
function validateMockRichTextSpans(spans2) {
|
|
3171
|
+
if (!Array.isArray(spans2)) {
|
|
3172
|
+
throw new Error("Rich-text patch spans must be an array");
|
|
3173
|
+
}
|
|
3174
|
+
for (const span of spans2) {
|
|
3175
|
+
if (!span || typeof span !== "object") {
|
|
3176
|
+
throw new Error("Rich-text span must be an object");
|
|
3177
|
+
}
|
|
3178
|
+
if (span.type === "text") {
|
|
3179
|
+
if (typeof span.value !== "string") {
|
|
3180
|
+
throw new Error("Rich-text text span value must be a string");
|
|
3181
|
+
}
|
|
3182
|
+
continue;
|
|
3183
|
+
}
|
|
3184
|
+
if (span.type === "block") {
|
|
3185
|
+
if (!span.value || typeof span.value !== "object" || Array.isArray(span.value)) {
|
|
3186
|
+
throw new Error("Rich-text block span value must be an object");
|
|
3187
|
+
}
|
|
3188
|
+
continue;
|
|
3189
|
+
}
|
|
3190
|
+
throw new Error("Rich-text span type must be 'text' or 'block'");
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
function validateMockRichTextPatch(patch) {
|
|
3194
|
+
validateMockJsonPath(patch.path, "Rich-text patch");
|
|
3195
|
+
const hasSpans = patch.spans !== void 0;
|
|
3196
|
+
const hasSequence = patch.spansSequence !== void 0;
|
|
3197
|
+
if (hasSpans === hasSequence) {
|
|
3198
|
+
throw new Error("Rich-text patch must include exactly one of spans or spansSequence");
|
|
3199
|
+
}
|
|
3200
|
+
if (hasSpans) {
|
|
3201
|
+
validateMockRichTextSpans(patch.spans);
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
if (!Array.isArray(patch.spansSequence) || patch.spansSequence.length === 0) {
|
|
3205
|
+
throw new Error("Rich-text patch spansSequence must be a non-empty array");
|
|
3206
|
+
}
|
|
3207
|
+
for (const spans2 of patch.spansSequence) {
|
|
3208
|
+
validateMockRichTextSpans(spans2);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
function validateMockRichTextStepPatch(patch) {
|
|
3212
|
+
validateMockJsonPath(patch.path, "Rich-text steps patch");
|
|
3213
|
+
if (!Array.isArray(patch.steps) || patch.steps.length === 0) {
|
|
3214
|
+
throw new Error("Rich-text steps patch steps must be a non-empty array");
|
|
3215
|
+
}
|
|
3216
|
+
for (const step of patch.steps) {
|
|
3217
|
+
if (!step || typeof step !== "object") {
|
|
3218
|
+
throw new Error("Rich-text step must be an object");
|
|
3219
|
+
}
|
|
3220
|
+
if (step.type !== "splice") {
|
|
3221
|
+
throw new Error("Rich-text step type must be 'splice'");
|
|
3222
|
+
}
|
|
3223
|
+
if (!Number.isInteger(step.index) || step.index < 0) {
|
|
3224
|
+
throw new Error("Rich-text splice step index must be a non-negative integer");
|
|
3225
|
+
}
|
|
3226
|
+
if (!Number.isInteger(step.deleteCount) || step.deleteCount < 0) {
|
|
3227
|
+
throw new Error("Rich-text splice step deleteCount must be a non-negative integer");
|
|
3228
|
+
}
|
|
3229
|
+
if (step.insert !== void 0 && typeof step.insert !== "string") {
|
|
3230
|
+
throw new Error("Rich-text splice step insert must be a string");
|
|
3231
|
+
}
|
|
3232
|
+
for (const range of step.marks ?? []) {
|
|
3233
|
+
if (!Number.isInteger(range.index) || range.index < 0) {
|
|
3234
|
+
throw new Error("Rich-text mark range index must be a non-negative integer");
|
|
3235
|
+
}
|
|
3236
|
+
if (!Number.isInteger(range.length) || range.length <= 0) {
|
|
3237
|
+
throw new Error("Rich-text mark range length must be a positive integer");
|
|
3238
|
+
}
|
|
3239
|
+
if (!range.marks || typeof range.marks !== "object" || Array.isArray(range.marks)) {
|
|
3240
|
+
throw new Error("Rich-text mark range marks must be an object");
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
function ensureMockRichTextPath(target, path) {
|
|
3246
|
+
let node = target;
|
|
3247
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
3248
|
+
const segment = path[index];
|
|
3249
|
+
if (node[segment] === void 0 || node[segment] === null) {
|
|
3250
|
+
node[segment] = typeof path[index + 1] === "number" ? [] : {};
|
|
3251
|
+
}
|
|
3252
|
+
node = node[segment];
|
|
3253
|
+
if (node === null || typeof node !== "object") {
|
|
3254
|
+
throw new Error(`Cannot apply rich-text patch through non-object path segment '${String(segment)}'`);
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
const leaf = path[path.length - 1];
|
|
3258
|
+
const current = node[leaf];
|
|
3259
|
+
if (current === void 0 || current === null) {
|
|
3260
|
+
node[leaf] = "";
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
if (typeof current !== "string") {
|
|
3264
|
+
throw new Error(`Cannot apply rich-text patch to non-string value at '${path.map(String).join(".")}'`);
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
function changeMockAutomergeDocumentOnFork(doc, baseHeads, apply) {
|
|
3268
|
+
const current = Automerge.getHeads(doc);
|
|
3269
|
+
if (!baseHeads || baseHeads.length === 0 || current.length === baseHeads.length && current.every((head) => baseHeads.includes(head))) {
|
|
3270
|
+
return Automerge.change(doc, apply);
|
|
3271
|
+
}
|
|
3272
|
+
const base = Automerge.view(doc, baseHeads);
|
|
3273
|
+
const actor = Automerge.getActorId(doc);
|
|
3274
|
+
const ownChangeSince = Automerge.getChanges(base, doc).some(
|
|
3275
|
+
(change2) => Automerge.decodeChange(change2).actor === actor
|
|
3276
|
+
);
|
|
3277
|
+
const fork = Automerge.clone(base, ownChangeSince ? void 0 : { actor });
|
|
3278
|
+
return Automerge.merge(doc, Automerge.change(fork, apply));
|
|
3279
|
+
}
|
|
3280
|
+
function isMockUpdateSpansBoundsError(error) {
|
|
3281
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3282
|
+
return /Cannot updateSpans|out of bounds/i.test(message);
|
|
3283
|
+
}
|
|
3284
|
+
function readMockAutomergeRichText(document2, path) {
|
|
3285
|
+
validateMockJsonPath(path, "Rich-text snapshot");
|
|
3286
|
+
const doc = loadMockAutomergeDocument(document2);
|
|
3287
|
+
return Automerge.spans(doc, path).map((span) => {
|
|
3288
|
+
const record = span;
|
|
3289
|
+
return record.type === "text" ? {
|
|
3290
|
+
type: "text",
|
|
3291
|
+
value: typeof record.value === "string" ? record.value : "",
|
|
3292
|
+
marks: record.marks ? dehydrateMockRichTextValue(record.marks) : void 0
|
|
3293
|
+
} : {
|
|
3294
|
+
type: "block",
|
|
3295
|
+
value: dehydrateMockRichTextValue(record.value)
|
|
3296
|
+
};
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
function commonMockBaseHeads(patch) {
|
|
3300
|
+
const all = [
|
|
3301
|
+
patch.json?.baseHeads,
|
|
3302
|
+
...(patch.text ?? []).map((p) => p.baseHeads),
|
|
3303
|
+
...(patch.richText ?? []).map((p) => p.baseHeads),
|
|
3304
|
+
...(patch.richTextSteps ?? []).map((p) => p.baseHeads)
|
|
3305
|
+
].filter((heads) => Array.isArray(heads) && heads.length > 0);
|
|
3306
|
+
if (all.length === 0) return void 0;
|
|
3307
|
+
const first = [...all[0]].sort().join(",");
|
|
3308
|
+
return all.every((heads) => [...heads].sort().join(",") === first) ? all[0] : null;
|
|
3309
|
+
}
|
|
3310
|
+
function applyMockRichTextSpansInChange(target, richTextPatch) {
|
|
3311
|
+
const sequence = richTextPatch.spansSequence ?? (richTextPatch.spans ? [richTextPatch.spans] : []);
|
|
3312
|
+
const config = richTextPatch.updateSpansConfig;
|
|
3313
|
+
ensureMockRichTextPath(target, richTextPatch.path);
|
|
3314
|
+
const path = richTextPatch.path;
|
|
3315
|
+
for (const spans2 of sequence) {
|
|
3316
|
+
const revived = reviveMockRichTextSpans(spans2);
|
|
3317
|
+
try {
|
|
3318
|
+
Automerge.updateSpans(target, path, revived, config);
|
|
3319
|
+
} catch (error) {
|
|
3320
|
+
if (!isMockUpdateSpansBoundsError(error)) {
|
|
3321
|
+
throw error;
|
|
3322
|
+
}
|
|
3323
|
+
setMockAutomergeValue(target, richTextPatch.path, "");
|
|
3324
|
+
Automerge.updateSpans(target, path, revived, config);
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
function applyMockRichTextStepsInChange(target, stepPatch) {
|
|
3329
|
+
ensureMockRichTextPath(target, stepPatch.path);
|
|
3330
|
+
const path = stepPatch.path;
|
|
3331
|
+
for (const step of stepPatch.steps) {
|
|
3332
|
+
Automerge.splice(target, path, step.index, step.deleteCount, step.insert ?? "");
|
|
3333
|
+
for (const range of step.marks ?? []) {
|
|
3334
|
+
for (const [name, value] of Object.entries(range.marks)) {
|
|
3335
|
+
Automerge.mark(
|
|
3336
|
+
target,
|
|
3337
|
+
path,
|
|
3338
|
+
{ start: range.index, end: range.index + range.length, expand: "none" },
|
|
3339
|
+
name,
|
|
3340
|
+
reviveMockRichTextValue(value)
|
|
3341
|
+
);
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
2853
3346
|
function applyMockAutomergeUpdate(doc, patch) {
|
|
3347
|
+
const common = commonMockBaseHeads(patch);
|
|
3348
|
+
if (common !== null) {
|
|
3349
|
+
if (patch.json) validateMockJsonPatch(patch.json);
|
|
3350
|
+
for (const textPatch of patch.text ?? []) validateMockJsonPath(textPatch.path, "Text patch");
|
|
3351
|
+
for (const richTextPatch of patch.richText ?? []) validateMockRichTextPatch(richTextPatch);
|
|
3352
|
+
for (const stepPatch of patch.richTextSteps ?? []) validateMockRichTextStepPatch(stepPatch);
|
|
3353
|
+
return changeMockAutomergeDocumentOnFork(doc, common, (target) => {
|
|
3354
|
+
for (const [key, value] of Object.entries(patch.set ?? {})) {
|
|
3355
|
+
if (value !== void 0) {
|
|
3356
|
+
target[key] = hydrateMockAutomergeValues(value);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
for (const key of patch.unset ?? []) {
|
|
3360
|
+
delete target[key];
|
|
3361
|
+
}
|
|
3362
|
+
if (patch.json) applyMockAutomergeJsonOperations(target, patch.json);
|
|
3363
|
+
for (const textPatch of patch.text ?? []) {
|
|
3364
|
+
ensureMockAutomergeText(target, textPatch.path, "text patch");
|
|
3365
|
+
for (const edit of textPatch.edits) {
|
|
3366
|
+
spliceMockAutomergeText(target, textPatch.path, edit.index, edit.deleteCount, edit.insert ?? "");
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
for (const richTextPatch of patch.richText ?? []) applyMockRichTextSpansInChange(target, richTextPatch);
|
|
3370
|
+
for (const stepPatch of patch.richTextSteps ?? []) applyMockRichTextStepsInChange(target, stepPatch);
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
2854
3373
|
let next = doc;
|
|
2855
3374
|
if (patch.set || (patch.unset?.length ?? 0) > 0) {
|
|
2856
3375
|
next = Automerge.change(next, (target) => {
|
|
@@ -2883,26 +3402,44 @@ function applyMockAutomergeUpdate(doc, patch) {
|
|
|
2883
3402
|
});
|
|
2884
3403
|
}
|
|
2885
3404
|
for (const richTextPatch of patch.richText ?? []) {
|
|
2886
|
-
|
|
2887
|
-
const
|
|
2888
|
-
|
|
2889
|
-
|
|
3405
|
+
validateMockRichTextPatch(richTextPatch);
|
|
3406
|
+
const sequence = richTextPatch.spansSequence ?? (richTextPatch.spans ? [richTextPatch.spans] : []);
|
|
3407
|
+
const config = richTextPatch.updateSpansConfig;
|
|
3408
|
+
next = changeMockAutomergeDocumentOnFork(next, richTextPatch.baseHeads, (target) => {
|
|
3409
|
+
ensureMockRichTextPath(target, richTextPatch.path);
|
|
3410
|
+
const path = richTextPatch.path;
|
|
3411
|
+
for (const spans2 of sequence) {
|
|
3412
|
+
const revived = reviveMockRichTextSpans(spans2);
|
|
3413
|
+
try {
|
|
3414
|
+
Automerge.updateSpans(target, path, revived, config);
|
|
3415
|
+
} catch (error) {
|
|
3416
|
+
if (!isMockUpdateSpansBoundsError(error)) {
|
|
3417
|
+
throw error;
|
|
3418
|
+
}
|
|
3419
|
+
setMockAutomergeValue(target, richTextPatch.path, "");
|
|
3420
|
+
Automerge.updateSpans(target, path, revived, config);
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
2890
3423
|
});
|
|
2891
3424
|
}
|
|
2892
3425
|
for (const stepPatch of patch.richTextSteps ?? []) {
|
|
2893
|
-
|
|
3426
|
+
validateMockRichTextStepPatch(stepPatch);
|
|
2894
3427
|
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");
|
|
3428
|
+
ensureMockRichTextPath(target, stepPatch.path);
|
|
3429
|
+
const path = stepPatch.path;
|
|
2904
3430
|
for (const step of stepPatch.steps) {
|
|
2905
|
-
|
|
3431
|
+
Automerge.splice(target, path, step.index, step.deleteCount, step.insert ?? "");
|
|
3432
|
+
for (const range of step.marks ?? []) {
|
|
3433
|
+
for (const [name, value] of Object.entries(range.marks)) {
|
|
3434
|
+
Automerge.mark(
|
|
3435
|
+
target,
|
|
3436
|
+
path,
|
|
3437
|
+
{ start: range.index, end: range.index + range.length, expand: "none" },
|
|
3438
|
+
name,
|
|
3439
|
+
reviveMockRichTextValue(value)
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
2906
3443
|
}
|
|
2907
3444
|
});
|
|
2908
3445
|
}
|
|
@@ -2925,8 +3462,8 @@ function forkMockAutomergeDocument(doc, baseHeads, actor) {
|
|
|
2925
3462
|
}
|
|
2926
3463
|
return Automerge.clone(Automerge.view(doc, [...heads]), { actor });
|
|
2927
3464
|
}
|
|
2928
|
-
function getFieldValue2(source,
|
|
2929
|
-
return
|
|
3465
|
+
function getFieldValue2(source, field2) {
|
|
3466
|
+
return field2.split(".").reduce((current, part) => {
|
|
2930
3467
|
if (current && typeof current === "object" && part in current) {
|
|
2931
3468
|
return current[part];
|
|
2932
3469
|
}
|
|
@@ -2938,7 +3475,7 @@ function matchesDocumentFilter(document2, filter) {
|
|
|
2938
3475
|
return true;
|
|
2939
3476
|
}
|
|
2940
3477
|
return Object.entries(filter).every(
|
|
2941
|
-
([
|
|
3478
|
+
([field2, expected]) => getFieldValue2(document2.data, field2) === expected
|
|
2942
3479
|
);
|
|
2943
3480
|
}
|
|
2944
3481
|
function withValidatedCreateIds(documents) {
|
|
@@ -3015,7 +3552,7 @@ function computeMockTextScore(document2, clause) {
|
|
|
3015
3552
|
if (terms.length === 0) {
|
|
3016
3553
|
return void 0;
|
|
3017
3554
|
}
|
|
3018
|
-
const haystack = clause.fields ? clause.fields.map((
|
|
3555
|
+
const haystack = clause.fields ? clause.fields.map((field2) => collectMockText(getFieldValue2(document2.data, field2))).join(" ") : collectMockText(
|
|
3019
3556
|
Object.fromEntries(
|
|
3020
3557
|
Object.entries(document2.data).filter(([key]) => !key.startsWith("_"))
|
|
3021
3558
|
)
|
|
@@ -3245,7 +3782,7 @@ function createLiveMockViewNavigator(initial, rebuild, subscribe) {
|
|
|
3245
3782
|
});
|
|
3246
3783
|
}
|
|
3247
3784
|
function projectMockFields(data, fields) {
|
|
3248
|
-
return fields ? Object.fromEntries(fields.map((
|
|
3785
|
+
return fields ? Object.fromEntries(fields.map((field2) => [field2, getFieldValue2(data, field2)])) : structuredClone(data);
|
|
3249
3786
|
}
|
|
3250
3787
|
function mockLastModified(document2) {
|
|
3251
3788
|
return document2.updatedAt ? Date.parse(document2.updatedAt) || 0 : 0;
|
|
@@ -3773,9 +4310,10 @@ async function mockSha256Hex(value) {
|
|
|
3773
4310
|
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
3774
4311
|
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
3775
4312
|
}
|
|
3776
|
-
function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
4313
|
+
function createDatabaseHandle(definition, peers, directoryUsers = () => [], localActor = MOCK_AUTOMERGE_LOCAL_ACTOR) {
|
|
3777
4314
|
let createCounter = 0;
|
|
3778
4315
|
let changeCounter = 0;
|
|
4316
|
+
const replicaTag = localActor === MOCK_AUTOMERGE_LOCAL_ACTOR ? "" : `-${localActor.slice(0, 6)}`;
|
|
3779
4317
|
const defaultViewFactory = async () => createDefaultViewNavigator();
|
|
3780
4318
|
const automergeMode = definition.automerge === true;
|
|
3781
4319
|
let changeSequence = 0;
|
|
@@ -3852,7 +4390,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3852
4390
|
return writeAutomergeDocument(
|
|
3853
4391
|
docId,
|
|
3854
4392
|
existing,
|
|
3855
|
-
Automerge.change(loadMockAutomergeDocument(existing,
|
|
4393
|
+
Automerge.change(loadMockAutomergeDocument(existing, localActor), (target) => {
|
|
3856
4394
|
target._encryptFor = hydrateMockAutomergeValues(nextEncryptFor);
|
|
3857
4395
|
})
|
|
3858
4396
|
);
|
|
@@ -3967,9 +4505,9 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3967
4505
|
};
|
|
3968
4506
|
}
|
|
3969
4507
|
const projectedData = query?.fields ? Object.fromEntries(
|
|
3970
|
-
query.fields.map((
|
|
3971
|
-
|
|
3972
|
-
getFieldValue2(document2.data,
|
|
4508
|
+
query.fields.map((field2) => [
|
|
4509
|
+
field2,
|
|
4510
|
+
getFieldValue2(document2.data, field2)
|
|
3973
4511
|
])
|
|
3974
4512
|
) : document2.data;
|
|
3975
4513
|
return {
|
|
@@ -4030,7 +4568,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
4030
4568
|
return {
|
|
4031
4569
|
path: [...path],
|
|
4032
4570
|
heads: document2.heads ? [...document2.heads] : void 0,
|
|
4033
|
-
spans: structuredClone(readRichTextSpansAtPath(document2.data, path))
|
|
4571
|
+
spans: automergeMode ? readMockAutomergeRichText(document2, path) : structuredClone(readRichTextSpansAtPath(document2.data, path))
|
|
4034
4572
|
};
|
|
4035
4573
|
},
|
|
4036
4574
|
// Mock cursors store the index they were created at and do not follow
|
|
@@ -4162,10 +4700,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
4162
4700
|
id = callerId;
|
|
4163
4701
|
} else if (typeof input.idPrefix === "string" && input.idPrefix.length > 0) {
|
|
4164
4702
|
createCounter += 1;
|
|
4165
|
-
id = `${input.idPrefix}_${String(createCounter).padStart(22, "0")}`;
|
|
4703
|
+
id = `${input.idPrefix}_${String(createCounter).padStart(22, "0")}${replicaTag}`;
|
|
4166
4704
|
} else {
|
|
4167
4705
|
createCounter += 1;
|
|
4168
|
-
id = `doc-${createCounter}`;
|
|
4706
|
+
id = `doc-${createCounter}${replicaTag}`;
|
|
4169
4707
|
}
|
|
4170
4708
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4171
4709
|
if (Array.isArray(input.recipients) && input.recipientOptions?.includeSelf === false && input.recipients.every((name) => name.trim().length === 0)) {
|
|
@@ -4187,7 +4725,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
4187
4725
|
stored2,
|
|
4188
4726
|
createMockAutomergeDocument(
|
|
4189
4727
|
{ ...input.set ?? {}, ...encryptFor },
|
|
4190
|
-
|
|
4728
|
+
localActor
|
|
4191
4729
|
)
|
|
4192
4730
|
);
|
|
4193
4731
|
storedDocuments.set(id, stored2);
|
|
@@ -4237,7 +4775,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
4237
4775
|
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4238
4776
|
const existing = storedDocuments.get(docId);
|
|
4239
4777
|
if (automergeMode) {
|
|
4240
|
-
const current = existing?.automergeBinary ? loadMockAutomergeDocument(existing,
|
|
4778
|
+
const current = existing?.automergeBinary ? loadMockAutomergeDocument(existing, localActor) : createMockAutomergeDocument(existing?.data ?? {}, localActor);
|
|
4241
4779
|
return writeAutomergeDocument(docId, existing, applyMockAutomergeUpdate(current, patch));
|
|
4242
4780
|
}
|
|
4243
4781
|
const updated = {
|
|
@@ -4559,7 +5097,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
4559
5097
|
if (!automergeMode || !existing.automergeBinary) {
|
|
4560
5098
|
return await defaultDocuments.update(docId, input);
|
|
4561
5099
|
}
|
|
4562
|
-
const current = loadMockAutomergeDocument(existing,
|
|
5100
|
+
const current = loadMockAutomergeDocument(existing, localActor);
|
|
4563
5101
|
const remote = applyMockAutomergeUpdate(
|
|
4564
5102
|
forkMockAutomergeDocument(
|
|
4565
5103
|
current,
|
|
@@ -4692,7 +5230,12 @@ function createMockSessionState(options = {}) {
|
|
|
4692
5230
|
capabilities: [...definition.info.capabilities]
|
|
4693
5231
|
}));
|
|
4694
5232
|
for (const definition of definitions) {
|
|
4695
|
-
const created = createDatabaseHandle(
|
|
5233
|
+
const created = createDatabaseHandle(
|
|
5234
|
+
definition,
|
|
5235
|
+
databasePeers,
|
|
5236
|
+
() => directoryUsers,
|
|
5237
|
+
options.automergeActor ?? MOCK_AUTOMERGE_LOCAL_ACTOR
|
|
5238
|
+
);
|
|
4696
5239
|
const databaseId = definition.info.id;
|
|
4697
5240
|
databaseHandles.set(
|
|
4698
5241
|
databaseId,
|
|
@@ -4706,35 +5249,48 @@ function createMockSessionState(options = {}) {
|
|
|
4706
5249
|
databaseDocumentStores.set(definition.info.id, created.documents);
|
|
4707
5250
|
databaseLiveQueryRefreshers.set(definition.info.id, created.notifyLiveQueries);
|
|
4708
5251
|
databaseRemoteUpdaters.set(definition.info.id, created.applyRemoteUpdate);
|
|
5252
|
+
const createView = async (input) => {
|
|
5253
|
+
const build = () => createEvaluatingViewNavigator(
|
|
5254
|
+
input,
|
|
5255
|
+
input.databaseIds.flatMap((databaseId2) => {
|
|
5256
|
+
const list = databaseViewDocumentLists.get(databaseId2);
|
|
5257
|
+
return list ? list() : [];
|
|
5258
|
+
})
|
|
5259
|
+
);
|
|
5260
|
+
const subscribe = (onChange) => {
|
|
5261
|
+
const listener = { databaseIds: [...input.databaseIds], onChange };
|
|
5262
|
+
viewChangeListeners.add(listener);
|
|
5263
|
+
return () => viewChangeListeners.delete(listener);
|
|
5264
|
+
};
|
|
5265
|
+
const evaluating = await build();
|
|
5266
|
+
if (evaluating) {
|
|
5267
|
+
return createLiveMockViewNavigator(evaluating, build, subscribe);
|
|
5268
|
+
}
|
|
5269
|
+
console.warn(
|
|
5270
|
+
"[mindoodb-app-sdk/testing] mindoodb peer not available; using empty view navigator. Install mindoodb as a devDependency for evaluating VirtualViews."
|
|
5271
|
+
);
|
|
5272
|
+
return createLiveMockViewNavigator(
|
|
5273
|
+
await createDefaultViewNavigator(),
|
|
5274
|
+
async () => null,
|
|
5275
|
+
subscribe
|
|
5276
|
+
);
|
|
5277
|
+
};
|
|
4709
5278
|
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);
|
|
5279
|
+
create: createView,
|
|
5280
|
+
// A view configured in the launch context, evaluated over the mock
|
|
5281
|
+
// documents of its sources: a fresh navigator per open, with the
|
|
5282
|
+
// open options applied, as Haven builds one.
|
|
5283
|
+
async open(viewId, openOptions) {
|
|
5284
|
+
const configured = launchContext.views.find((view2) => view2.id === viewId);
|
|
5285
|
+
if (!configured) {
|
|
5286
|
+
return await createDefaultViewNavigator();
|
|
4726
5287
|
}
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
subscribe
|
|
4734
|
-
);
|
|
4735
|
-
},
|
|
4736
|
-
async open(_viewId) {
|
|
4737
|
-
return await createDefaultViewNavigator();
|
|
5288
|
+
return await createView({
|
|
5289
|
+
databaseIds: configured.sources.map((source) => source.databaseId),
|
|
5290
|
+
definition: configuredViewDefinition(configured),
|
|
5291
|
+
categorizationStyle: configured.categorizationStyle,
|
|
5292
|
+
...openOptions ? { options: openOptions } : {}
|
|
5293
|
+
});
|
|
4738
5294
|
},
|
|
4739
5295
|
...definition.methods?.views ?? {}
|
|
4740
5296
|
});
|
|
@@ -4830,16 +5386,21 @@ function createMockSessionState(options = {}) {
|
|
|
4830
5386
|
}
|
|
4831
5387
|
const view2 = await api.create(input);
|
|
4832
5388
|
const viewId = input.definition.id || crypto.randomUUID();
|
|
4833
|
-
sessionViews.set(viewId,
|
|
5389
|
+
sessionViews.set(viewId, input);
|
|
4834
5390
|
return view2;
|
|
4835
5391
|
},
|
|
5392
|
+
// Every open is a navigator of its own with its own options, as in
|
|
5393
|
+
// Haven: disposing one leaves the others and later opens working.
|
|
4836
5394
|
async openViewNavigator(viewId, options2) {
|
|
4837
|
-
const
|
|
4838
|
-
if (
|
|
4839
|
-
|
|
5395
|
+
const created = sessionViews.get(viewId);
|
|
5396
|
+
if (created) {
|
|
5397
|
+
const api2 = databaseViewApis.get(created.databaseIds[0]);
|
|
5398
|
+
if (api2) {
|
|
5399
|
+
return await api2.create({ ...created, ...options2 ? { options: options2 } : {} });
|
|
5400
|
+
}
|
|
4840
5401
|
}
|
|
4841
5402
|
const sourceDatabaseId = launchContext.views.find(
|
|
4842
|
-
(
|
|
5403
|
+
(view2) => view2.id === viewId
|
|
4843
5404
|
)?.sources[0]?.databaseId;
|
|
4844
5405
|
if (!sourceDatabaseId) {
|
|
4845
5406
|
throw new Error(`Unknown test view: ${viewId}`);
|
|
@@ -4850,9 +5411,7 @@ function createMockSessionState(options = {}) {
|
|
|
4850
5411
|
`Unknown test database for view ${viewId}: ${sourceDatabaseId}`
|
|
4851
5412
|
);
|
|
4852
5413
|
}
|
|
4853
|
-
|
|
4854
|
-
sessionViews.set(viewId, view2);
|
|
4855
|
-
return view2;
|
|
5414
|
+
return await api.open(viewId, options2);
|
|
4856
5415
|
},
|
|
4857
5416
|
menus,
|
|
4858
5417
|
drag,
|
|
@@ -4981,6 +5540,8 @@ function createMockSessionState(options = {}) {
|
|
|
4981
5540
|
directoryUsers = [...users];
|
|
4982
5541
|
},
|
|
4983
5542
|
getDirectoryUsers: () => [...directoryUsers],
|
|
5543
|
+
replicaStores: () => databaseDocumentStores,
|
|
5544
|
+
notifyReplicaChange: (databaseId) => databaseLiveQueryRefreshers.get(databaseId)?.(),
|
|
4984
5545
|
embedHost,
|
|
4985
5546
|
getDatabase(databaseId) {
|
|
4986
5547
|
const database = databaseHandles.get(databaseId);
|
|
@@ -5098,9 +5659,92 @@ function buildMockIncomingContent(input, deliveryId) {
|
|
|
5098
5659
|
})
|
|
5099
5660
|
};
|
|
5100
5661
|
}
|
|
5662
|
+
var replicaStates = /* @__PURE__ */ new WeakMap();
|
|
5663
|
+
function sameMockHeads(a, b) {
|
|
5664
|
+
const x = a?.heads ?? [];
|
|
5665
|
+
const y = b?.heads ?? [];
|
|
5666
|
+
return x.length === y.length && x.every((head) => y.includes(head));
|
|
5667
|
+
}
|
|
5668
|
+
function mockReplicaDifferences(a, b) {
|
|
5669
|
+
const left = replicaStates.get(a)?.replicaStores();
|
|
5670
|
+
const right = replicaStates.get(b)?.replicaStores();
|
|
5671
|
+
if (!left || !right) throw new Error("syncMockReplicas: not a mock controller");
|
|
5672
|
+
let count = 0;
|
|
5673
|
+
for (const [databaseId, docsA] of left) {
|
|
5674
|
+
const docsB = right.get(databaseId);
|
|
5675
|
+
if (!docsB) continue;
|
|
5676
|
+
for (const id of /* @__PURE__ */ new Set([...docsA.keys(), ...docsB.keys()])) {
|
|
5677
|
+
const x = docsA.get(id);
|
|
5678
|
+
const y = docsB.get(id);
|
|
5679
|
+
if (!x || !y || x.isDeleted !== y.isDeleted || !sameMockHeads(x, y)) count += 1;
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
return count;
|
|
5683
|
+
}
|
|
5684
|
+
async function syncMockReplicas(a, b) {
|
|
5685
|
+
const stateA = replicaStates.get(a);
|
|
5686
|
+
const stateB = replicaStates.get(b);
|
|
5687
|
+
if (!stateA || !stateB) throw new Error("syncMockReplicas: not a mock controller");
|
|
5688
|
+
let changed = 0;
|
|
5689
|
+
for (const [databaseId, docsA] of stateA.replicaStores()) {
|
|
5690
|
+
const docsB = stateB.replicaStores().get(databaseId);
|
|
5691
|
+
if (!docsB) continue;
|
|
5692
|
+
const touchedA = /* @__PURE__ */ new Set();
|
|
5693
|
+
const touchedB = /* @__PURE__ */ new Set();
|
|
5694
|
+
for (const id of /* @__PURE__ */ new Set([...docsA.keys(), ...docsB.keys()])) {
|
|
5695
|
+
const x = docsA.get(id);
|
|
5696
|
+
const y = docsB.get(id);
|
|
5697
|
+
if (x && y && x.isDeleted === y.isDeleted && sameMockHeads(x, y)) continue;
|
|
5698
|
+
changed += 1;
|
|
5699
|
+
let merged;
|
|
5700
|
+
if (x?.automergeBinary && y?.automergeBinary) {
|
|
5701
|
+
const doc = Automerge.merge(Automerge.load(x.automergeBinary), Automerge.load(y.automergeBinary));
|
|
5702
|
+
merged = { ...x, ...y, attachments: structuredClone(x.attachments ?? y.attachments ?? []) };
|
|
5703
|
+
persistMockAutomergeDocument(merged, doc);
|
|
5704
|
+
} else if (x && y) {
|
|
5705
|
+
merged = structuredClone((x.updatedAt ?? "") >= (y.updatedAt ?? "") ? x : y);
|
|
5706
|
+
} else {
|
|
5707
|
+
merged = structuredClone(x ?? y);
|
|
5708
|
+
}
|
|
5709
|
+
merged.isDeleted = Boolean(x?.isDeleted || y?.isDeleted);
|
|
5710
|
+
merged.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5711
|
+
if (!x || !sameMockHeads(x, merged) || x.isDeleted !== merged.isDeleted) {
|
|
5712
|
+
docsA.set(id, structuredClone(merged));
|
|
5713
|
+
touchedA.add(id);
|
|
5714
|
+
}
|
|
5715
|
+
if (!y || !sameMockHeads(y, merged) || y.isDeleted !== merged.isDeleted) {
|
|
5716
|
+
docsB.set(id, structuredClone(merged));
|
|
5717
|
+
touchedB.add(id);
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
const dbA = stateA.getDatabase(databaseId);
|
|
5721
|
+
const dbB = stateB.getDatabase(databaseId);
|
|
5722
|
+
for (const id of /* @__PURE__ */ new Set([...docsA.keys()])) {
|
|
5723
|
+
if (docsA.get(id)?.isDeleted) continue;
|
|
5724
|
+
for (const [from2, to, touched] of [
|
|
5725
|
+
[dbA, dbB, touchedB],
|
|
5726
|
+
[dbB, dbA, touchedA]
|
|
5727
|
+
]) {
|
|
5728
|
+
const have = new Set((await to.attachments.list(id).catch(() => [])).map((f) => f.fileName));
|
|
5729
|
+
for (const file of await from2.attachments.list(id).catch(() => [])) {
|
|
5730
|
+
if (have.has(file.fileName)) continue;
|
|
5731
|
+
const reader = await from2.attachments.openReadStream(id, file.fileName);
|
|
5732
|
+
const writer = await to.attachments.openWriteStream(id, file.fileName, file.mimeType);
|
|
5733
|
+
for (let chunk = await reader.read(); chunk !== null; chunk = await reader.read()) await writer.write(chunk);
|
|
5734
|
+
await reader.close();
|
|
5735
|
+
await writer.close();
|
|
5736
|
+
touched.add(id);
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
if (touchedA.size) stateA.notifyReplicaChange(databaseId);
|
|
5741
|
+
if (touchedB.size) stateB.notifyReplicaChange(databaseId);
|
|
5742
|
+
}
|
|
5743
|
+
return changed;
|
|
5744
|
+
}
|
|
5101
5745
|
function createMockMindooDBAppSession(options = {}) {
|
|
5102
5746
|
const state = createMockSessionState(options);
|
|
5103
|
-
|
|
5747
|
+
const controller = {
|
|
5104
5748
|
bridge: state.bridge,
|
|
5105
5749
|
session: state.session,
|
|
5106
5750
|
getLaunchContext: state.getLaunchContext,
|
|
@@ -5129,10 +5773,13 @@ function createMockMindooDBAppSession(options = {}) {
|
|
|
5129
5773
|
setComponents: (components) => state.embedHost.setComponents(components),
|
|
5130
5774
|
getEmbeddingFinish: () => state.embedHost.getEmbeddingFinish(),
|
|
5131
5775
|
setEmbedDirty: (embedId, dirty) => state.embedHost.setEmbedDirty(embedId, dirty),
|
|
5776
|
+
setEmbedAgentTools: (embedId, names, toolOptions) => state.embedHost.setEmbedAgentTools(embedId, names, toolOptions),
|
|
5132
5777
|
failNextEmbedSave: (message) => state.embedHost.failNextSave(message),
|
|
5133
5778
|
getEmbeddingDirty: () => state.embedHost.getEmbeddingDirty(),
|
|
5134
5779
|
requestEmbeddingSave: () => state.embedHost.requestEmbeddingSave()
|
|
5135
5780
|
};
|
|
5781
|
+
replicaStates.set(controller, state);
|
|
5782
|
+
return controller;
|
|
5136
5783
|
}
|
|
5137
5784
|
function createMockMindooDBAppBridge(options = {}) {
|
|
5138
5785
|
return createMockMindooDBAppSession(options);
|
|
@@ -5151,6 +5798,7 @@ function createFakeBridgeHost(options = {}) {
|
|
|
5151
5798
|
...options.agentConsent !== void 0 ? { agentConsent: options.agentConsent } : {},
|
|
5152
5799
|
...options.agentInvokeTimeoutMs !== void 0 ? { agentInvokeTimeoutMs: options.agentInvokeTimeoutMs } : {},
|
|
5153
5800
|
appId: () => state.getLaunchContext().appId,
|
|
5801
|
+
embedded: () => Boolean(state.getLaunchContext().embed),
|
|
5154
5802
|
post(message) {
|
|
5155
5803
|
const port = [...connectedPorts].pop();
|
|
5156
5804
|
if (!port) return false;
|
|
@@ -5169,6 +5817,11 @@ function createFakeBridgeHost(options = {}) {
|
|
|
5169
5817
|
(port) => port.postMessage({ protocol: PROTOCOL, kind: "embed-event", event: { type: "dirty", ...event } })
|
|
5170
5818
|
);
|
|
5171
5819
|
});
|
|
5820
|
+
state.embedHost.onAgentTools((event) => {
|
|
5821
|
+
connectedPorts.forEach(
|
|
5822
|
+
(port) => port.postMessage({ protocol: PROTOCOL, kind: "embed-event", event: { type: "agentTools", ...event } })
|
|
5823
|
+
);
|
|
5824
|
+
});
|
|
5172
5825
|
const embeddingRequests = /* @__PURE__ */ new Map();
|
|
5173
5826
|
let embeddingRequestCounter = 0;
|
|
5174
5827
|
const beforeCloseWaiters = /* @__PURE__ */ new Map();
|
|
@@ -5421,9 +6074,18 @@ function createFakeBridgeHost(options = {}) {
|
|
|
5421
6074
|
case "embeds.save":
|
|
5422
6075
|
await state.embedHost.saveEmbed(String(params.embedId));
|
|
5423
6076
|
return { ok: true };
|
|
6077
|
+
case "embeds.setActive":
|
|
6078
|
+
state.embedHost.setActive(String(params.embedId));
|
|
6079
|
+
return { ok: true };
|
|
5424
6080
|
case "embedding.setDirty":
|
|
5425
6081
|
state.embedHost.setEmbeddingDirty(params.dirty === true);
|
|
5426
6082
|
return { ok: true };
|
|
6083
|
+
case "embedding.lookup":
|
|
6084
|
+
return await state.embedHost.embedding.lookup(String(params.field), {
|
|
6085
|
+
...typeof params.query === "string" ? { query: params.query } : {},
|
|
6086
|
+
...Array.isArray(params.docIds) ? { docIds: params.docIds.map(String) } : {},
|
|
6087
|
+
...typeof params.limit === "number" ? { limit: params.limit } : {}
|
|
6088
|
+
});
|
|
5427
6089
|
case "embedding.respond": {
|
|
5428
6090
|
const pending = embeddingRequests.get(String(params.requestId));
|
|
5429
6091
|
embeddingRequests.delete(String(params.requestId));
|
|
@@ -6060,6 +6722,7 @@ function createFakeBridgeHost(options = {}) {
|
|
|
6060
6722
|
closeEmbed: (embedId, reason, result) => state.embedHost.closeEmbed(embedId, reason, result),
|
|
6061
6723
|
getEmbeddingFinish: () => state.embedHost.getEmbeddingFinish(),
|
|
6062
6724
|
setEmbedDirty: (embedId, dirty) => state.embedHost.setEmbedDirty(embedId, dirty),
|
|
6725
|
+
setEmbedAgentTools: (embedId, names, toolOptions) => state.embedHost.setEmbedAgentTools(embedId, names, toolOptions),
|
|
6063
6726
|
failNextEmbedSave: (message) => state.embedHost.failNextSave(message),
|
|
6064
6727
|
getEmbeddingDirty: () => state.embedHost.getEmbeddingDirty(),
|
|
6065
6728
|
requestEmbeddingSave() {
|
|
@@ -6185,10 +6848,12 @@ function createFakeBridgeHost(options = {}) {
|
|
|
6185
6848
|
customRequestHandlers.delete(method);
|
|
6186
6849
|
}
|
|
6187
6850
|
};
|
|
6851
|
+
replicaStates.set(controller, state);
|
|
6188
6852
|
return controller;
|
|
6189
6853
|
}
|
|
6190
6854
|
export {
|
|
6191
6855
|
ALL_CAPABILITIES,
|
|
6856
|
+
DEFAULT_TEST_HOST_USERS,
|
|
6192
6857
|
MockForbiddenError,
|
|
6193
6858
|
TEST_HOST_LOCALES,
|
|
6194
6859
|
capabilitiesForDefinitionPermissions,
|
|
@@ -6203,8 +6868,12 @@ export {
|
|
|
6203
6868
|
loadMindoodbForTesting,
|
|
6204
6869
|
mockAgentToolPrefix,
|
|
6205
6870
|
mockDatabasesFromDefinition,
|
|
6871
|
+
mockReplicaDifferences,
|
|
6872
|
+
modeSwitch,
|
|
6206
6873
|
mountHavenTestHost,
|
|
6874
|
+
mountTwoUserTestHost,
|
|
6207
6875
|
previewJson,
|
|
6208
6876
|
readTestHostUrlSettings,
|
|
6877
|
+
syncMockReplicas,
|
|
6209
6878
|
validateMockAgentTools
|
|
6210
6879
|
};
|