@verdant-web/server 2.1.3 → 3.0.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/esm/ClientConnection.d.ts +1 -0
- package/dist/esm/ClientConnection.js.map +1 -1
- package/dist/esm/Presence.js.map +1 -1
- package/dist/esm/Profiles.js.map +1 -1
- package/dist/esm/ReplicaKeepaliveTimers.js.map +1 -1
- package/dist/esm/Server.d.ts +15 -11
- package/dist/esm/Server.js +29 -30
- package/dist/esm/Server.js.map +1 -1
- package/dist/esm/ServerLibrary.d.ts +10 -17
- package/dist/esm/ServerLibrary.js +151 -127
- package/dist/esm/ServerLibrary.js.map +1 -1
- package/dist/esm/ServerLibrary.test.js +369 -0
- package/dist/esm/ServerLibrary.test.js.map +1 -0
- package/dist/esm/TokenVerifier.js.map +1 -1
- package/dist/esm/files/FileStorage.js.map +1 -1
- package/dist/esm/storage/Storage.d.ts +67 -0
- package/dist/esm/storage/Storage.js +2 -0
- package/dist/esm/storage/Storage.js.map +1 -0
- package/dist/esm/storage/index.d.ts +2 -0
- package/dist/esm/storage/index.js +3 -0
- package/dist/esm/storage/index.js.map +1 -0
- package/dist/esm/storage/sql/SqlBaselines.d.ts +22 -0
- package/dist/esm/storage/sql/SqlBaselines.js +101 -0
- package/dist/esm/storage/sql/SqlBaselines.js.map +1 -0
- package/dist/esm/storage/sql/SqlFileMetadata.d.ts +18 -0
- package/dist/esm/storage/sql/SqlFileMetadata.js +69 -0
- package/dist/esm/storage/sql/SqlFileMetadata.js.map +1 -0
- package/dist/esm/storage/sql/SqlOperations.d.ts +19 -0
- package/dist/esm/storage/sql/SqlOperations.js +104 -0
- package/dist/esm/storage/sql/SqlOperations.js.map +1 -0
- package/dist/esm/storage/sql/SqlReplicas.d.ts +32 -0
- package/dist/esm/storage/sql/SqlReplicas.js +163 -0
- package/dist/esm/storage/sql/SqlReplicas.js.map +1 -0
- package/dist/esm/storage/sql/migrations/v1.d.ts +3 -0
- package/dist/esm/storage/sql/migrations/v1.js +59 -0
- package/dist/esm/storage/sql/migrations/v1.js.map +1 -0
- package/dist/esm/storage/sql/migrations.d.ts +5 -0
- package/dist/esm/storage/sql/migrations.js +3 -0
- package/dist/esm/storage/sql/migrations.js.map +1 -0
- package/dist/esm/storage/sql/sqlStorage.d.ts +5 -0
- package/dist/esm/storage/sql/sqlStorage.js +43 -0
- package/dist/esm/storage/sql/sqlStorage.js.map +1 -0
- package/dist/esm/storage/sql/tables.d.ts +40 -0
- package/dist/esm/storage/sql/tables.js +2 -0
- package/dist/esm/storage/sql/tables.js.map +1 -0
- package/dist/esm/types.d.ts +14 -5
- package/package.json +20 -14
- package/src/Server.ts +37 -43
- package/src/ServerLibrary.test.ts +447 -0
- package/src/ServerLibrary.ts +239 -163
- package/src/storage/Storage.ts +118 -0
- package/src/storage/index.ts +2 -0
- package/src/storage/sql/SqlBaselines.ts +141 -0
- package/src/storage/sql/SqlFileMetadata.ts +94 -0
- package/src/storage/sql/SqlOperations.ts +149 -0
- package/src/storage/sql/SqlReplicas.ts +234 -0
- package/src/storage/sql/migrations/v1.ts +64 -0
- package/src/storage/sql/migrations.ts +3 -0
- package/src/storage/sql/sqlStorage.ts +59 -0
- package/src/storage/sql/tables.ts +45 -0
- package/src/types.ts +15 -4
- package/bin/server.mjs +0 -115
- package/dist/cjs/Baselines.d.ts +0 -16
- package/dist/cjs/Baselines.js +0 -128
- package/dist/cjs/Baselines.js.map +0 -1
- package/dist/cjs/ClientConnection.d.ts +0 -20
- package/dist/cjs/ClientConnection.js +0 -139
- package/dist/cjs/ClientConnection.js.map +0 -1
- package/dist/cjs/MessageSender.d.ts +0 -5
- package/dist/cjs/MessageSender.js +0 -3
- package/dist/cjs/MessageSender.js.map +0 -1
- package/dist/cjs/OperationHistory.d.ts +0 -36
- package/dist/cjs/OperationHistory.js +0 -165
- package/dist/cjs/OperationHistory.js.map +0 -1
- package/dist/cjs/Presence.d.ts +0 -15
- package/dist/cjs/Presence.js +0 -60
- package/dist/cjs/Presence.js.map +0 -1
- package/dist/cjs/Profiles.d.ts +0 -10
- package/dist/cjs/Profiles.js +0 -20
- package/dist/cjs/Profiles.js.map +0 -1
- package/dist/cjs/ReplicaKeepaliveTimers.d.ts +0 -17
- package/dist/cjs/ReplicaKeepaliveTimers.js +0 -31
- package/dist/cjs/ReplicaKeepaliveTimers.js.map +0 -1
- package/dist/cjs/Replicas.d.ts +0 -25
- package/dist/cjs/Replicas.js +0 -196
- package/dist/cjs/Replicas.js.map +0 -1
- package/dist/cjs/Server.d.ts +0 -167
- package/dist/cjs/Server.js +0 -552
- package/dist/cjs/Server.js.map +0 -1
- package/dist/cjs/ServerLibrary.d.ts +0 -65
- package/dist/cjs/ServerLibrary.js +0 -505
- package/dist/cjs/ServerLibrary.js.map +0 -1
- package/dist/cjs/TokenProvider.d.ts +0 -19
- package/dist/cjs/TokenProvider.js +0 -27
- package/dist/cjs/TokenProvider.js.map +0 -1
- package/dist/cjs/TokenVerifier.d.ts +0 -16
- package/dist/cjs/TokenVerifier.js +0 -40
- package/dist/cjs/TokenVerifier.js.map +0 -1
- package/dist/cjs/files/FileMetadata.d.ts +0 -28
- package/dist/cjs/files/FileMetadata.js +0 -69
- package/dist/cjs/files/FileMetadata.js.map +0 -1
- package/dist/cjs/files/FileStorage.d.ts +0 -35
- package/dist/cjs/files/FileStorage.js +0 -72
- package/dist/cjs/files/FileStorage.js.map +0 -1
- package/dist/cjs/index.d.ts +0 -10
- package/dist/cjs/index.js +0 -12
- package/dist/cjs/index.js.map +0 -1
- package/dist/cjs/migrations/v0.d.ts +0 -1
- package/dist/cjs/migrations/v0.js +0 -30
- package/dist/cjs/migrations/v0.js.map +0 -1
- package/dist/cjs/migrations/v1.d.ts +0 -1
- package/dist/cjs/migrations/v1.js +0 -32
- package/dist/cjs/migrations/v1.js.map +0 -1
- package/dist/cjs/migrations/v2.d.ts +0 -1
- package/dist/cjs/migrations/v2.js +0 -29
- package/dist/cjs/migrations/v2.js.map +0 -1
- package/dist/cjs/migrations/v3.d.ts +0 -1
- package/dist/cjs/migrations/v3.js +0 -15
- package/dist/cjs/migrations/v3.js.map +0 -1
- package/dist/cjs/migrations/v4.d.ts +0 -1
- package/dist/cjs/migrations/v4.js +0 -9
- package/dist/cjs/migrations/v4.js.map +0 -1
- package/dist/cjs/migrations/v5.d.ts +0 -2
- package/dist/cjs/migrations/v5.js +0 -37
- package/dist/cjs/migrations/v5.js.map +0 -1
- package/dist/cjs/migrations/v6.d.ts +0 -1
- package/dist/cjs/migrations/v6.js +0 -8
- package/dist/cjs/migrations/v6.js.map +0 -1
- package/dist/cjs/migrations/v7.d.ts +0 -1
- package/dist/cjs/migrations/v7.js +0 -10
- package/dist/cjs/migrations/v7.js.map +0 -1
- package/dist/cjs/migrations.d.ts +0 -6
- package/dist/cjs/migrations.js +0 -72
- package/dist/cjs/migrations.js.map +0 -1
- package/dist/cjs/migrations.test.js +0 -121
- package/dist/cjs/migrations.test.js.map +0 -1
- package/dist/cjs/types.d.ts +0 -38
- package/dist/cjs/types.js +0 -3
- package/dist/cjs/types.js.map +0 -1
- package/dist/esm/Baselines.d.ts +0 -16
- package/dist/esm/Baselines.js +0 -124
- package/dist/esm/Baselines.js.map +0 -1
- package/dist/esm/OperationHistory.d.ts +0 -36
- package/dist/esm/OperationHistory.js +0 -161
- package/dist/esm/OperationHistory.js.map +0 -1
- package/dist/esm/Replicas.d.ts +0 -25
- package/dist/esm/Replicas.js +0 -192
- package/dist/esm/Replicas.js.map +0 -1
- package/dist/esm/files/FileMetadata.d.ts +0 -28
- package/dist/esm/files/FileMetadata.js +0 -65
- package/dist/esm/files/FileMetadata.js.map +0 -1
- package/dist/esm/migrations/v0.d.ts +0 -1
- package/dist/esm/migrations/v0.js +0 -27
- package/dist/esm/migrations/v0.js.map +0 -1
- package/dist/esm/migrations/v1.d.ts +0 -1
- package/dist/esm/migrations/v1.js +0 -29
- package/dist/esm/migrations/v1.js.map +0 -1
- package/dist/esm/migrations/v2.d.ts +0 -1
- package/dist/esm/migrations/v2.js +0 -26
- package/dist/esm/migrations/v2.js.map +0 -1
- package/dist/esm/migrations/v3.d.ts +0 -1
- package/dist/esm/migrations/v3.js +0 -12
- package/dist/esm/migrations/v3.js.map +0 -1
- package/dist/esm/migrations/v4.d.ts +0 -1
- package/dist/esm/migrations/v4.js +0 -6
- package/dist/esm/migrations/v4.js.map +0 -1
- package/dist/esm/migrations/v5.d.ts +0 -2
- package/dist/esm/migrations/v5.js +0 -33
- package/dist/esm/migrations/v5.js.map +0 -1
- package/dist/esm/migrations/v6.d.ts +0 -1
- package/dist/esm/migrations/v6.js +0 -5
- package/dist/esm/migrations/v6.js.map +0 -1
- package/dist/esm/migrations/v7.d.ts +0 -1
- package/dist/esm/migrations/v7.js +0 -7
- package/dist/esm/migrations/v7.js.map +0 -1
- package/dist/esm/migrations.d.ts +0 -6
- package/dist/esm/migrations.js +0 -45
- package/dist/esm/migrations.js.map +0 -1
- package/dist/esm/migrations.test.d.ts +0 -1
- package/dist/esm/migrations.test.js +0 -116
- package/dist/esm/migrations.test.js.map +0 -1
- package/dist/tsconfig-cjs.tsbuildinfo +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/Baselines.ts +0 -198
- package/src/OperationHistory.ts +0 -232
- package/src/Replicas.ts +0 -274
- package/src/files/FileMetadata.ts +0 -112
- package/src/migrations/v0.ts +0 -26
- package/src/migrations/v1.ts +0 -28
- package/src/migrations/v2.ts +0 -26
- package/src/migrations/v3.ts +0 -12
- package/src/migrations/v4.ts +0 -6
- package/src/migrations/v5.ts +0 -49
- package/src/migrations/v6.ts +0 -4
- package/src/migrations/v7.ts +0 -6
- package/src/migrations.test.ts +0 -133
- package/src/migrations.ts +0 -61
- /package/dist/{cjs/migrations.test.d.ts → esm/ServerLibrary.test.d.ts} +0 -0
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ReplicaInfos } from './Replicas.js';
|
|
3
|
-
import { OperationHistory } from './OperationHistory.js';
|
|
4
|
-
import { Baselines } from './Baselines.js';
|
|
1
|
+
import { EventSubscriber, ReplicaType, applyOperations, isFileRef, isObjectRef, } from '@verdant-web/common';
|
|
5
2
|
import { Presence } from './Presence.js';
|
|
6
3
|
export class ServerLibrary extends EventSubscriber {
|
|
7
|
-
constructor({
|
|
4
|
+
constructor({ storage, sender, profiles, log = () => { }, disableRebasing, fileStorage, }) {
|
|
8
5
|
super();
|
|
9
6
|
this.presences = new Presence();
|
|
10
7
|
/**
|
|
@@ -12,30 +9,37 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
12
9
|
* exist, a user may create it. But if it does exist, its user (clientId)
|
|
13
10
|
* must match the user making the request.
|
|
14
11
|
*/
|
|
15
|
-
this.validateReplicaAccess = (replicaId, info) => {
|
|
16
|
-
const replica = this.replicas.get(info.libraryId, replicaId);
|
|
12
|
+
this.validateReplicaAccess = async (replicaId, clientKey, info) => {
|
|
13
|
+
const replica = await this.storage.replicas.get(info.libraryId, replicaId);
|
|
17
14
|
if (replica && replica.clientId !== info.userId) {
|
|
18
|
-
|
|
15
|
+
this.sender.send(info.libraryId, clientKey, {
|
|
16
|
+
type: 'forbidden',
|
|
17
|
+
});
|
|
18
|
+
return false;
|
|
19
19
|
}
|
|
20
|
+
return true;
|
|
20
21
|
};
|
|
21
22
|
this.hasWriteAccess = (info) => {
|
|
22
23
|
return (info.type !== ReplicaType.ReadOnlyPull &&
|
|
23
24
|
info.type !== ReplicaType.ReadOnlyRealtime);
|
|
24
25
|
};
|
|
25
26
|
this.receive = async (message, clientKey, info) => {
|
|
26
|
-
this.
|
|
27
|
+
await this.storage.ready;
|
|
28
|
+
const allowed = await this.validateReplicaAccess(message.replicaId, clientKey, info);
|
|
29
|
+
if (!allowed)
|
|
30
|
+
return;
|
|
27
31
|
switch (message.type) {
|
|
28
32
|
case 'op':
|
|
29
|
-
this.handleOperation(message, clientKey, info);
|
|
33
|
+
await this.handleOperation(message, clientKey, info);
|
|
30
34
|
break;
|
|
31
35
|
case 'sync':
|
|
32
|
-
this.handleSync(message, clientKey, info);
|
|
36
|
+
await this.handleSync(message, clientKey, info);
|
|
33
37
|
break;
|
|
34
38
|
case 'ack':
|
|
35
|
-
this.handleAck(message, clientKey, info);
|
|
39
|
+
await this.handleAck(message, clientKey, info);
|
|
36
40
|
break;
|
|
37
41
|
case 'heartbeat':
|
|
38
|
-
this.handleHeartbeat(message, clientKey, info);
|
|
42
|
+
await this.handleHeartbeat(message, clientKey, info);
|
|
39
43
|
break;
|
|
40
44
|
case 'presence-update':
|
|
41
45
|
await this.handlePresenceUpdate(message, clientKey, info);
|
|
@@ -44,12 +48,12 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
44
48
|
this.log('Unknown message type', message.type);
|
|
45
49
|
break;
|
|
46
50
|
}
|
|
47
|
-
this.replicas.updateLastSeen(info.libraryId, message.replicaId);
|
|
51
|
+
await this.storage.replicas.updateLastSeen(info.libraryId, message.replicaId);
|
|
48
52
|
};
|
|
49
53
|
this.remove = (libraryId, replicaId) => {
|
|
50
54
|
this.presences.removeReplica(libraryId, replicaId);
|
|
51
55
|
};
|
|
52
|
-
this.handleOperation = (message, clientKey, info) => {
|
|
56
|
+
this.handleOperation = async (message, clientKey, info) => {
|
|
53
57
|
if (!message.operations.length)
|
|
54
58
|
return;
|
|
55
59
|
if (!this.hasWriteAccess(info)) {
|
|
@@ -58,70 +62,66 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
58
62
|
});
|
|
59
63
|
return;
|
|
60
64
|
}
|
|
61
|
-
const run = this.db.transaction(() => {
|
|
62
|
-
// insert patches into history
|
|
63
|
-
this.operations.insertAll(info.libraryId, message.replicaId, message.operations);
|
|
64
|
-
});
|
|
65
|
-
run();
|
|
66
|
-
this.enqueueRebase(info.libraryId);
|
|
67
65
|
// rebroadcast to whole library except the sender
|
|
68
|
-
this.rebroadcastOperations(info.libraryId, clientKey, message.replicaId, message.operations
|
|
66
|
+
this.rebroadcastOperations(info.libraryId, clientKey, message.replicaId, message.operations);
|
|
67
|
+
// TODO: can we defer this and rebroadcast in parallel?
|
|
68
|
+
// insert patches into history
|
|
69
|
+
await this.storage.operations.insertAll(info.libraryId, message.replicaId, message.operations);
|
|
70
|
+
this.enqueueRebase(info.libraryId);
|
|
69
71
|
// tell sender we got and processed their operation
|
|
70
72
|
this.sender.send(info.libraryId, clientKey, {
|
|
71
73
|
type: 'server-ack',
|
|
72
74
|
timestamp: message.timestamp,
|
|
73
75
|
});
|
|
74
|
-
this.updateHighwater(message.replicaId, message.operations[message.operations.length - 1].timestamp, info);
|
|
76
|
+
await this.updateHighwater(message.replicaId, message.operations[message.operations.length - 1].timestamp, info);
|
|
75
77
|
this.emit(`changes`, info, message.operations, []);
|
|
76
78
|
};
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.rebroadcastOperations = (libraryId, clientKey, replicaId, ops, baselines) => {
|
|
81
|
-
if (ops.length === 0 && baselines.length === 0)
|
|
79
|
+
this.rebroadcastOperations = async (libraryId, clientKey, replicaId, ops, baselines) => {
|
|
80
|
+
var _a, _b;
|
|
81
|
+
if (ops.length === 0 && !(baselines === null || baselines === void 0 ? void 0 : baselines.length))
|
|
82
82
|
return;
|
|
83
|
-
this.log('info', 'Rebroadcasting', ops.length, 'operations
|
|
83
|
+
this.log('info', 'Rebroadcasting', ops.length, 'operations', (_a = baselines === null || baselines === void 0 ? void 0 : baselines.length) !== null && _a !== void 0 ? _a : 0, 'baselines');
|
|
84
84
|
this.sender.broadcast(libraryId, {
|
|
85
85
|
type: 'op-re',
|
|
86
86
|
operations: ops,
|
|
87
87
|
baselines,
|
|
88
88
|
replicaId,
|
|
89
|
-
globalAckTimestamp: this.replicas.getGlobalAck(libraryId),
|
|
89
|
+
globalAckTimestamp: (_b = (await this.storage.replicas.getGlobalAck(libraryId))) !== null && _b !== void 0 ? _b : undefined,
|
|
90
90
|
}, [clientKey]);
|
|
91
91
|
};
|
|
92
|
-
this.handleSync = (message, clientKey, info) => {
|
|
92
|
+
this.handleSync = async (message, clientKey, info) => {
|
|
93
|
+
var _a;
|
|
93
94
|
const replicaId = message.replicaId;
|
|
94
|
-
// TODO: is this right
|
|
95
|
-
//
|
|
96
|
-
if (!this.hasWriteAccess(info)
|
|
95
|
+
// TODO: is this the right way to do this? should it just ignore
|
|
96
|
+
// read-only changes?
|
|
97
|
+
if (!this.hasWriteAccess(info) &&
|
|
98
|
+
(message.operations.length > 0 || message.baselines.length > 0)) {
|
|
97
99
|
this.sender.send(info.libraryId, clientKey, {
|
|
98
100
|
type: 'forbidden',
|
|
99
101
|
});
|
|
100
|
-
this.log('
|
|
102
|
+
this.log('warn', 'sync with changes from read-only replica', replicaId);
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
103
105
|
if (message.resyncAll) {
|
|
104
106
|
// forget our local understanding of the replica and reset it
|
|
105
|
-
this.replicas.delete(info.libraryId, replicaId);
|
|
107
|
+
await this.storage.replicas.delete(info.libraryId, replicaId);
|
|
106
108
|
}
|
|
107
|
-
const { status, replicaInfo: clientReplicaInfo } = this.replicas.getOrCreate(info.libraryId, replicaId, info);
|
|
109
|
+
const { status, replicaInfo: clientReplicaInfo } = await this.storage.replicas.getOrCreate(info.libraryId, replicaId, info);
|
|
108
110
|
const changesSince = status === 'existing' ? clientReplicaInfo.ackedLogicalTime : null;
|
|
109
|
-
this.log(`Sync from ${replicaId} (user: ${info.userId}) [ackedLogicalTime: ${changesSince}, ackedServerOrder: ${clientReplicaInfo.ackedServerOrder}, status: ${status}}]`);
|
|
111
|
+
this.log('info', `Sync from ${replicaId} (user: ${info.userId}) [ackedLogicalTime: ${changesSince}, ackedServerOrder: ${clientReplicaInfo.ackedServerOrder}, status: ${status}}]`);
|
|
110
112
|
// lookup operations after the last ack the client gave us
|
|
111
|
-
const ops = this.operations.
|
|
113
|
+
const ops = await this.storage.operations.getAfterServerOrder(info.libraryId, clientReplicaInfo.ackedServerOrder);
|
|
112
114
|
// new, unseen replicas should reset their existing storage
|
|
113
115
|
// to that of the server when joining a library.
|
|
114
116
|
// truant replicas should also reset their storage.
|
|
115
117
|
const replicaShouldReset = !!message.resyncAll || status !== 'existing';
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
const baselines =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// clientReplicaInfo.ackedServerOrder
|
|
124
|
-
// );
|
|
118
|
+
// only new replicas need baselines. by definition, a rebase only
|
|
119
|
+
// happens when all active replicas agree on history. every client
|
|
120
|
+
// replica will generate the same baseline locally, so it
|
|
121
|
+
// doesn't need to receive it from the server.
|
|
122
|
+
const baselines = replicaShouldReset
|
|
123
|
+
? await this.storage.baselines.getAll(info.libraryId)
|
|
124
|
+
: [];
|
|
125
125
|
// We detect that a library is new by checking if it has any history.
|
|
126
126
|
// If there are no existing operations or baselines (and the requested
|
|
127
127
|
// history timerange was "everything", i.e. "from null"), then this is
|
|
@@ -131,13 +131,16 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
131
131
|
// the definition of this field could be improved to be more explicit
|
|
132
132
|
// and not rely on seemingly unrelated data.
|
|
133
133
|
const isEmptyLibrary = changesSince === null && ops.length === 0 && baselines.length === 0;
|
|
134
|
+
if (isEmptyLibrary) {
|
|
135
|
+
this.log('info', 'Received sync from new library', replicaId);
|
|
136
|
+
}
|
|
134
137
|
let overwriteLocalData = replicaShouldReset && !isEmptyLibrary;
|
|
135
138
|
// if the local library is empty and the replica is new to us,
|
|
136
139
|
// but the replica is providing a "since" timestamp, this
|
|
137
140
|
// suggests the local data is incomplete or gone. we request
|
|
138
141
|
// this replica should respond with a full history.
|
|
139
142
|
if (isEmptyLibrary && status === 'new' && message.since !== null) {
|
|
140
|
-
this.log('Detected local data is incomplete, requesting full history from replica', replicaId);
|
|
143
|
+
this.log('info', 'Detected local data is incomplete, requesting full history from replica', replicaId);
|
|
141
144
|
this.sender.send(info.libraryId, clientKey, {
|
|
142
145
|
type: 'need-since',
|
|
143
146
|
since: null,
|
|
@@ -148,42 +151,45 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
148
151
|
// this would mean the replica is providing a full history
|
|
149
152
|
// but the library already has data. the replica should
|
|
150
153
|
// reset to the server version
|
|
151
|
-
this.log('Detected replica', replicaId, 'is providing a full history but the library already has data. Requesting replica reset.');
|
|
154
|
+
this.log('warn', 'Detected replica', replicaId, 'is providing a full history but the library already has data. Requesting replica reset.');
|
|
152
155
|
overwriteLocalData = true;
|
|
153
156
|
}
|
|
154
157
|
if (overwriteLocalData) {
|
|
155
|
-
this.log('Overwriting local data for replica', replicaId, 'with', baselines.length, 'baselines and', ops.length, 'operations');
|
|
158
|
+
this.log('info', 'Overwriting local data for replica', replicaId, 'with', baselines.length, 'baselines and', ops.length, 'operations');
|
|
156
159
|
}
|
|
157
160
|
// only write incoming replica data to storage if
|
|
158
161
|
// we are not overwriting the replica's data
|
|
159
162
|
if (!overwriteLocalData) {
|
|
160
163
|
// store all incoming operations and baselines
|
|
161
|
-
this.baselines.insertAll(info.libraryId, message.baselines);
|
|
162
|
-
this.log('Storing', message.baselines.length, 'baselines and', message.operations.length, 'operations');
|
|
163
|
-
this.operations.insertAll(info.libraryId, replicaId, message.operations);
|
|
164
|
-
this.replicas.
|
|
164
|
+
await this.storage.baselines.insertAll(info.libraryId, message.baselines);
|
|
165
|
+
this.log('debug', 'Storing', message.baselines.length, 'baselines and', message.operations.length, 'operations');
|
|
166
|
+
await this.storage.operations.insertAll(info.libraryId, replicaId, message.operations);
|
|
167
|
+
await this.storage.replicas.updateAcknowledgedLogicalTime(info.libraryId, replicaId, message.timestamp);
|
|
165
168
|
// will include the new global ack
|
|
166
|
-
this.rebroadcastOperations(info.libraryId, clientKey, message.replicaId, message.operations,
|
|
169
|
+
this.rebroadcastOperations(info.libraryId, clientKey, message.replicaId, message.operations,
|
|
170
|
+
// we only need to broadcast baselines if we just initialized
|
|
171
|
+
// this library, if any other clients (with empty libraries)
|
|
172
|
+
// are listening for initial data, too. this happens in
|
|
173
|
+
// rebasing.test.ts, for example.
|
|
174
|
+
isEmptyLibrary ? message.baselines : undefined);
|
|
167
175
|
if (message.operations.length || message.baselines.length) {
|
|
168
176
|
this.emit(`changes`, info, message.operations, message.baselines);
|
|
169
177
|
}
|
|
170
178
|
}
|
|
171
179
|
if (status === 'truant') {
|
|
172
|
-
this.log('A truant replica has reconnected', replicaId);
|
|
180
|
+
this.log('info', 'A truant replica has reconnected', replicaId);
|
|
173
181
|
}
|
|
174
182
|
// create the nonce by encoding the server order of the last operation
|
|
175
183
|
const ackThisNonce = this.createAckNonce(ops);
|
|
176
184
|
// respond to client
|
|
177
|
-
this.log('Sending sync response with', ops.length, 'operations and', baselines.length, 'baselines');
|
|
185
|
+
this.log('info', 'Sending sync response with', ops.length, 'operations and', baselines.length, 'baselines');
|
|
186
|
+
// mutating baselines/operations to remove extra data -
|
|
187
|
+
// mutating just to save on allocations here.
|
|
178
188
|
this.sender.send(info.libraryId, clientKey, {
|
|
179
189
|
type: 'sync-resp',
|
|
180
|
-
operations:
|
|
181
|
-
baselines:
|
|
182
|
-
|
|
183
|
-
snapshot: baseline.snapshot,
|
|
184
|
-
timestamp: baseline.timestamp,
|
|
185
|
-
})),
|
|
186
|
-
globalAckTimestamp: this.replicas.getGlobalAck(info.libraryId),
|
|
190
|
+
operations: this.removeOperationExtras(ops),
|
|
191
|
+
baselines: this.removeBaselineExtras(baselines),
|
|
192
|
+
globalAckTimestamp: (_a = (await this.storage.replicas.getGlobalAck(info.libraryId))) !== null && _a !== void 0 ? _a : undefined,
|
|
187
193
|
peerPresence: this.presences.all(info.libraryId),
|
|
188
194
|
// only request the client to overwrite local data if a reset is requested
|
|
189
195
|
// and there is data to overwrite it. otherwise the client may still
|
|
@@ -198,7 +204,7 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
198
204
|
? Buffer.from(JSON.stringify(ops[ops.length - 1].serverOrder)).toString('base64')
|
|
199
205
|
: undefined;
|
|
200
206
|
};
|
|
201
|
-
this.handleAck = (message, clientKey, info) => {
|
|
207
|
+
this.handleAck = async (message, clientKey, info) => {
|
|
202
208
|
if (message.nonce) {
|
|
203
209
|
const decodedNonce = JSON.parse(Buffer.from(message.nonce, 'base64').toString('utf8'));
|
|
204
210
|
this.log('Ack from', message.replicaId, '(user', info.userId, ')', 'with order', decodedNonce);
|
|
@@ -207,11 +213,11 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
207
213
|
return;
|
|
208
214
|
}
|
|
209
215
|
const replicaId = message.replicaId;
|
|
210
|
-
this.replicas.
|
|
216
|
+
await this.storage.replicas.updateAckedServerOrder(info.libraryId, replicaId, decodedNonce);
|
|
211
217
|
}
|
|
212
218
|
else if (message.timestamp) {
|
|
213
|
-
this.replicas.acknowledgeOperation(info.libraryId, message.replicaId, message.timestamp);
|
|
214
|
-
const globalAck = this.replicas.getGlobalAck(info.libraryId);
|
|
219
|
+
await this.storage.replicas.acknowledgeOperation(info.libraryId, message.replicaId, message.timestamp);
|
|
220
|
+
const globalAck = await this.storage.replicas.getGlobalAck(info.libraryId);
|
|
215
221
|
if (globalAck) {
|
|
216
222
|
this.sender.broadcast(info.libraryId, {
|
|
217
223
|
type: 'global-ack',
|
|
@@ -220,9 +226,9 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
220
226
|
}
|
|
221
227
|
}
|
|
222
228
|
};
|
|
223
|
-
this.updateHighwater = (replicaId, timestamp, info, ignoreClientKeys = []) => {
|
|
224
|
-
this.replicas.
|
|
225
|
-
const newGlobalAck = this.replicas.getGlobalAck(info.libraryId);
|
|
229
|
+
this.updateHighwater = async (replicaId, timestamp, info, ignoreClientKeys = []) => {
|
|
230
|
+
await this.storage.replicas.updateAcknowledgedLogicalTime(info.libraryId, replicaId, timestamp);
|
|
231
|
+
const newGlobalAck = await this.storage.replicas.getGlobalAck(info.libraryId);
|
|
226
232
|
if (newGlobalAck) {
|
|
227
233
|
this.sender.broadcast(info.libraryId, {
|
|
228
234
|
type: 'global-ack',
|
|
@@ -238,10 +244,10 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
238
244
|
setTimeout(() => this.rebase(libraryId), 0);
|
|
239
245
|
}
|
|
240
246
|
};
|
|
241
|
-
this.rebase = (libraryId) => {
|
|
247
|
+
this.rebase = async (libraryId) => {
|
|
242
248
|
if (this.disableRebasing)
|
|
243
249
|
return;
|
|
244
|
-
this.log('Performing rebase check');
|
|
250
|
+
this.log('debug', 'Performing rebase check');
|
|
245
251
|
// fundamentally a rebase occurs when some conditions are met:
|
|
246
252
|
// 1. the replica which created an operation has dropped that operation
|
|
247
253
|
// from their history stack, i.e. their oldest acked timestamp is after it.
|
|
@@ -258,18 +264,18 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
258
264
|
// for all actively connected replicas to ack regardless of their
|
|
259
265
|
// type.
|
|
260
266
|
const activeReplicaIds = Object.values(this.presences.all(libraryId)).map((p) => p.replicaId);
|
|
261
|
-
const globalAck = this.replicas.getGlobalAck(libraryId, activeReplicaIds);
|
|
262
|
-
const globalServerOrder = this.replicas.getEarliestAckedServerOrder(libraryId);
|
|
267
|
+
const globalAck = await this.storage.replicas.getGlobalAck(libraryId, activeReplicaIds);
|
|
268
|
+
const globalServerOrder = await this.storage.replicas.getEarliestAckedServerOrder(libraryId);
|
|
263
269
|
if (!globalAck) {
|
|
264
|
-
this.log('No global ack, skipping rebase');
|
|
270
|
+
this.log('debug', 'No global ack, skipping rebase');
|
|
265
271
|
return;
|
|
266
272
|
}
|
|
267
273
|
if (globalServerOrder === null) {
|
|
268
|
-
this.log('No global server order, skipping rebase');
|
|
274
|
+
this.log('debug', 'No global server order, skipping rebase');
|
|
269
275
|
return;
|
|
270
276
|
}
|
|
271
277
|
// these are in forward chronological order
|
|
272
|
-
const ops = this.operations.getBeforeServerOrder(libraryId, globalServerOrder);
|
|
278
|
+
const ops = await this.storage.operations.getBeforeServerOrder(libraryId, globalServerOrder);
|
|
273
279
|
const opsToApply = {};
|
|
274
280
|
// if we encounter a sequential operation in history which does
|
|
275
281
|
// not meet our conditions, we must ignore subsequent operations
|
|
@@ -288,8 +294,8 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
288
294
|
const deletedRefs = new Array();
|
|
289
295
|
for (const [documentId, ops] of Object.entries(opsToApply)) {
|
|
290
296
|
this.log('Rebasing', documentId);
|
|
291
|
-
this.baselines.applyOperations(libraryId, documentId, ops, deletedRefs)
|
|
292
|
-
|
|
297
|
+
await this.storage.baselines.applyOperations(libraryId, documentId, ops, deletedRefs);
|
|
298
|
+
await this.storage.operations.delete(libraryId, ops);
|
|
293
299
|
}
|
|
294
300
|
// hint to clients they can rebase too
|
|
295
301
|
this.sender.broadcast(libraryId, {
|
|
@@ -297,11 +303,9 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
297
303
|
timestamp: globalAck,
|
|
298
304
|
});
|
|
299
305
|
// cleanup deleted files
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
}
|
|
306
|
+
await Promise.all(deletedRefs
|
|
307
|
+
.filter(isFileRef)
|
|
308
|
+
.map((ref) => this.storage.fileMetadata.markPendingDelete(libraryId, ref.id)));
|
|
305
309
|
};
|
|
306
310
|
this.handleHeartbeat = (_, clientKey, info) => {
|
|
307
311
|
this.sender.send(info.libraryId, clientKey, {
|
|
@@ -329,25 +333,25 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
329
333
|
[]);
|
|
330
334
|
};
|
|
331
335
|
this.onPresenceLost = async (libraryId, replicaId, userId) => {
|
|
332
|
-
this.log('User disconnected from all replicas:', userId);
|
|
336
|
+
this.log('info', 'User disconnected from all replicas:', userId);
|
|
333
337
|
this.sender.broadcast(libraryId, {
|
|
334
338
|
type: 'presence-offline',
|
|
335
339
|
replicaId,
|
|
336
340
|
userId,
|
|
337
341
|
});
|
|
338
342
|
if (Object.keys(this.presences.all(libraryId)).length === 0) {
|
|
339
|
-
this.log(`All users have disconnected from ${libraryId}`);
|
|
343
|
+
this.log('info', `All users have disconnected from ${libraryId}`);
|
|
340
344
|
// could happen - if the server is shutting down manually
|
|
341
|
-
if (!this.
|
|
345
|
+
if (!this.storage.open) {
|
|
342
346
|
this.log('debug', 'Database not open, skipping cleanup');
|
|
343
347
|
return;
|
|
344
348
|
}
|
|
345
|
-
const pendingDelete = this.
|
|
349
|
+
const pendingDelete = await this.storage.fileMetadata.getPendingDelete(libraryId);
|
|
346
350
|
if (pendingDelete.length > 0) {
|
|
347
351
|
if (!this.fileStorage) {
|
|
348
352
|
throw new Error('File storage not configured, cannot delete files');
|
|
349
353
|
}
|
|
350
|
-
this.log('Deleting files:', pendingDelete.map((f) => f.fileId).join(', '));
|
|
354
|
+
this.log('info', 'Deleting files:', pendingDelete.map((f) => f.fileId).join(', '));
|
|
351
355
|
await Promise.all(pendingDelete.map(async (fileInfo) => {
|
|
352
356
|
var _a;
|
|
353
357
|
try {
|
|
@@ -357,10 +361,10 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
357
361
|
id: fileInfo.fileId,
|
|
358
362
|
type: fileInfo.type,
|
|
359
363
|
}));
|
|
360
|
-
this.
|
|
364
|
+
this.storage.fileMetadata.delete(libraryId, fileInfo.fileId);
|
|
361
365
|
}
|
|
362
366
|
catch (e) {
|
|
363
|
-
this.log('Failed to delete file', fileInfo.fileId, ' the file will remain in a pending delete state', e);
|
|
367
|
+
this.log('error', 'Failed to delete file', fileInfo.fileId, ' the file will remain in a pending delete state', e);
|
|
364
368
|
}
|
|
365
369
|
}));
|
|
366
370
|
}
|
|
@@ -368,13 +372,16 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
368
372
|
};
|
|
369
373
|
this.destroy = async (libraryId) => {
|
|
370
374
|
var _a;
|
|
375
|
+
this.log('info', 'Destroying library', libraryId);
|
|
371
376
|
this.presences.clear(libraryId);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
377
|
+
await Promise.all([
|
|
378
|
+
this.storage.replicas.deleteAll(libraryId),
|
|
379
|
+
this.storage.operations.deleteAll(libraryId),
|
|
380
|
+
this.storage.baselines.deleteAll(libraryId),
|
|
381
|
+
]);
|
|
382
|
+
const allFiles = await this.storage.fileMetadata.getAll(libraryId);
|
|
376
383
|
if (allFiles.length > 0) {
|
|
377
|
-
this.log(`Deleting ${allFiles.length} files for library ${libraryId}`, allFiles.map((f) => f.fileId).join(', '));
|
|
384
|
+
this.log('info', `Deleting ${allFiles.length} files for library ${libraryId}`, allFiles.map((f) => f.fileId).join(', '));
|
|
378
385
|
for (const fileInfo of allFiles) {
|
|
379
386
|
await ((_a = this.fileStorage) === null || _a === void 0 ? void 0 : _a.delete({
|
|
380
387
|
libraryId,
|
|
@@ -383,7 +390,7 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
383
390
|
type: fileInfo.type,
|
|
384
391
|
}));
|
|
385
392
|
}
|
|
386
|
-
this.
|
|
393
|
+
await this.storage.fileMetadata.deleteAll(libraryId);
|
|
387
394
|
}
|
|
388
395
|
};
|
|
389
396
|
this.getPresence = (libraryId) => {
|
|
@@ -391,7 +398,7 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
391
398
|
};
|
|
392
399
|
this.getInfo = async (libraryId) => {
|
|
393
400
|
var _a;
|
|
394
|
-
const rawReplicas = this.replicas.getAll(libraryId);
|
|
401
|
+
const rawReplicas = await this.storage.replicas.getAll(libraryId);
|
|
395
402
|
const profiles = await Promise.all(rawReplicas.map((r) => this.profiles.get(r.clientId)));
|
|
396
403
|
const replicas = rawReplicas.map((r, index) => ({
|
|
397
404
|
id: r.id,
|
|
@@ -399,16 +406,16 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
399
406
|
ackedServerOrder: r.ackedServerOrder,
|
|
400
407
|
type: r.type,
|
|
401
408
|
truant: !!r.lastSeenWallClockTime &&
|
|
402
|
-
r.lastSeenWallClockTime < this.replicas.truantCutoff,
|
|
409
|
+
r.lastSeenWallClockTime < this.storage.replicas.truantCutoff,
|
|
403
410
|
profile: profiles[index],
|
|
404
411
|
}));
|
|
405
412
|
const data = {
|
|
406
413
|
id: libraryId,
|
|
407
414
|
replicas,
|
|
408
|
-
latestServerOrder: this.operations.getLatestServerOrder(libraryId),
|
|
409
|
-
operationsCount: this.operations.getCount(libraryId),
|
|
410
|
-
baselinesCount: this.baselines.getCount(libraryId),
|
|
411
|
-
globalAck: (_a = this.replicas.getGlobalAck(libraryId)) !== null && _a !== void 0 ? _a : null,
|
|
415
|
+
latestServerOrder: await this.storage.operations.getLatestServerOrder(libraryId),
|
|
416
|
+
operationsCount: await this.storage.operations.getCount(libraryId),
|
|
417
|
+
baselinesCount: await this.storage.baselines.getCount(libraryId),
|
|
418
|
+
globalAck: (_a = (await this.storage.replicas.getGlobalAck(libraryId))) !== null && _a !== void 0 ? _a : null,
|
|
412
419
|
};
|
|
413
420
|
if (data.replicas.length === 0 &&
|
|
414
421
|
data.operationsCount === 0 &&
|
|
@@ -417,23 +424,25 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
417
424
|
}
|
|
418
425
|
return data;
|
|
419
426
|
};
|
|
420
|
-
this.evictUser = (libraryId, userId) => {
|
|
421
|
-
this.replicas.deleteAllForUser(libraryId, userId);
|
|
427
|
+
this.evictUser = async (libraryId, userId) => {
|
|
428
|
+
await this.storage.replicas.deleteAllForUser(libraryId, userId);
|
|
422
429
|
};
|
|
423
430
|
this.getDocumentSnapshot = (libraryId, oid) => {
|
|
424
431
|
return this.hydrateObject(libraryId, oid);
|
|
425
432
|
};
|
|
426
|
-
this.getObjectSnapshot = (libraryId, oid) => {
|
|
433
|
+
this.getObjectSnapshot = async (libraryId, oid) => {
|
|
427
434
|
var _a;
|
|
428
|
-
const baseline =
|
|
429
|
-
|
|
435
|
+
const [baseline, ops] = await Promise.all([
|
|
436
|
+
this.storage.baselines.get(libraryId, oid),
|
|
437
|
+
this.storage.operations.getAll(libraryId, oid),
|
|
438
|
+
]);
|
|
430
439
|
const snapshot = applyOperations((_a = baseline === null || baseline === void 0 ? void 0 : baseline.snapshot) !== null && _a !== void 0 ? _a : undefined, ops);
|
|
431
440
|
return snapshot;
|
|
432
441
|
};
|
|
433
|
-
this.hydrateObject = (libraryId, oid) => {
|
|
434
|
-
const snapshot = this.getObjectSnapshot(libraryId, oid);
|
|
442
|
+
this.hydrateObject = async (libraryId, oid) => {
|
|
443
|
+
const snapshot = await this.getObjectSnapshot(libraryId, oid);
|
|
435
444
|
if (Array.isArray(snapshot)) {
|
|
436
|
-
return snapshot.map((item
|
|
445
|
+
return Promise.all(snapshot.map(async (item) => {
|
|
437
446
|
if (isObjectRef(item)) {
|
|
438
447
|
return this.hydrateObject(libraryId, item.id);
|
|
439
448
|
}
|
|
@@ -443,27 +452,27 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
443
452
|
else {
|
|
444
453
|
return item;
|
|
445
454
|
}
|
|
446
|
-
});
|
|
455
|
+
}));
|
|
447
456
|
}
|
|
448
457
|
else if (snapshot && typeof snapshot === 'object') {
|
|
449
458
|
const hydrated = Object.assign({}, snapshot);
|
|
450
|
-
|
|
459
|
+
await Promise.all(Object.entries(snapshot).map(async ([key, value]) => {
|
|
451
460
|
if (isObjectRef(value)) {
|
|
452
|
-
hydrated[key] = this.hydrateObject(libraryId, value.id);
|
|
461
|
+
hydrated[key] = await this.hydrateObject(libraryId, value.id);
|
|
453
462
|
}
|
|
454
463
|
else if (isFileRef(value)) {
|
|
455
|
-
hydrated[key] = this.hydrateFile(libraryId, value.id);
|
|
464
|
+
hydrated[key] = await this.hydrateFile(libraryId, value.id);
|
|
456
465
|
}
|
|
457
|
-
}
|
|
466
|
+
}));
|
|
458
467
|
return hydrated;
|
|
459
468
|
}
|
|
460
469
|
else {
|
|
461
470
|
return snapshot;
|
|
462
471
|
}
|
|
463
472
|
};
|
|
464
|
-
this.hydrateFile = (libraryId, fileId) => {
|
|
473
|
+
this.hydrateFile = async (libraryId, fileId) => {
|
|
465
474
|
var _a, _b;
|
|
466
|
-
const data = this.
|
|
475
|
+
const data = await this.storage.fileMetadata.get(libraryId, fileId);
|
|
467
476
|
if (data) {
|
|
468
477
|
const url = (_b = (_a = this.fileStorage) === null || _a === void 0 ? void 0 : _a.getUrl({
|
|
469
478
|
fileName: data.name,
|
|
@@ -482,18 +491,33 @@ export class ServerLibrary extends EventSubscriber {
|
|
|
482
491
|
return null;
|
|
483
492
|
}
|
|
484
493
|
};
|
|
485
|
-
|
|
486
|
-
|
|
494
|
+
// MUTATES DATA. ONLY USE WHEN RETURNING DATA TO CLIENT WHICH WILL OTHERWISE
|
|
495
|
+
// NOT BE REFERENCED.
|
|
496
|
+
this.removeBaselineExtras = (baselines) => {
|
|
497
|
+
for (const baseline of baselines) {
|
|
498
|
+
// @ts-expect-error
|
|
499
|
+
delete baseline.libraryId;
|
|
500
|
+
}
|
|
501
|
+
return baselines;
|
|
502
|
+
};
|
|
503
|
+
// MUTATES DATA. ONLY USE WHEN RETURNING DATA TO CLIENT WHICH WILL OTHERWISE
|
|
504
|
+
// NOT BE REFERENCED.
|
|
505
|
+
this.removeOperationExtras = (operations) => {
|
|
506
|
+
for (const operation of operations) {
|
|
507
|
+
// @ts-expect-error
|
|
508
|
+
delete operation.libraryId;
|
|
509
|
+
// @ts-expect-error
|
|
510
|
+
delete operation.replicaId;
|
|
511
|
+
// @ts-expect-error
|
|
512
|
+
delete operation.serverOrder;
|
|
513
|
+
}
|
|
514
|
+
return operations;
|
|
487
515
|
};
|
|
488
|
-
this.db = db;
|
|
489
516
|
this.log = log;
|
|
490
517
|
this.disableRebasing = !!disableRebasing;
|
|
491
518
|
this.sender = sender;
|
|
492
519
|
this.profiles = profiles;
|
|
493
|
-
this.
|
|
494
|
-
this.operations = new OperationHistory(this.db);
|
|
495
|
-
this.baselines = new Baselines(this.db);
|
|
496
|
-
this.files = fileMetadata;
|
|
520
|
+
this.storage = storage;
|
|
497
521
|
this.fileStorage = fileStorage;
|
|
498
522
|
this.presences.on('lost', this.onPresenceLost);
|
|
499
523
|
}
|