@hocuspocus/provider 2.2.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hocuspocus-provider.cjs +797 -689
- package/dist/hocuspocus-provider.cjs.map +1 -1
- package/dist/hocuspocus-provider.esm.js +799 -691
- package/dist/hocuspocus-provider.esm.js.map +1 -1
- package/dist/packages/extension-redis/src/Redis.d.ts +1 -1
- package/dist/packages/provider/src/HocuspocusProvider.d.ts +23 -8
- package/dist/packages/provider/src/HocuspocusProviderWebsocket.d.ts +2 -1
- package/dist/packages/provider/src/MessageReceiver.d.ts +2 -1
- package/dist/packages/provider/src/TiptapCollabProvider.d.ts +15 -0
- package/dist/packages/provider/src/types.d.ts +6 -5
- package/dist/packages/server/src/Hocuspocus.d.ts +3 -2
- package/dist/packages/server/src/MessageReceiver.d.ts +1 -1
- package/dist/packages/server/src/OutgoingMessage.d.ts +1 -0
- package/dist/packages/server/src/types.d.ts +30 -6
- package/dist/tests/provider/hasUnsyncedChanges.d.ts +1 -0
- package/dist/tests/server/afterUnloadDocument.d.ts +1 -0
- package/package.json +3 -3
- package/src/HocuspocusProvider.ts +60 -31
- package/src/HocuspocusProviderWebsocket.ts +23 -7
- package/src/MessageReceiver.ts +12 -9
- package/src/TiptapCollabProvider.ts +50 -0
- package/src/types.ts +5 -4
|
@@ -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,102 +1604,460 @@ 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
|
-
|
|
1637
|
+
class HocuspocusProviderWebsocket extends EventEmitter {
|
|
1638
|
+
constructor(configuration) {
|
|
1639
|
+
super();
|
|
1640
|
+
this.messageQueue = [];
|
|
1641
|
+
this.configuration = {
|
|
1642
|
+
url: '',
|
|
1643
|
+
// @ts-ignore
|
|
1644
|
+
document: undefined,
|
|
1645
|
+
// @ts-ignore
|
|
1646
|
+
awareness: undefined,
|
|
1647
|
+
WebSocketPolyfill: undefined,
|
|
1648
|
+
parameters: {},
|
|
1649
|
+
connect: true,
|
|
1650
|
+
broadcast: true,
|
|
1651
|
+
forceSyncInterval: false,
|
|
1652
|
+
// TODO: this should depend on awareness.outdatedTime
|
|
1653
|
+
messageReconnectTimeout: 30000,
|
|
1654
|
+
// 1 second
|
|
1655
|
+
delay: 1000,
|
|
1656
|
+
// instant
|
|
1657
|
+
initialDelay: 0,
|
|
1658
|
+
// double the delay each time
|
|
1659
|
+
factor: 2,
|
|
1660
|
+
// unlimited retries
|
|
1661
|
+
maxAttempts: 0,
|
|
1662
|
+
// wait at least 1 second
|
|
1663
|
+
minDelay: 1000,
|
|
1664
|
+
// at least every 30 seconds
|
|
1665
|
+
maxDelay: 30000,
|
|
1666
|
+
// randomize
|
|
1667
|
+
jitter: true,
|
|
1668
|
+
// retry forever
|
|
1669
|
+
timeout: 0,
|
|
1670
|
+
onOpen: () => null,
|
|
1671
|
+
onConnect: () => null,
|
|
1672
|
+
onMessage: () => null,
|
|
1673
|
+
onOutgoingMessage: () => null,
|
|
1674
|
+
onStatus: () => null,
|
|
1675
|
+
onDisconnect: () => null,
|
|
1676
|
+
onClose: () => null,
|
|
1677
|
+
onDestroy: () => null,
|
|
1678
|
+
onAwarenessUpdate: () => null,
|
|
1679
|
+
onAwarenessChange: () => null,
|
|
1680
|
+
quiet: false,
|
|
1681
|
+
};
|
|
1682
|
+
this.subscribedToBroadcastChannel = false;
|
|
1683
|
+
this.webSocket = null;
|
|
1684
|
+
this.shouldConnect = true;
|
|
1685
|
+
this.status = exports.WebSocketStatus.Disconnected;
|
|
1686
|
+
this.lastMessageReceived = 0;
|
|
1687
|
+
this.mux = createMutex();
|
|
1688
|
+
this.intervals = {
|
|
1689
|
+
forceSync: null,
|
|
1690
|
+
connectionChecker: null,
|
|
1691
|
+
};
|
|
1692
|
+
this.connectionAttempt = null;
|
|
1693
|
+
this.receivedOnOpenPayload = undefined;
|
|
1694
|
+
this.receivedOnStatusPayload = undefined;
|
|
1695
|
+
this.boundConnect = this.connect.bind(this);
|
|
1696
|
+
this.setConfiguration(configuration);
|
|
1697
|
+
this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
|
|
1698
|
+
this.on('open', this.configuration.onOpen);
|
|
1699
|
+
this.on('open', this.onOpen.bind(this));
|
|
1700
|
+
this.on('connect', this.configuration.onConnect);
|
|
1701
|
+
this.on('message', this.configuration.onMessage);
|
|
1702
|
+
this.on('outgoingMessage', this.configuration.onOutgoingMessage);
|
|
1703
|
+
this.on('status', this.configuration.onStatus);
|
|
1704
|
+
this.on('status', this.onStatus.bind(this));
|
|
1705
|
+
this.on('disconnect', this.configuration.onDisconnect);
|
|
1706
|
+
this.on('close', this.configuration.onClose);
|
|
1707
|
+
this.on('destroy', this.configuration.onDestroy);
|
|
1708
|
+
this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
|
|
1709
|
+
this.on('awarenessChange', this.configuration.onAwarenessChange);
|
|
1710
|
+
this.on('close', this.onClose.bind(this));
|
|
1711
|
+
this.on('message', this.onMessage.bind(this));
|
|
1712
|
+
this.registerEventListeners();
|
|
1713
|
+
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
|
|
1714
|
+
if (typeof configuration.connect !== 'undefined') {
|
|
1715
|
+
this.shouldConnect = configuration.connect;
|
|
1716
|
+
}
|
|
1717
|
+
if (!this.shouldConnect) {
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
this.connect();
|
|
1721
|
+
}
|
|
1722
|
+
async onOpen(event) {
|
|
1723
|
+
this.receivedOnOpenPayload = event;
|
|
1724
|
+
}
|
|
1725
|
+
async onStatus(data) {
|
|
1726
|
+
this.receivedOnStatusPayload = data;
|
|
1727
|
+
}
|
|
1728
|
+
attach(provider) {
|
|
1729
|
+
if (this.status === exports.WebSocketStatus.Disconnected && this.shouldConnect) {
|
|
1730
|
+
this.connect();
|
|
1731
|
+
}
|
|
1732
|
+
if (this.receivedOnOpenPayload) {
|
|
1733
|
+
provider.onOpen(this.receivedOnOpenPayload);
|
|
1734
|
+
}
|
|
1735
|
+
if (this.receivedOnStatusPayload) {
|
|
1736
|
+
provider.onStatus(this.receivedOnStatusPayload);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
detach(provider) {
|
|
1740
|
+
// tell the server to remove the listener
|
|
1741
|
+
}
|
|
1742
|
+
setConfiguration(configuration = {}) {
|
|
1743
|
+
this.configuration = { ...this.configuration, ...configuration };
|
|
1744
|
+
}
|
|
1745
|
+
async connect() {
|
|
1746
|
+
if (this.status === exports.WebSocketStatus.Connected) {
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
// Always cancel any previously initiated connection retryer instances
|
|
1750
|
+
if (this.cancelWebsocketRetry) {
|
|
1751
|
+
this.cancelWebsocketRetry();
|
|
1752
|
+
this.cancelWebsocketRetry = undefined;
|
|
1753
|
+
}
|
|
1754
|
+
this.receivedOnOpenPayload = undefined;
|
|
1755
|
+
this.receivedOnStatusPayload = undefined;
|
|
1756
|
+
this.shouldConnect = true;
|
|
1757
|
+
const abortableRetry = () => {
|
|
1758
|
+
let cancelAttempt = false;
|
|
1759
|
+
const retryPromise = attempt.retry(this.createWebSocketConnection.bind(this), {
|
|
1760
|
+
delay: this.configuration.delay,
|
|
1761
|
+
initialDelay: this.configuration.initialDelay,
|
|
1762
|
+
factor: this.configuration.factor,
|
|
1763
|
+
maxAttempts: this.configuration.maxAttempts,
|
|
1764
|
+
minDelay: this.configuration.minDelay,
|
|
1765
|
+
maxDelay: this.configuration.maxDelay,
|
|
1766
|
+
jitter: this.configuration.jitter,
|
|
1767
|
+
timeout: this.configuration.timeout,
|
|
1768
|
+
beforeAttempt: context => {
|
|
1769
|
+
if (!this.shouldConnect || cancelAttempt) {
|
|
1770
|
+
context.abort();
|
|
1771
|
+
}
|
|
1772
|
+
},
|
|
1773
|
+
}).catch((error) => {
|
|
1774
|
+
// If we aborted the connection attempt then don’t throw an error
|
|
1775
|
+
// ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
|
|
1776
|
+
if (error && error.code !== 'ATTEMPT_ABORTED') {
|
|
1777
|
+
throw error;
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
return {
|
|
1781
|
+
retryPromise,
|
|
1782
|
+
cancelFunc: () => {
|
|
1783
|
+
cancelAttempt = true;
|
|
1784
|
+
},
|
|
1785
|
+
};
|
|
1786
|
+
};
|
|
1787
|
+
const { retryPromise, cancelFunc } = abortableRetry();
|
|
1788
|
+
this.cancelWebsocketRetry = cancelFunc;
|
|
1789
|
+
return retryPromise;
|
|
1790
|
+
}
|
|
1791
|
+
createWebSocketConnection() {
|
|
1792
|
+
return new Promise((resolve, reject) => {
|
|
1793
|
+
if (this.webSocket) {
|
|
1794
|
+
this.messageQueue = [];
|
|
1795
|
+
this.webSocket.close();
|
|
1796
|
+
this.webSocket = null;
|
|
1797
|
+
}
|
|
1798
|
+
// Init the WebSocket connection
|
|
1799
|
+
const ws = new this.configuration.WebSocketPolyfill(this.url);
|
|
1800
|
+
ws.binaryType = 'arraybuffer';
|
|
1801
|
+
ws.onmessage = (payload) => this.emit('message', payload);
|
|
1802
|
+
ws.onclose = (payload) => this.emit('close', { event: payload });
|
|
1803
|
+
ws.onopen = (payload) => this.emit('open', payload);
|
|
1804
|
+
ws.onerror = (err) => {
|
|
1805
|
+
reject(err);
|
|
1806
|
+
};
|
|
1807
|
+
this.webSocket = ws;
|
|
1808
|
+
// Reset the status
|
|
1809
|
+
this.status = exports.WebSocketStatus.Connecting;
|
|
1810
|
+
this.emit('status', { status: exports.WebSocketStatus.Connecting });
|
|
1811
|
+
// Store resolve/reject for later use
|
|
1812
|
+
this.connectionAttempt = {
|
|
1813
|
+
resolve,
|
|
1814
|
+
reject,
|
|
1815
|
+
};
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
onMessage(event) {
|
|
1819
|
+
this.resolveConnectionAttempt();
|
|
1820
|
+
this.lastMessageReceived = getUnixTime();
|
|
1821
|
+
}
|
|
1822
|
+
resolveConnectionAttempt() {
|
|
1823
|
+
if (this.connectionAttempt) {
|
|
1824
|
+
this.connectionAttempt.resolve();
|
|
1825
|
+
this.connectionAttempt = null;
|
|
1826
|
+
this.status = exports.WebSocketStatus.Connected;
|
|
1827
|
+
this.emit('status', { status: exports.WebSocketStatus.Connected });
|
|
1828
|
+
this.emit('connect');
|
|
1829
|
+
this.messageQueue.forEach(message => this.send(message));
|
|
1830
|
+
this.messageQueue = [];
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
stopConnectionAttempt() {
|
|
1834
|
+
this.connectionAttempt = null;
|
|
1835
|
+
}
|
|
1836
|
+
rejectConnectionAttempt() {
|
|
1837
|
+
var _a;
|
|
1838
|
+
(_a = this.connectionAttempt) === null || _a === void 0 ? void 0 : _a.reject();
|
|
1839
|
+
this.connectionAttempt = null;
|
|
1840
|
+
}
|
|
1841
|
+
checkConnection() {
|
|
1842
|
+
var _a;
|
|
1843
|
+
// Don’t check the connection when it’s not even established
|
|
1844
|
+
if (this.status !== exports.WebSocketStatus.Connected) {
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
// Don’t close then connection while waiting for the first message
|
|
1848
|
+
if (!this.lastMessageReceived) {
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
// Don’t close the connection when a message was received recently
|
|
1852
|
+
if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
// No message received in a long time, not even your own
|
|
1856
|
+
// Awareness updates, which are updated every 15 seconds.
|
|
1857
|
+
(_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
|
|
1858
|
+
this.messageQueue = [];
|
|
1859
|
+
}
|
|
1860
|
+
registerEventListeners() {
|
|
1861
|
+
if (typeof window === 'undefined') {
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
window.addEventListener('online', this.boundConnect);
|
|
1865
|
+
}
|
|
1866
|
+
// Ensure that the URL always ends with /
|
|
1867
|
+
get serverUrl() {
|
|
1868
|
+
while (this.configuration.url[this.configuration.url.length - 1] === '/') {
|
|
1869
|
+
return this.configuration.url.slice(0, this.configuration.url.length - 1);
|
|
1870
|
+
}
|
|
1871
|
+
return this.configuration.url;
|
|
1872
|
+
}
|
|
1873
|
+
get url() {
|
|
1874
|
+
const encodedParams = encodeQueryParams(this.configuration.parameters);
|
|
1875
|
+
return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
|
|
1876
|
+
}
|
|
1877
|
+
disconnect() {
|
|
1878
|
+
this.shouldConnect = false;
|
|
1879
|
+
if (this.webSocket === null) {
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
try {
|
|
1883
|
+
this.webSocket.close();
|
|
1884
|
+
this.messageQueue = [];
|
|
1885
|
+
}
|
|
1886
|
+
catch {
|
|
1887
|
+
//
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
send(message) {
|
|
1891
|
+
var _a;
|
|
1892
|
+
if (((_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.readyState) === common.WsReadyStates.Open) {
|
|
1893
|
+
this.webSocket.send(message);
|
|
1894
|
+
}
|
|
1895
|
+
else {
|
|
1896
|
+
this.messageQueue.push(message);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
onClose({ event }) {
|
|
1900
|
+
this.webSocket = null;
|
|
1901
|
+
if (this.status === exports.WebSocketStatus.Connected) {
|
|
1902
|
+
this.status = exports.WebSocketStatus.Disconnected;
|
|
1903
|
+
this.emit('status', { status: exports.WebSocketStatus.Disconnected });
|
|
1904
|
+
this.emit('disconnect', { event });
|
|
1905
|
+
}
|
|
1906
|
+
if (event.code === common.Unauthorized.code) {
|
|
1907
|
+
if (event.reason === common.Unauthorized.reason) {
|
|
1908
|
+
console.warn('[HocuspocusProvider] An authentication token is required, but you didn’t send one. Try adding a `token` to your HocuspocusProvider configuration. Won’t try again.');
|
|
1909
|
+
}
|
|
1910
|
+
else {
|
|
1911
|
+
console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
|
|
1912
|
+
}
|
|
1913
|
+
this.shouldConnect = false;
|
|
1914
|
+
}
|
|
1915
|
+
if (event.code === common.Forbidden.code) {
|
|
1916
|
+
if (!this.configuration.quiet) {
|
|
1917
|
+
console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
|
|
1918
|
+
return; // TODO REMOVE ME
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (event.code === common.MessageTooBig.code) {
|
|
1922
|
+
console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
|
|
1923
|
+
this.shouldConnect = false;
|
|
1924
|
+
}
|
|
1925
|
+
if (this.connectionAttempt) {
|
|
1926
|
+
// That connection attempt failed.
|
|
1927
|
+
this.rejectConnectionAttempt();
|
|
1928
|
+
}
|
|
1929
|
+
else if (this.shouldConnect) {
|
|
1930
|
+
// The connection was closed by the server. Let’s just try again.
|
|
1931
|
+
this.connect();
|
|
1932
|
+
}
|
|
1933
|
+
// If we’ll reconnect, we’re done for now.
|
|
1934
|
+
if (this.shouldConnect) {
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
// The status is set correctly already.
|
|
1938
|
+
if (this.status === exports.WebSocketStatus.Disconnected) {
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
// Let’s update the connection status.
|
|
1942
|
+
this.status = exports.WebSocketStatus.Disconnected;
|
|
1943
|
+
this.emit('status', { status: exports.WebSocketStatus.Disconnected });
|
|
1944
|
+
this.emit('disconnect', { event });
|
|
1945
|
+
}
|
|
1946
|
+
destroy() {
|
|
1947
|
+
this.emit('destroy');
|
|
1948
|
+
if (this.intervals.forceSync) {
|
|
1949
|
+
clearInterval(this.intervals.forceSync);
|
|
1950
|
+
}
|
|
1951
|
+
clearInterval(this.intervals.connectionChecker);
|
|
1952
|
+
// If there is still a connection attempt outstanding then we should stop
|
|
1953
|
+
// it before calling disconnect, otherwise it will be rejected in the onClose
|
|
1954
|
+
// handler and trigger a retry
|
|
1955
|
+
this.stopConnectionAttempt();
|
|
1956
|
+
this.disconnect();
|
|
1957
|
+
this.removeAllListeners();
|
|
1958
|
+
if (typeof window === 'undefined') {
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
window.removeEventListener('online', this.boundConnect);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
class IncomingMessage {
|
|
1966
|
+
constructor(data) {
|
|
1967
|
+
this.data = data;
|
|
1968
|
+
this.encoder = createEncoder();
|
|
1969
|
+
this.decoder = createDecoder(new Uint8Array(this.data));
|
|
1970
|
+
}
|
|
1971
|
+
readVarUint() {
|
|
1972
|
+
return readVarUint(this.decoder);
|
|
1973
|
+
}
|
|
1974
|
+
readVarString() {
|
|
1975
|
+
return readVarString(this.decoder);
|
|
1976
|
+
}
|
|
1977
|
+
readVarUint8Array() {
|
|
1978
|
+
return readVarUint8Array(this.decoder);
|
|
1979
|
+
}
|
|
1980
|
+
writeVarUint(type) {
|
|
1981
|
+
return writeVarUint(this.encoder, type);
|
|
1982
|
+
}
|
|
1983
|
+
writeVarString(string) {
|
|
1984
|
+
return writeVarString(this.encoder, string);
|
|
1985
|
+
}
|
|
1986
|
+
writeVarUint8Array(data) {
|
|
1987
|
+
return writeVarUint8Array(this.encoder, data);
|
|
1988
|
+
}
|
|
1989
|
+
length() {
|
|
1990
|
+
return length(this.encoder);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* @module sync-protocol
|
|
1996
|
+
*/
|
|
1997
|
+
|
|
1998
|
+
/**
|
|
1999
|
+
* @typedef {Map<number, number>} StateMap
|
|
2000
|
+
*/
|
|
2001
|
+
|
|
2002
|
+
/**
|
|
2003
|
+
* Core Yjs defines two message types:
|
|
2004
|
+
* • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
|
|
2005
|
+
* • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
|
|
2006
|
+
* received all information from the remote client.
|
|
2007
|
+
*
|
|
2008
|
+
* In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
|
|
2009
|
+
* with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
|
|
2010
|
+
* SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
|
|
2011
|
+
*
|
|
2012
|
+
* In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
|
|
2013
|
+
* When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
|
|
2014
|
+
* with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
|
|
2015
|
+
* client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
|
|
2016
|
+
* easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
|
|
2017
|
+
* Therefore it is necesarry that the client initiates the sync.
|
|
2018
|
+
*
|
|
2019
|
+
* Construction of a message:
|
|
2020
|
+
* [messageType : varUint, message definition..]
|
|
2021
|
+
*
|
|
2022
|
+
* Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
|
|
2023
|
+
*
|
|
2024
|
+
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
|
|
2025
|
+
*/
|
|
2026
|
+
|
|
2027
|
+
const messageYjsSyncStep1 = 0;
|
|
2028
|
+
const messageYjsSyncStep2 = 1;
|
|
2029
|
+
const messageYjsUpdate = 2;
|
|
2030
|
+
|
|
2031
|
+
/**
|
|
2032
|
+
* Create a sync step 1 message based on the state of the current shared document.
|
|
2033
|
+
*
|
|
2034
|
+
* @param {encoding.Encoder} encoder
|
|
2035
|
+
* @param {Y.Doc} doc
|
|
2036
|
+
*/
|
|
2037
|
+
const writeSyncStep1 = (encoder, doc) => {
|
|
2038
|
+
writeVarUint(encoder, messageYjsSyncStep1);
|
|
2039
|
+
const sv = Y__namespace.encodeStateVector(doc);
|
|
2040
|
+
writeVarUint8Array(encoder, sv);
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* @param {encoding.Encoder} encoder
|
|
2045
|
+
* @param {Y.Doc} doc
|
|
2046
|
+
* @param {Uint8Array} [encodedStateVector]
|
|
2047
|
+
*/
|
|
2048
|
+
const writeSyncStep2 = (encoder, doc, encodedStateVector) => {
|
|
2049
|
+
writeVarUint(encoder, messageYjsSyncStep2);
|
|
2050
|
+
writeVarUint8Array(encoder, Y__namespace.encodeStateAsUpdate(doc, encodedStateVector));
|
|
2051
|
+
};
|
|
2052
|
+
|
|
2053
|
+
/**
|
|
2054
|
+
* Read SyncStep1 message and reply with SyncStep2.
|
|
2055
|
+
*
|
|
2056
|
+
* @param {decoding.Decoder} decoder The reply to the received message
|
|
2057
|
+
* @param {encoding.Encoder} encoder The received message
|
|
2058
|
+
* @param {Y.Doc} doc
|
|
2059
|
+
*/
|
|
2060
|
+
const readSyncStep1 = (decoder, encoder, doc) =>
|
|
1664
2061
|
writeSyncStep2(encoder, doc, readVarUint8Array(decoder));
|
|
1665
2062
|
|
|
1666
2063
|
/**
|
|
@@ -1719,559 +2116,184 @@ const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => {
|
|
|
1719
2116
|
throw new Error('Unknown message type')
|
|
1720
2117
|
}
|
|
1721
2118
|
return messageType
|
|
1722
|
-
};
|
|
1723
|
-
|
|
1724
|
-
exports.MessageType = void 0;
|
|
1725
|
-
(function (MessageType) {
|
|
1726
|
-
MessageType[MessageType["Sync"] = 0] = "Sync";
|
|
1727
|
-
MessageType[MessageType["Awareness"] = 1] = "Awareness";
|
|
1728
|
-
MessageType[MessageType["Auth"] = 2] = "Auth";
|
|
1729
|
-
MessageType[MessageType["QueryAwareness"] = 3] = "QueryAwareness";
|
|
1730
|
-
MessageType[MessageType["Stateless"] = 5] = "Stateless";
|
|
1731
|
-
MessageType[MessageType["CLOSE"] = 7] = "CLOSE";
|
|
1732
|
-
})(exports.MessageType || (exports.MessageType = {}));
|
|
1733
|
-
exports.WebSocketStatus = void 0;
|
|
1734
|
-
(function (WebSocketStatus) {
|
|
1735
|
-
WebSocketStatus["Connecting"] = "connecting";
|
|
1736
|
-
WebSocketStatus["Connected"] = "connected";
|
|
1737
|
-
WebSocketStatus["Disconnected"] = "disconnected";
|
|
1738
|
-
})(exports.WebSocketStatus || (exports.WebSocketStatus = {}));
|
|
1739
|
-
|
|
1740
|
-
class OutgoingMessage {
|
|
1741
|
-
constructor() {
|
|
1742
|
-
this.encoder = createEncoder();
|
|
1743
|
-
}
|
|
1744
|
-
get(args) {
|
|
1745
|
-
return args.encoder;
|
|
1746
|
-
}
|
|
1747
|
-
toUint8Array() {
|
|
1748
|
-
return toUint8Array(this.encoder);
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
class MessageReceiver {
|
|
1753
|
-
constructor(message) {
|
|
1754
|
-
this.broadcasted = false;
|
|
1755
|
-
this.message = message;
|
|
1756
|
-
}
|
|
1757
|
-
setBroadcasted(value) {
|
|
1758
|
-
this.broadcasted = value;
|
|
1759
|
-
return this;
|
|
1760
|
-
}
|
|
1761
|
-
apply(provider, emitSynced = true) {
|
|
1762
|
-
const { message } = this;
|
|
1763
|
-
const type = message.readVarUint();
|
|
1764
|
-
const emptyMessageLength = message.length();
|
|
1765
|
-
switch (type) {
|
|
1766
|
-
case exports.MessageType.Sync:
|
|
1767
|
-
this.applySyncMessage(provider, emitSynced);
|
|
1768
|
-
break;
|
|
1769
|
-
case exports.MessageType.Awareness:
|
|
1770
|
-
this.applyAwarenessMessage(provider);
|
|
1771
|
-
break;
|
|
1772
|
-
case exports.MessageType.Auth:
|
|
1773
|
-
this.applyAuthMessage(provider);
|
|
1774
|
-
break;
|
|
1775
|
-
case exports.MessageType.QueryAwareness:
|
|
1776
|
-
this.applyQueryAwarenessMessage(provider);
|
|
1777
|
-
break;
|
|
1778
|
-
case exports.MessageType.Stateless:
|
|
1779
|
-
provider.receiveStateless(readVarString(message.decoder));
|
|
1780
|
-
break;
|
|
1781
|
-
default:
|
|
1782
|
-
throw new Error(`Can’t apply message of unknown type: ${type}`);
|
|
1783
|
-
}
|
|
1784
|
-
// Reply
|
|
1785
|
-
if (message.length() > emptyMessageLength + 1) { // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage)
|
|
1786
|
-
if (this.broadcasted) {
|
|
1787
|
-
// TODO: Some weird TypeScript error
|
|
1788
|
-
// @ts-ignore
|
|
1789
|
-
provider.broadcast(OutgoingMessage, { encoder: message.encoder });
|
|
1790
|
-
}
|
|
1791
|
-
else {
|
|
1792
|
-
// TODO: Some weird TypeScript error
|
|
1793
|
-
// @ts-ignore
|
|
1794
|
-
provider.send(OutgoingMessage, { encoder: message.encoder });
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
applySyncMessage(provider, emitSynced) {
|
|
1799
|
-
const { message } = this;
|
|
1800
|
-
message.writeVarUint(exports.MessageType.Sync);
|
|
1801
|
-
// Apply update
|
|
1802
|
-
const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
|
|
1803
|
-
// Synced once we receive Step2
|
|
1804
|
-
if (emitSynced && syncMessageType === messageYjsSyncStep2) {
|
|
1805
|
-
provider.synced = true;
|
|
1806
|
-
}
|
|
1807
|
-
if (syncMessageType === messageYjsUpdate || syncMessageType === messageYjsSyncStep2) {
|
|
1808
|
-
if (provider.unsyncedChanges > 0) {
|
|
1809
|
-
provider.updateUnsyncedChanges(-1);
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
applyAwarenessMessage(provider) {
|
|
1814
|
-
const { message } = this;
|
|
1815
|
-
applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
|
|
1816
|
-
}
|
|
1817
|
-
applyAuthMessage(provider) {
|
|
1818
|
-
const { message } = this;
|
|
1819
|
-
common.readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
|
|
1820
|
-
}
|
|
1821
|
-
applyQueryAwarenessMessage(provider) {
|
|
1822
|
-
const { message } = this;
|
|
1823
|
-
message.writeVarUint(exports.MessageType.Awareness);
|
|
1824
|
-
message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
class MessageSender {
|
|
1829
|
-
constructor(Message, args = {}) {
|
|
1830
|
-
this.message = new Message();
|
|
1831
|
-
this.encoder = this.message.get(args);
|
|
1832
|
-
}
|
|
1833
|
-
create() {
|
|
1834
|
-
return toUint8Array(this.encoder);
|
|
1835
|
-
}
|
|
1836
|
-
send(webSocket) {
|
|
1837
|
-
webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
|
|
1838
|
-
}
|
|
1839
|
-
broadcast(channel) {
|
|
1840
|
-
publish(channel, this.create());
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
class SyncStepOneMessage extends OutgoingMessage {
|
|
1845
|
-
constructor() {
|
|
1846
|
-
super(...arguments);
|
|
1847
|
-
this.type = exports.MessageType.Sync;
|
|
1848
|
-
this.description = 'First sync step';
|
|
1849
|
-
}
|
|
1850
|
-
get(args) {
|
|
1851
|
-
if (typeof args.document === 'undefined') {
|
|
1852
|
-
throw new Error('The sync step one message requires document as an argument');
|
|
1853
|
-
}
|
|
1854
|
-
writeVarString(this.encoder, args.documentName);
|
|
1855
|
-
writeVarUint(this.encoder, this.type);
|
|
1856
|
-
writeSyncStep1(this.encoder, args.document);
|
|
1857
|
-
return this.encoder;
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
class SyncStepTwoMessage extends OutgoingMessage {
|
|
1862
|
-
constructor() {
|
|
1863
|
-
super(...arguments);
|
|
1864
|
-
this.type = exports.MessageType.Sync;
|
|
1865
|
-
this.description = 'Second sync step';
|
|
1866
|
-
}
|
|
1867
|
-
get(args) {
|
|
1868
|
-
if (typeof args.document === 'undefined') {
|
|
1869
|
-
throw new Error('The sync step two message requires document as an argument');
|
|
1870
|
-
}
|
|
1871
|
-
writeVarString(this.encoder, args.documentName);
|
|
1872
|
-
writeVarUint(this.encoder, this.type);
|
|
1873
|
-
writeSyncStep2(this.encoder, args.document);
|
|
1874
|
-
return this.encoder;
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
class QueryAwarenessMessage extends OutgoingMessage {
|
|
1879
|
-
constructor() {
|
|
1880
|
-
super(...arguments);
|
|
1881
|
-
this.type = exports.MessageType.QueryAwareness;
|
|
1882
|
-
this.description = 'Queries awareness states';
|
|
1883
|
-
}
|
|
1884
|
-
get(args) {
|
|
1885
|
-
console.log('queryAwareness: writing string docName', args.documentName);
|
|
1886
|
-
console.log(this.encoder.cpos);
|
|
1887
|
-
writeVarString(this.encoder, args.documentName);
|
|
1888
|
-
writeVarUint(this.encoder, this.type);
|
|
1889
|
-
return this.encoder;
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
class AuthenticationMessage extends OutgoingMessage {
|
|
1894
|
-
constructor() {
|
|
1895
|
-
super(...arguments);
|
|
1896
|
-
this.type = exports.MessageType.Auth;
|
|
1897
|
-
this.description = 'Authentication';
|
|
1898
|
-
}
|
|
1899
|
-
get(args) {
|
|
1900
|
-
if (typeof args.token === 'undefined') {
|
|
1901
|
-
throw new Error('The authentication message requires `token` as an argument.');
|
|
1902
|
-
}
|
|
1903
|
-
writeVarString(this.encoder, args.documentName);
|
|
1904
|
-
writeVarUint(this.encoder, this.type);
|
|
1905
|
-
common.writeAuthentication(this.encoder, args.token);
|
|
1906
|
-
return this.encoder;
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
class AwarenessMessage extends OutgoingMessage {
|
|
1911
|
-
constructor() {
|
|
1912
|
-
super(...arguments);
|
|
1913
|
-
this.type = exports.MessageType.Awareness;
|
|
1914
|
-
this.description = 'Awareness states update';
|
|
1915
|
-
}
|
|
1916
|
-
get(args) {
|
|
1917
|
-
if (typeof args.awareness === 'undefined') {
|
|
1918
|
-
throw new Error('The awareness message requires awareness as an argument');
|
|
1919
|
-
}
|
|
1920
|
-
if (typeof args.clients === 'undefined') {
|
|
1921
|
-
throw new Error('The awareness message requires clients as an argument');
|
|
1922
|
-
}
|
|
1923
|
-
writeVarString(this.encoder, args.documentName);
|
|
1924
|
-
writeVarUint(this.encoder, this.type);
|
|
1925
|
-
let awarenessUpdate;
|
|
1926
|
-
if (args.states === undefined) {
|
|
1927
|
-
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients);
|
|
1928
|
-
}
|
|
1929
|
-
else {
|
|
1930
|
-
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
|
|
1931
|
-
}
|
|
1932
|
-
writeVarUint8Array(this.encoder, awarenessUpdate);
|
|
1933
|
-
return this.encoder;
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
2119
|
+
};
|
|
1936
2120
|
|
|
1937
|
-
class
|
|
2121
|
+
class OutgoingMessage {
|
|
1938
2122
|
constructor() {
|
|
1939
|
-
|
|
1940
|
-
this.type = exports.MessageType.Sync;
|
|
1941
|
-
this.description = 'A document update';
|
|
2123
|
+
this.encoder = createEncoder();
|
|
1942
2124
|
}
|
|
1943
2125
|
get(args) {
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
return this.encoder;
|
|
2126
|
+
return args.encoder;
|
|
2127
|
+
}
|
|
2128
|
+
toUint8Array() {
|
|
2129
|
+
return toUint8Array(this.encoder);
|
|
1948
2130
|
}
|
|
1949
2131
|
}
|
|
1950
2132
|
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
*/
|
|
1956
|
-
|
|
1957
|
-
/**
|
|
1958
|
-
* @param {Object<string,string>} params
|
|
1959
|
-
* @return {string}
|
|
1960
|
-
*/
|
|
1961
|
-
const encodeQueryParams = params =>
|
|
1962
|
-
map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
|
|
1963
|
-
|
|
1964
|
-
class HocuspocusProviderWebsocket extends EventEmitter {
|
|
1965
|
-
constructor(configuration) {
|
|
1966
|
-
super();
|
|
1967
|
-
this.configuration = {
|
|
1968
|
-
url: '',
|
|
1969
|
-
// @ts-ignore
|
|
1970
|
-
document: undefined,
|
|
1971
|
-
// @ts-ignore
|
|
1972
|
-
awareness: undefined,
|
|
1973
|
-
WebSocketPolyfill: undefined,
|
|
1974
|
-
parameters: {},
|
|
1975
|
-
connect: true,
|
|
1976
|
-
broadcast: true,
|
|
1977
|
-
forceSyncInterval: false,
|
|
1978
|
-
// TODO: this should depend on awareness.outdatedTime
|
|
1979
|
-
messageReconnectTimeout: 30000,
|
|
1980
|
-
// 1 second
|
|
1981
|
-
delay: 1000,
|
|
1982
|
-
// instant
|
|
1983
|
-
initialDelay: 0,
|
|
1984
|
-
// double the delay each time
|
|
1985
|
-
factor: 2,
|
|
1986
|
-
// unlimited retries
|
|
1987
|
-
maxAttempts: 0,
|
|
1988
|
-
// wait at least 1 second
|
|
1989
|
-
minDelay: 1000,
|
|
1990
|
-
// at least every 30 seconds
|
|
1991
|
-
maxDelay: 30000,
|
|
1992
|
-
// randomize
|
|
1993
|
-
jitter: true,
|
|
1994
|
-
// retry forever
|
|
1995
|
-
timeout: 0,
|
|
1996
|
-
onOpen: () => null,
|
|
1997
|
-
onConnect: () => null,
|
|
1998
|
-
onMessage: () => null,
|
|
1999
|
-
onOutgoingMessage: () => null,
|
|
2000
|
-
onStatus: () => null,
|
|
2001
|
-
onDisconnect: () => null,
|
|
2002
|
-
onClose: () => null,
|
|
2003
|
-
onDestroy: () => null,
|
|
2004
|
-
onAwarenessUpdate: () => null,
|
|
2005
|
-
onAwarenessChange: () => null,
|
|
2006
|
-
quiet: false,
|
|
2007
|
-
};
|
|
2008
|
-
this.subscribedToBroadcastChannel = false;
|
|
2009
|
-
this.webSocket = null;
|
|
2010
|
-
this.shouldConnect = true;
|
|
2011
|
-
this.status = exports.WebSocketStatus.Disconnected;
|
|
2012
|
-
this.lastMessageReceived = 0;
|
|
2013
|
-
this.mux = createMutex();
|
|
2014
|
-
this.intervals = {
|
|
2015
|
-
forceSync: null,
|
|
2016
|
-
connectionChecker: null,
|
|
2017
|
-
};
|
|
2018
|
-
this.connectionAttempt = null;
|
|
2019
|
-
this.receivedOnOpenPayload = undefined;
|
|
2020
|
-
this.receivedOnStatusPayload = undefined;
|
|
2021
|
-
this.boundConnect = this.connect.bind(this);
|
|
2022
|
-
this.setConfiguration(configuration);
|
|
2023
|
-
this.configuration.WebSocketPolyfill = configuration.WebSocketPolyfill ? configuration.WebSocketPolyfill : WebSocket;
|
|
2024
|
-
this.on('open', this.configuration.onOpen);
|
|
2025
|
-
this.on('open', this.onOpen.bind(this));
|
|
2026
|
-
this.on('connect', this.configuration.onConnect);
|
|
2027
|
-
this.on('message', this.configuration.onMessage);
|
|
2028
|
-
this.on('outgoingMessage', this.configuration.onOutgoingMessage);
|
|
2029
|
-
this.on('status', this.configuration.onStatus);
|
|
2030
|
-
this.on('status', this.onStatus.bind(this));
|
|
2031
|
-
this.on('disconnect', this.configuration.onDisconnect);
|
|
2032
|
-
this.on('close', this.configuration.onClose);
|
|
2033
|
-
this.on('destroy', this.configuration.onDestroy);
|
|
2034
|
-
this.on('awarenessUpdate', this.configuration.onAwarenessUpdate);
|
|
2035
|
-
this.on('awarenessChange', this.configuration.onAwarenessChange);
|
|
2036
|
-
this.on('close', this.onClose.bind(this));
|
|
2037
|
-
this.on('message', this.onMessage.bind(this));
|
|
2038
|
-
this.registerEventListeners();
|
|
2039
|
-
this.intervals.connectionChecker = setInterval(this.checkConnection.bind(this), this.configuration.messageReconnectTimeout / 10);
|
|
2040
|
-
if (typeof configuration.connect !== 'undefined') {
|
|
2041
|
-
this.shouldConnect = configuration.connect;
|
|
2042
|
-
}
|
|
2043
|
-
if (!this.shouldConnect) {
|
|
2044
|
-
return;
|
|
2045
|
-
}
|
|
2046
|
-
this.connect();
|
|
2047
|
-
}
|
|
2048
|
-
async onOpen(event) {
|
|
2049
|
-
this.receivedOnOpenPayload = event;
|
|
2133
|
+
class MessageReceiver {
|
|
2134
|
+
constructor(message) {
|
|
2135
|
+
this.broadcasted = false;
|
|
2136
|
+
this.message = message;
|
|
2050
2137
|
}
|
|
2051
|
-
|
|
2052
|
-
this.
|
|
2138
|
+
setBroadcasted(value) {
|
|
2139
|
+
this.broadcasted = value;
|
|
2140
|
+
return this;
|
|
2053
2141
|
}
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2142
|
+
apply(provider, emitSynced) {
|
|
2143
|
+
const { message } = this;
|
|
2144
|
+
const type = message.readVarUint();
|
|
2145
|
+
const emptyMessageLength = message.length();
|
|
2146
|
+
switch (type) {
|
|
2147
|
+
case exports.MessageType.Sync:
|
|
2148
|
+
this.applySyncMessage(provider, emitSynced);
|
|
2149
|
+
break;
|
|
2150
|
+
case exports.MessageType.Awareness:
|
|
2151
|
+
this.applyAwarenessMessage(provider);
|
|
2152
|
+
break;
|
|
2153
|
+
case exports.MessageType.Auth:
|
|
2154
|
+
this.applyAuthMessage(provider);
|
|
2155
|
+
break;
|
|
2156
|
+
case exports.MessageType.QueryAwareness:
|
|
2157
|
+
this.applyQueryAwarenessMessage(provider);
|
|
2158
|
+
break;
|
|
2159
|
+
case exports.MessageType.Stateless:
|
|
2160
|
+
provider.receiveStateless(readVarString(message.decoder));
|
|
2161
|
+
break;
|
|
2162
|
+
case exports.MessageType.SyncStatus:
|
|
2163
|
+
this.applySyncStatusMessage(provider, readVarInt(message.decoder) === 1);
|
|
2164
|
+
break;
|
|
2165
|
+
default:
|
|
2166
|
+
throw new Error(`Can’t apply message of unknown type: ${type}`);
|
|
2057
2167
|
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2168
|
+
// Reply
|
|
2169
|
+
if (message.length() > emptyMessageLength + 1) { // length of documentName (considered in emptyMessageLength plus length of yjs sync type, set in applySyncMessage)
|
|
2170
|
+
if (this.broadcasted) {
|
|
2171
|
+
// TODO: Some weird TypeScript error
|
|
2172
|
+
// @ts-ignore
|
|
2173
|
+
provider.broadcast(OutgoingMessage, { encoder: message.encoder });
|
|
2174
|
+
}
|
|
2175
|
+
else {
|
|
2176
|
+
// TODO: Some weird TypeScript error
|
|
2177
|
+
// @ts-ignore
|
|
2178
|
+
provider.send(OutgoingMessage, { encoder: message.encoder });
|
|
2179
|
+
}
|
|
2060
2180
|
}
|
|
2061
2181
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
return;
|
|
2071
|
-
}
|
|
2072
|
-
// Always cancel any previously initiated connection retryer instances
|
|
2073
|
-
if (this.cancelWebsocketRetry) {
|
|
2074
|
-
this.cancelWebsocketRetry();
|
|
2075
|
-
this.cancelWebsocketRetry = undefined;
|
|
2182
|
+
applySyncMessage(provider, emitSynced) {
|
|
2183
|
+
const { message } = this;
|
|
2184
|
+
message.writeVarUint(exports.MessageType.Sync);
|
|
2185
|
+
// Apply update
|
|
2186
|
+
const syncMessageType = readSyncMessage(message.decoder, message.encoder, provider.document, provider);
|
|
2187
|
+
// Synced once we receive Step2
|
|
2188
|
+
if (emitSynced && syncMessageType === messageYjsSyncStep2) {
|
|
2189
|
+
provider.synced = true;
|
|
2076
2190
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
delay: this.configuration.delay,
|
|
2082
|
-
initialDelay: this.configuration.initialDelay,
|
|
2083
|
-
factor: this.configuration.factor,
|
|
2084
|
-
maxAttempts: this.configuration.maxAttempts,
|
|
2085
|
-
minDelay: this.configuration.minDelay,
|
|
2086
|
-
maxDelay: this.configuration.maxDelay,
|
|
2087
|
-
jitter: this.configuration.jitter,
|
|
2088
|
-
timeout: this.configuration.timeout,
|
|
2089
|
-
beforeAttempt: context => {
|
|
2090
|
-
if (!this.shouldConnect || cancelAttempt) {
|
|
2091
|
-
context.abort();
|
|
2092
|
-
}
|
|
2093
|
-
},
|
|
2094
|
-
}).catch((error) => {
|
|
2095
|
-
// If we aborted the connection attempt then don’t throw an error
|
|
2096
|
-
// ref: https://github.com/lifeomic/attempt/blob/master/src/index.ts#L136
|
|
2097
|
-
if (error && error.code !== 'ATTEMPT_ABORTED') {
|
|
2098
|
-
throw error;
|
|
2099
|
-
}
|
|
2100
|
-
});
|
|
2101
|
-
return {
|
|
2102
|
-
retryPromise,
|
|
2103
|
-
cancelFunc: () => {
|
|
2104
|
-
cancelAttempt = true;
|
|
2105
|
-
},
|
|
2106
|
-
};
|
|
2107
|
-
};
|
|
2108
|
-
const { retryPromise, cancelFunc } = abortableRetry();
|
|
2109
|
-
this.cancelWebsocketRetry = cancelFunc;
|
|
2110
|
-
return retryPromise;
|
|
2111
|
-
}
|
|
2112
|
-
createWebSocketConnection() {
|
|
2113
|
-
return new Promise((resolve, reject) => {
|
|
2114
|
-
if (this.webSocket) {
|
|
2115
|
-
this.webSocket.close();
|
|
2116
|
-
this.webSocket = null;
|
|
2117
|
-
}
|
|
2118
|
-
// Init the WebSocket connection
|
|
2119
|
-
const ws = new this.configuration.WebSocketPolyfill(this.url);
|
|
2120
|
-
ws.binaryType = 'arraybuffer';
|
|
2121
|
-
ws.onmessage = (payload) => this.emit('message', payload);
|
|
2122
|
-
ws.onclose = (payload) => this.emit('close', { event: payload });
|
|
2123
|
-
ws.onopen = (payload) => this.emit('open', payload);
|
|
2124
|
-
ws.onerror = (err) => {
|
|
2125
|
-
reject(err);
|
|
2126
|
-
};
|
|
2127
|
-
this.webSocket = ws;
|
|
2128
|
-
// Reset the status
|
|
2129
|
-
this.status = exports.WebSocketStatus.Connecting;
|
|
2130
|
-
this.emit('status', { status: exports.WebSocketStatus.Connecting });
|
|
2131
|
-
// Store resolve/reject for later use
|
|
2132
|
-
this.connectionAttempt = {
|
|
2133
|
-
resolve,
|
|
2134
|
-
reject,
|
|
2135
|
-
};
|
|
2136
|
-
});
|
|
2137
|
-
}
|
|
2138
|
-
onMessage(event) {
|
|
2139
|
-
this.resolveConnectionAttempt();
|
|
2140
|
-
this.lastMessageReceived = getUnixTime();
|
|
2141
|
-
}
|
|
2142
|
-
resolveConnectionAttempt() {
|
|
2143
|
-
if (this.connectionAttempt) {
|
|
2144
|
-
this.connectionAttempt.resolve();
|
|
2145
|
-
this.connectionAttempt = null;
|
|
2146
|
-
this.status = exports.WebSocketStatus.Connected;
|
|
2147
|
-
this.emit('status', { status: exports.WebSocketStatus.Connected });
|
|
2148
|
-
this.emit('connect');
|
|
2191
|
+
}
|
|
2192
|
+
applySyncStatusMessage(provider, applied) {
|
|
2193
|
+
if (applied) {
|
|
2194
|
+
provider.decrementUnsyncedChanges();
|
|
2149
2195
|
}
|
|
2150
2196
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2197
|
+
applyAwarenessMessage(provider) {
|
|
2198
|
+
const { message } = this;
|
|
2199
|
+
applyAwarenessUpdate(provider.awareness, message.readVarUint8Array(), provider);
|
|
2153
2200
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
(
|
|
2157
|
-
this.connectionAttempt = null;
|
|
2201
|
+
applyAuthMessage(provider) {
|
|
2202
|
+
const { message } = this;
|
|
2203
|
+
common.readAuthMessage(message.decoder, provider.permissionDeniedHandler.bind(provider), provider.authenticatedHandler.bind(provider));
|
|
2158
2204
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
return;
|
|
2164
|
-
}
|
|
2165
|
-
// Don’t close then connection while waiting for the first message
|
|
2166
|
-
if (!this.lastMessageReceived) {
|
|
2167
|
-
return;
|
|
2168
|
-
}
|
|
2169
|
-
// Don’t close the connection when a message was received recently
|
|
2170
|
-
if (this.configuration.messageReconnectTimeout >= getUnixTime() - this.lastMessageReceived) {
|
|
2171
|
-
return;
|
|
2172
|
-
}
|
|
2173
|
-
// No message received in a long time, not even your own
|
|
2174
|
-
// Awareness updates, which are updated every 15 seconds.
|
|
2175
|
-
(_a = this.webSocket) === null || _a === void 0 ? void 0 : _a.close();
|
|
2205
|
+
applyQueryAwarenessMessage(provider) {
|
|
2206
|
+
const { message } = this;
|
|
2207
|
+
message.writeVarUint(exports.MessageType.Awareness);
|
|
2208
|
+
message.writeVarUint8Array(encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
|
|
2176
2209
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
class MessageSender {
|
|
2213
|
+
constructor(Message, args = {}) {
|
|
2214
|
+
this.message = new Message();
|
|
2215
|
+
this.encoder = this.message.get(args);
|
|
2182
2216
|
}
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
while (this.configuration.url[this.configuration.url.length - 1] === '/') {
|
|
2186
|
-
return this.configuration.url.slice(0, this.configuration.url.length - 1);
|
|
2187
|
-
}
|
|
2188
|
-
return this.configuration.url;
|
|
2217
|
+
create() {
|
|
2218
|
+
return toUint8Array(this.encoder);
|
|
2189
2219
|
}
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
return `${this.serverUrl}${encodedParams.length === 0 ? '' : `?${encodedParams}`}`;
|
|
2220
|
+
send(webSocket) {
|
|
2221
|
+
webSocket === null || webSocket === void 0 ? void 0 : webSocket.send(this.create());
|
|
2193
2222
|
}
|
|
2194
|
-
|
|
2195
|
-
this.
|
|
2196
|
-
if (this.webSocket === null) {
|
|
2197
|
-
return;
|
|
2198
|
-
}
|
|
2199
|
-
try {
|
|
2200
|
-
this.webSocket.close();
|
|
2201
|
-
}
|
|
2202
|
-
catch {
|
|
2203
|
-
//
|
|
2204
|
-
}
|
|
2223
|
+
broadcast(channel) {
|
|
2224
|
+
publish(channel, this.create());
|
|
2205
2225
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
class AuthenticationMessage extends OutgoingMessage {
|
|
2229
|
+
constructor() {
|
|
2230
|
+
super(...arguments);
|
|
2231
|
+
this.type = exports.MessageType.Auth;
|
|
2232
|
+
this.description = 'Authentication';
|
|
2211
2233
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
this.status = exports.WebSocketStatus.Disconnected;
|
|
2216
|
-
this.emit('status', { status: exports.WebSocketStatus.Disconnected });
|
|
2217
|
-
this.emit('disconnect', { event });
|
|
2218
|
-
}
|
|
2219
|
-
if (event.code === common.Unauthorized.code) {
|
|
2220
|
-
if (event.reason === common.Unauthorized.reason) {
|
|
2221
|
-
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.');
|
|
2222
|
-
}
|
|
2223
|
-
else {
|
|
2224
|
-
console.warn(`[HocuspocusProvider] Connection closed with status Unauthorized: ${event.reason}`);
|
|
2225
|
-
}
|
|
2226
|
-
this.shouldConnect = false;
|
|
2227
|
-
}
|
|
2228
|
-
if (event.code === common.Forbidden.code) {
|
|
2229
|
-
if (!this.configuration.quiet) {
|
|
2230
|
-
console.warn('[HocuspocusProvider] The provided authentication token isn’t allowed to connect to this server. Will try again.');
|
|
2231
|
-
return; // TODO REMOVE ME
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
if (event.code === common.MessageTooBig.code) {
|
|
2235
|
-
console.warn(`[HocuspocusProvider] Connection closed with status MessageTooBig: ${event.reason}`);
|
|
2236
|
-
this.shouldConnect = false;
|
|
2234
|
+
get(args) {
|
|
2235
|
+
if (typeof args.token === 'undefined') {
|
|
2236
|
+
throw new Error('The authentication message requires `token` as an argument.');
|
|
2237
2237
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2238
|
+
writeVarString(this.encoder, args.documentName);
|
|
2239
|
+
writeVarUint(this.encoder, this.type);
|
|
2240
|
+
common.writeAuthentication(this.encoder, args.token);
|
|
2241
|
+
return this.encoder;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
class AwarenessMessage extends OutgoingMessage {
|
|
2246
|
+
constructor() {
|
|
2247
|
+
super(...arguments);
|
|
2248
|
+
this.type = exports.MessageType.Awareness;
|
|
2249
|
+
this.description = 'Awareness states update';
|
|
2250
|
+
}
|
|
2251
|
+
get(args) {
|
|
2252
|
+
if (typeof args.awareness === 'undefined') {
|
|
2253
|
+
throw new Error('The awareness message requires awareness as an argument');
|
|
2241
2254
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
this.connect();
|
|
2255
|
+
if (typeof args.clients === 'undefined') {
|
|
2256
|
+
throw new Error('The awareness message requires clients as an argument');
|
|
2245
2257
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2258
|
+
writeVarString(this.encoder, args.documentName);
|
|
2259
|
+
writeVarUint(this.encoder, this.type);
|
|
2260
|
+
let awarenessUpdate;
|
|
2261
|
+
if (args.states === undefined) {
|
|
2262
|
+
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients);
|
|
2249
2263
|
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
return;
|
|
2264
|
+
else {
|
|
2265
|
+
awarenessUpdate = encodeAwarenessUpdate(args.awareness, args.clients, args.states);
|
|
2253
2266
|
}
|
|
2254
|
-
|
|
2255
|
-
this.
|
|
2256
|
-
this.emit('status', { status: exports.WebSocketStatus.Disconnected });
|
|
2257
|
-
this.emit('disconnect', { event });
|
|
2267
|
+
writeVarUint8Array(this.encoder, awarenessUpdate);
|
|
2268
|
+
return this.encoder;
|
|
2258
2269
|
}
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
this.
|
|
2269
|
-
this.
|
|
2270
|
-
this.
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
class CloseMessage extends OutgoingMessage {
|
|
2273
|
+
constructor() {
|
|
2274
|
+
super(...arguments);
|
|
2275
|
+
this.type = exports.MessageType.CLOSE;
|
|
2276
|
+
this.description = 'Ask the server to close the connection';
|
|
2277
|
+
}
|
|
2278
|
+
get(args) {
|
|
2279
|
+
writeVarString(this.encoder, args.documentName);
|
|
2280
|
+
writeVarUint(this.encoder, this.type);
|
|
2281
|
+
return this.encoder;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
class QueryAwarenessMessage extends OutgoingMessage {
|
|
2286
|
+
constructor() {
|
|
2287
|
+
super(...arguments);
|
|
2288
|
+
this.type = exports.MessageType.QueryAwareness;
|
|
2289
|
+
this.description = 'Queries awareness states';
|
|
2290
|
+
}
|
|
2291
|
+
get(args) {
|
|
2292
|
+
console.log('queryAwareness: writing string docName', args.documentName);
|
|
2293
|
+
console.log(this.encoder.cpos);
|
|
2294
|
+
writeVarString(this.encoder, args.documentName);
|
|
2295
|
+
writeVarUint(this.encoder, this.type);
|
|
2296
|
+
return this.encoder;
|
|
2275
2297
|
}
|
|
2276
2298
|
}
|
|
2277
2299
|
|
|
@@ -2290,15 +2312,50 @@ class StatelessMessage extends OutgoingMessage {
|
|
|
2290
2312
|
}
|
|
2291
2313
|
}
|
|
2292
2314
|
|
|
2293
|
-
class
|
|
2315
|
+
class SyncStepOneMessage extends OutgoingMessage {
|
|
2294
2316
|
constructor() {
|
|
2295
2317
|
super(...arguments);
|
|
2296
|
-
this.type = exports.MessageType.
|
|
2297
|
-
this.description = '
|
|
2318
|
+
this.type = exports.MessageType.Sync;
|
|
2319
|
+
this.description = 'First sync step';
|
|
2320
|
+
}
|
|
2321
|
+
get(args) {
|
|
2322
|
+
if (typeof args.document === 'undefined') {
|
|
2323
|
+
throw new Error('The sync step one message requires document as an argument');
|
|
2324
|
+
}
|
|
2325
|
+
writeVarString(this.encoder, args.documentName);
|
|
2326
|
+
writeVarUint(this.encoder, this.type);
|
|
2327
|
+
writeSyncStep1(this.encoder, args.document);
|
|
2328
|
+
return this.encoder;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
class SyncStepTwoMessage extends OutgoingMessage {
|
|
2333
|
+
constructor() {
|
|
2334
|
+
super(...arguments);
|
|
2335
|
+
this.type = exports.MessageType.Sync;
|
|
2336
|
+
this.description = 'Second sync step';
|
|
2337
|
+
}
|
|
2338
|
+
get(args) {
|
|
2339
|
+
if (typeof args.document === 'undefined') {
|
|
2340
|
+
throw new Error('The sync step two message requires document as an argument');
|
|
2341
|
+
}
|
|
2342
|
+
writeVarString(this.encoder, args.documentName);
|
|
2343
|
+
writeVarUint(this.encoder, this.type);
|
|
2344
|
+
writeSyncStep2(this.encoder, args.document);
|
|
2345
|
+
return this.encoder;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
class UpdateMessage extends OutgoingMessage {
|
|
2350
|
+
constructor() {
|
|
2351
|
+
super(...arguments);
|
|
2352
|
+
this.type = exports.MessageType.Sync;
|
|
2353
|
+
this.description = 'A document update';
|
|
2298
2354
|
}
|
|
2299
2355
|
get(args) {
|
|
2300
2356
|
writeVarString(this.encoder, args.documentName);
|
|
2301
2357
|
writeVarUint(this.encoder, this.type);
|
|
2358
|
+
writeUpdate(this.encoder, args.update);
|
|
2302
2359
|
return this.encoder;
|
|
2303
2360
|
}
|
|
2304
2361
|
}
|
|
@@ -2331,6 +2388,8 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2331
2388
|
onAwarenessChange: () => null,
|
|
2332
2389
|
onStateless: () => null,
|
|
2333
2390
|
quiet: false,
|
|
2391
|
+
connect: true,
|
|
2392
|
+
preserveConnection: true,
|
|
2334
2393
|
};
|
|
2335
2394
|
this.subscribedToBroadcastChannel = false;
|
|
2336
2395
|
this.isSynced = false;
|
|
@@ -2344,7 +2403,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2344
2403
|
};
|
|
2345
2404
|
this.isConnected = true;
|
|
2346
2405
|
this.boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this);
|
|
2347
|
-
this.
|
|
2406
|
+
this.boundPageUnload = this.pageUnload.bind(this);
|
|
2348
2407
|
this.boundOnOpen = this.onOpen.bind(this);
|
|
2349
2408
|
this.boundOnMessage = this.onMessage.bind(this);
|
|
2350
2409
|
this.boundOnClose = this.onClose.bind(this);
|
|
@@ -2404,6 +2463,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2404
2463
|
const websocketProviderConfig = configuration;
|
|
2405
2464
|
this.configuration.websocketProvider = new HocuspocusProviderWebsocket({
|
|
2406
2465
|
url: websocketProviderConfig.url,
|
|
2466
|
+
connect: websocketProviderConfig.connect,
|
|
2407
2467
|
parameters: websocketProviderConfig.parameters,
|
|
2408
2468
|
});
|
|
2409
2469
|
}
|
|
@@ -2418,21 +2478,28 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2418
2478
|
get hasUnsyncedChanges() {
|
|
2419
2479
|
return this.unsyncedChanges > 0;
|
|
2420
2480
|
}
|
|
2421
|
-
|
|
2422
|
-
this.unsyncedChanges +=
|
|
2481
|
+
incrementUnsyncedChanges() {
|
|
2482
|
+
this.unsyncedChanges += 1;
|
|
2483
|
+
this.emit('unsyncedChanges', this.unsyncedChanges);
|
|
2484
|
+
}
|
|
2485
|
+
decrementUnsyncedChanges() {
|
|
2486
|
+
this.unsyncedChanges -= 1;
|
|
2487
|
+
if (this.unsyncedChanges === 0) {
|
|
2488
|
+
this.synced = true;
|
|
2489
|
+
}
|
|
2423
2490
|
this.emit('unsyncedChanges', this.unsyncedChanges);
|
|
2424
2491
|
}
|
|
2425
2492
|
forceSync() {
|
|
2426
2493
|
this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
|
|
2427
2494
|
}
|
|
2428
|
-
|
|
2495
|
+
pageUnload() {
|
|
2429
2496
|
removeAwarenessStates(this.awareness, [this.document.clientID], 'window unload');
|
|
2430
2497
|
}
|
|
2431
2498
|
registerEventListeners() {
|
|
2432
2499
|
if (typeof window === 'undefined') {
|
|
2433
2500
|
return;
|
|
2434
2501
|
}
|
|
2435
|
-
window.addEventListener('
|
|
2502
|
+
window.addEventListener('unload', this.boundPageUnload);
|
|
2436
2503
|
}
|
|
2437
2504
|
sendStateless(payload) {
|
|
2438
2505
|
this.send(StatelessMessage, { documentName: this.configuration.name, payload });
|
|
@@ -2441,7 +2508,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2441
2508
|
if (origin === this) {
|
|
2442
2509
|
return;
|
|
2443
2510
|
}
|
|
2444
|
-
this.
|
|
2511
|
+
this.incrementUnsyncedChanges();
|
|
2445
2512
|
this.send(UpdateMessage, { update, documentName: this.configuration.name }, true);
|
|
2446
2513
|
}
|
|
2447
2514
|
awarenessUpdateHandler({ added, updated, removed }, origin) {
|
|
@@ -2452,6 +2519,12 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2452
2519
|
documentName: this.configuration.name,
|
|
2453
2520
|
}, true);
|
|
2454
2521
|
}
|
|
2522
|
+
/**
|
|
2523
|
+
* Indicates whether a first handshake with the server has been established
|
|
2524
|
+
*
|
|
2525
|
+
* Note: this does not mean all updates from the client have been persisted to the backend. For this,
|
|
2526
|
+
* use `hasUnsyncedChanges`.
|
|
2527
|
+
*/
|
|
2455
2528
|
get synced() {
|
|
2456
2529
|
return this.isSynced;
|
|
2457
2530
|
}
|
|
@@ -2459,9 +2532,6 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2459
2532
|
if (this.isSynced === state) {
|
|
2460
2533
|
return;
|
|
2461
2534
|
}
|
|
2462
|
-
if (state && this.unsyncedChanges > 0) {
|
|
2463
|
-
this.updateUnsyncedChanges(-1 * this.unsyncedChanges);
|
|
2464
|
-
}
|
|
2465
2535
|
this.isSynced = state;
|
|
2466
2536
|
this.emit('synced', { state });
|
|
2467
2537
|
this.emit('sync', { state });
|
|
@@ -2479,6 +2549,9 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2479
2549
|
disconnect() {
|
|
2480
2550
|
this.disconnectBroadcastChannel();
|
|
2481
2551
|
this.configuration.websocketProvider.detach(this);
|
|
2552
|
+
if (!this.configuration.preserveConnection) {
|
|
2553
|
+
this.configuration.websocketProvider.disconnect();
|
|
2554
|
+
}
|
|
2482
2555
|
}
|
|
2483
2556
|
async onOpen(event) {
|
|
2484
2557
|
this.isAuthenticated = false;
|
|
@@ -2499,6 +2572,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2499
2572
|
return this.configuration.token;
|
|
2500
2573
|
}
|
|
2501
2574
|
startSync() {
|
|
2575
|
+
this.incrementUnsyncedChanges();
|
|
2502
2576
|
this.send(SyncStepOneMessage, { document: this.document, documentName: this.configuration.name });
|
|
2503
2577
|
if (this.awareness.getLocalState() !== null) {
|
|
2504
2578
|
this.send(AwarenessMessage, {
|
|
@@ -2509,8 +2583,9 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2509
2583
|
}
|
|
2510
2584
|
}
|
|
2511
2585
|
send(message, args, broadcast = false) {
|
|
2512
|
-
if (!this.isConnected)
|
|
2586
|
+
if (!this.isConnected) {
|
|
2513
2587
|
return;
|
|
2588
|
+
}
|
|
2514
2589
|
if (broadcast) {
|
|
2515
2590
|
this.mux(() => { this.broadcast(message, args); });
|
|
2516
2591
|
}
|
|
@@ -2526,7 +2601,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2526
2601
|
}
|
|
2527
2602
|
message.writeVarString(documentName);
|
|
2528
2603
|
this.emit('message', { event, message: new IncomingMessage(event.data) });
|
|
2529
|
-
new MessageReceiver(message).apply(this);
|
|
2604
|
+
new MessageReceiver(message).apply(this, true);
|
|
2530
2605
|
}
|
|
2531
2606
|
onClose(event) {
|
|
2532
2607
|
this.isAuthenticated = false;
|
|
@@ -2562,7 +2637,7 @@ class HocuspocusProvider extends EventEmitter {
|
|
|
2562
2637
|
if (typeof window === 'undefined') {
|
|
2563
2638
|
return;
|
|
2564
2639
|
}
|
|
2565
|
-
window.removeEventListener('
|
|
2640
|
+
window.removeEventListener('unload', this.boundPageUnload);
|
|
2566
2641
|
}
|
|
2567
2642
|
permissionDeniedHandler(reason) {
|
|
2568
2643
|
this.emit('authenticationFailed', { reason });
|
|
@@ -2643,6 +2718,39 @@ class TiptapCollabProvider extends HocuspocusProvider {
|
|
|
2643
2718
|
configuration.token = 'notoken'; // need to send a token anyway (which will be ignored)
|
|
2644
2719
|
}
|
|
2645
2720
|
super(configuration);
|
|
2721
|
+
this.tiptapCollabConfigurationPrefix = '__tiptapcollab__';
|
|
2722
|
+
}
|
|
2723
|
+
createVersion(name) {
|
|
2724
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2725
|
+
return this.sendStateless(JSON.stringify({ action: 'version.create', name }));
|
|
2726
|
+
}
|
|
2727
|
+
revertToVersion(targetVersion) {
|
|
2728
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2729
|
+
return this.sendStateless(JSON.stringify({ action: 'version.revert', version: targetVersion }));
|
|
2730
|
+
}
|
|
2731
|
+
getVersions() {
|
|
2732
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2733
|
+
return this.configuration.document.getArray(`${this.tiptapCollabConfigurationPrefix}versions`).toArray();
|
|
2734
|
+
}
|
|
2735
|
+
watchVersions(callback) {
|
|
2736
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2737
|
+
return this.configuration.document.getArray('__tiptapcollab__versions').observe(callback);
|
|
2738
|
+
}
|
|
2739
|
+
unwatchVersions(callback) {
|
|
2740
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2741
|
+
return this.configuration.document.getArray('__tiptapcollab__versions').unobserve(callback);
|
|
2742
|
+
}
|
|
2743
|
+
isAutoVersioning() {
|
|
2744
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2745
|
+
return !!this.configuration.document.getMap(`${this.tiptapCollabConfigurationPrefix}config`).get('autoVersioning');
|
|
2746
|
+
}
|
|
2747
|
+
enableAutoVersioning() {
|
|
2748
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2749
|
+
return this.configuration.document.getMap(`${this.tiptapCollabConfigurationPrefix}config`).set('autoVersioning', 1);
|
|
2750
|
+
}
|
|
2751
|
+
disableAutoVersioning() {
|
|
2752
|
+
console.error('This doesnt work yet! If you want to join as a beta tester, send an email to humans@tiptap.dev');
|
|
2753
|
+
return this.configuration.document.getMap(`${this.tiptapCollabConfigurationPrefix}config`).set('autoVersioning', 0);
|
|
2646
2754
|
}
|
|
2647
2755
|
}
|
|
2648
2756
|
|