@hocuspocus/provider
Advanced tools
Comparing version 1.0.0-alpha.14 to 1.0.0-alpha.15
@@ -6,2 +6,10 @@ # Change Log | ||
# [1.0.0-alpha.15](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.14...@hocuspocus/provider@1.0.0-alpha.15) (2021-09-14) | ||
**Note:** Version bump only for package @hocuspocus/provider | ||
# [1.0.0-alpha.14](https://github.com/ueberdosis/hocuspocus/compare/@hocuspocus/provider@1.0.0-alpha.13...@hocuspocus/provider@1.0.0-alpha.14) (2021-09-03) | ||
@@ -8,0 +16,0 @@ |
import * as Y from 'yjs'; | ||
import { retry } from '@lifeomic/attempt'; | ||
@@ -261,4 +262,2 @@ /** | ||
const floor = Math.floor; | ||
const round = Math.round; | ||
const log10 = Math.log10; | ||
@@ -1759,3 +1758,2 @@ /** | ||
// @ts-nocheck | ||
var WebSocketStatus; | ||
@@ -1771,2 +1769,7 @@ (function (WebSocketStatus) { | ||
this.options = { | ||
// @ts-ignore | ||
document: undefined, | ||
// @ts-ignore | ||
awareness: undefined, | ||
WebSocketPolyfill: undefined, | ||
url: '', | ||
@@ -1776,10 +1779,23 @@ name: '', | ||
parameters: {}, | ||
debug: false, | ||
connect: true, | ||
broadcast: true, | ||
forceSyncInterval: false, | ||
reconnectTimeoutBase: 1200, | ||
maxReconnectTimeout: 2500, | ||
// TODO: this should depend on awareness.outdatedTime | ||
messageReconnectTimeout: 30000, | ||
// 1 second | ||
delay: 1000, | ||
// instant | ||
initialDelay: 0, | ||
// double the delay each time | ||
factor: 2, | ||
// unlimited retries | ||
maxAttempts: 0, | ||
// wait at least 1 second | ||
minDelay: 1000, | ||
// at least every 30 seconds | ||
maxDelay: 30000, | ||
// randomize | ||
jitter: true, | ||
// retry forever | ||
timeout: 0, | ||
onAuthenticated: () => null, | ||
@@ -1803,3 +1819,2 @@ onAuthenticationFailed: () => null, | ||
this.status = WebSocketStatus.Disconnected; | ||
this.failedConnectionAttempts = 0; | ||
this.isSynced = false; | ||
@@ -1813,7 +1828,6 @@ this.isAuthenticated = false; | ||
}; | ||
this.connectionAttempt = null; | ||
this.setOptions(options); | ||
this.options.document = options.document ? options.document : new Y.Doc(); | ||
this.options.awareness = options.awareness ? options.awareness : new Awareness(this.document); | ||
this.options.WebSocketPolyfill = options.WebSocketPolyfill ? options.WebSocketPolyfill : WebSocket; | ||
this.shouldConnect = options.connect !== undefined ? options.connect : this.shouldConnect; | ||
this.on('open', this.options.onOpen); | ||
@@ -1833,21 +1847,21 @@ this.on('authenticated', this.options.onAuthenticated); | ||
this.awareness.on('update', () => { | ||
this.emit('awarenessUpdate', { | ||
states: awarenessStatesToArray(this.awareness.getStates()), | ||
}); | ||
this.emit('awarenessUpdate', { states: awarenessStatesToArray(this.awareness.getStates()) }); | ||
}); | ||
this.awareness.on('change', () => { | ||
this.emit('awarenessChange', { | ||
states: awarenessStatesToArray(this.awareness.getStates()), | ||
}); | ||
this.emit('awarenessChange', { states: awarenessStatesToArray(this.awareness.getStates()) }); | ||
}); | ||
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.options.messageReconnectTimeout / 10); | ||
this.document.on('update', this.documentUpdateHandler.bind(this)); | ||
this.awareness.on('update', this.awarenessUpdateHandler.bind(this)); | ||
this.registerBeforeUnloadEventListener(); | ||
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.options.messageReconnectTimeout / 10); | ||
if (this.options.forceSyncInterval) { | ||
this.intervals.forceSync = setInterval(this.forceSync.bind(this), this.options.forceSyncInterval); | ||
} | ||
if (this.options.connect) { | ||
this.connect(); | ||
if (typeof options.connect !== 'undefined') { | ||
this.shouldConnect = options.connect; | ||
} | ||
if (!this.shouldConnect) { | ||
return; | ||
} | ||
this.connect(); | ||
} | ||
@@ -1857,2 +1871,51 @@ setOptions(options = {}) { | ||
} | ||
connect() { | ||
if (this.status === WebSocketStatus.Connected) { | ||
return; | ||
} | ||
this.shouldConnect = true; | ||
this.subscribeToBroadcastChannel(); | ||
retry(this.createWebSocketConnection.bind(this), { | ||
delay: this.options.delay, | ||
initialDelay: this.options.initialDelay, | ||
factor: this.options.factor, | ||
maxAttempts: this.options.maxAttempts, | ||
minDelay: this.options.minDelay, | ||
maxDelay: this.options.maxDelay, | ||
jitter: this.options.jitter, | ||
timeout: this.options.timeout, | ||
}); | ||
} | ||
createWebSocketConnection() { | ||
return new Promise((resolve, reject) => { | ||
// Init the WebSocket connection | ||
this.webSocket = new this.options.WebSocketPolyfill(this.url); | ||
this.webSocket.binaryType = 'arraybuffer'; | ||
this.webSocket.onmessage = this.onMessage.bind(this); | ||
this.webSocket.onclose = this.onClose.bind(this); | ||
this.webSocket.onopen = this.onOpen.bind(this); | ||
this.webSocket.onerror = () => { | ||
reject(); | ||
}; | ||
// Reset the status | ||
this.synced = false; | ||
this.status = WebSocketStatus.Connecting; | ||
this.emit('status', { status: 'connecting' }); | ||
// Store resolve/reject for later use | ||
this.connectionAttempt = { | ||
resolve, | ||
reject, | ||
}; | ||
}); | ||
} | ||
resolveConnectionAttempt() { | ||
var _a; | ||
(_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.resolve(); | ||
this.connectionAttempt = null; | ||
} | ||
rejectConnectionAttempt() { | ||
var _a; | ||
(_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject(); | ||
this.connectionAttempt = null; | ||
} | ||
get document() { | ||
@@ -1865,7 +1928,7 @@ return this.options.document; | ||
checkConnection() { | ||
// Don’t close the connection when it’s not established anyway | ||
// Don’t check the connection when it’s not even established | ||
if (this.status !== WebSocketStatus.Connected) { | ||
return; | ||
} | ||
// Don’t just close then connection while waiting for the first message | ||
// Don’t close then connection while waiting for the first message | ||
if (!this.lastMessageReceived) { | ||
@@ -1911,3 +1974,2 @@ return; | ||
this.emit('authenticationFailed', { reason }); | ||
this.log('Permission denied', reason); | ||
this.isAuthenticated = false; | ||
@@ -1946,9 +2008,2 @@ this.shouldConnect = false; | ||
} | ||
connect() { | ||
this.shouldConnect = true; | ||
if (this.status !== WebSocketStatus.Connected) { | ||
this.createWebSocketConnection(); | ||
this.subscribeToBroadcastChannel(); | ||
} | ||
} | ||
disconnect() { | ||
@@ -1967,15 +2022,2 @@ this.shouldConnect = false; | ||
} | ||
createWebSocketConnection() { | ||
if (this.webSocket !== null) { | ||
return; | ||
} | ||
this.webSocket = new this.options.WebSocketPolyfill(this.url); | ||
this.webSocket.binaryType = 'arraybuffer'; | ||
this.status = WebSocketStatus.Connecting; | ||
this.synced = false; | ||
this.webSocket.onmessage = this.onMessage.bind(this); | ||
this.webSocket.onclose = this.onClose.bind(this); | ||
this.webSocket.onopen = this.onOpen.bind(this); | ||
this.emit('status', { status: 'connecting' }); | ||
} | ||
onOpen(event) { | ||
@@ -1995,3 +2037,2 @@ this.emit('open', { event }); | ||
async webSocketConnectionEstablished() { | ||
this.failedConnectionAttempts = 0; | ||
this.status = WebSocketStatus.Connected; | ||
@@ -2001,7 +2042,9 @@ this.emit('status', { status: 'connected' }); | ||
if (this.isAuthenticationRequired) { | ||
const token = await this.getToken(); | ||
this.send(AuthenticationMessage, { token }); | ||
this.send(AuthenticationMessage, { | ||
token: await this.getToken(), | ||
}); | ||
return; | ||
} | ||
this.startSync(); | ||
this.resolveConnectionAttempt(); | ||
} | ||
@@ -2019,5 +2062,3 @@ startSync() { | ||
if (broadcast) { | ||
this.mux(() => { | ||
this.broadcast(Message, args); | ||
}); | ||
this.mux(() => { this.broadcast(Message, args); }); | ||
} | ||
@@ -2038,6 +2079,6 @@ if (this.status === WebSocketStatus.Connected) { | ||
this.emit('close', { event }); | ||
this.webSocket = null; | ||
this.isAuthenticated = false; | ||
this.webSocket = null; | ||
this.synced = false; | ||
if (this.status === WebSocketStatus.Connected) { | ||
this.synced = false; | ||
// update awareness (all users except local left) | ||
@@ -2049,16 +2090,22 @@ removeAwarenessStates(this.awareness, Array.from(this.awareness.getStates().keys()).filter(client => client !== this.document.clientID), this); | ||
} | ||
else { | ||
this.failedConnectionAttempts += 1; | ||
if (this.connectionAttempt) { | ||
// Okay, that connection attempt failed … | ||
this.rejectConnectionAttempt(); | ||
} | ||
else if (this.shouldConnect) { | ||
// The connection was closed by the server, so let’s just try again. | ||
this.connect(); | ||
} | ||
// If we’ll reconnect anyway, we’re done for now. | ||
if (this.shouldConnect) { | ||
const wait = round(min(log10(this.failedConnectionAttempts + 1) * this.options.reconnectTimeoutBase, this.options.maxReconnectTimeout)); | ||
this.log(`[close] Reconnecting in ${wait}ms …`); | ||
setTimeout(this.createWebSocketConnection.bind(this), wait); | ||
return; | ||
} | ||
if (this.status !== WebSocketStatus.Disconnected) { | ||
this.status = WebSocketStatus.Disconnected; | ||
this.emit('status', { status: 'disconnected' }); | ||
this.emit('disconnect', { event }); | ||
// The status is set correctly already. | ||
if (this.status === WebSocketStatus.Disconnected) { | ||
return; | ||
} | ||
// Let’s update the connection status. | ||
this.status = WebSocketStatus.Disconnected; | ||
this.emit('status', { status: 'disconnected' }); | ||
this.emit('disconnect', { event }); | ||
} | ||
@@ -2082,3 +2129,3 @@ destroy() { | ||
const message = new IncomingMessage(data); | ||
new MessageReceiver(message, this) | ||
new MessageReceiver(message) | ||
.setBroadcasted(true) | ||
@@ -2121,8 +2168,2 @@ .apply(this, false); | ||
} | ||
log(message) { | ||
if (!this.options.debug) { | ||
return; | ||
} | ||
console.log(message); | ||
} | ||
setAwarenessField(key, value) { | ||
@@ -2129,0 +2170,0 @@ this.awareness.setLocalStateField(key, value); |
@@ -7,2 +7,3 @@ import * as Y from 'yjs'; | ||
import { OutgoingMessage } from './OutgoingMessage'; | ||
import { ConstructableOutgoingMessage } from './types'; | ||
export declare enum WebSocketStatus { | ||
@@ -14,20 +15,83 @@ Connecting = "connecting", | ||
export interface HocuspocusProviderOptions { | ||
/** | ||
* URL of your @hocuspocus/server instance | ||
*/ | ||
url: string; | ||
/** | ||
* The identifier/name of your document | ||
*/ | ||
name: string; | ||
/** | ||
* The actual Y.js document | ||
*/ | ||
document: Y.Doc; | ||
/** | ||
* Pass `false` to start the connection manually. | ||
*/ | ||
connect: boolean; | ||
/** | ||
* Pass false to disable broadcasting between browser tabs. | ||
*/ | ||
broadcast: boolean; | ||
/** | ||
* An Awareness instance to keep the presence state of all clients. | ||
*/ | ||
awareness: Awareness; | ||
token: string | (() => string) | (() => Promise<string>); | ||
/** | ||
* A token that’s sent to the backend for authentication purposes. | ||
*/ | ||
token: string | (() => string) | (() => Promise<string>) | null; | ||
/** | ||
* URL parameters that should be added. | ||
*/ | ||
parameters: { | ||
[key: string]: any; | ||
}; | ||
/** | ||
* An optional WebSocket polyfill, for example for Node.js | ||
*/ | ||
WebSocketPolyfill: any; | ||
/** | ||
* Force syncing the document in the defined interval. | ||
*/ | ||
forceSyncInterval: false | number; | ||
reconnectTimeoutBase: number; | ||
maxReconnectTimeout: number; | ||
/** | ||
* Disconnect when no message is received for the defined amount of milliseconds. | ||
*/ | ||
messageReconnectTimeout: number; | ||
/** | ||
* The delay between each attempt in milliseconds. You can provide a factor to have the delay grow exponentially. | ||
*/ | ||
delay: number; | ||
/** | ||
* 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. | ||
*/ | ||
initialDelay: number; | ||
/** | ||
* The factor option is used to grow the delay exponentially. | ||
*/ | ||
factor: number; | ||
/** | ||
* The maximum number of attempts or 0 if there is no limit on number of attempts. | ||
*/ | ||
maxAttempts: number; | ||
/** | ||
* minDelay is used to set a lower bound of delay when jitter is enabled. This property has no effect if jitter is disabled. | ||
*/ | ||
minDelay: number; | ||
/** | ||
* 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. | ||
*/ | ||
maxDelay: number; | ||
/** | ||
* If jitter is true then the calculated delay will be a random integer value between minDelay and the calculated delay for the current iteration. | ||
*/ | ||
jitter: boolean; | ||
/** | ||
* 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. | ||
*/ | ||
timeout: number; | ||
onAuthenticated: () => void; | ||
onAuthenticationFailed: ({ reason: string }: { | ||
reason: any; | ||
onAuthenticationFailed: ({ reason }: { | ||
reason: string; | ||
}) => void; | ||
@@ -45,7 +109,5 @@ onOpen: (event: OpenEvent) => void; | ||
onAwarenessChange: (states: any) => void; | ||
debug: boolean; | ||
} | ||
export declare class HocuspocusProvider extends EventEmitter { | ||
options: HocuspocusProviderOptions; | ||
awareness: Awareness; | ||
subscribedToBroadcastChannel: boolean; | ||
@@ -55,3 +117,2 @@ webSocket: any; | ||
status: WebSocketStatus; | ||
failedConnectionAttempts: number; | ||
isSynced: boolean; | ||
@@ -62,4 +123,12 @@ isAuthenticated: boolean; | ||
intervals: any; | ||
connectionAttempt: { | ||
resolve: (value?: any) => void; | ||
reject: (reason?: any) => void; | ||
} | null; | ||
constructor(options?: Partial<HocuspocusProviderOptions>); | ||
setOptions(options?: Partial<HocuspocusProviderOptions>): void; | ||
connect(): void; | ||
createWebSocketConnection(): Promise<unknown>; | ||
resolveConnectionAttempt(): void; | ||
rejectConnectionAttempt(): void; | ||
get document(): Y.Doc; | ||
@@ -79,10 +148,8 @@ get awareness(): Awareness; | ||
get isAuthenticationRequired(): boolean; | ||
connect(): void; | ||
disconnect(): void; | ||
createWebSocketConnection(): void; | ||
onOpen(event: OpenEvent): void; | ||
getToken(): Promise<string>; | ||
getToken(): Promise<string | null>; | ||
webSocketConnectionEstablished(): Promise<void>; | ||
startSync(): void; | ||
send(Message: OutgoingMessage, args: any, broadcast?: boolean): void; | ||
send(Message: ConstructableOutgoingMessage, args: any, broadcast?: boolean): void; | ||
onMessage(event: MessageEvent): void; | ||
@@ -95,5 +162,4 @@ onClose(event: CloseEvent): void; | ||
disconnectBroadcastChannel(): void; | ||
broadcast(Message: OutgoingMessage, args: any): void; | ||
log(message: string): void; | ||
broadcast(Message: ConstructableOutgoingMessage, args?: any): void; | ||
setAwarenessField(key: string, value: any): void; | ||
} |
import { Encoder } from 'lib0/encoding'; | ||
import { AuthenticationMessage } from './OutgoingMessages/AuthenticationMessage'; | ||
import { AwarenessMessage } from './OutgoingMessages/AwarenessMessage'; | ||
import { QueryAwarenessMessage } from './OutgoingMessages/QueryAwarenessMessage'; | ||
import { SyncStepOneMessage } from './OutgoingMessages/SyncStepOneMessage'; | ||
import { SyncStepTwoMessage } from './OutgoingMessages/SyncStepTwoMessage'; | ||
import { UpdateMessage } from './OutgoingMessages/UpdateMessage'; | ||
import { Constructable } from './types'; | ||
import { ConstructableOutgoingMessage } from './types'; | ||
export declare class MessageSender { | ||
encoder: Encoder; | ||
message: any; | ||
constructor(Message: Constructable<AuthenticationMessage> | Constructable<AwarenessMessage> | Constructable<QueryAwarenessMessage> | Constructable<SyncStepOneMessage> | Constructable<SyncStepTwoMessage> | Constructable<UpdateMessage>, args?: any); | ||
constructor(Message: ConstructableOutgoingMessage, args?: any); | ||
create(): Uint8Array; | ||
@@ -14,0 +8,0 @@ send(webSocket: any): void; |
import { Awareness } from 'y-protocols/awareness'; | ||
import * as Y from 'yjs'; | ||
import { Encoder } from 'lib0/encoding'; | ||
import { AuthenticationMessage } from './OutgoingMessages/AuthenticationMessage'; | ||
import { AwarenessMessage } from './OutgoingMessages/AwarenessMessage'; | ||
import { QueryAwarenessMessage } from './OutgoingMessages/QueryAwarenessMessage'; | ||
import { SyncStepOneMessage } from './OutgoingMessages/SyncStepOneMessage'; | ||
import { SyncStepTwoMessage } from './OutgoingMessages/SyncStepTwoMessage'; | ||
import { UpdateMessage } from './OutgoingMessages/UpdateMessage'; | ||
export declare enum MessageType { | ||
@@ -28,1 +34,2 @@ Sync = 0, | ||
} | ||
export declare type ConstructableOutgoingMessage = Constructable<AuthenticationMessage> | Constructable<AwarenessMessage> | Constructable<QueryAwarenessMessage> | Constructable<SyncStepOneMessage> | Constructable<SyncStepTwoMessage> | Constructable<UpdateMessage>; |
@@ -46,5 +46,5 @@ import WebSocket from 'ws'; | ||
/** | ||
* Get the number of active connections | ||
* Get the number of active connections for this document | ||
*/ | ||
connectionsCount(): number; | ||
getConnectionsCount(): number; | ||
/** | ||
@@ -51,0 +51,0 @@ * Get an array of registered connections |
@@ -29,2 +29,10 @@ /// <reference types="node" /> | ||
/** | ||
* Get the total number of active documents | ||
*/ | ||
getDocumentsCount(): number; | ||
/** | ||
* Get the total number of active connections | ||
*/ | ||
getConnectionsCount(): number; | ||
/** | ||
* Force close one or more connections | ||
@@ -31,0 +39,0 @@ */ |
@@ -80,2 +80,3 @@ /// <reference types="node" /> | ||
documentName: string; | ||
instance: Hocuspocus; | ||
requestHeaders: IncomingHttpHeaders; | ||
@@ -91,2 +92,3 @@ requestParameters: URLSearchParams; | ||
documentName: string; | ||
instance: Hocuspocus; | ||
requestHeaders: IncomingHttpHeaders; | ||
@@ -102,2 +104,3 @@ requestParameters: URLSearchParams; | ||
documentName: string; | ||
instance: Hocuspocus; | ||
requestHeaders: IncomingHttpHeaders; | ||
@@ -122,2 +125,3 @@ requestParameters: URLSearchParams; | ||
export interface onDestroyPayload { | ||
instance: Hocuspocus; | ||
} | ||
@@ -124,0 +128,0 @@ export interface onConfigurePayload { |
{ | ||
"name": "@hocuspocus/provider", | ||
"version": "1.0.0-alpha.14", | ||
"version": "1.0.0-alpha.15", | ||
"description": "hocuspocus provider", | ||
@@ -26,2 +26,3 @@ "homepage": "https://hocuspocus.dev", | ||
"dependencies": { | ||
"@lifeomic/attempt": "^3.0.0", | ||
"lib0": "^0.2.42", | ||
@@ -31,3 +32,3 @@ "y-protocols": "^1.0.5", | ||
}, | ||
"gitHead": "3b1ae61a70eca44992e40e545b2d893a4cef75b2", | ||
"gitHead": "d84b5516f65dfe096fd6c633f96309a6f65a9811", | ||
"publishConfig": { | ||
@@ -34,0 +35,0 @@ "access": "public" |
@@ -1,2 +0,1 @@ | ||
// @ts-nocheck | ||
import * as Y from 'yjs' | ||
@@ -7,6 +6,5 @@ import * as bc from 'lib0/broadcastchannel' | ||
import * as mutex from 'lib0/mutex' | ||
import * as math from 'lib0/math' | ||
import * as url from 'lib0/url' | ||
import { CloseEvent, MessageEvent, OpenEvent } from 'ws' | ||
import { retry } from '@lifeomic/attempt' | ||
import EventEmitter from './EventEmitter' | ||
@@ -24,2 +22,3 @@ import { IncomingMessage } from './IncomingMessage' | ||
import awarenessStatesToArray from './utils/awarenessStatesToArray' | ||
import { ConstructableOutgoingMessage } from './types' | ||
@@ -33,17 +32,80 @@ export enum WebSocketStatus { | ||
export interface HocuspocusProviderOptions { | ||
/** | ||
* URL of your @hocuspocus/server instance | ||
*/ | ||
url: string, | ||
/** | ||
* The identifier/name of your document | ||
*/ | ||
name: string, | ||
/** | ||
* The actual Y.js document | ||
*/ | ||
document: Y.Doc, | ||
/** | ||
* Pass `false` to start the connection manually. | ||
*/ | ||
connect: boolean, | ||
/** | ||
* Pass false to disable broadcasting between browser tabs. | ||
*/ | ||
broadcast: boolean, | ||
/** | ||
* An Awareness instance to keep the presence state of all clients. | ||
*/ | ||
awareness: Awareness, | ||
token: string | (() => string) | (() => Promise<string>), | ||
/** | ||
* A token that’s sent to the backend for authentication purposes. | ||
*/ | ||
token: string | (() => string) | (() => Promise<string>) | null, | ||
/** | ||
* URL parameters that should be added. | ||
*/ | ||
parameters: { [key: string]: any }, | ||
/** | ||
* An optional WebSocket polyfill, for example for Node.js | ||
*/ | ||
WebSocketPolyfill: any, | ||
/** | ||
* Force syncing the document in the defined interval. | ||
*/ | ||
forceSyncInterval: false | number, | ||
reconnectTimeoutBase: number, | ||
maxReconnectTimeout: number, | ||
/** | ||
* Disconnect when no message is received for the defined amount of milliseconds. | ||
*/ | ||
messageReconnectTimeout: number, | ||
/** | ||
* The delay between each attempt in milliseconds. You can provide a factor to have the delay grow exponentially. | ||
*/ | ||
delay: number, | ||
/** | ||
* 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. | ||
*/ | ||
initialDelay: number, | ||
/** | ||
* The factor option is used to grow the delay exponentially. | ||
*/ | ||
factor: number, | ||
/** | ||
* The maximum number of attempts or 0 if there is no limit on number of attempts. | ||
*/ | ||
maxAttempts: number, | ||
/** | ||
* minDelay is used to set a lower bound of delay when jitter is enabled. This property has no effect if jitter is disabled. | ||
*/ | ||
minDelay: number, | ||
/** | ||
* 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. | ||
*/ | ||
maxDelay: number, | ||
/** | ||
* If jitter is true then the calculated delay will be a random integer value between minDelay and the calculated delay for the current iteration. | ||
*/ | ||
jitter: boolean, | ||
/** | ||
* 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. | ||
*/ | ||
timeout: number, | ||
onAuthenticated: () => void, | ||
onAuthenticationFailed: ({ reason: string }) => void, | ||
onAuthenticationFailed: ({ reason }: { reason: string }) => void, | ||
onOpen: (event: OpenEvent) => void, | ||
@@ -60,3 +122,2 @@ onConnect: () => void, | ||
onAwarenessChange: (states: any) => void, | ||
debug: boolean, | ||
} | ||
@@ -66,2 +127,7 @@ | ||
public options: HocuspocusProviderOptions = { | ||
// @ts-ignore | ||
document: undefined, | ||
// @ts-ignore | ||
awareness: undefined, | ||
WebSocketPolyfill: undefined, | ||
url: '', | ||
@@ -71,10 +137,23 @@ name: '', | ||
parameters: {}, | ||
debug: false, | ||
connect: true, | ||
broadcast: true, | ||
forceSyncInterval: false, | ||
reconnectTimeoutBase: 1200, | ||
maxReconnectTimeout: 2500, | ||
// TODO: this should depend on awareness.outdatedTime | ||
messageReconnectTimeout: 30000, | ||
// 1 second | ||
delay: 1000, | ||
// instant | ||
initialDelay: 0, | ||
// double the delay each time | ||
factor: 2, | ||
// unlimited retries | ||
maxAttempts: 0, | ||
// wait at least 1 second | ||
minDelay: 1000, | ||
// at least every 30 seconds | ||
maxDelay: 30000, | ||
// randomize | ||
jitter: true, | ||
// retry forever | ||
timeout: 0, | ||
onAuthenticated: () => null, | ||
@@ -95,4 +174,2 @@ onAuthenticationFailed: () => null, | ||
awareness: Awareness | ||
subscribedToBroadcastChannel = false | ||
@@ -104,6 +181,4 @@ | ||
status: WebSocketStatus = WebSocketStatus.Disconnected | ||
status = WebSocketStatus.Disconnected | ||
failedConnectionAttempts = 0 | ||
isSynced = false | ||
@@ -122,11 +197,13 @@ | ||
connectionAttempt: { | ||
resolve: (value?: any) => void | ||
reject: (reason?: any) => void | ||
} | null = null | ||
constructor(options: Partial<HocuspocusProviderOptions> = {}) { | ||
super() | ||
this.setOptions(options) | ||
this.options.document = options.document ? options.document : new Y.Doc() | ||
this.options.awareness = options.awareness ? options.awareness : new Awareness(this.document) | ||
this.options.WebSocketPolyfill = options.WebSocketPolyfill ? options.WebSocketPolyfill : WebSocket | ||
this.shouldConnect = options.connect !== undefined ? options.connect : this.shouldConnect | ||
@@ -148,13 +225,13 @@ this.on('open', this.options.onOpen) | ||
this.awareness.on('update', () => { | ||
this.emit('awarenessUpdate', { | ||
states: awarenessStatesToArray(this.awareness.getStates()), | ||
}) | ||
this.emit('awarenessUpdate', { states: awarenessStatesToArray(this.awareness.getStates()) }) | ||
}) | ||
this.awareness.on('change', () => { | ||
this.emit('awarenessChange', { | ||
states: awarenessStatesToArray(this.awareness.getStates()), | ||
}) | ||
this.emit('awarenessChange', { states: awarenessStatesToArray(this.awareness.getStates()) }) | ||
}) | ||
this.document.on('update', this.documentUpdateHandler.bind(this)) | ||
this.awareness.on('update', this.awarenessUpdateHandler.bind(this)) | ||
this.registerBeforeUnloadEventListener() | ||
this.intervals.connectionChecker = setInterval( | ||
@@ -165,6 +242,2 @@ this.checkConnection.bind(this), | ||
this.document.on('update', this.documentUpdateHandler.bind(this)) | ||
this.awareness.on('update', this.awarenessUpdateHandler.bind(this)) | ||
this.registerBeforeUnloadEventListener() | ||
if (this.options.forceSyncInterval) { | ||
@@ -177,5 +250,11 @@ this.intervals.forceSync = setInterval( | ||
if (this.options.connect) { | ||
this.connect() | ||
if (typeof options.connect !== 'undefined') { | ||
this.shouldConnect = options.connect | ||
} | ||
if (!this.shouldConnect) { | ||
return | ||
} | ||
this.connect() | ||
} | ||
@@ -187,2 +266,57 @@ | ||
connect() { | ||
if (this.status === WebSocketStatus.Connected) { | ||
return | ||
} | ||
this.shouldConnect = true | ||
this.subscribeToBroadcastChannel() | ||
retry(this.createWebSocketConnection.bind(this), { | ||
delay: this.options.delay, | ||
initialDelay: this.options.initialDelay, | ||
factor: this.options.factor, | ||
maxAttempts: this.options.maxAttempts, | ||
minDelay: this.options.minDelay, | ||
maxDelay: this.options.maxDelay, | ||
jitter: this.options.jitter, | ||
timeout: this.options.timeout, | ||
}) | ||
} | ||
createWebSocketConnection() { | ||
return new Promise((resolve, reject) => { | ||
// Init the WebSocket connection | ||
this.webSocket = new this.options.WebSocketPolyfill(this.url) | ||
this.webSocket.binaryType = 'arraybuffer' | ||
this.webSocket.onmessage = this.onMessage.bind(this) | ||
this.webSocket.onclose = this.onClose.bind(this) | ||
this.webSocket.onopen = this.onOpen.bind(this) | ||
this.webSocket.onerror = () => { | ||
reject() | ||
} | ||
// Reset the status | ||
this.synced = false | ||
this.status = WebSocketStatus.Connecting | ||
this.emit('status', { status: 'connecting' }) | ||
// Store resolve/reject for later use | ||
this.connectionAttempt = { | ||
resolve, | ||
reject, | ||
} | ||
}) | ||
} | ||
resolveConnectionAttempt() { | ||
this.connectionAttempt?.resolve() | ||
this.connectionAttempt = null | ||
} | ||
rejectConnectionAttempt() { | ||
this.connectionAttempt?.reject() | ||
this.connectionAttempt = null | ||
} | ||
get document() { | ||
@@ -197,3 +331,3 @@ return this.options.document | ||
checkConnection() { | ||
// Don’t close the connection when it’s not established anyway | ||
// Don’t check the connection when it’s not even established | ||
if (this.status !== WebSocketStatus.Connected) { | ||
@@ -203,3 +337,3 @@ return | ||
// Don’t just close then connection while waiting for the first message | ||
// Don’t close then connection while waiting for the first message | ||
if (!this.lastMessageReceived) { | ||
@@ -256,4 +390,2 @@ return | ||
this.emit('authenticationFailed', { reason }) | ||
this.log('Permission denied', reason) | ||
this.isAuthenticated = false | ||
@@ -303,11 +435,2 @@ this.shouldConnect = false | ||
connect() { | ||
this.shouldConnect = true | ||
if (this.status !== WebSocketStatus.Connected) { | ||
this.createWebSocketConnection() | ||
this.subscribeToBroadcastChannel() | ||
} | ||
} | ||
disconnect() { | ||
@@ -328,20 +451,2 @@ this.shouldConnect = false | ||
createWebSocketConnection() { | ||
if (this.webSocket !== null) { | ||
return | ||
} | ||
this.webSocket = new this.options.WebSocketPolyfill(this.url) | ||
this.webSocket.binaryType = 'arraybuffer' | ||
this.status = WebSocketStatus.Connecting | ||
this.synced = false | ||
this.webSocket.onmessage = this.onMessage.bind(this) | ||
this.webSocket.onclose = this.onClose.bind(this) | ||
this.webSocket.onopen = this.onOpen.bind(this) | ||
this.emit('status', { status: 'connecting' }) | ||
} | ||
onOpen(event: OpenEvent) { | ||
@@ -365,3 +470,2 @@ this.emit('open', { event }) | ||
async webSocketConnectionEstablished() { | ||
this.failedConnectionAttempts = 0 | ||
this.status = WebSocketStatus.Connected | ||
@@ -372,4 +476,5 @@ this.emit('status', { status: 'connected' }) | ||
if (this.isAuthenticationRequired) { | ||
const token = await this.getToken() | ||
this.send(AuthenticationMessage, { token }) | ||
this.send(AuthenticationMessage, { | ||
token: await this.getToken(), | ||
}) | ||
return | ||
@@ -379,2 +484,4 @@ } | ||
this.startSync() | ||
this.resolveConnectionAttempt() | ||
} | ||
@@ -393,7 +500,5 @@ | ||
send(Message: OutgoingMessage, args: any, broadcast = false) { | ||
send(Message: ConstructableOutgoingMessage, args: any, broadcast = false) { | ||
if (broadcast) { | ||
this.mux(() => { | ||
this.broadcast(Message, args) | ||
}) | ||
this.mux(() => { this.broadcast(Message, args) }) | ||
} | ||
@@ -422,8 +527,7 @@ | ||
this.webSocket = null | ||
this.isAuthenticated = false | ||
this.webSocket = null | ||
this.synced = false | ||
if (this.status === WebSocketStatus.Connected) { | ||
this.synced = false | ||
// update awareness (all users except local left) | ||
@@ -439,23 +543,26 @@ removeAwarenessStates( | ||
this.emit('disconnect', { event }) | ||
} else { | ||
this.failedConnectionAttempts += 1 | ||
} | ||
if (this.connectionAttempt) { | ||
// Okay, that connection attempt failed … | ||
this.rejectConnectionAttempt() | ||
} else if (this.shouldConnect) { | ||
// The connection was closed by the server, so let’s just try again. | ||
this.connect() | ||
} | ||
// If we’ll reconnect anyway, we’re done for now. | ||
if (this.shouldConnect) { | ||
const wait = math.round(math.min( | ||
math.log10(this.failedConnectionAttempts + 1) * this.options.reconnectTimeoutBase, | ||
this.options.maxReconnectTimeout, | ||
)) | ||
return | ||
} | ||
this.log(`[close] Reconnecting in ${wait}ms …`) | ||
setTimeout(this.createWebSocketConnection.bind(this), wait) | ||
// The status is set correctly already. | ||
if (this.status === WebSocketStatus.Disconnected) { | ||
return | ||
} | ||
if (this.status !== WebSocketStatus.Disconnected) { | ||
this.status = WebSocketStatus.Disconnected | ||
this.emit('status', { status: 'disconnected' }) | ||
this.emit('disconnect', { event }) | ||
} | ||
// Let’s update the connection status. | ||
this.status = WebSocketStatus.Disconnected | ||
this.emit('status', { status: 'disconnected' }) | ||
this.emit('disconnect', { event }) | ||
} | ||
@@ -487,3 +594,3 @@ | ||
const message = new IncomingMessage(data) | ||
new MessageReceiver(message, this) | ||
new MessageReceiver(message) | ||
.setBroadcasted(true) | ||
@@ -522,3 +629,3 @@ .apply(this, false) | ||
broadcast(Message: OutgoingMessage, args: any) { | ||
broadcast(Message: ConstructableOutgoingMessage, args?: any) { | ||
if (!this.options.broadcast) { | ||
@@ -535,10 +642,2 @@ return | ||
log(message: string): void { | ||
if (!this.options.debug) { | ||
return | ||
} | ||
console.log(message) | ||
} | ||
setAwarenessField(key: string, value: any) { | ||
@@ -545,0 +644,0 @@ this.awareness.setLocalStateField(key, value) |
import { Encoder, toUint8Array } from 'lib0/encoding' | ||
import * as bc from 'lib0/broadcastchannel' | ||
import { AuthenticationMessage } from './OutgoingMessages/AuthenticationMessage' | ||
import { AwarenessMessage } from './OutgoingMessages/AwarenessMessage' | ||
import { QueryAwarenessMessage } from './OutgoingMessages/QueryAwarenessMessage' | ||
import { SyncStepOneMessage } from './OutgoingMessages/SyncStepOneMessage' | ||
import { SyncStepTwoMessage } from './OutgoingMessages/SyncStepTwoMessage' | ||
import { UpdateMessage } from './OutgoingMessages/UpdateMessage' | ||
import { Constructable } from './types' | ||
import { ConstructableOutgoingMessage } from './types' | ||
@@ -17,10 +11,3 @@ export class MessageSender { | ||
constructor(Message: | ||
Constructable<AuthenticationMessage> | | ||
Constructable<AwarenessMessage> | | ||
Constructable<QueryAwarenessMessage> | | ||
Constructable<SyncStepOneMessage> | | ||
Constructable<SyncStepTwoMessage> | | ||
Constructable<UpdateMessage>, | ||
args: any = {}) { | ||
constructor(Message: ConstructableOutgoingMessage, args: any = {}) { | ||
this.message = new Message() | ||
@@ -27,0 +14,0 @@ this.encoder = this.message.get(args) |
import { Awareness } from 'y-protocols/awareness' | ||
import * as Y from 'yjs' | ||
import { Encoder } from 'lib0/encoding' | ||
import { AuthenticationMessage } from './OutgoingMessages/AuthenticationMessage' | ||
import { AwarenessMessage } from './OutgoingMessages/AwarenessMessage' | ||
import { QueryAwarenessMessage } from './OutgoingMessages/QueryAwarenessMessage' | ||
import { SyncStepOneMessage } from './OutgoingMessages/SyncStepOneMessage' | ||
import { SyncStepTwoMessage } from './OutgoingMessages/SyncStepTwoMessage' | ||
import { UpdateMessage } from './OutgoingMessages/UpdateMessage' | ||
@@ -30,1 +36,9 @@ export enum MessageType { | ||
} | ||
export type ConstructableOutgoingMessage = | ||
Constructable<AuthenticationMessage> | | ||
Constructable<AwarenessMessage> | | ||
Constructable<QueryAwarenessMessage> | | ||
Constructable<SyncStepOneMessage> | | ||
Constructable<SyncStepTwoMessage> | | ||
Constructable<UpdateMessage> |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
530530
5981
4
+ Added@lifeomic/attempt@^3.0.0
+ Added@lifeomic/attempt@3.1.0(transitive)