@devultur/convex 1.0.3 → 1.1.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.
Files changed (47) hide show
  1. package/README.md +126 -0
  2. package/dist/bin/init.js +1 -0
  3. package/dist/{chunk-FHWT3GD6.js → chunk-GKLFRSGI.js} +2 -0
  4. package/dist/component/_generated/api.d.ts +12 -0
  5. package/dist/component/_generated/api.d.ts.map +1 -1
  6. package/dist/component/_generated/api.js.map +1 -1
  7. package/dist/component/_generated/component.d.ts +49 -0
  8. package/dist/component/_generated/component.d.ts.map +1 -1
  9. package/dist/component/lock.d.ts +33 -0
  10. package/dist/component/lock.d.ts.map +1 -0
  11. package/dist/component/lock.js +47 -0
  12. package/dist/component/lock.js.map +1 -0
  13. package/dist/component/lockValidators.d.ts +25 -0
  14. package/dist/component/lockValidators.d.ts.map +1 -0
  15. package/dist/component/lockValidators.js +14 -0
  16. package/dist/component/lockValidators.js.map +1 -0
  17. package/dist/component/mediaClient.d.ts +3 -0
  18. package/dist/component/mediaClient.d.ts.map +1 -0
  19. package/dist/component/mediaClient.js +7 -0
  20. package/dist/component/mediaClient.js.map +1 -0
  21. package/dist/component/public.d.ts +72 -0
  22. package/dist/component/public.d.ts.map +1 -1
  23. package/dist/component/public.js +48 -13
  24. package/dist/component/public.js.map +1 -1
  25. package/dist/component/schema.d.ts +10 -1
  26. package/dist/component/schema.d.ts.map +1 -1
  27. package/dist/component/schema.js +10 -1
  28. package/dist/component/schema.js.map +1 -1
  29. package/dist/component/uploadKeys.d.ts +14 -0
  30. package/dist/component/uploadKeys.d.ts.map +1 -0
  31. package/dist/component/uploadKeys.js +23 -0
  32. package/dist/component/uploadKeys.js.map +1 -0
  33. package/dist/component/uploads.d.ts +119 -0
  34. package/dist/component/uploads.d.ts.map +1 -0
  35. package/dist/component/uploads.js +106 -0
  36. package/dist/component/uploads.js.map +1 -0
  37. package/dist/component/workerErrors.d.ts +17 -0
  38. package/dist/component/workerErrors.d.ts.map +1 -0
  39. package/dist/component/workerErrors.js +41 -0
  40. package/dist/component/workerErrors.js.map +1 -0
  41. package/dist/index.d.ts +138 -4
  42. package/dist/index.js +102 -4
  43. package/dist/react.d.ts +18 -5
  44. package/dist/react.js +35 -13
  45. package/dist/validators.d.ts +4 -2
  46. package/dist/validators.js +1 -1
  47. package/package.json +7 -3
@@ -0,0 +1,41 @@
1
+ // Worker errors across the component boundary. A plain `Error` thrown in a component function reaches the app as a
2
+ // message only; a `ConvexError` keeps its `data`. Every component action is built with `workerAction`, which rethrows
3
+ // the Worker's `DevulturApiError` as `ConvexError<DevulturErrorData>`, so the app reads `error.data.code`
4
+ // (`retention_active`, `lock_conflict`, `compliance_extend_only`, ...) and `error.data.details`.
5
+ import { DevulturApiError } from "@devultur/core";
6
+ import { ConvexError } from "convex/values";
7
+ import { action } from "./_generated/server";
8
+ function isApiError(error) {
9
+ if (error instanceof DevulturApiError)
10
+ return true;
11
+ // A second copy of `@devultur/core` (bundling) fails `instanceof`; its shape doesn't.
12
+ return (error instanceof Error &&
13
+ error.name === "DevulturApiError" &&
14
+ typeof error.status === "number");
15
+ }
16
+ /** The error to rethrow: a Worker refusal becomes a `ConvexError` with its code; anything else passes through. */
17
+ export function toComponentError(error) {
18
+ if (!isApiError(error))
19
+ return error;
20
+ const data = {
21
+ code: error.code ?? "worker_error",
22
+ message: error.message,
23
+ status: error.status,
24
+ // JSON from the Worker: valid Convex values once `undefined` is gone.
25
+ ...(error.details ? { details: JSON.parse(JSON.stringify(error.details)) } : {}),
26
+ };
27
+ return new ConvexError(data);
28
+ }
29
+ /** `action` whose handler rethrows Worker refusals as `ConvexError<DevulturErrorData>`. */
30
+ export const workerAction = ((definition) => action({
31
+ ...definition,
32
+ handler: async (ctx, args) => {
33
+ try {
34
+ return await definition.handler(ctx, args);
35
+ }
36
+ catch (error) {
37
+ throw toComponentError(error);
38
+ }
39
+ },
40
+ }));
41
+ //# sourceMappingURL=workerErrors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workerErrors.js","sourceRoot":"","sources":["../../src/component/workerErrors.ts"],"names":[],"mappings":"AAAA,mHAAmH;AACnH,sHAAsH;AACtH,0GAA0G;AAC1G,iGAAiG;AACjG,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAc,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAa7C,SAAS,UAAU,CAAC,KAAc;IACjC,IAAI,KAAK,YAAY,gBAAgB;QAAE,OAAO,IAAI,CAAC;IACnD,sFAAsF;IACtF,OAAO,CACN,KAAK,YAAY,KAAK;QACtB,KAAK,CAAC,IAAI,KAAK,kBAAkB;QACjC,OAAQ,KAA8B,CAAC,MAAM,KAAK,QAAQ,CAC1D,CAAC;AACH,CAAC;AAED,kHAAkH;AAClH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC9C,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACrC,MAAM,IAAI,GAAsB;QAC/B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,cAAc;QAClC,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,sEAAsE;QACtE,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAA0B,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACzG,CAAC;IACF,OAAO,IAAI,WAAW,CAAC,IAAwB,CAAC,CAAC;AAClD,CAAC;AAED,2FAA2F;AAC3F,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,UAA0E,EAAE,EAAE,CAC3G,MAAM,CAAC;IACN,GAAG,UAAU;IACb,OAAO,EAAE,KAAK,EAAE,GAAY,EAAE,IAAa,EAAE,EAAE;QAC9C,IAAI,CAAC;YACJ,OAAO,MAAM,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC5C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC;IACF,CAAC;CACQ,CAAC,CAA6B,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { VideoModuleConfig } from '@devultur/core';
1
+ import { VideoModuleConfig, ObjectLock, LockView, SetLockOptions, HoldPage, DevulturMedia } from '@devultur/core';
2
2
  export { CaptionLocale, DevulturConfig, DevulturKind, DevulturMedia, ImageFit, ImageFormat, SecurityLevel, SpeechProvider, StorageProvider, TranscodePreset, TranscodeProvider, TranslationProvider, VideoModuleConfig, Visibility } from '@devultur/core';
3
3
  import * as convex_server from 'convex/server';
4
- import { FunctionReference, GenericQueryCtx, GenericDataModel, GenericMutationCtx } from 'convex/server';
4
+ import { FunctionReference, GenericQueryCtx, GenericDataModel, GenericMutationCtx, GenericActionCtx, PaginationOptions, PaginationResult } from 'convex/server';
5
+ import { Value, ConvexError } from 'convex/values';
5
6
  export { dv } from './validators.js';
6
- import 'convex/values';
7
7
 
8
8
  /**
9
9
  * Generated `ComponentApi` utility.
@@ -26,6 +26,26 @@ import 'convex/values';
26
26
  * ```
27
27
  */
28
28
  type ComponentApi<Name extends string | undefined = string | undefined> = {
29
+ lock: {
30
+ getLock: FunctionReference<"action", "internal", {
31
+ key: string;
32
+ }, any, Name>;
33
+ releaseHold: FunctionReference<"action", "internal", {
34
+ keys: Array<string>;
35
+ }, any, Name>;
36
+ setHold: FunctionReference<"action", "internal", {
37
+ keys: Array<string>;
38
+ }, any, Name>;
39
+ setLock: FunctionReference<"action", "internal", {
40
+ bypassGovernance?: boolean;
41
+ derivatives?: "inherit" | "original-only";
42
+ key: string;
43
+ mode?: "governance" | "compliance";
44
+ period?: number;
45
+ remove?: true;
46
+ until?: string;
47
+ }, any, Name>;
48
+ };
29
49
  public: {
30
50
  createMedia: FunctionReference<"mutation", "internal", {
31
51
  contentType: string;
@@ -37,6 +57,16 @@ type ComponentApi<Name extends string | undefined = string | undefined> = {
37
57
  createUploadUrl: FunctionReference<"action", "internal", {
38
58
  contentType: string;
39
59
  filename: string;
60
+ hold?: boolean;
61
+ lock?: {
62
+ hold?: boolean;
63
+ retention?: {
64
+ derivatives?: "inherit" | "original-only";
65
+ mode: "governance" | "compliance";
66
+ period?: number;
67
+ until?: string;
68
+ };
69
+ };
40
70
  visibility?: "public" | "private";
41
71
  }, any, Name>;
42
72
  deleteMediaAction: FunctionReference<"action", "internal", {
@@ -62,6 +92,17 @@ type ComponentApi<Name extends string | undefined = string | undefined> = {
62
92
  listMedia: FunctionReference<"query", "internal", {
63
93
  type?: "video" | "image" | "file";
64
94
  }, any, Name>;
95
+ listMediaPage: FunctionReference<"query", "internal", {
96
+ paginationOpts: {
97
+ cursor: string | null;
98
+ endCursor?: string | null;
99
+ id?: number;
100
+ maximumBytesRead?: number;
101
+ maximumRowsRead?: number;
102
+ numItems: number;
103
+ };
104
+ type?: "video" | "image" | "file";
105
+ }, any, Name>;
65
106
  loadProgress: FunctionReference<"query", "internal", {
66
107
  userId: string;
67
108
  videoId: string;
@@ -108,17 +149,40 @@ type ComponentApi<Name extends string | undefined = string | undefined> = {
108
149
  vttUrl: string;
109
150
  }, any, Name>;
110
151
  };
152
+ uploads: {
153
+ confirmUpload: FunctionReference<"action", "internal", {
154
+ filename?: string;
155
+ height?: number;
156
+ key: string;
157
+ width?: number;
158
+ }, any, Name>;
159
+ };
111
160
  };
112
161
 
113
162
  interface DevulturComponentConfig {
114
163
  video?: VideoModuleConfig;
164
+ /**
165
+ * Lock every upload through the exposed `createUploadUrl` gets (retention and/or hold). Set here, on your server:
166
+ * the browser never chooses a lock. To lock per upload, call `devultur.createUploadUrl(ctx, { lock })` from your
167
+ * own function.
168
+ */
169
+ upload?: {
170
+ lock?: ObjectLock;
171
+ };
115
172
  }
116
173
  interface MediaRecord {
174
+ /** The media id (the `id` of its `DevulturMedia`), for every kind. */
117
175
  key: string;
118
176
  type: "video" | "image" | "file";
119
177
  filename: string;
120
178
  contentType: string;
121
179
  sizeBytes: number;
180
+ /** Set by a confirmed upload; unset on records from before 1.1.0. */
181
+ visibility?: "public" | "private";
182
+ /** Key of the uploaded object (a video's raw file). Set by a confirmed upload. */
183
+ objectKey?: string;
184
+ width?: number;
185
+ height?: number;
122
186
  transcodeStatus?: "queued" | "transcoding" | "captioning" | "ready" | "failed";
123
187
  transcodeProgress?: {
124
188
  current: number;
@@ -147,6 +211,23 @@ interface ProgressRecord {
147
211
  type DevulturComponent = ComponentApi;
148
212
  type QueryCtx = GenericQueryCtx<GenericDataModel>;
149
213
  type MutationCtx = GenericMutationCtx<GenericDataModel>;
214
+ type ActionCtx = GenericActionCtx<GenericDataModel>;
215
+ type UploadUrlArgs = {
216
+ filename: string;
217
+ contentType: string;
218
+ visibility?: "public" | "private";
219
+ };
220
+ type MediaType = "video" | "image" | "file";
221
+ type MediaPageArgs = {
222
+ type?: MediaType;
223
+ paginationOpts: PaginationOptions;
224
+ };
225
+ type ConfirmUploadArgs = {
226
+ key: string;
227
+ filename?: string;
228
+ width?: number;
229
+ height?: number;
230
+ };
150
231
  declare class Devultur {
151
232
  private component;
152
233
  constructor(component: DevulturComponent);
@@ -160,6 +241,34 @@ declare class Devultur {
160
241
  userId: string;
161
242
  videoId: string;
162
243
  }): Promise<any>;
244
+ /** An upload URL from your server, optionally locked (retention and/or hold, written with the bytes). */
245
+ createUploadUrl(ctx: ActionCtx, args: UploadUrlArgs & {
246
+ lock?: ObjectLock;
247
+ hold?: boolean;
248
+ }): Promise<any>;
249
+ /**
250
+ * Records an upload in the component once its bytes are stored (after the PUT): it then shows in `listMedia`.
251
+ * Returns the record, or `null` when nothing is stored at `key`. Uploads nobody confirms are recorded a day later.
252
+ */
253
+ confirmUpload(ctx: ActionCtx, args: ConfirmUploadArgs): Promise<MediaRecord | null>;
254
+ /**
255
+ * The recorded media (every confirmed upload, and every video sent to `processVideo`), optionally of one type.
256
+ * `mediaFromRecord(record)` turns a row into the reference `<Image>` and `dv.media()` take. Unbounded: it reads every
257
+ * record, so a media library pages with `listMediaPage`.
258
+ */
259
+ listMedia(ctx: QueryCtx, args?: {
260
+ type?: MediaType;
261
+ }): Promise<MediaRecord[]>;
262
+ /** One page of the recorded media, newest first, optionally of one type. Takes Convex's `paginationOpts`. */
263
+ listMediaPage(ctx: QueryCtx, args: MediaPageArgs): Promise<PaginationResult<MediaRecord>>;
264
+ /** The lock in force on `key`. */
265
+ getLock(ctx: ActionCtx, key: string): Promise<LockView>;
266
+ /** Sets or extends `key`'s retention (shortening a governance one needs `bypassGovernance` and the Worker's hook). */
267
+ setLock(ctx: ActionCtx, key: string, options: SetLockOptions): Promise<LockView>;
268
+ /** Holds `keys` (any number: sent in pages). A bulk hold by prefix uses `@devultur/core`'s client on your server. */
269
+ setHold(ctx: ActionCtx, keys: string[]): Promise<Pick<HoldPage, "updated" | "skipped">>;
270
+ /** Releases `keys`' holds: a retention `period` starts now; without a retention they're free. */
271
+ releaseHold(ctx: ActionCtx, keys: string[]): Promise<Pick<HoldPage, "updated" | "skipped">>;
163
272
  static exposeApi(component: DevulturComponent, config?: DevulturComponentConfig): {
164
273
  issueViewerToken: convex_server.RegisteredAction<"public", {
165
274
  sessionSecret?: string;
@@ -169,6 +278,7 @@ declare class Devultur {
169
278
  contentType: string;
170
279
  visibility?: "public" | "private";
171
280
  }, Promise<any>>;
281
+ confirmUpload: convex_server.RegisteredAction<"public", ConfirmUploadArgs, Promise<any>>;
172
282
  deleteMedia: convex_server.RegisteredAction<"public", {
173
283
  key: string;
174
284
  }, Promise<any>>;
@@ -182,6 +292,7 @@ declare class Devultur {
182
292
  getMedia: convex_server.RegisteredQuery<"public", {
183
293
  key: string;
184
294
  }, Promise<any>>;
295
+ listMediaPage: convex_server.RegisteredQuery<"public", MediaPageArgs, Promise<any>>;
185
296
  saveProgress: convex_server.RegisteredMutation<"public", {
186
297
  userId: string;
187
298
  videoId: string;
@@ -202,4 +313,27 @@ declare class Devultur {
202
313
  };
203
314
  }
204
315
 
205
- export { Devultur, type DevulturComponentConfig, type MediaRecord, type ProgressRecord };
316
+ /** `data` of the `ConvexError` a component action throws when the Worker refuses a request. */
317
+ interface DevulturErrorData {
318
+ /** The Worker's stable code (`retention_active`, `file_too_large`, ...), or `worker_error` when it sent none. */
319
+ code: string;
320
+ message: string;
321
+ /** HTTP status of the Worker's answer. */
322
+ status: number;
323
+ /** The rest of the Worker's error body, e.g. `{ key, mode, until, hold }` on `retention_active`. */
324
+ details?: Record<string, Value>;
325
+ }
326
+
327
+ /** True when `error` is a Worker refusal from a component action. */
328
+ declare function isDevulturError(error: unknown): error is ConvexError<DevulturErrorData & Record<string, never>>;
329
+ /** The refusal's `{ code, message, status, details? }`, or `null` when `error` isn't one. */
330
+ declare function devulturErrorOf(error: unknown): DevulturErrorData | null;
331
+
332
+ /**
333
+ * The `DevulturMedia` reference of a component record (a `listMedia` row), ready for `<Image>`, `<DownloadLink>` or a
334
+ * `dv.media()` field. `null` for a record without a visibility: one from before 1.1.0, or a video only `processVideo`
335
+ * recorded.
336
+ */
337
+ declare function mediaFromRecord(record: MediaRecord): DevulturMedia | null;
338
+
339
+ export { Devultur, type DevulturComponentConfig, type DevulturErrorData, type MediaRecord, type ProgressRecord, devulturErrorOf, isDevulturError, mediaFromRecord };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dv
3
- } from "./chunk-FHWT3GD6.js";
3
+ } from "./chunk-GKLFRSGI.js";
4
4
 
5
5
  // src/index.ts
6
6
  import {
@@ -17,7 +17,7 @@ import {
17
17
 
18
18
  // src/client.ts
19
19
  import { extractId } from "@devultur/core";
20
- import { actionGeneric, mutationGeneric, queryGeneric } from "convex/server";
20
+ import { actionGeneric, mutationGeneric, paginationOptsValidator, queryGeneric } from "convex/server";
21
21
  import { v } from "convex/values";
22
22
  var Devultur = class {
23
23
  component;
@@ -33,10 +33,50 @@ var Devultur = class {
33
33
  async loadProgress(ctx, args) {
34
34
  return ctx.runQuery(this.component.public.loadProgress, args);
35
35
  }
36
+ /** An upload URL from your server, optionally locked (retention and/or hold, written with the bytes). */
37
+ async createUploadUrl(ctx, args) {
38
+ return ctx.runAction(this.component.public.createUploadUrl, args);
39
+ }
40
+ /**
41
+ * Records an upload in the component once its bytes are stored (after the PUT): it then shows in `listMedia`.
42
+ * Returns the record, or `null` when nothing is stored at `key`. Uploads nobody confirms are recorded a day later.
43
+ */
44
+ async confirmUpload(ctx, args) {
45
+ return ctx.runAction(this.component.uploads.confirmUpload, args);
46
+ }
47
+ /**
48
+ * The recorded media (every confirmed upload, and every video sent to `processVideo`), optionally of one type.
49
+ * `mediaFromRecord(record)` turns a row into the reference `<Image>` and `dv.media()` take. Unbounded: it reads every
50
+ * record, so a media library pages with `listMediaPage`.
51
+ */
52
+ async listMedia(ctx, args) {
53
+ return ctx.runQuery(this.component.public.listMedia, args ?? {});
54
+ }
55
+ /** One page of the recorded media, newest first, optionally of one type. Takes Convex's `paginationOpts`. */
56
+ async listMediaPage(ctx, args) {
57
+ return ctx.runQuery(this.component.public.listMediaPage, args);
58
+ }
59
+ /** The lock in force on `key`. */
60
+ async getLock(ctx, key) {
61
+ return ctx.runAction(this.component.lock.getLock, { key });
62
+ }
63
+ /** Sets or extends `key`'s retention (shortening a governance one needs `bypassGovernance` and the Worker's hook). */
64
+ async setLock(ctx, key, options) {
65
+ return ctx.runAction(this.component.lock.setLock, { key, ...options });
66
+ }
67
+ /** Holds `keys` (any number: sent in pages). A bulk hold by prefix uses `@devultur/core`'s client on your server. */
68
+ async setHold(ctx, keys) {
69
+ return ctx.runAction(this.component.lock.setHold, { keys });
70
+ }
71
+ /** Releases `keys`' holds: a retention `period` starts now; without a retention they're free. */
72
+ async releaseHold(ctx, keys) {
73
+ return ctx.runAction(this.component.lock.releaseHold, { keys });
74
+ }
36
75
  static exposeApi(component, config) {
37
76
  const preset = config?.video?.transcodePreset;
38
77
  const locales = config?.video?.captionLocales ?? [];
39
78
  const visibilityArg = v.optional(v.union(v.literal("public"), v.literal("private")));
79
+ const uploadLock = config?.upload?.lock;
40
80
  return {
41
81
  issueViewerToken: actionGeneric({
42
82
  args: { sessionSecret: v.optional(v.string()) },
@@ -47,7 +87,23 @@ var Devultur = class {
47
87
  createUploadUrl: actionGeneric({
48
88
  args: { filename: v.string(), contentType: v.string(), visibility: visibilityArg },
49
89
  handler: async (ctx, args) => {
50
- return ctx.runAction(component.public.createUploadUrl, args);
90
+ return ctx.runAction(component.public.createUploadUrl, {
91
+ ...args,
92
+ ...uploadLock ? { lock: uploadLock } : {}
93
+ });
94
+ }
95
+ }),
96
+ // Records an upload once its bytes are stored (the Worker HEADs the key): a browser can't list anything
97
+ // that isn't in your bucket. `useDevultur().upload()` and `useMediaUpload` call it after the PUT.
98
+ confirmUpload: actionGeneric({
99
+ args: {
100
+ key: v.string(),
101
+ filename: v.optional(v.string()),
102
+ width: v.optional(v.number()),
103
+ height: v.optional(v.number())
104
+ },
105
+ handler: async (ctx, args) => {
106
+ return ctx.runAction(component.uploads.confirmUpload, args);
51
107
  }
52
108
  }),
53
109
  deleteMedia: actionGeneric({
@@ -79,6 +135,17 @@ var Devultur = class {
79
135
  return ctx.runQuery(component.public.getMediaByKey, { key: mediaKey });
80
136
  }
81
137
  }),
138
+ // The media library, a page at a time (for `usePaginatedQuery`). It lists every record, of every user: export
139
+ // it only when every caller may see the whole library, otherwise wrap `devultur.listMediaPage` with your checks.
140
+ listMediaPage: queryGeneric({
141
+ args: {
142
+ type: v.optional(v.union(v.literal("video"), v.literal("image"), v.literal("file"))),
143
+ paginationOpts: paginationOptsValidator
144
+ },
145
+ handler: async (ctx, args) => {
146
+ return ctx.runQuery(component.public.listMediaPage, args);
147
+ }
148
+ }),
82
149
  saveProgress: mutationGeneric({
83
150
  args: { userId: v.string(), videoId: v.string(), position: v.number() },
84
151
  handler: async (ctx, args) => {
@@ -106,6 +173,34 @@ var Devultur = class {
106
173
  };
107
174
  }
108
175
  };
176
+
177
+ // src/errors.ts
178
+ import { ConvexError } from "convex/values";
179
+ function isErrorData(data) {
180
+ if (typeof data !== "object" || data === null) return false;
181
+ const fields = data;
182
+ return typeof fields.code === "string" && typeof fields.status === "number" && typeof fields.message === "string";
183
+ }
184
+ function isDevulturError(error) {
185
+ return error instanceof ConvexError && isErrorData(error.data);
186
+ }
187
+ function devulturErrorOf(error) {
188
+ return isDevulturError(error) ? error.data : null;
189
+ }
190
+
191
+ // src/media-record.ts
192
+ function mediaFromRecord(record) {
193
+ if (!record.visibility) return null;
194
+ return {
195
+ id: record.key,
196
+ kind: record.type,
197
+ visibility: record.visibility,
198
+ filename: record.filename,
199
+ ...record.objectKey ? { key: record.objectKey } : {},
200
+ ...record.width !== void 0 ? { width: record.width } : {},
201
+ ...record.height !== void 0 ? { height: record.height } : {}
202
+ };
203
+ }
109
204
  export {
110
205
  CaptionLocale,
111
206
  Devultur,
@@ -117,5 +212,8 @@ export {
117
212
  TranscodePreset,
118
213
  TranscodeProvider,
119
214
  TranslationProvider,
120
- dv
215
+ devulturErrorOf,
216
+ dv,
217
+ isDevulturError,
218
+ mediaFromRecord
121
219
  };
package/dist/react.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { Visibility, DevulturMedia, MediaUrlOptions, ProcessingPhase, FailureReason, MediaStatusValue } from '@devultur/core';
3
- import { BakeOutputFormat, MediaUploadState, MediaUploadError } from '@devultur/react';
4
- export { MediaUploadError, MediaUploadState } from '@devultur/react';
3
+ import { BakeOutputFormat, MediaUploadState, MediaUploadError, UploadProgress } from '@devultur/react';
4
+ export { MediaUploadError, MediaUploadState, UploadProgress } from '@devultur/react';
5
5
  import { FunctionReference } from 'convex/server';
6
6
 
7
7
  interface CropUploadRenderProps {
@@ -59,6 +59,8 @@ type AnyQuery = FunctionReference<"query">;
59
59
  interface DevulturApi {
60
60
  issueViewerToken: AnyAction;
61
61
  createUploadUrl: AnyAction;
62
+ /** Records an upload in the component after its PUT, so it shows in `listMedia`. Optional (uploads still work). */
63
+ confirmUpload?: AnyAction;
62
64
  deleteMedia: AnyAction;
63
65
  /** Deletes the full folder of a video (raw/hls/thumbnails/captions). Required to delete videos. */
64
66
  deleteVideo?: AnyAction;
@@ -73,7 +75,18 @@ interface DevulturApi {
73
75
  interface UploadOptions {
74
76
  /** Visibility override for this resource. Omitted → inherits the router's default by type. */
75
77
  visibility?: Visibility;
78
+ /** Progress of the PUT: bytes sent, total bytes and their fraction (0 to 1). */
79
+ onProgress?: (progress: UploadProgress) => void;
80
+ /** Aborts the PUT → `upload()` rejects with an `AbortError`. */
81
+ signal?: AbortSignal;
76
82
  }
83
+ /**
84
+ * The reference `upload()` resolves to: a `DevulturMedia` whose `key` is always set (the uploaded object's key; a
85
+ * video's raw file, what `processVideo` takes). It stores as-is in a `dv.media()` field.
86
+ */
87
+ type UploadedMedia = DevulturMedia & {
88
+ readonly key: string;
89
+ };
77
90
  interface DevulturContextValue {
78
91
  token: string | null;
79
92
  baseUrl: string | null;
@@ -83,8 +96,8 @@ interface DevulturContextValue {
83
96
  url: string;
84
97
  key: string;
85
98
  }>;
86
- /** Uploads a file and returns the typed reference ready to save. */
87
- upload: (file: File, options?: UploadOptions) => Promise<DevulturMedia>;
99
+ /** Uploads a file (reporting progress), records it in the component, and returns the reference ready to save. */
100
+ upload: (file: File, options?: UploadOptions) => Promise<UploadedMedia>;
88
101
  /** Delivery URL from a reference (non-hook → usable in handlers and `.map`). Token-transparent. */
89
102
  url: (media: DevulturMedia, options?: MediaRenderOptions) => string;
90
103
  /** Downloads a video's subtitle text for a locale (auth-transparent). */
@@ -278,4 +291,4 @@ interface UseMediaUploadReturn {
278
291
  */
279
292
  declare function useMediaUpload(options?: UseMediaUploadOptions): UseMediaUploadReturn;
280
293
 
281
- export { type CaptionTrack, CropUpload, type CropUploadLabels, type CropUploadProps, type CropUploadRenderProps, DevulturProvider, DownloadLink, Image, type UseMediaUploadOptions, type UseMediaUploadReturn, type VideoProps, type VideoState, useDevultur, useDevulturMedia, useDevulturProgress, useMediaUpload, useMediaUrl, useVideoProps, useVideoState };
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 };
package/dist/react.js CHANGED
@@ -13,7 +13,10 @@ import {
13
13
  SESSION_HEADER,
14
14
  visibilityFromKey
15
15
  } from "@devultur/core";
16
- import { UploadHttpError, uploadFileWithProgress } from "@devultur/react";
16
+ import {
17
+ UploadHttpError,
18
+ uploadFileWithProgress
19
+ } from "@devultur/react";
17
20
  import { useAction, useConvex, useMutation, useQuery } from "convex/react";
18
21
 
19
22
  // src/image-recovery.ts
@@ -175,6 +178,7 @@ function DevulturProvider({ api, baseUrl, children }) {
175
178
  const issueViewerToken = useAction(api.issueViewerToken);
176
179
  const createUploadUrlAction = useAction(api.createUploadUrl);
177
180
  const deleteMediaAction = useAction(api.deleteMedia);
181
+ const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
178
182
  const deleteVideoAction = useAction(api.deleteVideo ?? api.deleteMedia);
179
183
  const refreshTimerRef = React2.useRef(null);
180
184
  const reissueInFlightRef = React2.useRef(null);
@@ -230,26 +234,23 @@ function DevulturProvider({ api, baseUrl, children }) {
230
234
  const upload = React2.useCallback(
231
235
  async (file, options) => {
232
236
  const dimensions = await readImageDimensions(file);
233
- const result = await createUploadUrlAction({
237
+ const target = await createUploadUrlAction({
234
238
  filename: file.name,
235
239
  contentType: file.type,
236
240
  visibility: options?.visibility
237
241
  });
238
- const uploadResponse = await fetch(result.url, {
239
- method: "PUT",
240
- body: file,
241
- headers: { "Content-Type": file.type }
242
- });
243
- if (!uploadResponse.ok) throw new Error(`Upload failed: ${uploadResponse.status}`);
242
+ await uploadFileWithProgress(target.url, file, { onBytes: options?.onProgress, signal: options?.signal });
243
+ if (api.confirmUpload) await confirmUploadBestEffort(confirmUploadAction, target.key, file, dimensions);
244
244
  return {
245
- id: result.id,
246
- kind: result.kind,
247
- visibility: result.visibility,
245
+ id: target.id,
246
+ kind: target.kind,
247
+ visibility: target.visibility,
248
248
  filename: file.name,
249
+ key: target.key,
249
250
  ...dimensions
250
251
  };
251
252
  },
252
- [createUploadUrlAction]
253
+ [createUploadUrlAction, confirmUploadAction, api.confirmUpload]
253
254
  );
254
255
  const url = React2.useCallback(
255
256
  (media, options) => {
@@ -381,6 +382,13 @@ function useMediaUrl(key, options) {
381
382
  return buildMediaUrl(baseUrl, key, { ...options, token: isPublic ? void 0 : token ?? void 0 });
382
383
  }, [key, baseUrl, token, options]);
383
384
  }
385
+ async function confirmUploadBestEffort(confirmUpload, key, file, dimensions) {
386
+ try {
387
+ await confirmUpload({ key, filename: file.name, ...dimensions });
388
+ } catch (error) {
389
+ console.warn("Devultur: could not record the upload; it is recorded later", error);
390
+ }
391
+ }
384
392
  async function readImageDimensions(file) {
385
393
  if (typeof createImageBitmap !== "function" || !file.type.startsWith("image/")) return {};
386
394
  try {
@@ -628,6 +636,7 @@ function useMediaUpload(options) {
628
636
  if (!api) throw new Error("useMediaUpload must be used within <DevulturProvider>");
629
637
  const createUploadUrlAction = useAction(api.createUploadUrl);
630
638
  const deleteMediaAction = useAction(api.deleteMedia);
639
+ const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
631
640
  const processVideoAction = useAction(api.processVideo ?? api.createUploadUrl);
632
641
  const [phase, setPhase] = React2.useState("idle");
633
642
  const [percent, setPercent] = React2.useState(0);
@@ -693,11 +702,13 @@ function useMediaUpload(options) {
693
702
  const rejected = err instanceof UploadHttpError && (err.status === 413 || err.status === 415);
694
703
  return fail(rejected ? "rejected" : "network", err instanceof Error ? err.message : "The upload failed");
695
704
  }
705
+ if (api.confirmUpload) await confirmUploadBestEffort(confirmUploadAction, result.key, file, dimensions);
696
706
  const uploaded = {
697
707
  id: result.id,
698
708
  kind: result.kind,
699
709
  visibility: result.visibility,
700
710
  filename: file.name,
711
+ key: result.key,
701
712
  ...dimensions
702
713
  };
703
714
  setMedia(uploaded);
@@ -708,7 +719,18 @@ function useMediaUpload(options) {
708
719
  if ((process ?? true) && uploaded.kind === "video") triggerProcess(result.key);
709
720
  return uploaded;
710
721
  },
711
- [createUploadUrlAction, deleteMediaAction, triggerProcess, visibility, replaces, process, onComplete, onError]
722
+ [
723
+ createUploadUrlAction,
724
+ confirmUploadAction,
725
+ api.confirmUpload,
726
+ deleteMediaAction,
727
+ triggerProcess,
728
+ visibility,
729
+ replaces,
730
+ process,
731
+ onComplete,
732
+ onError
733
+ ]
712
734
  );
713
735
  const cancel = React2.useCallback(() => {
714
736
  abortRef.current?.abort();
@@ -12,9 +12,10 @@ import { DevulturKind } from '@devultur/core';
12
12
  */
13
13
  declare const dv: {
14
14
  media(kind?: DevulturKind): convex_values.VObject<{
15
+ key?: string | undefined;
15
16
  filename?: string | undefined;
16
- width?: number | undefined;
17
17
  height?: number | undefined;
18
+ width?: number | undefined;
18
19
  visibility: "public" | "private";
19
20
  id: string;
20
21
  kind: DevulturKind;
@@ -23,9 +24,10 @@ declare const dv: {
23
24
  id: convex_values.VString<string, "required">;
24
25
  visibility: convex_values.VUnion<"public" | "private", [convex_values.VLiteral<"public", "required">, convex_values.VLiteral<"private", "required">], "required", never>;
25
26
  filename: convex_values.VString<string | undefined, "optional">;
27
+ key: convex_values.VString<string | undefined, "optional">;
26
28
  width: convex_values.VFloat64<number | undefined, "optional">;
27
29
  height: convex_values.VFloat64<number | undefined, "optional">;
28
- }, "required", "filename" | "visibility" | "id" | "kind" | "width" | "height">;
30
+ }, "required", "key" | "filename" | "visibility" | "height" | "width" | "id" | "kind">;
29
31
  };
30
32
 
31
33
  export { dv };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dv
3
- } from "./chunk-FHWT3GD6.js";
3
+ } from "./chunk-GKLFRSGI.js";
4
4
  export {
5
5
  dv
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devultur/convex",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Convex component for Devultur media: media records, uploads, transcodes, watch progress and React bindings",
5
5
  "keywords": [
6
6
  "convex",
@@ -51,8 +51,8 @@
51
51
  "dist"
52
52
  ],
53
53
  "dependencies": {
54
- "@devultur/core": "1.0.3",
55
- "@devultur/react": "1.0.3"
54
+ "@devultur/core": "1.1.0",
55
+ "@devultur/react": "1.1.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "convex": ">=1.25.0",
@@ -64,10 +64,14 @@
64
64
  }
65
65
  },
66
66
  "devDependencies": {
67
+ "@testing-library/react": "^16.3.3",
67
68
  "@types/node": "^22.0.0",
68
69
  "@types/react": "^19.0.0",
70
+ "@types/react-dom": "^19.3.0",
69
71
  "convex": "^1.21.0",
72
+ "jsdom": "^30.1.3",
70
73
  "react": "^19.0.0",
74
+ "react-dom": "^19.2.7",
71
75
  "tsup": "^8.0.0",
72
76
  "typescript": "^5.8.0"
73
77
  },