@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
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type * as Y from "yjs";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CommentNegotiationAction,
|
|
5
|
+
CommentNegotiationSnapshot,
|
|
6
|
+
} from "../api/comment-negotiation-types.ts";
|
|
7
|
+
import type {
|
|
8
|
+
CommentPresentationAction,
|
|
9
|
+
CommentPresentationSnapshot,
|
|
10
|
+
} from "../api/comment-presentation-types.ts";
|
|
11
|
+
import type {
|
|
12
|
+
Participant,
|
|
13
|
+
ParticipantRoster,
|
|
14
|
+
} from "../api/participants-types.ts";
|
|
15
|
+
import {
|
|
16
|
+
createCollabSessionFacet,
|
|
17
|
+
type CollabSessionFacet,
|
|
18
|
+
type DispatchContext,
|
|
19
|
+
type DispatchResult,
|
|
20
|
+
} from "./collab-session-facet.ts";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Event stream emitted by the collab session bridge. Hosts (or
|
|
24
|
+
* `DocumentRuntime` in a later slice) subscribe via
|
|
25
|
+
* `bridge.subscribe(fn)` and re-broadcast into their own event bus.
|
|
26
|
+
*
|
|
27
|
+
* Intentionally additive — this does NOT extend `DocumentRuntimeEvent`.
|
|
28
|
+
* That lets the bridge land today without perturbing the already-
|
|
29
|
+
* sealed `WordReviewEditorEvent` union. When the public-types surface
|
|
30
|
+
* opens up for collab extensions (P8d / P8e), these variants flow
|
|
31
|
+
* through by union.
|
|
32
|
+
*/
|
|
33
|
+
export type CollabSessionEvent =
|
|
34
|
+
| { type: "comment_negotiation_changed"; commentIds: string[] }
|
|
35
|
+
| { type: "comment_presentation_changed"; commentIds: string[] }
|
|
36
|
+
| { type: "participants_changed"; userIds: string[] }
|
|
37
|
+
| { type: "collab_attached" }
|
|
38
|
+
| { type: "collab_detached" };
|
|
39
|
+
|
|
40
|
+
export interface CollabSessionBridge {
|
|
41
|
+
isAttached(): boolean;
|
|
42
|
+
attach(ydoc: Y.Doc): void;
|
|
43
|
+
detach(): void;
|
|
44
|
+
destroy(): void;
|
|
45
|
+
|
|
46
|
+
subscribe(listener: (event: CollabSessionEvent) => void): () => void;
|
|
47
|
+
|
|
48
|
+
getCommentNegotiationSnapshot(): CommentNegotiationSnapshot;
|
|
49
|
+
getCommentPresentationSnapshot(): CommentPresentationSnapshot;
|
|
50
|
+
getParticipantRoster(): ParticipantRoster;
|
|
51
|
+
|
|
52
|
+
dispatchCommentNegotiation(
|
|
53
|
+
action: CommentNegotiationAction,
|
|
54
|
+
ctx: DispatchContext,
|
|
55
|
+
): DispatchResult;
|
|
56
|
+
|
|
57
|
+
dispatchCommentPresentation(
|
|
58
|
+
action: CommentPresentationAction,
|
|
59
|
+
): Promise<DispatchResult>;
|
|
60
|
+
|
|
61
|
+
upsertParticipant(entry: Participant): Participant | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CreateCollabSessionBridgeOptions {
|
|
65
|
+
ydoc?: Y.Doc;
|
|
66
|
+
/**
|
|
67
|
+
* Optional pre-built facet. Primarily for tests. When omitted the
|
|
68
|
+
* bridge creates its own via `createCollabSessionFacet`.
|
|
69
|
+
*/
|
|
70
|
+
facet?: CollabSessionFacet;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createCollabSessionBridge(
|
|
74
|
+
options: CreateCollabSessionBridgeOptions = {},
|
|
75
|
+
): CollabSessionBridge {
|
|
76
|
+
const facet = options.facet ?? createCollabSessionFacet(options.ydoc);
|
|
77
|
+
const listeners = new Set<(event: CollabSessionEvent) => void>();
|
|
78
|
+
const facetSubs = new Set<() => void>();
|
|
79
|
+
|
|
80
|
+
const emit = (event: CollabSessionEvent): void => {
|
|
81
|
+
for (const fn of [...listeners]) fn(event);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const wireFacetSubscriptions = (): void => {
|
|
85
|
+
if (!facet.isAttached()) return;
|
|
86
|
+
facetSubs.add(
|
|
87
|
+
facet.subscribe("negotiation", (commentIds) =>
|
|
88
|
+
emit({ type: "comment_negotiation_changed", commentIds }),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
facetSubs.add(
|
|
92
|
+
facet.subscribe("presentation", (commentIds) =>
|
|
93
|
+
emit({ type: "comment_presentation_changed", commentIds }),
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
facetSubs.add(
|
|
97
|
+
facet.subscribe("participants", (userIds) =>
|
|
98
|
+
emit({ type: "participants_changed", userIds }),
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const unwireFacetSubscriptions = (): void => {
|
|
104
|
+
for (const off of facetSubs) off();
|
|
105
|
+
facetSubs.clear();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (facet.isAttached()) wireFacetSubscriptions();
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
isAttached: () => facet.isAttached(),
|
|
112
|
+
|
|
113
|
+
attach(ydoc) {
|
|
114
|
+
const wasAttached = facet.isAttached();
|
|
115
|
+
unwireFacetSubscriptions();
|
|
116
|
+
facet.attach(ydoc);
|
|
117
|
+
wireFacetSubscriptions();
|
|
118
|
+
if (!wasAttached) emit({ type: "collab_attached" });
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
detach() {
|
|
122
|
+
if (!facet.isAttached()) return;
|
|
123
|
+
unwireFacetSubscriptions();
|
|
124
|
+
facet.detach();
|
|
125
|
+
emit({ type: "collab_detached" });
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
destroy() {
|
|
129
|
+
unwireFacetSubscriptions();
|
|
130
|
+
facet.destroy();
|
|
131
|
+
listeners.clear();
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
subscribe(listener) {
|
|
135
|
+
listeners.add(listener);
|
|
136
|
+
return () => {
|
|
137
|
+
listeners.delete(listener);
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
getCommentNegotiationSnapshot: () => facet.getCommentNegotiationSnapshot(),
|
|
142
|
+
getCommentPresentationSnapshot: () => facet.getCommentPresentationSnapshot(),
|
|
143
|
+
getParticipantRoster: () => facet.getParticipantRoster(),
|
|
144
|
+
|
|
145
|
+
dispatchCommentNegotiation(action, ctx) {
|
|
146
|
+
return facet.dispatchCommentNegotiation(action, ctx);
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async dispatchCommentPresentation(action) {
|
|
150
|
+
return facet.dispatchCommentPresentation(action);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
upsertParticipant(entry) {
|
|
154
|
+
return facet.upsertParticipant(entry);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CommentNegotiationAction,
|
|
5
|
+
CommentNegotiationSnapshot,
|
|
6
|
+
} from "../api/comment-negotiation-types.ts";
|
|
7
|
+
import type {
|
|
8
|
+
CommentPresentationAction,
|
|
9
|
+
CommentPresentationSnapshot,
|
|
10
|
+
} from "../api/comment-presentation-types.ts";
|
|
11
|
+
import type {
|
|
12
|
+
Participant,
|
|
13
|
+
ParticipantRoster,
|
|
14
|
+
} from "../api/participants-types.ts";
|
|
15
|
+
import {
|
|
16
|
+
createNegotiationSync,
|
|
17
|
+
type NegotiationSyncHandle,
|
|
18
|
+
} from "./comment-negotiation-sync.ts";
|
|
19
|
+
import {
|
|
20
|
+
createInitialEntry,
|
|
21
|
+
reduceNegotiation,
|
|
22
|
+
} from "./comment-negotiation.ts";
|
|
23
|
+
import type {
|
|
24
|
+
CollabBlockReason,
|
|
25
|
+
NegotiationRole,
|
|
26
|
+
} from "../api/comment-negotiation-types.ts";
|
|
27
|
+
import {
|
|
28
|
+
createPresentationStore,
|
|
29
|
+
type PresentationStoreHandle,
|
|
30
|
+
} from "./comment-presentation.ts";
|
|
31
|
+
import {
|
|
32
|
+
createParticipantRoster,
|
|
33
|
+
type ParticipantRosterStore,
|
|
34
|
+
} from "./participants.ts";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bundle of the three Y.Map-backed collab stores.
|
|
38
|
+
*
|
|
39
|
+
* Lazy: if no `Y.Doc` is attached the facet is "detached" — every
|
|
40
|
+
* snapshot returns an empty shape and every dispatch is a no-op that
|
|
41
|
+
* resolves without mutating anything. This lets `DocumentRuntime`
|
|
42
|
+
* expose the surface unconditionally without forcing hosts that don't
|
|
43
|
+
* want collab to supply a `Y.Doc`.
|
|
44
|
+
*
|
|
45
|
+
* Connect / disconnect is idempotent. Reconnecting with a different
|
|
46
|
+
* `Y.Doc` tears down the existing subscriptions first.
|
|
47
|
+
*/
|
|
48
|
+
export interface CollabSessionFacet {
|
|
49
|
+
isAttached(): boolean;
|
|
50
|
+
|
|
51
|
+
getCommentNegotiationSnapshot(): CommentNegotiationSnapshot;
|
|
52
|
+
getCommentPresentationSnapshot(): CommentPresentationSnapshot;
|
|
53
|
+
getParticipantRoster(): ParticipantRoster;
|
|
54
|
+
|
|
55
|
+
dispatchCommentNegotiation(
|
|
56
|
+
action: CommentNegotiationAction,
|
|
57
|
+
ctx: DispatchContext,
|
|
58
|
+
): DispatchResult;
|
|
59
|
+
|
|
60
|
+
dispatchCommentPresentation(
|
|
61
|
+
action: CommentPresentationAction,
|
|
62
|
+
): Promise<DispatchResult>;
|
|
63
|
+
|
|
64
|
+
upsertParticipant(entry: Participant): Participant | undefined;
|
|
65
|
+
|
|
66
|
+
subscribe(
|
|
67
|
+
kind: "negotiation" | "presentation" | "participants",
|
|
68
|
+
fn: (changedIds: string[]) => void,
|
|
69
|
+
): () => void;
|
|
70
|
+
|
|
71
|
+
attach(ydoc: Y.Doc): void;
|
|
72
|
+
detach(): void;
|
|
73
|
+
destroy(): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface DispatchContext {
|
|
77
|
+
role: NegotiationRole;
|
|
78
|
+
now: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type DispatchResult =
|
|
82
|
+
| { ok: true }
|
|
83
|
+
| { ok: false; reason: CollabBlockReason };
|
|
84
|
+
|
|
85
|
+
const EMPTY_NEGOTIATION: CommentNegotiationSnapshot = {
|
|
86
|
+
schemaVersion: 1,
|
|
87
|
+
entries: [],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const EMPTY_PRESENTATION: CommentPresentationSnapshot = {
|
|
91
|
+
schemaVersion: 1,
|
|
92
|
+
entries: [],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const EMPTY_ROSTER: ParticipantRoster = {
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
entries: [],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
interface AttachedStores {
|
|
101
|
+
ydoc: Y.Doc;
|
|
102
|
+
negotiation: NegotiationSyncHandle;
|
|
103
|
+
presentation: PresentationStoreHandle;
|
|
104
|
+
participants: ParticipantRosterStore;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function createCollabSessionFacet(
|
|
108
|
+
initialYdoc?: Y.Doc,
|
|
109
|
+
): CollabSessionFacet {
|
|
110
|
+
let stores: AttachedStores | null = null;
|
|
111
|
+
let destroyed = false;
|
|
112
|
+
|
|
113
|
+
const unwire = (): void => {
|
|
114
|
+
if (!stores) return;
|
|
115
|
+
stores.negotiation.destroy();
|
|
116
|
+
stores.presentation.destroy();
|
|
117
|
+
stores.participants.destroy();
|
|
118
|
+
stores = null;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const wire = (ydoc: Y.Doc): void => {
|
|
122
|
+
if (destroyed) return;
|
|
123
|
+
unwire();
|
|
124
|
+
stores = {
|
|
125
|
+
ydoc,
|
|
126
|
+
negotiation: createNegotiationSync(ydoc),
|
|
127
|
+
presentation: createPresentationStore(ydoc),
|
|
128
|
+
participants: createParticipantRoster(ydoc),
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (initialYdoc) wire(initialYdoc);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
isAttached: () => stores !== null,
|
|
136
|
+
|
|
137
|
+
getCommentNegotiationSnapshot: () =>
|
|
138
|
+
stores ? stores.negotiation.snapshot() : EMPTY_NEGOTIATION,
|
|
139
|
+
|
|
140
|
+
getCommentPresentationSnapshot: () =>
|
|
141
|
+
stores ? stores.presentation.snapshot() : EMPTY_PRESENTATION,
|
|
142
|
+
|
|
143
|
+
getParticipantRoster: () =>
|
|
144
|
+
stores ? stores.participants.snapshot() : EMPTY_ROSTER,
|
|
145
|
+
|
|
146
|
+
dispatchCommentNegotiation(action, ctx) {
|
|
147
|
+
if (!stores) return { ok: false, reason: "collab_not_attached" };
|
|
148
|
+
const prev =
|
|
149
|
+
stores.negotiation.readEntry(action.commentId) ??
|
|
150
|
+
createInitialEntry(action.commentId);
|
|
151
|
+
const result = reduceNegotiation(prev, action, ctx);
|
|
152
|
+
if (!result.ok) return { ok: false, reason: result.reason };
|
|
153
|
+
stores.negotiation.writeEntry(result.entry);
|
|
154
|
+
return { ok: true };
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
async dispatchCommentPresentation(action) {
|
|
158
|
+
if (!stores) return { ok: false, reason: "collab_not_attached" };
|
|
159
|
+
await stores.presentation.apply(action);
|
|
160
|
+
return { ok: true };
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
upsertParticipant(entry) {
|
|
164
|
+
if (!stores) return undefined;
|
|
165
|
+
return stores.participants.upsert(entry);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
subscribe(kind, fn) {
|
|
169
|
+
if (!stores) return () => {};
|
|
170
|
+
switch (kind) {
|
|
171
|
+
case "negotiation":
|
|
172
|
+
return stores.negotiation.subscribe(fn);
|
|
173
|
+
case "presentation":
|
|
174
|
+
return stores.presentation.subscribe(fn);
|
|
175
|
+
case "participants":
|
|
176
|
+
return stores.participants.subscribe(fn);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
attach(ydoc) {
|
|
181
|
+
wire(ydoc);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
detach() {
|
|
185
|
+
unwire();
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
destroy() {
|
|
189
|
+
destroyed = true;
|
|
190
|
+
unwire();
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import type * as Y from "yjs";
|
|
2
|
+
import type { Awareness } from "y-protocols/awareness";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
AwarenessIdentity,
|
|
6
|
+
CollabPosture,
|
|
7
|
+
PresenceSnapshot,
|
|
8
|
+
TransportStatus,
|
|
9
|
+
} from "../api/awareness-identity-types.ts";
|
|
10
|
+
import type {
|
|
11
|
+
ExternalCustodyResolver,
|
|
12
|
+
} from "../api/external-custody-types.ts";
|
|
13
|
+
import type {
|
|
14
|
+
PayloadSignature,
|
|
15
|
+
PayloadSigner,
|
|
16
|
+
PayloadVerifier,
|
|
17
|
+
} from "../io/ooxml/payload-signature.ts";
|
|
18
|
+
import {
|
|
19
|
+
clearLocalIdentity,
|
|
20
|
+
getCollabPosture,
|
|
21
|
+
getPresenceSnapshot,
|
|
22
|
+
setLocalIdentity,
|
|
23
|
+
} from "./awareness-identity.ts";
|
|
24
|
+
import {
|
|
25
|
+
createCollabSessionBridge,
|
|
26
|
+
type CollabSessionBridge,
|
|
27
|
+
type CollabSessionEvent,
|
|
28
|
+
} from "./collab-session-bridge.ts";
|
|
29
|
+
import type { CommentNegotiationAction } from "../api/comment-negotiation-types.ts";
|
|
30
|
+
import type { Participant } from "../api/participants-types.ts";
|
|
31
|
+
import type {
|
|
32
|
+
RuntimeSendToExternalArgs,
|
|
33
|
+
RuntimeSendToExternalResult,
|
|
34
|
+
} from "./external-send-runtime.ts";
|
|
35
|
+
import { runtimeSendToExternal } from "./external-send-runtime.ts";
|
|
36
|
+
import {
|
|
37
|
+
createTamperGate,
|
|
38
|
+
type MetadataIntegrity,
|
|
39
|
+
type TamperGate,
|
|
40
|
+
type TamperGateEvent,
|
|
41
|
+
} from "./tamper-gate.ts";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Event union emitted by the session. Merges the bridge's event
|
|
45
|
+
* stream with the tamper-gate stream into a single typed channel so
|
|
46
|
+
* hosts subscribe once.
|
|
47
|
+
*/
|
|
48
|
+
export type CollabSessionEventOrIntegrity =
|
|
49
|
+
| CollabSessionEvent
|
|
50
|
+
| TamperGateEvent;
|
|
51
|
+
|
|
52
|
+
export interface CollabSessionOptions {
|
|
53
|
+
ydoc?: Y.Doc;
|
|
54
|
+
awareness?: Awareness;
|
|
55
|
+
identity?: AwarenessIdentity;
|
|
56
|
+
signer: PayloadSigner;
|
|
57
|
+
verifier?: PayloadVerifier;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AttachPayloadArgs {
|
|
61
|
+
payloadXml: string;
|
|
62
|
+
signature: PayloadSignature | undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type SendToExternalCallArgs = Omit<
|
|
66
|
+
RuntimeSendToExternalArgs,
|
|
67
|
+
"bridge" | "tamperGate" | "signer" | "role" | "resolver"
|
|
68
|
+
> & {
|
|
69
|
+
role?: "author" | "reviewer" | "observer";
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Unified collab session — composes the P8a–P8f slices so hosts call
|
|
74
|
+
* `createCollabSession(...)` instead of wiring the bridge, tamper
|
|
75
|
+
* gate, resolver and identity channel by hand.
|
|
76
|
+
*/
|
|
77
|
+
export interface CollabSession {
|
|
78
|
+
readonly bridge: CollabSessionBridge;
|
|
79
|
+
readonly tamperGate: TamperGate;
|
|
80
|
+
|
|
81
|
+
isAttached(): boolean;
|
|
82
|
+
getMetadataIntegrity(): MetadataIntegrity;
|
|
83
|
+
|
|
84
|
+
attach(args: {
|
|
85
|
+
ydoc?: Y.Doc;
|
|
86
|
+
awareness?: Awareness;
|
|
87
|
+
identity?: AwarenessIdentity;
|
|
88
|
+
payload?: AttachPayloadArgs;
|
|
89
|
+
}): Promise<void>;
|
|
90
|
+
detach(): void;
|
|
91
|
+
destroy(): void;
|
|
92
|
+
|
|
93
|
+
// Negotiation + presentation + roster
|
|
94
|
+
dispatchCommentNegotiation: CollabSessionBridge["dispatchCommentNegotiation"];
|
|
95
|
+
dispatchCommentPresentation: CollabSessionBridge["dispatchCommentPresentation"];
|
|
96
|
+
upsertParticipant(entry: Participant): Participant | undefined;
|
|
97
|
+
|
|
98
|
+
getCommentNegotiationSnapshot: CollabSessionBridge["getCommentNegotiationSnapshot"];
|
|
99
|
+
getCommentPresentationSnapshot: CollabSessionBridge["getCommentPresentationSnapshot"];
|
|
100
|
+
getParticipantRoster: CollabSessionBridge["getParticipantRoster"];
|
|
101
|
+
|
|
102
|
+
// Presence + posture
|
|
103
|
+
setLocalIdentity(identity: AwarenessIdentity): void;
|
|
104
|
+
clearLocalIdentity(): void;
|
|
105
|
+
getPresenceSnapshot(opts?: {
|
|
106
|
+
transportStatus?: TransportStatus;
|
|
107
|
+
queuedLocalEvents?: number;
|
|
108
|
+
activeStoryFilter?: string;
|
|
109
|
+
}): PresenceSnapshot;
|
|
110
|
+
getCollabPosture(opts?: { transportStatus?: TransportStatus }): CollabPosture;
|
|
111
|
+
|
|
112
|
+
// Tamper gate
|
|
113
|
+
acknowledgeMetadataTampering(): void;
|
|
114
|
+
|
|
115
|
+
// External-send
|
|
116
|
+
registerExternalCustodyResolver(resolver: ExternalCustodyResolver | undefined): void;
|
|
117
|
+
sendToExternal(args: SendToExternalCallArgs): Promise<RuntimeSendToExternalResult>;
|
|
118
|
+
|
|
119
|
+
subscribe(listener: (event: CollabSessionEventOrIntegrity) => void): () => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type NegotiationAction = CommentNegotiationAction;
|
|
123
|
+
|
|
124
|
+
export function createCollabSession(
|
|
125
|
+
options: CollabSessionOptions,
|
|
126
|
+
): CollabSession {
|
|
127
|
+
const bridge = createCollabSessionBridge(
|
|
128
|
+
options.ydoc ? { ydoc: options.ydoc } : {},
|
|
129
|
+
);
|
|
130
|
+
const tamperGate = createTamperGate(
|
|
131
|
+
options.verifier ? { verifier: options.verifier } : {},
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
let awareness: Awareness | undefined = options.awareness;
|
|
135
|
+
let resolver: ExternalCustodyResolver | undefined;
|
|
136
|
+
let destroyed = false;
|
|
137
|
+
|
|
138
|
+
const listeners = new Set<(event: CollabSessionEventOrIntegrity) => void>();
|
|
139
|
+
const emit = (event: CollabSessionEventOrIntegrity): void => {
|
|
140
|
+
for (const fn of [...listeners]) fn(event);
|
|
141
|
+
};
|
|
142
|
+
bridge.subscribe(emit);
|
|
143
|
+
tamperGate.subscribe(emit);
|
|
144
|
+
|
|
145
|
+
if (options.identity && awareness) {
|
|
146
|
+
setLocalIdentity(awareness, options.identity);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
bridge,
|
|
151
|
+
tamperGate,
|
|
152
|
+
|
|
153
|
+
isAttached: () => bridge.isAttached(),
|
|
154
|
+
getMetadataIntegrity: () => tamperGate.state,
|
|
155
|
+
|
|
156
|
+
async attach(args) {
|
|
157
|
+
if (destroyed) return;
|
|
158
|
+
if (args.ydoc) bridge.attach(args.ydoc);
|
|
159
|
+
if (args.awareness) awareness = args.awareness;
|
|
160
|
+
if (args.identity && awareness) {
|
|
161
|
+
setLocalIdentity(awareness, args.identity);
|
|
162
|
+
}
|
|
163
|
+
if (args.payload) {
|
|
164
|
+
await tamperGate.attach(args.payload);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
detach() {
|
|
169
|
+
bridge.detach();
|
|
170
|
+
if (awareness) clearLocalIdentity(awareness);
|
|
171
|
+
tamperGate.detach();
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
destroy() {
|
|
175
|
+
destroyed = true;
|
|
176
|
+
bridge.destroy();
|
|
177
|
+
tamperGate.destroy();
|
|
178
|
+
listeners.clear();
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
dispatchCommentNegotiation(action: NegotiationAction, ctx) {
|
|
182
|
+
return bridge.dispatchCommentNegotiation(action, ctx);
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
async dispatchCommentPresentation(action) {
|
|
186
|
+
return bridge.dispatchCommentPresentation(action);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
upsertParticipant(entry) {
|
|
190
|
+
return bridge.upsertParticipant(entry);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
getCommentNegotiationSnapshot: () => bridge.getCommentNegotiationSnapshot(),
|
|
194
|
+
getCommentPresentationSnapshot: () => bridge.getCommentPresentationSnapshot(),
|
|
195
|
+
getParticipantRoster: () => bridge.getParticipantRoster(),
|
|
196
|
+
|
|
197
|
+
setLocalIdentity(identity) {
|
|
198
|
+
if (!awareness) {
|
|
199
|
+
throw new Error("collab session: no awareness attached");
|
|
200
|
+
}
|
|
201
|
+
setLocalIdentity(awareness, identity);
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
clearLocalIdentity() {
|
|
205
|
+
if (!awareness) return;
|
|
206
|
+
clearLocalIdentity(awareness);
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
getPresenceSnapshot(opts = {}) {
|
|
210
|
+
const args: Parameters<typeof getPresenceSnapshot>[0] = {};
|
|
211
|
+
if (awareness) args.awareness = awareness;
|
|
212
|
+
if (opts.transportStatus) args.transportStatus = opts.transportStatus;
|
|
213
|
+
if (opts.queuedLocalEvents !== undefined) {
|
|
214
|
+
args.queuedLocalEvents = opts.queuedLocalEvents;
|
|
215
|
+
}
|
|
216
|
+
if (opts.activeStoryFilter !== undefined) {
|
|
217
|
+
args.activeStoryFilter = opts.activeStoryFilter;
|
|
218
|
+
}
|
|
219
|
+
return getPresenceSnapshot(args);
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
getCollabPosture(opts = {}) {
|
|
223
|
+
const args: Parameters<typeof getCollabPosture>[0] = {};
|
|
224
|
+
if (awareness) args.awareness = awareness;
|
|
225
|
+
if (opts.transportStatus) args.transportStatus = opts.transportStatus;
|
|
226
|
+
return getCollabPosture(args);
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
acknowledgeMetadataTampering() {
|
|
230
|
+
tamperGate.acknowledge();
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
registerExternalCustodyResolver(next) {
|
|
234
|
+
resolver = next;
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
async sendToExternal(args) {
|
|
238
|
+
if (!resolver) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
"collab session: registerExternalCustodyResolver(...) not called",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const resolvedRole =
|
|
244
|
+
args.role ??
|
|
245
|
+
(awareness
|
|
246
|
+
? getCollabPosture({ awareness }).role
|
|
247
|
+
: "author");
|
|
248
|
+
return runtimeSendToExternal({
|
|
249
|
+
bridge,
|
|
250
|
+
tamperGate,
|
|
251
|
+
signer: options.signer,
|
|
252
|
+
payloadXml: args.payloadXml,
|
|
253
|
+
role: resolvedRole,
|
|
254
|
+
originDocumentId: args.originDocumentId,
|
|
255
|
+
originPayloadId: args.originPayloadId,
|
|
256
|
+
originContentHash: args.originContentHash,
|
|
257
|
+
resolver,
|
|
258
|
+
recipient: args.recipient,
|
|
259
|
+
sentBy: args.sentBy,
|
|
260
|
+
archiveRef: args.archiveRef,
|
|
261
|
+
...(args.custodyId !== undefined ? { custodyId: args.custodyId } : {}),
|
|
262
|
+
...(args.now !== undefined ? { now: args.now } : {}),
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
subscribe(listener) {
|
|
267
|
+
listeners.add(listener);
|
|
268
|
+
return () => {
|
|
269
|
+
listeners.delete(listener);
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CommentNegotiationEntry,
|
|
5
|
+
CommentNegotiationSnapshot,
|
|
6
|
+
} from "../api/comment-negotiation-types.ts";
|
|
7
|
+
|
|
8
|
+
export interface NegotiationSyncHandle {
|
|
9
|
+
readEntry(commentId: string): CommentNegotiationEntry | undefined;
|
|
10
|
+
writeEntry(entry: CommentNegotiationEntry): void;
|
|
11
|
+
snapshot(): CommentNegotiationSnapshot;
|
|
12
|
+
subscribe(fn: (changedIds: string[]) => void): () => void;
|
|
13
|
+
destroy(): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MAP_KEY = "commentNegotiation";
|
|
17
|
+
|
|
18
|
+
export function createNegotiationSync(ydoc: Y.Doc): NegotiationSyncHandle {
|
|
19
|
+
const yMap = ydoc.getMap<CommentNegotiationEntry>(MAP_KEY);
|
|
20
|
+
const listeners = new Set<(ids: string[]) => void>();
|
|
21
|
+
|
|
22
|
+
const onChange = (event: Y.YMapEvent<CommentNegotiationEntry>): void => {
|
|
23
|
+
if (listeners.size === 0) return;
|
|
24
|
+
const ids = Array.from(event.keysChanged);
|
|
25
|
+
for (const fn of listeners) fn(ids);
|
|
26
|
+
};
|
|
27
|
+
yMap.observe(onChange);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
readEntry(commentId) {
|
|
31
|
+
const entry = yMap.get(commentId);
|
|
32
|
+
return entry ? cloneEntry(entry) : undefined;
|
|
33
|
+
},
|
|
34
|
+
writeEntry(entry) {
|
|
35
|
+
yMap.set(entry.commentId, cloneEntry(entry));
|
|
36
|
+
},
|
|
37
|
+
snapshot() {
|
|
38
|
+
return {
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
entries: Array.from(yMap.values()).map(cloneEntry),
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
subscribe(fn) {
|
|
44
|
+
listeners.add(fn);
|
|
45
|
+
return () => {
|
|
46
|
+
listeners.delete(fn);
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
destroy() {
|
|
50
|
+
yMap.unobserve(onChange);
|
|
51
|
+
listeners.clear();
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function cloneEntry(entry: CommentNegotiationEntry): CommentNegotiationEntry {
|
|
57
|
+
const clone: CommentNegotiationEntry = {
|
|
58
|
+
commentId: entry.commentId,
|
|
59
|
+
state: entry.state,
|
|
60
|
+
requiredApprovers: [...entry.requiredApprovers],
|
|
61
|
+
votes: entry.votes.map((v) => ({ ...v })),
|
|
62
|
+
counterProposals: entry.counterProposals.map((p) => {
|
|
63
|
+
const copy: CommentNegotiationEntry["counterProposals"][number] = {
|
|
64
|
+
id: p.id,
|
|
65
|
+
authorId: p.authorId,
|
|
66
|
+
createdAt: p.createdAt,
|
|
67
|
+
body: p.body,
|
|
68
|
+
};
|
|
69
|
+
if (p.proposedRangeEdit) copy.proposedRangeEdit = { ...p.proposedRangeEdit };
|
|
70
|
+
if (p.supersededBy !== undefined) copy.supersededBy = p.supersededBy;
|
|
71
|
+
return copy;
|
|
72
|
+
}),
|
|
73
|
+
history: entry.history.map((h) => {
|
|
74
|
+
const row: CommentNegotiationEntry["history"][number] = {
|
|
75
|
+
from: h.from,
|
|
76
|
+
to: h.to,
|
|
77
|
+
actorId: h.actorId,
|
|
78
|
+
at: h.at,
|
|
79
|
+
action: h.action,
|
|
80
|
+
};
|
|
81
|
+
if (h.reasonCode !== undefined) row.reasonCode = h.reasonCode;
|
|
82
|
+
return row;
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
85
|
+
if (entry.acceptedProposalId !== undefined) {
|
|
86
|
+
clone.acceptedProposalId = entry.acceptedProposalId;
|
|
87
|
+
}
|
|
88
|
+
if (entry.lockedAt !== undefined) clone.lockedAt = entry.lockedAt;
|
|
89
|
+
if (entry.lockedBy !== undefined) clone.lockedBy = entry.lockedBy;
|
|
90
|
+
return clone;
|
|
91
|
+
}
|