@devultur/convex 1.3.1 → 1.3.2

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
@@ -86,6 +86,42 @@ The PUT goes through `XMLHttpRequest` (`fetch` reports no upload progress), in p
86
86
  optional field of `DevulturMedia` and of `dv.media()`. It is informational: delivery URLs are derived from `id`,
87
87
  `kind`, `visibility` and `filename`.
88
88
 
89
+ ## Multiple workspaces
90
+
91
+ An app that shows private media from several workspaces (an admin console that previews each one, for example) can
92
+ send extra arguments with every viewer token request. Pass them in `tokenArgs`, and pass your own action in
93
+ `issueViewerToken`: the component's `issueViewerToken` accepts only `sessionSecret`.
94
+
95
+ ```typescript
96
+ // convex/admin.ts
97
+ import { v } from "convex/values";
98
+ import { components } from "./_generated/api";
99
+ import { action } from "./_generated/server";
100
+
101
+ export const issueWorkspaceViewerToken = action({
102
+ args: { sessionSecret: v.string(), workspaceId: v.id("workspaces") },
103
+ handler: async (ctx, { sessionSecret, workspaceId }) => {
104
+ await assertWorkspaceAccess(ctx, workspaceId); // your own check
105
+ return ctx.runAction(components.devultur.public.issueViewerToken, { sessionSecret });
106
+ },
107
+ });
108
+ ```
109
+
110
+ ```tsx
111
+ <DevulturProvider
112
+ api={api.devultur}
113
+ issueViewerToken={api.admin.issueWorkspaceViewerToken}
114
+ tokenArgs={{ workspaceId }}
115
+ >
116
+ <WorkspaceMedia />
117
+ </DevulturProvider>
118
+ ```
119
+
120
+ The provider sends `{ sessionSecret, ...tokenArgs }` on the first issuance, on each renewal and on each re-issue.
121
+ `tokenArgs` are compared by value, so a new object with the same values changes nothing. When they change, the
122
+ provider drops the current token, private media wait for the new one, and a response for the previous arguments is
123
+ ignored. Your action returns what the component's action returns: `{ token, expiresIn }` or the token string.
124
+
89
125
  ## Media library
90
126
 
91
127
  Every upload, whatever its kind, gets a record in the component's `media` table once its bytes are stored, so
package/dist/react.d.ts CHANGED
@@ -3,9 +3,18 @@ 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 { Value } from 'convex/values';
6
7
  import { a as PdfjsLoader } from './pdf-render-CiCK6V1X.js';
7
8
  export { P as PdfjsModule } from './pdf-render-CiCK6V1X.js';
8
9
 
10
+ /** What a viewer token action returns: the token with its lifetime, or the bare token (default lifetime). */
11
+ type ViewerTokenResult = {
12
+ token: string;
13
+ expiresIn: number;
14
+ } | string;
15
+ /** Extra arguments sent with `sessionSecret` to the viewer token action. */
16
+ type ViewerTokenArgs = Record<string, Value>;
17
+
9
18
  interface CropUploadRenderProps {
10
19
  /** The state machine of the full cycle (idle → uploading → ready / failed). */
11
20
  state: MediaUploadState;
@@ -304,9 +313,23 @@ interface DevulturProviderProps {
304
313
  api: DevulturApi;
305
314
  /** Media worker base (e.g. `https://media.your-domain.com`). Required for `useMediaUrl`. */
306
315
  baseUrl?: string;
316
+ /**
317
+ * Extra arguments sent with `sessionSecret` on every viewer token issuance (first, renewal and re-issue), for
318
+ * example `{ workspaceId }`. Compared by value: when they change, the current token is dropped and a new one is
319
+ * issued. The component's own `issueViewerToken` accepts only `sessionSecret`, so pass your own action in
320
+ * `issueViewerToken` that takes these arguments.
321
+ */
322
+ tokenArgs?: ViewerTokenArgs;
323
+ /**
324
+ * Your own viewer token action, used instead of `api.issueViewerToken`. It receives `{ sessionSecret, ...tokenArgs }`
325
+ * and returns what the component's `issueViewerToken` returns.
326
+ */
327
+ issueViewerToken?: FunctionReference<"action", "public", {
328
+ sessionSecret: string;
329
+ }>;
307
330
  children: React.ReactNode;
308
331
  }
309
- declare function DevulturProvider({ api, baseUrl, children }: DevulturProviderProps): React.JSX.Element;
332
+ declare function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children }: DevulturProviderProps): React.JSX.Element;
310
333
  declare function useDevultur(): DevulturContextValue;
311
334
  declare function useDevulturProgress(): ProgressContextValue;
312
335
  /**
@@ -471,4 +494,4 @@ interface UseMediaUploadReturn {
471
494
  */
472
495
  declare function useMediaUpload(options?: UseMediaUploadOptions): UseMediaUploadReturn;
473
496
 
474
- 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 PreviewLabels, type PreviewProps, type UploadedMedia, type UseMediaObjectUrlOptions, type UseMediaUploadOptions, type UseMediaUploadReturn, type VideoProps, type VideoState, defaultPreviewLabels, useDevultur, useDevulturMedia, useDevulturProgress, useMediaObjectUrl, useMediaUpload, useMediaUrl, useVideoProps, useVideoState };
497
+ 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 PreviewLabels, type PreviewProps, type UploadedMedia, type UseMediaObjectUrlOptions, type UseMediaUploadOptions, type UseMediaUploadReturn, type VideoProps, type VideoState, type ViewerTokenArgs, type ViewerTokenResult, defaultPreviewLabels, useDevultur, useDevulturMedia, useDevulturProgress, useMediaObjectUrl, useMediaUpload, useMediaUrl, useVideoProps, useVideoState };
package/dist/react.js CHANGED
@@ -1,11 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  // src/react.tsx
4
- import React7 from "react";
4
+ import React8 from "react";
5
5
  import {
6
6
  buildMediaUrl,
7
7
  CaptionLocaleLabel,
8
- DEFAULT_TOKEN_EXPIRY_SECONDS,
9
8
  MediaStatus,
10
9
  mediaCaptionPath,
11
10
  mediaPath as mediaPath2,
@@ -214,8 +213,76 @@ function deriveUploadState(local, videoState, retry) {
214
213
  }
215
214
  }
216
215
 
217
- // src/crop-upload.tsx
216
+ // src/viewer-token.ts
218
217
  import React from "react";
218
+ import { DEFAULT_TOKEN_EXPIRY_SECONDS } from "@devultur/core";
219
+ import { convexToJson } from "convex/values";
220
+ var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
221
+ var MIN_REFRESH_MS = 6e4;
222
+ var DEFAULT_TTL_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3;
223
+ function extractTokenResult(result) {
224
+ const tokenValue = typeof result === "string" ? result : result.token;
225
+ const ttlMs = typeof result === "string" ? DEFAULT_TTL_MS : result.expiresIn * 1e3;
226
+ return { tokenValue, ttlMs };
227
+ }
228
+ function sortKeys(json) {
229
+ if (Array.isArray(json)) return json.map(sortKeys);
230
+ if (json === null || typeof json !== "object") return json;
231
+ return Object.fromEntries(
232
+ Object.keys(json).sort().map((name) => [name, sortKeys(json[name])])
233
+ );
234
+ }
235
+ function tokenArgsKey(tokenArgs) {
236
+ return JSON.stringify(sortKeys(convexToJson(tokenArgs ?? {})));
237
+ }
238
+ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs }) {
239
+ const argsKey = tokenArgsKey(tokenArgs);
240
+ const stableArgs = React.useMemo(() => tokenArgs ?? {}, [argsKey]);
241
+ const [issued, setIssued] = React.useState(null);
242
+ const token = issued?.argsKey === argsKey ? issued.token : null;
243
+ const currentArgsKeyRef = React.useRef(argsKey);
244
+ const refreshTimerRef = React.useRef(null);
245
+ const inFlightRef = React.useRef(null);
246
+ React.useEffect(() => {
247
+ currentArgsKeyRef.current = argsKey;
248
+ }, [argsKey]);
249
+ React.useEffect(() => {
250
+ return () => {
251
+ if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
252
+ };
253
+ }, []);
254
+ const request = React.useCallback(() => {
255
+ const renew = () => void request();
256
+ return issueViewerToken({ ...stableArgs, sessionSecret }).then((result) => {
257
+ if (currentArgsKeyRef.current !== argsKey) return null;
258
+ const { tokenValue, ttlMs } = extractTokenResult(result);
259
+ setIssued({ token: tokenValue, argsKey });
260
+ if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
261
+ refreshTimerRef.current = setTimeout(renew, Math.max(ttlMs - REFRESH_BUFFER_MS, MIN_REFRESH_MS));
262
+ return tokenValue;
263
+ });
264
+ }, [issueViewerToken, stableArgs, argsKey, sessionSecret]);
265
+ const issueOnce = React.useCallback(() => {
266
+ const inFlight = inFlightRef.current;
267
+ if (inFlight?.argsKey === argsKey) return inFlight.promise;
268
+ const promise = request();
269
+ const settle = () => {
270
+ if (inFlightRef.current?.promise === promise) inFlightRef.current = null;
271
+ };
272
+ promise.then(settle, settle);
273
+ inFlightRef.current = { argsKey, promise };
274
+ return promise;
275
+ }, [request, argsKey]);
276
+ React.useEffect(() => {
277
+ if (token) return;
278
+ issueOnce();
279
+ }, [token, issueOnce]);
280
+ const reissueToken = React.useCallback(() => issueOnce().catch(() => null), [issueOnce]);
281
+ return { token, reissueToken };
282
+ }
283
+
284
+ // src/crop-upload.tsx
285
+ import React2 from "react";
219
286
  import { ImageCropper } from "@devultur/react";
220
287
  import { jsx, jsxs } from "react/jsx-runtime";
221
288
  var DEFAULT_LABELS = {
@@ -239,18 +306,18 @@ function CropUpload({
239
306
  children
240
307
  }) {
241
308
  const text = { ...DEFAULT_LABELS, ...labels };
242
- const [pendingFile, setPendingFile] = React.useState(null);
243
- const inputRef = React.useRef(null);
309
+ const [pendingFile, setPendingFile] = React2.useState(null);
310
+ const inputRef = React2.useRef(null);
244
311
  const { upload, state, cancel, reset } = useMediaUpload({ visibility, replaces, onComplete });
245
- const pickFile = React.useCallback(() => inputRef.current?.click(), []);
246
- const handleCrop = React.useCallback(
312
+ const pickFile = React2.useCallback(() => inputRef.current?.click(), []);
313
+ const handleCrop = React2.useCallback(
247
314
  (baked) => {
248
315
  setPendingFile(null);
249
316
  upload(baked);
250
317
  },
251
318
  [upload]
252
319
  );
253
- const resetAll = React.useCallback(() => {
320
+ const resetAll = React2.useCallback(() => {
254
321
  setPendingFile(null);
255
322
  reset();
256
323
  }, [reset]);
@@ -303,7 +370,7 @@ function CropUpload({
303
370
  }
304
371
 
305
372
  // src/media-resource.ts
306
- import React2 from "react";
373
+ import React3 from "react";
307
374
  import { mediaPath, mediaUrl } from "@devultur/core";
308
375
  var IDLE = { value: null, contentType: null, isLoading: false, error: null };
309
376
  var LOADING = { value: null, contentType: null, isLoading: true, error: null };
@@ -313,12 +380,12 @@ function useMediaResource(media, options) {
313
380
  const isPrivate = media?.visibility === "private";
314
381
  const isReady = !isPrivate || devultur.token !== null;
315
382
  const requestKey = media && isEnabled ? [devultur.baseUrl, media.visibility, mediaPath(media), contentType ?? "", maxBytes ?? ""].join("|") : null;
316
- const [settled, setSettled] = React2.useState(null);
317
- const inputsRef = React2.useRef({ media, devultur, options });
318
- React2.useEffect(() => {
383
+ const [settled, setSettled] = React3.useState(null);
384
+ const inputsRef = React3.useRef({ media, devultur, options });
385
+ React3.useEffect(() => {
319
386
  inputsRef.current = { media, devultur, options };
320
387
  });
321
- React2.useEffect(() => {
388
+ React3.useEffect(() => {
322
389
  if (!requestKey || !isReady) return;
323
390
  const { media: target, devultur: context, options: current } = inputsRef.current;
324
391
  if (!target || !context.baseUrl) return;
@@ -380,7 +447,7 @@ function useMediaObjectUrl(media, options) {
380
447
  }
381
448
 
382
449
  // src/pdf-context.tsx
383
- import React3 from "react";
450
+ import React4 from "react";
384
451
 
385
452
  // src/preview-labels.ts
386
453
  var ERROR_TEXT = {
@@ -412,14 +479,14 @@ function mergePreviewLabels(...sources) {
412
479
 
413
480
  // src/pdf-context.tsx
414
481
  import { jsx as jsx2 } from "react/jsx-runtime";
415
- var PdfPreviewContext = React3.createContext({});
482
+ var PdfPreviewContext = React4.createContext({});
416
483
  function PdfPreviewProvider({
417
484
  pdfjs,
418
485
  workerSrc,
419
486
  labels,
420
487
  children
421
488
  }) {
422
- const config = React3.useMemo(() => ({ pdfjs, workerSrc, labels }), [pdfjs, workerSrc, labels]);
489
+ const config = React4.useMemo(() => ({ pdfjs, workerSrc, labels }), [pdfjs, workerSrc, labels]);
423
490
  return /* @__PURE__ */ jsx2(PdfPreviewContext.Provider, { value: config, children });
424
491
  }
425
492
  var loadedModules = /* @__PURE__ */ new WeakMap();
@@ -431,9 +498,9 @@ function loadOnce(loader) {
431
498
  return loading;
432
499
  }
433
500
  function usePdfjs(workerSrcOverride) {
434
- const { pdfjs: loader, workerSrc } = React3.useContext(PdfPreviewContext);
435
- const [loaded, setLoaded] = React3.useState(null);
436
- React3.useEffect(() => {
501
+ const { pdfjs: loader, workerSrc } = React4.useContext(PdfPreviewContext);
502
+ const [loaded, setLoaded] = React4.useState(null);
503
+ React4.useEffect(() => {
437
504
  if (!loader) return;
438
505
  let isCurrent = true;
439
506
  loadOnce(loader).then((pdfjs) => {
@@ -449,15 +516,15 @@ function usePdfjs(workerSrcOverride) {
449
516
  return { status: "ready", pdfjs: loaded.pdfjs, workerSrc: workerSrcOverride ?? workerSrc };
450
517
  }
451
518
  function usePreviewLabels(labels) {
452
- const providerLabels = React3.useContext(PdfPreviewContext).labels;
519
+ const providerLabels = React4.useContext(PdfPreviewContext).labels;
453
520
  return mergePreviewLabels(providerLabels, labels);
454
521
  }
455
522
 
456
523
  // src/pdf-thumbnail.tsx
457
- import React6 from "react";
524
+ import React7 from "react";
458
525
 
459
526
  // src/pdf-hooks.ts
460
- import React4 from "react";
527
+ import React5 from "react";
461
528
 
462
529
  // src/pdf-render.ts
463
530
  var MOBILE_MAX_CANVAS_PIXELS = 5242880;
@@ -501,8 +568,8 @@ var isRenderCancelled = (error) => error instanceof Error && error.name === "Ren
501
568
 
502
569
  // src/pdf-hooks.ts
503
570
  function useIsVisible(ref, rootMargin = "200px") {
504
- const [isVisible, setIsVisible] = React4.useState(false);
505
- React4.useEffect(() => {
571
+ const [isVisible, setIsVisible] = React5.useState(false);
572
+ React5.useEffect(() => {
506
573
  const element = ref.current;
507
574
  if (isVisible || !element) return;
508
575
  if (typeof IntersectionObserver === "undefined") {
@@ -523,12 +590,12 @@ function useIsVisible(ref, rootMargin = "200px") {
523
590
  return isVisible;
524
591
  }
525
592
  function usePdfDocument({ pdfjs, bytes, workerSrc }) {
526
- const [state, setState] = React4.useState({
593
+ const [state, setState] = React5.useState({
527
594
  bytes: null,
528
595
  document: null,
529
596
  error: null
530
597
  });
531
- React4.useEffect(() => {
598
+ React5.useEffect(() => {
532
599
  let isCurrent = true;
533
600
  const loadingTask = openPdf(pdfjs, bytes, workerSrc);
534
601
  loadingTask.promise.then((document2) => {
@@ -547,8 +614,8 @@ function usePdfDocument({ pdfjs, bytes, workerSrc }) {
547
614
  }
548
615
  function usePageRender(options) {
549
616
  const { document: document2, pageNumber, canvasRef, width, isEnabled, shouldExtractText = false } = options;
550
- const [state, setState] = React4.useState({ isRendered: false, text: "" });
551
- React4.useEffect(() => {
617
+ const [state, setState] = React5.useState({ isRendered: false, text: "" });
618
+ React5.useEffect(() => {
552
619
  const canvas = canvasRef.current;
553
620
  if (!document2 || !isEnabled || !canvas) return;
554
621
  let isCurrent = true;
@@ -578,7 +645,7 @@ function usePageRender(options) {
578
645
  }
579
646
 
580
647
  // src/pdf-preview.tsx
581
- import React5 from "react";
648
+ import React6 from "react";
582
649
  import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
583
650
  var VISUALLY_HIDDEN = {
584
651
  position: "absolute",
@@ -624,9 +691,9 @@ function PdfPreview(props) {
624
691
  function PdfCanvasDocument(props) {
625
692
  const { pdfjs, workerSrc, bytes, label, className, renderFallback, loading, labels, maxPages } = props;
626
693
  const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
627
- const containerRef = React5.useRef(null);
628
- const [width, setWidth] = React5.useState(null);
629
- React5.useLayoutEffect(() => {
694
+ const containerRef = React6.useRef(null);
695
+ const [width, setWidth] = React6.useState(null);
696
+ React6.useLayoutEffect(() => {
630
697
  if (document2) setWidth(containerRef.current?.clientWidth ?? 0);
631
698
  }, [document2]);
632
699
  if (error) return /* @__PURE__ */ jsx3(Fragment, { children: renderFallback(error) });
@@ -662,8 +729,8 @@ function PdfCanvasDocument(props) {
662
729
  );
663
730
  }
664
731
  function PdfPageView({ document: document2, pageNumber, width, label }) {
665
- const pageRef = React5.useRef(null);
666
- const canvasRef = React5.useRef(null);
732
+ const pageRef = React6.useRef(null);
733
+ const canvasRef = React6.useRef(null);
667
734
  const isVisible = useIsVisible(pageRef);
668
735
  const { isRendered, text } = usePageRender({
669
736
  document: document2,
@@ -680,8 +747,8 @@ function PdfPageView({ document: document2, pageNumber, width, label }) {
680
747
  ] });
681
748
  }
682
749
  function PdfFrame({ bytes, label, className }) {
683
- const [objectUrl, setObjectUrl] = React5.useState(null);
684
- React5.useEffect(() => {
750
+ const [objectUrl, setObjectUrl] = React6.useState(null);
751
+ React6.useEffect(() => {
685
752
  const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
686
753
  setObjectUrl(url);
687
754
  return () => URL.revokeObjectURL(url);
@@ -696,7 +763,7 @@ var DEFAULT_THUMBNAIL_WIDTH = 96;
696
763
  var DEFAULT_THUMBNAIL_MAX_BYTES = 50 * 1024 * 1024;
697
764
  function PdfThumbnail(props) {
698
765
  const { media, contentType, width = DEFAULT_THUMBNAIL_WIDTH, fallback = null, loading = null } = props;
699
- const containerRef = React6.useRef(null);
766
+ const containerRef = React7.useRef(null);
700
767
  const isVisible = useIsVisible(containerRef);
701
768
  const pdfjsState = usePdfjs(props.pdfWorkerSrc);
702
769
  const labels = usePreviewLabels(props.labels);
@@ -729,7 +796,7 @@ function PdfThumbnail(props) {
729
796
  return /* @__PURE__ */ jsx4("div", { ref: containerRef, className: props.className, style: { width }, children: content() });
730
797
  }
731
798
  function ThumbnailCanvas({ pdfjs, workerSrc, bytes, width, label, fallback, loading }) {
732
- const canvasRef = React6.useRef(null);
799
+ const canvasRef = React7.useRef(null);
733
800
  const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
734
801
  const { isRendered } = usePageRender({ document: document2, pageNumber: 1, canvasRef, width, isEnabled: true });
735
802
  if (error) return /* @__PURE__ */ jsx4(Fragment2, { children: fallback });
@@ -821,77 +888,29 @@ function TextPreview({
821
888
 
822
889
  // src/react.tsx
823
890
  import { Fragment as Fragment4, jsx as jsx6 } from "react/jsx-runtime";
824
- var DevulturContext = React7.createContext(null);
825
- var DevulturApiContext = React7.createContext(null);
826
- var ProgressContext = React7.createContext(null);
827
- var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
828
- var MIN_REFRESH_MS = 6e4;
829
- var DEFAULT_TTL_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3;
830
- function extractTokenResult(result) {
831
- const tokenValue = typeof result === "string" ? result : result.token;
832
- const ttlMs = typeof result === "string" ? DEFAULT_TTL_MS : result.expiresIn * 1e3;
833
- return { tokenValue, ttlMs };
834
- }
835
- function DevulturProvider({ api, baseUrl, children }) {
836
- const [token, setToken] = React7.useState(null);
837
- const [sessionSecret] = React7.useState(() => typeof crypto !== "undefined" ? crypto.randomUUID() : "");
838
- const issueViewerToken = useAction(api.issueViewerToken);
891
+ var DevulturContext = React8.createContext(null);
892
+ var DevulturApiContext = React8.createContext(null);
893
+ var ProgressContext = React8.createContext(null);
894
+ function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children }) {
895
+ const [sessionSecret] = React8.useState(() => typeof crypto !== "undefined" ? crypto.randomUUID() : "");
896
+ const issueViewerTokenAction = useAction(issueViewerToken ?? api.issueViewerToken);
897
+ const { token, reissueToken } = useViewerToken({
898
+ issueViewerToken: issueViewerTokenAction,
899
+ sessionSecret,
900
+ tokenArgs
901
+ });
839
902
  const createUploadUrlAction = useAction(api.createUploadUrl);
840
903
  const deleteMediaAction = useAction(api.deleteMedia);
841
904
  const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
842
905
  const deleteVideoAction = useAction(api.deleteVideo ?? api.deleteMedia);
843
- const refreshTimerRef = React7.useRef(null);
844
- const reissueInFlightRef = React7.useRef(null);
845
- const scheduleRefresh = React7.useCallback(
846
- (ttlMs) => {
847
- if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
848
- const refreshIn = Math.max(ttlMs - REFRESH_BUFFER_MS, MIN_REFRESH_MS);
849
- refreshTimerRef.current = setTimeout(() => {
850
- issueViewerToken({ sessionSecret }).then((result) => {
851
- const { tokenValue, ttlMs: nextTtl } = extractTokenResult(result);
852
- setToken(tokenValue);
853
- scheduleRefresh(nextTtl);
854
- });
855
- }, refreshIn);
856
- },
857
- [issueViewerToken, sessionSecret]
858
- );
859
- React7.useEffect(() => {
860
- if (token) return;
861
- issueViewerToken({ sessionSecret }).then((result) => {
862
- const { tokenValue, ttlMs } = extractTokenResult(result);
863
- setToken(tokenValue);
864
- scheduleRefresh(ttlMs);
865
- });
866
- }, [token, issueViewerToken, scheduleRefresh, sessionSecret]);
867
- const reissueToken = React7.useCallback(() => {
868
- if (reissueInFlightRef.current) return reissueInFlightRef.current;
869
- const pending = issueViewerToken({ sessionSecret }).then((result) => {
870
- const { tokenValue, ttlMs } = extractTokenResult(result);
871
- setToken(tokenValue);
872
- scheduleRefresh(ttlMs);
873
- reissueInFlightRef.current = null;
874
- return tokenValue;
875
- }).catch(() => {
876
- reissueInFlightRef.current = null;
877
- return null;
878
- });
879
- reissueInFlightRef.current = pending;
880
- return pending;
881
- }, [issueViewerToken, scheduleRefresh, sessionSecret]);
882
- React7.useEffect(() => {
883
- return () => {
884
- if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
885
- };
886
- }, []);
887
- const uploadUrl = React7.useCallback(
906
+ const uploadUrl = React8.useCallback(
888
907
  async (file) => {
889
908
  const result = await createUploadUrlAction({ filename: file.name, contentType: file.type });
890
909
  return { url: result.url, key: result.key };
891
910
  },
892
911
  [createUploadUrlAction]
893
912
  );
894
- const upload = React7.useCallback(
913
+ const upload = React8.useCallback(
895
914
  async (file, options) => {
896
915
  const dimensions = await readImageDimensions(file);
897
916
  const target = await createUploadUrlAction({
@@ -912,14 +931,14 @@ function DevulturProvider({ api, baseUrl, children }) {
912
931
  },
913
932
  [createUploadUrlAction, confirmUploadAction, api.confirmUpload]
914
933
  );
915
- const url = React7.useCallback(
934
+ const url = React8.useCallback(
916
935
  (media, options) => {
917
936
  if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
918
937
  return mediaUrl2(baseUrl, media, { ...options, token: token ?? void 0 });
919
938
  },
920
939
  [baseUrl, token]
921
940
  );
922
- const getCaptionsText = React7.useCallback(
941
+ const getCaptionsText = React8.useCallback(
923
942
  async (media, locale) => {
924
943
  if (!baseUrl) throw new Error("Devultur `getCaptionsText()` requires `baseUrl` on <DevulturProvider>");
925
944
  const path = mediaCaptionPath(media, locale);
@@ -938,7 +957,7 @@ function DevulturProvider({ api, baseUrl, children }) {
938
957
  },
939
958
  [baseUrl, token, reissueToken, sessionSecret]
940
959
  );
941
- const deleteMedia = React7.useCallback(
960
+ const deleteMedia = React8.useCallback(
942
961
  (media) => {
943
962
  if (media.kind === "video" && api.deleteVideo) {
944
963
  deleteVideoAction({ videoId: media.id, visibility: media.visibility }).catch(console.error);
@@ -948,7 +967,7 @@ function DevulturProvider({ api, baseUrl, children }) {
948
967
  },
949
968
  [deleteMediaAction, deleteVideoAction, api.deleteVideo]
950
969
  );
951
- const devulturValue = React7.useMemo(
970
+ const devulturValue = React8.useMemo(
952
971
  () => ({
953
972
  token,
954
973
  baseUrl: baseUrl ?? null,
@@ -988,13 +1007,13 @@ function ProgressBridge({
988
1007
  const saveMutation = useMutation(save);
989
1008
  const markCompleteMutation = useMutation(markComplete);
990
1009
  const resetMutation = useMutation(reset);
991
- const onSave = React7.useCallback(
1010
+ const onSave = React8.useCallback(
992
1011
  async (videoId, position) => {
993
1012
  await saveMutation({ videoId, position });
994
1013
  },
995
1014
  [saveMutation]
996
1015
  );
997
- const onLoad = React7.useCallback(
1016
+ const onLoad = React8.useCallback(
998
1017
  async (videoId) => {
999
1018
  const result = await convex.query(load, { videoId });
1000
1019
  if (!result) return null;
@@ -1005,37 +1024,37 @@ function ProgressBridge({
1005
1024
  },
1006
1025
  [convex, load]
1007
1026
  );
1008
- const onMarkComplete = React7.useCallback(
1027
+ const onMarkComplete = React8.useCallback(
1009
1028
  async (videoId) => {
1010
1029
  await markCompleteMutation({ videoId });
1011
1030
  },
1012
1031
  [markCompleteMutation]
1013
1032
  );
1014
- const onReset = React7.useCallback(
1033
+ const onReset = React8.useCallback(
1015
1034
  async (videoId) => {
1016
1035
  await resetMutation({ videoId });
1017
1036
  },
1018
1037
  [resetMutation]
1019
1038
  );
1020
- const value = React7.useMemo(
1039
+ const value = React8.useMemo(
1021
1040
  () => ({ save: onSave, load: onLoad, markComplete: onMarkComplete, reset: onReset }),
1022
1041
  [onSave, onLoad, onMarkComplete, onReset]
1023
1042
  );
1024
1043
  return /* @__PURE__ */ jsx6(ProgressContext, { value, children });
1025
1044
  }
1026
1045
  function useDevultur() {
1027
- const context = React7.use(DevulturContext);
1046
+ const context = React8.use(DevulturContext);
1028
1047
  if (!context) throw new Error("useDevultur must be used within <DevulturProvider>");
1029
1048
  return context;
1030
1049
  }
1031
1050
  function useDevulturProgress() {
1032
- const context = React7.use(ProgressContext);
1051
+ const context = React8.use(ProgressContext);
1033
1052
  if (!context) throw new Error("useDevulturProgress requires progress API in DevulturProvider");
1034
1053
  return context;
1035
1054
  }
1036
1055
  function useMediaUrl(key, options) {
1037
1056
  const { token, baseUrl } = useDevultur();
1038
- return React7.useMemo(() => {
1057
+ return React8.useMemo(() => {
1039
1058
  if (!key) return null;
1040
1059
  if (!baseUrl) throw new Error("useMediaUrl requires `baseUrl` on <DevulturProvider>");
1041
1060
  const isPublic = visibilityFromKey(key) === "public";
@@ -1063,7 +1082,7 @@ async function readImageDimensions(file) {
1063
1082
  function useVideoProps(media, options) {
1064
1083
  const { token, baseUrl, sessionSecret } = useDevultur();
1065
1084
  const locales = options?.locales;
1066
- return React7.useMemo(() => {
1085
+ return React8.useMemo(() => {
1067
1086
  if (media?.kind !== "video") return null;
1068
1087
  if (!baseUrl) throw new Error("useVideoProps requires `baseUrl` on <DevulturProvider>");
1069
1088
  const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
@@ -1095,19 +1114,19 @@ function Image({
1095
1114
  ...imgProps
1096
1115
  }) {
1097
1116
  const { url, token, baseUrl, reissueToken, sessionSecret } = useDevultur();
1098
- const [failed, setFailed] = React7.useState(false);
1099
- const [blobSrc, setBlobSrc] = React7.useState(null);
1100
- const sessionRef = React7.useRef({ token, reissueToken });
1101
- React7.useEffect(() => {
1117
+ const [failed, setFailed] = React8.useState(false);
1118
+ const [blobSrc, setBlobSrc] = React8.useState(null);
1119
+ const sessionRef = React8.useRef({ token, reissueToken });
1120
+ React8.useEffect(() => {
1102
1121
  sessionRef.current = { token, reissueToken };
1103
1122
  });
1104
1123
  const isPrivate = media?.visibility === "private";
1105
1124
  const hasToken = token !== null;
1106
- React7.useEffect(() => {
1125
+ React8.useEffect(() => {
1107
1126
  setFailed(false);
1108
1127
  setBlobSrc(null);
1109
1128
  }, [media]);
1110
- React7.useEffect(() => {
1129
+ React8.useEffect(() => {
1111
1130
  if (!isPrivate || failed || !media || !hasToken) return;
1112
1131
  let cancelled = false;
1113
1132
  let objectUrl = null;
@@ -1139,7 +1158,7 @@ function Image({
1139
1158
  if (objectUrl) URL.revokeObjectURL(objectUrl);
1140
1159
  };
1141
1160
  }, [isPrivate, failed, media, hasToken, baseUrl, sessionSecret, width, height, fit, format, quality]);
1142
- const handleError = React7.useCallback(() => setFailed(true), []);
1161
+ const handleError = React8.useCallback(() => setFailed(true), []);
1143
1162
  let src;
1144
1163
  if (failed || !media) {
1145
1164
  src = externalFallback ?? null;
@@ -1150,7 +1169,7 @@ function Image({
1150
1169
  }
1151
1170
  if (!src) return /* @__PURE__ */ jsx6(Fragment4, { children: fallback });
1152
1171
  if (render) {
1153
- return React7.cloneElement(render, {
1172
+ return React8.cloneElement(render, {
1154
1173
  style: { ...render.props.style, backgroundImage: `url("${src}")` }
1155
1174
  });
1156
1175
  }
@@ -1168,7 +1187,7 @@ function Image({
1168
1187
  }
1169
1188
  function DownloadLink({ media, filename, children, ...anchorProps }) {
1170
1189
  const { url, token, baseUrl, sessionSecret, reissueToken } = useDevultur();
1171
- const handleClick = React7.useCallback(
1190
+ const handleClick = React8.useCallback(
1172
1191
  async (event) => {
1173
1192
  if (media?.visibility !== "private" || !baseUrl) return;
1174
1193
  event.preventDefault();
@@ -1192,7 +1211,7 @@ function DownloadLink({ media, filename, children, ...anchorProps }) {
1192
1211
  return /* @__PURE__ */ jsx6("a", { href: url(media, { download: filename ?? true }), download: true, ...anchorProps, onClick: handleClick, children });
1193
1212
  }
1194
1213
  function useDevulturMedia(key) {
1195
- const apiCtx = React7.use(DevulturApiContext);
1214
+ const apiCtx = React8.use(DevulturApiContext);
1196
1215
  if (!apiCtx?.getMedia) {
1197
1216
  throw new Error("useDevulturMedia requires getMedia in DevulturProvider api");
1198
1217
  }
@@ -1249,7 +1268,7 @@ function useVideoState(media, options) {
1249
1268
  const { token, baseUrl, sessionSecret } = useDevultur();
1250
1269
  const status = useDevulturMedia(media?.kind === "video" ? media.id : null);
1251
1270
  const locales = options?.locales;
1252
- return React7.useMemo(() => {
1271
+ return React8.useMemo(() => {
1253
1272
  if (media?.kind !== "video") return { phase: "idle" };
1254
1273
  if (status.isFailed) {
1255
1274
  return {
@@ -1293,23 +1312,23 @@ function useVideoState(media, options) {
1293
1312
  }
1294
1313
  function useMediaUpload(options) {
1295
1314
  const { visibility, onComplete, onError, replaces, process, locales } = options ?? {};
1296
- const api = React7.use(DevulturApiContext);
1315
+ const api = React8.use(DevulturApiContext);
1297
1316
  if (!api) throw new Error("useMediaUpload must be used within <DevulturProvider>");
1298
1317
  const createUploadUrlAction = useAction(api.createUploadUrl);
1299
1318
  const deleteMediaAction = useAction(api.deleteMedia);
1300
1319
  const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
1301
1320
  const processVideoAction = useAction(api.processVideo ?? api.createUploadUrl);
1302
- const [phase, setPhase] = React7.useState("idle");
1303
- const [percent, setPercent] = React7.useState(0);
1304
- const [currentFile, setCurrentFile] = React7.useState(null);
1305
- const [media, setMedia] = React7.useState(null);
1306
- const [uploadFailure, setUploadFailure] = React7.useState(null);
1307
- const [processRequested, setProcessRequested] = React7.useState(false);
1308
- const abortRef = React7.useRef(null);
1309
- const lastFileRef = React7.useRef(null);
1310
- const lastKeyRef = React7.useRef(null);
1321
+ const [phase, setPhase] = React8.useState("idle");
1322
+ const [percent, setPercent] = React8.useState(0);
1323
+ const [currentFile, setCurrentFile] = React8.useState(null);
1324
+ const [media, setMedia] = React8.useState(null);
1325
+ const [uploadFailure, setUploadFailure] = React8.useState(null);
1326
+ const [processRequested, setProcessRequested] = React8.useState(false);
1327
+ const abortRef = React8.useRef(null);
1328
+ const lastFileRef = React8.useRef(null);
1329
+ const lastKeyRef = React8.useRef(null);
1311
1330
  const videoState = useVideoState(media, { locales });
1312
- const triggerProcess = React7.useCallback(
1331
+ const triggerProcess = React8.useCallback(
1313
1332
  (key) => {
1314
1333
  if (!api.processVideo) {
1315
1334
  throw new Error("useMediaUpload: processing a video requires `processVideo` in the <DevulturProvider> api");
@@ -1322,7 +1341,7 @@ function useMediaUpload(options) {
1322
1341
  },
1323
1342
  [api.processVideo, processVideoAction]
1324
1343
  );
1325
- const runUpload = React7.useCallback(
1344
+ const runUpload = React8.useCallback(
1326
1345
  async (file) => {
1327
1346
  const controller = new AbortController();
1328
1347
  abortRef.current = controller;
@@ -1393,10 +1412,10 @@ function useMediaUpload(options) {
1393
1412
  onError
1394
1413
  ]
1395
1414
  );
1396
- const cancel = React7.useCallback(() => {
1415
+ const cancel = React8.useCallback(() => {
1397
1416
  abortRef.current?.abort();
1398
1417
  }, []);
1399
- const reset = React7.useCallback(() => {
1418
+ const reset = React8.useCallback(() => {
1400
1419
  abortRef.current?.abort();
1401
1420
  setPhase("idle");
1402
1421
  setPercent(0);
@@ -1405,15 +1424,15 @@ function useMediaUpload(options) {
1405
1424
  setUploadFailure(null);
1406
1425
  setProcessRequested(false);
1407
1426
  }, []);
1408
- const retry = React7.useCallback(() => {
1427
+ const retry = React8.useCallback(() => {
1409
1428
  if (phase === "upload-failed" && lastFileRef.current) {
1410
1429
  runUpload(lastFileRef.current);
1411
1430
  return;
1412
1431
  }
1413
1432
  if (lastKeyRef.current) triggerProcess(lastKeyRef.current);
1414
1433
  }, [phase, runUpload, triggerProcess]);
1415
- React7.useEffect(() => () => abortRef.current?.abort(), []);
1416
- const state = React7.useMemo(
1434
+ React8.useEffect(() => () => abortRef.current?.abort(), []);
1435
+ const state = React8.useMemo(
1417
1436
  () => deriveUploadState(
1418
1437
  { phase, percent, file: currentFile, failure: uploadFailure, media, processRequested },
1419
1438
  videoState,
@@ -1421,7 +1440,7 @@ function useMediaUpload(options) {
1421
1440
  ),
1422
1441
  [phase, percent, currentFile, uploadFailure, media, processRequested, videoState, retry]
1423
1442
  );
1424
- const upload = React7.useCallback((file) => runUpload(file), [runUpload]);
1443
+ const upload = React8.useCallback((file) => runUpload(file), [runUpload]);
1425
1444
  return { upload, state, cancel, reset };
1426
1445
  }
1427
1446
  export {
@@ -12,22 +12,22 @@ import { DevulturKind } from '@devultur/core';
12
12
  */
13
13
  declare const dv: {
14
14
  media(kind?: DevulturKind): convex_values.VObject<{
15
- key?: string | undefined;
16
15
  filename?: string | undefined;
17
- height?: number | undefined;
16
+ key?: string | undefined;
18
17
  width?: number | undefined;
19
- visibility: "public" | "private";
18
+ height?: number | undefined;
20
19
  id: string;
21
20
  kind: DevulturKind;
21
+ visibility: "public" | "private";
22
22
  }, {
23
- kind: convex_values.VLiteral<DevulturKind, "required"> | convex_values.VUnion<"video" | "image" | "file", [convex_values.VLiteral<"image", "required">, convex_values.VLiteral<"video", "required">, convex_values.VLiteral<"file", "required">], "required", never>;
23
+ kind: convex_values.VLiteral<DevulturKind, "required"> | convex_values.VUnion<"image" | "video" | "file", [convex_values.VLiteral<"image", "required">, convex_values.VLiteral<"video", "required">, convex_values.VLiteral<"file", "required">], "required", never>;
24
24
  id: convex_values.VString<string, "required">;
25
25
  visibility: convex_values.VUnion<"public" | "private", [convex_values.VLiteral<"public", "required">, convex_values.VLiteral<"private", "required">], "required", never>;
26
26
  filename: convex_values.VString<string | undefined, "optional">;
27
27
  key: convex_values.VString<string | undefined, "optional">;
28
28
  width: convex_values.VFloat64<number | undefined, "optional">;
29
29
  height: convex_values.VFloat64<number | undefined, "optional">;
30
- }, "required", "key" | "filename" | "visibility" | "height" | "width" | "id" | "kind">;
30
+ }, "required", "id" | "kind" | "visibility" | "filename" | "key" | "width" | "height">;
31
31
  };
32
32
 
33
33
  export { dv };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devultur/convex",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Convex component for Devultur media: media records, uploads, transcodes, watch progress and React bindings",
5
5
  "keywords": [
6
6
  "convex",
@@ -55,8 +55,8 @@
55
55
  "dist"
56
56
  ],
57
57
  "dependencies": {
58
- "@devultur/core": "1.3.1",
59
- "@devultur/react": "1.3.1"
58
+ "@devultur/core": "1.3.2",
59
+ "@devultur/react": "1.3.2"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "convex": ">=1.25.0",