@verdant-web/server 3.3.11 → 4.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 (204) hide show
  1. package/bin/server.mjs +110 -107
  2. package/dist/esm/TokenVerifier.d.ts +1 -0
  3. package/dist/esm/TokenVerifier.js +3 -0
  4. package/dist/esm/TokenVerifier.js.map +1 -1
  5. package/dist/esm/adapters/node/errorHandler.d.ts +2 -0
  6. package/dist/esm/adapters/node/errorHandler.js +12 -0
  7. package/dist/esm/adapters/node/errorHandler.js.map +1 -0
  8. package/dist/esm/adapters/node/getRequestToken.d.ts +1 -0
  9. package/dist/esm/adapters/node/getRequestToken.js +25 -0
  10. package/dist/esm/adapters/node/getRequestToken.js.map +1 -0
  11. package/dist/esm/adapters/node/httpRouter.d.ts +47 -0
  12. package/dist/esm/adapters/node/httpRouter.js +88 -0
  13. package/dist/esm/adapters/node/httpRouter.js.map +1 -0
  14. package/dist/esm/adapters/node/index.d.ts +5 -0
  15. package/dist/esm/adapters/node/index.js +6 -0
  16. package/dist/esm/adapters/node/index.js.map +1 -0
  17. package/dist/esm/adapters/node/tokenMiddleware.d.ts +7 -0
  18. package/dist/esm/adapters/node/tokenMiddleware.js +22 -0
  19. package/dist/esm/adapters/node/tokenMiddleware.js.map +1 -0
  20. package/dist/esm/adapters/node/websocketHandler.d.ts +4 -0
  21. package/dist/esm/adapters/node/websocketHandler.js +54 -0
  22. package/dist/esm/adapters/node/websocketHandler.js.map +1 -0
  23. package/dist/esm/connections/ClientConnection.d.ts +25 -0
  24. package/dist/esm/{ClientConnection.js → connections/ClientConnection.js} +47 -38
  25. package/dist/esm/connections/ClientConnection.js.map +1 -0
  26. package/dist/esm/connections/MessageSender.d.ts +5 -0
  27. package/dist/esm/connections/MessageSender.js.map +1 -0
  28. package/dist/esm/connections/Presence.d.ts +23 -0
  29. package/dist/esm/connections/Presence.js +65 -0
  30. package/dist/esm/connections/Presence.js.map +1 -0
  31. package/dist/esm/core.d.ts +15 -0
  32. package/dist/esm/core.js +15 -0
  33. package/dist/esm/core.js.map +1 -0
  34. package/dist/esm/defaults.d.ts +2 -0
  35. package/dist/esm/defaults.js +3 -0
  36. package/dist/esm/defaults.js.map +1 -0
  37. package/dist/esm/files/FileStorage.d.ts +12 -3
  38. package/dist/esm/files/FileStorage.js +20 -0
  39. package/dist/esm/files/FileStorage.js.map +1 -1
  40. package/dist/esm/index.d.ts +7 -7
  41. package/dist/esm/index.js +2 -2
  42. package/dist/esm/index.js.map +1 -1
  43. package/dist/esm/internals.d.ts +14 -0
  44. package/dist/esm/internals.js +12 -0
  45. package/dist/esm/internals.js.map +1 -0
  46. package/dist/esm/libraries/Library.d.ts +78 -0
  47. package/dist/esm/{ServerLibrary.js → libraries/Library.js} +153 -144
  48. package/dist/esm/libraries/Library.js.map +1 -0
  49. package/dist/esm/{ServerLibrary.test.js → libraries/Library.test.js} +42 -33
  50. package/dist/esm/libraries/Library.test.js.map +1 -0
  51. package/dist/esm/libraries/generic.d.ts +8 -0
  52. package/dist/esm/libraries/generic.js +2 -0
  53. package/dist/esm/libraries/generic.js.map +1 -0
  54. package/dist/esm/libraries/singleNode.d.ts +53 -0
  55. package/dist/esm/libraries/singleNode.js +88 -0
  56. package/dist/esm/libraries/singleNode.js.map +1 -0
  57. package/dist/esm/logger.d.ts +1 -0
  58. package/dist/esm/logger.js +2 -0
  59. package/dist/esm/logger.js.map +1 -0
  60. package/dist/esm/storage/Storage.d.ts +39 -38
  61. package/dist/esm/storage/StorageManager.d.ts +11 -0
  62. package/dist/esm/storage/StorageManager.js +32 -0
  63. package/dist/esm/storage/StorageManager.js.map +1 -0
  64. package/dist/esm/storage/index.d.ts +1 -2
  65. package/dist/esm/storage/index.js +1 -2
  66. package/dist/esm/storage/index.js.map +1 -1
  67. package/dist/esm/storage/sqlShard/Databases.d.ts +5 -3
  68. package/dist/esm/storage/sqlShard/Databases.js +11 -7
  69. package/dist/esm/storage/sqlShard/Databases.js.map +1 -1
  70. package/dist/esm/storage/sqlShard/SqlBaselines.d.ts +16 -17
  71. package/dist/esm/storage/sqlShard/SqlBaselines.js +55 -59
  72. package/dist/esm/storage/sqlShard/SqlBaselines.js.map +1 -1
  73. package/dist/esm/storage/sqlShard/SqlFileMetadata.d.ts +13 -13
  74. package/dist/esm/storage/sqlShard/SqlFileMetadata.js +44 -57
  75. package/dist/esm/storage/sqlShard/SqlFileMetadata.js.map +1 -1
  76. package/dist/esm/storage/sqlShard/SqlOperations.d.ts +12 -12
  77. package/dist/esm/storage/sqlShard/SqlOperations.js +38 -73
  78. package/dist/esm/storage/sqlShard/SqlOperations.js.map +1 -1
  79. package/dist/esm/storage/sqlShard/SqlReplicas.d.ts +18 -17
  80. package/dist/esm/storage/sqlShard/SqlReplicas.js +63 -100
  81. package/dist/esm/storage/sqlShard/SqlReplicas.js.map +1 -1
  82. package/dist/esm/storage/sqlShard/database.d.ts +25 -5
  83. package/dist/esm/storage/sqlShard/database.js +47 -15
  84. package/dist/esm/storage/sqlShard/database.js.map +1 -1
  85. package/dist/esm/storage/sqlShard/migrations/v1.d.ts +2 -3
  86. package/dist/esm/storage/sqlShard/migrations/v1.js +42 -47
  87. package/dist/esm/storage/sqlShard/migrations/v1.js.map +1 -1
  88. package/dist/esm/storage/sqlShard/migrations/v2.d.ts +2 -3
  89. package/dist/esm/storage/sqlShard/migrations/v2.js +5 -15
  90. package/dist/esm/storage/sqlShard/migrations/v2.js.map +1 -1
  91. package/dist/esm/storage/sqlShard/migrations.d.ts +5 -7
  92. package/dist/esm/storage/sqlShard/migrations.js +48 -1
  93. package/dist/esm/storage/sqlShard/migrations.js.map +1 -1
  94. package/dist/esm/storage/sqlShard/sqlShardStorage.d.ts +25 -4
  95. package/dist/esm/storage/sqlShard/sqlShardStorage.js +43 -40
  96. package/dist/esm/storage/sqlShard/sqlShardStorage.js.map +1 -1
  97. package/dist/esm/storage/sqlShard/tables.d.ts +9 -6
  98. package/dist/esm/types.d.ts +1 -2
  99. package/package.json +24 -13
  100. package/src/TokenVerifier.ts +4 -0
  101. package/src/adapters/node/errorHandler.ts +18 -0
  102. package/src/adapters/node/getRequestToken.ts +34 -0
  103. package/src/adapters/node/httpRouter.ts +139 -0
  104. package/src/adapters/node/index.ts +5 -0
  105. package/src/adapters/node/tokenMiddleware.ts +32 -0
  106. package/src/adapters/node/websocketHandler.ts +79 -0
  107. package/src/{ClientConnection.ts → connections/ClientConnection.ts} +64 -57
  108. package/src/connections/MessageSender.ts +6 -0
  109. package/src/connections/Presence.ts +95 -0
  110. package/src/core.ts +36 -0
  111. package/src/defaults.ts +2 -0
  112. package/src/files/FileStorage.ts +36 -3
  113. package/src/index.ts +7 -7
  114. package/src/internals.ts +14 -0
  115. package/src/{ServerLibrary.test.ts → libraries/Library.test.ts} +51 -49
  116. package/src/{ServerLibrary.ts → libraries/Library.ts} +203 -206
  117. package/src/libraries/generic.ts +9 -0
  118. package/src/libraries/singleNode.ts +145 -0
  119. package/src/logger.ts +4 -0
  120. package/src/storage/Storage.ts +37 -66
  121. package/src/storage/StorageManager.ts +39 -0
  122. package/src/storage/index.ts +1 -2
  123. package/src/storage/sqlShard/Databases.ts +26 -13
  124. package/src/storage/sqlShard/SqlBaselines.ts +58 -67
  125. package/src/storage/sqlShard/SqlFileMetadata.ts +51 -72
  126. package/src/storage/sqlShard/SqlOperations.ts +57 -81
  127. package/src/storage/sqlShard/SqlReplicas.ts +80 -121
  128. package/src/storage/sqlShard/database.ts +82 -18
  129. package/src/storage/sqlShard/migrations/v1.ts +42 -52
  130. package/src/storage/sqlShard/migrations/v2.ts +5 -19
  131. package/src/storage/sqlShard/migrations.ts +66 -1
  132. package/src/storage/sqlShard/sqlShardStorage.ts +56 -55
  133. package/src/storage/sqlShard/tables.ts +6 -6
  134. package/src/types.ts +1 -2
  135. package/dist/esm/ClientConnection.d.ts +0 -19
  136. package/dist/esm/ClientConnection.js.map +0 -1
  137. package/dist/esm/MessageSender.d.ts +0 -5
  138. package/dist/esm/MessageSender.js.map +0 -1
  139. package/dist/esm/Presence.d.ts +0 -18
  140. package/dist/esm/Presence.js +0 -65
  141. package/dist/esm/Presence.js.map +0 -1
  142. package/dist/esm/ReplicaKeepaliveTimers.d.ts +0 -17
  143. package/dist/esm/ReplicaKeepaliveTimers.js +0 -27
  144. package/dist/esm/ReplicaKeepaliveTimers.js.map +0 -1
  145. package/dist/esm/Server.d.ts +0 -166
  146. package/dist/esm/Server.js +0 -572
  147. package/dist/esm/Server.js.map +0 -1
  148. package/dist/esm/ServerLibrary.d.ts +0 -64
  149. package/dist/esm/ServerLibrary.js.map +0 -1
  150. package/dist/esm/ServerLibrary.test.js.map +0 -1
  151. package/dist/esm/storage/sql/SqlBaselines.d.ts +0 -22
  152. package/dist/esm/storage/sql/SqlBaselines.js +0 -107
  153. package/dist/esm/storage/sql/SqlBaselines.js.map +0 -1
  154. package/dist/esm/storage/sql/SqlFileMetadata.d.ts +0 -18
  155. package/dist/esm/storage/sql/SqlFileMetadata.js +0 -69
  156. package/dist/esm/storage/sql/SqlFileMetadata.js.map +0 -1
  157. package/dist/esm/storage/sql/SqlOperations.d.ts +0 -19
  158. package/dist/esm/storage/sql/SqlOperations.js +0 -122
  159. package/dist/esm/storage/sql/SqlOperations.js.map +0 -1
  160. package/dist/esm/storage/sql/SqlReplicas.d.ts +0 -32
  161. package/dist/esm/storage/sql/SqlReplicas.js +0 -163
  162. package/dist/esm/storage/sql/SqlReplicas.js.map +0 -1
  163. package/dist/esm/storage/sql/database.d.ts +0 -9
  164. package/dist/esm/storage/sql/database.js +0 -24
  165. package/dist/esm/storage/sql/database.js.map +0 -1
  166. package/dist/esm/storage/sql/migrations/v1.d.ts +0 -3
  167. package/dist/esm/storage/sql/migrations/v1.js +0 -59
  168. package/dist/esm/storage/sql/migrations/v1.js.map +0 -1
  169. package/dist/esm/storage/sql/migrations/v2.d.ts +0 -3
  170. package/dist/esm/storage/sql/migrations/v2.js +0 -16
  171. package/dist/esm/storage/sql/migrations/v2.js.map +0 -1
  172. package/dist/esm/storage/sql/migrations.d.ts +0 -7
  173. package/dist/esm/storage/sql/migrations.js +0 -4
  174. package/dist/esm/storage/sql/migrations.js.map +0 -1
  175. package/dist/esm/storage/sql/sqlStorage.d.ts +0 -6
  176. package/dist/esm/storage/sql/sqlStorage.js +0 -35
  177. package/dist/esm/storage/sql/sqlStorage.js.map +0 -1
  178. package/dist/esm/storage/sql/tables.d.ts +0 -44
  179. package/dist/esm/storage/sql/tables.js +0 -2
  180. package/dist/esm/storage/sql/tables.js.map +0 -1
  181. package/dist/esm/storage/sqlShard/transfer.d.ts +0 -7
  182. package/dist/esm/storage/sqlShard/transfer.js +0 -90
  183. package/dist/esm/storage/sqlShard/transfer.js.map +0 -1
  184. package/dist/esm/storage/sqlShard/transfer.test.d.ts +0 -1
  185. package/dist/esm/storage/sqlShard/transfer.test.js +0 -109
  186. package/dist/esm/storage/sqlShard/transfer.test.js.map +0 -1
  187. package/src/MessageSender.ts +0 -10
  188. package/src/Presence.ts +0 -86
  189. package/src/ReplicaKeepaliveTimers.ts +0 -34
  190. package/src/Server.ts +0 -859
  191. package/src/storage/sql/SqlBaselines.ts +0 -147
  192. package/src/storage/sql/SqlFileMetadata.ts +0 -94
  193. package/src/storage/sql/SqlOperations.ts +0 -160
  194. package/src/storage/sql/SqlReplicas.ts +0 -234
  195. package/src/storage/sql/database.ts +0 -32
  196. package/src/storage/sql/migrations/v1.ts +0 -64
  197. package/src/storage/sql/migrations/v2.ts +0 -20
  198. package/src/storage/sql/migrations.ts +0 -4
  199. package/src/storage/sql/sqlStorage.ts +0 -57
  200. package/src/storage/sql/tables.ts +0 -51
  201. package/src/storage/sqlShard/transfer.test.ts +0 -149
  202. package/src/storage/sqlShard/transfer.ts +0 -123
  203. /package/dist/esm/{MessageSender.js → connections/MessageSender.js} +0 -0
  204. /package/dist/esm/{ServerLibrary.test.d.ts → libraries/Library.test.d.ts} +0 -0
@@ -1,34 +1,34 @@
1
1
  import {
2
2
  AckMessage,
3
+ applyOperations,
3
4
  ClientMessage,
4
5
  DocumentBaseline,
5
6
  EventSubscriber,
6
7
  HeartbeatMessage,
8
+ isFileRef,
9
+ isObjectRef,
7
10
  Operation,
8
11
  OperationMessage,
9
12
  PresenceUpdateMessage,
10
13
  Ref,
11
14
  ReplicaType,
12
- SyncMessage,
13
- applyOperations,
14
- isFileRef,
15
- isObjectRef,
16
15
  rewriteAuthzOriginator,
16
+ SyncMessage,
17
17
  } from '@verdant-web/common';
18
- import { MessageSender } from './MessageSender.js';
19
- import { Presence } from './Presence.js';
20
- import { UserProfileLoader } from './Profiles.js';
21
- import { TokenInfo } from './TokenVerifier.js';
22
- import { FileStorage } from './files/FileStorage.js';
23
- import { Storage } from './storage/Storage.js';
18
+ import { MessageSender } from '../connections/MessageSender.js';
19
+ import { Presence } from '../connections/Presence.js';
20
+ import { FileInfo, FileStorageLibraryDelegate } from '../files/FileStorage.js';
21
+ import { Logger } from '../logger.js';
22
+ import { Storage } from '../storage/Storage.js';
23
+ import { TokenInfo } from '../TokenVerifier.js';
24
24
  import {
25
25
  LibraryInfo,
26
26
  StoredDocumentBaseline,
27
27
  StoredOperation,
28
28
  StoredReplicaInfo,
29
- } from './types.js';
29
+ } from '../types.js';
30
30
 
31
- export type ServerLibraryEvents = {
31
+ export type LibraryEvents = {
32
32
  changes: (
33
33
  info: TokenInfo,
34
34
  operations: Operation[],
@@ -36,55 +36,63 @@ export type ServerLibraryEvents = {
36
36
  ) => void;
37
37
  };
38
38
 
39
- export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
39
+ export interface LibraryFileInfo {
40
+ id: string;
41
+ name: string;
42
+ type: string;
43
+ url: string | null;
44
+ libraryId: string;
45
+ }
46
+
47
+ export class Library {
40
48
  private storage;
41
49
  private sender;
42
- private profiles;
43
- private presences;
44
50
  private disableRebasing: boolean;
45
- private fileStorage: FileStorage | undefined;
51
+ private fileStorage;
52
+ private events;
53
+ private id: string;
54
+ private presence: Presence;
55
+
56
+ private log: Logger;
46
57
 
47
- private log: (...args: any[]) => void;
48
58
  constructor({
49
59
  storage,
50
60
  sender,
51
- profiles,
52
61
  log = () => {},
53
62
  disableRebasing,
54
63
  fileStorage,
64
+ events,
65
+ id,
66
+ presence,
55
67
  }: {
56
68
  storage: Storage;
57
69
  sender: MessageSender;
58
- profiles: UserProfileLoader<any>;
59
- log?: (...args: any[]) => void;
70
+ log?: Logger;
60
71
  disableRebasing?: boolean;
61
- fileStorage?: FileStorage;
72
+ fileStorage?: FileStorageLibraryDelegate;
73
+ events?: EventSubscriber<LibraryEvents>;
74
+ id: string;
75
+ presence: Presence;
62
76
  }) {
63
- super();
64
-
77
+ this.id = id;
78
+ this.storage = storage;
79
+ this.sender = sender;
65
80
  this.log = log;
66
81
  this.disableRebasing = !!disableRebasing;
67
- this.sender = sender;
68
- this.profiles = profiles;
69
- this.presences = new Presence(profiles);
70
- this.storage = storage;
71
82
  this.fileStorage = fileStorage;
72
- this.presences.on('lost', this.onPresenceLost);
83
+ this.events = events;
84
+ this.presence = presence;
85
+ this.presence.subscribe('lost', this.onPresenceLost);
73
86
  }
74
87
 
75
- /**
76
- * Validates a user's access to a replica. If the replica does not
77
- * exist, a user may create it. But if it does exist, its user (clientId)
78
- * must match the user making the request.
79
- */
80
88
  private validateReplicaAccess = async (
81
89
  replicaId: string,
82
90
  clientKey: string,
83
91
  info: TokenInfo,
84
92
  ) => {
85
- const replica = await this.storage.replicas.get(info.libraryId, replicaId);
93
+ const replica = await this.storage.replicas.get(replicaId);
86
94
  if (replica && replica.clientId !== info.userId) {
87
- this.sender.send(info.libraryId, clientKey, {
95
+ this.sender.respond(clientKey, {
88
96
  type: 'forbidden',
89
97
  });
90
98
  return false;
@@ -99,18 +107,22 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
99
107
  );
100
108
  };
101
109
 
102
- receive = async (
110
+ handleMessage = async (
103
111
  message: ClientMessage,
104
112
  clientKey: string,
105
113
  info: TokenInfo,
106
114
  ) => {
107
- await this.storage.ready;
115
+ if (message.type === 'heartbeat') {
116
+ this.handleHeartbeat(message, clientKey, info);
117
+ return;
118
+ }
108
119
 
109
120
  const allowed = await this.validateReplicaAccess(
110
121
  message.replicaId,
111
122
  clientKey,
112
123
  info,
113
124
  );
125
+
114
126
  if (!allowed) return;
115
127
 
116
128
  switch (message.type) {
@@ -123,14 +135,11 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
123
135
  case 'ack':
124
136
  await this.handleAck(message, clientKey, info);
125
137
  break;
126
- case 'heartbeat':
127
- await this.handleHeartbeat(message, clientKey, info);
128
- break;
129
138
  case 'presence-update':
130
139
  await this.handlePresenceUpdate(message, clientKey, info);
131
140
  break;
132
141
  default:
133
- this.log('Unknown message type', (message as any).type);
142
+ this.log('error', 'Unknown message type', (message as any).type);
134
143
  break;
135
144
  }
136
145
  // a bit fragile - skipping presence-update since currently
@@ -138,15 +147,39 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
138
147
  // sees a recent last seen for the replica even if it was
139
148
  // truant before the presence-update.
140
149
  if (message.type !== 'presence-update') {
141
- await this.storage.replicas.updateLastSeen(
142
- info.libraryId,
143
- message.replicaId,
144
- );
150
+ await this.storage.replicas.updateLastSeen(message.replicaId);
145
151
  }
146
152
  };
147
153
 
148
- remove = (libraryId: string, replicaId: string) => {
149
- this.presences.removeReplica(libraryId, replicaId);
154
+ private rebroadcastOperations = async (
155
+ libraryId: string,
156
+ clientKey: string,
157
+ replicaId: string,
158
+ ops: Operation[],
159
+ baselines?: DocumentBaseline[],
160
+ ) => {
161
+ if (ops.length === 0 && !baselines?.length) return;
162
+
163
+ this.log(
164
+ 'info',
165
+ 'Rebroadcasting',
166
+ ops.length,
167
+ 'operations',
168
+ baselines?.length ?? 0,
169
+ 'baselines',
170
+ );
171
+
172
+ this.sender.broadcast(
173
+ {
174
+ type: 'op-re',
175
+ operations: ops,
176
+ baselines,
177
+ replicaId,
178
+ globalAckTimestamp:
179
+ (await this.storage.replicas.getGlobalAck()) ?? undefined,
180
+ },
181
+ [clientKey],
182
+ );
150
183
  };
151
184
 
152
185
  private handleOperation = async (
@@ -157,7 +190,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
157
190
  if (!message.operations.length) return;
158
191
 
159
192
  if (!this.hasWriteAccess(info)) {
160
- this.sender.send(info.libraryId, clientKey, {
193
+ this.sender.respond(clientKey, {
161
194
  type: 'forbidden',
162
195
  });
163
196
  return;
@@ -175,7 +208,6 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
175
208
 
176
209
  // first, record the replica's serverOrder before insertion.
177
210
  const replicaInfo = await this.storage.replicas.getOrCreate(
178
- info.libraryId,
179
211
  message.replicaId,
180
212
  {
181
213
  userId: info.userId,
@@ -183,7 +215,6 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
183
215
  },
184
216
  );
185
217
  const newServerOrder = await this.storage.operations.insertAll(
186
- info.libraryId,
187
218
  message.replicaId,
188
219
  message.operations,
189
220
  );
@@ -193,10 +224,10 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
193
224
  message.operations.length,
194
225
  );
195
226
 
196
- this.enqueueRebase(info.libraryId);
227
+ this.enqueueRebase();
197
228
 
198
229
  // tell sender we got and processed their operation
199
- this.sender.send(info.libraryId, clientKey, {
230
+ this.sender.respond(clientKey, {
200
231
  type: 'server-ack',
201
232
  timestamp: message.timestamp,
202
233
  });
@@ -207,7 +238,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
207
238
  info,
208
239
  );
209
240
 
210
- this.emit(`changes`, info, message.operations, []);
241
+ this.events?.emit(`changes`, info, message.operations, []);
211
242
  };
212
243
 
213
244
  /**
@@ -225,7 +256,6 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
225
256
  insertedOperationsCount
226
257
  ) {
227
258
  await this.storage.replicas.updateAckedServerOrder(
228
- replicaInfo.libraryId,
229
259
  replicaInfo.id,
230
260
  newServerOrder,
231
261
  );
@@ -239,38 +269,6 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
239
269
  }
240
270
  };
241
271
 
242
- private rebroadcastOperations = async (
243
- libraryId: string,
244
- clientKey: string,
245
- replicaId: string,
246
- ops: Operation[],
247
- baselines?: DocumentBaseline[],
248
- ) => {
249
- if (ops.length === 0 && !baselines?.length) return;
250
-
251
- this.log(
252
- 'info',
253
- 'Rebroadcasting',
254
- ops.length,
255
- 'operations',
256
- baselines?.length ?? 0,
257
- 'baselines',
258
- );
259
-
260
- this.sender.broadcast(
261
- libraryId,
262
- {
263
- type: 'op-re',
264
- operations: ops,
265
- baselines,
266
- replicaId,
267
- globalAckTimestamp:
268
- (await this.storage.replicas.getGlobalAck(libraryId)) ?? undefined,
269
- },
270
- [clientKey],
271
- );
272
- };
273
-
274
272
  private handleSync = async (
275
273
  message: SyncMessage,
276
274
  clientKey: string,
@@ -284,7 +282,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
284
282
  !this.hasWriteAccess(info) &&
285
283
  (message.operations.length > 0 || message.baselines.length > 0)
286
284
  ) {
287
- this.sender.send(info.libraryId, clientKey, {
285
+ this.sender.respond(clientKey, {
288
286
  type: 'forbidden',
289
287
  });
290
288
  this.log('warn', 'sync with changes from read-only replica', replicaId);
@@ -293,11 +291,11 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
293
291
 
294
292
  if (message.resyncAll) {
295
293
  // forget our local understanding of the replica and reset it
296
- await this.storage.replicas.delete(info.libraryId, replicaId);
294
+ await this.storage.replicas.delete(replicaId);
297
295
  }
298
296
 
299
297
  const { status, replicaInfo: clientReplicaInfo } =
300
- await this.storage.replicas.getOrCreate(info.libraryId, replicaId, info);
298
+ await this.storage.replicas.getOrCreate(replicaId, info);
301
299
 
302
300
  this.log(
303
301
  'info',
@@ -309,7 +307,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
309
307
  let overrideTruant = false;
310
308
  if (status === 'truant') {
311
309
  const latestServerOrder =
312
- await this.storage.operations.getLatestServerOrder(info.libraryId);
310
+ await this.storage.operations.getLatestServerOrder();
313
311
  if (clientReplicaInfo.ackedServerOrder >= latestServerOrder) {
314
312
  overrideTruant = true;
315
313
  this.log(
@@ -321,7 +319,6 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
321
319
 
322
320
  // lookup operations after the last ack the client gave us
323
321
  const ops = await this.storage.operations.getAfterServerOrder(
324
- info.libraryId,
325
322
  clientReplicaInfo.ackedServerOrder,
326
323
  );
327
324
 
@@ -336,7 +333,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
336
333
  // replica will generate the same baseline locally, so it
337
334
  // doesn't need to receive it from the server.
338
335
  const baselines = replicaShouldReset
339
- ? await this.storage.baselines.getAll(info.libraryId)
336
+ ? await this.storage.baselines.getAll()
340
337
  : [];
341
338
 
342
339
  // We detect that a library is new by checking if it has any history.
@@ -377,7 +374,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
377
374
  'Detected local data is incomplete, requesting full history from replica',
378
375
  replicaId,
379
376
  );
380
- this.sender.send(info.libraryId, clientKey, {
377
+ this.sender.respond(clientKey, {
381
378
  type: 'need-since',
382
379
  since: null,
383
380
  });
@@ -412,7 +409,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
412
409
  // we are not overwriting the replica's data
413
410
  if (!overwriteReplicaData) {
414
411
  // store all incoming operations and baselines
415
- await this.storage.baselines.insertAll(info.libraryId, message.baselines);
412
+ await this.storage.baselines.insertAll(message.baselines);
416
413
 
417
414
  this.log(
418
415
  'debug',
@@ -423,7 +420,6 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
423
420
  'operations',
424
421
  );
425
422
  const newServerOrder = await this.storage.operations.insertAll(
426
- info.libraryId,
427
423
  replicaId,
428
424
  message.operations,
429
425
  );
@@ -433,7 +429,6 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
433
429
  message.operations.length,
434
430
  );
435
431
  await this.storage.replicas.updateAcknowledgedLogicalTime(
436
- info.libraryId,
437
432
  replicaId,
438
433
  message.timestamp,
439
434
  );
@@ -450,7 +445,12 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
450
445
  isEmptyLibrary ? message.baselines : undefined,
451
446
  );
452
447
  if (message.operations.length || message.baselines.length) {
453
- this.emit(`changes`, info, message.operations, message.baselines);
448
+ this.events?.emit(
449
+ `changes`,
450
+ info,
451
+ message.operations,
452
+ message.baselines,
453
+ );
454
454
  }
455
455
  }
456
456
 
@@ -476,25 +476,32 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
476
476
  'operations and',
477
477
  baselines.length,
478
478
  'baselines',
479
+ 'to client key',
480
+ clientKey,
479
481
  );
480
482
 
481
483
  // mutating baselines/operations to remove extra data -
482
484
  // mutating just to save on allocations here.
483
485
 
484
- this.sender.send(info.libraryId, clientKey, {
485
- type: 'sync-resp',
486
- operations: this.removeOperationExtras(ops),
487
- baselines: this.removeBaselineExtras(baselines),
488
- globalAckTimestamp:
489
- (await this.storage.replicas.getGlobalAck(info.libraryId)) ?? undefined,
490
- peerPresence: this.presences.all(info.libraryId),
491
- // only request the client to overwrite local data if a reset is requested
492
- // and there is data to overwrite it. otherwise the client may still
493
- // send its own history to us.
494
- overwriteLocalData: overwriteReplicaData,
495
- ackedTimestamp: message.timestamp,
496
- ackThisNonce,
497
- });
486
+ try {
487
+ this.sender.respond(clientKey, {
488
+ type: 'sync-resp',
489
+ operations: this.removeOperationExtras(ops),
490
+ baselines: this.removeBaselineExtras(baselines),
491
+ globalAckTimestamp:
492
+ (await this.storage.replicas.getGlobalAck()) ?? undefined,
493
+ peerPresence: this.presence.all(),
494
+ // only request the client to overwrite local data if a reset is requested
495
+ // and there is data to overwrite it. otherwise the client may still
496
+ // send its own history to us.
497
+ overwriteLocalData: overwriteReplicaData,
498
+ ackedTimestamp: message.timestamp,
499
+ ackThisNonce,
500
+ });
501
+ } catch (err) {
502
+ this.log('error', err);
503
+ throw err;
504
+ }
498
505
  };
499
506
 
500
507
  private createAckNonce = (serverOrder: number): string | undefined => {
@@ -511,6 +518,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
511
518
  Buffer.from(message.nonce, 'base64').toString('utf8'),
512
519
  );
513
520
  this.log(
521
+ 'debug',
514
522
  'Ack from',
515
523
  message.replicaId,
516
524
  '(user',
@@ -526,21 +534,17 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
526
534
 
527
535
  const replicaId = message.replicaId;
528
536
  await this.storage.replicas.updateAckedServerOrder(
529
- info.libraryId,
530
537
  replicaId,
531
538
  decodedNonce,
532
539
  );
533
540
  } else if (message.timestamp) {
534
541
  await this.storage.replicas.acknowledgeOperation(
535
- info.libraryId,
536
542
  message.replicaId,
537
543
  message.timestamp,
538
544
  );
539
- const globalAck = await this.storage.replicas.getGlobalAck(
540
- info.libraryId,
541
- );
545
+ const globalAck = await this.storage.replicas.getGlobalAck();
542
546
  if (globalAck) {
543
- this.sender.broadcast(info.libraryId, {
547
+ this.sender.broadcast({
544
548
  type: 'global-ack',
545
549
  timestamp: globalAck,
546
550
  });
@@ -555,16 +559,12 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
555
559
  ignoreClientKeys: string[] = [],
556
560
  ) => {
557
561
  await this.storage.replicas.updateAcknowledgedLogicalTime(
558
- info.libraryId,
559
562
  replicaId,
560
563
  timestamp,
561
564
  );
562
- const newGlobalAck = await this.storage.replicas.getGlobalAck(
563
- info.libraryId,
564
- );
565
+ const newGlobalAck = await this.storage.replicas.getGlobalAck();
565
566
  if (newGlobalAck) {
566
567
  this.sender.broadcast(
567
- info.libraryId,
568
568
  {
569
569
  type: 'global-ack',
570
570
  timestamp: newGlobalAck,
@@ -575,15 +575,15 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
575
575
  };
576
576
 
577
577
  private pendingRebaseTimeout: NodeJS.Timeout | null = null;
578
- private enqueueRebase = (libraryId: string) => {
578
+ private enqueueRebase = () => {
579
579
  if (this.disableRebasing) return;
580
580
 
581
581
  if (!this.pendingRebaseTimeout) {
582
- setTimeout(() => this.rebase(libraryId), 0);
582
+ setTimeout(() => this.rebase(), 0);
583
583
  }
584
584
  };
585
585
 
586
- private rebase = async (libraryId: string) => {
586
+ private rebase = async () => {
587
587
  if (this.disableRebasing) return;
588
588
 
589
589
  this.log('debug', 'Performing rebase check');
@@ -604,15 +604,13 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
604
604
  // for global ack, to determine consensus, also allow
605
605
  // for all actively connected replicas to ack regardless of their
606
606
  // type.
607
- const activeReplicaIds = Object.values(this.presences.all(libraryId)).map(
607
+ const activeReplicaIds = Object.values(this.presence.all()).map(
608
608
  (p) => p.replicaId,
609
609
  );
610
- const globalAck = await this.storage.replicas.getGlobalAck(
611
- libraryId,
612
- activeReplicaIds,
613
- );
610
+ const globalAck =
611
+ await this.storage.replicas.getGlobalAck(activeReplicaIds);
614
612
  const globalServerOrder =
615
- await this.storage.replicas.getEarliestAckedServerOrder(libraryId);
613
+ await this.storage.replicas.getEarliestAckedServerOrder();
616
614
 
617
615
  if (!globalAck) {
618
616
  this.log('debug', 'No global ack, skipping rebase');
@@ -625,10 +623,8 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
625
623
  }
626
624
 
627
625
  // these are in forward chronological order
628
- const ops = await this.storage.operations.getBeforeServerOrder(
629
- libraryId,
630
- globalServerOrder,
631
- );
626
+ const ops =
627
+ await this.storage.operations.getBeforeServerOrder(globalServerOrder);
632
628
 
633
629
  const opsToApply: Record<string, StoredOperation[]> = {};
634
630
  // if we encounter a sequential operation in history which does
@@ -648,18 +644,17 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
648
644
 
649
645
  const deletedRefs = new Array<Ref>();
650
646
  for (const [documentId, ops] of Object.entries(opsToApply)) {
651
- this.log('Rebasing', documentId);
647
+ this.log('debug', 'Rebasing', documentId);
652
648
  await this.storage.baselines.applyOperations(
653
- libraryId,
654
649
  documentId,
655
650
  ops,
656
651
  deletedRefs,
657
652
  );
658
- await this.storage.operations.delete(libraryId, ops);
653
+ await this.storage.operations.delete(ops);
659
654
  }
660
655
 
661
656
  // hint to clients they can rebase too
662
- this.sender.broadcast(libraryId, {
657
+ this.sender.broadcast({
663
658
  type: 'global-ack',
664
659
  timestamp: globalAck,
665
660
  });
@@ -668,9 +663,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
668
663
  await Promise.all(
669
664
  deletedRefs
670
665
  .filter(isFileRef)
671
- .map((ref) =>
672
- this.storage.fileMetadata.markPendingDelete(libraryId, ref.id),
673
- ),
666
+ .map((ref) => this.storage.fileMetadata.markPendingDelete(ref.id)),
674
667
  );
675
668
  };
676
669
 
@@ -679,7 +672,7 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
679
672
  clientKey: string,
680
673
  info: TokenInfo,
681
674
  ) => {
682
- this.sender.send(info.libraryId, clientKey, {
675
+ this.sender.respond(clientKey, {
683
676
  type: 'heartbeat-response',
684
677
  });
685
678
  };
@@ -689,14 +682,13 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
689
682
  clientKey: string,
690
683
  info: TokenInfo,
691
684
  ) => {
692
- const updated = await this.presences.set(info.libraryId, info.userId, {
685
+ const updated = await this.presence.set(clientKey, info.userId, {
693
686
  presence: message.presence,
694
687
  internal: message.internal,
695
688
  replicaId: message.replicaId,
696
689
  id: info.userId,
697
690
  });
698
691
  this.sender.broadcast(
699
- info.libraryId,
700
692
  {
701
693
  type: 'presence-changed',
702
694
  replicaId: message.replicaId,
@@ -707,27 +699,21 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
707
699
  );
708
700
  };
709
701
 
710
- private onPresenceLost = async (
711
- libraryId: string,
712
- replicaId: string,
713
- userId: string,
714
- ) => {
702
+ private onPresenceLost = async (replicaId: string, userId: string) => {
715
703
  this.log('info', 'User disconnected from all replicas:', userId);
716
- this.sender.broadcast(libraryId, {
704
+ this.sender.broadcast({
717
705
  type: 'presence-offline',
718
706
  replicaId,
719
707
  userId,
720
708
  });
721
- if (Object.keys(this.presences.all(libraryId)).length === 0) {
722
- this.log('info', `All users have disconnected from ${libraryId}`);
709
+ if (Object.keys(this.presence.all()).length === 0) {
710
+ this.log('info', `All users have disconnected`);
723
711
  // could happen - if the server is shutting down manually
724
712
  if (!this.storage.open) {
725
713
  this.log('debug', 'Database not open, skipping cleanup');
726
714
  return;
727
715
  }
728
- const pendingDelete = await this.storage.fileMetadata.getPendingDelete(
729
- libraryId,
730
- );
716
+ const pendingDelete = await this.storage.fileMetadata.getPendingDelete();
731
717
  if (pendingDelete.length > 0) {
732
718
  if (!this.fileStorage) {
733
719
  throw new Error('File storage not configured, cannot delete files');
@@ -741,12 +727,11 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
741
727
  pendingDelete.map(async (fileInfo): Promise<void> => {
742
728
  try {
743
729
  await this.fileStorage?.delete({
744
- libraryId: fileInfo.libraryId,
745
730
  fileName: fileInfo.name,
746
731
  id: fileInfo.fileId,
747
732
  type: fileInfo.type,
748
733
  });
749
- this.storage.fileMetadata.delete(libraryId, fileInfo.fileId);
734
+ await this.storage.fileMetadata.delete(fileInfo.fileId);
750
735
  } catch (e) {
751
736
  this.log(
752
737
  'error',
@@ -762,43 +747,39 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
762
747
  }
763
748
  };
764
749
 
765
- destroy = async (libraryId: string) => {
766
- this.log('info', 'Destroying library', libraryId);
767
- this.presences.clear(libraryId);
750
+ destroy = async () => {
751
+ this.log('info', 'Destroying library');
752
+ this.presence.clear();
768
753
  await Promise.all([
769
- this.storage.replicas.deleteAll(libraryId),
770
- this.storage.operations.deleteAll(libraryId),
771
- this.storage.baselines.deleteAll(libraryId),
754
+ this.storage.replicas.deleteAll(),
755
+ this.storage.operations.deleteAll(),
756
+ this.storage.baselines.deleteAll(),
772
757
  ]);
773
- const allFiles = await this.storage.fileMetadata.getAll(libraryId);
758
+ const allFiles = await this.storage.fileMetadata.getAll();
774
759
  if (allFiles.length > 0) {
775
760
  this.log(
776
761
  'info',
777
- `Deleting ${allFiles.length} files for library ${libraryId}`,
762
+ `Deleting ${allFiles.length} files`,
778
763
  allFiles.map((f) => f.fileId).join(', '),
779
764
  );
780
765
  for (const fileInfo of allFiles) {
781
766
  await this.fileStorage?.delete({
782
- libraryId,
783
767
  fileName: fileInfo.name,
784
768
  id: fileInfo.fileId,
785
769
  type: fileInfo.type,
786
770
  });
787
771
  }
788
- await this.storage.fileMetadata.deleteAll(libraryId);
772
+ await this.storage.fileMetadata.deleteAll();
789
773
  }
790
774
  };
791
775
 
792
- getPresence = (libraryId: string) => {
793
- return this.presences.all(libraryId);
776
+ getPresence = () => {
777
+ return Promise.resolve(this.presence.all());
794
778
  };
795
779
 
796
- getInfo = async (libraryId: string): Promise<LibraryInfo | null> => {
797
- const rawReplicas = await this.storage.replicas.getAll(libraryId);
798
- const profiles = await Promise.all(
799
- rawReplicas.map((r) => this.profiles.get(r.clientId)),
800
- );
801
- const replicas = rawReplicas.map((r, index) => ({
780
+ getInfo = async (): Promise<LibraryInfo | null> => {
781
+ const rawReplicas = await this.storage.replicas.getAll();
782
+ const replicas = rawReplicas.map((r) => ({
802
783
  id: r.id,
803
784
  ackedLogicalTime: r.ackedLogicalTime,
804
785
  ackedServerOrder: r.ackedServerOrder,
@@ -806,18 +787,15 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
806
787
  truant:
807
788
  !!r.lastSeenWallClockTime &&
808
789
  r.lastSeenWallClockTime < this.storage.replicas.truantCutoff,
809
- profile: profiles[index],
810
790
  }));
811
791
 
812
792
  const data = {
813
- id: libraryId,
793
+ id: this.id,
814
794
  replicas,
815
- latestServerOrder: await this.storage.operations.getLatestServerOrder(
816
- libraryId,
817
- ),
818
- operationsCount: await this.storage.operations.getCount(libraryId),
819
- baselinesCount: await this.storage.baselines.getCount(libraryId),
820
- globalAck: (await this.storage.replicas.getGlobalAck(libraryId)) ?? null,
795
+ latestServerOrder: await this.storage.operations.getLatestServerOrder(),
796
+ operationsCount: await this.storage.operations.getCount(),
797
+ baselinesCount: await this.storage.baselines.getCount(),
798
+ globalAck: (await this.storage.replicas.getGlobalAck()) ?? null,
821
799
  };
822
800
 
823
801
  if (
@@ -831,35 +809,53 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
831
809
  return data;
832
810
  };
833
811
 
834
- evictUser = async (libraryId: string, userId: string) => {
835
- await this.storage.replicas.deleteAllForUser(libraryId, userId);
812
+ evictUser = async (userId: string) => {
813
+ await this.storage.replicas.deleteAllForUser(userId);
814
+ };
815
+
816
+ forceTruant = async (replicaId: string) => {
817
+ await this.storage.replicas.forceTruant(replicaId);
836
818
  };
837
819
 
838
- getDocumentSnapshot = (libraryId: string, oid: string) => {
839
- return this.hydrateObject(libraryId, oid);
820
+ getDocumentSnapshot = (oid: string) => {
821
+ return this.hydrateObject(oid);
840
822
  };
841
823
 
842
- private getObjectSnapshot = async (libraryId: string, oid: string) => {
824
+ getFileInfo = async (fileId: string) => {
825
+ const rawFile = await this.hydrateFile(fileId);
826
+ if (rawFile) {
827
+ return {
828
+ ...rawFile,
829
+ libraryId: this.id,
830
+ };
831
+ } else {
832
+ return null;
833
+ }
834
+ };
835
+
836
+ putFileInfo: (info: FileInfo) => Promise<void> = async (info) => {
837
+ this.log('debug', 'Storing file info', info.id, info.fileName);
838
+ await this.storage.fileMetadata.put(info);
839
+ };
840
+
841
+ private getObjectSnapshot = async (oid: string) => {
843
842
  const [baseline, ops] = await Promise.all([
844
- this.storage.baselines.get(libraryId, oid),
845
- this.storage.operations.getAll(libraryId, oid),
843
+ this.storage.baselines.get(oid),
844
+ this.storage.operations.getAll(oid),
846
845
  ]);
847
846
  const snapshot = applyOperations(baseline?.snapshot ?? undefined, ops);
848
847
  return snapshot;
849
848
  };
850
849
 
851
- private hydrateObject = async (
852
- libraryId: string,
853
- oid: string,
854
- ): Promise<any> => {
855
- const snapshot = await this.getObjectSnapshot(libraryId, oid);
850
+ private hydrateObject = async (oid: string): Promise<any> => {
851
+ const snapshot = await this.getObjectSnapshot(oid);
856
852
  if (Array.isArray(snapshot)) {
857
853
  return Promise.all(
858
854
  snapshot.map(async (item: any) => {
859
855
  if (isObjectRef(item)) {
860
- return this.hydrateObject(libraryId, item.id);
856
+ return this.hydrateObject(item.id);
861
857
  } else if (isFileRef(item)) {
862
- return this.hydrateFile(libraryId, item.id);
858
+ return this.hydrateFile(item.id);
863
859
  } else {
864
860
  return item;
865
861
  }
@@ -870,9 +866,9 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
870
866
  await Promise.all(
871
867
  Object.entries(snapshot).map(async ([key, value]) => {
872
868
  if (isObjectRef(value)) {
873
- hydrated[key] = await this.hydrateObject(libraryId, value.id);
869
+ hydrated[key] = await this.hydrateObject(value.id);
874
870
  } else if (isFileRef(value)) {
875
- hydrated[key] = await this.hydrateFile(libraryId, value.id);
871
+ hydrated[key] = await this.hydrateFile(value.id);
876
872
  }
877
873
  }),
878
874
  );
@@ -882,22 +878,23 @@ export class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
882
878
  }
883
879
  };
884
880
 
885
- private hydrateFile = async (libraryId: string, fileId: string) => {
886
- const data = await this.storage.fileMetadata.get(libraryId, fileId);
881
+ private hydrateFile = async (
882
+ fileId: string,
883
+ ): Promise<Omit<LibraryFileInfo, 'libraryId'> | null> => {
884
+ const data = await this.storage.fileMetadata.get(fileId);
887
885
  if (data) {
888
886
  const url =
889
- this.fileStorage?.getUrl({
887
+ (await this.fileStorage?.getUrl({
890
888
  fileName: data.name,
891
889
  id: data.fileId,
892
- libraryId,
893
890
  type: data.type,
894
- }) ?? null;
891
+ })) ?? null;
895
892
 
896
893
  return {
897
- url,
898
894
  id: data.fileId,
899
895
  name: data.name,
900
896
  type: data.type,
897
+ url,
901
898
  };
902
899
  } else {
903
900
  return null;