@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
@@ -0,0 +1,118 @@
1
+ import {
2
+ DocumentBaseline,
3
+ Operation,
4
+ Ref,
5
+ ReplicaType,
6
+ } from '@verdant-web/common';
7
+ import {
8
+ HydratedDocumentBaseline,
9
+ StoredOperation,
10
+ StoredReplicaInfo,
11
+ FileMetadata,
12
+ } from '../types.js';
13
+ import { FileInfo } from '../files/FileStorage.js';
14
+
15
+ export interface ReplicaStorage {
16
+ readonly truantCutoff: number;
17
+ get(libraryId: string, replicaId: string): Promise<StoredReplicaInfo | null>;
18
+ getOrCreate(
19
+ libraryId: string,
20
+ replicaId: string,
21
+ info: { userId: string; type: ReplicaType },
22
+ ): Promise<{
23
+ status: 'new' | 'existing' | 'truant';
24
+ replicaInfo: StoredReplicaInfo;
25
+ }>;
26
+ getAll(
27
+ libraryId: string,
28
+ options?: { omitTruant: boolean },
29
+ ): Promise<StoredReplicaInfo[]>;
30
+ updateLastSeen(libraryId: string, replicaId: string): Promise<void>;
31
+ updateAckedServerOrder(
32
+ libraryId: string,
33
+ replicaId: string,
34
+ serverOrder: number,
35
+ ): Promise<void>;
36
+ updateAcknowledgedLogicalTime(
37
+ libraryId: string,
38
+ replicaId: string,
39
+ timestamp: string,
40
+ ): Promise<void>;
41
+ getEarliestAckedServerOrder(libraryId: string): Promise<number>;
42
+ acknowledgeOperation(
43
+ libraryId: string,
44
+ replicaId: string,
45
+ timestamp: string,
46
+ ): Promise<void>;
47
+ getGlobalAck(
48
+ libraryId: string,
49
+ onlineReplicaIds?: string[],
50
+ ): Promise<string | null>;
51
+ delete(libraryId: string, replicaId: string): Promise<void>;
52
+ deleteAll(libraryId: string): Promise<void>;
53
+ deleteAllForUser(libraryId: string, userId: string): Promise<void>;
54
+ }
55
+
56
+ export interface OperationStorage {
57
+ getAll(libraryId: string, oid: string): Promise<StoredOperation[]>;
58
+ getBeforeServerOrder(
59
+ libraryId: string,
60
+ beforeServerOrder: number,
61
+ ): Promise<StoredOperation[]>;
62
+ getAfterServerOrder(
63
+ libraryId: string,
64
+ afterServerOrder: number,
65
+ ): Promise<StoredOperation[]>;
66
+ getLatestServerOrder(libraryId: string): Promise<number>;
67
+ getCount(libraryId: string): Promise<number>;
68
+ insertAll(
69
+ libraryId: string,
70
+ replicaId: string,
71
+ operations: Operation[],
72
+ ): Promise<void>;
73
+ deleteAll(libraryId: string): Promise<void>;
74
+ delete(libraryId: string, operations: Operation[]): Promise<void>;
75
+ }
76
+
77
+ export interface BaselineStorage {
78
+ get(libraryId: string, oid: string): Promise<HydratedDocumentBaseline | null>;
79
+ getAll(libraryId: string): Promise<HydratedDocumentBaseline[]>;
80
+ insertAll(
81
+ libraryId: string,
82
+ baselines: DocumentBaseline<any>[],
83
+ ): Promise<void>;
84
+ deleteAll(libraryId: string): Promise<void>;
85
+ getCount(libraryId: string): Promise<number>;
86
+ applyOperations(
87
+ libraryId: string,
88
+ oid: string,
89
+ operations: StoredOperation[],
90
+ deletedRefs?: Ref[],
91
+ ): Promise<void>;
92
+ }
93
+
94
+ export interface FileMetadataStorage {
95
+ get(libraryId: string, fileId: string): Promise<FileMetadata | null>;
96
+ getAll(libraryId: string): Promise<FileMetadata[]>;
97
+ deleteAll(libraryId: string): Promise<void>;
98
+ put(libraryId: string, fileInfo: FileInfo): Promise<void>;
99
+ markPendingDelete(libraryId: string, fileId: string): Promise<void>;
100
+ delete(libraryId: string, fileId: string): Promise<void>;
101
+ getPendingDelete(libraryId: string): Promise<FileMetadata[]>;
102
+ }
103
+
104
+ export interface StorageOptions {
105
+ replicaTruancyMinutes: number;
106
+ fileDeleteExpirationDays: number;
107
+ }
108
+ export interface Storage {
109
+ replicas: ReplicaStorage;
110
+ operations: OperationStorage;
111
+ baselines: BaselineStorage;
112
+ fileMetadata: FileMetadataStorage;
113
+ close(): Promise<void>;
114
+ readonly open: boolean;
115
+ readonly ready: Promise<void>;
116
+ }
117
+
118
+ export type StorageFactory = (options: StorageOptions) => Storage;
@@ -0,0 +1,2 @@
1
+ export * from './Storage.js';
2
+ export * from './sql/sqlStorage.js';
@@ -0,0 +1,141 @@
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
+ })
71
+ .onConflict((cb) =>
72
+ cb.doUpdateSet({
73
+ snapshot: JSON.stringify(baseline.snapshot),
74
+ timestamp: baseline.timestamp,
75
+ }),
76
+ )
77
+ .execute();
78
+ };
79
+
80
+ insertAll = async (libraryId: string, baselines: DocumentBaseline[]) => {
81
+ await this.db.transaction().execute(async (tx) => {
82
+ for (const baseline of baselines) {
83
+ await this.set(libraryId, baseline, { tx });
84
+ }
85
+ });
86
+ };
87
+
88
+ private hydrate = (row: DocumentBaselineRow) => {
89
+ // avoiding extra allocation from .map by type-asserting here
90
+ row.snapshot = JSON.parse(row.snapshot);
91
+ };
92
+
93
+ applyOperations = async (
94
+ libraryId: string,
95
+ oid: string,
96
+ operations: StoredOperation[],
97
+ deletedRefs?: Ref[],
98
+ ) => {
99
+ if (operations.length === 0) return;
100
+
101
+ await this.db.transaction().execute(async (tx) => {
102
+ let baseline = await this.get(libraryId, oid, { tx });
103
+ if (!baseline) {
104
+ baseline = {
105
+ oid,
106
+ snapshot: {},
107
+ timestamp: operations[0].timestamp,
108
+ libraryId,
109
+ };
110
+ }
111
+ for (const operation of operations) {
112
+ baseline.snapshot = applyPatch(
113
+ baseline.snapshot,
114
+ operation.data,
115
+ deletedRefs,
116
+ );
117
+ }
118
+ baseline.timestamp = operations[operations.length - 1].timestamp;
119
+ await this.set(libraryId, baseline, { tx });
120
+ });
121
+ };
122
+
123
+ deleteAll = async (libraryId: string) => {
124
+ await this.db
125
+ .deleteFrom('DocumentBaseline')
126
+ .where('libraryId', '=', libraryId)
127
+ .execute();
128
+ };
129
+
130
+ getCount = async (libraryId: string) => {
131
+ return (
132
+ (
133
+ await this.db
134
+ .selectFrom('DocumentBaseline')
135
+ .where('libraryId', '=', libraryId)
136
+ .select(({ fn }) => fn.countAll<number>().as('count'))
137
+ .executeTakeFirst()
138
+ )?.count ?? 0
139
+ );
140
+ };
141
+ }
@@ -0,0 +1,94 @@
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
+ }
@@ -0,0 +1,149 @@
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<void> => {
97
+ // inserts all operations and updates server order
98
+ // FIXME: this whole thing is kinda sus
99
+ await this.db.transaction().execute(async (tx): Promise<void> => {
100
+ let orderResult = await tx
101
+ .selectFrom('OperationHistory')
102
+ .select(({ fn, val }) =>
103
+ fn.coalesce(fn.max<number>('serverOrder'), val(0)).as('serverOrder'),
104
+ )
105
+ .where('libraryId', '=', libraryId)
106
+ .executeTakeFirst();
107
+ let currentServerOrder = orderResult?.serverOrder ?? 0;
108
+ for (const item of operations) {
109
+ await tx
110
+ .insertInto('OperationHistory')
111
+ .values({
112
+ libraryId,
113
+ oid: item.oid,
114
+ data: JSON.stringify(item.data),
115
+ timestamp: item.timestamp,
116
+ replicaId,
117
+ serverOrder: ++currentServerOrder,
118
+ })
119
+ .onConflict((cb) =>
120
+ cb
121
+ .columns(['libraryId', 'replicaId', 'oid', 'timestamp'])
122
+ .doNothing(),
123
+ )
124
+ .execute();
125
+ }
126
+ });
127
+ };
128
+
129
+ deleteAll = async (libraryId: string): Promise<void> => {
130
+ await this.db
131
+ .deleteFrom('OperationHistory')
132
+ .where('libraryId', '=', libraryId)
133
+ .execute();
134
+ };
135
+ delete = async (
136
+ libraryId: string,
137
+ operations: Operation[],
138
+ ): Promise<void> => {
139
+ await this.db
140
+ .deleteFrom('OperationHistory')
141
+ .where('libraryId', '=', libraryId)
142
+ .where(
143
+ 'oid',
144
+ 'in',
145
+ operations.map((o) => o.oid),
146
+ )
147
+ .execute();
148
+ };
149
+ }