@defensestation/ysync-go 0.1.0-dev.7.0bd462a → 0.1.0-dev.9.f016116
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defensestation/ysync-go",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.9.f016116",
|
|
4
4
|
"description": "Browser Yjs providers (Connect RPC and WebSocket) for the ysync-go collaboration backend",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,8 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
|
-
"dist"
|
|
40
|
-
"src"
|
|
39
|
+
"dist"
|
|
41
40
|
],
|
|
42
41
|
"scripts": {
|
|
43
42
|
"generate": "cd .. && PATH=\"./npm/node_modules/.bin:$PATH\" buf generate --template npm/buf.gen.yaml",
|
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
import { Code, ConnectError, createClient, type Interceptor } from "@connectrpc/connect";
|
|
2
|
-
import { createConnectTransport } from "@connectrpc/connect-web";
|
|
3
|
-
import { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate } from "y-protocols/awareness";
|
|
4
|
-
import * as Y from "yjs";
|
|
5
|
-
import { type ServerFrame, YjsSyncService } from "../gen/ysync/v1/sync_pb.js";
|
|
6
|
-
import { makeClientId } from "./id.js";
|
|
7
|
-
import type { YjsProviderIdentity, YjsProviderIdentityListener, YjsProviderStatus, YjsProviderStatusListener } from "./provider-status.js";
|
|
8
|
-
|
|
9
|
-
const REMOTE_ORIGIN = "connectrpc-remote";
|
|
10
|
-
const DEFAULT_RECONNECT_INITIAL_DELAY_MS = 500;
|
|
11
|
-
const DEFAULT_RECONNECT_MAX_DELAY_MS = 5000;
|
|
12
|
-
const DEFAULT_AWARENESS_THROTTLE_MS = 150;
|
|
13
|
-
const DEFAULT_UPDATE_FLUSH_DELAY_MS = 100;
|
|
14
|
-
|
|
15
|
-
function defaultBaseUrl(): string {
|
|
16
|
-
return typeof window !== "undefined" && window.location?.origin ? window.location.origin : "http://localhost:8080";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function createDefaultClient(baseUrl: string, user?: { name: string; color?: string }) {
|
|
20
|
-
const interceptors: Interceptor[] = [];
|
|
21
|
-
if (user) {
|
|
22
|
-
// Actor identity for audit history; the server reads these headers on
|
|
23
|
-
// every RPC. Real deployments should derive identity from auth instead.
|
|
24
|
-
const asciiOnly = (value: string) => /^[\x20-\x7E]*$/.test(value);
|
|
25
|
-
interceptors.push((next) => (req) => {
|
|
26
|
-
if (asciiOnly(user.name)) req.header.set("x-user-name", user.name);
|
|
27
|
-
if (user.color && asciiOnly(user.color)) req.header.set("x-user-color", user.color);
|
|
28
|
-
return next(req);
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
return createClient(YjsSyncService, createConnectTransport({ baseUrl, useBinaryFormat: true, interceptors }));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type YjsSyncRpcClient = ReturnType<typeof createDefaultClient>;
|
|
35
|
-
|
|
36
|
-
export interface ConnectYjsProviderOptions {
|
|
37
|
-
ydoc: Y.Doc;
|
|
38
|
-
docId: string;
|
|
39
|
-
baseUrl?: string;
|
|
40
|
-
rpcClient?: YjsSyncRpcClient;
|
|
41
|
-
clientId?: string;
|
|
42
|
-
user: { name: string; color?: string };
|
|
43
|
-
reconnectInitialDelayMs?: number;
|
|
44
|
-
reconnectMaxDelayMs?: number;
|
|
45
|
-
awarenessThrottleMs?: number;
|
|
46
|
-
// Micro-batch window for document updates: the first local update in a
|
|
47
|
-
// window is pushed immediately, updates arriving within the window are
|
|
48
|
-
// merged into one request. 0 pushes every update individually.
|
|
49
|
-
updateFlushDelayMs?: number;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Browser-compatible Connect RPC provider. Browsers cannot open bidirectional
|
|
53
|
-
// Connect streams (fetch has no streaming request bodies), so this provider
|
|
54
|
-
// uses the unary/server-stream workflow from the proto contract:
|
|
55
|
-
// JoinDocument -> StreamDocumentFrames + PushYjsUpdate/PushAwarenessUpdate,
|
|
56
|
-
// then LeaveDocument on teardown. The bidirectional Sync RPC remains available
|
|
57
|
-
// for clients on transports that support it (e.g. connect-go over HTTP/2).
|
|
58
|
-
export class ConnectYjsProvider {
|
|
59
|
-
readonly awareness: Awareness;
|
|
60
|
-
readonly clientId: string;
|
|
61
|
-
|
|
62
|
-
private readonly client: YjsSyncRpcClient;
|
|
63
|
-
private readonly options: Required<Pick<ConnectYjsProviderOptions, "reconnectInitialDelayMs" | "reconnectMaxDelayMs" | "awarenessThrottleMs" | "updateFlushDelayMs">> & ConnectYjsProviderOptions;
|
|
64
|
-
private abortController?: AbortController;
|
|
65
|
-
private reconnectTimer?: ReturnType<typeof setTimeout>;
|
|
66
|
-
private awarenessSendTimer?: ReturnType<typeof setTimeout>;
|
|
67
|
-
private awarenessDirty = false;
|
|
68
|
-
private updateFlushTimer?: ReturnType<typeof setTimeout>;
|
|
69
|
-
private updatesDirty = false;
|
|
70
|
-
private destroyed = false;
|
|
71
|
-
private joined = false;
|
|
72
|
-
private reconnectDelayMs: number;
|
|
73
|
-
private pendingUpdates: Uint8Array[] = [];
|
|
74
|
-
// The current batch is frozen with its idempotency key so a retry after a
|
|
75
|
-
// failed push resends the exact same payload; updates queued in the
|
|
76
|
-
// meantime merge into the NEXT batch.
|
|
77
|
-
private inFlightBatch?: { key: string; update: Uint8Array };
|
|
78
|
-
private pushInFlight = false;
|
|
79
|
-
private updateCounter = 0;
|
|
80
|
-
private currentStatus: YjsProviderStatus = "connecting";
|
|
81
|
-
private readonly statusListeners = new Set<YjsProviderStatusListener>();
|
|
82
|
-
private currentIdentity?: YjsProviderIdentity;
|
|
83
|
-
private readonly identityListeners = new Set<YjsProviderIdentityListener>();
|
|
84
|
-
|
|
85
|
-
private readonly yjsUpdateHandler = (update: Uint8Array, origin: unknown) => {
|
|
86
|
-
if (origin === REMOTE_ORIGIN) return;
|
|
87
|
-
this.pendingUpdates.push(update);
|
|
88
|
-
this.scheduleUpdateFlush();
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// Only push changes to the LOCAL awareness state. Remote states arrive from
|
|
92
|
-
// the server already fanned out; re-broadcasting them would multiply every
|
|
93
|
-
// cursor move by the number of connected clients.
|
|
94
|
-
private readonly awarenessUpdateHandler = ({ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }) => {
|
|
95
|
-
const local = this.awareness.clientID;
|
|
96
|
-
if (!added.includes(local) && !updated.includes(local) && !removed.includes(local)) return;
|
|
97
|
-
this.scheduleAwarenessSend();
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
constructor(options: ConnectYjsProviderOptions) {
|
|
101
|
-
this.options = {
|
|
102
|
-
...options,
|
|
103
|
-
reconnectInitialDelayMs: options.reconnectInitialDelayMs ?? DEFAULT_RECONNECT_INITIAL_DELAY_MS,
|
|
104
|
-
reconnectMaxDelayMs: options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS,
|
|
105
|
-
awarenessThrottleMs: options.awarenessThrottleMs ?? DEFAULT_AWARENESS_THROTTLE_MS,
|
|
106
|
-
updateFlushDelayMs: options.updateFlushDelayMs ?? DEFAULT_UPDATE_FLUSH_DELAY_MS,
|
|
107
|
-
};
|
|
108
|
-
this.clientId = options.clientId ?? makeClientId();
|
|
109
|
-
this.client = options.rpcClient ?? createDefaultClient(options.baseUrl ?? defaultBaseUrl(), options.user);
|
|
110
|
-
this.reconnectDelayMs = this.options.reconnectInitialDelayMs;
|
|
111
|
-
this.awareness = new Awareness(options.ydoc);
|
|
112
|
-
this.awareness.setLocalStateField("user", { name: options.user.name, color: options.user.color });
|
|
113
|
-
this.options.ydoc.on("update", this.yjsUpdateHandler);
|
|
114
|
-
// No extra heartbeat timer: y-protocols renews the local awareness state
|
|
115
|
-
// (bumping its clock) roughly every 15s while idle, which fires this
|
|
116
|
-
// handler. A fixed-interval resend of the same clock would be ignored by
|
|
117
|
-
// receivers anyway.
|
|
118
|
-
this.awareness.on("update", this.awarenessUpdateHandler);
|
|
119
|
-
this.connect();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
get status(): YjsProviderStatus {
|
|
123
|
-
return this.currentStatus;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Server-assigned presence identity; undefined until the join completes.
|
|
127
|
-
get identity(): YjsProviderIdentity | undefined {
|
|
128
|
-
return this.currentIdentity;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Returns an unsubscribe function.
|
|
132
|
-
onIdentity(listener: YjsProviderIdentityListener): () => void {
|
|
133
|
-
this.identityListeners.add(listener);
|
|
134
|
-
return () => this.identityListeners.delete(listener);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private applyIdentity(name?: string, color?: string) {
|
|
138
|
-
if (!name && !color) return;
|
|
139
|
-
const identity: YjsProviderIdentity = {
|
|
140
|
-
name: name || this.options.user.name,
|
|
141
|
-
color: color ?? this.options.user.color,
|
|
142
|
-
};
|
|
143
|
-
this.currentIdentity = identity;
|
|
144
|
-
this.awareness.setLocalStateField("user", { name: identity.name, color: identity.color });
|
|
145
|
-
for (const listener of this.identityListeners) listener(identity);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Returns an unsubscribe function.
|
|
149
|
-
onStatus(listener: YjsProviderStatusListener): () => void {
|
|
150
|
-
this.statusListeners.add(listener);
|
|
151
|
-
return () => this.statusListeners.delete(listener);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private setStatus(status: YjsProviderStatus) {
|
|
155
|
-
if (this.currentStatus === status) return;
|
|
156
|
-
this.currentStatus = status;
|
|
157
|
-
for (const listener of this.statusListeners) listener(status);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
destroy() {
|
|
161
|
-
if (this.destroyed) return;
|
|
162
|
-
this.options.ydoc.off("update", this.yjsUpdateHandler);
|
|
163
|
-
this.awareness.off("update", this.awarenessUpdateHandler);
|
|
164
|
-
// Announce departure so peers drop the cursor immediately instead of
|
|
165
|
-
// after the 30s awareness timeout.
|
|
166
|
-
this.awareness.setLocalState(null);
|
|
167
|
-
if (this.joined) {
|
|
168
|
-
void this.client
|
|
169
|
-
.pushAwarenessUpdate({
|
|
170
|
-
docId: this.options.docId,
|
|
171
|
-
clientId: this.clientId,
|
|
172
|
-
awarenessUpdate: encodeAwarenessUpdate(this.awareness, [this.awareness.clientID]),
|
|
173
|
-
})
|
|
174
|
-
.catch(() => undefined);
|
|
175
|
-
}
|
|
176
|
-
this.destroyed = true;
|
|
177
|
-
this.setStatus("disconnected");
|
|
178
|
-
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
179
|
-
if (this.awarenessSendTimer) clearTimeout(this.awarenessSendTimer);
|
|
180
|
-
if (this.updateFlushTimer) clearTimeout(this.updateFlushTimer);
|
|
181
|
-
void this.client
|
|
182
|
-
.leaveDocument({ docId: this.options.docId, clientId: this.clientId })
|
|
183
|
-
.catch(() => undefined);
|
|
184
|
-
this.abortController?.abort();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private connect() {
|
|
188
|
-
if (this.destroyed) return;
|
|
189
|
-
this.abortController?.abort();
|
|
190
|
-
this.abortController = new AbortController();
|
|
191
|
-
this.setStatus("connecting");
|
|
192
|
-
void this.runSession(this.abortController.signal);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
private async runSession(signal: AbortSignal) {
|
|
196
|
-
try {
|
|
197
|
-
const join = await this.client.joinDocument(
|
|
198
|
-
{
|
|
199
|
-
docId: this.options.docId,
|
|
200
|
-
clientId: this.clientId,
|
|
201
|
-
stateVector: Y.encodeStateVector(this.options.ydoc),
|
|
202
|
-
},
|
|
203
|
-
{ signal },
|
|
204
|
-
);
|
|
205
|
-
for (const update of join.syncUpdates) {
|
|
206
|
-
if (update.length > 0) Y.applyUpdate(this.options.ydoc, update, REMOTE_ORIGIN);
|
|
207
|
-
}
|
|
208
|
-
this.joined = true;
|
|
209
|
-
this.applyIdentity(join.assignedDisplayName, join.assignedColor);
|
|
210
|
-
this.setStatus("connected");
|
|
211
|
-
this.reconnectDelayMs = this.options.reconnectInitialDelayMs;
|
|
212
|
-
this.sendLocalAwarenessNow();
|
|
213
|
-
void this.flushUpdates();
|
|
214
|
-
|
|
215
|
-
const frames = this.client.streamDocumentFrames(
|
|
216
|
-
{
|
|
217
|
-
docId: this.options.docId,
|
|
218
|
-
clientId: this.clientId,
|
|
219
|
-
afterSequence: join.sequence,
|
|
220
|
-
},
|
|
221
|
-
{ signal },
|
|
222
|
-
);
|
|
223
|
-
for await (const response of frames) {
|
|
224
|
-
if (response.frame) this.applyFrame(response.frame);
|
|
225
|
-
}
|
|
226
|
-
this.joined = false;
|
|
227
|
-
if (!this.destroyed && !signal.aborted) {
|
|
228
|
-
this.setStatus("disconnected");
|
|
229
|
-
this.scheduleReconnect();
|
|
230
|
-
}
|
|
231
|
-
} catch (error) {
|
|
232
|
-
this.joined = false;
|
|
233
|
-
if (!this.destroyed && !isAbortError(error)) {
|
|
234
|
-
this.setStatus("disconnected");
|
|
235
|
-
console.error("Yjs sync stream failed", error);
|
|
236
|
-
this.scheduleReconnect();
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
private scheduleReconnect() {
|
|
242
|
-
if (this.destroyed || this.reconnectTimer) return;
|
|
243
|
-
const delay = this.reconnectDelayMs;
|
|
244
|
-
this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, this.options.reconnectMaxDelayMs);
|
|
245
|
-
this.reconnectTimer = setTimeout(() => {
|
|
246
|
-
this.reconnectTimer = undefined;
|
|
247
|
-
this.connect();
|
|
248
|
-
}, delay);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Leading + trailing micro-batch, same shape as the awareness throttle.
|
|
252
|
-
private scheduleUpdateFlush() {
|
|
253
|
-
if (this.destroyed) return;
|
|
254
|
-
if (this.options.updateFlushDelayMs <= 0) {
|
|
255
|
-
void this.flushUpdates();
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
if (this.updateFlushTimer) {
|
|
259
|
-
this.updatesDirty = true;
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
void this.flushUpdates();
|
|
263
|
-
this.updateFlushTimer = setTimeout(() => {
|
|
264
|
-
this.updateFlushTimer = undefined;
|
|
265
|
-
if (this.updatesDirty) {
|
|
266
|
-
this.updatesDirty = false;
|
|
267
|
-
this.scheduleUpdateFlush();
|
|
268
|
-
}
|
|
269
|
-
}, this.options.updateFlushDelayMs);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
private async flushUpdates() {
|
|
273
|
-
if (this.pushInFlight || !this.joined) return;
|
|
274
|
-
this.pushInFlight = true;
|
|
275
|
-
try {
|
|
276
|
-
while ((this.inFlightBatch || this.pendingUpdates.length > 0) && this.joined && !this.destroyed) {
|
|
277
|
-
if (!this.inFlightBatch) {
|
|
278
|
-
// Merge everything queued so far into one push. Under network
|
|
279
|
-
// latency this naturally batches keystrokes: while one push is in
|
|
280
|
-
// flight, new updates accumulate and go out as a single request.
|
|
281
|
-
const batch = this.pendingUpdates.splice(0);
|
|
282
|
-
this.inFlightBatch = {
|
|
283
|
-
key: `${this.clientId}-${this.updateCounter++}`,
|
|
284
|
-
update: batch.length === 1 ? batch[0] : Y.mergeUpdates(batch),
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
try {
|
|
288
|
-
await this.client.pushYjsUpdate({
|
|
289
|
-
docId: this.options.docId,
|
|
290
|
-
clientId: this.clientId,
|
|
291
|
-
yjsUpdate: this.inFlightBatch.update,
|
|
292
|
-
idempotencyKey: this.inFlightBatch.key,
|
|
293
|
-
});
|
|
294
|
-
} catch (error) {
|
|
295
|
-
if (!this.destroyed && !isAbortError(error)) {
|
|
296
|
-
console.error("Yjs update push failed", error);
|
|
297
|
-
this.scheduleReconnect();
|
|
298
|
-
}
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
this.inFlightBatch = undefined;
|
|
302
|
-
}
|
|
303
|
-
} finally {
|
|
304
|
-
this.pushInFlight = false;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Leading + trailing throttle: the first change sends immediately, changes
|
|
309
|
-
// arriving inside the window (e.g. cursor movement while typing or
|
|
310
|
-
// selecting) collapse into one trailing send.
|
|
311
|
-
private scheduleAwarenessSend() {
|
|
312
|
-
if (this.destroyed) return;
|
|
313
|
-
if (this.awarenessSendTimer) {
|
|
314
|
-
this.awarenessDirty = true;
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
this.sendLocalAwarenessNow();
|
|
318
|
-
this.awarenessSendTimer = setTimeout(() => {
|
|
319
|
-
this.awarenessSendTimer = undefined;
|
|
320
|
-
if (this.awarenessDirty) {
|
|
321
|
-
this.awarenessDirty = false;
|
|
322
|
-
this.scheduleAwarenessSend();
|
|
323
|
-
}
|
|
324
|
-
}, this.options.awarenessThrottleMs);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
private sendLocalAwarenessNow() {
|
|
328
|
-
if (this.destroyed || !this.joined) return;
|
|
329
|
-
void this.client
|
|
330
|
-
.pushAwarenessUpdate({
|
|
331
|
-
docId: this.options.docId,
|
|
332
|
-
clientId: this.clientId,
|
|
333
|
-
awarenessUpdate: encodeAwarenessUpdate(this.awareness, [this.awareness.clientID]),
|
|
334
|
-
})
|
|
335
|
-
.catch(() => undefined);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
private applyFrame(frame: ServerFrame) {
|
|
339
|
-
switch (frame.payload.case) {
|
|
340
|
-
case "syncUpdate":
|
|
341
|
-
case "yjsUpdate":
|
|
342
|
-
Y.applyUpdate(this.options.ydoc, frame.payload.value, REMOTE_ORIGIN);
|
|
343
|
-
this.reconnectDelayMs = this.options.reconnectInitialDelayMs;
|
|
344
|
-
break;
|
|
345
|
-
case "awarenessUpdate":
|
|
346
|
-
applyAwarenessUpdate(this.awareness, frame.payload.value, REMOTE_ORIGIN);
|
|
347
|
-
break;
|
|
348
|
-
case "error":
|
|
349
|
-
console.error("Yjs sync server error", frame.payload.value);
|
|
350
|
-
break;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function isAbortError(error: unknown): boolean {
|
|
356
|
-
if (error instanceof DOMException && error.name === "AbortError") return true;
|
|
357
|
-
return error instanceof ConnectError && error.code === Code.Canceled;
|
|
358
|
-
}
|
package/src/client/id.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export function makeClientId() {
|
|
2
|
-
const cryptoApi = globalThis.crypto;
|
|
3
|
-
if (typeof cryptoApi?.randomUUID === "function") {
|
|
4
|
-
return cryptoApi.randomUUID();
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
if (typeof cryptoApi?.getRandomValues === "function") {
|
|
8
|
-
const bytes = cryptoApi.getRandomValues(new Uint8Array(16));
|
|
9
|
-
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
10
|
-
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
11
|
-
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0"));
|
|
12
|
-
return [
|
|
13
|
-
hex.slice(0, 4).join(""),
|
|
14
|
-
hex.slice(4, 6).join(""),
|
|
15
|
-
hex.slice(6, 8).join(""),
|
|
16
|
-
hex.slice(8, 10).join(""),
|
|
17
|
-
hex.slice(10, 16).join(""),
|
|
18
|
-
].join("-");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return `client-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
22
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export type YjsProviderStatus = "connecting" | "connected" | "disconnected";
|
|
2
|
-
|
|
3
|
-
export type YjsProviderStatusListener = (status: YjsProviderStatus) => void;
|
|
4
|
-
|
|
5
|
-
// Presence identity assigned by the server at join time: sanitized display
|
|
6
|
-
// name plus a palette color that is distinct among the room's participants.
|
|
7
|
-
export type YjsProviderIdentity = { name: string; color?: string };
|
|
8
|
-
|
|
9
|
-
export type YjsProviderIdentityListener = (identity: YjsProviderIdentity) => void;
|