@hocuspocus/provider 1.0.0-alpha.8 → 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 +378 -185
  2. package/dist/hocuspocus-provider.cjs.map +1 -1
  3. package/dist/hocuspocus-provider.esm.js +376 -181
  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 +117 -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 +415 -159
  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 -56
  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,54 +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
+ */
51
+ broadcast: boolean,
52
+ /**
53
+ * An Awareness instance to keep the presence state of all clients.
54
+ */
35
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
+ */
36
63
  parameters: { [key: string]: any },
64
+ /**
65
+ * An optional WebSocket polyfill, for example for Node.js
66
+ */
37
67
  WebSocketPolyfill: any,
68
+ /**
69
+ * Force syncing the document in the defined interval.
70
+ */
38
71
  forceSyncInterval: false | number,
39
- reconnectTimeoutBase: number,
40
- maxReconnectTimeout: number,
72
+ /**
73
+ * Disconnect when no message is received for the defined amount of milliseconds.
74
+ */
41
75
  messageReconnectTimeout: number,
42
- 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,
43
111
  onConnect: () => void,
44
- onMessage: (event: MessageEvent) => void,
45
- onOutgoingMessage: (message: OutgoingMessage) => void,
46
- onStatus: (status: any) => void,
47
- onSynced: () => void,
48
- onDisconnect: (event: CloseEvent) => void,
49
- 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,
50
118
  onDestroy: () => void,
51
- onAwarenessChange: (states: any) => void,
52
- debug: boolean,
119
+ onAwarenessUpdate: (data: onAwarenessUpdateParameters) => void,
120
+ onAwarenessChange: (data: onAwarenessChangeParameters) => void,
121
+ /**
122
+ * Don’t output any warnings.
123
+ */
124
+ quiet: boolean,
53
125
  }
54
126
 
55
127
  export class HocuspocusProvider extends EventEmitter {
56
- public options: HocuspocusProviderOptions = {
57
- url: '',
128
+ public configuration: CompleteHocuspocusProviderConfiguration = {
58
129
  name: '',
130
+ url: '',
131
+ // @ts-ignore
132
+ document: undefined,
133
+ // @ts-ignore
134
+ awareness: undefined,
135
+ WebSocketPolyfill: undefined,
136
+ token: null,
59
137
  parameters: {},
60
- debug: false,
61
138
  connect: true,
139
+ broadcast: true,
62
140
  forceSyncInterval: false,
63
- reconnectTimeoutBase: 1200,
64
- maxReconnectTimeout: 2500,
65
141
  // TODO: this should depend on awareness.outdatedTime
66
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,
67
161
  onOpen: () => null,
68
162
  onConnect: () => null,
69
163
  onMessage: () => null,
@@ -73,23 +167,25 @@ export class HocuspocusProvider extends EventEmitter {
73
167
  onDisconnect: () => null,
74
168
  onClose: () => null,
75
169
  onDestroy: () => null,
170
+ onAwarenessUpdate: () => null,
76
171
  onAwarenessChange: () => null,
172
+ quiet: false,
77
173
  }
78
174
 
79
- awareness: Awareness
80
-
81
175
  subscribedToBroadcastChannel = false
82
176
 
83
- webSocket: any = null
177
+ webSocket: WebSocket | null = null
84
178
 
85
179
  shouldConnect = true
86
180
 
87
- status: WebSocketStatus = WebSocketStatus.Disconnected
88
-
89
- failedConnectionAttempts = 0
181
+ status = WebSocketStatus.Disconnected
90
182
 
91
183
  isSynced = false
92
184
 
185
+ unsyncedChanges = 0
186
+
187
+ isAuthenticated = false
188
+
93
189
  lastMessageReceived = 0
94
190
 
95
191
  mux = mutex.createMutex()
@@ -99,78 +195,210 @@ export class HocuspocusProvider extends EventEmitter {
99
195
  connectionChecker: null,
100
196
  }
101
197
 
102
- constructor(options: Partial<HocuspocusProviderOptions> = {}) {
103
- super()
104
-
105
- this.setOptions(options)
198
+ connectionAttempt: {
199
+ resolve: (value?: any) => void
200
+ reject: (reason?: any) => void
201
+ } | null = null
106
202
 
107
- this.options.document = options.document ? options.document : new Y.Doc()
108
- this.options.awareness = options.awareness ? options.awareness : new Awareness(this.document)
109
- this.options.WebSocketPolyfill = options.WebSocketPolyfill ? options.WebSocketPolyfill : WebSocket
110
- this.shouldConnect = options.connect !== undefined ? options.connect : this.shouldConnect
111
-
112
- this.on('open', this.options.onOpen)
113
- this.on('connect', this.options.onConnect)
114
- this.on('message', this.options.onMessage)
115
- this.on('outgoingMessage', this.options.onOutgoingMessage)
116
- this.on('synced', this.options.onSynced)
117
- this.on('status', this.options.onStatus)
118
- this.on('disconnect', this.options.onDisconnect)
119
- this.on('close', this.options.onClose)
120
- this.on('destroy', this.options.onDestroy)
121
- 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
+ })
122
228
 
123
229
  this.awareness.on('change', () => {
124
- this.emit('awarenessChange', {
125
- states: awarenessStatesToArray(this.awareness.getStates()),
126
- })
230
+ this.emit('awarenessChange', { states: awarenessStatesToArray(this.awareness.getStates()) })
127
231
  })
128
232
 
233
+ this.document.on('update', this.documentUpdateHandler.bind(this))
234
+ this.awareness.on('update', this.awarenessUpdateHandler.bind(this))
235
+ this.registerEventListeners()
236
+
129
237
  this.intervals.connectionChecker = setInterval(
130
238
  this.checkConnection.bind(this),
131
- this.options.messageReconnectTimeout / 10,
239
+ this.configuration.messageReconnectTimeout / 10,
132
240
  )
133
241
 
134
- this.document.on('update', this.documentUpdateHandler.bind(this))
135
- this.awareness.on('update', this.awarenessUpdateHandler.bind(this))
136
- this.registerBeforeUnloadEventListener()
137
-
138
- if (this.options.forceSyncInterval) {
242
+ if (this.configuration.forceSyncInterval) {
139
243
  this.intervals.forceSync = setInterval(
140
244
  this.forceSync.bind(this),
141
- this.options.forceSyncInterval,
245
+ this.configuration.forceSyncInterval,
142
246
  )
143
247
  }
144
248
 
145
- if (this.options.connect) {
146
- 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
+ }
147
314
  }
315
+
316
+ const { retryPromise, cancelFunc } = abortableRetry()
317
+ this.cancelWebsocketRetry = cancelFunc
318
+
319
+ return retryPromise
148
320
  }
149
321
 
150
- public setOptions(options: Partial<HocuspocusProviderOptions> = {}): void {
151
- 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
152
369
  }
153
370
 
154
371
  get document() {
155
- return this.options.document
372
+ return this.configuration.document
156
373
  }
157
374
 
158
375
  get awareness() {
159
- return this.options.awareness
376
+ return this.configuration.awareness
377
+ }
378
+
379
+ get hasUnsyncedChanges() {
380
+ return this.unsyncedChanges > 0
160
381
  }
161
382
 
162
383
  checkConnection() {
384
+ // Don’t check the connection when it’s not even established
163
385
  if (this.status !== WebSocketStatus.Connected) {
164
386
  return
165
387
  }
166
388
 
167
- 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) {
168
396
  return
169
397
  }
170
398
 
171
399
  // No message received in a long time, not even your own
172
400
  // Awareness updates, which are updated every 15 seconds.
173
- this.webSocket.close()
401
+ this.webSocket?.close()
174
402
  }
175
403
 
176
404
  forceSync() {
@@ -181,14 +409,19 @@ export class HocuspocusProvider extends EventEmitter {
181
409
  this.send(SyncStepOneMessage, { document: this.document })
182
410
  }
183
411
 
184
- registerBeforeUnloadEventListener() {
412
+ boundBeforeUnload = this.beforeUnload.bind(this)
413
+
414
+ beforeUnload() {
415
+ removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload')
416
+ }
417
+
418
+ registerEventListeners() {
185
419
  if (typeof window === 'undefined') {
186
420
  return
187
421
  }
188
422
 
189
- window.addEventListener('beforeunload', () => {
190
- removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload')
191
- })
423
+ window.addEventListener('online', this.boundConnect)
424
+ window.addEventListener('beforeunload', this.boundBeforeUnload)
192
425
  }
193
426
 
194
427
  documentUpdateHandler(update: Uint8Array, origin: any) {
@@ -196,6 +429,7 @@ export class HocuspocusProvider extends EventEmitter {
196
429
  return
197
430
  }
198
431
 
432
+ this.unsyncedChanges += 1
199
433
  this.send(UpdateMessage, { update }, true)
200
434
  }
201
435
 
@@ -208,19 +442,32 @@ export class HocuspocusProvider extends EventEmitter {
208
442
  }, true)
209
443
  }
210
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
+
211
458
  // Ensure that the URL always ends with /
212
459
  get serverUrl() {
213
- while (this.options.url[this.options.url.length - 1] === '/') {
214
- 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)
215
462
  }
216
463
 
217
- return this.options.url
464
+ return this.configuration.url
218
465
  }
219
466
 
220
467
  get url() {
221
- const encodedParams = url.encodeQueryParams(this.options.parameters)
468
+ const encodedParams = url.encodeQueryParams(this.configuration.parameters)
222
469
 
223
- return `${this.serverUrl}/${this.options.name}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`
470
+ return `${this.serverUrl}/${this.configuration.name}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`
224
471
  }
225
472
 
226
473
  get synced(): boolean {
@@ -237,13 +484,8 @@ export class HocuspocusProvider extends EventEmitter {
237
484
  this.emit('sync', { state })
238
485
  }
239
486
 
240
- connect() {
241
- this.shouldConnect = true
242
-
243
- if (this.status !== WebSocketStatus.Connected) {
244
- this.createWebSocketConnection()
245
- this.subscribeToBroadcastChannel()
246
- }
487
+ get isAuthenticationRequired(): boolean {
488
+ return !!this.configuration.token && !this.isAuthenticated
247
489
  }
248
490
 
249
491
  disconnect() {
@@ -261,34 +503,29 @@ export class HocuspocusProvider extends EventEmitter {
261
503
  }
262
504
  }
263
505
 
264
- createWebSocketConnection() {
265
- 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
+ })
266
513
  return
267
514
  }
268
515
 
269
- this.webSocket = new this.options.WebSocketPolyfill(this.url)
270
- this.webSocket.binaryType = 'arraybuffer'
271
-
272
- this.status = WebSocketStatus.Connecting
273
- this.synced = false
274
-
275
- this.webSocket.onmessage = this.onMessage.bind(this)
276
- this.webSocket.onclose = this.onClose.bind(this)
277
- this.webSocket.onopen = this.onOpen.bind(this)
278
-
279
- this.emit('status', { status: 'connecting' })
516
+ this.startSync()
280
517
  }
281
518
 
282
- onOpen(event: OpenEvent) {
283
- this.emit('open', { event })
284
- }
519
+ async getToken() {
520
+ if (typeof this.configuration.token === 'function') {
521
+ const token = await this.configuration.token()
522
+ return token
523
+ }
285
524
 
286
- webSocketConnectionEstablished() {
287
- this.failedConnectionAttempts = 0
288
- this.status = WebSocketStatus.Connected
289
- this.emit('status', { status: 'connected' })
290
- this.emit('connect')
525
+ return this.configuration.token
526
+ }
291
527
 
528
+ startSync() {
292
529
  this.send(SyncStepOneMessage, { document: this.document })
293
530
 
294
531
  if (this.awareness.getLocalState() !== null) {
@@ -299,14 +536,12 @@ export class HocuspocusProvider extends EventEmitter {
299
536
  }
300
537
  }
301
538
 
302
- send(Message: OutgoingMessage, args: any, broadcast = false) {
539
+ send(Message: ConstructableOutgoingMessage, args: any, broadcast = false) {
303
540
  if (broadcast) {
304
- this.mux(() => {
305
- this.broadcast(Message, args)
306
- })
541
+ this.mux(() => { this.broadcast(Message, args) })
307
542
  }
308
543
 
309
- if (this.status === WebSocketStatus.Connected) {
544
+ if (this.webSocket?.readyState === WsReadyStates.Open) {
310
545
  const messageSender = new MessageSender(Message, args)
311
546
 
312
547
  this.emit('outgoingMessage', { message: messageSender.message })
@@ -315,9 +550,7 @@ export class HocuspocusProvider extends EventEmitter {
315
550
  }
316
551
 
317
552
  onMessage(event: MessageEvent) {
318
- if (this.status !== WebSocketStatus.Connected) {
319
- this.webSocketConnectionEstablished()
320
- }
553
+ this.resolveConnectionAttempt()
321
554
 
322
555
  this.lastMessageReceived = time.getUnixTime()
323
556
 
@@ -325,22 +558,17 @@ export class HocuspocusProvider extends EventEmitter {
325
558
 
326
559
  this.emit('message', { event, message })
327
560
 
328
- const encoder = new MessageReceiver(message, this).apply(this)
329
-
330
- // TODO: What’s that doing?
331
- // if (encoding.length(encoder) > 1) {
332
- // this.send(encoding.toUint8Array(encoder))
333
- // }
561
+ new MessageReceiver(message).apply(this)
334
562
  }
335
563
 
336
564
  onClose(event: CloseEvent) {
337
565
  this.emit('close', { event })
338
566
 
339
567
  this.webSocket = null
568
+ this.isAuthenticated = false
569
+ this.synced = false
340
570
 
341
571
  if (this.status === WebSocketStatus.Connected) {
342
- this.synced = false
343
-
344
572
  // update awareness (all users except local left)
345
573
  removeAwarenessStates(
346
574
  this.awareness,
@@ -349,29 +577,46 @@ export class HocuspocusProvider extends EventEmitter {
349
577
  )
350
578
 
351
579
  this.status = WebSocketStatus.Disconnected
352
- this.emit('status', { status: 'disconnected' })
580
+ this.emit('status', { status: WebSocketStatus.Disconnected })
353
581
  this.emit('disconnect', { event })
354
- } else {
355
- this.failedConnectionAttempts += 1
356
582
  }
357
583
 
358
- if (this.shouldConnect) {
359
- const wait = math.round(math.min(
360
- math.log10(this.failedConnectionAttempts + 1) * this.options.reconnectTimeoutBase,
361
- this.options.maxReconnectTimeout,
362
- ))
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
+ }
363
588
 
364
- this.log(`[close] Reconnecting in ${wait}ms …`)
365
- setTimeout(this.createWebSocketConnection.bind(this), wait)
589
+ this.shouldConnect = false
590
+ }
366
591
 
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
+ }
605
+
606
+ // If we’ll reconnect, we’re done for now.
607
+ if (this.shouldConnect) {
367
608
  return
368
609
  }
369
610
 
370
- if (this.status !== WebSocketStatus.Disconnected) {
371
- this.status = WebSocketStatus.Disconnected
372
- this.emit('status', { status: 'disconnected' })
373
- this.emit('disconnect', { event })
611
+ // The status is set correctly already.
612
+ if (this.status === WebSocketStatus.Disconnected) {
613
+ return
374
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 })
375
620
  }
376
621
 
377
622
  destroy() {
@@ -383,33 +628,46 @@ export class HocuspocusProvider extends EventEmitter {
383
628
 
384
629
  clearInterval(this.intervals.connectionChecker)
385
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
+
386
638
  this.disconnect()
387
639
 
388
640
  this.awareness.off('update', this.awarenessUpdateHandler)
389
641
  this.document.off('update', this.documentUpdateHandler)
390
642
 
391
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)
392
651
  }
393
652
 
394
653
  get broadcastChannel() {
395
- return `${this.serverUrl}/${this.options.name}`
654
+ return `${this.serverUrl}/${this.configuration.name}`
396
655
  }
397
656
 
657
+ boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this)
658
+
398
659
  broadcastChannelSubscriber(data: ArrayBuffer) {
399
660
  this.mux(() => {
400
661
  const message = new IncomingMessage(data)
401
- const encoder = new MessageReceiver(message, this).apply(this, false)
402
-
403
- // TODO: What’s that doing?
404
- // if (encoding.length(encoder) > 1) {
405
- // this.broadcast(encoding.toUint8Array(encoder))
406
- // }
662
+ new MessageReceiver(message)
663
+ .setBroadcasted(true)
664
+ .apply(this, false)
407
665
  })
408
666
  }
409
667
 
410
668
  subscribeToBroadcastChannel() {
411
669
  if (!this.subscribedToBroadcastChannel) {
412
- bc.subscribe(this.broadcastChannel, this.broadcastChannelSubscriber.bind(this))
670
+ bc.subscribe(this.broadcastChannel, this.boundBroadcastChannelSubscriber)
413
671
  this.subscribedToBroadcastChannel = true
414
672
  }
415
673
 
@@ -430,23 +688,21 @@ export class HocuspocusProvider extends EventEmitter {
430
688
  }, true)
431
689
 
432
690
  if (this.subscribedToBroadcastChannel) {
433
- bc.unsubscribe(this.broadcastChannel, this.broadcastChannelSubscriber.bind(this))
691
+ bc.unsubscribe(this.broadcastChannel, this.boundBroadcastChannelSubscriber)
434
692
  this.subscribedToBroadcastChannel = false
435
693
  }
436
694
  }
437
695
 
438
- broadcast(Message: OutgoingMessage, args: any) {
439
- if (this.subscribedToBroadcastChannel) {
440
- new MessageSender(Message, args).broadcast(this.broadcastChannel)
696
+ broadcast(Message: ConstructableOutgoingMessage, args?: any) {
697
+ if (!this.configuration.broadcast) {
698
+ return
441
699
  }
442
- }
443
700
 
444
- log(message: string): void {
445
- if (!this.options.debug) {
701
+ if (!this.subscribedToBroadcastChannel) {
446
702
  return
447
703
  }
448
704
 
449
- console.log(message)
705
+ new MessageSender(Message, args).broadcast(this.broadcastChannel)
450
706
  }
451
707
 
452
708
  setAwarenessField(key: string, value: any) {