@nomideusz/svelte-media 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-08-03
4
+
5
+ ### Added
6
+ - **`derive` / `onDeriveError` on `MediaConfig`, and `derived` on `StoredMedia`.**
7
+ A hook over the decoded bytes during processing, so anything computed from the
8
+ image — placeholder, blurhash, dominant colour, EXIF — happens while the bytes
9
+ are in memory rather than fetching them back later. `TDerived` is inferred from
10
+ the callback, so the package takes no dependency on what you derive.
11
+
12
+ The hook runs after every size is stored, and failures deliberately do not
13
+ propagate: by then the upload has succeeded, and rejecting would make the
14
+ caller retry a completed upload and orphan the objects already written.
15
+ `derived` is left undefined and `onDeriveError` is called.
16
+
17
+
3
18
  ## 0.2.0 — 2026-08-03
4
19
 
5
20
  ### Changed
package/README.md CHANGED
@@ -58,6 +58,32 @@ touches a database.
58
58
  **Output is always WebP**, whatever was uploaded; `generateMediaKey` names every
59
59
  file `<cuid2>.webp` and ignores the original extension.
60
60
 
61
+ ## Deriving extras at upload time
62
+
63
+ `processAndStore` already has the decoded bytes in memory. A `derive` hook lets
64
+ you compute something from them in the same pass — a placeholder, a blurhash, a
65
+ dominant colour, EXIF — instead of fetching the image back later:
66
+
67
+ ```ts
68
+ import { generatePlaceholder } from '@nomideusz/svelte-geometrize/node';
69
+
70
+ const stored = await processAndStore(storage, file, 'tours', tourId, {
71
+ derive: ({ buffer }) => generatePlaceholder(buffer),
72
+ onDeriveError: (err) => log.warn('placeholder failed', err),
73
+ });
74
+
75
+ stored.derived; // GeometrizePlaceholder | undefined — inferred from the hook
76
+ ```
77
+
78
+ The hook runs **after** every size is stored, and **failures do not propagate**:
79
+ by that point the upload has succeeded, and rejecting would make the caller
80
+ retry a completed upload — orphaning the objects already written. A failing
81
+ hook leaves `derived` undefined and calls `onDeriveError`. Wire that to your
82
+ logger, or the failure is silent.
83
+
84
+ The package takes no dependency on whatever you derive; `TDerived` is inferred
85
+ from the callback's return type.
86
+
61
87
  ## Sizes
62
88
 
63
89
  `processAndStore` always writes four variants:
@@ -1,4 +1,4 @@
1
- import type { ImageSize, MediaConfig, ValidationResult } from './types.js';
1
+ import type { ImageSize, ValidationConfig, ValidationResult } from './types.js';
2
2
  export declare const DEFAULT_MAX_SIZE: number;
3
3
  export declare const DEFAULT_ALLOWED_TYPES: string[];
4
4
  export declare const IMAGE_SIZES: {
@@ -20,6 +20,6 @@ export declare const IMAGE_SIZES: {
20
20
  };
21
21
  export declare const SIZE_PREFIXES: Record<ImageSize, string>;
22
22
  export declare const SIZE_QUALITY: Record<ImageSize, number>;
23
- export declare function validateImageFile(file: File, config?: MediaConfig): ValidationResult;
23
+ export declare function validateImageFile(file: File, config?: ValidationConfig): ValidationResult;
24
24
  export declare function generateMediaKey(_originalName?: string): string;
25
25
  export declare function getStorageKey(prefix: string, entityId: string, filename: string, size: ImageSize): string;
@@ -1,4 +1,4 @@
1
1
  import type { StorageAdapter, StoredMedia, ImageSize, MediaConfig } from './types.js';
2
- export declare function processAndStore(adapter: StorageAdapter, file: File, prefix: string, entityId: string, config?: MediaConfig): Promise<StoredMedia>;
2
+ export declare function processAndStore<TDerived = never>(adapter: StorageAdapter, file: File, prefix: string, entityId: string, config?: MediaConfig<TDerived>): Promise<StoredMedia<TDerived>>;
3
3
  export declare function deleteMedia(adapter: StorageAdapter, prefix: string, entityId: string, filename: string): Promise<void>;
4
4
  export declare function getMediaUrl(adapter: StorageAdapter, prefix: string, entityId: string, filename: string, size?: ImageSize): string;
@@ -29,7 +29,20 @@ export async function processAndStore(adapter, file, prefix, entityId, config) {
29
29
  }
30
30
  // ponytail: existing objects stay JPEG under their old keys — URLs derive
31
31
  // from the stored filename, so old rows keep working without migration.
32
- return { filename, originalName: file.name, prefix, entityId, sizes };
32
+ // Runs last, so a throwing hook cannot orphan a half-written upload — and it
33
+ // is caught, because by this point the image is stored and rejecting would
34
+ // make the caller retry a completed upload.
35
+ let derived;
36
+ if (config?.derive) {
37
+ const source = { buffer, filename, mimeType: file.type };
38
+ try {
39
+ derived = await config.derive(source);
40
+ }
41
+ catch (error) {
42
+ config.onDeriveError?.(error, source);
43
+ }
44
+ }
45
+ return { filename, originalName: file.name, prefix, entityId, sizes, derived };
33
46
  }
34
47
  export async function deleteMedia(adapter, prefix, entityId, filename) {
35
48
  const keys = ['original', 'thumbnail', 'medium', 'large'].map((size) => getStorageKey(prefix, entityId, filename, size));
@@ -17,7 +17,7 @@ export interface S3Config {
17
17
  /** Use path-style access (default: true for MinIO/R2, set false for Railway/AWS) */
18
18
  forcePathStyle?: boolean;
19
19
  }
20
- export interface StoredMedia {
20
+ export interface StoredMedia<TDerived = never> {
21
21
  filename: string;
22
22
  originalName: string;
23
23
  /** Storage prefix, e.g. 'tours' or 'avatars' */
@@ -26,6 +26,11 @@ export interface StoredMedia {
26
26
  entityId: string;
27
27
  /** Storage keys for each size */
28
28
  sizes: Record<ImageSize, string>;
29
+ /**
30
+ * Result of `config.derive`, when supplied. Undefined if no hook was given,
31
+ * or if it threw — see `MediaConfig.onDeriveError`.
32
+ */
33
+ derived?: TDerived;
29
34
  }
30
35
  export interface MediaValidationError {
31
36
  isValid: false;
@@ -41,7 +46,35 @@ export interface ValidationResult {
41
46
  valid: boolean;
42
47
  error?: string;
43
48
  }
44
- export interface MediaConfig {
49
+ /** What a `derive` hook receives — the image as uploaded, before any resizing. */
50
+ export interface DeriveSource {
51
+ /** Original bytes, pre-resize, EXIF intact. */
52
+ buffer: Buffer;
53
+ /** The generated storage filename, e.g. `k7x2m9.webp`. */
54
+ filename: string;
55
+ /** The uploaded file's MIME type. */
56
+ mimeType: string;
57
+ }
58
+ /** The validation half of MediaConfig — all `validateImageFile` needs. */
59
+ export interface ValidationConfig {
45
60
  maxFileSize?: number;
46
61
  allowedTypes?: string[];
47
62
  }
63
+ export interface MediaConfig<TDerived = never> extends ValidationConfig {
64
+ /**
65
+ * Optional side-computation over the image, run once during processing while
66
+ * the bytes are already in hand — a placeholder, a blurhash, a dominant
67
+ * colour, EXIF. Whatever it returns is attached to the result as `derived`.
68
+ *
69
+ * Runs after every size has been stored, so a failure cannot orphan a
70
+ * half-written upload. Failures do not propagate: the upload has already
71
+ * succeeded and a derived extra must never invalidate it. `derived` is left
72
+ * undefined and `onDeriveError` is called.
73
+ */
74
+ derive?(source: DeriveSource): Promise<TDerived>;
75
+ /**
76
+ * Called when `derive` throws. The upload still succeeds. Without this the
77
+ * failure is silent, so wire it to your logger.
78
+ */
79
+ onDeriveError?(error: unknown, source: DeriveSource): void;
80
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { StorageAdapter, S3Config, StoredMedia, ImageSize, MediaConfig, ValidationResult, } from './core/types.js';
1
+ export type { StorageAdapter, S3Config, StoredMedia, ImageSize, MediaConfig, ValidationResult, ValidationConfig, DeriveSource, } from './core/types.js';
2
2
  export { validateImageFile, generateMediaKey, getStorageKey, IMAGE_SIZES, DEFAULT_MAX_SIZE, DEFAULT_ALLOWED_TYPES, } from './core/media.js';
3
3
  export { default as ImageUpload } from './components/ImageUpload.svelte';
4
4
  export { default as ImageGallery } from './components/ImageGallery.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomideusz/svelte-media",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Image upload, processing, and S3-compatible storage for Svelte 5 apps.",
5
5
  "type": "module",
6
6
  "sideEffects": false,