bunqueue 2.8.42 → 2.8.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +3 -3
  2. package/dist/application/latencyTracker.js +3 -3
  3. package/dist/application/metricsExporter.d.ts +3 -1
  4. package/dist/application/metricsExporter.js +63 -16
  5. package/dist/application/prometheusOperationalMetrics.d.ts +31 -0
  6. package/dist/application/prometheusOperationalMetrics.js +53 -0
  7. package/dist/application/queueManager.d.ts +6 -0
  8. package/dist/application/queueManager.js +24 -1
  9. package/dist/application/types.d.ts +3 -0
  10. package/dist/application/types.js +1 -0
  11. package/dist/application/workerManager.d.ts +1 -0
  12. package/dist/application/workerManager.js +4 -1
  13. package/dist/cli/output.js +2 -1
  14. package/dist/config/resolve.d.ts +1 -0
  15. package/dist/config/resolve.js +10 -0
  16. package/dist/config/types.d.ts +6 -0
  17. package/dist/infrastructure/backup/backupTelemetry.d.ts +30 -0
  18. package/dist/infrastructure/backup/backupTelemetry.js +63 -0
  19. package/dist/infrastructure/backup/index.d.ts +1 -1
  20. package/dist/infrastructure/backup/index.js +1 -1
  21. package/dist/infrastructure/backup/s3Backup.d.ts +6 -2
  22. package/dist/infrastructure/backup/s3Backup.js +22 -9
  23. package/dist/infrastructure/backup/s3BackupConfig.d.ts +7 -0
  24. package/dist/infrastructure/backup/s3BackupConfig.js +5 -0
  25. package/dist/infrastructure/backup/s3BackupIo.d.ts +23 -0
  26. package/dist/infrastructure/backup/s3BackupIo.js +106 -0
  27. package/dist/infrastructure/backup/s3BackupOperations.d.ts +6 -15
  28. package/dist/infrastructure/backup/s3BackupOperations.js +139 -259
  29. package/dist/infrastructure/backup/sqliteBackupFiles.d.ts +22 -0
  30. package/dist/infrastructure/backup/sqliteBackupFiles.js +130 -0
  31. package/dist/infrastructure/cloud/statsRefresh.d.ts +1 -0
  32. package/dist/infrastructure/persistence/sqlite.js +6 -1
  33. package/dist/infrastructure/server/bootstrap.d.ts +2 -0
  34. package/dist/infrastructure/server/bootstrap.js +30 -1
  35. package/dist/infrastructure/server/http.d.ts +2 -0
  36. package/dist/infrastructure/server/http.js +9 -58
  37. package/dist/infrastructure/server/httpDashboardEndpoints.d.ts +10 -0
  38. package/dist/infrastructure/server/httpDashboardEndpoints.js +146 -0
  39. package/dist/infrastructure/server/httpEndpoints.d.ts +5 -9
  40. package/dist/infrastructure/server/httpEndpoints.js +16 -160
  41. package/dist/infrastructure/server/httpResponse.d.ts +2 -0
  42. package/dist/infrastructure/server/httpResponse.js +12 -0
  43. package/dist/infrastructure/server/httpRouter.d.ts +6 -0
  44. package/dist/infrastructure/server/httpRouter.js +50 -0
  45. package/dist/shared/histogram.d.ts +2 -2
  46. package/dist/shared/histogram.js +4 -4
  47. package/package.json +2 -2
@@ -5,7 +5,9 @@
5
5
  * Supports: AWS S3, Cloudflare R2, MinIO, DigitalOcean Spaces, etc.
6
6
  */
7
7
  import { type S3BackupConfig, type BackupResult, type BackupItem } from './s3BackupConfig';
8
+ import { type BackupMetrics } from './backupTelemetry';
8
9
  export type { S3BackupConfig, BackupResult } from './s3BackupConfig';
10
+ export type { BackupMetrics } from './backupTelemetry';
9
11
  /**
10
12
  * S3 Backup Manager
11
13
  * Handles automated and manual backups to S3-compatible storage
@@ -14,15 +16,15 @@ export declare class S3BackupManager {
14
16
  private readonly config;
15
17
  private readonly client;
16
18
  private readonly flushBeforeBackup?;
19
+ private readonly telemetry;
17
20
  private backupInterval;
18
21
  private initialBackupTimeout;
19
- private isBackupInProgress;
20
22
  private dashboardEmit;
21
23
  /** Set the dashboard event emitter callback */
22
24
  setDashboardEmit(callback: (event: string, data: Record<string, unknown>) => void): void;
23
25
  constructor(config: Partial<S3BackupConfig> & {
24
26
  databasePath: string;
25
- flushBeforeBackup?: () => Promise<void>;
27
+ flushBeforeBackup?: () => void | Promise<void>;
26
28
  });
27
29
  /**
28
30
  * Create configuration from environment variables
@@ -66,4 +68,6 @@ export declare class S3BackupManager {
66
68
  retention: number;
67
69
  isRunning: boolean;
68
70
  };
71
+ /** Snapshot label-free backup telemetry for the Prometheus collector. */
72
+ getMetrics(): BackupMetrics;
69
73
  }
@@ -8,6 +8,7 @@ import { S3Client } from 'bun';
8
8
  import { backupLog } from '../../shared/logger';
9
9
  import { DEFAULTS, configFromEnv, validateConfig, } from './s3BackupConfig';
10
10
  import { performBackup, listBackups, restoreBackup, cleanupOldBackups } from './s3BackupOperations';
11
+ import { BackupTelemetry } from './backupTelemetry';
11
12
  /**
12
13
  * S3 Backup Manager
13
14
  * Handles automated and manual backups to S3-compatible storage
@@ -16,9 +17,9 @@ export class S3BackupManager {
16
17
  config;
17
18
  client;
18
19
  flushBeforeBackup;
20
+ telemetry;
19
21
  backupInterval = null;
20
22
  initialBackupTimeout = null;
21
- isBackupInProgress = false;
22
23
  dashboardEmit = null;
23
24
  /** Set the dashboard event emitter callback */
24
25
  setDashboardEmit(callback) {
@@ -29,21 +30,27 @@ export class S3BackupManager {
29
30
  enabled: config.enabled ?? false,
30
31
  accessKeyId: config.accessKeyId ?? '',
31
32
  secretAccessKey: config.secretAccessKey ?? '',
33
+ sessionToken: config.sessionToken,
32
34
  bucket: config.bucket ?? '',
33
35
  endpoint: config.endpoint,
36
+ virtualHostedStyle: config.virtualHostedStyle,
34
37
  region: config.region ?? DEFAULTS.region,
35
38
  intervalMs: config.intervalMs ?? DEFAULTS.intervalMs,
36
39
  retention: config.retention ?? DEFAULTS.retention,
37
40
  prefix: config.prefix ?? DEFAULTS.prefix,
38
41
  databasePath: config.databasePath,
42
+ timeoutMs: config.timeoutMs,
39
43
  };
40
44
  this.flushBeforeBackup = config.flushBeforeBackup;
45
+ this.telemetry = new BackupTelemetry(this.config.enabled, this.config.intervalMs, this.config.retention);
41
46
  // Initialize S3 client
42
47
  this.client = new S3Client({
43
48
  accessKeyId: this.config.accessKeyId,
44
49
  secretAccessKey: this.config.secretAccessKey,
50
+ sessionToken: this.config.sessionToken,
45
51
  bucket: this.config.bucket,
46
52
  endpoint: this.config.endpoint,
53
+ virtualHostedStyle: this.config.virtualHostedStyle,
47
54
  region: this.config.region,
48
55
  });
49
56
  }
@@ -84,6 +91,7 @@ export class S3BackupManager {
84
91
  backupLog.error('Scheduled backup failed', { error: String(err) });
85
92
  });
86
93
  }, this.config.intervalMs);
94
+ this.telemetry.setSchedulerRunning(true);
87
95
  }
88
96
  /**
89
97
  * Stop automated backup scheduler
@@ -97,17 +105,18 @@ export class S3BackupManager {
97
105
  clearInterval(this.backupInterval);
98
106
  this.backupInterval = null;
99
107
  }
108
+ this.telemetry.setSchedulerRunning(false);
100
109
  }
101
110
  /**
102
111
  * Perform a backup
103
112
  */
104
113
  async backup() {
105
- if (this.isBackupInProgress) {
114
+ if (!this.telemetry.tryStart()) {
106
115
  return { success: false, error: 'Backup already in progress' };
107
116
  }
108
- this.isBackupInProgress = true;
109
- this.dashboardEmit?.('storage:backup-started', { bucket: this.config.bucket });
117
+ const startedAt = Date.now();
110
118
  try {
119
+ this.dashboardEmit?.('storage:backup-started', { bucket: this.config.bucket });
111
120
  if (this.flushBeforeBackup) {
112
121
  await this.flushBeforeBackup();
113
122
  }
@@ -118,36 +127,36 @@ export class S3BackupManager {
118
127
  bucket: this.config.bucket,
119
128
  key: result.key,
120
129
  });
130
+ this.telemetry.succeed(result, Date.now() - startedAt);
121
131
  }
122
132
  else {
123
133
  this.dashboardEmit?.('storage:backup-failed', {
124
134
  bucket: this.config.bucket,
125
135
  error: result.error,
126
136
  });
137
+ this.telemetry.fail(Date.now() - startedAt);
127
138
  }
128
139
  return result;
129
140
  }
130
141
  catch (err) {
142
+ this.telemetry.fail(Date.now() - startedAt);
131
143
  this.dashboardEmit?.('storage:backup-failed', {
132
144
  bucket: this.config.bucket,
133
145
  error: String(err),
134
146
  });
135
147
  throw err;
136
148
  }
137
- finally {
138
- this.isBackupInProgress = false;
139
- }
140
149
  }
141
150
  /**
142
151
  * List available backups
143
152
  */
144
- async listBackups() {
153
+ listBackups() {
145
154
  return listBackups(this.config, this.client);
146
155
  }
147
156
  /**
148
157
  * Restore from a backup
149
158
  */
150
- async restore(key) {
159
+ restore(key) {
151
160
  return restoreBackup(key, this.config, this.client);
152
161
  }
153
162
  /**
@@ -163,4 +172,8 @@ export class S3BackupManager {
163
172
  isRunning: this.backupInterval !== null,
164
173
  };
165
174
  }
175
+ /** Snapshot label-free backup telemetry for the Prometheus collector. */
176
+ getMetrics() {
177
+ return this.telemetry.snapshot();
178
+ }
166
179
  }
@@ -10,10 +10,14 @@ export interface S3BackupConfig {
10
10
  accessKeyId: string;
11
11
  /** S3 secret access key */
12
12
  secretAccessKey: string;
13
+ /** Temporary credential session token */
14
+ sessionToken?: string;
13
15
  /** S3 bucket name */
14
16
  bucket: string;
15
17
  /** S3 endpoint (optional, for non-AWS S3-compatible services) */
16
18
  endpoint?: string;
19
+ /** Use bucket-name-in-host S3 requests */
20
+ virtualHostedStyle?: boolean;
17
21
  /** S3 region (optional, default: us-east-1) */
18
22
  region?: string;
19
23
  /** Backup interval in milliseconds (default: 6 hours) */
@@ -31,7 +35,10 @@ export interface S3BackupConfig {
31
35
  export interface BackupResult {
32
36
  success: boolean;
33
37
  key?: string;
38
+ /** Uncompressed SQLite database size in bytes */
34
39
  size?: number;
40
+ /** Uploaded gzip object size in bytes */
41
+ compressedSize?: number;
35
42
  duration?: number;
36
43
  error?: string;
37
44
  }
@@ -13,12 +13,17 @@ export const DEFAULTS = {
13
13
  * Create configuration from environment variables
14
14
  */
15
15
  export function configFromEnv(databasePath) {
16
+ const virtualHostedStyle = Bun.env.S3_VIRTUAL_HOSTED_STYLE;
16
17
  return {
17
18
  enabled: Bun.env.S3_BACKUP_ENABLED === '1' || Bun.env.S3_BACKUP_ENABLED === 'true',
18
19
  accessKeyId: Bun.env.S3_ACCESS_KEY_ID ?? Bun.env.AWS_ACCESS_KEY_ID ?? '',
19
20
  secretAccessKey: Bun.env.S3_SECRET_ACCESS_KEY ?? Bun.env.AWS_SECRET_ACCESS_KEY ?? '',
21
+ sessionToken: Bun.env.S3_SESSION_TOKEN ?? Bun.env.AWS_SESSION_TOKEN,
20
22
  bucket: Bun.env.S3_BUCKET ?? Bun.env.AWS_BUCKET ?? '',
21
23
  endpoint: Bun.env.S3_ENDPOINT ?? Bun.env.AWS_ENDPOINT,
24
+ virtualHostedStyle: virtualHostedStyle === undefined
25
+ ? undefined
26
+ : virtualHostedStyle === '1' || virtualHostedStyle === 'true',
22
27
  region: Bun.env.S3_REGION ?? Bun.env.AWS_REGION ?? DEFAULTS.region,
23
28
  intervalMs: parseInt(Bun.env.S3_BACKUP_INTERVAL ?? '', 10) || DEFAULTS.intervalMs,
24
29
  retention: parseInt(Bun.env.S3_BACKUP_RETENTION ?? '', 10) || DEFAULTS.retention,
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared I/O helpers for S3 backup operations.
3
+ */
4
+ import type { S3Client } from 'bun';
5
+ export declare const DEFAULT_S3_TIMEOUT_MS = 30000;
6
+ /** Race a promise against a timeout and always release the timeout handle. */
7
+ export declare function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T>;
8
+ /** Retry an async operation with exponential backoff (500ms, 1000ms, 2000ms). */
9
+ export declare function withRetry<T>(fn: () => Promise<T>, label: string, maxRetries?: number, baseDelayMs?: number): Promise<T>;
10
+ /** Give every retry attempt its own configured operation timeout. */
11
+ export declare function retryWithTimeout<T>(fn: () => Promise<T>, timeoutMs: number, label: string): Promise<T>;
12
+ /** Async gzip compression using Web Streams. */
13
+ export declare function gzipAsync(data: Uint8Array): Promise<Uint8Array>;
14
+ /** Async gzip decompression using Web Streams. */
15
+ export declare function gunzipAsync(data: Uint8Array): Promise<Uint8Array>;
16
+ export declare function sha256(data: Uint8Array): string;
17
+ /**
18
+ * Remove a payload that failed to publish, then its now-orphaned metadata.
19
+ *
20
+ * A locally timed-out PUT may still complete because Bun's S3 write cannot be
21
+ * aborted. In that case metadata must remain so a late payload is still paired.
22
+ */
23
+ export declare function cleanupFailedPayload(client: S3Client, key: string, timeoutMs: number, uploadError: unknown): Promise<void>;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Shared I/O helpers for S3 backup operations.
3
+ */
4
+ import { backupLog } from '../../shared/logger';
5
+ export const DEFAULT_S3_TIMEOUT_MS = 30_000;
6
+ /** Race a promise against a timeout and always release the timeout handle. */
7
+ export async function withTimeout(promise, ms, label) {
8
+ let timer;
9
+ const timeout = new Promise((_, reject) => {
10
+ timer = setTimeout(() => {
11
+ reject(new Error(`${label} timed out after ${ms}ms`));
12
+ }, ms);
13
+ });
14
+ try {
15
+ return await Promise.race([promise, timeout]);
16
+ }
17
+ finally {
18
+ if (timer) {
19
+ clearTimeout(timer);
20
+ }
21
+ }
22
+ }
23
+ /** Check if an error is transient and worth retrying. */
24
+ function isTransientError(error) {
25
+ const message = error instanceof Error ? error.message : String(error);
26
+ const lower = message.toLowerCase();
27
+ return (lower.includes('connection reset') ||
28
+ lower.includes('econnreset') ||
29
+ lower.includes('timeout') ||
30
+ lower.includes('etimedout') ||
31
+ lower.includes('econnrefused') ||
32
+ lower.includes('socket hang up') ||
33
+ lower.includes('network') ||
34
+ lower.includes('503') ||
35
+ lower.includes('500') ||
36
+ lower.includes('502') ||
37
+ lower.includes('504') ||
38
+ lower.includes('service unavailable') ||
39
+ lower.includes('internal server error') ||
40
+ lower.includes('bad gateway') ||
41
+ lower.includes('gateway timeout') ||
42
+ lower.includes('transient'));
43
+ }
44
+ /** Retry an async operation with exponential backoff (500ms, 1000ms, 2000ms). */
45
+ export async function withRetry(fn, label, maxRetries = 3, baseDelayMs = 500) {
46
+ let lastError;
47
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
48
+ try {
49
+ return await fn();
50
+ }
51
+ catch (error) {
52
+ lastError = error;
53
+ if (attempt >= maxRetries || !isTransientError(error)) {
54
+ throw error;
55
+ }
56
+ const delay = baseDelayMs * Math.pow(2, attempt);
57
+ backupLog.warn(`${label} failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delay}ms`, { error: error instanceof Error ? error.message : String(error) });
58
+ await Bun.sleep(delay);
59
+ }
60
+ }
61
+ throw lastError;
62
+ }
63
+ /** Give every retry attempt its own configured operation timeout. */
64
+ export function retryWithTimeout(fn, timeoutMs, label) {
65
+ return withRetry(() => withTimeout(fn(), timeoutMs, label), label);
66
+ }
67
+ /** Async gzip compression using Web Streams. */
68
+ export async function gzipAsync(data) {
69
+ const stream = new Blob([data])
70
+ .stream()
71
+ .pipeThrough(new CompressionStream('gzip'));
72
+ return new Uint8Array(await new Response(stream).arrayBuffer());
73
+ }
74
+ /** Async gzip decompression using Web Streams. */
75
+ export async function gunzipAsync(data) {
76
+ const stream = new Blob([data])
77
+ .stream()
78
+ .pipeThrough(new DecompressionStream('gzip'));
79
+ return new Uint8Array(await new Response(stream).arrayBuffer());
80
+ }
81
+ export function sha256(data) {
82
+ const hasher = new Bun.CryptoHasher('sha256');
83
+ hasher.update(data);
84
+ return hasher.digest('hex');
85
+ }
86
+ /**
87
+ * Remove a payload that failed to publish, then its now-orphaned metadata.
88
+ *
89
+ * A locally timed-out PUT may still complete because Bun's S3 write cannot be
90
+ * aborted. In that case metadata must remain so a late payload is still paired.
91
+ */
92
+ export async function cleanupFailedPayload(client, key, timeoutMs, uploadError) {
93
+ if (uploadError instanceof Error && uploadError.message.includes('timed out after')) {
94
+ return;
95
+ }
96
+ try {
97
+ await retryWithTimeout(() => client.delete(key), timeoutMs, 'S3 failed payload cleanup');
98
+ await retryWithTimeout(() => client.delete(`${key}.meta.json`), timeoutMs, 'S3 orphaned metadata cleanup');
99
+ }
100
+ catch (error) {
101
+ backupLog.warn('Failed to clean incomplete backup publication', {
102
+ key,
103
+ error: String(error),
104
+ });
105
+ }
106
+ }
@@ -1,22 +1,13 @@
1
1
  /**
2
- * S3 Backup Operations
3
- * Core backup, restore, list, and cleanup operations
2
+ * S3 backup, restore, listing, and retention operations.
4
3
  */
5
4
  import type { S3Client } from 'bun';
6
- import type { S3BackupConfig, BackupResult, BackupItem } from './s3BackupConfig';
7
- /**
8
- * Perform a backup to S3
9
- */
5
+ import type { BackupItem, BackupResult, S3BackupConfig } from './s3BackupConfig';
6
+ /** Create and publish a consistent SQLite backup. */
10
7
  export declare function performBackup(config: S3BackupConfig, client: S3Client): Promise<BackupResult>;
11
- /**
12
- * List available backups
13
- */
8
+ /** List every backup object under the configured prefix. */
14
9
  export declare function listBackups(config: S3BackupConfig, client: S3Client): Promise<BackupItem[]>;
15
- /**
16
- * Restore from a backup
17
- */
10
+ /** Download, validate, and atomically install a backup. */
18
11
  export declare function restoreBackup(key: string, config: S3BackupConfig, client: S3Client): Promise<BackupResult>;
19
- /**
20
- * Clean up old backups based on retention policy
21
- */
12
+ /** Delete backup pairs beyond the configured retention count. */
22
13
  export declare function cleanupOldBackups(config: S3BackupConfig, client: S3Client): Promise<void>;