@hocuspocus/provider 2.2.2 → 2.3.0
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.
- package/dist/hocuspocus-provider.cjs +797 -689
- package/dist/hocuspocus-provider.cjs.map +1 -1
- package/dist/hocuspocus-provider.esm.js +799 -691
- package/dist/hocuspocus-provider.esm.js.map +1 -1
- package/dist/packages/extension-redis/src/Redis.d.ts +1 -1
- package/dist/packages/provider/src/HocuspocusProvider.d.ts +23 -8
- package/dist/packages/provider/src/HocuspocusProviderWebsocket.d.ts +2 -1
- package/dist/packages/provider/src/MessageReceiver.d.ts +2 -1
- package/dist/packages/provider/src/TiptapCollabProvider.d.ts +15 -0
- package/dist/packages/provider/src/types.d.ts +6 -5
- package/dist/packages/server/src/Hocuspocus.d.ts +3 -2
- package/dist/packages/server/src/MessageReceiver.d.ts +1 -1
- package/dist/packages/server/src/OutgoingMessage.d.ts +1 -0
- package/dist/packages/server/src/types.d.ts +30 -6
- package/dist/tests/provider/hasUnsyncedChanges.d.ts +1 -0
- package/dist/tests/server/afterUnloadDocument.d.ts +1 -0
- package/package.json +3 -3
- package/src/HocuspocusProvider.ts +60 -31
- package/src/HocuspocusProviderWebsocket.ts +23 -7
- package/src/MessageReceiver.ts +12 -9
- package/src/TiptapCollabProvider.ts +50 -0
- package/src/types.ts +5 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { WsReadyStates, Unauthorized, Forbidden, MessageTooBig, readAuthMessage, writeAuthentication, awarenessStatesToArray } from '@hocuspocus/common';
|
|
1
2
|
import * as Y from 'yjs';
|
|
2
|
-
import { readAuthMessage, writeAuthentication, WsReadyStates, Unauthorized, Forbidden, MessageTooBig, awarenessStatesToArray } from '@hocuspocus/common';
|
|
3
3
|
import { retry } from '@lifeomic/attempt';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -50,6 +50,22 @@ const setIfUndefined = (map, key, createT) => {
|
|
|
50
50
|
|
|
51
51
|
const create$1 = () => new Set();
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Utility module to work with Arrays.
|
|
55
|
+
*
|
|
56
|
+
* @module array
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Transforms something array-like to an actual Array.
|
|
61
|
+
*
|
|
62
|
+
* @function
|
|
63
|
+
* @template T
|
|
64
|
+
* @param {ArrayLike<T>|Iterable<T>} arraylike
|
|
65
|
+
* @return {T}
|
|
66
|
+
*/
|
|
67
|
+
const from = Array.from;
|
|
68
|
+
|
|
53
69
|
/**
|
|
54
70
|
* Utility module to work with strings.
|
|
55
71
|
*
|
|
@@ -212,22 +228,6 @@ const onChange = eventHandler => usePolyfill || addEventListener('storage', /**
|
|
|
212
228
|
/* c8 ignore next */
|
|
213
229
|
const offChange = eventHandler => usePolyfill || removeEventListener('storage', /** @type {any} */ (eventHandler));
|
|
214
230
|
|
|
215
|
-
/**
|
|
216
|
-
* Utility module to work with Arrays.
|
|
217
|
-
*
|
|
218
|
-
* @module array
|
|
219
|
-
*/
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Transforms something array-like to an actual Array.
|
|
223
|
-
*
|
|
224
|
-
* @function
|
|
225
|
-
* @template T
|
|
226
|
-
* @param {ArrayLike<T>|Iterable<T>} arraylike
|
|
227
|
-
* @return {T}
|
|
228
|
-
*/
|
|
229
|
-
const from = Array.from;
|
|
230
|
-
|
|
231
231
|
/**
|
|
232
232
|
* Utility functions for working with EcmaScript objects.
|
|
233
233
|
*
|
|
@@ -375,7 +375,6 @@ const equalityDeep = (a, b) => {
|
|
|
375
375
|
*/
|
|
376
376
|
// @ts-ignore
|
|
377
377
|
const isOneOf = (value, options) => options.includes(value);
|
|
378
|
-
/* c8 ignore stop */
|
|
379
378
|
|
|
380
379
|
/**
|
|
381
380
|
* Isomorphic module to work access the environment (query params, env variables).
|
|
@@ -507,7 +506,9 @@ const min = (a, b) => a < b ? a : b;
|
|
|
507
506
|
const max = (a, b) => a > b ? a : b;
|
|
508
507
|
|
|
509
508
|
/* eslint-env browser */
|
|
509
|
+
const BIT7 = 64;
|
|
510
510
|
const BIT8 = 128;
|
|
511
|
+
const BITS6 = 63;
|
|
511
512
|
const BITS7 = 127;
|
|
512
513
|
|
|
513
514
|
/**
|
|
@@ -865,6 +866,44 @@ const readVarUint = decoder => {
|
|
|
865
866
|
throw errorUnexpectedEndOfArray
|
|
866
867
|
};
|
|
867
868
|
|
|
869
|
+
/**
|
|
870
|
+
* Read signed integer (32bit) with variable length.
|
|
871
|
+
* 1/8th of the storage is used as encoding overhead.
|
|
872
|
+
* * numbers < 2^7 is stored in one bytlength
|
|
873
|
+
* * numbers < 2^14 is stored in two bylength
|
|
874
|
+
* @todo This should probably create the inverse ~num if number is negative - but this would be a breaking change.
|
|
875
|
+
*
|
|
876
|
+
* @function
|
|
877
|
+
* @param {Decoder} decoder
|
|
878
|
+
* @return {number} An unsigned integer.length
|
|
879
|
+
*/
|
|
880
|
+
const readVarInt = decoder => {
|
|
881
|
+
let r = decoder.arr[decoder.pos++];
|
|
882
|
+
let num = r & BITS6;
|
|
883
|
+
let mult = 64;
|
|
884
|
+
const sign = (r & BIT7) > 0 ? -1 : 1;
|
|
885
|
+
if ((r & BIT8) === 0) {
|
|
886
|
+
// don't continue reading
|
|
887
|
+
return sign * num
|
|
888
|
+
}
|
|
889
|
+
const len = decoder.arr.length;
|
|
890
|
+
while (decoder.pos < len) {
|
|
891
|
+
r = decoder.arr[decoder.pos++];
|
|
892
|
+
// num = num | ((r & binary.BITS7) << len)
|
|
893
|
+
num = num + (r & BITS7) * mult;
|
|
894
|
+
mult *= 128;
|
|
895
|
+
if (r < BIT8) {
|
|
896
|
+
return sign * num
|
|
897
|
+
}
|
|
898
|
+
/* c8 ignore start */
|
|
899
|
+
if (num > MAX_SAFE_INTEGER) {
|
|
900
|
+
throw errorIntegerOutOfRange
|
|
901
|
+
}
|
|
902
|
+
/* c8 ignore stop */
|
|
903
|
+
}
|
|
904
|
+
throw errorUnexpectedEndOfArray
|
|
905
|
+
};
|
|
906
|
+
|
|
868
907
|
/**
|
|
869
908
|
* We don't test this function anymore as we use native decoding/encoding by default now.
|
|
870
909
|
* Better not modify this anymore..
|
|
@@ -1112,6 +1151,50 @@ const publish = (room, data, origin = null) => {
|
|
|
1112
1151
|
c.subs.forEach(sub => sub(data, origin));
|
|
1113
1152
|
};
|
|
1114
1153
|
|
|
1154
|
+
/**
|
|
1155
|
+
* Mutual exclude for JavaScript.
|
|
1156
|
+
*
|
|
1157
|
+
* @module mutex
|
|
1158
|
+
*/
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* @callback mutex
|
|
1162
|
+
* @param {function():void} cb Only executed when this mutex is not in the current stack
|
|
1163
|
+
* @param {function():void} [elseCb] Executed when this mutex is in the current stack
|
|
1164
|
+
*/
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Creates a mutual exclude function with the following property:
|
|
1168
|
+
*
|
|
1169
|
+
* ```js
|
|
1170
|
+
* const mutex = createMutex()
|
|
1171
|
+
* mutex(() => {
|
|
1172
|
+
* // This function is immediately executed
|
|
1173
|
+
* mutex(() => {
|
|
1174
|
+
* // This function is not executed, as the mutex is already active.
|
|
1175
|
+
* })
|
|
1176
|
+
* })
|
|
1177
|
+
* ```
|
|
1178
|
+
*
|
|
1179
|
+
* @return {mutex} A mutual exclude function
|
|
1180
|
+
* @public
|
|
1181
|
+
*/
|
|
1182
|
+
const createMutex = () => {
|
|
1183
|
+
let token = true;
|
|
1184
|
+
return (f, g) => {
|
|
1185
|
+
if (token) {
|
|
1186
|
+
token = false;
|
|
1187
|
+
try {
|
|
1188
|
+
f();
|
|
1189
|
+
} finally {
|
|
1190
|
+
token = true;
|
|
1191
|
+
}
|
|
1192
|
+
} else if (g !== undefined) {
|
|
1193
|
+
g();
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1115
1198
|
/**
|
|
1116
1199
|
* Utility module to work with time.
|
|
1117
1200
|
*
|
|
@@ -1462,50 +1545,6 @@ const applyAwarenessUpdate = (awareness, update, origin) => {
|
|
|
1462
1545
|
}
|
|
1463
1546
|
};
|
|
1464
1547
|
|
|
1465
|
-
/**
|
|
1466
|
-
* Mutual exclude for JavaScript.
|
|
1467
|
-
*
|
|
1468
|
-
* @module mutex
|
|
1469
|
-
*/
|
|
1470
|
-
|
|
1471
|
-
/**
|
|
1472
|
-
* @callback mutex
|
|
1473
|
-
* @param {function():void} cb Only executed when this mutex is not in the current stack
|
|
1474
|
-
* @param {function():void} [elseCb] Executed when this mutex is in the current stack
|
|
1475
|
-
*/
|
|
1476
|
-
|
|
1477
|
-
/**
|
|
1478
|
-
* Creates a mutual exclude function with the following property:
|
|
1479
|
-
*
|
|
1480
|
-
* ```js
|
|
1481
|
-
* const mutex = createMutex()
|
|
1482
|
-
* mutex(() => {
|
|
1483
|
-
* // This function is immediately executed
|
|
1484
|
-
* mutex(() => {
|
|
1485
|
-
* // This function is not executed, as the mutex is already active.
|
|
1486
|
-
* })
|
|
1487
|
-
* })
|
|
1488
|
-
* ```
|
|
1489
|
-
*
|
|
1490
|
-
* @return {mutex} A mutual exclude function
|
|
1491
|
-
* @public
|
|
1492
|
-
*/
|
|
1493
|
-
const createMutex = () => {
|
|
1494
|
-
let token = true;
|
|
1495
|
-
return (f, g) => {
|
|
1496
|
-
if (token) {
|
|
1497
|
-
token = false;
|
|
1498
|
-
try {
|
|
1499
|
-
f();
|
|
1500
|
-
} finally {
|
|
1501
|
-
token = true;
|
|
1502
|
-
}
|
|
1503
|
-
} else if (g !== undefined) {
|
|
1504
|
-
g();
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
};
|
|
1508
|
-
|
|
1509
1548
|
class EventEmitter {
|
|
1510
1549
|
constructor() {
|
|
1511
1550
|
this.callbacks = {};
|
|
@@ -1541,103 +1580,461 @@ class EventEmitter {
|
|
|
1541
1580
|
}
|
|
1542
1581
|
}
|
|
1543
1582
|
|
|
1544
|
-
class IncomingMessage {
|
|
1545
|
-
constructor(data) {
|
|
1546
|
-
this.data = data;
|
|
1547
|
-
this.encoder = createEncoder();
|
|
1548
|
-
this.decoder = createDecoder(new Uint8Array(this.data));
|
|
1549
|
-
}
|
|
1550
|
-
readVarUint() {
|
|
1551
|
-
return readVarUint(this.decoder);
|
|
1552
|
-
}
|
|
1553
|
-
readVarString() {
|
|
1554
|
-
return readVarString(this.decoder);
|
|
1555
|
-
}
|
|
1556
|
-
readVarUint8Array() {
|
|
1557
|
-
return readVarUint8Array(this.decoder);
|
|
1558
|
-
}
|
|
1559
|
-
writeVarUint(type) {
|
|
1560
|
-
return writeVarUint(this.encoder, type);
|
|
1561
|
-
}
|
|
1562
|
-
writeVarString(string) {
|
|
1563
|
-
return writeVarString(this.encoder, string);
|
|
1564
|
-
}
|
|
1565
|
-
writeVarUint8Array(data) {
|
|
1566
|
-
return writeVarUint8Array(this.encoder, data);
|
|
1567
|
-
}
|
|
1568
|
-
length() {
|
|
1569
|
-
return length(this.encoder);
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
/**
|
|
1574
|
-
* @module sync-protocol
|
|
1575
|
-
*/
|
|
1576
|
-
|
|
1577
|
-
/**
|
|
1578
|
-
* @typedef {Map<number, number>} StateMap
|
|
1579
|
-
*/
|
|
1580
|
-
|
|
1581
|
-
/**
|
|
1582
|
-
* Core Yjs defines two message types:
|
|
1583
|
-
* • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
|
|
1584
|
-
* • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
|
|
1585
|
-
* received all information from the remote client.
|
|
1586
|
-
*
|
|
1587
|
-
* In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
|
|
1588
|
-
* with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
|
|
1589
|
-
* SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
|
|
1590
|
-
*
|
|
1591
|
-
* In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
|
|
1592
|
-
* When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
|
|
1593
|
-
* with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
|
|
1594
|
-
* client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
|
|
1595
|
-
* easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
|
|
1596
|
-
* Therefore it is necesarry that the client initiates the sync.
|
|
1597
|
-
*
|
|
1598
|
-
* Construction of a message:
|
|
1599
|
-
* [messageType : varUint, message definition..]
|
|
1600
|
-
*
|
|
1601
|
-
* Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
|
|
1602
|
-
*
|
|
1603
|
-
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
|
|
1604
|
-
*/
|
|
1605
|
-
|
|
1606
|
-
const messageYjsSyncStep1 = 0;
|
|
1607
|
-
const messageYjsSyncStep2 = 1;
|
|
1608
|
-
const messageYjsUpdate = 2;
|
|
1609
|
-
|
|
1610
1583
|
/**
|
|
1611
|
-
*
|
|
1584
|
+
* Utility module to work with urls.
|
|
1612
1585
|
*
|
|
1613
|
-
* @
|
|
1614
|
-
* @param {Y.Doc} doc
|
|
1586
|
+
* @module url
|
|
1615
1587
|
*/
|
|
1616
|
-
const writeSyncStep1 = (encoder, doc) => {
|
|
1617
|
-
writeVarUint(encoder, messageYjsSyncStep1);
|
|
1618
|
-
const sv = Y.encodeStateVector(doc);
|
|
1619
|
-
writeVarUint8Array(encoder, sv);
|
|
1620
|
-
};
|
|
1621
1588
|
|
|
1622
1589
|
/**
|
|
1623
|
-
* @param {
|
|
1624
|
-
* @
|
|
1625
|
-
* @param {Uint8Array} [encodedStateVector]
|
|
1590
|
+
* @param {Object<string,string>} params
|
|
1591
|
+
* @return {string}
|
|
1626
1592
|
*/
|
|
1627
|
-
const
|
|
1628
|
-
|
|
1629
|
-
writeVarUint8Array(encoder, Y.encodeStateAsUpdate(doc, encodedStateVector));
|
|
1630
|
-
};
|
|
1593
|
+
const encodeQueryParams = params =>
|
|
1594
|
+
map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
|
|
1631
1595
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1596
|
+
var MessageType;
|
|
1597
|
+
(function (MessageType) {
|
|
1598
|
+
MessageType[MessageType["Sync"] = 0] = "Sync";
|
|
1599
|
+
MessageType[MessageType["Awareness"] = 1] = "Awareness";
|
|
1600
|
+
MessageType[MessageType["Auth"] = 2] = "Auth";
|
|
1601
|
+
MessageType[MessageType["QueryAwareness"] = 3] = "QueryAwareness";
|
|
1602
|
+
MessageType[MessageType["Stateless"] = 5] = "Stateless";
|
|
1603
|
+
MessageType[MessageType["CLOSE"] = 7] = "CLOSE";
|
|
1604
|
+
MessageType[MessageType["SyncStatus"] = 8] = "SyncStatus";
|
|
1605
|
+
})(MessageType || (MessageType = {}));
|
|
1606
|
+
var WebSocketStatus;
|
|
1607
|
+
(function (WebSocketStatus) {
|
|
1608
|
+
WebSocketStatus["Connecting"] = "connecting";
|
|
1609
|
+
WebSocketStatus["Connected"] = "connected";
|
|
1610
|
+
WebSocketStatus["Disconnected"] = "disconnected";
|
|
1611
|
+
})(WebSocketStatus || (WebSocketStatus = {}));
|
|
1612
|
+
|
|
1613
|
+
class HocuspocusProviderWebsocket extends EventEmitter {
|
|
1614
|
+
constructor(configuration) {
|
|
1615
|
+
super();
|
|
1616
|
+
this.messageQueue = [];
|
|
1617
|
+
this.configuration = {
|
|
1618
|
+
url: '',
|
|
1619
|
+
// @ts-ignore
|
|
1620
|
+
document: undefined,
|
|
1621
|
+
// @ts-ignore
|
|
1622
|
+
awareness: undefined,
|
|
1623
|
+
WebSocketPolyfill: undefined,
|
|
1624
|
+
parameters: {},
|
|
1625
|
+
connect: true,
|
|
1626
|
+
broadcast: true,
|
|
1627
|
+
forceSyncInterval: false,
|
|
1628
|
+
// TODO: this should depend on awareness.outdatedTime
|
|
1629
|
+
messageReconnectTimeout: 30000,
|
|
1630
|
+
// 1 second
|
|
1631
|
+
delay: 1000,
|
|
1632
|
+
// instant
|
|
1633
|
+
initialDelay: 0,
|
|
1634
|
+
// double the delay each time
|
|
1635
|
+
factor: 2,
|
|
1636
|
+
// unlimited retries
|
|
1637
|
+
maxAttempts: 0,
|
|
1638
|
+
// wait at least 1 second
|
|
1639
|
+
minDelay: 1000,
|
|
1640
|
+
// at least every 30 seconds
|
|
1641
|
+
maxDelay: 30000,
|
|
1642
|
+
// randomize
|
|
1643
|
+
jitter: true,
|
|
1644
|
+
// retry forever
|
|
1645
|
+
timeout: 0,
|
|
1646
|
+
onOpen: () => null,
|
|
1647
|
+
onConnect: () => null,
|
|
1648
|
+
onMessage: () => null,
|
|
1649
|
+
onOutgoingMessage: () => null,
|
|
1650
|
+
onStatus: () => null,
|
|
1651
|
+
onDisconnect: () => null,
|
|
1652
|
+
onClose: () => null,
|
|
1653
|
+
onDestroy: () => null,
|
|
1654
|
+
onAwarenessUpdate: () => null,
|
|
1655
|
+
onAwarenessChange: () => null,
|
|
1656
|
+
quiet: false,
|
|
1657
|
+
};
|
|
1658
|
+
this.subscribedToBroadcastChannel = false;
|
|
1659
|
+
this.webSocket = null;
|
|
1660
|
+
this.shouldConnect = true;
|
|
1661
|
+
this.status = WebSocketStatus.Disconnected;
|
|
1662
|
+
this.lastMessageReceived = 0;
|
|
1663
|
+
this.mux = createMutex();
|
|
1664
|
+
this.intervals = {
|
|
1665
|
+
forceSync: null,
|
|
1666
|
+
connectionChecker: null,
|
|
1667
|
+
};
|
|
1668
|
+
this.connectionAttempt = null;
|
|
1669
|
+
this.receivedOnOpenPayload = undefined;
|
|
1670
|
+
this.receivedOnStatusPayload = undefined;
|
|
1671
|
+
this.boundConnect = this.connect.bind(this);
|
|
1672
|
+
this.setConfiguration(configuration);
|
|
1673
|
+
this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
|
|
1674
|
+
this.on('open', this.configuration.onOpen);
|
|
1675
|
+
this.on('open', this.onOpen.bind(this));
|
|
1676
|
+
this.on('connect', this.configuration.onConnect);
|
|
1677
|
+
this.on('message', this.configuration.onMessage);
|
|
1678
|
+
this.on('outgoingMessage', this.configuration.onOutgoingMessage);
|
|
1679
|
+
this.on('status', this.configuration.onStatus);
|
|
1680
|
+
this.on('status', this.onStatus.bind(this));
|
|
1681
|
+
this.on('disconnect', this.configuration.onDisconnect);
|
|
1682
|
+
this.on('close', this.configuration.onClose);
|
|
1683
|
+
this.on('destroy', this.configuration.onDestroy);
|
|
1684
|
+
this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
|
|
1685
|
+
this.on('awarenessChange', this.configuration.onAwarenessChange);
|
|
1686
|
+
this.on('close', this.onClose.bind(this));
|
|
1687
|
+
this.on('message', this.onMessage.bind(this));
|
|
1688
|
+
this.registerEventListeners();
|
|
1689
|
+
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
|
|
1690
|
+
if (typeof configuration.connect !== 'undefined') {
|
|
1691
|
+
this.shouldConnect = configuration.connect;
|
|
1692
|
+
}
|
|
1693
|
+
if (!this.shouldConnect) {
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
this.connect();
|
|
1697
|
+
}
|
|
1698
|
+
async onOpen(event) {
|
|
1699
|
+
this.receivedOnOpenPayload = event;
|
|
1700
|
+
}
|
|
1701
|
+
async onStatus(data) {
|
|
1702
|
+
this.receivedOnStatusPayload = data;
|
|
1703
|
+
}
|
|
1704
|
+
attach(provider) {
|
|
1705
|
+
if (this.status === WebSocketStatus.Disconnected && this.shouldConnect) {
|
|
1706
|
+
this.connect();
|
|
1707
|
+
}
|
|
1708
|
+
if (this.receivedOnOpenPayload) {
|
|
1709
|
+
provider.onOpen(this.receivedOnOpenPayload);
|
|
1710
|
+
}
|
|
1711
|
+
if (this.receivedOnStatusPayload) {
|
|
1712
|
+
provider.onStatus(this.receivedOnStatusPayload);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
detach(provider) {
|
|
1716
|
+
// tell the server to remove the listener
|
|
1717
|
+
}
|
|
1718
|
+
setConfiguration(configuration = {}) {
|
|
1719
|
+
this.configuration = { ...this.configuration, ...configuration };
|
|
1720
|
+
}
|
|
1721
|
+
async connect() {
|
|
1722
|
+
if (this.status === WebSocketStatus.Connected) {
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
// Always cancel any previously initiated connection retryer instances
|
|
1726
|
+
if (this.cancelWebsocketRetry) {
|
|
1727
|
+
this.cancelWebsocketRetry();
|
|
1728
|
+
this.cancelWebsocketRetry = undefined;
|
|
1729
|
+
}
|
|
1730
|
+
this.receivedOnOpenPayload = undefined;
|
|
1731
|
+
this.receivedOnStatusPayload = undefined;
|
|
1732
|
+
this.shouldConnect = true;
|
|
1733
|
+
const abortableRetry = () => {
|
|
1734
|
+
let cancelAttempt = false;
|
|
1735
|
+
const retryPromise = retry(this.createWebSocketConnection.bind(this), {
|
|
1736
|
+
delay: this.configuration.delay,
|
|
1737
|
+
initialDelay: this.configuration.initialDelay,
|
|
1738
|
+
factor: this.configuration.factor,
|
|
1739
|
+
maxAttempts: this.configuration.maxAttempts,
|
|
1740
|
+
minDelay: this.configuration.minDelay,
|
|
1741
|
+
maxDelay: this.configuration.maxDelay,
|
|
1742
|
+
jitter: this.configuration.jitter,
|
|
1743
|
+
timeout: this.configuration.timeout,
|
|
1744
|
+
beforeAttempt: context => {
|
|
1745
|
+
if (!this.shouldConnect || cancelAttempt) {
|
|
1746
|
+
context.abort();
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
}).catch((error) => {
|
|
1750
|
+
// If we aborted the connection attempt then don’t throw an error
|
|
1751
|
+
// ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
|
|
1752
|
+
if (error && error.code !== 'ATTEMPT_ABORTED') {
|
|
1753
|
+
throw error;
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
return {
|
|
1757
|
+
retryPromise,
|
|
1758
|
+
cancelFunc: () => {
|
|
1759
|
+
cancelAttempt = true;
|
|
1760
|
+
},
|
|
1761
|
+
};
|
|
1762
|
+
};
|
|
1763
|
+
const { retryPromise, cancelFunc } = abortableRetry();
|
|
1764
|
+
this.cancelWebsocketRetry = cancelFunc;
|
|
1765
|
+
return retryPromise;
|
|
1766
|
+
}
|
|
1767
|
+
createWebSocketConnection() {
|
|
1768
|
+
return new Promise((resolve, reject) => {
|
|
1769
|
+
if (this.webSocket) {
|
|
1770
|
+
this.messageQueue = [];
|
|
1771
|
+
this.webSocket.close();
|
|
1772
|
+
this.webSocket = null;
|
|
1773
|
+
}
|
|
1774
|
+
// Init the WebSocket connection
|
|
1775
|
+
const ws = new this.configuration.WebSocketPolyfill(this.url);
|
|
1776
|
+
ws.binaryType = 'arraybuffer';
|
|
1777
|
+
ws.onmessage = (payload) => this.emit('message', payload);
|
|
1778
|
+
ws.onclose = (payload) => this.emit('close', { event: payload });
|
|
1779
|
+
ws.onopen = (payload) => this.emit('open', payload);
|
|
1780
|
+
ws.onerror = (err) => {
|
|
1781
|
+
reject(err);
|
|
1782
|
+
};
|
|
1783
|
+
this.webSocket = ws;
|
|
1784
|
+
// Reset the status
|
|
1785
|
+
this.status = WebSocketStatus.Connecting;
|
|
1786
|
+
this.emit('status', { status: WebSocketStatus.Connecting });
|
|
1787
|
+
// Store resolve/reject for later use
|
|
1788
|
+
this.connectionAttempt = {
|
|
1789
|
+
resolve,
|
|
1790
|
+
reject,
|
|
1791
|
+
};
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
onMessage(event) {
|
|
1795
|
+
this.resolveConnectionAttempt();
|
|
1796
|
+
this.lastMessageReceived = getUnixTime();
|
|
1797
|
+
}
|
|
1798
|
+
resolveConnectionAttempt() {
|
|
1799
|
+
if (this.connectionAttempt) {
|
|
1800
|
+
this.connectionAttempt.resolve();
|
|
1801
|
+
this.connectionAttempt = null;
|
|
1802
|
+
this.status = WebSocketStatus.Connected;
|
|
1803
|
+
this.emit('status', { status: WebSocketStatus.Connected });
|
|
1804
|
+
this.emit('connect');
|
|
1805
|
+
this.messageQueue.forEach(message => this.send(message));
|
|
1806
|
+
this.messageQueue = [];
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
stopConnectionAttempt() {
|
|
1810
|
+
this.connectionAttempt = null;
|
|
1811
|
+
}
|
|
1812
|
+
rejectConnectionAttempt() {
|
|
1813
|
+
var _a;
|
|
1814
|
+
(_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
|
|
1815
|
+
this.connectionAttempt = null;
|
|
1816
|
+
}
|
|
1817
|
+
checkConnection() {
|
|
1818
|
+
var _a;
|
|
1819
|
+
// Don’t check the connection when it’s not even established
|
|
1820
|
+
if (this.status !== WebSocketStatus.Connected) {
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
// Don’t close then connection while waiting for the first message
|
|
1824
|
+
if (!this.lastMessageReceived) {
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
// Don’t close the connection when a message was received recently
|
|
1828
|
+
if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
// No message received in a long time, not even your own
|
|
1832
|
+
// Awareness updates, which are updated every 15 seconds.
|
|
1833
|
+
(_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
|
|
1834
|
+
this.messageQueue = [];
|
|
1835
|
+
}
|
|
1836
|
+
registerEventListeners() {
|
|
1837
|
+
if (typeof window === 'undefined') {
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
window.addEventListener('online', this.boundConnect);
|
|
1841
|
+
}
|
|
1842
|
+
// Ensure that the URL always ends with /
|
|
1843
|
+
get serverUrl() {
|
|
1844
|
+
while (this.configuration.url[this.configuration.url.length - 1] === '/') {
|
|
1845
|
+
return this.configuration.url.slice(0, this.configuration.url.length - 1);
|
|
1846
|
+
}
|
|
1847
|
+
return this.configuration.url;
|
|
1848
|
+
}
|
|
1849
|
+
get url() {
|
|
1850
|
+
const encodedParams = encodeQueryParams(this.configuration.parameters);
|
|
1851
|
+
return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
|
|
1852
|
+
}
|
|
1853
|
+
disconnect() {
|
|
1854
|
+
this.shouldConnect = false;
|
|
1855
|
+
if (this.webSocket === null) {
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
try {
|
|
1859
|
+
this.webSocket.close();
|
|
1860
|
+
this.messageQueue = [];
|
|
1861
|
+
}
|
|
1862
|
+
catch {
|
|
1863
|
+
//
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
send(message) {
|
|
1867
|
+
var _a;
|
|
1868
|
+
if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === WsReadyStates.Open) {
|
|
1869
|
+
this.webSocket.send(message);
|
|
1870
|
+
}
|
|
1871
|
+
else {
|
|
1872
|
+
this.messageQueue.push(message);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
onClose({ event }) {
|
|
1876
|
+
this.webSocket = null;
|
|
1877
|
+
if (this.status === WebSocketStatus.Connected) {
|
|
1878
|
+
this.status = WebSocketStatus.Disconnected;
|
|
1879
|
+
this.emit('status', { status: WebSocketStatus.Disconnected });
|
|
1880
|
+
this.emit('disconnect', { event });
|
|
1881
|
+
}
|
|
1882
|
+
if (event.code === Unauthorized.code) {
|
|
1883
|
+
if (event.reason === Unauthorized.reason) {
|
|
1884
|
+
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.');
|
|
1885
|
+
}
|
|
1886
|
+
else {
|
|
1887
|
+
console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
|
|
1888
|
+
}
|
|
1889
|
+
this.shouldConnect = false;
|
|
1890
|
+
}
|
|
1891
|
+
if (event.code === Forbidden.code) {
|
|
1892
|
+
if (!this.configuration.quiet) {
|
|
1893
|
+
console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
|
|
1894
|
+
return; // TODO REMOVE ME
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
if (event.code === MessageTooBig.code) {
|
|
1898
|
+
console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
|
|
1899
|
+
this.shouldConnect = false;
|
|
1900
|
+
}
|
|
1901
|
+
if (this.connectionAttempt) {
|
|
1902
|
+
// That connection attempt failed.
|
|
1903
|
+
this.rejectConnectionAttempt();
|
|
1904
|
+
}
|
|
1905
|
+
else if (this.shouldConnect) {
|
|
1906
|
+
// The connection was closed by the server. Let’s just try again.
|
|
1907
|
+
this.connect();
|
|
1908
|
+
}
|
|
1909
|
+
// If we’ll reconnect, we’re done for now.
|
|
1910
|
+
if (this.shouldConnect) {
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
// The status is set correctly already.
|
|
1914
|
+
if (this.status === WebSocketStatus.Disconnected) {
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
// Let’s update the connection status.
|
|
1918
|
+
this.status = WebSocketStatus.Disconnected;
|
|
1919
|
+
this.emit('status', { status: WebSocketStatus.Disconnected });
|
|
1920
|
+
this.emit('disconnect', { event });
|
|
1921
|
+
}
|
|
1922
|
+
destroy() {
|
|
1923
|
+
this.emit('destroy');
|
|
1924
|
+
if (this.intervals.forceSync) {
|
|
1925
|
+
clearInterval(this.intervals.forceSync);
|
|
1926
|
+
}
|
|
1927
|
+
clearInterval(this.intervals.connectionChecker);
|
|
1928
|
+
// If there is still a connection attempt outstanding then we should stop
|
|
1929
|
+
// it before calling disconnect, otherwise it will be rejected in the onClose
|
|
1930
|
+
// handler and trigger a retry
|
|
1931
|
+
this.stopConnectionAttempt();
|
|
1932
|
+
this.disconnect();
|
|
1933
|
+
this.removeAllListeners();
|
|
1934
|
+
if (typeof window === 'undefined') {
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
window.removeEventListener('online', this.boundConnect);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
class IncomingMessage {
|
|
1942
|
+
constructor(data) {
|
|
1943
|
+
this.data = data;
|
|
1944
|
+
this.encoder = createEncoder();
|
|
1945
|
+
this.decoder = createDecoder(new Uint8Array(this.data));
|
|
1946
|
+
}
|
|
1947
|
+
readVarUint() {
|
|
1948
|
+
return readVarUint(this.decoder);
|
|
1949
|
+
}
|
|
1950
|
+
readVarString() {
|
|
1951
|
+
return readVarString(this.decoder);
|
|
1952
|
+
}
|
|
1953
|
+
readVarUint8Array() {
|
|
1954
|
+
return readVarUint8Array(this.decoder);
|
|
1955
|
+
}
|
|
1956
|
+
writeVarUint(type) {
|
|
1957
|
+
return writeVarUint(this.encoder, type);
|
|
1958
|
+
}
|
|
1959
|
+
writeVarString(string) {
|
|
1960
|
+
return writeVarString(this.encoder, string);
|
|
1961
|
+
}
|
|
1962
|
+
writeVarUint8Array(data) {
|
|
1963
|
+
return writeVarUint8Array(this.encoder, data);
|
|
1964
|
+
}
|
|
1965
|
+
length() {
|
|
1966
|
+
return length(this.encoder);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/**
|
|
1971
|
+
* @module sync-protocol
|
|
1972
|
+
*/
|
|
1973
|
+
|
|
1974
|
+
/**
|
|
1975
|
+
* @typedef {Map<number, number>} StateMap
|
|
1976
|
+
*/
|
|
1977
|
+
|
|
1978
|
+
/**
|
|
1979
|
+
* Core Yjs defines two message types:
|
|
1980
|
+
* • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
|
|
1981
|
+
* • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
|
|
1982
|
+
* received all information from the remote client.
|
|
1983
|
+
*
|
|
1984
|
+
* In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
|
|
1985
|
+
* with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
|
|
1986
|
+
* SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
|
|
1987
|
+
*
|
|
1988
|
+
* In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
|
|
1989
|
+
* When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
|
|
1990
|
+
* with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
|
|
1991
|
+
* client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
|
|
1992
|
+
* easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
|
|
1993
|
+
* Therefore it is necesarry that the client initiates the sync.
|
|
1994
|
+
*
|
|
1995
|
+
* Construction of a message:
|
|
1996
|
+
* [messageType : varUint, message definition..]
|
|
1997
|
+
*
|
|
1998
|
+
* Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
|
|
1999
|
+
*
|
|
2000
|
+
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
|
|
2001
|
+
*/
|
|
2002
|
+
|
|
2003
|
+
const messageYjsSyncStep1 = 0;
|
|
2004
|
+
const messageYjsSyncStep2 = 1;
|
|
2005
|
+
const messageYjsUpdate = 2;
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Create a sync step 1 message based on the state of the current shared document.
|
|
2009
|
+
*
|
|
2010
|
+
* @param {encoding.Encoder} encoder
|
|
2011
|
+
* @param {Y.Doc} doc
|
|
2012
|
+
*/
|
|
2013
|
+
const writeSyncStep1 = (encoder, doc) => {
|
|
2014
|
+
writeVarUint(encoder, messageYjsSyncStep1);
|
|
2015
|
+
const sv = Y.encodeStateVector(doc);
|
|
2016
|
+
writeVarUint8Array(encoder, sv);
|
|
2017
|
+
};
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* @param {encoding.Encoder} encoder
|
|
2021
|
+
* @param {Y.Doc} doc
|
|
2022
|
+
* @param {Uint8Array} [encodedStateVector]
|
|
2023
|
+
*/
|
|
2024
|
+
const writeSyncStep2 = (encoder, doc, encodedStateVector) => {
|
|
2025
|
+
writeVarUint(encoder, messageYjsSyncStep2);
|
|
2026
|
+
writeVarUint8Array(encoder, Y.encodeStateAsUpdate(doc, encodedStateVector));
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
/**
|
|
2030
|
+
* Read SyncStep1 message and reply with SyncStep2.
|
|
2031
|
+
*
|
|
2032
|
+
* @param {decoding.Decoder} decoder The reply to the received message
|
|
2033
|
+
* @param {encoding.Encoder} encoder The received message
|
|
2034
|
+
* @param {Y.Doc} doc
|
|
2035
|
+
*/
|
|
2036
|
+
const readSyncStep1 = (decoder, encoder, doc) =>
|
|
2037
|
+
writeSyncStep2(encoder, doc, readVarUint8Array(decoder));
|
|
1641
2038
|
|
|
1642
2039
|
/**
|
|
1643
2040
|
* Read and apply Structs and then DeleteStore to a y instance.
|
|
@@ -1695,559 +2092,184 @@ const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => {
|
|
|
1695
2092
|
throw new Error('Unknown message type')
|
|
1696
2093
|
}
|
|
1697
2094
|
return messageType
|
|
1698
|
-
};
|
|
1699
|
-
|
|
1700
|
-
var MessageType;
|
|
1701
|
-
(function (MessageType) {
|
|
1702
|
-
MessageType[MessageType["Sync"] = 0] = "Sync";
|
|
1703
|
-
MessageType[MessageType["Awareness"] = 1] = "Awareness";
|
|
1704
|
-
MessageType[MessageType["Auth"] = 2] = "Auth";
|
|
1705
|
-
MessageType[MessageType["QueryAwareness"] = 3] = "QueryAwareness";
|
|
1706
|
-
MessageType[MessageType["Stateless"] = 5] = "Stateless";
|
|
1707
|
-
MessageType[MessageType["CLOSE"] = 7] = "CLOSE";
|
|
1708
|
-
})(MessageType || (MessageType = {}));
|
|
1709
|
-
var WebSocketStatus;
|
|
1710
|
-
(function (WebSocketStatus) {
|
|
1711
|
-
WebSocketStatus["Connecting"] = "connecting";
|
|
1712
|
-
WebSocketStatus["Connected"] = "connected";
|
|
1713
|
-
WebSocketStatus["Disconnected"] = "disconnected";
|
|
1714
|
-
})(WebSocketStatus || (WebSocketStatus = {}));
|
|
1715
|
-
|
|
1716
|
-
class OutgoingMessage {
|
|
1717
|
-
constructor() {
|
|
1718
|
-
this.encoder = createEncoder();
|
|
1719
|
-
}
|
|
1720
|
-
get(args) {
|
|
1721
|
-
return args.encoder;
|
|
1722
|
-
}
|
|
1723
|
-
toUint8Array() {
|
|
1724
|
-
return toUint8Array(this.encoder);
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
class MessageReceiver {
|
|
1729
|
-
constructor(message) {
|
|
1730
|
-
this.broadcasted = false;
|
|
1731
|
-
this.message = message;
|
|
1732
|
-
}
|
|
1733
|
-
setBroadcasted(value) {
|
|
1734
|
-
this.broadcasted = value;
|
|
1735
|
-
return this;
|
|
1736
|
-
}
|
|
1737
|
-
apply(provider, emitSynced = true) {
|
|
1738
|
-
const { message } = this;
|
|
1739
|
-
const type = message.readVarUint();
|
|
1740
|
-
const emptyMessageLength = message.length();
|
|
1741
|
-
switch (type) {
|
|
1742
|
-
case MessageType.Sync:
|
|
1743
|
-
this.applySyncMessage(provider, emitSynced);
|
|
1744
|
-
break;
|
|
1745
|
-
case MessageType.Awareness:
|
|
1746
|
-
this.applyAwarenessMessage(provider);
|
|
1747
|
-
break;
|
|
1748
|
-
case MessageType.Auth:
|
|
1749
|
-
this.applyAuthMessage(provider);
|
|
1750
|
-
break;
|
|
1751
|
-
case MessageType.QueryAwareness:
|
|
1752
|
-
this.applyQueryAwarenessMessage(provider);
|
|
1753
|
-
break;
|
|
1754
|
-
case MessageType.Stateless:
|
|
1755
|
-
provider.receiveStateless(readVarString(message.decoder));
|
|
1756
|
-
break;
|
|
1757
|
-
default:
|
|
1758
|
-
throw new Error(`Can’t apply message of unknown type: ${type}`);
|
|
1759
|
-
}
|
|
1760
|
-
// Reply
|
|
1761
|
-
if (message.length() > emptyMessageLength + 1) { // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage)
|
|
1762
|
-
if (this.broadcasted) {
|
|
1763
|
-
// TODO: Some weird TypeScript error
|
|
1764
|
-
// @ts-ignore
|
|
1765
|
-
provider.broadcast(OutgoingMessage, { encoder: message.encoder });
|
|
1766
|
-
}
|
|
1767
|
-
else {
|
|
1768
|
-
// TODO: Some weird TypeScript error
|
|
1769
|
-
// @ts-ignore
|
|
1770
|
-
provider.send(OutgoingMessage, { encoder: message.encoder });
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
applySyncMessage(provider, emitSynced) {
|
|
1775
|
-
const { message } = this;
|
|
1776
|
-
message.writeVarUint(MessageType.Sync);
|
|
1777
|
-
// Apply update
|
|
1778
|
-
const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
|
|
1779
|
-
// Synced once we receive Step2
|
|
1780
|
-
if (emitSynced && syncMessageType === messageYjsSyncStep2) {
|
|
1781
|
-
provider.synced = true;
|
|
1782
|
-
}
|
|
1783
|
-
if (syncMessageType === messageYjsUpdate || syncMessageType === messageYjsSyncStep2) {
|
|
1784
|
-
if (provider.unsyncedChanges > 0) {
|
|
1785
|
-
provider.updateUnsyncedChanges(-1);
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
applyAwarenessMessage(provider) {
|
|
1790
|
-
const { message } = this;
|
|
1791
|
-
applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
|
|
1792
|
-
}
|
|
1793
|
-
applyAuthMessage(provider) {
|
|
1794
|
-
const { message } = this;
|
|
1795
|
-
readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
|
|
1796
|
-
}
|
|
1797
|
-
applyQueryAwarenessMessage(provider) {
|
|
1798
|
-
const { message } = this;
|
|
1799
|
-
message.writeVarUint(MessageType.Awareness);
|
|
1800
|
-
message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
class MessageSender {
|
|
1805
|
-
constructor(Message, args = {}) {
|
|
1806
|
-
this.message = new Message();
|
|
1807
|
-
this.encoder = this.message.get(args);
|
|
1808
|
-
}
|
|
1809
|
-
create() {
|
|
1810
|
-
return toUint8Array(this.encoder);
|
|
1811
|
-
}
|
|
1812
|
-
send(webSocket) {
|
|
1813
|
-
webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
|
|
1814
|
-
}
|
|
1815
|
-
broadcast(channel) {
|
|
1816
|
-
publish(channel, this.create());
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
class SyncStepOneMessage extends OutgoingMessage {
|
|
1821
|
-
constructor() {
|
|
1822
|
-
super(...arguments);
|
|
1823
|
-
this.type = MessageType.Sync;
|
|
1824
|
-
this.description = 'First sync step';
|
|
1825
|
-
}
|
|
1826
|
-
get(args) {
|
|
1827
|
-
if (typeof args.document === 'undefined') {
|
|
1828
|
-
throw new Error('The sync step one message requires document as an argument');
|
|
1829
|
-
}
|
|
1830
|
-
writeVarString(this.encoder, args.documentName);
|
|
1831
|
-
writeVarUint(this.encoder, this.type);
|
|
1832
|
-
writeSyncStep1(this.encoder, args.document);
|
|
1833
|
-
return this.encoder;
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
class SyncStepTwoMessage extends OutgoingMessage {
|
|
1838
|
-
constructor() {
|
|
1839
|
-
super(...arguments);
|
|
1840
|
-
this.type = MessageType.Sync;
|
|
1841
|
-
this.description = 'Second sync step';
|
|
1842
|
-
}
|
|
1843
|
-
get(args) {
|
|
1844
|
-
if (typeof args.document === 'undefined') {
|
|
1845
|
-
throw new Error('The sync step two message requires document as an argument');
|
|
1846
|
-
}
|
|
1847
|
-
writeVarString(this.encoder, args.documentName);
|
|
1848
|
-
writeVarUint(this.encoder, this.type);
|
|
1849
|
-
writeSyncStep2(this.encoder, args.document);
|
|
1850
|
-
return this.encoder;
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
class QueryAwarenessMessage extends OutgoingMessage {
|
|
1855
|
-
constructor() {
|
|
1856
|
-
super(...arguments);
|
|
1857
|
-
this.type = MessageType.QueryAwareness;
|
|
1858
|
-
this.description = 'Queries awareness states';
|
|
1859
|
-
}
|
|
1860
|
-
get(args) {
|
|
1861
|
-
console.log('queryAwareness: writing string docName', args.documentName);
|
|
1862
|
-
console.log(this.encoder.cpos);
|
|
1863
|
-
writeVarString(this.encoder, args.documentName);
|
|
1864
|
-
writeVarUint(this.encoder, this.type);
|
|
1865
|
-
return this.encoder;
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
class AuthenticationMessage extends OutgoingMessage {
|
|
1870
|
-
constructor() {
|
|
1871
|
-
super(...arguments);
|
|
1872
|
-
this.type = MessageType.Auth;
|
|
1873
|
-
this.description = 'Authentication';
|
|
1874
|
-
}
|
|
1875
|
-
get(args) {
|
|
1876
|
-
if (typeof args.token === 'undefined') {
|
|
1877
|
-
throw new Error('The authentication message requires `token` as an argument.');
|
|
1878
|
-
}
|
|
1879
|
-
writeVarString(this.encoder, args.documentName);
|
|
1880
|
-
writeVarUint(this.encoder, this.type);
|
|
1881
|
-
writeAuthentication(this.encoder, args.token);
|
|
1882
|
-
return this.encoder;
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
class AwarenessMessage extends OutgoingMessage {
|
|
1887
|
-
constructor() {
|
|
1888
|
-
super(...arguments);
|
|
1889
|
-
this.type = MessageType.Awareness;
|
|
1890
|
-
this.description = 'Awareness states update';
|
|
1891
|
-
}
|
|
1892
|
-
get(args) {
|
|
1893
|
-
if (typeof args.awareness === 'undefined') {
|
|
1894
|
-
throw new Error('The awareness message requires awareness as an argument');
|
|
1895
|
-
}
|
|
1896
|
-
if (typeof args.clients === 'undefined') {
|
|
1897
|
-
throw new Error('The awareness message requires clients as an argument');
|
|
1898
|
-
}
|
|
1899
|
-
writeVarString(this.encoder, args.documentName);
|
|
1900
|
-
writeVarUint(this.encoder, this.type);
|
|
1901
|
-
let awarenessUpdate;
|
|
1902
|
-
if (args.states === undefined) {
|
|
1903
|
-
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients);
|
|
1904
|
-
}
|
|
1905
|
-
else {
|
|
1906
|
-
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
|
|
1907
|
-
}
|
|
1908
|
-
writeVarUint8Array(this.encoder, awarenessUpdate);
|
|
1909
|
-
return this.encoder;
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
2095
|
+
};
|
|
1912
2096
|
|
|
1913
|
-
class
|
|
2097
|
+
class OutgoingMessage {
|
|
1914
2098
|
constructor() {
|
|
1915
|
-
|
|
1916
|
-
this.type = MessageType.Sync;
|
|
1917
|
-
this.description = 'A document update';
|
|
2099
|
+
this.encoder = createEncoder();
|
|
1918
2100
|
}
|
|
1919
2101
|
get(args) {
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
return this.encoder;
|
|
2102
|
+
return args.encoder;
|
|
2103
|
+
}
|
|
2104
|
+
toUint8Array() {
|
|
2105
|
+
return toUint8Array(this.encoder);
|
|
1924
2106
|
}
|
|
1925
2107
|
}
|
|
1926
2108
|
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
*/
|
|
1932
|
-
|
|
1933
|
-
/**
|
|
1934
|
-
* @param {Object<string,string>} params
|
|
1935
|
-
* @return {string}
|
|
1936
|
-
*/
|
|
1937
|
-
const encodeQueryParams = params =>
|
|
1938
|
-
map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
|
|
1939
|
-
|
|
1940
|
-
class HocuspocusProviderWebsocket extends EventEmitter {
|
|
1941
|
-
constructor(configuration) {
|
|
1942
|
-
super();
|
|
1943
|
-
this.configuration = {
|
|
1944
|
-
url: '',
|
|
1945
|
-
// @ts-ignore
|
|
1946
|
-
document: undefined,
|
|
1947
|
-
// @ts-ignore
|
|
1948
|
-
awareness: undefined,
|
|
1949
|
-
WebSocketPolyfill: undefined,
|
|
1950
|
-
parameters: {},
|
|
1951
|
-
connect: true,
|
|
1952
|
-
broadcast: true,
|
|
1953
|
-
forceSyncInterval: false,
|
|
1954
|
-
// TODO: this should depend on awareness.outdatedTime
|
|
1955
|
-
messageReconnectTimeout: 30000,
|
|
1956
|
-
// 1 second
|
|
1957
|
-
delay: 1000,
|
|
1958
|
-
// instant
|
|
1959
|
-
initialDelay: 0,
|
|
1960
|
-
// double the delay each time
|
|
1961
|
-
factor: 2,
|
|
1962
|
-
// unlimited retries
|
|
1963
|
-
maxAttempts: 0,
|
|
1964
|
-
// wait at least 1 second
|
|
1965
|
-
minDelay: 1000,
|
|
1966
|
-
// at least every 30 seconds
|
|
1967
|
-
maxDelay: 30000,
|
|
1968
|
-
// randomize
|
|
1969
|
-
jitter: true,
|
|
1970
|
-
// retry forever
|
|
1971
|
-
timeout: 0,
|
|
1972
|
-
onOpen: () => null,
|
|
1973
|
-
onConnect: () => null,
|
|
1974
|
-
onMessage: () => null,
|
|
1975
|
-
onOutgoingMessage: () => null,
|
|
1976
|
-
onStatus: () => null,
|
|
1977
|
-
onDisconnect: () => null,
|
|
1978
|
-
onClose: () => null,
|
|
1979
|
-
onDestroy: () => null,
|
|
1980
|
-
onAwarenessUpdate: () => null,
|
|
1981
|
-
onAwarenessChange: () => null,
|
|
1982
|
-
quiet: false,
|
|
1983
|
-
};
|
|
1984
|
-
this.subscribedToBroadcastChannel = false;
|
|
1985
|
-
this.webSocket = null;
|
|
1986
|
-
this.shouldConnect = true;
|
|
1987
|
-
this.status = WebSocketStatus.Disconnected;
|
|
1988
|
-
this.lastMessageReceived = 0;
|
|
1989
|
-
this.mux = createMutex();
|
|
1990
|
-
this.intervals = {
|
|
1991
|
-
forceSync: null,
|
|
1992
|
-
connectionChecker: null,
|
|
1993
|
-
};
|
|
1994
|
-
this.connectionAttempt = null;
|
|
1995
|
-
this.receivedOnOpenPayload = undefined;
|
|
1996
|
-
this.receivedOnStatusPayload = undefined;
|
|
1997
|
-
this.boundConnect = this.connect.bind(this);
|
|
1998
|
-
this.setConfiguration(configuration);
|
|
1999
|
-
this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
|
|
2000
|
-
this.on('open', this.configuration.onOpen);
|
|
2001
|
-
this.on('open', this.onOpen.bind(this));
|
|
2002
|
-
this.on('connect', this.configuration.onConnect);
|
|
2003
|
-
this.on('message', this.configuration.onMessage);
|
|
2004
|
-
this.on('outgoingMessage', this.configuration.onOutgoingMessage);
|
|
2005
|
-
this.on('status', this.configuration.onStatus);
|
|
2006
|
-
this.on('status', this.onStatus.bind(this));
|
|
2007
|
-
this.on('disconnect', this.configuration.onDisconnect);
|
|
2008
|
-
this.on('close', this.configuration.onClose);
|
|
2009
|
-
this.on('destroy', this.configuration.onDestroy);
|
|
2010
|
-
this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
|
|
2011
|
-
this.on('awarenessChange', this.configuration.onAwarenessChange);
|
|
2012
|
-
this.on('close', this.onClose.bind(this));
|
|
2013
|
-
this.on('message', this.onMessage.bind(this));
|
|
2014
|
-
this.registerEventListeners();
|
|
2015
|
-
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
|
|
2016
|
-
if (typeof configuration.connect !== 'undefined') {
|
|
2017
|
-
this.shouldConnect = configuration.connect;
|
|
2018
|
-
}
|
|
2019
|
-
if (!this.shouldConnect) {
|
|
2020
|
-
return;
|
|
2021
|
-
}
|
|
2022
|
-
this.connect();
|
|
2023
|
-
}
|
|
2024
|
-
async onOpen(event) {
|
|
2025
|
-
this.receivedOnOpenPayload = event;
|
|
2109
|
+
class MessageReceiver {
|
|
2110
|
+
constructor(message) {
|
|
2111
|
+
this.broadcasted = false;
|
|
2112
|
+
this.message = message;
|
|
2026
2113
|
}
|
|
2027
|
-
|
|
2028
|
-
this.
|
|
2114
|
+
setBroadcasted(value) {
|
|
2115
|
+
this.broadcasted = value;
|
|
2116
|
+
return this;
|
|
2029
2117
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2118
|
+
apply(provider, emitSynced) {
|
|
2119
|
+
const { message } = this;
|
|
2120
|
+
const type = message.readVarUint();
|
|
2121
|
+
const emptyMessageLength = message.length();
|
|
2122
|
+
switch (type) {
|
|
2123
|
+
case MessageType.Sync:
|
|
2124
|
+
this.applySyncMessage(provider, emitSynced);
|
|
2125
|
+
break;
|
|
2126
|
+
case MessageType.Awareness:
|
|
2127
|
+
this.applyAwarenessMessage(provider);
|
|
2128
|
+
break;
|
|
2129
|
+
case MessageType.Auth:
|
|
2130
|
+
this.applyAuthMessage(provider);
|
|
2131
|
+
break;
|
|
2132
|
+
case MessageType.QueryAwareness:
|
|
2133
|
+
this.applyQueryAwarenessMessage(provider);
|
|
2134
|
+
break;
|
|
2135
|
+
case MessageType.Stateless:
|
|
2136
|
+
provider.receiveStateless(readVarString(message.decoder));
|
|
2137
|
+
break;
|
|
2138
|
+
case MessageType.SyncStatus:
|
|
2139
|
+
this.applySyncStatusMessage(provider, readVarInt(message.decoder) === 1);
|
|
2140
|
+
break;
|
|
2141
|
+
default:
|
|
2142
|
+
throw new Error(`Can’t apply message of unknown type: ${type}`);
|
|
2033
2143
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2144
|
+
// Reply
|
|
2145
|
+
if (message.length() > emptyMessageLength + 1) { // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage)
|
|
2146
|
+
if (this.broadcasted) {
|
|
2147
|
+
// TODO: Some weird TypeScript error
|
|
2148
|
+
// @ts-ignore
|
|
2149
|
+
provider.broadcast(OutgoingMessage, { encoder: message.encoder });
|
|
2150
|
+
}
|
|
2151
|
+
else {
|
|
2152
|
+
// TODO: Some weird TypeScript error
|
|
2153
|
+
// @ts-ignore
|
|
2154
|
+
provider.send(OutgoingMessage, { encoder: message.encoder });
|
|
2155
|
+
}
|
|
2036
2156
|
}
|
|
2037
2157
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
// Always cancel any previously initiated connection retryer instances
|
|
2049
|
-
if (this.cancelWebsocketRetry) {
|
|
2050
|
-
this.cancelWebsocketRetry();
|
|
2051
|
-
this.cancelWebsocketRetry = undefined;
|
|
2158
|
+
applySyncMessage(provider, emitSynced) {
|
|
2159
|
+
const { message } = this;
|
|
2160
|
+
message.writeVarUint(MessageType.Sync);
|
|
2161
|
+
// Apply update
|
|
2162
|
+
const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
|
|
2163
|
+
// Synced once we receive Step2
|
|
2164
|
+
if (emitSynced && syncMessageType === messageYjsSyncStep2) {
|
|
2165
|
+
provider.synced = true;
|
|
2052
2166
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
delay: this.configuration.delay,
|
|
2058
|
-
initialDelay: this.configuration.initialDelay,
|
|
2059
|
-
factor: this.configuration.factor,
|
|
2060
|
-
maxAttempts: this.configuration.maxAttempts,
|
|
2061
|
-
minDelay: this.configuration.minDelay,
|
|
2062
|
-
maxDelay: this.configuration.maxDelay,
|
|
2063
|
-
jitter: this.configuration.jitter,
|
|
2064
|
-
timeout: this.configuration.timeout,
|
|
2065
|
-
beforeAttempt: context => {
|
|
2066
|
-
if (!this.shouldConnect || cancelAttempt) {
|
|
2067
|
-
context.abort();
|
|
2068
|
-
}
|
|
2069
|
-
},
|
|
2070
|
-
}).catch((error) => {
|
|
2071
|
-
// If we aborted the connection attempt then don’t throw an error
|
|
2072
|
-
// ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
|
|
2073
|
-
if (error && error.code !== 'ATTEMPT_ABORTED') {
|
|
2074
|
-
throw error;
|
|
2075
|
-
}
|
|
2076
|
-
});
|
|
2077
|
-
return {
|
|
2078
|
-
retryPromise,
|
|
2079
|
-
cancelFunc: () => {
|
|
2080
|
-
cancelAttempt = true;
|
|
2081
|
-
},
|
|
2082
|
-
};
|
|
2083
|
-
};
|
|
2084
|
-
const { retryPromise, cancelFunc } = abortableRetry();
|
|
2085
|
-
this.cancelWebsocketRetry = cancelFunc;
|
|
2086
|
-
return retryPromise;
|
|
2087
|
-
}
|
|
2088
|
-
createWebSocketConnection() {
|
|
2089
|
-
return new Promise((resolve, reject) => {
|
|
2090
|
-
if (this.webSocket) {
|
|
2091
|
-
this.webSocket.close();
|
|
2092
|
-
this.webSocket = null;
|
|
2093
|
-
}
|
|
2094
|
-
// Init the WebSocket connection
|
|
2095
|
-
const ws = new this.configuration.WebSocketPolyfill(this.url);
|
|
2096
|
-
ws.binaryType = 'arraybuffer';
|
|
2097
|
-
ws.onmessage = (payload) => this.emit('message', payload);
|
|
2098
|
-
ws.onclose = (payload) => this.emit('close', { event: payload });
|
|
2099
|
-
ws.onopen = (payload) => this.emit('open', payload);
|
|
2100
|
-
ws.onerror = (err) => {
|
|
2101
|
-
reject(err);
|
|
2102
|
-
};
|
|
2103
|
-
this.webSocket = ws;
|
|
2104
|
-
// Reset the status
|
|
2105
|
-
this.status = WebSocketStatus.Connecting;
|
|
2106
|
-
this.emit('status', { status: WebSocketStatus.Connecting });
|
|
2107
|
-
// Store resolve/reject for later use
|
|
2108
|
-
this.connectionAttempt = {
|
|
2109
|
-
resolve,
|
|
2110
|
-
reject,
|
|
2111
|
-
};
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
|
-
onMessage(event) {
|
|
2115
|
-
this.resolveConnectionAttempt();
|
|
2116
|
-
this.lastMessageReceived = getUnixTime();
|
|
2117
|
-
}
|
|
2118
|
-
resolveConnectionAttempt() {
|
|
2119
|
-
if (this.connectionAttempt) {
|
|
2120
|
-
this.connectionAttempt.resolve();
|
|
2121
|
-
this.connectionAttempt = null;
|
|
2122
|
-
this.status = WebSocketStatus.Connected;
|
|
2123
|
-
this.emit('status', { status: WebSocketStatus.Connected });
|
|
2124
|
-
this.emit('connect');
|
|
2167
|
+
}
|
|
2168
|
+
applySyncStatusMessage(provider, applied) {
|
|
2169
|
+
if (applied) {
|
|
2170
|
+
provider.decrementUnsyncedChanges();
|
|
2125
2171
|
}
|
|
2126
2172
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2173
|
+
applyAwarenessMessage(provider) {
|
|
2174
|
+
const { message } = this;
|
|
2175
|
+
applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
|
|
2129
2176
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
(
|
|
2133
|
-
this.connectionAttempt = null;
|
|
2177
|
+
applyAuthMessage(provider) {
|
|
2178
|
+
const { message } = this;
|
|
2179
|
+
readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
|
|
2134
2180
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
return;
|
|
2140
|
-
}
|
|
2141
|
-
// Don’t close then connection while waiting for the first message
|
|
2142
|
-
if (!this.lastMessageReceived) {
|
|
2143
|
-
return;
|
|
2144
|
-
}
|
|
2145
|
-
// Don’t close the connection when a message was received recently
|
|
2146
|
-
if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
|
|
2147
|
-
return;
|
|
2148
|
-
}
|
|
2149
|
-
// No message received in a long time, not even your own
|
|
2150
|
-
// Awareness updates, which are updated every 15 seconds.
|
|
2151
|
-
(_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
|
|
2181
|
+
applyQueryAwarenessMessage(provider) {
|
|
2182
|
+
const { message } = this;
|
|
2183
|
+
message.writeVarUint(MessageType.Awareness);
|
|
2184
|
+
message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
|
|
2152
2185
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
class MessageSender {
|
|
2189
|
+
constructor(Message, args = {}) {
|
|
2190
|
+
this.message = new Message();
|
|
2191
|
+
this.encoder = this.message.get(args);
|
|
2158
2192
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
while (this.configuration.url[this.configuration.url.length - 1] === '/') {
|
|
2162
|
-
return this.configuration.url.slice(0, this.configuration.url.length - 1);
|
|
2163
|
-
}
|
|
2164
|
-
return this.configuration.url;
|
|
2193
|
+
create() {
|
|
2194
|
+
return toUint8Array(this.encoder);
|
|
2165
2195
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
|
|
2196
|
+
send(webSocket) {
|
|
2197
|
+
webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
|
|
2169
2198
|
}
|
|
2170
|
-
|
|
2171
|
-
this.
|
|
2172
|
-
if (this.webSocket === null) {
|
|
2173
|
-
return;
|
|
2174
|
-
}
|
|
2175
|
-
try {
|
|
2176
|
-
this.webSocket.close();
|
|
2177
|
-
}
|
|
2178
|
-
catch {
|
|
2179
|
-
//
|
|
2180
|
-
}
|
|
2199
|
+
broadcast(channel) {
|
|
2200
|
+
publish(channel, this.create());
|
|
2181
2201
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
class AuthenticationMessage extends OutgoingMessage {
|
|
2205
|
+
constructor() {
|
|
2206
|
+
super(...arguments);
|
|
2207
|
+
this.type = MessageType.Auth;
|
|
2208
|
+
this.description = 'Authentication';
|
|
2187
2209
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
this.status = WebSocketStatus.Disconnected;
|
|
2192
|
-
this.emit('status', { status: WebSocketStatus.Disconnected });
|
|
2193
|
-
this.emit('disconnect', { event });
|
|
2194
|
-
}
|
|
2195
|
-
if (event.code === Unauthorized.code) {
|
|
2196
|
-
if (event.reason === Unauthorized.reason) {
|
|
2197
|
-
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.');
|
|
2198
|
-
}
|
|
2199
|
-
else {
|
|
2200
|
-
console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
|
|
2201
|
-
}
|
|
2202
|
-
this.shouldConnect = false;
|
|
2203
|
-
}
|
|
2204
|
-
if (event.code === Forbidden.code) {
|
|
2205
|
-
if (!this.configuration.quiet) {
|
|
2206
|
-
console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
|
|
2207
|
-
return; // TODO REMOVE ME
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
if (event.code === MessageTooBig.code) {
|
|
2211
|
-
console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
|
|
2212
|
-
this.shouldConnect = false;
|
|
2210
|
+
get(args) {
|
|
2211
|
+
if (typeof args.token === 'undefined') {
|
|
2212
|
+
throw new Error('The authentication message requires `token` as an argument.');
|
|
2213
2213
|
}
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2214
|
+
writeVarString(this.encoder, args.documentName);
|
|
2215
|
+
writeVarUint(this.encoder, this.type);
|
|
2216
|
+
writeAuthentication(this.encoder, args.token);
|
|
2217
|
+
return this.encoder;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
class AwarenessMessage extends OutgoingMessage {
|
|
2222
|
+
constructor() {
|
|
2223
|
+
super(...arguments);
|
|
2224
|
+
this.type = MessageType.Awareness;
|
|
2225
|
+
this.description = 'Awareness states update';
|
|
2226
|
+
}
|
|
2227
|
+
get(args) {
|
|
2228
|
+
if (typeof args.awareness === 'undefined') {
|
|
2229
|
+
throw new Error('The awareness message requires awareness as an argument');
|
|
2217
2230
|
}
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
this.connect();
|
|
2231
|
+
if (typeof args.clients === 'undefined') {
|
|
2232
|
+
throw new Error('The awareness message requires clients as an argument');
|
|
2221
2233
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2234
|
+
writeVarString(this.encoder, args.documentName);
|
|
2235
|
+
writeVarUint(this.encoder, this.type);
|
|
2236
|
+
let awarenessUpdate;
|
|
2237
|
+
if (args.states === undefined) {
|
|
2238
|
+
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients);
|
|
2225
2239
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
return;
|
|
2240
|
+
else {
|
|
2241
|
+
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
|
|
2229
2242
|
}
|
|
2230
|
-
|
|
2231
|
-
this.
|
|
2232
|
-
this.emit('status', { status: WebSocketStatus.Disconnected });
|
|
2233
|
-
this.emit('disconnect', { event });
|
|
2243
|
+
writeVarUint8Array(this.encoder, awarenessUpdate);
|
|
2244
|
+
return this.encoder;
|
|
2234
2245
|
}
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
this.
|
|
2245
|
-
this.
|
|
2246
|
-
this.
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
class CloseMessage extends OutgoingMessage {
|
|
2249
|
+
constructor() {
|
|
2250
|
+
super(...arguments);
|
|
2251
|
+
this.type = MessageType.CLOSE;
|
|
2252
|
+
this.description = 'Ask the server to close the connection';
|
|
2253
|
+
}
|
|
2254
|
+
get(args) {
|
|
2255
|
+
writeVarString(this.encoder, args.documentName);
|
|
2256
|
+
writeVarUint(this.encoder, this.type);
|
|
2257
|
+
return this.encoder;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
class QueryAwarenessMessage extends OutgoingMessage {
|
|
2262
|
+
constructor() {
|
|
2263
|
+
super(...arguments);
|
|
2264
|
+
this.type = MessageType.QueryAwareness;
|
|
2265
|
+
this.description = 'Queries awareness states';
|
|
2266
|
+
}
|
|
2267
|
+
get(args) {
|
|
2268
|
+
console.log('queryAwareness: writing string docName', args.documentName);
|
|
2269
|
+
console.log(this.encoder.cpos);
|
|
2270
|
+
writeVarString(this.encoder, args.documentName);
|
|
2271
|
+
writeVarUint(this.encoder, this.type);
|
|
2272
|
+
return this.encoder;
|
|
2251
2273
|
}
|
|
2252
2274
|
}
|
|
2253
2275
|
|
|
@@ -2266,15 +2288,50 @@ class StatelessMessage extends OutgoingMessage {
|
|
|
2266
2288
|
}
|
|
2267
2289
|
}
|
|
2268
2290
|
|
|
2269
|
-
class
|
|
2291
|
+
class SyncStepOneMessage extends OutgoingMessage {
|
|
2270
2292
|
constructor() {
|
|
2271
2293
|
super(...arguments);
|
|
2272
|
-
this.type = MessageType.
|
|
2273
|
-
this.description = '
|
|
2294
|
+
this.type = MessageType.Sync;
|
|
2295
|
+
this.description = 'First sync step';
|
|
2296
|
+
}
|
|
2297
|
+
get(args) {
|
|
2298
|
+
if (typeof args.document === 'undefined') {
|
|
2299
|
+
throw new Error('The sync step one message requires document as an argument');
|
|
2300
|
+
}
|
|
2301
|
+
writeVarString(this.encoder, args.documentName);
|
|
2302
|
+
writeVarUint(this.encoder, this.type);
|
|
2303
|
+
writeSyncStep1(this.encoder, args.document);
|
|
2304
|
+
return this.encoder;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
class SyncStepTwoMessage extends OutgoingMessage {
|
|
2309
|
+
constructor() {
|
|
2310
|
+
super(...arguments);
|
|
2311
|
+
this.type = MessageType.Sync;
|
|
2312
|
+
this.description = 'Second sync step';
|
|
2313
|
+
}
|
|
2314
|
+
get(args) {
|
|
2315
|
+
if (typeof args.document === 'undefined') {
|
|
2316
|
+
throw new Error('The sync step two message requires document as an argument');
|
|
2317
|
+
}
|
|
2318
|
+
writeVarString(this.encoder, args.documentName);
|
|
2319
|
+
writeVarUint(this.encoder, this.type);
|
|
2320
|
+
writeSyncStep2(this.encoder, args.document);
|
|
2321
|
+
return this.encoder;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
class UpdateMessage extends OutgoingMessage {
|
|
2326
|
+
constructor() {
|
|
2327
|
+
super(...arguments);
|
|
2328
|
+
this.type = MessageType.Sync;
|
|
2329
|
+
this.description = 'A document update';
|
|
2274
2330
|
}
|
|
2275
2331
|
get(args) {
|
|
2276
2332
|
writeVarString(this.encoder, args.documentName);
|
|
2277
2333
|
writeVarUint(this.encoder, this.type);
|
|
2334
|
+
writeUpdate(this.encoder, args.update);
|
|
2278
2335
|
return this.encoder;
|
|
2279
2336
|
}
|
|
2280
2337
|
}
|
|
@@ -2307,6 +2364,8 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2307
2364
|
onAwarenessChange: () => null,
|
|
2308
2365
|
onStateless: () => null,
|
|
2309
2366
|
quiet: false,
|
|
2367
|
+
connect: true,
|
|
2368
|
+
preserveConnection: true,
|
|
2310
2369
|
};
|
|
2311
2370
|
this.subscribedToBroadcastChannel = false;
|
|
2312
2371
|
this.isSynced = false;
|
|
@@ -2320,7 +2379,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2320
2379
|
};
|
|
2321
2380
|
this.isConnected = true;
|
|
2322
2381
|
this.boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this);
|
|
2323
|
-
this.
|
|
2382
|
+
this.boundPageUnload = this.pageUnload.bind(this);
|
|
2324
2383
|
this.boundOnOpen = this.onOpen.bind(this);
|
|
2325
2384
|
this.boundOnMessage = this.onMessage.bind(this);
|
|
2326
2385
|
this.boundOnClose = this.onClose.bind(this);
|
|
@@ -2380,6 +2439,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2380
2439
|
const websocketProviderConfig = configuration;
|
|
2381
2440
|
this.configuration.websocketProvider = new HocuspocusProviderWebsocket({
|
|
2382
2441
|
url: websocketProviderConfig.url,
|
|
2442
|
+
connect: websocketProviderConfig.connect,
|
|
2383
2443
|
parameters: websocketProviderConfig.parameters,
|
|
2384
2444
|
});
|
|
2385
2445
|
}
|
|
@@ -2394,21 +2454,28 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2394
2454
|
get hasUnsyncedChanges() {
|
|
2395
2455
|
return this.unsyncedChanges > 0;
|
|
2396
2456
|
}
|
|
2397
|
-
|
|
2398
|
-
this.unsyncedChanges +=
|
|
2457
|
+
incrementUnsyncedChanges() {
|
|
2458
|
+
this.unsyncedChanges += 1;
|
|
2459
|
+
this.emit('unsyncedChanges', this.unsyncedChanges);
|
|
2460
|
+
}
|
|
2461
|
+
decrementUnsyncedChanges() {
|
|
2462
|
+
this.unsyncedChanges -= 1;
|
|
2463
|
+
if (this.unsyncedChanges === 0) {
|
|
2464
|
+
this.synced = true;
|
|
2465
|
+
}
|
|
2399
2466
|
this.emit('unsyncedChanges', this.unsyncedChanges);
|
|
2400
2467
|
}
|
|
2401
2468
|
forceSync() {
|
|
2402
2469
|
this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
|
|
2403
2470
|
}
|
|
2404
|
-
|
|
2471
|
+
pageUnload() {
|
|
2405
2472
|
removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload');
|
|
2406
2473
|
}
|
|
2407
2474
|
registerEventListeners() {
|
|
2408
2475
|
if (typeof window === 'undefined') {
|
|
2409
2476
|
return;
|
|
2410
2477
|
}
|
|
2411
|
-
window.addEventListener('
|
|
2478
|
+
window.addEventListener('unload', this.boundPageUnload);
|
|
2412
2479
|
}
|
|
2413
2480
|
sendStateless(payload) {
|
|
2414
2481
|
this.send(StatelessMessage, { documentName: this.configuration.name, payload });
|
|
@@ -2417,7 +2484,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2417
2484
|
if (origin === this) {
|
|
2418
2485
|
return;
|
|
2419
2486
|
}
|
|
2420
|
-
this.
|
|
2487
|
+
this.incrementUnsyncedChanges();
|
|
2421
2488
|
this.send(UpdateMessage, { update, documentName: this.configuration.name }, true);
|
|
2422
2489
|
}
|
|
2423
2490
|
awarenessUpdateHandler({ added, updated, removed }, origin) {
|
|
@@ -2428,6 +2495,12 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2428
2495
|
documentName: this.configuration.name,
|
|
2429
2496
|
}, true);
|
|
2430
2497
|
}
|
|
2498
|
+
/**
|
|
2499
|
+
* Indicates whether a first handshake with the server has been established
|
|
2500
|
+
*
|
|
2501
|
+
* Note: this does not mean all updates from the client have been persisted to the backend. For this,
|
|
2502
|
+
* use `hasUnsyncedChanges`.
|
|
2503
|
+
*/
|
|
2431
2504
|
get synced() {
|
|
2432
2505
|
return this.isSynced;
|
|
2433
2506
|
}
|
|
@@ -2435,9 +2508,6 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2435
2508
|
if (this.isSynced === state) {
|
|
2436
2509
|
return;
|
|
2437
2510
|
}
|
|
2438
|
-
if (state && this.unsyncedChanges > 0) {
|
|
2439
|
-
this.updateUnsyncedChanges(-1 * this.unsyncedChanges);
|
|
2440
|
-
}
|
|
2441
2511
|
this.isSynced = state;
|
|
2442
2512
|
this.emit('synced', { state });
|
|
2443
2513
|
this.emit('sync', { state });
|
|
@@ -2455,6 +2525,9 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2455
2525
|
disconnect() {
|
|
2456
2526
|
this.disconnectBroadcastChannel();
|
|
2457
2527
|
this.configuration.websocketProvider.detach(this);
|
|
2528
|
+
if (!this.configuration.preserveConnection) {
|
|
2529
|
+
this.configuration.websocketProvider.disconnect();
|
|
2530
|
+
}
|
|
2458
2531
|
}
|
|
2459
2532
|
async onOpen(event) {
|
|
2460
2533
|
this.isAuthenticated = false;
|
|
@@ -2475,6 +2548,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2475
2548
|
return this.configuration.token;
|
|
2476
2549
|
}
|
|
2477
2550
|
startSync() {
|
|
2551
|
+
this.incrementUnsyncedChanges();
|
|
2478
2552
|
this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
|
|
2479
2553
|
if (this.awareness.getLocalState() !== null) {
|
|
2480
2554
|
this.send(AwarenessMessage, {
|
|
@@ -2485,8 +2559,9 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2485
2559
|
}
|
|
2486
2560
|
}
|
|
2487
2561
|
send(message, args, broadcast = false) {
|
|
2488
|
-
if (!this.isConnected)
|
|
2562
|
+
if (!this.isConnected) {
|
|
2489
2563
|
return;
|
|
2564
|
+
}
|
|
2490
2565
|
if (broadcast) {
|
|
2491
2566
|
this.mux(() => { this.broadcast(message, args); });
|
|
2492
2567
|
}
|
|
@@ -2502,7 +2577,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2502
2577
|
}
|
|
2503
2578
|
message.writeVarString(documentName);
|
|
2504
2579
|
this.emit('message', { event, message: new IncomingMessage(event.data) });
|
|
2505
|
-
new MessageReceiver(message).apply(this);
|
|
2580
|
+
new MessageReceiver(message).apply(this, true);
|
|
2506
2581
|
}
|
|
2507
2582
|
onClose(event) {
|
|
2508
2583
|
this.isAuthenticated = false;
|
|
@@ -2538,7 +2613,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2538
2613
|
if (typeof window === 'undefined') {
|
|
2539
2614
|
return;
|
|
2540
2615
|
}
|
|
2541
|
-
window.removeEventListener('
|
|
2616
|
+
window.removeEventListener('unload', this.boundPageUnload);
|
|
2542
2617
|
}
|
|
2543
2618
|
permissionDeniedHandler(reason) {
|
|
2544
2619
|
this.emit('authenticationFailed', { reason });
|
|
@@ -2619,6 +2694,39 @@ class TiptapCollabProvider extends HocuspocusProvider {
|
|
|
2619
2694
|
configuration.token = 'notoken'; // need to send a token anyway (which will be ignored)
|
|
2620
2695
|
}
|
|
2621
2696
|
super(configuration);
|
|
2697
|
+
this.tiptapCollabConfigurationPrefix = '__tiptapcollab__';
|
|
2698
|
+
}
|
|
2699
|
+
createVersion(name) {
|
|
2700
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2701
|
+
return this.sendStateless(JSON.stringify({ action: 'version.create', name }));
|
|
2702
|
+
}
|
|
2703
|
+
revertToVersion(targetVersion) {
|
|
2704
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2705
|
+
return this.sendStateless(JSON.stringify({ action: 'version.revert', version: targetVersion }));
|
|
2706
|
+
}
|
|
2707
|
+
getVersions() {
|
|
2708
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2709
|
+
return this.configuration.document.getArray(`${this.tiptapCollabConfigurationPrefix}versions`).toArray();
|
|
2710
|
+
}
|
|
2711
|
+
watchVersions(callback) {
|
|
2712
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2713
|
+
return this.configuration.document.getArray('__tiptapcollab__versions').observe(callback);
|
|
2714
|
+
}
|
|
2715
|
+
unwatchVersions(callback) {
|
|
2716
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2717
|
+
return this.configuration.document.getArray('__tiptapcollab__versions').unobserve(callback);
|
|
2718
|
+
}
|
|
2719
|
+
isAutoVersioning() {
|
|
2720
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2721
|
+
return !!this.configuration.document.getMap(`${this.tiptapCollabConfigurationPrefix}config`).get('autoVersioning');
|
|
2722
|
+
}
|
|
2723
|
+
enableAutoVersioning() {
|
|
2724
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2725
|
+
return this.configuration.document.getMap(`${this.tiptapCollabConfigurationPrefix}config`).set('autoVersioning', 1);
|
|
2726
|
+
}
|
|
2727
|
+
disableAutoVersioning() {
|
|
2728
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2729
|
+
return this.configuration.document.getMap(`${this.tiptapCollabConfigurationPrefix}config`).set('autoVersioning', 0);
|
|
2622
2730
|
}
|
|
2623
2731
|
}
|
|
2624
2732
|
|