@verdant-web/server 3.0.0-next.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/ClientConnection.d.ts +21 -0
- package/dist/esm/MessageSender.d.ts +5 -0
- package/dist/esm/Presence.d.ts +15 -0
- package/dist/esm/Profiles.d.ts +10 -0
- package/dist/esm/ReplicaKeepaliveTimers.d.ts +17 -0
- package/dist/esm/Server.d.ts +171 -0
- package/dist/esm/ServerLibrary.d.ts +58 -0
- package/dist/esm/ServerLibrary.test.d.ts +1 -0
- package/dist/esm/TokenProvider.d.ts +19 -0
- package/dist/esm/TokenVerifier.d.ts +16 -0
- package/dist/esm/files/FileStorage.d.ts +35 -0
- package/dist/esm/index.d.ts +10 -0
- package/dist/esm/storage/Storage.d.ts +67 -0
- package/dist/esm/storage/index.d.ts +2 -0
- package/dist/esm/storage/sql/SqlBaselines.d.ts +22 -0
- package/dist/esm/storage/sql/SqlFileMetadata.d.ts +18 -0
- package/dist/esm/storage/sql/SqlOperations.d.ts +19 -0
- package/dist/esm/storage/sql/SqlReplicas.d.ts +32 -0
- package/dist/esm/storage/sql/migrations/v1.d.ts +3 -0
- package/dist/esm/storage/sql/migrations.d.ts +5 -0
- package/dist/esm/storage/sql/sqlStorage.d.ts +5 -0
- package/dist/esm/storage/sql/tables.d.ts +40 -0
- package/dist/esm/types.d.ts +47 -0
- package/package.json +2 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
+
import { ServerMessage } from '@verdant-web/common';
|
|
4
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
5
|
+
import { WebSocket } from 'ws';
|
|
6
|
+
import { TokenInfo } from './TokenVerifier.js';
|
|
7
|
+
export declare class ClientConnectionManager {
|
|
8
|
+
private readonly connections;
|
|
9
|
+
private getLibraryConnections;
|
|
10
|
+
addSocket: (libraryId: string, key: string, socket: WebSocket, info: TokenInfo) => void;
|
|
11
|
+
addRequest: (libraryId: string, key: string, request: IncomingMessage, response: ServerResponse, info: TokenInfo) => () => void;
|
|
12
|
+
addFetch: (libraryId: string, key: string, req: Request, info: TokenInfo) => (() => Response);
|
|
13
|
+
remove: (libraryId: string, key: string) => void;
|
|
14
|
+
respond: (libraryId: string, key: string, message: ServerMessage) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Broadcasts a message to all live-connected clients in a library.
|
|
17
|
+
* This does not reach push/pull clients.
|
|
18
|
+
*/
|
|
19
|
+
broadcast: (libraryId: string, message: ServerMessage, omitKeys?: string[]) => void;
|
|
20
|
+
disconnectAll: (libraryId: string) => void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { UserInfo } from '@verdant-web/common';
|
|
3
|
+
import EventEmitter from 'events';
|
|
4
|
+
/**
|
|
5
|
+
* Stores client presence in-memory for connected
|
|
6
|
+
* clients
|
|
7
|
+
*/
|
|
8
|
+
export declare class Presence extends EventEmitter {
|
|
9
|
+
private presences;
|
|
10
|
+
private getLibrary;
|
|
11
|
+
set: (libraryId: string, userId: string, presence: UserInfo<any, any>) => void;
|
|
12
|
+
removeReplica: (libraryId: string, replicaId: string) => void;
|
|
13
|
+
all: (libraryId: string) => Record<string, UserInfo<any, any>>;
|
|
14
|
+
clear: (libraryId: string) => void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface UserProfiles<Profile> {
|
|
2
|
+
get(userId: string): Promise<Profile>;
|
|
3
|
+
}
|
|
4
|
+
export declare class UserProfileLoader<Profile> {
|
|
5
|
+
private source;
|
|
6
|
+
private cache;
|
|
7
|
+
constructor(source: UserProfiles<Profile>);
|
|
8
|
+
get: (userId: string) => Promise<Profile>;
|
|
9
|
+
evict: (userId: string) => void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EventSubscriber } from '@verdant-web/common';
|
|
2
|
+
/**
|
|
3
|
+
* A set of timers per-replica which are
|
|
4
|
+
* refreshed whenever the replica sends
|
|
5
|
+
* a message to the server. If a timer expires,
|
|
6
|
+
* the replica is removed from presence. This
|
|
7
|
+
* is a failsafe for socket-based replicas but
|
|
8
|
+
* essential for pull-based replicas.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ReplicaKeepaliveTimers extends EventSubscriber<{
|
|
11
|
+
lost: (libraryId: string, replicaId: string) => void;
|
|
12
|
+
}> {
|
|
13
|
+
private timeout;
|
|
14
|
+
private timers;
|
|
15
|
+
constructor(timeout?: number);
|
|
16
|
+
refresh: (libraryId: string, replicaId: string) => void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
4
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
5
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
6
|
+
import { DocumentBaseline, Operation, ServerMessage } from '@verdant-web/common';
|
|
7
|
+
import EventEmitter from 'events';
|
|
8
|
+
import { Server as HttpServer, IncomingMessage, ServerResponse } from 'http';
|
|
9
|
+
import internal from 'stream';
|
|
10
|
+
import { FileStorage } from './files/FileStorage.js';
|
|
11
|
+
import { MessageSender } from './MessageSender.js';
|
|
12
|
+
import { UserProfiles } from './Profiles.js';
|
|
13
|
+
import { TokenInfo } from './TokenVerifier.js';
|
|
14
|
+
import { StorageFactory } from './storage/Storage.js';
|
|
15
|
+
export interface ServerOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Attach to a pre-existing HTTP server. Provide this if you have another server,
|
|
18
|
+
* like an Express server, that you want to add lofi to. If provided, you don't
|
|
19
|
+
* need to call .listen()
|
|
20
|
+
*/
|
|
21
|
+
httpServer?: HttpServer;
|
|
22
|
+
/**
|
|
23
|
+
* The secret value used to generate tokens from your authentication endpoint.
|
|
24
|
+
* Used to verify and decode user identity and library access on connection.
|
|
25
|
+
*/
|
|
26
|
+
tokenSecret: string;
|
|
27
|
+
/**
|
|
28
|
+
* Your storage implementation. See the sqlStorage export for a built-in
|
|
29
|
+
* option.
|
|
30
|
+
*/
|
|
31
|
+
storage: StorageFactory;
|
|
32
|
+
/**
|
|
33
|
+
* Optionally provide an implementation of the UserProfiles interface to look up
|
|
34
|
+
* static user profile data to use in persistence. This data is considered trusted
|
|
35
|
+
* and can be used to identify users with information like name or profile image.
|
|
36
|
+
*/
|
|
37
|
+
profiles?: UserProfiles<any>;
|
|
38
|
+
/**
|
|
39
|
+
* How many minutes of inactivity before a replica is considered truant and
|
|
40
|
+
* removed from the library. Defaults to 30 days.
|
|
41
|
+
*/
|
|
42
|
+
replicaTruancyMinutes?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Supply a logging function to log debug messages.
|
|
45
|
+
*/
|
|
46
|
+
log?: (...args: any[]) => void;
|
|
47
|
+
/**
|
|
48
|
+
* Disable history compaction. Storage usage will grow indefinitely. Not recommended.
|
|
49
|
+
*/
|
|
50
|
+
disableRebasing?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Provide a FileStorage backend to accept file uploads. Without a file backend,
|
|
53
|
+
* users will not be able to synchronize files, and all files added on peer replicas
|
|
54
|
+
* will be forever unavailable.
|
|
55
|
+
*
|
|
56
|
+
* A default, filesystem-backed FileStorage implementation is provided as an export
|
|
57
|
+
* of this library.
|
|
58
|
+
*/
|
|
59
|
+
fileStorage?: FileStorage;
|
|
60
|
+
fileConfig?: {
|
|
61
|
+
deleteExpirationDays?: number;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export interface ServerEvents {
|
|
65
|
+
error: (err: any) => void;
|
|
66
|
+
warning: (msg: string, ...args: any[]) => void;
|
|
67
|
+
changes: (info: Omit<TokenInfo, 'token'>, operations: Operation[], baselines: DocumentBaseline[]) => void;
|
|
68
|
+
request: (info: TokenInfo) => void;
|
|
69
|
+
'socket-close': (info: TokenInfo) => void;
|
|
70
|
+
'socket-connection': (info: TokenInfo) => void;
|
|
71
|
+
}
|
|
72
|
+
export declare interface Server {
|
|
73
|
+
on<Event extends keyof ServerEvents>(ev: Event, cb: ServerEvents[Event]): this;
|
|
74
|
+
off<Event extends keyof ServerEvents>(ev: Event, cb: ServerEvents[Event]): this;
|
|
75
|
+
emit<Event extends keyof ServerEvents>(ev: Event, ...args: Parameters<ServerEvents[Event]>): boolean;
|
|
76
|
+
}
|
|
77
|
+
export declare class Server extends EventEmitter implements MessageSender {
|
|
78
|
+
private httpServer;
|
|
79
|
+
private wss;
|
|
80
|
+
private fileStorage?;
|
|
81
|
+
private storage;
|
|
82
|
+
private library;
|
|
83
|
+
private clientConnections;
|
|
84
|
+
private keepalives;
|
|
85
|
+
private tokenVerifier;
|
|
86
|
+
private connectionToReplicaIdMap;
|
|
87
|
+
private log;
|
|
88
|
+
private __testMode;
|
|
89
|
+
constructor(options: ServerOptions);
|
|
90
|
+
/**
|
|
91
|
+
* Attaches the Verdant server to an HttpServer instance, which
|
|
92
|
+
* allows it to handle requests and websockets required for the
|
|
93
|
+
* Verdant protocol.
|
|
94
|
+
*
|
|
95
|
+
* You can pass httpPath = false if you want to handle HTTP requests
|
|
96
|
+
* yourself, and this will only attach the websocket handling.
|
|
97
|
+
*/
|
|
98
|
+
attach: (server: HttpServer, options?: {
|
|
99
|
+
httpPath?: string | false;
|
|
100
|
+
}) => void;
|
|
101
|
+
/**
|
|
102
|
+
* Handles an HTTP upgrade request to a websocket connection.
|
|
103
|
+
*/
|
|
104
|
+
handleUpgrade: (req: IncomingMessage, socket: internal.Duplex, head: Buffer) => Promise<void>;
|
|
105
|
+
private authorizeRequest;
|
|
106
|
+
private getRequestToken;
|
|
107
|
+
private createInternalRequestHandler;
|
|
108
|
+
/**
|
|
109
|
+
* Handles an HTTP request from a verdant client.
|
|
110
|
+
*/
|
|
111
|
+
handleRequest: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Handles a "fetch" style request. Complement to handleRequest, for servers
|
|
114
|
+
* that use Request/Response style handlers.
|
|
115
|
+
*/
|
|
116
|
+
handleFetch: (req: Request) => Promise<Response>;
|
|
117
|
+
private handleRequestBody;
|
|
118
|
+
private writeErrorResponse;
|
|
119
|
+
private getErrorResponse;
|
|
120
|
+
/**
|
|
121
|
+
* Handles a multipart upload of a file from a verdant client. The upload
|
|
122
|
+
* will include parameters for the file's ID, name, and type. The request
|
|
123
|
+
* must be authenticated with a token to tie it to a library.
|
|
124
|
+
*/
|
|
125
|
+
handleFileRequest: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Handles a "fetch" style file request. Complement to handleFileRequest,
|
|
128
|
+
* for servers that use Request/Response style handlers.
|
|
129
|
+
*/
|
|
130
|
+
handleFileFetch: (req: Request) => Promise<Response>;
|
|
131
|
+
private getFileStorageOrThrow;
|
|
132
|
+
private streamIncomingFile;
|
|
133
|
+
private getFileData;
|
|
134
|
+
broadcast: (libraryId: string, message: ServerMessage, omitKeys?: string[]) => void;
|
|
135
|
+
send: (libraryId: string, key: string, message: ServerMessage) => void;
|
|
136
|
+
private handleMessage;
|
|
137
|
+
private handleConnection;
|
|
138
|
+
listen: (handle: any, listeningListener?: (() => void) | undefined) => void;
|
|
139
|
+
close: () => Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* Completely remove a library from the server.
|
|
142
|
+
* This will remove all data associated with the library.
|
|
143
|
+
* Use it very carefully! Data will still be stored on user
|
|
144
|
+
* devices, so this is not as scary as it sounds - the next time
|
|
145
|
+
* any replica connects, it will repopulate the library. But
|
|
146
|
+
* you should still be careful.
|
|
147
|
+
*/
|
|
148
|
+
evictLibrary: (libraryId: string) => void;
|
|
149
|
+
/**
|
|
150
|
+
* Returns currently connected replica info for a library
|
|
151
|
+
*/
|
|
152
|
+
getLibraryPresence: (libraryId: string) => Record<string, import("@verdant-web/common").UserInfo<any, any>>;
|
|
153
|
+
/**
|
|
154
|
+
* Returns helpful information about a library.
|
|
155
|
+
* This is currently only the known replicas and some
|
|
156
|
+
* more low-level metadata.
|
|
157
|
+
*/
|
|
158
|
+
getLibraryInfo: (libraryId: string) => Promise<import("./types.js").LibraryInfo | null>;
|
|
159
|
+
/**
|
|
160
|
+
* Removes all replicas associated with a User ID from
|
|
161
|
+
* the server. Consistency algorithms will no longer wait on these
|
|
162
|
+
* replicas before confirming and squashing changes.
|
|
163
|
+
*/
|
|
164
|
+
evictUser: (libraryId: string, userId: string) => void;
|
|
165
|
+
/**
|
|
166
|
+
* Retrieves a snapshot of the full document contents at this time.
|
|
167
|
+
* Useful for capturing the shape of data for static usage outside
|
|
168
|
+
* of the live system.
|
|
169
|
+
*/
|
|
170
|
+
getDocumentSnapshot: (libraryId: string, collection: string, documentId: string) => Promise<any>;
|
|
171
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ClientMessage, DocumentBaseline, EventSubscriber, Operation } from '@verdant-web/common';
|
|
2
|
+
import { MessageSender } from './MessageSender.js';
|
|
3
|
+
import { UserProfileLoader } from './Profiles.js';
|
|
4
|
+
import { TokenInfo } from './TokenVerifier.js';
|
|
5
|
+
import { FileStorage } from './files/FileStorage.js';
|
|
6
|
+
import { Storage } from './storage/Storage.js';
|
|
7
|
+
import { LibraryInfo } from './types.js';
|
|
8
|
+
export type ServerLibraryEvents = {
|
|
9
|
+
changes: (info: TokenInfo, operations: Operation[], baselines: DocumentBaseline[]) => void;
|
|
10
|
+
};
|
|
11
|
+
export declare class ServerLibrary extends EventSubscriber<ServerLibraryEvents> {
|
|
12
|
+
private storage;
|
|
13
|
+
private sender;
|
|
14
|
+
private profiles;
|
|
15
|
+
private presences;
|
|
16
|
+
private disableRebasing;
|
|
17
|
+
private fileStorage;
|
|
18
|
+
private log;
|
|
19
|
+
constructor({ storage, sender, profiles, log, disableRebasing, fileStorage, }: {
|
|
20
|
+
storage: Storage;
|
|
21
|
+
sender: MessageSender;
|
|
22
|
+
profiles: UserProfileLoader<any>;
|
|
23
|
+
log?: (...args: any[]) => void;
|
|
24
|
+
disableRebasing?: boolean;
|
|
25
|
+
fileStorage?: FileStorage;
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Validates a user's access to a replica. If the replica does not
|
|
29
|
+
* exist, a user may create it. But if it does exist, its user (clientId)
|
|
30
|
+
* must match the user making the request.
|
|
31
|
+
*/
|
|
32
|
+
private validateReplicaAccess;
|
|
33
|
+
private hasWriteAccess;
|
|
34
|
+
receive: (message: ClientMessage, clientKey: string, info: TokenInfo) => Promise<void>;
|
|
35
|
+
remove: (libraryId: string, replicaId: string) => void;
|
|
36
|
+
private handleOperation;
|
|
37
|
+
private rebroadcastOperations;
|
|
38
|
+
private handleSync;
|
|
39
|
+
private createAckNonce;
|
|
40
|
+
private handleAck;
|
|
41
|
+
private updateHighwater;
|
|
42
|
+
private pendingRebaseTimeout;
|
|
43
|
+
private enqueueRebase;
|
|
44
|
+
private rebase;
|
|
45
|
+
private handleHeartbeat;
|
|
46
|
+
private handlePresenceUpdate;
|
|
47
|
+
private onPresenceLost;
|
|
48
|
+
destroy: (libraryId: string) => Promise<void>;
|
|
49
|
+
getPresence: (libraryId: string) => Record<string, import("@verdant-web/common").UserInfo<any, any>>;
|
|
50
|
+
getInfo: (libraryId: string) => Promise<LibraryInfo | null>;
|
|
51
|
+
evictUser: (libraryId: string, userId: string) => Promise<void>;
|
|
52
|
+
getDocumentSnapshot: (libraryId: string, oid: string) => Promise<any>;
|
|
53
|
+
private getObjectSnapshot;
|
|
54
|
+
private hydrateObject;
|
|
55
|
+
private hydrateFile;
|
|
56
|
+
private removeBaselineExtras;
|
|
57
|
+
private removeOperationExtras;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ReplicaType } from '@verdant-web/common';
|
|
2
|
+
export declare class TokenProvider {
|
|
3
|
+
private config;
|
|
4
|
+
constructor(config: {
|
|
5
|
+
secret: string;
|
|
6
|
+
});
|
|
7
|
+
getToken: ({ userId, libraryId, syncEndpoint, role, type, expiresIn, fileEndpoint, }: {
|
|
8
|
+
userId: string;
|
|
9
|
+
libraryId: string;
|
|
10
|
+
syncEndpoint: string;
|
|
11
|
+
fileEndpoint?: string | undefined;
|
|
12
|
+
role?: string | undefined;
|
|
13
|
+
type?: ReplicaType | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* The time when the token will expire. A string in the format of "3d" or a number of seconds.
|
|
16
|
+
*/
|
|
17
|
+
expiresIn?: string | number | undefined;
|
|
18
|
+
}) => string;
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ReplicaType } from '@verdant-web/common';
|
|
2
|
+
export declare class TokenVerifier {
|
|
3
|
+
private config;
|
|
4
|
+
constructor(config: {
|
|
5
|
+
secret: string;
|
|
6
|
+
});
|
|
7
|
+
verifyToken: (token: string) => TokenInfo;
|
|
8
|
+
}
|
|
9
|
+
export interface TokenInfo {
|
|
10
|
+
userId: string;
|
|
11
|
+
libraryId: string;
|
|
12
|
+
syncEndpoint: string;
|
|
13
|
+
role?: string;
|
|
14
|
+
type: ReplicaType;
|
|
15
|
+
token: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { Readable } from 'stream';
|
|
3
|
+
export interface FileInfo {
|
|
4
|
+
fileName: string;
|
|
5
|
+
id: string;
|
|
6
|
+
libraryId: string;
|
|
7
|
+
type: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The interface for implementing a file storage backend.
|
|
11
|
+
* verdant will supply file Blobs and metadata, and the storage backend
|
|
12
|
+
* must store them somewhere.
|
|
13
|
+
*
|
|
14
|
+
* The storage backend must also provide a way to get a URL for the file,
|
|
15
|
+
* even if the file is not yet uploaded, based on its metadata.
|
|
16
|
+
*/
|
|
17
|
+
export interface FileStorage {
|
|
18
|
+
put(fileStream: Readable, data: FileInfo): Promise<void>;
|
|
19
|
+
getUrl(data: FileInfo): string | Promise<string>;
|
|
20
|
+
delete(data: FileInfo): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export declare class LocalFileStorage implements FileStorage {
|
|
23
|
+
private readonly host;
|
|
24
|
+
private readonly rootDirectory;
|
|
25
|
+
constructor({ rootDirectory, host, }: {
|
|
26
|
+
rootDirectory: string;
|
|
27
|
+
host: string;
|
|
28
|
+
});
|
|
29
|
+
private prepareHost;
|
|
30
|
+
put(fileStream: Readable, data: FileInfo): Promise<void>;
|
|
31
|
+
getUrl(data: FileInfo): string;
|
|
32
|
+
delete(data: FileInfo): Promise<void>;
|
|
33
|
+
private getPath;
|
|
34
|
+
private getStorageLocation;
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { MessageSender } from './MessageSender.js';
|
|
2
|
+
export type { UserProfiles } from './Profiles.js';
|
|
3
|
+
export type { ClientMessage, ServerMessage } from '@verdant-web/common';
|
|
4
|
+
export { ReplicaType } from '@verdant-web/common';
|
|
5
|
+
export { Server } from './Server.js';
|
|
6
|
+
export { TokenProvider } from './TokenProvider.js';
|
|
7
|
+
export { LocalFileStorage } from './files/FileStorage.js';
|
|
8
|
+
export type { FileStorage, FileInfo } from './files/FileStorage.js';
|
|
9
|
+
export type { TokenInfo } from './TokenVerifier.js';
|
|
10
|
+
export type { LibraryInfo } from './types.js';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { DocumentBaseline, Operation, Ref, ReplicaType } from '@verdant-web/common';
|
|
2
|
+
import { HydratedDocumentBaseline, StoredOperation, StoredReplicaInfo, FileMetadata } from '../types.js';
|
|
3
|
+
import { FileInfo } from '../files/FileStorage.js';
|
|
4
|
+
export interface ReplicaStorage {
|
|
5
|
+
readonly truantCutoff: number;
|
|
6
|
+
get(libraryId: string, replicaId: string): Promise<StoredReplicaInfo | null>;
|
|
7
|
+
getOrCreate(libraryId: string, replicaId: string, info: {
|
|
8
|
+
userId: string;
|
|
9
|
+
type: ReplicaType;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
status: 'new' | 'existing' | 'truant';
|
|
12
|
+
replicaInfo: StoredReplicaInfo;
|
|
13
|
+
}>;
|
|
14
|
+
getAll(libraryId: string, options?: {
|
|
15
|
+
omitTruant: boolean;
|
|
16
|
+
}): Promise<StoredReplicaInfo[]>;
|
|
17
|
+
updateLastSeen(libraryId: string, replicaId: string): Promise<void>;
|
|
18
|
+
updateAckedServerOrder(libraryId: string, replicaId: string, serverOrder: number): Promise<void>;
|
|
19
|
+
updateAcknowledgedLogicalTime(libraryId: string, replicaId: string, timestamp: string): Promise<void>;
|
|
20
|
+
getEarliestAckedServerOrder(libraryId: string): Promise<number>;
|
|
21
|
+
acknowledgeOperation(libraryId: string, replicaId: string, timestamp: string): Promise<void>;
|
|
22
|
+
getGlobalAck(libraryId: string, onlineReplicaIds?: string[]): Promise<string | null>;
|
|
23
|
+
delete(libraryId: string, replicaId: string): Promise<void>;
|
|
24
|
+
deleteAll(libraryId: string): Promise<void>;
|
|
25
|
+
deleteAllForUser(libraryId: string, userId: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export interface OperationStorage {
|
|
28
|
+
getAll(libraryId: string, oid: string): Promise<StoredOperation[]>;
|
|
29
|
+
getBeforeServerOrder(libraryId: string, beforeServerOrder: number): Promise<StoredOperation[]>;
|
|
30
|
+
getAfterServerOrder(libraryId: string, afterServerOrder: number): Promise<StoredOperation[]>;
|
|
31
|
+
getLatestServerOrder(libraryId: string): Promise<number>;
|
|
32
|
+
getCount(libraryId: string): Promise<number>;
|
|
33
|
+
insertAll(libraryId: string, replicaId: string, operations: Operation[]): Promise<void>;
|
|
34
|
+
deleteAll(libraryId: string): Promise<void>;
|
|
35
|
+
delete(libraryId: string, operations: Operation[]): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export interface BaselineStorage {
|
|
38
|
+
get(libraryId: string, oid: string): Promise<HydratedDocumentBaseline | null>;
|
|
39
|
+
getAll(libraryId: string): Promise<HydratedDocumentBaseline[]>;
|
|
40
|
+
insertAll(libraryId: string, baselines: DocumentBaseline<any>[]): Promise<void>;
|
|
41
|
+
deleteAll(libraryId: string): Promise<void>;
|
|
42
|
+
getCount(libraryId: string): Promise<number>;
|
|
43
|
+
applyOperations(libraryId: string, oid: string, operations: StoredOperation[], deletedRefs?: Ref[]): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export interface FileMetadataStorage {
|
|
46
|
+
get(libraryId: string, fileId: string): Promise<FileMetadata | null>;
|
|
47
|
+
getAll(libraryId: string): Promise<FileMetadata[]>;
|
|
48
|
+
deleteAll(libraryId: string): Promise<void>;
|
|
49
|
+
put(libraryId: string, fileInfo: FileInfo): Promise<void>;
|
|
50
|
+
markPendingDelete(libraryId: string, fileId: string): Promise<void>;
|
|
51
|
+
delete(libraryId: string, fileId: string): Promise<void>;
|
|
52
|
+
getPendingDelete(libraryId: string): Promise<FileMetadata[]>;
|
|
53
|
+
}
|
|
54
|
+
export interface StorageOptions {
|
|
55
|
+
replicaTruancyMinutes: number;
|
|
56
|
+
fileDeleteExpirationDays: number;
|
|
57
|
+
}
|
|
58
|
+
export interface Storage {
|
|
59
|
+
replicas: ReplicaStorage;
|
|
60
|
+
operations: OperationStorage;
|
|
61
|
+
baselines: BaselineStorage;
|
|
62
|
+
fileMetadata: FileMetadataStorage;
|
|
63
|
+
close(): Promise<void>;
|
|
64
|
+
readonly open: boolean;
|
|
65
|
+
readonly ready: Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export type StorageFactory = (options: StorageOptions) => Storage;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Kysely } from 'kysely';
|
|
2
|
+
import { BaselineStorage } from '../Storage.js';
|
|
3
|
+
import { Database } from './tables.js';
|
|
4
|
+
import { DocumentBaseline, Ref } from '@verdant-web/common';
|
|
5
|
+
import { HydratedDocumentBaseline, StoredOperation } from '../../types.js';
|
|
6
|
+
export declare class SqlBaselines implements BaselineStorage {
|
|
7
|
+
private db;
|
|
8
|
+
private dialect;
|
|
9
|
+
constructor(db: Kysely<Database>, dialect: 'postgres' | 'sqlite');
|
|
10
|
+
get: (libraryId: string, oid: string, { tx }?: {
|
|
11
|
+
tx?: Kysely<Database> | undefined;
|
|
12
|
+
}) => Promise<HydratedDocumentBaseline | null>;
|
|
13
|
+
getAll: (libraryId: string) => Promise<any>;
|
|
14
|
+
set: (libraryId: string, baseline: DocumentBaseline, { tx, }: {
|
|
15
|
+
tx?: Kysely<Database> | undefined;
|
|
16
|
+
}) => Promise<void>;
|
|
17
|
+
insertAll: (libraryId: string, baselines: DocumentBaseline[]) => Promise<void>;
|
|
18
|
+
private hydrate;
|
|
19
|
+
applyOperations: (libraryId: string, oid: string, operations: StoredOperation[], deletedRefs?: Ref[]) => Promise<void>;
|
|
20
|
+
deleteAll: (libraryId: string) => Promise<void>;
|
|
21
|
+
getCount: (libraryId: string) => Promise<number>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Kysely } from 'kysely';
|
|
2
|
+
import { FileMetadataStorage } from '../Storage.js';
|
|
3
|
+
import { Database } from './tables.js';
|
|
4
|
+
import { FileMetadata } from '../../types.js';
|
|
5
|
+
import { FileInfo } from '../../files/FileStorage.js';
|
|
6
|
+
export declare class SqlFileMetadata implements FileMetadataStorage {
|
|
7
|
+
private db;
|
|
8
|
+
private deleteExpirationDays;
|
|
9
|
+
private dialect;
|
|
10
|
+
constructor(db: Kysely<Database>, deleteExpirationDays: number, dialect: 'postgres' | 'sqlite');
|
|
11
|
+
get: (libraryId: string, fileId: string) => Promise<FileMetadata | null>;
|
|
12
|
+
getAll: (libraryId: string) => Promise<FileMetadata[]>;
|
|
13
|
+
deleteAll: (libraryId: string) => Promise<void>;
|
|
14
|
+
put: (libraryId: string, fileInfo: FileInfo) => Promise<void>;
|
|
15
|
+
markPendingDelete: (libraryId: string, fileId: string) => Promise<void>;
|
|
16
|
+
delete: (libraryId: string, fileId: string) => Promise<void>;
|
|
17
|
+
getPendingDelete: (libraryId: string) => Promise<FileMetadata[]>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Operation } from '@verdant-web/common';
|
|
2
|
+
import { StoredOperation } from '../../types.js';
|
|
3
|
+
import { OperationStorage } from '../Storage.js';
|
|
4
|
+
import { Kysely } from 'kysely';
|
|
5
|
+
import { Database } from './tables.js';
|
|
6
|
+
export declare class SqlOperations implements OperationStorage {
|
|
7
|
+
private db;
|
|
8
|
+
private dialect;
|
|
9
|
+
constructor(db: Kysely<Database>, dialect: 'postgres' | 'sqlite');
|
|
10
|
+
private hydrate;
|
|
11
|
+
getAll: (libraryId: string, oid: string) => Promise<StoredOperation[]>;
|
|
12
|
+
getBeforeServerOrder: (libraryId: string, beforeServerOrder: number) => Promise<StoredOperation[]>;
|
|
13
|
+
getAfterServerOrder: (libraryId: string, afterServerOrder: number) => Promise<StoredOperation[]>;
|
|
14
|
+
getLatestServerOrder: (libraryId: string) => Promise<number>;
|
|
15
|
+
getCount: (libraryId: string) => Promise<number>;
|
|
16
|
+
insertAll: (libraryId: string, replicaId: string, operations: Operation[]) => Promise<void>;
|
|
17
|
+
deleteAll: (libraryId: string) => Promise<void>;
|
|
18
|
+
delete: (libraryId: string, operations: Operation[]) => Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { StoredReplicaInfo } from '../../types.js';
|
|
2
|
+
import { ReplicaStorage } from '../Storage.js';
|
|
3
|
+
import { type Kysely } from 'kysely';
|
|
4
|
+
import { Database } from './tables.js';
|
|
5
|
+
import { ReplicaType } from '@verdant-web/common';
|
|
6
|
+
export declare class SqlReplicas implements ReplicaStorage {
|
|
7
|
+
private db;
|
|
8
|
+
private readonly replicaTruancyMinutes;
|
|
9
|
+
private readonly dialect;
|
|
10
|
+
constructor(db: Kysely<Database>, replicaTruancyMinutes: number, dialect: 'sqlite' | 'postgres');
|
|
11
|
+
get truantCutoff(): number;
|
|
12
|
+
get: (libraryId: string, replicaId: string) => Promise<StoredReplicaInfo | null>;
|
|
13
|
+
getOrCreate: (libraryId: string, replicaId: string, info: {
|
|
14
|
+
userId: string;
|
|
15
|
+
type: ReplicaType;
|
|
16
|
+
}) => Promise<{
|
|
17
|
+
status: 'new' | 'existing' | 'truant';
|
|
18
|
+
replicaInfo: StoredReplicaInfo;
|
|
19
|
+
}>;
|
|
20
|
+
getAll: (libraryId: string, options?: {
|
|
21
|
+
omitTruant: boolean;
|
|
22
|
+
} | undefined) => Promise<StoredReplicaInfo[]>;
|
|
23
|
+
updateLastSeen: (libraryId: string, replicaId: string) => Promise<void>;
|
|
24
|
+
updateAckedServerOrder: (libraryId: string, replicaId: string, serverOrder: number) => Promise<void>;
|
|
25
|
+
updateAcknowledgedLogicalTime: (libraryId: string, replicaId: string, timestamp: string) => Promise<void>;
|
|
26
|
+
getEarliestAckedServerOrder: (libraryId: string) => Promise<number>;
|
|
27
|
+
acknowledgeOperation: (libraryId: string, replicaId: string, timestamp: string) => Promise<void>;
|
|
28
|
+
getGlobalAck: (libraryId: string, onlineReplicaIds?: string[] | undefined) => Promise<string | null>;
|
|
29
|
+
delete: (libraryId: string, replicaId: string) => Promise<void>;
|
|
30
|
+
deleteAll: (libraryId: string) => Promise<void>;
|
|
31
|
+
deleteAllForUser: (libraryId: string, userId: string) => Promise<void>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ReplicaType } from '@verdant-web/common';
|
|
2
|
+
import { Generated, Selectable } from 'kysely';
|
|
3
|
+
export interface Database {
|
|
4
|
+
OperationHistory: OperationHistoryTable;
|
|
5
|
+
DocumentBaseline: DocumentBaselineTable;
|
|
6
|
+
ReplicaInfo: ReplicaInfoTable;
|
|
7
|
+
FileMetadata: FileMetadataTable;
|
|
8
|
+
}
|
|
9
|
+
export interface OperationHistoryTable {
|
|
10
|
+
oid: string;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
data: string;
|
|
13
|
+
serverOrder: number;
|
|
14
|
+
replicaId: string;
|
|
15
|
+
libraryId: string;
|
|
16
|
+
}
|
|
17
|
+
export type OperationHistoryRow = Selectable<OperationHistoryTable>;
|
|
18
|
+
export interface DocumentBaselineTable {
|
|
19
|
+
oid: string;
|
|
20
|
+
snapshot: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
libraryId: string;
|
|
23
|
+
}
|
|
24
|
+
export type DocumentBaselineRow = Selectable<DocumentBaselineTable>;
|
|
25
|
+
export interface ReplicaInfoTable {
|
|
26
|
+
id: string;
|
|
27
|
+
libraryId: string;
|
|
28
|
+
clientId: string;
|
|
29
|
+
lastSeenWallClockTime: number | null;
|
|
30
|
+
ackedLogicalTime: string | null;
|
|
31
|
+
type: ReplicaType;
|
|
32
|
+
ackedServerOrder: Generated<number>;
|
|
33
|
+
}
|
|
34
|
+
export interface FileMetadataTable {
|
|
35
|
+
libraryId: string;
|
|
36
|
+
fileId: string;
|
|
37
|
+
name: string;
|
|
38
|
+
type: string;
|
|
39
|
+
pendingDeleteAt: number | null;
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DocumentBaseline, Operation, ReplicaInfo, ReplicaType } from '@verdant-web/common';
|
|
2
|
+
export interface StoredOperation extends Operation {
|
|
3
|
+
serverOrder: number;
|
|
4
|
+
replicaId: string;
|
|
5
|
+
}
|
|
6
|
+
export interface StoredDocumentBaseline extends Omit<DocumentBaseline<any>, 'snapshot'> {
|
|
7
|
+
snapshot: string;
|
|
8
|
+
libraryId: string;
|
|
9
|
+
}
|
|
10
|
+
export interface HydratedDocumentBaseline extends DocumentBaseline<any> {
|
|
11
|
+
libraryId: string;
|
|
12
|
+
}
|
|
13
|
+
export interface StoredReplicaInfo extends ReplicaInfo {
|
|
14
|
+
libraryId: string;
|
|
15
|
+
clientId: string;
|
|
16
|
+
lastSeenWallClockTime: number | null;
|
|
17
|
+
type: ReplicaType;
|
|
18
|
+
ackedServerOrder: number;
|
|
19
|
+
}
|
|
20
|
+
export type LibraryInfo<Profile = unknown> = {
|
|
21
|
+
id: string;
|
|
22
|
+
replicas: {
|
|
23
|
+
id: string;
|
|
24
|
+
ackedLogicalTime: string | null;
|
|
25
|
+
ackedServerOrder: number;
|
|
26
|
+
type: ReplicaType;
|
|
27
|
+
truant: boolean;
|
|
28
|
+
profile: Profile;
|
|
29
|
+
}[];
|
|
30
|
+
latestServerOrder: number;
|
|
31
|
+
operationsCount: number;
|
|
32
|
+
baselinesCount: number;
|
|
33
|
+
globalAck: string | null;
|
|
34
|
+
};
|
|
35
|
+
export interface FileMetadata {
|
|
36
|
+
libraryId: string;
|
|
37
|
+
fileId: string;
|
|
38
|
+
name: string;
|
|
39
|
+
type: string;
|
|
40
|
+
pendingDeleteAt: number | null;
|
|
41
|
+
}
|
|
42
|
+
declare global {
|
|
43
|
+
interface File {
|
|
44
|
+
readonly lastModified: number;
|
|
45
|
+
readonly name: string;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verdant-web/server",
|
|
3
|
-
"version": "3.0.0
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"access": "public",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"kysely-plugin-serialize": "^0.5.7",
|
|
34
34
|
"minimist": "^1.2.8",
|
|
35
35
|
"ws": "^8.9.0",
|
|
36
|
-
"@verdant-web/common": "2.3.0
|
|
36
|
+
"@verdant-web/common": "2.3.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/async-lock": "^1.3.0",
|