@hocuspocus/provider 2.2.3 → 2.3.1

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.
@@ -69,7 +69,7 @@ export declare class Redis implements Extension {
69
69
  private subKey;
70
70
  private lockKey;
71
71
  /**
72
- * Once a document is laoded, subscribe to the channel in Redis.
72
+ * Once a document is loaded, subscribe to the channel in Redis.
73
73
  */
74
74
  afterLoadDocument({ documentName, document }: afterLoadDocumentPayload): Promise<unknown>;
75
75
  /**
@@ -1,15 +1,15 @@
1
- import * as Y from 'yjs';
2
- import { Awareness } from 'y-protocols/awareness';
3
1
  import * as mutex from 'lib0/mutex';
4
2
  import type { CloseEvent, Event, MessageEvent } from 'ws';
3
+ import { Awareness } from 'y-protocols/awareness';
4
+ import * as Y from 'yjs';
5
5
  import EventEmitter from './EventEmitter.js';
6
- import { ConstructableOutgoingMessage, onAuthenticationFailedParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatelessParameters, onStatusParameters, onSyncedParameters, WebSocketStatus, onAwarenessChangeParameters, onAwarenessUpdateParameters } from './types.js';
7
6
  import { CompleteHocuspocusProviderWebsocketConfiguration, HocuspocusProviderWebsocket } from './HocuspocusProviderWebsocket.js';
7
+ import { ConstructableOutgoingMessage, WebSocketStatus, onAuthenticationFailedParameters, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatelessParameters, onStatusParameters, onSyncedParameters } from './types.js';
8
8
  export type HocuspocusProviderConfiguration = Required<Pick<CompleteHocuspocusProviderConfiguration, 'name'>> & Partial<CompleteHocuspocusProviderConfiguration> & (Required<Pick<CompleteHocuspocusProviderWebsocketConfiguration, 'url'>> | Required<Pick<CompleteHocuspocusProviderConfiguration, 'websocketProvider'>>);
9
9
  export interface CompleteHocuspocusProviderConfiguration {
10
10
  /**
11
- * The identifier/name of your document
12
- */
11
+ * The identifier/name of your document
12
+ */
13
13
  name: string;
14
14
  /**
15
15
  * The actual Y.js document
@@ -59,6 +59,14 @@ export interface CompleteHocuspocusProviderConfiguration {
59
59
  * Don’t output any warnings.
60
60
  */
61
61
  quiet: boolean;
62
+ /**
63
+ * Pass `false` to start the connection manually.
64
+ */
65
+ connect: boolean;
66
+ /**
67
+ * Pass `false` to close the connection manually.
68
+ */
69
+ preserveConnection: boolean;
62
70
  }
63
71
  export declare class HocuspocusProvider extends EventEmitter {
64
72
  configuration: CompleteHocuspocusProviderConfiguration;
@@ -73,7 +81,7 @@ export declare class HocuspocusProvider extends EventEmitter {
73
81
  isConnected: boolean;
74
82
  constructor(configuration: HocuspocusProviderConfiguration);
75
83
  boundBroadcastChannelSubscriber: (data: ArrayBuffer) => void;
76
- boundBeforeUnload: () => void;
84
+ boundPageUnload: () => void;
77
85
  boundOnOpen: (event: Event) => Promise<void>;
78
86
  boundOnMessage: (event: MessageEvent) => void;
79
87
  boundOnClose: (event: CloseEvent) => void;
@@ -88,13 +96,20 @@ export declare class HocuspocusProvider extends EventEmitter {
88
96
  get document(): Y.Doc;
89
97
  get awareness(): Awareness;
90
98
  get hasUnsyncedChanges(): boolean;
91
- updateUnsyncedChanges(unsyncedChanges?: number): void;
99
+ incrementUnsyncedChanges(): void;
100
+ decrementUnsyncedChanges(): void;
92
101
  forceSync(): void;
93
- beforeUnload(): void;
102
+ pageUnload(): void;
94
103
  registerEventListeners(): void;
95
104
  sendStateless(payload: string): void;
96
105
  documentUpdateHandler(update: Uint8Array, origin: any): void;
97
106
  awarenessUpdateHandler({ added, updated, removed }: any, origin: any): void;
107
+ /**
108
+ * Indicates whether a first handshake with the server has been established
109
+ *
110
+ * Note: this does not mean all updates from the client have been persisted to the backend. For this,
111
+ * use `hasUnsyncedChanges`.
112
+ */
98
113
  get synced(): boolean;
99
114
  set synced(state: boolean);
100
115
  receiveStateless(payload: string): void;
@@ -2,8 +2,8 @@ import * as mutex from 'lib0/mutex';
2
2
  import type { MessageEvent } from 'ws';
3
3
  import { Event } from 'ws';
4
4
  import EventEmitter from './EventEmitter.js';
5
- import { onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatusParameters, WebSocketStatus, onAwarenessChangeParameters, onAwarenessUpdateParameters } from './types.js';
6
5
  import { HocuspocusProvider } from './HocuspocusProvider.js';
6
+ import { WebSocketStatus, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatusParameters } from './types.js';
7
7
  export type HocuspocusProviderWebsocketConfiguration = Required<Pick<CompleteHocuspocusProviderWebsocketConfiguration, 'url'>> & Partial<CompleteHocuspocusProviderWebsocketConfiguration>;
8
8
  export interface CompleteHocuspocusProviderWebsocketConfiguration {
9
9
  /**
@@ -76,6 +76,7 @@ export interface CompleteHocuspocusProviderWebsocketConfiguration {
76
76
  quiet: boolean;
77
77
  }
78
78
  export declare class HocuspocusProviderWebsocket extends EventEmitter {
79
+ private messageQueue;
79
80
  configuration: CompleteHocuspocusProviderWebsocketConfiguration;
80
81
  subscribedToBroadcastChannel: boolean;
81
82
  webSocket: WebSocket | null;
@@ -104,6 +105,7 @@ export declare class HocuspocusProviderWebsocket extends EventEmitter {
104
105
  resolveConnectionAttempt(): void;
105
106
  stopConnectionAttempt(): void;
106
107
  rejectConnectionAttempt(): void;
108
+ closeTries: number;
107
109
  checkConnection(): void;
108
110
  registerEventListeners(): void;
109
111
  get serverUrl(): string;
@@ -5,8 +5,9 @@ export declare class MessageReceiver {
5
5
  broadcasted: boolean;
6
6
  constructor(message: IncomingMessage);
7
7
  setBroadcasted(value: boolean): this;
8
- apply(provider: HocuspocusProvider, emitSynced?: boolean): void;
8
+ apply(provider: HocuspocusProvider, emitSynced: boolean): void;
9
9
  private applySyncMessage;
10
+ applySyncStatusMessage(provider: HocuspocusProvider, applied: boolean): void;
10
11
  private applyAwarenessMessage;
11
12
  private applyAuthMessage;
12
13
  private applyQueryAwarenessMessage;
@@ -23,6 +23,6 @@ export declare class TiptapCollabProvider extends HocuspocusProvider {
23
23
  watchVersions(callback: Parameters<AbstractType<YArrayEvent<AuditHistoryVersion>>['observe']>[0]): void;
24
24
  unwatchVersions(callback: Parameters<AbstractType<YArrayEvent<AuditHistoryVersion>>['unobserve']>[0]): void;
25
25
  isAutoVersioning(): boolean;
26
- enableAutoVersioning(): number;
27
- disableAutoVersioning(): number;
26
+ enableAutoVersioning(): 1;
27
+ disableAutoVersioning(): 0;
28
28
  }
@@ -1,7 +1,8 @@
1
1
  import { Encoder } from 'lib0/encoding';
2
- import type { CloseEvent, Event, MessageEvent } from 'ws';
2
+ import type { Event, MessageEvent } from 'ws';
3
3
  import { Awareness } from 'y-protocols/awareness';
4
4
  import * as Y from 'yjs';
5
+ import { CloseEvent } from '@hocuspocus/common';
5
6
  import { IncomingMessage } from './IncomingMessage.js';
6
7
  import { OutgoingMessage } from './OutgoingMessage.js';
7
8
  import { AuthenticationMessage } from './OutgoingMessages/AuthenticationMessage.js';
@@ -5,7 +5,7 @@ import WebSocket, { AddressInfo, WebSocketServer } from 'ws';
5
5
  import { Debugger } from './Debugger.js';
6
6
  import { DirectConnection } from './DirectConnection.js';
7
7
  import Document from './Document.js';
8
- import { Configuration, ConnectionConfiguration, HookName, HookPayload, onListenPayload, onStoreDocumentPayload } from './types.js';
8
+ import { Configuration, ConnectionConfiguration, HookName, HookPayloadByName, onListenPayload, onStoreDocumentPayload } from './types.js';
9
9
  export declare const defaultConfiguration: {
10
10
  name: null;
11
11
  port: number;
@@ -91,7 +91,8 @@ export declare class Hocuspocus {
91
91
  * Run the given hook on all configured extensions.
92
92
  * Runs the given callback after each hook.
93
93
  */
94
- hooks(name: HookName, payload: HookPayload, callback?: Function | null): Promise<any>;
94
+ hooks<T extends HookName>(name: T, payload: HookPayloadByName[T], callback?: Function | null): Promise<any>;
95
+ unloadDocument(document: Document): void;
95
96
  enableDebugging(): void;
96
97
  enableMessageLogging(): void;
97
98
  disableLogging(): void;
@@ -1,7 +1,7 @@
1
1
  import Connection from './Connection.js';
2
- import { IncomingMessage } from './IncomingMessage.js';
3
2
  import { Debugger } from './Debugger.js';
4
3
  import Document from './Document.js';
4
+ import { IncomingMessage } from './IncomingMessage.js';
5
5
  export declare class MessageReceiver {
6
6
  message: IncomingMessage;
7
7
  logger: Debugger;
@@ -16,5 +16,6 @@ export declare class OutgoingMessage {
16
16
  writeUpdate(update: Uint8Array): OutgoingMessage;
17
17
  writeStateless(payload: string): OutgoingMessage;
18
18
  writeBroadcastStateless(payload: string): OutgoingMessage;
19
+ writeSyncStatus(updateSaved: boolean): OutgoingMessage;
19
20
  toUint8Array(): Uint8Array;
20
21
  }
@@ -4,9 +4,9 @@
4
4
  import { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'http';
5
5
  import { URLSearchParams } from 'url';
6
6
  import { Awareness } from 'y-protocols/awareness';
7
+ import Connection from './Connection.js';
7
8
  import Document from './Document.js';
8
9
  import { Hocuspocus } from './Hocuspocus.js';
9
- import Connection from './Connection.js';
10
10
  export declare enum MessageType {
11
11
  Unknown = -1,
12
12
  Sync = 0,
@@ -16,7 +16,8 @@ export declare enum MessageType {
16
16
  SyncReply = 4,
17
17
  Stateless = 5,
18
18
  BroadcastStateless = 6,
19
- CLOSE = 7
19
+ CLOSE = 7,
20
+ SyncStatus = 8
20
21
  }
21
22
  export interface AwarenessUpdate {
22
23
  added: Array<any>;
@@ -47,10 +48,31 @@ export interface Extension {
47
48
  onAwarenessUpdate?(data: onAwarenessUpdatePayload): Promise<any>;
48
49
  onRequest?(data: onRequestPayload): Promise<any>;
49
50
  onDisconnect?(data: onDisconnectPayload): Promise<any>;
51
+ afterUnloadDocument?(data: onLoadDocumentPayload): Promise<any>;
50
52
  onDestroy?(data: onDestroyPayload): Promise<any>;
51
53
  }
52
- export type HookName = 'onConfigure' | 'onListen' | 'onUpgrade' | 'onConnect' | 'connected' | 'onAuthenticate' | 'onLoadDocument' | 'afterLoadDocument' | 'beforeHandleMessage' | 'beforeBroadcastStateless' | 'onStateless' | 'onChange' | 'onStoreDocument' | 'afterStoreDocument' | 'onAwarenessUpdate' | 'onRequest' | 'onDisconnect' | 'onDestroy';
53
- export type HookPayload = onConfigurePayload | onListenPayload | onUpgradePayload | onConnectPayload | connectedPayload | onAuthenticatePayload | onLoadDocumentPayload | onStatelessPayload | beforeHandleMessagePayload | beforeBroadcastStatelessPayload | onChangePayload | onStoreDocumentPayload | afterStoreDocumentPayload | onAwarenessUpdatePayload | onRequestPayload | onDisconnectPayload | onDestroyPayload;
54
+ export type HookName = 'onConfigure' | 'onListen' | 'onUpgrade' | 'onConnect' | 'connected' | 'onAuthenticate' | 'onLoadDocument' | 'afterLoadDocument' | 'beforeHandleMessage' | 'beforeBroadcastStateless' | 'onStateless' | 'onChange' | 'onStoreDocument' | 'afterStoreDocument' | 'onAwarenessUpdate' | 'onRequest' | 'onDisconnect' | 'afterUnloadDocument' | 'onDestroy';
55
+ export type HookPayloadByName = {
56
+ onConfigure: onConfigurePayload;
57
+ onListen: onListenPayload;
58
+ onUpgrade: onUpgradePayload;
59
+ onConnect: onConnectPayload;
60
+ connected: connectedPayload;
61
+ onAuthenticate: onAuthenticatePayload;
62
+ onLoadDocument: onLoadDocumentPayload;
63
+ afterLoadDocument: onLoadDocumentPayload;
64
+ beforeHandleMessage: beforeHandleMessagePayload;
65
+ beforeBroadcastStateless: beforeBroadcastStatelessPayload;
66
+ onStateless: onStatelessPayload;
67
+ onChange: onChangePayload;
68
+ onStoreDocument: onStoreDocumentPayload;
69
+ afterStoreDocument: afterStoreDocumentPayload;
70
+ onAwarenessUpdate: onAwarenessUpdatePayload;
71
+ onRequest: onRequestPayload;
72
+ onDisconnect: onDisconnectPayload;
73
+ afterUnloadDocument: afterUnloadDocumentPayload;
74
+ onDestroy: onDestroyPayload;
75
+ };
54
76
  export interface Configuration extends Extension {
55
77
  /**
56
78
  * A name for the instance, used for logging.
@@ -198,14 +220,12 @@ export interface onStoreDocumentPayload {
198
220
  export interface afterStoreDocumentPayload extends onStoreDocumentPayload {
199
221
  }
200
222
  export interface onAwarenessUpdatePayload {
201
- clientsCount: number;
202
223
  context: any;
203
224
  document: Document;
204
225
  documentName: string;
205
226
  instance: Hocuspocus;
206
227
  requestHeaders: IncomingHttpHeaders;
207
228
  requestParameters: URLSearchParams;
208
- update: Uint8Array;
209
229
  socketId: string;
210
230
  added: number[];
211
231
  updated: number[];
@@ -264,6 +284,10 @@ export interface onConfigurePayload {
264
284
  configuration: Configuration;
265
285
  version: string;
266
286
  }
287
+ export interface afterUnloadDocumentPayload {
288
+ instance: Hocuspocus;
289
+ documentName: string;
290
+ }
267
291
  export interface DirectConnection {
268
292
  transact(transaction: (document: Document) => void): Promise<void>;
269
293
  disconnect(): void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hocuspocus/provider",
3
- "version": "2.2.3",
3
+ "version": "2.3.1",
4
4
  "description": "hocuspocus provider",
5
5
  "homepage": "https://hocuspocus.dev",
6
6
  "keywords": [
@@ -29,14 +29,14 @@
29
29
  "dist"
30
30
  ],
31
31
  "dependencies": {
32
- "@hocuspocus/common": "^2.2.3",
32
+ "@hocuspocus/common": "^2.3.1",
33
33
  "@lifeomic/attempt": "^3.0.2",
34
34
  "lib0": "^0.2.47",
35
35
  "ws": "^7.5.9"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "y-protocols": "^1.0.5",
39
- "yjs": "^13.5.29"
39
+ "yjs": "^13.6.4"
40
40
  },
41
41
  "gitHead": "cd788b6a315f608ef531524409abdce1e6790726"
42
42
  }
@@ -1,22 +1,31 @@
1
- import * as Y from 'yjs'
1
+ import { awarenessStatesToArray } from '@hocuspocus/common'
2
2
  import * as bc from 'lib0/broadcastchannel'
3
- import { Awareness, removeAwarenessStates } from 'y-protocols/awareness'
4
3
  import * as mutex from 'lib0/mutex'
5
4
  import type { CloseEvent, Event, MessageEvent } from 'ws'
6
- import { awarenessStatesToArray } from '@hocuspocus/common'
5
+ import { Awareness, removeAwarenessStates } from 'y-protocols/awareness'
6
+ import * as Y from 'yjs'
7
7
  import EventEmitter from './EventEmitter.js'
8
+ import {
9
+ CompleteHocuspocusProviderWebsocketConfiguration,
10
+ HocuspocusProviderWebsocket,
11
+ } from './HocuspocusProviderWebsocket.js'
8
12
  import { IncomingMessage } from './IncomingMessage.js'
9
13
  import { MessageReceiver } from './MessageReceiver.js'
10
14
  import { MessageSender } from './MessageSender.js'
11
- import { SyncStepOneMessage } from './OutgoingMessages/SyncStepOneMessage.js'
12
- import { SyncStepTwoMessage } from './OutgoingMessages/SyncStepTwoMessage.js'
13
- import { QueryAwarenessMessage } from './OutgoingMessages/QueryAwarenessMessage.js'
14
15
  import { AuthenticationMessage } from './OutgoingMessages/AuthenticationMessage.js'
15
16
  import { AwarenessMessage } from './OutgoingMessages/AwarenessMessage.js'
17
+ import { CloseMessage } from './OutgoingMessages/CloseMessage.js'
18
+ import { QueryAwarenessMessage } from './OutgoingMessages/QueryAwarenessMessage.js'
19
+ import { StatelessMessage } from './OutgoingMessages/StatelessMessage.js'
20
+ import { SyncStepOneMessage } from './OutgoingMessages/SyncStepOneMessage.js'
21
+ import { SyncStepTwoMessage } from './OutgoingMessages/SyncStepTwoMessage.js'
16
22
  import { UpdateMessage } from './OutgoingMessages/UpdateMessage.js'
17
23
  import {
18
24
  ConstructableOutgoingMessage,
25
+ WebSocketStatus,
19
26
  onAuthenticationFailedParameters,
27
+ onAwarenessChangeParameters,
28
+ onAwarenessUpdateParameters,
20
29
  onCloseParameters,
21
30
  onDisconnectParameters,
22
31
  onMessageParameters,
@@ -24,16 +33,7 @@ import {
24
33
  onOutgoingMessageParameters, onStatelessParameters,
25
34
  onStatusParameters,
26
35
  onSyncedParameters,
27
- WebSocketStatus,
28
- onAwarenessChangeParameters,
29
- onAwarenessUpdateParameters,
30
36
  } from './types.js'
31
- import {
32
- CompleteHocuspocusProviderWebsocketConfiguration,
33
- HocuspocusProviderWebsocket,
34
- } from './HocuspocusProviderWebsocket.js'
35
- import { StatelessMessage } from './OutgoingMessages/StatelessMessage.js'
36
- import { CloseMessage } from './OutgoingMessages/CloseMessage.js'
37
37
 
38
38
  export type HocuspocusProviderConfiguration =
39
39
  Required<Pick<CompleteHocuspocusProviderConfiguration, 'name'>>
@@ -43,9 +43,9 @@ export type HocuspocusProviderConfiguration =
43
43
  )
44
44
 
45
45
  export interface CompleteHocuspocusProviderConfiguration {
46
- /**
47
- * The identifier/name of your document
48
- */
46
+ /**
47
+ * The identifier/name of your document
48
+ */
49
49
  name: string,
50
50
  /**
51
51
  * The actual Y.js document
@@ -96,6 +96,16 @@ export interface CompleteHocuspocusProviderConfiguration {
96
96
  * Don’t output any warnings.
97
97
  */
98
98
  quiet: boolean,
99
+
100
+ /**
101
+ * Pass `false` to start the connection manually.
102
+ */
103
+ connect: boolean,
104
+
105
+ /**
106
+ * Pass `false` to close the connection manually.
107
+ */
108
+ preserveConnection: boolean,
99
109
  }
100
110
 
101
111
  export class HocuspocusProvider extends EventEmitter {
@@ -124,6 +134,8 @@ export class HocuspocusProvider extends EventEmitter {
124
134
  onAwarenessChange: () => null,
125
135
  onStateless: () => null,
126
136
  quiet: false,
137
+ connect: true,
138
+ preserveConnection: true,
127
139
  }
128
140
 
129
141
  subscribedToBroadcastChannel = false
@@ -209,7 +221,7 @@ export class HocuspocusProvider extends EventEmitter {
209
221
 
210
222
  boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this)
211
223
 
212
- boundBeforeUnload = this.beforeUnload.bind(this)
224
+ boundPageUnload = this.pageUnload.bind(this)
213
225
 
214
226
  boundOnOpen = this.onOpen.bind(this)
215
227
 
@@ -242,6 +254,7 @@ export class HocuspocusProvider extends EventEmitter {
242
254
 
243
255
  this.configuration.websocketProvider = new HocuspocusProviderWebsocket({
244
256
  url: websocketProviderConfig.url,
257
+ connect: websocketProviderConfig.connect,
245
258
  parameters: websocketProviderConfig.parameters,
246
259
  })
247
260
  }
@@ -261,8 +274,16 @@ export class HocuspocusProvider extends EventEmitter {
261
274
  return this.unsyncedChanges > 0
262
275
  }
263
276
 
264
- updateUnsyncedChanges(unsyncedChanges = 0) {
265
- this.unsyncedChanges += unsyncedChanges
277
+ incrementUnsyncedChanges() {
278
+ this.unsyncedChanges += 1
279
+ this.emit('unsyncedChanges', this.unsyncedChanges)
280
+ }
281
+
282
+ decrementUnsyncedChanges() {
283
+ this.unsyncedChanges -= 1
284
+ if (this.unsyncedChanges === 0) {
285
+ this.synced = true
286
+ }
266
287
  this.emit('unsyncedChanges', this.unsyncedChanges)
267
288
  }
268
289
 
@@ -270,7 +291,7 @@ export class HocuspocusProvider extends EventEmitter {
270
291
  this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name })
271
292
  }
272
293
 
273
- beforeUnload() {
294
+ pageUnload() {
274
295
  removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload')
275
296
  }
276
297
 
@@ -279,7 +300,7 @@ export class HocuspocusProvider extends EventEmitter {
279
300
  return
280
301
  }
281
302
 
282
- window.addEventListener('beforeunload', this.boundBeforeUnload)
303
+ window.addEventListener('unload', this.boundPageUnload)
283
304
  }
284
305
 
285
306
  sendStateless(payload: string) {
@@ -291,7 +312,7 @@ export class HocuspocusProvider extends EventEmitter {
291
312
  return
292
313
  }
293
314
 
294
- this.updateUnsyncedChanges(1)
315
+ this.incrementUnsyncedChanges()
295
316
  this.send(UpdateMessage, { update, documentName: this.configuration.name }, true)
296
317
  }
297
318
 
@@ -305,6 +326,12 @@ export class HocuspocusProvider extends EventEmitter {
305
326
  }, true)
306
327
  }
307
328
 
329
+ /**
330
+ * Indicates whether a first handshake with the server has been established
331
+ *
332
+ * Note: this does not mean all updates from the client have been persisted to the backend. For this,
333
+ * use `hasUnsyncedChanges`.
334
+ */
308
335
  get synced(): boolean {
309
336
  return this.isSynced
310
337
  }
@@ -314,10 +341,6 @@ export class HocuspocusProvider extends EventEmitter {
314
341
  return
315
342
  }
316
343
 
317
- if (state && this.unsyncedChanges > 0) {
318
- this.updateUnsyncedChanges(-1 * this.unsyncedChanges)
319
- }
320
-
321
344
  this.isSynced = state
322
345
  this.emit('synced', { state })
323
346
  this.emit('sync', { state })
@@ -339,6 +362,9 @@ export class HocuspocusProvider extends EventEmitter {
339
362
  disconnect() {
340
363
  this.disconnectBroadcastChannel()
341
364
  this.configuration.websocketProvider.detach(this)
365
+ if (!this.configuration.preserveConnection) {
366
+ this.configuration.websocketProvider.disconnect()
367
+ }
342
368
  }
343
369
 
344
370
  async onOpen(event: Event) {
@@ -366,6 +392,7 @@ export class HocuspocusProvider extends EventEmitter {
366
392
  }
367
393
 
368
394
  startSync() {
395
+ this.incrementUnsyncedChanges()
369
396
  this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name })
370
397
 
371
398
  if (this.awareness.getLocalState() !== null) {
@@ -378,7 +405,9 @@ export class HocuspocusProvider extends EventEmitter {
378
405
  }
379
406
 
380
407
  send(message: ConstructableOutgoingMessage, args: any, broadcast = false) {
381
- if (!this.isConnected) return
408
+ if (!this.isConnected) {
409
+ return
410
+ }
382
411
 
383
412
  if (broadcast) {
384
413
  this.mux(() => { this.broadcast(message, args) })
@@ -403,7 +432,7 @@ export class HocuspocusProvider extends EventEmitter {
403
432
 
404
433
  this.emit('message', { event, message: new IncomingMessage(event.data) })
405
434
 
406
- new MessageReceiver(message).apply(this)
435
+ new MessageReceiver(message).apply(this, true)
407
436
  }
408
437
 
409
438
  onClose(event: CloseEvent) {
@@ -455,7 +484,7 @@ export class HocuspocusProvider extends EventEmitter {
455
484
  return
456
485
  }
457
486
 
458
- window.removeEventListener('beforeunload', this.boundBeforeUnload)
487
+ window.removeEventListener('unload', this.boundPageUnload)
459
488
  }
460
489
 
461
490
  permissionDeniedHandler(reason: string) {
@@ -1,18 +1,19 @@
1
- import * as time from 'lib0/time'
2
- import * as mutex from 'lib0/mutex'
3
- import * as url from 'lib0/url'
4
- import type { MessageEvent } from 'ws'
5
- import { retry } from '@lifeomic/attempt'
6
1
  import {
7
2
  Forbidden, MessageTooBig, Unauthorized, WsReadyStates,
8
3
  } from '@hocuspocus/common'
4
+ import { retry } from '@lifeomic/attempt'
5
+ import * as mutex from 'lib0/mutex'
6
+ import * as time from 'lib0/time'
7
+ import * as url from 'lib0/url'
8
+ import type { MessageEvent } from 'ws'
9
9
  import { Event } from 'ws'
10
10
  import EventEmitter from './EventEmitter.js'
11
+ import { HocuspocusProvider } from './HocuspocusProvider.js'
11
12
  import {
12
- onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatusParameters, WebSocketStatus,
13
+ WebSocketStatus,
13
14
  onAwarenessChangeParameters, onAwarenessUpdateParameters,
15
+ onCloseParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onStatusParameters,
14
16
  } from './types.js'
15
- import { HocuspocusProvider } from './HocuspocusProvider.js'
16
17
 
17
18
  export type HocuspocusProviderWebsocketConfiguration =
18
19
  Required<Pick<CompleteHocuspocusProviderWebsocketConfiguration, 'url'>>
@@ -91,6 +92,8 @@ export interface CompleteHocuspocusProviderWebsocketConfiguration {
91
92
  }
92
93
 
93
94
  export class HocuspocusProviderWebsocket extends EventEmitter {
95
+ private messageQueue: any[] = []
96
+
94
97
  public configuration: CompleteHocuspocusProviderWebsocketConfiguration = {
95
98
  url: '',
96
99
  // @ts-ignore
@@ -208,6 +211,10 @@ export class HocuspocusProviderWebsocket extends EventEmitter {
208
211
  }
209
212
 
210
213
  attach(provider: HocuspocusProvider) {
214
+ if (this.status === WebSocketStatus.Disconnected && this.shouldConnect) {
215
+ this.connect()
216
+ }
217
+
211
218
  if (this.receivedOnOpenPayload) {
212
219
  provider.onOpen(this.receivedOnOpenPayload)
213
220
  }
@@ -241,6 +248,8 @@ export class HocuspocusProviderWebsocket extends EventEmitter {
241
248
  this.cancelWebsocketRetry = undefined
242
249
  }
243
250
 
251
+ this.receivedOnOpenPayload = undefined
252
+ this.receivedOnStatusPayload = undefined
244
253
  this.shouldConnect = true
245
254
 
246
255
  const abortableRetry = () => {
@@ -285,6 +294,7 @@ export class HocuspocusProviderWebsocket extends EventEmitter {
285
294
  createWebSocketConnection() {
286
295
  return new Promise((resolve, reject) => {
287
296
  if (this.webSocket) {
297
+ this.messageQueue = []
288
298
  this.webSocket.close()
289
299
  this.webSocket = null
290
300
  }
@@ -326,6 +336,8 @@ export class HocuspocusProviderWebsocket extends EventEmitter {
326
336
  this.status = WebSocketStatus.Connected
327
337
  this.emit('status', { status: WebSocketStatus.Connected })
328
338
  this.emit('connect')
339
+ this.messageQueue.forEach(message => this.send(message))
340
+ this.messageQueue = []
329
341
  }
330
342
  }
331
343
 
@@ -338,6 +350,8 @@ export class HocuspocusProviderWebsocket extends EventEmitter {
338
350
  this.connectionAttempt = null
339
351
  }
340
352
 
353
+ closeTries = 0
354
+
341
355
  checkConnection() {
342
356
  // Don’t check the connection when it’s not even established
343
357
  if (this.status !== WebSocketStatus.Connected) {
@@ -356,7 +370,21 @@ export class HocuspocusProviderWebsocket extends EventEmitter {
356
370
 
357
371
  // No message received in a long time, not even your own
358
372
  // Awareness updates, which are updated every 15 seconds.
359
- this.webSocket?.close()
373
+ this.closeTries += 1
374
+ // https://bugs.webkit.org/show_bug.cgi?id=247943
375
+ if (this.closeTries > 2) {
376
+ this.onClose({
377
+ event: {
378
+ code: 4408,
379
+ reason: 'forced',
380
+ },
381
+ })
382
+ this.closeTries = 0
383
+ } else {
384
+ this.webSocket?.close()
385
+ this.messageQueue = []
386
+ }
387
+
360
388
  }
361
389
 
362
390
  registerEventListeners() {
@@ -391,6 +419,7 @@ export class HocuspocusProviderWebsocket extends EventEmitter {
391
419
 
392
420
  try {
393
421
  this.webSocket.close()
422
+ this.messageQueue = []
394
423
  } catch {
395
424
  //
396
425
  }
@@ -399,10 +428,13 @@ export class HocuspocusProviderWebsocket extends EventEmitter {
399
428
  send(message: any) {
400
429
  if (this.webSocket?.readyState === WsReadyStates.Open) {
401
430
  this.webSocket.send(message)
431
+ } else {
432
+ this.messageQueue.push(message)
402
433
  }
403
434
  }
404
435
 
405
436
  onClose({ event }: onCloseParameters) {
437
+ this.closeTries = 0
406
438
  this.webSocket = null
407
439
 
408
440
  if (this.status === WebSocketStatus.Connected) {