@gentleduck/upload 0.2.1
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/dist/index.d.ts +1417 -0
- package/dist/index.js +6 -0
- package/package.json +43 -0
- package/src/core/client/config.types.ts +77 -0
- package/src/core/client/index.ts +4 -0
- package/src/core/client/plugins.types.ts +46 -0
- package/src/core/client/retry.types.ts +6 -0
- package/src/core/client/uploadClient.ts +19 -0
- package/src/core/contracts/backendApi.ts +62 -0
- package/src/core/contracts/cursor.types.ts +23 -0
- package/src/core/contracts/errors.types.ts +67 -0
- package/src/core/contracts/fingerprint.types.ts +18 -0
- package/src/core/contracts/index.ts +8 -0
- package/src/core/contracts/intent.types.ts +39 -0
- package/src/core/contracts/result.types.ts +11 -0
- package/src/core/contracts/strategy.types.ts +45 -0
- package/src/core/contracts/transport/index.ts +2 -0
- package/src/core/contracts/transport/transport.libs.ts +100 -0
- package/src/core/contracts/transport/transport.ts +166 -0
- package/src/core/contracts/transport/transport.types.ts +44 -0
- package/src/core/engine/commands.types.ts +17 -0
- package/src/core/engine/event-map.types.ts +45 -0
- package/src/core/engine/index.ts +8 -0
- package/src/core/engine/internal-events.types.ts +222 -0
- package/src/core/engine/outcome.types.ts +9 -0
- package/src/core/engine/progress.types.ts +15 -0
- package/src/core/engine/reducer.ts +435 -0
- package/src/core/engine/store/handlers/add-file.ts +103 -0
- package/src/core/engine/store/handlers/cancel.ts +56 -0
- package/src/core/engine/store/handlers/clean-up.ts +66 -0
- package/src/core/engine/store/handlers/create-intent.ts +76 -0
- package/src/core/engine/store/handlers/finalize.ts +59 -0
- package/src/core/engine/store/handlers/index.ts +7 -0
- package/src/core/engine/store/handlers/pause.ts +16 -0
- package/src/core/engine/store/handlers/run-upload.ts +192 -0
- package/src/core/engine/store/index.ts +123 -0
- package/src/core/engine/store/store.dispatch.ts +78 -0
- package/src/core/engine/store/store.libs.ts +169 -0
- package/src/core/engine/store/store.runtime.ts +336 -0
- package/src/core/engine/store/store.schedule.ts +116 -0
- package/src/core/engine/store/store.types.ts +250 -0
- package/src/core/engine/validation/file.ts +77 -0
- package/src/core/engine/validation/index.ts +3 -0
- package/src/core/engine/validation/intent.ts +55 -0
- package/src/core/engine/validation/list.ts +50 -0
- package/src/core/index.ts +5 -0
- package/src/core/persistence/adapters.indexeddb.ts +92 -0
- package/src/core/persistence/adapters.local.ts +22 -0
- package/src/core/persistence/adapters.memory.ts +15 -0
- package/src/core/persistence/index.ts +5 -0
- package/src/core/persistence/persistence.ts +194 -0
- package/src/core/persistence/persistence.types.ts +66 -0
- package/src/core/utils/async.ts +3 -0
- package/src/core/utils/constants.ts +32 -0
- package/src/core/utils/emitter/emitter.types.ts +23 -0
- package/src/core/utils/emitter/index.ts +68 -0
- package/src/core/utils/fingerprint.ts +20 -0
- package/src/core/utils/guards.ts +3 -0
- package/src/core/utils/id.ts +3 -0
- package/src/core/utils/index.ts +6 -0
- package/src/index.ts +3 -0
- package/src/react/index.ts +2 -0
- package/src/react/upload-provider.tsx +96 -0
- package/src/react/use-uploader.tsx +115 -0
- package/src/strategies/index.ts +15 -0
- package/src/strategies/multipart/index.ts +243 -0
- package/src/strategies/post/index.ts +93 -0
- package/src/strategies/registry/index.ts +30 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{jsxDEV as e}from"react/jsx-dev-runtime";import*as t from"react";import{createContext as n,useContext as r}from"react";const i=500,a=1e4,o=100,s=3,c=3,l=100;function u(e){return{maxConcurrentUploads:Math.max(1,e?.maxConcurrentUploads??3),autoStart:e?.autoStart,progressThrottleMs:Math.max(0,e?.progressThrottleMs??100),validation:e?.validation??{},maxAttempts:Math.max(1,e?.maxAttempts??3),retryPolicy:e?.retryPolicy,maxItems:Math.max(1,e?.maxItems??100),completedItemTTL:e?.completedItemTTL}}function d(){return`upload_${Date.now()}_${Math.random().toString(36).slice(2)}`}function f(e,t,n){let r=n.validation?.[t];if(!r)return null;if(e.size===0)return{code:`empty_file`};if(r.maxSizeBytes!==void 0&&e.size>r.maxSizeBytes)return{code:`file_too_large`,maxBytes:r.maxSizeBytes,size:e.size};if(r.minSizeBytes!==void 0&&e.size<r.minSizeBytes)return{code:`empty_file`};if(r.allowedTypes||r.allowedExtensions){let t=r.allowedTypes||[],n=r.allowedExtensions||[],i=e.name.split(`.`).pop()?.toLowerCase(),a=(e.type||``).toLowerCase(),o=t.length===0?!1:t.some(e=>{if(e.endsWith(`/*`)){let t=e.slice(0,-2).toLowerCase();return a.startsWith(t+`/`)}return a===e.toLowerCase()}),s=i&&n.length>0?n.some(e=>e.toLowerCase()===i):!1,c=t.length>0,l=n.length>0;if(!(c&&l?o||s:(!c||o)&&(!l||s)))return{code:`type_not_allowed`,allowed:[...t,...n],got:e.type||e.name}}return null}function p(e){return!!e&&typeof e==`object`}function m(e,t){if(!p(e))return Error(`Invalid intent: must be an object`);if(typeof e.strategy!=`string`)return Error(`Invalid intent: missing or invalid strategy field`);if(e.strategy!==t)return Error(`Invalid intent: strategy mismatch (expected ${t}, got ${e.strategy})`);if(typeof e.fileId!=`string`||!e.fileId)return Error(`Invalid intent: missing or invalid fileId`);if(t===`post`){if(typeof e.url!=`string`||!e.url)return Error(`Invalid post intent: missing or invalid url`);try{let t=new URL(e.url);if(![`http:`,`https:`].includes(t.protocol))return Error(`Invalid post intent: url must use http or https protocol`)}catch{return Error(`Invalid post intent: url is not a valid URL`)}if(!e.fields||typeof e.fields!=`object`)return Error(`Invalid post intent: missing or invalid fields`)}else if(t===`multipart`){if(typeof e.uploadId!=`string`||!e.uploadId)return Error(`Invalid multipart intent: missing or invalid uploadId`);if(typeof e.partSize!=`number`||e.partSize<=0)return Error(`Invalid multipart intent: missing or invalid partSize`);if(`parts`in e&&e.parts!==void 0&&!Array.isArray(e.parts))return Error(`Invalid multipart intent: parts must be an array if provided`)}return null}function h(e,t,n,r=0){let i=n.validation?.[t],a=[],o=[],s=i?.maxFiles,c=s===void 0?1/0:Math.max(0,s-r);for(let r of e){if(c<=0){let e=s??0;o.push({file:r,reason:{code:`too_many_files`,max:e}});continue}let e=f(r,t,n);if(e){o.push({file:r,reason:e});continue}a.push(r),--c}return{valid:a,rejected:o}}function g(e){return{name:e.name,size:e.size,type:e.type,lastModified:e.lastModified}}function _(e,t){return e.name===t.name&&e.size===t.size&&e.lastModified===t.lastModified}function v(e){return new Promise(t=>setTimeout(t,e))}async function ee(e){let t=await e.arrayBuffer(),n=await crypto.subtle.digest(`SHA-256`,t);return Array.from(new Uint8Array(n)).map(e=>e.toString(16).padStart(2,`0`)).join(``)}function te(e,t){let n=e.config.autoStart;return n===void 0?!1:Array.isArray(n)?n.includes(t):typeof n==`function`?n(t):!1}function y(e,t){if(t)return t(e);if(ie(e))return{code:`aborted`,message:`Upload aborted`,reason:String(e.reason??`unknown`),retryable:!1};let n=p(e)&&typeof e.message==`string`?e.message:`Unknown error`;if(String(n).toLowerCase().includes(`network`)||String(n).toLowerCase().includes(`fetch`))return{code:`network`,message:String(n),cause:e,retryable:!0};if(p(e)&&(typeof e.status==`number`||typeof e.statusCode==`number`)){let t=typeof e.status==`number`?e.status:e.statusCode,r=t>=500||t===429;return{code:`http`,status:t,message:String(n),cause:e,retryable:r}}return{code:`unknown`,message:String(n),cause:e,retryable:!1}}function b(e){return`intent`in e&&!!e.intent}function ne(e){return`file`in e&&!!e.file}function x(e){return`cursor`in e}function re(e){return p(e)&&e.strategy===`multipart`&&typeof e.fileId==`string`&&typeof e.uploadId==`string`}function ie(e){return p(e)&&e.code===`aborted`}function S(e,t){if(e.retryPolicy)return e.retryPolicy(t);if(t.error.code===`auth`||t.error.code===`validation_failed`||t.error.code===`strategy_missing`||t.error.code===`aborted`)return{retryable:!1};let n=e.maxAttempts;return t.attempt<n?{retryable:!0,delayMs:Math.min(a,500*2**(t.attempt-1))}:{retryable:!1}}function ae(e,t,n){let r=Array.from(e.state.items.values()).filter(e=>e.purpose===n&&e.phase!==`canceled`).length,{valid:i,rejected:a}=h(t,n,e.opts.config,r);for(let{file:t,reason:n}of a)e.emitter.emit(`file.rejected`,{reason:n,file:t});let o=Date.now(),s=[];for(let t of i){let r=d(),i=(e.opts.fingerprint??g)(t);s.push({localId:r,purpose:n,file:t,fingerprint:i,createdAt:o}),e.enqueueEffect(async()=>{let a;try{a=await ee(t);let n={...i,checksum:a};e.applyInternal({type:`fingerprint.updated`,localId:r,fingerprint:n})}catch(e){typeof window<`u`&&process.env.NODE_ENV===`development`&&console.warn(`[UploadEngine] Failed to calculate checksum:`,e)}if(a&&e.opts.api.findByChecksum)try{let t=await e.opts.api.findByChecksum({checksum:a,purpose:n});if(t){let n=e.state.items.get(r);if(n&&n.phase===`validating`){e.applyInternal({type:`dedupe.ok`,localId:r,result:t});return}}}catch(e){typeof window<`u`&&process.env.NODE_ENV===`development`&&console.warn(`[UploadEngine] Failed to check for existing file:`,e)}let o=e.opts.validateFile?.(t,n);if(o){e.applyInternal({type:`validation.failed`,localId:r,reason:o});return}let s=f(t,n,e.opts.config);if(s){e.applyInternal({type:`validation.failed`,localId:r,reason:s});return}e.applyInternal({type:`validation.ok`,localId:r})})}s.length>0&&e.applyInternal({type:`files.added`,items:s})}function oe(e,t){let n=e.state.items.get(t),r=e.inflightIntents.get(t);r&&(r.abort({reason:`cancel`}),e.inflightIntents.delete(t));let i=e.inflightCompletes.get(t);i&&(i.abort({reason:`cancel`}),e.inflightCompletes.delete(t));let a=e.inflightUploads.get(t);if(a&&(a.mode=`cancel`,a.controller.abort({reason:`cancel`})),n&&b(n)&&n.intent.strategy===`multipart`&&e.opts.api.multipart?.abort){let t=n.intent;re(t)&&e.enqueueEffect(async()=>{try{let n=e.opts.api.multipart?.abort;n&&await n({fileId:t.fileId,uploadId:t.uploadId})}catch{}})}}function se(e,t){let n=e.inflightUploads.get(t);if(n){n.mode=`pause`,n.controller.abort({reason:`pause`});return}}function ce(e,t){if(t.type===`startAll`){let n=t.purpose;Array.from(e.state.items.values()).filter(e=>e.phase===`ready`&&(!n||e.purpose===n)).forEach(t=>e.dispatch({type:`start`,localId:t.localId}));return}if(t.type===`pauseAll`){let n=t.purpose;Array.from(e.state.items.values()).filter(e=>e.phase===`uploading`&&(!n||e.purpose===n)).forEach(t=>e.dispatch({type:`pause`,localId:t.localId}));return}if(t.type===`cancelAll`){let n=t.purpose;Array.from(e.state.items.values()).filter(e=>e.phase!==`completed`&&e.phase!==`canceled`&&(!n||e.purpose===n)).forEach(t=>e.dispatch({type:`cancel`,localId:t.localId}));return}if(t.type===`addFiles`){ae(e,t.files,t.purpose);return}if(t.type===`pause`){se(e,t.localId),e.applyCommand(t);return}if(t.type===`cancel`){oe(e,t.localId),e.applyCommand(t);return}if(t.type===`start`){e.applyCommand(t);return}if(t.type===`resume`){e.applyCommand(t);return}e.applyCommand(t)}function le(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function C(e){return e?.reason}function ue(e){if(!e)return`unknown`;if(typeof e==`string`)return e===`pause`||e===`cancel`?e:`unknown`;if(p(e)&&`reason`in e){let t=e.reason;if(t===`pause`||t===`cancel`)return t}if(p(e)&&`kind`in e){let t=e.kind;if(t===`pause`||t===`cancel`)return t}return`unknown`}var de=class extends Error{constructor(e){super(`Upload aborted`),le(this,`code`,`aborted`),le(this,`reason`,void 0),this.reason=ue(e)}};function w(e){return new de(e)}function fe(e,t){return e.status===0?Error(`CORS error: The upload server does not allow requests from this origin. Please check CORS configuration.`):Error(`${t} (status: ${e.status})`)}function pe(e){let t={};return e.getAllResponseHeaders().split(`\r
|
|
2
|
+
`).map(e=>e.trim()).filter(Boolean).forEach(e=>{let n=e.indexOf(`:`);if(n<=0)return;let r=e.slice(0,n).trim().toLowerCase(),i=e.slice(n+1).trim();r&&(t[r]=i)}),t}function me(e){return/^https?:\/\//i.test(e)}function he(e){let t=e.getResponseHeader(`ETag`)??e.getResponseHeader(`etag`);if(t)return t.replace(/"/g,``)}function T(e){return new Promise((t,n)=>{if(!e.url||typeof e.url!=`string`){n(Error(`UploadTransport: missing url`));return}if(!me(e.url)){n(Error(`UploadTransport: expected absolute URL, got: ${String(e.url)}`));return}let r=new XMLHttpRequest,i=!1,a=e=>{i||(i=!0,s(),e())},o=()=>{try{r.abort()}finally{a(()=>n(w(C(e.signal))))}},s=()=>{e.signal&&e.signal.removeEventListener(`abort`,o),r.upload.onprogress=null,r.onload=null,r.onerror=null,r.onabort=null};if(e.signal?.aborted){a(()=>n(w(C(e.signal))));return}if(e.signal&&e.signal.addEventListener(`abort`,o,{once:!0}),e.onProgress&&(r.upload.onprogress=t=>{t.lengthComputable&&e.onProgress?.(t.loaded,t.total)}),r.onload=()=>{let e=r.status;a(e>=200&&e<300?()=>t({headers:pe(r),etag:he(r)}):()=>n(Error(`Upload failed with status ${e}`)))},r.onerror=()=>a(()=>n(fe(r,`Network error during upload`))),r.onabort=()=>a(()=>n(w(e.signal?.aborted?C(e.signal):`unknown`))),r.open(e.method,e.url),e.headers)for(let[t,n]of Object.entries(e.headers))r.setRequestHeader(t,n);r.send(e.body)})}function ge(){return{async postForm(e){let t=new FormData;for(let[n,r]of Object.entries(e.fields))t.append(n,r);return t.append(`file`,e.file,e.filename??`file`),{headers:(await T({method:`POST`,url:e.url,body:t,signal:e.signal,onProgress:e.onProgress})).headers}},async put(e){let t=await T({method:`PUT`,url:e.url,body:e.body,signal:e.signal,headers:e.headers,onProgress:e.onProgress});return{headers:t.headers,etag:t.etag}},async patch(e){return{headers:(await T({method:`PATCH`,url:e.url,body:e.body,signal:e.signal,headers:e.headers,onProgress:e.onProgress})).headers}}}}const E=`snapshots`;function D(){return new Promise((e,t)=>{let n=indexedDB.open(`upload-engine`,1);n.onupgradeneeded=()=>{let e=n.result;e.objectStoreNames.contains(E)||e.createObjectStore(E)},n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error)})}function _e(e){return new Promise((t,n)=>{e.onsuccess=()=>t(e.result),e.onerror=()=>n(e.error)})}function O(e){return new Promise((t,n)=>{e.oncomplete=()=>t(),e.onabort=()=>n(e.error??Error(`IndexedDB transaction aborted`)),e.onerror=()=>n(e.error??Error(`IndexedDB transaction error`))})}const ve={async load(e){if(typeof indexedDB>`u`)return null;let t=(await D()).transaction(E,`readonly`),n=t.objectStore(E);try{let r=await _e(n.get(e));return await O(t),r??null}catch{try{await O(t)}catch{}return null}},async save(e,t){if(typeof indexedDB>`u`)return;let n=(await D()).transaction(E,`readwrite`),r=n.objectStore(E);try{r.put(t,e),await O(n)}catch{try{n.abort()}catch{}}},async clear(e){if(typeof indexedDB>`u`)return;let t=(await D()).transaction(E,`readwrite`),n=t.objectStore(E);try{n.delete(e),await O(t)}catch{try{t.abort()}catch{}}}},ye={load(e){if(typeof localStorage>`u`)return null;let t=localStorage.getItem(e);if(!t)return null;try{return JSON.parse(t)}catch{return null}},save(e,t){typeof localStorage>`u`||localStorage.setItem(e,JSON.stringify(t))},clear(e){typeof localStorage>`u`||localStorage.removeItem(e)}},k=new Map,be={load(e){return k.get(e)??null},save(e,t){k.set(e,t)},clear(e){k.delete(e)}};function A(e,t){let n={};for(let t of e.items.values()){if(!b(t)||t.phase===`completed`||t.phase===`canceled`||t.phase===`error`)continue;let e=x(t)?t.cursor:void 0,r=`progress`in t&&t.progress?{uploadedBytes:t.progress.uploadedBytes,totalBytes:t.progress.totalBytes,pct:t.progress.pct}:void 0,i={id:t.localId,purpose:t.purpose,status:t.phase,file:{name:t.fingerprint.name,size:t.fingerprint.size,type:t.fingerprint.type,lastModified:t.fingerprint.lastModified,checksum:t.fingerprint.checksum},intent:t.intent,cursor:e,progress:r};n[i.id]=i}return{version:t,createdAt:Date.now(),items:n}}function j(e,t){if(!t.isPurpose||!t.isIntent||!xe(e))return null;let n=new Map;for(let r of Object.values(e.items)){let i=Se(r);if(!i||!t.isPurpose(i.purpose)||!t.isIntent(i.intent))continue;let a=i.intent.strategy;if(typeof a!=`string`||!t.hasStrategy(a)||!i.cursor||!we(i.cursor,t.hasStrategy)||i.cursor.strategy!==a)continue;let o=i.progress?.totalBytes??i.file.size,s=i.progress?.uploadedBytes??0,c=typeof i.progress?.pct==`number`?i.progress.pct:o>0?Math.min(100,Math.max(0,s/o*100)):0;n.set(i.id,{phase:`paused`,localId:i.id,purpose:i.purpose,fingerprint:{name:i.file.name,size:i.file.size,type:i.file.type,lastModified:i.file.lastModified,checksum:i.file.checksum},intent:i.intent,cursor:i.cursor,progress:{uploadedBytes:s,totalBytes:o,pct:c},pausedAt:Date.now(),createdAt:e.createdAt??Date.now(),file:void 0})}return{items:n}}function xe(e){return!p(e)||typeof e.version!=`number`||typeof e.createdAt!=`number`?!1:p(e.items)}function Se(e){if(!p(e))return null;let t=typeof e.id==`string`?e.id:null,n=typeof e.purpose==`string`?e.purpose:null,r=typeof e.status==`string`?e.status:null,i=e.intent;if(!t||!n||!r||!p(e.file))return null;let a=typeof e.file.name==`string`?e.file.name:null,o=typeof e.file.size==`number`?e.file.size:null,s=typeof e.file.type==`string`?e.file.type:null,c=typeof e.file.lastModified==`number`?e.file.lastModified:null,l=typeof e.file.checksum==`string`?e.file.checksum:void 0;if(!a||o===null||!s||c===null)return null;let u=Ce(e.progress);return{id:t,purpose:n,status:r,file:{name:a,size:o,type:s,lastModified:c,checksum:l},intent:i,cursor:e.cursor,progress:u}}function Ce(e){if(!p(e)||typeof e.uploadedBytes!=`number`||typeof e.totalBytes!=`number`)return;let t=typeof e.pct==`number`?e.pct:void 0;return{uploadedBytes:e.uploadedBytes,totalBytes:e.totalBytes,pct:t}}function we(e,t){return!(!p(e)||typeof e.strategy!=`string`||!t(e.strategy))}function M(){let e={};return{on(t,n){return(e[t]??(e[t]=new Set)).add(n),()=>{let r=e[t];r&&(r.delete(n),r.size===0&&delete e[t])}},off(t,n){let r=e[t];return r?(r.delete(n),r.size===0&&delete e[t],()=>{}):()=>{}},emit(t,n){let r=e[t];r&&r.forEach(e=>{try{e(n)}catch(e){typeof window<`u`&&process.env.NODE_ENV===`development`&&console.error(`[UploadEngine] Error in event listener for "${t}":`,e)}})}}}function N(){return function(e,t){let n=new Map(e.items),r=(e,t)=>n.set(e,t),i=Date.now();if((e=>`type`in e&&typeof e.type==`string`&&!e.type.includes(`.`))(t)){switch(t.type){case`addFiles`:break;case`start`:{let e=n.get(t.localId);if(!e||e.phase!==`ready`)break;r(t.localId,{...e,phase:`queued`,requestedAt:i});break}case`resume`:{let e=n.get(t.localId);if(!e||e.phase!==`paused`||!e.file)break;r(t.localId,{...e,file:e.file,phase:`queued`,requestedAt:i});break}case`pause`:{let e=n.get(t.localId);if(!e)break;e.phase===`queued`&&r(t.localId,{...e,phase:`ready`});break}case`cancel`:{let e=n.get(t.localId);if(!e||e.phase===`completed`||e.phase===`canceled`)break;r(t.localId,P(e,i));break}case`retry`:{let e=n.get(t.localId);if(!e||e.phase!==`error`||!e.retryable||!e.file)break;e.intent?e.progress&&e.progress.pct===100?r(t.localId,{...e,progress:e.progress,intent:e.intent,file:e.file,phase:`completing`,completingAt:i}):r(t.localId,{...e,file:e.file,intent:e.intent,phase:`ready`}):r(t.localId,{...e,phase:`creating_intent`,file:e.file,attempt:(e.attempt??1)+1});break}case`rebind`:{let e=n.get(t.localId);if(!e)break;e.phase===`paused`&&!e.file&&_(g(t.file),e.fingerprint)&&r(t.localId,{...e,file:t.file});break}case`remove`:n.delete(t.localId);break;case`startAll`:case`pauseAll`:case`cancelAll`:break}return{items:n}}switch(t.type){case`files.added`:{let e=t;for(let t of e.items)n.has(t.localId)||r(t.localId,{phase:`validating`,localId:t.localId,purpose:t.purpose,file:t.file,fingerprint:t.fingerprint,createdAt:t.createdAt});break}case`fingerprint.updated`:{let e=t,i=n.get(e.localId);if(!i)break;r(e.localId,{...i,fingerprint:e.fingerprint});break}case`validation.ok`:{let e=n.get(t.localId);if(!e||e.phase!==`validating`)break;r(t.localId,{...e,phase:`creating_intent`,attempt:1});break}case`validation.failed`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`validating`)break;r(e.localId,{...a,phase:`error`,error:{code:`validation_failed`,reason:e.reason,message:String(e.reason)},retryable:!1,attempt:1,failedAt:i});break}case`intent.ok`:{let e=t,i=n.get(e.localId);if(!i||i.phase!==`creating_intent`)break;r(e.localId,{...i,phase:`ready`,intent:e.intent});break}case`intent.failed`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`creating_intent`)break;r(e.localId,{...a,phase:`error`,error:e.error,retryable:e.retryable,failedAt:i});break}case`upload.begin`:{let e=t,i=n.get(e.localId);if(!i||i.phase!==`queued`)break;let a=i.file.size,o=i.progress,s=o?{...o,totalBytes:a,pct:Te(o.uploadedBytes,a)}:{uploadedBytes:0,totalBytes:a,pct:0};r(e.localId,{...i,phase:`uploading`,progress:s,startedAt:e.startedAt});break}case`upload.progress`:{let e=t,i=n.get(e.localId);if(!i||i.phase!==`uploading`)break;let a={uploadedBytes:e.uploadedBytes,totalBytes:e.totalBytes,pct:Te(e.uploadedBytes,e.totalBytes)};r(e.localId,{...i,progress:a});break}case`cursor.updated`:{let e=t,i=n.get(e.localId);if(!i)break;(i.phase===`uploading`||i.phase===`queued`||i.phase===`paused`)&&r(e.localId,{...i,cursor:e.cursor});break}case`upload.ok`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`uploading`)break;r(e.localId,{...a,phase:`completing`,progress:{uploadedBytes:a.file.size,totalBytes:a.file.size,pct:100},completingAt:i});break}case`upload.failed`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`uploading`)break;r(e.localId,{...a,phase:`error`,error:e.error,retryable:e.retryable,attempt:1,failedAt:i});break}case`paused`:{let e=t,i=n.get(e.localId);if(!i||i.phase!==`uploading`)break;r(e.localId,{...i,phase:`paused`,cursor:e.cursor,pausedAt:e.pausedAt});break}case`canceled`:{let e=t,i=n.get(e.localId);if(!i)break;r(e.localId,P(i,e.canceledAt));break}case`complete.ok`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`completing`)break;r(e.localId,{...a,phase:`completed`,completedBy:`upload`,result:e.result,completedAt:i});break}case`dedupe.ok`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`validating`)break;r(e.localId,{phase:`completed`,localId:a.localId,file:a.file,purpose:a.purpose,fingerprint:a.fingerprint,result:e.result,completedBy:`dedupe`,completedAt:i,createdAt:a.createdAt});break}case`complete.failed`:{let e=t,a=n.get(e.localId);if(!a||a.phase!==`completing`)break;let o=(a.attempt??1)+1;r(e.localId,{...a,phase:`error`,error:e.error,retryable:e.retryable,attempt:o,failedAt:i});break}}return{items:n}}}function P(e,t){return{phase:`canceled`,localId:e.localId,purpose:e.purpose,fingerprint:e.fingerprint,canceledAt:t,createdAt:e.createdAt,file:`file`in e?e.file:void 0,intent:`intent`in e?e.intent:void 0,cursor:`cursor`in e?e.cursor:void 0,progress:`progress`in e?e.progress:void 0,attempt:`attempt`in e?e.attempt:void 0}}function Te(e,t){return t<=0?0:Math.min(100,e/t*100)}function Ee(e,t){let n=e.config.maxItems,r=e.config.completedItemTTL,i=Date.now(),a=[],o=Array.from(t.items.entries());if(r!==void 0)for(let[e,t]of o)((t.phase===`completed`||t.phase===`canceled`)&&`completedAt`in t&&typeof t.completedAt==`number`&&i-t.completedAt>r||t.phase===`canceled`&&`canceledAt`in t&&typeof t.canceledAt==`number`&&i-t.canceledAt>r)&&a.push(e);let s=o.filter(([e])=>!a.includes(e));if(s.length>n){let e=s.filter(([,e])=>e.phase===`completed`||e.phase===`canceled`).sort(([,e],[,t])=>(`completedAt`in e?e.completedAt:`canceledAt`in e?e.canceledAt:0)-(`completedAt`in t?t.completedAt:`canceledAt`in t?t.canceledAt:0)),t=s.length-n;for(let n=0;n<t&&n<e.length;n++)a.push(e[n][0])}if(a.length===0)return null;let c=new Map(t.items);for(let e of a)c.delete(e);return{...t,items:c}}async function De(e,t){let n=e.state.items.get(t);if(!n||n.phase!==`creating_intent`||e.inflightIntents.has(t))return;let r=new AbortController;e.inflightIntents.set(t,r);try{let i=await e.opts.api.createIntent({purpose:n.purpose,contentType:n.file.type||`application/octet-stream`,size:n.file.size,filename:n.file.name,checksum:n.fingerprint.checksum},{signal:r.signal});e.inflightIntents.delete(t);let a=e.state.items.get(t);if(!a||a.phase!==`creating_intent`)return;let o=m(i,i.strategy);if(o){let n={code:`validation_failed`,message:`Invalid intent from backend: ${o.message}`,cause:o,retryable:!1};e.applyInternal({type:`intent.failed`,localId:t,error:n,retryable:!1});return}e.applyInternal({type:`intent.ok`,localId:t,intent:i})}catch(i){if(e.inflightIntents.delete(t),r.signal.aborted)return;let a=y(i,e.opts.errorNormalizer),o={...a,message:`${a.message} (file: ${n.fingerprint.name}, size: ${n.file.size} bytes, purpose: ${n.purpose})`},s=S(e.opts.config,{phase:`intent`,attempt:n.attempt,error:o});e.applyInternal({type:`intent.failed`,localId:t,error:o,retryable:s.retryable}),s.retryable&&s.delayMs!==void 0&&e.enqueueEffect(async()=>{await v(s.delayMs),e.dispatch({type:`retry`,localId:t})})}}async function Oe(e,t){let n=e.state.items.get(t);if(!n||n.phase!==`completing`||e.inflightCompletes.has(t))return;let r=new AbortController;e.inflightCompletes.set(t,r);try{let i=await e.opts.api.complete({fileId:n.intent.fileId},{signal:r.signal});e.inflightCompletes.delete(t);let a=e.state.items.get(t);if(!a||a.phase!==`completing`)return;e.applyInternal({type:`complete.ok`,localId:t,result:i})}catch(n){if(e.inflightCompletes.delete(t),r.signal.aborted)return;let i=y(n,e.opts.errorNormalizer),a=e.state.items.get(t),o=a?{...i,message:`${i.message} (file: ${a.fingerprint.name}, fileId: ${(b(a)&&a.intent?.fileId)??`unknown`})`}:i,s=a&&`attempt`in a?a.attempt??1:1,c=S(e.opts.config,{phase:`complete`,attempt:s,error:o});e.applyInternal({type:`complete.failed`,localId:t,error:o,retryable:c.retryable}),c.retryable&&c.delayMs!==void 0&&e.enqueueEffect(async()=>{await v(c.delayMs),e.dispatch({type:`retry`,localId:t})})}}async function ke(e,t){let n=e.state.items.get(t);if(!n||n.phase!==`uploading`||!b(n)){e.inflightUploads.delete(t);return}let r=e.inflightUploads.get(t);if(r||(r={controller:new AbortController,mode:`normal`,started:!1},e.inflightUploads.set(t,r)),r.started)return;r.started=!0;let i=r.controller,a=()=>{let n=e.state.items.get(t);!n||n.phase===`canceled`||e.applyInternal({type:`canceled`,localId:t,canceledAt:Date.now()})},o=n=>{let r=e.state.items.get(t);!r||r.phase===`paused`||e.applyInternal({type:`paused`,localId:t,cursor:n,pausedAt:Date.now()})};if(i.signal.aborted){if(e.inflightUploads.delete(t),r.mode===`pause`){let r=e.state.items.get(t),i;n&&b(n)&&(i=n.intent.strategy);let a=(r&&x(r)?r.cursor:void 0)||(i?{strategy:i,value:void 0}:void 0);a&&o(a);return}if(r.mode===`cancel`){a();return}return}let s=n.intent.strategy;if(!e.opts.strategies.has(s)){e.inflightUploads.delete(t);let n={code:`strategy_missing`,message:`Strategy not found: ${s}`,strategy:String(s),retryable:!1};e.applyInternal({type:`upload.failed`,localId:t,error:n,retryable:!1});return}let c=e.opts.strategies.get(s);if(!c){e.inflightUploads.delete(t);let r={code:`strategy_missing`,message:`Strategy not found: ${n.intent.strategy}`,strategy:String(n.intent.strategy),retryable:!1};e.applyInternal({type:`upload.failed`,localId:t,error:r,retryable:!1});return}let l=0,u=e.opts.config.progressThrottleMs;try{if(await c.start({file:n.file,intent:n.intent,signal:i.signal,transport:e.opts.transport,api:e.opts.api,readCursor:()=>{let n=e.state.items.get(t);if(!(!n||!x(n)||!n.cursor)&&n.cursor.strategy===s)return n.cursor.value},persistCursor:n=>{let r={strategy:s,value:n};e.applyInternal({type:`cursor.updated`,localId:t,cursor:r})},reportProgress:n=>{let r=Date.now();r-l<u||(e.applyInternal({type:`upload.progress`,localId:t,uploadedBytes:n.uploadedBytes,totalBytes:n.totalBytes}),l=r)}}),e.inflightUploads.delete(t),r?.mode===`cancel`){a();return}if(i.signal.aborted&&r?.mode===`pause`){let n=e.state.items.get(t);o((n&&x(n)?n.cursor:void 0)||{strategy:s,value:void 0});return}e.applyInternal({type:`upload.ok`,localId:t})}catch(n){if(e.inflightUploads.delete(t),i.signal.aborted){if(r?.mode===`pause`){let n=e.state.items.get(t);o((n&&x(n)?n.cursor:void 0)||{strategy:s,value:void 0});return}if(r?.mode===`cancel`){a();return}a();return}let c=y(n,e.opts.errorNormalizer),l=e.state.items.get(t),u=l?{...c,message:`${c.message} (file: ${l.fingerprint.name}, size: ${(ne(l)&&l.file?.size)??0} bytes, phase: ${l.phase})`}:c,d=S(e.opts.config,{phase:`upload`,attempt:1,error:u});e.applyInternal({type:`upload.failed`,localId:t,error:u,retryable:d.retryable}),d.retryable&&d.delayMs!==void 0&&e.enqueueEffect(async()=>{await v(d.delayMs),e.dispatch({type:`retry`,localId:t})})}}function Ae(e){if(!e.scheduling){e.scheduling=!0;try{je(e),Ne(e),Me(e)}finally{e.scheduling=!1}}}function je(e){for(let t of e.state.items.values())t.phase===`creating_intent`&&(e.inflightIntents.has(t.localId)||e.enqueueEffect(()=>De(e,t.localId)))}function Me(e){for(let t of e.state.items.values())t.phase===`completing`&&(e.inflightCompletes.has(t.localId)||e.enqueueEffect(()=>Oe(e,t.localId)))}function Ne(e){let t=Math.max(1,e.opts.config.maxConcurrentUploads),n=e.inflightUploads.size;for(let t of e.state.items.values())t.phase===`ready`&&te(e.opts,t.purpose)&&e.applyCommand({type:`start`,localId:t.localId});if(n>=t)return;let r=Array.from(e.state.items.values()).filter(Pe);if(r.length===0)return;let i=Math.max(0,t-n),a=r.slice(0,i);for(let t of a){if(e.inflightUploads.has(t.localId))continue;let n=new AbortController;e.inflightUploads.set(t.localId,{controller:n,mode:`normal`,started:!1}),e.applyInternal({type:`upload.begin`,localId:t.localId,startedAt:Date.now()}),ke(e,t.localId)}}function Pe(e){return e.phase===`queued`}function Fe(e){let t=Ie(e),n=t.persistence,r=null,i=Math.max(0,n?.debounceMs??200),a=async()=>{if(n)try{let e=(n.serialize??A)(s.state,n.version);if(!e.items||Object.keys(e.items).length===0){await n.adapter.clear(n.key);return}await n.adapter.save(n.key,e)}catch(e){typeof window<`u`&&process.env.NODE_ENV===`development`&&console.warn(`[UploadEngine] persistence flush failed:`,e)}},o=()=>{if(n){if(i===0){a();return}r&&clearTimeout(r),r=setTimeout(()=>void a(),i)}},s={opts:t,state:t.initialState??{items:new Map},listeners:new Set,emitter:M(),reduce:N(),inflightUploads:new Map,inflightIntents:new Map,inflightCompletes:new Map,effectQueue:[],processingEffects:!1,scheduling:!1,notify(){s.listeners.forEach(e=>e())},scheduleWork(){Ae(s)},applyInternal(e){let t=s.state;s.state=s.reduce(t,e),s.opts.hooks?.onInternalEvent?.(e,s.state),Le(s,e);let n=Ee(s.opts,s.state);n&&(s.state=n),o(),s.notify(),s.scheduleWork()},applyCommand(e){let t=s.state,n=s.reduce(t,e);s.state=n,Re(s,e,t,n);let r=Ee(s.opts,s.state);r&&(s.state=r),o(),s.notify(),s.scheduleWork()},enqueueEffect(e){s.effectQueue.push(e),s.processingEffects||s.processEffects()},async processEffects(){for(s.processingEffects=!0;s.effectQueue.length>0;){let e=s.effectQueue.shift();if(e)try{await e()}catch(e){if(typeof window<`u`&&process.env.NODE_ENV===`development`){let t=e instanceof Error?e:Error(String(e)),n={queueLength:s.effectQueue.length,processing:s.processingEffects,timestamp:new Date().toISOString()};console.error(`[UploadEngine] Effect error:`,t,n)}}}s.processingEffects=!1},dispatch:()=>{throw Error(`[UploadEngine] dispatch not initialized`)}};if(n&&!t.initialState&&typeof window<`u`)try{let e=n.adapter.load(n.key);Promise.resolve(e).then(e=>{if(!e)return;let r=(n.deserialize??j)(e,{isPurpose:n.isPurpose,isIntent:n.isIntent,hasStrategy:t.strategies.has});if(r){if(s.state.items.size===0)s.state=r;else{let e=new Map(s.state.items);for(let[t,n]of r.items.entries())e.has(t)||e.set(t,n);s.state={...s.state,items:e}}s.notify(),s.scheduleWork()}}).catch(()=>void 0)}catch{}return s}function Ie(e){return{...e,config:u(e.config),transport:e.transport??ge()}}function Le(e,t){switch(t.type){case`files.added`:for(let n of t.items)e.emitter.emit(`file.added`,{localId:n.localId,purpose:n.purpose,file:n.file,fingerprint:n.fingerprint});break;case`validation.ok`:e.emitter.emit(`validation.ok`,{localId:t.localId}),e.emitter.emit(`intent.creating`,{localId:t.localId});break;case`validation.failed`:e.emitter.emit(`validation.failed`,{localId:t.localId,reason:t.reason});break;case`intent.ok`:e.emitter.emit(`intent.created`,{localId:t.localId,intent:t.intent});break;case`intent.failed`:e.emitter.emit(`intent.failed`,{localId:t.localId,error:t.error,retryable:t.retryable});break;case`upload.begin`:e.emitter.emit(`upload.started`,{localId:t.localId});break;case`upload.progress`:{let n=t.totalBytes>0?Math.min(100,t.uploadedBytes/t.totalBytes*100):0;e.emitter.emit(`upload.progress`,{localId:t.localId,pct:n,uploadedBytes:t.uploadedBytes,totalBytes:t.totalBytes});break}case`cursor.updated`:e.emitter.emit(`upload.cursor`,{localId:t.localId,cursor:t.cursor});break;case`upload.ok`:e.emitter.emit(`upload.completing`,{localId:t.localId});break;case`upload.failed`:e.emitter.emit(`upload.error`,{localId:t.localId,error:t.error,retryable:t.retryable});break;case`dedupe.ok`:e.emitter.emit(`upload.completed`,{localId:t.localId,result:t.result,completedBy:`dedupe`});break;case`complete.ok`:e.emitter.emit(`upload.completed`,{localId:t.localId,result:t.result,completedBy:`upload`});break;case`complete.failed`:e.emitter.emit(`upload.error`,{localId:t.localId,error:t.error,retryable:t.retryable});break;case`paused`:e.emitter.emit(`upload.paused`,{localId:t.localId,cursor:t.cursor});break;case`canceled`:e.emitter.emit(`upload.canceled`,{localId:t.localId});break;case`fingerprint.updated`:break}}function Re(e,t,n,r){if(t.type===`start`||t.type===`resume`){let i=n.items.get(t.localId),a=r.items.get(t.localId);if(!i||!a||a.phase!==`queued`)return;i.phase===`paused`?e.emitter.emit(`upload.resumed`,{localId:t.localId}):e.emitter.emit(`upload.queued`,{localId:t.localId})}if(t.type===`cancel`){let i=n.items.get(t.localId),a=r.items.get(t.localId);if(!i||!a)return;a.phase===`canceled`&&i.phase!==`canceled`&&e.emitter.emit(`upload.canceled`,{localId:t.localId})}}function ze(e){let t=Fe(e);t.dispatch=e=>ce(t,e);let n={on:t.emitter.on.bind(t.emitter),off:t.emitter.off.bind(t.emitter),dispatch:t.dispatch,getSnapshot:()=>t.state};for(let t of e.plugins||[])try{t.setup(n)}catch(e){typeof window<`u`&&process.env.NODE_ENV===`development`&&console.error(`[UploadEngine] Plugin "${t.name}" failed to setup:`,e)}return{...n,subscribe:e=>(t.listeners.add(e),()=>t.listeners.delete(e)),waitFor(e){return Be(t,e)}}}function Be(e,t){if(t.length===0)return Promise.resolve([]);let n=new Set(t),r=new Map,i=()=>{for(let t of n){let i=e.state.items.get(t);if(!i){r.set(t,{localId:t,status:`missing`}),n.delete(t);continue}if(i.phase===`completed`){r.set(t,{localId:t,status:`completed`,completedBy:i.completedBy,result:i.result}),n.delete(t);continue}if(i.phase===`error`){r.set(t,{localId:t,status:`error`,error:i.error}),n.delete(t);continue}i.phase===`canceled`&&(r.set(t,{localId:t,status:`canceled`}),n.delete(t))}};return i(),n.size===0?Promise.resolve(t.map(e=>r.get(e)??{localId:e,status:`missing`})):new Promise(a=>{let o=()=>{i(),n.size===0&&(e.listeners.delete(o),a(t.map(e=>r.get(e)??{localId:e,status:`missing`})))};e.listeners.add(o)})}function Ve(e){return ze(e)}
|
|
3
|
+
/*! Copyright (c) Meta Platforms, Inc. and affiliates. **/
|
|
4
|
+
const F=Symbol.for(`react.forward_ref`),I=Symbol.for(`react.memo`);let He=new Map,Ue=new WeakMap,L=new WeakMap;const R=new WeakMap;let z=[];const We=new Map,Ge=new Map,Ke=new Set,qe=new Set;let B=new WeakMap,V=!1;function H(e){if(e.fullKey!==null)return e.fullKey;let t=e.ownKey,n;try{n=e.getCustomHooks()}catch{return e.forceReset=!0,e.fullKey=t,t}for(let r=0;r<n.length;r++){let i=n[r];if(typeof i!=`function`)return e.forceReset=!0,e.fullKey=t,t;let a=L.get(i);if(a===void 0)continue;let o=H(a);a.forceReset&&(e.forceReset=!0),t+=`
|
|
5
|
+
---
|
|
6
|
+
`+o}return e.fullKey=t,t}function Je(e,t){let n=L.get(e),r=L.get(t);return n===void 0&&r===void 0?!0:!(n===void 0||r===void 0||H(n)!==H(r)||r.forceReset)}function Ye(e){return e.prototype&&e.prototype.isReactComponent}function Xe(e,t){return Ye(e)||Ye(t)?!1:!!Je(e,t)}function Ze(e){return R.get(e)}function U(e,t){try{return e[t]}catch{return}}function Qe(){if(z.length===0||V)return null;V=!0;try{let e=new Set,t=new Set,n=z;z=[],n.forEach(([n,r])=>{let i=n.current;R.set(i,n),R.set(r,n),n.current=r,Xe(i,r)?t.add(n):e.add(n)});let r={updatedFamilies:t,staleFamilies:e};We.forEach(e=>{e.setRefreshHandler(Ze)});let i=!1,a=null,o=new Set(qe),s=new Set(Ke),c=new Map(Ge);if(o.forEach(e=>{let t=c.get(e);if(t===void 0)throw Error(`Could not find helpers for a root. This is a bug in React Refresh.`);if(qe.has(e),B===null||!B.has(e))return;let n=B.get(e);try{t.scheduleRoot(e,n)}catch(e){i||(i=!0,a=e)}}),s.forEach(e=>{let t=c.get(e);if(t===void 0)throw Error(`Could not find helpers for a root. This is a bug in React Refresh.`);Ke.has(e);try{t.scheduleRefresh(e,r)}catch(e){i||(i=!0,a=e)}}),i)throw a;return r}finally{V=!1}}function W(e,t){if(e===null||typeof e!=`function`&&typeof e!=`object`||Ue.has(e))return;let n=He.get(t);if(n===void 0?(n={current:e},He.set(t,n)):z.push([n,e]),Ue.set(e,n),typeof e==`object`&&e)switch(U(e,`$$typeof`)){case F:W(e.render,t+`$render`);break;case I:W(e.type,t+`$type`);break}}function G(e,t,n,r){if(L.has(e)||L.set(e,{forceReset:n,ownKey:t,fullKey:null,getCustomHooks:r||(()=>[])}),typeof e==`object`&&e)switch(U(e,`$$typeof`)){case F:G(e.render,t,n,r);break;case I:G(e.type,t,n,r);break}}function $e(e){let t=L.get(e);t!==void 0&&H(t)}function et(){let e,t,n=!1;return function(r,i,a,o){if(typeof i==`string`)return e||(e=r,t=typeof o==`function`),r!=null&&(typeof r==`function`||typeof r==`object`)&&G(r,i,a,o),r;!n&&t&&(n=!0,$e(e))}}function K(e){switch(typeof e){case`function`:{if(e.prototype!=null){if(e.prototype.isReactComponent)return!0;let t=Object.getOwnPropertyNames(e.prototype);if(t.length>1||t[0]!==`constructor`||e.prototype.__proto__!==Object.prototype)return!1}let t=e.name||e.displayName;return typeof t==`string`&&/^[A-Z]/.test(t)}case`object`:if(e!=null)switch(U(e,`$$typeof`)){case F:case I:return!0;default:return!1}return!1;default:return!1}}function tt(e){if(!nt(e))return!1;for(let t in e)if(!K(e[t]))return!1;return!0}function nt(e){return Object.prototype.toString.call(e)===`[object Object]`&&(e.constructor===Object||e.constructor===void 0)}function rt(e){return(t,n)=>W(t,e+` `+n)}function it(e,t){for(let n in t){if(n===`__esModule`)continue;let r=t[n];if(K(r))W(r,e+` export `+n);else if(tt(r))for(let t in r)W(r[t],e+` export `+n+`-`+t)}}function at(e,t){let n;return()=>{clearTimeout(n),n=setTimeout(e,t)}}const q=[];window.__registerBeforePerformReactRefresh=e=>{q.push(e)};const ot=at(async()=>{q.length&&await Promise.all(q.map(e=>e())),Qe()},16);function st(e,t,n){let r=window.__getReactRefreshIgnoredExports?.({id:e})??[];if(J(r,t,e=>e in n)!==!0)return`Could not Fast Refresh (export removed)`;if(J(r,n,e=>e in t)!==!0)return`Could not Fast Refresh (new export)`;let i=!1,a=J(r,n,(e,r)=>(i=!0,K(r)||tt(r)?!0:t[e]===n[e]));if(i&&a===!0)ot();else return`Could not Fast Refresh ("${a}" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-swc#consistent-components-exports`}function J(e,t,n){for(let r in t){if(r===`__esModule`||e.includes(r))continue;let i=Object.getOwnPropertyDescriptor(t,r);if(i&&i.get||!n(r,t[r]))return r}return!0}const ct=e=>import(e),Y=typeof WorkerGlobalScope<`u`&&self instanceof WorkerGlobalScope;let lt,ut;if(import.meta.hot&&!Y){if(!window.$RefreshReg$)throw Error(`@vitejs/plugin-react-swc can't detect preamble. Something is wrong.`);lt=window.$RefreshReg$,ut=window.$RefreshSig$,window.$RefreshReg$=rt(`/home/runner/work/duck-upload/duck-upload/packages/duck-upload/src/react/upload-provider.tsx`),window.$RefreshSig$=et}var dt=$RefreshSig$();const ft=n(null);function pt({store:t,children:n}){return e(ft.Provider,{value:t,children:n},void 0,!1,{fileName:`/home/runner/work/duck-upload/duck-upload/packages/duck-upload/src/react/upload-provider.tsx`,lineNumber:52,columnNumber:10},this)}Z=pt;function X(){dt();let e=r(ft);if(!e||!mt(e))throw Error(`useUploadStore must be used within UploadProvider`);return e}dt(X,`Wl1jJrZzTCxjtMcqth/dX4wg3Uo=`);function mt(e){return p(e)?typeof e.getSnapshot==`function`&&typeof e.subscribe==`function`&&typeof e.dispatch==`function`&&typeof e.on==`function`&&typeof e.off==`function`&&typeof e.waitFor==`function`:!1}var Z;$RefreshReg$(Z,`UploadProvider`),import.meta.hot&&!Y&&(window.$RefreshReg$=lt,window.$RefreshSig$=ut),import.meta.hot&&!Y&&ct(import.meta.url).then(e=>{it(`/home/runner/work/duck-upload/duck-upload/packages/duck-upload/src/react/upload-provider.tsx`,e),import.meta.hot.accept(t=>{if(!t)return;let n=st(`/home/runner/work/duck-upload/duck-upload/packages/duck-upload/src/react/upload-provider.tsx`,e,t);n&&import.meta.hot.invalidate(n)})});var ht=$RefreshSig$(),gt=$RefreshSig$();function _t(e){var t=$RefreshSig$();return t(()=>(t(),Q(e)),`koxc0gANW1Ok/JXE6Hee4XqLL8g=`,!1,function(){return[Q]})}function Q(e){ht();let n=e??X(),r=t.useSyncExternalStore(n.subscribe.bind(n),n.getSnapshot.bind(n),n.getSnapshot.bind(n)),i=t.useMemo(()=>Array.from(r.items.values()),[r.items]),a=t.useMemo(()=>{let e={};return i.forEach(t=>{e[t.phase]||(e[t.phase]=[]),e[t.phase].push(t)}),e},[i]);return{items:i,byPhase:a,dispatch:n.dispatch.bind(n),on:n.on.bind(n),off:n.off.bind(n),uploading:a.uploading||[],paused:a.paused||[],completed:a.completed||[],failed:a.error||[],ready:a.ready||[]}}ht(Q,`2QiAcG9/Y3tKduRxVnmpacwTbRk=`,!1,function(){return[X]});function $(){gt();let e=X();return{dispatch:t.useMemo(()=>e.dispatch.bind(e),[e]),on:t.useMemo(()=>e.on.bind(e),[e]),store:e}}gt($,`mmC0pzXKvfZAl3ye5rFLChFGvco=`,!1,function(){return[X]});function vt(e){return e instanceof Error&&(e.name===`AbortError`||/abort/i.test(e.message))}function yt(e){let t=Math.max(1,e?.maxPartConcurrency??4);return{id:`multipart`,resumable:!0,async start(e){let n=e.intent,r=e.file.size,i=Math.max(1,n.partSize),a=Math.max(1,n.partCount??Math.ceil(r/i)),o=e.readCursor(),s=new Map;if(o?.done)for(let e of o.done)s.set(e.partNumber,{etag:e.etag,size:e.size});let c=o?.completed===!0,l=new Map,u=0;for(let e of s.values())u+=e.size;let d=()=>{let t=0;for(let e of l.values())t+=e;e.reportProgress({uploadedBytes:u+t,totalBytes:r})},f=[];for(let e=1;e<=a;e++){if(s.has(e))continue;let t=(e-1)*i,n=Math.min(r,t+i);f.push({partNumber:e,start:t,end:n})}if(f.length===0){c||await _(),d();return}let p=f.slice(),m=new Set,h=async t=>{let r=n.parts?.find(e=>e.partNumber===t);if(r)return r;if(!e.api.multipart?.signPart)throw Error(`multipart.signPart is missing in UploadApi. Implement it to call your backend sign-part endpoint.`);let i=await e.api.multipart.signPart({fileId:n.fileId,uploadId:n.uploadId,partNumber:t},{signal:e.signal});return{partNumber:t,url:i.url,headers:i.headers}},g=async(t,n=0)=>{try{let n=e.file.slice(t.start,t.end),r=n.size,i=await h(t.partNumber),a=await e.transport.put({url:i.url,body:n,headers:i.headers,signal:e.signal,onProgress:e=>{l.set(t.partNumber,e),d()}});l.delete(t.partNumber);let o=a.etag;if(!o)throw Error(`Missing ETag from upload part response. Ensure MinIO/S3 CORS exposes ETag (Access-Control-Expose-Headers: ETag).`);u+=r,s.set(t.partNumber,{etag:o,size:r});let c={done:Array.from(s.entries()).map(([e,t])=>({partNumber:e,etag:t.etag,size:t.size})).sort((e,t)=>e.partNumber-t.partNumber)};e.persistCursor(c),d()}catch(r){if(l.delete(t.partNumber),e.signal?.aborted||vt(r))throw r;if(n<3){let e=r instanceof Error?r.message:String(r);if(/network/i.test(e)||/timeout/i.test(e)||/5\d\d/.test(e)||/ECONNRESET/i.test(e)||/EHOSTUNREACH/i.test(e))return await v(2**n*500),g(t,n+1)}throw r}};for(;p.length>0||m.size>0;){for(;p.length>0&&m.size<t;){let e=p.shift();if(!e)break;let t,n=g(e).finally(()=>{t&&m.delete(t)});t=n,m.add(n)}m.size>0&&await Promise.race(m)}await _(),d();async function _(){if(c)return;if(!e.api.multipart?.completeMultipart)throw Error(`multipart.completeMultipart is missing in UploadApi. Implement it to call your backend complete endpoint.`);let t=Array.from(s.entries()).map(([e,t])=>({partNumber:e,etag:t.etag})).sort((e,t)=>e.partNumber-t.partNumber);if(t.length!==a)throw Error(`Cannot complete multipart: expected ${a} parts, got ${t.length}`);await e.api.multipart.completeMultipart({fileId:n.fileId,uploadId:n.uploadId,parts:t},{signal:e.signal});let r={done:Array.from(s.entries()).map(([e,t])=>({partNumber:e,etag:t.etag,size:t.size})).sort((e,t)=>e.partNumber-t.partNumber),completed:!0};e.persistCursor(r)}}}}function bt(){return{id:`post`,resumable:!1,async start(e){let t=e.intent;if(!t.url)throw Error(`Direct strategy: intent missing url/uploadUrl`);await e.transport.postForm({url:t.url,file:e.file,fields:t.fields,filename:e.file.name,signal:e.signal,onProgress(t,n){e.reportProgress({uploadedBytes:t,totalBytes:n})}})}}}function xt(){let e={};return{get(t){return e[t]},has(t){return Object.hasOwn(e,t)},set(t){e[t.id]=t}}}export{s as DEFAULT_MAX_ATTEMPTS,c as DEFAULT_MAX_CONCURRENT_UPLOADS,l as DEFAULT_MAX_ITEMS,o as DEFAULT_PROGRESS_THROTTLE_MS,i as DEFAULT_RETRY_DELAY_BASE_MS,a as DEFAULT_RETRY_DELAY_MAX_MS,ve as IndexedDBAdapter,ye as LocalStorageAdapter,be as MemoryAdapter,bt as PostStrategy,pt as UploadProvider,g as computeFingerprint,N as createReducer,xt as createStrategyRegistry,M as createTypedEmitter,Ve as createUploadClient,_t as createUploadFactory,ze as createUploadStore,ge as createXHRTransport,j as deserializeSnapshot,_ as fingerprintMatches,d as generateId,p as isRecord,yt as multipartStrategy,u as resolveUploadConfig,A as serializeSnapshot,v as sleep,X as useUploadStore,Q as useUploader,$ as useUploaderActions,f as validateFile,h as validateFileList,m as validateIntent};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "@gentleduck/upload",
|
|
4
|
+
"version": "0.2.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./core": "./src/core/index.ts",
|
|
9
|
+
"./react": "./src/react/index.ts",
|
|
10
|
+
"./strategies": "./src/strategies/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsdown",
|
|
20
|
+
"dev": "tsdown --watch",
|
|
21
|
+
"check-types": "tsc --noEmit",
|
|
22
|
+
"test": "vitest --passWithNoTests",
|
|
23
|
+
"lint": "biome lint .",
|
|
24
|
+
"format": "biome format --write ."
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": "^19.2.3",
|
|
28
|
+
"react-dom": "^19.2.3"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@gentleduck/tsdown-config": "workspace:*",
|
|
32
|
+
"@gentleduck/typescript-config": "workspace:*",
|
|
33
|
+
"@types/react": "^19.2.14",
|
|
34
|
+
"@types/react-dom": "^19.2.3",
|
|
35
|
+
"tsdown": "0.18.3",
|
|
36
|
+
"typescript": "5.9.3",
|
|
37
|
+
"vite": "^7.3.1",
|
|
38
|
+
"vitest": "^4.0.18"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { UploadError, UploadValidationRules } from '../contracts'
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_MAX_ATTEMPTS,
|
|
4
|
+
DEFAULT_MAX_CONCURRENT_UPLOADS,
|
|
5
|
+
DEFAULT_MAX_ITEMS,
|
|
6
|
+
DEFAULT_PROGRESS_THROTTLE_MS,
|
|
7
|
+
} from '../utils/constants'
|
|
8
|
+
import type { RetryDecision } from './retry.types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Global upload engine configuration.
|
|
12
|
+
*
|
|
13
|
+
* @typeParam P - Union of allowed `purpose` strings for your app.
|
|
14
|
+
*/
|
|
15
|
+
export type UploadConfig<P extends string> = {
|
|
16
|
+
/** Maximum concurrent uploads. */
|
|
17
|
+
maxConcurrentUploads: number
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Auto-start uploads for specific purposes.
|
|
21
|
+
* - Array form: only those purposes auto-start
|
|
22
|
+
* - Function form: dynamic decision per purpose
|
|
23
|
+
*/
|
|
24
|
+
autoStart?: readonly P[] | ((purpose: P) => boolean)
|
|
25
|
+
|
|
26
|
+
/** Progress event throttle in milliseconds. */
|
|
27
|
+
progressThrottleMs: number
|
|
28
|
+
|
|
29
|
+
/** Validation rules keyed by purpose. */
|
|
30
|
+
validation: Partial<Record<P, UploadValidationRules>>
|
|
31
|
+
|
|
32
|
+
/** Maximum number of retry attempts (per item / per phase depending on implementation). */
|
|
33
|
+
maxAttempts: number
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Retry policy.
|
|
37
|
+
* Called whenever a phase fails and the engine needs a decision.
|
|
38
|
+
*/
|
|
39
|
+
retryPolicy?: (ctx: { phase: 'intent' | 'upload' | 'complete'; attempt: number; error: UploadError }) => RetryDecision
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Maximum number of items to keep in state.
|
|
43
|
+
* When exceeded, oldest completed/canceled items are removed automatically.
|
|
44
|
+
*
|
|
45
|
+
* Set to `undefined` for no limit (not recommended for long-running apps).
|
|
46
|
+
* Default often: `100`.
|
|
47
|
+
*/
|
|
48
|
+
maxItems: number
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Automatically remove completed items after this many milliseconds.
|
|
52
|
+
* Set to `undefined` to keep completed items indefinitely.
|
|
53
|
+
*/
|
|
54
|
+
completedItemTTL?: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Config input shape accepted by store creation.
|
|
59
|
+
* All fields are optional; defaults are applied by {@link resolveUploadConfig}.
|
|
60
|
+
*/
|
|
61
|
+
export type UploadConfigInput<P extends string> = Partial<UploadConfig<P>>
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Normalizes user config by applying sensible defaults.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveUploadConfig<P extends string>(input?: UploadConfigInput<P>): UploadConfig<P> {
|
|
67
|
+
return {
|
|
68
|
+
maxConcurrentUploads: Math.max(1, input?.maxConcurrentUploads ?? DEFAULT_MAX_CONCURRENT_UPLOADS),
|
|
69
|
+
autoStart: input?.autoStart,
|
|
70
|
+
progressThrottleMs: Math.max(0, input?.progressThrottleMs ?? DEFAULT_PROGRESS_THROTTLE_MS),
|
|
71
|
+
validation: input?.validation ?? {},
|
|
72
|
+
maxAttempts: Math.max(1, input?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS),
|
|
73
|
+
retryPolicy: input?.retryPolicy,
|
|
74
|
+
maxItems: Math.max(1, input?.maxItems ?? DEFAULT_MAX_ITEMS),
|
|
75
|
+
completedItemTTL: input?.completedItemTTL,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CursorMap, IntentMap, UploadResultBase } from '../contracts'
|
|
2
|
+
import type { InternalEvent } from '../engine/internal-events.types'
|
|
3
|
+
import type { UploadStore } from '../engine/store'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Optional hooks for observing internal behavior.
|
|
7
|
+
*
|
|
8
|
+
* Use these for diagnostics, analytics, logging, or devtools.
|
|
9
|
+
*/
|
|
10
|
+
export type UploadHooks<
|
|
11
|
+
M extends IntentMap,
|
|
12
|
+
C extends CursorMap<M>,
|
|
13
|
+
P extends string,
|
|
14
|
+
R extends UploadResultBase = UploadResultBase,
|
|
15
|
+
> = {
|
|
16
|
+
/**
|
|
17
|
+
* Called after internal events are processed (depending on your runtime wiring).
|
|
18
|
+
* Prefer this over subscribing to public events if you need full fidelity.
|
|
19
|
+
*/
|
|
20
|
+
onInternalEvent?: (
|
|
21
|
+
event: InternalEvent<M, C, P, R>,
|
|
22
|
+
state: import('../engine/reducer').UploadState<M, C, P, R>,
|
|
23
|
+
) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Store plugins.
|
|
28
|
+
*
|
|
29
|
+
* Plugins can subscribe to store events and dispatch commands.
|
|
30
|
+
* They should not reach into internal state mutably.
|
|
31
|
+
*/
|
|
32
|
+
export type UploadPlugin<
|
|
33
|
+
M extends IntentMap,
|
|
34
|
+
C extends CursorMap<M>,
|
|
35
|
+
P extends string,
|
|
36
|
+
R extends UploadResultBase = UploadResultBase,
|
|
37
|
+
> = {
|
|
38
|
+
/** Plugin name (for debugging). */
|
|
39
|
+
name: string
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Setup callback called once when the store is created.
|
|
43
|
+
* The plugin can register event listeners and drive behavior.
|
|
44
|
+
*/
|
|
45
|
+
setup: (ctx: Pick<UploadStore<M, C, P, R>, 'on' | 'dispatch' | 'getSnapshot'>) => void
|
|
46
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CursorMap, IntentMap, UploadResultBase } from '../contracts'
|
|
2
|
+
import type { StoreOptions, UploadStore } from '../engine/store'
|
|
3
|
+
import { createUploadStore } from '../engine/store'
|
|
4
|
+
|
|
5
|
+
export type UploadClient<
|
|
6
|
+
M extends IntentMap,
|
|
7
|
+
C extends CursorMap<M>,
|
|
8
|
+
P extends string,
|
|
9
|
+
R extends UploadResultBase = UploadResultBase,
|
|
10
|
+
> = UploadStore<M, C, P, R>
|
|
11
|
+
|
|
12
|
+
export function createUploadClient<
|
|
13
|
+
M extends IntentMap,
|
|
14
|
+
C extends CursorMap<M>,
|
|
15
|
+
P extends string,
|
|
16
|
+
R extends UploadResultBase = UploadResultBase,
|
|
17
|
+
>(opts: StoreOptions<M, C, P, R>): UploadClient<M, C, P, R> {
|
|
18
|
+
return createUploadStore(opts)
|
|
19
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { IntentMap } from './intent.types'
|
|
2
|
+
import type { UploadResultBase } from './result.types'
|
|
3
|
+
|
|
4
|
+
export type WithSignal = { signal?: AbortSignal }
|
|
5
|
+
|
|
6
|
+
// Backend API contract (typed, injected)
|
|
7
|
+
export interface UploadApi<M extends IntentMap, P extends string, R extends UploadResultBase = UploadResultBase> {
|
|
8
|
+
// Create upload intent - backend decides strategy, bucket, key
|
|
9
|
+
createIntent(
|
|
10
|
+
args: {
|
|
11
|
+
purpose: P
|
|
12
|
+
contentType: string
|
|
13
|
+
size: number
|
|
14
|
+
filename: string
|
|
15
|
+
checksum?: string
|
|
16
|
+
},
|
|
17
|
+
opts?: WithSignal,
|
|
18
|
+
): Promise<M[keyof M]>
|
|
19
|
+
|
|
20
|
+
// Finalize upload (mark as complete in DB, return metadata)
|
|
21
|
+
complete(args: { fileId: string }, opts?: WithSignal): Promise<R>
|
|
22
|
+
|
|
23
|
+
// Get signed preview URL
|
|
24
|
+
getSignedPreviewUrl?(args: { fileId: string; key: string; purpose: P }, opts?: WithSignal): Promise<string>
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Optional support for finding existing files by checksum for deduplication.
|
|
28
|
+
* If implemented, the upload engine will check for existing files before uploading.
|
|
29
|
+
* If a file with the same checksum exists, the upload will be skipped and the existing file will be linked.
|
|
30
|
+
*/
|
|
31
|
+
findByChecksum?(args: { checksum: string; purpose: P }, opts?: WithSignal): Promise<R | null>
|
|
32
|
+
|
|
33
|
+
// Multipart-specific operations (optional, for multipart strategy)
|
|
34
|
+
multipart?: {
|
|
35
|
+
signPart(
|
|
36
|
+
args: { fileId: string; uploadId: string; partNumber: number; checksum?: string },
|
|
37
|
+
opts?: WithSignal,
|
|
38
|
+
): Promise<{ url: string; headers?: Record<string, string> }>
|
|
39
|
+
|
|
40
|
+
completeMultipart(
|
|
41
|
+
args: { fileId: string; uploadId: string; parts: Array<{ partNumber: number; etag: string }> },
|
|
42
|
+
opts?: WithSignal,
|
|
43
|
+
): Promise<unknown>
|
|
44
|
+
|
|
45
|
+
listParts?(
|
|
46
|
+
args: { fileId: string; uploadId: string },
|
|
47
|
+
opts?: WithSignal,
|
|
48
|
+
): Promise<Array<{ partNumber: number; etag?: string; size?: number }>>
|
|
49
|
+
|
|
50
|
+
abort?(args: { fileId: string; uploadId: string }, opts?: WithSignal): Promise<void>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// TUS-specific operations (optional, for tus strategy)
|
|
54
|
+
tus?: {
|
|
55
|
+
create(
|
|
56
|
+
args: { fileId: string; size: number; filename: string; contentType: string },
|
|
57
|
+
opts?: WithSignal,
|
|
58
|
+
): Promise<{ uploadUrl: string }>
|
|
59
|
+
|
|
60
|
+
getOffset(args: { uploadUrl: string }, opts?: WithSignal): Promise<{ offset: number }>
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IntentMap, StrategyKey } from './intent.types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Registry of cursor payload types mapped by strategy name.
|
|
5
|
+
*
|
|
6
|
+
* A "cursor" is strategy-defined resume state (ex: multipart uploaded parts, next byte offset).
|
|
7
|
+
* Use `unknown` here and define concrete cursor payload types in your app.
|
|
8
|
+
*
|
|
9
|
+
* @typeParam M - The intent map, used to constrain strategy keys.
|
|
10
|
+
*/
|
|
11
|
+
export type CursorMap<M extends IntentMap> = Partial<Record<StrategyKey<M>, unknown>>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Discriminated union of all cursor variants.
|
|
15
|
+
*
|
|
16
|
+
* The cursor is always tagged with a `strategy` so it can be routed back to the correct strategy
|
|
17
|
+
* on resume/rebind.
|
|
18
|
+
*
|
|
19
|
+
* @typeParam C - Cursor registry (usually {@link CursorMap}).
|
|
20
|
+
*/
|
|
21
|
+
export type AnyCursor<C extends Record<string, unknown>> = {
|
|
22
|
+
[K in keyof C & string]: { strategy: K; value?: C[K] }
|
|
23
|
+
}[keyof C & string]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File rejection reasons produced by client-side validation.
|
|
3
|
+
*
|
|
4
|
+
* These should be safe to show in UI.
|
|
5
|
+
*/
|
|
6
|
+
export type RejectReason =
|
|
7
|
+
| { code: 'empty_file' }
|
|
8
|
+
| { code: 'file_too_large'; maxBytes: number; size: number }
|
|
9
|
+
| { code: 'type_not_allowed'; allowed: string[]; got: string }
|
|
10
|
+
| { code: 'too_many_files'; max: number }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base interface for custom errors.
|
|
14
|
+
*
|
|
15
|
+
* If you want to add domain-specific fields, extend this and intersect in {@link UploadError}.
|
|
16
|
+
*/
|
|
17
|
+
export interface UploadErrorBase {
|
|
18
|
+
/** Stable error code. */
|
|
19
|
+
code: string
|
|
20
|
+
/** Human-friendly message (safe for UI). */
|
|
21
|
+
message: string
|
|
22
|
+
/** Optional cause (original error, response, etc). */
|
|
23
|
+
cause?: unknown
|
|
24
|
+
/** Whether the error should be retried. */
|
|
25
|
+
retryable?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Built-in engine errors.
|
|
30
|
+
*
|
|
31
|
+
* Keep these narrow and explicit so reducer logic can rely on `code` as a discriminant.
|
|
32
|
+
*/
|
|
33
|
+
export type BuiltInUploadError =
|
|
34
|
+
| { code: 'validation_failed'; message: string; reason: RejectReason; retryable?: false }
|
|
35
|
+
| { code: 'strategy_missing'; message: string; strategy: string; retryable?: false }
|
|
36
|
+
| { code: 'aborted'; message: string; reason: 'pause' | 'cancel' | 'unknown'; cause?: unknown; retryable?: false }
|
|
37
|
+
| { code: 'network'; message: string; cause?: unknown; retryable?: boolean }
|
|
38
|
+
| { code: 'http'; message: string; status: number; statusText?: string; cause?: unknown; retryable?: boolean }
|
|
39
|
+
| { code: 'timeout'; message: string; cause?: unknown; retryable?: boolean }
|
|
40
|
+
| { code: 'auth'; message: string; cause?: unknown; retryable?: false }
|
|
41
|
+
| { code: 'rate_limit'; message: string; retryAfterMs?: number; cause?: unknown; retryable?: boolean }
|
|
42
|
+
| { code: 'server'; message: string; serverCode?: string; cause?: unknown; retryable?: boolean }
|
|
43
|
+
| { code: 'unknown'; message: string; cause?: unknown; retryable?: boolean }
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The error type used everywhere in the engine.
|
|
47
|
+
*
|
|
48
|
+
* You can extend this by intersecting your own shape with {@link UploadErrorBase}
|
|
49
|
+
* to add fields (example: `{ endpoint: string }`).
|
|
50
|
+
*/
|
|
51
|
+
export type UploadError = BuiltInUploadError | (UploadErrorBase & Record<string, unknown>)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validation rules applied on `addFiles` before an item enters the upload pipeline.
|
|
55
|
+
*/
|
|
56
|
+
export type UploadValidationRules = {
|
|
57
|
+
/** Maximum number of files allowed. */
|
|
58
|
+
maxFiles?: number
|
|
59
|
+
/** Maximum file size in bytes. */
|
|
60
|
+
maxSizeBytes?: number
|
|
61
|
+
/** Minimum file size in bytes. */
|
|
62
|
+
minSizeBytes?: number
|
|
63
|
+
/** Allowed MIME types. */
|
|
64
|
+
allowedTypes?: string[]
|
|
65
|
+
/** Allowed file extensions. */
|
|
66
|
+
allowedExtensions?: string[]
|
|
67
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic client-side identity for a file.
|
|
3
|
+
*
|
|
4
|
+
* Used to match the same file across refreshes (for rebind) and to deduplicate.
|
|
5
|
+
* If you include `checksum`, it should be stable and computed consistently.
|
|
6
|
+
*/
|
|
7
|
+
export type FileFingerprint = {
|
|
8
|
+
/** File name. */
|
|
9
|
+
name: string
|
|
10
|
+
/** File size in bytes. */
|
|
11
|
+
size: number
|
|
12
|
+
/** File MIME type. */
|
|
13
|
+
type: string
|
|
14
|
+
/** File lastModified timestamp (ms). */
|
|
15
|
+
lastModified: number
|
|
16
|
+
/** Optional checksum (example: SHA-256 hex). */
|
|
17
|
+
checksum?: string
|
|
18
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base shape for all upload intents.
|
|
3
|
+
*
|
|
4
|
+
* An "intent" is data provided by your backend that tells the client how to upload a file
|
|
5
|
+
* (for example: a presigned POST, presigned PUT, multipart upload IDs, part URLs, headers, etc).
|
|
6
|
+
*
|
|
7
|
+
* Every intent must include:
|
|
8
|
+
* - `strategy`: a discriminant used to select an upload strategy implementation
|
|
9
|
+
* - `fileId`: a stable backend identifier for the file being uploaded
|
|
10
|
+
*
|
|
11
|
+
* @typeParam K - Strategy key / discriminant string (example: `"direct" | "multipart"`).
|
|
12
|
+
*/
|
|
13
|
+
export type IntentBase<K extends string = string> = {
|
|
14
|
+
/** Strategy name (discriminant). */
|
|
15
|
+
strategy: K
|
|
16
|
+
/** Unique backend file identifier. */
|
|
17
|
+
fileId: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Registry of intent types mapped by strategy name.
|
|
22
|
+
*
|
|
23
|
+
* Example:
|
|
24
|
+
* ```ts
|
|
25
|
+
* type MyIntents = {
|
|
26
|
+
* direct: DirectIntent
|
|
27
|
+
* multipart: MultipartIntent
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* `M[keyof M]` is used throughout the engine as the "any intent" union.
|
|
32
|
+
*/
|
|
33
|
+
export type IntentMap = Record<string, IntentBase<string>>
|
|
34
|
+
|
|
35
|
+
/** Union of strategy keys for an intent map. */
|
|
36
|
+
export type StrategyKey<M extends IntentMap> = keyof M & string
|
|
37
|
+
|
|
38
|
+
/** Union of all intent variants for an intent map. */
|
|
39
|
+
export type AnyIntent<M extends IntentMap> = M[StrategyKey<M>]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base shape for upload completion results returned by your backend.
|
|
3
|
+
*
|
|
4
|
+
* Extend this with any additional metadata you return (e.g. width/height, variants).
|
|
5
|
+
*/
|
|
6
|
+
export type UploadResultBase = {
|
|
7
|
+
/** Backend file identifier. */
|
|
8
|
+
fileId: string
|
|
9
|
+
/** Storage key or path used for signed URLs. */
|
|
10
|
+
key: string
|
|
11
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { UploadApi } from './backendApi'
|
|
2
|
+
import type { CursorMap } from './cursor.types'
|
|
3
|
+
import type { IntentMap } from './intent.types'
|
|
4
|
+
import type { UploadResultBase } from './result.types'
|
|
5
|
+
import type { UploadTransport } from './transport/transport.types'
|
|
6
|
+
|
|
7
|
+
export type StrategyCtx<
|
|
8
|
+
M extends IntentMap,
|
|
9
|
+
C extends CursorMap<M>,
|
|
10
|
+
P extends string,
|
|
11
|
+
R extends UploadResultBase,
|
|
12
|
+
K extends keyof M & string,
|
|
13
|
+
> = {
|
|
14
|
+
file: File
|
|
15
|
+
intent: M[K]
|
|
16
|
+
signal: AbortSignal
|
|
17
|
+
transport: UploadTransport
|
|
18
|
+
api: UploadApi<M, P, R>
|
|
19
|
+
reportProgress: (p: { uploadedBytes: number; totalBytes: number }) => void
|
|
20
|
+
readCursor: () => C[K] | undefined
|
|
21
|
+
persistCursor: (cursor: C[K] | undefined) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type UploadStrategy<
|
|
25
|
+
M extends IntentMap,
|
|
26
|
+
C extends CursorMap<M>,
|
|
27
|
+
P extends string,
|
|
28
|
+
R extends UploadResultBase,
|
|
29
|
+
K extends keyof M & string,
|
|
30
|
+
> = {
|
|
31
|
+
id: K
|
|
32
|
+
resumable: boolean
|
|
33
|
+
start(ctx: StrategyCtx<M, C, P, R, K>): Promise<void>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface StrategyRegistry<
|
|
37
|
+
M extends IntentMap,
|
|
38
|
+
C extends CursorMap<M>,
|
|
39
|
+
P extends string = string,
|
|
40
|
+
R extends UploadResultBase = UploadResultBase,
|
|
41
|
+
> {
|
|
42
|
+
get<K extends keyof M & string>(id: K): UploadStrategy<M, C, P, R, K> | undefined
|
|
43
|
+
has(id: string): id is keyof M & string
|
|
44
|
+
set<K extends keyof M & string>(strategy: UploadStrategy<M, C, P, R, K>): void
|
|
45
|
+
}
|