@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
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var Y = require('yjs');
|
|
6
5
|
var common = require('@hocuspocus/common');
|
|
6
|
+
var Y = require('yjs');
|
|
7
7
|
var attempt = require('@lifeomic/attempt');
|
|
8
8
|
|
|
9
9
|
function _interopNamespace(e) {
|
|
@@ -74,6 +74,22 @@ const setIfUndefined = (map, key, createT) => {
|
|
|
74
74
|
|
|
75
75
|
const create$1 = () => new Set();
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Utility module to work with Arrays.
|
|
79
|
+
*
|
|
80
|
+
* @module array
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Transforms something array-like to an actual Array.
|
|
85
|
+
*
|
|
86
|
+
* @function
|
|
87
|
+
* @template T
|
|
88
|
+
* @param {ArrayLike<T>|Iterable<T>} arraylike
|
|
89
|
+
* @return {T}
|
|
90
|
+
*/
|
|
91
|
+
const from = Array.from;
|
|
92
|
+
|
|
77
93
|
/**
|
|
78
94
|
* Utility module to work with strings.
|
|
79
95
|
*
|
|
@@ -236,22 +252,6 @@ const onChange = eventHandler => usePolyfill || addEventListener('storage', /**
|
|
|
236
252
|
/* c8 ignore next */
|
|
237
253
|
const offChange = eventHandler => usePolyfill || removeEventListener('storage', /** @type {any} */ (eventHandler));
|
|
238
254
|
|
|
239
|
-
/**
|
|
240
|
-
* Utility module to work with Arrays.
|
|
241
|
-
*
|
|
242
|
-
* @module array
|
|
243
|
-
*/
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Transforms something array-like to an actual Array.
|
|
247
|
-
*
|
|
248
|
-
* @function
|
|
249
|
-
* @template T
|
|
250
|
-
* @param {ArrayLike<T>|Iterable<T>} arraylike
|
|
251
|
-
* @return {T}
|
|
252
|
-
*/
|
|
253
|
-
const from = Array.from;
|
|
254
|
-
|
|
255
255
|
/**
|
|
256
256
|
* Utility functions for working with EcmaScript objects.
|
|
257
257
|
*
|
|
@@ -399,7 +399,6 @@ const equalityDeep = (a, b) => {
|
|
|
399
399
|
*/
|
|
400
400
|
// @ts-ignore
|
|
401
401
|
const isOneOf = (value, options) => options.includes(value);
|
|
402
|
-
/* c8 ignore stop */
|
|
403
402
|
|
|
404
403
|
/**
|
|
405
404
|
* Isomorphic module to work access the environment (query params, env variables).
|
|
@@ -531,7 +530,9 @@ const min = (a, b) => a < b ? a : b;
|
|
|
531
530
|
const max = (a, b) => a > b ? a : b;
|
|
532
531
|
|
|
533
532
|
/* eslint-env browser */
|
|
533
|
+
const BIT7 = 64;
|
|
534
534
|
const BIT8 = 128;
|
|
535
|
+
const BITS6 = 63;
|
|
535
536
|
const BITS7 = 127;
|
|
536
537
|
|
|
537
538
|
/**
|
|
@@ -889,6 +890,44 @@ const readVarUint = decoder => {
|
|
|
889
890
|
throw errorUnexpectedEndOfArray
|
|
890
891
|
};
|
|
891
892
|
|
|
893
|
+
/**
|
|
894
|
+
* Read signed integer (32bit) with variable length.
|
|
895
|
+
* 1/8th of the storage is used as encoding overhead.
|
|
896
|
+
* * numbers < 2^7 is stored in one bytlength
|
|
897
|
+
* * numbers < 2^14 is stored in two bylength
|
|
898
|
+
* @todo This should probably create the inverse ~num if number is negative - but this would be a breaking change.
|
|
899
|
+
*
|
|
900
|
+
* @function
|
|
901
|
+
* @param {Decoder} decoder
|
|
902
|
+
* @return {number} An unsigned integer.length
|
|
903
|
+
*/
|
|
904
|
+
const readVarInt = decoder => {
|
|
905
|
+
let r = decoder.arr[decoder.pos++];
|
|
906
|
+
let num = r & BITS6;
|
|
907
|
+
let mult = 64;
|
|
908
|
+
const sign = (r & BIT7) > 0 ? -1 : 1;
|
|
909
|
+
if ((r & BIT8) === 0) {
|
|
910
|
+
// don't continue reading
|
|
911
|
+
return sign * num
|
|
912
|
+
}
|
|
913
|
+
const len = decoder.arr.length;
|
|
914
|
+
while (decoder.pos < len) {
|
|
915
|
+
r = decoder.arr[decoder.pos++];
|
|
916
|
+
// num = num | ((r & binary.BITS7) << len)
|
|
917
|
+
num = num + (r & BITS7) * mult;
|
|
918
|
+
mult *= 128;
|
|
919
|
+
if (r < BIT8) {
|
|
920
|
+
return sign * num
|
|
921
|
+
}
|
|
922
|
+
/* c8 ignore start */
|
|
923
|
+
if (num > MAX_SAFE_INTEGER) {
|
|
924
|
+
throw errorIntegerOutOfRange
|
|
925
|
+
}
|
|
926
|
+
/* c8 ignore stop */
|
|
927
|
+
}
|
|
928
|
+
throw errorUnexpectedEndOfArray
|
|
929
|
+
};
|
|
930
|
+
|
|
892
931
|
/**
|
|
893
932
|
* We don't test this function anymore as we use native decoding/encoding by default now.
|
|
894
933
|
* Better not modify this anymore..
|
|
@@ -1136,6 +1175,50 @@ const publish = (room, data, origin = null) => {
|
|
|
1136
1175
|
c.subs.forEach(sub => sub(data, origin));
|
|
1137
1176
|
};
|
|
1138
1177
|
|
|
1178
|
+
/**
|
|
1179
|
+
* Mutual exclude for JavaScript.
|
|
1180
|
+
*
|
|
1181
|
+
* @module mutex
|
|
1182
|
+
*/
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* @callback mutex
|
|
1186
|
+
* @param {function():void} cb Only executed when this mutex is not in the current stack
|
|
1187
|
+
* @param {function():void} [elseCb] Executed when this mutex is in the current stack
|
|
1188
|
+
*/
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Creates a mutual exclude function with the following property:
|
|
1192
|
+
*
|
|
1193
|
+
* ```js
|
|
1194
|
+
* const mutex = createMutex()
|
|
1195
|
+
* mutex(() => {
|
|
1196
|
+
* // This function is immediately executed
|
|
1197
|
+
* mutex(() => {
|
|
1198
|
+
* // This function is not executed, as the mutex is already active.
|
|
1199
|
+
* })
|
|
1200
|
+
* })
|
|
1201
|
+
* ```
|
|
1202
|
+
*
|
|
1203
|
+
* @return {mutex} A mutual exclude function
|
|
1204
|
+
* @public
|
|
1205
|
+
*/
|
|
1206
|
+
const createMutex = () => {
|
|
1207
|
+
let token = true;
|
|
1208
|
+
return (f, g) => {
|
|
1209
|
+
if (token) {
|
|
1210
|
+
token = false;
|
|
1211
|
+
try {
|
|
1212
|
+
f();
|
|
1213
|
+
} finally {
|
|
1214
|
+
token = true;
|
|
1215
|
+
}
|
|
1216
|
+
} else if (g !== undefined) {
|
|
1217
|
+
g();
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1139
1222
|
/**
|
|
1140
1223
|
* Utility module to work with time.
|
|
1141
1224
|
*
|
|
@@ -1486,50 +1569,6 @@ const applyAwarenessUpdate = (awareness, update, origin) => {
|
|
|
1486
1569
|
}
|
|
1487
1570
|
};
|
|
1488
1571
|
|
|
1489
|
-
/**
|
|
1490
|
-
* Mutual exclude for JavaScript.
|
|
1491
|
-
*
|
|
1492
|
-
* @module mutex
|
|
1493
|
-
*/
|
|
1494
|
-
|
|
1495
|
-
/**
|
|
1496
|
-
* @callback mutex
|
|
1497
|
-
* @param {function():void} cb Only executed when this mutex is not in the current stack
|
|
1498
|
-
* @param {function():void} [elseCb] Executed when this mutex is in the current stack
|
|
1499
|
-
*/
|
|
1500
|
-
|
|
1501
|
-
/**
|
|
1502
|
-
* Creates a mutual exclude function with the following property:
|
|
1503
|
-
*
|
|
1504
|
-
* ```js
|
|
1505
|
-
* const mutex = createMutex()
|
|
1506
|
-
* mutex(() => {
|
|
1507
|
-
* // This function is immediately executed
|
|
1508
|
-
* mutex(() => {
|
|
1509
|
-
* // This function is not executed, as the mutex is already active.
|
|
1510
|
-
* })
|
|
1511
|
-
* })
|
|
1512
|
-
* ```
|
|
1513
|
-
*
|
|
1514
|
-
* @return {mutex} A mutual exclude function
|
|
1515
|
-
* @public
|
|
1516
|
-
*/
|
|
1517
|
-
const createMutex = () => {
|
|
1518
|
-
let token = true;
|
|
1519
|
-
return (f, g) => {
|
|
1520
|
-
if (token) {
|
|
1521
|
-
token = false;
|
|
1522
|
-
try {
|
|
1523
|
-
f();
|
|
1524
|
-
} finally {
|
|
1525
|
-
token = true;
|
|
1526
|
-
}
|
|
1527
|
-
} else if (g !== undefined) {
|
|
1528
|
-
g();
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
};
|
|
1532
|
-
|
|
1533
1572
|
class EventEmitter {
|
|
1534
1573
|
constructor() {
|
|
1535
1574
|
this.callbacks = {};
|
|
@@ -1565,717 +1604,696 @@ class EventEmitter {
|
|
|
1565
1604
|
}
|
|
1566
1605
|
}
|
|
1567
1606
|
|
|
1568
|
-
class IncomingMessage {
|
|
1569
|
-
constructor(data) {
|
|
1570
|
-
this.data = data;
|
|
1571
|
-
this.encoder = createEncoder();
|
|
1572
|
-
this.decoder = createDecoder(new Uint8Array(this.data));
|
|
1573
|
-
}
|
|
1574
|
-
readVarUint() {
|
|
1575
|
-
return readVarUint(this.decoder);
|
|
1576
|
-
}
|
|
1577
|
-
readVarString() {
|
|
1578
|
-
return readVarString(this.decoder);
|
|
1579
|
-
}
|
|
1580
|
-
readVarUint8Array() {
|
|
1581
|
-
return readVarUint8Array(this.decoder);
|
|
1582
|
-
}
|
|
1583
|
-
writeVarUint(type) {
|
|
1584
|
-
return writeVarUint(this.encoder, type);
|
|
1585
|
-
}
|
|
1586
|
-
writeVarString(string) {
|
|
1587
|
-
return writeVarString(this.encoder, string);
|
|
1588
|
-
}
|
|
1589
|
-
writeVarUint8Array(data) {
|
|
1590
|
-
return writeVarUint8Array(this.encoder, data);
|
|
1591
|
-
}
|
|
1592
|
-
length() {
|
|
1593
|
-
return length(this.encoder);
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
1607
|
/**
|
|
1598
|
-
*
|
|
1599
|
-
*/
|
|
1600
|
-
|
|
1601
|
-
/**
|
|
1602
|
-
* @typedef {Map<number, number>} StateMap
|
|
1603
|
-
*/
|
|
1604
|
-
|
|
1605
|
-
/**
|
|
1606
|
-
* Core Yjs defines two message types:
|
|
1607
|
-
* • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
|
|
1608
|
-
* • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
|
|
1609
|
-
* received all information from the remote client.
|
|
1610
|
-
*
|
|
1611
|
-
* In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
|
|
1612
|
-
* with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
|
|
1613
|
-
* SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
|
|
1614
|
-
*
|
|
1615
|
-
* In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
|
|
1616
|
-
* When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
|
|
1617
|
-
* with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
|
|
1618
|
-
* client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
|
|
1619
|
-
* easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
|
|
1620
|
-
* Therefore it is necesarry that the client initiates the sync.
|
|
1621
|
-
*
|
|
1622
|
-
* Construction of a message:
|
|
1623
|
-
* [messageType : varUint, message definition..]
|
|
1624
|
-
*
|
|
1625
|
-
* Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
|
|
1608
|
+
* Utility module to work with urls.
|
|
1626
1609
|
*
|
|
1627
|
-
*
|
|
1610
|
+
* @module url
|
|
1628
1611
|
*/
|
|
1629
1612
|
|
|
1630
|
-
const messageYjsSyncStep1 = 0;
|
|
1631
|
-
const messageYjsSyncStep2 = 1;
|
|
1632
|
-
const messageYjsUpdate = 2;
|
|
1633
|
-
|
|
1634
1613
|
/**
|
|
1635
|
-
*
|
|
1636
|
-
*
|
|
1637
|
-
* @param {encoding.Encoder} encoder
|
|
1638
|
-
* @param {Y.Doc} doc
|
|
1614
|
+
* @param {Object<string,string>} params
|
|
1615
|
+
* @return {string}
|
|
1639
1616
|
*/
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
const sv = Y__namespace.encodeStateVector(doc);
|
|
1643
|
-
writeVarUint8Array(encoder, sv);
|
|
1644
|
-
};
|
|
1617
|
+
const encodeQueryParams = params =>
|
|
1618
|
+
map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
|
|
1645
1619
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1620
|
+
exports.MessageType = void 0;
|
|
1621
|
+
(function (MessageType) {
|
|
1622
|
+
MessageType[MessageType["Sync"] = 0] = "Sync";
|
|
1623
|
+
MessageType[MessageType["Awareness"] = 1] = "Awareness";
|
|
1624
|
+
MessageType[MessageType["Auth"] = 2] = "Auth";
|
|
1625
|
+
MessageType[MessageType["QueryAwareness"] = 3] = "QueryAwareness";
|
|
1626
|
+
MessageType[MessageType["Stateless"] = 5] = "Stateless";
|
|
1627
|
+
MessageType[MessageType["CLOSE"] = 7] = "CLOSE";
|
|
1628
|
+
MessageType[MessageType["SyncStatus"] = 8] = "SyncStatus";
|
|
1629
|
+
})(exports.MessageType || (exports.MessageType = {}));
|
|
1630
|
+
exports.WebSocketStatus = void 0;
|
|
1631
|
+
(function (WebSocketStatus) {
|
|
1632
|
+
WebSocketStatus["Connecting"] = "connecting";
|
|
1633
|
+
WebSocketStatus["Connected"] = "connected";
|
|
1634
|
+
WebSocketStatus["Disconnected"] = "disconnected";
|
|
1635
|
+
})(exports.WebSocketStatus || (exports.WebSocketStatus = {}));
|
|
1655
1636
|
|
|
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
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
(
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
class OutgoingMessage {
|
|
1742
|
-
constructor() {
|
|
1743
|
-
this.encoder = createEncoder();
|
|
1637
|
+
class HocuspocusProviderWebsocket extends EventEmitter {
|
|
1638
|
+
constructor(configuration) {
|
|
1639
|
+
super();
|
|
1640
|
+
this.messageQueue = [];
|
|
1641
|
+
this.configuration = {
|
|
1642
|
+
url: '',
|
|
1643
|
+
// @ts-ignore
|
|
1644
|
+
document: undefined,
|
|
1645
|
+
// @ts-ignore
|
|
1646
|
+
awareness: undefined,
|
|
1647
|
+
WebSocketPolyfill: undefined,
|
|
1648
|
+
parameters: {},
|
|
1649
|
+
connect: true,
|
|
1650
|
+
broadcast: true,
|
|
1651
|
+
forceSyncInterval: false,
|
|
1652
|
+
// TODO: this should depend on awareness.outdatedTime
|
|
1653
|
+
messageReconnectTimeout: 30000,
|
|
1654
|
+
// 1 second
|
|
1655
|
+
delay: 1000,
|
|
1656
|
+
// instant
|
|
1657
|
+
initialDelay: 0,
|
|
1658
|
+
// double the delay each time
|
|
1659
|
+
factor: 2,
|
|
1660
|
+
// unlimited retries
|
|
1661
|
+
maxAttempts: 0,
|
|
1662
|
+
// wait at least 1 second
|
|
1663
|
+
minDelay: 1000,
|
|
1664
|
+
// at least every 30 seconds
|
|
1665
|
+
maxDelay: 30000,
|
|
1666
|
+
// randomize
|
|
1667
|
+
jitter: true,
|
|
1668
|
+
// retry forever
|
|
1669
|
+
timeout: 0,
|
|
1670
|
+
onOpen: () => null,
|
|
1671
|
+
onConnect: () => null,
|
|
1672
|
+
onMessage: () => null,
|
|
1673
|
+
onOutgoingMessage: () => null,
|
|
1674
|
+
onStatus: () => null,
|
|
1675
|
+
onDisconnect: () => null,
|
|
1676
|
+
onClose: () => null,
|
|
1677
|
+
onDestroy: () => null,
|
|
1678
|
+
onAwarenessUpdate: () => null,
|
|
1679
|
+
onAwarenessChange: () => null,
|
|
1680
|
+
quiet: false,
|
|
1681
|
+
};
|
|
1682
|
+
this.subscribedToBroadcastChannel = false;
|
|
1683
|
+
this.webSocket = null;
|
|
1684
|
+
this.shouldConnect = true;
|
|
1685
|
+
this.status = exports.WebSocketStatus.Disconnected;
|
|
1686
|
+
this.lastMessageReceived = 0;
|
|
1687
|
+
this.mux = createMutex();
|
|
1688
|
+
this.intervals = {
|
|
1689
|
+
forceSync: null,
|
|
1690
|
+
connectionChecker: null,
|
|
1691
|
+
};
|
|
1692
|
+
this.connectionAttempt = null;
|
|
1693
|
+
this.receivedOnOpenPayload = undefined;
|
|
1694
|
+
this.receivedOnStatusPayload = undefined;
|
|
1695
|
+
this.boundConnect = this.connect.bind(this);
|
|
1696
|
+
this.setConfiguration(configuration);
|
|
1697
|
+
this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
|
|
1698
|
+
this.on('open', this.configuration.onOpen);
|
|
1699
|
+
this.on('open', this.onOpen.bind(this));
|
|
1700
|
+
this.on('connect', this.configuration.onConnect);
|
|
1701
|
+
this.on('message', this.configuration.onMessage);
|
|
1702
|
+
this.on('outgoingMessage', this.configuration.onOutgoingMessage);
|
|
1703
|
+
this.on('status', this.configuration.onStatus);
|
|
1704
|
+
this.on('status', this.onStatus.bind(this));
|
|
1705
|
+
this.on('disconnect', this.configuration.onDisconnect);
|
|
1706
|
+
this.on('close', this.configuration.onClose);
|
|
1707
|
+
this.on('destroy', this.configuration.onDestroy);
|
|
1708
|
+
this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
|
|
1709
|
+
this.on('awarenessChange', this.configuration.onAwarenessChange);
|
|
1710
|
+
this.on('close', this.onClose.bind(this));
|
|
1711
|
+
this.on('message', this.onMessage.bind(this));
|
|
1712
|
+
this.registerEventListeners();
|
|
1713
|
+
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
|
|
1714
|
+
if (typeof configuration.connect !== 'undefined') {
|
|
1715
|
+
this.shouldConnect = configuration.connect;
|
|
1716
|
+
}
|
|
1717
|
+
if (!this.shouldConnect) {
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
this.connect();
|
|
1744
1721
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1722
|
+
async onOpen(event) {
|
|
1723
|
+
this.receivedOnOpenPayload = event;
|
|
1747
1724
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1725
|
+
async onStatus(data) {
|
|
1726
|
+
this.receivedOnStatusPayload = data;
|
|
1750
1727
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
this.
|
|
1756
|
-
|
|
1728
|
+
attach(provider) {
|
|
1729
|
+
if (this.status === exports.WebSocketStatus.Disconnected && this.shouldConnect) {
|
|
1730
|
+
this.connect();
|
|
1731
|
+
}
|
|
1732
|
+
if (this.receivedOnOpenPayload) {
|
|
1733
|
+
provider.onOpen(this.receivedOnOpenPayload);
|
|
1734
|
+
}
|
|
1735
|
+
if (this.receivedOnStatusPayload) {
|
|
1736
|
+
provider.onStatus(this.receivedOnStatusPayload);
|
|
1737
|
+
}
|
|
1757
1738
|
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
return this;
|
|
1739
|
+
detach(provider) {
|
|
1740
|
+
// tell the server to remove the listener
|
|
1761
1741
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
this.applySyncMessage(provider, emitSynced);
|
|
1769
|
-
break;
|
|
1770
|
-
case exports.MessageType.Awareness:
|
|
1771
|
-
this.applyAwarenessMessage(provider);
|
|
1772
|
-
break;
|
|
1773
|
-
case exports.MessageType.Auth:
|
|
1774
|
-
this.applyAuthMessage(provider);
|
|
1775
|
-
break;
|
|
1776
|
-
case exports.MessageType.QueryAwareness:
|
|
1777
|
-
this.applyQueryAwarenessMessage(provider);
|
|
1778
|
-
break;
|
|
1779
|
-
case exports.MessageType.Stateless:
|
|
1780
|
-
provider.receiveStateless(readVarString(message.decoder));
|
|
1781
|
-
break;
|
|
1782
|
-
case exports.MessageType.SyncStatus:
|
|
1783
|
-
// nothing for now; forward-compatability
|
|
1784
|
-
break;
|
|
1785
|
-
default:
|
|
1786
|
-
throw new Error(`Can’t apply message of unknown type: ${type}`);
|
|
1742
|
+
setConfiguration(configuration = {}) {
|
|
1743
|
+
this.configuration = { ...this.configuration, ...configuration };
|
|
1744
|
+
}
|
|
1745
|
+
async connect() {
|
|
1746
|
+
if (this.status === exports.WebSocketStatus.Connected) {
|
|
1747
|
+
return;
|
|
1787
1748
|
}
|
|
1788
|
-
//
|
|
1789
|
-
if (
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1749
|
+
// Always cancel any previously initiated connection retryer instances
|
|
1750
|
+
if (this.cancelWebsocketRetry) {
|
|
1751
|
+
this.cancelWebsocketRetry();
|
|
1752
|
+
this.cancelWebsocketRetry = undefined;
|
|
1753
|
+
}
|
|
1754
|
+
this.receivedOnOpenPayload = undefined;
|
|
1755
|
+
this.receivedOnStatusPayload = undefined;
|
|
1756
|
+
this.shouldConnect = true;
|
|
1757
|
+
const abortableRetry = () => {
|
|
1758
|
+
let cancelAttempt = false;
|
|
1759
|
+
const retryPromise = attempt.retry(this.createWebSocketConnection.bind(this), {
|
|
1760
|
+
delay: this.configuration.delay,
|
|
1761
|
+
initialDelay: this.configuration.initialDelay,
|
|
1762
|
+
factor: this.configuration.factor,
|
|
1763
|
+
maxAttempts: this.configuration.maxAttempts,
|
|
1764
|
+
minDelay: this.configuration.minDelay,
|
|
1765
|
+
maxDelay: this.configuration.maxDelay,
|
|
1766
|
+
jitter: this.configuration.jitter,
|
|
1767
|
+
timeout: this.configuration.timeout,
|
|
1768
|
+
beforeAttempt: context => {
|
|
1769
|
+
if (!this.shouldConnect || cancelAttempt) {
|
|
1770
|
+
context.abort();
|
|
1771
|
+
}
|
|
1772
|
+
},
|
|
1773
|
+
}).catch((error) => {
|
|
1774
|
+
// If we aborted the connection attempt then don’t throw an error
|
|
1775
|
+
// ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
|
|
1776
|
+
if (error && error.code !== 'ATTEMPT_ABORTED') {
|
|
1777
|
+
throw error;
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
return {
|
|
1781
|
+
retryPromise,
|
|
1782
|
+
cancelFunc: () => {
|
|
1783
|
+
cancelAttempt = true;
|
|
1784
|
+
},
|
|
1785
|
+
};
|
|
1786
|
+
};
|
|
1787
|
+
const { retryPromise, cancelFunc } = abortableRetry();
|
|
1788
|
+
this.cancelWebsocketRetry = cancelFunc;
|
|
1789
|
+
return retryPromise;
|
|
1790
|
+
}
|
|
1791
|
+
createWebSocketConnection() {
|
|
1792
|
+
return new Promise((resolve, reject) => {
|
|
1793
|
+
if (this.webSocket) {
|
|
1794
|
+
this.messageQueue = [];
|
|
1795
|
+
this.webSocket.close();
|
|
1796
|
+
this.webSocket = null;
|
|
1799
1797
|
}
|
|
1798
|
+
// Init the WebSocket connection
|
|
1799
|
+
const ws = new this.configuration.WebSocketPolyfill(this.url);
|
|
1800
|
+
ws.binaryType = 'arraybuffer';
|
|
1801
|
+
ws.onmessage = (payload) => this.emit('message', payload);
|
|
1802
|
+
ws.onclose = (payload) => this.emit('close', { event: payload });
|
|
1803
|
+
ws.onopen = (payload) => this.emit('open', payload);
|
|
1804
|
+
ws.onerror = (err) => {
|
|
1805
|
+
reject(err);
|
|
1806
|
+
};
|
|
1807
|
+
this.webSocket = ws;
|
|
1808
|
+
// Reset the status
|
|
1809
|
+
this.status = exports.WebSocketStatus.Connecting;
|
|
1810
|
+
this.emit('status', { status: exports.WebSocketStatus.Connecting });
|
|
1811
|
+
// Store resolve/reject for later use
|
|
1812
|
+
this.connectionAttempt = {
|
|
1813
|
+
resolve,
|
|
1814
|
+
reject,
|
|
1815
|
+
};
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
onMessage(event) {
|
|
1819
|
+
this.resolveConnectionAttempt();
|
|
1820
|
+
this.lastMessageReceived = getUnixTime();
|
|
1821
|
+
}
|
|
1822
|
+
resolveConnectionAttempt() {
|
|
1823
|
+
if (this.connectionAttempt) {
|
|
1824
|
+
this.connectionAttempt.resolve();
|
|
1825
|
+
this.connectionAttempt = null;
|
|
1826
|
+
this.status = exports.WebSocketStatus.Connected;
|
|
1827
|
+
this.emit('status', { status: exports.WebSocketStatus.Connected });
|
|
1828
|
+
this.emit('connect');
|
|
1829
|
+
this.messageQueue.forEach(message => this.send(message));
|
|
1830
|
+
this.messageQueue = [];
|
|
1800
1831
|
}
|
|
1801
1832
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1833
|
+
stopConnectionAttempt() {
|
|
1834
|
+
this.connectionAttempt = null;
|
|
1835
|
+
}
|
|
1836
|
+
rejectConnectionAttempt() {
|
|
1837
|
+
var _a;
|
|
1838
|
+
(_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
|
|
1839
|
+
this.connectionAttempt = null;
|
|
1840
|
+
}
|
|
1841
|
+
checkConnection() {
|
|
1842
|
+
var _a;
|
|
1843
|
+
// Don’t check the connection when it’s not even established
|
|
1844
|
+
if (this.status !== exports.WebSocketStatus.Connected) {
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
// Don’t close then connection while waiting for the first message
|
|
1848
|
+
if (!this.lastMessageReceived) {
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
// Don’t close the connection when a message was received recently
|
|
1852
|
+
if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
// No message received in a long time, not even your own
|
|
1856
|
+
// Awareness updates, which are updated every 15 seconds.
|
|
1857
|
+
(_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
|
|
1858
|
+
this.messageQueue = [];
|
|
1859
|
+
}
|
|
1860
|
+
registerEventListeners() {
|
|
1861
|
+
if (typeof window === 'undefined') {
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
window.addEventListener('online', this.boundConnect);
|
|
1865
|
+
}
|
|
1866
|
+
// Ensure that the URL always ends with /
|
|
1867
|
+
get serverUrl() {
|
|
1868
|
+
while (this.configuration.url[this.configuration.url.length - 1] === '/') {
|
|
1869
|
+
return this.configuration.url.slice(0, this.configuration.url.length - 1);
|
|
1870
|
+
}
|
|
1871
|
+
return this.configuration.url;
|
|
1872
|
+
}
|
|
1873
|
+
get url() {
|
|
1874
|
+
const encodedParams = encodeQueryParams(this.configuration.parameters);
|
|
1875
|
+
return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
|
|
1876
|
+
}
|
|
1877
|
+
disconnect() {
|
|
1878
|
+
this.shouldConnect = false;
|
|
1879
|
+
if (this.webSocket === null) {
|
|
1880
|
+
return;
|
|
1810
1881
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1882
|
+
try {
|
|
1883
|
+
this.webSocket.close();
|
|
1884
|
+
this.messageQueue = [];
|
|
1885
|
+
}
|
|
1886
|
+
catch {
|
|
1887
|
+
//
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
send(message) {
|
|
1891
|
+
var _a;
|
|
1892
|
+
if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === common.WsReadyStates.Open) {
|
|
1893
|
+
this.webSocket.send(message);
|
|
1894
|
+
}
|
|
1895
|
+
else {
|
|
1896
|
+
this.messageQueue.push(message);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
onClose({ event }) {
|
|
1900
|
+
this.webSocket = null;
|
|
1901
|
+
if (this.status === exports.WebSocketStatus.Connected) {
|
|
1902
|
+
this.status = exports.WebSocketStatus.Disconnected;
|
|
1903
|
+
this.emit('status', { status: exports.WebSocketStatus.Disconnected });
|
|
1904
|
+
this.emit('disconnect', { event });
|
|
1905
|
+
}
|
|
1906
|
+
if (event.code === common.Unauthorized.code) {
|
|
1907
|
+
if (event.reason === common.Unauthorized.reason) {
|
|
1908
|
+
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.');
|
|
1909
|
+
}
|
|
1910
|
+
else {
|
|
1911
|
+
console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
|
|
1814
1912
|
}
|
|
1913
|
+
this.shouldConnect = false;
|
|
1914
|
+
}
|
|
1915
|
+
if (event.code === common.Forbidden.code) {
|
|
1916
|
+
if (!this.configuration.quiet) {
|
|
1917
|
+
console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
|
|
1918
|
+
return; // TODO REMOVE ME
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (event.code === common.MessageTooBig.code) {
|
|
1922
|
+
console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
|
|
1923
|
+
this.shouldConnect = false;
|
|
1924
|
+
}
|
|
1925
|
+
if (this.connectionAttempt) {
|
|
1926
|
+
// That connection attempt failed.
|
|
1927
|
+
this.rejectConnectionAttempt();
|
|
1928
|
+
}
|
|
1929
|
+
else if (this.shouldConnect) {
|
|
1930
|
+
// The connection was closed by the server. Let’s just try again.
|
|
1931
|
+
this.connect();
|
|
1932
|
+
}
|
|
1933
|
+
// If we’ll reconnect, we’re done for now.
|
|
1934
|
+
if (this.shouldConnect) {
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
// The status is set correctly already.
|
|
1938
|
+
if (this.status === exports.WebSocketStatus.Disconnected) {
|
|
1939
|
+
return;
|
|
1815
1940
|
}
|
|
1941
|
+
// Let’s update the connection status.
|
|
1942
|
+
this.status = exports.WebSocketStatus.Disconnected;
|
|
1943
|
+
this.emit('status', { status: exports.WebSocketStatus.Disconnected });
|
|
1944
|
+
this.emit('disconnect', { event });
|
|
1816
1945
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1946
|
+
destroy() {
|
|
1947
|
+
this.emit('destroy');
|
|
1948
|
+
if (this.intervals.forceSync) {
|
|
1949
|
+
clearInterval(this.intervals.forceSync);
|
|
1950
|
+
}
|
|
1951
|
+
clearInterval(this.intervals.connectionChecker);
|
|
1952
|
+
// If there is still a connection attempt outstanding then we should stop
|
|
1953
|
+
// it before calling disconnect, otherwise it will be rejected in the onClose
|
|
1954
|
+
// handler and trigger a retry
|
|
1955
|
+
this.stopConnectionAttempt();
|
|
1956
|
+
this.disconnect();
|
|
1957
|
+
this.removeAllListeners();
|
|
1958
|
+
if (typeof window === 'undefined') {
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
window.removeEventListener('online', this.boundConnect);
|
|
1829
1962
|
}
|
|
1830
1963
|
}
|
|
1831
1964
|
|
|
1832
|
-
class
|
|
1833
|
-
constructor(
|
|
1834
|
-
this.
|
|
1835
|
-
this.encoder =
|
|
1965
|
+
class IncomingMessage {
|
|
1966
|
+
constructor(data) {
|
|
1967
|
+
this.data = data;
|
|
1968
|
+
this.encoder = createEncoder();
|
|
1969
|
+
this.decoder = createDecoder(new Uint8Array(this.data));
|
|
1836
1970
|
}
|
|
1837
|
-
|
|
1838
|
-
return
|
|
1971
|
+
readVarUint() {
|
|
1972
|
+
return readVarUint(this.decoder);
|
|
1839
1973
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1974
|
+
readVarString() {
|
|
1975
|
+
return readVarString(this.decoder);
|
|
1842
1976
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1977
|
+
readVarUint8Array() {
|
|
1978
|
+
return readVarUint8Array(this.decoder);
|
|
1845
1979
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
class SyncStepOneMessage extends OutgoingMessage {
|
|
1849
|
-
constructor() {
|
|
1850
|
-
super(...arguments);
|
|
1851
|
-
this.type = exports.MessageType.Sync;
|
|
1852
|
-
this.description = 'First sync step';
|
|
1980
|
+
writeVarUint(type) {
|
|
1981
|
+
return writeVarUint(this.encoder, type);
|
|
1853
1982
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
throw new Error('The sync step one message requires document as an argument');
|
|
1857
|
-
}
|
|
1858
|
-
writeVarString(this.encoder, args.documentName);
|
|
1859
|
-
writeVarUint(this.encoder, this.type);
|
|
1860
|
-
writeSyncStep1(this.encoder, args.document);
|
|
1861
|
-
return this.encoder;
|
|
1983
|
+
writeVarString(string) {
|
|
1984
|
+
return writeVarString(this.encoder, string);
|
|
1862
1985
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
class SyncStepTwoMessage extends OutgoingMessage {
|
|
1866
|
-
constructor() {
|
|
1867
|
-
super(...arguments);
|
|
1868
|
-
this.type = exports.MessageType.Sync;
|
|
1869
|
-
this.description = 'Second sync step';
|
|
1986
|
+
writeVarUint8Array(data) {
|
|
1987
|
+
return writeVarUint8Array(this.encoder, data);
|
|
1870
1988
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
throw new Error('The sync step two message requires document as an argument');
|
|
1874
|
-
}
|
|
1875
|
-
writeVarString(this.encoder, args.documentName);
|
|
1876
|
-
writeVarUint(this.encoder, this.type);
|
|
1877
|
-
writeSyncStep2(this.encoder, args.document);
|
|
1878
|
-
return this.encoder;
|
|
1989
|
+
length() {
|
|
1990
|
+
return length(this.encoder);
|
|
1879
1991
|
}
|
|
1880
1992
|
}
|
|
1881
1993
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
this.type = exports.MessageType.QueryAwareness;
|
|
1886
|
-
this.description = 'Queries awareness states';
|
|
1887
|
-
}
|
|
1888
|
-
get(args) {
|
|
1889
|
-
console.log('queryAwareness: writing string docName', args.documentName);
|
|
1890
|
-
console.log(this.encoder.cpos);
|
|
1891
|
-
writeVarString(this.encoder, args.documentName);
|
|
1892
|
-
writeVarUint(this.encoder, this.type);
|
|
1893
|
-
return this.encoder;
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1994
|
+
/**
|
|
1995
|
+
* @module sync-protocol
|
|
1996
|
+
*/
|
|
1896
1997
|
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
this.type = exports.MessageType.Auth;
|
|
1901
|
-
this.description = 'Authentication';
|
|
1902
|
-
}
|
|
1903
|
-
get(args) {
|
|
1904
|
-
if (typeof args.token === 'undefined') {
|
|
1905
|
-
throw new Error('The authentication message requires `token` as an argument.');
|
|
1906
|
-
}
|
|
1907
|
-
writeVarString(this.encoder, args.documentName);
|
|
1908
|
-
writeVarUint(this.encoder, this.type);
|
|
1909
|
-
common.writeAuthentication(this.encoder, args.token);
|
|
1910
|
-
return this.encoder;
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1998
|
+
/**
|
|
1999
|
+
* @typedef {Map<number, number>} StateMap
|
|
2000
|
+
*/
|
|
1913
2001
|
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Core Yjs defines two message types:
|
|
2004
|
+
* • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
|
|
2005
|
+
* • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
|
|
2006
|
+
* received all information from the remote client.
|
|
2007
|
+
*
|
|
2008
|
+
* In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
|
|
2009
|
+
* with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
|
|
2010
|
+
* SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
|
|
2011
|
+
*
|
|
2012
|
+
* In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
|
|
2013
|
+
* When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
|
|
2014
|
+
* with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
|
|
2015
|
+
* client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
|
|
2016
|
+
* easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
|
|
2017
|
+
* Therefore it is necesarry that the client initiates the sync.
|
|
2018
|
+
*
|
|
2019
|
+
* Construction of a message:
|
|
2020
|
+
* [messageType : varUint, message definition..]
|
|
2021
|
+
*
|
|
2022
|
+
* Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
|
|
2023
|
+
*
|
|
2024
|
+
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
|
|
2025
|
+
*/
|
|
1940
2026
|
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
this.type = exports.MessageType.Sync;
|
|
1945
|
-
this.description = 'A document update';
|
|
1946
|
-
}
|
|
1947
|
-
get(args) {
|
|
1948
|
-
writeVarString(this.encoder, args.documentName);
|
|
1949
|
-
writeVarUint(this.encoder, this.type);
|
|
1950
|
-
writeUpdate(this.encoder, args.update);
|
|
1951
|
-
return this.encoder;
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
2027
|
+
const messageYjsSyncStep1 = 0;
|
|
2028
|
+
const messageYjsSyncStep2 = 1;
|
|
2029
|
+
const messageYjsUpdate = 2;
|
|
1954
2030
|
|
|
1955
2031
|
/**
|
|
1956
|
-
*
|
|
2032
|
+
* Create a sync step 1 message based on the state of the current shared document.
|
|
1957
2033
|
*
|
|
1958
|
-
* @
|
|
2034
|
+
* @param {encoding.Encoder} encoder
|
|
2035
|
+
* @param {Y.Doc} doc
|
|
2036
|
+
*/
|
|
2037
|
+
const writeSyncStep1 = (encoder, doc) => {
|
|
2038
|
+
writeVarUint(encoder, messageYjsSyncStep1);
|
|
2039
|
+
const sv = Y__namespace.encodeStateVector(doc);
|
|
2040
|
+
writeVarUint8Array(encoder, sv);
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* @param {encoding.Encoder} encoder
|
|
2045
|
+
* @param {Y.Doc} doc
|
|
2046
|
+
* @param {Uint8Array} [encodedStateVector]
|
|
2047
|
+
*/
|
|
2048
|
+
const writeSyncStep2 = (encoder, doc, encodedStateVector) => {
|
|
2049
|
+
writeVarUint(encoder, messageYjsSyncStep2);
|
|
2050
|
+
writeVarUint8Array(encoder, Y__namespace.encodeStateAsUpdate(doc, encodedStateVector));
|
|
2051
|
+
};
|
|
2052
|
+
|
|
2053
|
+
/**
|
|
2054
|
+
* Read SyncStep1 message and reply with SyncStep2.
|
|
2055
|
+
*
|
|
2056
|
+
* @param {decoding.Decoder} decoder The reply to the received message
|
|
2057
|
+
* @param {encoding.Encoder} encoder The received message
|
|
2058
|
+
* @param {Y.Doc} doc
|
|
2059
|
+
*/
|
|
2060
|
+
const readSyncStep1 = (decoder, encoder, doc) =>
|
|
2061
|
+
writeSyncStep2(encoder, doc, readVarUint8Array(decoder));
|
|
2062
|
+
|
|
2063
|
+
/**
|
|
2064
|
+
* Read and apply Structs and then DeleteStore to a y instance.
|
|
2065
|
+
*
|
|
2066
|
+
* @param {decoding.Decoder} decoder
|
|
2067
|
+
* @param {Y.Doc} doc
|
|
2068
|
+
* @param {any} transactionOrigin
|
|
2069
|
+
*/
|
|
2070
|
+
const readSyncStep2 = (decoder, doc, transactionOrigin) => {
|
|
2071
|
+
try {
|
|
2072
|
+
Y__namespace.applyUpdate(doc, readVarUint8Array(decoder), transactionOrigin);
|
|
2073
|
+
} catch (error) {
|
|
2074
|
+
// This catches errors that are thrown by event handlers
|
|
2075
|
+
console.error('Caught error while handling a Yjs update', error);
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
|
|
2079
|
+
/**
|
|
2080
|
+
* @param {encoding.Encoder} encoder
|
|
2081
|
+
* @param {Uint8Array} update
|
|
2082
|
+
*/
|
|
2083
|
+
const writeUpdate = (encoder, update) => {
|
|
2084
|
+
writeVarUint(encoder, messageYjsUpdate);
|
|
2085
|
+
writeVarUint8Array(encoder, update);
|
|
2086
|
+
};
|
|
2087
|
+
|
|
2088
|
+
/**
|
|
2089
|
+
* Read and apply Structs and then DeleteStore to a y instance.
|
|
2090
|
+
*
|
|
2091
|
+
* @param {decoding.Decoder} decoder
|
|
2092
|
+
* @param {Y.Doc} doc
|
|
2093
|
+
* @param {any} transactionOrigin
|
|
1959
2094
|
*/
|
|
2095
|
+
const readUpdate = readSyncStep2;
|
|
1960
2096
|
|
|
1961
2097
|
/**
|
|
1962
|
-
* @param {
|
|
1963
|
-
* @
|
|
2098
|
+
* @param {decoding.Decoder} decoder A message received from another client
|
|
2099
|
+
* @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
|
|
2100
|
+
* @param {Y.Doc} doc
|
|
2101
|
+
* @param {any} transactionOrigin
|
|
1964
2102
|
*/
|
|
1965
|
-
const
|
|
1966
|
-
|
|
2103
|
+
const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => {
|
|
2104
|
+
const messageType = readVarUint(decoder);
|
|
2105
|
+
switch (messageType) {
|
|
2106
|
+
case messageYjsSyncStep1:
|
|
2107
|
+
readSyncStep1(decoder, encoder, doc);
|
|
2108
|
+
break
|
|
2109
|
+
case messageYjsSyncStep2:
|
|
2110
|
+
readSyncStep2(decoder, doc, transactionOrigin);
|
|
2111
|
+
break
|
|
2112
|
+
case messageYjsUpdate:
|
|
2113
|
+
readUpdate(decoder, doc, transactionOrigin);
|
|
2114
|
+
break
|
|
2115
|
+
default:
|
|
2116
|
+
throw new Error('Unknown message type')
|
|
2117
|
+
}
|
|
2118
|
+
return messageType
|
|
2119
|
+
};
|
|
1967
2120
|
|
|
1968
|
-
class
|
|
1969
|
-
constructor(
|
|
1970
|
-
|
|
1971
|
-
this.configuration = {
|
|
1972
|
-
url: '',
|
|
1973
|
-
// @ts-ignore
|
|
1974
|
-
document: undefined,
|
|
1975
|
-
// @ts-ignore
|
|
1976
|
-
awareness: undefined,
|
|
1977
|
-
WebSocketPolyfill: undefined,
|
|
1978
|
-
parameters: {},
|
|
1979
|
-
connect: true,
|
|
1980
|
-
broadcast: true,
|
|
1981
|
-
forceSyncInterval: false,
|
|
1982
|
-
// TODO: this should depend on awareness.outdatedTime
|
|
1983
|
-
messageReconnectTimeout: 30000,
|
|
1984
|
-
// 1 second
|
|
1985
|
-
delay: 1000,
|
|
1986
|
-
// instant
|
|
1987
|
-
initialDelay: 0,
|
|
1988
|
-
// double the delay each time
|
|
1989
|
-
factor: 2,
|
|
1990
|
-
// unlimited retries
|
|
1991
|
-
maxAttempts: 0,
|
|
1992
|
-
// wait at least 1 second
|
|
1993
|
-
minDelay: 1000,
|
|
1994
|
-
// at least every 30 seconds
|
|
1995
|
-
maxDelay: 30000,
|
|
1996
|
-
// randomize
|
|
1997
|
-
jitter: true,
|
|
1998
|
-
// retry forever
|
|
1999
|
-
timeout: 0,
|
|
2000
|
-
onOpen: () => null,
|
|
2001
|
-
onConnect: () => null,
|
|
2002
|
-
onMessage: () => null,
|
|
2003
|
-
onOutgoingMessage: () => null,
|
|
2004
|
-
onStatus: () => null,
|
|
2005
|
-
onDisconnect: () => null,
|
|
2006
|
-
onClose: () => null,
|
|
2007
|
-
onDestroy: () => null,
|
|
2008
|
-
onAwarenessUpdate: () => null,
|
|
2009
|
-
onAwarenessChange: () => null,
|
|
2010
|
-
quiet: false,
|
|
2011
|
-
};
|
|
2012
|
-
this.subscribedToBroadcastChannel = false;
|
|
2013
|
-
this.webSocket = null;
|
|
2014
|
-
this.shouldConnect = true;
|
|
2015
|
-
this.status = exports.WebSocketStatus.Disconnected;
|
|
2016
|
-
this.lastMessageReceived = 0;
|
|
2017
|
-
this.mux = createMutex();
|
|
2018
|
-
this.intervals = {
|
|
2019
|
-
forceSync: null,
|
|
2020
|
-
connectionChecker: null,
|
|
2021
|
-
};
|
|
2022
|
-
this.connectionAttempt = null;
|
|
2023
|
-
this.receivedOnOpenPayload = undefined;
|
|
2024
|
-
this.receivedOnStatusPayload = undefined;
|
|
2025
|
-
this.boundConnect = this.connect.bind(this);
|
|
2026
|
-
this.setConfiguration(configuration);
|
|
2027
|
-
this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
|
|
2028
|
-
this.on('open', this.configuration.onOpen);
|
|
2029
|
-
this.on('open', this.onOpen.bind(this));
|
|
2030
|
-
this.on('connect', this.configuration.onConnect);
|
|
2031
|
-
this.on('message', this.configuration.onMessage);
|
|
2032
|
-
this.on('outgoingMessage', this.configuration.onOutgoingMessage);
|
|
2033
|
-
this.on('status', this.configuration.onStatus);
|
|
2034
|
-
this.on('status', this.onStatus.bind(this));
|
|
2035
|
-
this.on('disconnect', this.configuration.onDisconnect);
|
|
2036
|
-
this.on('close', this.configuration.onClose);
|
|
2037
|
-
this.on('destroy', this.configuration.onDestroy);
|
|
2038
|
-
this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
|
|
2039
|
-
this.on('awarenessChange', this.configuration.onAwarenessChange);
|
|
2040
|
-
this.on('close', this.onClose.bind(this));
|
|
2041
|
-
this.on('message', this.onMessage.bind(this));
|
|
2042
|
-
this.registerEventListeners();
|
|
2043
|
-
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
|
|
2044
|
-
if (typeof configuration.connect !== 'undefined') {
|
|
2045
|
-
this.shouldConnect = configuration.connect;
|
|
2046
|
-
}
|
|
2047
|
-
if (!this.shouldConnect) {
|
|
2048
|
-
return;
|
|
2049
|
-
}
|
|
2050
|
-
this.connect();
|
|
2051
|
-
}
|
|
2052
|
-
async onOpen(event) {
|
|
2053
|
-
this.receivedOnOpenPayload = event;
|
|
2121
|
+
class OutgoingMessage {
|
|
2122
|
+
constructor() {
|
|
2123
|
+
this.encoder = createEncoder();
|
|
2054
2124
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2125
|
+
get(args) {
|
|
2126
|
+
return args.encoder;
|
|
2057
2127
|
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
provider.onOpen(this.receivedOnOpenPayload);
|
|
2061
|
-
}
|
|
2062
|
-
if (this.receivedOnStatusPayload) {
|
|
2063
|
-
provider.onStatus(this.receivedOnStatusPayload);
|
|
2064
|
-
}
|
|
2128
|
+
toUint8Array() {
|
|
2129
|
+
return toUint8Array(this.encoder);
|
|
2065
2130
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
class MessageReceiver {
|
|
2134
|
+
constructor(message) {
|
|
2135
|
+
this.broadcasted = false;
|
|
2136
|
+
this.message = message;
|
|
2068
2137
|
}
|
|
2069
|
-
|
|
2070
|
-
this.
|
|
2138
|
+
setBroadcasted(value) {
|
|
2139
|
+
this.broadcasted = value;
|
|
2140
|
+
return this;
|
|
2071
2141
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2142
|
+
apply(provider, emitSynced) {
|
|
2143
|
+
const { message } = this;
|
|
2144
|
+
const type = message.readVarUint();
|
|
2145
|
+
const emptyMessageLength = message.length();
|
|
2146
|
+
switch (type) {
|
|
2147
|
+
case exports.MessageType.Sync:
|
|
2148
|
+
this.applySyncMessage(provider, emitSynced);
|
|
2149
|
+
break;
|
|
2150
|
+
case exports.MessageType.Awareness:
|
|
2151
|
+
this.applyAwarenessMessage(provider);
|
|
2152
|
+
break;
|
|
2153
|
+
case exports.MessageType.Auth:
|
|
2154
|
+
this.applyAuthMessage(provider);
|
|
2155
|
+
break;
|
|
2156
|
+
case exports.MessageType.QueryAwareness:
|
|
2157
|
+
this.applyQueryAwarenessMessage(provider);
|
|
2158
|
+
break;
|
|
2159
|
+
case exports.MessageType.Stateless:
|
|
2160
|
+
provider.receiveStateless(readVarString(message.decoder));
|
|
2161
|
+
break;
|
|
2162
|
+
case exports.MessageType.SyncStatus:
|
|
2163
|
+
this.applySyncStatusMessage(provider, readVarInt(message.decoder) === 1);
|
|
2164
|
+
break;
|
|
2165
|
+
default:
|
|
2166
|
+
throw new Error(`Can’t apply message of unknown type: ${type}`);
|
|
2080
2167
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
factor: this.configuration.factor,
|
|
2088
|
-
maxAttempts: this.configuration.maxAttempts,
|
|
2089
|
-
minDelay: this.configuration.minDelay,
|
|
2090
|
-
maxDelay: this.configuration.maxDelay,
|
|
2091
|
-
jitter: this.configuration.jitter,
|
|
2092
|
-
timeout: this.configuration.timeout,
|
|
2093
|
-
beforeAttempt: context => {
|
|
2094
|
-
if (!this.shouldConnect || cancelAttempt) {
|
|
2095
|
-
context.abort();
|
|
2096
|
-
}
|
|
2097
|
-
},
|
|
2098
|
-
}).catch((error) => {
|
|
2099
|
-
// If we aborted the connection attempt then don’t throw an error
|
|
2100
|
-
// ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
|
|
2101
|
-
if (error && error.code !== 'ATTEMPT_ABORTED') {
|
|
2102
|
-
throw error;
|
|
2103
|
-
}
|
|
2104
|
-
});
|
|
2105
|
-
return {
|
|
2106
|
-
retryPromise,
|
|
2107
|
-
cancelFunc: () => {
|
|
2108
|
-
cancelAttempt = true;
|
|
2109
|
-
},
|
|
2110
|
-
};
|
|
2111
|
-
};
|
|
2112
|
-
const { retryPromise, cancelFunc } = abortableRetry();
|
|
2113
|
-
this.cancelWebsocketRetry = cancelFunc;
|
|
2114
|
-
return retryPromise;
|
|
2115
|
-
}
|
|
2116
|
-
createWebSocketConnection() {
|
|
2117
|
-
return new Promise((resolve, reject) => {
|
|
2118
|
-
if (this.webSocket) {
|
|
2119
|
-
this.webSocket.close();
|
|
2120
|
-
this.webSocket = null;
|
|
2168
|
+
// Reply
|
|
2169
|
+
if (message.length() > emptyMessageLength + 1) { // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage)
|
|
2170
|
+
if (this.broadcasted) {
|
|
2171
|
+
// TODO: Some weird TypeScript error
|
|
2172
|
+
// @ts-ignore
|
|
2173
|
+
provider.broadcast(OutgoingMessage, { encoder: message.encoder });
|
|
2121
2174
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
ws.onerror = (err) => {
|
|
2129
|
-
reject(err);
|
|
2130
|
-
};
|
|
2131
|
-
this.webSocket = ws;
|
|
2132
|
-
// Reset the status
|
|
2133
|
-
this.status = exports.WebSocketStatus.Connecting;
|
|
2134
|
-
this.emit('status', { status: exports.WebSocketStatus.Connecting });
|
|
2135
|
-
// Store resolve/reject for later use
|
|
2136
|
-
this.connectionAttempt = {
|
|
2137
|
-
resolve,
|
|
2138
|
-
reject,
|
|
2139
|
-
};
|
|
2140
|
-
});
|
|
2175
|
+
else {
|
|
2176
|
+
// TODO: Some weird TypeScript error
|
|
2177
|
+
// @ts-ignore
|
|
2178
|
+
provider.send(OutgoingMessage, { encoder: message.encoder });
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2141
2181
|
}
|
|
2142
|
-
|
|
2143
|
-
this
|
|
2144
|
-
|
|
2182
|
+
applySyncMessage(provider, emitSynced) {
|
|
2183
|
+
const { message } = this;
|
|
2184
|
+
message.writeVarUint(exports.MessageType.Sync);
|
|
2185
|
+
// Apply update
|
|
2186
|
+
const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
|
|
2187
|
+
// Synced once we receive Step2
|
|
2188
|
+
if (emitSynced && syncMessageType === messageYjsSyncStep2) {
|
|
2189
|
+
provider.synced = true;
|
|
2190
|
+
}
|
|
2145
2191
|
}
|
|
2146
|
-
|
|
2147
|
-
if (
|
|
2148
|
-
|
|
2149
|
-
this.connectionAttempt = null;
|
|
2150
|
-
this.status = exports.WebSocketStatus.Connected;
|
|
2151
|
-
this.emit('status', { status: exports.WebSocketStatus.Connected });
|
|
2152
|
-
this.emit('connect');
|
|
2192
|
+
applySyncStatusMessage(provider, applied) {
|
|
2193
|
+
if (applied) {
|
|
2194
|
+
provider.decrementUnsyncedChanges();
|
|
2153
2195
|
}
|
|
2154
2196
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
2197
|
+
applyAwarenessMessage(provider) {
|
|
2198
|
+
const { message } = this;
|
|
2199
|
+
applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
|
|
2157
2200
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
(
|
|
2161
|
-
this.connectionAttempt = null;
|
|
2201
|
+
applyAuthMessage(provider) {
|
|
2202
|
+
const { message } = this;
|
|
2203
|
+
common.readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
|
|
2162
2204
|
}
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
return;
|
|
2168
|
-
}
|
|
2169
|
-
// Don’t close then connection while waiting for the first message
|
|
2170
|
-
if (!this.lastMessageReceived) {
|
|
2171
|
-
return;
|
|
2172
|
-
}
|
|
2173
|
-
// Don’t close the connection when a message was received recently
|
|
2174
|
-
if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
|
|
2175
|
-
return;
|
|
2176
|
-
}
|
|
2177
|
-
// No message received in a long time, not even your own
|
|
2178
|
-
// Awareness updates, which are updated every 15 seconds.
|
|
2179
|
-
(_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
|
|
2205
|
+
applyQueryAwarenessMessage(provider) {
|
|
2206
|
+
const { message } = this;
|
|
2207
|
+
message.writeVarUint(exports.MessageType.Awareness);
|
|
2208
|
+
message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
|
|
2180
2209
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
class MessageSender {
|
|
2213
|
+
constructor(Message, args = {}) {
|
|
2214
|
+
this.message = new Message();
|
|
2215
|
+
this.encoder = this.message.get(args);
|
|
2186
2216
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
while (this.configuration.url[this.configuration.url.length - 1] === '/') {
|
|
2190
|
-
return this.configuration.url.slice(0, this.configuration.url.length - 1);
|
|
2191
|
-
}
|
|
2192
|
-
return this.configuration.url;
|
|
2217
|
+
create() {
|
|
2218
|
+
return toUint8Array(this.encoder);
|
|
2193
2219
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
|
|
2220
|
+
send(webSocket) {
|
|
2221
|
+
webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
|
|
2197
2222
|
}
|
|
2198
|
-
|
|
2199
|
-
this.
|
|
2200
|
-
if (this.webSocket === null) {
|
|
2201
|
-
return;
|
|
2202
|
-
}
|
|
2203
|
-
try {
|
|
2204
|
-
this.webSocket.close();
|
|
2205
|
-
}
|
|
2206
|
-
catch {
|
|
2207
|
-
//
|
|
2208
|
-
}
|
|
2223
|
+
broadcast(channel) {
|
|
2224
|
+
publish(channel, this.create());
|
|
2209
2225
|
}
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
class AuthenticationMessage extends OutgoingMessage {
|
|
2229
|
+
constructor() {
|
|
2230
|
+
super(...arguments);
|
|
2231
|
+
this.type = exports.MessageType.Auth;
|
|
2232
|
+
this.description = 'Authentication';
|
|
2215
2233
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
this.status = exports.WebSocketStatus.Disconnected;
|
|
2220
|
-
this.emit('status', { status: exports.WebSocketStatus.Disconnected });
|
|
2221
|
-
this.emit('disconnect', { event });
|
|
2222
|
-
}
|
|
2223
|
-
if (event.code === common.Unauthorized.code) {
|
|
2224
|
-
if (event.reason === common.Unauthorized.reason) {
|
|
2225
|
-
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.');
|
|
2226
|
-
}
|
|
2227
|
-
else {
|
|
2228
|
-
console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
|
|
2229
|
-
}
|
|
2230
|
-
this.shouldConnect = false;
|
|
2231
|
-
}
|
|
2232
|
-
if (event.code === common.Forbidden.code) {
|
|
2233
|
-
if (!this.configuration.quiet) {
|
|
2234
|
-
console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
|
|
2235
|
-
return; // TODO REMOVE ME
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
if (event.code === common.MessageTooBig.code) {
|
|
2239
|
-
console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
|
|
2240
|
-
this.shouldConnect = false;
|
|
2234
|
+
get(args) {
|
|
2235
|
+
if (typeof args.token === 'undefined') {
|
|
2236
|
+
throw new Error('The authentication message requires `token` as an argument.');
|
|
2241
2237
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2238
|
+
writeVarString(this.encoder, args.documentName);
|
|
2239
|
+
writeVarUint(this.encoder, this.type);
|
|
2240
|
+
common.writeAuthentication(this.encoder, args.token);
|
|
2241
|
+
return this.encoder;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
class AwarenessMessage extends OutgoingMessage {
|
|
2246
|
+
constructor() {
|
|
2247
|
+
super(...arguments);
|
|
2248
|
+
this.type = exports.MessageType.Awareness;
|
|
2249
|
+
this.description = 'Awareness states update';
|
|
2250
|
+
}
|
|
2251
|
+
get(args) {
|
|
2252
|
+
if (typeof args.awareness === 'undefined') {
|
|
2253
|
+
throw new Error('The awareness message requires awareness as an argument');
|
|
2245
2254
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
this.connect();
|
|
2255
|
+
if (typeof args.clients === 'undefined') {
|
|
2256
|
+
throw new Error('The awareness message requires clients as an argument');
|
|
2249
2257
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2258
|
+
writeVarString(this.encoder, args.documentName);
|
|
2259
|
+
writeVarUint(this.encoder, this.type);
|
|
2260
|
+
let awarenessUpdate;
|
|
2261
|
+
if (args.states === undefined) {
|
|
2262
|
+
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients);
|
|
2253
2263
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
return;
|
|
2264
|
+
else {
|
|
2265
|
+
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
|
|
2257
2266
|
}
|
|
2258
|
-
|
|
2259
|
-
this.
|
|
2260
|
-
this.emit('status', { status: exports.WebSocketStatus.Disconnected });
|
|
2261
|
-
this.emit('disconnect', { event });
|
|
2267
|
+
writeVarUint8Array(this.encoder, awarenessUpdate);
|
|
2268
|
+
return this.encoder;
|
|
2262
2269
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
this.
|
|
2273
|
-
this.
|
|
2274
|
-
this.
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
class CloseMessage extends OutgoingMessage {
|
|
2273
|
+
constructor() {
|
|
2274
|
+
super(...arguments);
|
|
2275
|
+
this.type = exports.MessageType.CLOSE;
|
|
2276
|
+
this.description = 'Ask the server to close the connection';
|
|
2277
|
+
}
|
|
2278
|
+
get(args) {
|
|
2279
|
+
writeVarString(this.encoder, args.documentName);
|
|
2280
|
+
writeVarUint(this.encoder, this.type);
|
|
2281
|
+
return this.encoder;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
class QueryAwarenessMessage extends OutgoingMessage {
|
|
2286
|
+
constructor() {
|
|
2287
|
+
super(...arguments);
|
|
2288
|
+
this.type = exports.MessageType.QueryAwareness;
|
|
2289
|
+
this.description = 'Queries awareness states';
|
|
2290
|
+
}
|
|
2291
|
+
get(args) {
|
|
2292
|
+
console.log('queryAwareness: writing string docName', args.documentName);
|
|
2293
|
+
console.log(this.encoder.cpos);
|
|
2294
|
+
writeVarString(this.encoder, args.documentName);
|
|
2295
|
+
writeVarUint(this.encoder, this.type);
|
|
2296
|
+
return this.encoder;
|
|
2279
2297
|
}
|
|
2280
2298
|
}
|
|
2281
2299
|
|
|
@@ -2294,15 +2312,50 @@ class StatelessMessage extends OutgoingMessage {
|
|
|
2294
2312
|
}
|
|
2295
2313
|
}
|
|
2296
2314
|
|
|
2297
|
-
class
|
|
2315
|
+
class SyncStepOneMessage extends OutgoingMessage {
|
|
2298
2316
|
constructor() {
|
|
2299
2317
|
super(...arguments);
|
|
2300
|
-
this.type = exports.MessageType.
|
|
2301
|
-
this.description = '
|
|
2318
|
+
this.type = exports.MessageType.Sync;
|
|
2319
|
+
this.description = 'First sync step';
|
|
2320
|
+
}
|
|
2321
|
+
get(args) {
|
|
2322
|
+
if (typeof args.document === 'undefined') {
|
|
2323
|
+
throw new Error('The sync step one message requires document as an argument');
|
|
2324
|
+
}
|
|
2325
|
+
writeVarString(this.encoder, args.documentName);
|
|
2326
|
+
writeVarUint(this.encoder, this.type);
|
|
2327
|
+
writeSyncStep1(this.encoder, args.document);
|
|
2328
|
+
return this.encoder;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
class SyncStepTwoMessage extends OutgoingMessage {
|
|
2333
|
+
constructor() {
|
|
2334
|
+
super(...arguments);
|
|
2335
|
+
this.type = exports.MessageType.Sync;
|
|
2336
|
+
this.description = 'Second sync step';
|
|
2337
|
+
}
|
|
2338
|
+
get(args) {
|
|
2339
|
+
if (typeof args.document === 'undefined') {
|
|
2340
|
+
throw new Error('The sync step two message requires document as an argument');
|
|
2341
|
+
}
|
|
2342
|
+
writeVarString(this.encoder, args.documentName);
|
|
2343
|
+
writeVarUint(this.encoder, this.type);
|
|
2344
|
+
writeSyncStep2(this.encoder, args.document);
|
|
2345
|
+
return this.encoder;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
class UpdateMessage extends OutgoingMessage {
|
|
2350
|
+
constructor() {
|
|
2351
|
+
super(...arguments);
|
|
2352
|
+
this.type = exports.MessageType.Sync;
|
|
2353
|
+
this.description = 'A document update';
|
|
2302
2354
|
}
|
|
2303
2355
|
get(args) {
|
|
2304
2356
|
writeVarString(this.encoder, args.documentName);
|
|
2305
2357
|
writeVarUint(this.encoder, this.type);
|
|
2358
|
+
writeUpdate(this.encoder, args.update);
|
|
2306
2359
|
return this.encoder;
|
|
2307
2360
|
}
|
|
2308
2361
|
}
|
|
@@ -2335,6 +2388,8 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2335
2388
|
onAwarenessChange: () => null,
|
|
2336
2389
|
onStateless: () => null,
|
|
2337
2390
|
quiet: false,
|
|
2391
|
+
connect: true,
|
|
2392
|
+
preserveConnection: true,
|
|
2338
2393
|
};
|
|
2339
2394
|
this.subscribedToBroadcastChannel = false;
|
|
2340
2395
|
this.isSynced = false;
|
|
@@ -2348,7 +2403,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2348
2403
|
};
|
|
2349
2404
|
this.isConnected = true;
|
|
2350
2405
|
this.boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this);
|
|
2351
|
-
this.
|
|
2406
|
+
this.boundPageUnload = this.pageUnload.bind(this);
|
|
2352
2407
|
this.boundOnOpen = this.onOpen.bind(this);
|
|
2353
2408
|
this.boundOnMessage = this.onMessage.bind(this);
|
|
2354
2409
|
this.boundOnClose = this.onClose.bind(this);
|
|
@@ -2408,6 +2463,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2408
2463
|
const websocketProviderConfig = configuration;
|
|
2409
2464
|
this.configuration.websocketProvider = new HocuspocusProviderWebsocket({
|
|
2410
2465
|
url: websocketProviderConfig.url,
|
|
2466
|
+
connect: websocketProviderConfig.connect,
|
|
2411
2467
|
parameters: websocketProviderConfig.parameters,
|
|
2412
2468
|
});
|
|
2413
2469
|
}
|
|
@@ -2422,21 +2478,28 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2422
2478
|
get hasUnsyncedChanges() {
|
|
2423
2479
|
return this.unsyncedChanges > 0;
|
|
2424
2480
|
}
|
|
2425
|
-
|
|
2426
|
-
this.unsyncedChanges +=
|
|
2481
|
+
incrementUnsyncedChanges() {
|
|
2482
|
+
this.unsyncedChanges += 1;
|
|
2483
|
+
this.emit('unsyncedChanges', this.unsyncedChanges);
|
|
2484
|
+
}
|
|
2485
|
+
decrementUnsyncedChanges() {
|
|
2486
|
+
this.unsyncedChanges -= 1;
|
|
2487
|
+
if (this.unsyncedChanges === 0) {
|
|
2488
|
+
this.synced = true;
|
|
2489
|
+
}
|
|
2427
2490
|
this.emit('unsyncedChanges', this.unsyncedChanges);
|
|
2428
2491
|
}
|
|
2429
2492
|
forceSync() {
|
|
2430
2493
|
this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
|
|
2431
2494
|
}
|
|
2432
|
-
|
|
2495
|
+
pageUnload() {
|
|
2433
2496
|
removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload');
|
|
2434
2497
|
}
|
|
2435
2498
|
registerEventListeners() {
|
|
2436
2499
|
if (typeof window === 'undefined') {
|
|
2437
2500
|
return;
|
|
2438
2501
|
}
|
|
2439
|
-
window.addEventListener('
|
|
2502
|
+
window.addEventListener('unload', this.boundPageUnload);
|
|
2440
2503
|
}
|
|
2441
2504
|
sendStateless(payload) {
|
|
2442
2505
|
this.send(StatelessMessage, { documentName: this.configuration.name, payload });
|
|
@@ -2445,7 +2508,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2445
2508
|
if (origin === this) {
|
|
2446
2509
|
return;
|
|
2447
2510
|
}
|
|
2448
|
-
this.
|
|
2511
|
+
this.incrementUnsyncedChanges();
|
|
2449
2512
|
this.send(UpdateMessage, { update, documentName: this.configuration.name }, true);
|
|
2450
2513
|
}
|
|
2451
2514
|
awarenessUpdateHandler({ added, updated, removed }, origin) {
|
|
@@ -2456,6 +2519,12 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2456
2519
|
documentName: this.configuration.name,
|
|
2457
2520
|
}, true);
|
|
2458
2521
|
}
|
|
2522
|
+
/**
|
|
2523
|
+
* Indicates whether a first handshake with the server has been established
|
|
2524
|
+
*
|
|
2525
|
+
* Note: this does not mean all updates from the client have been persisted to the backend. For this,
|
|
2526
|
+
* use `hasUnsyncedChanges`.
|
|
2527
|
+
*/
|
|
2459
2528
|
get synced() {
|
|
2460
2529
|
return this.isSynced;
|
|
2461
2530
|
}
|
|
@@ -2463,9 +2532,6 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2463
2532
|
if (this.isSynced === state) {
|
|
2464
2533
|
return;
|
|
2465
2534
|
}
|
|
2466
|
-
if (state && this.unsyncedChanges > 0) {
|
|
2467
|
-
this.updateUnsyncedChanges(-1 * this.unsyncedChanges);
|
|
2468
|
-
}
|
|
2469
2535
|
this.isSynced = state;
|
|
2470
2536
|
this.emit('synced', { state });
|
|
2471
2537
|
this.emit('sync', { state });
|
|
@@ -2483,6 +2549,9 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2483
2549
|
disconnect() {
|
|
2484
2550
|
this.disconnectBroadcastChannel();
|
|
2485
2551
|
this.configuration.websocketProvider.detach(this);
|
|
2552
|
+
if (!this.configuration.preserveConnection) {
|
|
2553
|
+
this.configuration.websocketProvider.disconnect();
|
|
2554
|
+
}
|
|
2486
2555
|
}
|
|
2487
2556
|
async onOpen(event) {
|
|
2488
2557
|
this.isAuthenticated = false;
|
|
@@ -2503,6 +2572,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2503
2572
|
return this.configuration.token;
|
|
2504
2573
|
}
|
|
2505
2574
|
startSync() {
|
|
2575
|
+
this.incrementUnsyncedChanges();
|
|
2506
2576
|
this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
|
|
2507
2577
|
if (this.awareness.getLocalState() !== null) {
|
|
2508
2578
|
this.send(AwarenessMessage, {
|
|
@@ -2513,8 +2583,9 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2513
2583
|
}
|
|
2514
2584
|
}
|
|
2515
2585
|
send(message, args, broadcast = false) {
|
|
2516
|
-
if (!this.isConnected)
|
|
2586
|
+
if (!this.isConnected) {
|
|
2517
2587
|
return;
|
|
2588
|
+
}
|
|
2518
2589
|
if (broadcast) {
|
|
2519
2590
|
this.mux(() => { this.broadcast(message, args); });
|
|
2520
2591
|
}
|
|
@@ -2530,7 +2601,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2530
2601
|
}
|
|
2531
2602
|
message.writeVarString(documentName);
|
|
2532
2603
|
this.emit('message', { event, message: new IncomingMessage(event.data) });
|
|
2533
|
-
new MessageReceiver(message).apply(this);
|
|
2604
|
+
new MessageReceiver(message).apply(this, true);
|
|
2534
2605
|
}
|
|
2535
2606
|
onClose(event) {
|
|
2536
2607
|
this.isAuthenticated = false;
|
|
@@ -2566,7 +2637,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2566
2637
|
if (typeof window === 'undefined') {
|
|
2567
2638
|
return;
|
|
2568
2639
|
}
|
|
2569
|
-
window.removeEventListener('
|
|
2640
|
+
window.removeEventListener('unload', this.boundPageUnload);
|
|
2570
2641
|
}
|
|
2571
2642
|
permissionDeniedHandler(reason) {
|
|
2572
2643
|
this.emit('authenticationFailed', { reason });
|