@verdant-web/server 1.7.8

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 (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/cjs/Baselines.d.ts +14 -0
  4. package/dist/cjs/Baselines.js +101 -0
  5. package/dist/cjs/Baselines.js.map +1 -0
  6. package/dist/cjs/ClientConnection.d.ts +18 -0
  7. package/dist/cjs/ClientConnection.js +106 -0
  8. package/dist/cjs/ClientConnection.js.map +1 -0
  9. package/dist/cjs/MessageSender.d.ts +5 -0
  10. package/dist/cjs/MessageSender.js +3 -0
  11. package/dist/cjs/MessageSender.js.map +1 -0
  12. package/dist/cjs/OperationHistory.d.ts +34 -0
  13. package/dist/cjs/OperationHistory.js +147 -0
  14. package/dist/cjs/OperationHistory.js.map +1 -0
  15. package/dist/cjs/Presence.d.ts +15 -0
  16. package/dist/cjs/Presence.js +60 -0
  17. package/dist/cjs/Presence.js.map +1 -0
  18. package/dist/cjs/Profiles.d.ts +10 -0
  19. package/dist/cjs/Profiles.js +20 -0
  20. package/dist/cjs/Profiles.js.map +1 -0
  21. package/dist/cjs/ReplicaKeepaliveTimers.d.ts +17 -0
  22. package/dist/cjs/ReplicaKeepaliveTimers.js +31 -0
  23. package/dist/cjs/ReplicaKeepaliveTimers.js.map +1 -0
  24. package/dist/cjs/Replicas.d.ts +23 -0
  25. package/dist/cjs/Replicas.js +174 -0
  26. package/dist/cjs/Replicas.js.map +1 -0
  27. package/dist/cjs/Server.d.ts +121 -0
  28. package/dist/cjs/Server.js +378 -0
  29. package/dist/cjs/Server.js.map +1 -0
  30. package/dist/cjs/ServerLibrary.d.ts +63 -0
  31. package/dist/cjs/ServerLibrary.js +468 -0
  32. package/dist/cjs/ServerLibrary.js.map +1 -0
  33. package/dist/cjs/ServerStorage.d.ts +35 -0
  34. package/dist/cjs/ServerStorage.js +60 -0
  35. package/dist/cjs/ServerStorage.js.map +1 -0
  36. package/dist/cjs/TokenProvider.d.ts +16 -0
  37. package/dist/cjs/TokenProvider.js +27 -0
  38. package/dist/cjs/TokenProvider.js.map +1 -0
  39. package/dist/cjs/TokenVerifier.d.ts +16 -0
  40. package/dist/cjs/TokenVerifier.js +29 -0
  41. package/dist/cjs/TokenVerifier.js.map +1 -0
  42. package/dist/cjs/files/FileMetadata.d.ts +28 -0
  43. package/dist/cjs/files/FileMetadata.js +69 -0
  44. package/dist/cjs/files/FileMetadata.js.map +1 -0
  45. package/dist/cjs/files/FileStorage.d.ts +35 -0
  46. package/dist/cjs/files/FileStorage.js +72 -0
  47. package/dist/cjs/files/FileStorage.js.map +1 -0
  48. package/dist/cjs/index.d.ts +9 -0
  49. package/dist/cjs/index.js +12 -0
  50. package/dist/cjs/index.js.map +1 -0
  51. package/dist/cjs/migrations/v0.sql.d.ts +2 -0
  52. package/dist/cjs/migrations/v0.sql.js +29 -0
  53. package/dist/cjs/migrations/v0.sql.js.map +1 -0
  54. package/dist/cjs/migrations/v1.sql.d.ts +2 -0
  55. package/dist/cjs/migrations/v1.sql.js +31 -0
  56. package/dist/cjs/migrations/v1.sql.js.map +1 -0
  57. package/dist/cjs/migrations/v2.sql.d.ts +2 -0
  58. package/dist/cjs/migrations/v2.sql.js +28 -0
  59. package/dist/cjs/migrations/v2.sql.js.map +1 -0
  60. package/dist/cjs/migrations/v3.sql.d.ts +2 -0
  61. package/dist/cjs/migrations/v3.sql.js +14 -0
  62. package/dist/cjs/migrations/v3.sql.js.map +1 -0
  63. package/dist/cjs/migrations/v4.sql.d.ts +2 -0
  64. package/dist/cjs/migrations/v4.sql.js +8 -0
  65. package/dist/cjs/migrations/v4.sql.js.map +1 -0
  66. package/dist/cjs/migrations.d.ts +2 -0
  67. package/dist/cjs/migrations.js +41 -0
  68. package/dist/cjs/migrations.js.map +1 -0
  69. package/dist/cjs/types.d.ts +22 -0
  70. package/dist/cjs/types.js +3 -0
  71. package/dist/cjs/types.js.map +1 -0
  72. package/dist/esm/Baselines.d.ts +14 -0
  73. package/dist/esm/Baselines.js +97 -0
  74. package/dist/esm/Baselines.js.map +1 -0
  75. package/dist/esm/ClientConnection.d.ts +18 -0
  76. package/dist/esm/ClientConnection.js +102 -0
  77. package/dist/esm/ClientConnection.js.map +1 -0
  78. package/dist/esm/MessageSender.d.ts +5 -0
  79. package/dist/esm/MessageSender.js +2 -0
  80. package/dist/esm/MessageSender.js.map +1 -0
  81. package/dist/esm/OperationHistory.d.ts +34 -0
  82. package/dist/esm/OperationHistory.js +143 -0
  83. package/dist/esm/OperationHistory.js.map +1 -0
  84. package/dist/esm/Presence.d.ts +15 -0
  85. package/dist/esm/Presence.js +53 -0
  86. package/dist/esm/Presence.js.map +1 -0
  87. package/dist/esm/Profiles.d.ts +10 -0
  88. package/dist/esm/Profiles.js +16 -0
  89. package/dist/esm/Profiles.js.map +1 -0
  90. package/dist/esm/ReplicaKeepaliveTimers.d.ts +17 -0
  91. package/dist/esm/ReplicaKeepaliveTimers.js +27 -0
  92. package/dist/esm/ReplicaKeepaliveTimers.js.map +1 -0
  93. package/dist/esm/Replicas.d.ts +23 -0
  94. package/dist/esm/Replicas.js +170 -0
  95. package/dist/esm/Replicas.js.map +1 -0
  96. package/dist/esm/Server.d.ts +121 -0
  97. package/dist/esm/Server.js +371 -0
  98. package/dist/esm/Server.js.map +1 -0
  99. package/dist/esm/ServerLibrary.d.ts +63 -0
  100. package/dist/esm/ServerLibrary.js +464 -0
  101. package/dist/esm/ServerLibrary.js.map +1 -0
  102. package/dist/esm/ServerStorage.d.ts +44 -0
  103. package/dist/esm/ServerStorage.js +58 -0
  104. package/dist/esm/ServerStorage.js.map +1 -0
  105. package/dist/esm/TokenProvider.d.ts +16 -0
  106. package/dist/esm/TokenProvider.js +20 -0
  107. package/dist/esm/TokenProvider.js.map +1 -0
  108. package/dist/esm/TokenVerifier.d.ts +16 -0
  109. package/dist/esm/TokenVerifier.js +22 -0
  110. package/dist/esm/TokenVerifier.js.map +1 -0
  111. package/dist/esm/files/FileMetadata.d.ts +28 -0
  112. package/dist/esm/files/FileMetadata.js +65 -0
  113. package/dist/esm/files/FileMetadata.js.map +1 -0
  114. package/dist/esm/files/FileStorage.d.ts +35 -0
  115. package/dist/esm/files/FileStorage.js +45 -0
  116. package/dist/esm/files/FileStorage.js.map +1 -0
  117. package/dist/esm/index.d.ts +9 -0
  118. package/dist/esm/index.js +5 -0
  119. package/dist/esm/index.js.map +1 -0
  120. package/dist/esm/migrations/v0.sql.d.ts +2 -0
  121. package/dist/esm/migrations/v0.sql.js +27 -0
  122. package/dist/esm/migrations/v0.sql.js.map +1 -0
  123. package/dist/esm/migrations/v1.sql.d.ts +2 -0
  124. package/dist/esm/migrations/v1.sql.js +29 -0
  125. package/dist/esm/migrations/v1.sql.js.map +1 -0
  126. package/dist/esm/migrations/v2.sql.d.ts +2 -0
  127. package/dist/esm/migrations/v2.sql.js +26 -0
  128. package/dist/esm/migrations/v2.sql.js.map +1 -0
  129. package/dist/esm/migrations/v3.sql.d.ts +2 -0
  130. package/dist/esm/migrations/v3.sql.js +12 -0
  131. package/dist/esm/migrations/v3.sql.js.map +1 -0
  132. package/dist/esm/migrations/v4.sql.d.ts +2 -0
  133. package/dist/esm/migrations/v4.sql.js +6 -0
  134. package/dist/esm/migrations/v4.sql.js.map +1 -0
  135. package/dist/esm/migrations.d.ts +2 -0
  136. package/dist/esm/migrations.js +34 -0
  137. package/dist/esm/migrations.js.map +1 -0
  138. package/dist/esm/types.d.ts +22 -0
  139. package/dist/esm/types.js +2 -0
  140. package/dist/esm/types.js.map +1 -0
  141. package/dist/tsconfig-cjs.tsbuildinfo +1 -0
  142. package/dist/tsconfig.tsbuildinfo +1 -0
  143. package/package.json +43 -0
  144. package/src/Baselines.ts +158 -0
  145. package/src/ClientConnection.ts +150 -0
  146. package/src/MessageSender.ts +10 -0
  147. package/src/OperationHistory.ts +206 -0
  148. package/src/Presence.ts +63 -0
  149. package/src/Profiles.ts +21 -0
  150. package/src/ReplicaKeepaliveTimers.ts +34 -0
  151. package/src/Replicas.ts +238 -0
  152. package/src/Server.ts +553 -0
  153. package/src/ServerLibrary.ts +727 -0
  154. package/src/TokenProvider.ts +39 -0
  155. package/src/TokenVerifier.ts +30 -0
  156. package/src/files/FileMetadata.ts +112 -0
  157. package/src/files/FileStorage.ts +87 -0
  158. package/src/index.ts +9 -0
  159. package/src/migrations/v0.sql.ts +26 -0
  160. package/src/migrations/v1.sql.ts +28 -0
  161. package/src/migrations/v2.sql.ts +26 -0
  162. package/src/migrations/v3.sql.ts +12 -0
  163. package/src/migrations/v4.sql.ts +6 -0
  164. package/src/migrations.ts +51 -0
  165. package/src/types.ts +35 -0
@@ -0,0 +1,170 @@
1
+ export class ReplicaInfos {
2
+ constructor(db, replicaTruancyMinutes) {
3
+ this.db = db;
4
+ this.replicaTruancyMinutes = replicaTruancyMinutes;
5
+ this.get = (libraryId, replicaId) => {
6
+ const row = this.db
7
+ .prepare(`
8
+ SELECT * FROM ReplicaInfo
9
+ WHERE id = ? AND libraryId = ?
10
+ `)
11
+ .get(replicaId, libraryId);
12
+ if (!row) {
13
+ return null;
14
+ }
15
+ return {
16
+ id: row.id,
17
+ libraryId: row.libraryId,
18
+ clientId: row.clientId,
19
+ lastSeenWallClockTime: row.lastSeenWallClockTime,
20
+ ackedLogicalTime: row.ackedLogicalTime,
21
+ type: row.type,
22
+ ackedServerOrder: row.ackedServerOrder,
23
+ };
24
+ };
25
+ this.getOrCreate = (libraryId, replicaId, info) => {
26
+ const replicaInfo = this.db
27
+ .prepare(`
28
+ SELECT * FROM ReplicaInfo
29
+ WHERE id = ? AND libraryId = ?
30
+ `)
31
+ .get(replicaId, libraryId);
32
+ if (!replicaInfo) {
33
+ this.db
34
+ .prepare(`
35
+ INSERT INTO ReplicaInfo (id, clientId, libraryId, type)
36
+ VALUES (?, ?, ?, ?)
37
+ `)
38
+ .run(replicaId, info.userId, libraryId, info.type);
39
+ return {
40
+ status: 'new',
41
+ replicaInfo: {
42
+ id: replicaId,
43
+ clientId: info.userId,
44
+ ackedLogicalTime: null,
45
+ lastSeenWallClockTime: null,
46
+ libraryId: libraryId,
47
+ type: info.type,
48
+ ackedServerOrder: 0,
49
+ },
50
+ };
51
+ }
52
+ if (replicaInfo.type !== info.type) {
53
+ // type should be updated if a new token changes it
54
+ this.db
55
+ .prepare(`
56
+ UPDATE ReplicaInfo
57
+ SET type = ?
58
+ WHERE id = ? AND libraryId = ?
59
+ `)
60
+ .run(info.type, replicaId, libraryId);
61
+ }
62
+ if (replicaInfo.clientId !== info.userId) {
63
+ throw new Error(`Replica ${replicaId} already exists with a different user ID`);
64
+ }
65
+ if (replicaInfo.lastSeenWallClockTime &&
66
+ replicaInfo.lastSeenWallClockTime < this.truantCutoff) {
67
+ return {
68
+ status: 'truant',
69
+ replicaInfo,
70
+ };
71
+ }
72
+ return {
73
+ status: 'existing',
74
+ replicaInfo,
75
+ };
76
+ };
77
+ this.getAllNonTruant = (libraryId) => {
78
+ return this.db
79
+ .prepare(`
80
+ SELECT * FROM ReplicaInfo
81
+ WHERE libraryId = ? AND lastSeenWallClockTime > ?
82
+ `)
83
+ .all(libraryId, this.truantCutoff);
84
+ };
85
+ this.updateLastSeen = (libraryId, replicaId) => {
86
+ const clockTime = new Date().getTime();
87
+ return this.db
88
+ .prepare(`
89
+ UPDATE ReplicaInfo
90
+ SET lastSeenWallClockTime = ?
91
+ WHERE id = ? AND libraryId = ?
92
+ `)
93
+ .run(clockTime, replicaId, libraryId);
94
+ };
95
+ this.updateAcknowledged = (libraryId, replicaId, timestamp) => {
96
+ this.db
97
+ .prepare(`
98
+ UPDATE ReplicaInfo
99
+ SET ackedLogicalTime = ?
100
+ WHERE id = ? AND libraryId = ?
101
+ `)
102
+ .run(timestamp, replicaId, libraryId);
103
+ };
104
+ this.updateServerOrder = (libraryId, replicaId, serverOrder) => {
105
+ // only increment the server order, never set it to lower than before
106
+ this.db
107
+ .prepare(`
108
+ UPDATE ReplicaInfo
109
+ SET ackedServerOrder = MAX(ackedServerOrder, ?)
110
+ WHERE id = ? AND libraryId = ?
111
+ `)
112
+ .run(serverOrder, replicaId, libraryId);
113
+ };
114
+ this.acknowledgeOperation = (libraryId, replicaId, timestamp) => {
115
+ if (!timestamp)
116
+ return;
117
+ // get the server order from the operation with the timestamp and update the replica's server order
118
+ // to the max of the current server order and the server order of the operation
119
+ this.db
120
+ .prepare(`
121
+ UPDATE ReplicaInfo
122
+ SET ackedLogicalTime = ?, ackedServerOrder = MAX(ackedServerOrder, COALESCE((
123
+ SELECT serverOrder FROM OperationHistory
124
+ WHERE libraryId = ? AND timestamp = ?
125
+ ), 0))
126
+ WHERE id = ? AND libraryId = ?
127
+ `)
128
+ .run(timestamp, libraryId, timestamp, replicaId, libraryId);
129
+ };
130
+ this.getGlobalAck = (libraryId, onlineReplicaIds) => {
131
+ const nonTruant = this.getAllNonTruant(libraryId);
132
+ const globalAckEligible = nonTruant.filter((replica) => replica.type < 2 || (onlineReplicaIds === null || onlineReplicaIds === void 0 ? void 0 : onlineReplicaIds.includes(replica.id)));
133
+ // get the earliest acked time
134
+ return globalAckEligible.reduce((acc, replica) => {
135
+ if (!replica.ackedLogicalTime) {
136
+ return acc;
137
+ }
138
+ if (acc === undefined) {
139
+ return replica.ackedLogicalTime;
140
+ }
141
+ return acc < replica.ackedLogicalTime ? acc : replica.ackedLogicalTime;
142
+ }, undefined);
143
+ };
144
+ this.delete = (libraryId, replicaId) => {
145
+ return this.db
146
+ .prepare(`
147
+ DELETE FROM ReplicaInfo
148
+ WHERE id = ? AND libraryId = ?
149
+ `)
150
+ .run(replicaId, libraryId);
151
+ };
152
+ this.deleteAll = (libraryId) => {
153
+ return this.db
154
+ .prepare(`
155
+ DELETE FROM ReplicaInfo
156
+ WHERE libraryId = ?
157
+ `)
158
+ .run(libraryId);
159
+ };
160
+ this.deleteAllForUser = (libraryId, userId) => {
161
+ return this.db
162
+ .prepare(`DELETE FROM ReplicaInfo WHERE libraryId = ? AND clientId = ?`)
163
+ .run(libraryId, userId);
164
+ };
165
+ }
166
+ get truantCutoff() {
167
+ return Date.now() - this.replicaTruancyMinutes * 60 * 1000;
168
+ }
169
+ }
170
+ //# sourceMappingURL=Replicas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Replicas.js","sourceRoot":"","sources":["../../src/Replicas.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ /// <reference types="node" resolution-mode="require"/>
3
+ import { DocumentBaseline, Operation, ServerMessage } from '@verdant-web/common';
4
+ import EventEmitter from 'events';
5
+ import { IncomingMessage, Server as HttpServer, ServerResponse } from 'http';
6
+ import { UserProfiles } from './Profiles.js';
7
+ import { MessageSender } from './MessageSender.js';
8
+ import { TokenInfo } from './TokenVerifier.js';
9
+ import { FileStorage } from './files/FileStorage.js';
10
+ import { FileMetadataConfig } from './files/FileMetadata.js';
11
+ export interface ServerOptions {
12
+ /**
13
+ * Attach to a pre-existing HTTP server. Provide this if you have another server,
14
+ * like an Express server, that you want to add lofi to. If provided, you don't
15
+ * need to call .listen()
16
+ */
17
+ httpServer?: HttpServer;
18
+ /**
19
+ * The secret value used to generate tokens from your authentication endpoint.
20
+ * Used to verify and decode user identity and library access on connection.
21
+ */
22
+ tokenSecret: string;
23
+ /**
24
+ * The path to your SQLite database.
25
+ */
26
+ databaseFile: string;
27
+ /**
28
+ * Optionally provide an implementation of the UserProfiles interface to look up
29
+ * static user profile data to use in persistence. This data is considered trusted
30
+ * and can be used to identify users with information like name or profile image.
31
+ */
32
+ profiles?: UserProfiles<any>;
33
+ /**
34
+ * How many minutes of inactivity before a replica is considered truant and
35
+ * removed from the library. Defaults to 30 days.
36
+ */
37
+ replicaTruancyMinutes?: number;
38
+ /**
39
+ * Supply a logging function to log debug messages.
40
+ */
41
+ log?: (...args: any[]) => void;
42
+ /**
43
+ * Disable history compaction. Storage usage will grow indefinitely. Not recommended.
44
+ */
45
+ disableRebasing?: boolean;
46
+ /**
47
+ * Provide a FileStorage backend to accept file uploads. Without a file backend,
48
+ * users will not be able to synchronize files, and all files added on peer replicas
49
+ * will be forever unavailable.
50
+ *
51
+ * A default, filesystem-backed FileStorage implementation is provided as an export
52
+ * of this library.
53
+ */
54
+ fileStorage?: FileStorage;
55
+ fileConfig?: FileMetadataConfig;
56
+ }
57
+ export interface ServerEvents {
58
+ error: (err: any) => void;
59
+ warning: (msg: string, ...args: any[]) => void;
60
+ changes: (info: Omit<TokenInfo, 'token'>, operations: Operation[], baselines: DocumentBaseline[]) => void;
61
+ request: (info: TokenInfo) => void;
62
+ 'socket-close': (info: TokenInfo) => void;
63
+ 'socket-connection': (info: TokenInfo) => void;
64
+ }
65
+ export declare interface Server {
66
+ on<Event extends keyof ServerEvents>(ev: Event, cb: ServerEvents[Event]): this;
67
+ off<Event extends keyof ServerEvents>(ev: Event, cb: ServerEvents[Event]): this;
68
+ emit<Event extends keyof ServerEvents>(ev: Event, ...args: Parameters<ServerEvents[Event]>): boolean;
69
+ }
70
+ export declare class Server extends EventEmitter implements MessageSender {
71
+ readonly httpServer: HttpServer;
72
+ private wss;
73
+ private fileStorage?;
74
+ private fileMetadata;
75
+ private library;
76
+ private clientConnections;
77
+ private keepalives;
78
+ private tokenVerifier;
79
+ private connectionToReplicaIdMap;
80
+ private log;
81
+ private __testMode;
82
+ constructor(options: ServerOptions);
83
+ private authorizeRequest;
84
+ private getRequestToken;
85
+ private internalServerHandleRequest;
86
+ handleRequest: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
87
+ /**
88
+ * Handles a multipart upload of a file from a verdant client. The upload
89
+ * will include parameters for the file's ID, name, and type. The request
90
+ * must be authenticated with a token to tie it to a library.
91
+ */
92
+ handleFileRequest: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
93
+ broadcast: (libraryId: string, message: ServerMessage, omitKeys?: string[]) => void;
94
+ send: (libraryId: string, key: string, message: ServerMessage) => void;
95
+ private handleMessage;
96
+ private handleConnection;
97
+ listen: (handle: any, listeningListener?: (() => void) | undefined) => void;
98
+ close: () => Promise<void>;
99
+ /**
100
+ * Completely remove a library from the server.
101
+ * This will remove all data associated with the library.
102
+ * Use it very carefully! Data will still be stored on user
103
+ * devices, so this is not as scary as it sounds - the next time
104
+ * any replica connects, it will repopulate the library. But
105
+ * you should still be careful.
106
+ */
107
+ evictLibrary: (libraryId: string) => void;
108
+ getLibraryPresence: (libraryId: string) => Record<string, import("@verdant-web/common").UserInfo<any, any>>;
109
+ /**
110
+ * Removes all replicas associated with a User ID from
111
+ * the server. Consistency algorithms will no longer wait on these
112
+ * replicas before confirming and squashing changes.
113
+ */
114
+ evictUser: (libraryId: string, userId: string) => void;
115
+ /**
116
+ * Retrieves a snapshot of the full document contents at this time.
117
+ * Useful for capturing the shape of data for static usage outside
118
+ * of the live system.
119
+ */
120
+ getDocumentSnapshot: (libraryId: string, collection: string, documentId: string) => any;
121
+ }
@@ -0,0 +1,371 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { assert, createOid, generateId, } from '@verdant-web/common';
13
+ import EventEmitter from 'events';
14
+ import { Server as HttpServer } from 'http';
15
+ import { WebSocketServer } from 'ws';
16
+ import { UserProfileLoader } from './Profiles.js';
17
+ import create from 'better-sqlite3';
18
+ import { URL } from 'url';
19
+ import { ClientConnectionManager } from './ClientConnection.js';
20
+ import { ReplicaKeepaliveTimers } from './ReplicaKeepaliveTimers.js';
21
+ import { TokenVerifier } from './TokenVerifier.js';
22
+ import busboy from 'busboy';
23
+ import { Readable } from 'stream';
24
+ import { FileMetadata } from './files/FileMetadata.js';
25
+ import { ServerLibrary } from './ServerLibrary.js';
26
+ import { migrations } from './migrations.js';
27
+ class DefaultProfiles {
28
+ constructor() {
29
+ this.get = async (userId) => {
30
+ return { id: userId };
31
+ };
32
+ }
33
+ }
34
+ export class Server extends EventEmitter {
35
+ constructor(options) {
36
+ super();
37
+ this.clientConnections = new ClientConnectionManager();
38
+ this.keepalives = new ReplicaKeepaliveTimers();
39
+ this.connectionToReplicaIdMap = new WeakMap();
40
+ this.log = () => { };
41
+ this.__testMode = false;
42
+ this.authorizeRequest = (req) => {
43
+ return this.tokenVerifier.verifyToken(this.getRequestToken(req));
44
+ };
45
+ this.getRequestToken = (req) => {
46
+ if (req.headers.authorization) {
47
+ const [type, token] = req.headers.authorization.split(' ');
48
+ if (type === 'Bearer') {
49
+ return token;
50
+ }
51
+ }
52
+ if (req.headers['sec-websocket-protocol']) {
53
+ // hack: read the token from the websocket protocol header
54
+ const [type, token] = req.headers['sec-websocket-protocol'].split(',');
55
+ if (type === 'Bearer') {
56
+ return token.trim();
57
+ }
58
+ }
59
+ assert(req.url, 'Request URL is required');
60
+ const url = new URL(req.url, 'http://localhost');
61
+ const token = url.searchParams.get('token');
62
+ assert(token, 'Token is required');
63
+ return token;
64
+ };
65
+ this.internalServerHandleRequest = (req, res) => {
66
+ const url = new URL(req.url || '', 'http://localhost');
67
+ if (url.pathname.startsWith('lofi')) {
68
+ if (url.pathname === 'lofi') {
69
+ return this.handleRequest(req, res);
70
+ }
71
+ else if (url.pathname.startsWith('/lofi/files/')) {
72
+ return this.handleFileRequest(req, res);
73
+ }
74
+ }
75
+ else {
76
+ res.writeHead(404);
77
+ res.end();
78
+ }
79
+ };
80
+ this.handleRequest = async (req, res) => {
81
+ var _a;
82
+ try {
83
+ if (req.method === 'POST') {
84
+ const info = this.authorizeRequest(req);
85
+ const key = generateId();
86
+ const finish = this.clientConnections.addRequest(info.libraryId, key, req, res, info);
87
+ // FIXME: extremely naive compatibility with connect-like
88
+ // servers...
89
+ const body = (_a = req.body) !== null && _a !== void 0 ? _a : (await new Promise((resolve, reject) => {
90
+ let body = '';
91
+ req.on('data', (chunk) => {
92
+ body += chunk;
93
+ });
94
+ req.on('end', () => {
95
+ resolve(JSON.parse(body));
96
+ });
97
+ req.on('error', (e) => {
98
+ reject(e);
99
+ });
100
+ }));
101
+ for (const message of body.messages) {
102
+ await this.handleMessage(key, info, message);
103
+ }
104
+ // update our keepalive timers for presence management
105
+ const firstMessage = body.messages[0];
106
+ if (firstMessage) {
107
+ this.keepalives.refresh(info.libraryId, firstMessage.replicaId);
108
+ }
109
+ finish();
110
+ this.emit('request', info);
111
+ }
112
+ }
113
+ catch (e) {
114
+ this.emit('error', e);
115
+ res.writeHead(500);
116
+ res.end();
117
+ }
118
+ };
119
+ /**
120
+ * Handles a multipart upload of a file from a verdant client. The upload
121
+ * will include parameters for the file's ID, name, and type. The request
122
+ * must be authenticated with a token to tie it to a library.
123
+ */
124
+ this.handleFileRequest = async (req, res) => {
125
+ const fs = this.fileStorage;
126
+ if (!fs) {
127
+ this.emit('error', new Error('No file storage configured, but a client attempted to upload a file.'));
128
+ res.writeHead(500);
129
+ res.write('File storage is not configured');
130
+ res.end();
131
+ return;
132
+ }
133
+ // FIXME: rather than trying to support Express, I should
134
+ // just expose regular methods you call from whatever HTTP handler...
135
+ const url = new URL(req.originalUrl || req.baseUrl || req.url || '', 'http://localhost');
136
+ const id = url.pathname.split('/').pop();
137
+ if (!id || id === 'files') {
138
+ res.writeHead(400);
139
+ res.write('File ID is required to be in the URL path as the last parameter');
140
+ res.end();
141
+ return;
142
+ }
143
+ try {
144
+ if (req.method === 'POST') {
145
+ const info = this.authorizeRequest(req);
146
+ await new Promise((resolve, reject) => {
147
+ const bb = busboy({ headers: req.headers });
148
+ bb.on('file', (fieldName, stream, fileInfo) => {
149
+ // too many 'info's....
150
+ const lofiFileInfo = {
151
+ id,
152
+ libraryId: info.libraryId,
153
+ fileName: fileInfo.filename,
154
+ type: fileInfo.mimeType,
155
+ };
156
+ // write metadata to storage
157
+ try {
158
+ this.fileMetadata.put(info.libraryId, lofiFileInfo);
159
+ fs.put(stream, lofiFileInfo);
160
+ }
161
+ catch (e) {
162
+ reject(e);
163
+ }
164
+ });
165
+ bb.on('field', (fieldName, value) => {
166
+ if (fieldName === 'file') {
167
+ if (this.__testMode) {
168
+ // this isn't right in the real world, but in testing it's
169
+ // the only way we get file data.
170
+ // we create a stream from the data and pass it as if it
171
+ // were a file stream
172
+ const stream = new Readable();
173
+ stream.push(value);
174
+ stream.push(null);
175
+ const fileInfo = {
176
+ filename: 'test.txt',
177
+ mimeType: 'text/plain',
178
+ };
179
+ bb.emit('file', fieldName, stream, fileInfo);
180
+ }
181
+ else {
182
+ throw new Error('Invalid file upload');
183
+ }
184
+ }
185
+ });
186
+ req.pipe(bb);
187
+ bb.on('finish', resolve);
188
+ bb.on('error', reject);
189
+ });
190
+ this.log('File upload complete');
191
+ res.writeHead(200);
192
+ res.write(JSON.stringify({ success: true }));
193
+ res.end();
194
+ }
195
+ else if (req.method === 'GET') {
196
+ const info = this.authorizeRequest(req);
197
+ const fileInfo = this.fileMetadata.get(info.libraryId, id);
198
+ if (!fileInfo) {
199
+ res.writeHead(404);
200
+ res.end();
201
+ return;
202
+ }
203
+ const url = await fs.getUrl({
204
+ fileName: fileInfo.name,
205
+ id: fileInfo.fileId,
206
+ libraryId: info.libraryId,
207
+ type: fileInfo.type,
208
+ });
209
+ res.writeHead(200, {
210
+ 'Content-Type': 'application/json',
211
+ });
212
+ // we need to augment that data with the URL from the file backend.
213
+ // and generally enforce the FileData interface here...
214
+ const data = {
215
+ id: fileInfo.fileId,
216
+ url,
217
+ remote: true,
218
+ name: fileInfo.name,
219
+ type: fileInfo.type,
220
+ };
221
+ res.write(JSON.stringify(data));
222
+ res.end();
223
+ }
224
+ }
225
+ catch (e) {
226
+ this.log('Error handling file request', e);
227
+ this.emit('error', e);
228
+ res.writeHead(500);
229
+ res.end();
230
+ }
231
+ };
232
+ this.broadcast = (libraryId, message, omitKeys = []) => {
233
+ this.clientConnections.broadcast(libraryId, message, omitKeys);
234
+ };
235
+ this.send = (libraryId, key, message) => {
236
+ this.clientConnections.respond(libraryId, key, message);
237
+ };
238
+ this.handleMessage = (clientKey, info, message) => {
239
+ this.log('Got message from user', info.userId, ', library', info.libraryId, JSON.stringify(message));
240
+ return this.library.receive(message, clientKey, info);
241
+ };
242
+ this.handleConnection = (ws, req, info) => {
243
+ const key = generateId();
244
+ this.clientConnections.addSocket(info.libraryId, key, ws, info);
245
+ ws.on('message', async (message) => {
246
+ const data = JSON.parse(message.toString());
247
+ if (data.replicaId) {
248
+ this.connectionToReplicaIdMap.set(ws, data.replicaId);
249
+ this.keepalives.refresh(info.libraryId, data.replicaId);
250
+ }
251
+ await this.handleMessage(key, info, data);
252
+ });
253
+ ws.on('close', () => {
254
+ const replicaId = this.connectionToReplicaIdMap.get(ws);
255
+ if (!replicaId) {
256
+ this.emit('warning', 'Unknown replica disconnected', ws);
257
+ return;
258
+ }
259
+ this.library.remove(info.libraryId, replicaId);
260
+ this.emit('socket-close', info);
261
+ });
262
+ this.emit('socket-connection', info);
263
+ };
264
+ this.listen = (...params) => {
265
+ this.httpServer.listen(...params);
266
+ };
267
+ this.close = async () => {
268
+ await Promise.all([
269
+ new Promise((resolve) => {
270
+ setTimeout(() => {
271
+ console.warn('HTTP server close timed out');
272
+ resolve();
273
+ }, 5 * 1000);
274
+ this.httpServer.close(() => {
275
+ resolve();
276
+ this.log('HTTP server closed');
277
+ });
278
+ }),
279
+ new Promise((resolve) => {
280
+ setTimeout(() => {
281
+ console.warn('Socket server close timed out');
282
+ resolve();
283
+ }, 5 * 1000);
284
+ this.wss.close(() => {
285
+ resolve();
286
+ this.log('Socket server closed');
287
+ });
288
+ }),
289
+ ]);
290
+ this.library.close();
291
+ };
292
+ /**
293
+ * Completely remove a library from the server.
294
+ * This will remove all data associated with the library.
295
+ * Use it very carefully! Data will still be stored on user
296
+ * devices, so this is not as scary as it sounds - the next time
297
+ * any replica connects, it will repopulate the library. But
298
+ * you should still be careful.
299
+ */
300
+ this.evictLibrary = (libraryId) => {
301
+ // disconnect all clients
302
+ this.clientConnections.disconnectAll(libraryId);
303
+ this.library.destroy(libraryId);
304
+ this.log('Evicted library', libraryId);
305
+ };
306
+ this.getLibraryPresence = (libraryId) => {
307
+ return this.library.getPresence(libraryId);
308
+ };
309
+ /**
310
+ * Removes all replicas associated with a User ID from
311
+ * the server. Consistency algorithms will no longer wait on these
312
+ * replicas before confirming and squashing changes.
313
+ */
314
+ this.evictUser = (libraryId, userId) => {
315
+ this.library.evictUser(libraryId, userId);
316
+ this.log('Evicted user', userId, 'from library', libraryId);
317
+ };
318
+ /**
319
+ * Retrieves a snapshot of the full document contents at this time.
320
+ * Useful for capturing the shape of data for static usage outside
321
+ * of the live system.
322
+ */
323
+ this.getDocumentSnapshot = (libraryId, collection, documentId) => {
324
+ return this.library.getDocumentSnapshot(libraryId, createOid(collection, documentId, []));
325
+ };
326
+ this.__testMode =
327
+ process.env.NODE_ENV === 'test' && process.env.VITEST === 'true';
328
+ this.log = options.log || this.log;
329
+ this.tokenVerifier = new TokenVerifier({
330
+ secret: options.tokenSecret,
331
+ });
332
+ this.fileStorage = options.fileStorage;
333
+ const db = create(options.databaseFile);
334
+ migrations(db);
335
+ this.fileMetadata = new FileMetadata(db, options.fileConfig);
336
+ this.library = new ServerLibrary({
337
+ db,
338
+ sender: this,
339
+ profiles: new UserProfileLoader(options.profiles || new DefaultProfiles()),
340
+ log: this.log,
341
+ disableRebasing: options.disableRebasing,
342
+ fileMetadata: this.fileMetadata,
343
+ fileStorage: this.fileStorage,
344
+ replicaTruancyMinutes: options.replicaTruancyMinutes || 60 * 24 * 14,
345
+ });
346
+ this.library.subscribe('changes', (_a, operations, baselines) => {
347
+ var { token } = _a, info = __rest(_a, ["token"]);
348
+ this.emit('changes', info, operations, baselines);
349
+ });
350
+ this.wss = new WebSocketServer({
351
+ noServer: true,
352
+ });
353
+ this.wss.on('connection', this.handleConnection);
354
+ this.httpServer =
355
+ options.httpServer || new HttpServer(this.internalServerHandleRequest);
356
+ this.httpServer.on('upgrade', async (req, socket, head) => {
357
+ try {
358
+ const info = this.authorizeRequest(req);
359
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
360
+ this.wss.emit('connection', ws, req, info);
361
+ });
362
+ }
363
+ catch (e) {
364
+ this.emit('error', e);
365
+ socket.destroy();
366
+ }
367
+ });
368
+ this.keepalives.subscribe('lost', this.library.remove);
369
+ }
370
+ }
371
+ //# sourceMappingURL=Server.js.map