@beignet/react-uploads 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +88 -0
- package/dist/index.d.ts +202 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +212 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
- package/src/index.ts +589 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @beignet/react-uploads
|
|
2
|
+
|
|
3
|
+
## 0.0.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4cb1784: Add first-class upload router primitives, a typed browser upload client, a
|
|
8
|
+
React upload adapter, Next.js upload route helper, S3-compatible direct upload
|
|
9
|
+
signing, devtools upload watcher support, an upload generator, and a
|
|
10
|
+
first-class `beignet make feature` command with optional policy, event, job,
|
|
11
|
+
and upload artifacts for the standard vertical slice. Add first-class job retry
|
|
12
|
+
helpers, outbox retry policy integration, and job retry/dead-letter devtools
|
|
13
|
+
events. Add a Next.js outbox drain route helper and doctor warnings for
|
|
14
|
+
serverless background-work footguns.
|
|
15
|
+
- Updated dependencies [3160184]
|
|
16
|
+
- Updated dependencies [254ef6d]
|
|
17
|
+
- Updated dependencies [4cb1784]
|
|
18
|
+
- Updated dependencies [8bd9085]
|
|
19
|
+
- @beignet/core@0.0.3
|
|
20
|
+
|
|
21
|
+
## 0.0.2
|
|
22
|
+
|
|
23
|
+
- Initial Beignet React uploads integration.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @beignet/react-uploads
|
|
2
|
+
|
|
3
|
+
React upload hooks for Beignet upload clients.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @beignet/react-uploads react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Create a typed upload client with `@beignet/core/uploads/client`, then wrap it
|
|
14
|
+
once for React components:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// client/uploads.ts
|
|
18
|
+
import { createUploadClient } from "@beignet/core/uploads/client";
|
|
19
|
+
import { createReactUploads } from "@beignet/react-uploads";
|
|
20
|
+
import type { postUploads } from "@/features/posts/uploads";
|
|
21
|
+
|
|
22
|
+
export type AppUploads = typeof postUploads;
|
|
23
|
+
|
|
24
|
+
export const uploads = createUploadClient<AppUploads>({
|
|
25
|
+
baseUrl: "/api/uploads",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const reactUploads = createReactUploads({
|
|
29
|
+
uploads,
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use the hook in components:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
"use client";
|
|
37
|
+
|
|
38
|
+
import { reactUploads } from "@/client/uploads";
|
|
39
|
+
|
|
40
|
+
export function AttachmentPicker({ postSlug }: { postSlug: string }) {
|
|
41
|
+
const attachment = reactUploads.useUpload("posts.attachment");
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<input
|
|
45
|
+
type="file"
|
|
46
|
+
accept={attachment.accept}
|
|
47
|
+
disabled={attachment.isUploading}
|
|
48
|
+
onChange={(event) => {
|
|
49
|
+
const files = Array.from(event.currentTarget.files ?? []);
|
|
50
|
+
if (files.length === 0) return;
|
|
51
|
+
|
|
52
|
+
attachment.upload({
|
|
53
|
+
metadata: { postSlug },
|
|
54
|
+
files,
|
|
55
|
+
});
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The hook exposes `status`, `progress`, `error`, `result`, `reset`, and `abort`.
|
|
63
|
+
It keeps uploads imperative and does not hide React Query; invalidate queries in
|
|
64
|
+
`onSuccess` when an upload changes visible app state.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const attachment = reactUploads.useUpload("posts.attachment", {
|
|
68
|
+
onSuccess() {
|
|
69
|
+
queryClient.invalidateQueries({ queryKey: rq(getPost).key() });
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API
|
|
75
|
+
|
|
76
|
+
- `createReactUploads({ uploads })` creates an adapter bound to a typed upload
|
|
77
|
+
client.
|
|
78
|
+
- `adapter.useUpload(name, options?)` returns state and methods for one upload
|
|
79
|
+
workflow.
|
|
80
|
+
- `adapter.useUploadMany(name, options?)` returns the same state with an
|
|
81
|
+
`upload(files, options)` convenience method.
|
|
82
|
+
|
|
83
|
+
## Related
|
|
84
|
+
|
|
85
|
+
- [`@beignet/core/uploads`](https://beignet.dev/uploads) - upload definitions
|
|
86
|
+
and runtime router
|
|
87
|
+
- [`@beignet/core/uploads/client`](https://beignet.dev/uploads) - typed browser
|
|
88
|
+
upload client
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { CompleteUploadResult, InferUploadMetadata, InferUploadResult, UploadDef, UploadFileConstraints } from "@beignet/core/uploads";
|
|
2
|
+
import type { UploadByName, UploadClient, UploadClientFileEvent, UploadClientName, UploadClientProgressEvent, UploadClientRegistry, UploadClientUploadOptions } from "@beignet/core/uploads/client";
|
|
3
|
+
/**
|
|
4
|
+
* Upload status exposed by Beignet React upload hooks.
|
|
5
|
+
*/
|
|
6
|
+
export type ReactUploadStatus = "idle" | "preparing" | "uploading" | "completing" | "success" | "error";
|
|
7
|
+
/**
|
|
8
|
+
* Per-file upload state tracked by React upload hooks.
|
|
9
|
+
*/
|
|
10
|
+
export interface ReactUploadFileState {
|
|
11
|
+
/**
|
|
12
|
+
* Browser file name.
|
|
13
|
+
*/
|
|
14
|
+
fileName: string;
|
|
15
|
+
/**
|
|
16
|
+
* Zero-based file index from the caller's file array.
|
|
17
|
+
*/
|
|
18
|
+
index: number;
|
|
19
|
+
/**
|
|
20
|
+
* Prepared upload id when available.
|
|
21
|
+
*/
|
|
22
|
+
uploadId?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Storage key when available.
|
|
25
|
+
*/
|
|
26
|
+
key?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Uploaded bytes reported by the browser or upload client.
|
|
29
|
+
*/
|
|
30
|
+
loaded: number;
|
|
31
|
+
/**
|
|
32
|
+
* Total bytes expected for the file.
|
|
33
|
+
*/
|
|
34
|
+
total: number;
|
|
35
|
+
/**
|
|
36
|
+
* Upload progress for this file from `0` to `100`.
|
|
37
|
+
*/
|
|
38
|
+
progress: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* State returned by Beignet React upload hooks.
|
|
42
|
+
*/
|
|
43
|
+
export interface ReactUploadState<Upload extends UploadDef> {
|
|
44
|
+
/**
|
|
45
|
+
* Current upload lifecycle status.
|
|
46
|
+
*/
|
|
47
|
+
status: ReactUploadStatus;
|
|
48
|
+
/**
|
|
49
|
+
* Aggregate upload progress from `0` to `100`.
|
|
50
|
+
*/
|
|
51
|
+
progress: number;
|
|
52
|
+
/**
|
|
53
|
+
* Aggregate upload progress from `0` to `1`.
|
|
54
|
+
*/
|
|
55
|
+
progressFraction: number;
|
|
56
|
+
/**
|
|
57
|
+
* Latest error thrown by the upload client.
|
|
58
|
+
*/
|
|
59
|
+
error: unknown | null;
|
|
60
|
+
/**
|
|
61
|
+
* Latest successful upload result.
|
|
62
|
+
*/
|
|
63
|
+
result: CompleteUploadResult<InferUploadResult<Upload>> | null;
|
|
64
|
+
/**
|
|
65
|
+
* Per-file progress state for the active or latest upload.
|
|
66
|
+
*/
|
|
67
|
+
files: readonly ReactUploadFileState[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Lifecycle callbacks accepted by React upload hooks.
|
|
71
|
+
*/
|
|
72
|
+
export interface ReactUploadLifecycleOptions<Upload extends UploadDef> {
|
|
73
|
+
/**
|
|
74
|
+
* Called when a file is about to be uploaded.
|
|
75
|
+
*/
|
|
76
|
+
onFileBegin?(event: UploadClientFileEvent): void;
|
|
77
|
+
/**
|
|
78
|
+
* Called when the upload client reports file progress.
|
|
79
|
+
*/
|
|
80
|
+
onProgress?(event: UploadClientProgressEvent): void;
|
|
81
|
+
/**
|
|
82
|
+
* Called after the upload completes successfully.
|
|
83
|
+
*/
|
|
84
|
+
onSuccess?(result: CompleteUploadResult<InferUploadResult<Upload>>): void | Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Called after the upload client throws.
|
|
87
|
+
*/
|
|
88
|
+
onError?(error: unknown): void | Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Called after either success or failure.
|
|
91
|
+
*/
|
|
92
|
+
onSettled?(result: CompleteUploadResult<InferUploadResult<Upload>> | undefined, error: unknown | undefined): void | Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Options shared by every upload started from one hook instance.
|
|
96
|
+
*/
|
|
97
|
+
export type ReactUploadHookOptions<Upload extends UploadDef> = Omit<UploadClientUploadOptions<Upload>, "metadata" | "files" | "onFileBegin" | "onProgress"> & ReactUploadLifecycleOptions<Upload>;
|
|
98
|
+
/**
|
|
99
|
+
* Options for starting one upload from `useUpload(...)`.
|
|
100
|
+
*/
|
|
101
|
+
export type ReactUploadStartOptions<Upload extends UploadDef> = UploadClientUploadOptions<Upload> & ReactUploadLifecycleOptions<Upload>;
|
|
102
|
+
/**
|
|
103
|
+
* Options for starting one upload from `useUploadMany(...)`.
|
|
104
|
+
*/
|
|
105
|
+
export type ReactUploadManyStartOptions<Upload extends UploadDef> = Omit<ReactUploadStartOptions<Upload>, "files">;
|
|
106
|
+
/**
|
|
107
|
+
* Options for starting a single-file upload from `uploadFile(...)`.
|
|
108
|
+
*/
|
|
109
|
+
export type ReactUploadFileStartOptions<Upload extends UploadDef> = Omit<ReactUploadStartOptions<Upload>, "files">;
|
|
110
|
+
/**
|
|
111
|
+
* React hook controller for one Beignet upload workflow.
|
|
112
|
+
*/
|
|
113
|
+
export interface ReactUploadController<Upload extends UploadDef> extends ReactUploadState<Upload> {
|
|
114
|
+
/**
|
|
115
|
+
* File input `accept` value derived from the upload manifest when available.
|
|
116
|
+
*/
|
|
117
|
+
accept: string | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Client-safe file constraints derived from the upload manifest when available.
|
|
120
|
+
*/
|
|
121
|
+
constraints: UploadFileConstraints | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* True while a request is active.
|
|
124
|
+
*/
|
|
125
|
+
isPending: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* True while the hook has no active or completed upload.
|
|
128
|
+
*/
|
|
129
|
+
isIdle: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* True while files are being prepared, uploaded, or completed.
|
|
132
|
+
*/
|
|
133
|
+
isUploading: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* True after the last upload completed successfully.
|
|
136
|
+
*/
|
|
137
|
+
isSuccess: boolean;
|
|
138
|
+
/**
|
|
139
|
+
* True after the last upload failed.
|
|
140
|
+
*/
|
|
141
|
+
isError: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Start an upload through the wrapped upload client.
|
|
144
|
+
*/
|
|
145
|
+
upload(options: ReactUploadStartOptions<Upload>): Promise<CompleteUploadResult<InferUploadResult<Upload>>>;
|
|
146
|
+
/**
|
|
147
|
+
* Convenience helper for uploading exactly one file.
|
|
148
|
+
*/
|
|
149
|
+
uploadFile(file: File, options: ReactUploadFileStartOptions<Upload>): Promise<CompleteUploadResult<InferUploadResult<Upload>>>;
|
|
150
|
+
/**
|
|
151
|
+
* Abort the active upload request, when one exists.
|
|
152
|
+
*/
|
|
153
|
+
abort(): void;
|
|
154
|
+
/**
|
|
155
|
+
* Clear hook state without changing files already stored by the server.
|
|
156
|
+
*/
|
|
157
|
+
reset(): void;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* React hook controller for many-file upload ergonomics.
|
|
161
|
+
*/
|
|
162
|
+
export interface ReactUploadManyController<Upload extends UploadDef> extends Omit<ReactUploadController<Upload>, "upload" | "uploadFile"> {
|
|
163
|
+
/**
|
|
164
|
+
* Start an upload with an explicit file array.
|
|
165
|
+
*/
|
|
166
|
+
upload(files: readonly File[], options: ReactUploadManyStartOptions<Upload>): Promise<CompleteUploadResult<InferUploadResult<Upload>>>;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Options for `createReactUploads(...)`.
|
|
170
|
+
*/
|
|
171
|
+
export interface CreateReactUploadsOptions<Registry extends UploadClientRegistry> {
|
|
172
|
+
/**
|
|
173
|
+
* Typed Beignet upload client created by `createUploadClient(...)`.
|
|
174
|
+
*/
|
|
175
|
+
uploads: UploadClient<Registry>;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* React adapter bound to one typed Beignet upload client.
|
|
179
|
+
*/
|
|
180
|
+
export interface ReactUploadsAdapter<Registry extends UploadClientRegistry> {
|
|
181
|
+
/**
|
|
182
|
+
* Track state for one upload workflow.
|
|
183
|
+
*/
|
|
184
|
+
useUpload<Name extends UploadClientName<Registry>>(uploadName: Name, options?: ReactUploadHookOptions<UploadByName<Registry, Name>>): ReactUploadController<UploadByName<Registry, Name>>;
|
|
185
|
+
/**
|
|
186
|
+
* Track state for one many-file upload workflow.
|
|
187
|
+
*/
|
|
188
|
+
useUploadMany<Name extends UploadClientName<Registry>>(uploadName: Name, options?: ReactUploadHookOptions<UploadByName<Registry, Name>>): ReactUploadManyController<UploadByName<Registry, Name>>;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Create React hooks for a typed Beignet upload client.
|
|
192
|
+
*/
|
|
193
|
+
export declare function createReactUploads<Registry extends UploadClientRegistry>(options: CreateReactUploadsOptions<Registry>): ReactUploadsAdapter<Registry>;
|
|
194
|
+
/**
|
|
195
|
+
* Infer upload metadata for a named upload in a registry.
|
|
196
|
+
*/
|
|
197
|
+
export type InferReactUploadMetadata<Registry extends UploadClientRegistry, Name extends UploadClientName<Registry>> = InferUploadMetadata<UploadByName<Registry, Name>>;
|
|
198
|
+
/**
|
|
199
|
+
* Infer upload completion result for a named upload in a registry.
|
|
200
|
+
*/
|
|
201
|
+
export type InferReactUploadResult<Registry extends UploadClientRegistry, Name extends UploadClientName<Registry>> = InferUploadResult<UploadByName<Registry, Name>>;
|
|
202
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
|
+
const INITIAL_STATE = {
|
|
3
|
+
status: "idle",
|
|
4
|
+
progress: 0,
|
|
5
|
+
progressFraction: 0,
|
|
6
|
+
error: null,
|
|
7
|
+
result: null,
|
|
8
|
+
files: [],
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Create React hooks for a typed Beignet upload client.
|
|
12
|
+
*/
|
|
13
|
+
export function createReactUploads(options) {
|
|
14
|
+
const { uploads } = options;
|
|
15
|
+
function useUpload(uploadName, hookOptions = {}) {
|
|
16
|
+
const abortRef = useRef(null);
|
|
17
|
+
const progressRef = useRef(new Map());
|
|
18
|
+
const [state, setState] = useState(INITIAL_STATE);
|
|
19
|
+
const reset = useCallback(() => {
|
|
20
|
+
progressRef.current.clear();
|
|
21
|
+
setState(INITIAL_STATE);
|
|
22
|
+
}, []);
|
|
23
|
+
const abort = useCallback(() => {
|
|
24
|
+
abortRef.current?.abort();
|
|
25
|
+
}, []);
|
|
26
|
+
const upload = useCallback(async (startOptions) => {
|
|
27
|
+
abortRef.current?.abort();
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
abortRef.current = controller;
|
|
30
|
+
const { onFileBegin: hookOnFileBegin, onProgress: hookOnProgress, onSuccess: hookOnSuccess, onError: hookOnError, onSettled: hookOnSettled, ...hookClientOptions } = hookOptions;
|
|
31
|
+
const { onFileBegin: startOnFileBegin, onProgress: startOnProgress, onSuccess: startOnSuccess, onError: startOnError, onSettled: startOnSettled, ...startClientOptions } = startOptions;
|
|
32
|
+
const isCurrentUpload = () => abortRef.current === controller;
|
|
33
|
+
const externalSignal = startClientOptions.signal ?? hookClientOptions.signal;
|
|
34
|
+
const abortFromExternalSignal = () => controller.abort();
|
|
35
|
+
if (externalSignal?.aborted) {
|
|
36
|
+
controller.abort();
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
externalSignal?.addEventListener("abort", abortFromExternalSignal, {
|
|
40
|
+
once: true,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
progressRef.current.clear();
|
|
44
|
+
setState({
|
|
45
|
+
...INITIAL_STATE,
|
|
46
|
+
status: "preparing",
|
|
47
|
+
});
|
|
48
|
+
let settledResult;
|
|
49
|
+
let settledError;
|
|
50
|
+
try {
|
|
51
|
+
const result = await uploads.upload(uploadName, {
|
|
52
|
+
...hookClientOptions,
|
|
53
|
+
...startClientOptions,
|
|
54
|
+
signal: controller.signal,
|
|
55
|
+
onFileBegin(event) {
|
|
56
|
+
recordFileBegin(progressRef.current, event);
|
|
57
|
+
if (isCurrentUpload()) {
|
|
58
|
+
setState((current) => ({
|
|
59
|
+
...current,
|
|
60
|
+
status: "uploading",
|
|
61
|
+
files: currentFileState(progressRef.current),
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
hookOnFileBegin?.(event);
|
|
65
|
+
startOnFileBegin?.(event);
|
|
66
|
+
},
|
|
67
|
+
onProgress(event) {
|
|
68
|
+
recordFileProgress(progressRef.current, event);
|
|
69
|
+
const aggregate = aggregateProgress(progressRef.current);
|
|
70
|
+
if (isCurrentUpload()) {
|
|
71
|
+
setState((current) => ({
|
|
72
|
+
...current,
|
|
73
|
+
status: aggregate.progressFraction >= 1
|
|
74
|
+
? "completing"
|
|
75
|
+
: "uploading",
|
|
76
|
+
progress: aggregate.progress,
|
|
77
|
+
progressFraction: aggregate.progressFraction,
|
|
78
|
+
files: currentFileState(progressRef.current),
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
hookOnProgress?.(event);
|
|
82
|
+
startOnProgress?.(event);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
settledResult = result;
|
|
86
|
+
await hookOnSuccess?.(settledResult);
|
|
87
|
+
await startOnSuccess?.(settledResult);
|
|
88
|
+
if (isCurrentUpload()) {
|
|
89
|
+
setState((current) => ({
|
|
90
|
+
...current,
|
|
91
|
+
status: "success",
|
|
92
|
+
progress: 100,
|
|
93
|
+
progressFraction: 1,
|
|
94
|
+
error: null,
|
|
95
|
+
result: settledResult ?? null,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
return settledResult;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
settledError = error;
|
|
102
|
+
await hookOnError?.(error);
|
|
103
|
+
await startOnError?.(error);
|
|
104
|
+
if (isCurrentUpload()) {
|
|
105
|
+
setState((current) => ({
|
|
106
|
+
...current,
|
|
107
|
+
status: "error",
|
|
108
|
+
error,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
externalSignal?.removeEventListener("abort", abortFromExternalSignal);
|
|
115
|
+
if (abortRef.current === controller) {
|
|
116
|
+
abortRef.current = null;
|
|
117
|
+
}
|
|
118
|
+
await hookOnSettled?.(settledResult, settledError);
|
|
119
|
+
await startOnSettled?.(settledResult, settledError);
|
|
120
|
+
}
|
|
121
|
+
}, [hookOptions, uploadName]);
|
|
122
|
+
const uploadFile = useCallback((file, startOptions) => upload({
|
|
123
|
+
...startOptions,
|
|
124
|
+
files: [file],
|
|
125
|
+
}), [upload]);
|
|
126
|
+
return useMemo(() => {
|
|
127
|
+
const isIdle = state.status === "idle";
|
|
128
|
+
const isSuccess = state.status === "success";
|
|
129
|
+
const isError = state.status === "error";
|
|
130
|
+
const isUploading = state.status === "preparing" ||
|
|
131
|
+
state.status === "uploading" ||
|
|
132
|
+
state.status === "completing";
|
|
133
|
+
return {
|
|
134
|
+
...state,
|
|
135
|
+
accept: uploads.accept(uploadName),
|
|
136
|
+
constraints: uploads.constraints(uploadName),
|
|
137
|
+
isPending: isUploading,
|
|
138
|
+
isIdle,
|
|
139
|
+
isUploading,
|
|
140
|
+
isSuccess,
|
|
141
|
+
isError,
|
|
142
|
+
upload,
|
|
143
|
+
uploadFile,
|
|
144
|
+
abort,
|
|
145
|
+
reset,
|
|
146
|
+
};
|
|
147
|
+
}, [state, uploadName, upload, uploadFile, abort, reset]);
|
|
148
|
+
}
|
|
149
|
+
function useUploadMany(uploadName, hookOptions = {}) {
|
|
150
|
+
const controller = useUpload(uploadName, hookOptions);
|
|
151
|
+
const uploadMany = useCallback((files, startOptions) => controller.upload({
|
|
152
|
+
...startOptions,
|
|
153
|
+
files,
|
|
154
|
+
}), [controller.upload]);
|
|
155
|
+
return useMemo(() => ({
|
|
156
|
+
...controller,
|
|
157
|
+
upload: uploadMany,
|
|
158
|
+
}), [controller, uploadMany]);
|
|
159
|
+
}
|
|
160
|
+
return { useUpload, useUploadMany };
|
|
161
|
+
}
|
|
162
|
+
function recordFileBegin(progress, event) {
|
|
163
|
+
progress.set(event.index, {
|
|
164
|
+
fileName: event.fileName,
|
|
165
|
+
index: event.index,
|
|
166
|
+
uploadId: event.uploadId,
|
|
167
|
+
key: event.key,
|
|
168
|
+
loaded: 0,
|
|
169
|
+
total: event.file.size,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function recordFileProgress(progress, event) {
|
|
173
|
+
progress.set(event.index, {
|
|
174
|
+
fileName: event.fileName,
|
|
175
|
+
index: event.index,
|
|
176
|
+
uploadId: event.uploadId,
|
|
177
|
+
key: event.key,
|
|
178
|
+
loaded: event.loaded,
|
|
179
|
+
total: event.total,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function currentFileState(progress) {
|
|
183
|
+
return [...progress.values()]
|
|
184
|
+
.sort((a, b) => a.index - b.index)
|
|
185
|
+
.map((file) => ({
|
|
186
|
+
...file,
|
|
187
|
+
progress: percent(file.loaded, file.total),
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
function aggregateProgress(progress) {
|
|
191
|
+
const files = [...progress.values()];
|
|
192
|
+
if (files.length === 0) {
|
|
193
|
+
return { progress: 0, progressFraction: 0 };
|
|
194
|
+
}
|
|
195
|
+
const totals = files.reduce((acc, file) => ({
|
|
196
|
+
loaded: acc.loaded + file.loaded,
|
|
197
|
+
total: acc.total + file.total,
|
|
198
|
+
}), { loaded: 0, total: 0 });
|
|
199
|
+
const progressFraction = totals.total > 0
|
|
200
|
+
? Math.min(1, totals.loaded / totals.total)
|
|
201
|
+
: Math.min(1, files.reduce((sum, file) => sum + percent(file.loaded, file.total) / 100, 0) / files.length);
|
|
202
|
+
return {
|
|
203
|
+
progress: Math.round(progressFraction * 100),
|
|
204
|
+
progressFraction,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function percent(loaded, total) {
|
|
208
|
+
if (total <= 0)
|
|
209
|
+
return loaded > 0 ? 100 : 0;
|
|
210
|
+
return Math.round(Math.min(1, loaded / total) * 100);
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AA0P/D,MAAM,aAAa,GAAG;IACpB,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,CAAC;IACX,gBAAgB,EAAE,CAAC;IACnB,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,EAAE;CAC4B,CAAC;AAExC;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAA4C;IAE5C,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAE5B,SAAS,SAAS,CAChB,UAAgB,EAChB,cAAoE,EAAE;QAKtE,MAAM,QAAQ,GAAG,MAAM,CAAyB,IAAI,CAAC,CAAC;QACtD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,GAAG,EAAwB,CAAC,CAAC;QAC5D,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAChC,aAAyC,CAC1C,CAAC;QAEF,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;YAC7B,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YAC5B,QAAQ,CAAC,aAAyC,CAAC,CAAC;QACtD,CAAC,EAAE,EAAE,CAAC,CAAC;QAEP,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;YAC7B,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;QAC5B,CAAC,EAAE,EAAE,CAAC,CAAC;QAEP,MAAM,MAAM,GAAG,WAAW,CACxB,KAAK,EACH,YAA6C,EAC5B,EAAE;YACnB,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;YAC1B,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,QAAQ,CAAC,OAAO,GAAG,UAAU,CAAC;YAE9B,MAAM,EACJ,WAAW,EAAE,eAAe,EAC5B,UAAU,EAAE,cAAc,EAC1B,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,aAAa,EACxB,GAAG,iBAAiB,EACrB,GAAG,WAAW,CAAC;YAChB,MAAM,EACJ,WAAW,EAAE,gBAAgB,EAC7B,UAAU,EAAE,eAAe,EAC3B,SAAS,EAAE,cAAc,EACzB,OAAO,EAAE,YAAY,EACrB,SAAS,EAAE,cAAc,EACzB,GAAG,kBAAkB,EACtB,GAAG,YAAY,CAAC;YAEjB,MAAM,eAAe,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,KAAK,UAAU,CAAC;YAC9D,MAAM,cAAc,GAClB,kBAAkB,CAAC,MAAM,IAAI,iBAAiB,CAAC,MAAM,CAAC;YACxD,MAAM,uBAAuB,GAAG,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;YACzD,IAAI,cAAc,EAAE,OAAO,EAAE,CAAC;gBAC5B,UAAU,CAAC,KAAK,EAAE,CAAC;YACrB,CAAC;iBAAM,CAAC;gBACN,cAAc,EAAE,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,EAAE;oBACjE,IAAI,EAAE,IAAI;iBACX,CAAC,CAAC;YACL,CAAC;YAED,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YAC5B,QAAQ,CAAC;gBACP,GAAI,aAA0C;gBAC9C,MAAM,EAAE,WAAW;aACpB,CAAC,CAAC;YAEH,IAAI,aAAiC,CAAC;YACtC,IAAI,YAAqB,CAAC;YAE1B,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE;oBAC9C,GAAG,iBAAiB;oBACpB,GAAG,kBAAkB;oBACrB,MAAM,EAAE,UAAU,CAAC,MAAM;oBACzB,WAAW,CAAC,KAAK;wBACf,eAAe,CAAC,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;wBAC5C,IAAI,eAAe,EAAE,EAAE,CAAC;4BACtB,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gCACrB,GAAG,OAAO;gCACV,MAAM,EAAE,WAAW;gCACnB,KAAK,EAAE,gBAAgB,CAAC,WAAW,CAAC,OAAO,CAAC;6BAC7C,CAAC,CAAC,CAAC;wBACN,CAAC;wBACD,eAAe,EAAE,CAAC,KAAK,CAAC,CAAC;wBACzB,gBAAgB,EAAE,CAAC,KAAK,CAAC,CAAC;oBAC5B,CAAC;oBACD,UAAU,CAAC,KAAK;wBACd,kBAAkB,CAAC,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;wBAC/C,MAAM,SAAS,GAAG,iBAAiB,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;wBACzD,IAAI,eAAe,EAAE,EAAE,CAAC;4BACtB,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gCACrB,GAAG,OAAO;gCACV,MAAM,EACJ,SAAS,CAAC,gBAAgB,IAAI,CAAC;oCAC7B,CAAC,CAAC,YAAY;oCACd,CAAC,CAAC,WAAW;gCACjB,QAAQ,EAAE,SAAS,CAAC,QAAQ;gCAC5B,gBAAgB,EAAE,SAAS,CAAC,gBAAgB;gCAC5C,KAAK,EAAE,gBAAgB,CAAC,WAAW,CAAC,OAAO,CAAC;6BAC7C,CAAC,CAAC,CAAC;wBACN,CAAC;wBACD,cAAc,EAAE,CAAC,KAAK,CAAC,CAAC;wBACxB,eAAe,EAAE,CAAC,KAAK,CAAC,CAAC;oBAC3B,CAAC;iBACF,CAAC,CAAC;gBAEH,aAAa,GAAG,MAAgB,CAAC;gBACjC,MAAM,aAAa,EAAE,CAAC,aAAa,CAAC,CAAC;gBACrC,MAAM,cAAc,EAAE,CAAC,aAAa,CAAC,CAAC;gBACtC,IAAI,eAAe,EAAE,EAAE,CAAC;oBACtB,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;wBACrB,GAAG,OAAO;wBACV,MAAM,EAAE,SAAS;wBACjB,QAAQ,EAAE,GAAG;wBACb,gBAAgB,EAAE,CAAC;wBACnB,KAAK,EAAE,IAAI;wBACX,MAAM,EAAE,aAAa,IAAI,IAAI;qBAC9B,CAAC,CAAC,CAAC;gBACN,CAAC;gBACD,OAAO,aAAa,CAAC;YACvB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,YAAY,GAAG,KAAK,CAAC;gBACrB,MAAM,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC;gBAC3B,MAAM,YAAY,EAAE,CAAC,KAAK,CAAC,CAAC;gBAC5B,IAAI,eAAe,EAAE,EAAE,CAAC;oBACtB,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;wBACrB,GAAG,OAAO;wBACV,MAAM,EAAE,OAAO;wBACf,KAAK;qBACN,CAAC,CAAC,CAAC;gBACN,CAAC;gBACD,MAAM,KAAK,CAAC;YACd,CAAC;oBAAS,CAAC;gBACT,cAAc,EAAE,mBAAmB,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC;gBACtE,IAAI,QAAQ,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;oBACpC,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;gBAC1B,CAAC;gBACD,MAAM,aAAa,EAAE,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;gBACnD,MAAM,cAAc,EAAE,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;YACtD,CAAC;QACH,CAAC,EACD,CAAC,WAAW,EAAE,UAAU,CAAC,CAC1B,CAAC;QAEF,MAAM,UAAU,GAAG,WAAW,CAC5B,CACE,IAAU,EACV,YAAiD,EAChC,EAAE,CACnB,MAAM,CAAC;YACL,GAAG,YAAY;YACf,KAAK,EAAE,CAAC,IAAI,CAAC;SACd,CAAC,EACJ,CAAC,MAAM,CAAC,CACT,CAAC;QAEF,OAAO,OAAO,CAAC,GAAG,EAAE;YAClB,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,MAAM,CAAC;YACvC,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC;YAC7C,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,KAAK,OAAO,CAAC;YACzC,MAAM,WAAW,GACf,KAAK,CAAC,MAAM,KAAK,WAAW;gBAC5B,KAAK,CAAC,MAAM,KAAK,WAAW;gBAC5B,KAAK,CAAC,MAAM,KAAK,YAAY,CAAC;YAEhC,OAAO;gBACL,GAAG,KAAK;gBACR,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC;gBAClC,WAAW,EAAE,OAAO,CAAC,WAAW,CAAC,UAAU,CAAC;gBAC5C,SAAS,EAAE,WAAW;gBACtB,MAAM;gBACN,WAAW;gBACX,SAAS;gBACT,OAAO;gBACP,MAAM;gBACN,UAAU;gBACV,KAAK;gBACL,KAAK;aACN,CAAC;QACJ,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,SAAS,aAAa,CACpB,UAAgB,EAChB,cAAoE,EAAE;QAKtE,MAAM,UAAU,GAAG,SAAS,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QACtD,MAAM,UAAU,GAAG,WAAW,CAC5B,CACE,KAAsB,EACtB,YAAiD,EAChC,EAAE,CACnB,UAAU,CAAC,MAAM,CAAC;YAChB,GAAG,YAAY;YACf,KAAK;SACN,CAAC,EACJ,CAAC,UAAU,CAAC,MAAM,CAAC,CACpB,CAAC;QAEF,OAAO,OAAO,CACZ,GAAG,EAAE,CAAC,CAAC;YACL,GAAG,UAAU;YACb,MAAM,EAAE,UAAU;SACnB,CAAC,EACF,CAAC,UAAU,EAAE,UAAU,CAAC,CACzB,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC;AACtC,CAAC;AAED,SAAS,eAAe,CACtB,QAAmC,EACnC,KAA4B;IAE5B,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE;QACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,MAAM,EAAE,CAAC;QACT,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI;KACvB,CAAC,CAAC;AACL,CAAC;AAED,SAAS,kBAAkB,CACzB,QAAmC,EACnC,KAAgC;IAEhC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE;QACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,KAAK,EAAE,KAAK,CAAC,KAAK;KACnB,CAAC,CAAC;AACL,CAAC;AAED,SAAS,gBAAgB,CACvB,QAAmC;IAEnC,OAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;SAC1B,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;SACjC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACd,GAAG,IAAI;QACP,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC;KAC3C,CAAC,CAAC,CAAC;AACR,CAAC;AAED,SAAS,iBAAiB,CAAC,QAAmC;IAI5D,MAAM,KAAK,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC;IAC9C,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CACzB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QACd,MAAM,EAAE,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM;QAChC,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK;KAC9B,CAAC,EACF,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CACxB,CAAC;IAEF,MAAM,gBAAgB,GACpB,MAAM,CAAC,KAAK,GAAG,CAAC;QACd,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC;QAC3C,CAAC,CAAC,IAAI,CAAC,GAAG,CACN,CAAC,EACD,KAAK,CAAC,MAAM,CACV,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,EAC3D,CAAC,CACF,GAAG,KAAK,CAAC,MAAM,CACjB,CAAC;IAER,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,gBAAgB,GAAG,GAAG,CAAC;QAC5C,gBAAgB;KACjB,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,MAAc,EAAE,KAAa;IAC5C,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC;AACvD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beignet/react-uploads",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "React upload hooks for Beignet upload clients",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"!src/**/*.test.ts",
|
|
18
|
+
"!src/**/*.test.tsx",
|
|
19
|
+
"!src/**/*.test-d.ts",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"dev": "tsc --watch",
|
|
26
|
+
"clean": "rm -rf dist coverage .turbo",
|
|
27
|
+
"test": "bun test",
|
|
28
|
+
"test:coverage": "bun test --coverage",
|
|
29
|
+
"lint": "biome check ."
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"contract",
|
|
33
|
+
"api",
|
|
34
|
+
"typescript",
|
|
35
|
+
"react",
|
|
36
|
+
"uploads"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/taylorbryant/beignet.git",
|
|
42
|
+
"directory": "packages/react-uploads"
|
|
43
|
+
},
|
|
44
|
+
"author": "Taylor Bryant",
|
|
45
|
+
"homepage": "https://github.com/taylorbryant/beignet#readme",
|
|
46
|
+
"bugs": "https://github.com/taylorbryant/beignet/issues",
|
|
47
|
+
"sideEffects": false,
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@beignet/core": "*"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@testing-library/dom": "^9.3.4",
|
|
62
|
+
"@testing-library/react": "^14.3.1",
|
|
63
|
+
"@types/bun": "^1.3.13",
|
|
64
|
+
"@types/node": "^20.10.0",
|
|
65
|
+
"@types/react": "^19.0.0",
|
|
66
|
+
"happy-dom": "^20.0.11",
|
|
67
|
+
"react": "^19.0.0",
|
|
68
|
+
"react-dom": "^19.2.5",
|
|
69
|
+
"typescript": "^5.3.0",
|
|
70
|
+
"zod": "^4.0.0"
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CompleteUploadResult,
|
|
3
|
+
InferUploadMetadata,
|
|
4
|
+
InferUploadResult,
|
|
5
|
+
UploadDef,
|
|
6
|
+
UploadFileConstraints,
|
|
7
|
+
} from "@beignet/core/uploads";
|
|
8
|
+
import type {
|
|
9
|
+
UploadByName,
|
|
10
|
+
UploadClient,
|
|
11
|
+
UploadClientFileEvent,
|
|
12
|
+
UploadClientName,
|
|
13
|
+
UploadClientProgressEvent,
|
|
14
|
+
UploadClientRegistry,
|
|
15
|
+
UploadClientUploadOptions,
|
|
16
|
+
} from "@beignet/core/uploads/client";
|
|
17
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Upload status exposed by Beignet React upload hooks.
|
|
21
|
+
*/
|
|
22
|
+
export type ReactUploadStatus =
|
|
23
|
+
| "idle"
|
|
24
|
+
| "preparing"
|
|
25
|
+
| "uploading"
|
|
26
|
+
| "completing"
|
|
27
|
+
| "success"
|
|
28
|
+
| "error";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Per-file upload state tracked by React upload hooks.
|
|
32
|
+
*/
|
|
33
|
+
export interface ReactUploadFileState {
|
|
34
|
+
/**
|
|
35
|
+
* Browser file name.
|
|
36
|
+
*/
|
|
37
|
+
fileName: string;
|
|
38
|
+
/**
|
|
39
|
+
* Zero-based file index from the caller's file array.
|
|
40
|
+
*/
|
|
41
|
+
index: number;
|
|
42
|
+
/**
|
|
43
|
+
* Prepared upload id when available.
|
|
44
|
+
*/
|
|
45
|
+
uploadId?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Storage key when available.
|
|
48
|
+
*/
|
|
49
|
+
key?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Uploaded bytes reported by the browser or upload client.
|
|
52
|
+
*/
|
|
53
|
+
loaded: number;
|
|
54
|
+
/**
|
|
55
|
+
* Total bytes expected for the file.
|
|
56
|
+
*/
|
|
57
|
+
total: number;
|
|
58
|
+
/**
|
|
59
|
+
* Upload progress for this file from `0` to `100`.
|
|
60
|
+
*/
|
|
61
|
+
progress: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* State returned by Beignet React upload hooks.
|
|
66
|
+
*/
|
|
67
|
+
export interface ReactUploadState<Upload extends UploadDef> {
|
|
68
|
+
/**
|
|
69
|
+
* Current upload lifecycle status.
|
|
70
|
+
*/
|
|
71
|
+
status: ReactUploadStatus;
|
|
72
|
+
/**
|
|
73
|
+
* Aggregate upload progress from `0` to `100`.
|
|
74
|
+
*/
|
|
75
|
+
progress: number;
|
|
76
|
+
/**
|
|
77
|
+
* Aggregate upload progress from `0` to `1`.
|
|
78
|
+
*/
|
|
79
|
+
progressFraction: number;
|
|
80
|
+
/**
|
|
81
|
+
* Latest error thrown by the upload client.
|
|
82
|
+
*/
|
|
83
|
+
error: unknown | null;
|
|
84
|
+
/**
|
|
85
|
+
* Latest successful upload result.
|
|
86
|
+
*/
|
|
87
|
+
result: CompleteUploadResult<InferUploadResult<Upload>> | null;
|
|
88
|
+
/**
|
|
89
|
+
* Per-file progress state for the active or latest upload.
|
|
90
|
+
*/
|
|
91
|
+
files: readonly ReactUploadFileState[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Lifecycle callbacks accepted by React upload hooks.
|
|
96
|
+
*/
|
|
97
|
+
export interface ReactUploadLifecycleOptions<Upload extends UploadDef> {
|
|
98
|
+
/**
|
|
99
|
+
* Called when a file is about to be uploaded.
|
|
100
|
+
*/
|
|
101
|
+
onFileBegin?(event: UploadClientFileEvent): void;
|
|
102
|
+
/**
|
|
103
|
+
* Called when the upload client reports file progress.
|
|
104
|
+
*/
|
|
105
|
+
onProgress?(event: UploadClientProgressEvent): void;
|
|
106
|
+
/**
|
|
107
|
+
* Called after the upload completes successfully.
|
|
108
|
+
*/
|
|
109
|
+
onSuccess?(
|
|
110
|
+
result: CompleteUploadResult<InferUploadResult<Upload>>,
|
|
111
|
+
): void | Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Called after the upload client throws.
|
|
114
|
+
*/
|
|
115
|
+
onError?(error: unknown): void | Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Called after either success or failure.
|
|
118
|
+
*/
|
|
119
|
+
onSettled?(
|
|
120
|
+
result: CompleteUploadResult<InferUploadResult<Upload>> | undefined,
|
|
121
|
+
error: unknown | undefined,
|
|
122
|
+
): void | Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Options shared by every upload started from one hook instance.
|
|
127
|
+
*/
|
|
128
|
+
export type ReactUploadHookOptions<Upload extends UploadDef> = Omit<
|
|
129
|
+
UploadClientUploadOptions<Upload>,
|
|
130
|
+
"metadata" | "files" | "onFileBegin" | "onProgress"
|
|
131
|
+
> &
|
|
132
|
+
ReactUploadLifecycleOptions<Upload>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Options for starting one upload from `useUpload(...)`.
|
|
136
|
+
*/
|
|
137
|
+
export type ReactUploadStartOptions<Upload extends UploadDef> =
|
|
138
|
+
UploadClientUploadOptions<Upload> & ReactUploadLifecycleOptions<Upload>;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Options for starting one upload from `useUploadMany(...)`.
|
|
142
|
+
*/
|
|
143
|
+
export type ReactUploadManyStartOptions<Upload extends UploadDef> = Omit<
|
|
144
|
+
ReactUploadStartOptions<Upload>,
|
|
145
|
+
"files"
|
|
146
|
+
>;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Options for starting a single-file upload from `uploadFile(...)`.
|
|
150
|
+
*/
|
|
151
|
+
export type ReactUploadFileStartOptions<Upload extends UploadDef> = Omit<
|
|
152
|
+
ReactUploadStartOptions<Upload>,
|
|
153
|
+
"files"
|
|
154
|
+
>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* React hook controller for one Beignet upload workflow.
|
|
158
|
+
*/
|
|
159
|
+
export interface ReactUploadController<Upload extends UploadDef>
|
|
160
|
+
extends ReactUploadState<Upload> {
|
|
161
|
+
/**
|
|
162
|
+
* File input `accept` value derived from the upload manifest when available.
|
|
163
|
+
*/
|
|
164
|
+
accept: string | undefined;
|
|
165
|
+
/**
|
|
166
|
+
* Client-safe file constraints derived from the upload manifest when available.
|
|
167
|
+
*/
|
|
168
|
+
constraints: UploadFileConstraints | undefined;
|
|
169
|
+
/**
|
|
170
|
+
* True while a request is active.
|
|
171
|
+
*/
|
|
172
|
+
isPending: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* True while the hook has no active or completed upload.
|
|
175
|
+
*/
|
|
176
|
+
isIdle: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* True while files are being prepared, uploaded, or completed.
|
|
179
|
+
*/
|
|
180
|
+
isUploading: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* True after the last upload completed successfully.
|
|
183
|
+
*/
|
|
184
|
+
isSuccess: boolean;
|
|
185
|
+
/**
|
|
186
|
+
* True after the last upload failed.
|
|
187
|
+
*/
|
|
188
|
+
isError: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Start an upload through the wrapped upload client.
|
|
191
|
+
*/
|
|
192
|
+
upload(
|
|
193
|
+
options: ReactUploadStartOptions<Upload>,
|
|
194
|
+
): Promise<CompleteUploadResult<InferUploadResult<Upload>>>;
|
|
195
|
+
/**
|
|
196
|
+
* Convenience helper for uploading exactly one file.
|
|
197
|
+
*/
|
|
198
|
+
uploadFile(
|
|
199
|
+
file: File,
|
|
200
|
+
options: ReactUploadFileStartOptions<Upload>,
|
|
201
|
+
): Promise<CompleteUploadResult<InferUploadResult<Upload>>>;
|
|
202
|
+
/**
|
|
203
|
+
* Abort the active upload request, when one exists.
|
|
204
|
+
*/
|
|
205
|
+
abort(): void;
|
|
206
|
+
/**
|
|
207
|
+
* Clear hook state without changing files already stored by the server.
|
|
208
|
+
*/
|
|
209
|
+
reset(): void;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* React hook controller for many-file upload ergonomics.
|
|
214
|
+
*/
|
|
215
|
+
export interface ReactUploadManyController<Upload extends UploadDef>
|
|
216
|
+
extends Omit<ReactUploadController<Upload>, "upload" | "uploadFile"> {
|
|
217
|
+
/**
|
|
218
|
+
* Start an upload with an explicit file array.
|
|
219
|
+
*/
|
|
220
|
+
upload(
|
|
221
|
+
files: readonly File[],
|
|
222
|
+
options: ReactUploadManyStartOptions<Upload>,
|
|
223
|
+
): Promise<CompleteUploadResult<InferUploadResult<Upload>>>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Options for `createReactUploads(...)`.
|
|
228
|
+
*/
|
|
229
|
+
export interface CreateReactUploadsOptions<
|
|
230
|
+
Registry extends UploadClientRegistry,
|
|
231
|
+
> {
|
|
232
|
+
/**
|
|
233
|
+
* Typed Beignet upload client created by `createUploadClient(...)`.
|
|
234
|
+
*/
|
|
235
|
+
uploads: UploadClient<Registry>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* React adapter bound to one typed Beignet upload client.
|
|
240
|
+
*/
|
|
241
|
+
export interface ReactUploadsAdapter<Registry extends UploadClientRegistry> {
|
|
242
|
+
/**
|
|
243
|
+
* Track state for one upload workflow.
|
|
244
|
+
*/
|
|
245
|
+
useUpload<Name extends UploadClientName<Registry>>(
|
|
246
|
+
uploadName: Name,
|
|
247
|
+
options?: ReactUploadHookOptions<UploadByName<Registry, Name>>,
|
|
248
|
+
): ReactUploadController<UploadByName<Registry, Name>>;
|
|
249
|
+
/**
|
|
250
|
+
* Track state for one many-file upload workflow.
|
|
251
|
+
*/
|
|
252
|
+
useUploadMany<Name extends UploadClientName<Registry>>(
|
|
253
|
+
uploadName: Name,
|
|
254
|
+
options?: ReactUploadHookOptions<UploadByName<Registry, Name>>,
|
|
255
|
+
): ReactUploadManyController<UploadByName<Registry, Name>>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
type FileProgress = {
|
|
259
|
+
fileName: string;
|
|
260
|
+
index: number;
|
|
261
|
+
uploadId?: string;
|
|
262
|
+
key?: string;
|
|
263
|
+
loaded: number;
|
|
264
|
+
total: number;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const INITIAL_STATE = {
|
|
268
|
+
status: "idle",
|
|
269
|
+
progress: 0,
|
|
270
|
+
progressFraction: 0,
|
|
271
|
+
error: null,
|
|
272
|
+
result: null,
|
|
273
|
+
files: [],
|
|
274
|
+
} satisfies ReactUploadState<UploadDef>;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Create React hooks for a typed Beignet upload client.
|
|
278
|
+
*/
|
|
279
|
+
export function createReactUploads<Registry extends UploadClientRegistry>(
|
|
280
|
+
options: CreateReactUploadsOptions<Registry>,
|
|
281
|
+
): ReactUploadsAdapter<Registry> {
|
|
282
|
+
const { uploads } = options;
|
|
283
|
+
|
|
284
|
+
function useUpload<Name extends UploadClientName<Registry>>(
|
|
285
|
+
uploadName: Name,
|
|
286
|
+
hookOptions: ReactUploadHookOptions<UploadByName<Registry, Name>> = {},
|
|
287
|
+
): ReactUploadController<UploadByName<Registry, Name>> {
|
|
288
|
+
type Upload = UploadByName<Registry, Name>;
|
|
289
|
+
type Result = CompleteUploadResult<InferUploadResult<Upload>>;
|
|
290
|
+
|
|
291
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
292
|
+
const progressRef = useRef(new Map<number, FileProgress>());
|
|
293
|
+
const [state, setState] = useState<ReactUploadState<Upload>>(
|
|
294
|
+
INITIAL_STATE as ReactUploadState<Upload>,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const reset = useCallback(() => {
|
|
298
|
+
progressRef.current.clear();
|
|
299
|
+
setState(INITIAL_STATE as ReactUploadState<Upload>);
|
|
300
|
+
}, []);
|
|
301
|
+
|
|
302
|
+
const abort = useCallback(() => {
|
|
303
|
+
abortRef.current?.abort();
|
|
304
|
+
}, []);
|
|
305
|
+
|
|
306
|
+
const upload = useCallback(
|
|
307
|
+
async (
|
|
308
|
+
startOptions: ReactUploadStartOptions<Upload>,
|
|
309
|
+
): Promise<Result> => {
|
|
310
|
+
abortRef.current?.abort();
|
|
311
|
+
const controller = new AbortController();
|
|
312
|
+
abortRef.current = controller;
|
|
313
|
+
|
|
314
|
+
const {
|
|
315
|
+
onFileBegin: hookOnFileBegin,
|
|
316
|
+
onProgress: hookOnProgress,
|
|
317
|
+
onSuccess: hookOnSuccess,
|
|
318
|
+
onError: hookOnError,
|
|
319
|
+
onSettled: hookOnSettled,
|
|
320
|
+
...hookClientOptions
|
|
321
|
+
} = hookOptions;
|
|
322
|
+
const {
|
|
323
|
+
onFileBegin: startOnFileBegin,
|
|
324
|
+
onProgress: startOnProgress,
|
|
325
|
+
onSuccess: startOnSuccess,
|
|
326
|
+
onError: startOnError,
|
|
327
|
+
onSettled: startOnSettled,
|
|
328
|
+
...startClientOptions
|
|
329
|
+
} = startOptions;
|
|
330
|
+
|
|
331
|
+
const isCurrentUpload = () => abortRef.current === controller;
|
|
332
|
+
const externalSignal =
|
|
333
|
+
startClientOptions.signal ?? hookClientOptions.signal;
|
|
334
|
+
const abortFromExternalSignal = () => controller.abort();
|
|
335
|
+
if (externalSignal?.aborted) {
|
|
336
|
+
controller.abort();
|
|
337
|
+
} else {
|
|
338
|
+
externalSignal?.addEventListener("abort", abortFromExternalSignal, {
|
|
339
|
+
once: true,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
progressRef.current.clear();
|
|
344
|
+
setState({
|
|
345
|
+
...(INITIAL_STATE as ReactUploadState<Upload>),
|
|
346
|
+
status: "preparing",
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
let settledResult: Result | undefined;
|
|
350
|
+
let settledError: unknown;
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const result = await uploads.upload(uploadName, {
|
|
354
|
+
...hookClientOptions,
|
|
355
|
+
...startClientOptions,
|
|
356
|
+
signal: controller.signal,
|
|
357
|
+
onFileBegin(event) {
|
|
358
|
+
recordFileBegin(progressRef.current, event);
|
|
359
|
+
if (isCurrentUpload()) {
|
|
360
|
+
setState((current) => ({
|
|
361
|
+
...current,
|
|
362
|
+
status: "uploading",
|
|
363
|
+
files: currentFileState(progressRef.current),
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
hookOnFileBegin?.(event);
|
|
367
|
+
startOnFileBegin?.(event);
|
|
368
|
+
},
|
|
369
|
+
onProgress(event) {
|
|
370
|
+
recordFileProgress(progressRef.current, event);
|
|
371
|
+
const aggregate = aggregateProgress(progressRef.current);
|
|
372
|
+
if (isCurrentUpload()) {
|
|
373
|
+
setState((current) => ({
|
|
374
|
+
...current,
|
|
375
|
+
status:
|
|
376
|
+
aggregate.progressFraction >= 1
|
|
377
|
+
? "completing"
|
|
378
|
+
: "uploading",
|
|
379
|
+
progress: aggregate.progress,
|
|
380
|
+
progressFraction: aggregate.progressFraction,
|
|
381
|
+
files: currentFileState(progressRef.current),
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
hookOnProgress?.(event);
|
|
385
|
+
startOnProgress?.(event);
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
settledResult = result as Result;
|
|
390
|
+
await hookOnSuccess?.(settledResult);
|
|
391
|
+
await startOnSuccess?.(settledResult);
|
|
392
|
+
if (isCurrentUpload()) {
|
|
393
|
+
setState((current) => ({
|
|
394
|
+
...current,
|
|
395
|
+
status: "success",
|
|
396
|
+
progress: 100,
|
|
397
|
+
progressFraction: 1,
|
|
398
|
+
error: null,
|
|
399
|
+
result: settledResult ?? null,
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
return settledResult;
|
|
403
|
+
} catch (error) {
|
|
404
|
+
settledError = error;
|
|
405
|
+
await hookOnError?.(error);
|
|
406
|
+
await startOnError?.(error);
|
|
407
|
+
if (isCurrentUpload()) {
|
|
408
|
+
setState((current) => ({
|
|
409
|
+
...current,
|
|
410
|
+
status: "error",
|
|
411
|
+
error,
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
414
|
+
throw error;
|
|
415
|
+
} finally {
|
|
416
|
+
externalSignal?.removeEventListener("abort", abortFromExternalSignal);
|
|
417
|
+
if (abortRef.current === controller) {
|
|
418
|
+
abortRef.current = null;
|
|
419
|
+
}
|
|
420
|
+
await hookOnSettled?.(settledResult, settledError);
|
|
421
|
+
await startOnSettled?.(settledResult, settledError);
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
[hookOptions, uploadName],
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
const uploadFile = useCallback(
|
|
428
|
+
(
|
|
429
|
+
file: File,
|
|
430
|
+
startOptions: ReactUploadFileStartOptions<Upload>,
|
|
431
|
+
): Promise<Result> =>
|
|
432
|
+
upload({
|
|
433
|
+
...startOptions,
|
|
434
|
+
files: [file],
|
|
435
|
+
}),
|
|
436
|
+
[upload],
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
return useMemo(() => {
|
|
440
|
+
const isIdle = state.status === "idle";
|
|
441
|
+
const isSuccess = state.status === "success";
|
|
442
|
+
const isError = state.status === "error";
|
|
443
|
+
const isUploading =
|
|
444
|
+
state.status === "preparing" ||
|
|
445
|
+
state.status === "uploading" ||
|
|
446
|
+
state.status === "completing";
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
...state,
|
|
450
|
+
accept: uploads.accept(uploadName),
|
|
451
|
+
constraints: uploads.constraints(uploadName),
|
|
452
|
+
isPending: isUploading,
|
|
453
|
+
isIdle,
|
|
454
|
+
isUploading,
|
|
455
|
+
isSuccess,
|
|
456
|
+
isError,
|
|
457
|
+
upload,
|
|
458
|
+
uploadFile,
|
|
459
|
+
abort,
|
|
460
|
+
reset,
|
|
461
|
+
};
|
|
462
|
+
}, [state, uploadName, upload, uploadFile, abort, reset]);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function useUploadMany<Name extends UploadClientName<Registry>>(
|
|
466
|
+
uploadName: Name,
|
|
467
|
+
hookOptions: ReactUploadHookOptions<UploadByName<Registry, Name>> = {},
|
|
468
|
+
): ReactUploadManyController<UploadByName<Registry, Name>> {
|
|
469
|
+
type Upload = UploadByName<Registry, Name>;
|
|
470
|
+
type Result = CompleteUploadResult<InferUploadResult<Upload>>;
|
|
471
|
+
|
|
472
|
+
const controller = useUpload(uploadName, hookOptions);
|
|
473
|
+
const uploadMany = useCallback(
|
|
474
|
+
(
|
|
475
|
+
files: readonly File[],
|
|
476
|
+
startOptions: ReactUploadManyStartOptions<Upload>,
|
|
477
|
+
): Promise<Result> =>
|
|
478
|
+
controller.upload({
|
|
479
|
+
...startOptions,
|
|
480
|
+
files,
|
|
481
|
+
}),
|
|
482
|
+
[controller.upload],
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
return useMemo(
|
|
486
|
+
() => ({
|
|
487
|
+
...controller,
|
|
488
|
+
upload: uploadMany,
|
|
489
|
+
}),
|
|
490
|
+
[controller, uploadMany],
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return { useUpload, useUploadMany };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function recordFileBegin(
|
|
498
|
+
progress: Map<number, FileProgress>,
|
|
499
|
+
event: UploadClientFileEvent,
|
|
500
|
+
): void {
|
|
501
|
+
progress.set(event.index, {
|
|
502
|
+
fileName: event.fileName,
|
|
503
|
+
index: event.index,
|
|
504
|
+
uploadId: event.uploadId,
|
|
505
|
+
key: event.key,
|
|
506
|
+
loaded: 0,
|
|
507
|
+
total: event.file.size,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function recordFileProgress(
|
|
512
|
+
progress: Map<number, FileProgress>,
|
|
513
|
+
event: UploadClientProgressEvent,
|
|
514
|
+
): void {
|
|
515
|
+
progress.set(event.index, {
|
|
516
|
+
fileName: event.fileName,
|
|
517
|
+
index: event.index,
|
|
518
|
+
uploadId: event.uploadId,
|
|
519
|
+
key: event.key,
|
|
520
|
+
loaded: event.loaded,
|
|
521
|
+
total: event.total,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function currentFileState(
|
|
526
|
+
progress: Map<number, FileProgress>,
|
|
527
|
+
): ReactUploadFileState[] {
|
|
528
|
+
return [...progress.values()]
|
|
529
|
+
.sort((a, b) => a.index - b.index)
|
|
530
|
+
.map((file) => ({
|
|
531
|
+
...file,
|
|
532
|
+
progress: percent(file.loaded, file.total),
|
|
533
|
+
}));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function aggregateProgress(progress: Map<number, FileProgress>): {
|
|
537
|
+
progress: number;
|
|
538
|
+
progressFraction: number;
|
|
539
|
+
} {
|
|
540
|
+
const files = [...progress.values()];
|
|
541
|
+
if (files.length === 0) {
|
|
542
|
+
return { progress: 0, progressFraction: 0 };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const totals = files.reduce(
|
|
546
|
+
(acc, file) => ({
|
|
547
|
+
loaded: acc.loaded + file.loaded,
|
|
548
|
+
total: acc.total + file.total,
|
|
549
|
+
}),
|
|
550
|
+
{ loaded: 0, total: 0 },
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const progressFraction =
|
|
554
|
+
totals.total > 0
|
|
555
|
+
? Math.min(1, totals.loaded / totals.total)
|
|
556
|
+
: Math.min(
|
|
557
|
+
1,
|
|
558
|
+
files.reduce(
|
|
559
|
+
(sum, file) => sum + percent(file.loaded, file.total) / 100,
|
|
560
|
+
0,
|
|
561
|
+
) / files.length,
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
progress: Math.round(progressFraction * 100),
|
|
566
|
+
progressFraction,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function percent(loaded: number, total: number): number {
|
|
571
|
+
if (total <= 0) return loaded > 0 ? 100 : 0;
|
|
572
|
+
return Math.round(Math.min(1, loaded / total) * 100);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Infer upload metadata for a named upload in a registry.
|
|
577
|
+
*/
|
|
578
|
+
export type InferReactUploadMetadata<
|
|
579
|
+
Registry extends UploadClientRegistry,
|
|
580
|
+
Name extends UploadClientName<Registry>,
|
|
581
|
+
> = InferUploadMetadata<UploadByName<Registry, Name>>;
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Infer upload completion result for a named upload in a registry.
|
|
585
|
+
*/
|
|
586
|
+
export type InferReactUploadResult<
|
|
587
|
+
Registry extends UploadClientRegistry,
|
|
588
|
+
Name extends UploadClientName<Registry>,
|
|
589
|
+
> = InferUploadResult<UploadByName<Registry, Name>>;
|