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