@hocuspocus/provider 1.0.0-alpha.9 → 1.0.0-beta.2

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.
Files changed (134) hide show
  1. package/dist/hocuspocus-provider.cjs +374 -186
  2. package/dist/hocuspocus-provider.cjs.map +1 -1
  3. package/dist/hocuspocus-provider.esm.js +372 -182
  4. package/dist/hocuspocus-provider.esm.js.map +1 -1
  5. package/dist/packages/common/src/CloseEvents.d.ts +23 -0
  6. package/dist/packages/common/src/auth.d.ts +6 -0
  7. package/dist/packages/common/src/awarenessStatesToArray.d.ts +3 -0
  8. package/dist/packages/common/src/index.d.ts +4 -0
  9. package/dist/packages/common/src/types.d.ts +10 -0
  10. package/dist/packages/extension-database/src/Database.d.ts +30 -0
  11. package/dist/packages/extension-database/src/index.d.ts +1 -0
  12. package/dist/packages/extension-logger/src/Logger.d.ts +67 -0
  13. package/dist/packages/extension-logger/src/index.d.ts +1 -0
  14. package/dist/packages/{monitor → extension-monitor}/src/Collector.d.ts +4 -5
  15. package/dist/packages/{monitor → extension-monitor}/src/Dashboard.d.ts +2 -2
  16. package/dist/packages/{monitor → extension-monitor}/src/Storage.d.ts +0 -0
  17. package/dist/packages/{monitor → extension-monitor}/src/index.d.ts +3 -5
  18. package/dist/packages/extension-redis/src/Redis.d.ts +98 -0
  19. package/dist/packages/extension-redis/src/index.d.ts +1 -0
  20. package/dist/packages/extension-sqlite/src/SQLite.d.ts +26 -0
  21. package/dist/packages/extension-sqlite/src/index.d.ts +1 -0
  22. package/dist/packages/extension-throttle/src/index.d.ts +24 -0
  23. package/dist/packages/{webhook → extension-webhook}/src/index.d.ts +5 -11
  24. package/dist/packages/provider/src/EventEmitter.d.ts +1 -1
  25. package/dist/packages/provider/src/HocuspocusCloudProvider.d.ts +11 -0
  26. package/dist/packages/provider/src/HocuspocusProvider.d.ts +116 -33
  27. package/dist/packages/provider/src/IncomingMessage.d.ts +9 -6
  28. package/dist/packages/provider/src/MessageReceiver.d.ts +3 -2
  29. package/dist/packages/provider/src/MessageSender.d.ts +4 -9
  30. package/dist/packages/provider/src/OutgoingMessage.d.ts +5 -4
  31. package/dist/packages/provider/src/OutgoingMessages/AuthenticationMessage.d.ts +7 -0
  32. package/dist/packages/provider/src/OutgoingMessages/UpdateMessage.d.ts +1 -2
  33. package/dist/packages/provider/src/index.d.ts +1 -1
  34. package/dist/packages/provider/src/types.d.ts +54 -2
  35. package/dist/packages/server/src/Connection.d.ts +27 -7
  36. package/dist/packages/server/src/Debugger.d.ts +14 -0
  37. package/dist/packages/server/src/Document.d.ts +11 -5
  38. package/dist/packages/server/src/Hocuspocus.d.ts +63 -17
  39. package/dist/packages/server/src/IncomingMessage.d.ts +11 -7
  40. package/dist/packages/server/src/MessageReceiver.d.ts +13 -0
  41. package/dist/packages/server/src/OutgoingMessage.d.ts +6 -0
  42. package/dist/packages/server/src/index.d.ts +6 -0
  43. package/dist/packages/server/src/types.d.ts +180 -26
  44. package/dist/{demos/backend/src/create-document.d.ts → playground/backend/src/default.d.ts} +0 -0
  45. package/dist/{demos → playground}/backend/src/express.d.ts +0 -0
  46. package/dist/{demos → playground}/backend/src/koa.d.ts +0 -0
  47. package/dist/{demos/backend/src/minimal.d.ts → playground/backend/src/load-document.d.ts} +0 -0
  48. package/dist/{demos → playground}/backend/src/monitor.d.ts +0 -0
  49. package/dist/{demos → playground}/backend/src/redis.d.ts +0 -0
  50. package/dist/{demos → playground}/backend/src/slow.d.ts +0 -0
  51. package/dist/{demos → playground}/backend/src/webhook.d.ts +0 -0
  52. package/dist/tests/extension-database/fetch.d.ts +1 -0
  53. package/dist/tests/extension-logger/onListen.d.ts +1 -0
  54. package/dist/tests/extension-redis/closeConnections.d.ts +1 -0
  55. package/dist/tests/extension-redis/getConnectionCount.d.ts +1 -0
  56. package/dist/tests/extension-redis/getDocumentsCount.d.ts +1 -0
  57. package/dist/tests/extension-redis/onAwarenessChange.d.ts +1 -0
  58. package/dist/tests/extension-redis/onChange.d.ts +1 -0
  59. package/dist/tests/extension-redis/onStoreDocument.d.ts +1 -0
  60. package/dist/tests/extension-throttle/configuration.d.ts +1 -0
  61. package/dist/tests/provider/configuration.d.ts +1 -0
  62. package/dist/tests/provider/observe.d.ts +1 -0
  63. package/dist/tests/provider/observeDeep.d.ts +1 -0
  64. package/dist/tests/provider/onAuthenticated.d.ts +1 -0
  65. package/dist/tests/provider/onAuthenticationFailed.d.ts +1 -0
  66. package/dist/tests/provider/onAwarenessChange.d.ts +1 -0
  67. package/dist/tests/provider/onAwarenessUpdate.d.ts +1 -0
  68. package/dist/tests/provider/onClose.d.ts +1 -0
  69. package/dist/tests/provider/onConnect.d.ts +1 -0
  70. package/dist/tests/provider/onDisconnect.d.ts +1 -0
  71. package/dist/tests/provider/onMessage.d.ts +1 -0
  72. package/dist/tests/provider/onOpen.d.ts +1 -0
  73. package/dist/tests/provider/onSynced.d.ts +1 -0
  74. package/dist/tests/server/address.d.ts +1 -0
  75. package/dist/tests/server/afterStoreDocument.d.ts +1 -0
  76. package/dist/tests/server/beforeHandleMessage.d.ts +1 -0
  77. package/dist/tests/server/closeConnections.d.ts +1 -0
  78. package/dist/tests/server/getConnectionsCount.d.ts +1 -0
  79. package/dist/tests/server/getDocumentName.d.ts +1 -0
  80. package/dist/tests/server/getDocumentsCount.d.ts +1 -0
  81. package/dist/tests/server/getMessageLogs.d.ts +1 -0
  82. package/dist/tests/server/listen.d.ts +1 -0
  83. package/dist/tests/server/onAuthenticate.d.ts +1 -0
  84. package/dist/tests/server/onAwarenessUpdate.d.ts +1 -0
  85. package/dist/tests/server/onChange.d.ts +1 -0
  86. package/dist/tests/server/onConfigure.d.ts +1 -0
  87. package/dist/tests/server/onConnect.d.ts +1 -0
  88. package/dist/tests/server/onDestroy.d.ts +1 -0
  89. package/dist/tests/server/onDisconnect.d.ts +1 -0
  90. package/dist/tests/server/onListen.d.ts +1 -0
  91. package/dist/tests/server/onLoadDocument.d.ts +1 -0
  92. package/dist/tests/server/onRequest.d.ts +1 -0
  93. package/dist/tests/server/onStoreDocument.d.ts +1 -0
  94. package/dist/tests/server/onUpgrade.d.ts +1 -0
  95. package/dist/tests/server/requiresAuthentication.d.ts +1 -0
  96. package/dist/tests/server/websocketError.d.ts +1 -0
  97. package/dist/tests/transformer/TiptapTransformer.d.ts +1 -0
  98. package/dist/tests/utils/createDirectory.d.ts +1 -0
  99. package/dist/tests/utils/flushRedis.d.ts +1 -0
  100. package/dist/tests/utils/index.d.ts +8 -0
  101. package/dist/tests/utils/newHocuspocus.d.ts +2 -0
  102. package/dist/tests/utils/newHocuspocusProvider.d.ts +3 -0
  103. package/dist/tests/utils/randomInteger.d.ts +1 -0
  104. package/dist/tests/utils/redisConnectionSettings.d.ts +4 -0
  105. package/dist/tests/utils/removeDirectory.d.ts +1 -0
  106. package/dist/tests/utils/retryableAssertion.d.ts +2 -0
  107. package/dist/tests/utils/sleep.d.ts +1 -0
  108. package/package.json +14 -10
  109. package/src/EventEmitter.ts +1 -1
  110. package/src/HocuspocusCloudProvider.ts +34 -0
  111. package/src/HocuspocusProvider.ts +410 -162
  112. package/src/IncomingMessage.ts +35 -11
  113. package/src/MessageReceiver.ts +56 -24
  114. package/src/MessageSender.ts +5 -17
  115. package/src/OutgoingMessage.ts +9 -9
  116. package/src/OutgoingMessages/AuthenticationMessage.ts +21 -0
  117. package/src/OutgoingMessages/AwarenessMessage.ts +1 -1
  118. package/src/OutgoingMessages/SyncStepOneMessage.ts +0 -1
  119. package/src/OutgoingMessages/UpdateMessage.ts +4 -4
  120. package/src/index.ts +1 -1
  121. package/src/types.ts +70 -3
  122. package/CHANGELOG.md +0 -64
  123. package/LICENSE.md +0 -21
  124. package/dist/packages/logger/src/index.d.ts +0 -13
  125. package/dist/packages/provider/src/utils/awarenessStatesToArray.d.ts +0 -4
  126. package/dist/packages/provider/src/utils/index.d.ts +0 -1
  127. package/dist/packages/redis/src/Redis.d.ts +0 -22
  128. package/dist/packages/redis/src/RedisCluster.d.ts +0 -4
  129. package/dist/packages/redis/src/index.d.ts +0 -2
  130. package/dist/packages/rocksdb/src/index.d.ts +0 -30
  131. package/dist/packages/server/src/CloseEvents.d.ts +0 -3
  132. package/dist/packages/throttle/src/index.d.ts +0 -28
  133. package/src/utils/awarenessStatesToArray.ts +0 -8
  134. package/src/utils/index.ts +0 -1
@@ -1,14 +1,14 @@
1
- // @ts-nocheck
2
1
  import * as Y from 'yjs'
3
2
  import * as bc from 'lib0/broadcastchannel'
4
3
  import * as time from 'lib0/time'
5
- import * as encoding from 'lib0/encoding'
6
4
  import { Awareness, removeAwarenessStates } from 'y-protocols/awareness'
7
5
  import * as mutex from 'lib0/mutex'
8
- import * as math from 'lib0/math'
9
6
  import * as url from 'lib0/url'
10
-
11
- import { CloseEvent, MessageEvent, OpenEvent } from 'ws'
7
+ import type { Event, CloseEvent, MessageEvent } from 'ws'
8
+ import { retry } from '@lifeomic/attempt'
9
+ import {
10
+ awarenessStatesToArray, Forbidden, Unauthorized, WsReadyStates,
11
+ } from '@hocuspocus/common'
12
12
  import EventEmitter from './EventEmitter'
13
13
  import { IncomingMessage } from './IncomingMessage'
14
14
  import { MessageReceiver } from './MessageReceiver'
@@ -16,56 +16,148 @@ import { MessageSender } from './MessageSender'
16
16
  import { SyncStepOneMessage } from './OutgoingMessages/SyncStepOneMessage'
17
17
  import { SyncStepTwoMessage } from './OutgoingMessages/SyncStepTwoMessage'
18
18
  import { QueryAwarenessMessage } from './OutgoingMessages/QueryAwarenessMessage'
19
+ import { AuthenticationMessage } from './OutgoingMessages/AuthenticationMessage'
19
20
  import { AwarenessMessage } from './OutgoingMessages/AwarenessMessage'
20
21
  import { UpdateMessage } from './OutgoingMessages/UpdateMessage'
21
- import { OutgoingMessage } from './OutgoingMessage'
22
- import awarenessStatesToArray from './utils/awarenessStatesToArray'
23
-
24
- export enum WebSocketStatus {
25
- Connecting = 'connecting',
26
- Connected = 'connected',
27
- Disconnected = 'disconnected',
28
- }
29
-
30
- export interface HocuspocusProviderOptions {
31
- url: string,
32
- name: string,
22
+ import {
23
+ ConstructableOutgoingMessage, onAuthenticationFailedParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatusParameters, onSyncedParameters, WebSocketStatus,
24
+ } from './types'
25
+ import { onAwarenessChangeParameters, onAwarenessUpdateParameters } from '.'
26
+
27
+ export type HocuspocusProviderConfiguration =
28
+ Required<Pick<CompleteHocuspocusProviderConfiguration, 'url' | 'name'>>
29
+ & Partial<CompleteHocuspocusProviderConfiguration>
30
+
31
+ export interface CompleteHocuspocusProviderConfiguration {
32
+ /**
33
+ * URL of your @hocuspocus/server instance
34
+ */
35
+ url: string,
36
+ /**
37
+ * The identifier/name of your document
38
+ */
39
+ name: string,
40
+ /**
41
+ * The actual Y.js document
42
+ */
33
43
  document: Y.Doc,
44
+ /**
45
+ * Pass `false` to start the connection manually.
46
+ */
34
47
  connect: boolean,
48
+ /**
49
+ * Pass false to disable broadcasting between browser tabs.
50
+ */
35
51
  broadcast: boolean,
52
+ /**
53
+ * An Awareness instance to keep the presence state of all clients.
54
+ */
36
55
  awareness: Awareness,
56
+ /**
57
+ * A token that’s sent to the backend for authentication purposes.
58
+ */
59
+ token: string | (() => string) | (() => Promise<string>) | null,
60
+ /**
61
+ * URL parameters that should be added.
62
+ */
37
63
  parameters: { [key: string]: any },
64
+ /**
65
+ * An optional WebSocket polyfill, for example for Node.js
66
+ */
38
67
  WebSocketPolyfill: any,
68
+ /**
69
+ * Force syncing the document in the defined interval.
70
+ */
39
71
  forceSyncInterval: false | number,
40
- reconnectTimeoutBase: number,
41
- maxReconnectTimeout: number,
72
+ /**
73
+ * Disconnect when no message is received for the defined amount of milliseconds.
74
+ */
42
75
  messageReconnectTimeout: number,
43
- onOpen: (event: OpenEvent) => void,
76
+ /**
77
+ * The delay between each attempt in milliseconds. You can provide a factor to have the delay grow exponentially.
78
+ */
79
+ delay: number,
80
+ /**
81
+ * The intialDelay is the amount of time to wait before making the first attempt. This option should typically be 0 since you typically want the first attempt to happen immediately.
82
+ */
83
+ initialDelay: number,
84
+ /**
85
+ * The factor option is used to grow the delay exponentially.
86
+ */
87
+ factor: number,
88
+ /**
89
+ * The maximum number of attempts or 0 if there is no limit on number of attempts.
90
+ */
91
+ maxAttempts: number,
92
+ /**
93
+ * minDelay is used to set a lower bound of delay when jitter is enabled. This property has no effect if jitter is disabled.
94
+ */
95
+ minDelay: number,
96
+ /**
97
+ * The maxDelay option is used to set an upper bound for the delay when factor is enabled. A value of 0 can be provided if there should be no upper bound when calculating delay.
98
+ */
99
+ maxDelay: number,
100
+ /**
101
+ * If jitter is true then the calculated delay will be a random integer value between minDelay and the calculated delay for the current iteration.
102
+ */
103
+ jitter: boolean,
104
+ /**
105
+ * A timeout in milliseconds. If timeout is non-zero then a timer is set using setTimeout. If the timeout is triggered then future attempts will be aborted.
106
+ */
107
+ timeout: number,
108
+ onAuthenticated: () => void,
109
+ onAuthenticationFailed: (data: onAuthenticationFailedParameters) => void,
110
+ onOpen: (data: onOpenParameters) => void,
44
111
  onConnect: () => void,
45
- onMessage: (event: MessageEvent) => void,
46
- onOutgoingMessage: (message: OutgoingMessage) => void,
47
- onStatus: (status: any) => void,
48
- onSynced: () => void,
49
- onDisconnect: (event: CloseEvent) => void,
50
- onClose: (event: CloseEvent) => void,
112
+ onMessage: (data: onMessageParameters) => void,
113
+ onOutgoingMessage: (data: onOutgoingMessageParameters) => void,
114
+ onStatus: (data: onStatusParameters) => void,
115
+ onSynced: (data: onSyncedParameters) => void,
116
+ onDisconnect: (data: onDisconnectParameters) => void,
117
+ onClose: (data: onCloseParameters) => void,
51
118
  onDestroy: () => void,
52
- onAwarenessChange: (states: any) => void,
53
- debug: boolean,
119
+ onAwarenessUpdate: (data: onAwarenessUpdateParameters) => void,
120
+ onAwarenessChange: (data: onAwarenessChangeParameters) => void,
121
+ /**
122
+ * Don’t output any warnings.
123
+ */
124
+ quiet: boolean,
54
125
  }
55
126
 
56
127
  export class HocuspocusProvider extends EventEmitter {
57
- public options: HocuspocusProviderOptions = {
58
- url: '',
128
+ public configuration: CompleteHocuspocusProviderConfiguration = {
59
129
  name: '',
130
+ url: '',
131
+ // @ts-ignore
132
+ document: undefined,
133
+ // @ts-ignore
134
+ awareness: undefined,
135
+ WebSocketPolyfill: undefined,
136
+ token: null,
60
137
  parameters: {},
61
- debug: false,
62
138
  connect: true,
63
139
  broadcast: true,
64
140
  forceSyncInterval: false,
65
- reconnectTimeoutBase: 1200,
66
- maxReconnectTimeout: 2500,
67
141
  // TODO: this should depend on awareness.outdatedTime
68
142
  messageReconnectTimeout: 30000,
143
+ // 1 second
144
+ delay: 1000,
145
+ // instant
146
+ initialDelay: 0,
147
+ // double the delay each time
148
+ factor: 2,
149
+ // unlimited retries
150
+ maxAttempts: 0,
151
+ // wait at least 1 second
152
+ minDelay: 1000,
153
+ // at least every 30 seconds
154
+ maxDelay: 30000,
155
+ // randomize
156
+ jitter: true,
157
+ // retry forever
158
+ timeout: 0,
159
+ onAuthenticated: () => null,
160
+ onAuthenticationFailed: () => null,
69
161
  onOpen: () => null,
70
162
  onConnect: () => null,
71
163
  onMessage: () => null,
@@ -75,23 +167,25 @@ export class HocuspocusProvider extends EventEmitter {
75
167
  onDisconnect: () => null,
76
168
  onClose: () => null,
77
169
  onDestroy: () => null,
170
+ onAwarenessUpdate: () => null,
78
171
  onAwarenessChange: () => null,
172
+ quiet: false,
79
173
  }
80
174
 
81
- awareness: Awareness
82
-
83
175
  subscribedToBroadcastChannel = false
84
176
 
85
- webSocket: any = null
177
+ webSocket: WebSocket | null = null
86
178
 
87
179
  shouldConnect = true
88
180
 
89
- status: WebSocketStatus = WebSocketStatus.Disconnected
90
-
91
- failedConnectionAttempts = 0
181
+ status = WebSocketStatus.Disconnected
92
182
 
93
183
  isSynced = false
94
184
 
185
+ unsyncedChanges = 0
186
+
187
+ isAuthenticated = false
188
+
95
189
  lastMessageReceived = 0
96
190
 
97
191
  mux = mutex.createMutex()
@@ -101,78 +195,210 @@ export class HocuspocusProvider extends EventEmitter {
101
195
  connectionChecker: null,
102
196
  }
103
197
 
104
- constructor(options: Partial<HocuspocusProviderOptions> = {}) {
105
- super()
106
-
107
- this.setOptions(options)
198
+ connectionAttempt: {
199
+ resolve: (value?: any) => void
200
+ reject: (reason?: any) => void
201
+ } | null = null
108
202
 
109
- this.options.document = options.document ? options.document : new Y.Doc()
110
- this.options.awareness = options.awareness ? options.awareness : new Awareness(this.document)
111
- this.options.WebSocketPolyfill = options.WebSocketPolyfill ? options.WebSocketPolyfill : WebSocket
112
- this.shouldConnect = options.connect !== undefined ? options.connect : this.shouldConnect
113
-
114
- this.on('open', this.options.onOpen)
115
- this.on('connect', this.options.onConnect)
116
- this.on('message', this.options.onMessage)
117
- this.on('outgoingMessage', this.options.onOutgoingMessage)
118
- this.on('synced', this.options.onSynced)
119
- this.on('status', this.options.onStatus)
120
- this.on('disconnect', this.options.onDisconnect)
121
- this.on('close', this.options.onClose)
122
- this.on('destroy', this.options.onDestroy)
123
- this.on('awarenessChange', this.options.onAwarenessChange)
203
+ constructor(configuration: HocuspocusProviderConfiguration) {
204
+ super()
205
+ this.setConfiguration(configuration)
206
+
207
+ this.configuration.document = configuration.document ? configuration.document : new Y.Doc()
208
+ this.configuration.awareness = configuration.awareness ? configuration.awareness : new Awareness(this.document)
209
+ this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket
210
+
211
+ this.on('open', this.configuration.onOpen)
212
+ this.on('authenticated', this.configuration.onAuthenticated)
213
+ this.on('authenticationFailed', this.configuration.onAuthenticationFailed)
214
+ this.on('connect', this.configuration.onConnect)
215
+ this.on('message', this.configuration.onMessage)
216
+ this.on('outgoingMessage', this.configuration.onOutgoingMessage)
217
+ this.on('synced', this.configuration.onSynced)
218
+ this.on('status', this.configuration.onStatus)
219
+ this.on('disconnect', this.configuration.onDisconnect)
220
+ this.on('close', this.configuration.onClose)
221
+ this.on('destroy', this.configuration.onDestroy)
222
+ this.on('awarenessUpdate', this.configuration.onAwarenessUpdate)
223
+ this.on('awarenessChange', this.configuration.onAwarenessChange)
224
+
225
+ this.awareness.on('update', () => {
226
+ this.emit('awarenessUpdate', { states: awarenessStatesToArray(this.awareness.getStates()) })
227
+ })
124
228
 
125
229
  this.awareness.on('change', () => {
126
- this.emit('awarenessChange', {
127
- states: awarenessStatesToArray(this.awareness.getStates()),
128
- })
230
+ this.emit('awarenessChange', { states: awarenessStatesToArray(this.awareness.getStates()) })
129
231
  })
130
232
 
233
+ this.document.on('update', this.documentUpdateHandler.bind(this))
234
+ this.awareness.on('update', this.awarenessUpdateHandler.bind(this))
235
+ this.registerEventListeners()
236
+
131
237
  this.intervals.connectionChecker = setInterval(
132
238
  this.checkConnection.bind(this),
133
- this.options.messageReconnectTimeout / 10,
239
+ this.configuration.messageReconnectTimeout / 10,
134
240
  )
135
241
 
136
- this.document.on('update', this.documentUpdateHandler.bind(this))
137
- this.awareness.on('update', this.awarenessUpdateHandler.bind(this))
138
- this.registerBeforeUnloadEventListener()
139
-
140
- if (this.options.forceSyncInterval) {
242
+ if (this.configuration.forceSyncInterval) {
141
243
  this.intervals.forceSync = setInterval(
142
244
  this.forceSync.bind(this),
143
- this.options.forceSyncInterval,
245
+ this.configuration.forceSyncInterval,
144
246
  )
145
247
  }
146
248
 
147
- if (this.options.connect) {
148
- this.connect()
249
+ if (typeof configuration.connect !== 'undefined') {
250
+ this.shouldConnect = configuration.connect
251
+ }
252
+
253
+ if (!this.shouldConnect) {
254
+ return
255
+ }
256
+
257
+ this.connect()
258
+ }
259
+
260
+ public setConfiguration(configuration: Partial<HocuspocusProviderConfiguration> = {}): void {
261
+ this.configuration = { ...this.configuration, ...configuration }
262
+ }
263
+
264
+ boundConnect = this.connect.bind(this)
265
+
266
+ cancelWebsocketRetry?: () => void
267
+
268
+ async connect() {
269
+ if (this.status === WebSocketStatus.Connected) {
270
+ return
271
+ }
272
+
273
+ // Always cancel any previously initiated connection retryer instances
274
+ if (this.cancelWebsocketRetry) {
275
+ this.cancelWebsocketRetry()
276
+ this.cancelWebsocketRetry = undefined
277
+ }
278
+
279
+ this.unsyncedChanges = 0 // set to 0 in case we got reconnected
280
+ this.shouldConnect = true
281
+ this.subscribeToBroadcastChannel()
282
+
283
+ const abortableRetry = () => {
284
+ let cancelAttempt = false
285
+
286
+ const retryPromise = retry(this.createWebSocketConnection.bind(this), {
287
+ delay: this.configuration.delay,
288
+ initialDelay: this.configuration.initialDelay,
289
+ factor: this.configuration.factor,
290
+ maxAttempts: this.configuration.maxAttempts,
291
+ minDelay: this.configuration.minDelay,
292
+ maxDelay: this.configuration.maxDelay,
293
+ jitter: this.configuration.jitter,
294
+ timeout: this.configuration.timeout,
295
+ beforeAttempt: context => {
296
+ if (!this.shouldConnect || cancelAttempt) {
297
+ context.abort()
298
+ }
299
+ },
300
+ }).catch((error: any) => {
301
+ // If we aborted the connection attempt then don’t throw an error
302
+ // ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
303
+ if (error && error.code !== 'ATTEMPT_ABORTED') {
304
+ throw error
305
+ }
306
+ })
307
+
308
+ return {
309
+ retryPromise,
310
+ cancelFunc: () => {
311
+ cancelAttempt = true
312
+ },
313
+ }
149
314
  }
315
+
316
+ const { retryPromise, cancelFunc } = abortableRetry()
317
+ this.cancelWebsocketRetry = cancelFunc
318
+
319
+ return retryPromise
150
320
  }
151
321
 
152
- public setOptions(options: Partial<HocuspocusProviderOptions> = {}): void {
153
- this.options = { ...this.options, ...options }
322
+ createWebSocketConnection() {
323
+ return new Promise((resolve, reject) => {
324
+ if (this.webSocket) {
325
+ this.webSocket.close()
326
+ this.webSocket = null
327
+ }
328
+
329
+ // Init the WebSocket connection
330
+ const ws = new this.configuration.WebSocketPolyfill(this.url)
331
+ ws.binaryType = 'arraybuffer'
332
+ ws.onmessage = this.onMessage.bind(this)
333
+ ws.onclose = this.onClose.bind(this)
334
+ ws.onopen = this.onOpen.bind(this)
335
+ ws.onerror = (err: any) => {
336
+ reject(err)
337
+ }
338
+ this.webSocket = ws
339
+
340
+ // Reset the status
341
+ this.synced = false
342
+ this.status = WebSocketStatus.Connecting
343
+ this.emit('status', { status: WebSocketStatus.Connecting })
344
+
345
+ // Store resolve/reject for later use
346
+ this.connectionAttempt = {
347
+ resolve,
348
+ reject,
349
+ }
350
+ })
351
+ }
352
+
353
+ resolveConnectionAttempt() {
354
+ this.connectionAttempt?.resolve()
355
+ this.connectionAttempt = null
356
+
357
+ this.status = WebSocketStatus.Connected
358
+ this.emit('status', { status: WebSocketStatus.Connected })
359
+ this.emit('connect')
360
+ }
361
+
362
+ stopConnectionAttempt() {
363
+ this.connectionAttempt = null
364
+ }
365
+
366
+ rejectConnectionAttempt() {
367
+ this.connectionAttempt?.reject()
368
+ this.connectionAttempt = null
154
369
  }
155
370
 
156
371
  get document() {
157
- return this.options.document
372
+ return this.configuration.document
158
373
  }
159
374
 
160
375
  get awareness() {
161
- return this.options.awareness
376
+ return this.configuration.awareness
377
+ }
378
+
379
+ get hasUnsyncedChanges() {
380
+ return this.unsyncedChanges > 0
162
381
  }
163
382
 
164
383
  checkConnection() {
384
+ // Don’t check the connection when it’s not even established
165
385
  if (this.status !== WebSocketStatus.Connected) {
166
386
  return
167
387
  }
168
388
 
169
- if (this.options.messageReconnectTimeout >= time.getUnixTime() - this.lastMessageReceived) {
389
+ // Don’t close then connection while waiting for the first message
390
+ if (!this.lastMessageReceived) {
391
+ return
392
+ }
393
+
394
+ // Don’t close the connection when a message was received recently
395
+ if (this.configuration.messageReconnectTimeout >= time.getUnixTime() - this.lastMessageReceived) {
170
396
  return
171
397
  }
172
398
 
173
399
  // No message received in a long time, not even your own
174
400
  // Awareness updates, which are updated every 15 seconds.
175
- this.webSocket.close()
401
+ this.webSocket?.close()
176
402
  }
177
403
 
178
404
  forceSync() {
@@ -183,14 +409,19 @@ export class HocuspocusProvider extends EventEmitter {
183
409
  this.send(SyncStepOneMessage, { document: this.document })
184
410
  }
185
411
 
186
- registerBeforeUnloadEventListener() {
412
+ boundBeforeUnload = this.beforeUnload.bind(this)
413
+
414
+ beforeUnload() {
415
+ removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload')
416
+ }
417
+
418
+ registerEventListeners() {
187
419
  if (typeof window === 'undefined') {
188
420
  return
189
421
  }
190
422
 
191
- window.addEventListener('beforeunload', () => {
192
- removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload')
193
- })
423
+ window.addEventListener('online', this.boundConnect)
424
+ window.addEventListener('beforeunload', this.boundBeforeUnload)
194
425
  }
195
426
 
196
427
  documentUpdateHandler(update: Uint8Array, origin: any) {
@@ -198,6 +429,7 @@ export class HocuspocusProvider extends EventEmitter {
198
429
  return
199
430
  }
200
431
 
432
+ this.unsyncedChanges += 1
201
433
  this.send(UpdateMessage, { update }, true)
202
434
  }
203
435
 
@@ -210,19 +442,32 @@ export class HocuspocusProvider extends EventEmitter {
210
442
  }, true)
211
443
  }
212
444
 
445
+ permissionDeniedHandler(reason: string) {
446
+ this.emit('authenticationFailed', { reason })
447
+ this.isAuthenticated = false
448
+ this.shouldConnect = false
449
+ }
450
+
451
+ authenticatedHandler() {
452
+ this.isAuthenticated = true
453
+
454
+ this.emit('authenticated')
455
+ this.startSync()
456
+ }
457
+
213
458
  // Ensure that the URL always ends with /
214
459
  get serverUrl() {
215
- while (this.options.url[this.options.url.length - 1] === '/') {
216
- return this.options.url.slice(0, this.options.url.length - 1)
460
+ while (this.configuration.url[this.configuration.url.length - 1] === '/') {
461
+ return this.configuration.url.slice(0, this.configuration.url.length - 1)
217
462
  }
218
463
 
219
- return this.options.url
464
+ return this.configuration.url
220
465
  }
221
466
 
222
467
  get url() {
223
- const encodedParams = url.encodeQueryParams(this.options.parameters)
468
+ const encodedParams = url.encodeQueryParams(this.configuration.parameters)
224
469
 
225
- return `${this.serverUrl}/${this.options.name}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`
470
+ return `${this.serverUrl}/${this.configuration.name}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`
226
471
  }
227
472
 
228
473
  get synced(): boolean {
@@ -239,13 +484,8 @@ export class HocuspocusProvider extends EventEmitter {
239
484
  this.emit('sync', { state })
240
485
  }
241
486
 
242
- connect() {
243
- this.shouldConnect = true
244
-
245
- if (this.status !== WebSocketStatus.Connected) {
246
- this.createWebSocketConnection()
247
- this.subscribeToBroadcastChannel()
248
- }
487
+ get isAuthenticationRequired(): boolean {
488
+ return !!this.configuration.token && !this.isAuthenticated
249
489
  }
250
490
 
251
491
  disconnect() {
@@ -263,34 +503,29 @@ export class HocuspocusProvider extends EventEmitter {
263
503
  }
264
504
  }
265
505
 
266
- createWebSocketConnection() {
267
- if (this.webSocket !== null) {
506
+ async onOpen(event: Event) {
507
+ this.emit('open', { event })
508
+
509
+ if (this.isAuthenticationRequired) {
510
+ this.send(AuthenticationMessage, {
511
+ token: await this.getToken(),
512
+ })
268
513
  return
269
514
  }
270
515
 
271
- this.webSocket = new this.options.WebSocketPolyfill(this.url)
272
- this.webSocket.binaryType = 'arraybuffer'
273
-
274
- this.status = WebSocketStatus.Connecting
275
- this.synced = false
276
-
277
- this.webSocket.onmessage = this.onMessage.bind(this)
278
- this.webSocket.onclose = this.onClose.bind(this)
279
- this.webSocket.onopen = this.onOpen.bind(this)
280
-
281
- this.emit('status', { status: 'connecting' })
516
+ this.startSync()
282
517
  }
283
518
 
284
- onOpen(event: OpenEvent) {
285
- this.emit('open', { event })
286
- }
519
+ async getToken() {
520
+ if (typeof this.configuration.token === 'function') {
521
+ const token = await this.configuration.token()
522
+ return token
523
+ }
287
524
 
288
- webSocketConnectionEstablished() {
289
- this.failedConnectionAttempts = 0
290
- this.status = WebSocketStatus.Connected
291
- this.emit('status', { status: 'connected' })
292
- this.emit('connect')
525
+ return this.configuration.token
526
+ }
293
527
 
528
+ startSync() {
294
529
  this.send(SyncStepOneMessage, { document: this.document })
295
530
 
296
531
  if (this.awareness.getLocalState() !== null) {
@@ -301,14 +536,12 @@ export class HocuspocusProvider extends EventEmitter {
301
536
  }
302
537
  }
303
538
 
304
- send(Message: OutgoingMessage, args: any, broadcast = false) {
539
+ send(Message: ConstructableOutgoingMessage, args: any, broadcast = false) {
305
540
  if (broadcast) {
306
- this.mux(() => {
307
- this.broadcast(Message, args)
308
- })
541
+ this.mux(() => { this.broadcast(Message, args) })
309
542
  }
310
543
 
311
- if (this.status === WebSocketStatus.Connected) {
544
+ if (this.webSocket?.readyState === WsReadyStates.Open) {
312
545
  const messageSender = new MessageSender(Message, args)
313
546
 
314
547
  this.emit('outgoingMessage', { message: messageSender.message })
@@ -317,9 +550,7 @@ export class HocuspocusProvider extends EventEmitter {
317
550
  }
318
551
 
319
552
  onMessage(event: MessageEvent) {
320
- if (this.status !== WebSocketStatus.Connected) {
321
- this.webSocketConnectionEstablished()
322
- }
553
+ this.resolveConnectionAttempt()
323
554
 
324
555
  this.lastMessageReceived = time.getUnixTime()
325
556
 
@@ -327,22 +558,17 @@ export class HocuspocusProvider extends EventEmitter {
327
558
 
328
559
  this.emit('message', { event, message })
329
560
 
330
- const encoder = new MessageReceiver(message, this).apply(this)
331
-
332
- // TODO: What’s that doing?
333
- // if (encoding.length(encoder) > 1) {
334
- // this.send(encoding.toUint8Array(encoder))
335
- // }
561
+ new MessageReceiver(message).apply(this)
336
562
  }
337
563
 
338
564
  onClose(event: CloseEvent) {
339
565
  this.emit('close', { event })
340
566
 
341
567
  this.webSocket = null
568
+ this.isAuthenticated = false
569
+ this.synced = false
342
570
 
343
571
  if (this.status === WebSocketStatus.Connected) {
344
- this.synced = false
345
-
346
572
  // update awareness (all users except local left)
347
573
  removeAwarenessStates(
348
574
  this.awareness,
@@ -351,29 +577,46 @@ export class HocuspocusProvider extends EventEmitter {
351
577
  )
352
578
 
353
579
  this.status = WebSocketStatus.Disconnected
354
- this.emit('status', { status: 'disconnected' })
580
+ this.emit('status', { status: WebSocketStatus.Disconnected })
355
581
  this.emit('disconnect', { event })
356
- } else {
357
- this.failedConnectionAttempts += 1
358
582
  }
359
583
 
360
- if (this.shouldConnect) {
361
- const wait = math.round(math.min(
362
- math.log10(this.failedConnectionAttempts + 1) * this.options.reconnectTimeoutBase,
363
- this.options.maxReconnectTimeout,
364
- ))
584
+ if (event.code === Unauthorized.code) {
585
+ if (!this.configuration.quiet) {
586
+ console.warn('[HocuspocusProvider] An authentication token is required, but you didn’t send one. Try adding a `token` to your HocuspocusProvider configuration. Won’t try again.')
587
+ }
588
+
589
+ this.shouldConnect = false
590
+ }
365
591
 
366
- this.log(`[close] Reconnecting in ${wait}ms …`)
367
- setTimeout(this.createWebSocketConnection.bind(this), wait)
592
+ if (event.code === Forbidden.code) {
593
+ if (!this.configuration.quiet) {
594
+ console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.')
595
+ }
596
+ }
597
+
598
+ if (this.connectionAttempt) {
599
+ // That connection attempt failed.
600
+ this.rejectConnectionAttempt()
601
+ } else if (this.shouldConnect) {
602
+ // The connection was closed by the server. Let’s just try again.
603
+ this.connect()
604
+ }
368
605
 
606
+ // If we’ll reconnect, we’re done for now.
607
+ if (this.shouldConnect) {
369
608
  return
370
609
  }
371
610
 
372
- if (this.status !== WebSocketStatus.Disconnected) {
373
- this.status = WebSocketStatus.Disconnected
374
- this.emit('status', { status: 'disconnected' })
375
- this.emit('disconnect', { event })
611
+ // The status is set correctly already.
612
+ if (this.status === WebSocketStatus.Disconnected) {
613
+ return
376
614
  }
615
+
616
+ // Let’s update the connection status.
617
+ this.status = WebSocketStatus.Disconnected
618
+ this.emit('status', { status: WebSocketStatus.Disconnected })
619
+ this.emit('disconnect', { event })
377
620
  }
378
621
 
379
622
  destroy() {
@@ -385,33 +628,46 @@ export class HocuspocusProvider extends EventEmitter {
385
628
 
386
629
  clearInterval(this.intervals.connectionChecker)
387
630
 
631
+ removeAwarenessStates(this.awareness, [this.document.clientID], 'provider destroy')
632
+
633
+ // If there is still a connection attempt outstanding then we should stop
634
+ // it before calling disconnect, otherwise it will be rejected in the onClose
635
+ // handler and trigger a retry
636
+ this.stopConnectionAttempt()
637
+
388
638
  this.disconnect()
389
639
 
390
640
  this.awareness.off('update', this.awarenessUpdateHandler)
391
641
  this.document.off('update', this.documentUpdateHandler)
392
642
 
393
643
  this.removeAllListeners()
644
+
645
+ if (typeof window === 'undefined') {
646
+ return
647
+ }
648
+
649
+ window.removeEventListener('online', this.boundConnect)
650
+ window.removeEventListener('beforeunload', this.boundBeforeUnload)
394
651
  }
395
652
 
396
653
  get broadcastChannel() {
397
- return `${this.serverUrl}/${this.options.name}`
654
+ return `${this.serverUrl}/${this.configuration.name}`
398
655
  }
399
656
 
657
+ boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this)
658
+
400
659
  broadcastChannelSubscriber(data: ArrayBuffer) {
401
660
  this.mux(() => {
402
661
  const message = new IncomingMessage(data)
403
- const encoder = new MessageReceiver(message, this).apply(this, false)
404
-
405
- // TODO: What’s that doing?
406
- // if (encoding.length(encoder) > 1) {
407
- // this.broadcast(encoding.toUint8Array(encoder))
408
- // }
662
+ new MessageReceiver(message)
663
+ .setBroadcasted(true)
664
+ .apply(this, false)
409
665
  })
410
666
  }
411
667
 
412
668
  subscribeToBroadcastChannel() {
413
669
  if (!this.subscribedToBroadcastChannel) {
414
- bc.subscribe(this.broadcastChannel, this.broadcastChannelSubscriber.bind(this))
670
+ bc.subscribe(this.broadcastChannel, this.boundBroadcastChannelSubscriber)
415
671
  this.subscribedToBroadcastChannel = true
416
672
  }
417
673
 
@@ -432,13 +688,13 @@ export class HocuspocusProvider extends EventEmitter {
432
688
  }, true)
433
689
 
434
690
  if (this.subscribedToBroadcastChannel) {
435
- bc.unsubscribe(this.broadcastChannel, this.broadcastChannelSubscriber.bind(this))
691
+ bc.unsubscribe(this.broadcastChannel, this.boundBroadcastChannelSubscriber)
436
692
  this.subscribedToBroadcastChannel = false
437
693
  }
438
694
  }
439
695
 
440
- broadcast(Message: OutgoingMessage, args: any) {
441
- if (!this.options.broadcast) {
696
+ broadcast(Message: ConstructableOutgoingMessage, args?: any) {
697
+ if (!this.configuration.broadcast) {
442
698
  return
443
699
  }
444
700
 
@@ -449,14 +705,6 @@ export class HocuspocusProvider extends EventEmitter {
449
705
  new MessageSender(Message, args).broadcast(this.broadcastChannel)
450
706
  }
451
707
 
452
- log(message: string): void {
453
- if (!this.options.debug) {
454
- return
455
- }
456
-
457
- console.log(message)
458
- }
459
-
460
708
  setAwarenessField(key: string, value: any) {
461
709
  this.awareness.setLocalStateField(key, value)
462
710
  }