@verdant-web/server 3.3.11 → 4.0.1

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} +165 -151
  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 +2 -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} +205 -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 +2 -4
  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
@@ -0,0 +1,9 @@
1
+ import { LibraryInfo } from '../types.js';
2
+ import { LibraryFileInfo } from './Library.js';
3
+
4
+ export interface LibraryApi {
5
+ getFileInfo(fileId: string): Promise<LibraryFileInfo | null>;
6
+ forceTruant: (replicaId: string) => Promise<void>;
7
+ getInfo: () => Promise<LibraryInfo | null>;
8
+ getDocumentSnapshot: (collection: string, id: string) => any;
9
+ }
@@ -0,0 +1,145 @@
1
+ import {
2
+ ClientMessage,
3
+ createOid,
4
+ EventSubscriber,
5
+ VerdantError,
6
+ } from '@verdant-web/common';
7
+ import { Readable } from 'stream';
8
+ import { ReadableStream } from 'stream/web';
9
+ import { UserProfileLoader } from '../Profiles.js';
10
+ import { TokenInfo, TokenVerifier } from '../TokenVerifier.js';
11
+ import { ClientConnectionManager } from '../connections/ClientConnection.js';
12
+ import {
13
+ FileInfo,
14
+ FileStorage,
15
+ FileStorageLibraryDelegate,
16
+ } from '../files/FileStorage.js';
17
+ import { Library } from '../internals.js';
18
+ import { Logger } from '../logger.js';
19
+ import { StorageFactory } from '../storage/Storage.js';
20
+ import { LibraryEvents } from './Library.js';
21
+ import { LibraryApi } from './generic.js';
22
+
23
+ export interface SingleNodeMicroserverConfig {
24
+ storage: StorageFactory;
25
+ fileStorage?: FileStorage;
26
+ disableRebasing?: boolean;
27
+ profiles: UserProfileLoader<any>;
28
+ tokenSecret: string;
29
+ log?: Logger;
30
+ __testMode?: boolean;
31
+ }
32
+
33
+ const noop = () => {};
34
+
35
+ export class SingleNodeLibraryManager {
36
+ public readonly events = new EventSubscriber<LibraryEvents>();
37
+ private microserverPromises: Map<string, Promise<SingleNodeMicroserver>> =
38
+ new Map();
39
+ public readonly tokenVerifier: TokenVerifier;
40
+
41
+ constructor(private ctx: SingleNodeMicroserverConfig) {
42
+ this.tokenVerifier = new TokenVerifier({ secret: ctx.tokenSecret });
43
+ }
44
+
45
+ public get log() {
46
+ return this.ctx.log ?? noop;
47
+ }
48
+
49
+ public get __testMode() {
50
+ return this.ctx.__testMode === true;
51
+ }
52
+
53
+ get = async (libraryId: string) => {
54
+ let microserverPromise = this.microserverPromises.get(libraryId);
55
+ if (!microserverPromise) {
56
+ microserverPromise = this.constructMicroserver(libraryId);
57
+ this.microserverPromises.set(libraryId, microserverPromise);
58
+ }
59
+ return microserverPromise;
60
+ };
61
+
62
+ evict = async (libraryId: string) => {
63
+ const lib = await this.get(libraryId);
64
+ await lib.destroy();
65
+ this.microserverPromises.delete(libraryId);
66
+ };
67
+
68
+ private constructMicroserver = async (libraryId: string) => {
69
+ const clientConnections = new ClientConnectionManager({
70
+ profiles: this.ctx.profiles,
71
+ });
72
+ const storage = await this.ctx.storage(libraryId);
73
+ const library = new Library({
74
+ storage,
75
+ id: libraryId,
76
+ events: this.events,
77
+ sender: clientConnections,
78
+ presence: clientConnections.presence,
79
+ log: (level, ...args) =>
80
+ this.ctx.log?.(level, `[Library ${libraryId}]`, ...args),
81
+ fileStorage: this.ctx.fileStorage
82
+ ? new FileStorageLibraryDelegate(libraryId, this.ctx.fileStorage)
83
+ : undefined,
84
+ disableRebasing: this.ctx.disableRebasing || false,
85
+ });
86
+ return new SingleNodeMicroserver(library, clientConnections, this.ctx);
87
+ };
88
+ }
89
+
90
+ export class SingleNodeMicroserver implements LibraryApi {
91
+ constructor(
92
+ private library: Library,
93
+ public readonly clientConnections: ClientConnectionManager,
94
+ private ctx: SingleNodeMicroserverConfig,
95
+ ) {}
96
+
97
+ handleMessage = async (
98
+ key: string,
99
+ info: TokenInfo,
100
+ message: ClientMessage,
101
+ ) => {
102
+ return this.library.handleMessage(message, key, info);
103
+ };
104
+
105
+ uploadFile = async (
106
+ fileStream: Readable | ReadableStream | any,
107
+ info: FileInfo,
108
+ ) => {
109
+ if (!this.ctx.fileStorage) {
110
+ throw new VerdantError(
111
+ VerdantError.Code.ConfigurationError,
112
+ undefined,
113
+ 'File storage not configured',
114
+ );
115
+ }
116
+ await this.library.putFileInfo(info);
117
+ await this.ctx.fileStorage.put(fileStream, info);
118
+ };
119
+
120
+ getFileInfo = async (fileId: string) => {
121
+ return this.library.getFileInfo(fileId);
122
+ };
123
+
124
+ onRequest = this.clientConnections.addFetch.bind(this.clientConnections);
125
+
126
+ getDocumentSnapshot = async (
127
+ collection: string,
128
+ documentId: string,
129
+ ): Promise<any | null> => {
130
+ return this.library.getDocumentSnapshot(createOid(collection, documentId));
131
+ };
132
+
133
+ destroy = async () => {
134
+ this.clientConnections.disconnectAll();
135
+ await this.library.destroy();
136
+ };
137
+
138
+ getInfo = async () => {
139
+ return this.library.getInfo();
140
+ };
141
+
142
+ forceTruant = async (replicaId: string) => {
143
+ await this.library.forceTruant(replicaId);
144
+ };
145
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,4 @@
1
+ export type Logger = (
2
+ level: 'info' | 'warn' | 'error' | 'debug',
3
+ ...args: any[]
4
+ ) => void;
@@ -4,87 +4,58 @@ import {
4
4
  Ref,
5
5
  ReplicaType,
6
6
  } from '@verdant-web/common';
7
+ import { FileInfo } from '../files/FileStorage.js';
7
8
  import {
9
+ FileMetadata,
8
10
  HydratedDocumentBaseline,
9
11
  StoredOperation,
10
12
  StoredReplicaInfo,
11
- FileMetadata,
12
13
  } from '../types.js';
13
- import { FileInfo } from '../files/FileStorage.js';
14
14
 
15
15
  export interface ReplicaStorage {
16
16
  readonly truantCutoff: number;
17
- get(libraryId: string, replicaId: string): Promise<StoredReplicaInfo | null>;
17
+ get(replicaId: string): Promise<StoredReplicaInfo | null>;
18
18
  getOrCreate(
19
- libraryId: string,
20
19
  replicaId: string,
21
20
  info: { userId: string; type: ReplicaType },
22
21
  ): Promise<{
23
22
  status: 'new' | 'existing' | 'truant';
24
23
  replicaInfo: StoredReplicaInfo;
25
24
  }>;
26
- getAll(
27
- libraryId: string,
28
- options?: { omitTruant: boolean },
29
- ): Promise<StoredReplicaInfo[]>;
30
- updateLastSeen(libraryId: string, replicaId: string): Promise<void>;
31
- updateAckedServerOrder(
32
- libraryId: string,
33
- replicaId: string,
34
- serverOrder: number,
35
- ): Promise<void>;
25
+ getAll(options?: { omitTruant: boolean }): Promise<StoredReplicaInfo[]>;
26
+ updateLastSeen(replicaId: string): Promise<void>;
27
+ updateAckedServerOrder(replicaId: string, serverOrder: number): Promise<void>;
36
28
  updateAcknowledgedLogicalTime(
37
- libraryId: string,
38
29
  replicaId: string,
39
30
  timestamp: string,
40
31
  ): Promise<void>;
41
- getEarliestAckedServerOrder(libraryId: string): Promise<number>;
42
- acknowledgeOperation(
43
- libraryId: string,
44
- replicaId: string,
45
- timestamp: string,
46
- ): Promise<void>;
47
- getGlobalAck(
48
- libraryId: string,
49
- onlineReplicaIds?: string[],
50
- ): Promise<string | null>;
51
- delete(libraryId: string, replicaId: string): Promise<void>;
52
- deleteAll(libraryId: string): Promise<void>;
53
- deleteAllForUser(libraryId: string, userId: string): Promise<void>;
32
+ getEarliestAckedServerOrder(): Promise<number>;
33
+ acknowledgeOperation(replicaId: string, timestamp: string): Promise<void>;
34
+ getGlobalAck(onlineReplicaIds?: string[]): Promise<string | null>;
35
+ delete(replicaId: string): Promise<void>;
36
+ deleteAll(): Promise<void>;
37
+ deleteAllForUser(userId: string): Promise<void>;
38
+ forceTruant(replicaId: string): Promise<void>;
54
39
  }
55
40
 
56
41
  export interface OperationStorage {
57
- getAll(libraryId: string, oid: string): Promise<StoredOperation[]>;
58
- getBeforeServerOrder(
59
- libraryId: string,
60
- beforeServerOrder: number,
61
- ): Promise<StoredOperation[]>;
62
- getAfterServerOrder(
63
- libraryId: string,
64
- afterServerOrder: number,
65
- ): Promise<StoredOperation[]>;
66
- getLatestServerOrder(libraryId: string): Promise<number>;
67
- getCount(libraryId: string): Promise<number>;
68
- insertAll(
69
- libraryId: string,
70
- replicaId: string,
71
- operations: Operation[],
72
- ): Promise<number>;
73
- deleteAll(libraryId: string): Promise<void>;
74
- delete(libraryId: string, operations: Operation[]): Promise<void>;
42
+ getAll(oid: string): Promise<StoredOperation[]>;
43
+ getBeforeServerOrder(beforeServerOrder: number): Promise<StoredOperation[]>;
44
+ getAfterServerOrder(afterServerOrder: number): Promise<StoredOperation[]>;
45
+ getLatestServerOrder(): Promise<number>;
46
+ getCount(): Promise<number>;
47
+ insertAll(replicaId: string, operations: Operation[]): Promise<number>;
48
+ deleteAll(): Promise<void>;
49
+ delete(operations: Operation[]): Promise<void>;
75
50
  }
76
51
 
77
52
  export interface BaselineStorage {
78
- get(libraryId: string, oid: string): Promise<HydratedDocumentBaseline | null>;
79
- getAll(libraryId: string): Promise<HydratedDocumentBaseline[]>;
80
- insertAll(
81
- libraryId: string,
82
- baselines: DocumentBaseline<any>[],
83
- ): Promise<void>;
84
- deleteAll(libraryId: string): Promise<void>;
85
- getCount(libraryId: string): Promise<number>;
53
+ get(oid: string): Promise<HydratedDocumentBaseline | null>;
54
+ getAll(): Promise<HydratedDocumentBaseline[]>;
55
+ insertAll(baselines: DocumentBaseline<any>[]): Promise<void>;
56
+ deleteAll(): Promise<void>;
57
+ getCount(): Promise<number>;
86
58
  applyOperations(
87
- libraryId: string,
88
59
  oid: string,
89
60
  operations: StoredOperation[],
90
61
  deletedRefs?: Ref[],
@@ -92,18 +63,18 @@ export interface BaselineStorage {
92
63
  }
93
64
 
94
65
  export interface FileMetadataStorage {
95
- get(libraryId: string, fileId: string): Promise<FileMetadata | null>;
96
- getAll(libraryId: string): Promise<FileMetadata[]>;
97
- deleteAll(libraryId: string): Promise<void>;
98
- put(libraryId: string, fileInfo: FileInfo): Promise<void>;
99
- markPendingDelete(libraryId: string, fileId: string): Promise<void>;
100
- delete(libraryId: string, fileId: string): Promise<void>;
101
- getPendingDelete(libraryId: string): Promise<FileMetadata[]>;
66
+ get(fileId: string): Promise<FileMetadata | null>;
67
+ getAll(): Promise<FileMetadata[]>;
68
+ deleteAll(): Promise<void>;
69
+ put(fileInfo: FileInfo): Promise<void>;
70
+ markPendingDelete(fileId: string): Promise<void>;
71
+ delete(fileId: string): Promise<void>;
72
+ getPendingDelete(): Promise<FileMetadata[]>;
102
73
  }
103
74
 
104
75
  export interface StorageOptions {
105
- replicaTruancyMinutes: number;
106
- fileDeleteExpirationDays: number;
76
+ replicaTruancyMinutes?: number;
77
+ fileDeleteExpirationDays?: number;
107
78
  }
108
79
  export interface Storage {
109
80
  replicas: ReplicaStorage;
@@ -112,7 +83,7 @@ export interface Storage {
112
83
  fileMetadata: FileMetadataStorage;
113
84
  close(): Promise<void>;
114
85
  readonly open: boolean;
115
- readonly ready: Promise<void>;
116
86
  }
117
87
 
118
- export type StorageFactory = (options: StorageOptions) => Storage;
88
+ export type StorageFactory = (libraryId: string) => Promise<Storage>;
89
+ export type StorageFactoryFactory = (options: StorageOptions) => StorageFactory;
@@ -0,0 +1,39 @@
1
+ import { Storage, StorageFactory } from './Storage.js';
2
+
3
+ export class StorageManager {
4
+ private _cache: Map<string, Promise<Storage>> = new Map();
5
+ private _evicts: Map<string, NodeJS.Timeout> = new Map();
6
+
7
+ constructor(private _factory: StorageFactory) {}
8
+
9
+ open = async (libraryId: string) => {
10
+ let storage = this._cache.get(libraryId);
11
+ if (!storage) {
12
+ storage = this._factory(libraryId);
13
+ this._cache.set(libraryId, storage);
14
+ }
15
+ this.refresh(libraryId);
16
+ await storage;
17
+ return storage;
18
+ };
19
+
20
+ closeAll = async () => {
21
+ for (const [libraryId, storage] of this._cache) {
22
+ await (await storage).close();
23
+ this.evict(libraryId);
24
+ }
25
+ };
26
+
27
+ private refresh = (libraryId: string) => {
28
+ clearTimeout(this._evicts.get(libraryId));
29
+ this._evicts.set(
30
+ libraryId,
31
+ setTimeout(this.evict, 1000 * 60 * 60, libraryId),
32
+ );
33
+ };
34
+
35
+ private evict = (libraryId: string) => {
36
+ this._cache.delete(libraryId);
37
+ this._evicts.delete(libraryId);
38
+ };
39
+ }
@@ -1,3 +1,2 @@
1
- export * from './Storage.js';
2
- export * from './sql/sqlStorage.js';
3
1
  export * from './sqlShard/sqlShardStorage.js';
2
+ export * from './Storage.js';
@@ -1,23 +1,28 @@
1
- import { Kysely } from 'kysely';
2
- import { openDatabase } from './database.js';
3
- import { Database as DatabaseTypes } from './tables.js';
4
- import { mkdirSync, existsSync } from 'fs';
5
1
  import { assert } from '@verdant-web/common';
2
+ import { existsSync, mkdirSync } from 'fs';
3
+ import { Logger } from '../../logger.js';
4
+ import {
5
+ createFilesystemExecutor,
6
+ openDatabase,
7
+ SqliteExecutor,
8
+ } from './database.js';
6
9
 
7
10
  export class Databases {
8
- private openPromises: Record<string, Promise<Kysely<DatabaseTypes>>> = {};
9
- private cache: Record<string, Kysely<DatabaseTypes>> = {};
11
+ private openPromises: Record<string, Promise<SqliteExecutor>> = {};
12
+ private cache: Record<string, SqliteExecutor> = {};
10
13
  private closeTimeouts: Record<string, NodeJS.Timeout> = {};
11
14
  private openedPreviously: Record<string, boolean> = {};
12
15
  private directory;
13
16
  private closeTimeout;
14
17
  private disableWal = false;
15
- private closed = new WeakMap<Kysely<DatabaseTypes>, boolean>();
18
+ private closed = new WeakMap<SqliteExecutor, boolean>();
19
+ private log: Logger;
16
20
 
17
21
  constructor(config: {
18
22
  directory: string;
19
23
  closeTimeout?: number;
20
24
  disableWal?: boolean;
25
+ log?: Logger;
21
26
  }) {
22
27
  this.directory = config.directory;
23
28
  this.closeTimeout = config.closeTimeout ?? 1000 * 60 * 60;
@@ -25,9 +30,10 @@ export class Databases {
25
30
  if (this.directory !== ':memory:' && !existsSync(this.directory)) {
26
31
  mkdirSync(this.directory, { recursive: true });
27
32
  }
33
+ this.log = config.log ?? (() => {});
28
34
  }
29
35
 
30
- get = async (libraryId: string): Promise<Kysely<DatabaseTypes>> => {
36
+ get = async (libraryId: string): Promise<SqliteExecutor> => {
31
37
  let db = this.cache[libraryId];
32
38
  if (db) {
33
39
  if (!this.closed.get(db)) {
@@ -37,10 +43,17 @@ export class Databases {
37
43
  }
38
44
  let openPromise = this.openPromises[libraryId];
39
45
  if (!openPromise) {
40
- this.openPromises[libraryId] = openDatabase(this.directory, libraryId, {
41
- skipMigrations: this.openedPreviously[libraryId],
42
- disableWal: this.disableWal,
43
- });
46
+ // only a promise for potential "forward compatibility"
47
+ // but tbh I'm not sure a promise-based storage solution
48
+ // is on the horizon. SQLite is fine.
49
+ this.openPromises[libraryId] = Promise.resolve(
50
+ openDatabase(
51
+ createFilesystemExecutor(this.directory, libraryId, {
52
+ disableWal: this.disableWal,
53
+ log: this.log,
54
+ }),
55
+ ),
56
+ );
44
57
  openPromise = this.openPromises[libraryId];
45
58
  }
46
59
  db = await openPromise;
@@ -68,7 +81,7 @@ export class Databases {
68
81
  if (db) {
69
82
  this.closed.set(db, true);
70
83
  delete this.cache[libraryId];
71
- db.destroy();
84
+ db.close();
72
85
  }
73
86
  };
74
87
 
@@ -1,87 +1,82 @@
1
- import { Kysely } from 'kysely';
2
- import { BaselineStorage } from '../Storage.js';
3
- import { Database, DocumentBaselineRow } from './tables.js';
4
1
  import { DocumentBaseline, Ref, applyPatch } from '@verdant-web/common';
5
2
  import { HydratedDocumentBaseline, StoredOperation } from '../../types.js';
6
- import { Databases } from './Databases.js';
3
+ import { BaselineStorage } from '../Storage.js';
4
+ import { SqliteExecutor } from './database.js';
5
+ import { DocumentBaselineRow } from './tables.js';
7
6
 
8
7
  export class SqlBaselines implements BaselineStorage {
9
8
  constructor(
10
- private dbs: Databases,
11
- private dialect: 'postgres' | 'sqlite',
9
+ private db: SqliteExecutor,
10
+ private libraryId: string,
12
11
  ) {}
13
12
 
14
13
  get = async (
15
- libraryId: string,
16
14
  oid: string,
17
- { tx }: { tx?: Kysely<Database> } = {},
15
+ { tx }: { tx?: SqliteExecutor } = {},
18
16
  ): Promise<HydratedDocumentBaseline | null> => {
19
- const db = tx ?? (await this.dbs.get(libraryId));
20
- const raw =
21
- (await db
22
- .selectFrom('DocumentBaseline')
23
- .where('oid', '=', oid)
24
- .selectAll()
25
- .executeTakeFirst()) ?? null;
17
+ return this.#getSync(oid, { tx });
18
+ };
19
+ #getSync = (oid: string, { tx }: { tx?: SqliteExecutor } = {}) => {
20
+ const db = tx ?? this.db;
21
+ const raw = db.first<DocumentBaselineRow>(
22
+ `
23
+ SELECT * FROM DocumentBaseline
24
+ WHERE oid = ?
25
+ `,
26
+ [oid],
27
+ );
26
28
  if (!raw) return null;
27
29
  this.hydrate(raw);
28
30
  return raw as any;
29
31
  };
30
32
 
31
- getAll = async (libraryId: string) => {
32
- const db = await this.dbs.get(libraryId);
33
- const raw = await db
34
- .selectFrom('DocumentBaseline')
35
- .orderBy('timestamp', 'asc')
36
- .selectAll()
37
- .execute();
33
+ getAll = async () => {
34
+ const db = this.db;
35
+ const raw = db.query<DocumentBaselineRow>(
36
+ `SELECT * FROM DocumentBaseline ORDER BY timestamp ASC`,
37
+ );
38
38
 
39
39
  raw.forEach(this.hydrate);
40
40
 
41
41
  return raw as any;
42
42
  };
43
43
 
44
- set = async (
45
- libraryId: string,
44
+ set = (
46
45
  baseline: DocumentBaseline,
47
46
  {
48
47
  tx,
49
48
  }: {
50
- tx?: Kysely<Database>;
49
+ tx?: SqliteExecutor;
51
50
  },
52
51
  ) => {
53
- const db = tx ?? (await this.dbs.get(libraryId));
52
+ const db = tx ?? this.db;
54
53
  if (!baseline.snapshot) {
55
- await db
56
- .deleteFrom('DocumentBaseline')
57
- .where('oid', '=', baseline.oid)
58
- .execute();
59
- return;
54
+ return db.exec(`DELETE FROM DocumentBaseline WHERE oid = ?`, [
55
+ baseline.oid,
56
+ ]);
60
57
  }
61
58
 
62
- await db
63
- .insertInto('DocumentBaseline')
64
- .values({
65
- oid: baseline.oid,
66
- snapshot: JSON.stringify(baseline.snapshot),
67
- timestamp: baseline.timestamp,
68
- authz: baseline.authz,
69
- })
70
- .onConflict((cb) =>
71
- cb.doUpdateSet({
72
- snapshot: JSON.stringify(baseline.snapshot),
73
- timestamp: baseline.timestamp,
74
- authz: baseline.authz,
75
- }),
76
- )
77
- .execute();
59
+ return db.exec(
60
+ `INSERT INTO DocumentBaseline (oid, snapshot, timestamp, authz) VALUES (?, ?, ?, ?)
61
+ ON CONFLICT(oid) DO UPDATE SET
62
+ snapshot = excluded.snapshot,
63
+ timestamp = excluded.timestamp,
64
+ authz = excluded.authz
65
+ `,
66
+ [
67
+ baseline.oid,
68
+ JSON.stringify(baseline.snapshot),
69
+ baseline.timestamp,
70
+ baseline.authz,
71
+ ],
72
+ );
78
73
  };
79
74
 
80
- insertAll = async (libraryId: string, baselines: DocumentBaseline[]) => {
81
- const db = await this.dbs.get(libraryId);
82
- await db.transaction().execute(async (tx) => {
75
+ insertAll = async (baselines: DocumentBaseline[]) => {
76
+ const db = this.db;
77
+ return db.transaction((tx) => {
83
78
  for (const baseline of baselines) {
84
- await this.set(libraryId, baseline, { tx });
79
+ this.set(baseline, { tx });
85
80
  }
86
81
  });
87
82
  };
@@ -92,23 +87,22 @@ export class SqlBaselines implements BaselineStorage {
92
87
  };
93
88
 
94
89
  applyOperations = async (
95
- libraryId: string,
96
90
  oid: string,
97
91
  operations: StoredOperation[],
98
92
  deletedRefs?: Ref[],
99
93
  ) => {
100
94
  if (operations.length === 0) return;
101
95
 
102
- const db = await this.dbs.get(libraryId);
96
+ const db = this.db;
103
97
 
104
- await db.transaction().execute(async (tx) => {
105
- let baseline = await this.get(libraryId, oid, { tx });
98
+ await db.transaction((tx) => {
99
+ let baseline = this.#getSync(oid, { tx });
106
100
  if (!baseline) {
107
101
  baseline = {
108
102
  oid,
109
103
  snapshot: {},
110
104
  timestamp: operations[0].timestamp,
111
- libraryId,
105
+ libraryId: this.libraryId,
112
106
  authz: operations[0].authz,
113
107
  };
114
108
  }
@@ -123,23 +117,20 @@ export class SqlBaselines implements BaselineStorage {
123
117
  }
124
118
  }
125
119
  baseline.timestamp = operations[operations.length - 1].timestamp;
126
- await this.set(libraryId, baseline, { tx });
120
+ this.set(baseline, { tx });
127
121
  });
128
122
  };
129
123
 
130
- deleteAll = async (libraryId: string) => {
131
- const db = await this.dbs.get(libraryId);
132
- await db.deleteFrom('DocumentBaseline').execute();
124
+ deleteAll = async () => {
125
+ const db = this.db;
126
+ db.exec('DELETE FROM DocumentBaseline;');
133
127
  };
134
128
 
135
- getCount = async (libraryId: string) => {
136
- const db = await this.dbs.get(libraryId);
129
+ getCount = async () => {
130
+ const db = this.db;
137
131
  return (
138
- (
139
- await db
140
- .selectFrom('DocumentBaseline')
141
- .select(({ fn }) => fn.countAll<number>().as('count'))
142
- .executeTakeFirst()
132
+ db.first<{ count: number }>(
133
+ `SELECT COUNT(*) as count FROM DocumentBaseline;`,
143
134
  )?.count ?? 0
144
135
  );
145
136
  };