@hocuspocus/provider 1.0.0-alpha.4 → 1.0.0-alpha.40

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 (132) hide show
  1. package/dist/{hocuspocus-provider.js → hocuspocus-provider.cjs} +362 -186
  2. package/dist/hocuspocus-provider.cjs.map +1 -0
  3. package/dist/hocuspocus-provider.esm.js +359 -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 +116 -32
  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 +16 -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 +168 -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/backend/src/webhook.d.ts → playground/backend/src/redis.d.ts} +0 -0
  50. package/dist/playground/backend/src/slow.d.ts +1 -0
  51. package/dist/playground/backend/src/webhook.d.ts +1 -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/closeConnections.d.ts +1 -0
  77. package/dist/tests/server/getConnectionsCount.d.ts +1 -0
  78. package/dist/tests/server/getDocumentName.d.ts +1 -0
  79. package/dist/tests/server/getDocumentsCount.d.ts +1 -0
  80. package/dist/tests/server/getMessageLogs.d.ts +1 -0
  81. package/dist/tests/server/listen.d.ts +1 -0
  82. package/dist/tests/server/onAuthenticate.d.ts +1 -0
  83. package/dist/tests/server/onAwarenessUpdate.d.ts +1 -0
  84. package/dist/tests/server/onChange.d.ts +1 -0
  85. package/dist/tests/server/onConfigure.d.ts +1 -0
  86. package/dist/tests/server/onConnect.d.ts +1 -0
  87. package/dist/tests/server/onDestroy.d.ts +1 -0
  88. package/dist/tests/server/onDisconnect.d.ts +1 -0
  89. package/dist/tests/server/onListen.d.ts +1 -0
  90. package/dist/tests/server/onLoadDocument.d.ts +1 -0
  91. package/dist/tests/server/onRequest.d.ts +1 -0
  92. package/dist/tests/server/onStoreDocument.d.ts +1 -0
  93. package/dist/tests/server/onUpgrade.d.ts +1 -0
  94. package/dist/tests/server/requiresAuthentication.d.ts +1 -0
  95. package/dist/tests/transformer/TiptapTransformer.d.ts +1 -0
  96. package/dist/tests/utils/createDirectory.d.ts +1 -0
  97. package/dist/tests/utils/flushRedis.d.ts +1 -0
  98. package/dist/tests/utils/index.d.ts +8 -0
  99. package/dist/tests/utils/newHocuspocus.d.ts +2 -0
  100. package/dist/tests/utils/newHocuspocusProvider.d.ts +3 -0
  101. package/dist/tests/utils/randomInteger.d.ts +1 -0
  102. package/dist/tests/utils/redisConnectionSettings.d.ts +4 -0
  103. package/dist/tests/utils/removeDirectory.d.ts +1 -0
  104. package/dist/tests/utils/retryableAssertion.d.ts +2 -0
  105. package/dist/tests/utils/sleep.d.ts +1 -0
  106. package/package.json +15 -11
  107. package/src/EventEmitter.ts +1 -1
  108. package/src/HocuspocusCloudProvider.ts +34 -0
  109. package/src/HocuspocusProvider.ts +389 -159
  110. package/src/IncomingMessage.ts +35 -11
  111. package/src/MessageReceiver.ts +56 -24
  112. package/src/MessageSender.ts +5 -17
  113. package/src/OutgoingMessage.ts +9 -9
  114. package/src/OutgoingMessages/AuthenticationMessage.ts +21 -0
  115. package/src/OutgoingMessages/AwarenessMessage.ts +1 -1
  116. package/src/OutgoingMessages/SyncStepOneMessage.ts +0 -1
  117. package/src/OutgoingMessages/UpdateMessage.ts +4 -4
  118. package/src/index.ts +1 -1
  119. package/src/types.ts +70 -3
  120. package/CHANGELOG.md +0 -24
  121. package/dist/hocuspocus-provider.js.map +0 -1
  122. package/dist/packages/logger/src/index.d.ts +0 -13
  123. package/dist/packages/provider/src/utils/awarenessStatesToArray.d.ts +0 -4
  124. package/dist/packages/provider/src/utils/index.d.ts +0 -1
  125. package/dist/packages/redis/src/Redis.d.ts +0 -22
  126. package/dist/packages/redis/src/RedisCluster.d.ts +0 -4
  127. package/dist/packages/redis/src/index.d.ts +0 -2
  128. package/dist/packages/rocksdb/src/index.d.ts +0 -30
  129. package/dist/packages/server/src/CloseEvents.d.ts +0 -3
  130. package/dist/packages/throttle/src/index.d.ts +0 -28
  131. package/src/utils/awarenessStatesToArray.ts +0 -8
  132. 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,78 +16,176 @@ 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
- onSynced: () => void,
47
- onDisconnect: (event: CloseEvent) => void,
48
- 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,
49
118
  onDestroy: () => void,
50
- onAwarenessChange: (states: any) => void,
51
- debug: boolean,
119
+ onAwarenessUpdate: (data: onAwarenessUpdateParameters) => void,
120
+ onAwarenessChange: (data: onAwarenessChangeParameters) => void,
121
+ /**
122
+ * Don’t output any warnings.
123
+ */
124
+ quiet: boolean,
52
125
  }
53
126
 
54
127
  export class HocuspocusProvider extends EventEmitter {
55
- public options: HocuspocusProviderOptions = {
56
- url: '',
128
+ public configuration: CompleteHocuspocusProviderConfiguration = {
57
129
  name: '',
130
+ url: '',
131
+ // @ts-ignore
132
+ document: undefined,
133
+ // @ts-ignore
134
+ awareness: undefined,
135
+ WebSocketPolyfill: undefined,
136
+ token: null,
58
137
  parameters: {},
59
- debug: false,
60
138
  connect: true,
139
+ broadcast: true,
61
140
  forceSyncInterval: false,
62
- reconnectTimeoutBase: 1200,
63
- maxReconnectTimeout: 2500,
64
141
  // TODO: this should depend on awareness.outdatedTime
65
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,
66
161
  onOpen: () => null,
67
162
  onConnect: () => null,
68
163
  onMessage: () => null,
69
164
  onOutgoingMessage: () => null,
165
+ onStatus: () => null,
70
166
  onSynced: () => null,
71
167
  onDisconnect: () => null,
72
168
  onClose: () => null,
73
169
  onDestroy: () => null,
170
+ onAwarenessUpdate: () => null,
74
171
  onAwarenessChange: () => null,
172
+ quiet: false,
75
173
  }
76
174
 
77
- awareness: Awareness
78
-
79
175
  subscribedToBroadcastChannel = false
80
176
 
81
- webSocket: any = null
177
+ webSocket: WebSocket | null = null
82
178
 
83
179
  shouldConnect = true
84
180
 
85
- status: WebSocketStatus = WebSocketStatus.Disconnected
86
-
87
- failedConnectionAttempts = 0
181
+ status = WebSocketStatus.Disconnected
88
182
 
89
183
  isSynced = false
90
184
 
185
+ unsyncedChanges = 0
186
+
187
+ isAuthenticated = false
188
+
91
189
  lastMessageReceived = 0
92
190
 
93
191
  mux = mutex.createMutex()
@@ -97,77 +195,183 @@ export class HocuspocusProvider extends EventEmitter {
97
195
  connectionChecker: null,
98
196
  }
99
197
 
100
- constructor(options: Partial<HocuspocusProviderOptions> = {}) {
101
- super()
102
-
103
- this.setOptions(options)
198
+ connectionAttempt: {
199
+ resolve: (value?: any) => void
200
+ reject: (reason?: any) => void
201
+ } | null = null
104
202
 
105
- this.options.document = options.document ? options.document : new Y.Doc()
106
- this.options.awareness = options.awareness ? options.awareness : new Awareness(this.document)
107
- this.options.WebSocketPolyfill = options.WebSocketPolyfill ? options.WebSocketPolyfill : WebSocket
108
- this.shouldConnect = options.connect !== undefined ? options.connect : this.shouldConnect
109
-
110
- this.on('open', this.options.onOpen)
111
- this.on('connect', this.options.onConnect)
112
- this.on('message', this.options.onMessage)
113
- this.on('outgoingMessage', this.options.onOutgoingMessage)
114
- this.on('synced', this.options.onSynced)
115
- this.on('disconnect', this.options.onDisconnect)
116
- this.on('close', this.options.onClose)
117
- this.on('destroy', this.options.onDestroy)
118
- 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
+ })
119
228
 
120
229
  this.awareness.on('change', () => {
121
- this.emit('awarenessChange', {
122
- states: awarenessStatesToArray(this.awareness.getStates()),
123
- })
230
+ this.emit('awarenessChange', { states: awarenessStatesToArray(this.awareness.getStates()) })
124
231
  })
125
232
 
233
+ this.document.on('update', this.documentUpdateHandler.bind(this))
234
+ this.awareness.on('update', this.awarenessUpdateHandler.bind(this))
235
+ this.registerEventListeners()
236
+
126
237
  this.intervals.connectionChecker = setInterval(
127
238
  this.checkConnection.bind(this),
128
- this.options.messageReconnectTimeout / 10,
239
+ this.configuration.messageReconnectTimeout / 10,
129
240
  )
130
241
 
131
- this.document.on('update', this.documentUpdateHandler.bind(this))
132
- this.awareness.on('update', this.awarenessUpdateHandler.bind(this))
133
- this.registerBeforeUnloadEventListener()
134
-
135
- if (this.options.forceSyncInterval) {
242
+ if (this.configuration.forceSyncInterval) {
136
243
  this.intervals.forceSync = setInterval(
137
244
  this.forceSync.bind(this),
138
- this.options.forceSyncInterval,
245
+ this.configuration.forceSyncInterval,
139
246
  )
140
247
  }
141
248
 
142
- if (this.options.connect) {
143
- 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
+ async connect() {
267
+ if (this.status === WebSocketStatus.Connected) {
268
+ return
144
269
  }
270
+
271
+ this.unsyncedChanges = 0 // set to 0 in case we got reconnected
272
+ this.shouldConnect = true
273
+ this.subscribeToBroadcastChannel()
274
+
275
+ try {
276
+ await retry(this.createWebSocketConnection.bind(this), {
277
+ delay: this.configuration.delay,
278
+ initialDelay: this.configuration.initialDelay,
279
+ factor: this.configuration.factor,
280
+ maxAttempts: this.configuration.maxAttempts,
281
+ minDelay: this.configuration.minDelay,
282
+ maxDelay: this.configuration.maxDelay,
283
+ jitter: this.configuration.jitter,
284
+ timeout: this.configuration.timeout,
285
+ beforeAttempt: context => {
286
+ if (!this.shouldConnect) {
287
+ context.abort()
288
+ }
289
+ },
290
+ })
291
+ } catch (error: any) {
292
+ // If we aborted the connection attempt then don’t throw an error
293
+ // ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
294
+ if (error && error.code !== 'ATTEMPT_ABORTED') {
295
+ throw error
296
+ }
297
+ }
298
+ }
299
+
300
+ createWebSocketConnection() {
301
+ return new Promise((resolve, reject) => {
302
+ // Init the WebSocket connection
303
+ const ws = new this.configuration.WebSocketPolyfill(this.url)
304
+ ws.binaryType = 'arraybuffer'
305
+ ws.onmessage = this.onMessage.bind(this)
306
+ ws.onclose = this.onClose.bind(this)
307
+ ws.onopen = this.onOpen.bind(this)
308
+ ws.onerror = (err: any) => {
309
+ reject(err)
310
+ }
311
+ this.webSocket = ws
312
+
313
+ // Reset the status
314
+ this.synced = false
315
+ this.status = WebSocketStatus.Connecting
316
+ this.emit('status', { status: WebSocketStatus.Connecting })
317
+
318
+ // Store resolve/reject for later use
319
+ this.connectionAttempt = {
320
+ resolve,
321
+ reject,
322
+ }
323
+ })
324
+ }
325
+
326
+ resolveConnectionAttempt() {
327
+ this.connectionAttempt?.resolve()
328
+ this.connectionAttempt = null
329
+
330
+ this.status = WebSocketStatus.Connected
331
+ this.emit('status', { status: WebSocketStatus.Connected })
332
+ this.emit('connect')
333
+ }
334
+
335
+ stopConnectionAttempt() {
336
+ this.connectionAttempt = null
145
337
  }
146
338
 
147
- public setOptions(options: Partial<HocuspocusProviderOptions> = {}): void {
148
- this.options = { ...this.options, ...options }
339
+ rejectConnectionAttempt() {
340
+ this.connectionAttempt?.reject()
341
+ this.connectionAttempt = null
149
342
  }
150
343
 
151
344
  get document() {
152
- return this.options.document
345
+ return this.configuration.document
153
346
  }
154
347
 
155
348
  get awareness() {
156
- return this.options.awareness
349
+ return this.configuration.awareness
350
+ }
351
+
352
+ get hasUnsyncedChanges() {
353
+ return this.unsyncedChanges > 0
157
354
  }
158
355
 
159
356
  checkConnection() {
357
+ // Don’t check the connection when it’s not even established
160
358
  if (this.status !== WebSocketStatus.Connected) {
161
359
  return
162
360
  }
163
361
 
164
- if (this.options.messageReconnectTimeout >= time.getUnixTime() - this.lastMessageReceived) {
362
+ // Don’t close then connection while waiting for the first message
363
+ if (!this.lastMessageReceived) {
364
+ return
365
+ }
366
+
367
+ // Don’t close the connection when a message was received recently
368
+ if (this.configuration.messageReconnectTimeout >= time.getUnixTime() - this.lastMessageReceived) {
165
369
  return
166
370
  }
167
371
 
168
372
  // No message received in a long time, not even your own
169
373
  // Awareness updates, which are updated every 15 seconds.
170
- this.webSocket.close()
374
+ this.webSocket?.close()
171
375
  }
172
376
 
173
377
  forceSync() {
@@ -178,14 +382,19 @@ export class HocuspocusProvider extends EventEmitter {
178
382
  this.send(SyncStepOneMessage, { document: this.document })
179
383
  }
180
384
 
181
- registerBeforeUnloadEventListener() {
385
+ boundBeforeUnload = this.beforeUnload.bind(this)
386
+
387
+ beforeUnload() {
388
+ removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload')
389
+ }
390
+
391
+ registerEventListeners() {
182
392
  if (typeof window === 'undefined') {
183
393
  return
184
394
  }
185
395
 
186
- window.addEventListener('beforeunload', () => {
187
- removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload')
188
- })
396
+ window.addEventListener('online', this.boundConnect)
397
+ window.addEventListener('beforeunload', this.boundBeforeUnload)
189
398
  }
190
399
 
191
400
  documentUpdateHandler(update: Uint8Array, origin: any) {
@@ -193,6 +402,7 @@ export class HocuspocusProvider extends EventEmitter {
193
402
  return
194
403
  }
195
404
 
405
+ this.unsyncedChanges += 1
196
406
  this.send(UpdateMessage, { update }, true)
197
407
  }
198
408
 
@@ -205,19 +415,32 @@ export class HocuspocusProvider extends EventEmitter {
205
415
  }, true)
206
416
  }
207
417
 
418
+ permissionDeniedHandler(reason: string) {
419
+ this.emit('authenticationFailed', { reason })
420
+ this.isAuthenticated = false
421
+ this.shouldConnect = false
422
+ }
423
+
424
+ authenticatedHandler() {
425
+ this.isAuthenticated = true
426
+
427
+ this.emit('authenticated')
428
+ this.startSync()
429
+ }
430
+
208
431
  // Ensure that the URL always ends with /
209
432
  get serverUrl() {
210
- while (this.options.url[this.options.url.length - 1] === '/') {
211
- return this.options.url.slice(0, this.options.url.length - 1)
433
+ while (this.configuration.url[this.configuration.url.length - 1] === '/') {
434
+ return this.configuration.url.slice(0, this.configuration.url.length - 1)
212
435
  }
213
436
 
214
- return this.options.url
437
+ return this.configuration.url
215
438
  }
216
439
 
217
440
  get url() {
218
- const encodedParams = url.encodeQueryParams(this.options.parameters)
441
+ const encodedParams = url.encodeQueryParams(this.configuration.parameters)
219
442
 
220
- return `${this.serverUrl}/${this.options.name}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`
443
+ return `${this.serverUrl}/${this.configuration.name}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`
221
444
  }
222
445
 
223
446
  get synced(): boolean {
@@ -234,13 +457,8 @@ export class HocuspocusProvider extends EventEmitter {
234
457
  this.emit('sync', { state })
235
458
  }
236
459
 
237
- connect() {
238
- this.shouldConnect = true
239
-
240
- if (this.status !== WebSocketStatus.Connected) {
241
- this.createWebSocketConnection()
242
- this.subscribeToBroadcastChannel()
243
- }
460
+ get isAuthenticationRequired(): boolean {
461
+ return !!this.configuration.token && !this.isAuthenticated
244
462
  }
245
463
 
246
464
  disconnect() {
@@ -258,34 +476,29 @@ export class HocuspocusProvider extends EventEmitter {
258
476
  }
259
477
  }
260
478
 
261
- createWebSocketConnection() {
262
- if (this.webSocket !== null) {
479
+ async onOpen(event: Event) {
480
+ this.emit('open', { event })
481
+
482
+ if (this.isAuthenticationRequired) {
483
+ this.send(AuthenticationMessage, {
484
+ token: await this.getToken(),
485
+ })
263
486
  return
264
487
  }
265
488
 
266
- this.webSocket = new this.options.WebSocketPolyfill(this.url)
267
- this.webSocket.binaryType = 'arraybuffer'
268
-
269
- this.status = WebSocketStatus.Connecting
270
- this.synced = false
271
-
272
- this.webSocket.onmessage = this.onMessage.bind(this)
273
- this.webSocket.onclose = this.onClose.bind(this)
274
- this.webSocket.onopen = this.onOpen.bind(this)
275
-
276
- this.emit('status', { status: 'connecting' })
489
+ this.startSync()
277
490
  }
278
491
 
279
- onOpen(event: OpenEvent) {
280
- this.emit('open', { event })
281
- }
492
+ async getToken() {
493
+ if (typeof this.configuration.token === 'function') {
494
+ const token = await this.configuration.token()
495
+ return token
496
+ }
282
497
 
283
- webSocketConnectionEstablished() {
284
- this.failedConnectionAttempts = 0
285
- this.status = WebSocketStatus.Connected
286
- this.emit('status', { status: 'connected' })
287
- this.emit('connect')
498
+ return this.configuration.token
499
+ }
288
500
 
501
+ startSync() {
289
502
  this.send(SyncStepOneMessage, { document: this.document })
290
503
 
291
504
  if (this.awareness.getLocalState() !== null) {
@@ -296,16 +509,12 @@ export class HocuspocusProvider extends EventEmitter {
296
509
  }
297
510
  }
298
511
 
299
- send(Message: OutgoingMessage, args: any, broadcast = false) {
300
- const message = new Message()
301
-
512
+ send(Message: ConstructableOutgoingMessage, args: any, broadcast = false) {
302
513
  if (broadcast) {
303
- this.mux(() => {
304
- this.broadcast(Message, args)
305
- })
514
+ this.mux(() => { this.broadcast(Message, args) })
306
515
  }
307
516
 
308
- if (this.status === WebSocketStatus.Connected) {
517
+ if (this.webSocket?.readyState === WsReadyStates.Open) {
309
518
  const messageSender = new MessageSender(Message, args)
310
519
 
311
520
  this.emit('outgoingMessage', { message: messageSender.message })
@@ -314,9 +523,7 @@ export class HocuspocusProvider extends EventEmitter {
314
523
  }
315
524
 
316
525
  onMessage(event: MessageEvent) {
317
- if (this.status !== WebSocketStatus.Connected) {
318
- this.webSocketConnectionEstablished()
319
- }
526
+ this.resolveConnectionAttempt()
320
527
 
321
528
  this.lastMessageReceived = time.getUnixTime()
322
529
 
@@ -324,22 +531,17 @@ export class HocuspocusProvider extends EventEmitter {
324
531
 
325
532
  this.emit('message', { event, message })
326
533
 
327
- const encoder = new MessageReceiver(message, this).apply(this)
328
-
329
- // TODO: What’s that doing?
330
- // if (encoding.length(encoder) > 1) {
331
- // this.send(encoding.toUint8Array(encoder))
332
- // }
534
+ new MessageReceiver(message).apply(this)
333
535
  }
334
536
 
335
537
  onClose(event: CloseEvent) {
336
538
  this.emit('close', { event })
337
539
 
338
540
  this.webSocket = null
541
+ this.isAuthenticated = false
542
+ this.synced = false
339
543
 
340
544
  if (this.status === WebSocketStatus.Connected) {
341
- this.synced = false
342
-
343
545
  // update awareness (all users except local left)
344
546
  removeAwarenessStates(
345
547
  this.awareness,
@@ -348,29 +550,46 @@ export class HocuspocusProvider extends EventEmitter {
348
550
  )
349
551
 
350
552
  this.status = WebSocketStatus.Disconnected
351
- this.emit('status', { status: 'disconnected' })
553
+ this.emit('status', { status: WebSocketStatus.Disconnected })
352
554
  this.emit('disconnect', { event })
353
- } else {
354
- this.failedConnectionAttempts += 1
355
555
  }
356
556
 
357
- if (this.shouldConnect) {
358
- const wait = math.round(math.min(
359
- math.log10(this.failedConnectionAttempts + 1) * this.options.reconnectTimeoutBase,
360
- this.options.maxReconnectTimeout,
361
- ))
557
+ if (event.code === Unauthorized.code) {
558
+ if (!this.configuration.quiet) {
559
+ 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.')
560
+ }
362
561
 
363
- this.log(`[close] Reconnecting in ${wait}ms …`)
364
- setTimeout(this.createWebSocketConnection.bind(this), wait)
562
+ this.shouldConnect = false
563
+ }
365
564
 
565
+ if (event.code === Forbidden.code) {
566
+ if (!this.configuration.quiet) {
567
+ console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.')
568
+ }
569
+ }
570
+
571
+ if (this.connectionAttempt) {
572
+ // That connection attempt failed.
573
+ this.rejectConnectionAttempt()
574
+ } else if (this.shouldConnect) {
575
+ // The connection was closed by the server. Let’s just try again.
576
+ this.connect()
577
+ }
578
+
579
+ // If we’ll reconnect, we’re done for now.
580
+ if (this.shouldConnect) {
366
581
  return
367
582
  }
368
583
 
369
- if (this.status !== WebSocketStatus.Disconnected) {
370
- this.status = WebSocketStatus.Disconnected
371
- this.emit('status', { status: 'disconnected' })
372
- this.emit('disconnect', { event })
584
+ // The status is set correctly already.
585
+ if (this.status === WebSocketStatus.Disconnected) {
586
+ return
373
587
  }
588
+
589
+ // Let’s update the connection status.
590
+ this.status = WebSocketStatus.Disconnected
591
+ this.emit('status', { status: WebSocketStatus.Disconnected })
592
+ this.emit('disconnect', { event })
374
593
  }
375
594
 
376
595
  destroy() {
@@ -382,33 +601,46 @@ export class HocuspocusProvider extends EventEmitter {
382
601
 
383
602
  clearInterval(this.intervals.connectionChecker)
384
603
 
604
+ removeAwarenessStates(this.awareness, [this.document.clientID], 'provider destroy')
605
+
606
+ // If there is still a connection attempt outstanding then we should stop
607
+ // it before calling disconnect, otherwise it will be rejected in the onClose
608
+ // handler and trigger a retry
609
+ this.stopConnectionAttempt()
610
+
385
611
  this.disconnect()
386
612
 
387
613
  this.awareness.off('update', this.awarenessUpdateHandler)
388
614
  this.document.off('update', this.documentUpdateHandler)
389
615
 
390
616
  this.removeAllListeners()
617
+
618
+ if (typeof window === 'undefined') {
619
+ return
620
+ }
621
+
622
+ window.removeEventListener('online', this.boundConnect)
623
+ window.removeEventListener('beforeunload', this.boundBeforeUnload)
391
624
  }
392
625
 
393
626
  get broadcastChannel() {
394
- return `${this.serverUrl}/${this.options.name}`
627
+ return `${this.serverUrl}/${this.configuration.name}`
395
628
  }
396
629
 
630
+ boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this)
631
+
397
632
  broadcastChannelSubscriber(data: ArrayBuffer) {
398
633
  this.mux(() => {
399
634
  const message = new IncomingMessage(data)
400
- const encoder = new MessageReceiver(message, this).apply(this, false)
401
-
402
- // TODO: What’s that doing?
403
- // if (encoding.length(encoder) > 1) {
404
- // this.broadcast(encoding.toUint8Array(encoder))
405
- // }
635
+ new MessageReceiver(message)
636
+ .setBroadcasted(true)
637
+ .apply(this, false)
406
638
  })
407
639
  }
408
640
 
409
641
  subscribeToBroadcastChannel() {
410
642
  if (!this.subscribedToBroadcastChannel) {
411
- bc.subscribe(this.broadcastChannel, this.broadcastChannelSubscriber.bind(this))
643
+ bc.subscribe(this.broadcastChannel, this.boundBroadcastChannelSubscriber)
412
644
  this.subscribedToBroadcastChannel = true
413
645
  }
414
646
 
@@ -429,23 +661,21 @@ export class HocuspocusProvider extends EventEmitter {
429
661
  }, true)
430
662
 
431
663
  if (this.subscribedToBroadcastChannel) {
432
- bc.unsubscribe(this.broadcastChannel, this.broadcastChannelSubscriber.bind(this))
664
+ bc.unsubscribe(this.broadcastChannel, this.boundBroadcastChannelSubscriber)
433
665
  this.subscribedToBroadcastChannel = false
434
666
  }
435
667
  }
436
668
 
437
- broadcast(Message: OutgoingMessage, args: any) {
438
- if (this.subscribedToBroadcastChannel) {
439
- new MessageSender(Message, args).broadcast(this.broadcastChannel)
669
+ broadcast(Message: ConstructableOutgoingMessage, args?: any) {
670
+ if (!this.configuration.broadcast) {
671
+ return
440
672
  }
441
- }
442
673
 
443
- log(message: string): void {
444
- if (!this.options.debug) {
674
+ if (!this.subscribedToBroadcastChannel) {
445
675
  return
446
676
  }
447
677
 
448
- console.log(message)
678
+ new MessageSender(Message, args).broadcast(this.broadcastChannel)
449
679
  }
450
680
 
451
681
  setAwarenessField(key: string, value: any) {