superdoc 2.4.0-next.21 → 2.4.0-next.23

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.
Files changed (45) hide show
  1. package/dist/chunks/{create-super-doc-ui-CFsY0uxw.es.js → create-super-doc-ui-CmOSA_lN.es.js} +252 -657
  2. package/dist/chunks/{create-super-doc-ui-jSJQis_0.cjs → create-super-doc-ui-CqGmoMMA.cjs} +251 -662
  3. package/dist/collaboration-upgrade-engine.cjs +1 -1
  4. package/dist/collaboration-upgrade-engine.es.js +1 -1
  5. package/dist/document-api/src/citations/citations.types.d.ts +9 -0
  6. package/dist/document-api/src/types/receipt.d.ts +9 -5
  7. package/dist/layout-engine/contracts/src/index.d.ts +73 -1
  8. package/dist/layout-engine/contracts/src/inline-box.d.ts +19 -0
  9. package/dist/layout-engine/contracts/src/paragraph-visible-text.d.ts +19 -0
  10. package/dist/layout-engine/layout-bridge/src/incrementalLayout.d.ts +2 -0
  11. package/dist/layout-engine/layout-bridge/src/inline-box-key.d.ts +3 -0
  12. package/dist/layout-engine/layout-bridge/src/layoutHeaderFooter.d.ts +5 -1
  13. package/dist/layout-engine/layout-bridge/src/run-visual-marks.d.ts +2 -0
  14. package/dist/layout-engine/layout-bridge/src/text-measurement.d.ts +0 -12
  15. package/dist/layout-engine/painters/dom/src/runs/inline-box.d.ts +8 -0
  16. package/dist/public/ui-react.cjs +1 -1
  17. package/dist/public/ui-react.es.js +1 -1
  18. package/dist/public/ui.cjs +1 -1
  19. package/dist/public/ui.es.js +1 -1
  20. package/dist/style.css +93 -92
  21. package/dist/style.layered.css +93 -92
  22. package/dist/superdoc/src/components/CommentsLayer/CommentDialog.vue.d.ts +2 -0
  23. package/dist/superdoc/src/components/CommentsLayer/commentsList/ReviewDirectoryListItem.vue.d.ts +16 -0
  24. package/dist/superdoc/src/components/CommentsLayer/commentsList/commentsList.vue.d.ts +3 -1
  25. package/dist/superdoc/src/components/CommentsLayer/commentsList/super-comments-list.d.ts +12 -0
  26. package/dist/superdoc/src/core/SuperDoc.d.ts +6 -7
  27. package/dist/superdoc/src/core/extensions/index.d.ts +1 -1
  28. package/dist/superdoc/src/core/extensions/types.d.ts +63 -5
  29. package/dist/superdoc/src/core/types/index.d.ts +19 -0
  30. package/dist/superdoc/src/core/v2-integration/v2-integration.d.ts +3 -3
  31. package/dist/superdoc/src/helpers/v2-review-mutation-impact.d.ts +3 -5
  32. package/dist/superdoc/src/public/index.d.cts +6 -0
  33. package/dist/superdoc/src/public/index.d.ts +3 -0
  34. package/dist/superdoc/src/public/ui/types.d.ts +5 -5
  35. package/dist/superdoc/src/stores/comments-store.d.ts +81 -78
  36. package/dist/superdoc/src/stores/superdoc-store.d.ts +243 -234
  37. package/dist/superdoc.cjs +660 -857
  38. package/dist/superdoc.es.js +648 -846
  39. package/dist-cdn/style.layered.css +1 -1
  40. package/dist-cdn/superdoc.min.css +1 -1
  41. package/dist-cdn/superdoc.min.js +36 -36
  42. package/package.json +2 -2
  43. package/dist/superdoc/src/helpers/v2-remote-review-hydration.d.ts +0 -12
  44. package/dist/superdoc/src/helpers/v2-review-mutation-reconciler.d.ts +0 -19
  45. package/dist/superdoc/src/helpers/v2-typing-review-hydration.d.ts +0 -16
@@ -436,11 +436,9 @@ Object.freeze([
436
436
  * Classify the tracked-change identities carried by a v2 mutation event.
437
437
  *
438
438
  * Receipt entities are the durable invalidation contract between the document
439
- * kernel and derived review UI. Consumers can refresh `upsertIds` narrowly via
440
- * `trackChanges.get(...)`, drop `removedIds` immediately, and leave a bounded
441
- * full-list reconciliation as background verification. History results are
442
- * different: undo/redo can restore many identities at once, so consumers use
443
- * one authoritative catalog after render instead of issuing one read per id.
439
+ * kernel and derived review UI. Consumers apply removals/remaps locally; the
440
+ * next committed window supplies surviving or restored rows. History follows
441
+ * the same bounded path and never enumerates invisible review identities.
444
442
  */
445
443
  function getV2TrackedChangeMutationImpact(event) {
446
444
  if (event?.type !== "mutation:committed") return null;
@@ -480,7 +478,7 @@ function getV2TrackedChangeMutationImpact(event) {
480
478
  upsertIds,
481
479
  removedIds,
482
480
  remappedPairs,
483
- reconcileMode: event.origin === "history" ? "authoritative" : "targeted",
481
+ reconcileMode: "targeted",
484
482
  ...allResolved ? { allResolved } : {}
485
483
  };
486
484
  }
@@ -490,280 +488,6 @@ function readAllResolvedFact(event, receipt) {
490
488
  return fact;
491
489
  }
492
490
  //#endregion
493
- //#region src/helpers/v2-review-mutation-reconciler.js
494
- var normalizeIds = (values) => new Set(Array.from(values ?? []).filter((value) => typeof value === "string" && value.length > 0));
495
- var itemId = (item) => {
496
- if (typeof item?.id === "string" && item.id.length > 0) return item.id;
497
- if (typeof item?.changeId === "string" && item.changeId.length > 0) return item.changeId;
498
- if (typeof item?.address?.entityId === "string" && item.address.entityId.length > 0) return item.address.entityId;
499
- return null;
500
- };
501
- var contextMatches = (left, right) => left?.adapter === right?.adapter && left?.documentId === right?.documentId && left?.editor === right?.editor && left?.reconcileToken === right?.reconcileToken;
502
- /**
503
- * Reconcile tracked changes only after their canonical render has made the
504
- * read model observable. Command receipts use narrow reads; history results
505
- * use one authoritative catalog because undo/redo can restore hundreds of
506
- * identities in one transaction. An absent history identity only converges
507
- * when both catalog and source coverage are explicitly complete.
508
- */
509
- function createV2ReviewMutationReconciler({ getContext, reconcile, hydrate, onReconciled }) {
510
- const pendingUpserts = /* @__PURE__ */ new Map();
511
- let pendingAllResolved = null;
512
- let sequence = 0;
513
- let generation = 0;
514
- let flushInFlight = null;
515
- let flushRequested = false;
516
- const settlementWaiters = [];
517
- const resolveSettlementsThrough = (version, value) => {
518
- for (let index = settlementWaiters.length - 1; index >= 0; index -= 1) {
519
- const waiter = settlementWaiters[index];
520
- if (waiter.generation !== generation || waiter.version > version) continue;
521
- settlementWaiters.splice(index, 1);
522
- waiter.resolve(value === true && waiter.requiresAllResolved ? pendingAllResolved == null : value);
523
- }
524
- };
525
- const isCurrent = (capturedGeneration, context) => generation === capturedGeneration && contextMatches(context, getContext());
526
- const clearResolved = (ids, snapshot) => {
527
- const clearedIds = /* @__PURE__ */ new Set();
528
- for (const id of ids) {
529
- if (pendingUpserts.get(id) !== snapshot.get(id)) continue;
530
- pendingUpserts.delete(id);
531
- clearedIds.add(id);
532
- }
533
- return clearedIds;
534
- };
535
- const readySnapshot = (reconcileMode) => new Map([...pendingUpserts].filter(([, entry]) => entry.ready === true && entry.reconcileMode === reconcileMode));
536
- const markRenderGatedReady = () => {
537
- let changed = false;
538
- if (pendingAllResolved?.gate === "render" && !pendingAllResolved.ready) {
539
- pendingAllResolved.ready = true;
540
- changed = true;
541
- }
542
- for (const entry of pendingUpserts.values()) {
543
- if (entry.gate !== "render" || entry.ready) continue;
544
- entry.ready = true;
545
- changed = true;
546
- }
547
- return changed;
548
- };
549
- const markEntriesReadyThrough = (maxVersion) => {
550
- let changed = false;
551
- if (pendingAllResolved && pendingAllResolved.version <= maxVersion && !pendingAllResolved.ready) {
552
- pendingAllResolved.ready = true;
553
- changed = true;
554
- }
555
- for (const entry of pendingUpserts.values()) {
556
- if (entry.version > maxVersion || entry.ready) continue;
557
- entry.ready = true;
558
- changed = true;
559
- }
560
- return changed;
561
- };
562
- const notifyReconciled = async (context, ids, result, allowEmpty = false) => {
563
- if (!allowEmpty && ids.size === 0 || typeof onReconciled !== "function") return;
564
- try {
565
- await onReconciled(context, ids, result);
566
- } catch {}
567
- };
568
- const idsFromResult = (result) => normalizeIds(result?.resolvedIds ?? (result?.items ?? []).map(itemId).filter(Boolean));
569
- const reconcileTargetedSnapshot = async (context, snapshot, capturedGeneration) => {
570
- const result = await reconcile(context, {
571
- upsertIds: new Set(snapshot.keys()),
572
- removedIds: /* @__PURE__ */ new Set(),
573
- reconcileMode: "targeted"
574
- });
575
- if (!isCurrent(capturedGeneration, context)) return;
576
- const resolvedIds = idsFromResult(result);
577
- const clearedIds = clearResolved(resolvedIds, snapshot);
578
- await notifyReconciled(context, clearedIds, result);
579
- if (new Set([...snapshot].filter(([id, version]) => pendingUpserts.get(id) === version).map(([id]) => id)).size === 0) return;
580
- const fallback = await hydrate(context, {
581
- trackedChangesListMode: "interaction-prime",
582
- refreshReason: "mutation-render-reconcile",
583
- blocking: false
584
- });
585
- if (!isCurrent(capturedGeneration, context)) return;
586
- const fallbackResolvedIds = idsFromResult(fallback);
587
- const fallbackClearedIds = clearResolved(fallbackResolvedIds, snapshot);
588
- await notifyReconciled(context, fallbackClearedIds, fallback);
589
- };
590
- const reconcileAuthoritativeSnapshot = async (context, snapshot, capturedGeneration) => {
591
- const result = await hydrate(context, {
592
- trackedChangesListMode: "all",
593
- refreshReason: "mutation-history-render-reconcile",
594
- blocking: true
595
- });
596
- if (!isCurrent(capturedGeneration, context)) return;
597
- if (![...snapshot].some(([id, entry]) => pendingUpserts.get(id) === entry)) return;
598
- const transportSucceeded = result?.ok === true;
599
- const catalogIsAuthoritative = transportSucceeded && result?.complete === true && result?.sourceCoverageComplete === true;
600
- const returnedIds = transportSucceeded ? idsFromResult(result) : /* @__PURE__ */ new Set();
601
- const clearedIds = catalogIsAuthoritative ? clearResolved(new Set(snapshot.keys()), snapshot) : /* @__PURE__ */ new Set();
602
- await notifyReconciled(context, catalogIsAuthoritative ? clearedIds : returnedIds, {
603
- ...result,
604
- reconcileMode: "authoritative"
605
- }, true);
606
- };
607
- const flush = () => {
608
- if (flushInFlight) return flushInFlight;
609
- const allResolvedSnapshot = pendingAllResolved?.ready ? pendingAllResolved : null;
610
- const targetedSnapshot = readySnapshot("targeted");
611
- const authoritativeSnapshot = readySnapshot("authoritative");
612
- if (!allResolvedSnapshot && targetedSnapshot.size === 0 && authoritativeSnapshot.size === 0) return null;
613
- const context = getContext();
614
- if (!context?.adapter) return null;
615
- flushRequested = false;
616
- const capturedGeneration = generation;
617
- flushInFlight = Promise.resolve().then(async () => {
618
- if (allResolvedSnapshot) {
619
- let result = null;
620
- try {
621
- result = await reconcile(context, {
622
- upsertIds: /* @__PURE__ */ new Set(),
623
- removedIds: /* @__PURE__ */ new Set(),
624
- allResolved: allResolvedSnapshot.fact,
625
- reconcileMode: "targeted"
626
- });
627
- } catch {}
628
- if (!isCurrent(capturedGeneration, context)) return;
629
- if (result?.ok === true) {
630
- if (pendingAllResolved === allResolvedSnapshot) pendingAllResolved = null;
631
- await notifyReconciled(context, /* @__PURE__ */ new Set(), result, true);
632
- }
633
- }
634
- if (authoritativeSnapshot.size > 0) await reconcileAuthoritativeSnapshot(context, authoritativeSnapshot, capturedGeneration).catch(() => void 0);
635
- if (targetedSnapshot.size > 0) await reconcileTargetedSnapshot(context, targetedSnapshot, capturedGeneration).catch(() => void 0);
636
- }).catch(() => {}).finally(() => {
637
- flushInFlight = null;
638
- if (flushRequested) flush();
639
- });
640
- return flushInFlight;
641
- };
642
- const enqueueWithGate = (impact, gate) => {
643
- if (!impact) return;
644
- if (impact.allResolved) {
645
- pendingUpserts.clear();
646
- pendingAllResolved = {
647
- fact: impact.allResolved,
648
- version: ++sequence,
649
- gate,
650
- ready: false
651
- };
652
- }
653
- const reconcileMode = impact.reconcileMode === "authoritative" ? "authoritative" : "targeted";
654
- const removedIds = normalizeIds(impact.removedIds);
655
- const upsertIds = normalizeIds(impact.upsertIds);
656
- for (const id of removedIds) {
657
- const existing = pendingUpserts.get(id);
658
- if (existing?.reconcileMode === "authoritative") pendingUpserts.set(id, {
659
- ...existing,
660
- version: ++sequence,
661
- gate,
662
- ready: false
663
- });
664
- else pendingUpserts.delete(id);
665
- }
666
- const context = getContext();
667
- if (removedIds.size > 0 && upsertIds.size === 0 && context?.adapter && !impact.allResolved) try {
668
- Promise.resolve(reconcile(context, {
669
- upsertIds: /* @__PURE__ */ new Set(),
670
- removedIds
671
- })).catch(() => void 0);
672
- } catch {}
673
- for (const id of upsertIds) if (!removedIds.has(id)) {
674
- const existing = pendingUpserts.get(id);
675
- pendingUpserts.set(id, {
676
- version: ++sequence,
677
- gate,
678
- ready: false,
679
- reconcileMode: reconcileMode === "authoritative" || existing?.reconcileMode === "authoritative" ? "authoritative" : "targeted"
680
- });
681
- }
682
- };
683
- const enqueue = (impact) => enqueueWithGate(impact, "render");
684
- const onRender = (supersedesInFlight = false) => {
685
- const madeReady = markRenderGatedReady();
686
- if (flushInFlight && (madeReady || supersedesInFlight)) flushRequested = true;
687
- flush();
688
- };
689
- return {
690
- enqueue,
691
- enqueueAfterPaint(impact, waitForPaint) {
692
- if (!impact || typeof waitForPaint !== "function") {
693
- enqueue(impact);
694
- return Promise.resolve(false);
695
- }
696
- enqueueWithGate(impact, "exact-paint");
697
- const context = getContext();
698
- const capturedGeneration = generation;
699
- const capturedSequence = sequence;
700
- if (!context?.adapter) {
701
- markEntriesReadyThrough(capturedSequence);
702
- return Promise.resolve(false);
703
- }
704
- const completion = new Promise((resolve) => {
705
- settlementWaiters.push({
706
- generation: capturedGeneration,
707
- version: capturedSequence,
708
- requiresAllResolved: Boolean(impact.allResolved),
709
- resolve
710
- });
711
- });
712
- const impactedIds = /* @__PURE__ */ new Set([...normalizeIds(impact.upsertIds), ...normalizeIds(impact.removedIds)]);
713
- const capturedEntries = new Map([...impactedIds].flatMap((id) => {
714
- const entry = pendingUpserts.get(id);
715
- return entry ? [[id, entry]] : [];
716
- }));
717
- Promise.resolve().then(() => waitForPaint()).then(async () => {
718
- if (!isCurrent(capturedGeneration, context)) {
719
- resolveSettlementsThrough(capturedSequence, false);
720
- return;
721
- }
722
- let madeReady = markRenderGatedReady();
723
- if (markEntriesReadyThrough(capturedSequence)) madeReady = true;
724
- for (const [id, entry] of capturedEntries) {
725
- if (pendingUpserts.get(id) !== entry) continue;
726
- if (!entry.ready) madeReady = true;
727
- entry.ready = true;
728
- }
729
- if (flushInFlight && (madeReady || capturedSequence > 0)) flushRequested = true;
730
- const preceding = flushInFlight;
731
- const pending = flush();
732
- if (pending) await pending;
733
- if (preceding) {
734
- const followUp = flushInFlight;
735
- if (followUp && followUp !== preceding) await followUp;
736
- }
737
- if (!isCurrent(capturedGeneration, context)) {
738
- resolveSettlementsThrough(capturedSequence, false);
739
- return;
740
- }
741
- resolveSettlementsThrough(capturedSequence, true);
742
- }).catch(() => {
743
- if (!isCurrent(capturedGeneration, context)) {
744
- resolveSettlementsThrough(capturedSequence, false);
745
- return;
746
- }
747
- for (const [id, entry] of capturedEntries) if (pendingUpserts.get(id) === entry) entry.gate = "render";
748
- if (pendingAllResolved?.gate === "exact-paint") pendingAllResolved.gate = "render";
749
- resolveSettlementsThrough(capturedSequence, false);
750
- });
751
- return completion;
752
- },
753
- onRender,
754
- reset() {
755
- for (const waiter of settlementWaiters.splice(0)) waiter.resolve(false);
756
- generation += 1;
757
- pendingUpserts.clear();
758
- pendingAllResolved = null;
759
- flushRequested = false;
760
- },
761
- getPendingIds() {
762
- return new Set(pendingUpserts.keys());
763
- }
764
- };
765
- }
766
- //#endregion
767
491
  //#region src/public/ui/reasons.ts
768
492
  /**
769
493
  * Stable public reason taxonomy for the v2-native `superdoc/ui` controller.
@@ -1837,6 +1561,17 @@ function collectEntityHitsFromChain(start, stopAt) {
1837
1561
  for (const hit of pendingStory) hit.story = layoutStory;
1838
1562
  pendingStory = [];
1839
1563
  }
1564
+ const citationId = node.getAttribute("data-citation-id");
1565
+ if (citationId) {
1566
+ const key = `citation:${citationId}`;
1567
+ if (!seen.has(key)) {
1568
+ seen.add(key);
1569
+ hits.push({
1570
+ type: "citation",
1571
+ id: citationId
1572
+ });
1573
+ }
1574
+ }
1840
1575
  const commentIds = node.getAttribute("data-comment-ids");
1841
1576
  if (commentIds) for (const id of parseCommaSeparatedIds(commentIds)) {
1842
1577
  const key = `comment:${id}`;
@@ -2402,22 +2137,24 @@ function isHostGeometryTarget(target) {
2402
2137
  * address lets the host prefer the exact painted review carrier after the
2403
2138
  * containing block has been materialized.
2404
2139
  */
2405
- function readTrackedChangeNavigationTarget(row) {
2140
+ function readTrackedChangeNavigationTarget(row, options) {
2406
2141
  if (!row || typeof row !== "object") return null;
2407
2142
  const target = readEntityTarget(row);
2408
- if (isHostGeometryTarget(target)) return target;
2409
2143
  const record = row;
2410
2144
  const navigationTarget = record.navigationTarget && typeof record.navigationTarget === "object" ? record.navigationTarget : null;
2411
- if (navigationTarget?.kind === "block" && typeof navigationTarget.blockId === "string" && navigationTarget.blockId.length > 0) return {
2412
- kind: "text",
2413
- blockId: navigationTarget.blockId,
2414
- range: {
2415
- start: 0,
2416
- end: 0
2417
- },
2418
- ...navigationTarget.story && typeof navigationTarget.story === "object" ? { story: navigationTarget.story } : {},
2419
- ...record.address && typeof record.address === "object" ? { address: record.address } : {}
2420
- };
2145
+ if (navigationTarget?.kind === "block" && typeof navigationTarget.blockId === "string" && navigationTarget.blockId.length > 0) {
2146
+ const stableBlockTarget = {
2147
+ kind: "text",
2148
+ blockId: navigationTarget.blockId,
2149
+ range: {
2150
+ start: 0,
2151
+ end: 0
2152
+ },
2153
+ ...navigationTarget.story && typeof navigationTarget.story === "object" ? { story: navigationTarget.story } : {},
2154
+ ...record.address && typeof record.address === "object" ? { address: record.address } : {}
2155
+ };
2156
+ if (options?.preferStableBlock || !isHostGeometryTarget(target)) return stableBlockTarget;
2157
+ }
2421
2158
  return target;
2422
2159
  }
2423
2160
  function storyLocatorSignature(story) {
@@ -3732,8 +3469,8 @@ function createSuperDocUI(options) {
3732
3469
  const getV2ReviewWindowSource = () => {
3733
3470
  const editor = getEditor();
3734
3471
  if (editor?.editorVersion !== 2) return null;
3735
- const reviewHydration = editor.reviewHydration;
3736
- return reviewHydration && typeof reviewHydration === "object" ? reviewHydration : null;
3472
+ const reviewWindow = editor.reviewWindow;
3473
+ return reviewWindow && typeof reviewWindow === "object" ? reviewWindow : null;
3737
3474
  };
3738
3475
  const getV2ReviewWindowSnapshot = () => {
3739
3476
  const source = getV2ReviewWindowSource();
@@ -4012,7 +3749,6 @@ function createSuperDocUI(options) {
4012
3749
  return `e${id}`;
4013
3750
  };
4014
3751
  let documentMutationRevision = 0;
4015
- let reviewMutationToken = {};
4016
3752
  const uiTrackedChangesCatalogHost = options.superdoc;
4017
3753
  const uiTrackedChangesCatalogState = acquireSharedUiTrackedChangesCatalog(uiTrackedChangesCatalogHost);
4018
3754
  const supersedeUiTrackedChangesCatalogRead = () => {
@@ -4044,7 +3780,6 @@ function createSuperDocUI(options) {
4044
3780
  return fallback();
4045
3781
  }
4046
3782
  if (uiTrackedChangesCatalogState.activeMutationTokens.size > 0) return Promise.reject(/* @__PURE__ */ new Error("ui-review-catalog-superseded"));
4047
- if ((getEditor()?.reviewHydration?.getDiagnostics?.())?.inFlight === true) return Promise.reject(/* @__PURE__ */ new Error("shell-review-hydration-in-flight"));
4048
3783
  const generation = uiTrackedChangesCatalogState.generation;
4049
3784
  const existing = uiTrackedChangesCatalogState.inFlight;
4050
3785
  if (existing?.generation === generation) return existing.promise;
@@ -4070,15 +3805,6 @@ function createSuperDocUI(options) {
4070
3805
  });
4071
3806
  return promise;
4072
3807
  };
4073
- let authoritativeTrackChangesPendingToken = null;
4074
- let authoritativeTrackChangesRetryTimer = null;
4075
- let authoritativeTrackChangesFailureCount = 0;
4076
- const clearAuthoritativeTrackChangesRetry = () => {
4077
- if (authoritativeTrackChangesRetryTimer) clearTimeout(authoritativeTrackChangesRetryTimer);
4078
- authoritativeTrackChangesRetryTimer = null;
4079
- authoritativeTrackChangesFailureCount = 0;
4080
- };
4081
- let scheduleAuthoritativeTrackChangesRetry = (_reconcileToken) => void 0;
4082
3808
  let postDecisionTrackChangesToken = null;
4083
3809
  let postDecisionTrackChangeIds = /* @__PURE__ */ new Set();
4084
3810
  let allTrackedChangesResolvedToken = null;
@@ -4198,65 +3924,6 @@ function createSuperDocUI(options) {
4198
3924
  publishPostDecisionTrackChanges(newlyRemoved, receipt);
4199
3925
  };
4200
3926
  const postDecisionTrackChangeIdsForToken = (token) => postDecisionTrackChangesToken === token && postDecisionTrackChangeIds.size > 0 ? postDecisionTrackChangeIds : null;
4201
- /**
4202
- * Merge a narrow tracked-change read into the two settled review caches.
4203
- * Existing enriched adapter fields are retained while the fresh Document
4204
- * API fields win. The entry keeps its original content token, so a row that
4205
- * settles after the typing invalidation is truthfully served as stale rather
4206
- * than discarded or mislabeled as a complete current-token catalog.
4207
- */
4208
- const mergeTrackedChangeItemsIntoCache = (items, orderedItems = []) => {
4209
- const projected = items.map(projectTrackChangesItem).filter((item) => item != null && readEntityId(item) != null);
4210
- if (projected.length === 0) return;
4211
- const orderedIds = orderedItems.map(projectTrackChangesItem).map((item) => item == null ? null : readEntityId(item)).filter((id) => id != null);
4212
- for (const key of ["trackChanges", "trackChanges:all"]) {
4213
- const entry = asyncReads.get(key);
4214
- const nextValue = entry?.hasSettled && Array.isArray(entry.value) ? [...entry.value] : [];
4215
- const insertedIds = /* @__PURE__ */ new Set();
4216
- let changed = false;
4217
- for (const fresh of projected) {
4218
- const id = readEntityId(fresh);
4219
- if (!id) continue;
4220
- const index = nextValue.findIndex((candidate) => readEntityId(candidate) === id);
4221
- if (index < 0) {
4222
- nextValue.push(fresh);
4223
- insertedIds.add(id);
4224
- } else {
4225
- const previous = nextValue[index];
4226
- nextValue[index] = isLooseObject(previous) ? {
4227
- ...previous,
4228
- ...fresh
4229
- } : fresh;
4230
- }
4231
- changed = true;
4232
- }
4233
- if (key === "trackChanges" && insertedIds.size > 0 && orderedIds.length > 0) {
4234
- const placeableIds = new Set(orderedIds.filter((id) => insertedIds.has(id)));
4235
- const insertedRows = new Map(nextValue.filter((item) => placeableIds.has(readEntityId(item) ?? "")).map((item) => [readEntityId(item), item]));
4236
- const placedValue = nextValue.filter((item) => !placeableIds.has(readEntityId(item) ?? ""));
4237
- for (const [orderIndex, id] of orderedIds.entries()) {
4238
- if (!placeableIds.has(id)) continue;
4239
- const item = insertedRows.get(id);
4240
- if (!item) continue;
4241
- const followingAnchor = orderedIds.slice(orderIndex + 1).map((candidateId) => placedValue.findIndex((candidate) => readEntityId(candidate) === candidateId)).find((index) => index >= 0);
4242
- if (followingAnchor != null) {
4243
- placedValue.splice(followingAnchor, 0, item);
4244
- continue;
4245
- }
4246
- const precedingAnchor = orderedIds.slice(0, orderIndex).reverse().map((candidateId) => placedValue.findIndex((candidate) => readEntityId(candidate) === candidateId)).find((index) => index >= 0);
4247
- placedValue.splice(precedingAnchor == null ? placedValue.length : precedingAnchor + 1, 0, item);
4248
- }
4249
- nextValue.splice(0, nextValue.length, ...placedValue);
4250
- }
4251
- if (!changed) continue;
4252
- asyncReads.set(key, {
4253
- token: entry?.token ?? null,
4254
- value: nextValue,
4255
- hasSettled: true,
4256
- inflightToken: null
4257
- });
4258
- }
4259
- };
4260
3927
  const replaceTrackedChangeItemsInCache = (items) => {
4261
3928
  const token = contentToken();
4262
3929
  const suppressedIds = postDecisionTrackChangeIdsForToken(token);
@@ -4272,183 +3939,6 @@ function createSuperDocUI(options) {
4272
3939
  inflightToken: null
4273
3940
  });
4274
3941
  };
4275
- const hydrateTrackedChanges = async (context, options) => {
4276
- try {
4277
- const listTrackedChanges = context.v2TrackedChanges?.listTrackedChanges;
4278
- if (typeof listTrackedChanges === "function") {
4279
- const result = await Promise.resolve(listTrackedChanges.call(context.v2TrackedChanges, {
4280
- mode: options.trackedChangesListMode,
4281
- refreshReason: options.refreshReason,
4282
- blocking: options.blocking,
4283
- ...options.targetIds?.length ? { targetIds: options.targetIds } : {}
4284
- }));
4285
- const ok = options.trackedChangesListMode === "all" && options.refreshReason === "mutation-history-render-reconcile" ? result?.ok === true : result?.ok !== false;
4286
- return {
4287
- ok,
4288
- items: ok && Array.isArray(result?.items) ? result.items : [],
4289
- ...typeof result?.complete === "boolean" ? { complete: result.complete } : {},
4290
- ...typeof result?.sourceCoverageComplete === "boolean" ? { sourceCoverageComplete: result.sourceCoverageComplete } : {}
4291
- };
4292
- }
4293
- const list = context.docTrackChanges?.list;
4294
- if (typeof list !== "function") return {
4295
- ok: false,
4296
- items: []
4297
- };
4298
- const result = await Promise.resolve(list.call(context.docTrackChanges));
4299
- return {
4300
- ok: true,
4301
- items: Array.isArray(result?.items) ? result.items : []
4302
- };
4303
- } catch (error) {
4304
- return {
4305
- ok: false,
4306
- items: [],
4307
- failures: [error]
4308
- };
4309
- }
4310
- };
4311
- /**
4312
- * Read receipt-addressed review effects after their exact canonical paint.
4313
- * The adapter's narrow get preserves its enriched row projection; the public
4314
- * Document API is the fallback for hosts without that bridge method. Empty
4315
- * successful reads remain unresolved and immediately use one bounded
4316
- * interaction-prime list instead of waiting for the typing-idle catalog.
4317
- */
4318
- const reviewMutationReconciler = createV2ReviewMutationReconciler({
4319
- getContext: () => {
4320
- const editor = getEditor();
4321
- if (!editor) return null;
4322
- const v2TrackedChanges = getV2TrackedChanges();
4323
- const docTrackChanges = getDoc()?.trackChanges ?? null;
4324
- const adapter = v2TrackedChanges ?? docTrackChanges;
4325
- if (!adapter) return null;
4326
- const rawDocumentId = editor.documentId ?? v2TrackedChanges?.documentId;
4327
- return {
4328
- editor,
4329
- adapter,
4330
- documentId: rawDocumentId == null ? null : String(rawDocumentId),
4331
- reconcileToken: reviewMutationToken,
4332
- v2TrackedChanges,
4333
- docTrackChanges
4334
- };
4335
- },
4336
- reconcile: async (context, impact) => {
4337
- if (impact.removedIds.size > 0) markPostDecisionTrackChanges(new Set(impact.removedIds));
4338
- const requestedIds = [...new Set(impact.upsertIds)];
4339
- if (requestedIds.length === 0) return {
4340
- ok: true,
4341
- items: [],
4342
- resolvedIds: [],
4343
- unresolvedIds: []
4344
- };
4345
- const settledTrackChanges = asyncReads.get("trackChanges");
4346
- const settledTrackChangeItems = settledTrackChanges?.hasSettled && Array.isArray(settledTrackChanges.value) ? settledTrackChanges.value : null;
4347
- const cachedIds = new Set(settledTrackChangeItems ? settledTrackChangeItems.map(readEntityId).filter((id) => id != null) : []);
4348
- const settledCatalogIsProvenEmpty = settledTrackChangeItems?.length === 0 && hostSourceLoadPhase() === "complete";
4349
- const idsNeedingPlacement = new Set(requestedIds.filter((id) => !cachedIds.has(id)));
4350
- const getTrackedChange = context.v2TrackedChanges?.getTrackedChange;
4351
- const getRawTrackedChange = context.docTrackChanges?.get;
4352
- if (typeof getTrackedChange !== "function" && typeof getRawTrackedChange !== "function") return {
4353
- ok: false,
4354
- items: [],
4355
- resolvedIds: [],
4356
- unresolvedIds: requestedIds
4357
- };
4358
- const reads = await Promise.all(requestedIds.map(async (id) => {
4359
- try {
4360
- if (typeof getTrackedChange === "function") {
4361
- const result = await Promise.resolve(getTrackedChange.call(context.v2TrackedChanges, id));
4362
- return {
4363
- id,
4364
- ok: result?.ok !== false,
4365
- items: result?.ok !== false && Array.isArray(result?.items) ? result.items : [],
4366
- result
4367
- };
4368
- }
4369
- const result = await Promise.resolve(getRawTrackedChange.call(context.docTrackChanges, { id }));
4370
- return {
4371
- id,
4372
- ok: true,
4373
- items: result && typeof result === "object" ? [result] : [],
4374
- result
4375
- };
4376
- } catch (error) {
4377
- return {
4378
- id,
4379
- ok: false,
4380
- items: [],
4381
- result: error
4382
- };
4383
- }
4384
- }));
4385
- const items = [];
4386
- const resolvedIds = [];
4387
- const unresolvedIds = [];
4388
- for (const read of reads) {
4389
- const matchingItems = read.items.filter((item) => {
4390
- const projected = projectTrackChangesItem(item);
4391
- return projected != null && readEntityId(projected) === read.id;
4392
- });
4393
- if (read.ok && matchingItems.length > 0) {
4394
- items.push(...matchingItems);
4395
- resolvedIds.push(read.id);
4396
- } else unresolvedIds.push(read.id);
4397
- }
4398
- let orderedItems;
4399
- const placementIds = resolvedIds.filter((id) => idsNeedingPlacement.has(id));
4400
- if (placementIds.length > 0 && !settledCatalogIsProvenEmpty) {
4401
- const orderResult = await hydrateTrackedChanges(context, {
4402
- trackedChangesListMode: "interaction-prime",
4403
- refreshReason: "mutation-render-reconcile",
4404
- blocking: false,
4405
- targetIds: placementIds
4406
- });
4407
- if (orderResult.ok) orderedItems = orderResult.items;
4408
- }
4409
- return {
4410
- ok: unresolvedIds.length === 0,
4411
- items,
4412
- orderedItems,
4413
- resolvedIds,
4414
- unresolvedIds,
4415
- failures: reads.filter((read) => !read.ok).map((read) => read.result)
4416
- };
4417
- },
4418
- hydrate: hydrateTrackedChanges,
4419
- onReconciled: (context, ids, result) => {
4420
- if (result?.reconcileMode === "authoritative") {
4421
- if (context.reconcileToken !== reviewMutationToken) return;
4422
- if (result.ok === true && result.complete === true && result.sourceCoverageComplete === true) {
4423
- clearAuthoritativeTrackChangesRetry();
4424
- replaceTrackedChangeItemsInCache(result.items ?? []);
4425
- authoritativeTrackChangesPendingToken = null;
4426
- } else if (result.ok === true) {
4427
- clearAuthoritativeTrackChangesRetry();
4428
- mergeTrackedChangeItemsIntoCache(result.items ?? []);
4429
- } else scheduleAuthoritativeTrackChangesRetry(context.reconcileToken);
4430
- recompute();
4431
- return;
4432
- }
4433
- const items = (result?.items ?? []).filter((item) => {
4434
- const projected = projectTrackChangesItem(item);
4435
- const id = projected == null ? null : readEntityId(projected);
4436
- return id != null && ids.has(id);
4437
- });
4438
- mergeTrackedChangeItemsIntoCache(items, result?.orderedItems ?? result?.items ?? []);
4439
- recompute();
4440
- }
4441
- });
4442
- scheduleAuthoritativeTrackChangesRetry = (reconcileToken) => {
4443
- if (disposed || authoritativeTrackChangesRetryTimer) return;
4444
- const delayMs = Math.min(ASYNC_READ_FAILURE_RETRY_BASE_MS * 2 ** authoritativeTrackChangesFailureCount, ASYNC_READ_FAILURE_RETRY_MAX_MS);
4445
- authoritativeTrackChangesFailureCount += 1;
4446
- authoritativeTrackChangesRetryTimer = setTimeout(() => {
4447
- authoritativeTrackChangesRetryTimer = null;
4448
- if (disposed || authoritativeTrackChangesPendingToken === null || reconcileToken !== reviewMutationToken) return;
4449
- reviewMutationReconciler.onRender(true);
4450
- }, delayMs);
4451
- };
4452
3942
  /** Token for the live selection read, including the mode that controls its public projection. */
4453
3943
  const selectionReadToken = () => `${contentToken()}|m${readDocumentMode()}|s${selectionEpoch}`;
4454
3944
  /** Stable signature of a settled selection, used to key selection-scoped reads. */
@@ -4474,6 +3964,11 @@ function createSuperDocUI(options) {
4474
3964
  return "unknown";
4475
3965
  };
4476
3966
  const demandedHeavyReads = /* @__PURE__ */ new Map();
3967
+ const incompleteTrackChangesDirectoryReadTokens = /* @__PURE__ */ new Map();
3968
+ const postSourceCompletionRefreshTokens = /* @__PURE__ */ new Map();
3969
+ let sourceCompletionObservedToken = null;
3970
+ let commentsDirectoryLeaseCount = 0;
3971
+ let trackChangesDirectoryLeaseCount = 0;
4477
3972
  const demandHeavyDocRead = (key) => {
4478
3973
  const token = contentToken();
4479
3974
  if (demandedHeavyReads.get(key) === token) return;
@@ -4486,6 +3981,41 @@ function createSuperDocUI(options) {
4486
3981
  return Array.isArray(entry.value) && entry.value.length > 0;
4487
3982
  };
4488
3983
  const heavyReadDemandActive = (key, token) => demandedHeavyReads.get(key) === token;
3984
+ const refreshIncompleteTrackChangesDirectories = () => {
3985
+ const token = contentToken();
3986
+ let invalidated = false;
3987
+ for (const [key, incompleteToken] of incompleteTrackChangesDirectoryReadTokens) {
3988
+ if (incompleteToken !== token) {
3989
+ incompleteTrackChangesDirectoryReadTokens.delete(key);
3990
+ continue;
3991
+ }
3992
+ if (postSourceCompletionRefreshTokens.get(key) === token) continue;
3993
+ const entry = asyncReads.get(key);
3994
+ if (!entry?.hasSettled || entry.token !== token || entry.inflightToken != null) continue;
3995
+ asyncReads.set(key, {
3996
+ ...entry,
3997
+ token: null,
3998
+ inflightToken: null
3999
+ });
4000
+ postSourceCompletionRefreshTokens.set(key, token);
4001
+ invalidated = true;
4002
+ }
4003
+ if (invalidated) scheduleAsyncRefresh();
4004
+ };
4005
+ const acquireDirectoryLease = (family) => {
4006
+ if (family === "comments") commentsDirectoryLeaseCount += 1;
4007
+ else trackChangesDirectoryLeaseCount += 1;
4008
+ demandHeavyDocRead(family);
4009
+ let released = false;
4010
+ return () => {
4011
+ if (released) return;
4012
+ released = true;
4013
+ if (family === "comments") commentsDirectoryLeaseCount = Math.max(0, commentsDirectoryLeaseCount - 1);
4014
+ else trackChangesDirectoryLeaseCount = Math.max(0, trackChangesDirectoryLeaseCount - 1);
4015
+ if ((family === "comments" ? commentsDirectoryLeaseCount : trackChangesDirectoryLeaseCount) === 0 && demandedHeavyReads.get(family) === contentToken()) demandedHeavyReads.delete(family);
4016
+ scheduleAsyncRefresh();
4017
+ };
4018
+ };
4489
4019
  /**
4490
4020
  * Demand-driven route for the content-controls catalog (panel / API use).
4491
4021
  * Explicit demand bypasses both active-loading and post-complete idle holds.
@@ -4556,6 +4086,8 @@ function createSuperDocUI(options) {
4556
4086
  const handleLoadingSnapshot = (snapshot) => {
4557
4087
  const stage = snapshot?.sourceStage;
4558
4088
  if (stage === "source-complete" || stage === "source-failed" || stage === "source-cancelled") {
4089
+ sourceCompletionObservedToken = contentToken();
4090
+ refreshIncompleteTrackChangesDirectories();
4559
4091
  lastEditableMutationAtMs = Math.max(lastEditableMutationAtMs, Date.now());
4560
4092
  scheduleHeavyReadCompletionRecompute();
4561
4093
  }
@@ -4880,12 +4412,8 @@ function createSuperDocUI(options) {
4880
4412
  const editor = getEditor();
4881
4413
  if (editor === lastCoordinatorEditor) return;
4882
4414
  resetPostPaintContentRefresh();
4883
- reviewMutationReconciler.reset();
4884
- reviewMutationToken = {};
4885
4415
  lastCoordinatorEditor = editor;
4886
4416
  asyncReads.clear();
4887
- authoritativeTrackChangesPendingToken = null;
4888
- clearAuthoritativeTrackChangesRetry();
4889
4417
  if (asyncReadFailureRetryTimer) clearTimeout(asyncReadFailureRetryTimer);
4890
4418
  asyncReadFailureRetryTimer = null;
4891
4419
  asyncReadFailureRetryAtMs = 0;
@@ -4897,6 +4425,9 @@ function createSuperDocUI(options) {
4897
4425
  pendingSelectionSeedValidationToken = null;
4898
4426
  coldAsyncReadDeferrals.clear();
4899
4427
  demandedHeavyReads.clear();
4428
+ incompleteTrackChangesDirectoryReadTokens.clear();
4429
+ postSourceCompletionRefreshTokens.clear();
4430
+ sourceCompletionObservedToken = null;
4900
4431
  heavyReadsHeldUntilIdle = false;
4901
4432
  if (detachSourceLoading) detachSourceLoading();
4902
4433
  detachSourceLoading = null;
@@ -4906,9 +4437,10 @@ function createSuperDocUI(options) {
4906
4437
  };
4907
4438
  /** Bump the document-mutation revision so content reads re-fetch on the next compute. */
4908
4439
  const invalidateDocumentContent = () => {
4909
- const carryAuthoritativeTrackChangesHold = authoritativeTrackChangesPendingToken !== null;
4910
4440
  documentMutationRevision += 1;
4911
- if (carryAuthoritativeTrackChangesHold) authoritativeTrackChangesPendingToken = contentToken();
4441
+ incompleteTrackChangesDirectoryReadTokens.clear();
4442
+ postSourceCompletionRefreshTokens.clear();
4443
+ sourceCompletionObservedToken = null;
4912
4444
  clearPostDecisionTrackChanges();
4913
4445
  };
4914
4446
  /**
@@ -5136,17 +4668,51 @@ function createSuperDocUI(options) {
5136
4668
  recompute();
5137
4669
  return state.selection;
5138
4670
  };
5139
- const computeComments = (selection) => {
4671
+ const readCommentsDirectory = () => {
5140
4672
  const commentsApi = getDoc()?.comments;
4673
+ return readAsync("comments", contentToken(), () => commentsApi?.list ? commentsApi.list() : void 0, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
4674
+ };
4675
+ const readTrackChangesDirectory = (key = "trackChanges") => {
4676
+ const token = contentToken();
4677
+ const tcApi = getDoc()?.trackChanges;
4678
+ const v2TrackedChanges = getV2TrackedChanges();
4679
+ const listTrackedChanges = typeof v2TrackedChanges?.listTrackedChanges === "function" ? v2TrackedChanges.listTrackedChanges : null;
4680
+ const read = readAsync(key, token, () => {
4681
+ if (listTrackedChanges) return runUiTrackedChangesCatalogRead(() => tcApi?.list?.({ in: "all" }));
4682
+ return tcApi?.list ? tcApi.list({ in: "all" }) : void 0;
4683
+ }, (raw) => {
4684
+ const result = raw && typeof raw === "object" ? raw : null;
4685
+ if (result?.complete === false || result?.sourceCoverageComplete === false) {
4686
+ incompleteTrackChangesDirectoryReadTokens.set(key, token);
4687
+ armSourceLoadCompletionRecompute();
4688
+ if (sourceCompletionObservedToken === token || hostSourceLoadPhase() === "complete") {
4689
+ const refresh = () => {
4690
+ if (!disposed) refreshIncompleteTrackChangesDirectories();
4691
+ };
4692
+ if (typeof queueMicrotask === "function") queueMicrotask(refresh);
4693
+ else Promise.resolve().then(refresh);
4694
+ }
4695
+ } else if (incompleteTrackChangesDirectoryReadTokens.get(key) === token) incompleteTrackChangesDirectoryReadTokens.delete(key);
4696
+ return result && Array.isArray(result.items) ? result.items : [];
4697
+ });
4698
+ return incompleteTrackChangesDirectoryReadTokens.get(key) === token && read.status === "ready" ? {
4699
+ ...read,
4700
+ status: "stale"
4701
+ } : read;
4702
+ };
4703
+ const computeComments = (selection) => {
5141
4704
  const reviewWindow = getV2ReviewWindowSnapshot();
5142
- const catalog = readAsync("comments", contentToken(), () => commentsApi?.list ? commentsApi.list() : void 0, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
5143
- const windowItems = hasV2ReviewWindowFeed() && Array.isArray(reviewWindow?.commentItems) ? reviewWindow.commentItems : null;
5144
- const useWindowSlice = catalog.status === "pending" && windowItems != null;
5145
- const value = useWindowSlice ? windowItems : catalog.value;
5146
- const listStatus = useWindowSlice ? reviewWindowSliceStatus(reviewWindow) : catalog.status;
4705
+ const hasWindowFeed = hasV2ReviewWindowFeed();
4706
+ const catalog = !hasWindowFeed ? readCommentsDirectory() : null;
4707
+ const windowItems = hasWindowFeed && Array.isArray(reviewWindow?.commentItems) ? reviewWindow.commentItems : null;
4708
+ const value = hasWindowFeed ? windowItems : catalog.value;
4709
+ const listStatus = hasWindowFeed ? reviewWindowSliceStatus(reviewWindow) : catalog.status;
5147
4710
  const items = value ?? [];
5148
4711
  const activeIds = selection.activeCommentIds;
5149
- if (explicitActiveCommentId && !items.some((item) => readEntityId(item) === explicitActiveCommentId)) explicitActiveCommentId = null;
4712
+ const directory = asyncReads.get("comments");
4713
+ const directoryItems = directory?.token === contentToken() && directory.hasSettled && Array.isArray(directory.value) ? directory.value : null;
4714
+ const activeValidationItems = commentsDirectoryLeaseCount > 0 ? directoryItems : items;
4715
+ if (explicitActiveCommentId && activeValidationItems && !activeValidationItems.some((item) => readEntityId(item) === explicitActiveCommentId)) explicitActiveCommentId = null;
5150
4716
  const activeId = explicitActiveCommentId ?? activeIds[0] ?? null;
5151
4717
  return {
5152
4718
  status: combineStatus(listStatus, selection.status),
@@ -5157,51 +4723,32 @@ function createSuperDocUI(options) {
5157
4723
  activeId
5158
4724
  };
5159
4725
  };
5160
- const readAllStoryTrackChanges = () => {
5161
- const tcApi = getDoc()?.trackChanges;
5162
- const token = contentToken();
5163
- const heldEntry = asyncReads.get("trackChanges:all");
5164
- const { value, status } = authoritativeTrackChangesPendingToken === token ? {
5165
- value: heldEntry?.hasSettled ? heldEntry.value : null,
5166
- status: heldEntry?.hasSettled ? "stale" : "pending"
5167
- } : readAsync("trackChanges:all", token, () => {
5168
- if (!tcApi?.list) return void 0;
5169
- const fallback = () => tcApi.list({ in: "all" });
5170
- const bridgeOwnsCatalog = typeof getV2TrackedChanges()?.listTrackedChanges === "function";
5171
- const shared = runUiTrackedChangesCatalogRead(fallback);
5172
- if (!bridgeOwnsCatalog) return shared;
5173
- return Promise.resolve(shared).then((raw) => {
5174
- const record = isLooseObject(raw) ? raw : null;
5175
- return (Array.isArray(record?.items) ? record.items : []).some((item) => readEntityStory(item) == null) ? fallback() : raw;
5176
- });
5177
- }, (raw) => {
5178
- return (raw && Array.isArray(raw.items) ? raw.items : []).map(projectTrackChangesItem).filter((item) => item != null);
5179
- });
5180
- return status === "ready" ? value : null;
4726
+ const projectedTrackChangesCache = /* @__PURE__ */ new WeakMap();
4727
+ const projectTrackChangesItems = (source) => {
4728
+ const cached = projectedTrackChangesCache.get(source);
4729
+ if (cached) return cached;
4730
+ const projected = source.map(projectTrackChangesItem).filter((item) => item != null);
4731
+ projectedTrackChangesCache.set(source, projected);
4732
+ return projected;
5181
4733
  };
5182
- const computeTrackChanges = (selection) => {
5183
- const tcApi = getDoc()?.trackChanges;
5184
- const v2TrackedChanges = getV2TrackedChanges();
5185
- const listTrackedChanges = typeof v2TrackedChanges?.listTrackedChanges === "function" ? v2TrackedChanges.listTrackedChanges : null;
5186
- const token = contentToken();
5187
- const postDecisionIds = postDecisionTrackChangeIdsForToken(token);
5188
- const heldEntry = asyncReads.get("trackChanges");
5189
- const reviewWindow = getV2ReviewWindowSnapshot();
5190
- const catalog = authoritativeTrackChangesPendingToken === token ? {
5191
- value: heldEntry?.hasSettled ? heldEntry.value : null,
5192
- status: heldEntry?.hasSettled ? "stale" : "pending"
5193
- } : readAsync("trackChanges", token, () => {
5194
- if (listTrackedChanges) return runUiTrackedChangesCatalogRead(() => tcApi?.list?.());
5195
- return tcApi?.list ? tcApi.list({ in: "all" }) : void 0;
5196
- }, (raw) => raw && Array.isArray(raw.items) ? raw.items : []);
5197
- const windowItems = hasV2ReviewWindowFeed() && Array.isArray(reviewWindow?.trackedChangeItems) ? reviewWindow.trackedChangeItems : null;
5198
- const useWindowSlice = catalog.status === "pending" && windowItems != null;
5199
- const value = useWindowSlice ? windowItems : catalog.value;
5200
- const listStatus = useWindowSlice ? reviewWindowSliceStatus(reviewWindow) : catalog.status;
5201
- const items = (value ?? []).map(projectTrackChangesItem).filter((item) => item != null).filter((item) => {
4734
+ const filteredTrackChangesCache = /* @__PURE__ */ new WeakMap();
4735
+ const filterPostDecisionTrackChanges = (source, suppressedIds) => {
4736
+ const cached = filteredTrackChangesCache.get(source);
4737
+ if (cached?.suppressedIds === suppressedIds) return cached.value;
4738
+ const filtered = suppressedIds ? source.filter((item) => {
5202
4739
  const id = readEntityId(item);
5203
- return !id || !postDecisionIds?.has(id);
4740
+ return !id || !suppressedIds.has(id);
4741
+ }) : source;
4742
+ filteredTrackChangesCache.set(source, {
4743
+ suppressedIds,
4744
+ value: filtered
5204
4745
  });
4746
+ return filtered;
4747
+ };
4748
+ const trackChangeAuthorsCache = /* @__PURE__ */ new WeakMap();
4749
+ const trackChangeAuthors = (items) => {
4750
+ const cached = trackChangeAuthorsCache.get(items);
4751
+ if (cached) return cached;
5205
4752
  const authors = /* @__PURE__ */ new Set();
5206
4753
  for (const item of items) {
5207
4754
  const change = trackChangesItemPayload(item);
@@ -5209,12 +4756,38 @@ function createSuperDocUI(options) {
5209
4756
  const author = change.author ?? change.authorEmail ?? row.author ?? row.authorEmail;
5210
4757
  if (typeof author === "string") authors.add(author);
5211
4758
  }
5212
- const allStoryItems = tcApi?.list ? readAllStoryTrackChanges() : null;
4759
+ const value = [...authors];
4760
+ trackChangeAuthorsCache.set(items, value);
4761
+ return value;
4762
+ };
4763
+ const readAllStoryTrackChanges = () => {
4764
+ const token = contentToken();
4765
+ if (hasV2ReviewWindowFeed()) {
4766
+ const directory = asyncReads.get("trackChanges");
4767
+ const source = directory?.token === token && directory.hasSettled && Array.isArray(directory.value) ? directory.value : getV2ReviewWindowSnapshot()?.trackedChangeItems;
4768
+ if (!Array.isArray(source)) return null;
4769
+ return projectTrackChangesItems(source);
4770
+ }
4771
+ const { value, status } = readTrackChangesDirectory("trackChanges:all");
4772
+ return status === "ready" ? projectTrackChangesItems(value ?? []) : null;
4773
+ };
4774
+ const computeTrackChanges = (selection) => {
4775
+ const token = contentToken();
4776
+ const postDecisionIds = postDecisionTrackChangeIdsForToken(token);
4777
+ const reviewWindow = getV2ReviewWindowSnapshot();
4778
+ const hasWindowFeed = hasV2ReviewWindowFeed();
4779
+ const catalog = !hasWindowFeed ? readTrackChangesDirectory() : null;
4780
+ const windowItems = hasWindowFeed && Array.isArray(reviewWindow?.trackedChangeItems) ? reviewWindow.trackedChangeItems : null;
4781
+ const value = hasWindowFeed ? windowItems : catalog.value;
4782
+ const listStatus = hasWindowFeed ? reviewWindowSliceStatus(reviewWindow) : catalog.status;
4783
+ const items = filterPostDecisionTrackChanges(projectTrackChangesItems(value ?? []), postDecisionIds);
4784
+ const allStoryItems = readAllStoryTrackChanges();
5213
4785
  const active = explicitActiveChange;
5214
- if (active) {
5215
- if (active.story) {
5216
- if (allStoryItems && !allStoryItems.some((row) => entityRowMatchesRequest(row, active.id, active.story))) setExplicitActiveChange(null);
5217
- } else if (!items.some((item) => readEntityId(item) === active.id)) setExplicitActiveChange(null);
4786
+ if (active && !pendingTrackChangeRevealFocuses.has(active)) {
4787
+ const directory = asyncReads.get("trackChanges");
4788
+ const directoryItems = directory?.token === token && directory.hasSettled && Array.isArray(directory.value) ? filterPostDecisionTrackChanges(projectTrackChangesItems(directory.value), postDecisionIds) : null;
4789
+ const activeValidationItems = trackChangesDirectoryLeaseCount > 0 ? directoryItems : active.story ? allStoryItems : items;
4790
+ if (activeValidationItems && !activeValidationItems.some((row) => entityRowMatchesRequest(row, active.id, active.story))) setExplicitActiveChange(null);
5218
4791
  }
5219
4792
  const publicIdItems = allStoryItems ?? items;
5220
4793
  const selectionIdContext = buildStoryScopedTrackedChangeIdContext(publicIdItems, selectionStory(selection));
@@ -5235,7 +4808,37 @@ function createSuperDocUI(options) {
5235
4808
  items,
5236
4809
  total: items.length,
5237
4810
  activeId,
5238
- authors: [...authors]
4811
+ authors: trackChangeAuthors(items)
4812
+ };
4813
+ };
4814
+ const computeCommentsDirectorySnapshot = (selection, windowSnapshot) => {
4815
+ if (!hasV2ReviewWindowFeed()) return windowSnapshot;
4816
+ demandHeavyDocRead("comments");
4817
+ const directory = readCommentsDirectory();
4818
+ const items = directory.value ?? windowSnapshot.items;
4819
+ return {
4820
+ status: combineStatus(directory.status, selection.status),
4821
+ listStatus: directory.status,
4822
+ items,
4823
+ total: items.length,
4824
+ activeIds: selection.activeCommentIds,
4825
+ activeId: explicitActiveCommentId ?? selection.activeCommentIds[0] ?? null
4826
+ };
4827
+ };
4828
+ const computeTrackChangesDirectorySnapshot = (selection, windowSnapshot) => {
4829
+ if (!hasV2ReviewWindowFeed()) return windowSnapshot;
4830
+ demandHeavyDocRead("trackChanges");
4831
+ const directory = readTrackChangesDirectory();
4832
+ const postDecisionIds = postDecisionTrackChangeIdsForToken(contentToken());
4833
+ const items = filterPostDecisionTrackChanges(projectTrackChangesItems(directory.value ?? windowSnapshot.items), postDecisionIds);
4834
+ const explicitIdContext = explicitActiveChange?.story ? buildStoryScopedTrackedChangeIdContext(items, explicitActiveChange.story) : buildTrackedChangeIdContext(items);
4835
+ const activeId = explicitActiveChange ? explicitIdContext.toPublicId(explicitActiveChange.id) ?? explicitActiveChange.id : windowSnapshot.activeId;
4836
+ return {
4837
+ status: combineStatus(directory.status, selection.status),
4838
+ items,
4839
+ total: items.length,
4840
+ activeId,
4841
+ authors: trackChangeAuthors(items)
5239
4842
  };
5240
4843
  };
5241
4844
  const computeContentControls = (selection) => {
@@ -7168,54 +6771,19 @@ function createSuperDocUI(options) {
7168
6771
  }
7169
6772
  if (type === "mutation:committed" && event.reviewMutation) settleUiReviewMutation(event.reviewMutation.token);
7170
6773
  if (type === "source:complete" || type === "source:signals-complete") {
7171
- if (authoritativeTrackChangesPendingToken !== null) {
7172
- clearAuthoritativeTrackChangesRetry();
7173
- reviewMutationReconciler.onRender(true);
7174
- }
6774
+ sourceCompletionObservedToken = contentToken();
6775
+ refreshIncompleteTrackChangesDirectories();
7175
6776
  return;
7176
6777
  }
7177
6778
  if (type === "mutation:committed" || type === "collaboration:remote-changed") lastEditableMutationAtMs = Date.now();
7178
- if (type === "mutation:committed") {
7179
- clearAuthoritativeTrackChangesRetry();
7180
- reviewMutationToken = {};
7181
- }
7182
6779
  if (type === "mutation:committed" || type === "save:completed" || type === "collaboration:remote-changed") {
7183
6780
  const impact = type === "mutation:committed" ? getV2TrackedChangeMutationImpact(event) : null;
7184
- const supersedesPendingReviewReconcile = type === "mutation:committed" && reviewMutationReconciler.getPendingIds().size > 0;
7185
6781
  if (impact?.allResolved) {
7186
- reviewMutationReconciler.reset();
7187
6782
  replaceTrackedChangeItemsInCache([]);
7188
- authoritativeTrackChangesPendingToken = null;
7189
6783
  publishAllTrackedChangesResolved(event.receipt);
7190
6784
  return;
7191
6785
  }
7192
6786
  if (impact?.removedIds.size) markPostDecisionTrackChanges(new Set(impact.removedIds), event.receipt);
7193
- const authoritativeHistoryImpact = impact?.reconcileMode === "authoritative" && impact.upsertIds.size > 0;
7194
- if (authoritativeHistoryImpact) {
7195
- invalidateDocumentContent();
7196
- clearAuthoritativeTrackChangesRetry();
7197
- authoritativeTrackChangesPendingToken = contentToken();
7198
- recompute();
7199
- }
7200
- if (impact?.upsertIds.size || supersedesPendingReviewReconcile) {
7201
- const receipt = event.origin === "history" ? null : event.receipt;
7202
- const readiness = getEditor()?.documentMutationReadiness;
7203
- const canWaitForReceiptPaint = typeof receipt?.txId === "string" && receipt.txId.length > 0 && typeof readiness?.whenPainted === "function";
7204
- const historyRenderEpoch = event.origin === "history" && typeof readiness?.getRenderEpoch === "function" ? safeCall(() => readiness.getRenderEpoch.call(readiness), null) : null;
7205
- const canWaitForHistoryPaint = event.origin === "history" && typeof readiness?.whenPainted === "function";
7206
- const upsertImpact = {
7207
- upsertIds: impact?.upsertIds ?? /* @__PURE__ */ new Set(),
7208
- removedIds: /* @__PURE__ */ new Set(),
7209
- reconcileMode: impact?.reconcileMode === "authoritative" ? "authoritative" : "targeted"
7210
- };
7211
- if (canWaitForReceiptPaint) reviewMutationReconciler.enqueueAfterPaint(upsertImpact, () => readiness.whenPainted.call(readiness, receipt));
7212
- else if (canWaitForHistoryPaint) reviewMutationReconciler.enqueueAfterPaint(upsertImpact, () => readiness.whenPainted.call(readiness, typeof historyRenderEpoch === "number" ? { afterEpoch: historyRenderEpoch } : void 0));
7213
- else {
7214
- reviewMutationReconciler.enqueue(upsertImpact);
7215
- reviewMutationReconciler.onRender(supersedesPendingReviewReconcile);
7216
- }
7217
- }
7218
- if (authoritativeHistoryImpact) return;
7219
6787
  const isTypingBurst = type === "mutation:committed" && isEditableTextMutationEvent(event) || type === "collaboration:remote-changed" && Array.isArray(event.changedStoryIds) && event.changedStoryIds.length > 0;
7220
6788
  if (isTypingBurst) lastTypingMutationAtMs = Date.now();
7221
6789
  if (type === "mutation:committed" && isTypingBurst) {
@@ -7374,6 +6942,25 @@ function createSuperDocUI(options) {
7374
6942
  subscribe: (listener) => observe((snapshot) => listener({ snapshot }))
7375
6943
  };
7376
6944
  };
6945
+ const directorySnapshotHandle = (passiveSub, directorySub, family) => {
6946
+ const passive = snapshotHandle(passiveSub);
6947
+ const directory = snapshotHandle(directorySub);
6948
+ const observe = (listener) => {
6949
+ const release = acquireDirectoryLease(family);
6950
+ recompute();
6951
+ const unsubscribe = directory.observe(listener);
6952
+ return () => {
6953
+ unsubscribe();
6954
+ release();
6955
+ };
6956
+ };
6957
+ return {
6958
+ get: passive.get,
6959
+ getSnapshot: passive.getSnapshot,
6960
+ observe,
6961
+ subscribe: (listener) => observe((snapshot) => listener({ snapshot }))
6962
+ };
6963
+ };
7377
6964
  function readTrackDecisionTargetId(target) {
7378
6965
  return target && typeof target === "object" && typeof target.id === "string" ? String(target.id) : null;
7379
6966
  }
@@ -9178,7 +8765,7 @@ function createSuperDocUI(options) {
9178
8765
  const moveSide = namespace === "trackChanges" && loadedRecord?.type === "move" && (loadedRecord.subtype === "move-to" || loadedRecord.subtype === "move-from") ? loadedRecord.subtype : null;
9179
8766
  const trackedChangeLookupId = moveSide && typeof loadedRecord?.trackedChangeCanonicalId === "string" ? loadedRecord.trackedChangeCanonicalId : id;
9180
8767
  const readResolvedTarget = (row) => {
9181
- if (!moveSide) return readTarget(row);
8768
+ if (!moveSide) return namespace === "trackChanges" ? readTrackedChangeNavigationTarget(row, { preferStableBlock: true }) : readTarget(row);
9182
8769
  const moveTarget = readEntityTarget(row);
9183
8770
  if (moveTarget?.kind !== "move") return null;
9184
8771
  const sideTarget = moveSide === "move-to" ? moveTarget.destination : moveTarget.source;
@@ -9236,7 +8823,7 @@ function createSuperDocUI(options) {
9236
8823
  return filtered.slice(offset, end);
9237
8824
  };
9238
8825
  const commentsSub = sliceHandle((s) => s.comments);
9239
- const commentsSnap = snapshotHandle(commentsSub);
8826
+ const commentsSnap = directorySnapshotHandle(commentsSub, select((s) => computeCommentsDirectorySnapshot(s.selection, s.comments)), "comments");
9240
8827
  const comments = {
9241
8828
  get: commentsSnap.get,
9242
8829
  getSnapshot: commentsSnap.getSnapshot,
@@ -9244,12 +8831,8 @@ function createSuperDocUI(options) {
9244
8831
  observe: commentsSnap.observe,
9245
8832
  list: (query) => {
9246
8833
  ensureCommentsCatalog();
9247
- const snapshot = commentsSub.get();
9248
- if (snapshot.status !== "pending") return filterCommentsSnapshot(snapshot.items, query);
9249
- const commentsApi = getDoc()?.comments;
9250
- const result = safeCall(commentsApi?.list ? () => commentsApi.list(query) : void 0, null);
9251
- if (isPromiseLike(result)) return [];
9252
- return filterCommentsSnapshot(Array.isArray(result?.items) ? result.items : [], query);
8834
+ const directory = readCommentsDirectory();
8835
+ return filterCommentsSnapshot(directory.value ?? commentsSub.get().items, query);
9253
8836
  },
9254
8837
  getById: (commentId) => {
9255
8838
  const fromSnapshot = commentsSub.get().items.find((item) => readEntityId(item) === commentId) ?? null;
@@ -9335,10 +8918,13 @@ function createSuperDocUI(options) {
9335
8918
  }
9336
8919
  if (!getEditor()) return false;
9337
8920
  const snapshot = commentsSub.get();
9338
- if (snapshot.listStatus !== "ready") return false;
9339
- const resolvedId = resolveActiveCommentId(snapshot.items, commentId);
8921
+ const directory = asyncReads.get("comments");
8922
+ const directoryItems = directory?.token === contentToken() && directory.hasSettled && Array.isArray(directory.value) ? directory.value : null;
8923
+ const items = directoryItems ?? snapshot.items;
8924
+ if (!directoryItems && snapshot.listStatus !== "ready") return false;
8925
+ const resolvedId = resolveActiveCommentId(items, commentId);
9340
8926
  if (!resolvedId) return false;
9341
- if (!snapshot.items.some((entry) => readEntityId(entry) === resolvedId)) return false;
8927
+ if (!items.some((entry) => readEntityId(entry) === resolvedId)) return false;
9342
8928
  explicitActiveCommentId = resolvedId;
9343
8929
  recompute();
9344
8930
  return true;
@@ -9360,7 +8946,8 @@ function createSuperDocUI(options) {
9360
8946
  };
9361
8947
  const trackChangesSub = sliceHandle((s) => s.trackChanges);
9362
8948
  const navigateTrackChange = (step, options) => {
9363
- const allRows = trackChangesSub.get().items.filter((item) => readEntityId(item) !== null);
8949
+ ensureTrackChangesCatalog();
8950
+ const allRows = (readTrackChangesDirectory().value ?? trackChangesSub.get().items).map(projectTrackChangesItem).filter((item) => item != null && readEntityId(item) !== null);
9364
8951
  if (allRows.length === 0) return null;
9365
8952
  const currentId = state.trackChanges.activeId;
9366
8953
  const currentStory = explicitActiveChange?.id === currentId ? explicitActiveChange.story : void 0;
@@ -9449,7 +9036,7 @@ function createSuperDocUI(options) {
9449
9036
  trackChangeNavigationTail = run.then(() => void 0, () => void 0);
9450
9037
  return run;
9451
9038
  };
9452
- const trackChangesSnap = snapshotHandle(trackChangesSub);
9039
+ const trackChangesSnap = directorySnapshotHandle(trackChangesSub, select((s) => computeTrackChangesDirectorySnapshot(s.selection, s.trackChanges)), "trackChanges");
9453
9040
  const trackChanges = {
9454
9041
  get: trackChangesSnap.get,
9455
9042
  getSnapshot: trackChangesSnap.getSnapshot,
@@ -9457,7 +9044,12 @@ function createSuperDocUI(options) {
9457
9044
  observe: trackChangesSnap.observe,
9458
9045
  list: () => {
9459
9046
  ensureTrackChangesCatalog();
9460
- return trackChangesSub.get().items;
9047
+ const source = readTrackChangesDirectory().value ?? trackChangesSub.get().items;
9048
+ const suppressed = postDecisionTrackChangeIdsForToken(contentToken());
9049
+ return source.map(projectTrackChangesItem).filter((item) => item != null).filter((item) => {
9050
+ const id = readEntityId(item);
9051
+ return !id || !suppressed?.has(id);
9052
+ });
9461
9053
  },
9462
9054
  accept: (changeId) => executeTrackDecision("accept", changeId),
9463
9055
  reject: (changeId) => executeTrackDecision("reject", changeId),
@@ -9514,7 +9106,7 @@ function createSuperDocUI(options) {
9514
9106
  if (!allStoryItems.some((row) => entityRowMatchesRequest(row, publicId, requested.story))) return false;
9515
9107
  requested.id = publicId;
9516
9108
  } else {
9517
- const items = trackChangesSub.get().items;
9109
+ const items = readAllStoryTrackChanges() ?? trackChangesSub.get().items;
9518
9110
  const publicId = buildTrackedChangeIdContext(items).toPublicId(requested.id) ?? requested.id;
9519
9111
  const item = items.find((candidate) => readEntityId(candidate) === publicId);
9520
9112
  if (!item) return false;
@@ -10724,8 +10316,6 @@ function createSuperDocUI(options) {
10724
10316
  if (disposed) return;
10725
10317
  disposed = true;
10726
10318
  releaseSharedUiTrackedChangesCatalog(uiTrackedChangesCatalogHost, uiTrackedChangesCatalogState);
10727
- reviewMutationReconciler.reset();
10728
- clearAuthoritativeTrackChangesRetry();
10729
10319
  if (foregroundAsyncRetryTimer) {
10730
10320
  clearTimeout(foregroundAsyncRetryTimer);
10731
10321
  foregroundAsyncRetryTimer = null;
@@ -10751,7 +10341,12 @@ function createSuperDocUI(options) {
10751
10341
  if (detachSourceLoading) detachSourceLoading();
10752
10342
  detachSourceLoading = null;
10753
10343
  sourceLoadingSubscriptionHost = null;
10344
+ commentsDirectoryLeaseCount = 0;
10345
+ trackChangesDirectoryLeaseCount = 0;
10754
10346
  demandedHeavyReads.clear();
10347
+ incompleteTrackChangesDirectoryReadTokens.clear();
10348
+ postSourceCompletionRefreshTokens.clear();
10349
+ sourceCompletionObservedToken = null;
10755
10350
  coldAsyncReadDeferrals.clear();
10756
10351
  for (const detach of detachers.splice(0)) detach();
10757
10352
  if (detachHostSelection) detachHostSelection();
@@ -11010,4 +10605,4 @@ function commandIsActive(descriptor, selection) {
11010
10605
  return descriptor.activeMark ? selection.activeMarks.includes(descriptor.activeMark) : false;
11011
10606
  }
11012
10607
  //#endregion
11013
- export { getV2TrackedChangeMutationImpact as a, createV2ReviewMutationReconciler as i, shallowEqual as n, DOM_CLASS_NAMES as o, BUILT_IN_COMMAND_IDS as r, createSuperDocUI as t };
10608
+ export { DOM_CLASS_NAMES as a, getV2TrackedChangeMutationImpact as i, shallowEqual as n, BUILT_IN_COMMAND_IDS as r, createSuperDocUI as t };