@quereus/sync-coordinator 0.7.0 → 0.8.1

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.
@@ -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":"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"}
@@ -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;;;;;;GAMG;AAGH,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;AAgCD;;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;IA6BvB;;OAEG;YACW,OAAO;IAmBrB;;OAEG;YACW,QAAQ;IAoBtB;;OAEG;YACW,UAAU;CAYzB"}
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 database ID for use as a filesystem path.
18
+ * Sanitize a string for use as a filesystem path component.
15
19
  * Replaces unsafe characters with underscores.
16
20
  */
17
- function sanitizeDatabaseId(databaseId) {
21
+ function sanitizePathComponent(value) {
18
22
  // Allow alphanumeric, dash, underscore; replace others with underscore
19
- return databaseId.replace(/[^a-zA-Z0-9_-]/g, '_');
23
+ return value.replace(/[^a-zA-Z0-9_-]/g, '_');
20
24
  }
21
25
  /**
22
- * Default storage path resolver - just uses the sanitized database ID.
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 sanitizeDatabaseId(databaseId);
32
+ return sanitizePathComponent(databaseId);
26
33
  }
27
34
  /**
28
- * Default database ID validator - accepts non-empty strings.
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
- return typeof databaseId === 'string' && databaseId.length > 0;
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,