mindoodb 0.0.6 → 0.0.8

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.
@@ -8,6 +8,7 @@ const automerge_adapter_1 = require("./automerge-adapter");
8
8
  const uuid_1 = require("uuid");
9
9
  const bloom_1 = require("./appendonlystores/bloom");
10
10
  const MaterializationPlanner_1 = require("./appendonlystores/MaterializationPlanner");
11
+ const DocumentDagAnalysis_1 = require("./DocumentDagAnalysis");
11
12
  const AttachmentReadPlanner_1 = require("./appendonlystores/AttachmentReadPlanner");
12
13
  const idGeneration_1 = require("./utils/idGeneration");
13
14
  const errors_1 = require("./errors");
@@ -1370,6 +1371,245 @@ class BaseMindooDB {
1370
1371
  });
1371
1372
  return result;
1372
1373
  }
1374
+ async analyzeDocumentDagAtTimestamp(docId, timestamp) {
1375
+ const startedAt = Date.now();
1376
+ const resolvedTimestamp = this.resolveDagTimestamp(timestamp);
1377
+ const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
1378
+ const relevantEntries = allEntryMetadata
1379
+ .filter((entry) => entry.createdAt <= resolvedTimestamp && (0, DocumentDagAnalysis_1.isDagEntry)(entry));
1380
+ const result = (0, DocumentDagAnalysis_1.computeDocumentDagAnalysis)(docId, relevantEntries, resolvedTimestamp);
1381
+ const actorIdByEntryId = await this.decodeAutomergeActorIds(relevantEntries);
1382
+ result.entries = result.entries.map((entry) => ({
1383
+ ...entry,
1384
+ automergeActorId: actorIdByEntryId.get(entry.entryId) ?? null,
1385
+ }));
1386
+ this.performanceCallback?.onHistoryOperation?.({
1387
+ operation: "analyzeDocumentDagAtTimestamp",
1388
+ docId,
1389
+ time: Date.now() - startedAt,
1390
+ scannedEntries: relevantEntries.length,
1391
+ returnedEntries: result.entries.length,
1392
+ bounded: true,
1393
+ });
1394
+ return result;
1395
+ }
1396
+ /**
1397
+ * Decodes Automerge actor ids for replay entries so analysis consumers can color
1398
+ * or group nodes by the logical Automerge actor instead of transport metadata.
1399
+ */
1400
+ async decodeAutomergeActorIds(relevantEntries) {
1401
+ const result = new Map();
1402
+ const replayEntries = relevantEntries.filter((entry) => entry.entryType !== "doc_snapshot");
1403
+ if (replayEntries.length === 0) {
1404
+ return result;
1405
+ }
1406
+ const loadedEntries = await this.store.getEntries(replayEntries.map((entry) => entry.id));
1407
+ const entryById = new Map(loadedEntries.map((entry) => [entry.id, entry]));
1408
+ for (const metadata of replayEntries) {
1409
+ const entry = entryById.get(metadata.id);
1410
+ if (!entry) {
1411
+ result.set(metadata.id, null);
1412
+ continue;
1413
+ }
1414
+ if (this._isAdminOnlyDb && entry.createdByPublicKey !== this.getAdminPublicKey()) {
1415
+ result.set(metadata.id, null);
1416
+ continue;
1417
+ }
1418
+ const isValid = await this.tenant.verifySignature(entry.encryptedData, entry.signature, entry.createdByPublicKey);
1419
+ if (!isValid) {
1420
+ result.set(metadata.id, null);
1421
+ continue;
1422
+ }
1423
+ const decryptedPayload = await this.tenant.decryptPayload(entry.encryptedData, entry.decryptionKeyId);
1424
+ const decodedAutomergeChange = automerge_adapter_1.Automerge.decodeChange(decryptedPayload);
1425
+ result.set(metadata.id, typeof decodedAutomergeChange.actor === "string" ? decodedAutomergeChange.actor : null);
1426
+ }
1427
+ return result;
1428
+ }
1429
+ async materializeDocumentBranchAtEntry(docId, headEntryId) {
1430
+ const startedAt = Date.now();
1431
+ const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
1432
+ const plan = (0, DocumentDagAnalysis_1.computeBranchMaterializationPlan)(docId, allEntryMetadata, headEntryId);
1433
+ if (!plan) {
1434
+ this.performanceCallback?.onHistoryOperation?.({
1435
+ operation: "materializeDocumentBranchAtEntry",
1436
+ docId,
1437
+ time: Date.now() - startedAt,
1438
+ scannedEntries: allEntryMetadata.length,
1439
+ returnedEntries: 0,
1440
+ bounded: true,
1441
+ });
1442
+ return null;
1443
+ }
1444
+ const metadataById = new Map(allEntryMetadata.map((entry) => [entry.id, entry]));
1445
+ const branchEntries = plan.branchEntryIds
1446
+ .map((entryId) => metadataById.get(entryId))
1447
+ .filter((entry) => entry !== undefined);
1448
+ const internalDoc = await this.materializeDocumentFromPlan(docId, allEntryMetadata, branchEntries, plan.snapshotEntryId, plan.entryIdsToApply, plan.headCreatedAt);
1449
+ this.performanceCallback?.onHistoryOperation?.({
1450
+ operation: "materializeDocumentBranchAtEntry",
1451
+ docId,
1452
+ time: Date.now() - startedAt,
1453
+ scannedEntries: allEntryMetadata.length,
1454
+ returnedEntries: internalDoc ? 1 : 0,
1455
+ bounded: true,
1456
+ });
1457
+ if (!internalDoc) {
1458
+ return null;
1459
+ }
1460
+ return {
1461
+ docId,
1462
+ headEntryId: plan.headEntryId,
1463
+ headCreatedAt: plan.headCreatedAt,
1464
+ headCreatedByPublicKey: plan.headCreatedByPublicKey,
1465
+ snapshotEntryId: plan.snapshotEntryId,
1466
+ entryIdsApplied: [...plan.entryIdsToApply],
1467
+ branchEntryIds: [...plan.branchEntryIds],
1468
+ doc: this.wrapDocument(internalDoc),
1469
+ };
1470
+ }
1471
+ async materializeDocumentBranchAtTimestamp(docId, timestamp, headEntryId) {
1472
+ const startedAt = Date.now();
1473
+ const resolvedTimestamp = this.resolveDagTimestamp(timestamp);
1474
+ const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
1475
+ const relevantEntries = allEntryMetadata
1476
+ .filter((entry) => entry.createdAt <= resolvedTimestamp && (0, DocumentDagAnalysis_1.isDagEntry)(entry));
1477
+ const plan = (0, DocumentDagAnalysis_1.computeBranchMaterializationPlan)(docId, relevantEntries, headEntryId);
1478
+ if (!plan) {
1479
+ this.performanceCallback?.onHistoryOperation?.({
1480
+ operation: "materializeDocumentBranchAtTimestamp",
1481
+ docId,
1482
+ time: Date.now() - startedAt,
1483
+ scannedEntries: relevantEntries.length,
1484
+ returnedEntries: 0,
1485
+ bounded: true,
1486
+ });
1487
+ return null;
1488
+ }
1489
+ const metadataById = new Map(relevantEntries.map((entry) => [entry.id, entry]));
1490
+ const branchEntries = plan.branchEntryIds
1491
+ .map((entryId) => metadataById.get(entryId))
1492
+ .filter((entry) => entry !== undefined);
1493
+ const internalDoc = await this.materializeDocumentFromPlan(docId, relevantEntries, branchEntries, plan.snapshotEntryId, plan.entryIdsToApply, resolvedTimestamp);
1494
+ this.performanceCallback?.onHistoryOperation?.({
1495
+ operation: "materializeDocumentBranchAtTimestamp",
1496
+ docId,
1497
+ time: Date.now() - startedAt,
1498
+ scannedEntries: relevantEntries.length,
1499
+ returnedEntries: internalDoc ? 1 : 0,
1500
+ bounded: true,
1501
+ });
1502
+ if (!internalDoc) {
1503
+ return null;
1504
+ }
1505
+ return {
1506
+ docId,
1507
+ headEntryId: plan.headEntryId,
1508
+ headCreatedAt: plan.headCreatedAt,
1509
+ headCreatedByPublicKey: plan.headCreatedByPublicKey,
1510
+ snapshotEntryId: plan.snapshotEntryId,
1511
+ entryIdsApplied: [...plan.entryIdsToApply],
1512
+ branchEntryIds: [...plan.branchEntryIds],
1513
+ doc: this.wrapDocument(internalDoc),
1514
+ };
1515
+ }
1516
+ async describeDocumentDagEntry(docId, entryId) {
1517
+ const startedAt = Date.now();
1518
+ const metadata = await this.store.getEntryMetadata(entryId);
1519
+ if (!metadata || metadata.docId !== docId || !(0, DocumentDagAnalysis_1.isDagEntry)(metadata)) {
1520
+ this.performanceCallback?.onHistoryOperation?.({
1521
+ operation: "describeDocumentDagEntry",
1522
+ docId,
1523
+ time: Date.now() - startedAt,
1524
+ scannedEntries: metadata ? 1 : 0,
1525
+ returnedEntries: 0,
1526
+ bounded: true,
1527
+ });
1528
+ return null;
1529
+ }
1530
+ let decodedChange = null;
1531
+ if (metadata.entryType !== "doc_snapshot") {
1532
+ const allEntryMetadata = await this.scanAllMetadata(this.store, { docId });
1533
+ const entries = await this.store.getEntries([entryId]);
1534
+ const entry = entries[0];
1535
+ if (entry) {
1536
+ let isValid = false;
1537
+ if (this._isAdminOnlyDb && entry.createdByPublicKey !== this.getAdminPublicKey()) {
1538
+ this.logger.warn(`Admin-only DB: skipping DAG details for ${entry.id} not signed by admin key`);
1539
+ }
1540
+ else {
1541
+ isValid = await this.tenant.verifySignature(entry.encryptedData, entry.signature, entry.createdByPublicKey);
1542
+ }
1543
+ if (isValid) {
1544
+ const decryptedPayload = await this.tenant.decryptPayload(entry.encryptedData, entry.decryptionKeyId);
1545
+ const decodedAutomergeChange = automerge_adapter_1.Automerge.decodeChange(decryptedPayload);
1546
+ decodedChange = this.summarizeDecodedChange(decodedAutomergeChange);
1547
+ decodedChange.touchedPaths = await this.deriveReadableTouchedPaths(docId, metadata, allEntryMetadata, decodedChange.touchedKeys);
1548
+ }
1549
+ }
1550
+ }
1551
+ const parsed = (0, idGeneration_1.parseDocEntryId)(metadata.id);
1552
+ const result = {
1553
+ docId,
1554
+ entryId: metadata.id,
1555
+ entryType: metadata.entryType,
1556
+ createdAt: metadata.createdAt,
1557
+ createdByPublicKey: metadata.createdByPublicKey,
1558
+ dependencyIds: [...metadata.dependencyIds],
1559
+ snapshotHeadEntryIds: [...(metadata.snapshotHeadEntryIds ?? [])],
1560
+ snapshotHeadHashes: [...(metadata.snapshotHeadHashes ?? [])],
1561
+ automergeHash: parsed?.automergeHash ?? null,
1562
+ decodedChange,
1563
+ };
1564
+ this.performanceCallback?.onHistoryOperation?.({
1565
+ operation: "describeDocumentDagEntry",
1566
+ docId,
1567
+ time: Date.now() - startedAt,
1568
+ scannedEntries: 1,
1569
+ returnedEntries: 1,
1570
+ bounded: true,
1571
+ });
1572
+ return result;
1573
+ }
1574
+ /**
1575
+ * Derives human-readable changed field paths for one DAG entry.
1576
+ *
1577
+ * This compares the branch-local document state at the selected entry with each
1578
+ * reachable parent state, then normalizes array indices so the UI can show stable
1579
+ * paths such as `_attachments[].fileName` instead of raw Automerge op keys only.
1580
+ */
1581
+ async deriveReadableTouchedPaths(docId, entryMetadata, allEntryMetadata, touchedKeys) {
1582
+ if (entryMetadata.entryType === "doc_snapshot") {
1583
+ return [];
1584
+ }
1585
+ const currentDoc = await this.materializeBranchInternalDoc(docId, allEntryMetadata, entryMetadata.id);
1586
+ const currentData = this.getReadableDiffValueForDoc(currentDoc);
1587
+ const parentEntryIds = entryMetadata.dependencyIds.filter((dependencyId) => allEntryMetadata.some((entry) => entry.id === dependencyId));
1588
+ const diffPaths = new Set();
1589
+ if (parentEntryIds.length === 0) {
1590
+ this.collectReadableDiffPaths(undefined, currentData, "", diffPaths);
1591
+ }
1592
+ else {
1593
+ for (const parentEntryId of parentEntryIds) {
1594
+ const parentDoc = await this.materializeBranchInternalDoc(docId, allEntryMetadata, parentEntryId);
1595
+ const parentData = this.getReadableDiffValueForDoc(parentDoc);
1596
+ this.collectReadableDiffPaths(parentData, currentData, "", diffPaths);
1597
+ }
1598
+ }
1599
+ const normalizedPaths = Array.from(diffPaths)
1600
+ .map((path) => path.replace(/\[\d+\]/g, "[]"))
1601
+ .filter((path, index, allPaths) => path.length > 0 && allPaths.indexOf(path) === index)
1602
+ .sort();
1603
+ if (normalizedPaths.length === 0) {
1604
+ return [];
1605
+ }
1606
+ const touchedKeySet = new Set(touchedKeys);
1607
+ if (touchedKeySet.size === 0) {
1608
+ return normalizedPaths;
1609
+ }
1610
+ const filteredPaths = normalizedPaths.filter((path) => touchedKeySet.has(this.extractLeafPathSegment(path)));
1611
+ return filteredPaths.length > 0 ? filteredPaths : normalizedPaths;
1612
+ }
1373
1613
  async getAllDocumentIds() {
1374
1614
  // Return all non-deleted document IDs from index
1375
1615
  const docIds = [];
@@ -1988,6 +2228,261 @@ class BaseMindooDB {
1988
2228
  }
1989
2229
  return startIndex;
1990
2230
  }
2231
+ resolveDagTimestamp(timestamp) {
2232
+ return timestamp === "now" ? Date.now() : timestamp;
2233
+ }
2234
+ previewChangeValue(value) {
2235
+ if (value === undefined) {
2236
+ return null;
2237
+ }
2238
+ if (typeof value === "string") {
2239
+ return value.length > 80 ? `${value.slice(0, 77)}...` : value;
2240
+ }
2241
+ if (typeof value === "number" || typeof value === "boolean") {
2242
+ return String(value);
2243
+ }
2244
+ if (value === null) {
2245
+ return "null";
2246
+ }
2247
+ try {
2248
+ const serialized = JSON.stringify(value);
2249
+ if (!serialized) {
2250
+ return null;
2251
+ }
2252
+ return serialized.length > 80 ? `${serialized.slice(0, 77)}...` : serialized;
2253
+ }
2254
+ catch {
2255
+ return String(value);
2256
+ }
2257
+ }
2258
+ /**
2259
+ * Materializes a single branch-local document state for a specific DAG head.
2260
+ *
2261
+ * This is the internal building block used by DAG inspection helpers when they
2262
+ * need the document exactly as that branch would have looked at `headEntryId`.
2263
+ */
2264
+ async materializeBranchInternalDoc(docId, allEntryMetadata, headEntryId) {
2265
+ const plan = (0, DocumentDagAnalysis_1.computeBranchMaterializationPlan)(docId, allEntryMetadata, headEntryId);
2266
+ if (!plan) {
2267
+ return null;
2268
+ }
2269
+ const metadataById = new Map(allEntryMetadata.map((entry) => [entry.id, entry]));
2270
+ const branchEntries = plan.branchEntryIds
2271
+ .map((entryId) => metadataById.get(entryId))
2272
+ .filter((entry) => entry !== undefined);
2273
+ return this.materializeDocumentFromPlan(docId, allEntryMetadata, branchEntries, plan.snapshotEntryId, plan.entryIdsToApply, plan.headCreatedAt);
2274
+ }
2275
+ /**
2276
+ * Converts an internal doc into a plain JS value that is safe to diff for UI summaries.
2277
+ *
2278
+ * Deleted or missing docs are normalized to `undefined` so the diff code can treat
2279
+ * creation/deletion as ordinary before/after transitions.
2280
+ */
2281
+ getReadableDiffValueForDoc(internalDoc) {
2282
+ if (!internalDoc || internalDoc.isDeleted) {
2283
+ return undefined;
2284
+ }
2285
+ return this.wrapDocument(internalDoc).getData();
2286
+ }
2287
+ /**
2288
+ * Walks two plain JS values and records the changed field paths.
2289
+ *
2290
+ * Arrays are tracked by index first and later normalized to `[]` for display,
2291
+ * which keeps attachment-style changes readable in the DAG explorer.
2292
+ */
2293
+ collectReadableDiffPaths(beforeValue, afterValue, basePath, results) {
2294
+ if (Object.is(beforeValue, afterValue)) {
2295
+ return;
2296
+ }
2297
+ if (Array.isArray(beforeValue) || Array.isArray(afterValue)) {
2298
+ const beforeArray = Array.isArray(beforeValue) ? beforeValue : [];
2299
+ const afterArray = Array.isArray(afterValue) ? afterValue : [];
2300
+ const maxLength = Math.max(beforeArray.length, afterArray.length);
2301
+ if (maxLength === 0 && basePath) {
2302
+ results.add(basePath);
2303
+ return;
2304
+ }
2305
+ for (let index = 0; index < maxLength; index++) {
2306
+ this.collectReadableDiffPaths(beforeArray[index], afterArray[index], `${basePath}[${index}]`, results);
2307
+ }
2308
+ return;
2309
+ }
2310
+ if (this.isPlainDiffObject(beforeValue) || this.isPlainDiffObject(afterValue)) {
2311
+ const beforeObject = this.isPlainDiffObject(beforeValue) ? beforeValue : {};
2312
+ const afterObject = this.isPlainDiffObject(afterValue) ? afterValue : {};
2313
+ const childKeys = new Set([...Object.keys(beforeObject), ...Object.keys(afterObject)]);
2314
+ if (childKeys.size === 0 && basePath) {
2315
+ results.add(basePath);
2316
+ return;
2317
+ }
2318
+ for (const key of childKeys) {
2319
+ const nextPath = basePath ? `${basePath}.${key}` : key;
2320
+ this.collectReadableDiffPaths(beforeObject[key], afterObject[key], nextPath, results);
2321
+ }
2322
+ return;
2323
+ }
2324
+ if (basePath) {
2325
+ results.add(basePath);
2326
+ }
2327
+ }
2328
+ /**
2329
+ * Returns true for diffable object literals and false for arrays/binary payloads.
2330
+ */
2331
+ isPlainDiffObject(value) {
2332
+ return value !== null
2333
+ && typeof value === "object"
2334
+ && !Array.isArray(value)
2335
+ && !(value instanceof Uint8Array);
2336
+ }
2337
+ /**
2338
+ * Extracts the terminal field name from a normalized path for loose key/path matching.
2339
+ */
2340
+ extractLeafPathSegment(path) {
2341
+ const normalizedPath = path.replace(/\[\]/g, "");
2342
+ const segments = normalizedPath.split(".");
2343
+ return segments[segments.length - 1] ?? normalizedPath;
2344
+ }
2345
+ /**
2346
+ * Produces a compact, UI-oriented summary of a decoded Automerge change.
2347
+ *
2348
+ * The raw Automerge ops are reduced to counts, touched keys, and a short list of
2349
+ * previewable operations so Haven can show useful change context without dumping
2350
+ * the full decoded structure.
2351
+ */
2352
+ summarizeDecodedChange(decodedChange) {
2353
+ const operations = Array.isArray(decodedChange.ops) ? decodedChange.ops : [];
2354
+ const actionCounts = {};
2355
+ const touchedKeys = new Set();
2356
+ const summarizedOperations = operations.slice(0, 12).map((operation) => {
2357
+ const op = operation;
2358
+ const action = typeof op.action === "string" ? op.action : "unknown";
2359
+ actionCounts[action] = (actionCounts[action] ?? 0) + 1;
2360
+ const key = typeof op.key === "string"
2361
+ ? op.key
2362
+ : typeof op.elemId === "string"
2363
+ ? op.elemId
2364
+ : null;
2365
+ if (typeof op.key === "string") {
2366
+ touchedKeys.add(op.key);
2367
+ }
2368
+ return {
2369
+ action,
2370
+ key,
2371
+ obj: typeof op.obj === "string" ? op.obj : null,
2372
+ insert: typeof op.insert === "boolean" ? op.insert : undefined,
2373
+ valuePreview: this.previewChangeValue(op.value),
2374
+ };
2375
+ });
2376
+ for (const operation of operations.slice(12)) {
2377
+ const op = operation;
2378
+ const action = typeof op.action === "string" ? op.action : "unknown";
2379
+ actionCounts[action] = (actionCounts[action] ?? 0) + 1;
2380
+ if (typeof op.key === "string") {
2381
+ touchedKeys.add(op.key);
2382
+ }
2383
+ }
2384
+ return {
2385
+ actorId: typeof decodedChange.actor === "string" ? decodedChange.actor : null,
2386
+ hash: typeof decodedChange.hash === "string" ? decodedChange.hash : null,
2387
+ seq: typeof decodedChange.seq === "number" ? decodedChange.seq : null,
2388
+ message: typeof decodedChange.message === "string" ? decodedChange.message : null,
2389
+ dependencyHashes: Array.isArray(decodedChange.deps)
2390
+ ? decodedChange.deps.filter((dep) => typeof dep === "string")
2391
+ : [],
2392
+ opCount: operations.length,
2393
+ actionCounts,
2394
+ touchedKeys: Array.from(touchedKeys).sort(),
2395
+ touchedPaths: [],
2396
+ operations: summarizedOperations,
2397
+ };
2398
+ }
2399
+ /**
2400
+ * Rebuilds a document from a materialization plan produced by the DAG/history planners.
2401
+ *
2402
+ * The plan may start from a compatible snapshot and then replay incremental changes,
2403
+ * or fall back to a full replay if no usable snapshot is available.
2404
+ */
2405
+ async materializeDocumentFromPlan(docId, allEntryMetadata, replayEntriesForState, snapshotEntryId, entryIdsToApply, fallbackTimestamp) {
2406
+ if (replayEntriesForState.length === 0 && !snapshotEntryId) {
2407
+ return null;
2408
+ }
2409
+ const metadataById = new Map(allEntryMetadata.map((entry) => [entry.id, entry]));
2410
+ let startFromSnapshot = snapshotEntryId !== null;
2411
+ const snapshotMeta = snapshotEntryId ? (metadataById.get(snapshotEntryId) || null) : null;
2412
+ if (startFromSnapshot && !snapshotMeta) {
2413
+ this.logger.warn(`Materialization referenced snapshot ${snapshotEntryId} not found in metadata for ${docId}; falling back to replay without snapshot`);
2414
+ startFromSnapshot = false;
2415
+ }
2416
+ let doc = undefined;
2417
+ if (startFromSnapshot && snapshotMeta) {
2418
+ const snapshotEntries = await this.store.getEntries([snapshotMeta.id]);
2419
+ if (snapshotEntries.length > 0) {
2420
+ const snapshotData = snapshotEntries[0];
2421
+ let isValid = false;
2422
+ if (this._isAdminOnlyDb && snapshotData.createdByPublicKey !== this.getAdminPublicKey()) {
2423
+ this.logger.warn(`Admin-only DB: skipping snapshot ${snapshotData.id} not signed by admin key`);
2424
+ }
2425
+ else {
2426
+ isValid = await this.tenant.verifySignature(snapshotData.encryptedData, snapshotData.signature, snapshotData.createdByPublicKey);
2427
+ }
2428
+ if (!isValid) {
2429
+ this.logger.warn(`Invalid signature for snapshot ${snapshotData.id}, falling back to replay without snapshot`);
2430
+ startFromSnapshot = false;
2431
+ }
2432
+ else {
2433
+ const decryptedSnapshot = await this.tenant.decryptPayload(snapshotData.encryptedData, snapshotData.decryptionKeyId);
2434
+ doc = automerge_adapter_1.Automerge.load(decryptedSnapshot);
2435
+ const parsed = (0, idGeneration_1.parseDocEntryId)(snapshotData.id);
2436
+ if (parsed) {
2437
+ this.registerAutomergeHashMapping(docId, parsed.automergeHash, snapshotData.id);
2438
+ }
2439
+ }
2440
+ }
2441
+ }
2442
+ if (!doc) {
2443
+ doc = automerge_adapter_1.Automerge.init();
2444
+ }
2445
+ const entriesToApply = entryIdsToApply
2446
+ .map((id) => metadataById.get(id))
2447
+ .filter((entry) => entry !== undefined);
2448
+ const loadedEntries = entriesToApply.length > 0
2449
+ ? await this.store.getEntries(entriesToApply.map((entry) => entry.id))
2450
+ : [];
2451
+ const entryById = new Map(loadedEntries.map((entry) => [entry.id, entry]));
2452
+ for (const entryMeta of entriesToApply) {
2453
+ const entryData = entryById.get(entryMeta.id);
2454
+ if (!entryData) {
2455
+ this.logger.warn(`Entry ${entryMeta.id} not found in store, skipping`);
2456
+ continue;
2457
+ }
2458
+ if (this._isAdminOnlyDb && entryData.createdByPublicKey !== this.getAdminPublicKey()) {
2459
+ this.logger.warn(`Admin-only DB: skipping entry ${entryData.id} not signed by admin key`);
2460
+ continue;
2461
+ }
2462
+ const isValid = await this.tenant.verifySignature(entryData.encryptedData, entryData.signature, entryData.createdByPublicKey);
2463
+ if (!isValid) {
2464
+ this.logger.warn(`Invalid signature for entry ${entryData.id}, skipping`);
2465
+ continue;
2466
+ }
2467
+ const decryptedPayload = await this.tenant.decryptPayload(entryData.encryptedData, entryData.decryptionKeyId);
2468
+ doc = automerge_adapter_1.Automerge.loadIncremental(doc, decryptedPayload);
2469
+ const parsed = (0, idGeneration_1.parseDocEntryId)(entryData.id);
2470
+ if (parsed) {
2471
+ this.registerAutomergeHashMapping(docId, parsed.automergeHash, entryData.id);
2472
+ }
2473
+ }
2474
+ const orderedReplayEntries = [...replayEntriesForState].sort((left, right) => left.createdAt !== right.createdAt ? left.createdAt - right.createdAt : left.id.localeCompare(right.id));
2475
+ const firstReplayEntry = orderedReplayEntries[0] ?? null;
2476
+ const lastReplayEntry = orderedReplayEntries[orderedReplayEntries.length - 1] ?? null;
2477
+ return {
2478
+ id: docId,
2479
+ doc,
2480
+ createdAt: firstReplayEntry?.createdAt ?? snapshotMeta?.createdAt ?? fallbackTimestamp,
2481
+ lastModified: lastReplayEntry?.createdAt ?? snapshotMeta?.createdAt ?? fallbackTimestamp,
2482
+ decryptionKeyId: firstReplayEntry?.decryptionKeyId ?? snapshotMeta?.decryptionKeyId ?? "default",
2483
+ isDeleted: lastReplayEntry?.entryType === "doc_delete",
2484
+ };
2485
+ }
1991
2486
  async *iterateChangeMetadataSince(cursor) {
1992
2487
  const startedAt = Date.now();
1993
2488
  const actualCursor = cursor ?? {