@beyondwork/docx-react-component 1.0.41 → 1.0.43
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/package.json +38 -37
- package/src/api/awareness-identity-types.ts +35 -0
- package/src/api/comment-negotiation-types.ts +130 -0
- package/src/api/comment-presentation-types.ts +106 -0
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +541 -5
- package/src/api/scope-metadata-resolver-types.ts +88 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +601 -9
- package/src/core/search/search-text.ts +15 -2
- package/src/index.ts +131 -1
- package/src/io/docx-session.ts +672 -2
- package/src/io/export/escape-xml-attribute.ts +26 -0
- package/src/io/export/external-send.ts +188 -0
- package/src/io/export/serialize-comments.ts +13 -16
- package/src/io/export/serialize-footnotes.ts +17 -24
- package/src/io/export/serialize-headers-footers.ts +17 -24
- package/src/io/export/serialize-main-document.ts +59 -62
- package/src/io/export/serialize-numbering.ts +20 -27
- package/src/io/export/serialize-runtime-revisions.ts +2 -9
- package/src/io/export/serialize-tables.ts +8 -15
- package/src/io/export/table-properties-xml.ts +25 -32
- package/src/io/import/external-reimport.ts +40 -0
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +83 -0
- package/src/io/ooxml/bw-xml.ts +244 -0
- package/src/io/ooxml/canonicalize-payload.ts +301 -0
- package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
- package/src/io/ooxml/comment-presentation-payload.ts +311 -0
- package/src/io/ooxml/external-custody-payload.ts +102 -0
- package/src/io/ooxml/participants-payload.ts +97 -0
- package/src/io/ooxml/payload-signature.ts +112 -0
- package/src/io/ooxml/workflow-payload-validator.ts +367 -0
- package/src/io/ooxml/workflow-payload.ts +317 -7
- package/src/runtime/awareness-identity.ts +173 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab-session-bridge.ts +157 -0
- package/src/runtime/collab-session-facet.ts +193 -0
- package/src/runtime/collab-session.ts +273 -0
- package/src/runtime/comment-negotiation-sync.ts +91 -0
- package/src/runtime/comment-negotiation.ts +158 -0
- package/src/runtime/comment-presentation.ts +223 -0
- package/src/runtime/document-runtime.ts +639 -124
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +4 -0
- package/src/runtime/layout/layout-engine-instance.ts +139 -14
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +441 -48
- package/src/runtime/layout/public-facet.ts +585 -14
- package/src/runtime/layout/table-row-split.ts +316 -0
- package/src/runtime/markdown-sanitizer.ts +132 -0
- package/src/runtime/participants.ts +134 -0
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/resign-payload.ts +120 -0
- package/src/runtime/surface-projection.ts +10 -5
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +80 -16
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +654 -45
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +111 -11
- package/src/ui/editor-shell-view.tsx +21 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
- package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
- package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
- package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +106 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
- package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +167 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +10 -256
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +37 -1
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
- package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +455 -118
|
@@ -204,6 +204,30 @@ import {
|
|
|
204
204
|
incrementInvalidationCounter,
|
|
205
205
|
recordPerfSample,
|
|
206
206
|
} from "../ui-tailwind/editor-surface/perf-probe.ts";
|
|
207
|
+
import {
|
|
208
|
+
createLoadScheduler,
|
|
209
|
+
type LoadScheduler,
|
|
210
|
+
} from "../io/load-scheduler.ts";
|
|
211
|
+
import {
|
|
212
|
+
createEditorStateChannel,
|
|
213
|
+
type EditorStateChannel,
|
|
214
|
+
} from "./editor-state-channel.ts";
|
|
215
|
+
import { PerfCounters } from "./perf-counters.ts";
|
|
216
|
+
import type {
|
|
217
|
+
EditorStateNamespace,
|
|
218
|
+
EditorStatePolicy,
|
|
219
|
+
EditorStateResolver,
|
|
220
|
+
EditorStatePersister,
|
|
221
|
+
} from "../api/editor-state-types.ts";
|
|
222
|
+
import { collectEditorStateForSerialize } from "./editor-state-integration.ts";
|
|
223
|
+
import type { EditorStatePayload } from "../io/ooxml/workflow-payload.ts";
|
|
224
|
+
|
|
225
|
+
/** Internal extension of ExportDocxOptions that threads the collected
|
|
226
|
+
* editorState payload from the runtime to the docx serializer. */
|
|
227
|
+
interface InternalExportDocxOptions extends ExportDocxOptions {
|
|
228
|
+
/** @internal Schema 1.2 — collected by runtime before serialize. */
|
|
229
|
+
_editorState?: EditorStatePayload;
|
|
230
|
+
}
|
|
207
231
|
|
|
208
232
|
export type Unsubscribe = () => void;
|
|
209
233
|
|
|
@@ -319,6 +343,7 @@ export interface DocumentRuntime {
|
|
|
319
343
|
getSuggestionsSnapshot(): SuggestionsSnapshot;
|
|
320
344
|
setWorkflowOverlay(overlay: WorkflowOverlay): void;
|
|
321
345
|
clearWorkflowOverlay(): void;
|
|
346
|
+
getWorkflowOverlay(): WorkflowOverlay | null;
|
|
322
347
|
getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
|
|
323
348
|
getInteractionGuardSnapshot(): InteractionGuardSnapshot;
|
|
324
349
|
getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
|
|
@@ -340,6 +365,23 @@ export interface DocumentRuntime {
|
|
|
340
365
|
getRuntimeContextAnalytics(
|
|
341
366
|
query?: RuntimeContextAnalyticsQuery,
|
|
342
367
|
): RuntimeContextAnalyticsSnapshot | null;
|
|
368
|
+
// Schema 1.2 — EditorStateChannel delegation
|
|
369
|
+
configureEditorStatePolicy(policy: EditorStatePolicy): void;
|
|
370
|
+
registerEditorStateResolver(resolver: EditorStateResolver | null): void;
|
|
371
|
+
registerEditorStatePersister(persister: EditorStatePersister | null): void;
|
|
372
|
+
getEditorStateKey(namespace: EditorStateNamespace): string | undefined;
|
|
373
|
+
retryPendingPersist(namespace?: EditorStateNamespace): Promise<void>;
|
|
374
|
+
/** Internal: exposes the channel for load-path hydration in editor-runtime-boundary. */
|
|
375
|
+
readonly editorStateChannel: EditorStateChannel;
|
|
376
|
+
/**
|
|
377
|
+
* L7 render-perf instrumentation. Returns a string→number map of internal
|
|
378
|
+
* perf counters incremented during runtime activity. Used by perf benches
|
|
379
|
+
* and tests to assert that a given facet rebuild ran (or did not run) for
|
|
380
|
+
* a given operation. Counter names are not part of the public API contract
|
|
381
|
+
* — they may be added or renamed across L7 phases.
|
|
382
|
+
*/
|
|
383
|
+
getPerfCountersSnapshot(): Record<string, number>;
|
|
384
|
+
resetPerfCounters(): void;
|
|
343
385
|
}
|
|
344
386
|
|
|
345
387
|
export interface CommandAppliedMeta {
|
|
@@ -385,6 +427,16 @@ export interface CreateDocumentRuntimeOptions {
|
|
|
385
427
|
) => void;
|
|
386
428
|
initialViewState?: Partial<ViewState>;
|
|
387
429
|
protectionSnapshot?: ProtectionSnapshot;
|
|
430
|
+
/**
|
|
431
|
+
* Optional main-thread time-slicing scheduler. When provided, the runtime
|
|
432
|
+
* schedules post-`ready` idle work (e.g., sub-part hydration, compatibility
|
|
433
|
+
* report — reserved for future fastload passes) through it. When omitted,
|
|
434
|
+
* the runtime falls back to the sync scheduler — behavior is byte-identical
|
|
435
|
+
* to the pre-fastload runtime.
|
|
436
|
+
*
|
|
437
|
+
* See `docs/plans/fastload.md` and `src/io/load-scheduler.ts`.
|
|
438
|
+
*/
|
|
439
|
+
loadScheduler?: LoadScheduler;
|
|
388
440
|
}
|
|
389
441
|
|
|
390
442
|
interface HistoryState {
|
|
@@ -401,6 +453,29 @@ export function createDocumentRuntime(
|
|
|
401
453
|
const sessionId = createSessionId(options.documentId, clock());
|
|
402
454
|
const listeners = new Set<() => void>();
|
|
403
455
|
const eventListeners = new Set<(event: DocumentRuntimeEvent) => void>();
|
|
456
|
+
// Fastload P2: optional time-slicing scheduler. When caller does not pass
|
|
457
|
+
// one, use the sync backend so the runtime keeps its pre-fastload behavior
|
|
458
|
+
// end-to-end (no deferred work, idle tasks run inline). Future fastload
|
|
459
|
+
// passes (P6 async loader) will consume `scheduler.scheduleIdle` for
|
|
460
|
+
// post-ready work.
|
|
461
|
+
const loadScheduler: LoadScheduler =
|
|
462
|
+
options.loadScheduler ?? createLoadScheduler({ backendOverride: "sync" });
|
|
463
|
+
|
|
464
|
+
// L7 Phase 0 — perf counters. Each `refreshRenderSnapshot` call increments
|
|
465
|
+
// `refresh.all`. Phase 1 will add per-facet `facet.<name>.build` counters
|
|
466
|
+
// wired to the per-facet cached builders. Cost is one Map.set per call.
|
|
467
|
+
const perfCounters = new PerfCounters();
|
|
468
|
+
|
|
469
|
+
// L7 Phase 1 — context-analytics emit coalescing flag. Multiple events that
|
|
470
|
+
// each trigger context-analytics within one synchronous call stack collapse
|
|
471
|
+
// to a single microtask-deferred emit. Declared early because emit() (which
|
|
472
|
+
// checks this flag) runs during construction.
|
|
473
|
+
let analyticsEmitScheduled = false;
|
|
474
|
+
|
|
475
|
+
// Schema 1.2 — editor-state channel (policy, resolver/persister, debounce queues).
|
|
476
|
+
// Instantiated once per runtime; forwarded to the public interface.
|
|
477
|
+
const editorStateChannel = createEditorStateChannel();
|
|
478
|
+
|
|
404
479
|
const history: HistoryState = {
|
|
405
480
|
past: [],
|
|
406
481
|
future: [],
|
|
@@ -479,6 +554,7 @@ export function createDocumentRuntime(
|
|
|
479
554
|
zoomLevel: viewState.zoomLevel,
|
|
480
555
|
},
|
|
481
556
|
}),
|
|
557
|
+
canonicalDocument: () => state.document,
|
|
482
558
|
renderKernel: () => renderKernelRef,
|
|
483
559
|
getWorkflowRailInput: () => {
|
|
484
560
|
if (!workflowOverlay) return null;
|
|
@@ -543,6 +619,21 @@ export function createDocumentRuntime(
|
|
|
543
619
|
snapshot: SuggestionsSnapshot;
|
|
544
620
|
}
|
|
545
621
|
| undefined;
|
|
622
|
+
// L7 Phase 1 — review-work snapshot cache. Independent of selection.
|
|
623
|
+
// Inputs that bust it: workflowMarkup, comments, trackedChanges,
|
|
624
|
+
// document, navigation references. Per-keystroke profiling on the
|
|
625
|
+
// 40-page CCEP fixture showed createReviewWorkSnapshot at 211ms/call
|
|
626
|
+
// with no cache, dominated by per-item createDocumentLocationSnapshot.
|
|
627
|
+
let cachedReviewWork:
|
|
628
|
+
| {
|
|
629
|
+
comments: CommentSidebarSnapshot;
|
|
630
|
+
trackedChanges: TrackedChangesSnapshot;
|
|
631
|
+
workflowMarkup: WorkflowMarkupSnapshot;
|
|
632
|
+
document: CanonicalDocumentEnvelope;
|
|
633
|
+
navigation: DocumentNavigationSnapshot;
|
|
634
|
+
snapshot: ReviewWorkSnapshot;
|
|
635
|
+
}
|
|
636
|
+
| undefined;
|
|
546
637
|
let cachedPageLayout:
|
|
547
638
|
| {
|
|
548
639
|
revisionToken: string;
|
|
@@ -605,7 +696,9 @@ export function createDocumentRuntime(
|
|
|
605
696
|
{
|
|
606
697
|
revisionToken: string;
|
|
607
698
|
activeStoryKey: string;
|
|
608
|
-
|
|
699
|
+
// L7 Phase 1.6: null for non-selection queries (doc/scope/work_item)
|
|
700
|
+
// so selection movement doesn't invalidate these scope-stable analytics.
|
|
701
|
+
selection: EditorState["selection"] | null;
|
|
609
702
|
readOnly: boolean;
|
|
610
703
|
documentMode: DocumentMode;
|
|
611
704
|
workflowOverlay: WorkflowOverlay | null;
|
|
@@ -1365,6 +1458,10 @@ export function createDocumentRuntime(
|
|
|
1365
1458
|
protectionSnapshot,
|
|
1366
1459
|
preservation: state.document.preservation,
|
|
1367
1460
|
workflowMetadataSnapshot: deriveWorkflowMetadataSnapshot(),
|
|
1461
|
+
perfStage: (name, durationMs) => {
|
|
1462
|
+
perfCounters.increment(`wfMarkup.${name}.us`, Math.round(durationMs * 1000));
|
|
1463
|
+
perfCounters.increment(`wfMarkup.${name}.calls`);
|
|
1464
|
+
},
|
|
1368
1465
|
});
|
|
1369
1466
|
cachedWorkflowMarkupSnapshot = {
|
|
1370
1467
|
revisionToken: state.revisionToken,
|
|
@@ -1385,11 +1482,21 @@ export function createDocumentRuntime(
|
|
|
1385
1482
|
const activeStoryKey = storyTargetKey(activeStory);
|
|
1386
1483
|
const queryKey = getRuntimeContextAnalyticsQueryKey(query);
|
|
1387
1484
|
const cachedEntry = cachedContextAnalyticsSnapshots.get(queryKey);
|
|
1485
|
+
// L7 Phase 1.6 — only `scopeKind: "selection"` queries actually read
|
|
1486
|
+
// selection-anchored data (buildSelectionAnalytics filters markup items
|
|
1487
|
+
// that intersect the selection anchor). For document / workflow_scope /
|
|
1488
|
+
// work_item queries the selection is not an input, so changing it
|
|
1489
|
+
// shouldn't invalidate the cache. When selection moves within the same
|
|
1490
|
+
// workflow scope without text editing, document/scope/work_item analytics
|
|
1491
|
+
// hit; cursor-navigation bench drops accordingly.
|
|
1492
|
+
const effectiveScopeKind = query?.scopeKind ?? "selection";
|
|
1493
|
+
const selectionCacheKey =
|
|
1494
|
+
effectiveScopeKind === "selection" ? state.selection : null;
|
|
1388
1495
|
if (
|
|
1389
1496
|
cachedEntry &&
|
|
1390
1497
|
cachedEntry.revisionToken === state.revisionToken &&
|
|
1391
1498
|
cachedEntry.activeStoryKey === activeStoryKey &&
|
|
1392
|
-
cachedEntry.selection ===
|
|
1499
|
+
cachedEntry.selection === selectionCacheKey &&
|
|
1393
1500
|
cachedEntry.readOnly === state.readOnly &&
|
|
1394
1501
|
cachedEntry.documentMode === viewState.documentMode &&
|
|
1395
1502
|
cachedEntry.workflowOverlay === workflowOverlay &&
|
|
@@ -1400,28 +1507,76 @@ export function createDocumentRuntime(
|
|
|
1400
1507
|
return cachedEntry.snapshot;
|
|
1401
1508
|
}
|
|
1402
1509
|
|
|
1510
|
+
const tWf = performance.now();
|
|
1511
|
+
const wfScope = getCachedWorkflowScopeSnapshot();
|
|
1512
|
+
const wfGuard = getCachedInteractionGuardSnapshot();
|
|
1513
|
+
const wfMarkup = getCachedWorkflowMarkupSnapshot();
|
|
1514
|
+
perfCounters.increment("ctxa.workflow.us", Math.round((performance.now() - tWf) * 1000));
|
|
1515
|
+
|
|
1516
|
+
const tSugg = performance.now();
|
|
1517
|
+
const suggestions = getCachedSuggestionsSnapshot(state);
|
|
1518
|
+
perfCounters.increment("ctxa.suggestions.us", Math.round((performance.now() - tSugg) * 1000));
|
|
1519
|
+
|
|
1520
|
+
const tNav = performance.now();
|
|
1521
|
+
const navigation = getCachedDocumentNavigationSnapshot(state, activeStory);
|
|
1522
|
+
perfCounters.increment("ctxa.navigation.us", Math.round((performance.now() - tNav) * 1000));
|
|
1523
|
+
|
|
1524
|
+
const tReview = performance.now();
|
|
1525
|
+
let reviewWork: ReviewWorkSnapshot;
|
|
1526
|
+
if (
|
|
1527
|
+
cachedReviewWork &&
|
|
1528
|
+
cachedReviewWork.comments === cachedRenderSnapshot.comments &&
|
|
1529
|
+
cachedReviewWork.trackedChanges === cachedRenderSnapshot.trackedChanges &&
|
|
1530
|
+
cachedReviewWork.workflowMarkup === wfMarkup &&
|
|
1531
|
+
cachedReviewWork.document === state.document &&
|
|
1532
|
+
cachedReviewWork.navigation === navigation
|
|
1533
|
+
) {
|
|
1534
|
+
reviewWork = cachedReviewWork.snapshot;
|
|
1535
|
+
perfCounters.increment("ctxa.reviewWork.cacheHit");
|
|
1536
|
+
} else {
|
|
1537
|
+
reviewWork = createReviewWorkSnapshot({
|
|
1538
|
+
comments: cachedRenderSnapshot.comments,
|
|
1539
|
+
trackedChanges: cachedRenderSnapshot.trackedChanges,
|
|
1540
|
+
workflowMarkup: wfMarkup,
|
|
1541
|
+
document: state.document,
|
|
1542
|
+
navigation,
|
|
1543
|
+
});
|
|
1544
|
+
cachedReviewWork = {
|
|
1545
|
+
comments: cachedRenderSnapshot.comments,
|
|
1546
|
+
trackedChanges: cachedRenderSnapshot.trackedChanges,
|
|
1547
|
+
workflowMarkup: wfMarkup,
|
|
1548
|
+
document: state.document,
|
|
1549
|
+
navigation,
|
|
1550
|
+
snapshot: reviewWork,
|
|
1551
|
+
};
|
|
1552
|
+
perfCounters.increment("ctxa.reviewWork.cacheMiss");
|
|
1553
|
+
}
|
|
1554
|
+
perfCounters.increment("ctxa.reviewWork.us", Math.round((performance.now() - tReview) * 1000));
|
|
1555
|
+
|
|
1556
|
+
const tCompat = performance.now();
|
|
1557
|
+
const compat = toPublicCompatibilityReport(createDerivedCompatibility(state));
|
|
1558
|
+
perfCounters.increment("ctxa.compat.us", Math.round((performance.now() - tCompat) * 1000));
|
|
1559
|
+
|
|
1560
|
+
const tBuild = performance.now();
|
|
1403
1561
|
const snapshot = createRuntimeContextAnalyticsSnapshot({
|
|
1404
1562
|
query,
|
|
1405
1563
|
renderSnapshot: cachedRenderSnapshot,
|
|
1406
1564
|
workflowOverlay,
|
|
1407
|
-
workflowScopeSnapshot:
|
|
1408
|
-
interactionGuardSnapshot:
|
|
1409
|
-
workflowMarkupSnapshot:
|
|
1410
|
-
suggestionsSnapshot:
|
|
1411
|
-
reviewWorkSnapshot:
|
|
1412
|
-
comments: cachedRenderSnapshot.comments,
|
|
1413
|
-
trackedChanges: cachedRenderSnapshot.trackedChanges,
|
|
1414
|
-
workflowMarkup: getCachedWorkflowMarkupSnapshot(),
|
|
1415
|
-
document: state.document,
|
|
1416
|
-
navigation: getCachedDocumentNavigationSnapshot(state, activeStory),
|
|
1417
|
-
}),
|
|
1565
|
+
workflowScopeSnapshot: wfScope,
|
|
1566
|
+
interactionGuardSnapshot: wfGuard,
|
|
1567
|
+
workflowMarkupSnapshot: wfMarkup,
|
|
1568
|
+
suggestionsSnapshot: suggestions,
|
|
1569
|
+
reviewWorkSnapshot: reviewWork,
|
|
1418
1570
|
warnings: state.warnings.map(toPublicWarning),
|
|
1419
|
-
compatibility:
|
|
1571
|
+
compatibility: compat,
|
|
1420
1572
|
});
|
|
1573
|
+
perfCounters.increment("ctxa.build.us", Math.round((performance.now() - tBuild) * 1000));
|
|
1421
1574
|
cachedContextAnalyticsSnapshots.set(queryKey, {
|
|
1422
1575
|
revisionToken: state.revisionToken,
|
|
1423
1576
|
activeStoryKey,
|
|
1424
|
-
|
|
1577
|
+
// L7 Phase 1.6: store the selectionCacheKey we tested against, so the
|
|
1578
|
+
// same scopeKind invariant applies to future reads.
|
|
1579
|
+
selection: selectionCacheKey,
|
|
1425
1580
|
readOnly: state.readOnly,
|
|
1426
1581
|
documentMode: viewState.documentMode,
|
|
1427
1582
|
workflowOverlay,
|
|
@@ -1508,8 +1663,17 @@ export function createDocumentRuntime(
|
|
|
1508
1663
|
return true;
|
|
1509
1664
|
}
|
|
1510
1665
|
|
|
1666
|
+
function timeFacet<T>(name: string, fn: () => T): T {
|
|
1667
|
+
const t0 = performance.now();
|
|
1668
|
+
const result = fn();
|
|
1669
|
+
perfCounters.increment(`facet.${name}.us`, Math.round((performance.now() - t0) * 1000));
|
|
1670
|
+
perfCounters.increment(`facet.${name}.calls`);
|
|
1671
|
+
return result;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1511
1674
|
function refreshRenderSnapshot(): RuntimeRenderSnapshot {
|
|
1512
|
-
|
|
1675
|
+
perfCounters.increment("refresh.all");
|
|
1676
|
+
const surface = timeFacet("surface", () => getCachedSurface(state.document, activeStory));
|
|
1513
1677
|
return {
|
|
1514
1678
|
documentId: state.documentId,
|
|
1515
1679
|
sessionId: state.sessionId,
|
|
@@ -1521,11 +1685,11 @@ export function createDocumentRuntime(
|
|
|
1521
1685
|
documentMode: viewState.documentMode,
|
|
1522
1686
|
selection: toPublicSelectionSnapshot(state.selection, activeStory),
|
|
1523
1687
|
activeStory,
|
|
1524
|
-
pageLayout: getCachedPageLayoutSnapshot(state, activeStory) ?? undefined,
|
|
1688
|
+
pageLayout: timeFacet("layout", () => getCachedPageLayoutSnapshot(state, activeStory)) ?? undefined,
|
|
1525
1689
|
documentStats: toPublicDocumentStats(state),
|
|
1526
|
-
comments: getCachedCommentSidebarSnapshot(state),
|
|
1527
|
-
trackedChanges: getCachedTrackedChangesSnapshot(state, surface),
|
|
1528
|
-
compatibility: getCachedCompatibilityReport(state),
|
|
1690
|
+
comments: timeFacet("comments", () => getCachedCommentSidebarSnapshot(state)),
|
|
1691
|
+
trackedChanges: timeFacet("trackedChanges", () => getCachedTrackedChangesSnapshot(state, surface)),
|
|
1692
|
+
compatibility: timeFacet("compatibility", () => getCachedCompatibilityReport(state)),
|
|
1529
1693
|
warnings: state.warnings.map((warning) => toPublicWarning(warning)),
|
|
1530
1694
|
fatalError: state.fatalError ? toPublicError(state.fatalError) : undefined,
|
|
1531
1695
|
commandState: {
|
|
@@ -1603,6 +1767,40 @@ export function createDocumentRuntime(
|
|
|
1603
1767
|
});
|
|
1604
1768
|
}
|
|
1605
1769
|
|
|
1770
|
+
// Schema 1.2: forward channel events to the runtime event bus.
|
|
1771
|
+
editorStateChannel.addListener((event) => {
|
|
1772
|
+
switch (event.kind) {
|
|
1773
|
+
case "load_failed":
|
|
1774
|
+
emit({
|
|
1775
|
+
type: "editor_state_part_load_failed",
|
|
1776
|
+
documentId: state.documentId,
|
|
1777
|
+
failure: event.failure,
|
|
1778
|
+
});
|
|
1779
|
+
break;
|
|
1780
|
+
case "persist_failed":
|
|
1781
|
+
emit({
|
|
1782
|
+
type: "editor_state_part_persist_failed",
|
|
1783
|
+
documentId: state.documentId,
|
|
1784
|
+
failure: event.failure,
|
|
1785
|
+
});
|
|
1786
|
+
break;
|
|
1787
|
+
case "policy_migrated":
|
|
1788
|
+
emit({
|
|
1789
|
+
type: "editor_state_policy_migrated",
|
|
1790
|
+
documentId: state.documentId,
|
|
1791
|
+
migration: event.migration,
|
|
1792
|
+
});
|
|
1793
|
+
break;
|
|
1794
|
+
case "unknown_namespace":
|
|
1795
|
+
emit({
|
|
1796
|
+
type: "editor_state_unknown_namespace",
|
|
1797
|
+
documentId: state.documentId,
|
|
1798
|
+
namespace: event.namespace,
|
|
1799
|
+
});
|
|
1800
|
+
break;
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1606
1804
|
return {
|
|
1607
1805
|
subscribe(listener) {
|
|
1608
1806
|
listeners.add(listener);
|
|
@@ -1662,11 +1860,34 @@ export function createDocumentRuntime(
|
|
|
1662
1860
|
applyHistory("redo");
|
|
1663
1861
|
return;
|
|
1664
1862
|
}
|
|
1863
|
+
|
|
1864
|
+
if (isRuntimeStateOverlayCommand(command)) {
|
|
1865
|
+
applyRuntimeStateOverlayCommand(command);
|
|
1866
|
+
const context = {
|
|
1867
|
+
timestamp: normalizeCommandTimestamp(command.origin?.timestamp) ?? clock(),
|
|
1868
|
+
documentMode: getEffectiveDocumentMode(commandSelection),
|
|
1869
|
+
defaultAuthorId: defaultAuthorId ?? undefined,
|
|
1870
|
+
} as const;
|
|
1871
|
+
const noopTransaction: EditorTransaction = {
|
|
1872
|
+
nextState: state,
|
|
1873
|
+
mapping: createEmptyMapping(),
|
|
1874
|
+
effects: { warningsAdded: [], warningsCleared: [] },
|
|
1875
|
+
historyBoundary: "skip",
|
|
1876
|
+
markDirty: false,
|
|
1877
|
+
};
|
|
1878
|
+
options.onCommandApplied?.(command, noopTransaction, context, {
|
|
1879
|
+
preSelection: state.selection,
|
|
1880
|
+
activeStory,
|
|
1881
|
+
});
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1665
1885
|
try {
|
|
1666
1886
|
const context = {
|
|
1667
1887
|
timestamp: normalizeCommandTimestamp(command.origin?.timestamp) ?? clock(),
|
|
1668
1888
|
documentMode: getEffectiveDocumentMode(commandSelection),
|
|
1669
1889
|
defaultAuthorId: defaultAuthorId ?? undefined,
|
|
1890
|
+
renderSnapshot: cachedRenderSnapshot,
|
|
1670
1891
|
} as const;
|
|
1671
1892
|
const preSelection = commandSelection;
|
|
1672
1893
|
const preActiveStory = activeStory;
|
|
@@ -1685,6 +1906,10 @@ export function createDocumentRuntime(
|
|
|
1685
1906
|
if (command.type === "history.undo" || command.type === "history.redo") {
|
|
1686
1907
|
return;
|
|
1687
1908
|
}
|
|
1909
|
+
if (isRuntimeStateOverlayCommand(command)) {
|
|
1910
|
+
applyRuntimeStateOverlayCommand(command);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1688
1913
|
if (meta?.activeStory && !storyTargetsEqual(meta.activeStory, activeStory)) {
|
|
1689
1914
|
activeStory = meta.activeStory;
|
|
1690
1915
|
storySelections.set(
|
|
@@ -1695,7 +1920,17 @@ export function createDocumentRuntime(
|
|
|
1695
1920
|
const replayState = meta?.preSelection
|
|
1696
1921
|
? { ...state, selection: meta.preSelection }
|
|
1697
1922
|
: state;
|
|
1698
|
-
const
|
|
1923
|
+
const replaySnapshot = meta?.preSelection
|
|
1924
|
+
? {
|
|
1925
|
+
...cachedRenderSnapshot,
|
|
1926
|
+
selection: toPublicSelectionSnapshot(meta.preSelection, activeStory),
|
|
1927
|
+
}
|
|
1928
|
+
: cachedRenderSnapshot;
|
|
1929
|
+
const replayContext = {
|
|
1930
|
+
...context,
|
|
1931
|
+
renderSnapshot: replaySnapshot,
|
|
1932
|
+
};
|
|
1933
|
+
const transaction = executeEditorCommand(replayState, command, replayContext);
|
|
1699
1934
|
commitRemote(transaction);
|
|
1700
1935
|
} catch (error) {
|
|
1701
1936
|
emitError(toRuntimeError(error));
|
|
@@ -2156,6 +2391,9 @@ export function createDocumentRuntime(
|
|
|
2156
2391
|
state.selection.head,
|
|
2157
2392
|
activeStory,
|
|
2158
2393
|
options,
|
|
2394
|
+
// P5 — TOC entries print Word's display number (honors page-
|
|
2395
|
+
// number restarts), not the raw 0-based pageIndex+1.
|
|
2396
|
+
(pageIndex: number) => layoutFacet.getDisplayPageNumber(pageIndex),
|
|
2159
2397
|
);
|
|
2160
2398
|
if (refreshed.changed) {
|
|
2161
2399
|
this.dispatch({
|
|
@@ -2213,7 +2451,37 @@ export function createDocumentRuntime(
|
|
|
2213
2451
|
throw toStructuredRuntimeException(error);
|
|
2214
2452
|
}
|
|
2215
2453
|
|
|
2216
|
-
|
|
2454
|
+
// Schema 1.2: collect editor-state payload (flushes pending persists).
|
|
2455
|
+
const collectedEditorState = await collectEditorStateForSerialize({
|
|
2456
|
+
channel: editorStateChannel,
|
|
2457
|
+
getNamespaceData: (ns) => {
|
|
2458
|
+
switch (ns) {
|
|
2459
|
+
case "hostAnnotations": {
|
|
2460
|
+
const snap = deriveHostAnnotationSnapshot();
|
|
2461
|
+
if (!snap || snap.annotations.length === 0) return null;
|
|
2462
|
+
return { schemaVersion: "host-annotation-overlay/1", data: snap };
|
|
2463
|
+
}
|
|
2464
|
+
case "workflowOverlay": {
|
|
2465
|
+
const ov = workflowOverlay;
|
|
2466
|
+
if (!ov) return null;
|
|
2467
|
+
return { schemaVersion: "workflow-overlay/1", data: ov };
|
|
2468
|
+
}
|
|
2469
|
+
case "workflowMetadata": {
|
|
2470
|
+
const meta = deriveWorkflowMetadataSnapshot();
|
|
2471
|
+
if (!meta || (meta.definitions.length === 0 && meta.entries.length === 0)) return null;
|
|
2472
|
+
return { schemaVersion: "workflow-metadata/1", data: meta };
|
|
2473
|
+
}
|
|
2474
|
+
case "workItems":
|
|
2475
|
+
return null;
|
|
2476
|
+
}
|
|
2477
|
+
},
|
|
2478
|
+
});
|
|
2479
|
+
const internalOptions: InternalExportDocxOptions = {
|
|
2480
|
+
...exportOptions,
|
|
2481
|
+
_editorState: collectedEditorState,
|
|
2482
|
+
};
|
|
2483
|
+
|
|
2484
|
+
const result = await options.exportDocx(this.getSessionState(), internalOptions);
|
|
2217
2485
|
|
|
2218
2486
|
emit({
|
|
2219
2487
|
type: "export_completed",
|
|
@@ -2224,47 +2492,25 @@ export function createDocumentRuntime(
|
|
|
2224
2492
|
return result;
|
|
2225
2493
|
},
|
|
2226
2494
|
setWorkflowOverlay(overlay) {
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2495
|
+
this.dispatch({
|
|
2496
|
+
type: "workflow.set-overlay",
|
|
2497
|
+
overlay,
|
|
2498
|
+
origin: createOrigin("api", clock()),
|
|
2499
|
+
});
|
|
2500
|
+
editorStateChannel.recordMutation("workflowOverlay", {
|
|
2501
|
+
namespace: "workflowOverlay",
|
|
2502
|
+
schemaVersion: "workflow-overlay/1",
|
|
2503
|
+
data: overlay,
|
|
2234
2504
|
});
|
|
2235
|
-
if (workflowOverlay.activeWorkItemId !== undefined) {
|
|
2236
|
-
emit({
|
|
2237
|
-
type: "workflow_active_work_item_changed",
|
|
2238
|
-
documentId: state.documentId,
|
|
2239
|
-
activeWorkItemId: workflowOverlay.activeWorkItemId ?? null,
|
|
2240
|
-
});
|
|
2241
|
-
}
|
|
2242
|
-
for (const listener of listeners) {
|
|
2243
|
-
listener();
|
|
2244
|
-
}
|
|
2245
2505
|
},
|
|
2246
2506
|
clearWorkflowOverlay() {
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
type: "workflow_active_work_item_changed",
|
|
2251
|
-
documentId: state.documentId,
|
|
2252
|
-
activeWorkItemId: null,
|
|
2253
|
-
});
|
|
2254
|
-
emit({
|
|
2255
|
-
type: "workflow_overlay_changed",
|
|
2256
|
-
documentId: state.documentId,
|
|
2257
|
-
snapshot: {
|
|
2258
|
-
overlayPresent: false,
|
|
2259
|
-
activeWorkItemId: null,
|
|
2260
|
-
scopes: [],
|
|
2261
|
-
candidates: [],
|
|
2262
|
-
blockedReasons: [],
|
|
2263
|
-
},
|
|
2507
|
+
this.dispatch({
|
|
2508
|
+
type: "workflow.clear-overlay",
|
|
2509
|
+
origin: createOrigin("api", clock()),
|
|
2264
2510
|
});
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2511
|
+
},
|
|
2512
|
+
getWorkflowOverlay() {
|
|
2513
|
+
return workflowOverlay;
|
|
2268
2514
|
},
|
|
2269
2515
|
getWorkflowScopeSnapshot() {
|
|
2270
2516
|
return getCachedWorkflowScopeSnapshot();
|
|
@@ -2276,77 +2522,56 @@ export function createDocumentRuntime(
|
|
|
2276
2522
|
return getCachedWorkflowMarkupSnapshot();
|
|
2277
2523
|
},
|
|
2278
2524
|
setWorkflowMetadataDefinitions(definitions) {
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
documentId: state.documentId,
|
|
2284
|
-
snapshot,
|
|
2525
|
+
this.dispatch({
|
|
2526
|
+
type: "workflow.set-metadata-definitions",
|
|
2527
|
+
definitions,
|
|
2528
|
+
origin: createOrigin("api", clock()),
|
|
2285
2529
|
});
|
|
2286
|
-
for (const listener of listeners) {
|
|
2287
|
-
listener();
|
|
2288
|
-
}
|
|
2289
2530
|
},
|
|
2290
2531
|
clearWorkflowMetadataDefinitions() {
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
type: "workflow_metadata_changed",
|
|
2295
|
-
documentId: state.documentId,
|
|
2296
|
-
snapshot,
|
|
2532
|
+
this.dispatch({
|
|
2533
|
+
type: "workflow.clear-metadata-definitions",
|
|
2534
|
+
origin: createOrigin("api", clock()),
|
|
2297
2535
|
});
|
|
2298
|
-
for (const listener of listeners) {
|
|
2299
|
-
listener();
|
|
2300
|
-
}
|
|
2301
2536
|
},
|
|
2302
2537
|
setWorkflowMetadataEntries(entries) {
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2538
|
+
this.dispatch({
|
|
2539
|
+
type: "workflow.set-metadata-entries",
|
|
2540
|
+
entries,
|
|
2541
|
+
origin: createOrigin("api", clock()),
|
|
2542
|
+
});
|
|
2543
|
+
editorStateChannel.recordMutation("workflowMetadata", {
|
|
2544
|
+
namespace: "workflowMetadata",
|
|
2545
|
+
schemaVersion: "workflow-metadata/1",
|
|
2546
|
+
data: entries,
|
|
2309
2547
|
});
|
|
2310
|
-
for (const listener of listeners) {
|
|
2311
|
-
listener();
|
|
2312
|
-
}
|
|
2313
2548
|
},
|
|
2314
2549
|
clearWorkflowMetadataEntries() {
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
type: "workflow_metadata_changed",
|
|
2319
|
-
documentId: state.documentId,
|
|
2320
|
-
snapshot,
|
|
2550
|
+
this.dispatch({
|
|
2551
|
+
type: "workflow.clear-metadata-entries",
|
|
2552
|
+
origin: createOrigin("api", clock()),
|
|
2321
2553
|
});
|
|
2322
|
-
for (const listener of listeners) {
|
|
2323
|
-
listener();
|
|
2324
|
-
}
|
|
2325
2554
|
},
|
|
2326
2555
|
getWorkflowMetadataSnapshot() {
|
|
2327
2556
|
return deriveWorkflowMetadataSnapshot();
|
|
2328
2557
|
},
|
|
2329
2558
|
setHostAnnotationOverlay(overlay) {
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2559
|
+
this.dispatch({
|
|
2560
|
+
type: "host-annotation.set-overlay",
|
|
2561
|
+
overlay,
|
|
2562
|
+
origin: createOrigin("api", clock()),
|
|
2563
|
+
});
|
|
2564
|
+
editorStateChannel.recordMutation("hostAnnotations", {
|
|
2565
|
+
namespace: "hostAnnotations",
|
|
2566
|
+
schemaVersion: "host-annotation-overlay/1",
|
|
2567
|
+
data: overlay,
|
|
2335
2568
|
});
|
|
2336
|
-
for (const listener of listeners) {
|
|
2337
|
-
listener();
|
|
2338
|
-
}
|
|
2339
2569
|
},
|
|
2340
2570
|
clearHostAnnotationOverlay() {
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
documentId: state.documentId,
|
|
2345
|
-
snapshot: deriveHostAnnotationSnapshot(),
|
|
2571
|
+
this.dispatch({
|
|
2572
|
+
type: "host-annotation.clear-overlay",
|
|
2573
|
+
origin: createOrigin("api", clock()),
|
|
2346
2574
|
});
|
|
2347
|
-
for (const listener of listeners) {
|
|
2348
|
-
listener();
|
|
2349
|
-
}
|
|
2350
2575
|
},
|
|
2351
2576
|
getHostAnnotationSnapshot() {
|
|
2352
2577
|
return deriveHostAnnotationSnapshot();
|
|
@@ -2408,6 +2633,31 @@ export function createDocumentRuntime(
|
|
|
2408
2633
|
getRuntimeContextAnalytics(query) {
|
|
2409
2634
|
return getCachedRuntimeContextAnalytics(query);
|
|
2410
2635
|
},
|
|
2636
|
+
// Schema 1.2 — EditorStateChannel delegation
|
|
2637
|
+
configureEditorStatePolicy(policy) {
|
|
2638
|
+
editorStateChannel.setPolicy(policy);
|
|
2639
|
+
},
|
|
2640
|
+
registerEditorStateResolver(resolver) {
|
|
2641
|
+
editorStateChannel.setResolver(resolver);
|
|
2642
|
+
},
|
|
2643
|
+
registerEditorStatePersister(persister) {
|
|
2644
|
+
editorStateChannel.setPersister(persister);
|
|
2645
|
+
},
|
|
2646
|
+
getEditorStateKey(namespace) {
|
|
2647
|
+
return editorStateChannel.getKey(namespace);
|
|
2648
|
+
},
|
|
2649
|
+
async retryPendingPersist(namespace) {
|
|
2650
|
+
await editorStateChannel.retry(namespace);
|
|
2651
|
+
},
|
|
2652
|
+
get editorStateChannel() {
|
|
2653
|
+
return editorStateChannel;
|
|
2654
|
+
},
|
|
2655
|
+
getPerfCountersSnapshot() {
|
|
2656
|
+
return perfCounters.snapshot();
|
|
2657
|
+
},
|
|
2658
|
+
resetPerfCounters() {
|
|
2659
|
+
perfCounters.reset();
|
|
2660
|
+
},
|
|
2411
2661
|
};
|
|
2412
2662
|
|
|
2413
2663
|
function applyHistory(direction: "undo" | "redo"): void {
|
|
@@ -2455,15 +2705,14 @@ export function createDocumentRuntime(
|
|
|
2455
2705
|
function applyTransactionToState(transaction: EditorTransaction): void {
|
|
2456
2706
|
const previous = state;
|
|
2457
2707
|
|
|
2708
|
+
const tApply0 = performance.now();
|
|
2458
2709
|
protectionSnapshot = remapProtectionSnapshot(protectionSnapshot, transaction.mapping);
|
|
2710
|
+
const tFinalize0 = performance.now();
|
|
2459
2711
|
state = finalizeState(transaction.nextState, transaction.markDirty, clock());
|
|
2712
|
+
perfCounters.increment("commit.finalizeState.us", Math.round((performance.now() - tFinalize0) * 1000));
|
|
2460
2713
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
2461
2714
|
|
|
2462
|
-
|
|
2463
|
-
// next layout query can splice rather than rebuild the full graph. The
|
|
2464
|
-
// engine analyzes the reason against its cached graph and falls back to
|
|
2465
|
-
// a full rebuild when the edit crosses section boundaries or reaches a
|
|
2466
|
-
// page the engine cannot safely resume from.
|
|
2715
|
+
const tInvalidate0 = performance.now();
|
|
2467
2716
|
if (transaction.markDirty && transaction.mapping.steps.length > 0) {
|
|
2468
2717
|
let minFrom = Infinity;
|
|
2469
2718
|
let maxTo = -Infinity;
|
|
@@ -2476,17 +2725,20 @@ export function createDocumentRuntime(
|
|
|
2476
2725
|
layoutEngine.invalidate({ kind: "content-edit", from: minFrom, to: maxTo });
|
|
2477
2726
|
}
|
|
2478
2727
|
}
|
|
2479
|
-
|
|
2480
|
-
// Font-loader refresh on subParts identity change — this is the
|
|
2481
|
-
// lightweight proxy for "a change that could affect which fonts the
|
|
2482
|
-
// canvas backend measures against". Typing edits don't rebuild
|
|
2483
|
-
// subParts; style + font + numbering imports do.
|
|
2484
2728
|
if (previous.document.subParts !== state.document.subParts) {
|
|
2485
2729
|
fontLoader.refresh(collectFontLoaderInput(state.document));
|
|
2730
|
+
layoutEngine.invalidateMeasurementCache();
|
|
2486
2731
|
}
|
|
2732
|
+
perfCounters.increment("commit.invalidate.us", Math.round((performance.now() - tInvalidate0) * 1000));
|
|
2487
2733
|
|
|
2734
|
+
const tRefresh0 = performance.now();
|
|
2488
2735
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
2736
|
+
perfCounters.increment("commit.refresh.us", Math.round((performance.now() - tRefresh0) * 1000));
|
|
2737
|
+
|
|
2738
|
+
const tNotify0 = performance.now();
|
|
2489
2739
|
notify(previous, state, transaction);
|
|
2740
|
+
perfCounters.increment("commit.notify.us", Math.round((performance.now() - tNotify0) * 1000));
|
|
2741
|
+
perfCounters.increment("commit.total.us", Math.round((performance.now() - tApply0) * 1000));
|
|
2490
2742
|
}
|
|
2491
2743
|
|
|
2492
2744
|
function notify(
|
|
@@ -2715,6 +2967,7 @@ export function createDocumentRuntime(
|
|
|
2715
2967
|
timestamp,
|
|
2716
2968
|
documentMode: getEffectiveDocumentMode(selection),
|
|
2717
2969
|
defaultAuthorId: defaultAuthorId ?? undefined,
|
|
2970
|
+
renderSnapshot: cachedRenderSnapshot,
|
|
2718
2971
|
} as const;
|
|
2719
2972
|
const baseState = selection === state.selection
|
|
2720
2973
|
? state
|
|
@@ -2946,10 +3199,142 @@ export function createDocumentRuntime(
|
|
|
2946
3199
|
return nextReview;
|
|
2947
3200
|
}
|
|
2948
3201
|
|
|
3202
|
+
// L7 Phase 1 — context-analytics emissions are coalesced per microtask.
|
|
3203
|
+
// A single keystroke commit fires multiple events that each independently
|
|
3204
|
+
// trigger `emitContextAnalyticsChanged` (selection_changed, change_authored,
|
|
3205
|
+
// …). Each emission re-walks every workflow markup item (~211 ms on a
|
|
3206
|
+
// 40-page CCEP fixture) so unbatched, an N-event commit costs N × 211 ms.
|
|
3207
|
+
// Coalescing collapses the burst to a single emit at the tail of the
|
|
3208
|
+
// synchronous call stack — one per commit. (Flag declared above near
|
|
3209
|
+
// perfCounters so it is initialized before construction-time emits run.)
|
|
3210
|
+
function scheduleContextAnalyticsEmit(): void {
|
|
3211
|
+
if (analyticsEmitScheduled) {
|
|
3212
|
+
perfCounters.increment("emit.contextAnalytics.coalesced");
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
analyticsEmitScheduled = true;
|
|
3216
|
+
queueMicrotask(() => {
|
|
3217
|
+
// Reset BEFORE the emit so any synchronous re-entrant emits triggered
|
|
3218
|
+
// by listener callbacks schedule a fresh second microtask (no lost
|
|
3219
|
+
// updates). Reversing this order would drop bursts originating in
|
|
3220
|
+
// listeners.
|
|
3221
|
+
analyticsEmitScheduled = false;
|
|
3222
|
+
const t = performance.now();
|
|
3223
|
+
emitContextAnalyticsChanged();
|
|
3224
|
+
perfCounters.increment("emit.contextAnalytics.us", Math.round((performance.now() - t) * 1000));
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
|
|
2949
3228
|
function emit(event: DocumentRuntimeEvent): void {
|
|
3229
|
+
perfCounters.increment(`emit.${event.type}.calls`);
|
|
3230
|
+
const t0 = performance.now();
|
|
2950
3231
|
emitInternal(event);
|
|
3232
|
+
perfCounters.increment(`emit.${event.type}.internal.us`, Math.round((performance.now() - t0) * 1000));
|
|
2951
3233
|
if (shouldEmitContextAnalyticsChanged(event)) {
|
|
2952
|
-
|
|
3234
|
+
scheduleContextAnalyticsEmit();
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
function applyRuntimeStateOverlayCommand(
|
|
3239
|
+
command: RuntimeStateOverlayCommand,
|
|
3240
|
+
): void {
|
|
3241
|
+
switch (command.type) {
|
|
3242
|
+
case "workflow.set-overlay": {
|
|
3243
|
+
workflowOverlay = structuredClone(command.overlay);
|
|
3244
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
3245
|
+
const snapshot = deriveWorkflowScopeSnapshot()!;
|
|
3246
|
+
emit({
|
|
3247
|
+
type: "workflow_overlay_changed",
|
|
3248
|
+
documentId: state.documentId,
|
|
3249
|
+
snapshot,
|
|
3250
|
+
});
|
|
3251
|
+
if (workflowOverlay.activeWorkItemId !== undefined) {
|
|
3252
|
+
emit({
|
|
3253
|
+
type: "workflow_active_work_item_changed",
|
|
3254
|
+
documentId: state.documentId,
|
|
3255
|
+
activeWorkItemId: workflowOverlay.activeWorkItemId ?? null,
|
|
3256
|
+
});
|
|
3257
|
+
}
|
|
3258
|
+
break;
|
|
3259
|
+
}
|
|
3260
|
+
case "workflow.clear-overlay": {
|
|
3261
|
+
workflowOverlay = null;
|
|
3262
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
3263
|
+
emit({
|
|
3264
|
+
type: "workflow_active_work_item_changed",
|
|
3265
|
+
documentId: state.documentId,
|
|
3266
|
+
activeWorkItemId: null,
|
|
3267
|
+
});
|
|
3268
|
+
emit({
|
|
3269
|
+
type: "workflow_overlay_changed",
|
|
3270
|
+
documentId: state.documentId,
|
|
3271
|
+
snapshot: {
|
|
3272
|
+
overlayPresent: false,
|
|
3273
|
+
activeWorkItemId: null,
|
|
3274
|
+
scopes: [],
|
|
3275
|
+
candidates: [],
|
|
3276
|
+
blockedReasons: [],
|
|
3277
|
+
},
|
|
3278
|
+
});
|
|
3279
|
+
break;
|
|
3280
|
+
}
|
|
3281
|
+
case "workflow.set-metadata-definitions": {
|
|
3282
|
+
workflowMetadataDefinitions = structuredClone(command.definitions);
|
|
3283
|
+
emit({
|
|
3284
|
+
type: "workflow_metadata_changed",
|
|
3285
|
+
documentId: state.documentId,
|
|
3286
|
+
snapshot: deriveWorkflowMetadataSnapshot(),
|
|
3287
|
+
});
|
|
3288
|
+
break;
|
|
3289
|
+
}
|
|
3290
|
+
case "workflow.clear-metadata-definitions": {
|
|
3291
|
+
workflowMetadataDefinitions = [];
|
|
3292
|
+
emit({
|
|
3293
|
+
type: "workflow_metadata_changed",
|
|
3294
|
+
documentId: state.documentId,
|
|
3295
|
+
snapshot: deriveWorkflowMetadataSnapshot(),
|
|
3296
|
+
});
|
|
3297
|
+
break;
|
|
3298
|
+
}
|
|
3299
|
+
case "workflow.set-metadata-entries": {
|
|
3300
|
+
workflowMetadataEntries = structuredClone(command.entries);
|
|
3301
|
+
emit({
|
|
3302
|
+
type: "workflow_metadata_changed",
|
|
3303
|
+
documentId: state.documentId,
|
|
3304
|
+
snapshot: deriveWorkflowMetadataSnapshot(),
|
|
3305
|
+
});
|
|
3306
|
+
break;
|
|
3307
|
+
}
|
|
3308
|
+
case "workflow.clear-metadata-entries": {
|
|
3309
|
+
workflowMetadataEntries = [];
|
|
3310
|
+
emit({
|
|
3311
|
+
type: "workflow_metadata_changed",
|
|
3312
|
+
documentId: state.documentId,
|
|
3313
|
+
snapshot: deriveWorkflowMetadataSnapshot(),
|
|
3314
|
+
});
|
|
3315
|
+
break;
|
|
3316
|
+
}
|
|
3317
|
+
case "host-annotation.set-overlay": {
|
|
3318
|
+
hostAnnotationOverlay = structuredClone(command.overlay);
|
|
3319
|
+
emit({
|
|
3320
|
+
type: "host_annotation_overlay_changed",
|
|
3321
|
+
documentId: state.documentId,
|
|
3322
|
+
snapshot: deriveHostAnnotationSnapshot(),
|
|
3323
|
+
});
|
|
3324
|
+
break;
|
|
3325
|
+
}
|
|
3326
|
+
case "host-annotation.clear-overlay": {
|
|
3327
|
+
hostAnnotationOverlay = null;
|
|
3328
|
+
emit({
|
|
3329
|
+
type: "host_annotation_overlay_changed",
|
|
3330
|
+
documentId: state.documentId,
|
|
3331
|
+
snapshot: deriveHostAnnotationSnapshot(),
|
|
3332
|
+
});
|
|
3333
|
+
break;
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
for (const listener of listeners) {
|
|
3337
|
+
listener();
|
|
2953
3338
|
}
|
|
2954
3339
|
}
|
|
2955
3340
|
|
|
@@ -2993,7 +3378,23 @@ export function createDocumentRuntime(
|
|
|
2993
3378
|
}
|
|
2994
3379
|
|
|
2995
3380
|
function emitContextAnalyticsChanged(): void {
|
|
3381
|
+
// L7 Phase 1 — short-circuit when no consumer can observe the event.
|
|
3382
|
+
// The downstream cost is dominated by createRuntimeContextAnalyticsSnapshot,
|
|
3383
|
+
// which assembles ~8 facets (suggestions, navigation, review-work,
|
|
3384
|
+
// workflow-markup, etc.) and runs > 300 ms on a 40-page CCEP fixture
|
|
3385
|
+
// because its cache key includes `selection` and selection moves on
|
|
3386
|
+
// every keystroke. Skipping here when nobody is subscribed eliminates
|
|
3387
|
+
// the cost in headless/SSR/test usage. Production keeps its `onEvent`
|
|
3388
|
+
// handler, so the production typing-latency win lives in Phase 1.5
|
|
3389
|
+
// (microtask coalescing) and Phase 1.6 (per-input-axis caching of the
|
|
3390
|
+
// analytics inputs).
|
|
3391
|
+
if (options.onEvent === undefined && eventListeners.size === 0) {
|
|
3392
|
+
perfCounters.increment("emit.contextAnalytics.skippedNoListeners");
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
const t0 = performance.now();
|
|
2996
3396
|
const trackedSnapshots = collectTrackedContextAnalyticsSnapshots();
|
|
3397
|
+
perfCounters.increment("emit.contextAnalytics.collect.us", Math.round((performance.now() - t0) * 1000));
|
|
2997
3398
|
if (
|
|
2998
3399
|
lastEmittedContextAnalyticsSnapshots !== undefined &&
|
|
2999
3400
|
trackedContextAnalyticsSnapshotsEqual(lastEmittedContextAnalyticsSnapshots, trackedSnapshots)
|
|
@@ -3111,6 +3512,39 @@ function normalizeCommandTimestamp(value: unknown): string | undefined {
|
|
|
3111
3512
|
return undefined;
|
|
3112
3513
|
}
|
|
3113
3514
|
|
|
3515
|
+
type RuntimeStateOverlayCommand = Extract<
|
|
3516
|
+
EditorCommand,
|
|
3517
|
+
{
|
|
3518
|
+
type:
|
|
3519
|
+
| "workflow.set-overlay"
|
|
3520
|
+
| "workflow.clear-overlay"
|
|
3521
|
+
| "workflow.set-metadata-definitions"
|
|
3522
|
+
| "workflow.clear-metadata-definitions"
|
|
3523
|
+
| "workflow.set-metadata-entries"
|
|
3524
|
+
| "workflow.clear-metadata-entries"
|
|
3525
|
+
| "host-annotation.set-overlay"
|
|
3526
|
+
| "host-annotation.clear-overlay";
|
|
3527
|
+
}
|
|
3528
|
+
>;
|
|
3529
|
+
|
|
3530
|
+
function isRuntimeStateOverlayCommand(
|
|
3531
|
+
command: EditorCommand,
|
|
3532
|
+
): command is RuntimeStateOverlayCommand {
|
|
3533
|
+
switch (command.type) {
|
|
3534
|
+
case "workflow.set-overlay":
|
|
3535
|
+
case "workflow.clear-overlay":
|
|
3536
|
+
case "workflow.set-metadata-definitions":
|
|
3537
|
+
case "workflow.clear-metadata-definitions":
|
|
3538
|
+
case "workflow.set-metadata-entries":
|
|
3539
|
+
case "workflow.clear-metadata-entries":
|
|
3540
|
+
case "host-annotation.set-overlay":
|
|
3541
|
+
case "host-annotation.clear-overlay":
|
|
3542
|
+
return true;
|
|
3543
|
+
default:
|
|
3544
|
+
return false;
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3114
3548
|
function finalizeState(
|
|
3115
3549
|
state: EditorState,
|
|
3116
3550
|
markDirty: boolean,
|
|
@@ -3684,6 +4118,14 @@ const NON_MUTATION_COMMANDS = new Set([
|
|
|
3684
4118
|
"warning.add",
|
|
3685
4119
|
"warning.clear",
|
|
3686
4120
|
"comment.open",
|
|
4121
|
+
"workflow.set-overlay",
|
|
4122
|
+
"workflow.clear-overlay",
|
|
4123
|
+
"workflow.set-metadata-definitions",
|
|
4124
|
+
"workflow.clear-metadata-definitions",
|
|
4125
|
+
"workflow.set-metadata-entries",
|
|
4126
|
+
"workflow.clear-metadata-entries",
|
|
4127
|
+
"host-annotation.set-overlay",
|
|
4128
|
+
"host-annotation.clear-overlay",
|
|
3687
4129
|
]);
|
|
3688
4130
|
|
|
3689
4131
|
/** Mutation commands that are not yet supported in suggesting mode. */
|
|
@@ -4008,6 +4450,7 @@ function refreshDocumentTableOfContents(
|
|
|
4008
4450
|
selectionHead: number,
|
|
4009
4451
|
activeStory: EditorStoryTarget,
|
|
4010
4452
|
options?: TocRefreshOptions,
|
|
4453
|
+
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
4011
4454
|
): {
|
|
4012
4455
|
document: CanonicalDocumentEnvelope;
|
|
4013
4456
|
result: TocRefreshResult;
|
|
@@ -4038,7 +4481,7 @@ function refreshDocumentTableOfContents(
|
|
|
4038
4481
|
}
|
|
4039
4482
|
const nextField: FieldNode = {
|
|
4040
4483
|
...field,
|
|
4041
|
-
children: buildTocInlineNodes(entries),
|
|
4484
|
+
children: buildTocInlineNodes(entries, resolveDisplayPageNumber),
|
|
4042
4485
|
refreshStatus: "current",
|
|
4043
4486
|
};
|
|
4044
4487
|
if (flattenInlineDisplayText(nextField.children) !== flattenInlineDisplayText(field.children)) {
|
|
@@ -4221,14 +4664,26 @@ function buildInlineNodesFromDisplayText(text: string): InlineNode[] {
|
|
|
4221
4664
|
return children;
|
|
4222
4665
|
}
|
|
4223
4666
|
|
|
4667
|
+
/**
|
|
4668
|
+
* P5 — TOC entry rendering with display-number resolution. When
|
|
4669
|
+
* `resolveDisplayPageNumber` is supplied, TOC entries print the number
|
|
4670
|
+
* Word would print on the page (honors `w:pgNumType/@w:start` restarts
|
|
4671
|
+
* for front-matter roman numerals → body arabic restart). Without the
|
|
4672
|
+
* resolver, falls back to the raw `pageIndex + 1` (pre-P5 behavior).
|
|
4673
|
+
*/
|
|
4224
4674
|
function buildTocInlineNodes(
|
|
4225
4675
|
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
|
|
4676
|
+
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
4226
4677
|
): InlineNode[] {
|
|
4227
4678
|
const children: InlineNode[] = [];
|
|
4228
4679
|
entries.forEach((entry, index) => {
|
|
4229
4680
|
children.push({ type: "text", text: entry.text });
|
|
4230
4681
|
children.push({ type: "tab" });
|
|
4231
|
-
|
|
4682
|
+
const displayed = resolveDisplayPageNumber?.(entry.pageIndex);
|
|
4683
|
+
children.push({
|
|
4684
|
+
type: "text",
|
|
4685
|
+
text: String(displayed ?? entry.pageIndex + 1),
|
|
4686
|
+
});
|
|
4232
4687
|
if (index < entries.length - 1) {
|
|
4233
4688
|
children.push({ type: "hard_break" });
|
|
4234
4689
|
}
|
|
@@ -4236,6 +4691,14 @@ function buildTocInlineNodes(
|
|
|
4236
4691
|
return children;
|
|
4237
4692
|
}
|
|
4238
4693
|
|
|
4694
|
+
/** Test-only export of `buildTocInlineNodes` (P5 unit tests). */
|
|
4695
|
+
export function __buildTocInlineNodes(
|
|
4696
|
+
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
|
|
4697
|
+
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
4698
|
+
): InlineNode[] {
|
|
4699
|
+
return buildTocInlineNodes(entries, resolveDisplayPageNumber);
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4239
4702
|
function collectFieldsFromSubParts(
|
|
4240
4703
|
subParts: SubPartsCatalog | undefined,
|
|
4241
4704
|
entries: FieldEntrySnapshot[],
|
|
@@ -4284,6 +4747,25 @@ function resolveSupportedFieldDisplay(
|
|
|
4284
4747
|
if (field.fieldFamily === "TOC") {
|
|
4285
4748
|
return undefined;
|
|
4286
4749
|
}
|
|
4750
|
+
if (field.fieldFamily === "PAGE") {
|
|
4751
|
+
const page = resolveRepresentativePageForStory(navigation, storyTarget);
|
|
4752
|
+
if (!page) {
|
|
4753
|
+
return { displayText: "", refreshStatus: "unresolvable" };
|
|
4754
|
+
}
|
|
4755
|
+
return {
|
|
4756
|
+
displayText: String(resolveDisplayedPageNumber(page)),
|
|
4757
|
+
refreshStatus: "current",
|
|
4758
|
+
};
|
|
4759
|
+
}
|
|
4760
|
+
if (field.fieldFamily === "NUMPAGES") {
|
|
4761
|
+
if (navigation.pageCount === 0) {
|
|
4762
|
+
return { displayText: "", refreshStatus: "unresolvable" };
|
|
4763
|
+
}
|
|
4764
|
+
return {
|
|
4765
|
+
displayText: String(navigation.pageCount),
|
|
4766
|
+
refreshStatus: "current",
|
|
4767
|
+
};
|
|
4768
|
+
}
|
|
4287
4769
|
if (!field.fieldTarget) {
|
|
4288
4770
|
return { displayText: "", refreshStatus: "unresolvable" };
|
|
4289
4771
|
}
|
|
@@ -4575,8 +5057,38 @@ function remapProtectionSnapshot(
|
|
|
4575
5057
|
* let the loader register system fonts it finds via
|
|
4576
5058
|
* `document.fonts.check(...)`.
|
|
4577
5059
|
*/
|
|
5060
|
+
// P14.d — memoize the font-family walk by `(content, styles)` reference
|
|
5061
|
+
// identity. Both shapes change identity only on a real edit / import /
|
|
5062
|
+
// style mutation; per-keystroke edits keep the same references because
|
|
5063
|
+
// `finalizeState` shallow-spreads `state.document`. WeakMap two-level
|
|
5064
|
+
// cache so the entries free with the documents that own them.
|
|
5065
|
+
const fontLoaderInputCache = new WeakMap<
|
|
5066
|
+
object,
|
|
5067
|
+
WeakMap<object, { families: readonly string[] }>
|
|
5068
|
+
>();
|
|
5069
|
+
|
|
4578
5070
|
function collectFontLoaderInput(
|
|
4579
5071
|
document: CanonicalDocumentEnvelope,
|
|
5072
|
+
): { families: readonly string[] } {
|
|
5073
|
+
const contentKey = document.content as unknown as object;
|
|
5074
|
+
const stylesKey = (document.styles ?? FONT_LOADER_EMPTY_STYLES_KEY) as unknown as object;
|
|
5075
|
+
let stylesCache = fontLoaderInputCache.get(contentKey);
|
|
5076
|
+
if (stylesCache) {
|
|
5077
|
+
const cached = stylesCache.get(stylesKey);
|
|
5078
|
+
if (cached) return cached;
|
|
5079
|
+
} else {
|
|
5080
|
+
stylesCache = new WeakMap();
|
|
5081
|
+
fontLoaderInputCache.set(contentKey, stylesCache);
|
|
5082
|
+
}
|
|
5083
|
+
const result = collectFontLoaderInputUncached(document);
|
|
5084
|
+
stylesCache.set(stylesKey, result);
|
|
5085
|
+
return result;
|
|
5086
|
+
}
|
|
5087
|
+
|
|
5088
|
+
const FONT_LOADER_EMPTY_STYLES_KEY = Object.freeze({});
|
|
5089
|
+
|
|
5090
|
+
function collectFontLoaderInputUncached(
|
|
5091
|
+
document: CanonicalDocumentEnvelope,
|
|
4580
5092
|
): { families: readonly string[] } {
|
|
4581
5093
|
try {
|
|
4582
5094
|
const families = new Set<string>();
|
|
@@ -4604,6 +5116,9 @@ function collectFontLoaderInput(
|
|
|
4604
5116
|
}
|
|
4605
5117
|
}
|
|
4606
5118
|
|
|
5119
|
+
/** Test-only export of the uncached walk so memoization tests can spy on it. */
|
|
5120
|
+
export const __collectFontLoaderInputUncached = collectFontLoaderInputUncached;
|
|
5121
|
+
|
|
4607
5122
|
/**
|
|
4608
5123
|
* Asynchronously upgrade the engine's measurement backend to canvas once
|
|
4609
5124
|
* the platform supports it and fonts have resolved. Errors are swallowed
|