@verdant-web/server 2.1.3 → 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.
Files changed (198) hide show
  1. package/dist/esm/ClientConnection.d.ts +1 -0
  2. package/dist/esm/ClientConnection.js.map +1 -1
  3. package/dist/esm/Presence.js.map +1 -1
  4. package/dist/esm/Profiles.js.map +1 -1
  5. package/dist/esm/ReplicaKeepaliveTimers.js.map +1 -1
  6. package/dist/esm/Server.d.ts +15 -11
  7. package/dist/esm/Server.js +29 -30
  8. package/dist/esm/Server.js.map +1 -1
  9. package/dist/esm/ServerLibrary.d.ts +10 -17
  10. package/dist/esm/ServerLibrary.js +151 -127
  11. package/dist/esm/ServerLibrary.js.map +1 -1
  12. package/dist/esm/ServerLibrary.test.js +369 -0
  13. package/dist/esm/ServerLibrary.test.js.map +1 -0
  14. package/dist/esm/TokenVerifier.js.map +1 -1
  15. package/dist/esm/files/FileStorage.js.map +1 -1
  16. package/dist/esm/storage/Storage.d.ts +67 -0
  17. package/dist/esm/storage/Storage.js +2 -0
  18. package/dist/esm/storage/Storage.js.map +1 -0
  19. package/dist/esm/storage/index.d.ts +2 -0
  20. package/dist/esm/storage/index.js +3 -0
  21. package/dist/esm/storage/index.js.map +1 -0
  22. package/dist/esm/storage/sql/SqlBaselines.d.ts +22 -0
  23. package/dist/esm/storage/sql/SqlBaselines.js +101 -0
  24. package/dist/esm/storage/sql/SqlBaselines.js.map +1 -0
  25. package/dist/esm/storage/sql/SqlFileMetadata.d.ts +18 -0
  26. package/dist/esm/storage/sql/SqlFileMetadata.js +69 -0
  27. package/dist/esm/storage/sql/SqlFileMetadata.js.map +1 -0
  28. package/dist/esm/storage/sql/SqlOperations.d.ts +19 -0
  29. package/dist/esm/storage/sql/SqlOperations.js +104 -0
  30. package/dist/esm/storage/sql/SqlOperations.js.map +1 -0
  31. package/dist/esm/storage/sql/SqlReplicas.d.ts +32 -0
  32. package/dist/esm/storage/sql/SqlReplicas.js +163 -0
  33. package/dist/esm/storage/sql/SqlReplicas.js.map +1 -0
  34. package/dist/esm/storage/sql/migrations/v1.d.ts +3 -0
  35. package/dist/esm/storage/sql/migrations/v1.js +59 -0
  36. package/dist/esm/storage/sql/migrations/v1.js.map +1 -0
  37. package/dist/esm/storage/sql/migrations.d.ts +5 -0
  38. package/dist/esm/storage/sql/migrations.js +3 -0
  39. package/dist/esm/storage/sql/migrations.js.map +1 -0
  40. package/dist/esm/storage/sql/sqlStorage.d.ts +5 -0
  41. package/dist/esm/storage/sql/sqlStorage.js +43 -0
  42. package/dist/esm/storage/sql/sqlStorage.js.map +1 -0
  43. package/dist/esm/storage/sql/tables.d.ts +40 -0
  44. package/dist/esm/storage/sql/tables.js +2 -0
  45. package/dist/esm/storage/sql/tables.js.map +1 -0
  46. package/dist/esm/types.d.ts +14 -5
  47. package/package.json +20 -14
  48. package/src/Server.ts +37 -43
  49. package/src/ServerLibrary.test.ts +447 -0
  50. package/src/ServerLibrary.ts +239 -163
  51. package/src/storage/Storage.ts +118 -0
  52. package/src/storage/index.ts +2 -0
  53. package/src/storage/sql/SqlBaselines.ts +141 -0
  54. package/src/storage/sql/SqlFileMetadata.ts +94 -0
  55. package/src/storage/sql/SqlOperations.ts +149 -0
  56. package/src/storage/sql/SqlReplicas.ts +234 -0
  57. package/src/storage/sql/migrations/v1.ts +64 -0
  58. package/src/storage/sql/migrations.ts +3 -0
  59. package/src/storage/sql/sqlStorage.ts +59 -0
  60. package/src/storage/sql/tables.ts +45 -0
  61. package/src/types.ts +15 -4
  62. package/bin/server.mjs +0 -115
  63. package/dist/cjs/Baselines.d.ts +0 -16
  64. package/dist/cjs/Baselines.js +0 -128
  65. package/dist/cjs/Baselines.js.map +0 -1
  66. package/dist/cjs/ClientConnection.d.ts +0 -20
  67. package/dist/cjs/ClientConnection.js +0 -139
  68. package/dist/cjs/ClientConnection.js.map +0 -1
  69. package/dist/cjs/MessageSender.d.ts +0 -5
  70. package/dist/cjs/MessageSender.js +0 -3
  71. package/dist/cjs/MessageSender.js.map +0 -1
  72. package/dist/cjs/OperationHistory.d.ts +0 -36
  73. package/dist/cjs/OperationHistory.js +0 -165
  74. package/dist/cjs/OperationHistory.js.map +0 -1
  75. package/dist/cjs/Presence.d.ts +0 -15
  76. package/dist/cjs/Presence.js +0 -60
  77. package/dist/cjs/Presence.js.map +0 -1
  78. package/dist/cjs/Profiles.d.ts +0 -10
  79. package/dist/cjs/Profiles.js +0 -20
  80. package/dist/cjs/Profiles.js.map +0 -1
  81. package/dist/cjs/ReplicaKeepaliveTimers.d.ts +0 -17
  82. package/dist/cjs/ReplicaKeepaliveTimers.js +0 -31
  83. package/dist/cjs/ReplicaKeepaliveTimers.js.map +0 -1
  84. package/dist/cjs/Replicas.d.ts +0 -25
  85. package/dist/cjs/Replicas.js +0 -196
  86. package/dist/cjs/Replicas.js.map +0 -1
  87. package/dist/cjs/Server.d.ts +0 -167
  88. package/dist/cjs/Server.js +0 -552
  89. package/dist/cjs/Server.js.map +0 -1
  90. package/dist/cjs/ServerLibrary.d.ts +0 -65
  91. package/dist/cjs/ServerLibrary.js +0 -505
  92. package/dist/cjs/ServerLibrary.js.map +0 -1
  93. package/dist/cjs/TokenProvider.d.ts +0 -19
  94. package/dist/cjs/TokenProvider.js +0 -27
  95. package/dist/cjs/TokenProvider.js.map +0 -1
  96. package/dist/cjs/TokenVerifier.d.ts +0 -16
  97. package/dist/cjs/TokenVerifier.js +0 -40
  98. package/dist/cjs/TokenVerifier.js.map +0 -1
  99. package/dist/cjs/files/FileMetadata.d.ts +0 -28
  100. package/dist/cjs/files/FileMetadata.js +0 -69
  101. package/dist/cjs/files/FileMetadata.js.map +0 -1
  102. package/dist/cjs/files/FileStorage.d.ts +0 -35
  103. package/dist/cjs/files/FileStorage.js +0 -72
  104. package/dist/cjs/files/FileStorage.js.map +0 -1
  105. package/dist/cjs/index.d.ts +0 -10
  106. package/dist/cjs/index.js +0 -12
  107. package/dist/cjs/index.js.map +0 -1
  108. package/dist/cjs/migrations/v0.d.ts +0 -1
  109. package/dist/cjs/migrations/v0.js +0 -30
  110. package/dist/cjs/migrations/v0.js.map +0 -1
  111. package/dist/cjs/migrations/v1.d.ts +0 -1
  112. package/dist/cjs/migrations/v1.js +0 -32
  113. package/dist/cjs/migrations/v1.js.map +0 -1
  114. package/dist/cjs/migrations/v2.d.ts +0 -1
  115. package/dist/cjs/migrations/v2.js +0 -29
  116. package/dist/cjs/migrations/v2.js.map +0 -1
  117. package/dist/cjs/migrations/v3.d.ts +0 -1
  118. package/dist/cjs/migrations/v3.js +0 -15
  119. package/dist/cjs/migrations/v3.js.map +0 -1
  120. package/dist/cjs/migrations/v4.d.ts +0 -1
  121. package/dist/cjs/migrations/v4.js +0 -9
  122. package/dist/cjs/migrations/v4.js.map +0 -1
  123. package/dist/cjs/migrations/v5.d.ts +0 -2
  124. package/dist/cjs/migrations/v5.js +0 -37
  125. package/dist/cjs/migrations/v5.js.map +0 -1
  126. package/dist/cjs/migrations/v6.d.ts +0 -1
  127. package/dist/cjs/migrations/v6.js +0 -8
  128. package/dist/cjs/migrations/v6.js.map +0 -1
  129. package/dist/cjs/migrations/v7.d.ts +0 -1
  130. package/dist/cjs/migrations/v7.js +0 -10
  131. package/dist/cjs/migrations/v7.js.map +0 -1
  132. package/dist/cjs/migrations.d.ts +0 -6
  133. package/dist/cjs/migrations.js +0 -72
  134. package/dist/cjs/migrations.js.map +0 -1
  135. package/dist/cjs/migrations.test.js +0 -121
  136. package/dist/cjs/migrations.test.js.map +0 -1
  137. package/dist/cjs/types.d.ts +0 -38
  138. package/dist/cjs/types.js +0 -3
  139. package/dist/cjs/types.js.map +0 -1
  140. package/dist/esm/Baselines.d.ts +0 -16
  141. package/dist/esm/Baselines.js +0 -124
  142. package/dist/esm/Baselines.js.map +0 -1
  143. package/dist/esm/OperationHistory.d.ts +0 -36
  144. package/dist/esm/OperationHistory.js +0 -161
  145. package/dist/esm/OperationHistory.js.map +0 -1
  146. package/dist/esm/Replicas.d.ts +0 -25
  147. package/dist/esm/Replicas.js +0 -192
  148. package/dist/esm/Replicas.js.map +0 -1
  149. package/dist/esm/files/FileMetadata.d.ts +0 -28
  150. package/dist/esm/files/FileMetadata.js +0 -65
  151. package/dist/esm/files/FileMetadata.js.map +0 -1
  152. package/dist/esm/migrations/v0.d.ts +0 -1
  153. package/dist/esm/migrations/v0.js +0 -27
  154. package/dist/esm/migrations/v0.js.map +0 -1
  155. package/dist/esm/migrations/v1.d.ts +0 -1
  156. package/dist/esm/migrations/v1.js +0 -29
  157. package/dist/esm/migrations/v1.js.map +0 -1
  158. package/dist/esm/migrations/v2.d.ts +0 -1
  159. package/dist/esm/migrations/v2.js +0 -26
  160. package/dist/esm/migrations/v2.js.map +0 -1
  161. package/dist/esm/migrations/v3.d.ts +0 -1
  162. package/dist/esm/migrations/v3.js +0 -12
  163. package/dist/esm/migrations/v3.js.map +0 -1
  164. package/dist/esm/migrations/v4.d.ts +0 -1
  165. package/dist/esm/migrations/v4.js +0 -6
  166. package/dist/esm/migrations/v4.js.map +0 -1
  167. package/dist/esm/migrations/v5.d.ts +0 -2
  168. package/dist/esm/migrations/v5.js +0 -33
  169. package/dist/esm/migrations/v5.js.map +0 -1
  170. package/dist/esm/migrations/v6.d.ts +0 -1
  171. package/dist/esm/migrations/v6.js +0 -5
  172. package/dist/esm/migrations/v6.js.map +0 -1
  173. package/dist/esm/migrations/v7.d.ts +0 -1
  174. package/dist/esm/migrations/v7.js +0 -7
  175. package/dist/esm/migrations/v7.js.map +0 -1
  176. package/dist/esm/migrations.d.ts +0 -6
  177. package/dist/esm/migrations.js +0 -45
  178. package/dist/esm/migrations.js.map +0 -1
  179. package/dist/esm/migrations.test.d.ts +0 -1
  180. package/dist/esm/migrations.test.js +0 -116
  181. package/dist/esm/migrations.test.js.map +0 -1
  182. package/dist/tsconfig-cjs.tsbuildinfo +0 -1
  183. package/dist/tsconfig.tsbuildinfo +0 -1
  184. package/src/Baselines.ts +0 -198
  185. package/src/OperationHistory.ts +0 -232
  186. package/src/Replicas.ts +0 -274
  187. package/src/files/FileMetadata.ts +0 -112
  188. package/src/migrations/v0.ts +0 -26
  189. package/src/migrations/v1.ts +0 -28
  190. package/src/migrations/v2.ts +0 -26
  191. package/src/migrations/v3.ts +0 -12
  192. package/src/migrations/v4.ts +0 -6
  193. package/src/migrations/v5.ts +0 -49
  194. package/src/migrations/v6.ts +0 -4
  195. package/src/migrations/v7.ts +0 -6
  196. package/src/migrations.test.ts +0 -133
  197. package/src/migrations.ts +0 -61
  198. /package/dist/{cjs/migrations.test.d.ts → esm/ServerLibrary.test.d.ts} +0 -0
package/src/Baselines.ts DELETED
@@ -1,198 +0,0 @@
1
- import {
2
- applyPatch,
3
- DocumentBaseline,
4
- getAllFileFields,
5
- Operation,
6
- Ref,
7
- } from '@verdant-web/common';
8
- import { Database } from 'better-sqlite3';
9
- import { DocumentBaselineSpec } from './types.js';
10
-
11
- export class Baselines {
12
- constructor(private db: Database) {}
13
-
14
- get = (libraryId: string, oid: string): DocumentBaseline<any> | null => {
15
- const row = this.db
16
- .prepare(
17
- `
18
- SELECT * FROM DocumentBaseline
19
- WHERE libraryId = ? AND oid = ?
20
- `,
21
- )
22
- .get(libraryId, oid);
23
-
24
- if (!row) {
25
- return null;
26
- }
27
-
28
- return this.hydrateSnapshot(row);
29
- };
30
-
31
- getAllWithSubObjects = (
32
- libraryId: string,
33
- oid: string,
34
- ): DocumentBaseline<any>[] => {
35
- const rows = this.db
36
- .prepare(
37
- `
38
- SELECT * FROM DocumentBaseline
39
- WHERE libraryId = ? AND oid LIKE ?
40
- `,
41
- )
42
- .all(libraryId, `${oid}%`);
43
-
44
- return rows.map(this.hydrateSnapshot);
45
- };
46
-
47
- set = (libraryId: string, baseline: DocumentBaseline) => {
48
- if (!baseline.snapshot) {
49
- return this.db
50
- .prepare(
51
- `
52
- DELETE FROM DocumentBaseline
53
- WHERE libraryId = ? AND oid = ?
54
- `,
55
- )
56
- .run(libraryId, baseline.oid);
57
- }
58
-
59
- return this.db
60
- .prepare(
61
- `
62
- INSERT OR REPLACE INTO DocumentBaseline (libraryId, oid, snapshot, timestamp)
63
- VALUES (?, ?, ?, ?)
64
- `,
65
- )
66
- .run(
67
- libraryId,
68
- baseline.oid,
69
- JSON.stringify(baseline.snapshot),
70
- baseline.timestamp,
71
- );
72
- };
73
-
74
- insertAll = (libraryId: string, baselines: DocumentBaseline[]) => {
75
- const tx = this.db.transaction(() => {
76
- for (const baseline of baselines) {
77
- this.db
78
- .prepare(
79
- `
80
- INSERT OR REPLACE INTO DocumentBaseline (libraryId, oid, snapshot, timestamp)
81
- VALUES (?, ?, ?, ?)
82
- `,
83
- )
84
- .run(
85
- libraryId,
86
- baseline.oid,
87
- JSON.stringify(baseline.snapshot),
88
- baseline.timestamp,
89
- );
90
- }
91
- });
92
- tx();
93
- };
94
-
95
- private hydrateSnapshot = (
96
- snapshot: DocumentBaselineSpec,
97
- ): DocumentBaseline<any> => {
98
- return {
99
- ...snapshot,
100
- snapshot: JSON.parse(snapshot.snapshot),
101
- };
102
- };
103
-
104
- getAllAfter = (
105
- libraryId: string,
106
- timestamp: string | null,
107
- ): DocumentBaseline<any>[] => {
108
- if (!timestamp) {
109
- return this.db
110
- .prepare(
111
- `
112
- SELECT * FROM DocumentBaseline
113
- WHERE libraryId = ?
114
- ORDER BY timestamp ASC
115
- `,
116
- )
117
- .all(libraryId)
118
- .map(this.hydrateSnapshot);
119
- }
120
- return this.db
121
- .prepare(
122
- `
123
- SELECT * FROM DocumentBaseline
124
- WHERE libraryId = ? AND timestamp > ?
125
- ORDER BY timestamp ASC
126
- `,
127
- )
128
- .all(libraryId, timestamp)
129
- .map(this.hydrateSnapshot);
130
- };
131
-
132
- getFromServerOrder = (
133
- libraryId: string,
134
- serverOrder: number,
135
- ): DocumentBaseline<any>[] => {
136
- return this.db
137
- .prepare(
138
- `
139
- SELECT * FROM DocumentBaseline
140
- WHERE libraryId = ? AND serverOrder >= ?
141
- ORDER BY serverOrder ASC
142
- `,
143
- )
144
- .all(libraryId, serverOrder)
145
- .map(this.hydrateSnapshot);
146
- };
147
-
148
- applyOperations = (
149
- libraryId: string,
150
- oid: string,
151
- operations: Operation[],
152
- deletedRefs?: Ref[],
153
- ) => {
154
- if (operations.length === 0) return [];
155
-
156
- let baseline = this.get(libraryId, oid);
157
- if (!baseline) {
158
- baseline = {
159
- oid,
160
- snapshot: {},
161
- timestamp: operations[0].timestamp,
162
- };
163
- }
164
- for (const operation of operations) {
165
- baseline.snapshot = applyPatch(
166
- baseline.snapshot,
167
- operation.data,
168
- deletedRefs,
169
- );
170
- }
171
- baseline.timestamp = operations[operations.length - 1].timestamp;
172
- this.set(libraryId, baseline);
173
- };
174
-
175
- deleteAll = (libraryId: string) => {
176
- this.db
177
- .prepare(
178
- `
179
- DELETE FROM DocumentBaseline
180
- WHERE libraryId = ?
181
- `,
182
- )
183
- .run(libraryId);
184
- };
185
-
186
- getCount = (libraryId: string): number => {
187
- return (
188
- this.db
189
- .prepare(
190
- `
191
- SELECT COUNT(*) AS count FROM DocumentBaseline
192
- WHERE libraryId = ?
193
- `,
194
- )
195
- .get(libraryId)?.count ?? 0
196
- );
197
- };
198
- }
@@ -1,232 +0,0 @@
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 server order
80
- * in ascending chronological order
81
- */
82
- getBeforeServerOrder = (libraryId: string, beforeServerOrder: number) => {
83
- return this.db
84
- .prepare(
85
- `
86
- SELECT * FROM OperationHistory
87
- WHERE libraryId = ? AND serverOrder < ?
88
- ORDER BY timestamp ASC
89
- `,
90
- )
91
- .all(libraryId, beforeServerOrder)
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
- getLatestServerOrder = (libraryId: string): number => {
114
- const result = this.db
115
- .prepare(
116
- `
117
- SELECT MAX(serverOrder) AS serverOrder FROM OperationHistory
118
- WHERE libraryId = ?
119
- `,
120
- )
121
- .get(libraryId);
122
-
123
- return result?.serverOrder || 0;
124
- };
125
-
126
- getCount = (libraryId: string): number => {
127
- const result = this.db
128
- .prepare(
129
- `
130
- SELECT COUNT(*) AS count FROM OperationHistory
131
- WHERE libraryId = ?
132
- `,
133
- )
134
- .get(libraryId);
135
-
136
- return result?.count || 0;
137
- };
138
-
139
- /**
140
- * Adds a new operation or replaces an existing one.
141
- * The server order will be set to the current max + 1
142
- */
143
- insert = (libraryId: string, replicaId: string, item: Operation) => {
144
- return this.db
145
- .prepare(
146
- `
147
- INSERT OR REPLACE INTO OperationHistory (libraryId, oid, data, timestamp, replicaId, serverOrder)
148
- VALUES (?, ?, ?, ?, ?, (SELECT COALESCE(MAX(serverOrder), 0) FROM OperationHistory WHERE libraryId = ?) + 1)
149
- `,
150
- )
151
- .run(
152
- libraryId,
153
- item.oid,
154
- JSON.stringify(item.data),
155
- item.timestamp,
156
- replicaId,
157
- );
158
- };
159
-
160
- /**
161
- * Inserts all operations and updates the server
162
- * order for this library
163
- */
164
- insertAll = async (
165
- libraryId: string,
166
- replicaId: string,
167
- items: Operation[],
168
- ) => {
169
- const tx = this.db.transaction(() => {
170
- let orderResult = this.db
171
- .prepare(
172
- `
173
- SELECT COALESCE(MAX(serverOrder), 0) AS serverOrder FROM OperationHistory WHERE libraryId = ?
174
- `,
175
- )
176
- .get(libraryId);
177
- let currentServerOrder = orderResult.serverOrder || 0;
178
- const insertStatement = this.db.prepare(
179
- `
180
- INSERT OR REPLACE INTO OperationHistory (libraryId, oid, data, timestamp, replicaId, serverOrder)
181
- VALUES (?, ?, ?, ?, ?, ?)
182
- `,
183
- );
184
- for (const item of items) {
185
- insertStatement.run(
186
- libraryId,
187
- item.oid,
188
- JSON.stringify(item.data),
189
- item.timestamp,
190
- replicaId,
191
- ++currentServerOrder,
192
- );
193
- }
194
- });
195
-
196
- tx();
197
- };
198
-
199
- dropAll = async (
200
- libraryId: string,
201
- items: Pick<OperationSpec, 'replicaId' | 'oid' | 'timestamp'>[],
202
- ) => {
203
- const deleteStatement = this.db.prepare(
204
- `
205
- DELETE FROM OperationHistory
206
- WHERE libraryId = ? AND replicaId = ? AND oid = ? AND timestamp = ?
207
- `,
208
- );
209
-
210
- this.db.transaction(() => {
211
- for (const item of items) {
212
- deleteStatement.run(
213
- libraryId,
214
- item.replicaId,
215
- item.oid,
216
- item.timestamp,
217
- );
218
- }
219
- })();
220
- };
221
-
222
- deleteAll = (libraryId: string) => {
223
- this.db
224
- .prepare(
225
- `
226
- DELETE FROM OperationHistory
227
- WHERE libraryId = ?
228
- `,
229
- )
230
- .run(libraryId);
231
- };
232
- }
package/src/Replicas.ts DELETED
@@ -1,274 +0,0 @@
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
- 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
- getAll = (libraryId: string): ReplicaInfoSpec[] => {
126
- return this.db
127
- .prepare(
128
- `
129
- SELECT * FROM ReplicaInfo
130
- WHERE libraryId = ?
131
- `,
132
- )
133
- .all(libraryId);
134
- };
135
-
136
- updateLastSeen = (libraryId: string, replicaId: string) => {
137
- const clockTime = new Date().getTime();
138
- return this.db
139
- .prepare(
140
- `
141
- UPDATE ReplicaInfo
142
- SET lastSeenWallClockTime = ?
143
- WHERE id = ? AND libraryId = ?
144
- `,
145
- )
146
- .run(clockTime, replicaId, libraryId);
147
- };
148
-
149
- updateAcknowledged = (
150
- libraryId: string,
151
- replicaId: string,
152
- timestamp: string,
153
- ) => {
154
- this.db
155
- .prepare(
156
- `
157
- UPDATE ReplicaInfo
158
- SET ackedLogicalTime = ?
159
- WHERE id = ? AND libraryId = ?
160
- `,
161
- )
162
- .run(timestamp, replicaId, libraryId);
163
- };
164
-
165
- updateServerOrder = (
166
- libraryId: string,
167
- replicaId: string,
168
- serverOrder: number,
169
- ) => {
170
- // only increment the server order, never set it to lower than before
171
- this.db
172
- .prepare(
173
- `
174
- UPDATE ReplicaInfo
175
- SET ackedServerOrder = MAX(ackedServerOrder, ?)
176
- WHERE id = ? AND libraryId = ?
177
- `,
178
- )
179
- .run(serverOrder, replicaId, libraryId);
180
- };
181
-
182
- getEarliestAckedServerOrder = (libraryId: string) => {
183
- // get the earliest acked server order of all non-truant replicas
184
- const allAcks = (
185
- this.db
186
- .prepare(
187
- `
188
- SELECT ackedServerOrder FROM ReplicaInfo
189
- WHERE libraryId = ?
190
- AND lastSeenWallClockTime > ?
191
- `,
192
- )
193
- .all(libraryId, this.truantCutoff) as {
194
- ackedServerOrder: number | null;
195
- }[]
196
- ).map((row: { ackedServerOrder: number | null }) => row.ackedServerOrder);
197
-
198
- if (allAcks.some((ack) => ack === null)) {
199
- return null;
200
- }
201
- return Math.min(...(allAcks as number[]));
202
- };
203
-
204
- acknowledgeOperation = (
205
- libraryId: string,
206
- replicaId: string,
207
- timestamp: string,
208
- ) => {
209
- if (!timestamp) return;
210
-
211
- // get the server order from the operation with the timestamp and update the replica's server order
212
- // to the max of the current server order and the server order of the operation
213
- this.db
214
- .prepare(
215
- `
216
- UPDATE ReplicaInfo
217
- SET ackedLogicalTime = ?, ackedServerOrder = MAX(ackedServerOrder, COALESCE((
218
- SELECT serverOrder FROM OperationHistory
219
- WHERE libraryId = ? AND timestamp = ?
220
- ), 0))
221
- WHERE id = ? AND libraryId = ?
222
- `,
223
- )
224
- .run(timestamp, libraryId, timestamp, replicaId, libraryId);
225
- };
226
-
227
- getGlobalAck = (libraryId: string, onlineReplicaIds?: string[]) => {
228
- const nonTruant = this.getAllNonTruant(libraryId);
229
- const globalAckEligible = nonTruant.filter(
230
- (replica) => replica.type < 2 || onlineReplicaIds?.includes(replica.id),
231
- );
232
- // get the earliest acked time
233
- return globalAckEligible.reduce(
234
- (acc, replica) => {
235
- if (!replica.ackedLogicalTime) {
236
- return acc;
237
- }
238
- if (acc === undefined) {
239
- return replica.ackedLogicalTime;
240
- }
241
- return acc < replica.ackedLogicalTime ? acc : replica.ackedLogicalTime;
242
- },
243
- undefined as string | undefined,
244
- );
245
- };
246
-
247
- delete = (libraryId: string, replicaId: string) => {
248
- return this.db
249
- .prepare(
250
- `
251
- DELETE FROM ReplicaInfo
252
- WHERE id = ? AND libraryId = ?
253
- `,
254
- )
255
- .run(replicaId, libraryId);
256
- };
257
-
258
- deleteAll = (libraryId: string) => {
259
- return this.db
260
- .prepare(
261
- `
262
- DELETE FROM ReplicaInfo
263
- WHERE libraryId = ?
264
- `,
265
- )
266
- .run(libraryId);
267
- };
268
-
269
- deleteAllForUser = (libraryId: string, userId: string) => {
270
- return this.db
271
- .prepare(`DELETE FROM ReplicaInfo WHERE libraryId = ? AND clientId = ?`)
272
- .run(libraryId, userId);
273
- };
274
- }