@defensestation/ysync-go 0.1.0 → 0.2.0-dev.13.f029dd9
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/README.md +44 -0
- package/dist/compaction/cli.d.ts +3 -0
- package/dist/compaction/cli.d.ts.map +1 -0
- package/dist/compaction/cli.js +124 -0
- package/dist/compaction/cli.js.map +1 -0
- package/dist/compaction/compaction.d.ts +67 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/compaction/compaction.js +153 -0
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/index.d.ts +3 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +2 -0
- package/dist/compaction/index.js.map +1 -0
- package/package.json +9 -3
- package/src/client/connect-yjs-provider.ts +0 -358
- package/src/client/id.ts +0 -22
- package/src/client/provider-status.ts +0 -9
- package/src/client/websocket-yjs-provider.ts +0 -335
- package/src/gen/ysync/v1/sync_pb.ts +0 -732
- package/src/index.ts +0 -7
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
import { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate } from "y-protocols/awareness";
|
|
2
|
-
import * as Y from "yjs";
|
|
3
|
-
import { makeClientId } from "./id.js";
|
|
4
|
-
import type { YjsProviderIdentity, YjsProviderIdentityListener, YjsProviderStatus, YjsProviderStatusListener } from "./provider-status.js";
|
|
5
|
-
|
|
6
|
-
const REMOTE_ORIGIN = "websocket-remote";
|
|
7
|
-
const DEFAULT_RECONNECT_INITIAL_DELAY_MS = 500;
|
|
8
|
-
const DEFAULT_RECONNECT_MAX_DELAY_MS = 5000;
|
|
9
|
-
const DEFAULT_AWARENESS_THROTTLE_MS = 150;
|
|
10
|
-
|
|
11
|
-
type WebSocketMessage = {
|
|
12
|
-
type: string;
|
|
13
|
-
docId?: string;
|
|
14
|
-
clientId?: string;
|
|
15
|
-
senderClientId?: string;
|
|
16
|
-
sequence?: number;
|
|
17
|
-
stateVector?: string;
|
|
18
|
-
update?: string;
|
|
19
|
-
errorCode?: string;
|
|
20
|
-
errorMessage?: string;
|
|
21
|
-
userId?: string;
|
|
22
|
-
userName?: string;
|
|
23
|
-
userColor?: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export interface WebSocketYjsProviderOptions {
|
|
27
|
-
ydoc: Y.Doc;
|
|
28
|
-
docId: string;
|
|
29
|
-
url?: string;
|
|
30
|
-
baseUrl?: string;
|
|
31
|
-
clientId?: string;
|
|
32
|
-
user: {
|
|
33
|
-
name: string;
|
|
34
|
-
color?: string;
|
|
35
|
-
};
|
|
36
|
-
reconnectInitialDelayMs?: number;
|
|
37
|
-
reconnectMaxDelayMs?: number;
|
|
38
|
-
awarenessThrottleMs?: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export class WebSocketYjsProvider {
|
|
42
|
-
readonly awareness: Awareness;
|
|
43
|
-
readonly clientId: string;
|
|
44
|
-
|
|
45
|
-
private readonly options: Required<
|
|
46
|
-
Pick<
|
|
47
|
-
WebSocketYjsProviderOptions,
|
|
48
|
-
"reconnectInitialDelayMs" | "reconnectMaxDelayMs" | "awarenessThrottleMs"
|
|
49
|
-
>
|
|
50
|
-
> &
|
|
51
|
-
WebSocketYjsProviderOptions;
|
|
52
|
-
private socket?: WebSocket;
|
|
53
|
-
private pending: WebSocketMessage[] = [];
|
|
54
|
-
private reconnectTimer?: ReturnType<typeof setTimeout>;
|
|
55
|
-
private awarenessSendTimer?: ReturnType<typeof setTimeout>;
|
|
56
|
-
private awarenessDirty = false;
|
|
57
|
-
private reconnectDelayMs: number;
|
|
58
|
-
private destroyed = false;
|
|
59
|
-
private currentStatus: YjsProviderStatus = "connecting";
|
|
60
|
-
private readonly statusListeners = new Set<YjsProviderStatusListener>();
|
|
61
|
-
private currentIdentity?: YjsProviderIdentity;
|
|
62
|
-
private readonly identityListeners = new Set<YjsProviderIdentityListener>();
|
|
63
|
-
|
|
64
|
-
private readonly yjsUpdateHandler = (update: Uint8Array, origin: unknown) => {
|
|
65
|
-
if (origin === REMOTE_ORIGIN) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
this.send({
|
|
69
|
-
type: "yjs_update",
|
|
70
|
-
docId: this.options.docId,
|
|
71
|
-
clientId: this.clientId,
|
|
72
|
-
update: bytesToBase64(update),
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// Only push changes to the LOCAL awareness state. Remote states arrive from
|
|
77
|
-
// the server already fanned out; re-broadcasting them would multiply every
|
|
78
|
-
// cursor move by the number of connected clients.
|
|
79
|
-
private readonly awarenessUpdateHandler = ({ added, updated, removed }: {
|
|
80
|
-
added: number[];
|
|
81
|
-
updated: number[];
|
|
82
|
-
removed: number[];
|
|
83
|
-
}) => {
|
|
84
|
-
const local = this.awareness.clientID;
|
|
85
|
-
if (!added.includes(local) && !updated.includes(local) && !removed.includes(local)) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
this.scheduleAwarenessSend();
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
constructor(options: WebSocketYjsProviderOptions) {
|
|
92
|
-
this.options = {
|
|
93
|
-
...options,
|
|
94
|
-
reconnectInitialDelayMs:
|
|
95
|
-
options.reconnectInitialDelayMs ?? DEFAULT_RECONNECT_INITIAL_DELAY_MS,
|
|
96
|
-
reconnectMaxDelayMs:
|
|
97
|
-
options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS,
|
|
98
|
-
awarenessThrottleMs:
|
|
99
|
-
options.awarenessThrottleMs ?? DEFAULT_AWARENESS_THROTTLE_MS,
|
|
100
|
-
};
|
|
101
|
-
this.clientId = options.clientId ?? makeClientId();
|
|
102
|
-
this.reconnectDelayMs = this.options.reconnectInitialDelayMs;
|
|
103
|
-
this.awareness = new Awareness(options.ydoc);
|
|
104
|
-
this.awareness.setLocalStateField("user", {
|
|
105
|
-
name: options.user.name,
|
|
106
|
-
color: options.user.color,
|
|
107
|
-
});
|
|
108
|
-
this.options.ydoc.on("update", this.yjsUpdateHandler);
|
|
109
|
-
// No extra heartbeat timer: y-protocols renews the local awareness state
|
|
110
|
-
// (bumping its clock) roughly every 15s while idle, which fires this
|
|
111
|
-
// handler. A fixed-interval resend of the same clock would be ignored by
|
|
112
|
-
// receivers anyway.
|
|
113
|
-
this.awareness.on("update", this.awarenessUpdateHandler);
|
|
114
|
-
this.connect();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
get status(): YjsProviderStatus {
|
|
118
|
-
return this.currentStatus;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Server-assigned presence identity; undefined until the join is
|
|
122
|
-
// acknowledged.
|
|
123
|
-
get identity(): YjsProviderIdentity | undefined {
|
|
124
|
-
return this.currentIdentity;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Returns an unsubscribe function.
|
|
128
|
-
onIdentity(listener: YjsProviderIdentityListener): () => void {
|
|
129
|
-
this.identityListeners.add(listener);
|
|
130
|
-
return () => this.identityListeners.delete(listener);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private applyIdentity(name?: string, color?: string) {
|
|
134
|
-
if (!name && !color) return;
|
|
135
|
-
const identity: YjsProviderIdentity = {
|
|
136
|
-
name: name || this.options.user.name,
|
|
137
|
-
color: color ?? this.options.user.color,
|
|
138
|
-
};
|
|
139
|
-
this.currentIdentity = identity;
|
|
140
|
-
this.awareness.setLocalStateField("user", { name: identity.name, color: identity.color });
|
|
141
|
-
for (const listener of this.identityListeners) listener(identity);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Returns an unsubscribe function.
|
|
145
|
-
onStatus(listener: YjsProviderStatusListener): () => void {
|
|
146
|
-
this.statusListeners.add(listener);
|
|
147
|
-
return () => this.statusListeners.delete(listener);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private setStatus(status: YjsProviderStatus) {
|
|
151
|
-
if (this.currentStatus === status) return;
|
|
152
|
-
this.currentStatus = status;
|
|
153
|
-
for (const listener of this.statusListeners) listener(status);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
destroy() {
|
|
157
|
-
if (this.destroyed) return;
|
|
158
|
-
this.options.ydoc.off("update", this.yjsUpdateHandler);
|
|
159
|
-
this.awareness.off("update", this.awarenessUpdateHandler);
|
|
160
|
-
// Announce departure while the socket is still usable so peers drop the
|
|
161
|
-
// cursor immediately instead of after the 30s awareness timeout.
|
|
162
|
-
this.awareness.setLocalState(null);
|
|
163
|
-
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
164
|
-
this.sendNow({
|
|
165
|
-
type: "awareness_update",
|
|
166
|
-
docId: this.options.docId,
|
|
167
|
-
clientId: this.clientId,
|
|
168
|
-
update: bytesToBase64(encodeAwarenessUpdate(this.awareness, [this.awareness.clientID])),
|
|
169
|
-
});
|
|
170
|
-
this.sendNow({ type: "leave", docId: this.options.docId, clientId: this.clientId });
|
|
171
|
-
}
|
|
172
|
-
this.destroyed = true;
|
|
173
|
-
this.setStatus("disconnected");
|
|
174
|
-
if (this.reconnectTimer) {
|
|
175
|
-
clearTimeout(this.reconnectTimer);
|
|
176
|
-
}
|
|
177
|
-
if (this.awarenessSendTimer) {
|
|
178
|
-
clearTimeout(this.awarenessSendTimer);
|
|
179
|
-
}
|
|
180
|
-
this.socket?.close();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
private connect() {
|
|
184
|
-
if (this.destroyed) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
this.setStatus("connecting");
|
|
188
|
-
const socket = new WebSocket(this.options.url ?? websocketURL(this.options.baseUrl));
|
|
189
|
-
this.socket = socket;
|
|
190
|
-
socket.addEventListener("open", () => {
|
|
191
|
-
this.setStatus("connected");
|
|
192
|
-
this.reconnectDelayMs = this.options.reconnectInitialDelayMs;
|
|
193
|
-
this.sendNow({
|
|
194
|
-
type: "join",
|
|
195
|
-
docId: this.options.docId,
|
|
196
|
-
clientId: this.clientId,
|
|
197
|
-
stateVector: bytesToBase64(Y.encodeStateVector(this.options.ydoc)),
|
|
198
|
-
userName: this.options.user.name,
|
|
199
|
-
userColor: this.options.user.color,
|
|
200
|
-
});
|
|
201
|
-
this.sendLocalAwarenessNow();
|
|
202
|
-
for (const message of this.pending.splice(0)) {
|
|
203
|
-
this.sendNow(message);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
socket.addEventListener("message", (event) => {
|
|
207
|
-
if (typeof event.data !== "string") {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
this.applyMessage(JSON.parse(event.data) as WebSocketMessage);
|
|
211
|
-
});
|
|
212
|
-
socket.addEventListener("close", () => {
|
|
213
|
-
if (this.socket === socket) {
|
|
214
|
-
this.socket = undefined;
|
|
215
|
-
}
|
|
216
|
-
this.setStatus("disconnected");
|
|
217
|
-
this.scheduleReconnect();
|
|
218
|
-
});
|
|
219
|
-
socket.addEventListener("error", () => {
|
|
220
|
-
socket.close();
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
private scheduleReconnect() {
|
|
225
|
-
if (this.destroyed || this.reconnectTimer) {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
const delay = this.reconnectDelayMs;
|
|
229
|
-
this.reconnectDelayMs = Math.min(
|
|
230
|
-
this.reconnectDelayMs * 2,
|
|
231
|
-
this.options.reconnectMaxDelayMs,
|
|
232
|
-
);
|
|
233
|
-
this.reconnectTimer = setTimeout(() => {
|
|
234
|
-
this.reconnectTimer = undefined;
|
|
235
|
-
this.connect();
|
|
236
|
-
}, delay);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
private send(message: WebSocketMessage) {
|
|
240
|
-
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
241
|
-
this.sendNow(message);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
this.pending.push(message);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private sendNow(message: WebSocketMessage) {
|
|
248
|
-
this.socket?.send(JSON.stringify(message));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Leading + trailing throttle: the first change sends immediately, changes
|
|
252
|
-
// arriving inside the window (e.g. cursor movement while typing or
|
|
253
|
-
// selecting) collapse into one trailing send.
|
|
254
|
-
private scheduleAwarenessSend() {
|
|
255
|
-
if (this.destroyed) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
if (this.awarenessSendTimer) {
|
|
259
|
-
this.awarenessDirty = true;
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
this.sendLocalAwarenessNow();
|
|
263
|
-
this.awarenessSendTimer = setTimeout(() => {
|
|
264
|
-
this.awarenessSendTimer = undefined;
|
|
265
|
-
if (this.awarenessDirty) {
|
|
266
|
-
this.awarenessDirty = false;
|
|
267
|
-
this.scheduleAwarenessSend();
|
|
268
|
-
}
|
|
269
|
-
}, this.options.awarenessThrottleMs);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
private sendLocalAwarenessNow() {
|
|
273
|
-
if (this.destroyed) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
this.send({
|
|
277
|
-
type: "awareness_update",
|
|
278
|
-
docId: this.options.docId,
|
|
279
|
-
clientId: this.clientId,
|
|
280
|
-
update: bytesToBase64(encodeAwarenessUpdate(this.awareness, [this.awareness.clientID])),
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
private applyMessage(message: WebSocketMessage) {
|
|
285
|
-
switch (message.type) {
|
|
286
|
-
case "joined":
|
|
287
|
-
this.applyIdentity(message.userName, message.userColor);
|
|
288
|
-
break;
|
|
289
|
-
case "sync_update":
|
|
290
|
-
case "yjs_update":
|
|
291
|
-
if (message.update) {
|
|
292
|
-
Y.applyUpdate(this.options.ydoc, base64ToBytes(message.update), REMOTE_ORIGIN);
|
|
293
|
-
}
|
|
294
|
-
break;
|
|
295
|
-
case "awareness_update":
|
|
296
|
-
if (message.update) {
|
|
297
|
-
applyAwarenessUpdate(this.awareness, base64ToBytes(message.update), REMOTE_ORIGIN);
|
|
298
|
-
}
|
|
299
|
-
break;
|
|
300
|
-
case "error":
|
|
301
|
-
console.error("Yjs WebSocket server error", message.errorCode, message.errorMessage);
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function websocketURL(baseUrl?: string): string {
|
|
308
|
-
const fallback =
|
|
309
|
-
typeof window !== "undefined" && window.location?.origin
|
|
310
|
-
? window.location.origin
|
|
311
|
-
: "http://localhost:8080";
|
|
312
|
-
const url = new URL(baseUrl ?? fallback);
|
|
313
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
314
|
-
url.pathname = "/ysync/ws";
|
|
315
|
-
url.search = "";
|
|
316
|
-
url.hash = "";
|
|
317
|
-
return url.toString();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function bytesToBase64(bytes: Uint8Array): string {
|
|
321
|
-
let binary = "";
|
|
322
|
-
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
323
|
-
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
324
|
-
}
|
|
325
|
-
return btoa(binary);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function base64ToBytes(base64: string): Uint8Array {
|
|
329
|
-
const binary = atob(base64);
|
|
330
|
-
const bytes = new Uint8Array(binary.length);
|
|
331
|
-
for (let i = 0; i < binary.length; i++) {
|
|
332
|
-
bytes[i] = binary.charCodeAt(i);
|
|
333
|
-
}
|
|
334
|
-
return bytes;
|
|
335
|
-
}
|