@verdant-web/server 3.3.10 → 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.
- package/bin/server.mjs +110 -107
- package/dist/esm/TokenVerifier.d.ts +1 -0
- package/dist/esm/TokenVerifier.js +3 -0
- package/dist/esm/TokenVerifier.js.map +1 -1
- package/dist/esm/adapters/node/errorHandler.d.ts +2 -0
- package/dist/esm/adapters/node/errorHandler.js +12 -0
- package/dist/esm/adapters/node/errorHandler.js.map +1 -0
- package/dist/esm/adapters/node/getRequestToken.d.ts +1 -0
- package/dist/esm/adapters/node/getRequestToken.js +25 -0
- package/dist/esm/adapters/node/getRequestToken.js.map +1 -0
- package/dist/esm/adapters/node/httpRouter.d.ts +47 -0
- package/dist/esm/adapters/node/httpRouter.js +88 -0
- package/dist/esm/adapters/node/httpRouter.js.map +1 -0
- package/dist/esm/adapters/node/index.d.ts +5 -0
- package/dist/esm/adapters/node/index.js +6 -0
- package/dist/esm/adapters/node/index.js.map +1 -0
- package/dist/esm/adapters/node/tokenMiddleware.d.ts +7 -0
- package/dist/esm/adapters/node/tokenMiddleware.js +22 -0
- package/dist/esm/adapters/node/tokenMiddleware.js.map +1 -0
- package/dist/esm/adapters/node/websocketHandler.d.ts +4 -0
- package/dist/esm/adapters/node/websocketHandler.js +54 -0
- package/dist/esm/adapters/node/websocketHandler.js.map +1 -0
- package/dist/esm/connections/ClientConnection.d.ts +25 -0
- package/dist/esm/{ClientConnection.js → connections/ClientConnection.js} +47 -38
- package/dist/esm/connections/ClientConnection.js.map +1 -0
- package/dist/esm/connections/MessageSender.d.ts +5 -0
- package/dist/esm/connections/MessageSender.js.map +1 -0
- package/dist/esm/connections/Presence.d.ts +23 -0
- package/dist/esm/connections/Presence.js +65 -0
- package/dist/esm/connections/Presence.js.map +1 -0
- package/dist/esm/core.d.ts +15 -0
- package/dist/esm/core.js +15 -0
- package/dist/esm/core.js.map +1 -0
- package/dist/esm/defaults.d.ts +2 -0
- package/dist/esm/defaults.js +3 -0
- package/dist/esm/defaults.js.map +1 -0
- package/dist/esm/files/FileStorage.d.ts +12 -3
- package/dist/esm/files/FileStorage.js +20 -0
- package/dist/esm/files/FileStorage.js.map +1 -1
- package/dist/esm/index.d.ts +7 -7
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internals.d.ts +14 -0
- package/dist/esm/internals.js +12 -0
- package/dist/esm/internals.js.map +1 -0
- package/dist/esm/libraries/Library.d.ts +78 -0
- package/dist/esm/{ServerLibrary.js → libraries/Library.js} +153 -144
- package/dist/esm/libraries/Library.js.map +1 -0
- package/dist/esm/{ServerLibrary.test.js → libraries/Library.test.js} +42 -33
- package/dist/esm/libraries/Library.test.js.map +1 -0
- package/dist/esm/libraries/generic.d.ts +8 -0
- package/dist/esm/libraries/generic.js +2 -0
- package/dist/esm/libraries/generic.js.map +1 -0
- package/dist/esm/libraries/singleNode.d.ts +53 -0
- package/dist/esm/libraries/singleNode.js +88 -0
- package/dist/esm/libraries/singleNode.js.map +1 -0
- package/dist/esm/logger.d.ts +1 -0
- package/dist/esm/logger.js +2 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/storage/Storage.d.ts +39 -38
- package/dist/esm/storage/StorageManager.d.ts +11 -0
- package/dist/esm/storage/StorageManager.js +32 -0
- package/dist/esm/storage/StorageManager.js.map +1 -0
- package/dist/esm/storage/index.d.ts +1 -2
- package/dist/esm/storage/index.js +1 -2
- package/dist/esm/storage/index.js.map +1 -1
- package/dist/esm/storage/sqlShard/Databases.d.ts +5 -3
- package/dist/esm/storage/sqlShard/Databases.js +11 -7
- package/dist/esm/storage/sqlShard/Databases.js.map +1 -1
- package/dist/esm/storage/sqlShard/SqlBaselines.d.ts +16 -17
- package/dist/esm/storage/sqlShard/SqlBaselines.js +55 -59
- package/dist/esm/storage/sqlShard/SqlBaselines.js.map +1 -1
- package/dist/esm/storage/sqlShard/SqlFileMetadata.d.ts +13 -13
- package/dist/esm/storage/sqlShard/SqlFileMetadata.js +44 -57
- package/dist/esm/storage/sqlShard/SqlFileMetadata.js.map +1 -1
- package/dist/esm/storage/sqlShard/SqlOperations.d.ts +12 -12
- package/dist/esm/storage/sqlShard/SqlOperations.js +38 -73
- package/dist/esm/storage/sqlShard/SqlOperations.js.map +1 -1
- package/dist/esm/storage/sqlShard/SqlReplicas.d.ts +18 -17
- package/dist/esm/storage/sqlShard/SqlReplicas.js +63 -100
- package/dist/esm/storage/sqlShard/SqlReplicas.js.map +1 -1
- package/dist/esm/storage/sqlShard/database.d.ts +25 -5
- package/dist/esm/storage/sqlShard/database.js +47 -15
- package/dist/esm/storage/sqlShard/database.js.map +1 -1
- package/dist/esm/storage/sqlShard/migrations/v1.d.ts +2 -3
- package/dist/esm/storage/sqlShard/migrations/v1.js +42 -47
- package/dist/esm/storage/sqlShard/migrations/v1.js.map +1 -1
- package/dist/esm/storage/sqlShard/migrations/v2.d.ts +2 -3
- package/dist/esm/storage/sqlShard/migrations/v2.js +5 -15
- package/dist/esm/storage/sqlShard/migrations/v2.js.map +1 -1
- package/dist/esm/storage/sqlShard/migrations.d.ts +5 -7
- package/dist/esm/storage/sqlShard/migrations.js +48 -1
- package/dist/esm/storage/sqlShard/migrations.js.map +1 -1
- package/dist/esm/storage/sqlShard/sqlShardStorage.d.ts +25 -4
- package/dist/esm/storage/sqlShard/sqlShardStorage.js +43 -40
- package/dist/esm/storage/sqlShard/sqlShardStorage.js.map +1 -1
- package/dist/esm/storage/sqlShard/tables.d.ts +9 -6
- package/dist/esm/types.d.ts +1 -2
- package/package.json +24 -13
- package/src/TokenVerifier.ts +4 -0
- package/src/adapters/node/errorHandler.ts +18 -0
- package/src/adapters/node/getRequestToken.ts +34 -0
- package/src/adapters/node/httpRouter.ts +139 -0
- package/src/adapters/node/index.ts +5 -0
- package/src/adapters/node/tokenMiddleware.ts +32 -0
- package/src/adapters/node/websocketHandler.ts +79 -0
- package/src/{ClientConnection.ts → connections/ClientConnection.ts} +64 -57
- package/src/connections/MessageSender.ts +6 -0
- package/src/connections/Presence.ts +95 -0
- package/src/core.ts +36 -0
- package/src/defaults.ts +2 -0
- package/src/files/FileStorage.ts +36 -3
- package/src/index.ts +7 -7
- package/src/internals.ts +14 -0
- package/src/{ServerLibrary.test.ts → libraries/Library.test.ts} +51 -49
- package/src/{ServerLibrary.ts → libraries/Library.ts} +203 -206
- package/src/libraries/generic.ts +9 -0
- package/src/libraries/singleNode.ts +145 -0
- package/src/logger.ts +4 -0
- package/src/storage/Storage.ts +37 -66
- package/src/storage/StorageManager.ts +39 -0
- package/src/storage/index.ts +1 -2
- package/src/storage/sqlShard/Databases.ts +26 -13
- package/src/storage/sqlShard/SqlBaselines.ts +58 -67
- package/src/storage/sqlShard/SqlFileMetadata.ts +51 -72
- package/src/storage/sqlShard/SqlOperations.ts +57 -81
- package/src/storage/sqlShard/SqlReplicas.ts +80 -121
- package/src/storage/sqlShard/database.ts +82 -18
- package/src/storage/sqlShard/migrations/v1.ts +42 -52
- package/src/storage/sqlShard/migrations/v2.ts +5 -19
- package/src/storage/sqlShard/migrations.ts +66 -1
- package/src/storage/sqlShard/sqlShardStorage.ts +56 -55
- package/src/storage/sqlShard/tables.ts +6 -6
- package/src/types.ts +1 -2
- package/dist/esm/ClientConnection.d.ts +0 -19
- package/dist/esm/ClientConnection.js.map +0 -1
- package/dist/esm/MessageSender.d.ts +0 -5
- package/dist/esm/MessageSender.js.map +0 -1
- package/dist/esm/Presence.d.ts +0 -18
- package/dist/esm/Presence.js +0 -65
- package/dist/esm/Presence.js.map +0 -1
- package/dist/esm/ReplicaKeepaliveTimers.d.ts +0 -17
- package/dist/esm/ReplicaKeepaliveTimers.js +0 -27
- package/dist/esm/ReplicaKeepaliveTimers.js.map +0 -1
- package/dist/esm/Server.d.ts +0 -166
- package/dist/esm/Server.js +0 -572
- package/dist/esm/Server.js.map +0 -1
- package/dist/esm/ServerLibrary.d.ts +0 -64
- package/dist/esm/ServerLibrary.js.map +0 -1
- package/dist/esm/ServerLibrary.test.js.map +0 -1
- package/dist/esm/storage/sql/SqlBaselines.d.ts +0 -22
- package/dist/esm/storage/sql/SqlBaselines.js +0 -107
- package/dist/esm/storage/sql/SqlBaselines.js.map +0 -1
- package/dist/esm/storage/sql/SqlFileMetadata.d.ts +0 -18
- package/dist/esm/storage/sql/SqlFileMetadata.js +0 -69
- package/dist/esm/storage/sql/SqlFileMetadata.js.map +0 -1
- package/dist/esm/storage/sql/SqlOperations.d.ts +0 -19
- package/dist/esm/storage/sql/SqlOperations.js +0 -122
- package/dist/esm/storage/sql/SqlOperations.js.map +0 -1
- package/dist/esm/storage/sql/SqlReplicas.d.ts +0 -32
- package/dist/esm/storage/sql/SqlReplicas.js +0 -163
- package/dist/esm/storage/sql/SqlReplicas.js.map +0 -1
- package/dist/esm/storage/sql/database.d.ts +0 -9
- package/dist/esm/storage/sql/database.js +0 -24
- package/dist/esm/storage/sql/database.js.map +0 -1
- package/dist/esm/storage/sql/migrations/v1.d.ts +0 -3
- package/dist/esm/storage/sql/migrations/v1.js +0 -59
- package/dist/esm/storage/sql/migrations/v1.js.map +0 -1
- package/dist/esm/storage/sql/migrations/v2.d.ts +0 -3
- package/dist/esm/storage/sql/migrations/v2.js +0 -16
- package/dist/esm/storage/sql/migrations/v2.js.map +0 -1
- package/dist/esm/storage/sql/migrations.d.ts +0 -7
- package/dist/esm/storage/sql/migrations.js +0 -4
- package/dist/esm/storage/sql/migrations.js.map +0 -1
- package/dist/esm/storage/sql/sqlStorage.d.ts +0 -6
- package/dist/esm/storage/sql/sqlStorage.js +0 -35
- package/dist/esm/storage/sql/sqlStorage.js.map +0 -1
- package/dist/esm/storage/sql/tables.d.ts +0 -44
- package/dist/esm/storage/sql/tables.js +0 -2
- package/dist/esm/storage/sql/tables.js.map +0 -1
- package/dist/esm/storage/sqlShard/transfer.d.ts +0 -7
- package/dist/esm/storage/sqlShard/transfer.js +0 -90
- package/dist/esm/storage/sqlShard/transfer.js.map +0 -1
- package/dist/esm/storage/sqlShard/transfer.test.d.ts +0 -1
- package/dist/esm/storage/sqlShard/transfer.test.js +0 -109
- package/dist/esm/storage/sqlShard/transfer.test.js.map +0 -1
- package/src/MessageSender.ts +0 -10
- package/src/Presence.ts +0 -86
- package/src/ReplicaKeepaliveTimers.ts +0 -34
- package/src/Server.ts +0 -859
- package/src/storage/sql/SqlBaselines.ts +0 -147
- package/src/storage/sql/SqlFileMetadata.ts +0 -94
- package/src/storage/sql/SqlOperations.ts +0 -160
- package/src/storage/sql/SqlReplicas.ts +0 -234
- package/src/storage/sql/database.ts +0 -32
- package/src/storage/sql/migrations/v1.ts +0 -64
- package/src/storage/sql/migrations/v2.ts +0 -20
- package/src/storage/sql/migrations.ts +0 -4
- package/src/storage/sql/sqlStorage.ts +0 -57
- package/src/storage/sql/tables.ts +0 -51
- package/src/storage/sqlShard/transfer.test.ts +0 -149
- package/src/storage/sqlShard/transfer.ts +0 -123
- /package/dist/esm/{MessageSender.js → connections/MessageSender.js} +0 -0
- /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
package/src/storage/Storage.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
28
|
-
|
|
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(
|
|
42
|
-
acknowledgeOperation(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
): Promise<void>;
|
|
47
|
-
|
|
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(
|
|
58
|
-
getBeforeServerOrder(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
): Promise<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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(
|
|
79
|
-
getAll(
|
|
80
|
-
insertAll(
|
|
81
|
-
|
|
82
|
-
|
|
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(
|
|
96
|
-
getAll(
|
|
97
|
-
deleteAll(
|
|
98
|
-
put(
|
|
99
|
-
markPendingDelete(
|
|
100
|
-
delete(
|
|
101
|
-
getPendingDelete(
|
|
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
|
|
106
|
-
fileDeleteExpirationDays
|
|
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 = (
|
|
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
|
+
}
|
package/src/storage/index.ts
CHANGED
|
@@ -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<
|
|
9
|
-
private cache: Record<string,
|
|
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<
|
|
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<
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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 {
|
|
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
|
|
11
|
-
private
|
|
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?:
|
|
15
|
+
{ tx }: { tx?: SqliteExecutor } = {},
|
|
18
16
|
): Promise<HydratedDocumentBaseline | null> => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 (
|
|
32
|
-
const db =
|
|
33
|
-
const raw =
|
|
34
|
-
|
|
35
|
-
|
|
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 =
|
|
45
|
-
libraryId: string,
|
|
44
|
+
set = (
|
|
46
45
|
baseline: DocumentBaseline,
|
|
47
46
|
{
|
|
48
47
|
tx,
|
|
49
48
|
}: {
|
|
50
|
-
tx?:
|
|
49
|
+
tx?: SqliteExecutor;
|
|
51
50
|
},
|
|
52
51
|
) => {
|
|
53
|
-
const db = tx ??
|
|
52
|
+
const db = tx ?? this.db;
|
|
54
53
|
if (!baseline.snapshot) {
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
.execute();
|
|
59
|
-
return;
|
|
54
|
+
return db.exec(`DELETE FROM DocumentBaseline WHERE oid = ?`, [
|
|
55
|
+
baseline.oid,
|
|
56
|
+
]);
|
|
60
57
|
}
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 (
|
|
81
|
-
const db =
|
|
82
|
-
|
|
75
|
+
insertAll = async (baselines: DocumentBaseline[]) => {
|
|
76
|
+
const db = this.db;
|
|
77
|
+
return db.transaction((tx) => {
|
|
83
78
|
for (const baseline of baselines) {
|
|
84
|
-
|
|
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 =
|
|
96
|
+
const db = this.db;
|
|
103
97
|
|
|
104
|
-
await db.transaction(
|
|
105
|
-
let baseline =
|
|
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
|
-
|
|
120
|
+
this.set(baseline, { tx });
|
|
127
121
|
});
|
|
128
122
|
};
|
|
129
123
|
|
|
130
|
-
deleteAll = async (
|
|
131
|
-
const db =
|
|
132
|
-
|
|
124
|
+
deleteAll = async () => {
|
|
125
|
+
const db = this.db;
|
|
126
|
+
db.exec('DELETE FROM DocumentBaseline;');
|
|
133
127
|
};
|
|
134
128
|
|
|
135
|
-
getCount = async (
|
|
136
|
-
const db =
|
|
129
|
+
getCount = async () => {
|
|
130
|
+
const db = this.db;
|
|
137
131
|
return (
|
|
138
|
-
(
|
|
139
|
-
|
|
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
|
};
|