@web-ppt/collab 0.5.0-beta.2 → 0.5.0
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/CHANGELOG.md +183 -0
- package/README.md +12 -0
- package/dist/binding-DoPW07so.js +1060 -0
- package/dist/binding.d.ts +5 -0
- package/dist/collab.js +9 -878
- package/dist/evaluate.d.ts +3 -1
- package/dist/extension-addresses.d.ts +4 -0
- package/dist/message.d.ts +2 -0
- package/dist/migration/copies.d.ts +9 -0
- package/dist/migration/evaluate.d.ts +6 -0
- package/dist/migration/evidence.d.ts +2 -0
- package/dist/migration/index.d.ts +5 -0
- package/dist/migration/receipt-registers.d.ts +4 -0
- package/dist/migration/resolve.d.ts +4 -0
- package/dist/migration.js +412 -0
- package/dist/section-conflict.d.ts +3 -0
- package/dist/section-order.d.ts +4 -0
- package/dist/slide-conflict.d.ts +2 -0
- package/dist/state.d.ts +10 -5
- package/dist/table-conflict.d.ts +9 -0
- package/dist/types.d.ts +11 -0
- package/package.json +24 -5
package/dist/evaluate.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { EditDoc, Patch } from '@web-ppt/edit-core';
|
|
2
|
+
import { stageExternalPatches } from '@web-ppt/edit-core';
|
|
2
3
|
import type { CollaborationSession } from './state';
|
|
3
4
|
import type { CollabMessage } from './types';
|
|
5
|
+
export { stageExternalPatches };
|
|
4
6
|
export interface PatchAvailability {
|
|
5
7
|
readonly elements: Set<string>;
|
|
6
8
|
readonly slides: Set<string>;
|
|
@@ -13,4 +15,4 @@ export interface EvaluatedMessage {
|
|
|
13
15
|
}
|
|
14
16
|
export declare function patchAvailability(patches: readonly Patch[], seed?: PatchAvailability): PatchAvailability;
|
|
15
17
|
/** 纯读求值;任一依赖缺失时调用方必须延迟整条消息,不能提交其中的结构子集。 */
|
|
16
|
-
export declare function evaluateRemoteMessage(doc: EditDoc, session: CollaborationSession, raw: CollabMessage, seed?: PatchAvailability): EvaluatedMessage;
|
|
18
|
+
export declare function evaluateRemoteMessage(doc: EditDoc, session: CollaborationSession, raw: CollabMessage, seed?: PatchAvailability, planned?: readonly Patch[]): EvaluatedMessage;
|
package/dist/message.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { isExtensionMigrationPatch } from '@web-ppt/edit-core';
|
|
1
2
|
import type { Patch } from '@web-ppt/edit-core';
|
|
2
3
|
import type { CollabMessage, CollabStamp } from './types';
|
|
4
|
+
export { isExtensionMigrationPatch };
|
|
3
5
|
export declare function compareStamp(left: CollabStamp, right: CollabStamp): number;
|
|
4
6
|
export declare function assertCollabMessage(value: unknown): asserts value is CollabMessage;
|
|
5
7
|
export declare const pathKey: (patch: Patch) => string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EditDoc, ExtensionCopyInput, Patch, SlideTreeSnapshot } from '@web-ppt/edit-core';
|
|
2
|
+
import type { Register } from '../state';
|
|
3
|
+
export declare function copiedInputs(patches: readonly Patch[]): readonly ExtensionCopyInput[];
|
|
4
|
+
/** 来源版本只在没有更新目标版本时继承;撤销复活不能重置目标已有的寄存器。 */
|
|
5
|
+
export declare function recordCopiedInputs(registers: Map<string, Register>, patches: readonly Patch[]): void;
|
|
6
|
+
/** 复活元素不构成字段写入;接收端已获胜的原操作不能被另一端的旧结构快照重置。 */
|
|
7
|
+
export declare function rebaseCopiedPatches(doc: EditDoc, patches: readonly Patch[], registers: ReadonlyMap<string, Register>): Patch[];
|
|
8
|
+
/** 按事务原顺序读取已执行命令;同批较早的真实写入可以提供版本,复制动作自身不能。 */
|
|
9
|
+
export declare function captureCopiedInputs(doc: EditDoc, snapshot: SlideTreeSnapshot, previous: readonly Patch[], current: ReadonlyMap<string, Register>): readonly ExtensionCopyInput[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { EditDoc } from '@web-ppt/edit-core';
|
|
2
|
+
import type { EvaluatedMessage, PatchAvailability } from '../evaluate';
|
|
3
|
+
import type { CollaborationSession } from '../state';
|
|
4
|
+
import type { CollabMessage } from '../types';
|
|
5
|
+
/** 只在实际消费时裁决;延迟消息不能复用首次接收时已经过期的模型或寄存器。 */
|
|
6
|
+
export declare function evaluateMigrationMessage(doc: EditDoc, session: CollaborationSession, raw: CollabMessage, seed?: PatchAvailability): EvaluatedMessage;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Editor } from '@web-ppt/edit-core';
|
|
2
|
+
import type { CollaborationBinding, CollaborationOptions } from '../types';
|
|
3
|
+
export type { CollabExtensionOperation } from '../types';
|
|
4
|
+
/** 可选迁移入口记录原操作;仍使用基础绑定的同一会话、时钟与原子接收批次。 */
|
|
5
|
+
export declare function bindCollaboration(editor: Editor, options: CollaborationOptions): CollaborationBinding;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { EditDoc, Patch } from '@web-ppt/edit-core';
|
|
2
|
+
import type { Register } from '../state';
|
|
3
|
+
/** 迁移元数据的运输时钟不能覆盖赢家原时钟;寄存器对象替换仍随基础事务快照一起回滚。 */
|
|
4
|
+
export declare function recordReceiptWinners(doc: EditDoc, registers: Map<string, Register>, patches: readonly Patch[]): void;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { EditDoc, DocumentExtensionPatch, ExtensionMigrationInput } from '@web-ppt/edit-core';
|
|
2
|
+
import type { Register } from '../state';
|
|
3
|
+
/** 只有实际原操作可以裁决矛盾值;模型值和复制消息时钟都不能补造缺失的字段证据。 */
|
|
4
|
+
export declare function resolveMigration(doc: EditDoc, declarations: readonly DocumentExtensionPatch[], registers: ReadonlyMap<string, Register>, arriving?: readonly ExtensionMigrationInput[], force?: boolean): DocumentExtensionPatch[] | undefined;
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { readExtensionAddress as T, canonicalExtensionPatch as R, canonicalExtensionMigrationInputs as I, EXTENSION_MIGRATIONS as te, isExtensionMigrationPatch as z, readExtensionMigration as B, readExtensionCopies as H, rebaseExtensionCopySnapshot as se, stageExternalPatches as $, extensionMigrationPatches as ne, isExtensionAddressPatch as Q, setExtensionMigrationResolver as oe, refreshExtensionMigrations as re, canonicalExtensionPath as ie } from "@web-ppt/edit-core";
|
|
2
|
+
import { c as O, e as _, r as ae, s as pe, m as ce, d as le, a as q, f as D, g as fe, p as ue } from "./binding-DoPW07so.js";
|
|
3
|
+
const N = /* @__PURE__ */ new WeakMap(), K = (t, n) => t.op === n.op && (t.op === "del" || t.value === n.value), ge = (t, n) => t.length > n.length && n.every((o, i) => t[i] === o);
|
|
4
|
+
function U(t, n, o, i = [], p = !1) {
|
|
5
|
+
var k, J;
|
|
6
|
+
if (!n.length) return;
|
|
7
|
+
const r = n.map((e) => ({
|
|
8
|
+
source: [e.path[3], e.path[4]],
|
|
9
|
+
address: e.op === "set" && typeof e.value == "string" ? e.value : ""
|
|
10
|
+
})), a = T(r[0].address).target, u = new Set(r.map((e) => JSON.stringify(e.source))), h = (e) => ge(e, a) || e[0] === "elements" && e[2] === "ovr" && e[3] === "extensions" && u.has(JSON.stringify([e[1], e[4]])), c = /* @__PURE__ */ new Map();
|
|
11
|
+
for (const e of i.filter((s) => h(s.path))) {
|
|
12
|
+
const s = JSON.stringify(e.path), l = c.get(s);
|
|
13
|
+
if (l && O(e.stamp, l.stamp) === 0 && !K(e, l)) return [];
|
|
14
|
+
(!l || O(e.stamp, l.stamp) > 0) && c.set(s, e);
|
|
15
|
+
}
|
|
16
|
+
const f = /* @__PURE__ */ new Map(), g = /* @__PURE__ */ new Map(), v = [];
|
|
17
|
+
for (const [e, s] of o) {
|
|
18
|
+
const l = N.get(s);
|
|
19
|
+
if (s.kind !== "field") continue;
|
|
20
|
+
let y;
|
|
21
|
+
try {
|
|
22
|
+
y = JSON.parse(e);
|
|
23
|
+
} catch {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!Array.isArray(y) || !h(y)) continue;
|
|
27
|
+
if (!l) {
|
|
28
|
+
v.push({ path: y, op: "del", stamp: s.stamp });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const d = { path: y, ...l, stamp: s.stamp };
|
|
32
|
+
f.set(e, d);
|
|
33
|
+
const m = R(t, { ...d, origin: "migration" }), w = {
|
|
34
|
+
path: m.path,
|
|
35
|
+
...l,
|
|
36
|
+
...m.op === "set" ? { value: m.value } : {},
|
|
37
|
+
stamp: s.stamp
|
|
38
|
+
}, j = JSON.stringify(m.path), A = g.get(j);
|
|
39
|
+
(!A || O(s.stamp, A.stamp) > 0) && g.set(j, w);
|
|
40
|
+
}
|
|
41
|
+
if (v.length) {
|
|
42
|
+
const e = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const s of I(t, { routes: r, inputs: [...c.values()] })) {
|
|
44
|
+
const l = JSON.stringify(s.path), y = e.get(l);
|
|
45
|
+
(!y || O(s.stamp, y.stamp) > 0) && e.set(l, s);
|
|
46
|
+
}
|
|
47
|
+
for (const s of I(t, { routes: r, inputs: v })) {
|
|
48
|
+
const l = e.get(JSON.stringify(s.path));
|
|
49
|
+
if (!l || O(l.stamp, s.stamp) <= 0) return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
let S = !0;
|
|
53
|
+
const x = (e, s, l) => {
|
|
54
|
+
if (e !== void 0)
|
|
55
|
+
if (e && typeof e == "object")
|
|
56
|
+
for (const [y, d] of Object.entries(e)) x(d, [...s, y], l);
|
|
57
|
+
else {
|
|
58
|
+
const y = l.get(JSON.stringify(s));
|
|
59
|
+
if (!y || y.op !== "set" || y.value !== e) {
|
|
60
|
+
const d = JSON.stringify(s), m = c.get(d), w = o.get(d);
|
|
61
|
+
(!m || w && O(m.stamp, w.stamp) <= 0) && (S = !1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
for (const e of r) x(
|
|
66
|
+
(J = (k = t.elements[e.source[0]]) == null ? void 0 : k.ovr.extensions) == null ? void 0 : J[e.source[1]],
|
|
67
|
+
["elements", e.source[0], "ovr", "extensions", e.source[1]],
|
|
68
|
+
f
|
|
69
|
+
);
|
|
70
|
+
let M = t.extensions;
|
|
71
|
+
for (const e of a.slice(2)) M = M == null ? void 0 : M[e];
|
|
72
|
+
if (x(M, [...a], g), !S) return [];
|
|
73
|
+
for (const [e, s] of c) {
|
|
74
|
+
const l = f.get(e);
|
|
75
|
+
if (l && O(s.stamp, l.stamp) === 0 && !K(s, l)) return [];
|
|
76
|
+
(!l || O(s.stamp, l.stamp) > 0) && f.set(e, s);
|
|
77
|
+
}
|
|
78
|
+
if (!f.size) return;
|
|
79
|
+
const C = [...f.values()].sort((e, s) => JSON.stringify(e.path) < JSON.stringify(s.path) ? -1 : 1), E = I(t, { routes: r, inputs: C }), b = /* @__PURE__ */ new Map();
|
|
80
|
+
let P = !1;
|
|
81
|
+
for (const [e, s] of E.entries()) {
|
|
82
|
+
const l = JSON.stringify(s.path), y = b.get(l);
|
|
83
|
+
if (y === void 0) {
|
|
84
|
+
b.set(l, e);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const d = O(s.stamp, E[y].stamp);
|
|
88
|
+
if (!K(s, E[y])) {
|
|
89
|
+
if (!d) return [];
|
|
90
|
+
P = !0;
|
|
91
|
+
}
|
|
92
|
+
d > 0 && b.set(l, e);
|
|
93
|
+
}
|
|
94
|
+
if (!P && !p && !c.size) return;
|
|
95
|
+
const W = { version: 1, routes: r, inputs: C, winners: [...b.values()] };
|
|
96
|
+
return [{
|
|
97
|
+
op: "set",
|
|
98
|
+
origin: "chart-shared",
|
|
99
|
+
path: ["document", "extensions", te, JSON.stringify(a)],
|
|
100
|
+
value: JSON.stringify(W)
|
|
101
|
+
}];
|
|
102
|
+
}
|
|
103
|
+
function V(t, n, o) {
|
|
104
|
+
for (const i of o.filter(z)) {
|
|
105
|
+
const p = B(i.op === "set" ? i.value : void 0), r = I(t, p);
|
|
106
|
+
for (const a of [...p.inputs, ...p.winners.map((u) => r[u])]) {
|
|
107
|
+
const u = JSON.stringify(a.path), h = n.get(u);
|
|
108
|
+
if (!a.stamp || h && O(a.stamp, h.stamp) < 0) continue;
|
|
109
|
+
const c = { stamp: a.stamp, kind: "field" }, f = a.op === "del" ? { op: "del" } : { op: "set", value: a.value };
|
|
110
|
+
n.set(u, c), N.set(c, f);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function G(t) {
|
|
115
|
+
return t.flatMap((n) => n.op === "insert" && n.path[0] === "slides" && n.path.length === 2 ? H(n.value) : []);
|
|
116
|
+
}
|
|
117
|
+
function Y(t, n) {
|
|
118
|
+
for (const o of G(n)) {
|
|
119
|
+
const i = JSON.stringify(o.path), p = t.get(i);
|
|
120
|
+
if (p && O(p.stamp, o.stamp) >= 0) continue;
|
|
121
|
+
const r = { kind: "field", stamp: { clock: o.stamp.clock, replicaId: o.stamp.replicaId } };
|
|
122
|
+
t.set(i, r), N.set(r, o.op === "del" ? { op: "del" } : { op: "set", value: o.value });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function de(t, n, o) {
|
|
126
|
+
return n.map((i) => {
|
|
127
|
+
if (i.op !== "insert" || i.path[0] !== "slides" || i.path.length !== 2) return i;
|
|
128
|
+
const p = i.value, r = new Map(H(p).map((h) => [JSON.stringify(h.path), h])), a = [];
|
|
129
|
+
for (const [h, c] of o) {
|
|
130
|
+
const f = N.get(c);
|
|
131
|
+
if (c.kind !== "field" || !f) continue;
|
|
132
|
+
let g;
|
|
133
|
+
try {
|
|
134
|
+
g = JSON.parse(h);
|
|
135
|
+
} catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(g) || g[0] !== "elements" || g[2] !== "ovr" || g[3] !== "extensions" || !Object.prototype.hasOwnProperty.call(p.records, g[1])) continue;
|
|
139
|
+
const v = r.get(h);
|
|
140
|
+
if (v && O(c.stamp, v.stamp) <= 0) continue;
|
|
141
|
+
const S = { ...f, path: ["elements", g[1], "ovr", "extensions", g[4], ...g.slice(5)], origin: "migration" };
|
|
142
|
+
R(t, S).path[0] === "elements" && a.push(S);
|
|
143
|
+
}
|
|
144
|
+
const u = se(
|
|
145
|
+
t,
|
|
146
|
+
p,
|
|
147
|
+
a,
|
|
148
|
+
(h, c, f) => Z(h, c, f, o)
|
|
149
|
+
);
|
|
150
|
+
return u === p ? i : { ...i, value: u };
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function Z(t, n, o, i) {
|
|
154
|
+
const p = new Map(i), r = t.identity.allocation, a = { clock: r.clock + 1, replicaId: r.replicaId };
|
|
155
|
+
for (const c of o) {
|
|
156
|
+
Y(p, [c]);
|
|
157
|
+
const f = R(t, c), g = f.path;
|
|
158
|
+
if (g[0] !== "elements" || g[2] !== "ovr" || g[3] !== "extensions" || g.length < 6 || f.op !== "set" && f.op !== "del") continue;
|
|
159
|
+
const v = { kind: "field", stamp: a };
|
|
160
|
+
p.set(JSON.stringify(g), v), N.set(v, f.op === "del" ? { op: "del" } : { op: "set", value: f.value });
|
|
161
|
+
}
|
|
162
|
+
const u = /* @__PURE__ */ new Map();
|
|
163
|
+
for (const [c, f] of p) {
|
|
164
|
+
if (f.kind !== "field" || !N.has(f)) continue;
|
|
165
|
+
let g;
|
|
166
|
+
try {
|
|
167
|
+
g = JSON.parse(c);
|
|
168
|
+
} catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!Array.isArray(g) || g[0] !== "elements" || g[2] !== "ovr" || g[3] !== "extensions" || g.length < 6) continue;
|
|
172
|
+
const v = u.get(g[1]) ?? [];
|
|
173
|
+
v.push({ path: g, register: f }), u.set(g[1], v);
|
|
174
|
+
}
|
|
175
|
+
const h = [];
|
|
176
|
+
for (const [c, f] of Object.entries(n.copySources ?? {}))
|
|
177
|
+
for (const { path: g, register: v } of u.get(f) ?? []) {
|
|
178
|
+
const S = N.get(v);
|
|
179
|
+
let x = n.records[c].ovr.extensions;
|
|
180
|
+
for (const M of g.slice(4)) x = x && typeof x == "object" && Object.prototype.hasOwnProperty.call(x, M) ? x[M] : void 0;
|
|
181
|
+
(S.op === "set" ? S.value !== x : x !== void 0) || h.push({ source: f, path: ["elements", c, ...g.slice(2)], ...S, stamp: v.stamp });
|
|
182
|
+
}
|
|
183
|
+
return h;
|
|
184
|
+
}
|
|
185
|
+
const L = (t) => Q(t) || z(t), F = (t) => !L(t) && (t.path[0] === "document" && t.path[1] === "extensions" || t.path[0] === "elements" && t.path[2] === "ovr" && t.path[3] === "extensions");
|
|
186
|
+
function me(t, n, o, i) {
|
|
187
|
+
if (G(o.patches).some((e) => e.stamp.clock > o.stamp.clock))
|
|
188
|
+
throw new Error("扩展复制的原操作超过运输因果水位");
|
|
189
|
+
o = { ...o, patches: de(t, o.patches, n.registers) };
|
|
190
|
+
const p = o.patches.filter(z), r = /* @__PURE__ */ new Map(), a = (e, s = !1) => {
|
|
191
|
+
const l = e[0], y = T(l.op === "set" ? l.value : void 0).target, d = JSON.stringify(y), m = r.get(d) ?? { routes: [], paths: /* @__PURE__ */ new Map(), inputs: [], remote: s };
|
|
192
|
+
for (const w of e) {
|
|
193
|
+
const j = JSON.stringify(w.path), A = m.paths.get(j);
|
|
194
|
+
if (w.op !== "set" || A !== void 0 && A !== w.value) throw new Error("迁移凭据地址冲突");
|
|
195
|
+
A === void 0 && (m.routes.push(w), m.paths.set(j, w.value));
|
|
196
|
+
}
|
|
197
|
+
return m.remote || (m.remote = s), r.set(d, m), m;
|
|
198
|
+
};
|
|
199
|
+
let u = !1;
|
|
200
|
+
for (const e of o.patches)
|
|
201
|
+
if (!L(e)) u = !0;
|
|
202
|
+
else if (u) throw new Error("协同迁移声明必须先于字段和结构补丁");
|
|
203
|
+
for (const e of p)
|
|
204
|
+
try {
|
|
205
|
+
const s = B(e.op === "set" ? e.value : void 0), l = T(s.routes[0].address).target;
|
|
206
|
+
if (e.path.length !== 4 || e.path[3] !== JSON.stringify(l) || s.inputs.some((d) => !d.stamp || d.stamp.clock > o.stamp.clock)) throw new Error("缺少原版本");
|
|
207
|
+
if (I(t, s).some((d) => d.path.length <= l.length || l.some((m, w) => d.path[w] !== m)))
|
|
208
|
+
throw new Error("字段超出目标");
|
|
209
|
+
a(s.routes.map((d) => ({
|
|
210
|
+
op: "set",
|
|
211
|
+
origin: e.origin,
|
|
212
|
+
path: ["document", "extensions", "edit-addresses", ...d.source],
|
|
213
|
+
value: d.address
|
|
214
|
+
})), !0).inputs.push(...s.inputs);
|
|
215
|
+
} catch (s) {
|
|
216
|
+
throw new Error(`迁移凭据无法本地裁决:${String(s)}`);
|
|
217
|
+
}
|
|
218
|
+
let h = t;
|
|
219
|
+
const c = [], f = [];
|
|
220
|
+
if (o.patches.some((e) => e.op === "insert" && e.path[0] === "slides")) {
|
|
221
|
+
const e = _(
|
|
222
|
+
t,
|
|
223
|
+
n,
|
|
224
|
+
{ ...o, patches: o.patches.filter((s) => !z(s)) },
|
|
225
|
+
i,
|
|
226
|
+
[]
|
|
227
|
+
);
|
|
228
|
+
if (e.missingDependency && !p.length) return e;
|
|
229
|
+
if (!e.missingDependency) {
|
|
230
|
+
const s = e.accepted.filter((d) => !F(d) && !L(d)), l = {
|
|
231
|
+
...n,
|
|
232
|
+
registers: new Map(n.registers),
|
|
233
|
+
elementLifecycles: new Map(n.elementLifecycles),
|
|
234
|
+
slideLifecycles: new Map(n.slideLifecycles),
|
|
235
|
+
slideMoves: new Map(n.slideMoves),
|
|
236
|
+
sectionMoves: new Map(n.sectionMoves),
|
|
237
|
+
onRecord: void 0
|
|
238
|
+
};
|
|
239
|
+
ae(l, e.recorded, o.stamp);
|
|
240
|
+
const y = new Set(t.slideOrder);
|
|
241
|
+
for (const d of s) {
|
|
242
|
+
const m = pe(d);
|
|
243
|
+
(m == null ? void 0 : m.state) === "present" ? y.add(m.id) : m && y.delete(m.id);
|
|
244
|
+
}
|
|
245
|
+
h = $(t, ce(
|
|
246
|
+
t.slideOrder,
|
|
247
|
+
le(n.baseSlideOrder, y, l.slideMoves),
|
|
248
|
+
s
|
|
249
|
+
));
|
|
250
|
+
for (const d of s)
|
|
251
|
+
if (!(d.op !== "insert" || d.path[0] !== "slides" || d.path.length !== 2))
|
|
252
|
+
for (const m of Object.values(d.value.records))
|
|
253
|
+
for (const w of Object.keys(m.ovr.extensions ?? {})) c.push({
|
|
254
|
+
op: "del",
|
|
255
|
+
origin: "migration",
|
|
256
|
+
path: ["elements", m.id, "ovr", "extensions", w, "probe"]
|
|
257
|
+
});
|
|
258
|
+
f.push(...G(s).map(({ source: d, ...m }) => m));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const g = [...o.patches, ...c, ...[...r.values()].flatMap((e) => e.routes.map((s) => ({
|
|
262
|
+
op: "del",
|
|
263
|
+
origin: "migration",
|
|
264
|
+
path: ["elements", s.path[3], "ovr", "extensions", s.path[4], "probe"]
|
|
265
|
+
})))];
|
|
266
|
+
let v;
|
|
267
|
+
if (ne(h, g, (e, s) => {
|
|
268
|
+
try {
|
|
269
|
+
a(s);
|
|
270
|
+
} catch (l) {
|
|
271
|
+
v = l;
|
|
272
|
+
}
|
|
273
|
+
return [];
|
|
274
|
+
}), v) throw v;
|
|
275
|
+
const S = o.patches.filter((e) => F(e) && (e.op === "set" || e.op === "del")).map((e) => ({
|
|
276
|
+
path: e.path,
|
|
277
|
+
op: e.op,
|
|
278
|
+
stamp: o.stamp,
|
|
279
|
+
...e.op === "set" ? { value: e.value } : {}
|
|
280
|
+
})), x = [...new Map(S.map((e) => [JSON.stringify(e.path), e])).values()], M = [];
|
|
281
|
+
for (const e of r.values()) {
|
|
282
|
+
const s = U(h, e.routes, n.registers, [...e.inputs, ...f, ...x], e.remote);
|
|
283
|
+
if (e.remote && !(s != null && s.length)) throw new Error("迁移凭据无法本地裁决:原操作证据不足或矛盾");
|
|
284
|
+
s && M.push(...s);
|
|
285
|
+
}
|
|
286
|
+
if (!M.length && !p.length) return r.size ? _(t, n, o, i, []) : _(t, n, o, i);
|
|
287
|
+
const C = o.patches.filter(Q), E = [...M, ...C], b = $(t, E), P = new Map(n.registers);
|
|
288
|
+
V(b, P, M);
|
|
289
|
+
const W = q(b, n.registers), k = q(b, P);
|
|
290
|
+
for (const e of o.patches.filter(F)) {
|
|
291
|
+
const s = R(b, e), l = JSON.stringify(s.path), y = k.get(l), d = W.get(l);
|
|
292
|
+
y && O(y.stamp, o.stamp) === 0 && (!d || O(o.stamp, d.stamp) > 0) && k.delete(l);
|
|
293
|
+
}
|
|
294
|
+
const J = _(
|
|
295
|
+
b,
|
|
296
|
+
{ ...n, registers: k },
|
|
297
|
+
{ ...o, patches: o.patches.filter((e) => !L(e)) },
|
|
298
|
+
i,
|
|
299
|
+
[]
|
|
300
|
+
);
|
|
301
|
+
return { ...J, accepted: [...E, ...J.accepted], recorded: [...E, ...J.recorded] };
|
|
302
|
+
}
|
|
303
|
+
const ee = (t) => t[0] === "document" && t[1] === "extensions" && t[2] !== "edit-addresses" && t[2] !== "edit-migrations" && t.length >= 4 || (t[0] === "elements" || t[0] === "slides") && t[2] === "ovr" && t[3] === "extensions" && t.length >= 6;
|
|
304
|
+
function X(t) {
|
|
305
|
+
var r, a;
|
|
306
|
+
if (!t || typeof t != "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(t)))
|
|
307
|
+
throw new Error("协同扩展字段的原操作无效");
|
|
308
|
+
const n = Object.getOwnPropertyDescriptors(t), o = Reflect.ownKeys(n);
|
|
309
|
+
if (o.some((u) => u !== "op" && u !== "value") || Object.values(n).some((u) => !u.enumerable || !("value" in u)))
|
|
310
|
+
throw new Error("协同扩展字段的原操作无效");
|
|
311
|
+
const i = (r = n.op) == null ? void 0 : r.value, p = (a = n.value) == null ? void 0 : a.value;
|
|
312
|
+
if (i === "del" && o.length === 1) return { op: i };
|
|
313
|
+
if (i === "set" && o.length === 2 && (p === null || typeof p == "boolean" || typeof p == "number" && Number.isFinite(p) || typeof p == "string" && p.length <= 1e6)) return { op: i, value: p };
|
|
314
|
+
throw new Error("协同扩展字段的原操作无效");
|
|
315
|
+
}
|
|
316
|
+
function he(t) {
|
|
317
|
+
var p;
|
|
318
|
+
const n = t == null ? void 0 : t.extensionOperations;
|
|
319
|
+
if (n === void 0) return /* @__PURE__ */ new Map();
|
|
320
|
+
if (!Array.isArray(n) || n.length > 1e5 || !Array.isArray(t.registers))
|
|
321
|
+
throw new Error("协同扩展字段的原操作列表无效");
|
|
322
|
+
const o = new Map(t.registers), i = /* @__PURE__ */ new Map();
|
|
323
|
+
for (const r of n) {
|
|
324
|
+
if (!Array.isArray(r) || r.length !== 2 || typeof r[0] != "string" || i.has(r[0]) || ((p = o.get(r[0])) == null ? void 0 : p.kind) !== "field")
|
|
325
|
+
throw new Error("协同扩展字段的原操作缺少唯一原寄存器");
|
|
326
|
+
let a;
|
|
327
|
+
try {
|
|
328
|
+
a = JSON.parse(r[0]);
|
|
329
|
+
} catch {
|
|
330
|
+
throw new Error("协同扩展字段的原操作路径无效");
|
|
331
|
+
}
|
|
332
|
+
if (!Array.isArray(a) || !ee(a) || a.length > 21 || a.some((u) => typeof u != "string" || !u || u.length > 1024 || ["__proto__", "constructor", "prototype"].includes(u)))
|
|
333
|
+
throw new Error("协同扩展字段的原操作路径无效");
|
|
334
|
+
i.set(r[0], X(r[1]));
|
|
335
|
+
}
|
|
336
|
+
return i;
|
|
337
|
+
}
|
|
338
|
+
function ye(t, n, o) {
|
|
339
|
+
for (const i of n) {
|
|
340
|
+
if (!ee(i.path) || i.op !== "set" && i.op !== "del") continue;
|
|
341
|
+
const p = ue(i), r = t.registers.get(p);
|
|
342
|
+
if ((r == null ? void 0 : r.kind) !== "field" || O(r.stamp, o) !== 0) continue;
|
|
343
|
+
const a = i.op === "del" ? { op: "del" } : { op: "set", value: i.value };
|
|
344
|
+
N.set(r, X(a));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function ve(t, n) {
|
|
348
|
+
const o = /* @__PURE__ */ new Map();
|
|
349
|
+
for (const [i, p] of n) {
|
|
350
|
+
let r = i, a = N.get(p);
|
|
351
|
+
if (i.startsWith('["elements",'))
|
|
352
|
+
try {
|
|
353
|
+
const h = JSON.parse(i);
|
|
354
|
+
if (r = JSON.stringify(ie(t, h)), a) {
|
|
355
|
+
const c = R(t, { ...a, path: h, origin: "collab" }), f = c.op === "set" ? { op: "set", value: c.value } : { op: "del" };
|
|
356
|
+
a = X(f);
|
|
357
|
+
}
|
|
358
|
+
} catch (h) {
|
|
359
|
+
if (a) throw h;
|
|
360
|
+
r = i;
|
|
361
|
+
}
|
|
362
|
+
const u = o.get(r);
|
|
363
|
+
if (!u || O(p.stamp, u.register.stamp) > 0) o.set(r, { register: p, operation: a });
|
|
364
|
+
else if (O(p.stamp, u.register.stamp) === 0 && a && u.operation && JSON.stringify(a) !== JSON.stringify(u.operation))
|
|
365
|
+
throw new Error("协同扩展字段的同版本原操作冲突");
|
|
366
|
+
}
|
|
367
|
+
return [...o].flatMap(([i, p]) => p.operation ? [[i, p.operation]] : []).sort(([i], [p]) => i < p ? -1 : i > p ? 1 : 0);
|
|
368
|
+
}
|
|
369
|
+
function xe(t, n) {
|
|
370
|
+
let o;
|
|
371
|
+
try {
|
|
372
|
+
o = D(t);
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
const i = o ? /* @__PURE__ */ new Map() : he(n.checkpoint), p = fe(t, { ...n, provider: {
|
|
376
|
+
send: (r) => n.provider.send(r),
|
|
377
|
+
subscribe(r) {
|
|
378
|
+
const a = D(t), u = a.onRecord;
|
|
379
|
+
for (const [c, f] of i)
|
|
380
|
+
N.set(a.registers.get(c), f);
|
|
381
|
+
a.onRecord = (c, f) => {
|
|
382
|
+
u == null || u(c, f), Y(a.registers, c), V(t.doc, a.registers, c), ye(a, c, f);
|
|
383
|
+
};
|
|
384
|
+
const h = oe(
|
|
385
|
+
t.doc,
|
|
386
|
+
(c, f) => U(c, f, a.registers),
|
|
387
|
+
(c, f, g) => Z(c, f, g, a.registers)
|
|
388
|
+
);
|
|
389
|
+
try {
|
|
390
|
+
const c = n.provider.subscribe(r);
|
|
391
|
+
if (typeof c != "function") throw new Error("provider.subscribe 必须返回取消订阅函数");
|
|
392
|
+
return () => {
|
|
393
|
+
h(), a.onRecord = u, c();
|
|
394
|
+
};
|
|
395
|
+
} catch (c) {
|
|
396
|
+
throw h(), a.onRecord = u, c;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} }, me);
|
|
400
|
+
try {
|
|
401
|
+
re(t.doc);
|
|
402
|
+
} catch (r) {
|
|
403
|
+
throw p.dispose(), r;
|
|
404
|
+
}
|
|
405
|
+
return { ...p, checkpoint() {
|
|
406
|
+
const r = p.checkpoint(), a = ve(t.doc, D(t).registers);
|
|
407
|
+
return { ...r, extensionOperations: structuredClone(a) };
|
|
408
|
+
} };
|
|
409
|
+
}
|
|
410
|
+
export {
|
|
411
|
+
xe as bindCollaboration
|
|
412
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Patch } from '@web-ppt/edit-core';
|
|
2
|
+
import type { SectionMove } from './state';
|
|
3
|
+
/** 用全量节顺序意图重写批次最后一个节快照,使不同目标的并发移动也与到达顺序无关。 */
|
|
4
|
+
export declare function materializeSectionOrder(base: readonly string[], moves: ReadonlyMap<string, SectionMove>, patches: Patch[]): Patch[];
|
package/dist/slide-conflict.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { EditDoc, SlideTreePatch } from '@web-ppt/edit-core';
|
|
2
|
+
/** 节删除胜过并发复制的旧归属;页面本身仍可插入,降级为未分节。 */
|
|
3
|
+
export declare function rebaseSlideInsertion(doc: EditDoc, patch: SlideTreePatch): SlideTreePatch;
|
|
2
4
|
/** 远端删页必须以接收端当前整页闭包落模,否则并发新增元素会变成孤儿。 */
|
|
3
5
|
export declare function rebaseSlideRemoval(doc: EditDoc, patch: SlideTreePatch): SlideTreePatch | null;
|
package/dist/state.d.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import type { EditDoc, Patch } from '@web-ppt/edit-core';
|
|
2
2
|
import type { SeenMap } from './seen';
|
|
3
|
-
import type { CollabMessage, CollabStamp } from './types';
|
|
3
|
+
import type { CollabMessage, CollabStamp, CollabRegisterCheckpoint } from './types';
|
|
4
4
|
export declare const hierarchyKey: (id: string) => string;
|
|
5
|
-
export type Register =
|
|
6
|
-
readonly stamp: CollabStamp;
|
|
7
|
-
readonly kind: 'field' | 'hierarchy';
|
|
8
|
-
};
|
|
5
|
+
export type Register = CollabRegisterCheckpoint;
|
|
9
6
|
export type Lifecycle = {
|
|
10
7
|
readonly stamp: CollabStamp;
|
|
11
8
|
readonly state: 'present' | 'removed';
|
|
@@ -15,6 +12,7 @@ export type SlideMove = {
|
|
|
15
12
|
readonly after: string | null;
|
|
16
13
|
readonly ordinal: number;
|
|
17
14
|
};
|
|
15
|
+
export type SectionMove = SlideMove;
|
|
18
16
|
export interface DeferredPatch {
|
|
19
17
|
readonly message: CollabMessage;
|
|
20
18
|
readonly patch: Patch;
|
|
@@ -28,12 +26,15 @@ export interface CollaborationSession {
|
|
|
28
26
|
readonly elementLifecycles: Map<string, Lifecycle>;
|
|
29
27
|
readonly slideLifecycles: Map<string, Lifecycle>;
|
|
30
28
|
readonly slideMoves: Map<string, SlideMove>;
|
|
29
|
+
readonly sectionMoves: Map<string, SectionMove>;
|
|
31
30
|
readonly seen: SeenMap;
|
|
32
31
|
readonly baseSlideOrder: readonly string[];
|
|
32
|
+
readonly baseSectionOrder: readonly string[];
|
|
33
33
|
deferred: DeferredPatch[];
|
|
34
34
|
clock: number;
|
|
35
35
|
sequence: number;
|
|
36
36
|
active: boolean;
|
|
37
|
+
onRecord?: (patches: readonly Patch[], stamp: CollabStamp) => void;
|
|
37
38
|
}
|
|
38
39
|
export declare const elementLifecycle: (patch: Patch) => {
|
|
39
40
|
id: string;
|
|
@@ -47,6 +48,10 @@ export declare const slideMove: (patch: Patch) => {
|
|
|
47
48
|
id: string;
|
|
48
49
|
after: string | null;
|
|
49
50
|
} | null;
|
|
51
|
+
export declare const sectionMove: (patch: Patch) => {
|
|
52
|
+
id: string;
|
|
53
|
+
after: string | null;
|
|
54
|
+
} | null;
|
|
50
55
|
export declare const elementHierarchy: (patch: Patch) => Readonly<Record<string, unknown>> | null;
|
|
51
56
|
export declare const newer: (stamp: CollabStamp, current?: {
|
|
52
57
|
stamp: CollabStamp;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EditDoc, Patch } from '@web-ppt/edit-core';
|
|
2
|
+
import type { CollaborationSession } from './state';
|
|
3
|
+
import type { CollabStamp } from './types';
|
|
4
|
+
export interface RebasedTablePatches {
|
|
5
|
+
readonly accepted: Patch[];
|
|
6
|
+
readonly recorded: Patch[];
|
|
7
|
+
}
|
|
8
|
+
/** tombstone 只控制可见性;字段继续独立收敛,撤销后才能恢复同一身份的完整状态。 */
|
|
9
|
+
export declare function rebaseTablePatches(doc: EditDoc, session: CollaborationSession, stamp: CollabStamp, patches: readonly Patch[]): RebasedTablePatches;
|
package/dist/types.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export interface CollabRegisterCheckpoint {
|
|
|
11
11
|
readonly stamp: CollabStamp;
|
|
12
12
|
readonly kind: 'field' | 'hierarchy';
|
|
13
13
|
}
|
|
14
|
+
export type CollabExtensionOperation = {
|
|
15
|
+
readonly op: 'set';
|
|
16
|
+
readonly value: string | number | boolean | null;
|
|
17
|
+
} | {
|
|
18
|
+
readonly op: 'del';
|
|
19
|
+
};
|
|
14
20
|
export interface CollabSlideMoveCheckpoint {
|
|
15
21
|
readonly stamp: CollabStamp;
|
|
16
22
|
readonly after: string | null;
|
|
@@ -38,12 +44,17 @@ export interface CollaborationCheckpoint {
|
|
|
38
44
|
readonly clock: number;
|
|
39
45
|
readonly sequence: number;
|
|
40
46
|
readonly baseSlideOrder: readonly string[];
|
|
47
|
+
/** 旧 checkpoint 没有节排序元数据;恢复时以已恢复模型为基线。 */
|
|
48
|
+
readonly baseSectionOrder?: readonly string[];
|
|
41
49
|
readonly registers: readonly (readonly [string, CollabRegisterCheckpoint])[];
|
|
42
50
|
readonly elementLifecycles: readonly (readonly [string, CollabLifecycleCheckpoint])[];
|
|
43
51
|
readonly slideLifecycles: readonly (readonly [string, CollabLifecycleCheckpoint])[];
|
|
44
52
|
readonly slideMoves: readonly (readonly [string, CollabSlideMoveCheckpoint])[];
|
|
53
|
+
readonly sectionMoves?: readonly (readonly [string, CollabSlideMoveCheckpoint])[];
|
|
45
54
|
readonly seen: readonly CollabSeenCheckpoint[];
|
|
46
55
|
readonly deferred: readonly CollabDeferredCheckpoint[];
|
|
56
|
+
/** 可选 migration 入口保留原操作;缺省表示证据未知,不能把缺失值猜成 del。 */
|
|
57
|
+
readonly extensionOperations?: readonly (readonly [string, CollabExtensionOperation])[];
|
|
47
58
|
}
|
|
48
59
|
/** 一条消息对应一个已经原子落模的 Editor 补丁批次。 */
|
|
49
60
|
export interface CollabMessage {
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@web-ppt/collab",
|
|
3
|
-
"version": "0.5.0
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Optional field-level LWW collaboration adapter for @web-ppt/edit-core with pluggable providers.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pptx",
|
|
7
|
+
"powerpoint",
|
|
8
|
+
"collaboration",
|
|
9
|
+
"offline",
|
|
10
|
+
"lww",
|
|
11
|
+
"browser",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
6
14
|
"license": "MIT",
|
|
7
15
|
"author": "unStone",
|
|
8
16
|
"homepage": "https://github.com/unStone/web-ppt#readme",
|
|
@@ -21,16 +29,27 @@
|
|
|
21
29
|
"types": "./dist/index.d.ts",
|
|
22
30
|
"import": "./dist/collab.js"
|
|
23
31
|
},
|
|
32
|
+
"./migration": {
|
|
33
|
+
"types": "./dist/migration/index.d.ts",
|
|
34
|
+
"import": "./dist/migration.js"
|
|
35
|
+
},
|
|
24
36
|
"./package.json": "./package.json"
|
|
25
37
|
},
|
|
26
|
-
"files": [
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"CHANGELOG.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
27
44
|
"scripts": {
|
|
28
45
|
"build": "vite build && tsc -p tsconfig.build.json",
|
|
29
46
|
"prepublishOnly": "npm run build",
|
|
30
47
|
"prepack": "node ../../tooling/sync-package-docs.mjs"
|
|
31
48
|
},
|
|
32
49
|
"peerDependencies": {
|
|
33
|
-
"@web-ppt/edit-core": "^0.5.0
|
|
50
|
+
"@web-ppt/edit-core": "^0.5.0"
|
|
34
51
|
},
|
|
35
|
-
"publishConfig": {
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
36
55
|
}
|