@verdant-web/server 3.0.4 → 3.1.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.
- package/LICENSE +650 -21
- package/dist/esm/ServerLibrary.test.js +10 -0
- package/dist/esm/ServerLibrary.test.js.map +1 -1
- package/dist/esm/storage/index.d.ts +1 -0
- package/dist/esm/storage/index.js +1 -0
- package/dist/esm/storage/index.js.map +1 -1
- package/dist/esm/storage/sql/database.d.ts +9 -0
- package/dist/esm/storage/sql/database.js +24 -0
- package/dist/esm/storage/sql/database.js.map +1 -0
- package/dist/esm/storage/sql/sqlStorage.d.ts +2 -1
- package/dist/esm/storage/sql/sqlStorage.js +5 -13
- package/dist/esm/storage/sql/sqlStorage.js.map +1 -1
- package/dist/esm/storage/sql/tables.d.ts +2 -0
- package/dist/esm/storage/sqlShard/Databases.d.ts +21 -0
- package/dist/esm/storage/sqlShard/Databases.js +64 -0
- package/dist/esm/storage/sqlShard/Databases.js.map +1 -0
- package/dist/esm/storage/sqlShard/SqlBaselines.d.ts +23 -0
- package/dist/esm/storage/sqlShard/SqlBaselines.js +98 -0
- package/dist/esm/storage/sqlShard/SqlBaselines.js.map +1 -0
- package/dist/esm/storage/sqlShard/SqlFileMetadata.d.ts +18 -0
- package/dist/esm/storage/sqlShard/SqlFileMetadata.js +70 -0
- package/dist/esm/storage/sqlShard/SqlFileMetadata.js.map +1 -0
- package/dist/esm/storage/sqlShard/SqlOperations.d.ts +18 -0
- package/dist/esm/storage/sqlShard/SqlOperations.js +104 -0
- package/dist/esm/storage/sqlShard/SqlOperations.js.map +1 -0
- package/dist/esm/storage/sqlShard/SqlReplicas.d.ts +32 -0
- package/dist/esm/storage/sqlShard/SqlReplicas.js +164 -0
- package/dist/esm/storage/sqlShard/SqlReplicas.js.map +1 -0
- package/dist/esm/storage/sqlShard/_testData/unifiedData.d.ts +30 -0
- package/dist/esm/storage/sqlShard/_testData/unifiedData.js +42 -0
- package/dist/esm/storage/sqlShard/_testData/unifiedData.js.map +1 -0
- package/dist/esm/storage/sqlShard/database.d.ts +6 -0
- package/dist/esm/storage/sqlShard/database.js +28 -0
- package/dist/esm/storage/sqlShard/database.js.map +1 -0
- package/dist/esm/storage/sqlShard/migrations/v1.d.ts +3 -0
- package/dist/esm/storage/sqlShard/migrations/v1.js +48 -0
- package/dist/esm/storage/sqlShard/migrations/v1.js.map +1 -0
- package/dist/esm/storage/sqlShard/migrations.d.ts +5 -0
- package/dist/esm/storage/sqlShard/migrations.js +3 -0
- package/dist/esm/storage/sqlShard/migrations.js.map +1 -0
- package/dist/esm/storage/sqlShard/sqlShardStorage.d.ts +7 -0
- package/dist/esm/storage/sqlShard/sqlShardStorage.js +55 -0
- package/dist/esm/storage/sqlShard/sqlShardStorage.js.map +1 -0
- package/dist/esm/storage/sqlShard/tables.d.ts +38 -0
- package/dist/esm/storage/sqlShard/tables.js +2 -0
- package/dist/esm/storage/sqlShard/tables.js.map +1 -0
- package/dist/esm/storage/sqlShard/transfer.d.ts +6 -0
- package/dist/esm/storage/sqlShard/transfer.js +76 -0
- package/dist/esm/storage/sqlShard/transfer.js.map +1 -0
- package/dist/esm/storage/sqlShard/transfer.test.d.ts +1 -0
- package/dist/esm/storage/sqlShard/transfer.test.js +94 -0
- package/dist/esm/storage/sqlShard/transfer.test.js.map +1 -0
- package/package.json +3 -2
- package/src/ServerLibrary.test.ts +10 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/sql/database.ts +32 -0
- package/src/storage/sql/sqlStorage.ts +6 -8
- package/src/storage/sql/tables.ts +4 -0
- package/src/storage/sqlShard/Databases.ts +80 -0
- package/src/storage/sqlShard/SqlBaselines.ts +140 -0
- package/src/storage/sqlShard/SqlFileMetadata.ts +98 -0
- package/src/storage/sqlShard/SqlOperations.ts +146 -0
- package/src/storage/sqlShard/SqlReplicas.ts +237 -0
- package/src/storage/sqlShard/_testData/unifiedData.ts +52 -0
- package/src/storage/sqlShard/database.ts +37 -0
- package/src/storage/sqlShard/migrations/v1.ts +53 -0
- package/src/storage/sqlShard/migrations.ts +3 -0
- package/src/storage/sqlShard/sqlShardStorage.ts +81 -0
- package/src/storage/sqlShard/tables.ts +45 -0
- package/src/storage/sqlShard/transfer.test.ts +130 -0
- package/src/storage/sqlShard/transfer.ts +107 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { openDatabase as openUnifiedDatabase } from '../sql/database.js';
|
|
3
|
+
import { randomBaselines, randomFileMetadata, randomOperations, randomReplicaInfo, } from './_testData/unifiedData.js';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { transferToShards } from './transfer.js';
|
|
6
|
+
import { rm, mkdir } from 'fs/promises';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
const testTempDir = join(dirname(fileURLToPath(import.meta.url)), '_testData', 'tmp');
|
|
9
|
+
describe('sql-shard storage transfer utility', () => {
|
|
10
|
+
const databases = new Array();
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
try {
|
|
13
|
+
await rm(testTempDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
catch (err) { }
|
|
16
|
+
// create a temp dir
|
|
17
|
+
await mkdir(testTempDir, { recursive: true });
|
|
18
|
+
});
|
|
19
|
+
afterAll(async () => {
|
|
20
|
+
await Promise.all(databases.map((db) => db.destroy()));
|
|
21
|
+
// empty the temp dir
|
|
22
|
+
await rm(testTempDir, { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
it('should transfer data from unified database to shards', async () => {
|
|
25
|
+
const unifiedFile = join(testTempDir, 'unified.sqlite');
|
|
26
|
+
const { db: unifiedDb, ready } = openUnifiedDatabase(unifiedFile);
|
|
27
|
+
databases.push(unifiedDb);
|
|
28
|
+
await ready;
|
|
29
|
+
const libraryIds = ['library-1', 'library-2', 'library-3'];
|
|
30
|
+
const randomData = {};
|
|
31
|
+
for (const libraryId of libraryIds) {
|
|
32
|
+
randomData[libraryId] = {
|
|
33
|
+
operations: randomOperations(libraryId),
|
|
34
|
+
baselines: randomBaselines(libraryId),
|
|
35
|
+
replicas: randomReplicaInfo(libraryId),
|
|
36
|
+
fileMetadata: randomFileMetadata(libraryId),
|
|
37
|
+
};
|
|
38
|
+
await unifiedDb
|
|
39
|
+
.insertInto('ReplicaInfo')
|
|
40
|
+
.values(randomData[libraryId].replicas)
|
|
41
|
+
.execute();
|
|
42
|
+
await unifiedDb
|
|
43
|
+
.insertInto('OperationHistory')
|
|
44
|
+
.values(randomData[libraryId].operations)
|
|
45
|
+
.execute();
|
|
46
|
+
await unifiedDb
|
|
47
|
+
.insertInto('DocumentBaseline')
|
|
48
|
+
.values(randomData[libraryId].baselines)
|
|
49
|
+
.execute();
|
|
50
|
+
await unifiedDb
|
|
51
|
+
.insertInto('FileMetadata')
|
|
52
|
+
.values(randomData[libraryId].fileMetadata)
|
|
53
|
+
.execute();
|
|
54
|
+
}
|
|
55
|
+
const shards = await transferToShards({
|
|
56
|
+
directory: testTempDir,
|
|
57
|
+
file: unifiedFile,
|
|
58
|
+
});
|
|
59
|
+
Object.values(shards).map((db) => databases.push(db));
|
|
60
|
+
expect(Object.keys(shards).sort()).toEqual(libraryIds.sort());
|
|
61
|
+
for (const [libraryId, db] of Object.entries(shards)) {
|
|
62
|
+
const operations = await db
|
|
63
|
+
.selectFrom('OperationHistory')
|
|
64
|
+
.selectAll()
|
|
65
|
+
.execute();
|
|
66
|
+
const baselines = await db
|
|
67
|
+
.selectFrom('DocumentBaseline')
|
|
68
|
+
.selectAll()
|
|
69
|
+
.execute();
|
|
70
|
+
const replicas = await db.selectFrom('ReplicaInfo').selectAll().execute();
|
|
71
|
+
const fileMetadata = await db
|
|
72
|
+
.selectFrom('FileMetadata')
|
|
73
|
+
.selectAll()
|
|
74
|
+
.execute();
|
|
75
|
+
expect(operations.length).toBe(randomData[libraryId].operations.length);
|
|
76
|
+
expect(baselines.length).toBe(randomData[libraryId].baselines.length);
|
|
77
|
+
expect(replicas.length).toBe(randomData[libraryId].replicas.length);
|
|
78
|
+
expect(fileMetadata.length).toBe(randomData[libraryId].fileMetadata.length);
|
|
79
|
+
for (const { oid } of randomData[libraryId].operations) {
|
|
80
|
+
expect(operations.find((op) => op.oid === oid)).toBeDefined();
|
|
81
|
+
}
|
|
82
|
+
for (const { oid } of randomData[libraryId].baselines) {
|
|
83
|
+
expect(baselines.find((baseline) => baseline.oid === oid)).toBeDefined();
|
|
84
|
+
}
|
|
85
|
+
for (const { id } of randomData[libraryId].replicas) {
|
|
86
|
+
expect(replicas.find((replica) => replica.id === id)).toBeDefined();
|
|
87
|
+
}
|
|
88
|
+
for (const { fileId } of randomData[libraryId].fileMetadata) {
|
|
89
|
+
expect(fileMetadata.find((file) => file.fileId === fileId)).toBeDefined();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
//# sourceMappingURL=transfer.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transfer.test.js","sourceRoot":"","sources":["../../../../src/storage/sqlShard/transfer.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verdant-web/server",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"access": "public",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"kysely-plugin-serialize": "^0.5.7",
|
|
34
34
|
"minimist": "^1.2.8",
|
|
35
35
|
"ws": "^8.9.0",
|
|
36
|
-
"@verdant-web/common": "2.3.
|
|
36
|
+
"@verdant-web/common": "2.3.4"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/async-lock": "^1.3.0",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"prepublish": "pnpm run build",
|
|
52
52
|
"link": "pnpm link --global",
|
|
53
53
|
"test": "vitest",
|
|
54
|
+
"ci:test:unit": "vitest run",
|
|
54
55
|
"perf": "pnpm run build && cross-env VERDANT_LOG=false node ./perf/perf.mjs"
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -143,6 +143,16 @@ describe('ServerLibrary', () => {
|
|
|
143
143
|
'library-1',
|
|
144
144
|
{
|
|
145
145
|
type: 'op-re',
|
|
146
|
+
baselines: [
|
|
147
|
+
{
|
|
148
|
+
oid: 'notes/1',
|
|
149
|
+
timestamp: `faketime-0`,
|
|
150
|
+
snapshot: {
|
|
151
|
+
title: 'My Note',
|
|
152
|
+
content: 'This is my note.',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
],
|
|
146
156
|
operations: [
|
|
147
157
|
{
|
|
148
158
|
oid: 'notes/1',
|
package/src/storage/index.ts
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
}
|
|
@@ -8,23 +8,21 @@ import { Database as DatabaseTypes } from './tables.js';
|
|
|
8
8
|
import { SqlFileMetadata } from './SqlFileMetadata.js';
|
|
9
9
|
import { migrateToLatest } from '@a-type/kysely';
|
|
10
10
|
import migrations from './migrations.js';
|
|
11
|
+
import { openDatabase } from './database.js';
|
|
11
12
|
|
|
12
13
|
export const sqlStorage = ({
|
|
13
14
|
databaseFile: dbFile,
|
|
14
15
|
skipMigrations,
|
|
16
|
+
disableWal,
|
|
15
17
|
}: {
|
|
16
18
|
databaseFile: string;
|
|
17
19
|
skipMigrations?: boolean;
|
|
20
|
+
disableWal?: boolean;
|
|
18
21
|
}): StorageFactory => {
|
|
19
|
-
const db =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}),
|
|
22
|
+
const { db, ready } = openDatabase(dbFile, {
|
|
23
|
+
skipMigrations,
|
|
24
|
+
disableWal,
|
|
23
25
|
});
|
|
24
|
-
let ready = Promise.resolve<void>(undefined);
|
|
25
|
-
if (!skipMigrations) {
|
|
26
|
-
ready = migrateToLatest(db, migrations);
|
|
27
|
-
}
|
|
28
26
|
return (options) => {
|
|
29
27
|
const baselines = new SqlBaselines(db, 'sqlite');
|
|
30
28
|
const operations = new SqlOperations(db, 'sqlite');
|
|
@@ -36,6 +36,8 @@ export interface ReplicaInfoTable {
|
|
|
36
36
|
ackedServerOrder: Generated<number>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export type ReplicaInfoRow = Selectable<ReplicaInfoTable>;
|
|
40
|
+
|
|
39
41
|
export interface FileMetadataTable {
|
|
40
42
|
libraryId: string;
|
|
41
43
|
fileId: string;
|
|
@@ -43,3 +45,5 @@ export interface FileMetadataTable {
|
|
|
43
45
|
type: string;
|
|
44
46
|
pendingDeleteAt: number | null;
|
|
45
47
|
}
|
|
48
|
+
|
|
49
|
+
export type FileMetadataRow = Selectable<FileMetadataTable>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Kysely } from 'kysely';
|
|
2
|
+
import { openDatabase } from './database.js';
|
|
3
|
+
import { Database as DatabaseTypes } from './tables.js';
|
|
4
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
5
|
+
import { assert } from '@verdant-web/common';
|
|
6
|
+
|
|
7
|
+
export class Databases {
|
|
8
|
+
private openPromises: Record<string, Promise<Kysely<DatabaseTypes>>> = {};
|
|
9
|
+
private cache: Record<string, Kysely<DatabaseTypes>> = {};
|
|
10
|
+
private closeTimeouts: Record<string, NodeJS.Timeout> = {};
|
|
11
|
+
private openedPreviously: Record<string, boolean> = {};
|
|
12
|
+
private directory;
|
|
13
|
+
private closeTimeout;
|
|
14
|
+
private disableWal = false;
|
|
15
|
+
private closed = new WeakMap<Kysely<DatabaseTypes>, boolean>();
|
|
16
|
+
|
|
17
|
+
constructor(config: {
|
|
18
|
+
directory: string;
|
|
19
|
+
closeTimeout?: number;
|
|
20
|
+
disableWal?: boolean;
|
|
21
|
+
}) {
|
|
22
|
+
this.directory = config.directory;
|
|
23
|
+
this.closeTimeout = config.closeTimeout ?? 1000 * 60 * 60;
|
|
24
|
+
this.disableWal = config.disableWal ?? false;
|
|
25
|
+
if (this.directory !== ':memory:' && !existsSync(this.directory)) {
|
|
26
|
+
mkdirSync(this.directory, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get = async (libraryId: string): Promise<Kysely<DatabaseTypes>> => {
|
|
31
|
+
let db = this.cache[libraryId];
|
|
32
|
+
if (db) {
|
|
33
|
+
if (!this.closed.get(db)) {
|
|
34
|
+
this.enqueueClose(libraryId);
|
|
35
|
+
return this.cache[libraryId];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
let openPromise = this.openPromises[libraryId];
|
|
39
|
+
if (!openPromise) {
|
|
40
|
+
this.openPromises[libraryId] = openDatabase(this.directory, libraryId, {
|
|
41
|
+
skipMigrations: this.openedPreviously[libraryId],
|
|
42
|
+
disableWal: this.disableWal,
|
|
43
|
+
});
|
|
44
|
+
openPromise = this.openPromises[libraryId];
|
|
45
|
+
}
|
|
46
|
+
db = await openPromise;
|
|
47
|
+
this.openedPreviously[libraryId] = true;
|
|
48
|
+
this.cache[libraryId] = db;
|
|
49
|
+
delete this.openPromises[libraryId];
|
|
50
|
+
this.enqueueClose(libraryId);
|
|
51
|
+
assert(db, `Database was not loaded: ${libraryId}`);
|
|
52
|
+
return db;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
private enqueueClose = (libraryId: string) => {
|
|
56
|
+
clearTimeout(this.closeTimeouts[libraryId]);
|
|
57
|
+
this.closeTimeouts[libraryId] = setTimeout(
|
|
58
|
+
this.close,
|
|
59
|
+
this.closeTimeout,
|
|
60
|
+
libraryId,
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
close = (libraryId: string) => {
|
|
65
|
+
clearTimeout(this.closeTimeouts[libraryId]);
|
|
66
|
+
delete this.openPromises[libraryId];
|
|
67
|
+
const db = this.cache[libraryId];
|
|
68
|
+
if (db) {
|
|
69
|
+
this.closed.set(db, true);
|
|
70
|
+
delete this.cache[libraryId];
|
|
71
|
+
db.destroy();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
destroy = async () => {
|
|
76
|
+
for (const libraryId in this.cache) {
|
|
77
|
+
this.close(libraryId);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
import { Databases } from './Databases.js';
|
|
7
|
+
|
|
8
|
+
export class SqlBaselines implements BaselineStorage {
|
|
9
|
+
constructor(
|
|
10
|
+
private dbs: Databases,
|
|
11
|
+
private dialect: 'postgres' | 'sqlite',
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
get = async (
|
|
15
|
+
libraryId: string,
|
|
16
|
+
oid: string,
|
|
17
|
+
{ tx }: { tx?: Kysely<Database> } = {},
|
|
18
|
+
): Promise<HydratedDocumentBaseline | null> => {
|
|
19
|
+
const db = tx ?? (await this.dbs.get(libraryId));
|
|
20
|
+
const raw =
|
|
21
|
+
(await db
|
|
22
|
+
.selectFrom('DocumentBaseline')
|
|
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 db = await this.dbs.get(libraryId);
|
|
33
|
+
const raw = await db
|
|
34
|
+
.selectFrom('DocumentBaseline')
|
|
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 ?? (await this.dbs.get(libraryId));
|
|
54
|
+
if (!baseline.snapshot) {
|
|
55
|
+
await db
|
|
56
|
+
.deleteFrom('DocumentBaseline')
|
|
57
|
+
.where('oid', '=', baseline.oid)
|
|
58
|
+
.execute();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await db
|
|
63
|
+
.insertInto('DocumentBaseline')
|
|
64
|
+
.values({
|
|
65
|
+
oid: baseline.oid,
|
|
66
|
+
snapshot: JSON.stringify(baseline.snapshot),
|
|
67
|
+
timestamp: baseline.timestamp,
|
|
68
|
+
})
|
|
69
|
+
.onConflict((cb) =>
|
|
70
|
+
cb.doUpdateSet({
|
|
71
|
+
snapshot: JSON.stringify(baseline.snapshot),
|
|
72
|
+
timestamp: baseline.timestamp,
|
|
73
|
+
}),
|
|
74
|
+
)
|
|
75
|
+
.execute();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
insertAll = async (libraryId: string, baselines: DocumentBaseline[]) => {
|
|
79
|
+
const db = await this.dbs.get(libraryId);
|
|
80
|
+
await db.transaction().execute(async (tx) => {
|
|
81
|
+
for (const baseline of baselines) {
|
|
82
|
+
await this.set(libraryId, baseline, { tx });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
private hydrate = (row: DocumentBaselineRow) => {
|
|
88
|
+
// avoiding extra allocation from .map by type-asserting here
|
|
89
|
+
row.snapshot = JSON.parse(row.snapshot);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
applyOperations = async (
|
|
93
|
+
libraryId: string,
|
|
94
|
+
oid: string,
|
|
95
|
+
operations: StoredOperation[],
|
|
96
|
+
deletedRefs?: Ref[],
|
|
97
|
+
) => {
|
|
98
|
+
if (operations.length === 0) return;
|
|
99
|
+
|
|
100
|
+
const db = await this.dbs.get(libraryId);
|
|
101
|
+
|
|
102
|
+
await db.transaction().execute(async (tx) => {
|
|
103
|
+
let baseline = await this.get(libraryId, oid, { tx });
|
|
104
|
+
if (!baseline) {
|
|
105
|
+
baseline = {
|
|
106
|
+
oid,
|
|
107
|
+
snapshot: {},
|
|
108
|
+
timestamp: operations[0].timestamp,
|
|
109
|
+
libraryId,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
for (const operation of operations) {
|
|
113
|
+
baseline.snapshot = applyPatch(
|
|
114
|
+
baseline.snapshot,
|
|
115
|
+
operation.data,
|
|
116
|
+
deletedRefs,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
baseline.timestamp = operations[operations.length - 1].timestamp;
|
|
120
|
+
await this.set(libraryId, baseline, { tx });
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
deleteAll = async (libraryId: string) => {
|
|
125
|
+
const db = await this.dbs.get(libraryId);
|
|
126
|
+
await db.deleteFrom('DocumentBaseline').execute();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
getCount = async (libraryId: string) => {
|
|
130
|
+
const db = await this.dbs.get(libraryId);
|
|
131
|
+
return (
|
|
132
|
+
(
|
|
133
|
+
await db
|
|
134
|
+
.selectFrom('DocumentBaseline')
|
|
135
|
+
.select(({ fn }) => fn.countAll<number>().as('count'))
|
|
136
|
+
.executeTakeFirst()
|
|
137
|
+
)?.count ?? 0
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Kysely } from 'kysely';
|
|
2
|
+
import { FileMetadataStorage } from '../Storage.js';
|
|
3
|
+
import { Database, FileMetadataRow } from './tables.js';
|
|
4
|
+
import { FileMetadata } from '../../types.js';
|
|
5
|
+
import { FileInfo } from '../../files/FileStorage.js';
|
|
6
|
+
import { Databases } from './Databases.js';
|
|
7
|
+
|
|
8
|
+
export class SqlFileMetadata implements FileMetadataStorage {
|
|
9
|
+
constructor(
|
|
10
|
+
private dbs: Databases,
|
|
11
|
+
private deleteExpirationDays: number,
|
|
12
|
+
private dialect: 'postgres' | 'sqlite',
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
private attachLibrary = (libraryId: string, row: FileMetadataRow) => {
|
|
16
|
+
if (!row) return row;
|
|
17
|
+
(row as any).libraryId = libraryId;
|
|
18
|
+
return row as FileMetadata;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
get = async (
|
|
22
|
+
libraryId: string,
|
|
23
|
+
fileId: string,
|
|
24
|
+
): Promise<FileMetadata | null> => {
|
|
25
|
+
const db = await this.dbs.get(libraryId);
|
|
26
|
+
const row =
|
|
27
|
+
(await db
|
|
28
|
+
.selectFrom('FileMetadata')
|
|
29
|
+
.where('fileId', '=', fileId)
|
|
30
|
+
.selectAll()
|
|
31
|
+
.executeTakeFirst()) ?? null;
|
|
32
|
+
if (row) return this.attachLibrary(libraryId, row);
|
|
33
|
+
return row;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
getAll = async (libraryId: string): Promise<FileMetadata[]> => {
|
|
37
|
+
const db = await this.dbs.get(libraryId);
|
|
38
|
+
return (await db.selectFrom('FileMetadata').selectAll().execute()).map(
|
|
39
|
+
this.attachLibrary.bind(this, libraryId),
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
deleteAll = async (libraryId: string): Promise<void> => {
|
|
44
|
+
const db = await this.dbs.get(libraryId);
|
|
45
|
+
await db.deleteFrom('FileMetadata').execute();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
put = async (libraryId: string, fileInfo: FileInfo): Promise<void> => {
|
|
49
|
+
const db = await this.dbs.get(libraryId);
|
|
50
|
+
await db
|
|
51
|
+
.insertInto('FileMetadata')
|
|
52
|
+
.values({
|
|
53
|
+
fileId: fileInfo.id,
|
|
54
|
+
name: fileInfo.fileName,
|
|
55
|
+
type: fileInfo.type,
|
|
56
|
+
})
|
|
57
|
+
.onConflict((cb) =>
|
|
58
|
+
cb.column('fileId').doUpdateSet({
|
|
59
|
+
name: fileInfo.fileName,
|
|
60
|
+
type: fileInfo.type,
|
|
61
|
+
pendingDeleteAt: null,
|
|
62
|
+
}),
|
|
63
|
+
)
|
|
64
|
+
.execute();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
markPendingDelete = async (
|
|
68
|
+
libraryId: string,
|
|
69
|
+
fileId: string,
|
|
70
|
+
): Promise<void> => {
|
|
71
|
+
const db = await this.dbs.get(libraryId);
|
|
72
|
+
await db
|
|
73
|
+
.updateTable('FileMetadata')
|
|
74
|
+
.set({ pendingDeleteAt: Date.now() })
|
|
75
|
+
.where('fileId', '=', fileId)
|
|
76
|
+
.execute();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
delete = async (libraryId: string, fileId: string): Promise<void> => {
|
|
80
|
+
const db = await this.dbs.get(libraryId);
|
|
81
|
+
await db.deleteFrom('FileMetadata').where('fileId', '=', fileId).execute();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
getPendingDelete = async (libraryId: string): Promise<FileMetadata[]> => {
|
|
85
|
+
const db = await this.dbs.get(libraryId);
|
|
86
|
+
return (
|
|
87
|
+
await db
|
|
88
|
+
.selectFrom('FileMetadata')
|
|
89
|
+
.where(
|
|
90
|
+
'pendingDeleteAt',
|
|
91
|
+
'<',
|
|
92
|
+
Date.now() - 1000 * 60 * 60 * 24 * this.deleteExpirationDays,
|
|
93
|
+
)
|
|
94
|
+
.selectAll()
|
|
95
|
+
.execute()
|
|
96
|
+
).map(this.attachLibrary.bind(this, libraryId));
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
import { Databases } from './Databases.js';
|
|
7
|
+
|
|
8
|
+
export class SqlOperations implements OperationStorage {
|
|
9
|
+
constructor(
|
|
10
|
+
private dbs: Databases,
|
|
11
|
+
private dialect: 'postgres' | 'sqlite',
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
private hydrate = (row: OperationHistoryRow) => {
|
|
15
|
+
// avoiding extra allocation from .map by type-asserting here
|
|
16
|
+
row.data = JSON.parse(row.data);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
getAll = async (
|
|
20
|
+
libraryId: string,
|
|
21
|
+
oid: string,
|
|
22
|
+
): Promise<StoredOperation[]> => {
|
|
23
|
+
const db = await this.dbs.get(libraryId);
|
|
24
|
+
const raw = await db
|
|
25
|
+
.selectFrom('OperationHistory')
|
|
26
|
+
.where('oid', '=', oid)
|
|
27
|
+
.orderBy('timestamp', 'asc')
|
|
28
|
+
.selectAll()
|
|
29
|
+
.execute();
|
|
30
|
+
|
|
31
|
+
raw.forEach(this.hydrate);
|
|
32
|
+
return raw as unknown as StoredOperation[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
getBeforeServerOrder = async (
|
|
36
|
+
libraryId: string,
|
|
37
|
+
beforeServerOrder: number,
|
|
38
|
+
): Promise<StoredOperation[]> => {
|
|
39
|
+
const db = await this.dbs.get(libraryId);
|
|
40
|
+
const raw = await db
|
|
41
|
+
.selectFrom('OperationHistory')
|
|
42
|
+
.where('serverOrder', '<', beforeServerOrder)
|
|
43
|
+
.orderBy('timestamp', 'asc')
|
|
44
|
+
.selectAll()
|
|
45
|
+
.execute();
|
|
46
|
+
|
|
47
|
+
raw.forEach(this.hydrate);
|
|
48
|
+
return raw as unknown as StoredOperation[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
getAfterServerOrder = async (
|
|
52
|
+
libraryId: string,
|
|
53
|
+
afterServerOrder: number,
|
|
54
|
+
): Promise<StoredOperation[]> => {
|
|
55
|
+
const db = await this.dbs.get(libraryId);
|
|
56
|
+
const raw = await db
|
|
57
|
+
.selectFrom('OperationHistory')
|
|
58
|
+
.where('serverOrder', '>', afterServerOrder)
|
|
59
|
+
.orderBy('timestamp', 'asc')
|
|
60
|
+
.selectAll()
|
|
61
|
+
.execute();
|
|
62
|
+
|
|
63
|
+
raw.forEach(this.hydrate);
|
|
64
|
+
return raw as unknown as StoredOperation[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
getLatestServerOrder = async (libraryId: string): Promise<number> => {
|
|
68
|
+
const db = await this.dbs.get(libraryId);
|
|
69
|
+
return (
|
|
70
|
+
(
|
|
71
|
+
await db
|
|
72
|
+
.selectFrom('OperationHistory')
|
|
73
|
+
.orderBy('serverOrder', 'desc')
|
|
74
|
+
.select('serverOrder')
|
|
75
|
+
.limit(1)
|
|
76
|
+
.executeTakeFirst()
|
|
77
|
+
)?.serverOrder ?? 0
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
getCount = async (libraryId: string): Promise<number> => {
|
|
82
|
+
const db = await this.dbs.get(libraryId);
|
|
83
|
+
return (
|
|
84
|
+
(
|
|
85
|
+
await db
|
|
86
|
+
.selectFrom('OperationHistory')
|
|
87
|
+
.select(({ fn }) => fn.countAll<number>().as('count'))
|
|
88
|
+
.executeTakeFirst()
|
|
89
|
+
)?.count ?? 0
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
insertAll = async (
|
|
94
|
+
libraryId: string,
|
|
95
|
+
replicaId: string,
|
|
96
|
+
operations: Operation[],
|
|
97
|
+
): Promise<void> => {
|
|
98
|
+
const db = await this.dbs.get(libraryId);
|
|
99
|
+
// inserts all operations and updates server order
|
|
100
|
+
// FIXME: this whole thing is kinda sus
|
|
101
|
+
await db.transaction().execute(async (tx): Promise<void> => {
|
|
102
|
+
let orderResult = await tx
|
|
103
|
+
.selectFrom('OperationHistory')
|
|
104
|
+
.select(({ fn, val }) =>
|
|
105
|
+
fn.coalesce(fn.max<number>('serverOrder'), val(0)).as('serverOrder'),
|
|
106
|
+
)
|
|
107
|
+
.executeTakeFirst();
|
|
108
|
+
let currentServerOrder = orderResult?.serverOrder ?? 0;
|
|
109
|
+
for (const item of operations) {
|
|
110
|
+
await tx
|
|
111
|
+
.insertInto('OperationHistory')
|
|
112
|
+
.values({
|
|
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.columns(['replicaId', 'oid', 'timestamp']).doNothing(),
|
|
121
|
+
)
|
|
122
|
+
.execute();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
deleteAll = async (libraryId: string): Promise<void> => {
|
|
128
|
+
const db = await this.dbs.get(libraryId);
|
|
129
|
+
await db.deleteFrom('OperationHistory').execute();
|
|
130
|
+
};
|
|
131
|
+
delete = async (
|
|
132
|
+
libraryId: string,
|
|
133
|
+
operations: Operation[],
|
|
134
|
+
): Promise<void> => {
|
|
135
|
+
const db = await this.dbs.get(libraryId);
|
|
136
|
+
await db.transaction().execute(async (tx) => {
|
|
137
|
+
for (const item of operations) {
|
|
138
|
+
await tx
|
|
139
|
+
.deleteFrom('OperationHistory')
|
|
140
|
+
.where('oid', '=', item.oid)
|
|
141
|
+
.where('timestamp', '=', item.timestamp)
|
|
142
|
+
.execute();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
}
|