@verdant-web/server 3.3.10 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/bin/server.mjs +110 -107
  2. package/dist/esm/TokenVerifier.d.ts +1 -0
  3. package/dist/esm/TokenVerifier.js +3 -0
  4. package/dist/esm/TokenVerifier.js.map +1 -1
  5. package/dist/esm/adapters/node/errorHandler.d.ts +2 -0
  6. package/dist/esm/adapters/node/errorHandler.js +12 -0
  7. package/dist/esm/adapters/node/errorHandler.js.map +1 -0
  8. package/dist/esm/adapters/node/getRequestToken.d.ts +1 -0
  9. package/dist/esm/adapters/node/getRequestToken.js +25 -0
  10. package/dist/esm/adapters/node/getRequestToken.js.map +1 -0
  11. package/dist/esm/adapters/node/httpRouter.d.ts +47 -0
  12. package/dist/esm/adapters/node/httpRouter.js +88 -0
  13. package/dist/esm/adapters/node/httpRouter.js.map +1 -0
  14. package/dist/esm/adapters/node/index.d.ts +5 -0
  15. package/dist/esm/adapters/node/index.js +6 -0
  16. package/dist/esm/adapters/node/index.js.map +1 -0
  17. package/dist/esm/adapters/node/tokenMiddleware.d.ts +7 -0
  18. package/dist/esm/adapters/node/tokenMiddleware.js +22 -0
  19. package/dist/esm/adapters/node/tokenMiddleware.js.map +1 -0
  20. package/dist/esm/adapters/node/websocketHandler.d.ts +4 -0
  21. package/dist/esm/adapters/node/websocketHandler.js +54 -0
  22. package/dist/esm/adapters/node/websocketHandler.js.map +1 -0
  23. package/dist/esm/connections/ClientConnection.d.ts +25 -0
  24. package/dist/esm/{ClientConnection.js → connections/ClientConnection.js} +47 -38
  25. package/dist/esm/connections/ClientConnection.js.map +1 -0
  26. package/dist/esm/connections/MessageSender.d.ts +5 -0
  27. package/dist/esm/connections/MessageSender.js.map +1 -0
  28. package/dist/esm/connections/Presence.d.ts +23 -0
  29. package/dist/esm/connections/Presence.js +65 -0
  30. package/dist/esm/connections/Presence.js.map +1 -0
  31. package/dist/esm/core.d.ts +15 -0
  32. package/dist/esm/core.js +15 -0
  33. package/dist/esm/core.js.map +1 -0
  34. package/dist/esm/defaults.d.ts +2 -0
  35. package/dist/esm/defaults.js +3 -0
  36. package/dist/esm/defaults.js.map +1 -0
  37. package/dist/esm/files/FileStorage.d.ts +12 -3
  38. package/dist/esm/files/FileStorage.js +20 -0
  39. package/dist/esm/files/FileStorage.js.map +1 -1
  40. package/dist/esm/index.d.ts +7 -7
  41. package/dist/esm/index.js +2 -2
  42. package/dist/esm/index.js.map +1 -1
  43. package/dist/esm/internals.d.ts +14 -0
  44. package/dist/esm/internals.js +12 -0
  45. package/dist/esm/internals.js.map +1 -0
  46. package/dist/esm/libraries/Library.d.ts +78 -0
  47. package/dist/esm/{ServerLibrary.js → libraries/Library.js} +153 -144
  48. package/dist/esm/libraries/Library.js.map +1 -0
  49. package/dist/esm/{ServerLibrary.test.js → libraries/Library.test.js} +42 -33
  50. package/dist/esm/libraries/Library.test.js.map +1 -0
  51. package/dist/esm/libraries/generic.d.ts +8 -0
  52. package/dist/esm/libraries/generic.js +2 -0
  53. package/dist/esm/libraries/generic.js.map +1 -0
  54. package/dist/esm/libraries/singleNode.d.ts +53 -0
  55. package/dist/esm/libraries/singleNode.js +88 -0
  56. package/dist/esm/libraries/singleNode.js.map +1 -0
  57. package/dist/esm/logger.d.ts +1 -0
  58. package/dist/esm/logger.js +2 -0
  59. package/dist/esm/logger.js.map +1 -0
  60. package/dist/esm/storage/Storage.d.ts +39 -38
  61. package/dist/esm/storage/StorageManager.d.ts +11 -0
  62. package/dist/esm/storage/StorageManager.js +32 -0
  63. package/dist/esm/storage/StorageManager.js.map +1 -0
  64. package/dist/esm/storage/index.d.ts +1 -2
  65. package/dist/esm/storage/index.js +1 -2
  66. package/dist/esm/storage/index.js.map +1 -1
  67. package/dist/esm/storage/sqlShard/Databases.d.ts +5 -3
  68. package/dist/esm/storage/sqlShard/Databases.js +11 -7
  69. package/dist/esm/storage/sqlShard/Databases.js.map +1 -1
  70. package/dist/esm/storage/sqlShard/SqlBaselines.d.ts +16 -17
  71. package/dist/esm/storage/sqlShard/SqlBaselines.js +55 -59
  72. package/dist/esm/storage/sqlShard/SqlBaselines.js.map +1 -1
  73. package/dist/esm/storage/sqlShard/SqlFileMetadata.d.ts +13 -13
  74. package/dist/esm/storage/sqlShard/SqlFileMetadata.js +44 -57
  75. package/dist/esm/storage/sqlShard/SqlFileMetadata.js.map +1 -1
  76. package/dist/esm/storage/sqlShard/SqlOperations.d.ts +12 -12
  77. package/dist/esm/storage/sqlShard/SqlOperations.js +38 -73
  78. package/dist/esm/storage/sqlShard/SqlOperations.js.map +1 -1
  79. package/dist/esm/storage/sqlShard/SqlReplicas.d.ts +18 -17
  80. package/dist/esm/storage/sqlShard/SqlReplicas.js +63 -100
  81. package/dist/esm/storage/sqlShard/SqlReplicas.js.map +1 -1
  82. package/dist/esm/storage/sqlShard/database.d.ts +25 -5
  83. package/dist/esm/storage/sqlShard/database.js +47 -15
  84. package/dist/esm/storage/sqlShard/database.js.map +1 -1
  85. package/dist/esm/storage/sqlShard/migrations/v1.d.ts +2 -3
  86. package/dist/esm/storage/sqlShard/migrations/v1.js +42 -47
  87. package/dist/esm/storage/sqlShard/migrations/v1.js.map +1 -1
  88. package/dist/esm/storage/sqlShard/migrations/v2.d.ts +2 -3
  89. package/dist/esm/storage/sqlShard/migrations/v2.js +5 -15
  90. package/dist/esm/storage/sqlShard/migrations/v2.js.map +1 -1
  91. package/dist/esm/storage/sqlShard/migrations.d.ts +5 -7
  92. package/dist/esm/storage/sqlShard/migrations.js +48 -1
  93. package/dist/esm/storage/sqlShard/migrations.js.map +1 -1
  94. package/dist/esm/storage/sqlShard/sqlShardStorage.d.ts +25 -4
  95. package/dist/esm/storage/sqlShard/sqlShardStorage.js +43 -40
  96. package/dist/esm/storage/sqlShard/sqlShardStorage.js.map +1 -1
  97. package/dist/esm/storage/sqlShard/tables.d.ts +9 -6
  98. package/dist/esm/types.d.ts +1 -2
  99. package/package.json +24 -13
  100. package/src/TokenVerifier.ts +4 -0
  101. package/src/adapters/node/errorHandler.ts +18 -0
  102. package/src/adapters/node/getRequestToken.ts +34 -0
  103. package/src/adapters/node/httpRouter.ts +139 -0
  104. package/src/adapters/node/index.ts +5 -0
  105. package/src/adapters/node/tokenMiddleware.ts +32 -0
  106. package/src/adapters/node/websocketHandler.ts +79 -0
  107. package/src/{ClientConnection.ts → connections/ClientConnection.ts} +64 -57
  108. package/src/connections/MessageSender.ts +6 -0
  109. package/src/connections/Presence.ts +95 -0
  110. package/src/core.ts +36 -0
  111. package/src/defaults.ts +2 -0
  112. package/src/files/FileStorage.ts +36 -3
  113. package/src/index.ts +7 -7
  114. package/src/internals.ts +14 -0
  115. package/src/{ServerLibrary.test.ts → libraries/Library.test.ts} +51 -49
  116. package/src/{ServerLibrary.ts → libraries/Library.ts} +203 -206
  117. package/src/libraries/generic.ts +9 -0
  118. package/src/libraries/singleNode.ts +145 -0
  119. package/src/logger.ts +4 -0
  120. package/src/storage/Storage.ts +37 -66
  121. package/src/storage/StorageManager.ts +39 -0
  122. package/src/storage/index.ts +1 -2
  123. package/src/storage/sqlShard/Databases.ts +26 -13
  124. package/src/storage/sqlShard/SqlBaselines.ts +58 -67
  125. package/src/storage/sqlShard/SqlFileMetadata.ts +51 -72
  126. package/src/storage/sqlShard/SqlOperations.ts +57 -81
  127. package/src/storage/sqlShard/SqlReplicas.ts +80 -121
  128. package/src/storage/sqlShard/database.ts +82 -18
  129. package/src/storage/sqlShard/migrations/v1.ts +42 -52
  130. package/src/storage/sqlShard/migrations/v2.ts +5 -19
  131. package/src/storage/sqlShard/migrations.ts +66 -1
  132. package/src/storage/sqlShard/sqlShardStorage.ts +56 -55
  133. package/src/storage/sqlShard/tables.ts +6 -6
  134. package/src/types.ts +1 -2
  135. package/dist/esm/ClientConnection.d.ts +0 -19
  136. package/dist/esm/ClientConnection.js.map +0 -1
  137. package/dist/esm/MessageSender.d.ts +0 -5
  138. package/dist/esm/MessageSender.js.map +0 -1
  139. package/dist/esm/Presence.d.ts +0 -18
  140. package/dist/esm/Presence.js +0 -65
  141. package/dist/esm/Presence.js.map +0 -1
  142. package/dist/esm/ReplicaKeepaliveTimers.d.ts +0 -17
  143. package/dist/esm/ReplicaKeepaliveTimers.js +0 -27
  144. package/dist/esm/ReplicaKeepaliveTimers.js.map +0 -1
  145. package/dist/esm/Server.d.ts +0 -166
  146. package/dist/esm/Server.js +0 -572
  147. package/dist/esm/Server.js.map +0 -1
  148. package/dist/esm/ServerLibrary.d.ts +0 -64
  149. package/dist/esm/ServerLibrary.js.map +0 -1
  150. package/dist/esm/ServerLibrary.test.js.map +0 -1
  151. package/dist/esm/storage/sql/SqlBaselines.d.ts +0 -22
  152. package/dist/esm/storage/sql/SqlBaselines.js +0 -107
  153. package/dist/esm/storage/sql/SqlBaselines.js.map +0 -1
  154. package/dist/esm/storage/sql/SqlFileMetadata.d.ts +0 -18
  155. package/dist/esm/storage/sql/SqlFileMetadata.js +0 -69
  156. package/dist/esm/storage/sql/SqlFileMetadata.js.map +0 -1
  157. package/dist/esm/storage/sql/SqlOperations.d.ts +0 -19
  158. package/dist/esm/storage/sql/SqlOperations.js +0 -122
  159. package/dist/esm/storage/sql/SqlOperations.js.map +0 -1
  160. package/dist/esm/storage/sql/SqlReplicas.d.ts +0 -32
  161. package/dist/esm/storage/sql/SqlReplicas.js +0 -163
  162. package/dist/esm/storage/sql/SqlReplicas.js.map +0 -1
  163. package/dist/esm/storage/sql/database.d.ts +0 -9
  164. package/dist/esm/storage/sql/database.js +0 -24
  165. package/dist/esm/storage/sql/database.js.map +0 -1
  166. package/dist/esm/storage/sql/migrations/v1.d.ts +0 -3
  167. package/dist/esm/storage/sql/migrations/v1.js +0 -59
  168. package/dist/esm/storage/sql/migrations/v1.js.map +0 -1
  169. package/dist/esm/storage/sql/migrations/v2.d.ts +0 -3
  170. package/dist/esm/storage/sql/migrations/v2.js +0 -16
  171. package/dist/esm/storage/sql/migrations/v2.js.map +0 -1
  172. package/dist/esm/storage/sql/migrations.d.ts +0 -7
  173. package/dist/esm/storage/sql/migrations.js +0 -4
  174. package/dist/esm/storage/sql/migrations.js.map +0 -1
  175. package/dist/esm/storage/sql/sqlStorage.d.ts +0 -6
  176. package/dist/esm/storage/sql/sqlStorage.js +0 -35
  177. package/dist/esm/storage/sql/sqlStorage.js.map +0 -1
  178. package/dist/esm/storage/sql/tables.d.ts +0 -44
  179. package/dist/esm/storage/sql/tables.js +0 -2
  180. package/dist/esm/storage/sql/tables.js.map +0 -1
  181. package/dist/esm/storage/sqlShard/transfer.d.ts +0 -7
  182. package/dist/esm/storage/sqlShard/transfer.js +0 -90
  183. package/dist/esm/storage/sqlShard/transfer.js.map +0 -1
  184. package/dist/esm/storage/sqlShard/transfer.test.d.ts +0 -1
  185. package/dist/esm/storage/sqlShard/transfer.test.js +0 -109
  186. package/dist/esm/storage/sqlShard/transfer.test.js.map +0 -1
  187. package/src/MessageSender.ts +0 -10
  188. package/src/Presence.ts +0 -86
  189. package/src/ReplicaKeepaliveTimers.ts +0 -34
  190. package/src/Server.ts +0 -859
  191. package/src/storage/sql/SqlBaselines.ts +0 -147
  192. package/src/storage/sql/SqlFileMetadata.ts +0 -94
  193. package/src/storage/sql/SqlOperations.ts +0 -160
  194. package/src/storage/sql/SqlReplicas.ts +0 -234
  195. package/src/storage/sql/database.ts +0 -32
  196. package/src/storage/sql/migrations/v1.ts +0 -64
  197. package/src/storage/sql/migrations/v2.ts +0 -20
  198. package/src/storage/sql/migrations.ts +0 -4
  199. package/src/storage/sql/sqlStorage.ts +0 -57
  200. package/src/storage/sql/tables.ts +0 -51
  201. package/src/storage/sqlShard/transfer.test.ts +0 -149
  202. package/src/storage/sqlShard/transfer.ts +0 -123
  203. /package/dist/esm/{MessageSender.js → connections/MessageSender.js} +0 -0
  204. /package/dist/esm/{ServerLibrary.test.d.ts → libraries/Library.test.d.ts} +0 -0
@@ -1,147 +0,0 @@
1
- import { Kysely } from 'kysely';
2
- import { BaselineStorage } from '../Storage.js';
3
- import { Database, DocumentBaselineRow } from './tables.js';
4
- import { DocumentBaseline, Ref, applyPatch } from '@verdant-web/common';
5
- import { HydratedDocumentBaseline, StoredOperation } from '../../types.js';
6
-
7
- export class SqlBaselines implements BaselineStorage {
8
- constructor(
9
- private db: Kysely<Database>,
10
- private dialect: 'postgres' | 'sqlite',
11
- ) {}
12
-
13
- get = async (
14
- libraryId: string,
15
- oid: string,
16
- { tx }: { tx?: Kysely<Database> } = {},
17
- ): Promise<HydratedDocumentBaseline | null> => {
18
- const db = tx ?? this.db;
19
- const raw =
20
- (await db
21
- .selectFrom('DocumentBaseline')
22
- .where('libraryId', '=', libraryId)
23
- .where('oid', '=', oid)
24
- .selectAll()
25
- .executeTakeFirst()) ?? null;
26
- if (!raw) return null;
27
- this.hydrate(raw);
28
- return raw as any;
29
- };
30
-
31
- getAll = async (libraryId: string) => {
32
- const raw = await this.db
33
- .selectFrom('DocumentBaseline')
34
- .where('libraryId', '=', libraryId)
35
- .orderBy('timestamp', 'asc')
36
- .selectAll()
37
- .execute();
38
-
39
- raw.forEach(this.hydrate);
40
-
41
- return raw as any;
42
- };
43
-
44
- set = async (
45
- libraryId: string,
46
- baseline: DocumentBaseline,
47
- {
48
- tx,
49
- }: {
50
- tx?: Kysely<Database>;
51
- },
52
- ) => {
53
- const db = tx ?? this.db;
54
- if (!baseline.snapshot) {
55
- await db
56
- .deleteFrom('DocumentBaseline')
57
- .where('libraryId', '=', libraryId)
58
- .where('oid', '=', baseline.oid)
59
- .execute();
60
- return;
61
- }
62
-
63
- await db
64
- .insertInto('DocumentBaseline')
65
- .values({
66
- libraryId,
67
- oid: baseline.oid,
68
- snapshot: JSON.stringify(baseline.snapshot),
69
- timestamp: baseline.timestamp,
70
- authz: baseline.authz,
71
- })
72
- .onConflict((cb) =>
73
- cb.doUpdateSet({
74
- snapshot: JSON.stringify(baseline.snapshot),
75
- timestamp: baseline.timestamp,
76
- authz: baseline.authz,
77
- }),
78
- )
79
- .execute();
80
- };
81
-
82
- insertAll = async (libraryId: string, baselines: DocumentBaseline[]) => {
83
- await this.db.transaction().execute(async (tx) => {
84
- for (const baseline of baselines) {
85
- await this.set(libraryId, baseline, { tx });
86
- }
87
- });
88
- };
89
-
90
- private hydrate = (row: DocumentBaselineRow) => {
91
- // avoiding extra allocation from .map by type-asserting here
92
- row.snapshot = JSON.parse(row.snapshot);
93
- };
94
-
95
- applyOperations = async (
96
- libraryId: string,
97
- oid: string,
98
- operations: StoredOperation[],
99
- deletedRefs?: Ref[],
100
- ) => {
101
- if (operations.length === 0) return;
102
-
103
- await this.db.transaction().execute(async (tx) => {
104
- let baseline = await this.get(libraryId, oid, { tx });
105
- if (!baseline) {
106
- baseline = {
107
- oid,
108
- snapshot: {},
109
- timestamp: operations[0].timestamp,
110
- libraryId,
111
- authz: operations[0].authz,
112
- };
113
- }
114
- for (const operation of operations) {
115
- baseline.snapshot = applyPatch(
116
- baseline.snapshot,
117
- operation.data,
118
- deletedRefs,
119
- );
120
- if (operation.data.op === 'initialize') {
121
- baseline.authz = operation.authz;
122
- }
123
- }
124
- baseline.timestamp = operations[operations.length - 1].timestamp;
125
- await this.set(libraryId, baseline, { tx });
126
- });
127
- };
128
-
129
- deleteAll = async (libraryId: string) => {
130
- await this.db
131
- .deleteFrom('DocumentBaseline')
132
- .where('libraryId', '=', libraryId)
133
- .execute();
134
- };
135
-
136
- getCount = async (libraryId: string) => {
137
- return (
138
- (
139
- await this.db
140
- .selectFrom('DocumentBaseline')
141
- .where('libraryId', '=', libraryId)
142
- .select(({ fn }) => fn.countAll<number>().as('count'))
143
- .executeTakeFirst()
144
- )?.count ?? 0
145
- );
146
- };
147
- }
@@ -1,94 +0,0 @@
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
-
7
- export class SqlFileMetadata implements FileMetadataStorage {
8
- constructor(
9
- private db: Kysely<Database>,
10
- private deleteExpirationDays: number,
11
- private dialect: 'postgres' | 'sqlite',
12
- ) {}
13
-
14
- get = async (
15
- libraryId: string,
16
- fileId: string,
17
- ): Promise<FileMetadata | null> => {
18
- return (
19
- (await this.db
20
- .selectFrom('FileMetadata')
21
- .where('libraryId', '=', libraryId)
22
- .where('fileId', '=', fileId)
23
- .selectAll()
24
- .executeTakeFirst()) ?? null
25
- );
26
- };
27
-
28
- getAll = async (libraryId: string): Promise<FileMetadata[]> => {
29
- return this.db
30
- .selectFrom('FileMetadata')
31
- .where('libraryId', '=', libraryId)
32
- .selectAll()
33
- .execute();
34
- };
35
-
36
- deleteAll = async (libraryId: string): Promise<void> => {
37
- await this.db
38
- .deleteFrom('FileMetadata')
39
- .where('libraryId', '=', libraryId)
40
- .execute();
41
- };
42
-
43
- put = async (libraryId: string, fileInfo: FileInfo): Promise<void> => {
44
- await this.db
45
- .insertInto('FileMetadata')
46
- .values({
47
- libraryId,
48
- fileId: fileInfo.id,
49
- name: fileInfo.fileName,
50
- type: fileInfo.type,
51
- })
52
- .onConflict((cb) =>
53
- cb.columns(['libraryId', 'fileId']).doUpdateSet({
54
- name: fileInfo.fileName,
55
- type: fileInfo.type,
56
- pendingDeleteAt: null,
57
- }),
58
- )
59
- .execute();
60
- };
61
-
62
- markPendingDelete = async (
63
- libraryId: string,
64
- fileId: string,
65
- ): Promise<void> => {
66
- await this.db
67
- .updateTable('FileMetadata')
68
- .set({ pendingDeleteAt: Date.now() })
69
- .where('libraryId', '=', libraryId)
70
- .where('fileId', '=', fileId)
71
- .execute();
72
- };
73
-
74
- delete = async (libraryId: string, fileId: string): Promise<void> => {
75
- await this.db
76
- .deleteFrom('FileMetadata')
77
- .where('libraryId', '=', libraryId)
78
- .where('fileId', '=', fileId)
79
- .execute();
80
- };
81
-
82
- getPendingDelete = async (libraryId: string): Promise<FileMetadata[]> => {
83
- return this.db
84
- .selectFrom('FileMetadata')
85
- .where('libraryId', '=', libraryId)
86
- .where(
87
- 'pendingDeleteAt',
88
- '<',
89
- Date.now() - 1000 * 60 * 60 * 24 * this.deleteExpirationDays,
90
- )
91
- .selectAll()
92
- .execute();
93
- };
94
- }
@@ -1,160 +0,0 @@
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, OperationHistoryRow } from './tables.js';
6
-
7
- export class SqlOperations implements OperationStorage {
8
- constructor(
9
- private db: Kysely<Database>,
10
- private dialect: 'postgres' | 'sqlite',
11
- ) {}
12
-
13
- private hydrate = (row: OperationHistoryRow) => {
14
- // avoiding extra allocation from .map by type-asserting here
15
- row.data = JSON.parse(row.data);
16
- };
17
-
18
- getAll = async (
19
- libraryId: string,
20
- oid: string,
21
- ): Promise<StoredOperation[]> => {
22
- const raw = await this.db
23
- .selectFrom('OperationHistory')
24
- .where('libraryId', '=', libraryId)
25
- .where('oid', '=', oid)
26
- .orderBy('timestamp', 'asc')
27
- .selectAll()
28
- .execute();
29
-
30
- raw.forEach(this.hydrate);
31
- return raw as unknown as StoredOperation[];
32
- };
33
-
34
- getBeforeServerOrder = async (
35
- libraryId: string,
36
- beforeServerOrder: number,
37
- ): Promise<StoredOperation[]> => {
38
- const raw = await this.db
39
- .selectFrom('OperationHistory')
40
- .where('libraryId', '=', libraryId)
41
- .where('serverOrder', '<', beforeServerOrder)
42
- .orderBy('timestamp', 'asc')
43
- .selectAll()
44
- .execute();
45
-
46
- raw.forEach(this.hydrate);
47
- return raw as unknown as StoredOperation[];
48
- };
49
-
50
- getAfterServerOrder = async (
51
- libraryId: string,
52
- afterServerOrder: number,
53
- ): Promise<StoredOperation[]> => {
54
- const raw = await this.db
55
- .selectFrom('OperationHistory')
56
- .where('libraryId', '=', libraryId)
57
- .where('serverOrder', '>', afterServerOrder)
58
- .orderBy('timestamp', 'asc')
59
- .selectAll()
60
- .execute();
61
-
62
- raw.forEach(this.hydrate);
63
- return raw as unknown as StoredOperation[];
64
- };
65
-
66
- getLatestServerOrder = async (libraryId: string): Promise<number> => {
67
- return (
68
- (
69
- await this.db
70
- .selectFrom('OperationHistory')
71
- .where('libraryId', '=', libraryId)
72
- .orderBy('serverOrder', 'desc')
73
- .select('serverOrder')
74
- .limit(1)
75
- .executeTakeFirst()
76
- )?.serverOrder ?? 0
77
- );
78
- };
79
-
80
- getCount = async (libraryId: string): Promise<number> => {
81
- return (
82
- (
83
- await this.db
84
- .selectFrom('OperationHistory')
85
- .where('libraryId', '=', libraryId)
86
- .select(({ fn }) => fn.countAll<number>().as('count'))
87
- .executeTakeFirst()
88
- )?.count ?? 0
89
- );
90
- };
91
-
92
- insertAll = async (
93
- libraryId: string,
94
- replicaId: string,
95
- operations: Operation[],
96
- ): Promise<number> => {
97
- // inserts all operations and updates server order
98
- // FIXME: this whole thing is kinda sus
99
- return await this.db.transaction().execute(async (tx): Promise<number> => {
100
- let orderResult = await tx
101
- .selectFrom('OperationHistory')
102
- .select('serverOrder')
103
- .where('libraryId', '=', libraryId)
104
- .orderBy('serverOrder', 'desc')
105
- .limit(1)
106
- .executeTakeFirst();
107
- let currentServerOrder = orderResult?.serverOrder ?? 0;
108
- for (const item of operations) {
109
- // utilizing returned serverOrder accommodates for conflicts
110
- const result = await tx
111
- .insertInto('OperationHistory')
112
- .values({
113
- libraryId,
114
- oid: item.oid,
115
- data: JSON.stringify(item.data),
116
- timestamp: item.timestamp,
117
- replicaId,
118
- serverOrder: currentServerOrder + 1,
119
- authz: item.authz,
120
- })
121
- .onConflict((cb) =>
122
- cb
123
- .columns(['libraryId', 'replicaId', 'oid', 'timestamp'])
124
- .doNothing(),
125
- )
126
- .returning('serverOrder')
127
- .executeTakeFirst();
128
- if (result) {
129
- currentServerOrder = result.serverOrder;
130
- } else {
131
- // on conflict, nothing is returned.
132
- // this would mean an operation was synced twice
133
- }
134
- }
135
- return currentServerOrder;
136
- });
137
- };
138
-
139
- deleteAll = async (libraryId: string): Promise<void> => {
140
- await this.db
141
- .deleteFrom('OperationHistory')
142
- .where('libraryId', '=', libraryId)
143
- .execute();
144
- };
145
- delete = async (
146
- libraryId: string,
147
- operations: Operation[],
148
- ): Promise<void> => {
149
- await this.db.transaction().execute(async (tx) => {
150
- for (const item of operations) {
151
- await tx
152
- .deleteFrom('OperationHistory')
153
- .where('libraryId', '=', libraryId)
154
- .where('oid', '=', item.oid)
155
- .where('timestamp', '=', item.timestamp)
156
- .execute();
157
- }
158
- });
159
- };
160
- }
@@ -1,234 +0,0 @@
1
- import { StoredReplicaInfo } from '../../types.js';
2
- import { ReplicaStorage } from '../Storage.js';
3
- import { sql, type Kysely } from 'kysely';
4
- import { Database } from './tables.js';
5
- import { ReplicaType, VerdantError } from '@verdant-web/common';
6
-
7
- export class SqlReplicas implements ReplicaStorage {
8
- constructor(
9
- private db: Kysely<Database>,
10
- private readonly replicaTruancyMinutes: number,
11
- private readonly dialect: 'sqlite' | 'postgres',
12
- ) {}
13
-
14
- get truantCutoff(): number {
15
- return Date.now() - this.replicaTruancyMinutes * 60 * 1000;
16
- }
17
-
18
- get = async (
19
- libraryId: string,
20
- replicaId: string,
21
- ): Promise<StoredReplicaInfo | null> => {
22
- return (
23
- (await this.db
24
- .selectFrom('ReplicaInfo')
25
- .where('libraryId', '=', libraryId)
26
- .where('id', '=', replicaId)
27
- .selectAll()
28
- .executeTakeFirst()) ?? null
29
- );
30
- };
31
- getOrCreate = async (
32
- libraryId: string,
33
- replicaId: string,
34
- info: { userId: string; type: ReplicaType },
35
- ): Promise<{
36
- status: 'new' | 'existing' | 'truant';
37
- replicaInfo: StoredReplicaInfo;
38
- }> => {
39
- const existing = await this.get(libraryId, replicaId);
40
- if (!existing) {
41
- const created = await this.db
42
- .insertInto('ReplicaInfo')
43
- .values({
44
- id: replicaId,
45
- clientId: info.userId,
46
- libraryId,
47
- type: info.type,
48
- ackedServerOrder: 0,
49
- })
50
- .returningAll()
51
- .executeTakeFirst();
52
- if (!created) {
53
- throw new VerdantError(
54
- VerdantError.Code.Unexpected,
55
- undefined,
56
- 'Failed to create replica',
57
- );
58
- }
59
- return { status: 'new', replicaInfo: created };
60
- }
61
-
62
- if (existing.type !== info.type) {
63
- // type should be updated if a new token changes it
64
- await this.db
65
- .updateTable('ReplicaInfo')
66
- .set({ type: info.type })
67
- .where('libraryId', '=', libraryId)
68
- .where('id', '=', replicaId)
69
- .execute();
70
- }
71
-
72
- if (existing.clientId !== info.userId) {
73
- // replicas cannot change hands - this is a security issue
74
- throw new VerdantError(
75
- VerdantError.Code.Forbidden,
76
- undefined,
77
- 'Another user is already using this replica ID',
78
- );
79
- }
80
-
81
- if (
82
- existing.lastSeenWallClockTime !== null &&
83
- existing.lastSeenWallClockTime < this.truantCutoff
84
- ) {
85
- return { status: 'truant', replicaInfo: existing };
86
- }
87
-
88
- return { status: 'existing', replicaInfo: existing };
89
- };
90
-
91
- getAll = async (
92
- libraryId: string,
93
- options?: { omitTruant: boolean } | undefined,
94
- ): Promise<StoredReplicaInfo[]> => {
95
- let builder = this.db
96
- .selectFrom('ReplicaInfo')
97
- .where('libraryId', '=', libraryId);
98
-
99
- if (options?.omitTruant) {
100
- builder = builder.where('lastSeenWallClockTime', '>', this.truantCutoff);
101
- }
102
-
103
- return builder.selectAll().execute();
104
- };
105
-
106
- updateLastSeen = async (
107
- libraryId: string,
108
- replicaId: string,
109
- ): Promise<void> => {
110
- const clockTime = Date.now();
111
- await this.db
112
- .updateTable('ReplicaInfo')
113
- .set({ lastSeenWallClockTime: clockTime })
114
- .where('libraryId', '=', libraryId)
115
- .where('id', '=', replicaId)
116
- .execute();
117
- };
118
-
119
- updateAckedServerOrder = async (
120
- libraryId: string,
121
- replicaId: string,
122
- serverOrder: number,
123
- ): Promise<void> => {
124
- const max = this.dialect === 'postgres' ? 'GREATEST' : 'MAX';
125
- await this.db
126
- .updateTable('ReplicaInfo')
127
- .set(
128
- 'ackedServerOrder',
129
- ({ val }) =>
130
- sql<number>`${sql.raw(max)}(ackedServerOrder, ${val(serverOrder)})`,
131
- )
132
- .where('libraryId', '=', libraryId)
133
- .where('id', '=', replicaId)
134
- .execute();
135
- };
136
-
137
- updateAcknowledgedLogicalTime = async (
138
- libraryId: string,
139
- replicaId: string,
140
- timestamp: string,
141
- ): Promise<void> => {
142
- await this.db
143
- .updateTable('ReplicaInfo')
144
- .set({ ackedLogicalTime: timestamp })
145
- .where('libraryId', '=', libraryId)
146
- .where('id', '=', replicaId)
147
- .execute();
148
- };
149
-
150
- getEarliestAckedServerOrder = async (libraryId: string): Promise<number> => {
151
- // gets earliest acked server order of all non-truant replicas.
152
- const res = await this.db
153
- .selectFrom('ReplicaInfo')
154
- .where('libraryId', '=', libraryId)
155
- .where('lastSeenWallClockTime', '>', this.truantCutoff)
156
- .orderBy('ackedServerOrder', 'asc')
157
- .select('ackedServerOrder')
158
- .executeTakeFirst();
159
- return res?.ackedServerOrder ?? 0;
160
- };
161
-
162
- acknowledgeOperation = async (
163
- libraryId: string,
164
- replicaId: string,
165
- timestamp: string,
166
- ): Promise<void> => {
167
- if (!timestamp) return;
168
- // when acking an operation, we also set the replica's server order
169
- // to that operation's server order, if it's greater.
170
- const max = this.dialect === 'postgres' ? 'GREATEST' : 'MAX';
171
- await this.db
172
- .updateTable('ReplicaInfo')
173
- .set({ ackedLogicalTime: timestamp })
174
- .set(
175
- 'ackedServerOrder',
176
- ({ val }) => sql<number>`${sql.raw(max)}(
177
- ackedServerOrder,
178
- COALESCE(
179
- (
180
- SELECT serverOrder FROM OperationHistory
181
- WHERE libraryId = ${val(libraryId)} AND timestamp = ${val(timestamp)}
182
- ),
183
- 0
184
- )
185
- )`,
186
- )
187
- .where('libraryId', '=', libraryId)
188
- .where('id', '=', replicaId)
189
- .execute();
190
- };
191
-
192
- getGlobalAck = async (
193
- libraryId: string,
194
- onlineReplicaIds?: string[] | undefined,
195
- ): Promise<string | null> => {
196
- const nonTruant = await this.getAll(libraryId, { omitTruant: true });
197
- if (nonTruant.length === 0) return null;
198
- const globalAckEligible = nonTruant.filter(
199
- (replica) => replica.type < 2 || onlineReplicaIds?.includes(replica.id),
200
- );
201
-
202
- return globalAckEligible.reduce(
203
- (acc, replica) => {
204
- if (!replica.ackedLogicalTime) return acc;
205
- if (acc === null) return replica.ackedLogicalTime;
206
- return acc < replica.ackedLogicalTime ? acc : replica.ackedLogicalTime;
207
- },
208
- null as string | null,
209
- );
210
- };
211
- delete = async (libraryId: string, replicaId: string): Promise<void> => {
212
- await this.db
213
- .deleteFrom('ReplicaInfo')
214
- .where('libraryId', '=', libraryId)
215
- .where('id', '=', replicaId)
216
- .execute();
217
- };
218
- deleteAll = async (libraryId: string): Promise<void> => {
219
- await this.db
220
- .deleteFrom('ReplicaInfo')
221
- .where('libraryId', '=', libraryId)
222
- .execute();
223
- };
224
- deleteAllForUser = async (
225
- libraryId: string,
226
- userId: string,
227
- ): Promise<void> => {
228
- await this.db
229
- .deleteFrom('ReplicaInfo')
230
- .where('libraryId', '=', libraryId)
231
- .where('clientId', '=', userId)
232
- .execute();
233
- };
234
- }
@@ -1,32 +0,0 @@
1
- import { Kysely, SqliteDialect } from 'kysely';
2
- import { Database as DatabaseTypes } from './tables.js';
3
- import Database from 'better-sqlite3';
4
- import { migrateToLatest } from '@a-type/kysely';
5
- import migrations from './migrations.js';
6
-
7
- export function openDatabase(
8
- file: string,
9
- options: {
10
- skipMigrations?: boolean;
11
- disableWal?: boolean;
12
- } = {},
13
- ) {
14
- const internalDb = new Database(file);
15
- if (!options.disableWal) {
16
- internalDb.pragma('journal_mode = WAL');
17
- }
18
- const db = new Kysely<DatabaseTypes>({
19
- dialect: new SqliteDialect({
20
- database: internalDb,
21
- }),
22
- });
23
- let ready = Promise.resolve<void>(undefined);
24
- if (!options.skipMigrations) {
25
- ready = migrateToLatest(db, migrations);
26
- }
27
-
28
- return {
29
- db,
30
- ready,
31
- };
32
- }