@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
package/src/ServerLibrary.ts
CHANGED
|
@@ -1,34 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AckMessage,
|
|
3
|
-
applyOperations,
|
|
4
3
|
ClientMessage,
|
|
5
4
|
DocumentBaseline,
|
|
5
|
+
EventSubscriber,
|
|
6
6
|
HeartbeatMessage,
|
|
7
|
-
omit,
|
|
8
7
|
Operation,
|
|
9
8
|
OperationMessage,
|
|
10
9
|
PresenceUpdateMessage,
|
|
10
|
+
Ref,
|
|
11
11
|
ReplicaType,
|
|
12
12
|
SyncMessage,
|
|
13
|
-
|
|
14
|
-
FileRef,
|
|
15
|
-
ObjectIdentifier,
|
|
16
|
-
Ref,
|
|
13
|
+
applyOperations,
|
|
17
14
|
isFileRef,
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
isObjectRef,
|
|
16
|
+
omit,
|
|
20
17
|
} from '@verdant-web/common';
|
|
21
|
-
import { Database } from 'better-sqlite3';
|
|
22
|
-
import { ReplicaInfos } from './Replicas.js';
|
|
23
18
|
import { MessageSender } from './MessageSender.js';
|
|
24
|
-
import { OperationHistory } from './OperationHistory.js';
|
|
25
|
-
import { Baselines } from './Baselines.js';
|
|
26
19
|
import { Presence } from './Presence.js';
|
|
27
20
|
import { UserProfileLoader } from './Profiles.js';
|
|
28
21
|
import { TokenInfo } from './TokenVerifier.js';
|
|
29
|
-
import { FileMetadata } from './files/FileMetadata.js';
|
|
30
22
|
import { FileStorage } from './files/FileStorage.js';
|
|
31
|
-
import {
|
|
23
|
+
import { Storage } from './storage/Storage.js';
|
|
24
|
+
import {
|
|
25
|
+
LibraryInfo,
|
|
26
|
+
StoredDocumentBaseline,
|
|
27
|
+
StoredOperation,
|
|
28
|
+
} from './types.js';
|
|
32
29
|
|
|
33
30
|
export type ServerLibraryEvents = {
|
|
34
31
|
changes: (
|
|
@@ -39,48 +36,36 @@ export type ServerLibraryEvents = {
|
|
|
39
36
|
};
|
|
40
37
|
|
|
41
38
|
export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
42
|
-
private
|
|
39
|
+
private storage;
|
|
43
40
|
private sender;
|
|
44
41
|
private profiles;
|
|
45
|
-
private replicas;
|
|
46
|
-
private operations;
|
|
47
|
-
private baselines;
|
|
48
42
|
private presences = new Presence();
|
|
49
43
|
private disableRebasing: boolean;
|
|
50
|
-
private files;
|
|
51
44
|
private fileStorage: FileStorage | undefined;
|
|
52
45
|
|
|
53
46
|
private log: (...args: any[]) => void;
|
|
54
47
|
constructor({
|
|
55
|
-
|
|
48
|
+
storage,
|
|
56
49
|
sender,
|
|
57
50
|
profiles,
|
|
58
|
-
replicaTruancyMinutes,
|
|
59
51
|
log = () => {},
|
|
60
52
|
disableRebasing,
|
|
61
|
-
fileMetadata,
|
|
62
53
|
fileStorage,
|
|
63
54
|
}: {
|
|
64
|
-
|
|
55
|
+
storage: Storage;
|
|
65
56
|
sender: MessageSender;
|
|
66
57
|
profiles: UserProfileLoader<any>;
|
|
67
|
-
replicaTruancyMinutes: number;
|
|
68
58
|
log?: (...args: any[]) => void;
|
|
69
59
|
disableRebasing?: boolean;
|
|
70
|
-
fileMetadata: FileMetadata;
|
|
71
60
|
fileStorage?: FileStorage;
|
|
72
61
|
}) {
|
|
73
62
|
super();
|
|
74
63
|
|
|
75
|
-
this.db = db;
|
|
76
64
|
this.log = log;
|
|
77
65
|
this.disableRebasing = !!disableRebasing;
|
|
78
66
|
this.sender = sender;
|
|
79
67
|
this.profiles = profiles;
|
|
80
|
-
this.
|
|
81
|
-
this.operations = new OperationHistory(this.db);
|
|
82
|
-
this.baselines = new Baselines(this.db);
|
|
83
|
-
this.files = fileMetadata;
|
|
68
|
+
this.storage = storage;
|
|
84
69
|
this.fileStorage = fileStorage;
|
|
85
70
|
this.presences.on('lost', this.onPresenceLost);
|
|
86
71
|
}
|
|
@@ -90,13 +75,19 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
90
75
|
* exist, a user may create it. But if it does exist, its user (clientId)
|
|
91
76
|
* must match the user making the request.
|
|
92
77
|
*/
|
|
93
|
-
private validateReplicaAccess = (
|
|
94
|
-
|
|
78
|
+
private validateReplicaAccess = async (
|
|
79
|
+
replicaId: string,
|
|
80
|
+
clientKey: string,
|
|
81
|
+
info: TokenInfo,
|
|
82
|
+
) => {
|
|
83
|
+
const replica = await this.storage.replicas.get(info.libraryId, replicaId);
|
|
95
84
|
if (replica && replica.clientId !== info.userId) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
);
|
|
85
|
+
this.sender.send(info.libraryId, clientKey, {
|
|
86
|
+
type: 'forbidden',
|
|
87
|
+
});
|
|
88
|
+
return false;
|
|
99
89
|
}
|
|
90
|
+
return true;
|
|
100
91
|
};
|
|
101
92
|
|
|
102
93
|
private hasWriteAccess = (info: TokenInfo) => {
|
|
@@ -111,20 +102,27 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
111
102
|
clientKey: string,
|
|
112
103
|
info: TokenInfo,
|
|
113
104
|
) => {
|
|
114
|
-
this.
|
|
105
|
+
await this.storage.ready;
|
|
106
|
+
|
|
107
|
+
const allowed = await this.validateReplicaAccess(
|
|
108
|
+
message.replicaId,
|
|
109
|
+
clientKey,
|
|
110
|
+
info,
|
|
111
|
+
);
|
|
112
|
+
if (!allowed) return;
|
|
115
113
|
|
|
116
114
|
switch (message.type) {
|
|
117
115
|
case 'op':
|
|
118
|
-
this.handleOperation(message, clientKey, info);
|
|
116
|
+
await this.handleOperation(message, clientKey, info);
|
|
119
117
|
break;
|
|
120
118
|
case 'sync':
|
|
121
|
-
this.handleSync(message, clientKey, info);
|
|
119
|
+
await this.handleSync(message, clientKey, info);
|
|
122
120
|
break;
|
|
123
121
|
case 'ack':
|
|
124
|
-
this.handleAck(message, clientKey, info);
|
|
122
|
+
await this.handleAck(message, clientKey, info);
|
|
125
123
|
break;
|
|
126
124
|
case 'heartbeat':
|
|
127
|
-
this.handleHeartbeat(message, clientKey, info);
|
|
125
|
+
await this.handleHeartbeat(message, clientKey, info);
|
|
128
126
|
break;
|
|
129
127
|
case 'presence-update':
|
|
130
128
|
await this.handlePresenceUpdate(message, clientKey, info);
|
|
@@ -133,14 +131,17 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
133
131
|
this.log('Unknown message type', (message as any).type);
|
|
134
132
|
break;
|
|
135
133
|
}
|
|
136
|
-
this.replicas.updateLastSeen(
|
|
134
|
+
await this.storage.replicas.updateLastSeen(
|
|
135
|
+
info.libraryId,
|
|
136
|
+
message.replicaId,
|
|
137
|
+
);
|
|
137
138
|
};
|
|
138
139
|
|
|
139
140
|
remove = (libraryId: string, replicaId: string) => {
|
|
140
141
|
this.presences.removeReplica(libraryId, replicaId);
|
|
141
142
|
};
|
|
142
143
|
|
|
143
|
-
private handleOperation = (
|
|
144
|
+
private handleOperation = async (
|
|
144
145
|
message: OperationMessage,
|
|
145
146
|
clientKey: string,
|
|
146
147
|
info: TokenInfo,
|
|
@@ -154,35 +155,31 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
154
155
|
return;
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
const run = this.db.transaction(() => {
|
|
158
|
-
// insert patches into history
|
|
159
|
-
this.operations.insertAll(
|
|
160
|
-
info.libraryId,
|
|
161
|
-
message.replicaId,
|
|
162
|
-
message.operations,
|
|
163
|
-
);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
run();
|
|
167
|
-
|
|
168
|
-
this.enqueueRebase(info.libraryId);
|
|
169
|
-
|
|
170
158
|
// rebroadcast to whole library except the sender
|
|
171
159
|
this.rebroadcastOperations(
|
|
172
160
|
info.libraryId,
|
|
173
161
|
clientKey,
|
|
174
162
|
message.replicaId,
|
|
175
163
|
message.operations,
|
|
176
|
-
[],
|
|
177
164
|
);
|
|
178
165
|
|
|
166
|
+
// TODO: can we defer this and rebroadcast in parallel?
|
|
167
|
+
// insert patches into history
|
|
168
|
+
await this.storage.operations.insertAll(
|
|
169
|
+
info.libraryId,
|
|
170
|
+
message.replicaId,
|
|
171
|
+
message.operations,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
this.enqueueRebase(info.libraryId);
|
|
175
|
+
|
|
179
176
|
// tell sender we got and processed their operation
|
|
180
177
|
this.sender.send(info.libraryId, clientKey, {
|
|
181
178
|
type: 'server-ack',
|
|
182
179
|
timestamp: message.timestamp,
|
|
183
180
|
});
|
|
184
181
|
|
|
185
|
-
this.updateHighwater(
|
|
182
|
+
await this.updateHighwater(
|
|
186
183
|
message.replicaId,
|
|
187
184
|
message.operations[message.operations.length - 1].timestamp,
|
|
188
185
|
info,
|
|
@@ -191,25 +188,21 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
191
188
|
this.emit(`changes`, info, message.operations, []);
|
|
192
189
|
};
|
|
193
190
|
|
|
194
|
-
private
|
|
195
|
-
return omit(operation, ['replicaId', 'serverOrder']);
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
private rebroadcastOperations = (
|
|
191
|
+
private rebroadcastOperations = async (
|
|
199
192
|
libraryId: string,
|
|
200
193
|
clientKey: string,
|
|
201
194
|
replicaId: string,
|
|
202
195
|
ops: Operation[],
|
|
203
|
-
baselines
|
|
196
|
+
baselines?: DocumentBaseline[],
|
|
204
197
|
) => {
|
|
205
|
-
if (ops.length === 0 && baselines
|
|
198
|
+
if (ops.length === 0 && !baselines?.length) return;
|
|
206
199
|
|
|
207
200
|
this.log(
|
|
208
201
|
'info',
|
|
209
202
|
'Rebroadcasting',
|
|
210
203
|
ops.length,
|
|
211
|
-
'operations
|
|
212
|
-
baselines
|
|
204
|
+
'operations',
|
|
205
|
+
baselines?.length ?? 0,
|
|
213
206
|
'baselines',
|
|
214
207
|
);
|
|
215
208
|
|
|
@@ -220,45 +213,50 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
220
213
|
operations: ops,
|
|
221
214
|
baselines,
|
|
222
215
|
replicaId,
|
|
223
|
-
globalAckTimestamp:
|
|
216
|
+
globalAckTimestamp:
|
|
217
|
+
(await this.storage.replicas.getGlobalAck(libraryId)) ?? undefined,
|
|
224
218
|
},
|
|
225
219
|
[clientKey],
|
|
226
220
|
);
|
|
227
221
|
};
|
|
228
222
|
|
|
229
|
-
private handleSync = (
|
|
223
|
+
private handleSync = async (
|
|
230
224
|
message: SyncMessage,
|
|
231
225
|
clientKey: string,
|
|
232
226
|
info: TokenInfo,
|
|
233
227
|
) => {
|
|
234
228
|
const replicaId = message.replicaId;
|
|
235
229
|
|
|
236
|
-
// TODO: is this right
|
|
237
|
-
//
|
|
238
|
-
if (
|
|
230
|
+
// TODO: is this the right way to do this? should it just ignore
|
|
231
|
+
// read-only changes?
|
|
232
|
+
if (
|
|
233
|
+
!this.hasWriteAccess(info) &&
|
|
234
|
+
(message.operations.length > 0 || message.baselines.length > 0)
|
|
235
|
+
) {
|
|
239
236
|
this.sender.send(info.libraryId, clientKey, {
|
|
240
237
|
type: 'forbidden',
|
|
241
238
|
});
|
|
242
|
-
this.log('
|
|
239
|
+
this.log('warn', 'sync with changes from read-only replica', replicaId);
|
|
243
240
|
return;
|
|
244
241
|
}
|
|
245
242
|
|
|
246
243
|
if (message.resyncAll) {
|
|
247
244
|
// forget our local understanding of the replica and reset it
|
|
248
|
-
this.replicas.delete(info.libraryId, replicaId);
|
|
245
|
+
await this.storage.replicas.delete(info.libraryId, replicaId);
|
|
249
246
|
}
|
|
250
247
|
|
|
251
248
|
const { status, replicaInfo: clientReplicaInfo } =
|
|
252
|
-
this.replicas.getOrCreate(info.libraryId, replicaId, info);
|
|
249
|
+
await this.storage.replicas.getOrCreate(info.libraryId, replicaId, info);
|
|
253
250
|
|
|
254
251
|
const changesSince =
|
|
255
252
|
status === 'existing' ? clientReplicaInfo.ackedLogicalTime : null;
|
|
256
253
|
this.log(
|
|
254
|
+
'info',
|
|
257
255
|
`Sync from ${replicaId} (user: ${info.userId}) [ackedLogicalTime: ${changesSince}, ackedServerOrder: ${clientReplicaInfo.ackedServerOrder}, status: ${status}}]`,
|
|
258
256
|
);
|
|
259
257
|
|
|
260
258
|
// lookup operations after the last ack the client gave us
|
|
261
|
-
const ops = this.operations.
|
|
259
|
+
const ops = await this.storage.operations.getAfterServerOrder(
|
|
262
260
|
info.libraryId,
|
|
263
261
|
clientReplicaInfo.ackedServerOrder,
|
|
264
262
|
);
|
|
@@ -268,15 +266,13 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
268
266
|
// truant replicas should also reset their storage.
|
|
269
267
|
const replicaShouldReset = !!message.resyncAll || status !== 'existing';
|
|
270
268
|
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
const baselines =
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// clientReplicaInfo.ackedServerOrder
|
|
279
|
-
// );
|
|
269
|
+
// only new replicas need baselines. by definition, a rebase only
|
|
270
|
+
// happens when all active replicas agree on history. every client
|
|
271
|
+
// replica will generate the same baseline locally, so it
|
|
272
|
+
// doesn't need to receive it from the server.
|
|
273
|
+
const baselines = replicaShouldReset
|
|
274
|
+
? await this.storage.baselines.getAll(info.libraryId)
|
|
275
|
+
: [];
|
|
280
276
|
|
|
281
277
|
// We detect that a library is new by checking if it has any history.
|
|
282
278
|
// If there are no existing operations or baselines (and the requested
|
|
@@ -288,6 +284,9 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
288
284
|
// and not rely on seemingly unrelated data.
|
|
289
285
|
const isEmptyLibrary =
|
|
290
286
|
changesSince === null && ops.length === 0 && baselines.length === 0;
|
|
287
|
+
if (isEmptyLibrary) {
|
|
288
|
+
this.log('info', 'Received sync from new library', replicaId);
|
|
289
|
+
}
|
|
291
290
|
|
|
292
291
|
let overwriteLocalData = replicaShouldReset && !isEmptyLibrary;
|
|
293
292
|
|
|
@@ -297,6 +296,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
297
296
|
// this replica should respond with a full history.
|
|
298
297
|
if (isEmptyLibrary && status === 'new' && message.since !== null) {
|
|
299
298
|
this.log(
|
|
299
|
+
'info',
|
|
300
300
|
'Detected local data is incomplete, requesting full history from replica',
|
|
301
301
|
replicaId,
|
|
302
302
|
);
|
|
@@ -310,6 +310,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
310
310
|
// but the library already has data. the replica should
|
|
311
311
|
// reset to the server version
|
|
312
312
|
this.log(
|
|
313
|
+
'warn',
|
|
313
314
|
'Detected replica',
|
|
314
315
|
replicaId,
|
|
315
316
|
'is providing a full history but the library already has data. Requesting replica reset.',
|
|
@@ -319,6 +320,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
319
320
|
|
|
320
321
|
if (overwriteLocalData) {
|
|
321
322
|
this.log(
|
|
323
|
+
'info',
|
|
322
324
|
'Overwriting local data for replica',
|
|
323
325
|
replicaId,
|
|
324
326
|
'with',
|
|
@@ -333,17 +335,22 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
333
335
|
// we are not overwriting the replica's data
|
|
334
336
|
if (!overwriteLocalData) {
|
|
335
337
|
// store all incoming operations and baselines
|
|
336
|
-
this.baselines.insertAll(info.libraryId, message.baselines);
|
|
338
|
+
await this.storage.baselines.insertAll(info.libraryId, message.baselines);
|
|
337
339
|
|
|
338
340
|
this.log(
|
|
341
|
+
'debug',
|
|
339
342
|
'Storing',
|
|
340
343
|
message.baselines.length,
|
|
341
344
|
'baselines and',
|
|
342
345
|
message.operations.length,
|
|
343
346
|
'operations',
|
|
344
347
|
);
|
|
345
|
-
this.operations.insertAll(
|
|
346
|
-
|
|
348
|
+
await this.storage.operations.insertAll(
|
|
349
|
+
info.libraryId,
|
|
350
|
+
replicaId,
|
|
351
|
+
message.operations,
|
|
352
|
+
);
|
|
353
|
+
await this.storage.replicas.updateAcknowledgedLogicalTime(
|
|
347
354
|
info.libraryId,
|
|
348
355
|
replicaId,
|
|
349
356
|
message.timestamp,
|
|
@@ -354,7 +361,11 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
354
361
|
clientKey,
|
|
355
362
|
message.replicaId,
|
|
356
363
|
message.operations,
|
|
357
|
-
|
|
364
|
+
// we only need to broadcast baselines if we just initialized
|
|
365
|
+
// this library, if any other clients (with empty libraries)
|
|
366
|
+
// are listening for initial data, too. this happens in
|
|
367
|
+
// rebasing.test.ts, for example.
|
|
368
|
+
isEmptyLibrary ? message.baselines : undefined,
|
|
358
369
|
);
|
|
359
370
|
if (message.operations.length || message.baselines.length) {
|
|
360
371
|
this.emit(`changes`, info, message.operations, message.baselines);
|
|
@@ -362,7 +373,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
362
373
|
}
|
|
363
374
|
|
|
364
375
|
if (status === 'truant') {
|
|
365
|
-
this.log('A truant replica has reconnected', replicaId);
|
|
376
|
+
this.log('info', 'A truant replica has reconnected', replicaId);
|
|
366
377
|
}
|
|
367
378
|
|
|
368
379
|
// create the nonce by encoding the server order of the last operation
|
|
@@ -371,21 +382,23 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
371
382
|
// respond to client
|
|
372
383
|
|
|
373
384
|
this.log(
|
|
385
|
+
'info',
|
|
374
386
|
'Sending sync response with',
|
|
375
387
|
ops.length,
|
|
376
388
|
'operations and',
|
|
377
389
|
baselines.length,
|
|
378
390
|
'baselines',
|
|
379
391
|
);
|
|
392
|
+
|
|
393
|
+
// mutating baselines/operations to remove extra data -
|
|
394
|
+
// mutating just to save on allocations here.
|
|
395
|
+
|
|
380
396
|
this.sender.send(info.libraryId, clientKey, {
|
|
381
397
|
type: 'sync-resp',
|
|
382
|
-
operations:
|
|
383
|
-
baselines:
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
timestamp: baseline.timestamp,
|
|
387
|
-
})),
|
|
388
|
-
globalAckTimestamp: this.replicas.getGlobalAck(info.libraryId),
|
|
398
|
+
operations: this.removeOperationExtras(ops),
|
|
399
|
+
baselines: this.removeBaselineExtras(baselines),
|
|
400
|
+
globalAckTimestamp:
|
|
401
|
+
(await this.storage.replicas.getGlobalAck(info.libraryId)) ?? undefined,
|
|
389
402
|
peerPresence: this.presences.all(info.libraryId),
|
|
390
403
|
// only request the client to overwrite local data if a reset is requested
|
|
391
404
|
// and there is data to overwrite it. otherwise the client may still
|
|
@@ -396,7 +409,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
396
409
|
});
|
|
397
410
|
};
|
|
398
411
|
|
|
399
|
-
private createAckNonce = (ops:
|
|
412
|
+
private createAckNonce = (ops: StoredOperation[]): string | undefined => {
|
|
400
413
|
return ops.length
|
|
401
414
|
? Buffer.from(JSON.stringify(ops[ops.length - 1].serverOrder)).toString(
|
|
402
415
|
'base64',
|
|
@@ -404,7 +417,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
404
417
|
: undefined;
|
|
405
418
|
};
|
|
406
419
|
|
|
407
|
-
private handleAck = (
|
|
420
|
+
private handleAck = async (
|
|
408
421
|
message: AckMessage,
|
|
409
422
|
clientKey: string,
|
|
410
423
|
info: TokenInfo,
|
|
@@ -428,14 +441,20 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
428
441
|
}
|
|
429
442
|
|
|
430
443
|
const replicaId = message.replicaId;
|
|
431
|
-
this.replicas.
|
|
444
|
+
await this.storage.replicas.updateAckedServerOrder(
|
|
445
|
+
info.libraryId,
|
|
446
|
+
replicaId,
|
|
447
|
+
decodedNonce,
|
|
448
|
+
);
|
|
432
449
|
} else if (message.timestamp) {
|
|
433
|
-
this.replicas.acknowledgeOperation(
|
|
450
|
+
await this.storage.replicas.acknowledgeOperation(
|
|
434
451
|
info.libraryId,
|
|
435
452
|
message.replicaId,
|
|
436
453
|
message.timestamp,
|
|
437
454
|
);
|
|
438
|
-
const globalAck = this.replicas.getGlobalAck(
|
|
455
|
+
const globalAck = await this.storage.replicas.getGlobalAck(
|
|
456
|
+
info.libraryId,
|
|
457
|
+
);
|
|
439
458
|
if (globalAck) {
|
|
440
459
|
this.sender.broadcast(info.libraryId, {
|
|
441
460
|
type: 'global-ack',
|
|
@@ -445,14 +464,20 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
445
464
|
}
|
|
446
465
|
};
|
|
447
466
|
|
|
448
|
-
private updateHighwater = (
|
|
467
|
+
private updateHighwater = async (
|
|
449
468
|
replicaId: string,
|
|
450
469
|
timestamp: string,
|
|
451
470
|
info: TokenInfo,
|
|
452
471
|
ignoreClientKeys: string[] = [],
|
|
453
472
|
) => {
|
|
454
|
-
this.replicas.
|
|
455
|
-
|
|
473
|
+
await this.storage.replicas.updateAcknowledgedLogicalTime(
|
|
474
|
+
info.libraryId,
|
|
475
|
+
replicaId,
|
|
476
|
+
timestamp,
|
|
477
|
+
);
|
|
478
|
+
const newGlobalAck = await this.storage.replicas.getGlobalAck(
|
|
479
|
+
info.libraryId,
|
|
480
|
+
);
|
|
456
481
|
if (newGlobalAck) {
|
|
457
482
|
this.sender.broadcast(
|
|
458
483
|
info.libraryId,
|
|
@@ -474,10 +499,10 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
474
499
|
}
|
|
475
500
|
};
|
|
476
501
|
|
|
477
|
-
private rebase = (libraryId: string) => {
|
|
502
|
+
private rebase = async (libraryId: string) => {
|
|
478
503
|
if (this.disableRebasing) return;
|
|
479
504
|
|
|
480
|
-
this.log('Performing rebase check');
|
|
505
|
+
this.log('debug', 'Performing rebase check');
|
|
481
506
|
|
|
482
507
|
// fundamentally a rebase occurs when some conditions are met:
|
|
483
508
|
// 1. the replica which created an operation has dropped that operation
|
|
@@ -498,27 +523,30 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
498
523
|
const activeReplicaIds = Object.values(this.presences.all(libraryId)).map(
|
|
499
524
|
(p) => p.replicaId,
|
|
500
525
|
);
|
|
501
|
-
const globalAck = this.replicas.getGlobalAck(
|
|
526
|
+
const globalAck = await this.storage.replicas.getGlobalAck(
|
|
527
|
+
libraryId,
|
|
528
|
+
activeReplicaIds,
|
|
529
|
+
);
|
|
502
530
|
const globalServerOrder =
|
|
503
|
-
this.replicas.getEarliestAckedServerOrder(libraryId);
|
|
531
|
+
await this.storage.replicas.getEarliestAckedServerOrder(libraryId);
|
|
504
532
|
|
|
505
533
|
if (!globalAck) {
|
|
506
|
-
this.log('No global ack, skipping rebase');
|
|
534
|
+
this.log('debug', 'No global ack, skipping rebase');
|
|
507
535
|
return;
|
|
508
536
|
}
|
|
509
537
|
|
|
510
538
|
if (globalServerOrder === null) {
|
|
511
|
-
this.log('No global server order, skipping rebase');
|
|
539
|
+
this.log('debug', 'No global server order, skipping rebase');
|
|
512
540
|
return;
|
|
513
541
|
}
|
|
514
542
|
|
|
515
543
|
// these are in forward chronological order
|
|
516
|
-
const ops = this.operations.getBeforeServerOrder(
|
|
544
|
+
const ops = await this.storage.operations.getBeforeServerOrder(
|
|
517
545
|
libraryId,
|
|
518
546
|
globalServerOrder,
|
|
519
547
|
);
|
|
520
548
|
|
|
521
|
-
const opsToApply: Record<string,
|
|
549
|
+
const opsToApply: Record<string, StoredOperation[]> = {};
|
|
522
550
|
// if we encounter a sequential operation in history which does
|
|
523
551
|
// not meet our conditions, we must ignore subsequent operations
|
|
524
552
|
// applied to that document.
|
|
@@ -537,8 +565,13 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
537
565
|
const deletedRefs = new Array<Ref>();
|
|
538
566
|
for (const [documentId, ops] of Object.entries(opsToApply)) {
|
|
539
567
|
this.log('Rebasing', documentId);
|
|
540
|
-
this.baselines.applyOperations(
|
|
541
|
-
|
|
568
|
+
await this.storage.baselines.applyOperations(
|
|
569
|
+
libraryId,
|
|
570
|
+
documentId,
|
|
571
|
+
ops,
|
|
572
|
+
deletedRefs,
|
|
573
|
+
);
|
|
574
|
+
await this.storage.operations.delete(libraryId, ops);
|
|
542
575
|
}
|
|
543
576
|
|
|
544
577
|
// hint to clients they can rebase too
|
|
@@ -548,11 +581,13 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
548
581
|
});
|
|
549
582
|
|
|
550
583
|
// cleanup deleted files
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
584
|
+
await Promise.all(
|
|
585
|
+
deletedRefs
|
|
586
|
+
.filter(isFileRef)
|
|
587
|
+
.map((ref) =>
|
|
588
|
+
this.storage.fileMetadata.markPendingDelete(libraryId, ref.id),
|
|
589
|
+
),
|
|
590
|
+
);
|
|
556
591
|
};
|
|
557
592
|
|
|
558
593
|
private handleHeartbeat = (
|
|
@@ -598,30 +633,32 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
598
633
|
replicaId: string,
|
|
599
634
|
userId: string,
|
|
600
635
|
) => {
|
|
601
|
-
this.log('User disconnected from all replicas:', userId);
|
|
636
|
+
this.log('info', 'User disconnected from all replicas:', userId);
|
|
602
637
|
this.sender.broadcast(libraryId, {
|
|
603
638
|
type: 'presence-offline',
|
|
604
639
|
replicaId,
|
|
605
640
|
userId,
|
|
606
641
|
});
|
|
607
642
|
if (Object.keys(this.presences.all(libraryId)).length === 0) {
|
|
608
|
-
this.log(`All users have disconnected from ${libraryId}`);
|
|
643
|
+
this.log('info', `All users have disconnected from ${libraryId}`);
|
|
609
644
|
// could happen - if the server is shutting down manually
|
|
610
|
-
if (!this.
|
|
645
|
+
if (!this.storage.open) {
|
|
611
646
|
this.log('debug', 'Database not open, skipping cleanup');
|
|
612
647
|
return;
|
|
613
648
|
}
|
|
614
|
-
const pendingDelete =
|
|
649
|
+
const pendingDelete =
|
|
650
|
+
await this.storage.fileMetadata.getPendingDelete(libraryId);
|
|
615
651
|
if (pendingDelete.length > 0) {
|
|
616
652
|
if (!this.fileStorage) {
|
|
617
653
|
throw new Error('File storage not configured, cannot delete files');
|
|
618
654
|
}
|
|
619
655
|
this.log(
|
|
656
|
+
'info',
|
|
620
657
|
'Deleting files:',
|
|
621
658
|
pendingDelete.map((f) => f.fileId).join(', '),
|
|
622
659
|
);
|
|
623
660
|
await Promise.all(
|
|
624
|
-
pendingDelete.map(async (fileInfo) => {
|
|
661
|
+
pendingDelete.map(async (fileInfo): Promise<void> => {
|
|
625
662
|
try {
|
|
626
663
|
await this.fileStorage?.delete({
|
|
627
664
|
libraryId: fileInfo.libraryId,
|
|
@@ -629,9 +666,10 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
629
666
|
id: fileInfo.fileId,
|
|
630
667
|
type: fileInfo.type,
|
|
631
668
|
});
|
|
632
|
-
this.
|
|
669
|
+
this.storage.fileMetadata.delete(libraryId, fileInfo.fileId);
|
|
633
670
|
} catch (e) {
|
|
634
671
|
this.log(
|
|
672
|
+
'error',
|
|
635
673
|
'Failed to delete file',
|
|
636
674
|
fileInfo.fileId,
|
|
637
675
|
' the file will remain in a pending delete state',
|
|
@@ -645,13 +683,17 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
645
683
|
};
|
|
646
684
|
|
|
647
685
|
destroy = async (libraryId: string) => {
|
|
686
|
+
this.log('info', 'Destroying library', libraryId);
|
|
648
687
|
this.presences.clear(libraryId);
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
688
|
+
await Promise.all([
|
|
689
|
+
this.storage.replicas.deleteAll(libraryId),
|
|
690
|
+
this.storage.operations.deleteAll(libraryId),
|
|
691
|
+
this.storage.baselines.deleteAll(libraryId),
|
|
692
|
+
]);
|
|
693
|
+
const allFiles = await this.storage.fileMetadata.getAll(libraryId);
|
|
653
694
|
if (allFiles.length > 0) {
|
|
654
695
|
this.log(
|
|
696
|
+
'info',
|
|
655
697
|
`Deleting ${allFiles.length} files for library ${libraryId}`,
|
|
656
698
|
allFiles.map((f) => f.fileId).join(', '),
|
|
657
699
|
);
|
|
@@ -663,7 +705,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
663
705
|
type: fileInfo.type,
|
|
664
706
|
});
|
|
665
707
|
}
|
|
666
|
-
this.
|
|
708
|
+
await this.storage.fileMetadata.deleteAll(libraryId);
|
|
667
709
|
}
|
|
668
710
|
};
|
|
669
711
|
|
|
@@ -672,7 +714,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
672
714
|
};
|
|
673
715
|
|
|
674
716
|
getInfo = async (libraryId: string): Promise<LibraryInfo | null> => {
|
|
675
|
-
const rawReplicas = this.replicas.getAll(libraryId);
|
|
717
|
+
const rawReplicas = await this.storage.replicas.getAll(libraryId);
|
|
676
718
|
const profiles = await Promise.all(
|
|
677
719
|
rawReplicas.map((r) => this.profiles.get(r.clientId)),
|
|
678
720
|
);
|
|
@@ -683,17 +725,18 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
683
725
|
type: r.type,
|
|
684
726
|
truant:
|
|
685
727
|
!!r.lastSeenWallClockTime &&
|
|
686
|
-
r.lastSeenWallClockTime < this.replicas.truantCutoff,
|
|
728
|
+
r.lastSeenWallClockTime < this.storage.replicas.truantCutoff,
|
|
687
729
|
profile: profiles[index],
|
|
688
730
|
}));
|
|
689
731
|
|
|
690
732
|
const data = {
|
|
691
733
|
id: libraryId,
|
|
692
734
|
replicas,
|
|
693
|
-
latestServerOrder:
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
735
|
+
latestServerOrder:
|
|
736
|
+
await this.storage.operations.getLatestServerOrder(libraryId),
|
|
737
|
+
operationsCount: await this.storage.operations.getCount(libraryId),
|
|
738
|
+
baselinesCount: await this.storage.baselines.getCount(libraryId),
|
|
739
|
+
globalAck: (await this.storage.replicas.getGlobalAck(libraryId)) ?? null,
|
|
697
740
|
};
|
|
698
741
|
|
|
699
742
|
if (
|
|
@@ -707,50 +750,59 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
707
750
|
return data;
|
|
708
751
|
};
|
|
709
752
|
|
|
710
|
-
evictUser = (libraryId: string, userId: string) => {
|
|
711
|
-
this.replicas.deleteAllForUser(libraryId, userId);
|
|
753
|
+
evictUser = async (libraryId: string, userId: string) => {
|
|
754
|
+
await this.storage.replicas.deleteAllForUser(libraryId, userId);
|
|
712
755
|
};
|
|
713
756
|
|
|
714
757
|
getDocumentSnapshot = (libraryId: string, oid: string) => {
|
|
715
758
|
return this.hydrateObject(libraryId, oid);
|
|
716
759
|
};
|
|
717
760
|
|
|
718
|
-
private getObjectSnapshot = (libraryId: string, oid: string) => {
|
|
719
|
-
const baseline =
|
|
720
|
-
|
|
761
|
+
private getObjectSnapshot = async (libraryId: string, oid: string) => {
|
|
762
|
+
const [baseline, ops] = await Promise.all([
|
|
763
|
+
this.storage.baselines.get(libraryId, oid),
|
|
764
|
+
this.storage.operations.getAll(libraryId, oid),
|
|
765
|
+
]);
|
|
721
766
|
const snapshot = applyOperations(baseline?.snapshot ?? undefined, ops);
|
|
722
767
|
return snapshot;
|
|
723
768
|
};
|
|
724
769
|
|
|
725
|
-
private hydrateObject = (
|
|
726
|
-
|
|
770
|
+
private hydrateObject = async (
|
|
771
|
+
libraryId: string,
|
|
772
|
+
oid: string,
|
|
773
|
+
): Promise<any> => {
|
|
774
|
+
const snapshot = await this.getObjectSnapshot(libraryId, oid);
|
|
727
775
|
if (Array.isArray(snapshot)) {
|
|
728
|
-
return
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
776
|
+
return Promise.all(
|
|
777
|
+
snapshot.map(async (item: any) => {
|
|
778
|
+
if (isObjectRef(item)) {
|
|
779
|
+
return this.hydrateObject(libraryId, item.id);
|
|
780
|
+
} else if (isFileRef(item)) {
|
|
781
|
+
return this.hydrateFile(libraryId, item.id);
|
|
782
|
+
} else {
|
|
783
|
+
return item;
|
|
784
|
+
}
|
|
785
|
+
}),
|
|
786
|
+
);
|
|
737
787
|
} else if (snapshot && typeof snapshot === 'object') {
|
|
738
788
|
const hydrated = { ...snapshot };
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
789
|
+
await Promise.all(
|
|
790
|
+
Object.entries(snapshot).map(async ([key, value]) => {
|
|
791
|
+
if (isObjectRef(value)) {
|
|
792
|
+
hydrated[key] = await this.hydrateObject(libraryId, value.id);
|
|
793
|
+
} else if (isFileRef(value)) {
|
|
794
|
+
hydrated[key] = await this.hydrateFile(libraryId, value.id);
|
|
795
|
+
}
|
|
796
|
+
}),
|
|
797
|
+
);
|
|
746
798
|
return hydrated;
|
|
747
799
|
} else {
|
|
748
800
|
return snapshot;
|
|
749
801
|
}
|
|
750
802
|
};
|
|
751
803
|
|
|
752
|
-
private hydrateFile = (libraryId: string, fileId: string) => {
|
|
753
|
-
const data = this.
|
|
804
|
+
private hydrateFile = async (libraryId: string, fileId: string) => {
|
|
805
|
+
const data = await this.storage.fileMetadata.get(libraryId, fileId);
|
|
754
806
|
if (data) {
|
|
755
807
|
const url =
|
|
756
808
|
this.fileStorage?.getUrl({
|
|
@@ -771,7 +823,31 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
|
771
823
|
}
|
|
772
824
|
};
|
|
773
825
|
|
|
774
|
-
|
|
775
|
-
|
|
826
|
+
// MUTATES DATA. ONLY USE WHEN RETURNING DATA TO CLIENT WHICH WILL OTHERWISE
|
|
827
|
+
// NOT BE REFERENCED.
|
|
828
|
+
private removeBaselineExtras = (
|
|
829
|
+
baselines: StoredDocumentBaseline[],
|
|
830
|
+
): DocumentBaseline[] => {
|
|
831
|
+
for (const baseline of baselines) {
|
|
832
|
+
// @ts-expect-error
|
|
833
|
+
delete baseline.libraryId;
|
|
834
|
+
}
|
|
835
|
+
return baselines as unknown as DocumentBaseline[];
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
// MUTATES DATA. ONLY USE WHEN RETURNING DATA TO CLIENT WHICH WILL OTHERWISE
|
|
839
|
+
// NOT BE REFERENCED.
|
|
840
|
+
private removeOperationExtras = (
|
|
841
|
+
operations: StoredOperation[],
|
|
842
|
+
): Operation[] => {
|
|
843
|
+
for (const operation of operations) {
|
|
844
|
+
// @ts-expect-error
|
|
845
|
+
delete operation.libraryId;
|
|
846
|
+
// @ts-expect-error
|
|
847
|
+
delete operation.replicaId;
|
|
848
|
+
// @ts-expect-error
|
|
849
|
+
delete operation.serverOrder;
|
|
850
|
+
}
|
|
851
|
+
return operations as unknown as Operation[];
|
|
776
852
|
};
|
|
777
853
|
}
|