@sobree/collab-providers 0.1.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/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +127 -0
- package/dist/index.js.map +1 -0
- package/dist/indexeddb.d.ts +37 -0
- package/dist/loopback.d.ts +16 -0
- package/dist/types.d.ts +61 -0
- package/dist/webrtc.d.ts +29 -0
- package/dist/websocket.d.ts +50 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sobree.dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# @sobree/collab-providers
|
|
2
|
+
|
|
3
|
+
Yjs provider helpers for [`@sobree/core`](https://www.npmjs.com/package/@sobree/core).
|
|
4
|
+
|
|
5
|
+
→ Docs: **[docs.sobree.dev/api/collab-providers](https://docs.sobree.dev/api/collab-providers/)**
|
|
6
|
+
|
|
7
|
+
Sobree's editor is backed by a `Y.Doc` (the Yjs CRDT) — see
|
|
8
|
+
`editor.ydoc`. This package wraps the canonical Yjs providers in
|
|
9
|
+
narrow, normalized factories so attaching persistence /
|
|
10
|
+
collaboration is a 5-line job.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add @sobree/core @sobree/collab-providers
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then install **only** the underlying provider you actually use — they're
|
|
19
|
+
optional peer deps:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pnpm add y-websocket # for real-time collaboration
|
|
23
|
+
pnpm add y-indexeddb # for local persistence
|
|
24
|
+
pnpm add y-webrtc # for peer-to-peer ad-hoc collab
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Real-time collaboration (`y-websocket`)
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import * as Y from "yjs";
|
|
33
|
+
import { createSobree } from "@sobree/core";
|
|
34
|
+
import { attachWebsocketProvider } from "@sobree/collab-providers";
|
|
35
|
+
|
|
36
|
+
const ydoc = new Y.Doc();
|
|
37
|
+
const handle = await attachWebsocketProvider(ydoc, {
|
|
38
|
+
url: "wss://collab.example.com",
|
|
39
|
+
room: "doc-q2-brief",
|
|
40
|
+
name: "Alice",
|
|
41
|
+
color: "#f59e0b",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await handle.synced; // optional — wait for initial state from peers
|
|
45
|
+
|
|
46
|
+
const editor = createSobree("#editor", { ydoc });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Local persistence (`y-indexeddb`)
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import * as Y from "yjs";
|
|
53
|
+
import { createSobree } from "@sobree/core";
|
|
54
|
+
import { attachIndexedDBProvider } from "@sobree/collab-providers";
|
|
55
|
+
|
|
56
|
+
const ydoc = new Y.Doc();
|
|
57
|
+
const handle = await attachIndexedDBProvider(ydoc, { dbName: "doc-q2-brief" });
|
|
58
|
+
await handle.synced; // load persisted snapshot
|
|
59
|
+
|
|
60
|
+
const editor = createSobree("#editor", { ydoc });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Peer-to-peer (`y-webrtc`)
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import * as Y from "yjs";
|
|
67
|
+
import { attachWebRTCProvider } from "@sobree/collab-providers";
|
|
68
|
+
|
|
69
|
+
const ydoc = new Y.Doc();
|
|
70
|
+
const handle = await attachWebRTCProvider(ydoc, {
|
|
71
|
+
room: "doc-q2-brief",
|
|
72
|
+
password: "shared-secret-please",
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Best for small (≤4 peer) ad-hoc collab where you don't want a relay
|
|
77
|
+
server. For more peers / persistence / authoritative state, use
|
|
78
|
+
`attachWebsocketProvider` against `@sobree/collab-server`.
|
|
79
|
+
|
|
80
|
+
### In-memory loopback (tests / demos)
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { loopback } from "@sobree/collab-providers";
|
|
84
|
+
const { a, b, destroy } = loopback();
|
|
85
|
+
// a and b are two Y.Docs that auto-sync. Use a as one editor's ydoc,
|
|
86
|
+
// b as another's; mutations on either propagate.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## CollabHandle
|
|
90
|
+
|
|
91
|
+
Every factory returns the same shape:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
interface CollabHandle {
|
|
95
|
+
readonly provider: unknown; // peek for advanced provider-specific methods
|
|
96
|
+
readonly awareness: Awareness | null; // null for y-indexeddb (no presence channel)
|
|
97
|
+
readonly synced: Promise<void>; // resolves after initial sync
|
|
98
|
+
destroy(): void; // disconnect + remove listeners
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sobree/collab-providers — thin Yjs provider helpers for @sobree/core.
|
|
3
|
+
*
|
|
4
|
+
* Each helper is a one-line factory around the canonical Yjs provider
|
|
5
|
+
* (`y-websocket`, `y-indexeddb`, `y-webrtc`). They normalize the
|
|
6
|
+
* different provider APIs to a single `CollabHandle` shape:
|
|
7
|
+
*
|
|
8
|
+
* { provider, awareness, synced, destroy() }
|
|
9
|
+
*
|
|
10
|
+
* The underlying provider packages are **optional peer deps** —
|
|
11
|
+
* install only the ones you need. The factories lazy-import them and
|
|
12
|
+
* throw a clear error if missing.
|
|
13
|
+
*
|
|
14
|
+
* Plus an in-memory `loopback()` for tests / demos that wires two
|
|
15
|
+
* Y.Docs together with no network.
|
|
16
|
+
*/
|
|
17
|
+
export { attachWebsocketProvider } from './websocket';
|
|
18
|
+
export type { WebsocketProviderOptions } from './websocket';
|
|
19
|
+
export { attachIndexedDBProvider } from './indexeddb';
|
|
20
|
+
export type { IndexedDBProviderOptions } from './indexeddb';
|
|
21
|
+
export { attachWebRTCProvider } from './webrtc';
|
|
22
|
+
export type { WebRTCProviderOptions } from './webrtc';
|
|
23
|
+
export { loopback } from './loopback';
|
|
24
|
+
export type { BasicProvider, CollabHandle, CollabSessionPayload, IdentityOptions, } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as i from "yjs";
|
|
2
|
+
async function f(r, e) {
|
|
3
|
+
const a = await m(), t = e.params ?? {}, n = new a.WebsocketProvider(e.url, e.room, r, {
|
|
4
|
+
params: t,
|
|
5
|
+
connect: e.connect ?? !0
|
|
6
|
+
});
|
|
7
|
+
(e.name || e.color || e.userId) && n.awareness.setLocalStateField("user", {
|
|
8
|
+
id: e.userId ?? y(),
|
|
9
|
+
name: e.name ?? "Anonymous",
|
|
10
|
+
color: e.color ?? "#888"
|
|
11
|
+
});
|
|
12
|
+
const o = new Promise((c) => {
|
|
13
|
+
if (n.synced) {
|
|
14
|
+
c();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const s = (d) => {
|
|
18
|
+
d && (n.off("sync", s), c());
|
|
19
|
+
};
|
|
20
|
+
n.on("sync", s);
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
provider: n,
|
|
24
|
+
awareness: n.awareness,
|
|
25
|
+
synced: o,
|
|
26
|
+
destroy() {
|
|
27
|
+
n.disconnect(), n.destroy();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function m() {
|
|
32
|
+
try {
|
|
33
|
+
return await import("y-websocket");
|
|
34
|
+
} catch (r) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"y-websocket is not installed. Run `pnpm add y-websocket` (or `npm install y-websocket`) and import this module again. See https://github.com/yjs/y-websocket.",
|
|
37
|
+
{ cause: r }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function y() {
|
|
42
|
+
return Math.random().toString(36).slice(2, 10);
|
|
43
|
+
}
|
|
44
|
+
async function h(r, e) {
|
|
45
|
+
const a = await l(), t = new a.IndexeddbPersistence(e.dbName, r), n = new Promise((o) => {
|
|
46
|
+
t.once("synced", () => o());
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
provider: t,
|
|
50
|
+
awareness: null,
|
|
51
|
+
synced: n,
|
|
52
|
+
destroy() {
|
|
53
|
+
t.destroy();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function l() {
|
|
58
|
+
try {
|
|
59
|
+
return await import("y-indexeddb");
|
|
60
|
+
} catch (r) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"y-indexeddb is not installed. Run `pnpm add y-indexeddb` (or `npm install y-indexeddb`) and import this module again. See https://github.com/yjs/y-indexeddb.",
|
|
63
|
+
{ cause: r }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function p(r, e) {
|
|
68
|
+
const a = await w(), t = {};
|
|
69
|
+
e.password && (t.password = e.password), e.signaling && (t.signaling = e.signaling);
|
|
70
|
+
const n = new a.WebrtcProvider(e.room, r, t);
|
|
71
|
+
(e.name || e.color || e.userId) && n.awareness.setLocalStateField("user", {
|
|
72
|
+
id: e.userId ?? b(),
|
|
73
|
+
name: e.name ?? "Anonymous",
|
|
74
|
+
color: e.color ?? "#888"
|
|
75
|
+
});
|
|
76
|
+
const o = new Promise((c) => {
|
|
77
|
+
let s = !1;
|
|
78
|
+
const d = () => {
|
|
79
|
+
s || (s = !0, c());
|
|
80
|
+
};
|
|
81
|
+
n.on("peers", ({ added: u }) => {
|
|
82
|
+
u.length > 0 && d();
|
|
83
|
+
}), setTimeout(d, 1e3);
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
provider: n,
|
|
87
|
+
awareness: n.awareness,
|
|
88
|
+
synced: o,
|
|
89
|
+
destroy() {
|
|
90
|
+
n.disconnect(), n.destroy();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function w() {
|
|
95
|
+
try {
|
|
96
|
+
return await import("y-webrtc");
|
|
97
|
+
} catch (r) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"y-webrtc is not installed. Run `pnpm add y-webrtc` (or `npm install y-webrtc`) and import this module again. See https://github.com/yjs/y-webrtc.",
|
|
100
|
+
{ cause: r }
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function b() {
|
|
105
|
+
return Math.random().toString(36).slice(2, 10);
|
|
106
|
+
}
|
|
107
|
+
function g() {
|
|
108
|
+
const r = new i.Doc(), e = new i.Doc(), a = (n, o) => {
|
|
109
|
+
o !== "remote" && i.applyUpdate(e, n, "remote");
|
|
110
|
+
}, t = (n, o) => {
|
|
111
|
+
o !== "remote" && i.applyUpdate(r, n, "remote");
|
|
112
|
+
};
|
|
113
|
+
return r.on("update", a), e.on("update", t), {
|
|
114
|
+
a: r,
|
|
115
|
+
b: e,
|
|
116
|
+
destroy() {
|
|
117
|
+
r.off("update", a), e.off("update", t);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export {
|
|
122
|
+
h as attachIndexedDBProvider,
|
|
123
|
+
p as attachWebRTCProvider,
|
|
124
|
+
f as attachWebsocketProvider,
|
|
125
|
+
g as loopback
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/websocket.ts","../src/indexeddb.ts","../src/webrtc.ts","../src/loopback.ts"],"sourcesContent":["import type { CollabHandle, IdentityOptions } from \"./types\";\nimport type * as Y from \"yjs\";\n\nexport interface WebsocketProviderOptions extends IdentityOptions {\n /** WebSocket server URL (e.g. `wss://collab.example.com`). The room\n * name is appended as a path. */\n url: string;\n /** Room name. Each room is one Y.Doc shared across peers. */\n room: string;\n /** Auth params appended to the WebSocket URL as query string. */\n params?: Record<string, string>;\n /** Connect immediately (default true). Set to false to call\n * `provider.connect()` manually. */\n connect?: boolean;\n}\n\n/**\n * Attach a `y-websocket` provider to a Y.Doc.\n *\n * Requires `y-websocket` as a peer dep — install it explicitly:\n *\n * ```sh\n * pnpm add y-websocket\n * ```\n *\n * Returns a `CollabHandle` with the connected provider, awareness\n * (cursor / presence channel), and a `synced` promise that resolves\n * when the initial sync completes.\n *\n * Usage:\n *\n * ```ts\n * import * as Y from \"yjs\";\n * import { createSobree } from \"@sobree/core\";\n * import { attachWebsocketProvider } from \"@sobree/collab-providers\";\n *\n * const ydoc = new Y.Doc();\n * const handle = await attachWebsocketProvider(ydoc, {\n * url: \"wss://collab.example.com\",\n * room: \"doc-q2-brief\",\n * name: \"Alice\",\n * color: \"#f59e0b\",\n * });\n * const editor = createSobree(\"#editor\", { ydoc });\n *\n * // Later:\n * editor.destroy();\n * handle.destroy();\n * ydoc.destroy();\n * ```\n */\nexport async function attachWebsocketProvider(\n ydoc: Y.Doc,\n opts: WebsocketProviderOptions,\n): Promise<CollabHandle> {\n const yws = await loadYWebsocket();\n const params = opts.params ?? {};\n const provider = new yws.WebsocketProvider(opts.url, opts.room, ydoc, {\n params,\n connect: opts.connect ?? true,\n });\n\n if (opts.name || opts.color || opts.userId) {\n provider.awareness.setLocalStateField(\"user\", {\n id: opts.userId ?? randomId(),\n name: opts.name ?? \"Anonymous\",\n color: opts.color ?? \"#888\",\n });\n }\n\n const synced = new Promise<void>((resolve) => {\n if (provider.synced) {\n resolve();\n return;\n }\n const handler = (isSynced: boolean) => {\n if (isSynced) {\n provider.off(\"sync\", handler);\n resolve();\n }\n };\n provider.on(\"sync\", handler);\n });\n\n return {\n provider,\n awareness: provider.awareness,\n synced,\n destroy(): void {\n provider.disconnect();\n provider.destroy();\n },\n };\n}\n\nasync function loadYWebsocket(): Promise<typeof import(\"y-websocket\")> {\n try {\n return await import(\"y-websocket\");\n } catch (err) {\n throw new Error(\n \"y-websocket is not installed. Run `pnpm add y-websocket` (or `npm install y-websocket`) \" +\n \"and import this module again. See https://github.com/yjs/y-websocket.\",\n { cause: err },\n );\n }\n}\n\nfunction randomId(): string {\n // Sufficient for awareness peer ids; not crypto.\n return Math.random().toString(36).slice(2, 10);\n}\n","import type { CollabHandle } from \"./types\";\nimport type * as Y from \"yjs\";\n\nexport interface IndexedDBProviderOptions {\n /** Database name. Each name is a separate IndexedDB store. Use one\n * per document if you want isolated persistence. */\n dbName: string;\n}\n\n/**\n * Attach a `y-indexeddb` provider for local persistence. The Y.Doc's\n * state is stored in the browser's IndexedDB; reloading the page\n * restores the document.\n *\n * Requires `y-indexeddb` as a peer dep — install it explicitly:\n *\n * ```sh\n * pnpm add y-indexeddb\n * ```\n *\n * No awareness — local persistence only. The `synced` promise resolves\n * when the existing snapshot has been loaded into the Y.Doc.\n *\n * Usage:\n *\n * ```ts\n * import * as Y from \"yjs\";\n * import { createSobree } from \"@sobree/core\";\n * import { attachIndexedDBProvider } from \"@sobree/collab-providers\";\n *\n * const ydoc = new Y.Doc();\n * const handle = await attachIndexedDBProvider(ydoc, {\n * dbName: \"doc-q2-brief\",\n * });\n * await handle.synced; // optional — wait for initial load\n * const editor = createSobree(\"#editor\", { ydoc });\n * ```\n */\nexport async function attachIndexedDBProvider(\n ydoc: Y.Doc,\n opts: IndexedDBProviderOptions,\n): Promise<CollabHandle> {\n const yidb = await loadYIndexedDB();\n const provider = new yidb.IndexeddbPersistence(opts.dbName, ydoc);\n\n const synced = new Promise<void>((resolve) => {\n provider.once(\"synced\", () => resolve());\n });\n\n return {\n provider,\n awareness: null,\n synced,\n destroy(): void {\n provider.destroy();\n },\n };\n}\n\nasync function loadYIndexedDB(): Promise<typeof import(\"y-indexeddb\")> {\n try {\n return await import(\"y-indexeddb\");\n } catch (err) {\n throw new Error(\n \"y-indexeddb is not installed. Run `pnpm add y-indexeddb` (or `npm install y-indexeddb`) \" +\n \"and import this module again. See https://github.com/yjs/y-indexeddb.\",\n { cause: err },\n );\n }\n}\n","import type { CollabHandle, IdentityOptions } from \"./types\";\nimport type * as Y from \"yjs\";\n\nexport interface WebRTCProviderOptions extends IdentityOptions {\n /** Room name. Each room is one Y.Doc shared across peers. */\n room: string;\n /** Pre-shared room password. Optional but recommended for any\n * non-public collaboration. */\n password?: string;\n /** Custom signaling servers. Defaults to the public servers shipped\n * with `y-webrtc`; for production set up your own. */\n signaling?: string[];\n}\n\n/**\n * Attach a `y-webrtc` provider — peer-to-peer collaboration, with a\n * tiny signaling server (or shared public ones) used only for initial\n * peer discovery.\n *\n * Requires `y-webrtc` as a peer dep — install it explicitly:\n *\n * ```sh\n * pnpm add y-webrtc\n * ```\n *\n * Best for small (≤4 peer) ad-hoc collaboration where you don't want\n * to host a relay server. For more peers / persistence /\n * authoritative state, use `attachWebsocketProvider` against\n * `@sobree/collab-server` (Phase 3).\n */\nexport async function attachWebRTCProvider(\n ydoc: Y.Doc,\n opts: WebRTCProviderOptions,\n): Promise<CollabHandle> {\n const ywebrtc = await loadYWebRTC();\n const providerOpts: Record<string, unknown> = {};\n if (opts.password) providerOpts.password = opts.password;\n if (opts.signaling) providerOpts.signaling = opts.signaling;\n\n const provider = new ywebrtc.WebrtcProvider(opts.room, ydoc, providerOpts);\n\n if (opts.name || opts.color || opts.userId) {\n provider.awareness.setLocalStateField(\"user\", {\n id: opts.userId ?? randomId(),\n name: opts.name ?? \"Anonymous\",\n color: opts.color ?? \"#888\",\n });\n }\n\n // y-webrtc has no canonical 'synced' event (peer-to-peer; no\n // single source of truth). Resolve once the first peer connects,\n // or after a short timeout if we're alone in the room.\n const synced = new Promise<void>((resolve) => {\n let resolved = false;\n const finish = () => {\n if (!resolved) {\n resolved = true;\n resolve();\n }\n };\n provider.on(\"peers\", ({ added }: { added: unknown[] }) => {\n if (added.length > 0) finish();\n });\n setTimeout(finish, 1000);\n });\n\n return {\n provider,\n awareness: provider.awareness,\n synced,\n destroy(): void {\n provider.disconnect();\n provider.destroy();\n },\n };\n}\n\nasync function loadYWebRTC(): Promise<typeof import(\"y-webrtc\")> {\n try {\n return await import(\"y-webrtc\");\n } catch (err) {\n throw new Error(\n \"y-webrtc is not installed. Run `pnpm add y-webrtc` (or `npm install y-webrtc`) \" +\n \"and import this module again. See https://github.com/yjs/y-webrtc.\",\n { cause: err },\n );\n }\n}\n\nfunction randomId(): string {\n return Math.random().toString(36).slice(2, 10);\n}\n","import * as Y from \"yjs\";\n\n/**\n * Two Y.Docs wired together in-memory — useful for tests and demos.\n *\n * Constructs a pair: `{ a: Y.Doc, b: Y.Doc }`. Updates applied to `a`\n * automatically replicate to `b` and vice versa. No network involved.\n *\n * The returned `destroy()` removes both observers — the Y.Docs survive\n * for the caller to inspect / use. `.destroy()` on the docs themselves\n * still works the usual way.\n */\nexport function loopback(): {\n a: Y.Doc;\n b: Y.Doc;\n destroy(): void;\n} {\n const a = new Y.Doc();\n const b = new Y.Doc();\n\n const ab = (update: Uint8Array, origin: unknown) => {\n if (origin === \"remote\") return;\n Y.applyUpdate(b, update, \"remote\");\n };\n const ba = (update: Uint8Array, origin: unknown) => {\n if (origin === \"remote\") return;\n Y.applyUpdate(a, update, \"remote\");\n };\n a.on(\"update\", ab);\n b.on(\"update\", ba);\n\n return {\n a,\n b,\n destroy(): void {\n a.off(\"update\", ab);\n b.off(\"update\", ba);\n },\n };\n}\n"],"names":["attachWebsocketProvider","ydoc","opts","yws","loadYWebsocket","params","provider","randomId","synced","resolve","handler","isSynced","err","attachIndexedDBProvider","yidb","loadYIndexedDB","attachWebRTCProvider","ywebrtc","loadYWebRTC","providerOpts","resolved","finish","added","loopback","a","Y","b","ab","update","origin","ba"],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CollabHandle } from './types';
|
|
2
|
+
import type * as Y from "yjs";
|
|
3
|
+
export interface IndexedDBProviderOptions {
|
|
4
|
+
/** Database name. Each name is a separate IndexedDB store. Use one
|
|
5
|
+
* per document if you want isolated persistence. */
|
|
6
|
+
dbName: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Attach a `y-indexeddb` provider for local persistence. The Y.Doc's
|
|
10
|
+
* state is stored in the browser's IndexedDB; reloading the page
|
|
11
|
+
* restores the document.
|
|
12
|
+
*
|
|
13
|
+
* Requires `y-indexeddb` as a peer dep — install it explicitly:
|
|
14
|
+
*
|
|
15
|
+
* ```sh
|
|
16
|
+
* pnpm add y-indexeddb
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* No awareness — local persistence only. The `synced` promise resolves
|
|
20
|
+
* when the existing snapshot has been loaded into the Y.Doc.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* import * as Y from "yjs";
|
|
26
|
+
* import { createSobree } from "@sobree/core";
|
|
27
|
+
* import { attachIndexedDBProvider } from "@sobree/collab-providers";
|
|
28
|
+
*
|
|
29
|
+
* const ydoc = new Y.Doc();
|
|
30
|
+
* const handle = await attachIndexedDBProvider(ydoc, {
|
|
31
|
+
* dbName: "doc-q2-brief",
|
|
32
|
+
* });
|
|
33
|
+
* await handle.synced; // optional — wait for initial load
|
|
34
|
+
* const editor = createSobree("#editor", { ydoc });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function attachIndexedDBProvider(ydoc: Y.Doc, opts: IndexedDBProviderOptions): Promise<CollabHandle>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
/**
|
|
3
|
+
* Two Y.Docs wired together in-memory — useful for tests and demos.
|
|
4
|
+
*
|
|
5
|
+
* Constructs a pair: `{ a: Y.Doc, b: Y.Doc }`. Updates applied to `a`
|
|
6
|
+
* automatically replicate to `b` and vice versa. No network involved.
|
|
7
|
+
*
|
|
8
|
+
* The returned `destroy()` removes both observers — the Y.Docs survive
|
|
9
|
+
* for the caller to inspect / use. `.destroy()` on the docs themselves
|
|
10
|
+
* still works the usual way.
|
|
11
|
+
*/
|
|
12
|
+
export declare function loopback(): {
|
|
13
|
+
a: Y.Doc;
|
|
14
|
+
b: Y.Doc;
|
|
15
|
+
destroy(): void;
|
|
16
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type * as Y from "yjs";
|
|
2
|
+
/**
|
|
3
|
+
* Common shape for every provider helper in this package. The
|
|
4
|
+
* underlying Yjs providers don't share an interface (each has its own
|
|
5
|
+
* connect / disconnect / awareness API), so we normalize to this
|
|
6
|
+
* triple. `provider` is typed as `unknown` deliberately — peek at it
|
|
7
|
+
* for provider-specific advanced use, but most callers should only
|
|
8
|
+
* need `awareness` and `destroy`.
|
|
9
|
+
*/
|
|
10
|
+
export interface CollabHandle {
|
|
11
|
+
/** The underlying Yjs provider instance. Type-cast to its concrete
|
|
12
|
+
* type if you need provider-specific events / methods. */
|
|
13
|
+
readonly provider: unknown;
|
|
14
|
+
/** Awareness state (cursors, presence). May be `null` for providers
|
|
15
|
+
* that don't expose awareness (e.g. `y-indexeddb`). */
|
|
16
|
+
readonly awareness: import('y-protocols/awareness').Awareness | null;
|
|
17
|
+
/** Resolves once the provider has finished its initial sync. For
|
|
18
|
+
* network providers this means "caught up to the room"; for
|
|
19
|
+
* `y-indexeddb` this means "loaded local snapshot". */
|
|
20
|
+
readonly synced: Promise<void>;
|
|
21
|
+
/** Tear down: disconnect, remove listeners. Idempotent. */
|
|
22
|
+
destroy(): void;
|
|
23
|
+
}
|
|
24
|
+
/** Bare-minimum shape Sobree needs from a provider. Useful for custom
|
|
25
|
+
* providers (BroadcastChannel, MessagePort, in-memory test loopback). */
|
|
26
|
+
export interface BasicProvider {
|
|
27
|
+
destroy(): void;
|
|
28
|
+
}
|
|
29
|
+
export interface IdentityOptions {
|
|
30
|
+
/** Stable id for this peer. Defaults to a random uuid per page load. */
|
|
31
|
+
userId?: string;
|
|
32
|
+
/** Display name shown to other peers in awareness. */
|
|
33
|
+
name?: string;
|
|
34
|
+
/** CSS color for the user's caret / range highlight. */
|
|
35
|
+
color?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Optional first-class doc store on a Y.Doc — what `editor.ydoc` is. */
|
|
38
|
+
export type AnyYDoc = Y.Doc;
|
|
39
|
+
/**
|
|
40
|
+
* Session payload — Sobree-extension session message (type 2) sent
|
|
41
|
+
* by `@sobree/collab-server` immediately after a peer joins a room.
|
|
42
|
+
* Mirrors the type in `@sobree/collab-server`'s `protocol.ts`.
|
|
43
|
+
*
|
|
44
|
+
* Embedders connecting to a Sobree collab-server can read this via a
|
|
45
|
+
* custom y-websocket message handler; it tells the client whether
|
|
46
|
+
* the room was empty (safe to seed `initialDocument`) and whether
|
|
47
|
+
* they're allowed to mutate the document.
|
|
48
|
+
*
|
|
49
|
+
* See the collab-server README "leader election" section for the
|
|
50
|
+
* wire-level integration pattern.
|
|
51
|
+
*/
|
|
52
|
+
export interface CollabSessionPayload {
|
|
53
|
+
/** True iff the Y.Doc was empty when this peer joined. Only one
|
|
54
|
+
* peer per fresh room sees this set. */
|
|
55
|
+
isEmpty: boolean;
|
|
56
|
+
/** Whether the server will accept Y sync-update messages from
|
|
57
|
+
* this peer. */
|
|
58
|
+
isWritable: boolean;
|
|
59
|
+
/** Other peers currently in the room (excluding this one). */
|
|
60
|
+
peerCount: number;
|
|
61
|
+
}
|
package/dist/webrtc.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CollabHandle, IdentityOptions } from './types';
|
|
2
|
+
import type * as Y from "yjs";
|
|
3
|
+
export interface WebRTCProviderOptions extends IdentityOptions {
|
|
4
|
+
/** Room name. Each room is one Y.Doc shared across peers. */
|
|
5
|
+
room: string;
|
|
6
|
+
/** Pre-shared room password. Optional but recommended for any
|
|
7
|
+
* non-public collaboration. */
|
|
8
|
+
password?: string;
|
|
9
|
+
/** Custom signaling servers. Defaults to the public servers shipped
|
|
10
|
+
* with `y-webrtc`; for production set up your own. */
|
|
11
|
+
signaling?: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Attach a `y-webrtc` provider — peer-to-peer collaboration, with a
|
|
15
|
+
* tiny signaling server (or shared public ones) used only for initial
|
|
16
|
+
* peer discovery.
|
|
17
|
+
*
|
|
18
|
+
* Requires `y-webrtc` as a peer dep — install it explicitly:
|
|
19
|
+
*
|
|
20
|
+
* ```sh
|
|
21
|
+
* pnpm add y-webrtc
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Best for small (≤4 peer) ad-hoc collaboration where you don't want
|
|
25
|
+
* to host a relay server. For more peers / persistence /
|
|
26
|
+
* authoritative state, use `attachWebsocketProvider` against
|
|
27
|
+
* `@sobree/collab-server` (Phase 3).
|
|
28
|
+
*/
|
|
29
|
+
export declare function attachWebRTCProvider(ydoc: Y.Doc, opts: WebRTCProviderOptions): Promise<CollabHandle>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { CollabHandle, IdentityOptions } from './types';
|
|
2
|
+
import type * as Y from "yjs";
|
|
3
|
+
export interface WebsocketProviderOptions extends IdentityOptions {
|
|
4
|
+
/** WebSocket server URL (e.g. `wss://collab.example.com`). The room
|
|
5
|
+
* name is appended as a path. */
|
|
6
|
+
url: string;
|
|
7
|
+
/** Room name. Each room is one Y.Doc shared across peers. */
|
|
8
|
+
room: string;
|
|
9
|
+
/** Auth params appended to the WebSocket URL as query string. */
|
|
10
|
+
params?: Record<string, string>;
|
|
11
|
+
/** Connect immediately (default true). Set to false to call
|
|
12
|
+
* `provider.connect()` manually. */
|
|
13
|
+
connect?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Attach a `y-websocket` provider to a Y.Doc.
|
|
17
|
+
*
|
|
18
|
+
* Requires `y-websocket` as a peer dep — install it explicitly:
|
|
19
|
+
*
|
|
20
|
+
* ```sh
|
|
21
|
+
* pnpm add y-websocket
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Returns a `CollabHandle` with the connected provider, awareness
|
|
25
|
+
* (cursor / presence channel), and a `synced` promise that resolves
|
|
26
|
+
* when the initial sync completes.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* import * as Y from "yjs";
|
|
32
|
+
* import { createSobree } from "@sobree/core";
|
|
33
|
+
* import { attachWebsocketProvider } from "@sobree/collab-providers";
|
|
34
|
+
*
|
|
35
|
+
* const ydoc = new Y.Doc();
|
|
36
|
+
* const handle = await attachWebsocketProvider(ydoc, {
|
|
37
|
+
* url: "wss://collab.example.com",
|
|
38
|
+
* room: "doc-q2-brief",
|
|
39
|
+
* name: "Alice",
|
|
40
|
+
* color: "#f59e0b",
|
|
41
|
+
* });
|
|
42
|
+
* const editor = createSobree("#editor", { ydoc });
|
|
43
|
+
*
|
|
44
|
+
* // Later:
|
|
45
|
+
* editor.destroy();
|
|
46
|
+
* handle.destroy();
|
|
47
|
+
* ydoc.destroy();
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function attachWebsocketProvider(ydoc: Y.Doc, opts: WebsocketProviderOptions): Promise<CollabHandle>;
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sobree/collab-providers",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Yjs provider helpers for @sobree/core — y-websocket / y-indexeddb / y-webrtc factories with sane defaults. Optional. Bring your own provider if you prefer.",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"sobree",
|
|
10
|
+
"wysiwyg",
|
|
11
|
+
"editor",
|
|
12
|
+
"yjs",
|
|
13
|
+
"collaboration",
|
|
14
|
+
"crdt",
|
|
15
|
+
"y-websocket",
|
|
16
|
+
"y-indexeddb",
|
|
17
|
+
"y-webrtc"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "sobree.dev",
|
|
21
|
+
"homepage": "https://docs.sobree.dev/api/collab-providers/",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/khayll/sobree/issues"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/khayll/sobree.git",
|
|
28
|
+
"directory": "packages/collab-providers"
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"development": "./src/index.ts",
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"yjs": "^13.6.0",
|
|
48
|
+
"@sobree/core": "0.1.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"y-websocket": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"y-indexeddb": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"y-webrtc": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"y-indexeddb": "^9.0.12",
|
|
63
|
+
"y-protocols": "^1.0.7",
|
|
64
|
+
"y-webrtc": "^10.3.0",
|
|
65
|
+
"y-websocket": "^3.0.0",
|
|
66
|
+
"@sobree/core": "0.1.0"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"typecheck": "tsc --noEmit",
|
|
70
|
+
"test": "vitest run",
|
|
71
|
+
"build": "vite build",
|
|
72
|
+
"clean": "rm -rf dist"
|
|
73
|
+
}
|
|
74
|
+
}
|