@hocuspocus/provider 2.2.3 → 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 +789 -718
- package/dist/hocuspocus-provider.cjs.map +1 -1
- package/dist/hocuspocus-provider.esm.js +789 -718
- 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 +2 -2
- 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 +10 -11
|
@@ -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,717 +1580,696 @@ 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
1583
|
/**
|
|
1582
|
-
*
|
|
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!
|
|
1584
|
+
* Utility module to work with urls.
|
|
1602
1585
|
*
|
|
1603
|
-
*
|
|
1586
|
+
* @module url
|
|
1604
1587
|
*/
|
|
1605
1588
|
|
|
1606
|
-
const messageYjsSyncStep1 = 0;
|
|
1607
|
-
const messageYjsSyncStep2 = 1;
|
|
1608
|
-
const messageYjsUpdate = 2;
|
|
1609
|
-
|
|
1610
1589
|
/**
|
|
1611
|
-
*
|
|
1612
|
-
*
|
|
1613
|
-
* @param {encoding.Encoder} encoder
|
|
1614
|
-
* @param {Y.Doc} doc
|
|
1590
|
+
* @param {Object<string,string>} params
|
|
1591
|
+
* @return {string}
|
|
1615
1592
|
*/
|
|
1616
|
-
const
|
|
1617
|
-
|
|
1618
|
-
const sv = Y.encodeStateVector(doc);
|
|
1619
|
-
writeVarUint8Array(encoder, sv);
|
|
1620
|
-
};
|
|
1593
|
+
const encodeQueryParams = params =>
|
|
1594
|
+
map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
|
|
1621
1595
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
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 = {}));
|
|
1631
1612
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
(
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
class OutgoingMessage {
|
|
1718
|
-
constructor() {
|
|
1719
|
-
this.encoder = createEncoder();
|
|
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();
|
|
1720
1697
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1698
|
+
async onOpen(event) {
|
|
1699
|
+
this.receivedOnOpenPayload = event;
|
|
1723
1700
|
}
|
|
1724
|
-
|
|
1725
|
-
|
|
1701
|
+
async onStatus(data) {
|
|
1702
|
+
this.receivedOnStatusPayload = data;
|
|
1726
1703
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
this.
|
|
1732
|
-
|
|
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
|
+
}
|
|
1733
1714
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
return this;
|
|
1715
|
+
detach(provider) {
|
|
1716
|
+
// tell the server to remove the listener
|
|
1737
1717
|
}
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
this.applySyncMessage(provider, emitSynced);
|
|
1745
|
-
break;
|
|
1746
|
-
case MessageType.Awareness:
|
|
1747
|
-
this.applyAwarenessMessage(provider);
|
|
1748
|
-
break;
|
|
1749
|
-
case MessageType.Auth:
|
|
1750
|
-
this.applyAuthMessage(provider);
|
|
1751
|
-
break;
|
|
1752
|
-
case MessageType.QueryAwareness:
|
|
1753
|
-
this.applyQueryAwarenessMessage(provider);
|
|
1754
|
-
break;
|
|
1755
|
-
case MessageType.Stateless:
|
|
1756
|
-
provider.receiveStateless(readVarString(message.decoder));
|
|
1757
|
-
break;
|
|
1758
|
-
case MessageType.SyncStatus:
|
|
1759
|
-
// nothing for now; forward-compatability
|
|
1760
|
-
break;
|
|
1761
|
-
default:
|
|
1762
|
-
throw new Error(`Can’t apply message of unknown type: ${type}`);
|
|
1718
|
+
setConfiguration(configuration = {}) {
|
|
1719
|
+
this.configuration = { ...this.configuration, ...configuration };
|
|
1720
|
+
}
|
|
1721
|
+
async connect() {
|
|
1722
|
+
if (this.status === WebSocketStatus.Connected) {
|
|
1723
|
+
return;
|
|
1763
1724
|
}
|
|
1764
|
-
//
|
|
1765
|
-
if (
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
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;
|
|
1775
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 = [];
|
|
1776
1807
|
}
|
|
1777
1808
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
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);
|
|
1786
1846
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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.');
|
|
1790
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;
|
|
1791
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 });
|
|
1792
1921
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
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);
|
|
1805
1938
|
}
|
|
1806
1939
|
}
|
|
1807
1940
|
|
|
1808
|
-
class
|
|
1809
|
-
constructor(
|
|
1810
|
-
this.
|
|
1811
|
-
this.encoder =
|
|
1941
|
+
class IncomingMessage {
|
|
1942
|
+
constructor(data) {
|
|
1943
|
+
this.data = data;
|
|
1944
|
+
this.encoder = createEncoder();
|
|
1945
|
+
this.decoder = createDecoder(new Uint8Array(this.data));
|
|
1812
1946
|
}
|
|
1813
|
-
|
|
1814
|
-
return
|
|
1947
|
+
readVarUint() {
|
|
1948
|
+
return readVarUint(this.decoder);
|
|
1815
1949
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1950
|
+
readVarString() {
|
|
1951
|
+
return readVarString(this.decoder);
|
|
1818
1952
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1953
|
+
readVarUint8Array() {
|
|
1954
|
+
return readVarUint8Array(this.decoder);
|
|
1821
1955
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
class SyncStepOneMessage extends OutgoingMessage {
|
|
1825
|
-
constructor() {
|
|
1826
|
-
super(...arguments);
|
|
1827
|
-
this.type = MessageType.Sync;
|
|
1828
|
-
this.description = 'First sync step';
|
|
1956
|
+
writeVarUint(type) {
|
|
1957
|
+
return writeVarUint(this.encoder, type);
|
|
1829
1958
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
throw new Error('The sync step one message requires document as an argument');
|
|
1833
|
-
}
|
|
1834
|
-
writeVarString(this.encoder, args.documentName);
|
|
1835
|
-
writeVarUint(this.encoder, this.type);
|
|
1836
|
-
writeSyncStep1(this.encoder, args.document);
|
|
1837
|
-
return this.encoder;
|
|
1959
|
+
writeVarString(string) {
|
|
1960
|
+
return writeVarString(this.encoder, string);
|
|
1838
1961
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
class SyncStepTwoMessage extends OutgoingMessage {
|
|
1842
|
-
constructor() {
|
|
1843
|
-
super(...arguments);
|
|
1844
|
-
this.type = MessageType.Sync;
|
|
1845
|
-
this.description = 'Second sync step';
|
|
1962
|
+
writeVarUint8Array(data) {
|
|
1963
|
+
return writeVarUint8Array(this.encoder, data);
|
|
1846
1964
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
throw new Error('The sync step two message requires document as an argument');
|
|
1850
|
-
}
|
|
1851
|
-
writeVarString(this.encoder, args.documentName);
|
|
1852
|
-
writeVarUint(this.encoder, this.type);
|
|
1853
|
-
writeSyncStep2(this.encoder, args.document);
|
|
1854
|
-
return this.encoder;
|
|
1965
|
+
length() {
|
|
1966
|
+
return length(this.encoder);
|
|
1855
1967
|
}
|
|
1856
1968
|
}
|
|
1857
1969
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
this.type = MessageType.QueryAwareness;
|
|
1862
|
-
this.description = 'Queries awareness states';
|
|
1863
|
-
}
|
|
1864
|
-
get(args) {
|
|
1865
|
-
console.log('queryAwareness: writing string docName', args.documentName);
|
|
1866
|
-
console.log(this.encoder.cpos);
|
|
1867
|
-
writeVarString(this.encoder, args.documentName);
|
|
1868
|
-
writeVarUint(this.encoder, this.type);
|
|
1869
|
-
return this.encoder;
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1970
|
+
/**
|
|
1971
|
+
* @module sync-protocol
|
|
1972
|
+
*/
|
|
1872
1973
|
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
this.type = MessageType.Auth;
|
|
1877
|
-
this.description = 'Authentication';
|
|
1878
|
-
}
|
|
1879
|
-
get(args) {
|
|
1880
|
-
if (typeof args.token === 'undefined') {
|
|
1881
|
-
throw new Error('The authentication message requires `token` as an argument.');
|
|
1882
|
-
}
|
|
1883
|
-
writeVarString(this.encoder, args.documentName);
|
|
1884
|
-
writeVarUint(this.encoder, this.type);
|
|
1885
|
-
writeAuthentication(this.encoder, args.token);
|
|
1886
|
-
return this.encoder;
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1974
|
+
/**
|
|
1975
|
+
* @typedef {Map<number, number>} StateMap
|
|
1976
|
+
*/
|
|
1889
1977
|
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
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
|
+
*/
|
|
1916
2002
|
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
this.type = MessageType.Sync;
|
|
1921
|
-
this.description = 'A document update';
|
|
1922
|
-
}
|
|
1923
|
-
get(args) {
|
|
1924
|
-
writeVarString(this.encoder, args.documentName);
|
|
1925
|
-
writeVarUint(this.encoder, this.type);
|
|
1926
|
-
writeUpdate(this.encoder, args.update);
|
|
1927
|
-
return this.encoder;
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
2003
|
+
const messageYjsSyncStep1 = 0;
|
|
2004
|
+
const messageYjsSyncStep2 = 1;
|
|
2005
|
+
const messageYjsUpdate = 2;
|
|
1930
2006
|
|
|
1931
2007
|
/**
|
|
1932
|
-
*
|
|
2008
|
+
* Create a sync step 1 message based on the state of the current shared document.
|
|
1933
2009
|
*
|
|
1934
|
-
* @
|
|
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));
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* Read and apply Structs and then DeleteStore to a y instance.
|
|
2041
|
+
*
|
|
2042
|
+
* @param {decoding.Decoder} decoder
|
|
2043
|
+
* @param {Y.Doc} doc
|
|
2044
|
+
* @param {any} transactionOrigin
|
|
2045
|
+
*/
|
|
2046
|
+
const readSyncStep2 = (decoder, doc, transactionOrigin) => {
|
|
2047
|
+
try {
|
|
2048
|
+
Y.applyUpdate(doc, readVarUint8Array(decoder), transactionOrigin);
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
// This catches errors that are thrown by event handlers
|
|
2051
|
+
console.error('Caught error while handling a Yjs update', error);
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
|
|
2055
|
+
/**
|
|
2056
|
+
* @param {encoding.Encoder} encoder
|
|
2057
|
+
* @param {Uint8Array} update
|
|
2058
|
+
*/
|
|
2059
|
+
const writeUpdate = (encoder, update) => {
|
|
2060
|
+
writeVarUint(encoder, messageYjsUpdate);
|
|
2061
|
+
writeVarUint8Array(encoder, update);
|
|
2062
|
+
};
|
|
2063
|
+
|
|
2064
|
+
/**
|
|
2065
|
+
* Read and apply Structs and then DeleteStore to a y instance.
|
|
2066
|
+
*
|
|
2067
|
+
* @param {decoding.Decoder} decoder
|
|
2068
|
+
* @param {Y.Doc} doc
|
|
2069
|
+
* @param {any} transactionOrigin
|
|
1935
2070
|
*/
|
|
2071
|
+
const readUpdate = readSyncStep2;
|
|
1936
2072
|
|
|
1937
2073
|
/**
|
|
1938
|
-
* @param {
|
|
1939
|
-
* @
|
|
2074
|
+
* @param {decoding.Decoder} decoder A message received from another client
|
|
2075
|
+
* @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
|
|
2076
|
+
* @param {Y.Doc} doc
|
|
2077
|
+
* @param {any} transactionOrigin
|
|
1940
2078
|
*/
|
|
1941
|
-
const
|
|
1942
|
-
|
|
2079
|
+
const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => {
|
|
2080
|
+
const messageType = readVarUint(decoder);
|
|
2081
|
+
switch (messageType) {
|
|
2082
|
+
case messageYjsSyncStep1:
|
|
2083
|
+
readSyncStep1(decoder, encoder, doc);
|
|
2084
|
+
break
|
|
2085
|
+
case messageYjsSyncStep2:
|
|
2086
|
+
readSyncStep2(decoder, doc, transactionOrigin);
|
|
2087
|
+
break
|
|
2088
|
+
case messageYjsUpdate:
|
|
2089
|
+
readUpdate(decoder, doc, transactionOrigin);
|
|
2090
|
+
break
|
|
2091
|
+
default:
|
|
2092
|
+
throw new Error('Unknown message type')
|
|
2093
|
+
}
|
|
2094
|
+
return messageType
|
|
2095
|
+
};
|
|
1943
2096
|
|
|
1944
|
-
class
|
|
1945
|
-
constructor(
|
|
1946
|
-
|
|
1947
|
-
this.configuration = {
|
|
1948
|
-
url: '',
|
|
1949
|
-
// @ts-ignore
|
|
1950
|
-
document: undefined,
|
|
1951
|
-
// @ts-ignore
|
|
1952
|
-
awareness: undefined,
|
|
1953
|
-
WebSocketPolyfill: undefined,
|
|
1954
|
-
parameters: {},
|
|
1955
|
-
connect: true,
|
|
1956
|
-
broadcast: true,
|
|
1957
|
-
forceSyncInterval: false,
|
|
1958
|
-
// TODO: this should depend on awareness.outdatedTime
|
|
1959
|
-
messageReconnectTimeout: 30000,
|
|
1960
|
-
// 1 second
|
|
1961
|
-
delay: 1000,
|
|
1962
|
-
// instant
|
|
1963
|
-
initialDelay: 0,
|
|
1964
|
-
// double the delay each time
|
|
1965
|
-
factor: 2,
|
|
1966
|
-
// unlimited retries
|
|
1967
|
-
maxAttempts: 0,
|
|
1968
|
-
// wait at least 1 second
|
|
1969
|
-
minDelay: 1000,
|
|
1970
|
-
// at least every 30 seconds
|
|
1971
|
-
maxDelay: 30000,
|
|
1972
|
-
// randomize
|
|
1973
|
-
jitter: true,
|
|
1974
|
-
// retry forever
|
|
1975
|
-
timeout: 0,
|
|
1976
|
-
onOpen: () => null,
|
|
1977
|
-
onConnect: () => null,
|
|
1978
|
-
onMessage: () => null,
|
|
1979
|
-
onOutgoingMessage: () => null,
|
|
1980
|
-
onStatus: () => null,
|
|
1981
|
-
onDisconnect: () => null,
|
|
1982
|
-
onClose: () => null,
|
|
1983
|
-
onDestroy: () => null,
|
|
1984
|
-
onAwarenessUpdate: () => null,
|
|
1985
|
-
onAwarenessChange: () => null,
|
|
1986
|
-
quiet: false,
|
|
1987
|
-
};
|
|
1988
|
-
this.subscribedToBroadcastChannel = false;
|
|
1989
|
-
this.webSocket = null;
|
|
1990
|
-
this.shouldConnect = true;
|
|
1991
|
-
this.status = WebSocketStatus.Disconnected;
|
|
1992
|
-
this.lastMessageReceived = 0;
|
|
1993
|
-
this.mux = createMutex();
|
|
1994
|
-
this.intervals = {
|
|
1995
|
-
forceSync: null,
|
|
1996
|
-
connectionChecker: null,
|
|
1997
|
-
};
|
|
1998
|
-
this.connectionAttempt = null;
|
|
1999
|
-
this.receivedOnOpenPayload = undefined;
|
|
2000
|
-
this.receivedOnStatusPayload = undefined;
|
|
2001
|
-
this.boundConnect = this.connect.bind(this);
|
|
2002
|
-
this.setConfiguration(configuration);
|
|
2003
|
-
this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
|
|
2004
|
-
this.on('open', this.configuration.onOpen);
|
|
2005
|
-
this.on('open', this.onOpen.bind(this));
|
|
2006
|
-
this.on('connect', this.configuration.onConnect);
|
|
2007
|
-
this.on('message', this.configuration.onMessage);
|
|
2008
|
-
this.on('outgoingMessage', this.configuration.onOutgoingMessage);
|
|
2009
|
-
this.on('status', this.configuration.onStatus);
|
|
2010
|
-
this.on('status', this.onStatus.bind(this));
|
|
2011
|
-
this.on('disconnect', this.configuration.onDisconnect);
|
|
2012
|
-
this.on('close', this.configuration.onClose);
|
|
2013
|
-
this.on('destroy', this.configuration.onDestroy);
|
|
2014
|
-
this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
|
|
2015
|
-
this.on('awarenessChange', this.configuration.onAwarenessChange);
|
|
2016
|
-
this.on('close', this.onClose.bind(this));
|
|
2017
|
-
this.on('message', this.onMessage.bind(this));
|
|
2018
|
-
this.registerEventListeners();
|
|
2019
|
-
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
|
|
2020
|
-
if (typeof configuration.connect !== 'undefined') {
|
|
2021
|
-
this.shouldConnect = configuration.connect;
|
|
2022
|
-
}
|
|
2023
|
-
if (!this.shouldConnect) {
|
|
2024
|
-
return;
|
|
2025
|
-
}
|
|
2026
|
-
this.connect();
|
|
2027
|
-
}
|
|
2028
|
-
async onOpen(event) {
|
|
2029
|
-
this.receivedOnOpenPayload = event;
|
|
2097
|
+
class OutgoingMessage {
|
|
2098
|
+
constructor() {
|
|
2099
|
+
this.encoder = createEncoder();
|
|
2030
2100
|
}
|
|
2031
|
-
|
|
2032
|
-
|
|
2101
|
+
get(args) {
|
|
2102
|
+
return args.encoder;
|
|
2033
2103
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
provider.onOpen(this.receivedOnOpenPayload);
|
|
2037
|
-
}
|
|
2038
|
-
if (this.receivedOnStatusPayload) {
|
|
2039
|
-
provider.onStatus(this.receivedOnStatusPayload);
|
|
2040
|
-
}
|
|
2104
|
+
toUint8Array() {
|
|
2105
|
+
return toUint8Array(this.encoder);
|
|
2041
2106
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
class MessageReceiver {
|
|
2110
|
+
constructor(message) {
|
|
2111
|
+
this.broadcasted = false;
|
|
2112
|
+
this.message = message;
|
|
2044
2113
|
}
|
|
2045
|
-
|
|
2046
|
-
this.
|
|
2114
|
+
setBroadcasted(value) {
|
|
2115
|
+
this.broadcasted = value;
|
|
2116
|
+
return this;
|
|
2047
2117
|
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
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}`);
|
|
2056
2143
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
factor: this.configuration.factor,
|
|
2064
|
-
maxAttempts: this.configuration.maxAttempts,
|
|
2065
|
-
minDelay: this.configuration.minDelay,
|
|
2066
|
-
maxDelay: this.configuration.maxDelay,
|
|
2067
|
-
jitter: this.configuration.jitter,
|
|
2068
|
-
timeout: this.configuration.timeout,
|
|
2069
|
-
beforeAttempt: context => {
|
|
2070
|
-
if (!this.shouldConnect || cancelAttempt) {
|
|
2071
|
-
context.abort();
|
|
2072
|
-
}
|
|
2073
|
-
},
|
|
2074
|
-
}).catch((error) => {
|
|
2075
|
-
// If we aborted the connection attempt then don’t throw an error
|
|
2076
|
-
// ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
|
|
2077
|
-
if (error && error.code !== 'ATTEMPT_ABORTED') {
|
|
2078
|
-
throw error;
|
|
2079
|
-
}
|
|
2080
|
-
});
|
|
2081
|
-
return {
|
|
2082
|
-
retryPromise,
|
|
2083
|
-
cancelFunc: () => {
|
|
2084
|
-
cancelAttempt = true;
|
|
2085
|
-
},
|
|
2086
|
-
};
|
|
2087
|
-
};
|
|
2088
|
-
const { retryPromise, cancelFunc } = abortableRetry();
|
|
2089
|
-
this.cancelWebsocketRetry = cancelFunc;
|
|
2090
|
-
return retryPromise;
|
|
2091
|
-
}
|
|
2092
|
-
createWebSocketConnection() {
|
|
2093
|
-
return new Promise((resolve, reject) => {
|
|
2094
|
-
if (this.webSocket) {
|
|
2095
|
-
this.webSocket.close();
|
|
2096
|
-
this.webSocket = null;
|
|
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 });
|
|
2097
2150
|
}
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
ws.onerror = (err) => {
|
|
2105
|
-
reject(err);
|
|
2106
|
-
};
|
|
2107
|
-
this.webSocket = ws;
|
|
2108
|
-
// Reset the status
|
|
2109
|
-
this.status = WebSocketStatus.Connecting;
|
|
2110
|
-
this.emit('status', { status: WebSocketStatus.Connecting });
|
|
2111
|
-
// Store resolve/reject for later use
|
|
2112
|
-
this.connectionAttempt = {
|
|
2113
|
-
resolve,
|
|
2114
|
-
reject,
|
|
2115
|
-
};
|
|
2116
|
-
});
|
|
2151
|
+
else {
|
|
2152
|
+
// TODO: Some weird TypeScript error
|
|
2153
|
+
// @ts-ignore
|
|
2154
|
+
provider.send(OutgoingMessage, { encoder: message.encoder });
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2117
2157
|
}
|
|
2118
|
-
|
|
2119
|
-
this
|
|
2120
|
-
|
|
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;
|
|
2166
|
+
}
|
|
2121
2167
|
}
|
|
2122
|
-
|
|
2123
|
-
if (
|
|
2124
|
-
|
|
2125
|
-
this.connectionAttempt = null;
|
|
2126
|
-
this.status = WebSocketStatus.Connected;
|
|
2127
|
-
this.emit('status', { status: WebSocketStatus.Connected });
|
|
2128
|
-
this.emit('connect');
|
|
2168
|
+
applySyncStatusMessage(provider, applied) {
|
|
2169
|
+
if (applied) {
|
|
2170
|
+
provider.decrementUnsyncedChanges();
|
|
2129
2171
|
}
|
|
2130
2172
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2173
|
+
applyAwarenessMessage(provider) {
|
|
2174
|
+
const { message } = this;
|
|
2175
|
+
applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
|
|
2133
2176
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
(
|
|
2137
|
-
this.connectionAttempt = null;
|
|
2177
|
+
applyAuthMessage(provider) {
|
|
2178
|
+
const { message } = this;
|
|
2179
|
+
readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
|
|
2138
2180
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
return;
|
|
2144
|
-
}
|
|
2145
|
-
// Don’t close then connection while waiting for the first message
|
|
2146
|
-
if (!this.lastMessageReceived) {
|
|
2147
|
-
return;
|
|
2148
|
-
}
|
|
2149
|
-
// Don’t close the connection when a message was received recently
|
|
2150
|
-
if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
|
|
2151
|
-
return;
|
|
2152
|
-
}
|
|
2153
|
-
// No message received in a long time, not even your own
|
|
2154
|
-
// Awareness updates, which are updated every 15 seconds.
|
|
2155
|
-
(_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())));
|
|
2156
2185
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
class MessageSender {
|
|
2189
|
+
constructor(Message, args = {}) {
|
|
2190
|
+
this.message = new Message();
|
|
2191
|
+
this.encoder = this.message.get(args);
|
|
2162
2192
|
}
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
while (this.configuration.url[this.configuration.url.length - 1] === '/') {
|
|
2166
|
-
return this.configuration.url.slice(0, this.configuration.url.length - 1);
|
|
2167
|
-
}
|
|
2168
|
-
return this.configuration.url;
|
|
2193
|
+
create() {
|
|
2194
|
+
return toUint8Array(this.encoder);
|
|
2169
2195
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
|
|
2196
|
+
send(webSocket) {
|
|
2197
|
+
webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
|
|
2173
2198
|
}
|
|
2174
|
-
|
|
2175
|
-
this.
|
|
2176
|
-
if (this.webSocket === null) {
|
|
2177
|
-
return;
|
|
2178
|
-
}
|
|
2179
|
-
try {
|
|
2180
|
-
this.webSocket.close();
|
|
2181
|
-
}
|
|
2182
|
-
catch {
|
|
2183
|
-
//
|
|
2184
|
-
}
|
|
2199
|
+
broadcast(channel) {
|
|
2200
|
+
publish(channel, this.create());
|
|
2185
2201
|
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
class AuthenticationMessage extends OutgoingMessage {
|
|
2205
|
+
constructor() {
|
|
2206
|
+
super(...arguments);
|
|
2207
|
+
this.type = MessageType.Auth;
|
|
2208
|
+
this.description = 'Authentication';
|
|
2191
2209
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
this.status = WebSocketStatus.Disconnected;
|
|
2196
|
-
this.emit('status', { status: WebSocketStatus.Disconnected });
|
|
2197
|
-
this.emit('disconnect', { event });
|
|
2198
|
-
}
|
|
2199
|
-
if (event.code === Unauthorized.code) {
|
|
2200
|
-
if (event.reason === Unauthorized.reason) {
|
|
2201
|
-
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.');
|
|
2202
|
-
}
|
|
2203
|
-
else {
|
|
2204
|
-
console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
|
|
2205
|
-
}
|
|
2206
|
-
this.shouldConnect = false;
|
|
2207
|
-
}
|
|
2208
|
-
if (event.code === Forbidden.code) {
|
|
2209
|
-
if (!this.configuration.quiet) {
|
|
2210
|
-
console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
|
|
2211
|
-
return; // TODO REMOVE ME
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
if (event.code === MessageTooBig.code) {
|
|
2215
|
-
console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
|
|
2216
|
-
this.shouldConnect = false;
|
|
2210
|
+
get(args) {
|
|
2211
|
+
if (typeof args.token === 'undefined') {
|
|
2212
|
+
throw new Error('The authentication message requires `token` as an argument.');
|
|
2217
2213
|
}
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
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');
|
|
2221
2230
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
this.connect();
|
|
2231
|
+
if (typeof args.clients === 'undefined') {
|
|
2232
|
+
throw new Error('The awareness message requires clients as an argument');
|
|
2225
2233
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
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);
|
|
2229
2239
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
return;
|
|
2240
|
+
else {
|
|
2241
|
+
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
|
|
2233
2242
|
}
|
|
2234
|
-
|
|
2235
|
-
this.
|
|
2236
|
-
this.emit('status', { status: WebSocketStatus.Disconnected });
|
|
2237
|
-
this.emit('disconnect', { event });
|
|
2243
|
+
writeVarUint8Array(this.encoder, awarenessUpdate);
|
|
2244
|
+
return this.encoder;
|
|
2238
2245
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
this.
|
|
2249
|
-
this.
|
|
2250
|
-
this.
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
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;
|
|
2255
2273
|
}
|
|
2256
2274
|
}
|
|
2257
2275
|
|
|
@@ -2270,15 +2288,50 @@ class StatelessMessage extends OutgoingMessage {
|
|
|
2270
2288
|
}
|
|
2271
2289
|
}
|
|
2272
2290
|
|
|
2273
|
-
class
|
|
2291
|
+
class SyncStepOneMessage extends OutgoingMessage {
|
|
2274
2292
|
constructor() {
|
|
2275
2293
|
super(...arguments);
|
|
2276
|
-
this.type = MessageType.
|
|
2277
|
-
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';
|
|
2278
2330
|
}
|
|
2279
2331
|
get(args) {
|
|
2280
2332
|
writeVarString(this.encoder, args.documentName);
|
|
2281
2333
|
writeVarUint(this.encoder, this.type);
|
|
2334
|
+
writeUpdate(this.encoder, args.update);
|
|
2282
2335
|
return this.encoder;
|
|
2283
2336
|
}
|
|
2284
2337
|
}
|
|
@@ -2311,6 +2364,8 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2311
2364
|
onAwarenessChange: () => null,
|
|
2312
2365
|
onStateless: () => null,
|
|
2313
2366
|
quiet: false,
|
|
2367
|
+
connect: true,
|
|
2368
|
+
preserveConnection: true,
|
|
2314
2369
|
};
|
|
2315
2370
|
this.subscribedToBroadcastChannel = false;
|
|
2316
2371
|
this.isSynced = false;
|
|
@@ -2324,7 +2379,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2324
2379
|
};
|
|
2325
2380
|
this.isConnected = true;
|
|
2326
2381
|
this.boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this);
|
|
2327
|
-
this.
|
|
2382
|
+
this.boundPageUnload = this.pageUnload.bind(this);
|
|
2328
2383
|
this.boundOnOpen = this.onOpen.bind(this);
|
|
2329
2384
|
this.boundOnMessage = this.onMessage.bind(this);
|
|
2330
2385
|
this.boundOnClose = this.onClose.bind(this);
|
|
@@ -2384,6 +2439,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2384
2439
|
const websocketProviderConfig = configuration;
|
|
2385
2440
|
this.configuration.websocketProvider = new HocuspocusProviderWebsocket({
|
|
2386
2441
|
url: websocketProviderConfig.url,
|
|
2442
|
+
connect: websocketProviderConfig.connect,
|
|
2387
2443
|
parameters: websocketProviderConfig.parameters,
|
|
2388
2444
|
});
|
|
2389
2445
|
}
|
|
@@ -2398,21 +2454,28 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2398
2454
|
get hasUnsyncedChanges() {
|
|
2399
2455
|
return this.unsyncedChanges > 0;
|
|
2400
2456
|
}
|
|
2401
|
-
|
|
2402
|
-
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
|
+
}
|
|
2403
2466
|
this.emit('unsyncedChanges', this.unsyncedChanges);
|
|
2404
2467
|
}
|
|
2405
2468
|
forceSync() {
|
|
2406
2469
|
this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
|
|
2407
2470
|
}
|
|
2408
|
-
|
|
2471
|
+
pageUnload() {
|
|
2409
2472
|
removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload');
|
|
2410
2473
|
}
|
|
2411
2474
|
registerEventListeners() {
|
|
2412
2475
|
if (typeof window === 'undefined') {
|
|
2413
2476
|
return;
|
|
2414
2477
|
}
|
|
2415
|
-
window.addEventListener('
|
|
2478
|
+
window.addEventListener('unload', this.boundPageUnload);
|
|
2416
2479
|
}
|
|
2417
2480
|
sendStateless(payload) {
|
|
2418
2481
|
this.send(StatelessMessage, { documentName: this.configuration.name, payload });
|
|
@@ -2421,7 +2484,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2421
2484
|
if (origin === this) {
|
|
2422
2485
|
return;
|
|
2423
2486
|
}
|
|
2424
|
-
this.
|
|
2487
|
+
this.incrementUnsyncedChanges();
|
|
2425
2488
|
this.send(UpdateMessage, { update, documentName: this.configuration.name }, true);
|
|
2426
2489
|
}
|
|
2427
2490
|
awarenessUpdateHandler({ added, updated, removed }, origin) {
|
|
@@ -2432,6 +2495,12 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2432
2495
|
documentName: this.configuration.name,
|
|
2433
2496
|
}, true);
|
|
2434
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
|
+
*/
|
|
2435
2504
|
get synced() {
|
|
2436
2505
|
return this.isSynced;
|
|
2437
2506
|
}
|
|
@@ -2439,9 +2508,6 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2439
2508
|
if (this.isSynced === state) {
|
|
2440
2509
|
return;
|
|
2441
2510
|
}
|
|
2442
|
-
if (state && this.unsyncedChanges > 0) {
|
|
2443
|
-
this.updateUnsyncedChanges(-1 * this.unsyncedChanges);
|
|
2444
|
-
}
|
|
2445
2511
|
this.isSynced = state;
|
|
2446
2512
|
this.emit('synced', { state });
|
|
2447
2513
|
this.emit('sync', { state });
|
|
@@ -2459,6 +2525,9 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2459
2525
|
disconnect() {
|
|
2460
2526
|
this.disconnectBroadcastChannel();
|
|
2461
2527
|
this.configuration.websocketProvider.detach(this);
|
|
2528
|
+
if (!this.configuration.preserveConnection) {
|
|
2529
|
+
this.configuration.websocketProvider.disconnect();
|
|
2530
|
+
}
|
|
2462
2531
|
}
|
|
2463
2532
|
async onOpen(event) {
|
|
2464
2533
|
this.isAuthenticated = false;
|
|
@@ -2479,6 +2548,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2479
2548
|
return this.configuration.token;
|
|
2480
2549
|
}
|
|
2481
2550
|
startSync() {
|
|
2551
|
+
this.incrementUnsyncedChanges();
|
|
2482
2552
|
this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
|
|
2483
2553
|
if (this.awareness.getLocalState() !== null) {
|
|
2484
2554
|
this.send(AwarenessMessage, {
|
|
@@ -2489,8 +2559,9 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2489
2559
|
}
|
|
2490
2560
|
}
|
|
2491
2561
|
send(message, args, broadcast = false) {
|
|
2492
|
-
if (!this.isConnected)
|
|
2562
|
+
if (!this.isConnected) {
|
|
2493
2563
|
return;
|
|
2564
|
+
}
|
|
2494
2565
|
if (broadcast) {
|
|
2495
2566
|
this.mux(() => { this.broadcast(message, args); });
|
|
2496
2567
|
}
|
|
@@ -2506,7 +2577,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2506
2577
|
}
|
|
2507
2578
|
message.writeVarString(documentName);
|
|
2508
2579
|
this.emit('message', { event, message: new IncomingMessage(event.data) });
|
|
2509
|
-
new MessageReceiver(message).apply(this);
|
|
2580
|
+
new MessageReceiver(message).apply(this, true);
|
|
2510
2581
|
}
|
|
2511
2582
|
onClose(event) {
|
|
2512
2583
|
this.isAuthenticated = false;
|
|
@@ -2542,7 +2613,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2542
2613
|
if (typeof window === 'undefined') {
|
|
2543
2614
|
return;
|
|
2544
2615
|
}
|
|
2545
|
-
window.removeEventListener('
|
|
2616
|
+
window.removeEventListener('unload', this.boundPageUnload);
|
|
2546
2617
|
}
|
|
2547
2618
|
permissionDeniedHandler(reason) {
|
|
2548
2619
|
this.emit('authenticationFailed', { reason });
|