@kedataindo/docflow-core 0.0.71 → 0.0.73
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/Collaboration-CGv9OZlB.d.cts +109 -0
- package/dist/Collaboration-CGv9OZlB.d.ts +109 -0
- package/dist/chunk-XJG72F7M.js +140 -0
- package/dist/collab/index.cjs +286 -0
- package/dist/collab/index.d.cts +71 -0
- package/dist/collab/index.d.ts +71 -0
- package/dist/collab/index.js +116 -0
- package/dist/index.cjs +155 -127
- package/dist/index.d.cts +18 -150
- package/dist/index.d.ts +18 -150
- package/dist/index.js +151 -254
- package/package.json +6 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AnyExtension } from '@tiptap/core';
|
|
2
|
+
import { Awareness } from 'y-protocols/awareness';
|
|
3
|
+
import { IndexeddbPersistence } from 'y-indexeddb';
|
|
4
|
+
import { WebrtcProvider } from 'y-webrtc';
|
|
5
|
+
import { WebsocketProvider } from 'y-websocket';
|
|
6
|
+
import * as Y from 'yjs';
|
|
7
|
+
|
|
8
|
+
interface AwarenessState {
|
|
9
|
+
clientId: number;
|
|
10
|
+
user: {
|
|
11
|
+
name: string;
|
|
12
|
+
color: string;
|
|
13
|
+
};
|
|
14
|
+
cursor?: {
|
|
15
|
+
from: number;
|
|
16
|
+
to: number;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* Phase 9 PR2 — *present* flag. False when the local user has navigated
|
|
20
|
+
* away from the editor route (the Yjs provider is still alive but the
|
|
21
|
+
* host has gated cursor emission off, per the §6 *\"TOC sidebar orphan
|
|
22
|
+
* wiring\"* / REST-heartbeat scope notes). Peers see this instantly
|
|
23
|
+
* via the awareness `change` event — far faster than the 15s REST
|
|
24
|
+
* heartbeat fallback. Defaults to true when the user is in the editor.
|
|
25
|
+
*/
|
|
26
|
+
present: boolean;
|
|
27
|
+
}
|
|
28
|
+
interface CollaborationOptions {
|
|
29
|
+
room: string;
|
|
30
|
+
provider?: 'webrtc' | 'websocket';
|
|
31
|
+
websocketUrl?: string;
|
|
32
|
+
signaling?: string[];
|
|
33
|
+
user: {
|
|
34
|
+
name: string;
|
|
35
|
+
color: string;
|
|
36
|
+
};
|
|
37
|
+
onAwarenessChange?: (states: AwarenessState[]) => void;
|
|
38
|
+
initialStorageState?: Uint8Array;
|
|
39
|
+
/**
|
|
40
|
+
* Phase 9 PR2 — when false, the local user is treated as
|
|
41
|
+
* out-of-editor: their cursor field is nulled and `present` is false.
|
|
42
|
+
* Default is true (in editor). Toggle at runtime via
|
|
43
|
+
* `setLocalCursorEnabled()` returned from `createCollaboration`.
|
|
44
|
+
*/
|
|
45
|
+
emitCursor?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Phase 9 OF1 — when true, mirror the room's Y.Doc into IndexedDB
|
|
48
|
+
* (`docflow-<room>`) via y-indexeddb so edits survive offline and reload.
|
|
49
|
+
* Reconcile-on-reconnect is plain Yjs merge — no conflict resolution
|
|
50
|
+
* needed. The IndexedDB mirror is NEVER a seed source: the server (or the
|
|
51
|
+
* legacy-JSON seed path) still owns initial state; the mirror only
|
|
52
|
+
* contributes local offline edits via the normal Yjs update exchange.
|
|
53
|
+
*/
|
|
54
|
+
offline?: boolean;
|
|
55
|
+
}
|
|
56
|
+
interface CollaborationSetup {
|
|
57
|
+
ydoc: Y.Doc;
|
|
58
|
+
provider: WebrtcProvider | WebsocketProvider | null;
|
|
59
|
+
awareness: Awareness;
|
|
60
|
+
/**
|
|
61
|
+
* Phase 9 OF1 — y-indexeddb persistence instance, present only when
|
|
62
|
+
* `options.offline` is true. Exposed so hosts/tests can `await
|
|
63
|
+
* persistence.whenSynced` before asserting the mirror contents. Do not
|
|
64
|
+
* use it as a seed source.
|
|
65
|
+
*/
|
|
66
|
+
persistence?: IndexeddbPersistence;
|
|
67
|
+
/**
|
|
68
|
+
* Issue #117 — resolves after BOTH the offline mirror (if `offline`) AND
|
|
69
|
+
* the provider's first sync (if `websocket`) have completed. Hosts MUST
|
|
70
|
+
* `await setup.whenReady` before binding the editor, otherwise the
|
|
71
|
+
* TipTap view can render against the IndexedDB cache before the
|
|
72
|
+
* authoritative server state arrives, briefly showing — and on a
|
|
73
|
+
* disconnected peer potentially re-broadcasting — nodes the server has
|
|
74
|
+
* since deleted. Resolves immediately when neither side applies.
|
|
75
|
+
*/
|
|
76
|
+
whenReady: Promise<void>;
|
|
77
|
+
destroy: () => void;
|
|
78
|
+
/**
|
|
79
|
+
* Phase 9 PR2 — toggle the local cursor emission + present flag.
|
|
80
|
+
* Pass `false` when the editor route unmounts, `true` when it remounts.
|
|
81
|
+
* Triggers an awareness `change` event so peers see the leave/rejoin
|
|
82
|
+
* instantly (no REST-heartbeat lag).
|
|
83
|
+
*/
|
|
84
|
+
setLocalCursorEnabled: (enabled: boolean) => void;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build the collaboration runtime for a room.
|
|
88
|
+
*
|
|
89
|
+
* Async by design (issue fe-aktifai#230): the network sync providers are
|
|
90
|
+
* imported lazily — `y-webrtc` only when `provider: 'webrtc'`, `y-websocket`
|
|
91
|
+
* only when `provider: 'websocket'`, `y-indexeddb` only when `offline` is
|
|
92
|
+
* set. The main docflow barrel therefore never pulls the collab stack into
|
|
93
|
+
* bundles where collaboration is disabled; hosts that opt in simply
|
|
94
|
+
* `await createCollaboration(...)` and pass the resulting `CollaborationSetup`
|
|
95
|
+
* to `createEditor` / `<DocsEditor>`.
|
|
96
|
+
*/
|
|
97
|
+
declare function createCollaboration(options: CollaborationOptions): Promise<CollaborationSetup>;
|
|
98
|
+
/**
|
|
99
|
+
* Build the TipTap extensions that bind an editor to a collaboration room.
|
|
100
|
+
*
|
|
101
|
+
* Takes a prebuilt `CollaborationSetup` only (issue fe-aktifai#230): the
|
|
102
|
+
* raw-options convenience path was removed because it required a
|
|
103
|
+
* *synchronous* provider construction, which is impossible now that
|
|
104
|
+
* `createCollaboration` lazy-imports `y-webrtc`/`y-websocket` and is async.
|
|
105
|
+
* Hosts must `await createCollaboration(...)` first and pass the result.
|
|
106
|
+
*/
|
|
107
|
+
declare function collaborationExtensions(setup: CollaborationSetup): AnyExtension[];
|
|
108
|
+
|
|
109
|
+
export { type AwarenessState as A, type CollaborationOptions as C, type CollaborationSetup as a, createCollaboration as b, collaborationExtensions as c };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AnyExtension } from '@tiptap/core';
|
|
2
|
+
import { Awareness } from 'y-protocols/awareness';
|
|
3
|
+
import { IndexeddbPersistence } from 'y-indexeddb';
|
|
4
|
+
import { WebrtcProvider } from 'y-webrtc';
|
|
5
|
+
import { WebsocketProvider } from 'y-websocket';
|
|
6
|
+
import * as Y from 'yjs';
|
|
7
|
+
|
|
8
|
+
interface AwarenessState {
|
|
9
|
+
clientId: number;
|
|
10
|
+
user: {
|
|
11
|
+
name: string;
|
|
12
|
+
color: string;
|
|
13
|
+
};
|
|
14
|
+
cursor?: {
|
|
15
|
+
from: number;
|
|
16
|
+
to: number;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* Phase 9 PR2 — *present* flag. False when the local user has navigated
|
|
20
|
+
* away from the editor route (the Yjs provider is still alive but the
|
|
21
|
+
* host has gated cursor emission off, per the §6 *\"TOC sidebar orphan
|
|
22
|
+
* wiring\"* / REST-heartbeat scope notes). Peers see this instantly
|
|
23
|
+
* via the awareness `change` event — far faster than the 15s REST
|
|
24
|
+
* heartbeat fallback. Defaults to true when the user is in the editor.
|
|
25
|
+
*/
|
|
26
|
+
present: boolean;
|
|
27
|
+
}
|
|
28
|
+
interface CollaborationOptions {
|
|
29
|
+
room: string;
|
|
30
|
+
provider?: 'webrtc' | 'websocket';
|
|
31
|
+
websocketUrl?: string;
|
|
32
|
+
signaling?: string[];
|
|
33
|
+
user: {
|
|
34
|
+
name: string;
|
|
35
|
+
color: string;
|
|
36
|
+
};
|
|
37
|
+
onAwarenessChange?: (states: AwarenessState[]) => void;
|
|
38
|
+
initialStorageState?: Uint8Array;
|
|
39
|
+
/**
|
|
40
|
+
* Phase 9 PR2 — when false, the local user is treated as
|
|
41
|
+
* out-of-editor: their cursor field is nulled and `present` is false.
|
|
42
|
+
* Default is true (in editor). Toggle at runtime via
|
|
43
|
+
* `setLocalCursorEnabled()` returned from `createCollaboration`.
|
|
44
|
+
*/
|
|
45
|
+
emitCursor?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Phase 9 OF1 — when true, mirror the room's Y.Doc into IndexedDB
|
|
48
|
+
* (`docflow-<room>`) via y-indexeddb so edits survive offline and reload.
|
|
49
|
+
* Reconcile-on-reconnect is plain Yjs merge — no conflict resolution
|
|
50
|
+
* needed. The IndexedDB mirror is NEVER a seed source: the server (or the
|
|
51
|
+
* legacy-JSON seed path) still owns initial state; the mirror only
|
|
52
|
+
* contributes local offline edits via the normal Yjs update exchange.
|
|
53
|
+
*/
|
|
54
|
+
offline?: boolean;
|
|
55
|
+
}
|
|
56
|
+
interface CollaborationSetup {
|
|
57
|
+
ydoc: Y.Doc;
|
|
58
|
+
provider: WebrtcProvider | WebsocketProvider | null;
|
|
59
|
+
awareness: Awareness;
|
|
60
|
+
/**
|
|
61
|
+
* Phase 9 OF1 — y-indexeddb persistence instance, present only when
|
|
62
|
+
* `options.offline` is true. Exposed so hosts/tests can `await
|
|
63
|
+
* persistence.whenSynced` before asserting the mirror contents. Do not
|
|
64
|
+
* use it as a seed source.
|
|
65
|
+
*/
|
|
66
|
+
persistence?: IndexeddbPersistence;
|
|
67
|
+
/**
|
|
68
|
+
* Issue #117 — resolves after BOTH the offline mirror (if `offline`) AND
|
|
69
|
+
* the provider's first sync (if `websocket`) have completed. Hosts MUST
|
|
70
|
+
* `await setup.whenReady` before binding the editor, otherwise the
|
|
71
|
+
* TipTap view can render against the IndexedDB cache before the
|
|
72
|
+
* authoritative server state arrives, briefly showing — and on a
|
|
73
|
+
* disconnected peer potentially re-broadcasting — nodes the server has
|
|
74
|
+
* since deleted. Resolves immediately when neither side applies.
|
|
75
|
+
*/
|
|
76
|
+
whenReady: Promise<void>;
|
|
77
|
+
destroy: () => void;
|
|
78
|
+
/**
|
|
79
|
+
* Phase 9 PR2 — toggle the local cursor emission + present flag.
|
|
80
|
+
* Pass `false` when the editor route unmounts, `true` when it remounts.
|
|
81
|
+
* Triggers an awareness `change` event so peers see the leave/rejoin
|
|
82
|
+
* instantly (no REST-heartbeat lag).
|
|
83
|
+
*/
|
|
84
|
+
setLocalCursorEnabled: (enabled: boolean) => void;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build the collaboration runtime for a room.
|
|
88
|
+
*
|
|
89
|
+
* Async by design (issue fe-aktifai#230): the network sync providers are
|
|
90
|
+
* imported lazily — `y-webrtc` only when `provider: 'webrtc'`, `y-websocket`
|
|
91
|
+
* only when `provider: 'websocket'`, `y-indexeddb` only when `offline` is
|
|
92
|
+
* set. The main docflow barrel therefore never pulls the collab stack into
|
|
93
|
+
* bundles where collaboration is disabled; hosts that opt in simply
|
|
94
|
+
* `await createCollaboration(...)` and pass the resulting `CollaborationSetup`
|
|
95
|
+
* to `createEditor` / `<DocsEditor>`.
|
|
96
|
+
*/
|
|
97
|
+
declare function createCollaboration(options: CollaborationOptions): Promise<CollaborationSetup>;
|
|
98
|
+
/**
|
|
99
|
+
* Build the TipTap extensions that bind an editor to a collaboration room.
|
|
100
|
+
*
|
|
101
|
+
* Takes a prebuilt `CollaborationSetup` only (issue fe-aktifai#230): the
|
|
102
|
+
* raw-options convenience path was removed because it required a
|
|
103
|
+
* *synchronous* provider construction, which is impossible now that
|
|
104
|
+
* `createCollaboration` lazy-imports `y-webrtc`/`y-websocket` and is async.
|
|
105
|
+
* Hosts must `await createCollaboration(...)` first and pass the result.
|
|
106
|
+
*/
|
|
107
|
+
declare function collaborationExtensions(setup: CollaborationSetup): AnyExtension[];
|
|
108
|
+
|
|
109
|
+
export { type AwarenessState as A, type CollaborationOptions as C, type CollaborationSetup as a, createCollaboration as b, collaborationExtensions as c };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// src/Collaboration.ts
|
|
2
|
+
import { Collaboration } from "@tiptap/extension-collaboration";
|
|
3
|
+
import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor";
|
|
4
|
+
import { Awareness } from "y-protocols/awareness";
|
|
5
|
+
import * as Y from "yjs";
|
|
6
|
+
var LOCAL_SIGNALING = ["ws://localhost:4444"];
|
|
7
|
+
var warnedLocalSignaling = false;
|
|
8
|
+
function resolveSignalingUrls(signaling) {
|
|
9
|
+
if (signaling && signaling.length > 0) return signaling;
|
|
10
|
+
if (!warnedLocalSignaling) {
|
|
11
|
+
warnedLocalSignaling = true;
|
|
12
|
+
console.warn(
|
|
13
|
+
'[docflow] webrtc provider has no `signaling` configured \u2014 falling back to localhost-only sync (ws://localhost:4444 + same-browser tabs). For production self-host, run your own signaling server and pass `signaling`, or use `provider: "websocket"` with your own websocketUrl.'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return LOCAL_SIGNALING;
|
|
17
|
+
}
|
|
18
|
+
var LOCAL_PRESENT_DEFAULT = true;
|
|
19
|
+
function createAwarenessStates(awareness) {
|
|
20
|
+
const states = [];
|
|
21
|
+
awareness.getStates().forEach((state, clientId) => {
|
|
22
|
+
const raw = state;
|
|
23
|
+
states.push({
|
|
24
|
+
clientId,
|
|
25
|
+
user: raw.user ?? { name: "", color: "" },
|
|
26
|
+
cursor: raw.cursor ?? null,
|
|
27
|
+
// `present` defaults to true for backwards compat (legacy states
|
|
28
|
+
// don't set it) and for the local user (always considered present
|
|
29
|
+
// until PR2 toggles it off).
|
|
30
|
+
present: raw.present ?? LOCAL_PRESENT_DEFAULT
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
return states;
|
|
34
|
+
}
|
|
35
|
+
async function createCollaboration(options) {
|
|
36
|
+
const ydoc = new Y.Doc();
|
|
37
|
+
if (options.initialStorageState) {
|
|
38
|
+
Y.applyUpdate(ydoc, options.initialStorageState);
|
|
39
|
+
}
|
|
40
|
+
let persistence;
|
|
41
|
+
if (options.offline) {
|
|
42
|
+
const { IndexeddbPersistence } = await import("y-indexeddb");
|
|
43
|
+
persistence = new IndexeddbPersistence(`docflow-${options.room}`, ydoc);
|
|
44
|
+
}
|
|
45
|
+
let provider = null;
|
|
46
|
+
if (options.provider === "webrtc") {
|
|
47
|
+
const { WebrtcProvider } = await import("y-webrtc");
|
|
48
|
+
provider = new WebrtcProvider(options.room, ydoc, {
|
|
49
|
+
signaling: resolveSignalingUrls(options.signaling)
|
|
50
|
+
});
|
|
51
|
+
} else if (options.provider === "websocket") {
|
|
52
|
+
if (!options.websocketUrl) {
|
|
53
|
+
throw new Error('[Collaboration] websocketUrl is required when provider is "websocket"');
|
|
54
|
+
}
|
|
55
|
+
const { WebsocketProvider } = await import("y-websocket");
|
|
56
|
+
provider = new WebsocketProvider(options.websocketUrl, options.room, ydoc);
|
|
57
|
+
}
|
|
58
|
+
const awareness = provider?.awareness ?? new Awareness(ydoc);
|
|
59
|
+
awareness.setLocalStateField("user", options.user);
|
|
60
|
+
let emitCursor = options.emitCursor ?? true;
|
|
61
|
+
const setLocalCursorEnabled = (enabled) => {
|
|
62
|
+
if (emitCursor === enabled) return;
|
|
63
|
+
emitCursor = enabled;
|
|
64
|
+
if (!enabled) {
|
|
65
|
+
awareness.setLocalStateField("cursor", null);
|
|
66
|
+
}
|
|
67
|
+
awareness.setLocalStateField("emitCursor", enabled);
|
|
68
|
+
};
|
|
69
|
+
awareness.setLocalStateField("present", true);
|
|
70
|
+
awareness.setLocalStateField("emitCursor", emitCursor);
|
|
71
|
+
let awarenessHandler;
|
|
72
|
+
if (options.onAwarenessChange) {
|
|
73
|
+
const notify = () => {
|
|
74
|
+
options.onAwarenessChange?.(createAwarenessStates(awareness));
|
|
75
|
+
};
|
|
76
|
+
awarenessHandler = notify;
|
|
77
|
+
awareness.on("change", notify);
|
|
78
|
+
notify();
|
|
79
|
+
}
|
|
80
|
+
const readyParts = [];
|
|
81
|
+
if (persistence) {
|
|
82
|
+
readyParts.push(
|
|
83
|
+
persistence.whenSynced.then(
|
|
84
|
+
() => void 0,
|
|
85
|
+
() => void 0
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (provider && options.provider === "websocket") {
|
|
90
|
+
const ws = provider;
|
|
91
|
+
readyParts.push(
|
|
92
|
+
new Promise((resolve) => {
|
|
93
|
+
let settled = false;
|
|
94
|
+
const done = () => {
|
|
95
|
+
if (settled) return;
|
|
96
|
+
settled = true;
|
|
97
|
+
resolve();
|
|
98
|
+
};
|
|
99
|
+
if (ws.synced) {
|
|
100
|
+
done();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
ws.once("sync", done);
|
|
104
|
+
ws.once("synced", done);
|
|
105
|
+
setTimeout(done, 5e3);
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const whenReady = readyParts.length === 0 ? Promise.resolve() : Promise.all(readyParts).then(() => void 0);
|
|
110
|
+
const destroy = () => {
|
|
111
|
+
if (awarenessHandler) {
|
|
112
|
+
awareness.off("change", awarenessHandler);
|
|
113
|
+
}
|
|
114
|
+
awareness.setLocalState(null);
|
|
115
|
+
provider?.destroy?.();
|
|
116
|
+
awareness.destroy?.();
|
|
117
|
+
void persistence?.destroy();
|
|
118
|
+
ydoc.destroy();
|
|
119
|
+
};
|
|
120
|
+
return { ydoc, provider, awareness, persistence, whenReady, destroy, setLocalCursorEnabled };
|
|
121
|
+
}
|
|
122
|
+
function collaborationExtensions(setup) {
|
|
123
|
+
const extensions = [
|
|
124
|
+
Collaboration.configure({ document: setup.ydoc })
|
|
125
|
+
];
|
|
126
|
+
if (setup.provider) {
|
|
127
|
+
extensions.push(
|
|
128
|
+
CollaborationCursor.configure({
|
|
129
|
+
provider: setup.provider,
|
|
130
|
+
user: setup.awareness.getLocalState()?.user ?? { name: "", color: "" }
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return extensions;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export {
|
|
138
|
+
createCollaboration,
|
|
139
|
+
collaborationExtensions
|
|
140
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/collab/index.ts
|
|
31
|
+
var collab_exports = {};
|
|
32
|
+
__export(collab_exports, {
|
|
33
|
+
SubdocumentProvider: () => SubdocumentProvider,
|
|
34
|
+
collaborationExtensions: () => collaborationExtensions,
|
|
35
|
+
createCollaboration: () => createCollaboration
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(collab_exports);
|
|
38
|
+
|
|
39
|
+
// src/SubdocumentProvider.ts
|
|
40
|
+
var import_awareness = require("y-protocols/awareness");
|
|
41
|
+
var import_y_websocket = require("y-websocket");
|
|
42
|
+
var Y = __toESM(require("yjs"), 1);
|
|
43
|
+
var SubdocumentProvider = class {
|
|
44
|
+
parentDoc;
|
|
45
|
+
subdocs;
|
|
46
|
+
awareness;
|
|
47
|
+
options;
|
|
48
|
+
states = /* @__PURE__ */ new Map();
|
|
49
|
+
activeQueue = [];
|
|
50
|
+
parentProvider = null;
|
|
51
|
+
constructor(options) {
|
|
52
|
+
this.options = {
|
|
53
|
+
maxActive: options.maxActive ?? 5,
|
|
54
|
+
onStateChange: options.onStateChange ?? (() => {
|
|
55
|
+
}),
|
|
56
|
+
...options
|
|
57
|
+
};
|
|
58
|
+
this.parentDoc = new Y.Doc();
|
|
59
|
+
this.subdocs = this.parentDoc.getMap("subdocs");
|
|
60
|
+
this.awareness = new import_awareness.Awareness(this.parentDoc);
|
|
61
|
+
this.awareness.setLocalStateField("user", this.options.user);
|
|
62
|
+
this.parentProvider = new import_y_websocket.WebsocketProvider(
|
|
63
|
+
this.options.websocketUrl,
|
|
64
|
+
`${this.options.roomPrefix}/_parent`,
|
|
65
|
+
this.parentDoc
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
/** Create a new subdocument. Does NOT activate it. */
|
|
69
|
+
createSubdoc(id, initialState) {
|
|
70
|
+
if (this.states.has(id)) return this.states.get(id).doc;
|
|
71
|
+
const doc = new Y.Doc();
|
|
72
|
+
if (initialState) Y.applyUpdate(doc, initialState);
|
|
73
|
+
doc.getXmlFragment("default");
|
|
74
|
+
this.subdocs.set(id, doc);
|
|
75
|
+
this.states.set(id, {
|
|
76
|
+
id,
|
|
77
|
+
doc,
|
|
78
|
+
provider: null,
|
|
79
|
+
active: false
|
|
80
|
+
});
|
|
81
|
+
this.options.onStateChange(this.getAllStates());
|
|
82
|
+
return doc;
|
|
83
|
+
}
|
|
84
|
+
/** Activate a subdocument — connects it to the network for live sync. */
|
|
85
|
+
activate(id) {
|
|
86
|
+
const state = this.states.get(id);
|
|
87
|
+
if (!state || state.active) return;
|
|
88
|
+
if (this.activeQueue.length >= this.options.maxActive) {
|
|
89
|
+
const oldest = this.activeQueue.shift();
|
|
90
|
+
if (oldest) this.deactivate(oldest);
|
|
91
|
+
}
|
|
92
|
+
const doc = state.doc;
|
|
93
|
+
const roomName = `${this.options.roomPrefix}/${id}`;
|
|
94
|
+
const provider = new import_y_websocket.WebsocketProvider(
|
|
95
|
+
this.options.websocketUrl,
|
|
96
|
+
roomName,
|
|
97
|
+
doc
|
|
98
|
+
);
|
|
99
|
+
provider.awareness.setLocalStateField("user", this.options.user);
|
|
100
|
+
state.provider = provider;
|
|
101
|
+
state.active = true;
|
|
102
|
+
this.activeQueue.push(id);
|
|
103
|
+
this.options.onStateChange(this.getAllStates());
|
|
104
|
+
}
|
|
105
|
+
/** Deactivate a subdocument — disconnects it from the network. */
|
|
106
|
+
deactivate(id) {
|
|
107
|
+
const state = this.states.get(id);
|
|
108
|
+
if (!state || !state.active) return;
|
|
109
|
+
state.provider?.destroy();
|
|
110
|
+
state.provider = null;
|
|
111
|
+
state.active = false;
|
|
112
|
+
const idx = this.activeQueue.indexOf(id);
|
|
113
|
+
if (idx >= 0) this.activeQueue.splice(idx, 1);
|
|
114
|
+
this.options.onStateChange(this.getAllStates());
|
|
115
|
+
}
|
|
116
|
+
/** Get the Y.XmlFragment for a subdocument. */
|
|
117
|
+
getFragment(id) {
|
|
118
|
+
const doc = this.states.get(id)?.doc ?? this.subdocs.get(id);
|
|
119
|
+
if (!doc) return null;
|
|
120
|
+
return doc.getXmlFragment("default");
|
|
121
|
+
}
|
|
122
|
+
/** Check if a subdocument is currently active (syncing). */
|
|
123
|
+
isActive(id) {
|
|
124
|
+
return this.states.get(id)?.active ?? false;
|
|
125
|
+
}
|
|
126
|
+
/** Get all subdocument states. */
|
|
127
|
+
getAllStates() {
|
|
128
|
+
return Array.from(this.states.values());
|
|
129
|
+
}
|
|
130
|
+
/** Get IDs of all active subdocuments. */
|
|
131
|
+
getActiveIds() {
|
|
132
|
+
return [...this.activeQueue];
|
|
133
|
+
}
|
|
134
|
+
/** Destroy all subdocuments and providers. */
|
|
135
|
+
destroy() {
|
|
136
|
+
for (const [id] of this.states) {
|
|
137
|
+
this.deactivate(id);
|
|
138
|
+
}
|
|
139
|
+
this.parentProvider?.destroy();
|
|
140
|
+
this.parentDoc.destroy();
|
|
141
|
+
this.states.clear();
|
|
142
|
+
this.activeQueue.length = 0;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// src/Collaboration.ts
|
|
147
|
+
var import_extension_collaboration = require("@tiptap/extension-collaboration");
|
|
148
|
+
var import_extension_collaboration_cursor = require("@tiptap/extension-collaboration-cursor");
|
|
149
|
+
var import_awareness2 = require("y-protocols/awareness");
|
|
150
|
+
var Y2 = __toESM(require("yjs"), 1);
|
|
151
|
+
var LOCAL_SIGNALING = ["ws://localhost:4444"];
|
|
152
|
+
var warnedLocalSignaling = false;
|
|
153
|
+
function resolveSignalingUrls(signaling) {
|
|
154
|
+
if (signaling && signaling.length > 0) return signaling;
|
|
155
|
+
if (!warnedLocalSignaling) {
|
|
156
|
+
warnedLocalSignaling = true;
|
|
157
|
+
console.warn(
|
|
158
|
+
'[docflow] webrtc provider has no `signaling` configured \u2014 falling back to localhost-only sync (ws://localhost:4444 + same-browser tabs). For production self-host, run your own signaling server and pass `signaling`, or use `provider: "websocket"` with your own websocketUrl.'
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return LOCAL_SIGNALING;
|
|
162
|
+
}
|
|
163
|
+
var LOCAL_PRESENT_DEFAULT = true;
|
|
164
|
+
function createAwarenessStates(awareness) {
|
|
165
|
+
const states = [];
|
|
166
|
+
awareness.getStates().forEach((state, clientId) => {
|
|
167
|
+
const raw = state;
|
|
168
|
+
states.push({
|
|
169
|
+
clientId,
|
|
170
|
+
user: raw.user ?? { name: "", color: "" },
|
|
171
|
+
cursor: raw.cursor ?? null,
|
|
172
|
+
// `present` defaults to true for backwards compat (legacy states
|
|
173
|
+
// don't set it) and for the local user (always considered present
|
|
174
|
+
// until PR2 toggles it off).
|
|
175
|
+
present: raw.present ?? LOCAL_PRESENT_DEFAULT
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
return states;
|
|
179
|
+
}
|
|
180
|
+
async function createCollaboration(options) {
|
|
181
|
+
const ydoc = new Y2.Doc();
|
|
182
|
+
if (options.initialStorageState) {
|
|
183
|
+
Y2.applyUpdate(ydoc, options.initialStorageState);
|
|
184
|
+
}
|
|
185
|
+
let persistence;
|
|
186
|
+
if (options.offline) {
|
|
187
|
+
const { IndexeddbPersistence } = await import("y-indexeddb");
|
|
188
|
+
persistence = new IndexeddbPersistence(`docflow-${options.room}`, ydoc);
|
|
189
|
+
}
|
|
190
|
+
let provider = null;
|
|
191
|
+
if (options.provider === "webrtc") {
|
|
192
|
+
const { WebrtcProvider } = await import("y-webrtc");
|
|
193
|
+
provider = new WebrtcProvider(options.room, ydoc, {
|
|
194
|
+
signaling: resolveSignalingUrls(options.signaling)
|
|
195
|
+
});
|
|
196
|
+
} else if (options.provider === "websocket") {
|
|
197
|
+
if (!options.websocketUrl) {
|
|
198
|
+
throw new Error('[Collaboration] websocketUrl is required when provider is "websocket"');
|
|
199
|
+
}
|
|
200
|
+
const { WebsocketProvider: WebsocketProvider2 } = await import("y-websocket");
|
|
201
|
+
provider = new WebsocketProvider2(options.websocketUrl, options.room, ydoc);
|
|
202
|
+
}
|
|
203
|
+
const awareness = provider?.awareness ?? new import_awareness2.Awareness(ydoc);
|
|
204
|
+
awareness.setLocalStateField("user", options.user);
|
|
205
|
+
let emitCursor = options.emitCursor ?? true;
|
|
206
|
+
const setLocalCursorEnabled = (enabled) => {
|
|
207
|
+
if (emitCursor === enabled) return;
|
|
208
|
+
emitCursor = enabled;
|
|
209
|
+
if (!enabled) {
|
|
210
|
+
awareness.setLocalStateField("cursor", null);
|
|
211
|
+
}
|
|
212
|
+
awareness.setLocalStateField("emitCursor", enabled);
|
|
213
|
+
};
|
|
214
|
+
awareness.setLocalStateField("present", true);
|
|
215
|
+
awareness.setLocalStateField("emitCursor", emitCursor);
|
|
216
|
+
let awarenessHandler;
|
|
217
|
+
if (options.onAwarenessChange) {
|
|
218
|
+
const notify = () => {
|
|
219
|
+
options.onAwarenessChange?.(createAwarenessStates(awareness));
|
|
220
|
+
};
|
|
221
|
+
awarenessHandler = notify;
|
|
222
|
+
awareness.on("change", notify);
|
|
223
|
+
notify();
|
|
224
|
+
}
|
|
225
|
+
const readyParts = [];
|
|
226
|
+
if (persistence) {
|
|
227
|
+
readyParts.push(
|
|
228
|
+
persistence.whenSynced.then(
|
|
229
|
+
() => void 0,
|
|
230
|
+
() => void 0
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
if (provider && options.provider === "websocket") {
|
|
235
|
+
const ws = provider;
|
|
236
|
+
readyParts.push(
|
|
237
|
+
new Promise((resolve) => {
|
|
238
|
+
let settled = false;
|
|
239
|
+
const done = () => {
|
|
240
|
+
if (settled) return;
|
|
241
|
+
settled = true;
|
|
242
|
+
resolve();
|
|
243
|
+
};
|
|
244
|
+
if (ws.synced) {
|
|
245
|
+
done();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
ws.once("sync", done);
|
|
249
|
+
ws.once("synced", done);
|
|
250
|
+
setTimeout(done, 5e3);
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
const whenReady = readyParts.length === 0 ? Promise.resolve() : Promise.all(readyParts).then(() => void 0);
|
|
255
|
+
const destroy = () => {
|
|
256
|
+
if (awarenessHandler) {
|
|
257
|
+
awareness.off("change", awarenessHandler);
|
|
258
|
+
}
|
|
259
|
+
awareness.setLocalState(null);
|
|
260
|
+
provider?.destroy?.();
|
|
261
|
+
awareness.destroy?.();
|
|
262
|
+
void persistence?.destroy();
|
|
263
|
+
ydoc.destroy();
|
|
264
|
+
};
|
|
265
|
+
return { ydoc, provider, awareness, persistence, whenReady, destroy, setLocalCursorEnabled };
|
|
266
|
+
}
|
|
267
|
+
function collaborationExtensions(setup) {
|
|
268
|
+
const extensions = [
|
|
269
|
+
import_extension_collaboration.Collaboration.configure({ document: setup.ydoc })
|
|
270
|
+
];
|
|
271
|
+
if (setup.provider) {
|
|
272
|
+
extensions.push(
|
|
273
|
+
import_extension_collaboration_cursor.CollaborationCursor.configure({
|
|
274
|
+
provider: setup.provider,
|
|
275
|
+
user: setup.awareness.getLocalState()?.user ?? { name: "", color: "" }
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return extensions;
|
|
280
|
+
}
|
|
281
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
282
|
+
0 && (module.exports = {
|
|
283
|
+
SubdocumentProvider,
|
|
284
|
+
collaborationExtensions,
|
|
285
|
+
createCollaboration
|
|
286
|
+
});
|