@beyondwork/docx-react-component 1.0.52 → 1.0.53
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 +31 -40
- package/src/api/public-types.ts +32 -0
- package/src/io/chart-preview-resolver.ts +41 -0
- package/src/io/docx-session.ts +187 -17
- package/src/runtime/collab/runtime-collab-sync.ts +87 -6
- package/src/runtime/document-runtime.ts +159 -0
- package/src/runtime/layout/layout-engine-version.ts +40 -2
- package/src/runtime/layout/public-facet.ts +43 -1
- package/src/runtime/prerender/cache-envelope.ts +30 -0
- package/src/runtime/prerender/customxml-cache.ts +17 -3
- package/src/runtime/prerender/prerender-document.ts +17 -1
- package/src/runtime/render/render-kernel.ts +67 -19
- package/src/runtime/surface-projection.ts +28 -0
- package/src/runtime/table-schema.ts +27 -0
- package/src/runtime/table-style-resolver.ts +51 -0
- package/src/ui/editor-runtime-boundary.ts +39 -2
- package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +54 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +107 -3
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +2 -2
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +224 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +2 -2
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +2 -2
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +11 -147
- package/src/ui-tailwind/theme/chart-palette-adapter.ts +57 -0
- package/src/ui-tailwind/theme/editor-theme.css +26 -24
- package/src/ui-tailwind/theme/tokens.css +345 -0
- package/src/ui-tailwind/theme/tokens.ts +313 -0
- package/src/ui-tailwind/theme/use-density.ts +60 -0
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.53",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
|
-
"packageManager": "pnpm@10.30.3",
|
|
7
6
|
"type": "module",
|
|
8
7
|
"sideEffects": [
|
|
9
8
|
"**/*.css"
|
|
@@ -93,35 +92,6 @@
|
|
|
93
92
|
"./ui-tailwind/theme/editor-theme.css": "./src/ui-tailwind/theme/editor-theme.css"
|
|
94
93
|
},
|
|
95
94
|
"types": "./src/index.ts",
|
|
96
|
-
"scripts": {
|
|
97
|
-
"build": "tsup",
|
|
98
|
-
"test": "bash scripts/run-workspace-tests.sh",
|
|
99
|
-
"test:repo": "node scripts/ci-check-layout-engine-version.mjs && node scripts/run-repo-tests.mjs core",
|
|
100
|
-
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
101
|
-
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
102
|
-
"test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
|
|
103
|
-
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
104
|
-
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
105
|
-
"test:visual": "VISUAL_SMOKE_PROFILE=bare pnpm exec playwright test --project=chromium",
|
|
106
|
-
"test:visual:chrome": "VISUAL_SMOKE_PROFILE=chrome-cycle pnpm exec playwright test --project=chromium",
|
|
107
|
-
"visual:list-runs": "node scripts/visual-smoke-list-runs.mjs",
|
|
108
|
-
"mcp:visual-smoke": "node scripts/visual-smoke-mcp.mjs",
|
|
109
|
-
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
110
|
-
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
111
|
-
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
112
|
-
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
113
|
-
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
114
|
-
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
115
|
-
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
116
|
-
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
117
|
-
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
118
|
-
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
119
|
-
"wave:status": "bash scripts/wave-status.sh",
|
|
120
|
-
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
121
|
-
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
122
|
-
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
123
|
-
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
124
|
-
},
|
|
125
95
|
"keywords": [
|
|
126
96
|
"docx",
|
|
127
97
|
"word",
|
|
@@ -205,14 +175,35 @@
|
|
|
205
175
|
"y-protocols": "^1.0.7",
|
|
206
176
|
"yjs": "^13.6.30"
|
|
207
177
|
},
|
|
208
|
-
"
|
|
209
|
-
"
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
"
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
178
|
+
"scripts": {
|
|
179
|
+
"build": "tsup",
|
|
180
|
+
"test": "bash scripts/run-workspace-tests.sh",
|
|
181
|
+
"test:repo": "node scripts/ci-check-layout-engine-version.mjs && node scripts/run-repo-tests.mjs core",
|
|
182
|
+
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
183
|
+
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
184
|
+
"test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
|
|
185
|
+
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
186
|
+
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
187
|
+
"test:visual": "VISUAL_SMOKE_PROFILE=bare pnpm exec playwright test --project=chromium",
|
|
188
|
+
"test:visual:chrome": "VISUAL_SMOKE_PROFILE=chrome-cycle pnpm exec playwright test --project=chromium",
|
|
189
|
+
"visual:list-runs": "node scripts/visual-smoke-list-runs.mjs",
|
|
190
|
+
"mcp:visual-smoke": "node scripts/visual-smoke-mcp.mjs",
|
|
191
|
+
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
192
|
+
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
193
|
+
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
194
|
+
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
195
|
+
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
196
|
+
"generate:token-reference": "node scripts/generate-token-reference.mjs",
|
|
197
|
+
"check:token-reference": "node scripts/generate-token-reference.mjs --check",
|
|
198
|
+
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
199
|
+
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
200
|
+
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
201
|
+
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
202
|
+
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
203
|
+
"wave:status": "bash scripts/wave-status.sh",
|
|
204
|
+
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
205
|
+
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
206
|
+
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
207
|
+
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
217
208
|
}
|
|
218
209
|
}
|
package/src/api/public-types.ts
CHANGED
|
@@ -892,6 +892,13 @@ export interface SurfaceTableCellSnapshot {
|
|
|
892
892
|
borderRight?: string | null;
|
|
893
893
|
borderBottom?: string | null;
|
|
894
894
|
borderLeft?: string | null;
|
|
895
|
+
/**
|
|
896
|
+
* R3.a Phase 2: per-cell text-flow direction copied from
|
|
897
|
+
* `TableCellNode.textDirection`. The node-view maps these to CSS
|
|
898
|
+
* `writing-mode` (`tbRl` → `vertical-rl`, `btLr` → `vertical-lr`, `lrTb` →
|
|
899
|
+
* unset / horizontal default). Cells with no direction stay horizontal.
|
|
900
|
+
*/
|
|
901
|
+
textDirection?: "lrTb" | "tbRl" | "btLr" | null;
|
|
895
902
|
/**
|
|
896
903
|
* R2a: space-joined CSS class names from the resolved table-style conditional
|
|
897
904
|
* regions (e.g. "band-firstRow band-band1Horz"). Consumers apply these to the
|
|
@@ -985,6 +992,31 @@ export type SurfaceBlockSnapshot =
|
|
|
985
992
|
noHBand?: boolean;
|
|
986
993
|
noVBand?: boolean;
|
|
987
994
|
};
|
|
995
|
+
/**
|
|
996
|
+
* R3.a Phase 2: resolved table-level properties (width / layoutMode /
|
|
997
|
+
* cellSpacing / borders) projected from the canonical TableNode + its
|
|
998
|
+
* resolved table-style cascade. Borders carry the typed `BorderSpec`
|
|
999
|
+
* per side; the node-view converts top/right/bottom/left to CSS
|
|
1000
|
+
* shorthand. insideH / insideV are stamped here for round-trip and
|
|
1001
|
+
* future renderer use, but at render time their visual effect is
|
|
1002
|
+
* realized through per-cell borders on the cell snapshot (CSS has no
|
|
1003
|
+
* direct "table-inside-border" property).
|
|
1004
|
+
*/
|
|
1005
|
+
tableResolved?: {
|
|
1006
|
+
width?: number | null;
|
|
1007
|
+
widthType?: "auto" | "dxa" | "pct" | "nil" | null;
|
|
1008
|
+
layoutMode?: "fixed" | "autofit" | null;
|
|
1009
|
+
cellSpacing?: number | null;
|
|
1010
|
+
cellSpacingType?: "auto" | "dxa" | "pct" | "nil" | null;
|
|
1011
|
+
borders?: {
|
|
1012
|
+
top?: { value?: string; size?: number; space?: number; color?: string } | null;
|
|
1013
|
+
right?: { value?: string; size?: number; space?: number; color?: string } | null;
|
|
1014
|
+
bottom?: { value?: string; size?: number; space?: number; color?: string } | null;
|
|
1015
|
+
left?: { value?: string; size?: number; space?: number; color?: string } | null;
|
|
1016
|
+
insideH?: { value?: string; size?: number; space?: number; color?: string } | null;
|
|
1017
|
+
insideV?: { value?: string; size?: number; space?: number; color?: string } | null;
|
|
1018
|
+
} | null;
|
|
1019
|
+
};
|
|
988
1020
|
rows: SurfaceTableRowSnapshot[];
|
|
989
1021
|
}
|
|
990
1022
|
| {
|
|
@@ -138,6 +138,47 @@ export async function resolveChartPreviewsForDocument(
|
|
|
138
138
|
return applyResolutions(doc, successful);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* C3b — Phase A/B deferred chart preview resolution.
|
|
143
|
+
*
|
|
144
|
+
* Phase A (cheap, synchronous): check whether there are any unresolved
|
|
145
|
+
* chart_preview nodes. If not, return immediately without scheduling
|
|
146
|
+
* anything. The collect walk is O(paragraphs) and takes ~0 ms.
|
|
147
|
+
*
|
|
148
|
+
* Phase B (deferred): schedule the actual `renderChartPreview` calls
|
|
149
|
+
* outside the critical load path via requestIdleCallback (browser) or
|
|
150
|
+
* setTimeout(0) (Node / SSR). Fires `onReady` with the resolved
|
|
151
|
+
* CanonicalDocument when all charts complete.
|
|
152
|
+
*
|
|
153
|
+
* The caller (loadDocxEditorSessionAsync) returns the session without
|
|
154
|
+
* chart previews. When `onReady` fires, the consumer can update the
|
|
155
|
+
* runtime's media catalog (e.g. via a follow-up `hydrateChartPreviews`
|
|
156
|
+
* call, or by accepting the updated CanonicalDocument into a new
|
|
157
|
+
* progressive render). Expected win: 30–80 ms removed from the warm
|
|
158
|
+
* cold-open path on extra-large docs with ~12 charts.
|
|
159
|
+
*/
|
|
160
|
+
export function scheduleChartPreviewResolution(
|
|
161
|
+
doc: CanonicalDocument,
|
|
162
|
+
pkg: OpcPackage,
|
|
163
|
+
adapter: EditorHostAdapter | undefined,
|
|
164
|
+
onReady: (resolved: CanonicalDocument) => void,
|
|
165
|
+
): void {
|
|
166
|
+
if (!adapter?.renderChartPreview) return;
|
|
167
|
+
// Phase A: fast collect — avoids scheduling idle work when there are no
|
|
168
|
+
// unresolved charts (the common case for most non-chart documents).
|
|
169
|
+
const pending = collectUnresolvedChartPreviews(doc, pkg);
|
|
170
|
+
if (pending.length === 0) return;
|
|
171
|
+
|
|
172
|
+
const schedule =
|
|
173
|
+
typeof requestIdleCallback !== "undefined"
|
|
174
|
+
? (fn: () => void) => { requestIdleCallback(fn); }
|
|
175
|
+
: (fn: () => void) => { setTimeout(fn, 0); };
|
|
176
|
+
|
|
177
|
+
schedule(() => {
|
|
178
|
+
void resolveChartPreviewsForDocument(doc, pkg, adapter).then(onReady);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
141
182
|
/**
|
|
142
183
|
* Walk the whole content tree and yield one PendingResolution per
|
|
143
184
|
* chart_preview node that (a) has no previewMediaId yet and (b) can
|
package/src/io/docx-session.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
EditorError,
|
|
4
4
|
EditorHostAdapter,
|
|
5
5
|
EditorSessionState,
|
|
6
|
+
EditorSurfaceSnapshot,
|
|
6
7
|
EditorWarning as PublicEditorWarning,
|
|
7
8
|
EditorAnchorProjection as PublicEditorAnchorProjection,
|
|
8
9
|
ExportDocxOptions,
|
|
@@ -26,7 +27,13 @@ import type {
|
|
|
26
27
|
RevisionRecord as RuntimeRevisionRecord,
|
|
27
28
|
EditorWarning as InternalEditorWarning,
|
|
28
29
|
} from "../core/state/editor-state.ts";
|
|
29
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
createCanonicalDocumentId,
|
|
32
|
+
createDefaultCanonicalDocument,
|
|
33
|
+
createSelectionSnapshot,
|
|
34
|
+
} from "../core/state/editor-state.ts";
|
|
35
|
+
import { createEditorSurfaceSnapshot } from "../runtime/surface-projection.ts";
|
|
36
|
+
import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
|
|
30
37
|
import {
|
|
31
38
|
createDetachedAnchor,
|
|
32
39
|
storyTargetsEqual,
|
|
@@ -44,7 +51,7 @@ import {
|
|
|
44
51
|
normalizeParsedTextDocument,
|
|
45
52
|
normalizeParsedTextDocumentAsync,
|
|
46
53
|
} from "./normalize/normalize-text.ts";
|
|
47
|
-
import { createChartPartLookup, resolveChartPreviewsForDocument } from "./chart-preview-resolver.ts";
|
|
54
|
+
import { createChartPartLookup, resolveChartPreviewsForDocument, scheduleChartPreviewResolution } from "./chart-preview-resolver.ts";
|
|
48
55
|
import { type LoadScheduler } from "./load-scheduler.ts";
|
|
49
56
|
import type { CacheEnvelope } from "../runtime/prerender/cache-envelope.ts";
|
|
50
57
|
import {
|
|
@@ -122,6 +129,7 @@ import type {
|
|
|
122
129
|
SubPartsCatalog,
|
|
123
130
|
} from "../model/canonical-document.ts";
|
|
124
131
|
import { createCanonicalDocumentSignature } from "../model/canonical-document.ts";
|
|
132
|
+
import type { CanonicalDocument } from "../model/canonical-document.ts";
|
|
125
133
|
import type {
|
|
126
134
|
CommentImportDiagnostic,
|
|
127
135
|
ImportedCommentDefinition,
|
|
@@ -959,6 +967,53 @@ export interface LoadDocxEditorSessionAsyncOptions extends LoadDocxEditorSession
|
|
|
959
967
|
* path because their outputs are required by downstream consumers.
|
|
960
968
|
*/
|
|
961
969
|
laycacheEnvelope?: CacheEnvelope;
|
|
970
|
+
/**
|
|
971
|
+
* L7 Phase 2 Finale C2 — progressive initial mount. When supplied, the
|
|
972
|
+
* async loader fires this callback exactly once, after the body stage
|
|
973
|
+
* completes but before styles / sub-parts / compatibility / snapshot
|
|
974
|
+
* assembly, with a viewport-windowed `EditorSurfaceSnapshot`. The
|
|
975
|
+
* callback lets UI consumers show the first page's text before the
|
|
976
|
+
* rest of the load finishes, measurably shrinking perceived cold-open.
|
|
977
|
+
*
|
|
978
|
+
* The progressive surface is synthesized from a provisional
|
|
979
|
+
* `CanonicalDocumentEnvelope`: `content` is the normalized body; all
|
|
980
|
+
* other catalogs (styles, numbering, media, review, preservation) are
|
|
981
|
+
* empty. Viewport blocks render live; blocks beyond the viewport
|
|
982
|
+
* render as placeholders via the existing `cullBuild` flag. Consumers
|
|
983
|
+
* must treat the progressive surface as provisional — the final
|
|
984
|
+
* `LoadedDocxEditorSession` (returned from the same `await`) carries
|
|
985
|
+
* the real styled envelope.
|
|
986
|
+
*
|
|
987
|
+
* The callback receives `blocksRealized` (the viewport window size)
|
|
988
|
+
* and `blocksTotal` (total block count at body-stage time) so the
|
|
989
|
+
* consumer can size its viewport-commit telemetry.
|
|
990
|
+
*
|
|
991
|
+
* Optional. When absent, the async loader does not perform the
|
|
992
|
+
* provisional-envelope synthesis — no cold-path regression for
|
|
993
|
+
* consumers that do not opt in.
|
|
994
|
+
*
|
|
995
|
+
* Omitted on the Plan B short-circuit path (laycacheEnvelope !== undefined):
|
|
996
|
+
* the short-circuit is already fast enough that a progressive pre-commit
|
|
997
|
+
* adds more overhead than it saves.
|
|
998
|
+
*/
|
|
999
|
+
onProgressiveSnapshot?: (partial: {
|
|
1000
|
+
surface: EditorSurfaceSnapshot;
|
|
1001
|
+
phase: "viewport";
|
|
1002
|
+
blocksRealized: number;
|
|
1003
|
+
blocksTotal: number;
|
|
1004
|
+
}) => void;
|
|
1005
|
+
/**
|
|
1006
|
+
* C3b — deferred chart preview resolution. When supplied, chart preview
|
|
1007
|
+
* rendering (`renderChartPreview()` calls) is removed from the critical
|
|
1008
|
+
* load path: the session returns without chart previews, and this
|
|
1009
|
+
* callback fires later (via requestIdleCallback / setTimeout) with the
|
|
1010
|
+
* CanonicalDocument that has all chart previews resolved. Expected win:
|
|
1011
|
+
* 30–80 ms on extra-large warm-path docs with ~12 charts.
|
|
1012
|
+
*
|
|
1013
|
+
* Consumers that need chart previews on first render should not supply
|
|
1014
|
+
* this callback — the blocking path remains available by omitting it.
|
|
1015
|
+
*/
|
|
1016
|
+
onChartPreviewsReady?: (resolvedDoc: CanonicalDocument) => void;
|
|
962
1017
|
}
|
|
963
1018
|
|
|
964
1019
|
/**
|
|
@@ -979,6 +1034,23 @@ export interface LoadDocxEditorSessionAsyncOptions extends LoadDocxEditorSession
|
|
|
979
1034
|
* and SSR. The DOM boundary in `editor-runtime-boundary.ts` calls this
|
|
980
1035
|
* async path so the browser can paint the skeleton mid-parse.
|
|
981
1036
|
*/
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* L7 Phase 2 Finale C2 — progressive initial mount viewport window size.
|
|
1040
|
+
*
|
|
1041
|
+
* Sized to cover the first page of a typical paginated document (~20
|
|
1042
|
+
* body blocks = ~1 page on CCEP-scale templates at default margins). The
|
|
1043
|
+
* window is intentionally small so the provisional-envelope synthesis +
|
|
1044
|
+
* surface projection stays under ~30 ms — making `firstViewportCommit`
|
|
1045
|
+
* significantly faster than the full load.
|
|
1046
|
+
*
|
|
1047
|
+
* Blocks beyond this window render as `placeholder-culled` entries via
|
|
1048
|
+
* Phase 2.9's cullBuild flag (auto-derived from `!isInViewport`) — they
|
|
1049
|
+
* preserve `from`/`to` offsets for selection stability while costing ~0
|
|
1050
|
+
* style-cascade work.
|
|
1051
|
+
*/
|
|
1052
|
+
const PROGRESSIVE_VIEWPORT_BLOCKS = 20;
|
|
1053
|
+
|
|
982
1054
|
export async function loadDocxEditorSessionAsync(
|
|
983
1055
|
options: LoadDocxEditorSessionAsyncOptions,
|
|
984
1056
|
): Promise<LoadedDocxEditorSession> {
|
|
@@ -1090,18 +1162,45 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1090
1162
|
const protectionSnapshot = buildProtectionSnapshot(documentProtection, []);
|
|
1091
1163
|
|
|
1092
1164
|
// Chart previews (`previewMediaId` is host-dependent) aren't cached
|
|
1093
|
-
// in the envelope
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1165
|
+
// in the envelope. C3b: when onChartPreviewsReady is provided, defer
|
|
1166
|
+
// resolution out of the critical path. Otherwise block (legacy behavior).
|
|
1167
|
+
let documentWithChartPreviews: CanonicalDocumentEnvelope;
|
|
1168
|
+
if (options.onChartPreviewsReady) {
|
|
1169
|
+
scheduleChartPreviewResolution(
|
|
1170
|
+
canonicalDocument,
|
|
1171
|
+
sourcePackage,
|
|
1172
|
+
options.hostAdapter,
|
|
1173
|
+
options.onChartPreviewsReady,
|
|
1174
|
+
);
|
|
1175
|
+
documentWithChartPreviews = canonicalDocument as CanonicalDocumentEnvelope;
|
|
1176
|
+
} else {
|
|
1177
|
+
documentWithChartPreviews = (await resolveChartPreviewsForDocument(
|
|
1178
|
+
canonicalDocument,
|
|
1179
|
+
sourcePackage,
|
|
1180
|
+
options.hostAdapter,
|
|
1181
|
+
)) as CanonicalDocumentEnvelope;
|
|
1182
|
+
}
|
|
1099
1183
|
|
|
1100
1184
|
const timestamp = new Date().toISOString();
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1185
|
+
// Phase 2 Finale C3: skip `buildCompatibilityReport` (60–100 ms on
|
|
1186
|
+
// extra-large) when the envelope carries a pre-computed report.
|
|
1187
|
+
// Pure-function determinism of the report is enforced by
|
|
1188
|
+
// `canonicalDocumentHash` (5th input to `deriveCacheKey`): any
|
|
1189
|
+
// change to the canonical doc flips the hash and rejects the
|
|
1190
|
+
// envelope on load.
|
|
1191
|
+
//
|
|
1192
|
+
// The cached report's `generatedAt` is a fixed sentinel
|
|
1193
|
+
// (`CACHE_NORMALIZED_GENERATED_AT`) for envelope byte-identity.
|
|
1194
|
+
// Swap it for the live ISO8601 timestamp here because downstream
|
|
1195
|
+
// `validatePersistedEditorSnapshot` requires
|
|
1196
|
+
// `$.compatibility.generatedAt` to be ISO 8601.
|
|
1197
|
+
const cachedReport = options.laycacheEnvelope?.compatibilityReport;
|
|
1198
|
+
const compatibility = cachedReport
|
|
1199
|
+
? { ...cachedReport, generatedAt: timestamp }
|
|
1200
|
+
: buildCompatibilityReport({
|
|
1201
|
+
document: documentWithChartPreviews,
|
|
1202
|
+
generatedAt: timestamp,
|
|
1203
|
+
});
|
|
1105
1204
|
await scheduler.yield();
|
|
1106
1205
|
|
|
1107
1206
|
const snapshot = createImportedSnapshot({
|
|
@@ -1225,6 +1324,64 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1225
1324
|
);
|
|
1226
1325
|
stages.emit("body");
|
|
1227
1326
|
await scheduler.yield();
|
|
1327
|
+
|
|
1328
|
+
// L7 Phase 2 Finale C2 — progressive initial mount.
|
|
1329
|
+
//
|
|
1330
|
+
// Fire `onProgressiveSnapshot` exactly once, after the body stage and
|
|
1331
|
+
// its post-yield. At this point `normalizedDocument.content` carries
|
|
1332
|
+
// the full block tree with per-block runProperties already resolved
|
|
1333
|
+
// during `normalizeParsedTextDocumentAsync`. We synthesize a
|
|
1334
|
+
// throw-away `CanonicalDocumentEnvelope` using the normalized content
|
|
1335
|
+
// + empty style/review/preservation catalogs, then project a
|
|
1336
|
+
// viewport-windowed `EditorSurfaceSnapshot` (first `PROGRESSIVE_VIEWPORT_BLOCKS`
|
|
1337
|
+
// blocks real, rest as culled placeholders via the Phase 2.9 flag).
|
|
1338
|
+
//
|
|
1339
|
+
// The bench's measured signal: time from `loadDocxEditorSessionAsync`
|
|
1340
|
+
// entry to this callback's fire is `firstViewportCommitMs` — the
|
|
1341
|
+
// metric C2 gates on.
|
|
1342
|
+
//
|
|
1343
|
+
// Skipped on the Plan B short-circuit: `laycacheEnvelope !== undefined`
|
|
1344
|
+
// already completes ~376 ms faster than cold — adding a progressive
|
|
1345
|
+
// synthesis on top costs more than it saves. The short-circuit path
|
|
1346
|
+
// returns the real snapshot fast enough.
|
|
1347
|
+
if (
|
|
1348
|
+
options.onProgressiveSnapshot !== undefined &&
|
|
1349
|
+
options.laycacheEnvelope === undefined
|
|
1350
|
+
) {
|
|
1351
|
+
const provisionalDoc: CanonicalDocumentEnvelope = {
|
|
1352
|
+
...createDefaultCanonicalDocument(
|
|
1353
|
+
options.documentId,
|
|
1354
|
+
new Date().toISOString(),
|
|
1355
|
+
),
|
|
1356
|
+
content: normalizedDocument.content,
|
|
1357
|
+
};
|
|
1358
|
+
const blocksTotal = normalizedDocument.content.children.length;
|
|
1359
|
+
const blocksRealized = Math.min(
|
|
1360
|
+
PROGRESSIVE_VIEWPORT_BLOCKS,
|
|
1361
|
+
blocksTotal,
|
|
1362
|
+
);
|
|
1363
|
+
const progressiveSurface = createEditorSurfaceSnapshot(
|
|
1364
|
+
provisionalDoc,
|
|
1365
|
+
createSelectionSnapshot(0, 0),
|
|
1366
|
+
MAIN_STORY_TARGET,
|
|
1367
|
+
blocksRealized < blocksTotal
|
|
1368
|
+
? { viewportBlockRange: { start: 0, end: blocksRealized } }
|
|
1369
|
+
: undefined,
|
|
1370
|
+
);
|
|
1371
|
+
try {
|
|
1372
|
+
options.onProgressiveSnapshot({
|
|
1373
|
+
surface: progressiveSurface,
|
|
1374
|
+
phase: "viewport",
|
|
1375
|
+
blocksRealized,
|
|
1376
|
+
blocksTotal,
|
|
1377
|
+
});
|
|
1378
|
+
} catch {
|
|
1379
|
+
// A throwing consumer must not abort the load. Progressive is
|
|
1380
|
+
// a best-effort optimization; errors on the callback side
|
|
1381
|
+
// silently fall through to the normal full-commit path.
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1228
1385
|
const commentsPartPath = resolveCommentsPartPath(
|
|
1229
1386
|
sourcePackage,
|
|
1230
1387
|
mainDocumentPath,
|
|
@@ -1579,11 +1736,24 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1579
1736
|
// chart_preview nodes inline so the first snapshot already carries the
|
|
1580
1737
|
// synthesized `previewMediaId`. Fallback-safe: returning null or throwing
|
|
1581
1738
|
// is per-chart — the typed badge renders as if the adapter weren't set.
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1739
|
+
// C3b: when onChartPreviewsReady is provided, defer resolution out of
|
|
1740
|
+
// the critical path (same pattern as the short-circuit branch above).
|
|
1741
|
+
let document: CanonicalDocumentEnvelope;
|
|
1742
|
+
if (options.onChartPreviewsReady) {
|
|
1743
|
+
scheduleChartPreviewResolution(
|
|
1744
|
+
importedDocument,
|
|
1745
|
+
sourcePackage,
|
|
1746
|
+
options.hostAdapter,
|
|
1747
|
+
options.onChartPreviewsReady,
|
|
1748
|
+
);
|
|
1749
|
+
document = importedDocument as CanonicalDocumentEnvelope;
|
|
1750
|
+
} else {
|
|
1751
|
+
document = (await resolveChartPreviewsForDocument(
|
|
1752
|
+
importedDocument,
|
|
1753
|
+
sourcePackage,
|
|
1754
|
+
options.hostAdapter,
|
|
1755
|
+
)) as CanonicalDocumentEnvelope;
|
|
1756
|
+
}
|
|
1587
1757
|
const compatibility = buildCompatibilityReport({
|
|
1588
1758
|
document,
|
|
1589
1759
|
generatedAt: timestamp,
|
|
@@ -11,6 +11,7 @@ import type { BlockNode } from "../../model/canonical-document.ts";
|
|
|
11
11
|
import type {
|
|
12
12
|
CommandAppliedMeta,
|
|
13
13
|
DocumentRuntime,
|
|
14
|
+
RemoteCommandEnvelope,
|
|
14
15
|
Unsubscribe,
|
|
15
16
|
} from "../document-runtime.ts";
|
|
16
17
|
import {
|
|
@@ -200,6 +201,30 @@ export interface RuntimeCollabSyncOptions {
|
|
|
200
201
|
* - `"observer"`: all writes refused with `collab_observer_readonly`.
|
|
201
202
|
*/
|
|
202
203
|
role?: "author" | "reviewer" | "observer";
|
|
204
|
+
/**
|
|
205
|
+
* R1 — coalesce inbound remote-replay events into one commit per
|
|
206
|
+
* animation frame. When `true` (default), N events arriving within a
|
|
207
|
+
* single rAF tick (or `setTimeout(0)` in non-DOM contexts) drain via
|
|
208
|
+
* one `runtime.applyRemoteCommandBatch(...)` call → one
|
|
209
|
+
* `refreshRenderSnapshot` / one `notify`. Observer semantics change:
|
|
210
|
+
* subscribers see one `DocumentRuntimeEvent` per burst rather than
|
|
211
|
+
* one per event. Hosts that depend on per-event semantics (rare —
|
|
212
|
+
* counted-event tests, granular per-comment-add observers) opt out
|
|
213
|
+
* with `false`. The whole batch is atomic: any per-command effect
|
|
214
|
+
* collision falls back to per-event replay automatically.
|
|
215
|
+
*
|
|
216
|
+
* Defaults to `true` in both DOM and Node contexts so tests exercise
|
|
217
|
+
* the coalesced path without ceremony.
|
|
218
|
+
*/
|
|
219
|
+
coalesceRemoteReplay?: boolean;
|
|
220
|
+
/**
|
|
221
|
+
* R1 hook point for R3 idle-priority plumbing. Invoked once per
|
|
222
|
+
* scheduled drain (NOT per queued event). R3's future implementation
|
|
223
|
+
* will use this to abort any in-flight idle prerender task on
|
|
224
|
+
* `src/io/load-scheduler.ts` so the upcoming commit doesn't compete
|
|
225
|
+
* with idle work. Safe to omit; R1 functions identically without it.
|
|
226
|
+
*/
|
|
227
|
+
onRemoteReplayScheduled?: () => void;
|
|
203
228
|
}
|
|
204
229
|
|
|
205
230
|
export interface RuntimeCollabSyncHandle {
|
|
@@ -268,6 +293,48 @@ export function createRuntimeCollabSync(
|
|
|
268
293
|
let readOnly = false;
|
|
269
294
|
let baseDocFingerprint: string | null = null;
|
|
270
295
|
|
|
296
|
+
// R1 — coalesced remote-replay scheduler. Default: ON in both DOM and
|
|
297
|
+
// Node contexts. Falls back to setTimeout(0) when requestAnimationFrame
|
|
298
|
+
// is missing so tests and non-DOM hosts exercise the same drain path.
|
|
299
|
+
const useCoalescing = options.coalesceRemoteReplay !== false;
|
|
300
|
+
const replayQueue: CommandEvent[] = [];
|
|
301
|
+
let rafHandle: number | null = null;
|
|
302
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
303
|
+
|
|
304
|
+
function scheduleDrain(): void {
|
|
305
|
+
if (rafHandle !== null || timeoutHandle !== null) return;
|
|
306
|
+
options.onRemoteReplayScheduled?.();
|
|
307
|
+
if (typeof requestAnimationFrame === "function") {
|
|
308
|
+
rafHandle = requestAnimationFrame(() => {
|
|
309
|
+
rafHandle = null;
|
|
310
|
+
drainReplayQueue();
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
timeoutHandle = setTimeout(() => {
|
|
314
|
+
timeoutHandle = null;
|
|
315
|
+
drainReplayQueue();
|
|
316
|
+
}, 0);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function drainReplayQueue(): void {
|
|
321
|
+
if (replayQueue.length === 0) return;
|
|
322
|
+
const batch = replayQueue.splice(0, replayQueue.length);
|
|
323
|
+
const envelopes = batch.map(eventToEnvelope);
|
|
324
|
+
runtime.applyRemoteCommandBatch(envelopes);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function cancelPendingDrain(): void {
|
|
328
|
+
if (rafHandle !== null) {
|
|
329
|
+
if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(rafHandle);
|
|
330
|
+
rafHandle = null;
|
|
331
|
+
}
|
|
332
|
+
if (timeoutHandle !== null) {
|
|
333
|
+
clearTimeout(timeoutHandle);
|
|
334
|
+
timeoutHandle = null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
271
338
|
// Events emitted before any subscriber exists are buffered and flushed
|
|
272
339
|
// to the first subscriber. This lets hosts react to the attach-path
|
|
273
340
|
// base-doc mismatch and schema-version mismatch for pre-existing
|
|
@@ -494,6 +561,10 @@ export function createRuntimeCollabSync(
|
|
|
494
561
|
|
|
495
562
|
return {
|
|
496
563
|
destroy() {
|
|
564
|
+
// R1 — drain any queued replay events synchronously before tearing
|
|
565
|
+
// down so callers don't lose remote events on an early unmount.
|
|
566
|
+
cancelPendingDrain();
|
|
567
|
+
drainReplayQueue();
|
|
497
568
|
unsubscribeCommandApplied();
|
|
498
569
|
yEvents.unobserve(onYEventsChange);
|
|
499
570
|
yMeta.unobserve(checkFingerprintAgainstMeta);
|
|
@@ -607,19 +678,29 @@ export function createRuntimeCollabSync(
|
|
|
607
678
|
return true;
|
|
608
679
|
}
|
|
609
680
|
|
|
610
|
-
function
|
|
611
|
-
|
|
612
|
-
event.command,
|
|
613
|
-
{
|
|
681
|
+
function eventToEnvelope(event: CommandEvent): RemoteCommandEnvelope {
|
|
682
|
+
return {
|
|
683
|
+
command: event.command,
|
|
684
|
+
context: {
|
|
614
685
|
timestamp: event.timestamp,
|
|
615
686
|
documentMode: event.context.documentMode,
|
|
616
687
|
defaultAuthorId: event.context.defaultAuthorId ?? event.authorId,
|
|
617
688
|
},
|
|
618
|
-
{
|
|
689
|
+
meta: {
|
|
619
690
|
preSelection: event.context.preSelection,
|
|
620
691
|
activeStory: event.context.activeStory,
|
|
621
692
|
},
|
|
622
|
-
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function applyEventToRuntime(event: CommandEvent): void {
|
|
697
|
+
if (useCoalescing) {
|
|
698
|
+
replayQueue.push(event);
|
|
699
|
+
scheduleDrain();
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const env = eventToEnvelope(event);
|
|
703
|
+
runtime.applyRemoteCommand(env.command, env.context, env.meta);
|
|
623
704
|
}
|
|
624
705
|
}
|
|
625
706
|
|