@quereus/sync-coordinator 0.6.0 → 0.8.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 +34 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/service/coordinator-service.d.ts +28 -2
- package/dist/src/service/coordinator-service.d.ts.map +1 -1
- package/dist/src/service/coordinator-service.js +87 -9
- package/dist/src/service/coordinator-service.js.map +1 -1
- package/dist/src/service/index.d.ts +7 -0
- package/dist/src/service/index.d.ts.map +1 -1
- package/dist/src/service/index.js +7 -0
- package/dist/src/service/index.js.map +1 -1
- package/dist/src/service/s3-batch-store.d.ts +63 -0
- package/dist/src/service/s3-batch-store.d.ts.map +1 -0
- package/dist/src/service/s3-batch-store.js +89 -0
- package/dist/src/service/s3-batch-store.js.map +1 -0
- package/dist/src/service/s3-config.d.ts +81 -0
- package/dist/src/service/s3-config.d.ts.map +1 -0
- package/dist/src/service/s3-config.js +94 -0
- package/dist/src/service/s3-config.js.map +1 -0
- package/dist/src/service/s3-snapshot-store.d.ts +121 -0
- package/dist/src/service/s3-snapshot-store.d.ts.map +1 -0
- package/dist/src/service/s3-snapshot-store.js +236 -0
- package/dist/src/service/s3-snapshot-store.js.map +1 -0
- package/dist/src/service/store-manager.d.ts +3 -0
- package/dist/src/service/store-manager.d.ts.map +1 -1
- package/dist/src/service/store-manager.js +24 -7
- package/dist/src/service/store-manager.js.map +1 -1
- package/package.json +16 -15
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3 configuration for durable batch storage.
|
|
3
|
+
*
|
|
4
|
+
* Supports both AWS S3 and S3-compatible services like MinIO.
|
|
5
|
+
*/
|
|
6
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
7
|
+
/**
|
|
8
|
+
* S3 storage configuration.
|
|
9
|
+
*/
|
|
10
|
+
export interface S3StorageConfig {
|
|
11
|
+
/** S3 bucket name for storing sync batches */
|
|
12
|
+
bucket: string;
|
|
13
|
+
/** AWS region (e.g., 'us-east-1') */
|
|
14
|
+
region: string;
|
|
15
|
+
/**
|
|
16
|
+
* Optional endpoint URL for S3-compatible services (e.g., MinIO).
|
|
17
|
+
* If not provided, uses AWS S3.
|
|
18
|
+
*
|
|
19
|
+
* @example 'http://localhost:9000' for local MinIO
|
|
20
|
+
*/
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
/**
|
|
23
|
+
* AWS credentials. If not provided, uses default credential chain.
|
|
24
|
+
*/
|
|
25
|
+
credentials?: {
|
|
26
|
+
accessKeyId: string;
|
|
27
|
+
secretAccessKey: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Force path-style URLs (required for MinIO and some S3-compatible services).
|
|
31
|
+
* Default: false (uses virtual-hosted style for AWS S3)
|
|
32
|
+
*/
|
|
33
|
+
forcePathStyle?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Key prefix for all objects in the bucket.
|
|
36
|
+
* Useful for organizing data or sharing a bucket across environments.
|
|
37
|
+
*
|
|
38
|
+
* @example 'sync-batches/' or 'dev/sync-batches/'
|
|
39
|
+
*/
|
|
40
|
+
keyPrefix?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create an S3 client from configuration.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createS3Client(config: S3StorageConfig): S3Client;
|
|
46
|
+
/**
|
|
47
|
+
* Build the S3 key for a sync batch.
|
|
48
|
+
*
|
|
49
|
+
* Key format: <prefix><storagePath>/batches/<timestamp>_<batch_id>.json
|
|
50
|
+
*
|
|
51
|
+
* @param config - S3 storage configuration
|
|
52
|
+
* @param storagePath - Storage path for the database (e.g., 'org123/s_abc123')
|
|
53
|
+
* @param batchId - Unique batch identifier
|
|
54
|
+
* @param timestamp - Batch timestamp (ISO format)
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildBatchKey(config: S3StorageConfig, storagePath: string, batchId: string, timestamp: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Build the S3 key for a database snapshot.
|
|
59
|
+
*
|
|
60
|
+
* Key format: <prefix><storagePath>/snapshots/<timestamp>_<snapshot_id>.json
|
|
61
|
+
*
|
|
62
|
+
* @param config - S3 storage configuration
|
|
63
|
+
* @param storagePath - Storage path for the database (e.g., 'org123/s_abc123')
|
|
64
|
+
* @param snapshotId - Unique snapshot identifier
|
|
65
|
+
* @param timestamp - Snapshot timestamp (ISO format)
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildSnapshotKey(config: S3StorageConfig, storagePath: string, snapshotId: string, timestamp: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Parse S3 configuration from environment variables.
|
|
70
|
+
*
|
|
71
|
+
* Environment variables:
|
|
72
|
+
* - S3_BUCKET: Bucket name (required)
|
|
73
|
+
* - S3_REGION: AWS region (default: 'us-east-1')
|
|
74
|
+
* - S3_ENDPOINT: Custom endpoint for MinIO/compatible services
|
|
75
|
+
* - S3_ACCESS_KEY_ID: Access key (optional, uses default chain if not set)
|
|
76
|
+
* - S3_SECRET_ACCESS_KEY: Secret key (optional)
|
|
77
|
+
* - S3_FORCE_PATH_STYLE: Set to 'true' for MinIO
|
|
78
|
+
* - S3_KEY_PREFIX: Key prefix for all objects
|
|
79
|
+
*/
|
|
80
|
+
export declare function parseS3ConfigFromEnv(): S3StorageConfig | undefined;
|
|
81
|
+
//# sourceMappingURL=s3-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-config.d.ts","sourceRoot":"","sources":["../../../src/service/s3-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAuB,MAAM,oBAAoB,CAAC;AAEnE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IAEf,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IAEf;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,WAAW,CAAC,EAAE;QACZ,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IAEF;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,QAAQ,CAkBhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,eAAe,EACvB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,MAAM,CAKR;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,eAAe,EACvB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,MAAM,CAIR;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,IAAI,eAAe,GAAG,SAAS,CA+BlE"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3 configuration for durable batch storage.
|
|
3
|
+
*
|
|
4
|
+
* Supports both AWS S3 and S3-compatible services like MinIO.
|
|
5
|
+
*/
|
|
6
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
7
|
+
/**
|
|
8
|
+
* Create an S3 client from configuration.
|
|
9
|
+
*/
|
|
10
|
+
export function createS3Client(config) {
|
|
11
|
+
const clientConfig = {
|
|
12
|
+
region: config.region,
|
|
13
|
+
};
|
|
14
|
+
if (config.endpoint) {
|
|
15
|
+
clientConfig.endpoint = config.endpoint;
|
|
16
|
+
}
|
|
17
|
+
if (config.credentials) {
|
|
18
|
+
clientConfig.credentials = config.credentials;
|
|
19
|
+
}
|
|
20
|
+
if (config.forcePathStyle) {
|
|
21
|
+
clientConfig.forcePathStyle = true;
|
|
22
|
+
}
|
|
23
|
+
return new S3Client(clientConfig);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the S3 key for a sync batch.
|
|
27
|
+
*
|
|
28
|
+
* Key format: <prefix><storagePath>/batches/<timestamp>_<batch_id>.json
|
|
29
|
+
*
|
|
30
|
+
* @param config - S3 storage configuration
|
|
31
|
+
* @param storagePath - Storage path for the database (e.g., 'org123/s_abc123')
|
|
32
|
+
* @param batchId - Unique batch identifier
|
|
33
|
+
* @param timestamp - Batch timestamp (ISO format)
|
|
34
|
+
*/
|
|
35
|
+
export function buildBatchKey(config, storagePath, batchId, timestamp) {
|
|
36
|
+
const prefix = config.keyPrefix ?? '';
|
|
37
|
+
// Use timestamp prefix for chronological ordering in S3 listings
|
|
38
|
+
const timestampPrefix = timestamp.replace(/[:.]/g, '-');
|
|
39
|
+
return `${prefix}${storagePath}/batches/${timestampPrefix}_${batchId}.json`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build the S3 key for a database snapshot.
|
|
43
|
+
*
|
|
44
|
+
* Key format: <prefix><storagePath>/snapshots/<timestamp>_<snapshot_id>.json
|
|
45
|
+
*
|
|
46
|
+
* @param config - S3 storage configuration
|
|
47
|
+
* @param storagePath - Storage path for the database (e.g., 'org123/s_abc123')
|
|
48
|
+
* @param snapshotId - Unique snapshot identifier
|
|
49
|
+
* @param timestamp - Snapshot timestamp (ISO format)
|
|
50
|
+
*/
|
|
51
|
+
export function buildSnapshotKey(config, storagePath, snapshotId, timestamp) {
|
|
52
|
+
const prefix = config.keyPrefix ?? '';
|
|
53
|
+
const timestampPrefix = timestamp.replace(/[:.]/g, '-');
|
|
54
|
+
return `${prefix}${storagePath}/snapshots/${timestampPrefix}_${snapshotId}.json`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parse S3 configuration from environment variables.
|
|
58
|
+
*
|
|
59
|
+
* Environment variables:
|
|
60
|
+
* - S3_BUCKET: Bucket name (required)
|
|
61
|
+
* - S3_REGION: AWS region (default: 'us-east-1')
|
|
62
|
+
* - S3_ENDPOINT: Custom endpoint for MinIO/compatible services
|
|
63
|
+
* - S3_ACCESS_KEY_ID: Access key (optional, uses default chain if not set)
|
|
64
|
+
* - S3_SECRET_ACCESS_KEY: Secret key (optional)
|
|
65
|
+
* - S3_FORCE_PATH_STYLE: Set to 'true' for MinIO
|
|
66
|
+
* - S3_KEY_PREFIX: Key prefix for all objects
|
|
67
|
+
*/
|
|
68
|
+
export function parseS3ConfigFromEnv() {
|
|
69
|
+
const bucket = process.env.S3_BUCKET;
|
|
70
|
+
if (!bucket) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const config = {
|
|
74
|
+
bucket,
|
|
75
|
+
region: process.env.S3_REGION ?? 'us-east-1',
|
|
76
|
+
};
|
|
77
|
+
if (process.env.S3_ENDPOINT) {
|
|
78
|
+
config.endpoint = process.env.S3_ENDPOINT;
|
|
79
|
+
}
|
|
80
|
+
if (process.env.S3_ACCESS_KEY_ID && process.env.S3_SECRET_ACCESS_KEY) {
|
|
81
|
+
config.credentials = {
|
|
82
|
+
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
|
83
|
+
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (process.env.S3_FORCE_PATH_STYLE === 'true') {
|
|
87
|
+
config.forcePathStyle = true;
|
|
88
|
+
}
|
|
89
|
+
if (process.env.S3_KEY_PREFIX) {
|
|
90
|
+
config.keyPrefix = process.env.S3_KEY_PREFIX;
|
|
91
|
+
}
|
|
92
|
+
return config;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=s3-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-config.js","sourceRoot":"","sources":["../../../src/service/s3-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAuB,MAAM,oBAAoB,CAAC;AA2CnE;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,MAAuB;IACpD,MAAM,YAAY,GAAmB;QACnC,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAC;IAEF,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,YAAY,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IAC1C,CAAC;IAED,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACvB,YAAY,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;IAChD,CAAC;IAED,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;QAC1B,YAAY,CAAC,cAAc,GAAG,IAAI,CAAC;IACrC,CAAC;IAED,OAAO,IAAI,QAAQ,CAAC,YAAY,CAAC,CAAC;AACpC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAuB,EACvB,WAAmB,EACnB,OAAe,EACf,SAAiB;IAEjB,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC;IACtC,iEAAiE;IACjE,MAAM,eAAe,GAAG,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACxD,OAAO,GAAG,MAAM,GAAG,WAAW,YAAY,eAAe,IAAI,OAAO,OAAO,CAAC;AAC9E,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAuB,EACvB,WAAmB,EACnB,UAAkB,EAClB,SAAiB;IAEjB,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC;IACtC,MAAM,eAAe,GAAG,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACxD,OAAO,GAAG,MAAM,GAAG,WAAW,cAAc,eAAe,IAAI,UAAU,OAAO,CAAC;AACnF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;IACrC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,MAAM,GAAoB;QAC9B,MAAM;QACN,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,WAAW;KAC7C,CAAC;IAEF,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;QAC5B,MAAM,CAAC,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IAC5C,CAAC;IAED,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,CAAC;QACrE,MAAM,CAAC,WAAW,GAAG;YACnB,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB;YACzC,eAAe,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB;SAClD,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,KAAK,MAAM,EAAE,CAAC;QAC/C,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC;IAC/B,CAAC;IAED,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;QAC9B,MAAM,CAAC,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAC/C,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3 Snapshot Store - Full database snapshots for faster restore.
|
|
3
|
+
*
|
|
4
|
+
* Stores periodic full snapshots to S3 at:
|
|
5
|
+
* <prefix><storage_path>/snapshots/<timestamp>_<snapshot_id>.json.gz
|
|
6
|
+
*
|
|
7
|
+
* Snapshots are triggered by:
|
|
8
|
+
* - Time interval (e.g., every 5 minutes)
|
|
9
|
+
* - Change volume threshold (e.g., every 1000 changes)
|
|
10
|
+
*/
|
|
11
|
+
import { type S3Client } from '@aws-sdk/client-s3';
|
|
12
|
+
import { type S3StorageConfig } from './s3-config.js';
|
|
13
|
+
import type { SyncManager } from '@quereus/sync';
|
|
14
|
+
/**
|
|
15
|
+
* Snapshot metadata stored alongside the snapshot.
|
|
16
|
+
*/
|
|
17
|
+
export interface SnapshotMetadata {
|
|
18
|
+
/** Unique snapshot identifier */
|
|
19
|
+
snapshotId: string;
|
|
20
|
+
/** Database ID this snapshot belongs to */
|
|
21
|
+
databaseId: string;
|
|
22
|
+
/** Timestamp when snapshot was created */
|
|
23
|
+
timestamp: string;
|
|
24
|
+
/** Total number of rows in the snapshot */
|
|
25
|
+
totalRows: number;
|
|
26
|
+
/** Total number of tables in the snapshot */
|
|
27
|
+
totalTables: number;
|
|
28
|
+
/** Compressed size in bytes */
|
|
29
|
+
compressedSizeBytes: number;
|
|
30
|
+
/** HLC timestamp of latest change in snapshot */
|
|
31
|
+
hlcTimestamp?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Configuration for periodic snapshots.
|
|
35
|
+
*/
|
|
36
|
+
export interface SnapshotScheduleConfig {
|
|
37
|
+
/** Interval in milliseconds between snapshots (default: 5 minutes) */
|
|
38
|
+
intervalMs: number;
|
|
39
|
+
/** Change count threshold to trigger snapshot (default: 1000) */
|
|
40
|
+
changeThreshold: number;
|
|
41
|
+
/** Maximum number of snapshots to retain per database (default: 5) */
|
|
42
|
+
maxRetained: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Tracker for pending snapshot operations.
|
|
46
|
+
*/
|
|
47
|
+
interface DatabaseSnapshotState {
|
|
48
|
+
lastSnapshotAt: number;
|
|
49
|
+
changesSinceSnapshot: number;
|
|
50
|
+
snapshotInProgress: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Function to resolve a database ID to a storage path for S3 keys.
|
|
54
|
+
*/
|
|
55
|
+
export type StoragePathResolver = (databaseId: string) => string;
|
|
56
|
+
/**
|
|
57
|
+
* S3 Snapshot Store for full database snapshots.
|
|
58
|
+
*/
|
|
59
|
+
export declare class S3SnapshotStore {
|
|
60
|
+
private readonly client;
|
|
61
|
+
private readonly config;
|
|
62
|
+
private readonly scheduleConfig;
|
|
63
|
+
private readonly resolveStoragePath;
|
|
64
|
+
private readonly databaseStates;
|
|
65
|
+
private checkTimer;
|
|
66
|
+
constructor(client: S3Client, config: S3StorageConfig, scheduleConfig?: Partial<SnapshotScheduleConfig>, resolveStoragePath?: StoragePathResolver);
|
|
67
|
+
/**
|
|
68
|
+
* Start periodic snapshot checks.
|
|
69
|
+
*/
|
|
70
|
+
start(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Stop periodic snapshot checks.
|
|
73
|
+
*/
|
|
74
|
+
stop(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Record that changes have been applied to a database.
|
|
77
|
+
*/
|
|
78
|
+
recordChanges(databaseId: string, changeCount: number): void;
|
|
79
|
+
/**
|
|
80
|
+
* Check if a database needs a snapshot based on time or change volume.
|
|
81
|
+
*/
|
|
82
|
+
needsSnapshot(databaseId: string): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Check all tracked databases for scheduled snapshots.
|
|
85
|
+
*/
|
|
86
|
+
private checkScheduledSnapshots;
|
|
87
|
+
/**
|
|
88
|
+
* Create and store a full snapshot for a database.
|
|
89
|
+
*/
|
|
90
|
+
createSnapshot(databaseId: string, syncManager: SyncManager): Promise<SnapshotMetadata>;
|
|
91
|
+
/**
|
|
92
|
+
* Type guard to check if a chunk is a column-versions chunk.
|
|
93
|
+
*/
|
|
94
|
+
private isColumnVersionsChunk;
|
|
95
|
+
/**
|
|
96
|
+
* Check if a snapshot exists for a database.
|
|
97
|
+
*/
|
|
98
|
+
hasSnapshot(databaseId: string): Promise<boolean>;
|
|
99
|
+
/**
|
|
100
|
+
* Compress data using gzip.
|
|
101
|
+
*/
|
|
102
|
+
private compressData;
|
|
103
|
+
/**
|
|
104
|
+
* Get databases that need snapshots (for external scheduling).
|
|
105
|
+
*/
|
|
106
|
+
getDatabasesNeedingSnapshot(): string[];
|
|
107
|
+
/**
|
|
108
|
+
* Force a snapshot for a database (ignoring schedule).
|
|
109
|
+
*/
|
|
110
|
+
forceSnapshot(databaseId: string, syncManager: SyncManager): Promise<SnapshotMetadata>;
|
|
111
|
+
/**
|
|
112
|
+
* Get snapshot state for a database.
|
|
113
|
+
*/
|
|
114
|
+
getState(databaseId: string): DatabaseSnapshotState | undefined;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Create an S3 snapshot store from configuration.
|
|
118
|
+
*/
|
|
119
|
+
export declare function createS3SnapshotStore(client: S3Client, config: S3StorageConfig, scheduleConfig?: Partial<SnapshotScheduleConfig>, resolveStoragePath?: StoragePathResolver): S3SnapshotStore;
|
|
120
|
+
export {};
|
|
121
|
+
//# sourceMappingURL=s3-snapshot-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-snapshot-store.d.ts","sourceRoot":"","sources":["../../../src/service/s3-snapshot-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAoB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAIrE,OAAO,EAAE,KAAK,eAAe,EAAoB,MAAM,gBAAgB,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAA8C,MAAM,eAAe,CAAC;AAE7F;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;IAEnB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IAEnB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAElB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAElB,6CAA6C;IAC7C,WAAW,EAAE,MAAM,CAAC;IAEpB,+BAA+B;IAC/B,mBAAmB,EAAE,MAAM,CAAC;IAE5B,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAC;IAEnB,iEAAiE;IACjE,eAAe,EAAE,MAAM,CAAC;IAExB,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;CACrB;AAQD;;GAEG;AACH,UAAU,qBAAqB;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;AASjE;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAsB;IACzD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA4C;IAC3E,OAAO,CAAC,UAAU,CAA+C;gBAG/D,MAAM,EAAE,QAAQ,EAChB,MAAM,EAAE,eAAe,EACvB,cAAc,GAAE,OAAO,CAAC,sBAAsB,CAAM,EACpD,kBAAkB,CAAC,EAAE,mBAAmB;IAQ1C;;OAEG;IACH,KAAK,IAAI,IAAI;IAQb;;OAEG;IACH,IAAI,IAAI,IAAI;IAOZ;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAa5D;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAoB1C;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAU/B;;OAEG;IACG,cAAc,CAClB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC,gBAAgB,CAAC;IA2E5B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAI7B;;OAEG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASvD;;OAEG;YACW,YAAY;IAa1B;;OAEG;IACH,2BAA2B,IAAI,MAAM,EAAE;IAUvC;;OAEG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAI5F;;OAEG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS;CAGhE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,QAAQ,EAChB,MAAM,EAAE,eAAe,EACvB,cAAc,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,EAChD,kBAAkB,CAAC,EAAE,mBAAmB,GACvC,eAAe,CAEjB"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3 Snapshot Store - Full database snapshots for faster restore.
|
|
3
|
+
*
|
|
4
|
+
* Stores periodic full snapshots to S3 at:
|
|
5
|
+
* <prefix><storage_path>/snapshots/<timestamp>_<snapshot_id>.json.gz
|
|
6
|
+
*
|
|
7
|
+
* Snapshots are triggered by:
|
|
8
|
+
* - Time interval (e.g., every 5 minutes)
|
|
9
|
+
* - Change volume threshold (e.g., every 1000 changes)
|
|
10
|
+
*/
|
|
11
|
+
import { PutObjectCommand } from '@aws-sdk/client-s3';
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { createGzip } from 'node:zlib';
|
|
14
|
+
import { serviceLog } from '../common/logger.js';
|
|
15
|
+
import { buildSnapshotKey } from './s3-config.js';
|
|
16
|
+
const DEFAULT_SCHEDULE_CONFIG = {
|
|
17
|
+
intervalMs: 5 * 60 * 1000, // 5 minutes
|
|
18
|
+
changeThreshold: 1000,
|
|
19
|
+
maxRetained: 5,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Default storage path resolver - sanitizes databaseId for use as S3 path.
|
|
23
|
+
*/
|
|
24
|
+
function defaultStoragePathResolver(databaseId) {
|
|
25
|
+
return databaseId.replace(/:/g, '/').replace(/[^a-zA-Z0-9/_-]/g, '_');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* S3 Snapshot Store for full database snapshots.
|
|
29
|
+
*/
|
|
30
|
+
export class S3SnapshotStore {
|
|
31
|
+
client;
|
|
32
|
+
config;
|
|
33
|
+
scheduleConfig;
|
|
34
|
+
resolveStoragePath;
|
|
35
|
+
databaseStates = new Map();
|
|
36
|
+
checkTimer = null;
|
|
37
|
+
constructor(client, config, scheduleConfig = {}, resolveStoragePath) {
|
|
38
|
+
this.client = client;
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.scheduleConfig = { ...DEFAULT_SCHEDULE_CONFIG, ...scheduleConfig };
|
|
41
|
+
this.resolveStoragePath = resolveStoragePath ?? defaultStoragePathResolver;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Start periodic snapshot checks.
|
|
45
|
+
*/
|
|
46
|
+
start() {
|
|
47
|
+
if (this.checkTimer)
|
|
48
|
+
return;
|
|
49
|
+
// Check every 30 seconds for databases needing snapshots
|
|
50
|
+
this.checkTimer = setInterval(() => this.checkScheduledSnapshots(), 30_000);
|
|
51
|
+
serviceLog('S3SnapshotStore started with interval=%dms, threshold=%d', this.scheduleConfig.intervalMs, this.scheduleConfig.changeThreshold);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Stop periodic snapshot checks.
|
|
55
|
+
*/
|
|
56
|
+
stop() {
|
|
57
|
+
if (this.checkTimer) {
|
|
58
|
+
clearInterval(this.checkTimer);
|
|
59
|
+
this.checkTimer = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Record that changes have been applied to a database.
|
|
64
|
+
*/
|
|
65
|
+
recordChanges(databaseId, changeCount) {
|
|
66
|
+
let state = this.databaseStates.get(databaseId);
|
|
67
|
+
if (!state) {
|
|
68
|
+
state = {
|
|
69
|
+
lastSnapshotAt: 0,
|
|
70
|
+
changesSinceSnapshot: 0,
|
|
71
|
+
snapshotInProgress: false,
|
|
72
|
+
};
|
|
73
|
+
this.databaseStates.set(databaseId, state);
|
|
74
|
+
}
|
|
75
|
+
state.changesSinceSnapshot += changeCount;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Check if a database needs a snapshot based on time or change volume.
|
|
79
|
+
*/
|
|
80
|
+
needsSnapshot(databaseId) {
|
|
81
|
+
const state = this.databaseStates.get(databaseId);
|
|
82
|
+
if (!state || state.snapshotInProgress)
|
|
83
|
+
return false;
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const timeSinceSnapshot = now - state.lastSnapshotAt;
|
|
86
|
+
// Check time interval
|
|
87
|
+
if (timeSinceSnapshot >= this.scheduleConfig.intervalMs) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
// Check change threshold
|
|
91
|
+
if (state.changesSinceSnapshot >= this.scheduleConfig.changeThreshold) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check all tracked databases for scheduled snapshots.
|
|
98
|
+
*/
|
|
99
|
+
checkScheduledSnapshots() {
|
|
100
|
+
for (const databaseId of this.databaseStates.keys()) {
|
|
101
|
+
if (this.needsSnapshot(databaseId)) {
|
|
102
|
+
serviceLog('Scheduled snapshot triggered for: %s', databaseId);
|
|
103
|
+
// Note: actual snapshot creation requires the SyncManager,
|
|
104
|
+
// which should be called by the coordinator
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Create and store a full snapshot for a database.
|
|
110
|
+
*/
|
|
111
|
+
async createSnapshot(databaseId, syncManager) {
|
|
112
|
+
const state = this.databaseStates.get(databaseId) ?? {
|
|
113
|
+
lastSnapshotAt: 0,
|
|
114
|
+
changesSinceSnapshot: 0,
|
|
115
|
+
snapshotInProgress: false,
|
|
116
|
+
};
|
|
117
|
+
this.databaseStates.set(databaseId, state);
|
|
118
|
+
if (state.snapshotInProgress) {
|
|
119
|
+
throw new Error(`Snapshot already in progress for ${databaseId}`);
|
|
120
|
+
}
|
|
121
|
+
state.snapshotInProgress = true;
|
|
122
|
+
const snapshotId = randomUUID();
|
|
123
|
+
const timestamp = new Date().toISOString();
|
|
124
|
+
try {
|
|
125
|
+
const storagePath = this.resolveStoragePath(databaseId);
|
|
126
|
+
const key = buildSnapshotKey(this.config, storagePath, snapshotId, timestamp);
|
|
127
|
+
// Stream snapshot chunks through gzip compression
|
|
128
|
+
let totalEntries = 0;
|
|
129
|
+
let totalTables = 0;
|
|
130
|
+
const chunks = [];
|
|
131
|
+
for await (const chunk of syncManager.getSnapshotStream()) {
|
|
132
|
+
chunks.push(chunk);
|
|
133
|
+
if (this.isColumnVersionsChunk(chunk)) {
|
|
134
|
+
totalEntries += chunk.entries?.length ?? 0;
|
|
135
|
+
}
|
|
136
|
+
else if (chunk.type === 'table-start') {
|
|
137
|
+
totalTables++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Serialize and compress
|
|
141
|
+
const jsonData = JSON.stringify({ snapshotId, databaseId, timestamp, chunks });
|
|
142
|
+
const compressed = await this.compressData(jsonData);
|
|
143
|
+
// Upload to S3
|
|
144
|
+
await this.client.send(new PutObjectCommand({
|
|
145
|
+
Bucket: this.config.bucket,
|
|
146
|
+
Key: key,
|
|
147
|
+
Body: compressed,
|
|
148
|
+
ContentType: 'application/gzip',
|
|
149
|
+
ContentEncoding: 'gzip',
|
|
150
|
+
Metadata: {
|
|
151
|
+
'x-snapshot-id': snapshotId,
|
|
152
|
+
'x-database-id': databaseId,
|
|
153
|
+
'x-entry-count': String(totalEntries),
|
|
154
|
+
'x-table-count': String(totalTables),
|
|
155
|
+
},
|
|
156
|
+
}));
|
|
157
|
+
const metadata = {
|
|
158
|
+
snapshotId,
|
|
159
|
+
databaseId,
|
|
160
|
+
timestamp,
|
|
161
|
+
totalRows: totalEntries, // Using entries count as "rows"
|
|
162
|
+
totalTables,
|
|
163
|
+
compressedSizeBytes: compressed.length,
|
|
164
|
+
};
|
|
165
|
+
serviceLog('Snapshot created: %s (%d entries, %d tables, %d bytes)', snapshotId, totalEntries, totalTables, compressed.length);
|
|
166
|
+
// Update state
|
|
167
|
+
state.lastSnapshotAt = Date.now();
|
|
168
|
+
state.changesSinceSnapshot = 0;
|
|
169
|
+
return metadata;
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
state.snapshotInProgress = false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Type guard to check if a chunk is a column-versions chunk.
|
|
177
|
+
*/
|
|
178
|
+
isColumnVersionsChunk(chunk) {
|
|
179
|
+
return chunk.type === 'column-versions';
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Check if a snapshot exists for a database.
|
|
183
|
+
*/
|
|
184
|
+
async hasSnapshot(databaseId) {
|
|
185
|
+
const storagePath = this.resolveStoragePath(databaseId);
|
|
186
|
+
// Check for latest snapshot pattern
|
|
187
|
+
const prefix = this.config.keyPrefix ?? '';
|
|
188
|
+
void `${prefix}${storagePath}/snapshots/`; // Key pattern for listing
|
|
189
|
+
// Would need list operation to check, simplified for now
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Compress data using gzip.
|
|
194
|
+
*/
|
|
195
|
+
async compressData(data) {
|
|
196
|
+
const gzip = createGzip();
|
|
197
|
+
const buffers = [];
|
|
198
|
+
gzip.on('data', (chunk) => buffers.push(chunk));
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
gzip.on('end', () => resolve(Buffer.concat(buffers)));
|
|
201
|
+
gzip.on('error', reject);
|
|
202
|
+
gzip.end(Buffer.from(data, 'utf-8'));
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get databases that need snapshots (for external scheduling).
|
|
207
|
+
*/
|
|
208
|
+
getDatabasesNeedingSnapshot() {
|
|
209
|
+
const result = [];
|
|
210
|
+
for (const databaseId of this.databaseStates.keys()) {
|
|
211
|
+
if (this.needsSnapshot(databaseId)) {
|
|
212
|
+
result.push(databaseId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Force a snapshot for a database (ignoring schedule).
|
|
219
|
+
*/
|
|
220
|
+
async forceSnapshot(databaseId, syncManager) {
|
|
221
|
+
return this.createSnapshot(databaseId, syncManager);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Get snapshot state for a database.
|
|
225
|
+
*/
|
|
226
|
+
getState(databaseId) {
|
|
227
|
+
return this.databaseStates.get(databaseId);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Create an S3 snapshot store from configuration.
|
|
232
|
+
*/
|
|
233
|
+
export function createS3SnapshotStore(client, config, scheduleConfig, resolveStoragePath) {
|
|
234
|
+
return new S3SnapshotStore(client, config, scheduleConfig, resolveStoragePath);
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=s3-snapshot-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"s3-snapshot-store.js","sourceRoot":"","sources":["../../../src/service/s3-snapshot-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,gBAAgB,EAAiB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAwB,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AA2CxE,MAAM,uBAAuB,GAA2B;IACtD,UAAU,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,EAAE,YAAY;IACvC,eAAe,EAAE,IAAI;IACrB,WAAW,EAAE,CAAC;CACf,CAAC;AAgBF;;GAEG;AACH,SAAS,0BAA0B,CAAC,UAAkB;IACpD,OAAO,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;AACxE,CAAC;AAED;;GAEG;AACH,MAAM,OAAO,eAAe;IACT,MAAM,CAAW;IACjB,MAAM,CAAkB;IACxB,cAAc,CAAyB;IACvC,kBAAkB,CAAsB;IACxC,cAAc,GAAG,IAAI,GAAG,EAAiC,CAAC;IACnE,UAAU,GAA0C,IAAI,CAAC;IAEjE,YACE,MAAgB,EAChB,MAAuB,EACvB,iBAAkD,EAAE,EACpD,kBAAwC;QAExC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,cAAc,GAAG,EAAE,GAAG,uBAAuB,EAAE,GAAG,cAAc,EAAE,CAAC;QACxE,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,IAAI,0BAA0B,CAAC;IAC7E,CAAC;IAED;;OAEG;IACH,KAAK;QACH,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC5B,yDAAyD;QACzD,IAAI,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,uBAAuB,EAAE,EAAE,MAAM,CAAC,CAAC;QAC5E,UAAU,CAAC,0DAA0D,EACnE,IAAI,CAAC,cAAc,CAAC,UAAU,EAAE,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;IACzE,CAAC;IAED;;OAEG;IACH,IAAI;QACF,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC/B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC;IACH,CAAC;IAED;;OAEG;IACH,aAAa,CAAC,UAAkB,EAAE,WAAmB;QACnD,IAAI,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAChD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,KAAK,GAAG;gBACN,cAAc,EAAE,CAAC;gBACjB,oBAAoB,EAAE,CAAC;gBACvB,kBAAkB,EAAE,KAAK;aAC1B,CAAC;YACF,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAC7C,CAAC;QACD,KAAK,CAAC,oBAAoB,IAAI,WAAW,CAAC;IAC5C,CAAC;IAED;;OAEG;IACH,aAAa,CAAC,UAAkB;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAClD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,kBAAkB;YAAE,OAAO,KAAK,CAAC;QAErD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,iBAAiB,GAAG,GAAG,GAAG,KAAK,CAAC,cAAc,CAAC;QAErD,sBAAsB;QACtB,IAAI,iBAAiB,IAAI,IAAI,CAAC,cAAc,CAAC,UAAU,EAAE,CAAC;YACxD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,yBAAyB;QACzB,IAAI,KAAK,CAAC,oBAAoB,IAAI,IAAI,CAAC,cAAc,CAAC,eAAe,EAAE,CAAC;YACtE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;OAEG;IACK,uBAAuB;QAC7B,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,EAAE,CAAC;YACpD,IAAI,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC;gBACnC,UAAU,CAAC,sCAAsC,EAAE,UAAU,CAAC,CAAC;gBAC/D,2DAA2D;gBAC3D,4CAA4C;YAC9C,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,cAAc,CAClB,UAAkB,EAClB,WAAwB;QAExB,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI;YACnD,cAAc,EAAE,CAAC;YACjB,oBAAoB,EAAE,CAAC;YACvB,kBAAkB,EAAE,KAAK;SAC1B,CAAC;QACF,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAE3C,IAAI,KAAK,CAAC,kBAAkB,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,oCAAoC,UAAU,EAAE,CAAC,CAAC;QACpE,CAAC;QAED,KAAK,CAAC,kBAAkB,GAAG,IAAI,CAAC;QAChC,MAAM,UAAU,GAAG,UAAU,EAAE,CAAC;QAChC,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAE3C,IAAI,CAAC;YACH,MAAM,WAAW,GAAG,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;YACxD,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;YAE9E,kDAAkD;YAClD,IAAI,YAAY,GAAG,CAAC,CAAC;YACrB,IAAI,WAAW,GAAG,CAAC,CAAC;YACpB,MAAM,MAAM,GAAoB,EAAE,CAAC;YAEnC,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,WAAW,CAAC,iBAAiB,EAAE,EAAE,CAAC;gBAC1D,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACnB,IAAI,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,EAAE,CAAC;oBACtC,YAAY,IAAI,KAAK,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC,CAAC;gBAC7C,CAAC;qBAAM,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;oBACxC,WAAW,EAAE,CAAC;gBAChB,CAAC;YACH,CAAC;YAED,yBAAyB;YACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;YAC/E,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;YAErD,eAAe;YACf,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,gBAAgB,CAAC;gBAC1C,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;gBAC1B,GAAG,EAAE,GAAG;gBACR,IAAI,EAAE,UAAU;gBAChB,WAAW,EAAE,kBAAkB;gBAC/B,eAAe,EAAE,MAAM;gBACvB,QAAQ,EAAE;oBACR,eAAe,EAAE,UAAU;oBAC3B,eAAe,EAAE,UAAU;oBAC3B,eAAe,EAAE,MAAM,CAAC,YAAY,CAAC;oBACrC,eAAe,EAAE,MAAM,CAAC,WAAW,CAAC;iBACrC;aACF,CAAC,CAAC,CAAC;YAEJ,MAAM,QAAQ,GAAqB;gBACjC,UAAU;gBACV,UAAU;gBACV,SAAS;gBACT,SAAS,EAAE,YAAY,EAAE,gCAAgC;gBACzD,WAAW;gBACX,mBAAmB,EAAE,UAAU,CAAC,MAAM;aACvC,CAAC;YAEF,UAAU,CAAC,wDAAwD,EACjE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;YAE5D,eAAe;YACf,KAAK,CAAC,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAClC,KAAK,CAAC,oBAAoB,GAAG,CAAC,CAAC;YAE/B,OAAO,QAAQ,CAAC;QAClB,CAAC;gBAAS,CAAC;YACT,KAAK,CAAC,kBAAkB,GAAG,KAAK,CAAC;QACnC,CAAC;IACH,CAAC;IAED;;OAEG;IACK,qBAAqB,CAAC,KAAoB;QAChD,OAAO,KAAK,CAAC,IAAI,KAAK,iBAAiB,CAAC;IAC1C,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,WAAW,CAAC,UAAkB;QAClC,MAAM,WAAW,GAAG,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QACxD,oCAAoC;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC;QAC3C,KAAK,GAAG,MAAM,GAAG,WAAW,aAAa,CAAC,CAAC,0BAA0B;QACrE,yDAAyD;QACzD,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,YAAY,CAAC,IAAY;QACrC,MAAM,IAAI,GAAG,UAAU,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAa,EAAE,CAAC;QAE7B,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAEhD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YACtD,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YACzB,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACH,2BAA2B;QACzB,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,EAAE,CAAC;YACpD,IAAI,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC;gBACnC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,aAAa,CAAC,UAAkB,EAAE,WAAwB;QAC9D,OAAO,IAAI,CAAC,cAAc,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;IACtD,CAAC;IAED;;OAEG;IACH,QAAQ,CAAC,UAAkB;QACzB,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAC7C,CAAC;CACF;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAAgB,EAChB,MAAuB,EACvB,cAAgD,EAChD,kBAAwC;IAExC,OAAO,IAAI,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,kBAAkB,CAAC,CAAC;AACjF,CAAC"}
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Manages lazy loading and cleanup of per-database LevelDB stores.
|
|
5
5
|
* Each database gets its own isolated store, opened on-demand and
|
|
6
6
|
* closed after idle timeout.
|
|
7
|
+
*
|
|
8
|
+
* This is a generic implementation. Applications provide custom database ID
|
|
9
|
+
* parsing and path resolution via hooks in StoreManagerConfig.
|
|
7
10
|
*/
|
|
8
11
|
import { StoreEventEmitter } from '@quereus/store';
|
|
9
12
|
import { LevelDBStore } from '@quereus/plugin-leveldb';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-manager.d.ts","sourceRoot":"","sources":["../../../src/service/store-manager.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"store-manager.d.ts","sourceRoot":"","sources":["../../../src/service/store-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAEL,KAAK,WAAW,EACjB,MAAM,eAAe,CAAC;AAGvB,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;IACpB,WAAW,EAAE,WAAW,CAAC;IACzB,WAAW,EAAE,iBAAiB,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,KAAK,MAAM,CAAC;IAE5E;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,KAAK,OAAO,CAAC;CAC7E;AAED,MAAM,WAAW,kBAAkB;IACjC,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,aAAa,EAAE,MAAM,CAAC;IACtB,kCAAkC;IAClC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,6CAA6C;IAC7C,UAAU,CAAC,EAAE;QACX,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,qCAAqC;IACrC,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AA0CD;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAyD;IAC5F,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA0D;IAC5F,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IACxD,OAAO,CAAC,YAAY,CAA+C;IACnE,OAAO,CAAC,eAAe,CAA8B;gBAEzC,MAAM,GAAE,OAAO,CAAC,kBAAkB,CAAM;IAMpD;;OAEG;IACH,KAAK,IAAI,IAAI;IAMb;;;;OAIG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC;IAsB9E;;OAEG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IASjC;;OAEG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAInC;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAI/C;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED;;;;OAIG;IACH,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO;IAIvE;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YA0BjB,SAAS;IAiCvB;;OAEG;YACW,OAAO;IAmBrB;;OAEG;YACW,QAAQ;IAoBtB;;OAEG;YACW,UAAU;CAYzB"}
|
|
@@ -4,31 +4,45 @@
|
|
|
4
4
|
* Manages lazy loading and cleanup of per-database LevelDB stores.
|
|
5
5
|
* Each database gets its own isolated store, opened on-demand and
|
|
6
6
|
* closed after idle timeout.
|
|
7
|
+
*
|
|
8
|
+
* This is a generic implementation. Applications provide custom database ID
|
|
9
|
+
* parsing and path resolution via hooks in StoreManagerConfig.
|
|
7
10
|
*/
|
|
8
11
|
import { join } from 'node:path';
|
|
12
|
+
import { mkdir } from 'node:fs/promises';
|
|
9
13
|
import { StoreEventEmitter } from '@quereus/store';
|
|
10
14
|
import { LevelDBStore } from '@quereus/plugin-leveldb';
|
|
11
15
|
import { createSyncModule, } from '@quereus/sync';
|
|
12
16
|
import { serviceLog } from '../common/logger.js';
|
|
13
17
|
/**
|
|
14
|
-
* Sanitize a
|
|
18
|
+
* Sanitize a string for use as a filesystem path component.
|
|
15
19
|
* Replaces unsafe characters with underscores.
|
|
16
20
|
*/
|
|
17
|
-
function
|
|
21
|
+
function sanitizePathComponent(value) {
|
|
18
22
|
// Allow alphanumeric, dash, underscore; replace others with underscore
|
|
19
|
-
return
|
|
23
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
20
24
|
}
|
|
21
25
|
/**
|
|
22
|
-
* Default storage path resolver -
|
|
26
|
+
* Default storage path resolver - simple sanitized passthrough.
|
|
27
|
+
*
|
|
28
|
+
* Applications can provide a custom resolveStoragePath hook for
|
|
29
|
+
* org-based folder structures or other custom layouts.
|
|
23
30
|
*/
|
|
24
31
|
function defaultResolveStoragePath(databaseId, _context) {
|
|
25
|
-
return
|
|
32
|
+
return sanitizePathComponent(databaseId);
|
|
26
33
|
}
|
|
27
34
|
/**
|
|
28
|
-
* Default database ID validator - accepts non-empty
|
|
35
|
+
* Default database ID validator - accepts any non-empty string with safe characters.
|
|
36
|
+
*
|
|
37
|
+
* Applications can provide a custom isValidDatabaseId hook for
|
|
38
|
+
* stricter validation (e.g., org:type_id format).
|
|
29
39
|
*/
|
|
30
40
|
function defaultIsValidDatabaseId(databaseId, _context) {
|
|
31
|
-
|
|
41
|
+
if (typeof databaseId !== 'string' || databaseId.length === 0) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// Accept alphanumeric with common separators
|
|
45
|
+
return /^[a-zA-Z0-9_:.-]+$/.test(databaseId);
|
|
32
46
|
}
|
|
33
47
|
const DEFAULT_CONFIG = {
|
|
34
48
|
dataDir: './data',
|
|
@@ -154,6 +168,9 @@ export class StoreManager {
|
|
|
154
168
|
}
|
|
155
169
|
const storagePath = this.resolveStoragePath(databaseId, context);
|
|
156
170
|
const fullPath = join(this.config.dataDir, storagePath);
|
|
171
|
+
// Ensure parent directories exist (org folder for new org-based format)
|
|
172
|
+
const parentPath = join(this.config.dataDir, storagePath.split('/')[0]);
|
|
173
|
+
await mkdir(parentPath, { recursive: true });
|
|
157
174
|
serviceLog('Opening store at: %s', fullPath);
|
|
158
175
|
const store = await LevelDBStore.open({
|
|
159
176
|
path: fullPath,
|