mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 23:56:43 +02:00
feat(storage): add upload hooks, metrics integration, and presigned multipart
Upload hooks: - StorageHooks class with fire-and-forget event emitter pattern - Events: upload, upload:error, delete, delete:error, download - All StorageClient operations now emit appropriate events - Unsubscribe functions for cleanup Metrics: - StorageMetricsCollector interface (decoupled from prom-client) - InMemoryMetrics for testing and local dev - attachMetrics() wires hooks to any collector automatically - Backends can create a Prometheus collector via MetricsService Presigned multipart upload (browser direct-upload): - createMultipartUpload() initiates and returns uploadId - getMultipartUploadUrls() generates presigned PUT URLs per part - completeMultipartUpload() finalizes with part ETags - abortMultipartUpload() for cleanup on abandoned uploads 90 tests passing across 5 test files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
822e75368a
commit
b0e5a9c5ff
8 changed files with 708 additions and 28 deletions
91
packages/shared-storage/src/hooks.ts
Normal file
91
packages/shared-storage/src/hooks.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import type { UploadResult } from './types';
|
||||
|
||||
/**
|
||||
* Storage event types
|
||||
*/
|
||||
export type StorageEventType = 'upload' | 'upload:error' | 'delete' | 'delete:error' | 'download';
|
||||
|
||||
/**
|
||||
* Payload for upload events
|
||||
*/
|
||||
export interface UploadEventPayload {
|
||||
bucket: string;
|
||||
key: string;
|
||||
sizeBytes?: number;
|
||||
contentType?: string;
|
||||
result?: UploadResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for delete events
|
||||
*/
|
||||
export interface DeleteEventPayload {
|
||||
bucket: string;
|
||||
keys: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for download events
|
||||
*/
|
||||
export interface DownloadEventPayload {
|
||||
bucket: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for error events
|
||||
*/
|
||||
export interface ErrorEventPayload {
|
||||
bucket: string;
|
||||
key?: string;
|
||||
error: Error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event payload map
|
||||
*/
|
||||
export interface StorageEventMap {
|
||||
upload: UploadEventPayload;
|
||||
'upload:error': ErrorEventPayload;
|
||||
delete: DeleteEventPayload;
|
||||
'delete:error': ErrorEventPayload;
|
||||
download: DownloadEventPayload;
|
||||
}
|
||||
|
||||
export type StorageHook<T extends StorageEventType> = (payload: StorageEventMap[T]) => void;
|
||||
|
||||
/**
|
||||
* Simple event emitter for storage lifecycle hooks.
|
||||
* Hooks are fire-and-forget — errors in hooks do not affect storage operations.
|
||||
*/
|
||||
export class StorageHooks {
|
||||
private listeners = new Map<StorageEventType, Set<StorageHook<StorageEventType>>>();
|
||||
|
||||
on<T extends StorageEventType>(event: T, hook: StorageHook<T>): () => void {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, new Set());
|
||||
}
|
||||
const set = this.listeners.get(event) as Set<StorageHook<T>>;
|
||||
set.add(hook);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => set.delete(hook);
|
||||
}
|
||||
|
||||
emit<T extends StorageEventType>(event: T, payload: StorageEventMap[T]): void {
|
||||
const hooks = this.listeners.get(event);
|
||||
if (!hooks) return;
|
||||
|
||||
for (const hook of hooks) {
|
||||
try {
|
||||
hook(payload);
|
||||
} catch {
|
||||
// Hooks are fire-and-forget — swallow errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeAll(): void {
|
||||
this.listeners.clear();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue