nuxt-convex-module 0.0.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/LICENSE +21 -0
- package/README.md +242 -0
- package/dist/chunks/index.mjs +64 -0
- package/dist/devtools-client/200.html +1 -0
- package/dist/devtools-client/404.html +1 -0
- package/dist/devtools-client/_nuxt/B29GGJ5n.js +1 -0
- package/dist/devtools-client/_nuxt/BSDEpQet.js +1 -0
- package/dist/devtools-client/_nuxt/BY23OQOo.js +1 -0
- package/dist/devtools-client/_nuxt/BZYRaL-G.js +1 -0
- package/dist/devtools-client/_nuxt/Byw6FxnC.js +1 -0
- package/dist/devtools-client/_nuxt/CJJ_hcZN.js +1 -0
- package/dist/devtools-client/_nuxt/CS6WMFOx.js +2 -0
- package/dist/devtools-client/_nuxt/CZbAGcNC.js +1 -0
- package/dist/devtools-client/_nuxt/Cr6_aklh.js +2 -0
- package/dist/devtools-client/_nuxt/DNfpyBVU.js +3 -0
- package/dist/devtools-client/_nuxt/DPWXUmcI.js +1 -0
- package/dist/devtools-client/_nuxt/builds/latest.json +1 -0
- package/dist/devtools-client/_nuxt/builds/meta/a7708498-bd09-4de2-b5ab-19b380293c77.json +1 -0
- package/dist/devtools-client/_nuxt/dy_xXcv9.js +1 -0
- package/dist/devtools-client/_nuxt/entry.DbTJhkHT.css +1 -0
- package/dist/devtools-client/_nuxt/error-404.CwGGO2Kz.css +1 -0
- package/dist/devtools-client/_nuxt/error-500.8ZJqCQm8.css +1 -0
- package/dist/devtools-client/_nuxt/ncgEdbEt.js +1 -0
- package/dist/devtools-client/_nuxt/queries.CE_dtt9-.css +1 -0
- package/dist/devtools-client/auth/index.html +1 -0
- package/dist/devtools-client/icon.svg +6 -0
- package/dist/devtools-client/index.html +1 -0
- package/dist/devtools-client/logs/index.html +1 -0
- package/dist/devtools-client/queries/index.html +1 -0
- package/dist/module.d.ts +189 -0
- package/dist/module.js +489 -0
- package/dist/module.json +13 -0
- package/dist/runtime/auth0/vue/index.d.ts +58 -0
- package/dist/runtime/auth0/vue/index.js +33 -0
- package/dist/runtime/better-auth/nuxt/middleware.d.ts +14 -0
- package/dist/runtime/better-auth/nuxt/middleware.js +39 -0
- package/dist/runtime/better-auth/nuxt/proxy.d.ts +6 -0
- package/dist/runtime/better-auth/nuxt/proxy.js +11 -0
- package/dist/runtime/better-auth/nuxt/server.d.ts +106 -0
- package/dist/runtime/better-auth/nuxt/server.js +123 -0
- package/dist/runtime/better-auth/vue/auth-boundary.d.ts +131 -0
- package/dist/runtime/better-auth/vue/auth-boundary.js +78 -0
- package/dist/runtime/better-auth/vue/client.d.ts +358 -0
- package/dist/runtime/better-auth/vue/client.js +5 -0
- package/dist/runtime/better-auth/vue/cross-domain.d.ts +39 -0
- package/dist/runtime/better-auth/vue/cross-domain.js +45 -0
- package/dist/runtime/better-auth/vue/hydration.d.ts +20 -0
- package/dist/runtime/better-auth/vue/hydration.js +52 -0
- package/dist/runtime/better-auth/vue/index.d.ts +12 -0
- package/dist/runtime/better-auth/vue/index.js +6 -0
- package/dist/runtime/better-auth/vue/plugin.client.d.ts +6 -0
- package/dist/runtime/better-auth/vue/plugin.client.js +32 -0
- package/dist/runtime/better-auth/vue/plugin.server.d.ts +24 -0
- package/dist/runtime/better-auth/vue/plugin.server.js +42 -0
- package/dist/runtime/better-auth/vue/use-auth.d.ts +92 -0
- package/dist/runtime/better-auth/vue/use-auth.js +82 -0
- package/dist/runtime/clerk/vue/index.d.ts +81 -0
- package/dist/runtime/clerk/vue/index.js +49 -0
- package/dist/runtime/devtools/bridge.d.ts +12 -0
- package/dist/runtime/devtools/bridge.js +191 -0
- package/dist/runtime/devtools/plugin.client.d.ts +8 -0
- package/dist/runtime/devtools/plugin.client.js +34 -0
- package/dist/runtime/devtools/types.d.ts +78 -0
- package/dist/runtime/devtools/types.js +0 -0
- package/dist/runtime/nuxt/composables/use-async-query.d.ts +144 -0
- package/dist/runtime/nuxt/composables/use-async-query.js +104 -0
- package/dist/runtime/nuxt/config.d.ts +7 -0
- package/dist/runtime/nuxt/config.js +12 -0
- package/dist/runtime/nuxt/index.d.ts +136 -0
- package/dist/runtime/nuxt/index.js +91 -0
- package/dist/runtime/polar/vue/components.d.ts +184 -0
- package/dist/runtime/polar/vue/components.js +115 -0
- package/dist/runtime/vue/auth/helpers.d.ts +67 -0
- package/dist/runtime/vue/auth/helpers.js +50 -0
- package/dist/runtime/vue/auth/index.d.ts +125 -0
- package/dist/runtime/vue/auth/index.js +97 -0
- package/dist/runtime/vue/client.d.ts +305 -0
- package/dist/runtime/vue/client.js +382 -0
- package/dist/runtime/vue/composables/use-action.d.ts +62 -0
- package/dist/runtime/vue/composables/use-action.js +13 -0
- package/dist/runtime/vue/composables/use-connection-state.d.ts +30 -0
- package/dist/runtime/vue/composables/use-connection-state.js +14 -0
- package/dist/runtime/vue/composables/use-mutation.d.ts +76 -0
- package/dist/runtime/vue/composables/use-mutation.js +34 -0
- package/dist/runtime/vue/composables/use-paginated-query.d.ts +132 -0
- package/dist/runtime/vue/composables/use-paginated-query.js +513 -0
- package/dist/runtime/vue/composables/use-queries.d.ts +62 -0
- package/dist/runtime/vue/composables/use-queries.js +37 -0
- package/dist/runtime/vue/composables/use-query.d.ts +87 -0
- package/dist/runtime/vue/composables/use-query.js +53 -0
- package/dist/runtime/vue/composables/use-storage-url.d.ts +46 -0
- package/dist/runtime/vue/composables/use-storage-url.js +9 -0
- package/dist/runtime/vue/composables/use-subscription.d.ts +15 -0
- package/dist/runtime/vue/composables/use-subscription.js +33 -0
- package/dist/runtime/vue/composables/use-upload-queue.d.ts +117 -0
- package/dist/runtime/vue/composables/use-upload-queue.js +115 -0
- package/dist/runtime/vue/composables/use-upload.d.ts +134 -0
- package/dist/runtime/vue/composables/use-upload.js +114 -0
- package/dist/runtime/vue/hydration.d.ts +66 -0
- package/dist/runtime/vue/hydration.js +46 -0
- package/dist/runtime/vue/index.d.ts +94 -0
- package/dist/runtime/vue/index.js +42 -0
- package/dist/runtime/vue/plugin.d.ts +18 -0
- package/dist/runtime/vue/plugin.js +16 -0
- package/dist/runtime/vue/provide.d.ts +39 -0
- package/dist/runtime/vue/provide.js +15 -0
- package/dist/runtime/vue/queries-observer.d.ts +41 -0
- package/dist/runtime/vue/queries-observer.js +126 -0
- package/dist/types.d.ts +3 -0
- package/package.json +233 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { makeFunctionReference } from "convex/server";
|
|
2
|
+
import { computed, toValue } from "vue";
|
|
3
|
+
import { useConvexQueries } from "./use-queries.js";
|
|
4
|
+
export function useQuery(query, ...args) {
|
|
5
|
+
const queryReference = typeof query === "string" ? makeFunctionReference(query) : query;
|
|
6
|
+
const queries = computed(() => {
|
|
7
|
+
const rawArgs = toValue(args[0]) ?? {};
|
|
8
|
+
const skip = rawArgs === "skip";
|
|
9
|
+
const argsObject = rawArgs === "skip" ? {} : rawArgs;
|
|
10
|
+
return skip ? {} : { query: { query: queryReference, args: argsObject } };
|
|
11
|
+
});
|
|
12
|
+
const results = useConvexQueries(queries);
|
|
13
|
+
return computed(() => {
|
|
14
|
+
const result = results.value["query"];
|
|
15
|
+
if (result instanceof Error) {
|
|
16
|
+
throw result;
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function useQuery_experimental(options) {
|
|
22
|
+
const throwOnError = options.throwOnError ?? false;
|
|
23
|
+
const queryReference = typeof options.query === "string" ? makeFunctionReference(options.query) : options.query;
|
|
24
|
+
const queries = computed(() => {
|
|
25
|
+
const rawArgs = toValue(options.args);
|
|
26
|
+
const skip = rawArgs === "skip";
|
|
27
|
+
const argsObject = !skip ? rawArgs : {};
|
|
28
|
+
return skip ? {} : { query: { query: queryReference, args: argsObject } };
|
|
29
|
+
});
|
|
30
|
+
const results = useConvexQueries(queries);
|
|
31
|
+
return computed(() => {
|
|
32
|
+
const result = results.value["query"];
|
|
33
|
+
if (result instanceof Error) {
|
|
34
|
+
if (throwOnError) {
|
|
35
|
+
throw result;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
error: result,
|
|
39
|
+
status: "error"
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (result === void 0) {
|
|
43
|
+
return {
|
|
44
|
+
status: "pending"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
data: result,
|
|
49
|
+
status: "success"
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export const useConvexQuery = useQuery;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { FunctionReference } from 'convex/server';
|
|
2
|
+
import { type ComputedRef, type MaybeRefOrGetter } from 'vue';
|
|
3
|
+
import type { StorageId } from './use-upload.js';
|
|
4
|
+
/**
|
|
5
|
+
* A `getUrl` query: takes `{ storageId }` and returns the file's served URL,
|
|
6
|
+
* or `null` if the file no longer exists (`await ctx.storage.getUrl(id)`).
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export type GetStorageUrl = FunctionReference<'query', 'public', {
|
|
11
|
+
storageId: StorageId;
|
|
12
|
+
}, string | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Reactively resolve the served URL for a Convex stored file.
|
|
15
|
+
*
|
|
16
|
+
* Thin wrapper over {@link useQuery} that handles the common ergonomics of
|
|
17
|
+
* serving stored files: it accepts a reactive storage id and automatically
|
|
18
|
+
* skips the query (returning `undefined`) while the id is `null`/`undefined`,
|
|
19
|
+
* so you can bind it straight to an id ref without juggling `'skip'`.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```vue
|
|
23
|
+
* <script setup lang="ts">
|
|
24
|
+
* import { useStorageUrl } from '#imports'
|
|
25
|
+
* import { api } from '#convex/api'
|
|
26
|
+
*
|
|
27
|
+
* const { storageId } = useUpload(api.images.generateUploadUrl)
|
|
28
|
+
* const url = useStorageUrl(api.images.getUrl, storageId)
|
|
29
|
+
* </script>
|
|
30
|
+
*
|
|
31
|
+
* <template>
|
|
32
|
+
* <img v-if="url" :src="url">
|
|
33
|
+
* </template>
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @param getUrl - A `FunctionReference` for the public `getUrl` query.
|
|
37
|
+
* @param storageId - The storage id to resolve. Accepts a ref, computed, or
|
|
38
|
+
* getter; `null`/`undefined` skips the query.
|
|
39
|
+
* @returns A computed ref of the served URL: `string | null` once loaded
|
|
40
|
+
* (`null` if the file is gone), or `undefined` while loading or skipped.
|
|
41
|
+
*
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
export declare function useStorageUrl(getUrl: GetStorageUrl, storageId: MaybeRefOrGetter<StorageId | string | null | undefined>): ComputedRef<string | null | undefined>;
|
|
45
|
+
/** @public */
|
|
46
|
+
export declare const useConvexStorageUrl: typeof useStorageUrl;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { toValue } from "vue";
|
|
2
|
+
import { useQuery } from "./use-query.js";
|
|
3
|
+
export function useStorageUrl(getUrl, storageId) {
|
|
4
|
+
return useQuery(getUrl, () => {
|
|
5
|
+
const id = toValue(storageId);
|
|
6
|
+
return id ? { storageId: id } : "skip";
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export const useConvexStorageUrl = useStorageUrl;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ShallowRef } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Composable used for safely managing subscriptions in concurrent mode.
|
|
4
|
+
*
|
|
5
|
+
* In order to avoid removing and re-adding subscriptions each time this composable is called,
|
|
6
|
+
* the parameters passed to this composable should be memoized in some way–
|
|
7
|
+
* either by wrapping the entire params object with useMemo()
|
|
8
|
+
* or by wrapping the individual callbacks with useCallback().
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function useSubscription<Value>({ getCurrentValue, subscribe, }: {
|
|
13
|
+
getCurrentValue: () => Value;
|
|
14
|
+
subscribe: (callback: () => void) => () => void;
|
|
15
|
+
}): ShallowRef<Value>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { shallowRef, watchEffect } from "vue";
|
|
2
|
+
export function useSubscription({
|
|
3
|
+
// (Synchronously) returns the current value of our subscription.
|
|
4
|
+
getCurrentValue,
|
|
5
|
+
// This function is passed an event handler to attach to the subscription.
|
|
6
|
+
// It should return an unsubscribe function that removes the handler.
|
|
7
|
+
subscribe
|
|
8
|
+
}) {
|
|
9
|
+
const state = shallowRef(getCurrentValue());
|
|
10
|
+
watchEffect((onCleanup) => {
|
|
11
|
+
if (import.meta.server) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
let didUnsubscribe = false;
|
|
15
|
+
const checkForUpdates = () => {
|
|
16
|
+
if (didUnsubscribe) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const value = getCurrentValue();
|
|
20
|
+
if (state.value === value) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
state.value = value;
|
|
24
|
+
};
|
|
25
|
+
const unsubscribe = subscribe(checkForUpdates);
|
|
26
|
+
checkForUpdates();
|
|
27
|
+
onCleanup(() => {
|
|
28
|
+
didUnsubscribe = true;
|
|
29
|
+
unsubscribe();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { type Ref } from 'vue';
|
|
2
|
+
import { type GenerateUploadUrl, type StorageId } from './use-upload.js';
|
|
3
|
+
/**
|
|
4
|
+
* Lifecycle status of a single item in an upload queue.
|
|
5
|
+
*
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
export type UploadItemStatus = 'pending' | 'uploading' | 'success' | 'error';
|
|
9
|
+
/**
|
|
10
|
+
* A single entry in the queue managed by {@link useUploadQueue}.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
export interface UploadQueueItem {
|
|
15
|
+
/** Stable id for this queue entry (not the storage id). */
|
|
16
|
+
id: string;
|
|
17
|
+
/** The file or blob being uploaded. */
|
|
18
|
+
file: Blob;
|
|
19
|
+
/** Current upload status. */
|
|
20
|
+
status: UploadItemStatus;
|
|
21
|
+
/** Upload progress for this item, from `0` to `1`. */
|
|
22
|
+
progress: number;
|
|
23
|
+
/** The storage id once the upload succeeds, or `null`. */
|
|
24
|
+
storageId: StorageId | null;
|
|
25
|
+
/** The error if this item failed, or `null`. */
|
|
26
|
+
error: Error | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Options for {@link useUploadQueue}.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export interface UseUploadQueueOptions {
|
|
34
|
+
/** Maximum number of uploads to run at once. Defaults to `3`. */
|
|
35
|
+
concurrency?: number;
|
|
36
|
+
/** Called when an individual item finishes uploading. */
|
|
37
|
+
onItemSuccess?: (storageId: StorageId, item: UploadQueueItem) => void | Promise<void>;
|
|
38
|
+
/** Called when an individual item fails. */
|
|
39
|
+
onItemError?: (error: Error, item: UploadQueueItem) => void;
|
|
40
|
+
/** Called once every queued item has settled (succeeded or failed). */
|
|
41
|
+
onComplete?: (items: UploadQueueItem[]) => void;
|
|
42
|
+
}
|
|
43
|
+
/** Accepted inputs to {@link VueUploadQueue.enqueue}. */
|
|
44
|
+
type EnqueueInput = Blob | Blob[] | FileList | null | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The reactive queue returned by {@link useUploadQueue}.
|
|
47
|
+
*
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export interface VueUploadQueue {
|
|
51
|
+
/** Reactive list of queued items, in insertion order. */
|
|
52
|
+
items: Readonly<Ref<UploadQueueItem[]>>;
|
|
53
|
+
/** Add one or more files to the queue; uploading starts automatically. */
|
|
54
|
+
enqueue: (files: EnqueueInput) => void;
|
|
55
|
+
/** `true` while any item is pending or uploading. */
|
|
56
|
+
isUploading: Readonly<Ref<boolean>>;
|
|
57
|
+
/** Aggregate progress across all items, from `0` to `1`. */
|
|
58
|
+
progress: Readonly<Ref<number>>;
|
|
59
|
+
/** Number of items waiting to start. */
|
|
60
|
+
pendingCount: Readonly<Ref<number>>;
|
|
61
|
+
/** Number of items currently uploading. */
|
|
62
|
+
activeCount: Readonly<Ref<number>>;
|
|
63
|
+
/** Abort all in-flight uploads (queued items stay `pending`). */
|
|
64
|
+
cancel: () => void;
|
|
65
|
+
/** Remove an item by id, aborting it first if it is uploading. */
|
|
66
|
+
remove: (id: string) => void;
|
|
67
|
+
/** Abort everything and empty the queue. */
|
|
68
|
+
clear: () => void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Upload many files to Convex file storage with bounded concurrency and
|
|
72
|
+
* per-item reactive progress.
|
|
73
|
+
*
|
|
74
|
+
* Built on the same flow as {@link useUpload}, but manages a queue: call
|
|
75
|
+
* {@link VueUploadQueue.enqueue} with one or more files (or a `FileList` from
|
|
76
|
+
* a multi-file `<input>`) and the queue uploads up to `concurrency` at a time,
|
|
77
|
+
* exposing per-item and aggregate progress.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```vue
|
|
81
|
+
* <script setup lang="ts">
|
|
82
|
+
* import { useUploadQueue, useMutation } from '#imports'
|
|
83
|
+
* import { api } from '#convex/api'
|
|
84
|
+
*
|
|
85
|
+
* const saveImage = useMutation(api.images.save)
|
|
86
|
+
* const { items, enqueue, progress, isUploading } = useUploadQueue(
|
|
87
|
+
* api.images.generateUploadUrl,
|
|
88
|
+
* { concurrency: 4, onItemSuccess: storageId => saveImage({ storageId }) },
|
|
89
|
+
* )
|
|
90
|
+
*
|
|
91
|
+
* function onPick(event: Event) {
|
|
92
|
+
* enqueue((event.target as HTMLInputElement).files)
|
|
93
|
+
* }
|
|
94
|
+
* </script>
|
|
95
|
+
*
|
|
96
|
+
* <template>
|
|
97
|
+
* <input type="file" multiple @change="onPick">
|
|
98
|
+
* <progress v-if="isUploading" :value="progress" />
|
|
99
|
+
* <ul>
|
|
100
|
+
* <li v-for="item in items" :key="item.id">
|
|
101
|
+
* {{ item.status }} — {{ Math.round(item.progress * 100) }}%
|
|
102
|
+
* </li>
|
|
103
|
+
* </ul>
|
|
104
|
+
* </template>
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* @param generateUploadUrl - A `FunctionReference` for the public
|
|
108
|
+
* `generateUploadUrl` mutation.
|
|
109
|
+
* @param options - Optional {@link UseUploadQueueOptions}.
|
|
110
|
+
* @returns A {@link VueUploadQueue}.
|
|
111
|
+
*
|
|
112
|
+
* @public
|
|
113
|
+
*/
|
|
114
|
+
export declare function useUploadQueue(generateUploadUrl: GenerateUploadUrl, options?: UseUploadQueueOptions): VueUploadQueue;
|
|
115
|
+
/** @public */
|
|
116
|
+
export declare const useConvexUploadQueue: typeof useUploadQueue;
|
|
117
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import { useConvexOrThrow } from "../client.js";
|
|
3
|
+
import { uploadFile } from "./use-upload.js";
|
|
4
|
+
function normalizeFiles(input) {
|
|
5
|
+
if (!input) return [];
|
|
6
|
+
if (input instanceof Blob) return [input];
|
|
7
|
+
if (Array.isArray(input)) return input;
|
|
8
|
+
return Array.from(input);
|
|
9
|
+
}
|
|
10
|
+
let queueItemId = 0;
|
|
11
|
+
function nextQueueItemId() {
|
|
12
|
+
return `upload-${++queueItemId}`;
|
|
13
|
+
}
|
|
14
|
+
export function useUploadQueue(generateUploadUrl, options = {}) {
|
|
15
|
+
const convex = useConvexOrThrow("useUploadQueue");
|
|
16
|
+
const concurrency = Math.max(1, Math.floor(options.concurrency ?? 3));
|
|
17
|
+
const items = ref([]);
|
|
18
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
19
|
+
const activeCount = computed(
|
|
20
|
+
() => items.value.filter((item) => item.status === "uploading").length
|
|
21
|
+
);
|
|
22
|
+
const pendingCount = computed(
|
|
23
|
+
() => items.value.filter((item) => item.status === "pending").length
|
|
24
|
+
);
|
|
25
|
+
const isUploading = computed(() => activeCount.value > 0 || pendingCount.value > 0);
|
|
26
|
+
const progress = computed(() => {
|
|
27
|
+
if (items.value.length === 0) return 0;
|
|
28
|
+
const total = items.value.reduce((sum, item) => sum + item.progress, 0);
|
|
29
|
+
return total / items.value.length;
|
|
30
|
+
});
|
|
31
|
+
function pump() {
|
|
32
|
+
if (typeof XMLHttpRequest === "undefined") return;
|
|
33
|
+
let slots = concurrency - activeCount.value;
|
|
34
|
+
for (const item of items.value) {
|
|
35
|
+
if (slots <= 0) break;
|
|
36
|
+
if (item.status !== "pending") continue;
|
|
37
|
+
slots--;
|
|
38
|
+
void run(item);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function run(item) {
|
|
42
|
+
item.status = "uploading";
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
controllers.set(item.id, controller);
|
|
45
|
+
try {
|
|
46
|
+
const url = await convex.mutation(generateUploadUrl, {});
|
|
47
|
+
const storageId = await uploadFile({
|
|
48
|
+
url,
|
|
49
|
+
file: item.file,
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
onProgress: (value) => {
|
|
52
|
+
item.progress = value;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
item.storageId = storageId;
|
|
56
|
+
item.progress = 1;
|
|
57
|
+
item.status = "success";
|
|
58
|
+
await options.onItemSuccess?.(storageId, item);
|
|
59
|
+
} catch (caught) {
|
|
60
|
+
const failure = caught instanceof Error ? caught : new Error(String(caught));
|
|
61
|
+
item.error = failure;
|
|
62
|
+
item.status = "error";
|
|
63
|
+
options.onItemError?.(failure, item);
|
|
64
|
+
} finally {
|
|
65
|
+
controllers.delete(item.id);
|
|
66
|
+
if (pendingCount.value === 0 && activeCount.value === 0) {
|
|
67
|
+
options.onComplete?.(items.value);
|
|
68
|
+
} else {
|
|
69
|
+
pump();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function enqueue(files) {
|
|
74
|
+
const incoming = normalizeFiles(files);
|
|
75
|
+
if (incoming.length === 0) return;
|
|
76
|
+
items.value.push(
|
|
77
|
+
...incoming.map((file) => ({
|
|
78
|
+
id: nextQueueItemId(),
|
|
79
|
+
file,
|
|
80
|
+
status: "pending",
|
|
81
|
+
progress: 0,
|
|
82
|
+
storageId: null,
|
|
83
|
+
error: null
|
|
84
|
+
}))
|
|
85
|
+
);
|
|
86
|
+
pump();
|
|
87
|
+
}
|
|
88
|
+
function cancel() {
|
|
89
|
+
for (const controller of controllers.values()) {
|
|
90
|
+
controller.abort();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function remove(id) {
|
|
94
|
+
controllers.get(id)?.abort();
|
|
95
|
+
controllers.delete(id);
|
|
96
|
+
items.value = items.value.filter((item) => item.id !== id);
|
|
97
|
+
}
|
|
98
|
+
function clear() {
|
|
99
|
+
cancel();
|
|
100
|
+
controllers.clear();
|
|
101
|
+
items.value = [];
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
items,
|
|
105
|
+
enqueue,
|
|
106
|
+
isUploading,
|
|
107
|
+
progress,
|
|
108
|
+
pendingCount,
|
|
109
|
+
activeCount,
|
|
110
|
+
cancel,
|
|
111
|
+
remove,
|
|
112
|
+
clear
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export const useConvexUploadQueue = useUploadQueue;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { FunctionReference } from 'convex/server';
|
|
2
|
+
import type { GenericId } from 'convex/values';
|
|
3
|
+
import { type Ref } from 'vue';
|
|
4
|
+
/**
|
|
5
|
+
* A `generateUploadUrl` mutation: takes no arguments and returns a one-time
|
|
6
|
+
* Convex upload URL string (`await ctx.storage.generateUploadUrl()`).
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export type GenerateUploadUrl = FunctionReference<'mutation', 'public', Record<string, never>, string>;
|
|
11
|
+
/**
|
|
12
|
+
* A Convex file storage id (`Id<'_storage'>`), branded so it is directly
|
|
13
|
+
* assignable to mutation/query args validated with `v.id('_storage')`.
|
|
14
|
+
*
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
export type StorageId = GenericId<'_storage'>;
|
|
18
|
+
/**
|
|
19
|
+
* Options for the low-level {@link uploadFile} helper.
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export interface UploadFileOptions {
|
|
24
|
+
/** The one-time Convex upload URL from a `generateUploadUrl` mutation. */
|
|
25
|
+
url: string;
|
|
26
|
+
/** The file or blob to upload. */
|
|
27
|
+
file: Blob;
|
|
28
|
+
/** Receives upload progress as a fraction from `0` to `1`. */
|
|
29
|
+
onProgress?: (progress: number) => void;
|
|
30
|
+
/** Abort signal to cancel the in-flight upload. */
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Upload a single file to a Convex upload URL, resolving to its storage id.
|
|
35
|
+
*
|
|
36
|
+
* Low-level building block used by {@link useUpload} and
|
|
37
|
+
* {@link useUploadQueue}. Uses `XMLHttpRequest` so real upload progress and
|
|
38
|
+
* cancellation are available — `fetch` cannot report request-body progress.
|
|
39
|
+
*
|
|
40
|
+
* Browser-only: throws if no `XMLHttpRequest` global is available (e.g. during
|
|
41
|
+
* SSR).
|
|
42
|
+
*
|
|
43
|
+
* @param options - {@link UploadFileOptions}.
|
|
44
|
+
* @returns A promise of the new {@link StorageId}.
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
export declare function uploadFile(options: UploadFileOptions): Promise<StorageId>;
|
|
49
|
+
/**
|
|
50
|
+
* Options for {@link useUpload}.
|
|
51
|
+
*
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
export interface UseUploadOptions {
|
|
55
|
+
/** Called with upload progress as a fraction from `0` to `1`. */
|
|
56
|
+
onProgress?: (progress: number) => void;
|
|
57
|
+
/** Called once the file is stored, with the new storage id and the file. */
|
|
58
|
+
onSuccess?: (storageId: StorageId, file: Blob) => void | Promise<void>;
|
|
59
|
+
/** Called if generating the upload URL or the upload itself fails. */
|
|
60
|
+
onError?: (error: Error, file: Blob) => void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The reactive uploader returned by {@link useUpload}.
|
|
64
|
+
*
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
67
|
+
export interface VueUpload {
|
|
68
|
+
/**
|
|
69
|
+
* Upload a file or blob, resolving to its {@link StorageId}.
|
|
70
|
+
*
|
|
71
|
+
* Resolves to `null` (instead of throwing) if the upload fails or is called
|
|
72
|
+
* outside the browser — inspect {@link VueUpload.error} for the reason.
|
|
73
|
+
*/
|
|
74
|
+
upload: (file: Blob) => Promise<StorageId | null>;
|
|
75
|
+
/** `true` while an upload is in flight. */
|
|
76
|
+
isUploading: Readonly<Ref<boolean>>;
|
|
77
|
+
/** Upload progress of the current/last upload, from `0` to `1`. */
|
|
78
|
+
progress: Readonly<Ref<number>>;
|
|
79
|
+
/** The error from the most recent failed upload, or `null`. */
|
|
80
|
+
error: Readonly<Ref<Error | null>>;
|
|
81
|
+
/** The storage id from the most recent successful upload, or `null`. */
|
|
82
|
+
storageId: Readonly<Ref<StorageId | null>>;
|
|
83
|
+
/** Abort the in-flight upload, if any. */
|
|
84
|
+
cancel: () => void;
|
|
85
|
+
/** Reset `progress`, `error`, and `storageId` back to their initial values. */
|
|
86
|
+
reset: () => void;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Upload files to Convex file storage from a component, with reactive
|
|
90
|
+
* progress, error, and cancellation state.
|
|
91
|
+
*
|
|
92
|
+
* Wraps the standard Convex flow — call a `generateUploadUrl` mutation, `POST`
|
|
93
|
+
* the file to the returned URL, and read back the storage id — and exposes it
|
|
94
|
+
* as reactive refs. Pass the resulting {@link StorageId} to your own mutation
|
|
95
|
+
* to persist it in your data model.
|
|
96
|
+
*
|
|
97
|
+
* Uploads are client-only: calling `upload()` during SSR resolves to `null`
|
|
98
|
+
* and sets {@link VueUpload.error}.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```vue
|
|
102
|
+
* <script setup lang="ts">
|
|
103
|
+
* import { useUpload, useMutation } from '#imports'
|
|
104
|
+
* import { api } from '#convex/api'
|
|
105
|
+
*
|
|
106
|
+
* const saveImage = useMutation(api.images.save)
|
|
107
|
+
* const { upload, isUploading, progress, error } = useUpload(
|
|
108
|
+
* api.images.generateUploadUrl,
|
|
109
|
+
* { onSuccess: storageId => saveImage({ storageId }) },
|
|
110
|
+
* )
|
|
111
|
+
*
|
|
112
|
+
* async function onPick(event: Event) {
|
|
113
|
+
* const file = (event.target as HTMLInputElement).files?.[0]
|
|
114
|
+
* if (file) await upload(file)
|
|
115
|
+
* }
|
|
116
|
+
* </script>
|
|
117
|
+
*
|
|
118
|
+
* <template>
|
|
119
|
+
* <input type="file" :disabled="isUploading" @change="onPick">
|
|
120
|
+
* <progress v-if="isUploading" :value="progress" />
|
|
121
|
+
* <p v-if="error">{{ error.message }}</p>
|
|
122
|
+
* </template>
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @param generateUploadUrl - A `FunctionReference` for the public
|
|
126
|
+
* `generateUploadUrl` mutation.
|
|
127
|
+
* @param options - Optional {@link UseUploadOptions} callbacks.
|
|
128
|
+
* @returns A {@link VueUpload}.
|
|
129
|
+
*
|
|
130
|
+
* @public
|
|
131
|
+
*/
|
|
132
|
+
export declare function useUpload(generateUploadUrl: GenerateUploadUrl, options?: UseUploadOptions): VueUpload;
|
|
133
|
+
/** @public */
|
|
134
|
+
export declare const useConvexUpload: typeof useUpload;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ref, shallowRef } from "vue";
|
|
2
|
+
import { useConvexOrThrow } from "../client.js";
|
|
3
|
+
export function uploadFile(options) {
|
|
4
|
+
const { url, file, onProgress, signal } = options;
|
|
5
|
+
if (typeof XMLHttpRequest === "undefined") {
|
|
6
|
+
return Promise.reject(
|
|
7
|
+
new Error("uploadFile can only run in the browser (no XMLHttpRequest available).")
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
if (signal?.aborted) {
|
|
12
|
+
reject(new DOMException("Upload aborted.", "AbortError"));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const xhr = new XMLHttpRequest();
|
|
16
|
+
xhr.open("POST", url, true);
|
|
17
|
+
if (file.type) {
|
|
18
|
+
xhr.setRequestHeader("Content-Type", file.type);
|
|
19
|
+
}
|
|
20
|
+
const onAbort = () => xhr.abort();
|
|
21
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
22
|
+
const cleanup = () => signal?.removeEventListener("abort", onAbort);
|
|
23
|
+
xhr.upload.onprogress = (event) => {
|
|
24
|
+
if (event.lengthComputable && event.total > 0) {
|
|
25
|
+
onProgress?.(event.loaded / event.total);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
xhr.onload = () => {
|
|
29
|
+
cleanup();
|
|
30
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
31
|
+
try {
|
|
32
|
+
const { storageId } = JSON.parse(xhr.responseText);
|
|
33
|
+
onProgress?.(1);
|
|
34
|
+
resolve(storageId);
|
|
35
|
+
} catch {
|
|
36
|
+
reject(new Error(`Convex upload returned an unexpected response: ${xhr.responseText}`));
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
reject(new Error(`Convex upload failed with status ${xhr.status}: ${xhr.responseText}`));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
xhr.onerror = () => {
|
|
43
|
+
cleanup();
|
|
44
|
+
reject(new Error("Network error during Convex file upload."));
|
|
45
|
+
};
|
|
46
|
+
xhr.onabort = () => {
|
|
47
|
+
cleanup();
|
|
48
|
+
reject(new DOMException("Upload aborted.", "AbortError"));
|
|
49
|
+
};
|
|
50
|
+
xhr.send(file);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function useUpload(generateUploadUrl, options = {}) {
|
|
54
|
+
const convex = useConvexOrThrow("useUpload");
|
|
55
|
+
const isUploading = ref(false);
|
|
56
|
+
const progress = ref(0);
|
|
57
|
+
const error = shallowRef(null);
|
|
58
|
+
const storageId = shallowRef(null);
|
|
59
|
+
let controller = null;
|
|
60
|
+
async function upload(file) {
|
|
61
|
+
if (typeof XMLHttpRequest === "undefined") {
|
|
62
|
+
const browserOnly = new Error("useUpload().upload() can only run in the browser.");
|
|
63
|
+
error.value = browserOnly;
|
|
64
|
+
options.onError?.(browserOnly, file);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
controller = new AbortController();
|
|
68
|
+
isUploading.value = true;
|
|
69
|
+
progress.value = 0;
|
|
70
|
+
error.value = null;
|
|
71
|
+
try {
|
|
72
|
+
const url = await convex.mutation(generateUploadUrl, {});
|
|
73
|
+
const id = await uploadFile({
|
|
74
|
+
url,
|
|
75
|
+
file,
|
|
76
|
+
signal: controller.signal,
|
|
77
|
+
onProgress: (value) => {
|
|
78
|
+
progress.value = value;
|
|
79
|
+
options.onProgress?.(value);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
storageId.value = id;
|
|
83
|
+
progress.value = 1;
|
|
84
|
+
await options.onSuccess?.(id, file);
|
|
85
|
+
return id;
|
|
86
|
+
} catch (caught) {
|
|
87
|
+
const failure = caught instanceof Error ? caught : new Error(String(caught));
|
|
88
|
+
error.value = failure;
|
|
89
|
+
options.onError?.(failure, file);
|
|
90
|
+
return null;
|
|
91
|
+
} finally {
|
|
92
|
+
isUploading.value = false;
|
|
93
|
+
controller = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function cancel() {
|
|
97
|
+
controller?.abort();
|
|
98
|
+
}
|
|
99
|
+
function reset() {
|
|
100
|
+
progress.value = 0;
|
|
101
|
+
error.value = null;
|
|
102
|
+
storageId.value = null;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
upload,
|
|
106
|
+
isUploading,
|
|
107
|
+
progress,
|
|
108
|
+
error,
|
|
109
|
+
storageId,
|
|
110
|
+
cancel,
|
|
111
|
+
reset
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export const useConvexUpload = useUpload;
|