@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,206 @@
1
+ import { Operation } from '@verdant-web/common';
2
+ import { Database } from 'better-sqlite3';
3
+ import { OperationSpec } from './types.js';
4
+
5
+ type StoredOperationHistoryItem = Omit<Operation, 'data'> & {
6
+ data: string;
7
+ libraryId: string;
8
+ replicaId: string;
9
+ serverOrder: number;
10
+ };
11
+
12
+ export class OperationHistory {
13
+ constructor(private db: Database) {}
14
+
15
+ private hydratePatch = ({
16
+ libraryId: _,
17
+ data,
18
+ ...rest
19
+ }: StoredOperationHistoryItem): OperationSpec => {
20
+ return {
21
+ ...rest,
22
+ data: JSON.parse(data),
23
+ };
24
+ };
25
+
26
+ getAllFor = (libraryId: string, oid: string) => {
27
+ return this.db
28
+ .prepare(
29
+ `
30
+ SELECT * FROM OperationHistory
31
+ WHERE libraryId = ? AND oid = ?
32
+ ORDER BY timestamp ASC
33
+ `,
34
+ )
35
+ .all(libraryId, oid)
36
+ .map(this.hydratePatch);
37
+ };
38
+
39
+ getEarliest = (libraryId: string, count: number) => {
40
+ return this.db
41
+ .prepare(
42
+ `
43
+ SELECT * FROM OperationHistory
44
+ WHERE libraryId = ?
45
+ ORDER BY timestamp ASC
46
+ LIMIT ?
47
+ `,
48
+ )
49
+ .all(libraryId, count)
50
+ .map(this.hydratePatch);
51
+ };
52
+
53
+ getAfter = (libraryId: string, timestamp: string | null = null) => {
54
+ if (timestamp === null) {
55
+ return this.db
56
+ .prepare(
57
+ `
58
+ SELECT * FROM OperationHistory
59
+ WHERE libraryId = ?
60
+ ORDER BY timestamp ASC
61
+ `,
62
+ )
63
+ .all(libraryId)
64
+ .map(this.hydratePatch);
65
+ }
66
+ return this.db
67
+ .prepare(
68
+ `
69
+ SELECT * FROM OperationHistory
70
+ WHERE libraryId = ? AND timestamp > ?
71
+ ORDER BY timestamp ASC
72
+ `,
73
+ )
74
+ .all(libraryId, timestamp)
75
+ .map(this.hydratePatch);
76
+ };
77
+
78
+ /**
79
+ * Returns all operations before the given timestamp
80
+ * in ascending chronological order
81
+ */
82
+ getBefore = (libraryId: string, before: string) => {
83
+ return this.db
84
+ .prepare(
85
+ `
86
+ SELECT * FROM OperationHistory
87
+ WHERE libraryId = ? AND timestamp < ?
88
+ ORDER BY timestamp ASC
89
+ `,
90
+ )
91
+ .all(libraryId, before)
92
+ .map(this.hydratePatch);
93
+ };
94
+
95
+ /**
96
+ * Returns all operations after a given server order. Useful
97
+ * for retrieving operations that were created after a client
98
+ * last synced.
99
+ */
100
+ getFromServerOrder = (libraryId: string, serverOrder: number) => {
101
+ const result = this.db
102
+ .prepare(
103
+ `
104
+ SELECT * FROM OperationHistory
105
+ WHERE libraryId = ? AND serverOrder > ?
106
+ `,
107
+ )
108
+ .all(libraryId, serverOrder);
109
+
110
+ return (result || []).map(this.hydratePatch);
111
+ };
112
+
113
+ /**
114
+ * Adds a new operation or replaces an existing one.
115
+ * The server order will be set to the current max + 1
116
+ */
117
+ insert = (libraryId: string, replicaId: string, item: Operation) => {
118
+ return this.db
119
+ .prepare(
120
+ `
121
+ INSERT OR REPLACE INTO OperationHistory (libraryId, oid, data, timestamp, replicaId, serverOrder)
122
+ VALUES (?, ?, ?, ?, ?, (SELECT COALESCE(MAX(serverOrder), 0) FROM OperationHistory WHERE libraryId = ?) + 1)
123
+ `,
124
+ )
125
+ .run(
126
+ libraryId,
127
+ item.oid,
128
+ JSON.stringify(item.data),
129
+ item.timestamp,
130
+ replicaId,
131
+ );
132
+ };
133
+
134
+ /**
135
+ * Inserts all operations and updates the server
136
+ * order for this library
137
+ */
138
+ insertAll = async (
139
+ libraryId: string,
140
+ replicaId: string,
141
+ items: Operation[],
142
+ ) => {
143
+ const tx = this.db.transaction(() => {
144
+ let orderResult = this.db
145
+ .prepare(
146
+ `
147
+ SELECT COALESCE(MAX(serverOrder), 0) AS serverOrder FROM OperationHistory WHERE libraryId = ?
148
+ `,
149
+ )
150
+ .get(libraryId);
151
+ let currentServerOrder = orderResult.serverOrder || 0;
152
+ const insertStatement = this.db.prepare(
153
+ `
154
+ INSERT OR REPLACE INTO OperationHistory (libraryId, oid, data, timestamp, replicaId, serverOrder)
155
+ VALUES (?, ?, ?, ?, ?, ?)
156
+ `,
157
+ );
158
+ for (const item of items) {
159
+ insertStatement.run(
160
+ libraryId,
161
+ item.oid,
162
+ JSON.stringify(item.data),
163
+ item.timestamp,
164
+ replicaId,
165
+ ++currentServerOrder,
166
+ );
167
+ }
168
+ });
169
+
170
+ tx();
171
+ };
172
+
173
+ dropAll = async (
174
+ libraryId: string,
175
+ items: Pick<OperationSpec, 'replicaId' | 'oid' | 'timestamp'>[],
176
+ ) => {
177
+ const deleteStatement = this.db.prepare(
178
+ `
179
+ DELETE FROM OperationHistory
180
+ WHERE libraryId = ? AND replicaId = ? AND oid = ? AND timestamp = ?
181
+ `,
182
+ );
183
+
184
+ this.db.transaction(() => {
185
+ for (const item of items) {
186
+ deleteStatement.run(
187
+ libraryId,
188
+ item.replicaId,
189
+ item.oid,
190
+ item.timestamp,
191
+ );
192
+ }
193
+ })();
194
+ };
195
+
196
+ deleteAll = (libraryId: string) => {
197
+ this.db
198
+ .prepare(
199
+ `
200
+ DELETE FROM OperationHistory
201
+ WHERE libraryId = ?
202
+ `,
203
+ )
204
+ .run(libraryId);
205
+ };
206
+ }
@@ -0,0 +1,63 @@
1
+ import { UserInfo } from '@verdant-web/common';
2
+ import EventEmitter from 'events';
3
+
4
+ /**
5
+ * Stores client presence in-memory for connected
6
+ * clients
7
+ */
8
+ export class Presence extends EventEmitter {
9
+ private presences: Map<
10
+ string,
11
+ {
12
+ presences: Record<string, UserInfo<any, any>>;
13
+ replicaToUser: Record<string, string>;
14
+ userToReplica: Record<string, Set<string>>;
15
+ }
16
+ > = new Map();
17
+
18
+ private getLibrary = (libraryId: string) => {
19
+ let map = this.presences.get(libraryId);
20
+ if (!map) {
21
+ map = {
22
+ presences: {},
23
+ replicaToUser: {},
24
+ userToReplica: {},
25
+ };
26
+ this.presences.set(libraryId, map);
27
+ }
28
+ return map;
29
+ };
30
+
31
+ set = (libraryId: string, userId: string, presence: UserInfo<any, any>) => {
32
+ const lib = this.getLibrary(libraryId);
33
+ lib.presences[userId] = presence;
34
+ lib.replicaToUser[presence.replicaId] = userId;
35
+ lib.userToReplica[userId] = lib.userToReplica[userId] || new Set<string>();
36
+ lib.userToReplica[userId].add(presence.replicaId);
37
+ };
38
+
39
+ removeReplica = (libraryId: string, replicaId: string) => {
40
+ const lib = this.getLibrary(libraryId);
41
+ const userId = lib.replicaToUser[replicaId];
42
+ if (!userId) return;
43
+
44
+ lib.userToReplica[userId].delete(replicaId);
45
+ if (lib.userToReplica[userId].size === 0) {
46
+ delete lib.presences[userId];
47
+ this.emit('lost', libraryId, replicaId, userId);
48
+
49
+ // memory cleanup
50
+ if (Object.keys(lib.presences).length === 0) {
51
+ this.clear(libraryId);
52
+ }
53
+ }
54
+ };
55
+
56
+ all = (libraryId: string) => {
57
+ return this.presences.get(libraryId)?.presences || {};
58
+ };
59
+
60
+ clear = (libraryId: string) => {
61
+ this.presences.delete(libraryId);
62
+ };
63
+ }
@@ -0,0 +1,21 @@
1
+ export interface UserProfiles<Profile> {
2
+ get(userId: string): Promise<Profile>;
3
+ }
4
+
5
+ export class UserProfileLoader<Profile> {
6
+ private cache = new Map<string, Promise<Profile>>();
7
+
8
+ constructor(private source: UserProfiles<Profile>) {}
9
+
10
+ get = async (userId: string) => {
11
+ if (!this.cache.has(userId)) {
12
+ this.cache.set(userId, this.source.get(userId));
13
+ }
14
+
15
+ return this.cache.get(userId)!;
16
+ };
17
+
18
+ evict = (userId: string) => {
19
+ this.cache.delete(userId);
20
+ };
21
+ }
@@ -0,0 +1,34 @@
1
+ import { EventSubscriber } from '@verdant-web/common';
2
+
3
+ /**
4
+ * A set of timers per-replica which are
5
+ * refreshed whenever the replica sends
6
+ * a message to the server. If a timer expires,
7
+ * the replica is removed from presence. This
8
+ * is a failsafe for socket-based replicas but
9
+ * essential for pull-based replicas.
10
+ */
11
+ export class ReplicaKeepaliveTimers extends EventSubscriber<{
12
+ lost: (libraryId: string, replicaId: string) => void;
13
+ }> {
14
+ private timers = new Map<string, NodeJS.Timeout>();
15
+
16
+ constructor(private timeout = 30 * 1000) {
17
+ super();
18
+ }
19
+
20
+ refresh = (libraryId: string, replicaId: string) => {
21
+ const existing = this.timers.get(replicaId);
22
+ if (existing) {
23
+ clearTimeout(existing);
24
+ }
25
+
26
+ this.timers.set(
27
+ replicaId,
28
+ setTimeout(() => {
29
+ this.emit('lost', libraryId, replicaId);
30
+ this.timers.delete(replicaId);
31
+ }, this.timeout),
32
+ );
33
+ };
34
+ }
@@ -0,0 +1,238 @@
1
+ import { Database } from 'better-sqlite3';
2
+ import { TokenInfo } from './TokenVerifier.js';
3
+ import { ReplicaInfoSpec } from './types.js';
4
+
5
+ export class ReplicaInfos {
6
+ constructor(
7
+ private db: Database,
8
+ private readonly replicaTruancyMinutes: number,
9
+ ) {}
10
+
11
+ private get truantCutoff() {
12
+ return Date.now() - this.replicaTruancyMinutes * 60 * 1000;
13
+ }
14
+
15
+ get = (libraryId: string, replicaId: string): ReplicaInfoSpec | null => {
16
+ const row = this.db
17
+ .prepare(
18
+ `
19
+ SELECT * FROM ReplicaInfo
20
+ WHERE id = ? AND libraryId = ?
21
+ `,
22
+ )
23
+ .get(replicaId, libraryId);
24
+ if (!row) {
25
+ return null;
26
+ }
27
+ return {
28
+ id: row.id,
29
+ libraryId: row.libraryId,
30
+ clientId: row.clientId,
31
+ lastSeenWallClockTime: row.lastSeenWallClockTime,
32
+ ackedLogicalTime: row.ackedLogicalTime,
33
+ type: row.type,
34
+ ackedServerOrder: row.ackedServerOrder,
35
+ };
36
+ };
37
+
38
+ getOrCreate = (
39
+ libraryId: string,
40
+ replicaId: string,
41
+ info: TokenInfo,
42
+ ): {
43
+ status: 'new' | 'existing' | 'truant';
44
+ replicaInfo: ReplicaInfoSpec;
45
+ } => {
46
+ const replicaInfo: ReplicaInfoSpec = this.db
47
+ .prepare(
48
+ `
49
+ SELECT * FROM ReplicaInfo
50
+ WHERE id = ? AND libraryId = ?
51
+ `,
52
+ )
53
+ .get(replicaId, libraryId);
54
+
55
+ if (!replicaInfo) {
56
+ this.db
57
+ .prepare(
58
+ `
59
+ INSERT INTO ReplicaInfo (id, clientId, libraryId, type)
60
+ VALUES (?, ?, ?, ?)
61
+ `,
62
+ )
63
+ .run(replicaId, info.userId, libraryId, info.type);
64
+
65
+ return {
66
+ status: 'new',
67
+ replicaInfo: {
68
+ id: replicaId,
69
+ clientId: info.userId,
70
+ ackedLogicalTime: null,
71
+ lastSeenWallClockTime: null,
72
+ libraryId: libraryId,
73
+ type: info.type,
74
+ ackedServerOrder: 0,
75
+ },
76
+ };
77
+ }
78
+
79
+ if (replicaInfo.type !== info.type) {
80
+ // type should be updated if a new token changes it
81
+ this.db
82
+ .prepare(
83
+ `
84
+ UPDATE ReplicaInfo
85
+ SET type = ?
86
+ WHERE id = ? AND libraryId = ?
87
+ `,
88
+ )
89
+ .run(info.type, replicaId, libraryId);
90
+ }
91
+
92
+ if (replicaInfo.clientId !== info.userId) {
93
+ throw new Error(
94
+ `Replica ${replicaId} already exists with a different user ID`,
95
+ );
96
+ }
97
+
98
+ if (
99
+ replicaInfo.lastSeenWallClockTime &&
100
+ replicaInfo.lastSeenWallClockTime < this.truantCutoff
101
+ ) {
102
+ return {
103
+ status: 'truant',
104
+ replicaInfo,
105
+ };
106
+ }
107
+
108
+ return {
109
+ status: 'existing',
110
+ replicaInfo,
111
+ };
112
+ };
113
+
114
+ getAllNonTruant = (libraryId: string): ReplicaInfoSpec[] => {
115
+ return this.db
116
+ .prepare(
117
+ `
118
+ SELECT * FROM ReplicaInfo
119
+ WHERE libraryId = ? AND lastSeenWallClockTime > ?
120
+ `,
121
+ )
122
+ .all(libraryId, this.truantCutoff);
123
+ };
124
+
125
+ updateLastSeen = (libraryId: string, replicaId: string) => {
126
+ const clockTime = new Date().getTime();
127
+ return this.db
128
+ .prepare(
129
+ `
130
+ UPDATE ReplicaInfo
131
+ SET lastSeenWallClockTime = ?
132
+ WHERE id = ? AND libraryId = ?
133
+ `,
134
+ )
135
+ .run(clockTime, replicaId, libraryId);
136
+ };
137
+
138
+ updateAcknowledged = (
139
+ libraryId: string,
140
+ replicaId: string,
141
+ timestamp: string,
142
+ ) => {
143
+ this.db
144
+ .prepare(
145
+ `
146
+ UPDATE ReplicaInfo
147
+ SET ackedLogicalTime = ?
148
+ WHERE id = ? AND libraryId = ?
149
+ `,
150
+ )
151
+ .run(timestamp, replicaId, libraryId);
152
+ };
153
+
154
+ updateServerOrder = (
155
+ libraryId: string,
156
+ replicaId: string,
157
+ serverOrder: number,
158
+ ) => {
159
+ // only increment the server order, never set it to lower than before
160
+ this.db
161
+ .prepare(
162
+ `
163
+ UPDATE ReplicaInfo
164
+ SET ackedServerOrder = MAX(ackedServerOrder, ?)
165
+ WHERE id = ? AND libraryId = ?
166
+ `,
167
+ )
168
+ .run(serverOrder, replicaId, libraryId);
169
+ };
170
+
171
+ acknowledgeOperation = (
172
+ libraryId: string,
173
+ replicaId: string,
174
+ timestamp: string,
175
+ ) => {
176
+ if (!timestamp) return;
177
+
178
+ // get the server order from the operation with the timestamp and update the replica's server order
179
+ // to the max of the current server order and the server order of the operation
180
+ this.db
181
+ .prepare(
182
+ `
183
+ UPDATE ReplicaInfo
184
+ SET ackedLogicalTime = ?, ackedServerOrder = MAX(ackedServerOrder, COALESCE((
185
+ SELECT serverOrder FROM OperationHistory
186
+ WHERE libraryId = ? AND timestamp = ?
187
+ ), 0))
188
+ WHERE id = ? AND libraryId = ?
189
+ `,
190
+ )
191
+ .run(timestamp, libraryId, timestamp, replicaId, libraryId);
192
+ };
193
+
194
+ getGlobalAck = (libraryId: string, onlineReplicaIds?: string[]) => {
195
+ const nonTruant = this.getAllNonTruant(libraryId);
196
+ const globalAckEligible = nonTruant.filter(
197
+ (replica) => replica.type < 2 || onlineReplicaIds?.includes(replica.id),
198
+ );
199
+ // get the earliest acked time
200
+ return globalAckEligible.reduce((acc, replica) => {
201
+ if (!replica.ackedLogicalTime) {
202
+ return acc;
203
+ }
204
+ if (acc === undefined) {
205
+ return replica.ackedLogicalTime;
206
+ }
207
+ return acc < replica.ackedLogicalTime ? acc : replica.ackedLogicalTime;
208
+ }, undefined as string | undefined);
209
+ };
210
+
211
+ delete = (libraryId: string, replicaId: string) => {
212
+ return this.db
213
+ .prepare(
214
+ `
215
+ DELETE FROM ReplicaInfo
216
+ WHERE id = ? AND libraryId = ?
217
+ `,
218
+ )
219
+ .run(replicaId, libraryId);
220
+ };
221
+
222
+ deleteAll = (libraryId: string) => {
223
+ return this.db
224
+ .prepare(
225
+ `
226
+ DELETE FROM ReplicaInfo
227
+ WHERE libraryId = ?
228
+ `,
229
+ )
230
+ .run(libraryId);
231
+ };
232
+
233
+ deleteAllForUser = (libraryId: string, userId: string) => {
234
+ return this.db
235
+ .prepare(`DELETE FROM ReplicaInfo WHERE libraryId = ? AND clientId = ?`)
236
+ .run(libraryId, userId);
237
+ };
238
+ }