Skip to content

Commit e1082a8

Browse files
authored
[y sweet/client] support provider.on() (#301)
When we [reworked the provider to support reconnections](#292), we lost the `on`/`off`/`once` methods from the `Observer` base class that used to exist on `YSweetProvider`. It turns out those methods are often used by third-party libraries. This PR adds support for those back in.
1 parent b6326c1 commit e1082a8

File tree

6 files changed

+57
-31
lines changed

6 files changed

+57
-31
lines changed

examples/nextjs/src/app/(demos)/slate/SlateEditor.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ export function SlateEditor() {
1818
}, [yDoc])
1919

2020
useEffect(() => {
21-
provider.observable.on('sync', setConnected)
22-
return () => provider.observable.off('sync', setConnected)
21+
provider.on('sync', setConnected)
22+
return () => provider.off('sync', setConnected)
2323
}, [provider])
2424

2525
if (!connected) return 'Loading...'

examples/nextjs/src/app/tldraw/useYjsStore.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,18 +232,18 @@ export function useYjsStore() {
232232
return
233233
}
234234

235-
provider.observable.off('sync', handleSync)
235+
provider.off('sync', handleSync)
236236

237237
if (status === 'connected') {
238238
if (hasConnectedBefore) return
239239
hasConnectedBefore = true
240-
provider.observable.on('sync', handleSync)
241-
unsubs.push(() => provider.observable.off('sync', handleSync))
240+
provider.on('sync', handleSync)
241+
unsubs.push(() => provider.off('sync', handleSync))
242242
}
243243
}
244244

245-
provider.observable.on('status', handleStatusChange)
246-
unsubs.push(() => provider.observable.off('status', handleStatusChange))
245+
provider.on('status', handleStatusChange)
246+
unsubs.push(() => provider.off('status', handleStatusChange))
247247

248248
return () => {
249249
unsubs.forEach((fn) => fn())

js-pkg/client/src/provider.ts

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ const setupWS = (provider: YSweetProvider) => {
114114
}
115115
}
116116
websocket.onerror = (event) => {
117-
provider.observable.emit('connection-error', [event, provider])
117+
provider.emit('connection-error', [event, provider])
118118
}
119119
websocket.onclose = (event) => {
120-
provider.observable.emit('connection-close', [event, provider])
120+
provider.emit('connection-close', [event, provider])
121121
provider.ws = null
122122
provider.wsconnecting = false
123123
if (provider.wsconnected) {
@@ -131,7 +131,7 @@ const setupWS = (provider: YSweetProvider) => {
131131
),
132132
provider,
133133
)
134-
provider.observable.emit('status', [
134+
provider.emit('status', [
135135
{
136136
status: 'disconnected',
137137
},
@@ -161,7 +161,7 @@ const setupWS = (provider: YSweetProvider) => {
161161
provider.wsconnecting = false
162162
provider.wsconnected = true
163163
provider.wsUnsuccessfulReconnects = 0
164-
provider.observable.emit('status', [
164+
provider.emit('status', [
165165
{
166166
status: 'connected',
167167
},
@@ -182,7 +182,7 @@ const setupWS = (provider: YSweetProvider) => {
182182
websocket.send(encoding.toUint8Array(encoderAwarenessState))
183183
}
184184
}
185-
provider.observable.emit('status', [
185+
provider.emit('status', [
186186
{
187187
status: 'connecting',
188188
},
@@ -224,7 +224,6 @@ export type YSweetProviderParams = {
224224
resyncInterval?: number
225225
maxBackoffTime?: number
226226
disableBc?: boolean
227-
observable?: Observable<string>
228227
}
229228

230229
/**
@@ -238,14 +237,13 @@ export type YSweetProviderParams = {
238237
* const doc = new Y.Doc()
239238
* const provider = new YSweetProvider('http://localhost:1234', 'my-document-name', doc)
240239
*/
241-
export class YSweetProvider {
240+
export class YSweetProvider extends Observable<string> {
242241
onFailureHandlers: Array<() => void> = []
243242
maxBackoffTime: number
244243
bcChannel: string
245244
url: string
246245
roomname: string
247246
doc: Y.Doc
248-
observable: Observable<string>
249247
_WS: WebSocketPolyfillType
250248
awareness: awarenessProtocol.Awareness
251249
wsconnected: boolean
@@ -277,7 +275,6 @@ export class YSweetProvider {
277275
* @param opts.resyncInterval - resync interval
278276
* @param opts.maxBackoffTime - maximum backoff time
279277
* @param opts.disableBc - disable broadcast channel
280-
* @param opts.observable - an observable instance to emit events on
281278
*/
282279
constructor(
283280
serverUrl: string,
@@ -291,15 +288,14 @@ export class YSweetProvider {
291288
resyncInterval = -1,
292289
maxBackoffTime = 2500,
293290
disableBc = false,
294-
observable = new Observable<string>(),
295291
}: YSweetProviderParams = {},
296292
) {
293+
super()
297294
// ensure that url is always ends with /
298295
while (serverUrl[serverUrl.length - 1] === '/') {
299296
serverUrl = serverUrl.slice(0, serverUrl.length - 1)
300297
}
301298
const encodedParams = url.encodeQueryParams(params)
302-
this.observable = observable
303299
this.maxBackoffTime = maxBackoffTime
304300
this.bcChannel = serverUrl + '/' + roomname
305301
this.url = serverUrl + '/' + roomname + (encodedParams.length === 0 ? '' : '?' + encodedParams)
@@ -407,8 +403,8 @@ export class YSweetProvider {
407403
set synced(state) {
408404
if (this._synced !== state) {
409405
this._synced = state
410-
this.observable.emit('synced', [state])
411-
this.observable.emit('sync', [state])
406+
this.emit('synced', [state])
407+
this.emit('sync', [state])
412408
}
413409
}
414410

@@ -425,6 +421,7 @@ export class YSweetProvider {
425421
}
426422
this.awareness.off('update', this._awarenessUpdateHandler)
427423
this.doc.off('update', this._updateHandler)
424+
super.destroy()
428425
}
429426

430427
connectBc() {
@@ -531,9 +528,15 @@ export async function ySweetProviderWrapper(
531528
doc: Y.Doc,
532529
providerParams: YSweetProviderParams = {},
533530
): Promise<YSweetProviderWithClientToken> {
534-
const observable = providerParams.observable ?? new Observable<string>()
531+
// we use an observable that lives outside the provider to store event listeners
532+
// so that we can re-subscribe to events when the provider is re-created
533+
const observable = new Observable<string>()
534+
// keep track of which events have been subscribed to on the local observable
535+
// so we can re-subscribe to them when the provider is re-created
536+
const subscribedEvents = new Set<string>()
537+
535538
const awareness = providerParams.awareness ?? new awarenessProtocol.Awareness(doc)
536-
providerParams = { ...providerParams, observable, awareness }
539+
providerParams = { ...providerParams, awareness }
537540

538541
let _clientToken = await getClientToken(authEndpoint, roomname)
539542
let _provider = new YSweetProvider(_clientToken.url, roomname, doc, {
@@ -548,10 +551,33 @@ export async function ySweetProviderWrapper(
548551
connect: true,
549552
})
550553
_provider.addOnFailureHandler(recreateProvider)
554+
// the previous provider's destroy() method should have been called before
555+
// recreateProvider() is called, so we don't need to unsubscribe from events
556+
// before re-subscribing to events here
557+
for (const event of subscribedEvents) {
558+
subscribeToEvent(event)
559+
}
560+
}
561+
562+
// for each event that is subscribed to on the local observable, make sure to
563+
// subscribe to it on the provider
564+
function subscribeToEvent(name: string) {
565+
_provider.on(name, (...args: any[]) => observable.emit(name, args))
566+
subscribedEvents.add(name)
551567
}
552568

553569
return {
554-
observable,
570+
on: (name: string, f: (...args: any[]) => void) => {
571+
if (!subscribedEvents.has(name)) subscribeToEvent(name)
572+
observable.on(name, f)
573+
},
574+
once: (name: string, f: (...args: any[]) => void) => {
575+
if (!subscribedEvents.has(name)) subscribeToEvent(name)
576+
observable.once(name, f)
577+
},
578+
off: (name: string, f: (...args: any[]) => void) => {
579+
observable.off(name, f)
580+
},
555581
awareness,
556582
get clientToken() {
557583
return _clientToken
@@ -619,5 +645,5 @@ export async function ySweetProviderWrapper(
619645
get shouldConnect() {
620646
return _provider.shouldConnect
621647
},
622-
} as YSweetProviderWithClientToken
648+
} as unknown as YSweetProviderWithClientToken
623649
}

tests/src/convert.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async function connectToDoc(server: Server, docId: string): Promise<Y.Doc> {
3636
})
3737

3838
await new Promise<void>((resolve, reject) => {
39-
provider.observable.on('synced', resolve)
39+
provider.on('synced', resolve)
4040

4141
setTimeout(() => {
4242
reject('Timed out waiting for sync')

tests/src/index.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ describe.each(CONFIGURATIONS)(
127127
const provider = await createYjsProvider(doc, docResult.docId, getClientToken, {})
128128

129129
await new Promise((resolve, reject) => {
130-
provider.observable.on('synced', resolve)
131-
provider.observable.on('syncing', reject)
130+
provider.on('synced', resolve)
131+
provider.on('syncing', reject)
132132
})
133133
})
134134

@@ -227,7 +227,7 @@ describe.each(CONFIGURATIONS)(
227227

228228
await new Promise<void>((resolve, reject) => {
229229
setTimeout(() => reject('Expected to disconnect.'), 1_000)
230-
provider.observable.on('connection-close', () => {
230+
provider.on('connection-close', () => {
231231
resolve()
232232
})
233233
})
@@ -240,7 +240,7 @@ describe.each(CONFIGURATIONS)(
240240
// Reconnect to the doc.
241241
provider.connect()
242242
await new Promise<void>((resolve, reject) => {
243-
provider.observable.on('status', (event: { status: string }) => {
243+
provider.on('status', (event: { status: string }) => {
244244
if (event.status === 'connected') {
245245
resolve()
246246
} else {

tests/src/util.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { YSweetProvider } from '@y-sweet/react'
22

33
export async function waitForProviderSync(provider: YSweetProvider, timeoutMillis: number = 1_000) {
44
return new Promise((resolve, reject) => {
5-
provider.observable.on('synced', resolve)
6-
provider.observable.on('syncing', reject)
5+
provider.on('synced', resolve)
6+
provider.on('syncing', reject)
77

88
setTimeout(() => reject('Timed out waiting for provider to sync.'), timeoutMillis)
99
})

0 commit comments

Comments
 (0)