@p10node/office-engine 0.0.0 → 0.1.1
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/LICENSE +21 -0
- package/README.md +65 -1
- package/dist/DocViewer-kfAVEXOO.js +10533 -0
- package/dist/DocViewer-kfAVEXOO.js.map +1 -0
- package/dist/THIRD_PARTY_LICENSES.txt +79 -0
- package/dist/assets/worker-DbLPJnSo.js.map +1 -0
- package/dist/collab-BzN1ZI-z.js +1984 -0
- package/dist/collab-BzN1ZI-z.js.map +1 -0
- package/dist/collab.js +2 -0
- package/dist/core.js +2 -0
- package/dist/default-worker-f546VALp.js +9196 -0
- package/dist/default-worker-f546VALp.js.map +1 -0
- package/dist/embed.js +91 -0
- package/dist/embed.js.map +1 -0
- package/dist/index.js +5 -0
- package/dist/office-engine.embed.js +81 -0
- package/dist/office-engine.worker.js +9092 -0
- package/dist/types/collab/codec.d.ts +3 -0
- package/dist/types/collab/docx.d.ts +166 -0
- package/dist/types/collab/hosts.d.ts +24 -0
- package/dist/types/collab/hub.d.ts +41 -0
- package/dist/types/collab/index.d.ts +43 -0
- package/dist/types/collab/pptx-ops.d.ts +60 -0
- package/dist/types/collab/pptx-worker.d.ts +35 -0
- package/dist/types/collab/pptx.d.ts +47 -0
- package/dist/types/collab/protocol.d.ts +76 -0
- package/dist/types/collab/react.d.ts +23 -0
- package/dist/types/collab/session.d.ts +115 -0
- package/dist/types/collab/transport.d.ts +47 -0
- package/dist/types/collab/xlsx-ops.d.ts +72 -0
- package/dist/types/collab/xlsx-worker.d.ts +29 -0
- package/dist/types/collab/xlsx.d.ts +51 -0
- package/dist/types/core/color.d.ts +25 -0
- package/dist/types/core/convert.d.ts +29 -0
- package/dist/types/core/default-worker.d.ts +1 -0
- package/dist/types/core/docx/numbering.d.ts +33 -0
- package/dist/types/core/docx/parser.d.ts +126 -0
- package/dist/types/core/docx/spec.d.ts +4 -0
- package/dist/types/core/docx/styles.d.ts +63 -0
- package/dist/types/core/docx/types.d.ts +325 -0
- package/dist/types/core/docx/writer.d.ts +35 -0
- package/dist/types/core/fonts.d.ts +1 -0
- package/dist/types/core/index.d.ts +15 -0
- package/dist/types/core/opc.d.ts +56 -0
- package/dist/types/core/pptx/edit.d.ts +10 -0
- package/dist/types/core/pptx/parser.d.ts +52 -0
- package/dist/types/core/pptx/types.d.ts +238 -0
- package/dist/types/core/raw.d.ts +40 -0
- package/dist/types/core/source.d.ts +65 -0
- package/dist/types/core/theme.d.ts +11 -0
- package/dist/types/core/units.d.ts +7 -0
- package/dist/types/core/worker/client.d.ts +48 -0
- package/dist/types/core/worker/engine.d.ts +27 -0
- package/dist/types/core/worker/protocol.d.ts +112 -0
- package/dist/types/core/worker/worker.d.ts +1 -0
- package/dist/types/core/xlsx/calc.d.ts +31 -0
- package/dist/types/core/xlsx/editor.d.ts +95 -0
- package/dist/types/core/xlsx/formula/assist.d.ts +35 -0
- package/dist/types/core/xlsx/formula/catalog.d.ts +14 -0
- package/dist/types/core/xlsx/formula/dates.d.ts +23 -0
- package/dist/types/core/xlsx/formula/evaluate.d.ts +33 -0
- package/dist/types/core/xlsx/formula/fn-date.d.ts +2 -0
- package/dist/types/core/xlsx/formula/fn-logic.d.ts +3 -0
- package/dist/types/core/xlsx/formula/fn-lookup.d.ts +3 -0
- package/dist/types/core/xlsx/formula/fn-math.d.ts +3 -0
- package/dist/types/core/xlsx/formula/fn-stats.d.ts +3 -0
- package/dist/types/core/xlsx/formula/fn-text.d.ts +3 -0
- package/dist/types/core/xlsx/formula/fn.d.ts +64 -0
- package/dist/types/core/xlsx/formula/format.d.ts +5 -0
- package/dist/types/core/xlsx/formula/functions.d.ts +4 -0
- package/dist/types/core/xlsx/formula/index.d.ts +17 -0
- package/dist/types/core/xlsx/formula/lexer.d.ts +54 -0
- package/dist/types/core/xlsx/formula/parser.d.ts +53 -0
- package/dist/types/core/xlsx/formula/refs.d.ts +23 -0
- package/dist/types/core/xlsx/formula/transform.d.ts +21 -0
- package/dist/types/core/xlsx/formula/types.d.ts +59 -0
- package/dist/types/core/xlsx/formula/values.d.ts +49 -0
- package/dist/types/core/xlsx/numfmt.d.ts +11 -0
- package/dist/types/core/xlsx/structure.d.ts +31 -0
- package/dist/types/core/xlsx/styles.d.ts +17 -0
- package/dist/types/core/xlsx/types.d.ts +193 -0
- package/dist/types/core/xlsx/workbook.d.ts +94 -0
- package/dist/types/core/xml.d.ts +80 -0
- package/dist/types/core/zip.d.ts +35 -0
- package/dist/types/core/zipwrite.d.ts +11 -0
- package/dist/types/embed.d.ts +30 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/react/DocViewer.d.ts +61 -0
- package/dist/types/react/common/ContextMenu.d.ts +21 -0
- package/dist/types/react/common/Dialog.d.ts +35 -0
- package/dist/types/react/common/Slots.d.ts +38 -0
- package/dist/types/react/common/Status.d.ts +6 -0
- package/dist/types/react/common/ZoomControl.d.ts +6 -0
- package/dist/types/react/common/hooks.d.ts +20 -0
- package/dist/types/react/docx/DocxEditor.d.ts +172 -0
- package/dist/types/react/docx/DocxToolbar.d.ts +19 -0
- package/dist/types/react/docx/DocxView.d.ts +8 -0
- package/dist/types/react/docx/FindBar.d.ts +22 -0
- package/dist/types/react/docx/html.d.ts +8 -0
- package/dist/types/react/docx/render.d.ts +24 -0
- package/dist/types/react/docx/tables.d.ts +20 -0
- package/dist/types/react/edit/Toolbar.d.ts +134 -0
- package/dist/types/react/edit/input.d.ts +81 -0
- package/dist/types/react/edit/rich.d.ts +40 -0
- package/dist/types/react/file/DetailsDialog.d.ts +7 -0
- package/dist/types/react/file/FileMenu.d.ts +48 -0
- package/dist/types/react/file/importFile.d.ts +10 -0
- package/dist/types/react/file/print.d.ts +16 -0
- package/dist/types/react/file/text.d.ts +10 -0
- package/dist/types/react/injectStyles.d.ts +4 -0
- package/dist/types/react/pptx/PptxEditor.d.ts +95 -0
- package/dist/types/react/pptx/PptxToolbar.d.ts +36 -0
- package/dist/types/react/pptx/PptxView.d.ts +24 -0
- package/dist/types/react/pptx/Presenter.d.ts +9 -0
- package/dist/types/react/pptx/Slide.d.ts +28 -0
- package/dist/types/react/pptx/geometry.d.ts +5 -0
- package/dist/types/react/useDocument.d.ts +11 -0
- package/dist/types/react/xlsx/XlsxToolbar.d.ts +27 -0
- package/dist/types/react/xlsx/XlsxView.d.ts +18 -0
- package/dist/types/react/xlsx/axis.d.ts +19 -0
- package/dist/types/react/xlsx/print.d.ts +17 -0
- package/dist/xlsx-ops-CsjNpMI0.js +244 -0
- package/dist/xlsx-ops-CsjNpMI0.js.map +1 -0
- package/package.json +75 -2
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOCX collaboration: operations on the main-thread paragraph model (DocxEditor),
|
|
3
|
+
* which mirrors every change to the worker itself.
|
|
4
|
+
*
|
|
5
|
+
* - `text` splice inside one paragraph (typing, deleting, inline formatting) — merges
|
|
6
|
+
* character-wise with concurrent edits of the same paragraph
|
|
7
|
+
* - `paras` contiguous paragraphs replaced (Enter, join, paste, paragraph formatting…) — last writer wins
|
|
8
|
+
* - `blocks` top-level blocks replaced (tables)
|
|
9
|
+
* - `image` / `link` package resources, shared by key (relationship ids differ per client)
|
|
10
|
+
* - `call` other document-level worker calls (e.g. adding a built-in style), run in hub order
|
|
11
|
+
*/
|
|
12
|
+
import type { Block, Inline, Paragraph } from '../core/docx/types';
|
|
13
|
+
import type { DocumentClient } from '../core/worker/client';
|
|
14
|
+
import type { Methods } from '../core/worker/protocol';
|
|
15
|
+
import type { DocState, DocxEditor } from '../react/docx/DocxEditor';
|
|
16
|
+
import type { Adapter, ApplyResult } from './session';
|
|
17
|
+
export interface DocxHost {
|
|
18
|
+
editor: DocxEditor;
|
|
19
|
+
st: DocState;
|
|
20
|
+
}
|
|
21
|
+
/** top-level block identity: paragraph id, or table (regenerated id / original body item) */
|
|
22
|
+
export type BKey = {
|
|
23
|
+
p: number;
|
|
24
|
+
} | {
|
|
25
|
+
tid?: number;
|
|
26
|
+
item?: number;
|
|
27
|
+
};
|
|
28
|
+
export type DocxOp = {
|
|
29
|
+
t: 'text';
|
|
30
|
+
pid: number;
|
|
31
|
+
at: number;
|
|
32
|
+
del: number;
|
|
33
|
+
ins: Inline[];
|
|
34
|
+
} | {
|
|
35
|
+
t: 'paras';
|
|
36
|
+
before: number[];
|
|
37
|
+
next: Paragraph[];
|
|
38
|
+
} | {
|
|
39
|
+
t: 'blocks';
|
|
40
|
+
before: BKey[];
|
|
41
|
+
prev: BKey | null;
|
|
42
|
+
next: Block[];
|
|
43
|
+
} | {
|
|
44
|
+
t: 'image';
|
|
45
|
+
key: string;
|
|
46
|
+
bytes: Uint8Array;
|
|
47
|
+
ext: string;
|
|
48
|
+
} | {
|
|
49
|
+
t: 'link';
|
|
50
|
+
key: string;
|
|
51
|
+
url: string;
|
|
52
|
+
} | {
|
|
53
|
+
t: 'call';
|
|
54
|
+
method: string;
|
|
55
|
+
args: unknown[];
|
|
56
|
+
};
|
|
57
|
+
/** a point in the document that survives structural edits: paragraph id, page-break part, offset */
|
|
58
|
+
export interface DPos {
|
|
59
|
+
pid: number;
|
|
60
|
+
k: number;
|
|
61
|
+
o: number;
|
|
62
|
+
}
|
|
63
|
+
export interface DocxPresence {
|
|
64
|
+
k: 'docx';
|
|
65
|
+
a: DPos;
|
|
66
|
+
f: DPos;
|
|
67
|
+
}
|
|
68
|
+
/** how to undo one model change; `paras` with an empty `next` re-inserts `before` next to a neighbour */
|
|
69
|
+
type Step = {
|
|
70
|
+
t: 'paras';
|
|
71
|
+
before: Paragraph[];
|
|
72
|
+
next: Paragraph[];
|
|
73
|
+
prev?: {
|
|
74
|
+
pid: number;
|
|
75
|
+
k: number;
|
|
76
|
+
};
|
|
77
|
+
after?: {
|
|
78
|
+
pid: number;
|
|
79
|
+
k: number;
|
|
80
|
+
};
|
|
81
|
+
} | {
|
|
82
|
+
t: 'blocks';
|
|
83
|
+
before: Block[];
|
|
84
|
+
next: Block[];
|
|
85
|
+
prev: BKey | null;
|
|
86
|
+
};
|
|
87
|
+
/** the steps of an op, undone in reverse order */
|
|
88
|
+
type Undo = Step[] | null;
|
|
89
|
+
/** single replaced range between two run lists (common prefix / suffix, formatting included) */
|
|
90
|
+
export declare function diffRuns(a: Inline[], b: Inline[]): {
|
|
91
|
+
at: number;
|
|
92
|
+
del: number;
|
|
93
|
+
ins: Inline[];
|
|
94
|
+
} | null;
|
|
95
|
+
export declare function spliceRuns(runs: Inline[], at: number, del: number, ins: Inline[]): Inline[];
|
|
96
|
+
export declare class DocxCollab implements Adapter<DocxOp> {
|
|
97
|
+
private client;
|
|
98
|
+
private host?;
|
|
99
|
+
/** > 0 while this adapter drives the editor (its commits are not local ops) */
|
|
100
|
+
private suppress;
|
|
101
|
+
/** resource key → this client's value ({ rid, path } for images, link key for links) */
|
|
102
|
+
private res;
|
|
103
|
+
/** this client's rid / link key → resource key */
|
|
104
|
+
private resKey;
|
|
105
|
+
private results;
|
|
106
|
+
private snap;
|
|
107
|
+
/** the model was changed since the last `settled` */
|
|
108
|
+
private touchedModel;
|
|
109
|
+
private site;
|
|
110
|
+
private counter;
|
|
111
|
+
private offHost?;
|
|
112
|
+
/** sends presence (set by the session owner) */
|
|
113
|
+
onPresence?: (p: DocxPresence) => void;
|
|
114
|
+
private lastPresence;
|
|
115
|
+
private raf;
|
|
116
|
+
/** registers a local op the editor already applied */
|
|
117
|
+
push?: (op: DocxOp, undo: Undo) => void;
|
|
118
|
+
/** shares a local op through the session (applied by `apply`) */
|
|
119
|
+
submit?: (op: DocxOp) => Promise<unknown>;
|
|
120
|
+
constructor(client: DocumentClient);
|
|
121
|
+
ready(): Promise<void>;
|
|
122
|
+
/** gives this client its own range of paragraph / table ids */
|
|
123
|
+
setSite(site: number): void;
|
|
124
|
+
private allocPids;
|
|
125
|
+
dispose(): void;
|
|
126
|
+
captures(method: keyof Methods): boolean;
|
|
127
|
+
/** client.call interceptor: package resources and document-level calls become ops */
|
|
128
|
+
intercept(method: keyof Methods, args: unknown[]): Promise<unknown> | undefined;
|
|
129
|
+
private key;
|
|
130
|
+
private get st();
|
|
131
|
+
private get ed();
|
|
132
|
+
/** every block of paragraph `pid` (page-break parts) with its address */
|
|
133
|
+
private locate;
|
|
134
|
+
private topIndex;
|
|
135
|
+
/** contiguous paragraph addresses from the first block of pids[0], following `pids` */
|
|
136
|
+
private region;
|
|
137
|
+
private remap;
|
|
138
|
+
/** tags runs referring to resources this client created, so others can map them */
|
|
139
|
+
private tag;
|
|
140
|
+
private fetchRes;
|
|
141
|
+
private onCommit;
|
|
142
|
+
transform(op: DocxOp, prior: DocxOp): DocxOp | null;
|
|
143
|
+
private touched;
|
|
144
|
+
conflicts(pending: DocxOp[], remote: DocxOp[]): boolean;
|
|
145
|
+
pessimistic(op: DocxOp): boolean;
|
|
146
|
+
prepare(ops: DocxOp[]): Promise<void>;
|
|
147
|
+
apply(op: DocxOp, kind: 'local' | 'replay' | 'remote'): Promise<ApplyResult>;
|
|
148
|
+
/** applies a model op through the editor; returns how to revert it */
|
|
149
|
+
private run;
|
|
150
|
+
revert(undo: unknown): void;
|
|
151
|
+
private undoStep;
|
|
152
|
+
private toD;
|
|
153
|
+
private fromD;
|
|
154
|
+
/** remembers the selection (by paragraph id + text) before the model changes */
|
|
155
|
+
private capture;
|
|
156
|
+
/** the remembered selection in the current model */
|
|
157
|
+
private sel;
|
|
158
|
+
settled(): void;
|
|
159
|
+
private onSel;
|
|
160
|
+
/** screen rectangles of a collaborator's selection (caret = last rect) inside `root` */
|
|
161
|
+
rects(root: HTMLElement, p: DocxPresence): {
|
|
162
|
+
caret: DOMRect | null;
|
|
163
|
+
sel: DOMRect[];
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Views register their live editing objects here so a collaboration session
|
|
3
|
+
* (created independently, e.g. by DocViewer) can reach them. Tiny on purpose:
|
|
4
|
+
* views import only this file.
|
|
5
|
+
*/
|
|
6
|
+
/** registers `host` for (client, kind); returns the unregister function */
|
|
7
|
+
export declare function registerHost<T>(client: object, kind: string, host: T): () => void;
|
|
8
|
+
export declare function getHost<T>(client: object, kind: string): T | undefined;
|
|
9
|
+
/** calls `fn` with the current host and every host registered later */
|
|
10
|
+
export declare function watchHost<T>(client: object, kind: string, fn: (host: T) => void): () => void;
|
|
11
|
+
export interface PeerView {
|
|
12
|
+
site: number;
|
|
13
|
+
name: string;
|
|
14
|
+
color: string;
|
|
15
|
+
presence?: unknown;
|
|
16
|
+
}
|
|
17
|
+
/** session → views: collaborators and where they are */
|
|
18
|
+
export declare function setPeers(client: object, peers: PeerView[]): void;
|
|
19
|
+
/** stable between changes (usable as a useSyncExternalStore snapshot) */
|
|
20
|
+
export declare function getPeers(client: object): PeerView[];
|
|
21
|
+
export declare function subscribePeers(client: object, fn: () => void): () => void;
|
|
22
|
+
/** views → session: the local user's position (selection, active slide…) */
|
|
23
|
+
export declare function publishLocal(client: object, data: unknown): void;
|
|
24
|
+
export declare function onLocal(client: object, fn: (data: unknown) => void): () => void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-agnostic collaboration hub: one sequencer + op log per room.
|
|
3
|
+
* Wire it to any socket server (see scripts/collab-server.ts for Bun):
|
|
4
|
+
*
|
|
5
|
+
* const conn = hub.connect((msg) => socket.send(encode(msg)))
|
|
6
|
+
* socket.onmessage = (e) => conn.receive(decode(e.data))
|
|
7
|
+
* socket.onclose = () => conn.close()
|
|
8
|
+
*/
|
|
9
|
+
import { type Batch, type ClientMsg, type HubMsg, type PeerInfo } from './protocol';
|
|
10
|
+
export interface RoomState {
|
|
11
|
+
id: string;
|
|
12
|
+
seq: number;
|
|
13
|
+
log: Batch[];
|
|
14
|
+
ids: Set<string>;
|
|
15
|
+
peers: Map<number, {
|
|
16
|
+
info: PeerInfo;
|
|
17
|
+
send: (m: HubMsg) => void;
|
|
18
|
+
}>;
|
|
19
|
+
nextSite: number;
|
|
20
|
+
}
|
|
21
|
+
export interface HubOptions {
|
|
22
|
+
/** called for every sequenced batch (persist it to rebuild rooms after a restart) */
|
|
23
|
+
onBatch?: (room: string, batch: Batch) => void;
|
|
24
|
+
/** rebuilds a room log (e.g. from storage) the first time the room is used */
|
|
25
|
+
loadLog?: (room: string) => Batch[] | undefined;
|
|
26
|
+
/** called when the last peer left a room */
|
|
27
|
+
onIdle?: (room: string) => void;
|
|
28
|
+
}
|
|
29
|
+
export interface HubConnection {
|
|
30
|
+
receive(msg: ClientMsg): void;
|
|
31
|
+
close(): void;
|
|
32
|
+
}
|
|
33
|
+
export declare class CollabHub {
|
|
34
|
+
private opts;
|
|
35
|
+
readonly rooms: Map<string, RoomState>;
|
|
36
|
+
constructor(opts?: HubOptions);
|
|
37
|
+
room(id: string): RoomState;
|
|
38
|
+
/** forgets a room (its log and peers) — e.g. when a new base file is uploaded */
|
|
39
|
+
reset(id: string): void;
|
|
40
|
+
connect(send: (m: HubMsg) => void): HubConnection;
|
|
41
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-time collaboration for office-engine documents.
|
|
3
|
+
*
|
|
4
|
+
* const collab = startCollab(client, { transport: 'wss://host/collab', room: 'doc-42', user: { name: 'Ann' } })
|
|
5
|
+
* collab.on('status', console.log)
|
|
6
|
+
* collab.close()
|
|
7
|
+
*
|
|
8
|
+
* Every participant must open the same base file; the hub keeps the operation log on top of it.
|
|
9
|
+
* DocViewer does all of this through its `collab` prop.
|
|
10
|
+
*/
|
|
11
|
+
import type { DocumentClient } from '../core/worker/client';
|
|
12
|
+
import { type PeerView } from './hosts';
|
|
13
|
+
import type { PeerInfo, UserInfo } from './protocol';
|
|
14
|
+
import { CollabSession, type CollabStatus, type SessionEvents } from './session';
|
|
15
|
+
import { type Transport } from './transport';
|
|
16
|
+
export interface CollabOptions {
|
|
17
|
+
/** hub connection: a WebSocket URL or any Transport */
|
|
18
|
+
transport: Transport | string;
|
|
19
|
+
/** shared document id */
|
|
20
|
+
room: string;
|
|
21
|
+
user: UserInfo;
|
|
22
|
+
}
|
|
23
|
+
export interface Collab {
|
|
24
|
+
readonly session: CollabSession<unknown>;
|
|
25
|
+
readonly status: CollabStatus;
|
|
26
|
+
/** collaborators currently connected (without the local user) */
|
|
27
|
+
readonly peers: PeerInfo[];
|
|
28
|
+
/** local operations not yet confirmed by the hub */
|
|
29
|
+
readonly pending: number;
|
|
30
|
+
on<K extends keyof SessionEvents>(event: K, fn: (d: SessionEvents[K]) => void): () => void;
|
|
31
|
+
/** screen rectangles of a collaborator's DOCX selection (for custom overlays) */
|
|
32
|
+
rects(root: HTMLElement, peer: PeerView): {
|
|
33
|
+
caret: DOMRect | null;
|
|
34
|
+
sel: DOMRect[];
|
|
35
|
+
} | null;
|
|
36
|
+
close(): void;
|
|
37
|
+
}
|
|
38
|
+
export declare function startCollab(client: DocumentClient, opts: CollabOptions): Collab;
|
|
39
|
+
export { CollabHub, type HubOptions, type HubConnection } from './hub';
|
|
40
|
+
export { CollabSession, type Adapter, type CollabStatus } from './session';
|
|
41
|
+
export { MemoryTransport, WebSocketTransport, type Transport, type TransportStatus } from './transport';
|
|
42
|
+
export type { Batch, ClientMsg, HubMsg, PeerInfo, UserInfo } from './protocol';
|
|
43
|
+
export { encode, decode } from './codec';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** PPTX collaboration operations. Slides are addressed by part path (indexes move), shapes by element id. */
|
|
2
|
+
import type { PParaSpec, ShapeOp } from '../core/pptx/types';
|
|
3
|
+
export type PSlideOp = {
|
|
4
|
+
op: 'add';
|
|
5
|
+
after: string | null;
|
|
6
|
+
layout?: 'title' | 'content' | 'blank';
|
|
7
|
+
} | {
|
|
8
|
+
op: 'duplicate';
|
|
9
|
+
path: string;
|
|
10
|
+
} | {
|
|
11
|
+
op: 'delete';
|
|
12
|
+
path: string;
|
|
13
|
+
} | {
|
|
14
|
+
op: 'move';
|
|
15
|
+
path: string;
|
|
16
|
+
to: number;
|
|
17
|
+
} | {
|
|
18
|
+
op: 'hide';
|
|
19
|
+
path: string;
|
|
20
|
+
hidden: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type POp =
|
|
23
|
+
/** whole text of shape `sid` (last writer wins per shape) */
|
|
24
|
+
{
|
|
25
|
+
t: 'text';
|
|
26
|
+
id: string;
|
|
27
|
+
slide: string;
|
|
28
|
+
sid: number;
|
|
29
|
+
paras: PParaSpec[];
|
|
30
|
+
}
|
|
31
|
+
/** shape edits; an inserted picture refers to its image by `key` */
|
|
32
|
+
| {
|
|
33
|
+
t: 'shapes';
|
|
34
|
+
id: string;
|
|
35
|
+
slide: string;
|
|
36
|
+
ops: (ShapeOp & {
|
|
37
|
+
key?: string;
|
|
38
|
+
})[];
|
|
39
|
+
} | {
|
|
40
|
+
t: 'image';
|
|
41
|
+
id: string;
|
|
42
|
+
key: string;
|
|
43
|
+
slide: string;
|
|
44
|
+
bytes: Uint8Array;
|
|
45
|
+
ext: string;
|
|
46
|
+
} | {
|
|
47
|
+
t: 'slides';
|
|
48
|
+
id: string;
|
|
49
|
+
op: PSlideOp;
|
|
50
|
+
}
|
|
51
|
+
/** puts the slide touched by op `target` back to its state before (`prev`) / after (`next`) it */
|
|
52
|
+
| {
|
|
53
|
+
t: 'revert';
|
|
54
|
+
id: string;
|
|
55
|
+
target: string;
|
|
56
|
+
which: 'prev' | 'next';
|
|
57
|
+
};
|
|
58
|
+
export declare function transformP(op: POp, prior: POp): POp | null;
|
|
59
|
+
/** whether remote ops interact with pending local ones (→ rebase) */
|
|
60
|
+
export declare function conflictsP(pending: POp[], remote: POp[]): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PptxPresentation } from '../core/pptx/parser';
|
|
2
|
+
import type { PresentationMeta, SlideModel } from '../core/pptx/types';
|
|
3
|
+
import type { POp } from './pptx-ops';
|
|
4
|
+
export interface PptxCollabResult {
|
|
5
|
+
/** slide index + its new model */
|
|
6
|
+
slide?: number;
|
|
7
|
+
model?: SlideModel;
|
|
8
|
+
/** slide list changed */
|
|
9
|
+
meta?: PresentationMeta;
|
|
10
|
+
/** image relationship id (image ops) */
|
|
11
|
+
rid?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class PptxCollabWorker {
|
|
14
|
+
private pres;
|
|
15
|
+
private recs;
|
|
16
|
+
private byOp;
|
|
17
|
+
private seq;
|
|
18
|
+
/** image key → relationship id, per slide */
|
|
19
|
+
private images;
|
|
20
|
+
constructor(pres: PptxPresentation);
|
|
21
|
+
/** text templates of every slide, taken in the shared state (before any op / right after slide-list changes) */
|
|
22
|
+
private originals;
|
|
23
|
+
private ready?;
|
|
24
|
+
private index;
|
|
25
|
+
private get genMaps();
|
|
26
|
+
private snap;
|
|
27
|
+
private restore;
|
|
28
|
+
private model;
|
|
29
|
+
apply(op: POp): Promise<{
|
|
30
|
+
res: PptxCollabResult;
|
|
31
|
+
rec: number | null;
|
|
32
|
+
}>;
|
|
33
|
+
/** exactly undoes an applied operation (rebase) */
|
|
34
|
+
revert(id: number): Promise<PptxCollabResult>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DocumentClient } from '../core/worker/client';
|
|
2
|
+
import type { Methods } from '../core/worker/protocol';
|
|
3
|
+
import { type POp } from './pptx-ops';
|
|
4
|
+
import type { Adapter, ApplyResult } from './session';
|
|
5
|
+
export interface PptxPresence {
|
|
6
|
+
k: 'pptx';
|
|
7
|
+
/** slide part path */
|
|
8
|
+
slide: string;
|
|
9
|
+
/** selected element id */
|
|
10
|
+
top?: number;
|
|
11
|
+
}
|
|
12
|
+
/** what PptxView publishes about the local selection */
|
|
13
|
+
export interface PptxLocalSel {
|
|
14
|
+
slide: number;
|
|
15
|
+
top?: number;
|
|
16
|
+
}
|
|
17
|
+
/** PptxView registers this so remote slide-list changes can close its text editing first */
|
|
18
|
+
export interface PptxHost {
|
|
19
|
+
endEditing(): void;
|
|
20
|
+
}
|
|
21
|
+
export declare class PptxCollab implements Adapter<POp> {
|
|
22
|
+
private client;
|
|
23
|
+
submit?: (op: POp) => Promise<unknown>;
|
|
24
|
+
nextId?: () => string;
|
|
25
|
+
onPresence?: (p: PptxPresence) => void;
|
|
26
|
+
private undoStack;
|
|
27
|
+
private redoStack;
|
|
28
|
+
/** this client's image relationship id → image key */
|
|
29
|
+
private ridKey;
|
|
30
|
+
private offLocal;
|
|
31
|
+
constructor(client: DocumentClient);
|
|
32
|
+
dispose(): void;
|
|
33
|
+
private get meta();
|
|
34
|
+
private path;
|
|
35
|
+
private id;
|
|
36
|
+
captures(method: keyof Methods): boolean;
|
|
37
|
+
intercept(method: keyof Methods, args: unknown[]): Promise<unknown> | undefined;
|
|
38
|
+
private pslide;
|
|
39
|
+
private local;
|
|
40
|
+
private history;
|
|
41
|
+
transform(op: POp, prior: POp): POp | null;
|
|
42
|
+
conflicts(pending: POp[], remote: POp[]): boolean;
|
|
43
|
+
pessimistic(op: POp): boolean;
|
|
44
|
+
prepare(ops: POp[]): void;
|
|
45
|
+
apply(op: POp, kind: 'local' | 'replay' | 'remote'): Promise<ApplyResult>;
|
|
46
|
+
revert(undo: unknown): Promise<void>;
|
|
47
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire protocol between collaboration clients and a hub (sequencer).
|
|
3
|
+
*
|
|
4
|
+
* The hub is format-agnostic: it assigns a global sequence number to every
|
|
5
|
+
* batch of operations, stores the log and relays presence. All transformation
|
|
6
|
+
* and conflict handling happens in the clients, deterministically, so every
|
|
7
|
+
* client that applies the log ends up with the same document.
|
|
8
|
+
*/
|
|
9
|
+
export interface UserInfo {
|
|
10
|
+
/** stable user id (optional, for the host app) */
|
|
11
|
+
id?: string;
|
|
12
|
+
name: string;
|
|
13
|
+
/** CSS color; the hub assigns one when missing */
|
|
14
|
+
color?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface PeerInfo extends UserInfo {
|
|
17
|
+
/** per-room connection id, assigned by the hub */
|
|
18
|
+
site: number;
|
|
19
|
+
color: string;
|
|
20
|
+
presence?: unknown;
|
|
21
|
+
}
|
|
22
|
+
/** A batch of operations as stored in the hub log. */
|
|
23
|
+
export interface Batch<Op = unknown> {
|
|
24
|
+
seq: number;
|
|
25
|
+
site: number;
|
|
26
|
+
/** last seq the author had applied when creating the batch */
|
|
27
|
+
base: number;
|
|
28
|
+
/** client generated id (dedupes resends after reconnects) */
|
|
29
|
+
id: string;
|
|
30
|
+
ops: Op[];
|
|
31
|
+
}
|
|
32
|
+
export type ClientMsg =
|
|
33
|
+
/** join a room; `have`/`site` on reconnect */
|
|
34
|
+
{
|
|
35
|
+
t: 'hello';
|
|
36
|
+
room: string;
|
|
37
|
+
user: UserInfo;
|
|
38
|
+
have?: number;
|
|
39
|
+
site?: number;
|
|
40
|
+
} | {
|
|
41
|
+
t: 'ops';
|
|
42
|
+
id: string;
|
|
43
|
+
base: number;
|
|
44
|
+
ops: unknown[];
|
|
45
|
+
} | {
|
|
46
|
+
t: 'presence';
|
|
47
|
+
data: unknown;
|
|
48
|
+
};
|
|
49
|
+
export type HubMsg =
|
|
50
|
+
/** `log` holds every batch with seq > `from`; `reset` when the hub lost history newer than `have` */
|
|
51
|
+
{
|
|
52
|
+
t: 'welcome';
|
|
53
|
+
site: number;
|
|
54
|
+
seq: number;
|
|
55
|
+
from: number;
|
|
56
|
+
log: Batch[];
|
|
57
|
+
peers: PeerInfo[];
|
|
58
|
+
reset?: boolean;
|
|
59
|
+
} | {
|
|
60
|
+
t: 'batch';
|
|
61
|
+
b: Batch;
|
|
62
|
+
} | {
|
|
63
|
+
t: 'presence';
|
|
64
|
+
site: number;
|
|
65
|
+
data: unknown;
|
|
66
|
+
} | {
|
|
67
|
+
t: 'join';
|
|
68
|
+
peer: PeerInfo;
|
|
69
|
+
} | {
|
|
70
|
+
t: 'leave';
|
|
71
|
+
site: number;
|
|
72
|
+
} | {
|
|
73
|
+
t: 'error';
|
|
74
|
+
message: string;
|
|
75
|
+
};
|
|
76
|
+
export declare const PEER_COLORS: string[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DocumentClient } from '../core/worker/client';
|
|
2
|
+
import { type Collab, type CollabOptions } from './index';
|
|
3
|
+
import type { CollabStatus } from './session';
|
|
4
|
+
export interface CollabState {
|
|
5
|
+
collab: Collab | null;
|
|
6
|
+
status: CollabStatus | null;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
/** runs a collaboration session for `client` while `opts` is set */
|
|
10
|
+
export declare function useCollab(client: DocumentClient | null | undefined, opts: CollabOptions | undefined): CollabState;
|
|
11
|
+
/** status chip + collaborators' avatars (top bar) */
|
|
12
|
+
export declare function CollabBar({ client, state }: {
|
|
13
|
+
client: DocumentClient;
|
|
14
|
+
state: CollabState;
|
|
15
|
+
}): import("react").JSX.Element | null;
|
|
16
|
+
/**
|
|
17
|
+
* Collaborators' cursors drawn over the document (DOCX carets / selections, PPTX selected shapes).
|
|
18
|
+
* Spreadsheets draw their own (in-grid) selection boxes. Render inside a positioned container.
|
|
19
|
+
*/
|
|
20
|
+
export declare function PeerOverlay({ client, collab }: {
|
|
21
|
+
client: DocumentClient;
|
|
22
|
+
collab: Collab | null;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client side of the collaboration protocol (format-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* Model: the hub totally orders batches of operations. Every client applies
|
|
5
|
+
* the log in that order, each batch first transformed (deterministically)
|
|
6
|
+
* against the concurrent batches sequenced before it — so all clients reach
|
|
7
|
+
* the same state. Local operations are applied optimistically; when a remote
|
|
8
|
+
* batch arrives while local ones are still unconfirmed, the client rebases:
|
|
9
|
+
* reverts its pending ops, applies the remote batch, then replays the pending
|
|
10
|
+
* ops transformed against it. Convergence therefore only needs `transform` and
|
|
11
|
+
* `apply` to be deterministic (not the classic OT TP1/TP2 properties).
|
|
12
|
+
*
|
|
13
|
+
* Operations an adapter flags as `pessimistic` (structural ones that are hard
|
|
14
|
+
* to rebase, like deleting a sheet) are not applied locally until the hub has
|
|
15
|
+
* sequenced them.
|
|
16
|
+
*/
|
|
17
|
+
import type { PeerInfo, UserInfo } from './protocol';
|
|
18
|
+
import type { Transport } from './transport';
|
|
19
|
+
export interface ApplyResult {
|
|
20
|
+
/** whatever `revert` needs to undo the op exactly (local ops only) */
|
|
21
|
+
undo?: unknown;
|
|
22
|
+
/** returned to the caller of `submit` */
|
|
23
|
+
result?: unknown;
|
|
24
|
+
}
|
|
25
|
+
export type ApplyKind = 'local' | 'replay' | 'remote';
|
|
26
|
+
export interface Adapter<Op> {
|
|
27
|
+
/** resolves when the document can take operations (e.g. fully loaded) */
|
|
28
|
+
ready?(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* `op` adjusted to apply after `prior`, a concurrent op of another site that was sequenced first.
|
|
31
|
+
* Must be deterministic; return `op` itself when unaffected, null to drop it.
|
|
32
|
+
*/
|
|
33
|
+
transform(op: Op, prior: Op): Op | null;
|
|
34
|
+
/**
|
|
35
|
+
* Async work before ops get applied (fetching resources, waiting for IME composition to end…),
|
|
36
|
+
* so that `revert` / `apply` during a rebase can run without yielding to user input.
|
|
37
|
+
*/
|
|
38
|
+
prepare?(ops: Op[]): Promise<void> | void;
|
|
39
|
+
/** applies `op` to the local document */
|
|
40
|
+
apply(op: Op, kind: ApplyKind): Promise<ApplyResult> | ApplyResult;
|
|
41
|
+
/** exactly undoes a pending local op (given its `undo` token) */
|
|
42
|
+
revert(undo: unknown): Promise<void> | void;
|
|
43
|
+
/** op must be sequenced by the hub before it is applied */
|
|
44
|
+
pessimistic?(op: Op): boolean;
|
|
45
|
+
/** whether remote ops interact with pending local ones (default: true → rebase) */
|
|
46
|
+
conflicts?(pending: Op[], remote: Op[]): boolean;
|
|
47
|
+
/** after a remote batch / rebase was applied (views refresh here) */
|
|
48
|
+
settled?(): void;
|
|
49
|
+
}
|
|
50
|
+
export type CollabStatus = 'connecting' | 'syncing' | 'live' | 'offline' | 'error';
|
|
51
|
+
export interface SessionEvents {
|
|
52
|
+
status: CollabStatus;
|
|
53
|
+
peers: PeerInfo[];
|
|
54
|
+
/** remote changes were applied */
|
|
55
|
+
change: void;
|
|
56
|
+
error: Error;
|
|
57
|
+
}
|
|
58
|
+
export interface SessionOptions {
|
|
59
|
+
room: string;
|
|
60
|
+
user: UserInfo;
|
|
61
|
+
/** canonical batches kept to transform late batches (default 5000) */
|
|
62
|
+
logWindow?: number;
|
|
63
|
+
}
|
|
64
|
+
export declare class CollabSession<Op = unknown> {
|
|
65
|
+
private transport;
|
|
66
|
+
private adapter;
|
|
67
|
+
private opts;
|
|
68
|
+
site: number;
|
|
69
|
+
/** last applied sequence number */
|
|
70
|
+
rev: number;
|
|
71
|
+
status: CollabStatus;
|
|
72
|
+
error?: string;
|
|
73
|
+
readonly peers: Map<number, PeerInfo>;
|
|
74
|
+
private log;
|
|
75
|
+
private inflight;
|
|
76
|
+
private buffer;
|
|
77
|
+
private queue;
|
|
78
|
+
private joined;
|
|
79
|
+
private online;
|
|
80
|
+
private counter;
|
|
81
|
+
private listeners;
|
|
82
|
+
private presence;
|
|
83
|
+
private presenceTimer?;
|
|
84
|
+
private presenceAt;
|
|
85
|
+
private closed;
|
|
86
|
+
constructor(transport: Transport, adapter: Adapter<Op>, opts: SessionOptions);
|
|
87
|
+
/** local ops not yet confirmed by the hub */
|
|
88
|
+
get pending(): number;
|
|
89
|
+
/** a unique id for ops created by this client */
|
|
90
|
+
nextId(): string;
|
|
91
|
+
/** applies a local op (through the adapter) and shares it; resolves with the adapter's result */
|
|
92
|
+
submit(op: Op): Promise<unknown>;
|
|
93
|
+
/** registers a local op the caller already applied (synchronously); `undo` reverts it */
|
|
94
|
+
push(op: Op, undo: unknown): void;
|
|
95
|
+
setPresence(data: unknown): void;
|
|
96
|
+
on<K extends keyof SessionEvents>(event: K, fn: (d: SessionEvents[K]) => void): () => void;
|
|
97
|
+
/** resolves once everything queued so far was processed */
|
|
98
|
+
idle(): Promise<void>;
|
|
99
|
+
close(): void;
|
|
100
|
+
private emit;
|
|
101
|
+
private setStatus;
|
|
102
|
+
private fail;
|
|
103
|
+
private enqueue;
|
|
104
|
+
private onMessage;
|
|
105
|
+
private onBatch;
|
|
106
|
+
/** batch ops transformed against the concurrent batches sequenced before it */
|
|
107
|
+
private canonical;
|
|
108
|
+
private revertAll;
|
|
109
|
+
private replay;
|
|
110
|
+
private remote;
|
|
111
|
+
private applyRemote;
|
|
112
|
+
/** our in-flight batch was sequenced */
|
|
113
|
+
private ack;
|
|
114
|
+
private flush;
|
|
115
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CollabHub } from './hub';
|
|
2
|
+
import type { ClientMsg, HubMsg } from './protocol';
|
|
3
|
+
export type TransportStatus = 'connecting' | 'open' | 'closed';
|
|
4
|
+
/** Message pipe to a hub. Implement it to plug collaboration into your own backend. */
|
|
5
|
+
export interface Transport {
|
|
6
|
+
connect(): void;
|
|
7
|
+
send(msg: ClientMsg): void;
|
|
8
|
+
close(): void;
|
|
9
|
+
onMessage?: (msg: HubMsg) => void;
|
|
10
|
+
onStatus?: (s: TransportStatus) => void;
|
|
11
|
+
}
|
|
12
|
+
/** WebSocket transport with automatic reconnection (exponential backoff). */
|
|
13
|
+
export declare class WebSocketTransport implements Transport {
|
|
14
|
+
private url;
|
|
15
|
+
private protocols?;
|
|
16
|
+
onMessage?: (msg: HubMsg) => void;
|
|
17
|
+
onStatus?: (s: TransportStatus) => void;
|
|
18
|
+
private ws?;
|
|
19
|
+
private closed;
|
|
20
|
+
private retry;
|
|
21
|
+
private timer?;
|
|
22
|
+
constructor(url: string, protocols?: string | string[] | undefined);
|
|
23
|
+
connect(): void;
|
|
24
|
+
private open;
|
|
25
|
+
send(msg: ClientMsg): void;
|
|
26
|
+
close(): void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* In-process transport to a `CollabHub` (tests, demos with several viewers on one page).
|
|
30
|
+
* Messages go through the JSON codec and are delivered asynchronously, optionally with latency.
|
|
31
|
+
*/
|
|
32
|
+
export declare class MemoryTransport implements Transport {
|
|
33
|
+
private hub;
|
|
34
|
+
private latency;
|
|
35
|
+
onMessage?: (msg: HubMsg) => void;
|
|
36
|
+
onStatus?: (s: TransportStatus) => void;
|
|
37
|
+
private conn?;
|
|
38
|
+
private up;
|
|
39
|
+
private down;
|
|
40
|
+
constructor(hub: CollabHub, latency?: number | (() => number));
|
|
41
|
+
private delay;
|
|
42
|
+
connect(): void;
|
|
43
|
+
send(msg: ClientMsg): void;
|
|
44
|
+
/** simulates a dropped connection (messages in flight are lost) */
|
|
45
|
+
drop(): void;
|
|
46
|
+
close(): void;
|
|
47
|
+
}
|