@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.
Files changed (198) hide show
  1. package/dist/esm/ClientConnection.d.ts +1 -0
  2. package/dist/esm/ClientConnection.js.map +1 -1
  3. package/dist/esm/Presence.js.map +1 -1
  4. package/dist/esm/Profiles.js.map +1 -1
  5. package/dist/esm/ReplicaKeepaliveTimers.js.map +1 -1
  6. package/dist/esm/Server.d.ts +15 -11
  7. package/dist/esm/Server.js +29 -30
  8. package/dist/esm/Server.js.map +1 -1
  9. package/dist/esm/ServerLibrary.d.ts +10 -17
  10. package/dist/esm/ServerLibrary.js +151 -127
  11. package/dist/esm/ServerLibrary.js.map +1 -1
  12. package/dist/esm/ServerLibrary.test.js +369 -0
  13. package/dist/esm/ServerLibrary.test.js.map +1 -0
  14. package/dist/esm/TokenVerifier.js.map +1 -1
  15. package/dist/esm/files/FileStorage.js.map +1 -1
  16. package/dist/esm/storage/Storage.d.ts +67 -0
  17. package/dist/esm/storage/Storage.js +2 -0
  18. package/dist/esm/storage/Storage.js.map +1 -0
  19. package/dist/esm/storage/index.d.ts +2 -0
  20. package/dist/esm/storage/index.js +3 -0
  21. package/dist/esm/storage/index.js.map +1 -0
  22. package/dist/esm/storage/sql/SqlBaselines.d.ts +22 -0
  23. package/dist/esm/storage/sql/SqlBaselines.js +101 -0
  24. package/dist/esm/storage/sql/SqlBaselines.js.map +1 -0
  25. package/dist/esm/storage/sql/SqlFileMetadata.d.ts +18 -0
  26. package/dist/esm/storage/sql/SqlFileMetadata.js +69 -0
  27. package/dist/esm/storage/sql/SqlFileMetadata.js.map +1 -0
  28. package/dist/esm/storage/sql/SqlOperations.d.ts +19 -0
  29. package/dist/esm/storage/sql/SqlOperations.js +104 -0
  30. package/dist/esm/storage/sql/SqlOperations.js.map +1 -0
  31. package/dist/esm/storage/sql/SqlReplicas.d.ts +32 -0
  32. package/dist/esm/storage/sql/SqlReplicas.js +163 -0
  33. package/dist/esm/storage/sql/SqlReplicas.js.map +1 -0
  34. package/dist/esm/storage/sql/migrations/v1.d.ts +3 -0
  35. package/dist/esm/storage/sql/migrations/v1.js +59 -0
  36. package/dist/esm/storage/sql/migrations/v1.js.map +1 -0
  37. package/dist/esm/storage/sql/migrations.d.ts +5 -0
  38. package/dist/esm/storage/sql/migrations.js +3 -0
  39. package/dist/esm/storage/sql/migrations.js.map +1 -0
  40. package/dist/esm/storage/sql/sqlStorage.d.ts +5 -0
  41. package/dist/esm/storage/sql/sqlStorage.js +43 -0
  42. package/dist/esm/storage/sql/sqlStorage.js.map +1 -0
  43. package/dist/esm/storage/sql/tables.d.ts +40 -0
  44. package/dist/esm/storage/sql/tables.js +2 -0
  45. package/dist/esm/storage/sql/tables.js.map +1 -0
  46. package/dist/esm/types.d.ts +14 -5
  47. package/package.json +20 -14
  48. package/src/Server.ts +37 -43
  49. package/src/ServerLibrary.test.ts +447 -0
  50. package/src/ServerLibrary.ts +239 -163
  51. package/src/storage/Storage.ts +118 -0
  52. package/src/storage/index.ts +2 -0
  53. package/src/storage/sql/SqlBaselines.ts +141 -0
  54. package/src/storage/sql/SqlFileMetadata.ts +94 -0
  55. package/src/storage/sql/SqlOperations.ts +149 -0
  56. package/src/storage/sql/SqlReplicas.ts +234 -0
  57. package/src/storage/sql/migrations/v1.ts +64 -0
  58. package/src/storage/sql/migrations.ts +3 -0
  59. package/src/storage/sql/sqlStorage.ts +59 -0
  60. package/src/storage/sql/tables.ts +45 -0
  61. package/src/types.ts +15 -4
  62. package/bin/server.mjs +0 -115
  63. package/dist/cjs/Baselines.d.ts +0 -16
  64. package/dist/cjs/Baselines.js +0 -128
  65. package/dist/cjs/Baselines.js.map +0 -1
  66. package/dist/cjs/ClientConnection.d.ts +0 -20
  67. package/dist/cjs/ClientConnection.js +0 -139
  68. package/dist/cjs/ClientConnection.js.map +0 -1
  69. package/dist/cjs/MessageSender.d.ts +0 -5
  70. package/dist/cjs/MessageSender.js +0 -3
  71. package/dist/cjs/MessageSender.js.map +0 -1
  72. package/dist/cjs/OperationHistory.d.ts +0 -36
  73. package/dist/cjs/OperationHistory.js +0 -165
  74. package/dist/cjs/OperationHistory.js.map +0 -1
  75. package/dist/cjs/Presence.d.ts +0 -15
  76. package/dist/cjs/Presence.js +0 -60
  77. package/dist/cjs/Presence.js.map +0 -1
  78. package/dist/cjs/Profiles.d.ts +0 -10
  79. package/dist/cjs/Profiles.js +0 -20
  80. package/dist/cjs/Profiles.js.map +0 -1
  81. package/dist/cjs/ReplicaKeepaliveTimers.d.ts +0 -17
  82. package/dist/cjs/ReplicaKeepaliveTimers.js +0 -31
  83. package/dist/cjs/ReplicaKeepaliveTimers.js.map +0 -1
  84. package/dist/cjs/Replicas.d.ts +0 -25
  85. package/dist/cjs/Replicas.js +0 -196
  86. package/dist/cjs/Replicas.js.map +0 -1
  87. package/dist/cjs/Server.d.ts +0 -167
  88. package/dist/cjs/Server.js +0 -552
  89. package/dist/cjs/Server.js.map +0 -1
  90. package/dist/cjs/ServerLibrary.d.ts +0 -65
  91. package/dist/cjs/ServerLibrary.js +0 -505
  92. package/dist/cjs/ServerLibrary.js.map +0 -1
  93. package/dist/cjs/TokenProvider.d.ts +0 -19
  94. package/dist/cjs/TokenProvider.js +0 -27
  95. package/dist/cjs/TokenProvider.js.map +0 -1
  96. package/dist/cjs/TokenVerifier.d.ts +0 -16
  97. package/dist/cjs/TokenVerifier.js +0 -40
  98. package/dist/cjs/TokenVerifier.js.map +0 -1
  99. package/dist/cjs/files/FileMetadata.d.ts +0 -28
  100. package/dist/cjs/files/FileMetadata.js +0 -69
  101. package/dist/cjs/files/FileMetadata.js.map +0 -1
  102. package/dist/cjs/files/FileStorage.d.ts +0 -35
  103. package/dist/cjs/files/FileStorage.js +0 -72
  104. package/dist/cjs/files/FileStorage.js.map +0 -1
  105. package/dist/cjs/index.d.ts +0 -10
  106. package/dist/cjs/index.js +0 -12
  107. package/dist/cjs/index.js.map +0 -1
  108. package/dist/cjs/migrations/v0.d.ts +0 -1
  109. package/dist/cjs/migrations/v0.js +0 -30
  110. package/dist/cjs/migrations/v0.js.map +0 -1
  111. package/dist/cjs/migrations/v1.d.ts +0 -1
  112. package/dist/cjs/migrations/v1.js +0 -32
  113. package/dist/cjs/migrations/v1.js.map +0 -1
  114. package/dist/cjs/migrations/v2.d.ts +0 -1
  115. package/dist/cjs/migrations/v2.js +0 -29
  116. package/dist/cjs/migrations/v2.js.map +0 -1
  117. package/dist/cjs/migrations/v3.d.ts +0 -1
  118. package/dist/cjs/migrations/v3.js +0 -15
  119. package/dist/cjs/migrations/v3.js.map +0 -1
  120. package/dist/cjs/migrations/v4.d.ts +0 -1
  121. package/dist/cjs/migrations/v4.js +0 -9
  122. package/dist/cjs/migrations/v4.js.map +0 -1
  123. package/dist/cjs/migrations/v5.d.ts +0 -2
  124. package/dist/cjs/migrations/v5.js +0 -37
  125. package/dist/cjs/migrations/v5.js.map +0 -1
  126. package/dist/cjs/migrations/v6.d.ts +0 -1
  127. package/dist/cjs/migrations/v6.js +0 -8
  128. package/dist/cjs/migrations/v6.js.map +0 -1
  129. package/dist/cjs/migrations/v7.d.ts +0 -1
  130. package/dist/cjs/migrations/v7.js +0 -10
  131. package/dist/cjs/migrations/v7.js.map +0 -1
  132. package/dist/cjs/migrations.d.ts +0 -6
  133. package/dist/cjs/migrations.js +0 -72
  134. package/dist/cjs/migrations.js.map +0 -1
  135. package/dist/cjs/migrations.test.js +0 -121
  136. package/dist/cjs/migrations.test.js.map +0 -1
  137. package/dist/cjs/types.d.ts +0 -38
  138. package/dist/cjs/types.js +0 -3
  139. package/dist/cjs/types.js.map +0 -1
  140. package/dist/esm/Baselines.d.ts +0 -16
  141. package/dist/esm/Baselines.js +0 -124
  142. package/dist/esm/Baselines.js.map +0 -1
  143. package/dist/esm/OperationHistory.d.ts +0 -36
  144. package/dist/esm/OperationHistory.js +0 -161
  145. package/dist/esm/OperationHistory.js.map +0 -1
  146. package/dist/esm/Replicas.d.ts +0 -25
  147. package/dist/esm/Replicas.js +0 -192
  148. package/dist/esm/Replicas.js.map +0 -1
  149. package/dist/esm/files/FileMetadata.d.ts +0 -28
  150. package/dist/esm/files/FileMetadata.js +0 -65
  151. package/dist/esm/files/FileMetadata.js.map +0 -1
  152. package/dist/esm/migrations/v0.d.ts +0 -1
  153. package/dist/esm/migrations/v0.js +0 -27
  154. package/dist/esm/migrations/v0.js.map +0 -1
  155. package/dist/esm/migrations/v1.d.ts +0 -1
  156. package/dist/esm/migrations/v1.js +0 -29
  157. package/dist/esm/migrations/v1.js.map +0 -1
  158. package/dist/esm/migrations/v2.d.ts +0 -1
  159. package/dist/esm/migrations/v2.js +0 -26
  160. package/dist/esm/migrations/v2.js.map +0 -1
  161. package/dist/esm/migrations/v3.d.ts +0 -1
  162. package/dist/esm/migrations/v3.js +0 -12
  163. package/dist/esm/migrations/v3.js.map +0 -1
  164. package/dist/esm/migrations/v4.d.ts +0 -1
  165. package/dist/esm/migrations/v4.js +0 -6
  166. package/dist/esm/migrations/v4.js.map +0 -1
  167. package/dist/esm/migrations/v5.d.ts +0 -2
  168. package/dist/esm/migrations/v5.js +0 -33
  169. package/dist/esm/migrations/v5.js.map +0 -1
  170. package/dist/esm/migrations/v6.d.ts +0 -1
  171. package/dist/esm/migrations/v6.js +0 -5
  172. package/dist/esm/migrations/v6.js.map +0 -1
  173. package/dist/esm/migrations/v7.d.ts +0 -1
  174. package/dist/esm/migrations/v7.js +0 -7
  175. package/dist/esm/migrations/v7.js.map +0 -1
  176. package/dist/esm/migrations.d.ts +0 -6
  177. package/dist/esm/migrations.js +0 -45
  178. package/dist/esm/migrations.js.map +0 -1
  179. package/dist/esm/migrations.test.d.ts +0 -1
  180. package/dist/esm/migrations.test.js +0 -116
  181. package/dist/esm/migrations.test.js.map +0 -1
  182. package/dist/tsconfig-cjs.tsbuildinfo +0 -1
  183. package/dist/tsconfig.tsbuildinfo +0 -1
  184. package/src/Baselines.ts +0 -198
  185. package/src/OperationHistory.ts +0 -232
  186. package/src/Replicas.ts +0 -274
  187. package/src/files/FileMetadata.ts +0 -112
  188. package/src/migrations/v0.ts +0 -26
  189. package/src/migrations/v1.ts +0 -28
  190. package/src/migrations/v2.ts +0 -26
  191. package/src/migrations/v3.ts +0 -12
  192. package/src/migrations/v4.ts +0 -6
  193. package/src/migrations/v5.ts +0 -49
  194. package/src/migrations/v6.ts +0 -4
  195. package/src/migrations/v7.ts +0 -6
  196. package/src/migrations.test.ts +0 -133
  197. package/src/migrations.ts +0 -61
  198. /package/dist/{cjs/migrations.test.d.ts → esm/ServerLibrary.test.d.ts} +0 -0
@@ -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
- isObjectRef,
14
- FileRef,
15
- ObjectIdentifier,
16
- Ref,
13
+ applyOperations,
17
14
  isFileRef,
18
- SyncAckMessage,
19
- EventSubscriber,
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 { LibraryInfo, OperationSpec } from './types.js';
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 db;
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
- db,
48
+ storage,
56
49
  sender,
57
50
  profiles,
58
- replicaTruancyMinutes,
59
51
  log = () => {},
60
52
  disableRebasing,
61
- fileMetadata,
62
53
  fileStorage,
63
54
  }: {
64
- db: Database;
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.replicas = new ReplicaInfos(this.db, replicaTruancyMinutes);
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 = (replicaId: string, info: TokenInfo) => {
94
- const replica = this.replicas.get(info.libraryId, replicaId);
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
- throw new Error(
97
- `Replica ${replicaId} does not belong to client ${info.userId}`,
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.validateReplicaAccess(message.replicaId, info);
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(info.libraryId, message.replicaId);
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 removeExtraOperationData = (operation: OperationSpec): Operation => {
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: DocumentBaseline[],
196
+ baselines?: DocumentBaseline[],
204
197
  ) => {
205
- if (ops.length === 0 && baselines.length === 0) return;
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 and',
212
- baselines.length,
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: this.replicas.getGlobalAck(libraryId),
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? shouldn't read-only replicas still be able to sync,
237
- // just not send ops?
238
- if (!this.hasWriteAccess(info)) {
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('warning', 'sync from read-only replica', replicaId);
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.getFromServerOrder(
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
- // TODO: assumption: only new replicas need baselines
272
- // const baselines = replicaShouldReset
273
- // ? []
274
- // : this.baselines.getAllAfter(info.libraryId, changesSince);
275
- const baselines = this.baselines.getAllAfter(info.libraryId, changesSince);
276
- // const baselines = this.baselines.getFromServerOrder(
277
- // info.libraryId,
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(info.libraryId, replicaId, message.operations);
346
- this.replicas.updateAcknowledged(
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
- message.baselines,
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: ops.map(this.removeExtraOperationData),
383
- baselines: baselines.map((baseline) => ({
384
- oid: baseline.oid,
385
- snapshot: baseline.snapshot,
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: OperationSpec[]): string | undefined => {
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.updateServerOrder(info.libraryId, replicaId, decodedNonce);
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(info.libraryId);
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.updateAcknowledged(info.libraryId, replicaId, timestamp);
455
- const newGlobalAck = this.replicas.getGlobalAck(info.libraryId);
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(libraryId, activeReplicaIds);
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, OperationSpec[]> = {};
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(libraryId, documentId, ops, deletedRefs),
541
- this.operations.dropAll(libraryId, ops);
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
- for (const ref of deletedRefs) {
552
- if (isFileRef(ref)) {
553
- this.files.markPendingDelete(libraryId, ref.id);
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.db.open) {
645
+ if (!this.storage.open) {
611
646
  this.log('debug', 'Database not open, skipping cleanup');
612
647
  return;
613
648
  }
614
- const pendingDelete = this.files.getPendingDeletes(libraryId);
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.files.delete(libraryId, fileInfo.fileId);
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
- this.replicas.deleteAll(libraryId);
650
- this.operations.deleteAll(libraryId);
651
- this.baselines.deleteAll(libraryId);
652
- const allFiles = this.files.getAll(libraryId);
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.files.deleteAll(libraryId);
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: this.operations.getLatestServerOrder(libraryId),
694
- operationsCount: this.operations.getCount(libraryId),
695
- baselinesCount: this.baselines.getCount(libraryId),
696
- globalAck: this.replicas.getGlobalAck(libraryId) ?? null,
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 = this.baselines.get(libraryId, oid);
720
- const ops = this.operations.getAllFor(libraryId, oid);
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 = (libraryId: string, oid: string): any => {
726
- const snapshot = this.getObjectSnapshot(libraryId, oid);
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 snapshot.map((item, index) => {
729
- if (isObjectRef(item)) {
730
- return this.hydrateObject(libraryId, item.id);
731
- } else if (isFileRef(item)) {
732
- return this.hydrateFile(libraryId, item.id);
733
- } else {
734
- return item;
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
- for (const [key, value] of Object.entries(snapshot)) {
740
- if (isObjectRef(value)) {
741
- hydrated[key] = this.hydrateObject(libraryId, value.id);
742
- } else if (isFileRef(value)) {
743
- hydrated[key] = this.hydrateFile(libraryId, value.id);
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.files.get(libraryId, fileId);
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
- close = () => {
775
- this.db.close();
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
  }