@mce/collaboration 0.24.4
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/dist/AbstractProvider.d.ts +61 -0
- package/dist/AbstractProvider.test.d.ts +1 -0
- package/dist/CollaborationStatus.vue.d.ts +3 -0
- package/dist/Presence.vue.d.ts +3 -0
- package/dist/WebsocketProvider.d.ts +36 -0
- package/dist/WebsocketProvider.test.d.ts +1 -0
- package/dist/collaborationPlugin.d.ts +61 -0
- package/dist/index.css +97 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +909 -0
- package/dist/presencePlugin.d.ts +51 -0
- package/package.json +64 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { YDoc } from 'mce';
|
|
2
|
+
import type { ObservableEvents } from 'modern-idoc';
|
|
3
|
+
import { Observable } from 'modern-idoc';
|
|
4
|
+
import * as awarenessProtocol from 'y-protocols/awareness';
|
|
5
|
+
/**
|
|
6
|
+
* 顶层消息类型。一个消息 = [messageType: varUint, payload...],房间名由传输层在 URL 等处携带。
|
|
7
|
+
*/
|
|
8
|
+
export declare const MESSAGE_SYNC = 0;
|
|
9
|
+
export declare const MESSAGE_AWARENESS = 1;
|
|
10
|
+
export declare const MESSAGE_QUERY_AWARENESS = 3;
|
|
11
|
+
export interface AbstractProviderEvents extends ObservableEvents {
|
|
12
|
+
/** 连接状态变化。 */
|
|
13
|
+
status: [{
|
|
14
|
+
connected: boolean;
|
|
15
|
+
}];
|
|
16
|
+
/** 首次与对端完成 sync step2,本端已拿到全量状态。 */
|
|
17
|
+
synced: [boolean];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 可插拔传输层基类。负责 Yjs 同步协议(state vector 握手 + 增量)与 Awareness(在场感知)的
|
|
21
|
+
* 编解码与路由;具体字节怎么收发由子类实现(WebSocket / WebRTC / BroadcastChannel / 自定义)。
|
|
22
|
+
*
|
|
23
|
+
* 关注点分离:
|
|
24
|
+
* - 本类只懂「协议」:何时发 syncStep1、如何把 update / awareness 编成消息、收到消息如何分发。
|
|
25
|
+
* - 子类只懂「传输」:实现 {@link send},在连接打开时调用 {@link onOpen},收到字节时调用 {@link receive}。
|
|
26
|
+
*
|
|
27
|
+
* 回环防护:远端增量一律以 `this`(provider 实例)为 origin apply 到文档——既不进入本端
|
|
28
|
+
* UndoManager,又能刷新视图,且本端 'update' 监听据 `origin === this` 跳过再广播。
|
|
29
|
+
*/
|
|
30
|
+
export declare abstract class AbstractProvider<Events extends AbstractProviderEvents = AbstractProviderEvents> extends Observable<Events> {
|
|
31
|
+
readonly awareness: awarenessProtocol.Awareness;
|
|
32
|
+
synced: boolean;
|
|
33
|
+
/** 传输层连接状态。随 onOpen / onClose 翻转,供晚挂监听的上层补读初值。 */
|
|
34
|
+
connected: boolean;
|
|
35
|
+
protected _ydoc: YDoc;
|
|
36
|
+
protected _doc: YDoc['_yDoc'];
|
|
37
|
+
private _onUpdate;
|
|
38
|
+
private _onAwarenessUpdate;
|
|
39
|
+
constructor(ydoc: YDoc);
|
|
40
|
+
/** 子类实现:把字节发往对端。连接未就绪时应自行缓冲或丢弃(同步协议可在重连后重放)。 */
|
|
41
|
+
protected abstract send(data: Uint8Array): void;
|
|
42
|
+
/**
|
|
43
|
+
* 把本端全量状态作为一条 sync update 主动推给对端。
|
|
44
|
+
*
|
|
45
|
+
* 用于**无中心服务端的 P2P 传输**(如 BroadcastChannel / WebRTC):step1/step2 握手只能让
|
|
46
|
+
* 「发起方」拉到对端缺失的内容,无法让已在场的对端获得本端新内容。当本端在会话中途换上一份
|
|
47
|
+
* 新内容(如 setDoc 后 provider 重建)时,调用此方法把内容推过去,对端 applyUpdate 即合并。
|
|
48
|
+
* 有服务端的传输(WebSocket)由服务端持久化 + 转发,通常不需要。
|
|
49
|
+
*/
|
|
50
|
+
protected broadcastState(): void;
|
|
51
|
+
/** 连接(重新)建立后由子类调用:发起 sync step1 并广播本端 awareness。 */
|
|
52
|
+
protected onOpen(): void;
|
|
53
|
+
/** 连接断开后由子类调用。 */
|
|
54
|
+
protected onClose(): void;
|
|
55
|
+
/** 子类收到一帧消息字节后调用,路由到 sync / awareness 处理。 */
|
|
56
|
+
protected receive(data: Uint8Array): void;
|
|
57
|
+
private _encodeAwareness;
|
|
58
|
+
private _removeAwareness;
|
|
59
|
+
protected _setSynced(value: boolean): void;
|
|
60
|
+
destroy(): void;
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { YDoc } from 'mce';
|
|
2
|
+
import { AbstractProvider } from './AbstractProvider';
|
|
3
|
+
export interface WebsocketProviderOptions {
|
|
4
|
+
/** 是否构造后立即连接,默认 true。 */
|
|
5
|
+
connect?: boolean;
|
|
6
|
+
/** 断线重连基础间隔(ms),按指数退避,默认 1000。 */
|
|
7
|
+
reconnectInterval?: number;
|
|
8
|
+
/** 最大重连间隔(ms),默认 30000。 */
|
|
9
|
+
maxReconnectInterval?: number;
|
|
10
|
+
/** WebSocket 子协议。 */
|
|
11
|
+
protocols?: string | string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 基于 WebSocket 的传输实现,协议与 y-websocket 服务端兼容:
|
|
15
|
+
* 连接地址为 `${url}/${room}`,二进制帧承载 {@link AbstractProvider} 定义的 sync / awareness 消息。
|
|
16
|
+
*
|
|
17
|
+
* 特性:指数退避重连、连接未就绪时缓冲消息、`onOpen` 自动发起 state vector 握手。
|
|
18
|
+
*/
|
|
19
|
+
export declare class WebsocketProvider extends AbstractProvider {
|
|
20
|
+
readonly url: string;
|
|
21
|
+
ws: WebSocket | null;
|
|
22
|
+
shouldConnect: boolean;
|
|
23
|
+
private readonly _reconnectInterval;
|
|
24
|
+
private readonly _maxReconnectInterval;
|
|
25
|
+
private readonly _protocols?;
|
|
26
|
+
private _reconnectAttempts;
|
|
27
|
+
private _reconnectTimer;
|
|
28
|
+
/** 连接未 OPEN 时缓冲待发字节,OPEN 后冲刷。 */
|
|
29
|
+
private _pending;
|
|
30
|
+
constructor(url: string, room: string, ydoc: YDoc, options?: WebsocketProviderOptions);
|
|
31
|
+
connect(): void;
|
|
32
|
+
disconnect(): void;
|
|
33
|
+
protected send(data: Uint8Array): void;
|
|
34
|
+
private _scheduleReconnect;
|
|
35
|
+
destroy(): void;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { YDoc } from 'mce';
|
|
2
|
+
import type { Ref, ShallowRef } from 'vue';
|
|
3
|
+
import type { AbstractProvider } from './AbstractProvider';
|
|
4
|
+
import type { WebsocketProviderOptions } from './WebsocketProvider';
|
|
5
|
+
declare global {
|
|
6
|
+
namespace Mce {
|
|
7
|
+
/**
|
|
8
|
+
* 传输层工厂:拿到当前文档的底层 YDoc,返回一个 Provider 实例。
|
|
9
|
+
* 文档切换时框架会以新文档的 YDoc 重新调用,以重建会话。
|
|
10
|
+
*/
|
|
11
|
+
type ProviderFactory = (doc: YDoc) => AbstractProvider;
|
|
12
|
+
interface CollaborationConnectOptions {
|
|
13
|
+
/** WebSocket 服务端地址(与 y-websocket 兼容)。与 provider 二选一。 */
|
|
14
|
+
url?: string;
|
|
15
|
+
/** 房间名,默认取当前文档 id。 */
|
|
16
|
+
room?: string;
|
|
17
|
+
/** 透传给 WebsocketProvider 的可选项(重连间隔、子协议等)。 */
|
|
18
|
+
websocket?: WebsocketProviderOptions;
|
|
19
|
+
/** 自定义传输层工厂(WebRTC / BroadcastChannel / 自定义)。提供时忽略 url / room。 */
|
|
20
|
+
provider?: ProviderFactory;
|
|
21
|
+
}
|
|
22
|
+
interface Collaboration {
|
|
23
|
+
/** 当前 Provider 实例;未连接时为 undefined。 */
|
|
24
|
+
provider: ShallowRef<AbstractProvider | undefined>;
|
|
25
|
+
/** 传输层连接状态。 */
|
|
26
|
+
connected: Ref<boolean>;
|
|
27
|
+
/** 是否已与对端完成首次全量同步。 */
|
|
28
|
+
synced: Ref<boolean>;
|
|
29
|
+
/** 是否存在活跃的协同会话(connect 后、disconnect 前恒为 true,跨文档切换保持)。 */
|
|
30
|
+
active: Ref<boolean>;
|
|
31
|
+
/** 建立协同会话。重复调用会先断开旧会话。返回新建的 Provider(参数非法时返回 undefined)。 */
|
|
32
|
+
connect: (options: CollaborationConnectOptions) => AbstractProvider | undefined;
|
|
33
|
+
/** 断开并销毁当前 Provider,结束协同会话。 */
|
|
34
|
+
disconnect: () => void;
|
|
35
|
+
}
|
|
36
|
+
interface Options {
|
|
37
|
+
/** 启动时自动建立协同会话。 */
|
|
38
|
+
collaboration?: CollaborationConnectOptions;
|
|
39
|
+
}
|
|
40
|
+
interface Editor {
|
|
41
|
+
collaboration: Collaboration;
|
|
42
|
+
}
|
|
43
|
+
interface Events {
|
|
44
|
+
/** 传输层连接状态变化。 */
|
|
45
|
+
collaborationStatus: [connected: boolean];
|
|
46
|
+
/** 首次全量同步完成 / 失效。 */
|
|
47
|
+
collaborationSynced: [synced: boolean];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 协同会话接入:把可插拔的传输层({@link AbstractProvider} 子类)接到编辑器的文档生命周期上。
|
|
53
|
+
*
|
|
54
|
+
* 职责边界:本插件只管「会话生命周期」——建立 / 销毁 Provider、跨文档切换重建、暴露连接 /
|
|
55
|
+
* 同步状态。协议编解码(sync / awareness)在 {@link AbstractProvider},字节收发在具体 Provider。
|
|
56
|
+
*
|
|
57
|
+
* 关键不变量:Provider 绑定在某个具体的底层 YDoc 上(`Doc._yDoc`)。`setDoc` 切换文档时旧
|
|
58
|
+
* YDoc 会被 destroy,故必须以新文档的 YDoc 重建 Provider —— 这里订阅 `docSet` 事件完成重建。
|
|
59
|
+
*/
|
|
60
|
+
declare const _default: import("mce").Plugin;
|
|
61
|
+
export default _default;
|
package/dist/index.css
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
.m-collab-status[data-v-d870b105] {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 6px;
|
|
5
|
+
}
|
|
6
|
+
.m-collab-status__dot[data-v-d870b105] {
|
|
7
|
+
width: 8px;
|
|
8
|
+
height: 8px;
|
|
9
|
+
border-radius: 50%;
|
|
10
|
+
flex-shrink: 0;
|
|
11
|
+
}
|
|
12
|
+
.m-collab-status__dot--synced[data-v-d870b105] {
|
|
13
|
+
background-color: #0CA678;
|
|
14
|
+
}
|
|
15
|
+
.m-collab-status__dot--connecting[data-v-d870b105] {
|
|
16
|
+
background-color: #F59F00;
|
|
17
|
+
}
|
|
18
|
+
.m-collab-status__dot--offline[data-v-d870b105] {
|
|
19
|
+
background-color: rgba(var(--m-theme-on-surface), 0.3);
|
|
20
|
+
}
|
|
21
|
+
.m-collab-status__label[data-v-d870b105] {
|
|
22
|
+
opacity: 0.7;
|
|
23
|
+
}
|
|
24
|
+
.m-collab-status__avatars[data-v-d870b105] {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
margin-left: 4px;
|
|
28
|
+
}
|
|
29
|
+
.m-collab-status__avatar[data-v-d870b105] {
|
|
30
|
+
width: 18px;
|
|
31
|
+
height: 18px;
|
|
32
|
+
border-radius: 50%;
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
color: #fff;
|
|
37
|
+
font-size: 0.625rem;
|
|
38
|
+
font-weight: bold;
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
border: 1.5px solid rgba(var(--m-theme-surface), 1);
|
|
41
|
+
margin-left: -6px;
|
|
42
|
+
}
|
|
43
|
+
.m-collab-status__avatar[data-v-d870b105]:first-child {
|
|
44
|
+
margin-left: 0;
|
|
45
|
+
}
|
|
46
|
+
.m-collab-status__avatar img[data-v-d870b105] {
|
|
47
|
+
width: 100%;
|
|
48
|
+
height: 100%;
|
|
49
|
+
object-fit: cover;
|
|
50
|
+
}
|
|
51
|
+
.m-collab-status__avatar--more[data-v-d870b105] {
|
|
52
|
+
background-color: rgba(var(--m-theme-on-surface), 0.3);
|
|
53
|
+
}.m-presence {
|
|
54
|
+
position: absolute;
|
|
55
|
+
inset: 0;
|
|
56
|
+
pointer-events: none;
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
z-index: 10;
|
|
59
|
+
}
|
|
60
|
+
.m-presence-selection {
|
|
61
|
+
position: absolute;
|
|
62
|
+
border: 1.5px solid;
|
|
63
|
+
pointer-events: none;
|
|
64
|
+
}
|
|
65
|
+
.m-presence-tag {
|
|
66
|
+
position: absolute;
|
|
67
|
+
top: 0;
|
|
68
|
+
left: -1.5px;
|
|
69
|
+
transform: translateY(-100%);
|
|
70
|
+
padding: 1px 6px;
|
|
71
|
+
border-radius: 4px 4px 4px 0;
|
|
72
|
+
color: #fff;
|
|
73
|
+
font-size: 12px;
|
|
74
|
+
line-height: 16px;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
}
|
|
77
|
+
.m-presence-cursor {
|
|
78
|
+
position: absolute;
|
|
79
|
+
top: 0;
|
|
80
|
+
left: 0;
|
|
81
|
+
will-change: transform;
|
|
82
|
+
transition: transform 80ms linear;
|
|
83
|
+
}
|
|
84
|
+
.m-presence-cursor svg {
|
|
85
|
+
display: block;
|
|
86
|
+
}
|
|
87
|
+
.m-presence-label {
|
|
88
|
+
position: absolute;
|
|
89
|
+
top: 16px;
|
|
90
|
+
left: 12px;
|
|
91
|
+
padding: 1px 6px;
|
|
92
|
+
border-radius: 4px;
|
|
93
|
+
color: #fff;
|
|
94
|
+
font-size: 12px;
|
|
95
|
+
line-height: 18px;
|
|
96
|
+
white-space: nowrap;
|
|
97
|
+
}/*$vite$:1*/
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './AbstractProvider';
|
|
2
|
+
export { default as collaborationPlugin } from './collaborationPlugin';
|
|
3
|
+
export { default as presencePlugin } from './presencePlugin';
|
|
4
|
+
export * from './WebsocketProvider';
|
|
5
|
+
/** 注册「协同会话」+「在场感知」两个插件(presence 依赖 collaboration 的 provider,顺序固定)。 */
|
|
6
|
+
export declare function plugin(): import("mce").Plugin[];
|
|
7
|
+
export default plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,909 @@
|
|
|
1
|
+
import { INTERNAL_ORIGIN, definePlugin, useEditor } from "mce";
|
|
2
|
+
import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, defineComponent, normalizeClass, normalizeStyle, onScopeDispose, openBlock, ref, renderList, shallowRef, toDisplayString, unref, watch } from "vue";
|
|
3
|
+
import { Observable } from "modern-idoc";
|
|
4
|
+
import * as awarenessProtocol from "y-protocols/awareness";
|
|
5
|
+
import * as syncProtocol from "y-protocols/sync";
|
|
6
|
+
//#region src/CollaborationStatus.vue?vue&type=script&setup=true&lang.ts
|
|
7
|
+
var _hoisted_1$1 = {
|
|
8
|
+
key: 0,
|
|
9
|
+
class: "m-collab-status"
|
|
10
|
+
};
|
|
11
|
+
var _hoisted_2$1 = { class: "m-collab-status__label" };
|
|
12
|
+
var _hoisted_3$1 = {
|
|
13
|
+
key: 0,
|
|
14
|
+
class: "m-collab-status__avatars"
|
|
15
|
+
};
|
|
16
|
+
var _hoisted_4 = ["title"];
|
|
17
|
+
var _hoisted_5 = ["src", "alt"];
|
|
18
|
+
var _hoisted_6 = {
|
|
19
|
+
key: 0,
|
|
20
|
+
class: "m-collab-status__avatar m-collab-status__avatar--more"
|
|
21
|
+
};
|
|
22
|
+
var MAX_AVATARS = 5;
|
|
23
|
+
var CollaborationStatus_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
24
|
+
__name: "CollaborationStatus",
|
|
25
|
+
setup(__props) {
|
|
26
|
+
const { collaboration, presence, t } = useEditor();
|
|
27
|
+
/** 连接 / 同步状态:offline → connecting → synced。 */
|
|
28
|
+
const status = computed(() => {
|
|
29
|
+
if (!collaboration.connected.value) return "offline";
|
|
30
|
+
return collaboration.synced.value ? "synced" : "connecting";
|
|
31
|
+
});
|
|
32
|
+
const avatars = computed(() => {
|
|
33
|
+
return presence.peers.value.slice(0, MAX_AVATARS).map((peer) => ({
|
|
34
|
+
clientId: peer.clientId,
|
|
35
|
+
name: peer.user?.name ?? "",
|
|
36
|
+
color: peer.user?.color ?? "#868E96",
|
|
37
|
+
avatar: peer.user?.avatar,
|
|
38
|
+
initial: (peer.user?.name ?? "?").trim().charAt(0).toUpperCase() || "?"
|
|
39
|
+
}));
|
|
40
|
+
});
|
|
41
|
+
const overflow = computed(() => Math.max(0, presence.peers.value.length - MAX_AVATARS));
|
|
42
|
+
return (_ctx, _cache) => {
|
|
43
|
+
return unref(collaboration).active.value ? (openBlock(), createElementBlock("div", _hoisted_1$1, [
|
|
44
|
+
createElementVNode("span", { class: normalizeClass(["m-collab-status__dot", `m-collab-status__dot--${status.value}`]) }, null, 2),
|
|
45
|
+
createElementVNode("span", _hoisted_2$1, toDisplayString(unref(t)(`collaboration:${status.value}`)), 1),
|
|
46
|
+
avatars.value.length ? (openBlock(), createElementBlock("div", _hoisted_3$1, [(openBlock(true), createElementBlock(Fragment, null, renderList(avatars.value, (peer) => {
|
|
47
|
+
return openBlock(), createElementBlock("span", {
|
|
48
|
+
key: peer.clientId,
|
|
49
|
+
class: "m-collab-status__avatar",
|
|
50
|
+
style: normalizeStyle({ backgroundColor: peer.color }),
|
|
51
|
+
title: peer.name
|
|
52
|
+
}, [peer.avatar ? (openBlock(), createElementBlock("img", {
|
|
53
|
+
key: 0,
|
|
54
|
+
src: peer.avatar,
|
|
55
|
+
alt: peer.name
|
|
56
|
+
}, null, 8, _hoisted_5)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(peer.initial), 1)], 64))], 12, _hoisted_4);
|
|
57
|
+
}), 128)), overflow.value ? (openBlock(), createElementBlock("span", _hoisted_6, " +" + toDisplayString(overflow.value), 1)) : createCommentVNode("", true)])) : createCommentVNode("", true)
|
|
58
|
+
])) : createCommentVNode("", true);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region \0plugin-vue:export-helper
|
|
64
|
+
var _plugin_vue_export_helper_default = (sfc, props) => {
|
|
65
|
+
const target = sfc.__vccOpts || sfc;
|
|
66
|
+
for (const [key, val] of props) target[key] = val;
|
|
67
|
+
return target;
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/CollaborationStatus.vue
|
|
71
|
+
var CollaborationStatus_default = /*#__PURE__*/ _plugin_vue_export_helper_default(CollaborationStatus_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-d870b105"]]);
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region ../../node_modules/.pnpm/lib0@0.2.117/node_modules/lib0/math.js
|
|
74
|
+
/**
|
|
75
|
+
* Common Math expressions.
|
|
76
|
+
*
|
|
77
|
+
* @module math
|
|
78
|
+
*/
|
|
79
|
+
var floor = Math.floor;
|
|
80
|
+
/**
|
|
81
|
+
* @function
|
|
82
|
+
* @param {number} a
|
|
83
|
+
* @param {number} b
|
|
84
|
+
* @return {number} The smaller element of a and b
|
|
85
|
+
*/
|
|
86
|
+
var min = (a, b) => a < b ? a : b;
|
|
87
|
+
/**
|
|
88
|
+
* @function
|
|
89
|
+
* @param {number} a
|
|
90
|
+
* @param {number} b
|
|
91
|
+
* @return {number} The bigger element of a and b
|
|
92
|
+
*/
|
|
93
|
+
var max = (a, b) => a > b ? a : b;
|
|
94
|
+
Number.isNaN;
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region ../../node_modules/.pnpm/lib0@0.2.117/node_modules/lib0/number.js
|
|
97
|
+
/**
|
|
98
|
+
* Utility helpers for working with numbers.
|
|
99
|
+
*
|
|
100
|
+
* @module number
|
|
101
|
+
*/
|
|
102
|
+
var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
|
|
103
|
+
Number.MIN_SAFE_INTEGER;
|
|
104
|
+
Number.isInteger;
|
|
105
|
+
Number.isNaN;
|
|
106
|
+
Number.parseInt;
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region ../../node_modules/.pnpm/lib0@0.2.117/node_modules/lib0/string.js
|
|
109
|
+
/**
|
|
110
|
+
* Utility module to work with strings.
|
|
111
|
+
*
|
|
112
|
+
* @module string
|
|
113
|
+
*/
|
|
114
|
+
var fromCharCode = String.fromCharCode;
|
|
115
|
+
String.fromCodePoint;
|
|
116
|
+
fromCharCode(65535);
|
|
117
|
+
/* c8 ignore next */
|
|
118
|
+
var utf8TextEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
|
|
119
|
+
/* c8 ignore next */
|
|
120
|
+
var utf8TextDecoder = typeof TextDecoder === "undefined" ? null : new TextDecoder("utf-8", {
|
|
121
|
+
fatal: true,
|
|
122
|
+
ignoreBOM: true
|
|
123
|
+
});
|
|
124
|
+
/* c8 ignore start */
|
|
125
|
+
if (utf8TextDecoder && utf8TextDecoder.decode(new Uint8Array()).length === 1)
|
|
126
|
+
/* c8 ignore next */
|
|
127
|
+
utf8TextDecoder = null;
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region ../../node_modules/.pnpm/lib0@0.2.117/node_modules/lib0/error.js
|
|
130
|
+
/**
|
|
131
|
+
* Error helpers.
|
|
132
|
+
*
|
|
133
|
+
* @module error
|
|
134
|
+
*/
|
|
135
|
+
/**
|
|
136
|
+
* @param {string} s
|
|
137
|
+
* @return {Error}
|
|
138
|
+
*/
|
|
139
|
+
/* c8 ignore next */
|
|
140
|
+
var create = (s) => new Error(s);
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region ../../node_modules/.pnpm/lib0@0.2.117/node_modules/lib0/encoding.js
|
|
143
|
+
/**
|
|
144
|
+
* Efficient schema-less binary encoding with support for variable length encoding.
|
|
145
|
+
*
|
|
146
|
+
* Use [lib0/encoding] with [lib0/decoding]. Every encoding function has a corresponding decoding function.
|
|
147
|
+
*
|
|
148
|
+
* Encodes numbers in little-endian order (least to most significant byte order)
|
|
149
|
+
* and is compatible with Golang's binary encoding (https://golang.org/pkg/encoding/binary/)
|
|
150
|
+
* which is also used in Protocol Buffers.
|
|
151
|
+
*
|
|
152
|
+
* ```js
|
|
153
|
+
* // encoding step
|
|
154
|
+
* const encoder = encoding.createEncoder()
|
|
155
|
+
* encoding.writeVarUint(encoder, 256)
|
|
156
|
+
* encoding.writeVarString(encoder, 'Hello world!')
|
|
157
|
+
* const buf = encoding.toUint8Array(encoder)
|
|
158
|
+
* ```
|
|
159
|
+
*
|
|
160
|
+
* ```js
|
|
161
|
+
* // decoding step
|
|
162
|
+
* const decoder = decoding.createDecoder(buf)
|
|
163
|
+
* decoding.readVarUint(decoder) // => 256
|
|
164
|
+
* decoding.readVarString(decoder) // => 'Hello world!'
|
|
165
|
+
* decoding.hasContent(decoder) // => false - all data is read
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* @module encoding
|
|
169
|
+
*/
|
|
170
|
+
/**
|
|
171
|
+
* A BinaryEncoder handles the encoding to an Uint8Array.
|
|
172
|
+
*/
|
|
173
|
+
var Encoder = class {
|
|
174
|
+
constructor() {
|
|
175
|
+
this.cpos = 0;
|
|
176
|
+
this.cbuf = new Uint8Array(100);
|
|
177
|
+
/**
|
|
178
|
+
* @type {Array<Uint8Array>}
|
|
179
|
+
*/
|
|
180
|
+
this.bufs = [];
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* @function
|
|
185
|
+
* @return {Encoder}
|
|
186
|
+
*/
|
|
187
|
+
var createEncoder = () => new Encoder();
|
|
188
|
+
/**
|
|
189
|
+
* The current length of the encoded data.
|
|
190
|
+
*
|
|
191
|
+
* @function
|
|
192
|
+
* @param {Encoder} encoder
|
|
193
|
+
* @return {number}
|
|
194
|
+
*/
|
|
195
|
+
var length = (encoder) => {
|
|
196
|
+
let len = encoder.cpos;
|
|
197
|
+
for (let i = 0; i < encoder.bufs.length; i++) len += encoder.bufs[i].length;
|
|
198
|
+
return len;
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Transform to Uint8Array.
|
|
202
|
+
*
|
|
203
|
+
* @function
|
|
204
|
+
* @param {Encoder} encoder
|
|
205
|
+
* @return {Uint8Array<ArrayBuffer>} The created ArrayBuffer.
|
|
206
|
+
*/
|
|
207
|
+
var toUint8Array = (encoder) => {
|
|
208
|
+
const uint8arr = new Uint8Array(length(encoder));
|
|
209
|
+
let curPos = 0;
|
|
210
|
+
for (let i = 0; i < encoder.bufs.length; i++) {
|
|
211
|
+
const d = encoder.bufs[i];
|
|
212
|
+
uint8arr.set(d, curPos);
|
|
213
|
+
curPos += d.length;
|
|
214
|
+
}
|
|
215
|
+
uint8arr.set(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos), curPos);
|
|
216
|
+
return uint8arr;
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Write one byte to the encoder.
|
|
220
|
+
*
|
|
221
|
+
* @function
|
|
222
|
+
* @param {Encoder} encoder
|
|
223
|
+
* @param {number} num The byte that is to be encoded.
|
|
224
|
+
*/
|
|
225
|
+
var write = (encoder, num) => {
|
|
226
|
+
const bufferLen = encoder.cbuf.length;
|
|
227
|
+
if (encoder.cpos === bufferLen) {
|
|
228
|
+
encoder.bufs.push(encoder.cbuf);
|
|
229
|
+
encoder.cbuf = new Uint8Array(bufferLen * 2);
|
|
230
|
+
encoder.cpos = 0;
|
|
231
|
+
}
|
|
232
|
+
encoder.cbuf[encoder.cpos++] = num;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* Write a variable length unsigned integer. Max encodable integer is 2^53.
|
|
236
|
+
*
|
|
237
|
+
* @function
|
|
238
|
+
* @param {Encoder} encoder
|
|
239
|
+
* @param {number} num The number that is to be encoded.
|
|
240
|
+
*/
|
|
241
|
+
var writeVarUint = (encoder, num) => {
|
|
242
|
+
while (num > 127) {
|
|
243
|
+
write(encoder, 128 | 127 & num);
|
|
244
|
+
num = floor(num / 128);
|
|
245
|
+
}
|
|
246
|
+
write(encoder, 127 & num);
|
|
247
|
+
};
|
|
248
|
+
new Uint8Array(3e4).length / 3;
|
|
249
|
+
utf8TextEncoder && utf8TextEncoder.encodeInto;
|
|
250
|
+
/**
|
|
251
|
+
* Append fixed-length Uint8Array to the encoder.
|
|
252
|
+
*
|
|
253
|
+
* @function
|
|
254
|
+
* @param {Encoder} encoder
|
|
255
|
+
* @param {Uint8Array} uint8Array
|
|
256
|
+
*/
|
|
257
|
+
var writeUint8Array = (encoder, uint8Array) => {
|
|
258
|
+
const bufferLen = encoder.cbuf.length;
|
|
259
|
+
const cpos = encoder.cpos;
|
|
260
|
+
const leftCopyLen = min(bufferLen - cpos, uint8Array.length);
|
|
261
|
+
const rightCopyLen = uint8Array.length - leftCopyLen;
|
|
262
|
+
encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos);
|
|
263
|
+
encoder.cpos += leftCopyLen;
|
|
264
|
+
if (rightCopyLen > 0) {
|
|
265
|
+
encoder.bufs.push(encoder.cbuf);
|
|
266
|
+
encoder.cbuf = new Uint8Array(max(bufferLen * 2, rightCopyLen));
|
|
267
|
+
encoder.cbuf.set(uint8Array.subarray(leftCopyLen));
|
|
268
|
+
encoder.cpos = rightCopyLen;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* Append an Uint8Array to Encoder.
|
|
273
|
+
*
|
|
274
|
+
* @function
|
|
275
|
+
* @param {Encoder} encoder
|
|
276
|
+
* @param {Uint8Array} uint8Array
|
|
277
|
+
*/
|
|
278
|
+
var writeVarUint8Array = (encoder, uint8Array) => {
|
|
279
|
+
writeVarUint(encoder, uint8Array.byteLength);
|
|
280
|
+
writeUint8Array(encoder, uint8Array);
|
|
281
|
+
};
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region ../../node_modules/.pnpm/lib0@0.2.117/node_modules/lib0/decoding.js
|
|
284
|
+
/**
|
|
285
|
+
* Efficient schema-less binary decoding with support for variable length encoding.
|
|
286
|
+
*
|
|
287
|
+
* Use [lib0/decoding] with [lib0/encoding]. Every encoding function has a corresponding decoding function.
|
|
288
|
+
*
|
|
289
|
+
* Encodes numbers in little-endian order (least to most significant byte order)
|
|
290
|
+
* and is compatible with Golang's binary encoding (https://golang.org/pkg/encoding/binary/)
|
|
291
|
+
* which is also used in Protocol Buffers.
|
|
292
|
+
*
|
|
293
|
+
* ```js
|
|
294
|
+
* // encoding step
|
|
295
|
+
* const encoder = encoding.createEncoder()
|
|
296
|
+
* encoding.writeVarUint(encoder, 256)
|
|
297
|
+
* encoding.writeVarString(encoder, 'Hello world!')
|
|
298
|
+
* const buf = encoding.toUint8Array(encoder)
|
|
299
|
+
* ```
|
|
300
|
+
*
|
|
301
|
+
* ```js
|
|
302
|
+
* // decoding step
|
|
303
|
+
* const decoder = decoding.createDecoder(buf)
|
|
304
|
+
* decoding.readVarUint(decoder) // => 256
|
|
305
|
+
* decoding.readVarString(decoder) // => 'Hello world!'
|
|
306
|
+
* decoding.hasContent(decoder) // => false - all data is read
|
|
307
|
+
* ```
|
|
308
|
+
*
|
|
309
|
+
* @module decoding
|
|
310
|
+
*/
|
|
311
|
+
var errorUnexpectedEndOfArray = create("Unexpected end of array");
|
|
312
|
+
var errorIntegerOutOfRange = create("Integer out of Range");
|
|
313
|
+
/**
|
|
314
|
+
* A Decoder handles the decoding of an Uint8Array.
|
|
315
|
+
* @template {ArrayBufferLike} [Buf=ArrayBufferLike]
|
|
316
|
+
*/
|
|
317
|
+
var Decoder = class {
|
|
318
|
+
/**
|
|
319
|
+
* @param {Uint8Array<Buf>} uint8Array Binary data to decode
|
|
320
|
+
*/
|
|
321
|
+
constructor(uint8Array) {
|
|
322
|
+
/**
|
|
323
|
+
* Decoding target.
|
|
324
|
+
*
|
|
325
|
+
* @type {Uint8Array<Buf>}
|
|
326
|
+
*/
|
|
327
|
+
this.arr = uint8Array;
|
|
328
|
+
/**
|
|
329
|
+
* Current decoding position.
|
|
330
|
+
*
|
|
331
|
+
* @type {number}
|
|
332
|
+
*/
|
|
333
|
+
this.pos = 0;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
/**
|
|
337
|
+
* @function
|
|
338
|
+
* @template {ArrayBufferLike} Buf
|
|
339
|
+
* @param {Uint8Array<Buf>} uint8Array
|
|
340
|
+
* @return {Decoder<Buf>}
|
|
341
|
+
*/
|
|
342
|
+
var createDecoder = (uint8Array) => new Decoder(uint8Array);
|
|
343
|
+
/**
|
|
344
|
+
* Create an Uint8Array view of the next `len` bytes and advance the position by `len`.
|
|
345
|
+
*
|
|
346
|
+
* Important: The Uint8Array still points to the underlying ArrayBuffer. Make sure to discard the result as soon as possible to prevent any memory leaks.
|
|
347
|
+
* Use `buffer.copyUint8Array` to copy the result into a new Uint8Array.
|
|
348
|
+
*
|
|
349
|
+
* @function
|
|
350
|
+
* @template {ArrayBufferLike} Buf
|
|
351
|
+
* @param {Decoder<Buf>} decoder The decoder instance
|
|
352
|
+
* @param {number} len The length of bytes to read
|
|
353
|
+
* @return {Uint8Array<Buf>}
|
|
354
|
+
*/
|
|
355
|
+
var readUint8Array = (decoder, len) => {
|
|
356
|
+
const view = new Uint8Array(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len);
|
|
357
|
+
decoder.pos += len;
|
|
358
|
+
return view;
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* Read variable length Uint8Array.
|
|
362
|
+
*
|
|
363
|
+
* Important: The Uint8Array still points to the underlying ArrayBuffer. Make sure to discard the result as soon as possible to prevent any memory leaks.
|
|
364
|
+
* Use `buffer.copyUint8Array` to copy the result into a new Uint8Array.
|
|
365
|
+
*
|
|
366
|
+
* @function
|
|
367
|
+
* @template {ArrayBufferLike} Buf
|
|
368
|
+
* @param {Decoder<Buf>} decoder
|
|
369
|
+
* @return {Uint8Array<Buf>}
|
|
370
|
+
*/
|
|
371
|
+
var readVarUint8Array = (decoder) => readUint8Array(decoder, readVarUint(decoder));
|
|
372
|
+
/**
|
|
373
|
+
* Read unsigned integer (32bit) with variable length.
|
|
374
|
+
* 1/8th of the storage is used as encoding overhead.
|
|
375
|
+
* * numbers < 2^7 is stored in one bytlength
|
|
376
|
+
* * numbers < 2^14 is stored in two bylength
|
|
377
|
+
*
|
|
378
|
+
* @function
|
|
379
|
+
* @param {Decoder} decoder
|
|
380
|
+
* @return {number} An unsigned integer.length
|
|
381
|
+
*/
|
|
382
|
+
var readVarUint = (decoder) => {
|
|
383
|
+
let num = 0;
|
|
384
|
+
let mult = 1;
|
|
385
|
+
const len = decoder.arr.length;
|
|
386
|
+
while (decoder.pos < len) {
|
|
387
|
+
const r = decoder.arr[decoder.pos++];
|
|
388
|
+
num = num + (r & 127) * mult;
|
|
389
|
+
mult *= 128;
|
|
390
|
+
if (r < 128) return num;
|
|
391
|
+
/* c8 ignore start */
|
|
392
|
+
if (num > MAX_SAFE_INTEGER) throw errorIntegerOutOfRange;
|
|
393
|
+
}
|
|
394
|
+
throw errorUnexpectedEndOfArray;
|
|
395
|
+
};
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/AbstractProvider.ts
|
|
398
|
+
/**
|
|
399
|
+
* 顶层消息类型。一个消息 = [messageType: varUint, payload...],房间名由传输层在 URL 等处携带。
|
|
400
|
+
*/
|
|
401
|
+
var MESSAGE_SYNC = 0;
|
|
402
|
+
var MESSAGE_AWARENESS = 1;
|
|
403
|
+
var MESSAGE_QUERY_AWARENESS = 3;
|
|
404
|
+
/**
|
|
405
|
+
* 可插拔传输层基类。负责 Yjs 同步协议(state vector 握手 + 增量)与 Awareness(在场感知)的
|
|
406
|
+
* 编解码与路由;具体字节怎么收发由子类实现(WebSocket / WebRTC / BroadcastChannel / 自定义)。
|
|
407
|
+
*
|
|
408
|
+
* 关注点分离:
|
|
409
|
+
* - 本类只懂「协议」:何时发 syncStep1、如何把 update / awareness 编成消息、收到消息如何分发。
|
|
410
|
+
* - 子类只懂「传输」:实现 {@link send},在连接打开时调用 {@link onOpen},收到字节时调用 {@link receive}。
|
|
411
|
+
*
|
|
412
|
+
* 回环防护:远端增量一律以 `this`(provider 实例)为 origin apply 到文档——既不进入本端
|
|
413
|
+
* UndoManager,又能刷新视图,且本端 'update' 监听据 `origin === this` 跳过再广播。
|
|
414
|
+
*/
|
|
415
|
+
var AbstractProvider = class extends Observable {
|
|
416
|
+
awareness;
|
|
417
|
+
synced = false;
|
|
418
|
+
/** 传输层连接状态。随 onOpen / onClose 翻转,供晚挂监听的上层补读初值。 */
|
|
419
|
+
connected = false;
|
|
420
|
+
_ydoc;
|
|
421
|
+
_doc;
|
|
422
|
+
_onUpdate;
|
|
423
|
+
_onAwarenessUpdate;
|
|
424
|
+
constructor(ydoc) {
|
|
425
|
+
super();
|
|
426
|
+
this._ydoc = ydoc;
|
|
427
|
+
this._doc = ydoc._yDoc;
|
|
428
|
+
this.awareness = new awarenessProtocol.Awareness(this._doc);
|
|
429
|
+
this._onUpdate = (update, origin) => {
|
|
430
|
+
if (origin === this || origin === INTERNAL_ORIGIN) return;
|
|
431
|
+
const encoder = createEncoder();
|
|
432
|
+
writeVarUint(encoder, 0);
|
|
433
|
+
syncProtocol.writeUpdate(encoder, update);
|
|
434
|
+
this.send(toUint8Array(encoder));
|
|
435
|
+
};
|
|
436
|
+
this._onAwarenessUpdate = ({ added, updated, removed }, origin) => {
|
|
437
|
+
if (origin === this) return;
|
|
438
|
+
const changed = added.concat(updated, removed);
|
|
439
|
+
this.send(this._encodeAwareness(changed));
|
|
440
|
+
};
|
|
441
|
+
ydoc.on("update", this._onUpdate);
|
|
442
|
+
this.awareness.on("update", this._onAwarenessUpdate);
|
|
443
|
+
if (typeof addEventListener !== "undefined") addEventListener("beforeunload", this._removeAwareness);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* 把本端全量状态作为一条 sync update 主动推给对端。
|
|
447
|
+
*
|
|
448
|
+
* 用于**无中心服务端的 P2P 传输**(如 BroadcastChannel / WebRTC):step1/step2 握手只能让
|
|
449
|
+
* 「发起方」拉到对端缺失的内容,无法让已在场的对端获得本端新内容。当本端在会话中途换上一份
|
|
450
|
+
* 新内容(如 setDoc 后 provider 重建)时,调用此方法把内容推过去,对端 applyUpdate 即合并。
|
|
451
|
+
* 有服务端的传输(WebSocket)由服务端持久化 + 转发,通常不需要。
|
|
452
|
+
*/
|
|
453
|
+
broadcastState() {
|
|
454
|
+
const encoder = createEncoder();
|
|
455
|
+
writeVarUint(encoder, 0);
|
|
456
|
+
syncProtocol.writeUpdate(encoder, this._ydoc.encodeStateAsUpdate());
|
|
457
|
+
this.send(toUint8Array(encoder));
|
|
458
|
+
}
|
|
459
|
+
/** 连接(重新)建立后由子类调用:发起 sync step1 并广播本端 awareness。 */
|
|
460
|
+
onOpen() {
|
|
461
|
+
const encoder = createEncoder();
|
|
462
|
+
writeVarUint(encoder, 0);
|
|
463
|
+
syncProtocol.writeSyncStep1(encoder, this._doc);
|
|
464
|
+
this.send(toUint8Array(encoder));
|
|
465
|
+
if (this.awareness.getLocalState() !== null) this.send(this._encodeAwareness([this._doc.clientID]));
|
|
466
|
+
this.connected = true;
|
|
467
|
+
this.emit("status", { connected: true });
|
|
468
|
+
}
|
|
469
|
+
/** 连接断开后由子类调用。 */
|
|
470
|
+
onClose() {
|
|
471
|
+
awarenessProtocol.removeAwarenessStates(this.awareness, Array.from(this.awareness.getStates().keys()).filter((id) => id !== this._doc.clientID), this);
|
|
472
|
+
this.connected = false;
|
|
473
|
+
this.emit("status", { connected: false });
|
|
474
|
+
}
|
|
475
|
+
/** 子类收到一帧消息字节后调用,路由到 sync / awareness 处理。 */
|
|
476
|
+
receive(data) {
|
|
477
|
+
const decoder = createDecoder(data);
|
|
478
|
+
const encoder = createEncoder();
|
|
479
|
+
switch (readVarUint(decoder)) {
|
|
480
|
+
case 0: {
|
|
481
|
+
writeVarUint(encoder, 0);
|
|
482
|
+
const syncType = syncProtocol.readSyncMessage(decoder, encoder, this._doc, this);
|
|
483
|
+
if (!this.synced && syncType === syncProtocol.messageYjsSyncStep2) this._setSynced(true);
|
|
484
|
+
if (length(encoder) > 1) this.send(toUint8Array(encoder));
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case 1:
|
|
488
|
+
awarenessProtocol.applyAwarenessUpdate(this.awareness, readVarUint8Array(decoder), this);
|
|
489
|
+
break;
|
|
490
|
+
case 3:
|
|
491
|
+
this.send(this._encodeAwareness(Array.from(this.awareness.getStates().keys())));
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
_encodeAwareness(clients) {
|
|
496
|
+
const encoder = createEncoder();
|
|
497
|
+
writeVarUint(encoder, 1);
|
|
498
|
+
writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, clients));
|
|
499
|
+
return toUint8Array(encoder);
|
|
500
|
+
}
|
|
501
|
+
_removeAwareness = () => {
|
|
502
|
+
awarenessProtocol.removeAwarenessStates(this.awareness, [this._doc.clientID], "unload");
|
|
503
|
+
};
|
|
504
|
+
_setSynced(value) {
|
|
505
|
+
if (this.synced !== value) {
|
|
506
|
+
this.synced = value;
|
|
507
|
+
this.emit("synced", value);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
destroy() {
|
|
511
|
+
this._removeAwareness();
|
|
512
|
+
this._ydoc.off("update", this._onUpdate);
|
|
513
|
+
this.awareness.off("update", this._onAwarenessUpdate);
|
|
514
|
+
this.awareness.destroy();
|
|
515
|
+
if (typeof removeEventListener !== "undefined") removeEventListener("beforeunload", this._removeAwareness);
|
|
516
|
+
super.destroy?.();
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/WebsocketProvider.ts
|
|
521
|
+
/**
|
|
522
|
+
* 基于 WebSocket 的传输实现,协议与 y-websocket 服务端兼容:
|
|
523
|
+
* 连接地址为 `${url}/${room}`,二进制帧承载 {@link AbstractProvider} 定义的 sync / awareness 消息。
|
|
524
|
+
*
|
|
525
|
+
* 特性:指数退避重连、连接未就绪时缓冲消息、`onOpen` 自动发起 state vector 握手。
|
|
526
|
+
*/
|
|
527
|
+
var WebsocketProvider = class extends AbstractProvider {
|
|
528
|
+
url;
|
|
529
|
+
ws = null;
|
|
530
|
+
shouldConnect;
|
|
531
|
+
_reconnectInterval;
|
|
532
|
+
_maxReconnectInterval;
|
|
533
|
+
_protocols;
|
|
534
|
+
_reconnectAttempts = 0;
|
|
535
|
+
_reconnectTimer = null;
|
|
536
|
+
/** 连接未 OPEN 时缓冲待发字节,OPEN 后冲刷。 */
|
|
537
|
+
_pending = [];
|
|
538
|
+
constructor(url, room, ydoc, options = {}) {
|
|
539
|
+
super(ydoc);
|
|
540
|
+
this.url = `${url.replace(/\/$/, "")}/${room}`;
|
|
541
|
+
this._reconnectInterval = options.reconnectInterval ?? 1e3;
|
|
542
|
+
this._maxReconnectInterval = options.maxReconnectInterval ?? 3e4;
|
|
543
|
+
this._protocols = options.protocols;
|
|
544
|
+
this.shouldConnect = options.connect ?? true;
|
|
545
|
+
if (this.shouldConnect) this.connect();
|
|
546
|
+
}
|
|
547
|
+
connect() {
|
|
548
|
+
this.shouldConnect = true;
|
|
549
|
+
if (this.ws) return;
|
|
550
|
+
const ws = new WebSocket(this.url, this._protocols);
|
|
551
|
+
ws.binaryType = "arraybuffer";
|
|
552
|
+
this.ws = ws;
|
|
553
|
+
ws.onopen = () => {
|
|
554
|
+
this._reconnectAttempts = 0;
|
|
555
|
+
const pending = this._pending;
|
|
556
|
+
this._pending = [];
|
|
557
|
+
pending.forEach((data) => ws.send(data));
|
|
558
|
+
this.onOpen();
|
|
559
|
+
};
|
|
560
|
+
ws.onmessage = (event) => {
|
|
561
|
+
this.receive(new Uint8Array(event.data));
|
|
562
|
+
};
|
|
563
|
+
ws.onclose = () => {
|
|
564
|
+
this.ws = null;
|
|
565
|
+
this.onClose();
|
|
566
|
+
this._setSynced(false);
|
|
567
|
+
if (this.shouldConnect) this._scheduleReconnect();
|
|
568
|
+
};
|
|
569
|
+
ws.onerror = () => {
|
|
570
|
+
ws.close();
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
disconnect() {
|
|
574
|
+
this.shouldConnect = false;
|
|
575
|
+
if (this._reconnectTimer) {
|
|
576
|
+
clearTimeout(this._reconnectTimer);
|
|
577
|
+
this._reconnectTimer = null;
|
|
578
|
+
}
|
|
579
|
+
this.ws?.close();
|
|
580
|
+
}
|
|
581
|
+
send(data) {
|
|
582
|
+
const ws = this.ws;
|
|
583
|
+
if (ws && ws.readyState === WebSocket.OPEN) ws.send(data);
|
|
584
|
+
else this._pending.push(data);
|
|
585
|
+
}
|
|
586
|
+
_scheduleReconnect() {
|
|
587
|
+
if (this._reconnectTimer) return;
|
|
588
|
+
const delay = Math.min(this._reconnectInterval * 2 ** this._reconnectAttempts, this._maxReconnectInterval);
|
|
589
|
+
this._reconnectAttempts++;
|
|
590
|
+
this._reconnectTimer = setTimeout(() => {
|
|
591
|
+
this._reconnectTimer = null;
|
|
592
|
+
if (this.shouldConnect) this.connect();
|
|
593
|
+
}, delay);
|
|
594
|
+
}
|
|
595
|
+
destroy() {
|
|
596
|
+
this.disconnect();
|
|
597
|
+
super.destroy();
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region src/collaborationPlugin.ts
|
|
602
|
+
/**
|
|
603
|
+
* 协同会话接入:把可插拔的传输层({@link AbstractProvider} 子类)接到编辑器的文档生命周期上。
|
|
604
|
+
*
|
|
605
|
+
* 职责边界:本插件只管「会话生命周期」——建立 / 销毁 Provider、跨文档切换重建、暴露连接 /
|
|
606
|
+
* 同步状态。协议编解码(sync / awareness)在 {@link AbstractProvider},字节收发在具体 Provider。
|
|
607
|
+
*
|
|
608
|
+
* 关键不变量:Provider 绑定在某个具体的底层 YDoc 上(`Doc._yDoc`)。`setDoc` 切换文档时旧
|
|
609
|
+
* YDoc 会被 destroy,故必须以新文档的 YDoc 重建 Provider —— 这里订阅 `docSet` 事件完成重建。
|
|
610
|
+
*/
|
|
611
|
+
var collaborationPlugin_default = definePlugin((editor, options) => {
|
|
612
|
+
const provider = shallowRef();
|
|
613
|
+
const connected = ref(false);
|
|
614
|
+
const synced = ref(false);
|
|
615
|
+
const active = ref(false);
|
|
616
|
+
let factory;
|
|
617
|
+
/** 销毁当前 Provider 并复位状态(保留 factory / active,供文档切换后重建)。 */
|
|
618
|
+
function teardown() {
|
|
619
|
+
const p = provider.value;
|
|
620
|
+
if (p) {
|
|
621
|
+
p.destroy();
|
|
622
|
+
provider.value = void 0;
|
|
623
|
+
}
|
|
624
|
+
connected.value = false;
|
|
625
|
+
synced.value = false;
|
|
626
|
+
}
|
|
627
|
+
/** 用当前 factory 针对当前文档的 YDoc 建立 Provider 并接线状态。 */
|
|
628
|
+
function spawn() {
|
|
629
|
+
if (!factory) return;
|
|
630
|
+
const ydoc = editor.root.value._yDoc;
|
|
631
|
+
const p = factory(ydoc);
|
|
632
|
+
p.on("status", ({ connected: value }) => {
|
|
633
|
+
connected.value = value;
|
|
634
|
+
editor.emit("collaborationStatus", value);
|
|
635
|
+
});
|
|
636
|
+
p.on("synced", (value) => {
|
|
637
|
+
synced.value = value;
|
|
638
|
+
editor.emit("collaborationSynced", value);
|
|
639
|
+
});
|
|
640
|
+
connected.value = p.connected;
|
|
641
|
+
synced.value = p.synced;
|
|
642
|
+
provider.value = p;
|
|
643
|
+
return p;
|
|
644
|
+
}
|
|
645
|
+
const connect = (options) => {
|
|
646
|
+
teardown();
|
|
647
|
+
if (options.provider) factory = options.provider;
|
|
648
|
+
else if (options.url) {
|
|
649
|
+
const { url, room, websocket } = options;
|
|
650
|
+
factory = (doc) => new WebsocketProvider(url, room ?? editor.root.value.id, doc, websocket);
|
|
651
|
+
} else {
|
|
652
|
+
console.warn("[collaboration] connect 需要提供 url 或 provider");
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
active.value = true;
|
|
656
|
+
return spawn();
|
|
657
|
+
};
|
|
658
|
+
const disconnect = () => {
|
|
659
|
+
active.value = false;
|
|
660
|
+
factory = void 0;
|
|
661
|
+
teardown();
|
|
662
|
+
};
|
|
663
|
+
function onDocSet() {
|
|
664
|
+
if (!active.value) return;
|
|
665
|
+
try {
|
|
666
|
+
teardown();
|
|
667
|
+
} finally {
|
|
668
|
+
spawn();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
editor.collaboration = {
|
|
672
|
+
provider,
|
|
673
|
+
connected,
|
|
674
|
+
synced,
|
|
675
|
+
active,
|
|
676
|
+
connect,
|
|
677
|
+
disconnect
|
|
678
|
+
};
|
|
679
|
+
editor.registerStatusbarItem(CollaborationStatus_default);
|
|
680
|
+
return {
|
|
681
|
+
name: "mce:collaboration",
|
|
682
|
+
messages: {
|
|
683
|
+
en: {
|
|
684
|
+
"collaboration:synced": "Synced",
|
|
685
|
+
"collaboration:connecting": "Connecting…",
|
|
686
|
+
"collaboration:offline": "Offline"
|
|
687
|
+
},
|
|
688
|
+
zhHans: {
|
|
689
|
+
"collaboration:synced": "已同步",
|
|
690
|
+
"collaboration:connecting": "连接中…",
|
|
691
|
+
"collaboration:offline": "离线"
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
setup: () => {
|
|
695
|
+
editor.on("docSet", onDocSet);
|
|
696
|
+
if (options.collaboration) connect(options.collaboration);
|
|
697
|
+
onScopeDispose(() => {
|
|
698
|
+
editor.off("docSet", onDocSet);
|
|
699
|
+
factory = void 0;
|
|
700
|
+
active.value = false;
|
|
701
|
+
teardown();
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
});
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/Presence.vue?vue&type=script&setup=true&lang.ts
|
|
708
|
+
var _hoisted_1 = { class: "m-presence" };
|
|
709
|
+
var _hoisted_2 = {
|
|
710
|
+
width: "20",
|
|
711
|
+
height: "20",
|
|
712
|
+
viewBox: "0 0 20 20",
|
|
713
|
+
fill: "none"
|
|
714
|
+
};
|
|
715
|
+
var _hoisted_3 = ["fill"];
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/Presence.vue
|
|
718
|
+
var Presence_default = /* @__PURE__ */ defineComponent({
|
|
719
|
+
__name: "Presence",
|
|
720
|
+
setup(__props) {
|
|
721
|
+
const { presence, camera, getNodeById, getObb } = useEditor();
|
|
722
|
+
/** 全局画布坐标 → 画板(屏幕)像素,与 box mixin 的 obbToDrawboardObb 同公式。 */
|
|
723
|
+
function toDrawboard(p) {
|
|
724
|
+
const { zoom, position } = camera.value;
|
|
725
|
+
return {
|
|
726
|
+
x: p.x * zoom.x - position.x,
|
|
727
|
+
y: p.y * zoom.y - position.y
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const cursors = computed(() => {
|
|
731
|
+
return presence.peers.value.filter((peer) => peer.cursor).map((peer) => {
|
|
732
|
+
const { x, y } = toDrawboard(peer.cursor);
|
|
733
|
+
return {
|
|
734
|
+
clientId: peer.clientId,
|
|
735
|
+
color: peer.user?.color ?? "#1C7ED6",
|
|
736
|
+
name: peer.user?.name ?? "",
|
|
737
|
+
x,
|
|
738
|
+
y
|
|
739
|
+
};
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
const selections = computed(() => {
|
|
743
|
+
const boxes = [];
|
|
744
|
+
presence.peers.value.forEach((peer) => {
|
|
745
|
+
const color = peer.user?.color ?? "#1C7ED6";
|
|
746
|
+
const name = peer.user?.name ?? "";
|
|
747
|
+
let labeled = false;
|
|
748
|
+
peer.selection?.forEach((id) => {
|
|
749
|
+
const node = getNodeById(id);
|
|
750
|
+
if (!node) return;
|
|
751
|
+
boxes.push({
|
|
752
|
+
key: `${peer.clientId}:${id}`,
|
|
753
|
+
color,
|
|
754
|
+
name: labeled ? "" : name,
|
|
755
|
+
style: getObb(node, "drawboard").toCssStyle()
|
|
756
|
+
});
|
|
757
|
+
labeled = true;
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
return boxes;
|
|
761
|
+
});
|
|
762
|
+
return (_ctx, _cache) => {
|
|
763
|
+
return openBlock(), createElementBlock("div", _hoisted_1, [(openBlock(true), createElementBlock(Fragment, null, renderList(selections.value, (box) => {
|
|
764
|
+
return openBlock(), createElementBlock("div", {
|
|
765
|
+
key: box.key,
|
|
766
|
+
class: "m-presence-selection",
|
|
767
|
+
style: normalizeStyle({
|
|
768
|
+
...box.style,
|
|
769
|
+
borderColor: box.color
|
|
770
|
+
})
|
|
771
|
+
}, [box.name ? (openBlock(), createElementBlock("span", {
|
|
772
|
+
key: 0,
|
|
773
|
+
class: "m-presence-tag",
|
|
774
|
+
style: normalizeStyle({ backgroundColor: box.color })
|
|
775
|
+
}, toDisplayString(box.name), 5)) : createCommentVNode("", true)], 4);
|
|
776
|
+
}), 128)), (openBlock(true), createElementBlock(Fragment, null, renderList(cursors.value, (cursor) => {
|
|
777
|
+
return openBlock(), createElementBlock("div", {
|
|
778
|
+
key: cursor.clientId,
|
|
779
|
+
class: "m-presence-cursor",
|
|
780
|
+
style: normalizeStyle({ transform: `translate(${cursor.x}px, ${cursor.y}px)` })
|
|
781
|
+
}, [(openBlock(), createElementBlock("svg", _hoisted_2, [createElementVNode("path", {
|
|
782
|
+
d: "M3 3L10 17L11.5 11.5L17 10L3 3Z",
|
|
783
|
+
fill: cursor.color,
|
|
784
|
+
stroke: "#fff",
|
|
785
|
+
"stroke-width": "1",
|
|
786
|
+
"stroke-linejoin": "round"
|
|
787
|
+
}, null, 8, _hoisted_3)])), cursor.name ? (openBlock(), createElementBlock("span", {
|
|
788
|
+
key: 0,
|
|
789
|
+
class: "m-presence-label",
|
|
790
|
+
style: normalizeStyle({ backgroundColor: cursor.color })
|
|
791
|
+
}, toDisplayString(cursor.name), 5)) : createCommentVNode("", true)], 4);
|
|
792
|
+
}), 128))]);
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
//#endregion
|
|
797
|
+
//#region src/presencePlugin.ts
|
|
798
|
+
/** 默认调色板:按 clientId 取色,保证同一会话内每端颜色稳定。 */
|
|
799
|
+
var COLORS = [
|
|
800
|
+
"#F76707",
|
|
801
|
+
"#0CA678",
|
|
802
|
+
"#1C7ED6",
|
|
803
|
+
"#AE3EC9",
|
|
804
|
+
"#E64980",
|
|
805
|
+
"#F59F00",
|
|
806
|
+
"#15AABF",
|
|
807
|
+
"#7048E8"
|
|
808
|
+
];
|
|
809
|
+
function colorFromId(id) {
|
|
810
|
+
return COLORS[Math.abs(id) % COLORS.length];
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* 在场感知(Awareness):把本端的用户信息 / 光标 / 选区广播给协同对端,并把对端状态收敛为
|
|
814
|
+
* 响应式 `peers` 供 UI 渲染(远端光标、选框)。
|
|
815
|
+
*
|
|
816
|
+
* 职责边界:本插件只管「在场状态」。传输与会话生命周期在 {@link file://./collaboration.ts},
|
|
817
|
+
* Awareness 协议编解码在 `AbstractProvider`。Awareness 实例随 Provider 走 —— Provider 重建
|
|
818
|
+
* (连接 / 文档切换)后这里会重新接线并补发本端状态。
|
|
819
|
+
*/
|
|
820
|
+
var presencePlugin_default = definePlugin((editor) => {
|
|
821
|
+
const peers = ref([]);
|
|
822
|
+
const localUser = ref({});
|
|
823
|
+
function getAwareness() {
|
|
824
|
+
return editor.collaboration.provider.value?.awareness;
|
|
825
|
+
}
|
|
826
|
+
/** 把对端 awareness 状态收敛为 peers(剔除本端)。 */
|
|
827
|
+
function syncPeers() {
|
|
828
|
+
const awareness = getAwareness();
|
|
829
|
+
if (!awareness) {
|
|
830
|
+
peers.value = [];
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const local = awareness.clientID;
|
|
834
|
+
const list = [];
|
|
835
|
+
awareness.getStates().forEach((state, clientId) => {
|
|
836
|
+
if (clientId === local) return;
|
|
837
|
+
list.push({
|
|
838
|
+
clientId,
|
|
839
|
+
...state
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
peers.value = list;
|
|
843
|
+
}
|
|
844
|
+
/** 广播本端 user 字段(补齐默认色 / 名)。 */
|
|
845
|
+
function applyLocalUser() {
|
|
846
|
+
const awareness = getAwareness();
|
|
847
|
+
if (!awareness) return;
|
|
848
|
+
const clientId = awareness.clientID;
|
|
849
|
+
const u = localUser.value;
|
|
850
|
+
awareness.setLocalStateField("user", {
|
|
851
|
+
...u,
|
|
852
|
+
color: u.color ?? colorFromId(clientId),
|
|
853
|
+
name: u.name ?? `Guest-${clientId % 1e3}`
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
const setUser = (user) => {
|
|
857
|
+
localUser.value = {
|
|
858
|
+
...localUser.value,
|
|
859
|
+
...user
|
|
860
|
+
};
|
|
861
|
+
applyLocalUser();
|
|
862
|
+
};
|
|
863
|
+
editor.presence = {
|
|
864
|
+
peers,
|
|
865
|
+
localUser,
|
|
866
|
+
setUser
|
|
867
|
+
};
|
|
868
|
+
return {
|
|
869
|
+
name: "mce:presence",
|
|
870
|
+
components: [{
|
|
871
|
+
type: "overlay",
|
|
872
|
+
component: Presence_default,
|
|
873
|
+
order: "before"
|
|
874
|
+
}],
|
|
875
|
+
setup: () => {
|
|
876
|
+
const { collaboration, drawboardPointer, selection, getGlobalPointer } = editor;
|
|
877
|
+
watch(collaboration.provider, (provider, _old, onCleanup) => {
|
|
878
|
+
if (!provider) {
|
|
879
|
+
peers.value = [];
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const { awareness } = provider;
|
|
883
|
+
applyLocalUser();
|
|
884
|
+
awareness.setLocalStateField("selection", selection.value.map((n) => n.id));
|
|
885
|
+
awareness.on("change", syncPeers);
|
|
886
|
+
syncPeers();
|
|
887
|
+
onCleanup(() => awareness.off("change", syncPeers));
|
|
888
|
+
}, { immediate: true });
|
|
889
|
+
watch(drawboardPointer, () => {
|
|
890
|
+
const awareness = getAwareness();
|
|
891
|
+
if (!awareness) return;
|
|
892
|
+
awareness.setLocalStateField("cursor", drawboardPointer.value ? { ...getGlobalPointer() } : null);
|
|
893
|
+
});
|
|
894
|
+
watch(selection, () => {
|
|
895
|
+
const awareness = getAwareness();
|
|
896
|
+
if (!awareness) return;
|
|
897
|
+
awareness.setLocalStateField("selection", selection.value.map((n) => n.id));
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
});
|
|
902
|
+
//#endregion
|
|
903
|
+
//#region src/index.ts
|
|
904
|
+
/** 注册「协同会话」+「在场感知」两个插件(presence 依赖 collaboration 的 provider,顺序固定)。 */
|
|
905
|
+
function plugin() {
|
|
906
|
+
return [collaborationPlugin_default, presencePlugin_default];
|
|
907
|
+
}
|
|
908
|
+
//#endregion
|
|
909
|
+
export { AbstractProvider, MESSAGE_AWARENESS, MESSAGE_QUERY_AWARENESS, MESSAGE_SYNC, WebsocketProvider, collaborationPlugin_default as collaborationPlugin, plugin as default, plugin, presencePlugin_default as presencePlugin };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
declare global {
|
|
3
|
+
namespace Mce {
|
|
4
|
+
interface PresenceUser {
|
|
5
|
+
/** 业务侧用户 id(可选,用于去重 / 头像)。 */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** 展示名。缺省时按 clientId 生成访客名。 */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** 主题色(光标 / 选框 / 头像描边)。缺省时按 clientId 从调色板取色。 */
|
|
10
|
+
color?: string;
|
|
11
|
+
/** 头像 URL。 */
|
|
12
|
+
avatar?: string;
|
|
13
|
+
}
|
|
14
|
+
/** 本端广播 / 远端接收的在场状态。坐标统一用全局画布坐标,与缩放 / 平移无关。 */
|
|
15
|
+
interface PresenceState {
|
|
16
|
+
user?: PresenceUser;
|
|
17
|
+
/** 光标位置(全局画布坐标);离开画布时为 null。 */
|
|
18
|
+
cursor?: {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
} | null;
|
|
22
|
+
/** 选中的节点 id 列表。 */
|
|
23
|
+
selection?: string[];
|
|
24
|
+
}
|
|
25
|
+
interface Peer extends PresenceState {
|
|
26
|
+
/** yjs clientID,远端唯一标识。 */
|
|
27
|
+
clientId: number;
|
|
28
|
+
}
|
|
29
|
+
interface Presence {
|
|
30
|
+
/** 远端在场用户列表(不含本端),随 awareness 变化响应式更新。 */
|
|
31
|
+
peers: Ref<Peer[]>;
|
|
32
|
+
/** 本端用户信息。 */
|
|
33
|
+
localUser: Ref<PresenceUser>;
|
|
34
|
+
/** 设置本端用户信息并立即广播(连接前调用会在连接后自动应用)。 */
|
|
35
|
+
setUser: (user: Partial<PresenceUser>) => void;
|
|
36
|
+
}
|
|
37
|
+
interface Editor {
|
|
38
|
+
presence: Presence;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 在场感知(Awareness):把本端的用户信息 / 光标 / 选区广播给协同对端,并把对端状态收敛为
|
|
44
|
+
* 响应式 `peers` 供 UI 渲染(远端光标、选框)。
|
|
45
|
+
*
|
|
46
|
+
* 职责边界:本插件只管「在场状态」。传输与会话生命周期在 {@link file://./collaboration.ts},
|
|
47
|
+
* Awareness 协议编解码在 `AbstractProvider`。Awareness 实例随 Provider 走 —— Provider 重建
|
|
48
|
+
* (连接 / 文档切换)后这里会重新接线并补发本端状态。
|
|
49
|
+
*/
|
|
50
|
+
declare const _default: import("mce").Plugin;
|
|
51
|
+
export default _default;
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mce/collaboration",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.24.4",
|
|
5
|
+
"description": "Collaboration plugin for mce",
|
|
6
|
+
"author": "wxm",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/qq15725/mce",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/qq15725/mce.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/qq15725/mce/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mce",
|
|
18
|
+
"collaboration",
|
|
19
|
+
"plugin"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": true,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js",
|
|
26
|
+
"require": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./*.mjs": "./*.js",
|
|
29
|
+
"./*": "./*"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"module": "./dist/index.js",
|
|
33
|
+
"browser": "./dist/index.js",
|
|
34
|
+
"typings": "dist/index.d.ts",
|
|
35
|
+
"types": "dist/index.d.ts",
|
|
36
|
+
"typesVersions": {
|
|
37
|
+
"*": {
|
|
38
|
+
"*": [
|
|
39
|
+
"*",
|
|
40
|
+
"dist/*",
|
|
41
|
+
"dist/*.d.ts"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist"
|
|
47
|
+
],
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@vitejs/plugin-vue": "^6.0.7",
|
|
50
|
+
"mce": "0.24.4"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"mce": "^0",
|
|
54
|
+
"vue": "^3.5.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build:code": "vite build",
|
|
58
|
+
"build:tsc": "NODE_ENV=production vue-tsc --emitDeclarationOnly --project tsconfig.json",
|
|
59
|
+
"build": "pnpm build:code && pnpm build:tsc",
|
|
60
|
+
"lint": "eslint src",
|
|
61
|
+
"typecheck": "vue-tsc --noEmit --project tsconfig.json",
|
|
62
|
+
"test": "vitest --run"
|
|
63
|
+
}
|
|
64
|
+
}
|