@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,10 +1,7 @@
1
- import { applyOperations, omit, ReplicaType, isObjectRef, isFileRef, EventSubscriber, } from '@verdant-web/common';
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({ db, sender, profiles, replicaTruancyMinutes, log = () => { }, disableRebasing, fileMetadata, fileStorage, }) {
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
- throw new Error(`Replica ${replicaId} does not belong to client ${info.userId}`);
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.validateReplicaAccess(message.replicaId, info);
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.removeExtraOperationData = (operation) => {
78
- return omit(operation, ['replicaId', 'serverOrder']);
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 and', baselines.length, 'baselines');
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? shouldn't read-only replicas still be able to sync,
95
- // just not send ops?
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('warning', 'sync from read-only replica', replicaId);
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.getFromServerOrder(info.libraryId, clientReplicaInfo.ackedServerOrder);
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
- // TODO: assumption: only new replicas need baselines
117
- // const baselines = replicaShouldReset
118
- // ? []
119
- // : this.baselines.getAllAfter(info.libraryId, changesSince);
120
- const baselines = this.baselines.getAllAfter(info.libraryId, changesSince);
121
- // const baselines = this.baselines.getFromServerOrder(
122
- // info.libraryId,
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.updateAcknowledged(info.libraryId, replicaId, message.timestamp);
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, message.baselines);
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: ops.map(this.removeExtraOperationData),
181
- baselines: baselines.map((baseline) => ({
182
- oid: baseline.oid,
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.updateServerOrder(info.libraryId, replicaId, decodedNonce);
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.updateAcknowledged(info.libraryId, replicaId, timestamp);
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
- this.operations.dropAll(libraryId, ops);
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
- for (const ref of deletedRefs) {
301
- if (isFileRef(ref)) {
302
- this.files.markPendingDelete(libraryId, ref.id);
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.db.open) {
345
+ if (!this.storage.open) {
342
346
  this.log('debug', 'Database not open, skipping cleanup');
343
347
  return;
344
348
  }
345
- const pendingDelete = this.files.getPendingDeletes(libraryId);
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.files.delete(libraryId, fileInfo.fileId);
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
- this.replicas.deleteAll(libraryId);
373
- this.operations.deleteAll(libraryId);
374
- this.baselines.deleteAll(libraryId);
375
- const allFiles = this.files.getAll(libraryId);
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.files.deleteAll(libraryId);
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 = this.baselines.get(libraryId, oid);
429
- const ops = this.operations.getAllFor(libraryId, oid);
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, index) => {
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
- for (const [key, value] of Object.entries(snapshot)) {
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.files.get(libraryId, fileId);
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
- this.close = () => {
486
- this.db.close();
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.replicas = new ReplicaInfos(this.db, replicaTruancyMinutes);
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
  }