@devultur/convex 1.2.0 → 1.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/README.md CHANGED
@@ -83,8 +83,8 @@ await saveCover({ cover: media }); // a DevulturMedia: stores as-is in a dv.medi
83
83
  ```
84
84
 
85
85
  The PUT goes through `XMLHttpRequest` (`fetch` reports no upload progress), in parts for large files. `key` is an
86
- optional field of `DevulturMedia` and of `dv.media()` (since 1.1.0), so references stored without it stay valid. It
87
- is informational: delivery URLs are still derived from `id`, `kind`, `visibility` and `filename`.
86
+ optional field of `DevulturMedia` and of `dv.media()`. It is informational: delivery URLs are derived from `id`,
87
+ `kind`, `visibility` and `filename`.
88
88
 
89
89
  ## Media library
90
90
 
@@ -148,12 +148,11 @@ exposed functions: export your own action that checks `ctx.auth` and then calls
148
148
  The caller only names the key: the record's visibility, size and content type come from the Worker's `HEAD` of the
149
149
  object, never from the arguments.
150
150
 
151
- `deleteMedia` removes an image's or a file's record once
152
- the Worker deleted the object (a delete refused by a lock keeps it). `deleteVideo` removes a video's.
151
+ `deleteMedia` removes an image's or a file's record once the Worker
152
+ deleted the object (a delete refused by a lock keeps it). `deleteVideo` removes a video's.
153
153
 
154
- Records from before 1.1.0 (and a video only `processVideo` recorded) have no `visibility` or `objectKey`;
155
- `mediaFromRecord` returns `null` for them. Nothing needs migrating: the new fields are optional. `listMedia` doesn't
156
- report an object's lock: read it from an action with `devultur.getLock(ctx, record.objectKey)`.
154
+ A video only `processVideo` recorded has no `visibility` or `objectKey`, and `mediaFromRecord` returns `null` for
155
+ it. `listMedia` doesn't report an object's lock: read it from an action with `devultur.getLock(ctx, record.objectKey)`.
157
156
 
158
157
  ## Using a self-hosted Worker
159
158
 
@@ -0,0 +1,43 @@
1
+ interface PdfViewport {
2
+ width: number;
3
+ height: number;
4
+ }
5
+ interface PdfRenderTask {
6
+ promise: Promise<void>;
7
+ cancel(): void;
8
+ }
9
+ interface PdfPage {
10
+ getViewport(parameters: {
11
+ scale: number;
12
+ }): PdfViewport;
13
+ render(parameters: {
14
+ canvas: HTMLCanvasElement | null;
15
+ viewport: PdfViewport;
16
+ }): PdfRenderTask;
17
+ getTextContent(): Promise<{
18
+ items: ReadonlyArray<object>;
19
+ }>;
20
+ cleanup(): unknown;
21
+ }
22
+ interface PdfDocument {
23
+ numPages: number;
24
+ getPage(pageNumber: number): Promise<PdfPage>;
25
+ }
26
+ interface PdfLoadingTask {
27
+ promise: Promise<PdfDocument>;
28
+ /** Destroys the document and its worker (pdf.js 6 has no `PDFDocumentProxy.destroy`). */
29
+ destroy(): Promise<void>;
30
+ }
31
+ /** The parts of `pdfjs-dist` (legacy build) that the previews use. */
32
+ interface PdfjsModule {
33
+ GlobalWorkerOptions: {
34
+ workerSrc: string;
35
+ };
36
+ getDocument(source: {
37
+ data: Uint8Array;
38
+ }): PdfLoadingTask;
39
+ }
40
+ /** Imports pdf.js, e.g. `() => import("pdfjs-dist/legacy/build/pdf.mjs")`. */
41
+ type PdfjsLoader = () => Promise<PdfjsModule>;
42
+
43
+ export type { PdfjsModule as P, PdfjsLoader as a };
@@ -0,0 +1,11 @@
1
+ import { P as PdfjsModule } from './pdf-render-CiCK6V1X.js';
2
+
3
+ /** The worker next to the legacy build, resolved by your bundler from this package. */
4
+ declare const defaultPdfWorkerSrc: () => string;
5
+ /**
6
+ * Imports the pdf.js legacy build (Safari and iOS 18+; the modern build needs iOS 26.2) and, unless a worker is set
7
+ * already, points `GlobalWorkerOptions.workerSrc` at `defaultPdfWorkerSrc()`.
8
+ */
9
+ declare function loadPdfjs(): Promise<PdfjsModule>;
10
+
11
+ export { defaultPdfWorkerSrc, loadPdfjs };
package/dist/pdfjs.js ADDED
@@ -0,0 +1,11 @@
1
+ // src/pdfjs.ts
2
+ var defaultPdfWorkerSrc = () => new URL("pdfjs-dist/legacy/build/pdf.worker.min.mjs", import.meta.url).href;
3
+ async function loadPdfjs() {
4
+ const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
5
+ if (!pdfjs.GlobalWorkerOptions.workerSrc) pdfjs.GlobalWorkerOptions.workerSrc = defaultPdfWorkerSrc();
6
+ return pdfjs;
7
+ }
8
+ export {
9
+ defaultPdfWorkerSrc,
10
+ loadPdfjs
11
+ };
package/dist/react.d.ts CHANGED
@@ -3,6 +3,8 @@ import { Visibility, DevulturMedia, MediaUrlOptions, ProcessingPhase, FailureRea
3
3
  import { BakeOutputFormat, MediaUploadState, MediaUploadError, UploadProgress } from '@devultur/react';
4
4
  export { MediaUploadError, MediaUploadState, UploadProgress } from '@devultur/react';
5
5
  import { FunctionReference } from 'convex/server';
6
+ import { a as PdfjsLoader } from './pdf-render-CiCK6V1X.js';
7
+ export { P as PdfjsModule } from './pdf-render-CiCK6V1X.js';
6
8
 
7
9
  interface CropUploadRenderProps {
8
10
  /** The state machine of the full cycle (idle → uploading → ready / failed). */
@@ -51,6 +53,146 @@ interface CropUploadProps {
51
53
  */
52
54
  declare function CropUpload({ aspectRatio, visibility, outputFormat, maxWidth, minSourceWidth, replaces, onComplete, onReject, accept, cropperClassName, labels, children, }: CropUploadProps): React.JSX.Element;
53
55
 
56
+ /** Why a media fetch was refused. Stable: match on it, not on `message`. */
57
+ type MediaFetchErrorCode =
58
+ /** The delivery answered with a non-2xx status (`status` has it). */
59
+ "http_error"
60
+ /** `Content-Length`, or the streamed body, exceeds `maxBytes`. */
61
+ | "too_large"
62
+ /** The expected content type isn't one that can be previewed inline. */
63
+ | "unsupported_type"
64
+ /** The response's `Content-Type` doesn't agree with the expected one. */
65
+ | "type_mismatch";
66
+ declare class MediaFetchError extends Error {
67
+ readonly code: MediaFetchErrorCode;
68
+ /** HTTP status, for `http_error`. */
69
+ readonly status?: number;
70
+ constructor(code: MediaFetchErrorCode, message: string, status?: number);
71
+ }
72
+
73
+ interface UseMediaObjectUrlOptions {
74
+ /** `false` → nothing is fetched (e.g. a list row that isn't selected). Default `true`. */
75
+ enabled?: boolean;
76
+ /** Refuses bodies larger than this, by `Content-Length` and while streaming (`MediaFetchError` `too_large`). */
77
+ maxBytes?: number;
78
+ /**
79
+ * The type you know the media has. The response's `Content-Type` must agree with it, and the object URL is
80
+ * created with it (`type_mismatch` / `unsupported_type` otherwise). Without it, the object URL keeps the
81
+ * response's type only when previewable (see the guide's allowlist), else `application/octet-stream`.
82
+ * With it, public media is fetched and verified too, instead of returning its unverified delivery URL.
83
+ */
84
+ contentType?: string;
85
+ }
86
+ interface MediaObjectUrl {
87
+ /**
88
+ * `blob:` URL for private media (and public media with `contentType`); the delivery URL for other public media,
89
+ * whose type is whatever the server says, so don't frame it. `null` while loading, disabled or failed.
90
+ */
91
+ objectUrl: string | null;
92
+ /** The type the object URL was created with (not the server's verbatim). `null` for an unverified public URL. */
93
+ contentType: string | null;
94
+ isLoading: boolean;
95
+ /** A `MediaFetchError` (stable `code`), or a network error. */
96
+ error: Error | null;
97
+ }
98
+ /**
99
+ * An in-memory URL for any media, for elements that can't send headers (`<iframe>`, `<audio>`, `<img>`). Private
100
+ * media is fetched with the session header (one re-issue and retry on a 401 `session_mismatch`) into an object
101
+ * URL that is revoked when the media changes and on unmount. Public media returns its delivery URL, unfetched,
102
+ * unless `contentType` asks for it to be verified.
103
+ *
104
+ * @example
105
+ * const { objectUrl, error } = useMediaObjectUrl(document.file, { contentType: "application/pdf", maxBytes: 20e6 });
106
+ * return objectUrl && <iframe src={objectUrl} title={document.file.filename} />;
107
+ */
108
+ declare function useMediaObjectUrl(media: DevulturMedia | null | undefined, options?: UseMediaObjectUrlOptions): MediaObjectUrl;
109
+
110
+ interface PdfPreviewConfig {
111
+ /**
112
+ * Imports pdf.js. Without it, PDFs render in an `<iframe>` (the browser's own viewer) and `<PdfThumbnail>` renders
113
+ * its fallback. `loadPdfjs` from `@devultur/convex/pdfjs` imports the legacy build and sets a default worker.
114
+ */
115
+ pdfjs?: PdfjsLoader;
116
+ /** URL of `pdf.worker.min.mjs`, set on `GlobalWorkerOptions.workerSrc` before a document opens. */
117
+ workerSrc?: string;
118
+ }
119
+ /**
120
+ * Renders the PDFs of every `<Preview>` and `<PdfThumbnail>` below it with pdf.js, on a `<canvas>`.
121
+ *
122
+ * @example
123
+ * import { loadPdfjs } from "@devultur/convex/pdfjs";
124
+ * <PdfPreviewProvider pdfjs={loadPdfjs}>{children}</PdfPreviewProvider>
125
+ */
126
+ declare function PdfPreviewProvider({ pdfjs, workerSrc, children }: PdfPreviewConfig & {
127
+ children: React.ReactNode;
128
+ }): React.JSX.Element;
129
+
130
+ interface PdfThumbnailProps {
131
+ /** Typed reference. `null`/`undefined` → renders nothing. */
132
+ media: DevulturMedia | null | undefined;
133
+ /** The type you know the media has. Default: inferred from `media.filename`'s extension. */
134
+ contentType?: string;
135
+ /** CSS width of the thumbnail in pixels; the height follows the page. Default 96. */
136
+ width?: number;
137
+ /** Larger files render `fallback`. Default 50 MiB. */
138
+ maxBytes?: number;
139
+ /** Shown when the media isn't a PDF, pdf.js isn't available (no `PdfPreviewProvider`) or loading fails. */
140
+ fallback?: React.ReactNode;
141
+ /** Shown until page 1 is drawn, including while off-screen. Default: nothing. */
142
+ loading?: React.ReactNode;
143
+ /** Accessible name of the thumbnail. Default: the filename. */
144
+ title?: string;
145
+ className?: string;
146
+ /** Overrides the provider's `workerSrc`. */
147
+ pdfWorkerSrc?: string;
148
+ }
149
+ /**
150
+ * Page 1 of a PDF on a small canvas, for lists and upload queues. Nothing is fetched until it nears the viewport.
151
+ * Needs pdf.js from a `PdfPreviewProvider`; without it, renders `fallback`.
152
+ *
153
+ * @example
154
+ * <PdfThumbnail media={row.file} width={64} fallback={<FileIcon />} />
155
+ */
156
+ declare function PdfThumbnail(props: PdfThumbnailProps): React.JSX.Element | null;
157
+
158
+ interface PreviewProps {
159
+ /** Typed reference. `null`/`undefined` → renders nothing. */
160
+ media: DevulturMedia | null | undefined;
161
+ /** The type you know the media has (e.g. stored with it). Default: inferred from `media.filename`'s extension. */
162
+ contentType?: string;
163
+ /** Size cap for images, PDFs and audio; larger files render `fallback`. Default 50 MiB. */
164
+ maxBytes?: number;
165
+ /** Size cap for text previews. Default 1 MiB. */
166
+ maxTextBytes?: number;
167
+ /** Shown for types that don't preview inline and when the fetch fails. Default: a `<DownloadLink>`. */
168
+ fallback?: React.ReactNode;
169
+ /** Shown while a private file is loading. Default: nothing. */
170
+ loading?: React.ReactNode;
171
+ /** Accessible name of the preview (`alt`, `title`, `aria-label`). Default: the filename (text: none). */
172
+ title?: string;
173
+ className?: string;
174
+ /**
175
+ * PDF pages rendered with pdf.js; past it, a "first N of M pages" note and `fallback`. Default 500 (a tiny file
176
+ * can declare millions of pages).
177
+ */
178
+ maxPages?: number;
179
+ /** URL of pdf.js's worker for this preview; overrides `PdfPreviewProvider`'s `workerSrc`. */
180
+ pdfWorkerSrc?: string;
181
+ }
182
+ /**
183
+ * Renders a media inline by its content type: images in an `<img>`, PDFs on `<canvas>` pages with pdf.js under a
184
+ * `PdfPreviewProvider` (otherwise in an `<iframe>`), audio in an `<audio>`,
185
+ * plain text, CSV and JSON in a `<pre>`, and video in the streaming `<VideoPlayer>`. Everything else (HTML, SVG,
186
+ * XML, unknown types) and every refused fetch render `fallback`. Private media is token-transparent.
187
+ *
188
+ * @example
189
+ * <Preview media={document.file} maxBytes={20_000_000} className="h-[80vh] w-full" />
190
+ */
191
+ declare function Preview({ media, contentType, fallback, ...viewProps }: PreviewProps): React.JSX.Element | null;
192
+
193
+ /** How a content type renders inline: `<img>`, a PDF view (pdf.js canvases or `<iframe>`), `<audio>` or `<pre>`. */
194
+ type PreviewKind = "image" | "pdf" | "audio" | "text";
195
+
54
196
  /** Transform/delivery options for media URLs (without `token`, which the context injects). */
55
197
  type MediaRenderOptions = Omit<MediaUrlOptions, "token">;
56
198
  type AnyAction = FunctionReference<"action">;
@@ -291,4 +433,4 @@ interface UseMediaUploadReturn {
291
433
  */
292
434
  declare function useMediaUpload(options?: UseMediaUploadOptions): UseMediaUploadReturn;
293
435
 
294
- export { type CaptionTrack, CropUpload, type CropUploadLabels, type CropUploadProps, type CropUploadRenderProps, DevulturProvider, DownloadLink, Image, type UploadedMedia, type UseMediaUploadOptions, type UseMediaUploadReturn, type VideoProps, type VideoState, useDevultur, useDevulturMedia, useDevulturProgress, useMediaUpload, useMediaUrl, useVideoProps, useVideoState };
436
+ export { type CaptionTrack, CropUpload, type CropUploadLabels, type CropUploadProps, type CropUploadRenderProps, DevulturProvider, DownloadLink, Image, MediaFetchError, type MediaFetchErrorCode, type MediaObjectUrl, type PdfPreviewConfig, PdfPreviewProvider, PdfThumbnail, type PdfThumbnailProps, PdfjsLoader, Preview, type PreviewKind, type PreviewProps, type UploadedMedia, type UseMediaObjectUrlOptions, type UseMediaUploadOptions, type UseMediaUploadReturn, type VideoProps, type VideoState, useDevultur, useDevulturMedia, useDevulturProgress, useMediaObjectUrl, useMediaUpload, useMediaUrl, useVideoProps, useVideoState };