@mauryasumit/driftdb 2.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.
- package/README.md +810 -0
- package/dist/db.d.ts +30 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +115 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/orm/model.d.ts +35 -0
- package/dist/orm/model.d.ts.map +1 -0
- package/dist/orm/model.js +34 -0
- package/dist/orm/model.js.map +1 -0
- package/dist/orm/query-builder.d.ts +8 -0
- package/dist/orm/query-builder.d.ts.map +1 -0
- package/dist/orm/query-builder.js +90 -0
- package/dist/orm/query-builder.js.map +1 -0
- package/dist/orm/repository.d.ts +38 -0
- package/dist/orm/repository.d.ts.map +1 -0
- package/dist/orm/repository.js +107 -0
- package/dist/orm/repository.js.map +1 -0
- package/dist/orm/schema.d.ts +20 -0
- package/dist/orm/schema.d.ts.map +1 -0
- package/dist/orm/schema.js +81 -0
- package/dist/orm/schema.js.map +1 -0
- package/dist/queue/queue.d.ts +17 -0
- package/dist/queue/queue.d.ts.map +1 -0
- package/dist/queue/queue.js +109 -0
- package/dist/queue/queue.js.map +1 -0
- package/dist/storage/s3-adapter.d.ts +21 -0
- package/dist/storage/s3-adapter.d.ts.map +1 -0
- package/dist/storage/s3-adapter.js +133 -0
- package/dist/storage/s3-adapter.js.map +1 -0
- package/dist/sync/change-log.d.ts +15 -0
- package/dist/sync/change-log.d.ts.map +1 -0
- package/dist/sync/change-log.js +78 -0
- package/dist/sync/change-log.js.map +1 -0
- package/dist/sync/engine.d.ts +31 -0
- package/dist/sync/engine.d.ts.map +1 -0
- package/dist/sync/engine.js +210 -0
- package/dist/sync/engine.js.map +1 -0
- package/dist/sync/snapshot-manager.d.ts +17 -0
- package/dist/sync/snapshot-manager.d.ts.map +1 -0
- package/dist/sync/snapshot-manager.js +91 -0
- package/dist/sync/snapshot-manager.js.map +1 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/compress.d.ts +3 -0
- package/dist/utils/compress.d.ts.map +1 -0
- package/dist/utils/compress.js +16 -0
- package/dist/utils/compress.js.map +1 -0
- package/dist/utils/crypto.d.ts +4 -0
- package/dist/utils/crypto.d.ts.map +1 -0
- package/dist/utils/crypto.js +35 -0
- package/dist/utils/crypto.js.map +1 -0
- package/dist/utils/id.d.ts +3 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +13 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/retry.d.ts +5 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +36 -0
- package/dist/utils/retry.js.map +1 -0
- package/package.json +55 -0
- package/src/db.ts +154 -0
- package/src/index.ts +24 -0
- package/src/orm/model.ts +95 -0
- package/src/orm/query-builder.ts +100 -0
- package/src/orm/repository.ts +156 -0
- package/src/orm/schema.ts +92 -0
- package/src/queue/queue.ts +138 -0
- package/src/storage/s3-adapter.ts +181 -0
- package/src/sync/change-log.ts +101 -0
- package/src/sync/engine.ts +249 -0
- package/src/sync/snapshot-manager.ts +80 -0
- package/src/types.ts +130 -0
- package/src/utils/compress.ts +14 -0
- package/src/utils/crypto.ts +33 -0
- package/src/utils/id.ts +10 -0
- package/src/utils/retry.ts +38 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { mkdirSync, copyFileSync, unlinkSync } from 'fs';
|
|
5
|
+
import type Database from 'better-sqlite3';
|
|
6
|
+
import type { S3Adapter } from '../storage/s3-adapter.js';
|
|
7
|
+
import type { S3UploadOptions } from '../storage/s3-adapter.js';
|
|
8
|
+
|
|
9
|
+
export class SnapshotManager {
|
|
10
|
+
private readonly db: Database.Database;
|
|
11
|
+
private readonly s3: S3Adapter;
|
|
12
|
+
private readonly nodeId: string;
|
|
13
|
+
private readonly sqlitePath: string;
|
|
14
|
+
private readonly uploadOptions: S3UploadOptions;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
db: Database.Database,
|
|
18
|
+
s3: S3Adapter,
|
|
19
|
+
nodeId: string,
|
|
20
|
+
sqlitePath: string,
|
|
21
|
+
uploadOptions: S3UploadOptions
|
|
22
|
+
) {
|
|
23
|
+
this.db = db;
|
|
24
|
+
this.s3 = s3;
|
|
25
|
+
this.nodeId = nodeId;
|
|
26
|
+
this.sqlitePath = sqlitePath;
|
|
27
|
+
this.uploadOptions = uploadOptions;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async takeAndUpload(): Promise<{ key: string; timestamp: number }> {
|
|
31
|
+
const timestamp = Date.now();
|
|
32
|
+
const tempPath = join(tmpdir(), `driftdb-snap-${this.nodeId}-${timestamp}.sqlite`);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
this.db.exec('PRAGMA wal_checkpoint(FULL)');
|
|
36
|
+
|
|
37
|
+
if (this.sqlitePath === ':memory:') {
|
|
38
|
+
const backup = this.db.serialize();
|
|
39
|
+
await this.s3.upload(
|
|
40
|
+
this.s3.snapshotKey(this.nodeId, timestamp),
|
|
41
|
+
Buffer.from(backup),
|
|
42
|
+
this.uploadOptions
|
|
43
|
+
);
|
|
44
|
+
} else {
|
|
45
|
+
copyFileSync(this.sqlitePath, tempPath);
|
|
46
|
+
const data = readFileSync(tempPath);
|
|
47
|
+
await this.s3.upload(
|
|
48
|
+
this.s3.snapshotKey(this.nodeId, timestamp),
|
|
49
|
+
data,
|
|
50
|
+
this.uploadOptions
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const key = this.s3.snapshotKey(this.nodeId, timestamp);
|
|
55
|
+
return { key, timestamp };
|
|
56
|
+
} finally {
|
|
57
|
+
if (existsSync(tempPath)) {
|
|
58
|
+
try { unlinkSync(tempPath); } catch { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async restoreLatest(): Promise<boolean> {
|
|
64
|
+
const manifest = await this.s3.getManifest(this.nodeId);
|
|
65
|
+
if (!manifest?.latestSnapshotKey) return false;
|
|
66
|
+
|
|
67
|
+
const data = await this.s3.download(manifest.latestSnapshotKey, this.uploadOptions);
|
|
68
|
+
|
|
69
|
+
const restorePath = this.sqlitePath !== ':memory:' ? this.sqlitePath : null;
|
|
70
|
+
if (!restorePath) return false;
|
|
71
|
+
|
|
72
|
+
const dir = dirname(restorePath);
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
|
|
75
|
+
const { writeFileSync } = await import('fs');
|
|
76
|
+
writeFileSync(restorePath, data);
|
|
77
|
+
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export type ColumnType = 'TEXT' | 'INTEGER' | 'REAL' | 'BLOB' | 'BOOLEAN';
|
|
2
|
+
|
|
3
|
+
export interface ColumnDef {
|
|
4
|
+
type: ColumnType;
|
|
5
|
+
notNull?: boolean;
|
|
6
|
+
unique?: boolean;
|
|
7
|
+
default?: string | number | boolean | null;
|
|
8
|
+
index?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ModelSchema = Record<string, ColumnDef>;
|
|
12
|
+
|
|
13
|
+
export interface BaseRecord {
|
|
14
|
+
id: string;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type WhereValue<V> =
|
|
20
|
+
| V
|
|
21
|
+
| { $gt?: V; $gte?: V; $lt?: V; $lte?: V; $in?: V[]; $like?: string; $ne?: V };
|
|
22
|
+
|
|
23
|
+
export type WhereClause<T> = {
|
|
24
|
+
[K in keyof T]?: WhereValue<T[K]>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface FindOptions<T> {
|
|
28
|
+
where?: WhereClause<T>;
|
|
29
|
+
orderBy?: Partial<Record<keyof T, 'ASC' | 'DESC'>>;
|
|
30
|
+
limit?: number;
|
|
31
|
+
offset?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface S3Config {
|
|
35
|
+
bucket: string;
|
|
36
|
+
region: string;
|
|
37
|
+
prefix?: string;
|
|
38
|
+
accessKeyId?: string;
|
|
39
|
+
secretAccessKey?: string;
|
|
40
|
+
endpoint?: string;
|
|
41
|
+
forcePathStyle?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RetryConfig {
|
|
45
|
+
maxRetries: number;
|
|
46
|
+
baseDelayMs: number;
|
|
47
|
+
maxDelayMs: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface EncryptionConfig {
|
|
51
|
+
key: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DBConfig {
|
|
55
|
+
sqlitePath: string;
|
|
56
|
+
s3Config?: S3Config;
|
|
57
|
+
nodeId?: string;
|
|
58
|
+
syncIntervalMs?: number;
|
|
59
|
+
snapshotEveryNLogs?: number;
|
|
60
|
+
maxBatchSize?: number;
|
|
61
|
+
compression?: boolean;
|
|
62
|
+
encryption?: EncryptionConfig;
|
|
63
|
+
retryConfig?: RetryConfig;
|
|
64
|
+
autoSync?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ChangeLogEntry {
|
|
68
|
+
sequence: number;
|
|
69
|
+
timestamp: number;
|
|
70
|
+
nodeId: string;
|
|
71
|
+
table: string;
|
|
72
|
+
operation: 'insert' | 'update' | 'delete';
|
|
73
|
+
data: string | null;
|
|
74
|
+
synced: 0 | 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface LogBatch {
|
|
78
|
+
version: 1;
|
|
79
|
+
nodeId: string;
|
|
80
|
+
fromSequence: number;
|
|
81
|
+
toSequence: number;
|
|
82
|
+
entries: Array<{
|
|
83
|
+
sequence: number;
|
|
84
|
+
timestamp: number;
|
|
85
|
+
table: string;
|
|
86
|
+
operation: 'insert' | 'update' | 'delete';
|
|
87
|
+
data: Record<string, unknown> | null;
|
|
88
|
+
}>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SyncJob {
|
|
92
|
+
id: string;
|
|
93
|
+
type: 'upload_log' | 'upload_snapshot';
|
|
94
|
+
payload: string;
|
|
95
|
+
status: 'pending' | 'processing' | 'done' | 'failed';
|
|
96
|
+
attempts: number;
|
|
97
|
+
nextRetryAt: number;
|
|
98
|
+
createdAt: number;
|
|
99
|
+
error: string | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface SyncManifest {
|
|
103
|
+
nodeId: string;
|
|
104
|
+
latestSnapshotKey: string | null;
|
|
105
|
+
latestSnapshotTimestamp: number | null;
|
|
106
|
+
latestLogSequence: number;
|
|
107
|
+
updatedAt: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface SyncMetrics {
|
|
111
|
+
lastSyncAt: number | null;
|
|
112
|
+
lastSnapshotAt: number | null;
|
|
113
|
+
pendingChanges: number;
|
|
114
|
+
dbSizeBytes: number;
|
|
115
|
+
totalSynced: number;
|
|
116
|
+
syncErrors: number;
|
|
117
|
+
isRunning: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface UploadLogPayload {
|
|
121
|
+
fromSequence: number;
|
|
122
|
+
toSequence: number;
|
|
123
|
+
s3Key: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface UploadSnapshotPayload {
|
|
127
|
+
timestamp: number;
|
|
128
|
+
s3Key: string;
|
|
129
|
+
dbPath: string;
|
|
130
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { gzip, gunzip, constants } from 'zlib';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
|
|
4
|
+
const gzipAsync = promisify(gzip);
|
|
5
|
+
const gunzipAsync = promisify(gunzip);
|
|
6
|
+
|
|
7
|
+
export async function compress(data: Buffer | string): Promise<Buffer> {
|
|
8
|
+
const buf = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
|
|
9
|
+
return gzipAsync(buf, { level: constants.Z_BEST_SPEED });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function decompress(data: Buffer): Promise<Buffer> {
|
|
13
|
+
return gunzipAsync(data);
|
|
14
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
|
|
2
|
+
|
|
3
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
4
|
+
const IV_LENGTH = 12;
|
|
5
|
+
const TAG_LENGTH = 16;
|
|
6
|
+
const KEY_LENGTH = 32;
|
|
7
|
+
|
|
8
|
+
export function normalizeKey(hexKey: string): Buffer {
|
|
9
|
+
const buf = Buffer.from(hexKey, 'hex');
|
|
10
|
+
if (buf.length !== KEY_LENGTH) {
|
|
11
|
+
throw new Error(`Encryption key must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes)`);
|
|
12
|
+
}
|
|
13
|
+
return buf;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function encrypt(data: Buffer, hexKey: string): Buffer {
|
|
17
|
+
const key = normalizeKey(hexKey);
|
|
18
|
+
const iv = randomBytes(IV_LENGTH);
|
|
19
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
20
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
21
|
+
const tag = cipher.getAuthTag();
|
|
22
|
+
return Buffer.concat([iv, tag, encrypted]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function decrypt(data: Buffer, hexKey: string): Buffer {
|
|
26
|
+
const key = normalizeKey(hexKey);
|
|
27
|
+
const iv = data.subarray(0, IV_LENGTH);
|
|
28
|
+
const tag = data.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
29
|
+
const encrypted = data.subarray(IV_LENGTH + TAG_LENGTH);
|
|
30
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
31
|
+
decipher.setAuthTag(tag);
|
|
32
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
33
|
+
}
|
package/src/utils/id.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RetryConfig } from '../types.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
4
|
+
maxRetries: 5,
|
|
5
|
+
baseDelayMs: 500,
|
|
6
|
+
maxDelayMs: 30_000,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export async function withRetry<T>(
|
|
10
|
+
fn: () => Promise<T>,
|
|
11
|
+
config: Partial<RetryConfig> = {}
|
|
12
|
+
): Promise<T> {
|
|
13
|
+
const cfg = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
14
|
+
let lastError: Error | undefined;
|
|
15
|
+
|
|
16
|
+
for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
|
|
17
|
+
try {
|
|
18
|
+
return await fn();
|
|
19
|
+
} catch (err) {
|
|
20
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
21
|
+
if (attempt === cfg.maxRetries) break;
|
|
22
|
+
const delay = Math.min(cfg.baseDelayMs * Math.pow(2, attempt), cfg.maxDelayMs);
|
|
23
|
+
const jitter = Math.random() * delay * 0.2;
|
|
24
|
+
await sleep(delay + jitter);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw lastError ?? new Error('Retry failed');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function sleep(ms: number): Promise<void> {
|
|
32
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function nextRetryAt(attempt: number, config: RetryConfig = DEFAULT_RETRY_CONFIG): number {
|
|
36
|
+
const delay = Math.min(config.baseDelayMs * Math.pow(2, attempt), config.maxDelayMs);
|
|
37
|
+
return Date.now() + delay;
|
|
38
|
+
}
|