@hocuspocus/provider 2.2.3 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hocuspocus-provider.cjs +804 -718
- package/dist/hocuspocus-provider.cjs.map +1 -1
- package/dist/hocuspocus-provider.esm.js +804 -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 +3 -1
- package/dist/packages/provider/src/MessageReceiver.d.ts +2 -1
- package/dist/packages/provider/src/TiptapCollabProvider.d.ts +2 -2
- package/dist/packages/provider/src/types.d.ts +2 -1
- 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 +40 -8
- package/src/MessageReceiver.ts +10 -11
- package/src/types.ts +2 -1
|
@@ -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,711 @@ 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.closeTries = 0;
|
|
1697
|
+
this.setConfiguration(configuration);
|
|
1698
|
+
this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
|
|
1699
|
+
this.on('open', this.configuration.onOpen);
|
|
1700
|
+
this.on('open', this.onOpen.bind(this));
|
|
1701
|
+
this.on('connect', this.configuration.onConnect);
|
|
1702
|
+
this.on('message', this.configuration.onMessage);
|
|
1703
|
+
this.on('outgoingMessage', this.configuration.onOutgoingMessage);
|
|
1704
|
+
this.on('status', this.configuration.onStatus);
|
|
1705
|
+
this.on('status', this.onStatus.bind(this));
|
|
1706
|
+
this.on('disconnect', this.configuration.onDisconnect);
|
|
1707
|
+
this.on('close', this.configuration.onClose);
|
|
1708
|
+
this.on('destroy', this.configuration.onDestroy);
|
|
1709
|
+
this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
|
|
1710
|
+
this.on('awarenessChange', this.configuration.onAwarenessChange);
|
|
1711
|
+
this.on('close', this.onClose.bind(this));
|
|
1712
|
+
this.on('message', this.onMessage.bind(this));
|
|
1713
|
+
this.registerEventListeners();
|
|
1714
|
+
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
|
|
1715
|
+
if (typeof configuration.connect !== 'undefined') {
|
|
1716
|
+
this.shouldConnect = configuration.connect;
|
|
1717
|
+
}
|
|
1718
|
+
if (!this.shouldConnect) {
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
this.connect();
|
|
1744
1722
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1723
|
+
async onOpen(event) {
|
|
1724
|
+
this.receivedOnOpenPayload = event;
|
|
1747
1725
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1726
|
+
async onStatus(data) {
|
|
1727
|
+
this.receivedOnStatusPayload = data;
|
|
1750
1728
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
this.
|
|
1756
|
-
|
|
1729
|
+
attach(provider) {
|
|
1730
|
+
if (this.status === exports.WebSocketStatus.Disconnected && this.shouldConnect) {
|
|
1731
|
+
this.connect();
|
|
1732
|
+
}
|
|
1733
|
+
if (this.receivedOnOpenPayload) {
|
|
1734
|
+
provider.onOpen(this.receivedOnOpenPayload);
|
|
1735
|
+
}
|
|
1736
|
+
if (this.receivedOnStatusPayload) {
|
|
1737
|
+
provider.onStatus(this.receivedOnStatusPayload);
|
|
1738
|
+
}
|
|
1757
1739
|
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
return this;
|
|
1740
|
+
detach(provider) {
|
|
1741
|
+
// tell the server to remove the listener
|
|
1761
1742
|
}
|
|
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}`);
|
|
1743
|
+
setConfiguration(configuration = {}) {
|
|
1744
|
+
this.configuration = { ...this.configuration, ...configuration };
|
|
1745
|
+
}
|
|
1746
|
+
async connect() {
|
|
1747
|
+
if (this.status === exports.WebSocketStatus.Connected) {
|
|
1748
|
+
return;
|
|
1787
1749
|
}
|
|
1788
|
-
//
|
|
1789
|
-
if (
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1750
|
+
// Always cancel any previously initiated connection retryer instances
|
|
1751
|
+
if (this.cancelWebsocketRetry) {
|
|
1752
|
+
this.cancelWebsocketRetry();
|
|
1753
|
+
this.cancelWebsocketRetry = undefined;
|
|
1754
|
+
}
|
|
1755
|
+
this.receivedOnOpenPayload = undefined;
|
|
1756
|
+
this.receivedOnStatusPayload = undefined;
|
|
1757
|
+
this.shouldConnect = true;
|
|
1758
|
+
const abortableRetry = () => {
|
|
1759
|
+
let cancelAttempt = false;
|
|
1760
|
+
const retryPromise = attempt.retry(this.createWebSocketConnection.bind(this), {
|
|
1761
|
+
delay: this.configuration.delay,
|
|
1762
|
+
initialDelay: this.configuration.initialDelay,
|
|
1763
|
+
factor: this.configuration.factor,
|
|
1764
|
+
maxAttempts: this.configuration.maxAttempts,
|
|
1765
|
+
minDelay: this.configuration.minDelay,
|
|
1766
|
+
maxDelay: this.configuration.maxDelay,
|
|
1767
|
+
jitter: this.configuration.jitter,
|
|
1768
|
+
timeout: this.configuration.timeout,
|
|
1769
|
+
beforeAttempt: context => {
|
|
1770
|
+
if (!this.shouldConnect || cancelAttempt) {
|
|
1771
|
+
context.abort();
|
|
1772
|
+
}
|
|
1773
|
+
},
|
|
1774
|
+
}).catch((error) => {
|
|
1775
|
+
// If we aborted the connection attempt then don’t throw an error
|
|
1776
|
+
// ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
|
|
1777
|
+
if (error && error.code !== 'ATTEMPT_ABORTED') {
|
|
1778
|
+
throw error;
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
return {
|
|
1782
|
+
retryPromise,
|
|
1783
|
+
cancelFunc: () => {
|
|
1784
|
+
cancelAttempt = true;
|
|
1785
|
+
},
|
|
1786
|
+
};
|
|
1787
|
+
};
|
|
1788
|
+
const { retryPromise, cancelFunc } = abortableRetry();
|
|
1789
|
+
this.cancelWebsocketRetry = cancelFunc;
|
|
1790
|
+
return retryPromise;
|
|
1791
|
+
}
|
|
1792
|
+
createWebSocketConnection() {
|
|
1793
|
+
return new Promise((resolve, reject) => {
|
|
1794
|
+
if (this.webSocket) {
|
|
1795
|
+
this.messageQueue = [];
|
|
1796
|
+
this.webSocket.close();
|
|
1797
|
+
this.webSocket = null;
|
|
1798
|
+
}
|
|
1799
|
+
// Init the WebSocket connection
|
|
1800
|
+
const ws = new this.configuration.WebSocketPolyfill(this.url);
|
|
1801
|
+
ws.binaryType = 'arraybuffer';
|
|
1802
|
+
ws.onmessage = (payload) => this.emit('message', payload);
|
|
1803
|
+
ws.onclose = (payload) => this.emit('close', { event: payload });
|
|
1804
|
+
ws.onopen = (payload) => this.emit('open', payload);
|
|
1805
|
+
ws.onerror = (err) => {
|
|
1806
|
+
reject(err);
|
|
1807
|
+
};
|
|
1808
|
+
this.webSocket = ws;
|
|
1809
|
+
// Reset the status
|
|
1810
|
+
this.status = exports.WebSocketStatus.Connecting;
|
|
1811
|
+
this.emit('status', { status: exports.WebSocketStatus.Connecting });
|
|
1812
|
+
// Store resolve/reject for later use
|
|
1813
|
+
this.connectionAttempt = {
|
|
1814
|
+
resolve,
|
|
1815
|
+
reject,
|
|
1816
|
+
};
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
onMessage(event) {
|
|
1820
|
+
this.resolveConnectionAttempt();
|
|
1821
|
+
this.lastMessageReceived = getUnixTime();
|
|
1822
|
+
}
|
|
1823
|
+
resolveConnectionAttempt() {
|
|
1824
|
+
if (this.connectionAttempt) {
|
|
1825
|
+
this.connectionAttempt.resolve();
|
|
1826
|
+
this.connectionAttempt = null;
|
|
1827
|
+
this.status = exports.WebSocketStatus.Connected;
|
|
1828
|
+
this.emit('status', { status: exports.WebSocketStatus.Connected });
|
|
1829
|
+
this.emit('connect');
|
|
1830
|
+
this.messageQueue.forEach(message => this.send(message));
|
|
1831
|
+
this.messageQueue = [];
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
stopConnectionAttempt() {
|
|
1835
|
+
this.connectionAttempt = null;
|
|
1836
|
+
}
|
|
1837
|
+
rejectConnectionAttempt() {
|
|
1838
|
+
var _a;
|
|
1839
|
+
(_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
|
|
1840
|
+
this.connectionAttempt = null;
|
|
1841
|
+
}
|
|
1842
|
+
checkConnection() {
|
|
1843
|
+
var _a;
|
|
1844
|
+
// Don’t check the connection when it’s not even established
|
|
1845
|
+
if (this.status !== exports.WebSocketStatus.Connected) {
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
// Don’t close then connection while waiting for the first message
|
|
1849
|
+
if (!this.lastMessageReceived) {
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
// Don’t close the connection when a message was received recently
|
|
1853
|
+
if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
// No message received in a long time, not even your own
|
|
1857
|
+
// Awareness updates, which are updated every 15 seconds.
|
|
1858
|
+
this.closeTries += 1;
|
|
1859
|
+
// https://bugs.webkit.org/show_bug.cgi?id=247943
|
|
1860
|
+
if (this.closeTries > 2) {
|
|
1861
|
+
this.onClose({
|
|
1862
|
+
event: {
|
|
1863
|
+
code: 4408,
|
|
1864
|
+
reason: 'forced',
|
|
1865
|
+
},
|
|
1866
|
+
});
|
|
1867
|
+
this.closeTries = 0;
|
|
1868
|
+
}
|
|
1869
|
+
else {
|
|
1870
|
+
(_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
|
|
1871
|
+
this.messageQueue = [];
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
registerEventListeners() {
|
|
1875
|
+
if (typeof window === 'undefined') {
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
window.addEventListener('online', this.boundConnect);
|
|
1879
|
+
}
|
|
1880
|
+
// Ensure that the URL always ends with /
|
|
1881
|
+
get serverUrl() {
|
|
1882
|
+
while (this.configuration.url[this.configuration.url.length - 1] === '/') {
|
|
1883
|
+
return this.configuration.url.slice(0, this.configuration.url.length - 1);
|
|
1884
|
+
}
|
|
1885
|
+
return this.configuration.url;
|
|
1886
|
+
}
|
|
1887
|
+
get url() {
|
|
1888
|
+
const encodedParams = encodeQueryParams(this.configuration.parameters);
|
|
1889
|
+
return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
|
|
1890
|
+
}
|
|
1891
|
+
disconnect() {
|
|
1892
|
+
this.shouldConnect = false;
|
|
1893
|
+
if (this.webSocket === null) {
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
try {
|
|
1897
|
+
this.webSocket.close();
|
|
1898
|
+
this.messageQueue = [];
|
|
1899
|
+
}
|
|
1900
|
+
catch {
|
|
1901
|
+
//
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
send(message) {
|
|
1905
|
+
var _a;
|
|
1906
|
+
if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === common.WsReadyStates.Open) {
|
|
1907
|
+
this.webSocket.send(message);
|
|
1908
|
+
}
|
|
1909
|
+
else {
|
|
1910
|
+
this.messageQueue.push(message);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
onClose({ event }) {
|
|
1914
|
+
this.closeTries = 0;
|
|
1915
|
+
this.webSocket = null;
|
|
1916
|
+
if (this.status === exports.WebSocketStatus.Connected) {
|
|
1917
|
+
this.status = exports.WebSocketStatus.Disconnected;
|
|
1918
|
+
this.emit('status', { status: exports.WebSocketStatus.Disconnected });
|
|
1919
|
+
this.emit('disconnect', { event });
|
|
1920
|
+
}
|
|
1921
|
+
if (event.code === common.Unauthorized.code) {
|
|
1922
|
+
if (event.reason === common.Unauthorized.reason) {
|
|
1923
|
+
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.');
|
|
1794
1924
|
}
|
|
1795
1925
|
else {
|
|
1796
|
-
|
|
1797
|
-
// @ts-ignore
|
|
1798
|
-
provider.send(OutgoingMessage, { encoder: message.encoder });
|
|
1926
|
+
console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
|
|
1799
1927
|
}
|
|
1928
|
+
this.shouldConnect = false;
|
|
1800
1929
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1930
|
+
if (event.code === common.Forbidden.code) {
|
|
1931
|
+
if (!this.configuration.quiet) {
|
|
1932
|
+
console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
|
|
1933
|
+
return; // TODO REMOVE ME
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
if (event.code === common.MessageTooBig.code) {
|
|
1937
|
+
console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
|
|
1938
|
+
this.shouldConnect = false;
|
|
1939
|
+
}
|
|
1940
|
+
if (this.connectionAttempt) {
|
|
1941
|
+
// That connection attempt failed.
|
|
1942
|
+
this.rejectConnectionAttempt();
|
|
1943
|
+
}
|
|
1944
|
+
else if (this.shouldConnect) {
|
|
1945
|
+
// The connection was closed by the server. Let’s just try again.
|
|
1946
|
+
this.connect();
|
|
1947
|
+
}
|
|
1948
|
+
// If we’ll reconnect, we’re done for now.
|
|
1949
|
+
if (this.shouldConnect) {
|
|
1950
|
+
return;
|
|
1810
1951
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
}
|
|
1952
|
+
// The status is set correctly already.
|
|
1953
|
+
if (this.status === exports.WebSocketStatus.Disconnected) {
|
|
1954
|
+
return;
|
|
1815
1955
|
}
|
|
1956
|
+
// Let’s update the connection status.
|
|
1957
|
+
this.status = exports.WebSocketStatus.Disconnected;
|
|
1958
|
+
this.emit('status', { status: exports.WebSocketStatus.Disconnected });
|
|
1959
|
+
this.emit('disconnect', { event });
|
|
1816
1960
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1961
|
+
destroy() {
|
|
1962
|
+
this.emit('destroy');
|
|
1963
|
+
if (this.intervals.forceSync) {
|
|
1964
|
+
clearInterval(this.intervals.forceSync);
|
|
1965
|
+
}
|
|
1966
|
+
clearInterval(this.intervals.connectionChecker);
|
|
1967
|
+
// If there is still a connection attempt outstanding then we should stop
|
|
1968
|
+
// it before calling disconnect, otherwise it will be rejected in the onClose
|
|
1969
|
+
// handler and trigger a retry
|
|
1970
|
+
this.stopConnectionAttempt();
|
|
1971
|
+
this.disconnect();
|
|
1972
|
+
this.removeAllListeners();
|
|
1973
|
+
if (typeof window === 'undefined') {
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
window.removeEventListener('online', this.boundConnect);
|
|
1829
1977
|
}
|
|
1830
1978
|
}
|
|
1831
1979
|
|
|
1832
|
-
class
|
|
1833
|
-
constructor(
|
|
1834
|
-
this.
|
|
1835
|
-
this.encoder =
|
|
1980
|
+
class IncomingMessage {
|
|
1981
|
+
constructor(data) {
|
|
1982
|
+
this.data = data;
|
|
1983
|
+
this.encoder = createEncoder();
|
|
1984
|
+
this.decoder = createDecoder(new Uint8Array(this.data));
|
|
1836
1985
|
}
|
|
1837
|
-
|
|
1838
|
-
return
|
|
1986
|
+
readVarUint() {
|
|
1987
|
+
return readVarUint(this.decoder);
|
|
1839
1988
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1989
|
+
readVarString() {
|
|
1990
|
+
return readVarString(this.decoder);
|
|
1842
1991
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1992
|
+
readVarUint8Array() {
|
|
1993
|
+
return readVarUint8Array(this.decoder);
|
|
1845
1994
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
class SyncStepOneMessage extends OutgoingMessage {
|
|
1849
|
-
constructor() {
|
|
1850
|
-
super(...arguments);
|
|
1851
|
-
this.type = exports.MessageType.Sync;
|
|
1852
|
-
this.description = 'First sync step';
|
|
1995
|
+
writeVarUint(type) {
|
|
1996
|
+
return writeVarUint(this.encoder, type);
|
|
1853
1997
|
}
|
|
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;
|
|
1998
|
+
writeVarString(string) {
|
|
1999
|
+
return writeVarString(this.encoder, string);
|
|
1862
2000
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
class SyncStepTwoMessage extends OutgoingMessage {
|
|
1866
|
-
constructor() {
|
|
1867
|
-
super(...arguments);
|
|
1868
|
-
this.type = exports.MessageType.Sync;
|
|
1869
|
-
this.description = 'Second sync step';
|
|
2001
|
+
writeVarUint8Array(data) {
|
|
2002
|
+
return writeVarUint8Array(this.encoder, data);
|
|
1870
2003
|
}
|
|
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;
|
|
2004
|
+
length() {
|
|
2005
|
+
return length(this.encoder);
|
|
1879
2006
|
}
|
|
1880
2007
|
}
|
|
1881
2008
|
|
|
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
|
-
}
|
|
2009
|
+
/**
|
|
2010
|
+
* @module sync-protocol
|
|
2011
|
+
*/
|
|
1896
2012
|
|
|
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
|
-
}
|
|
2013
|
+
/**
|
|
2014
|
+
* @typedef {Map<number, number>} StateMap
|
|
2015
|
+
*/
|
|
1913
2016
|
|
|
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
|
-
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Core Yjs defines two message types:
|
|
2019
|
+
* • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
|
|
2020
|
+
* • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
|
|
2021
|
+
* received all information from the remote client.
|
|
2022
|
+
*
|
|
2023
|
+
* In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
|
|
2024
|
+
* with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
|
|
2025
|
+
* SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
|
|
2026
|
+
*
|
|
2027
|
+
* In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
|
|
2028
|
+
* When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
|
|
2029
|
+
* with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
|
|
2030
|
+
* client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
|
|
2031
|
+
* easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
|
|
2032
|
+
* Therefore it is necesarry that the client initiates the sync.
|
|
2033
|
+
*
|
|
2034
|
+
* Construction of a message:
|
|
2035
|
+
* [messageType : varUint, message definition..]
|
|
2036
|
+
*
|
|
2037
|
+
* Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
|
|
2038
|
+
*
|
|
2039
|
+
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
|
|
2040
|
+
*/
|
|
1940
2041
|
|
|
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
|
-
}
|
|
2042
|
+
const messageYjsSyncStep1 = 0;
|
|
2043
|
+
const messageYjsSyncStep2 = 1;
|
|
2044
|
+
const messageYjsUpdate = 2;
|
|
1954
2045
|
|
|
1955
2046
|
/**
|
|
1956
|
-
*
|
|
2047
|
+
* Create a sync step 1 message based on the state of the current shared document.
|
|
2048
|
+
*
|
|
2049
|
+
* @param {encoding.Encoder} encoder
|
|
2050
|
+
* @param {Y.Doc} doc
|
|
2051
|
+
*/
|
|
2052
|
+
const writeSyncStep1 = (encoder, doc) => {
|
|
2053
|
+
writeVarUint(encoder, messageYjsSyncStep1);
|
|
2054
|
+
const sv = Y__namespace.encodeStateVector(doc);
|
|
2055
|
+
writeVarUint8Array(encoder, sv);
|
|
2056
|
+
};
|
|
2057
|
+
|
|
2058
|
+
/**
|
|
2059
|
+
* @param {encoding.Encoder} encoder
|
|
2060
|
+
* @param {Y.Doc} doc
|
|
2061
|
+
* @param {Uint8Array} [encodedStateVector]
|
|
2062
|
+
*/
|
|
2063
|
+
const writeSyncStep2 = (encoder, doc, encodedStateVector) => {
|
|
2064
|
+
writeVarUint(encoder, messageYjsSyncStep2);
|
|
2065
|
+
writeVarUint8Array(encoder, Y__namespace.encodeStateAsUpdate(doc, encodedStateVector));
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* Read SyncStep1 message and reply with SyncStep2.
|
|
2070
|
+
*
|
|
2071
|
+
* @param {decoding.Decoder} decoder The reply to the received message
|
|
2072
|
+
* @param {encoding.Encoder} encoder The received message
|
|
2073
|
+
* @param {Y.Doc} doc
|
|
2074
|
+
*/
|
|
2075
|
+
const readSyncStep1 = (decoder, encoder, doc) =>
|
|
2076
|
+
writeSyncStep2(encoder, doc, readVarUint8Array(decoder));
|
|
2077
|
+
|
|
2078
|
+
/**
|
|
2079
|
+
* Read and apply Structs and then DeleteStore to a y instance.
|
|
2080
|
+
*
|
|
2081
|
+
* @param {decoding.Decoder} decoder
|
|
2082
|
+
* @param {Y.Doc} doc
|
|
2083
|
+
* @param {any} transactionOrigin
|
|
2084
|
+
*/
|
|
2085
|
+
const readSyncStep2 = (decoder, doc, transactionOrigin) => {
|
|
2086
|
+
try {
|
|
2087
|
+
Y__namespace.applyUpdate(doc, readVarUint8Array(decoder), transactionOrigin);
|
|
2088
|
+
} catch (error) {
|
|
2089
|
+
// This catches errors that are thrown by event handlers
|
|
2090
|
+
console.error('Caught error while handling a Yjs update', error);
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
|
|
2094
|
+
/**
|
|
2095
|
+
* @param {encoding.Encoder} encoder
|
|
2096
|
+
* @param {Uint8Array} update
|
|
2097
|
+
*/
|
|
2098
|
+
const writeUpdate = (encoder, update) => {
|
|
2099
|
+
writeVarUint(encoder, messageYjsUpdate);
|
|
2100
|
+
writeVarUint8Array(encoder, update);
|
|
2101
|
+
};
|
|
2102
|
+
|
|
2103
|
+
/**
|
|
2104
|
+
* Read and apply Structs and then DeleteStore to a y instance.
|
|
1957
2105
|
*
|
|
1958
|
-
* @
|
|
2106
|
+
* @param {decoding.Decoder} decoder
|
|
2107
|
+
* @param {Y.Doc} doc
|
|
2108
|
+
* @param {any} transactionOrigin
|
|
1959
2109
|
*/
|
|
2110
|
+
const readUpdate = readSyncStep2;
|
|
1960
2111
|
|
|
1961
2112
|
/**
|
|
1962
|
-
* @param {
|
|
1963
|
-
* @
|
|
2113
|
+
* @param {decoding.Decoder} decoder A message received from another client
|
|
2114
|
+
* @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
|
|
2115
|
+
* @param {Y.Doc} doc
|
|
2116
|
+
* @param {any} transactionOrigin
|
|
1964
2117
|
*/
|
|
1965
|
-
const
|
|
1966
|
-
|
|
2118
|
+
const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => {
|
|
2119
|
+
const messageType = readVarUint(decoder);
|
|
2120
|
+
switch (messageType) {
|
|
2121
|
+
case messageYjsSyncStep1:
|
|
2122
|
+
readSyncStep1(decoder, encoder, doc);
|
|
2123
|
+
break
|
|
2124
|
+
case messageYjsSyncStep2:
|
|
2125
|
+
readSyncStep2(decoder, doc, transactionOrigin);
|
|
2126
|
+
break
|
|
2127
|
+
case messageYjsUpdate:
|
|
2128
|
+
readUpdate(decoder, doc, transactionOrigin);
|
|
2129
|
+
break
|
|
2130
|
+
default:
|
|
2131
|
+
throw new Error('Unknown message type')
|
|
2132
|
+
}
|
|
2133
|
+
return messageType
|
|
2134
|
+
};
|
|
1967
2135
|
|
|
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;
|
|
2136
|
+
class OutgoingMessage {
|
|
2137
|
+
constructor() {
|
|
2138
|
+
this.encoder = createEncoder();
|
|
2054
2139
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2140
|
+
get(args) {
|
|
2141
|
+
return args.encoder;
|
|
2057
2142
|
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
provider.onOpen(this.receivedOnOpenPayload);
|
|
2061
|
-
}
|
|
2062
|
-
if (this.receivedOnStatusPayload) {
|
|
2063
|
-
provider.onStatus(this.receivedOnStatusPayload);
|
|
2064
|
-
}
|
|
2143
|
+
toUint8Array() {
|
|
2144
|
+
return toUint8Array(this.encoder);
|
|
2065
2145
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
class MessageReceiver {
|
|
2149
|
+
constructor(message) {
|
|
2150
|
+
this.broadcasted = false;
|
|
2151
|
+
this.message = message;
|
|
2068
2152
|
}
|
|
2069
|
-
|
|
2070
|
-
this.
|
|
2153
|
+
setBroadcasted(value) {
|
|
2154
|
+
this.broadcasted = value;
|
|
2155
|
+
return this;
|
|
2071
2156
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2157
|
+
apply(provider, emitSynced) {
|
|
2158
|
+
const { message } = this;
|
|
2159
|
+
const type = message.readVarUint();
|
|
2160
|
+
const emptyMessageLength = message.length();
|
|
2161
|
+
switch (type) {
|
|
2162
|
+
case exports.MessageType.Sync:
|
|
2163
|
+
this.applySyncMessage(provider, emitSynced);
|
|
2164
|
+
break;
|
|
2165
|
+
case exports.MessageType.Awareness:
|
|
2166
|
+
this.applyAwarenessMessage(provider);
|
|
2167
|
+
break;
|
|
2168
|
+
case exports.MessageType.Auth:
|
|
2169
|
+
this.applyAuthMessage(provider);
|
|
2170
|
+
break;
|
|
2171
|
+
case exports.MessageType.QueryAwareness:
|
|
2172
|
+
this.applyQueryAwarenessMessage(provider);
|
|
2173
|
+
break;
|
|
2174
|
+
case exports.MessageType.Stateless:
|
|
2175
|
+
provider.receiveStateless(readVarString(message.decoder));
|
|
2176
|
+
break;
|
|
2177
|
+
case exports.MessageType.SyncStatus:
|
|
2178
|
+
this.applySyncStatusMessage(provider, readVarInt(message.decoder) === 1);
|
|
2179
|
+
break;
|
|
2180
|
+
default:
|
|
2181
|
+
throw new Error(`Can’t apply message of unknown type: ${type}`);
|
|
2080
2182
|
}
|
|
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;
|
|
2183
|
+
// Reply
|
|
2184
|
+
if (message.length() > emptyMessageLength + 1) { // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage)
|
|
2185
|
+
if (this.broadcasted) {
|
|
2186
|
+
// TODO: Some weird TypeScript error
|
|
2187
|
+
// @ts-ignore
|
|
2188
|
+
provider.broadcast(OutgoingMessage, { encoder: message.encoder });
|
|
2121
2189
|
}
|
|
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
|
-
});
|
|
2190
|
+
else {
|
|
2191
|
+
// TODO: Some weird TypeScript error
|
|
2192
|
+
// @ts-ignore
|
|
2193
|
+
provider.send(OutgoingMessage, { encoder: message.encoder });
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2141
2196
|
}
|
|
2142
|
-
|
|
2143
|
-
this
|
|
2144
|
-
|
|
2197
|
+
applySyncMessage(provider, emitSynced) {
|
|
2198
|
+
const { message } = this;
|
|
2199
|
+
message.writeVarUint(exports.MessageType.Sync);
|
|
2200
|
+
// Apply update
|
|
2201
|
+
const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
|
|
2202
|
+
// Synced once we receive Step2
|
|
2203
|
+
if (emitSynced && syncMessageType === messageYjsSyncStep2) {
|
|
2204
|
+
provider.synced = true;
|
|
2205
|
+
}
|
|
2145
2206
|
}
|
|
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');
|
|
2207
|
+
applySyncStatusMessage(provider, applied) {
|
|
2208
|
+
if (applied) {
|
|
2209
|
+
provider.decrementUnsyncedChanges();
|
|
2153
2210
|
}
|
|
2154
2211
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
2212
|
+
applyAwarenessMessage(provider) {
|
|
2213
|
+
const { message } = this;
|
|
2214
|
+
applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
|
|
2157
2215
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
(
|
|
2161
|
-
this.connectionAttempt = null;
|
|
2216
|
+
applyAuthMessage(provider) {
|
|
2217
|
+
const { message } = this;
|
|
2218
|
+
common.readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
|
|
2162
2219
|
}
|
|
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();
|
|
2220
|
+
applyQueryAwarenessMessage(provider) {
|
|
2221
|
+
const { message } = this;
|
|
2222
|
+
message.writeVarUint(exports.MessageType.Awareness);
|
|
2223
|
+
message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
|
|
2180
2224
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
class MessageSender {
|
|
2228
|
+
constructor(Message, args = {}) {
|
|
2229
|
+
this.message = new Message();
|
|
2230
|
+
this.encoder = this.message.get(args);
|
|
2186
2231
|
}
|
|
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;
|
|
2232
|
+
create() {
|
|
2233
|
+
return toUint8Array(this.encoder);
|
|
2193
2234
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
|
|
2235
|
+
send(webSocket) {
|
|
2236
|
+
webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
|
|
2197
2237
|
}
|
|
2198
|
-
|
|
2199
|
-
this.
|
|
2200
|
-
if (this.webSocket === null) {
|
|
2201
|
-
return;
|
|
2202
|
-
}
|
|
2203
|
-
try {
|
|
2204
|
-
this.webSocket.close();
|
|
2205
|
-
}
|
|
2206
|
-
catch {
|
|
2207
|
-
//
|
|
2208
|
-
}
|
|
2238
|
+
broadcast(channel) {
|
|
2239
|
+
publish(channel, this.create());
|
|
2209
2240
|
}
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
class AuthenticationMessage extends OutgoingMessage {
|
|
2244
|
+
constructor() {
|
|
2245
|
+
super(...arguments);
|
|
2246
|
+
this.type = exports.MessageType.Auth;
|
|
2247
|
+
this.description = 'Authentication';
|
|
2215
2248
|
}
|
|
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;
|
|
2249
|
+
get(args) {
|
|
2250
|
+
if (typeof args.token === 'undefined') {
|
|
2251
|
+
throw new Error('The authentication message requires `token` as an argument.');
|
|
2241
2252
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2253
|
+
writeVarString(this.encoder, args.documentName);
|
|
2254
|
+
writeVarUint(this.encoder, this.type);
|
|
2255
|
+
common.writeAuthentication(this.encoder, args.token);
|
|
2256
|
+
return this.encoder;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
class AwarenessMessage extends OutgoingMessage {
|
|
2261
|
+
constructor() {
|
|
2262
|
+
super(...arguments);
|
|
2263
|
+
this.type = exports.MessageType.Awareness;
|
|
2264
|
+
this.description = 'Awareness states update';
|
|
2265
|
+
}
|
|
2266
|
+
get(args) {
|
|
2267
|
+
if (typeof args.awareness === 'undefined') {
|
|
2268
|
+
throw new Error('The awareness message requires awareness as an argument');
|
|
2245
2269
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
this.connect();
|
|
2270
|
+
if (typeof args.clients === 'undefined') {
|
|
2271
|
+
throw new Error('The awareness message requires clients as an argument');
|
|
2249
2272
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2273
|
+
writeVarString(this.encoder, args.documentName);
|
|
2274
|
+
writeVarUint(this.encoder, this.type);
|
|
2275
|
+
let awarenessUpdate;
|
|
2276
|
+
if (args.states === undefined) {
|
|
2277
|
+
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients);
|
|
2253
2278
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
return;
|
|
2279
|
+
else {
|
|
2280
|
+
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
|
|
2257
2281
|
}
|
|
2258
|
-
|
|
2259
|
-
this.
|
|
2260
|
-
this.emit('status', { status: exports.WebSocketStatus.Disconnected });
|
|
2261
|
-
this.emit('disconnect', { event });
|
|
2282
|
+
writeVarUint8Array(this.encoder, awarenessUpdate);
|
|
2283
|
+
return this.encoder;
|
|
2262
2284
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
this.
|
|
2273
|
-
this.
|
|
2274
|
-
this.
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
class CloseMessage extends OutgoingMessage {
|
|
2288
|
+
constructor() {
|
|
2289
|
+
super(...arguments);
|
|
2290
|
+
this.type = exports.MessageType.CLOSE;
|
|
2291
|
+
this.description = 'Ask the server to close the connection';
|
|
2292
|
+
}
|
|
2293
|
+
get(args) {
|
|
2294
|
+
writeVarString(this.encoder, args.documentName);
|
|
2295
|
+
writeVarUint(this.encoder, this.type);
|
|
2296
|
+
return this.encoder;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
class QueryAwarenessMessage extends OutgoingMessage {
|
|
2301
|
+
constructor() {
|
|
2302
|
+
super(...arguments);
|
|
2303
|
+
this.type = exports.MessageType.QueryAwareness;
|
|
2304
|
+
this.description = 'Queries awareness states';
|
|
2305
|
+
}
|
|
2306
|
+
get(args) {
|
|
2307
|
+
console.log('queryAwareness: writing string docName', args.documentName);
|
|
2308
|
+
console.log(this.encoder.cpos);
|
|
2309
|
+
writeVarString(this.encoder, args.documentName);
|
|
2310
|
+
writeVarUint(this.encoder, this.type);
|
|
2311
|
+
return this.encoder;
|
|
2279
2312
|
}
|
|
2280
2313
|
}
|
|
2281
2314
|
|
|
@@ -2294,15 +2327,50 @@ class StatelessMessage extends OutgoingMessage {
|
|
|
2294
2327
|
}
|
|
2295
2328
|
}
|
|
2296
2329
|
|
|
2297
|
-
class
|
|
2330
|
+
class SyncStepOneMessage extends OutgoingMessage {
|
|
2298
2331
|
constructor() {
|
|
2299
2332
|
super(...arguments);
|
|
2300
|
-
this.type = exports.MessageType.
|
|
2301
|
-
this.description = '
|
|
2333
|
+
this.type = exports.MessageType.Sync;
|
|
2334
|
+
this.description = 'First sync step';
|
|
2335
|
+
}
|
|
2336
|
+
get(args) {
|
|
2337
|
+
if (typeof args.document === 'undefined') {
|
|
2338
|
+
throw new Error('The sync step one message requires document as an argument');
|
|
2339
|
+
}
|
|
2340
|
+
writeVarString(this.encoder, args.documentName);
|
|
2341
|
+
writeVarUint(this.encoder, this.type);
|
|
2342
|
+
writeSyncStep1(this.encoder, args.document);
|
|
2343
|
+
return this.encoder;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
class SyncStepTwoMessage extends OutgoingMessage {
|
|
2348
|
+
constructor() {
|
|
2349
|
+
super(...arguments);
|
|
2350
|
+
this.type = exports.MessageType.Sync;
|
|
2351
|
+
this.description = 'Second sync step';
|
|
2352
|
+
}
|
|
2353
|
+
get(args) {
|
|
2354
|
+
if (typeof args.document === 'undefined') {
|
|
2355
|
+
throw new Error('The sync step two message requires document as an argument');
|
|
2356
|
+
}
|
|
2357
|
+
writeVarString(this.encoder, args.documentName);
|
|
2358
|
+
writeVarUint(this.encoder, this.type);
|
|
2359
|
+
writeSyncStep2(this.encoder, args.document);
|
|
2360
|
+
return this.encoder;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
class UpdateMessage extends OutgoingMessage {
|
|
2365
|
+
constructor() {
|
|
2366
|
+
super(...arguments);
|
|
2367
|
+
this.type = exports.MessageType.Sync;
|
|
2368
|
+
this.description = 'A document update';
|
|
2302
2369
|
}
|
|
2303
2370
|
get(args) {
|
|
2304
2371
|
writeVarString(this.encoder, args.documentName);
|
|
2305
2372
|
writeVarUint(this.encoder, this.type);
|
|
2373
|
+
writeUpdate(this.encoder, args.update);
|
|
2306
2374
|
return this.encoder;
|
|
2307
2375
|
}
|
|
2308
2376
|
}
|
|
@@ -2335,6 +2403,8 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2335
2403
|
onAwarenessChange: () => null,
|
|
2336
2404
|
onStateless: () => null,
|
|
2337
2405
|
quiet: false,
|
|
2406
|
+
connect: true,
|
|
2407
|
+
preserveConnection: true,
|
|
2338
2408
|
};
|
|
2339
2409
|
this.subscribedToBroadcastChannel = false;
|
|
2340
2410
|
this.isSynced = false;
|
|
@@ -2348,7 +2418,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2348
2418
|
};
|
|
2349
2419
|
this.isConnected = true;
|
|
2350
2420
|
this.boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this);
|
|
2351
|
-
this.
|
|
2421
|
+
this.boundPageUnload = this.pageUnload.bind(this);
|
|
2352
2422
|
this.boundOnOpen = this.onOpen.bind(this);
|
|
2353
2423
|
this.boundOnMessage = this.onMessage.bind(this);
|
|
2354
2424
|
this.boundOnClose = this.onClose.bind(this);
|
|
@@ -2408,6 +2478,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2408
2478
|
const websocketProviderConfig = configuration;
|
|
2409
2479
|
this.configuration.websocketProvider = new HocuspocusProviderWebsocket({
|
|
2410
2480
|
url: websocketProviderConfig.url,
|
|
2481
|
+
connect: websocketProviderConfig.connect,
|
|
2411
2482
|
parameters: websocketProviderConfig.parameters,
|
|
2412
2483
|
});
|
|
2413
2484
|
}
|
|
@@ -2422,21 +2493,28 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2422
2493
|
get hasUnsyncedChanges() {
|
|
2423
2494
|
return this.unsyncedChanges > 0;
|
|
2424
2495
|
}
|
|
2425
|
-
|
|
2426
|
-
this.unsyncedChanges +=
|
|
2496
|
+
incrementUnsyncedChanges() {
|
|
2497
|
+
this.unsyncedChanges += 1;
|
|
2498
|
+
this.emit('unsyncedChanges', this.unsyncedChanges);
|
|
2499
|
+
}
|
|
2500
|
+
decrementUnsyncedChanges() {
|
|
2501
|
+
this.unsyncedChanges -= 1;
|
|
2502
|
+
if (this.unsyncedChanges === 0) {
|
|
2503
|
+
this.synced = true;
|
|
2504
|
+
}
|
|
2427
2505
|
this.emit('unsyncedChanges', this.unsyncedChanges);
|
|
2428
2506
|
}
|
|
2429
2507
|
forceSync() {
|
|
2430
2508
|
this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
|
|
2431
2509
|
}
|
|
2432
|
-
|
|
2510
|
+
pageUnload() {
|
|
2433
2511
|
removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload');
|
|
2434
2512
|
}
|
|
2435
2513
|
registerEventListeners() {
|
|
2436
2514
|
if (typeof window === 'undefined') {
|
|
2437
2515
|
return;
|
|
2438
2516
|
}
|
|
2439
|
-
window.addEventListener('
|
|
2517
|
+
window.addEventListener('unload', this.boundPageUnload);
|
|
2440
2518
|
}
|
|
2441
2519
|
sendStateless(payload) {
|
|
2442
2520
|
this.send(StatelessMessage, { documentName: this.configuration.name, payload });
|
|
@@ -2445,7 +2523,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2445
2523
|
if (origin === this) {
|
|
2446
2524
|
return;
|
|
2447
2525
|
}
|
|
2448
|
-
this.
|
|
2526
|
+
this.incrementUnsyncedChanges();
|
|
2449
2527
|
this.send(UpdateMessage, { update, documentName: this.configuration.name }, true);
|
|
2450
2528
|
}
|
|
2451
2529
|
awarenessUpdateHandler({ added, updated, removed }, origin) {
|
|
@@ -2456,6 +2534,12 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2456
2534
|
documentName: this.configuration.name,
|
|
2457
2535
|
}, true);
|
|
2458
2536
|
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Indicates whether a first handshake with the server has been established
|
|
2539
|
+
*
|
|
2540
|
+
* Note: this does not mean all updates from the client have been persisted to the backend. For this,
|
|
2541
|
+
* use `hasUnsyncedChanges`.
|
|
2542
|
+
*/
|
|
2459
2543
|
get synced() {
|
|
2460
2544
|
return this.isSynced;
|
|
2461
2545
|
}
|
|
@@ -2463,9 +2547,6 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2463
2547
|
if (this.isSynced === state) {
|
|
2464
2548
|
return;
|
|
2465
2549
|
}
|
|
2466
|
-
if (state && this.unsyncedChanges > 0) {
|
|
2467
|
-
this.updateUnsyncedChanges(-1 * this.unsyncedChanges);
|
|
2468
|
-
}
|
|
2469
2550
|
this.isSynced = state;
|
|
2470
2551
|
this.emit('synced', { state });
|
|
2471
2552
|
this.emit('sync', { state });
|
|
@@ -2483,6 +2564,9 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2483
2564
|
disconnect() {
|
|
2484
2565
|
this.disconnectBroadcastChannel();
|
|
2485
2566
|
this.configuration.websocketProvider.detach(this);
|
|
2567
|
+
if (!this.configuration.preserveConnection) {
|
|
2568
|
+
this.configuration.websocketProvider.disconnect();
|
|
2569
|
+
}
|
|
2486
2570
|
}
|
|
2487
2571
|
async onOpen(event) {
|
|
2488
2572
|
this.isAuthenticated = false;
|
|
@@ -2503,6 +2587,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2503
2587
|
return this.configuration.token;
|
|
2504
2588
|
}
|
|
2505
2589
|
startSync() {
|
|
2590
|
+
this.incrementUnsyncedChanges();
|
|
2506
2591
|
this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
|
|
2507
2592
|
if (this.awareness.getLocalState() !== null) {
|
|
2508
2593
|
this.send(AwarenessMessage, {
|
|
@@ -2513,8 +2598,9 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2513
2598
|
}
|
|
2514
2599
|
}
|
|
2515
2600
|
send(message, args, broadcast = false) {
|
|
2516
|
-
if (!this.isConnected)
|
|
2601
|
+
if (!this.isConnected) {
|
|
2517
2602
|
return;
|
|
2603
|
+
}
|
|
2518
2604
|
if (broadcast) {
|
|
2519
2605
|
this.mux(() => { this.broadcast(message, args); });
|
|
2520
2606
|
}
|
|
@@ -2530,7 +2616,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2530
2616
|
}
|
|
2531
2617
|
message.writeVarString(documentName);
|
|
2532
2618
|
this.emit('message', { event, message: new IncomingMessage(event.data) });
|
|
2533
|
-
new MessageReceiver(message).apply(this);
|
|
2619
|
+
new MessageReceiver(message).apply(this, true);
|
|
2534
2620
|
}
|
|
2535
2621
|
onClose(event) {
|
|
2536
2622
|
this.isAuthenticated = false;
|
|
@@ -2566,7 +2652,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2566
2652
|
if (typeof window === 'undefined') {
|
|
2567
2653
|
return;
|
|
2568
2654
|
}
|
|
2569
|
-
window.removeEventListener('
|
|
2655
|
+
window.removeEventListener('unload', this.boundPageUnload);
|
|
2570
2656
|
}
|
|
2571
2657
|
permissionDeniedHandler(reason) {
|
|
2572
2658
|
this.emit('authenticationFailed', { reason });
|