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