@uploadista/client-browser 0.0.20 → 0.1.0-beta.5
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.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -7
- package/src/__tests__/setup.ts +135 -0
- package/src/framework-utils.test.ts +519 -0
- package/src/http-client.test.ts +402 -0
- package/src/http-client.ts +11 -0
- package/src/services/abort-controller-factory.test.ts +163 -0
- package/src/services/checksum-service.test.ts +70 -0
- package/src/services/create-browser-services.test.ts +248 -0
- package/src/services/file-reader.test.ts +171 -0
- package/src/services/fingerprint-service.test.ts +123 -0
- package/src/services/id-generation/id-generation.test.ts +54 -0
- package/src/services/platform-service.test.ts +169 -0
- package/src/services/storage/local-storage-service.test.ts +168 -0
- package/src/services/storage/session-storage-service.test.ts +168 -0
- package/src/services/websocket-factory.test.ts +245 -0
- package/src/utils/hash-util.test.ts +84 -0
- package/vitest.config.ts +3 -1
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/upload-input.ts","../src/client/create-uploadista-client.ts","../src/framework-utils.ts","../src/http-client.ts","../src/services/file-reader.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;AAwBA;;;;ACQA;;;;;;AAmGA;;;;;KD3GY,kBAAA,GAAqB,OAAO;;;;;;;;;;AAAxC;;;;ACQA;;;;;;AAmGA;;;;;UAnGiB,uBAAA,SACP,KACN,0BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;sBAsCV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2DN,sBAAA,UAAgC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/upload-input.ts","../src/client/create-uploadista-client.ts","../src/framework-utils.ts","../src/http-client.ts","../src/services/file-reader.ts"],"sourcesContent":[],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{createClientStorage as e,createLogger as t,createUploadistaClient as n}from"@uploadista/client-core";export*from"@uploadista/client-core";function r(e){return new i(e)}var i=class{config;metrics;connectionTimes=[];requestCount=0;connectionCount=0;errorCount=0;timeoutCount=0;retryCount=0;startTime=Date.now();http2Info;constructor(e={}){this.config={maxConnectionsPerHost:e.maxConnectionsPerHost??6,connectionTimeout:e.connectionTimeout??3e4,keepAliveTimeout:e.keepAliveTimeout??6e4,enableHttp2:e.enableHttp2??!0,retryOnConnectionError:e.retryOnConnectionError??!0},this.metrics={activeConnections:0,totalConnections:0,reuseRate:0,averageConnectionTime:0},this.http2Info=this.detectHttp2Support()}detectHttp2Support(){let e=`serviceWorker`in navigator&&`fetch`in window,t=`ReadableStream`in window&&`WritableStream`in window&&`TransformStream`in window;return{supported:e,detected:!1,version:t?`h2`:`h1.1`,multiplexingActive:t&&this.config.enableHttp2}}async request(e,t={}){this.requestCount++;let n={method:t.method||`GET`,headers:{Connection:`keep-alive`,"Keep-Alive":`timeout=${this.config.keepAliveTimeout/1e3}`,...t.headers},body:t.body,credentials:t.credentials||`include`,signal:t.signal};if(t.timeout){let r=new AbortController,i=setTimeout(()=>r.abort(),t.timeout);t.signal&&t.signal.addEventListener(`abort`,()=>r.abort()),n.signal=r.signal;try{let t=await this.makeRequest(e,n);return clearTimeout(i),t}catch(e){throw clearTimeout(i),e}}return this.makeRequest(e,n)}async makeRequest(e,t){let n=Date.now();try{let r=await fetch(e,t),i=Date.now()-n;return this.recordConnectionMetrics(i),{status:r.status,statusText:r.statusText,headers:r.headers,ok:r.ok,json:()=>r.json(),text:()=>r.text(),arrayBuffer:()=>r.arrayBuffer()}}catch(e){throw this.connectionCount++,e}}recordConnectionMetrics(e){this.connectionTimes.push(e),this.connectionCount++,this.connectionTimes.length>100&&this.connectionTimes.shift(),this.metrics.totalConnections=this.connectionCount,this.metrics.averageConnectionTime=this.connectionTimes.reduce((e,t)=>e+t,0)/this.connectionTimes.length;let t=this.connectionTimes.filter(e=>e<100).length;this.metrics.reuseRate=t/this.connectionTimes.length}getMetrics(){return{...this.metrics}}getDetailedMetrics(){let e=this.calculateConnectionHealth(),t=(Date.now()-this.startTime)/1e3,n=t>0?this.requestCount/t:0,r=this.requestCount>0?this.errorCount/this.requestCount:0,i=this.connectionTimes.filter(e=>e<100).length,a=this.connectionTimes.length-i;return{...this.metrics,health:e,requestsPerSecond:n,errorRate:r,timeouts:this.timeoutCount,retries:this.retryCount,fastConnections:i,slowConnections:a,http2Info:this.http2Info}}calculateConnectionHealth(){let e=[],t=[],n=100;this.metrics.reuseRate<.3?(e.push(`Low connection reuse rate`),t.push(`Check if keep-alive headers are working`),n-=30):this.metrics.reuseRate<.7&&(e.push(`Moderate connection reuse rate`),t.push(`Consider adjusting keep-alive timeout`),n-=15);let r=this.requestCount>0?this.errorCount/this.requestCount:0;r>.1?(e.push(`High error rate`),t.push(`Check network stability and server configuration`),n-=25):r>.05&&(e.push(`Moderate error rate`),t.push(`Monitor network conditions`),n-=10),this.metrics.averageConnectionTime>1e3?(e.push(`Slow connection establishment`),t.push(`Check network latency and DNS resolution`),n-=20):this.metrics.averageConnectionTime>500&&(e.push(`Moderate connection latency`),t.push(`Consider connection warming`),n-=10);let i;return i=n>=80?`healthy`:n>=60?`degraded`:`poor`,{status:i,score:Math.max(0,n),issues:e,recommendations:t}}async warmupConnections(e){if(e.length===0)return;console.log(`Warming up connections to ${e.length} hosts...`);let t=e.map(async e=>{try{await this.request(e,{method:`HEAD`,timeout:5e3})}catch(t){console.warn(`Connection warmup failed for ${e}:`,t)}});await Promise.allSettled(t),console.log(`Connection warmup completed`)}reset(){this.connectionTimes=[],this.requestCount=0,this.connectionCount=0,this.errorCount=0,this.timeoutCount=0,this.retryCount=0,this.startTime=Date.now(),this.metrics={activeConnections:0,totalConnections:0,reuseRate:0,averageConnectionTime:0},this.http2Info=this.detectHttp2Support()}async close(){console.log(`Gracefully shutting down HTTP client...`),await new Promise(e=>setTimeout(e,100));let e=this.getDetailedMetrics();console.log(`Final connection metrics:`,{totalRequests:this.requestCount,connectionReuse:`${Math.round(e.reuseRate*100)}%`,averageConnectionTime:`${Math.round(e.averageConnectionTime)}ms`,health:e.health.status}),this.reset(),console.log(`HTTP client shutdown complete`)}},a=class{native;constructor(){this.native=new AbortController}get signal(){return this.native.signal}abort(e){this.native.abort(e)}};const o=()=>({create:()=>new a});async function s(e){try{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(``)}catch(e){throw Error(`Failed to compute file checksum: ${e instanceof Error?e.message:`Unknown error`}`)}}function c(){return{computeChecksum:async e=>s(new Blob([e]))}}function l(e){return{input:e,size:e.size,slice:async(t,n)=>{let r=e.slice(t,n),i=r.size,a=n>=e.size;return{value:new Uint8Array(await r.arrayBuffer()),size:i,done:a}},close:()=>{}}}function u(){return{openFile:async(e,t)=>{if(e instanceof Blob){let t=l(e);return{input:t.input,size:t.size,slice:t.slice,close:t.close,name:t.input instanceof File?t.input.name:null,type:t.input instanceof File?t.input.type:null,lastModified:t.input instanceof File?t.input.lastModified:null}}throw Error(`source object may only be an instance of File, Blob in this environment`)}}}function d(){return{computeFingerprint:async(e,t)=>s(e)}}function f(){return{generate:()=>crypto.randomUUID()}}function p(){return{setTimeout:(e,t)=>globalThis.setTimeout(e,t),clearTimeout:e=>{globalThis.clearTimeout(e)},isBrowser:()=>typeof window<`u`,isOnline:()=>typeof navigator<`u`?navigator.onLine:!0,isFileLike:e=>e instanceof File,getFileName:e=>{if(e instanceof File)return e.name},getFileType:e=>{if(e instanceof File)return e.type},getFileSize:e=>{if(e instanceof File)return e.size},getFileLastModified:e=>{if(e instanceof File)return e.lastModified}}}function m(){let e=e=>{let t={};for(let n in localStorage)if(n.startsWith(e)){let e=localStorage.getItem(n);e&&(t[n]=e)}return t};return{async getItem(e){return localStorage.getItem(e)},async setItem(e,t){localStorage.setItem(e,t)},async removeItem(e){localStorage.removeItem(e)},async findAll(){return e(``)},async find(t){return e(t)}}}var h=class{CONNECTING=0;OPEN=1;CLOSING=2;CLOSED=3;readyState;onopen=null;onclose=null;onerror=null;onmessage=null;native;constructor(e){this.native=new WebSocket(e),this.readyState=this.native.readyState,this.native.onopen=()=>{this.readyState=this.native.readyState,this.onopen?.()},this.native.onclose=e=>{this.readyState=this.native.readyState;let t=e;this.onclose?.({code:t.code,reason:t.reason})},this.native.onerror=e=>{this.onerror?.({message:`WebSocket error`})},this.native.onmessage=e=>{let t=e;this.onmessage?.({data:t.data})}}send(e){this.native.send(e)}close(e,t){this.native.close(e,t)}};const g=()=>({create:e=>new h(e)});function _(e={}){let{connectionPooling:t,useLocalStorage:n=!0}=e,i=m(),a=f(),s=r(t),l=u(),h=g(),_=o(),v=c(),y=d();return{platform:p(),storage:i,idGeneration:a,httpClient:s,fileReader:l,websocket:h,abortController:_,checksumService:v,fingerprintService:y}}function v(r){let i=_({connectionPooling:r.connectionPooling});return n({...r,webSocketFactory:i.websocket,abortControllerFactory:i.abortController,platformService:i.platform,httpClient:i.httpClient,fileReader:i.fileReader,generateId:i.idGeneration,fingerprintService:i.fingerprintService,checksumService:i.checksumService,logger:t(!1,()=>{}),clientStorage:e(i.storage)})}function y(e){let t=e.length,n=e.filter(e=>e.status===`success`).length,r=e.filter(e=>e.status===`error`).length,i=e.reduce((e,t)=>e+t.totalBytes,0),a=e.reduce((e,t)=>e+t.bytesUploaded,0);return{totalFiles:t,completedFiles:n,failedFiles:r,totalBytes:i,uploadedBytes:a,totalProgress:i>0?a/i*100:0,allComplete:e.every(e=>e.status===`success`),hasErrors:e.some(e=>e.status===`error`)}}function b(e){if(e===0)return`0 Bytes`;let t=1024,n=[`Bytes`,`KB`,`MB`,`GB`,`TB`],r=Math.floor(Math.log(e)/Math.log(t));return`${Number.parseFloat((e/t**r).toFixed(2))} ${n[r]}`}function x(e){return`${Math.round(e)}%`}function S(e){let t=e.lastIndexOf(`.`);return t===-1?``:e.slice(t+1).toLowerCase()}function C(e){return e.type.startsWith(`image/`)}function w(e){return e.type.startsWith(`video/`)}function T(e){return t=>t.size>e?{valid:!1,error:`File size exceeds maximum of ${b(e)}`}:{valid:!0}}function E(e){return t=>{let n=t.type.toLowerCase(),r=S(t.name);return e.some(e=>{if(e.startsWith(`.`))return e.slice(1)===r;if(e.includes(`*`)){let t=e.replace(`*`,``);return n.startsWith(t)}return n===e})?{valid:!0}:{valid:!1,error:`File type not allowed. Allowed types: ${e.join(`, `)}`}}}function D(...e){return t=>{for(let n of e){let e=n(t);if(!e.valid)return e}return{valid:!0}}}function O(){return`upload-${Date.now()}-${Math.random().toString(36).substr(2,9)}`}function k(e){return new Promise(t=>setTimeout(t,e))}function A(e,t=1e3,n=3e4){return Math.min(t*2**e,n)+Math.random()*1e3}function j(e,t=3,n=()=>!0){return async()=>{let r;for(let i=0;i<t;i++)try{return await e()}catch(e){if(r=e,i<t-1&&n(e)){await k(A(i));continue}break}throw r}}function M(e){return e instanceof Error?e.message.includes(`network`)||e.message.includes(`timeout`)||e.message.includes(`connection`)||e.message.includes(`ECONNREFUSED`)||e.message.includes(`ETIMEDOUT`):!1}function N(e){return e instanceof Error?e.name===`AbortError`||e.message.includes(`abort`):!1}function P(e){if(e===0)return`0 B/s`;let t=1024,n=[`B/s`,`KB/s`,`MB/s`,`GB/s`],r=Math.floor(Math.log(e)/Math.log(t));return`${parseFloat((e/t**r).toFixed(1))} ${n[r]}`}function F(e){if(e<1e3)return`${Math.round(e)}ms`;if(e<6e4)return`${Math.round(e/1e3)}s`;if(e<36e5){let t=Math.floor(e/6e4),n=Math.round(e%6e4/1e3);return n>0?`${t}m ${n}s`:`${t}m`}let t=Math.floor(e/36e5),n=Math.round(e%36e5/6e4);return n>0?`${t}h ${n}m`:`${t}h`}function I(e,t){return!t||t.length===0?!0:t.some(t=>{if(t.startsWith(`.`))return e.name.toLowerCase().endsWith(t.toLowerCase());if(t.endsWith(`/*`)){let n=t.slice(0,-2);return e.type.startsWith(n)}return e.type===t})}function L(e){return e.type.startsWith(`audio/`)}function R(e){return[`application/pdf`,`application/msword`,`application/vnd.openxmlformats-officedocument.wordprocessingml.document`,`application/vnd.ms-excel`,`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,`application/vnd.ms-powerpoint`,`application/vnd.openxmlformats-officedocument.presentationml.presentation`,`text/plain`,`text/csv`,`application/rtf`].includes(e.type)}function z(e){return C(e)||w(e)||L(e)?URL.createObjectURL(e):null}function B(e){URL.revokeObjectURL(e)}function V(e,t){return t===0?0:Math.min(100,Math.max(0,Math.round(e/t*100)))}export{A as calculateBackoff,y as calculateMultiUploadStats,V as calculateProgress,D as composeValidators,u as createBrowserFileReaderService,z as createFilePreview,T as createFileSizeValidator,E as createFileTypeValidator,r as createHttpClient,j as createRetryWrapper,v as createUploadistaClient,k as delay,F as formatDuration,b as formatFileSize,x as formatProgress,P as formatSpeed,O as generateUploadId,S as getFileExtension,N as isAbortError,L as isAudioFile,R as isDocumentFile,C as isImageFile,M as isNetworkError,w as isVideoFile,B as revokeFilePreview,I as validateFileType};
|
|
1
|
+
import{createClientStorage as e,createLogger as t,createUploadistaClient as n}from"@uploadista/client-core";export*from"@uploadista/client-core";function r(e){return new i(e)}var i=class{config;metrics;connectionTimes=[];requestCount=0;connectionCount=0;errorCount=0;timeoutCount=0;retryCount=0;startTime=Date.now();http2Info;constructor(e={}){this.config={maxConnectionsPerHost:e.maxConnectionsPerHost??6,connectionTimeout:e.connectionTimeout??3e4,keepAliveTimeout:e.keepAliveTimeout??6e4,enableHttp2:e.enableHttp2??!0,retryOnConnectionError:e.retryOnConnectionError??!0},this.metrics={activeConnections:0,totalConnections:0,reuseRate:0,averageConnectionTime:0},this.http2Info=this.detectHttp2Support()}detectHttp2Support(){if(typeof window>`u`||typeof navigator>`u`)return{supported:!1,detected:!1,version:`h1.1`,multiplexingActive:!1};let e=`serviceWorker`in navigator&&`fetch`in window,t=`ReadableStream`in window&&`WritableStream`in window&&`TransformStream`in window;return{supported:e,detected:!1,version:t?`h2`:`h1.1`,multiplexingActive:t&&this.config.enableHttp2}}async request(e,t={}){this.requestCount++;let n={method:t.method||`GET`,headers:{Connection:`keep-alive`,"Keep-Alive":`timeout=${this.config.keepAliveTimeout/1e3}`,...t.headers},body:t.body,credentials:t.credentials||`include`,signal:t.signal};if(t.timeout){let r=new AbortController,i=setTimeout(()=>r.abort(),t.timeout);t.signal&&t.signal.addEventListener(`abort`,()=>r.abort()),n.signal=r.signal;try{let t=await this.makeRequest(e,n);return clearTimeout(i),t}catch(e){throw clearTimeout(i),e}}return this.makeRequest(e,n)}async makeRequest(e,t){let n=Date.now();try{let r=await fetch(e,t),i=Date.now()-n;return this.recordConnectionMetrics(i),{status:r.status,statusText:r.statusText,headers:r.headers,ok:r.ok,json:()=>r.json(),text:()=>r.text(),arrayBuffer:()=>r.arrayBuffer()}}catch(e){throw this.connectionCount++,e}}recordConnectionMetrics(e){this.connectionTimes.push(e),this.connectionCount++,this.connectionTimes.length>100&&this.connectionTimes.shift(),this.metrics.totalConnections=this.connectionCount,this.metrics.averageConnectionTime=this.connectionTimes.reduce((e,t)=>e+t,0)/this.connectionTimes.length;let t=this.connectionTimes.filter(e=>e<100).length;this.metrics.reuseRate=t/this.connectionTimes.length}getMetrics(){return{...this.metrics}}getDetailedMetrics(){let e=this.calculateConnectionHealth(),t=(Date.now()-this.startTime)/1e3,n=t>0?this.requestCount/t:0,r=this.requestCount>0?this.errorCount/this.requestCount:0,i=this.connectionTimes.filter(e=>e<100).length,a=this.connectionTimes.length-i;return{...this.metrics,health:e,requestsPerSecond:n,errorRate:r,timeouts:this.timeoutCount,retries:this.retryCount,fastConnections:i,slowConnections:a,http2Info:this.http2Info}}calculateConnectionHealth(){let e=[],t=[],n=100;this.metrics.reuseRate<.3?(e.push(`Low connection reuse rate`),t.push(`Check if keep-alive headers are working`),n-=30):this.metrics.reuseRate<.7&&(e.push(`Moderate connection reuse rate`),t.push(`Consider adjusting keep-alive timeout`),n-=15);let r=this.requestCount>0?this.errorCount/this.requestCount:0;r>.1?(e.push(`High error rate`),t.push(`Check network stability and server configuration`),n-=25):r>.05&&(e.push(`Moderate error rate`),t.push(`Monitor network conditions`),n-=10),this.metrics.averageConnectionTime>1e3?(e.push(`Slow connection establishment`),t.push(`Check network latency and DNS resolution`),n-=20):this.metrics.averageConnectionTime>500&&(e.push(`Moderate connection latency`),t.push(`Consider connection warming`),n-=10);let i;return i=n>=80?`healthy`:n>=60?`degraded`:`poor`,{status:i,score:Math.max(0,n),issues:e,recommendations:t}}async warmupConnections(e){if(e.length===0)return;console.log(`Warming up connections to ${e.length} hosts...`);let t=e.map(async e=>{try{await this.request(e,{method:`HEAD`,timeout:5e3})}catch(t){console.warn(`Connection warmup failed for ${e}:`,t)}});await Promise.allSettled(t),console.log(`Connection warmup completed`)}reset(){this.connectionTimes=[],this.requestCount=0,this.connectionCount=0,this.errorCount=0,this.timeoutCount=0,this.retryCount=0,this.startTime=Date.now(),this.metrics={activeConnections:0,totalConnections:0,reuseRate:0,averageConnectionTime:0},this.http2Info=this.detectHttp2Support()}async close(){console.log(`Gracefully shutting down HTTP client...`),await new Promise(e=>setTimeout(e,100));let e=this.getDetailedMetrics();console.log(`Final connection metrics:`,{totalRequests:this.requestCount,connectionReuse:`${Math.round(e.reuseRate*100)}%`,averageConnectionTime:`${Math.round(e.averageConnectionTime)}ms`,health:e.health.status}),this.reset(),console.log(`HTTP client shutdown complete`)}},a=class{native;constructor(){this.native=new AbortController}get signal(){return this.native.signal}abort(e){this.native.abort(e)}};const o=()=>({create:()=>new a});async function s(e){try{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(``)}catch(e){throw Error(`Failed to compute file checksum: ${e instanceof Error?e.message:`Unknown error`}`)}}function c(){return{computeChecksum:async e=>s(new Blob([e]))}}function l(e){return{input:e,size:e.size,slice:async(t,n)=>{let r=e.slice(t,n),i=r.size,a=n>=e.size;return{value:new Uint8Array(await r.arrayBuffer()),size:i,done:a}},close:()=>{}}}function u(){return{openFile:async(e,t)=>{if(e instanceof Blob){let t=l(e);return{input:t.input,size:t.size,slice:t.slice,close:t.close,name:t.input instanceof File?t.input.name:null,type:t.input instanceof File?t.input.type:null,lastModified:t.input instanceof File?t.input.lastModified:null}}throw Error(`source object may only be an instance of File, Blob in this environment`)}}}function d(){return{computeFingerprint:async(e,t)=>s(e)}}function f(){return{generate:()=>crypto.randomUUID()}}function p(){return{setTimeout:(e,t)=>globalThis.setTimeout(e,t),clearTimeout:e=>{globalThis.clearTimeout(e)},isBrowser:()=>typeof window<`u`,isOnline:()=>typeof navigator<`u`?navigator.onLine:!0,isFileLike:e=>e instanceof File,getFileName:e=>{if(e instanceof File)return e.name},getFileType:e=>{if(e instanceof File)return e.type},getFileSize:e=>{if(e instanceof File)return e.size},getFileLastModified:e=>{if(e instanceof File)return e.lastModified}}}function m(){let e=e=>{let t={};for(let n in localStorage)if(n.startsWith(e)){let e=localStorage.getItem(n);e&&(t[n]=e)}return t};return{async getItem(e){return localStorage.getItem(e)},async setItem(e,t){localStorage.setItem(e,t)},async removeItem(e){localStorage.removeItem(e)},async findAll(){return e(``)},async find(t){return e(t)}}}var h=class{CONNECTING=0;OPEN=1;CLOSING=2;CLOSED=3;readyState;onopen=null;onclose=null;onerror=null;onmessage=null;native;constructor(e){this.native=new WebSocket(e),this.readyState=this.native.readyState,this.native.onopen=()=>{this.readyState=this.native.readyState,this.onopen?.()},this.native.onclose=e=>{this.readyState=this.native.readyState;let t=e;this.onclose?.({code:t.code,reason:t.reason})},this.native.onerror=e=>{this.onerror?.({message:`WebSocket error`})},this.native.onmessage=e=>{let t=e;this.onmessage?.({data:t.data})}}send(e){this.native.send(e)}close(e,t){this.native.close(e,t)}};const g=()=>({create:e=>new h(e)});function _(e={}){let{connectionPooling:t,useLocalStorage:n=!0}=e,i=m(),a=f(),s=r(t),l=u(),h=g(),_=o(),v=c(),y=d();return{platform:p(),storage:i,idGeneration:a,httpClient:s,fileReader:l,websocket:h,abortController:_,checksumService:v,fingerprintService:y}}function v(r){let i=_({connectionPooling:r.connectionPooling});return n({...r,webSocketFactory:i.websocket,abortControllerFactory:i.abortController,platformService:i.platform,httpClient:i.httpClient,fileReader:i.fileReader,generateId:i.idGeneration,fingerprintService:i.fingerprintService,checksumService:i.checksumService,logger:t(!1,()=>{}),clientStorage:e(i.storage)})}function y(e){let t=e.length,n=e.filter(e=>e.status===`success`).length,r=e.filter(e=>e.status===`error`).length,i=e.reduce((e,t)=>e+t.totalBytes,0),a=e.reduce((e,t)=>e+t.bytesUploaded,0);return{totalFiles:t,completedFiles:n,failedFiles:r,totalBytes:i,uploadedBytes:a,totalProgress:i>0?a/i*100:0,allComplete:e.every(e=>e.status===`success`),hasErrors:e.some(e=>e.status===`error`)}}function b(e){if(e===0)return`0 Bytes`;let t=1024,n=[`Bytes`,`KB`,`MB`,`GB`,`TB`],r=Math.floor(Math.log(e)/Math.log(t));return`${Number.parseFloat((e/t**r).toFixed(2))} ${n[r]}`}function x(e){return`${Math.round(e)}%`}function S(e){let t=e.lastIndexOf(`.`);return t===-1?``:e.slice(t+1).toLowerCase()}function C(e){return e.type.startsWith(`image/`)}function w(e){return e.type.startsWith(`video/`)}function T(e){return t=>t.size>e?{valid:!1,error:`File size exceeds maximum of ${b(e)}`}:{valid:!0}}function E(e){return t=>{let n=t.type.toLowerCase(),r=S(t.name);return e.some(e=>{if(e.startsWith(`.`))return e.slice(1)===r;if(e.includes(`*`)){let t=e.replace(`*`,``);return n.startsWith(t)}return n===e})?{valid:!0}:{valid:!1,error:`File type not allowed. Allowed types: ${e.join(`, `)}`}}}function D(...e){return t=>{for(let n of e){let e=n(t);if(!e.valid)return e}return{valid:!0}}}function O(){return`upload-${Date.now()}-${Math.random().toString(36).substr(2,9)}`}function k(e){return new Promise(t=>setTimeout(t,e))}function A(e,t=1e3,n=3e4){return Math.min(t*2**e,n)+Math.random()*1e3}function j(e,t=3,n=()=>!0){return async()=>{let r;for(let i=0;i<t;i++)try{return await e()}catch(e){if(r=e,i<t-1&&n(e)){await k(A(i));continue}break}throw r}}function M(e){return e instanceof Error?e.message.includes(`network`)||e.message.includes(`timeout`)||e.message.includes(`connection`)||e.message.includes(`ECONNREFUSED`)||e.message.includes(`ETIMEDOUT`):!1}function N(e){return e instanceof Error?e.name===`AbortError`||e.message.includes(`abort`):!1}function P(e){if(e===0)return`0 B/s`;let t=1024,n=[`B/s`,`KB/s`,`MB/s`,`GB/s`],r=Math.floor(Math.log(e)/Math.log(t));return`${parseFloat((e/t**r).toFixed(1))} ${n[r]}`}function F(e){if(e<1e3)return`${Math.round(e)}ms`;if(e<6e4)return`${Math.round(e/1e3)}s`;if(e<36e5){let t=Math.floor(e/6e4),n=Math.round(e%6e4/1e3);return n>0?`${t}m ${n}s`:`${t}m`}let t=Math.floor(e/36e5),n=Math.round(e%36e5/6e4);return n>0?`${t}h ${n}m`:`${t}h`}function I(e,t){return!t||t.length===0?!0:t.some(t=>{if(t.startsWith(`.`))return e.name.toLowerCase().endsWith(t.toLowerCase());if(t.endsWith(`/*`)){let n=t.slice(0,-2);return e.type.startsWith(n)}return e.type===t})}function L(e){return e.type.startsWith(`audio/`)}function R(e){return[`application/pdf`,`application/msword`,`application/vnd.openxmlformats-officedocument.wordprocessingml.document`,`application/vnd.ms-excel`,`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`,`application/vnd.ms-powerpoint`,`application/vnd.openxmlformats-officedocument.presentationml.presentation`,`text/plain`,`text/csv`,`application/rtf`].includes(e.type)}function z(e){return C(e)||w(e)||L(e)?URL.createObjectURL(e):null}function B(e){URL.revokeObjectURL(e)}function V(e,t){return t===0?0:Math.min(100,Math.max(0,Math.round(e/t*100)))}export{A as calculateBackoff,y as calculateMultiUploadStats,V as calculateProgress,D as composeValidators,u as createBrowserFileReaderService,z as createFilePreview,T as createFileSizeValidator,E as createFileTypeValidator,r as createHttpClient,j as createRetryWrapper,v as createUploadistaClient,k as delay,F as formatDuration,b as formatFileSize,x as formatProgress,P as formatSpeed,O as generateUploadId,S as getFileExtension,N as isAbortError,L as isAudioFile,R as isDocumentFile,C as isImageFile,M as isNetworkError,w as isVideoFile,B as revokeFilePreview,I as validateFileType};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["fetchOptions: RequestInit","issues: string[]","recommendations: string[]","status: \"healthy\" | \"degraded\" | \"poor\"","results: Record<string, string>","createUploadistaClientCore","lastError: unknown","minutes"],"sources":["../src/http-client.ts","../src/services/abort-controller-factory.ts","../src/utils/hash-util.ts","../src/services/checksum-service.ts","../src/services/file-reader.ts","../src/services/fingerprint-service.ts","../src/services/id-generation/id-generation.ts","../src/services/platform-service.ts","../src/services/storage/local-storage-service.ts","../src/services/websocket-factory.ts","../src/services/create-browser-services.ts","../src/client/create-uploadista-client.ts","../src/framework-utils.ts"],"sourcesContent":["import type {\n ConnectionHealth,\n ConnectionMetrics,\n ConnectionPoolConfig,\n DetailedConnectionMetrics,\n Http2Info,\n HttpClient,\n HttpRequestOptions,\n HttpResponse,\n} from \"@uploadista/client-core\";\n\n/**\n * Creates a browser-optimized HTTP client using the Fetch API.\n *\n * This factory function returns an HttpClient implementation that uses the browser's\n * native fetch() API with connection keep-alive headers for optimal performance.\n * The client automatically manages connection pooling, tracks metrics, and provides\n * connection health monitoring.\n *\n * @param config - Optional connection pooling configuration\n * @returns A configured HTTP client ready for making requests\n *\n * @example\n * ```typescript\n * import { createHttpClient } from '@uploadista/client-browser';\n *\n * // Basic usage with defaults\n * const client = createHttpClient();\n *\n * // With custom configuration\n * const client = createHttpClient({\n * maxConnectionsPerHost: 10,\n * connectionTimeout: 60000,\n * keepAliveTimeout: 120000,\n * enableHttp2: true,\n * retryOnConnectionError: true\n * });\n *\n * // Make a request\n * const response = await client.request('https://api.example.com/data', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ key: 'value' })\n * });\n *\n * // Check connection health\n * const metrics = client.getDetailedMetrics();\n * console.log('Connection health:', metrics.health.status);\n * ```\n *\n * @see {@link BrowserHttpClient} for implementation details\n */\nexport function createHttpClient(config?: ConnectionPoolConfig): HttpClient {\n return new BrowserHttpClient(config);\n}\n\n/**\n * Browser-optimized HTTP client implementation using the Fetch API with connection keep-alive.\n *\n * This class implements the HttpClient interface and provides:\n * - Connection pooling via keep-alive headers\n * - Connection metrics tracking (reuse rate, latency, error rates)\n * - HTTP/2 multiplexing detection and support\n * - Connection health monitoring with actionable recommendations\n * - Connection warmup capabilities\n * - Automatic timeout handling\n *\n * The browser manages actual connection pooling, but this client optimizes for\n * reuse through proper HTTP headers and provides visibility into connection performance.\n *\n * @example\n * ```typescript\n * const client = new BrowserHttpClient({\n * maxConnectionsPerHost: 6,\n * keepAliveTimeout: 60000,\n * enableHttp2: true\n * });\n *\n * // Monitor connection health\n * setInterval(() => {\n * const health = client.getDetailedMetrics().health;\n * if (health.status === 'poor') {\n * console.warn('Connection issues:', health.issues);\n * console.log('Recommendations:', health.recommendations);\n * }\n * }, 30000);\n * ```\n */\nclass BrowserHttpClient implements HttpClient {\n private config: Required<ConnectionPoolConfig>;\n private metrics: ConnectionMetrics;\n private connectionTimes: number[] = [];\n private requestCount = 0;\n private connectionCount = 0;\n private errorCount = 0;\n private timeoutCount = 0;\n private retryCount = 0;\n private startTime = Date.now();\n private http2Info: Http2Info;\n\n /**\n * Creates a new browser HTTP client instance.\n *\n * @param config - Connection pooling configuration with optional overrides\n */\n constructor(config: ConnectionPoolConfig = {}) {\n this.config = {\n maxConnectionsPerHost: config.maxConnectionsPerHost ?? 6,\n connectionTimeout: config.connectionTimeout ?? 30000,\n keepAliveTimeout: config.keepAliveTimeout ?? 60000,\n enableHttp2: config.enableHttp2 ?? true,\n retryOnConnectionError: config.retryOnConnectionError ?? true,\n };\n\n this.metrics = {\n activeConnections: 0,\n totalConnections: 0,\n reuseRate: 0,\n averageConnectionTime: 0,\n };\n\n // Initialize HTTP/2 detection\n this.http2Info = this.detectHttp2Support();\n }\n\n /**\n * Detects HTTP/2 support in the current browser environment.\n *\n * This method uses feature detection to determine if the browser supports\n * HTTP/2 features like multiplexing. In browsers, we can't directly query\n * the protocol version, so we infer support based on modern stream APIs.\n *\n * @returns HTTP/2 information with support status and detected features\n * @private\n */\n private detectHttp2Support(): Http2Info {\n // Check if the browser supports HTTP/2\n const supported = \"serviceWorker\" in navigator && \"fetch\" in window;\n\n // In browsers, we can't directly detect HTTP/2 protocol version\n // but we can make educated guesses based on browser features\n const hasModernFeatures =\n \"ReadableStream\" in window &&\n \"WritableStream\" in window &&\n \"TransformStream\" in window;\n\n return {\n supported,\n detected: false, // Will be updated during actual requests\n version: hasModernFeatures ? \"h2\" : \"h1.1\",\n multiplexingActive: hasModernFeatures && this.config.enableHttp2,\n };\n }\n\n /**\n * Makes an HTTP request using the Fetch API with optimized connection settings.\n *\n * This method automatically adds keep-alive headers for connection reuse,\n * handles timeouts via AbortController, and tracks connection metrics.\n *\n * @param url - The URL to request\n * @param options - Request options including method, headers, body, credentials, signal, and timeout\n * @returns Promise resolving to the HTTP response\n *\n * @throws {Error} When the request fails or times out\n *\n * @example\n * ```typescript\n * // Simple GET request\n * const response = await client.request('https://api.example.com/data');\n * const data = await response.json();\n *\n * // POST with timeout\n * const response = await client.request('https://api.example.com/upload', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ file: 'data' }),\n * timeout: 10000, // 10 seconds\n * signal: abortController.signal\n * });\n * ```\n */\n async request(\n url: string,\n options: HttpRequestOptions = {},\n ): Promise<HttpResponse> {\n this.requestCount++;\n\n // Create optimized fetch options for connection reuse\n const fetchOptions: RequestInit = {\n method: options.method || \"GET\",\n headers: {\n // Add connection keep-alive headers for better reuse\n Connection: \"keep-alive\",\n \"Keep-Alive\": `timeout=${this.config.keepAliveTimeout / 1000}`,\n ...options.headers,\n },\n body: options.body as BodyInit | null | undefined,\n credentials: options.credentials || \"include\",\n signal: options.signal as AbortSignal | undefined,\n };\n\n // Add timeout if specified\n if (options.timeout) {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), options.timeout);\n\n if (options.signal) {\n options.signal.addEventListener(\"abort\", () => controller.abort());\n }\n\n fetchOptions.signal = controller.signal;\n\n try {\n const response = await this.makeRequest(url, fetchOptions);\n clearTimeout(timeoutId);\n return response;\n } catch (error) {\n clearTimeout(timeoutId);\n throw error;\n }\n }\n\n return this.makeRequest(url, fetchOptions);\n }\n\n /**\n * Internal method to execute the fetch request and track metrics.\n *\n * @param url - The URL to request\n * @param options - Native fetch RequestInit options\n * @returns Promise resolving to the HTTP response\n * @throws {Error} When the fetch fails\n * @private\n */\n private async makeRequest(\n url: string,\n options: RequestInit,\n ): Promise<HttpResponse> {\n const startTime = Date.now();\n\n try {\n const response = await fetch(url, options);\n const connectionTime = Date.now() - startTime;\n\n this.recordConnectionMetrics(connectionTime);\n\n return {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n ok: response.ok,\n json: () => response.json(),\n text: () => response.text(),\n arrayBuffer: () => response.arrayBuffer(),\n };\n } catch (error) {\n // Record failed connection attempt\n this.connectionCount++;\n throw error;\n }\n }\n\n /**\n * Records connection timing metrics and updates statistics.\n *\n * Tracks connection times, calculates reuse rates based on latency patterns,\n * and maintains a rolling window of the last 100 measurements.\n *\n * @param connectionTime - Time in milliseconds for this connection\n * @private\n */\n private recordConnectionMetrics(connectionTime: number): void {\n this.connectionTimes.push(connectionTime);\n this.connectionCount++;\n\n // Keep only last 100 measurements for average calculation\n if (this.connectionTimes.length > 100) {\n this.connectionTimes.shift();\n }\n\n // Update metrics\n this.metrics.totalConnections = this.connectionCount;\n this.metrics.averageConnectionTime =\n this.connectionTimes.reduce((sum, time) => sum + time, 0) /\n this.connectionTimes.length;\n\n // Estimate reuse rate based on connection time patterns\n // Faster connections are likely reused connections\n const fastConnections = this.connectionTimes.filter(\n (time) => time < 100,\n ).length;\n this.metrics.reuseRate = fastConnections / this.connectionTimes.length;\n }\n\n /**\n * Retrieves basic connection metrics.\n *\n * @returns Current connection metrics including total connections, reuse rate, and average connection time\n *\n * @example\n * ```typescript\n * const metrics = client.getMetrics();\n * console.log(`Reuse rate: ${Math.round(metrics.reuseRate * 100)}%`);\n * console.log(`Avg connection time: ${Math.round(metrics.averageConnectionTime)}ms`);\n * ```\n */\n getMetrics(): ConnectionMetrics {\n return { ...this.metrics };\n }\n\n /**\n * Retrieves detailed connection metrics with health assessment.\n *\n * Provides comprehensive metrics including error rates, request throughput,\n * connection health status with score, identified issues, and actionable\n * recommendations for improving connection performance.\n *\n * @returns Detailed metrics with health information, request rates, and HTTP/2 info\n *\n * @example\n * ```typescript\n * const detailed = client.getDetailedMetrics();\n *\n * // Check health\n * if (detailed.health.status === 'degraded') {\n * console.warn('Issues:', detailed.health.issues);\n * console.log('Try:', detailed.health.recommendations);\n * }\n *\n * // Monitor performance\n * console.log(`Requests/sec: ${detailed.requestsPerSecond.toFixed(2)}`);\n * console.log(`Error rate: ${(detailed.errorRate * 100).toFixed(1)}%`);\n * console.log(`Fast connections: ${detailed.fastConnections}/${detailed.fastConnections + detailed.slowConnections}`);\n * ```\n */\n getDetailedMetrics(): DetailedConnectionMetrics {\n const health = this.calculateConnectionHealth();\n const elapsed = (Date.now() - this.startTime) / 1000; // seconds\n const requestsPerSecond = elapsed > 0 ? this.requestCount / elapsed : 0;\n const errorRate =\n this.requestCount > 0 ? this.errorCount / this.requestCount : 0;\n\n const fastConnections = this.connectionTimes.filter(\n (time) => time < 100,\n ).length;\n const slowConnections = this.connectionTimes.length - fastConnections;\n\n return {\n ...this.metrics,\n health,\n requestsPerSecond,\n errorRate,\n timeouts: this.timeoutCount,\n retries: this.retryCount,\n fastConnections,\n slowConnections,\n http2Info: this.http2Info,\n };\n }\n\n /**\n * Calculates connection health status based on current metrics.\n *\n * Analyzes reuse rates, error rates, and connection latency to determine\n * overall health (healthy/degraded/poor), assigns a health score (0-100),\n * and provides specific issues and recommendations.\n *\n * @returns Health assessment with status, score, issues, and recommendations\n * @private\n */\n private calculateConnectionHealth(): ConnectionHealth {\n const issues: string[] = [];\n const recommendations: string[] = [];\n let score = 100;\n\n // Check reuse rate\n if (this.metrics.reuseRate < 0.3) {\n issues.push(\"Low connection reuse rate\");\n recommendations.push(\"Check if keep-alive headers are working\");\n score -= 30;\n } else if (this.metrics.reuseRate < 0.7) {\n issues.push(\"Moderate connection reuse rate\");\n recommendations.push(\"Consider adjusting keep-alive timeout\");\n score -= 15;\n }\n\n // Check error rate\n const errorRate =\n this.requestCount > 0 ? this.errorCount / this.requestCount : 0;\n if (errorRate > 0.1) {\n issues.push(\"High error rate\");\n recommendations.push(\"Check network stability and server configuration\");\n score -= 25;\n } else if (errorRate > 0.05) {\n issues.push(\"Moderate error rate\");\n recommendations.push(\"Monitor network conditions\");\n score -= 10;\n }\n\n // Check average connection time\n if (this.metrics.averageConnectionTime > 1000) {\n issues.push(\"Slow connection establishment\");\n recommendations.push(\"Check network latency and DNS resolution\");\n score -= 20;\n } else if (this.metrics.averageConnectionTime > 500) {\n issues.push(\"Moderate connection latency\");\n recommendations.push(\"Consider connection warming\");\n score -= 10;\n }\n\n // Determine status\n let status: \"healthy\" | \"degraded\" | \"poor\";\n if (score >= 80) {\n status = \"healthy\";\n } else if (score >= 60) {\n status = \"degraded\";\n } else {\n status = \"poor\";\n }\n\n return {\n status,\n score: Math.max(0, score),\n issues,\n recommendations,\n };\n }\n\n /**\n * Warms up HTTP connections to specified URLs by making lightweight HEAD requests.\n *\n * This is useful for establishing connections before actual data transfer,\n * reducing latency for subsequent requests. Particularly beneficial when\n * uploading to multiple endpoints or when connection setup time is critical.\n *\n * @param urls - Array of URLs to warm up connections to\n *\n * @example\n * ```typescript\n * // Warm up connections before uploading\n * await client.warmupConnections([\n * 'https://upload1.example.com',\n * 'https://upload2.example.com',\n * 'https://cdn.example.com'\n * ]);\n *\n * // Now actual uploads will use pre-warmed connections\n * await client.request('https://upload1.example.com/file', {\n * method: 'PUT',\n * body: fileData\n * });\n * ```\n */\n async warmupConnections(urls: string[]): Promise<void> {\n if (urls.length === 0) return;\n\n console.log(`Warming up connections to ${urls.length} hosts...`);\n\n // Make lightweight HEAD requests to warm up connections\n const warmupPromises = urls.map(async (url) => {\n try {\n await this.request(url, {\n method: \"HEAD\",\n timeout: 5000, // 5 second timeout for warmup\n });\n } catch (error) {\n // Ignore warmup failures - they're optional\n console.warn(`Connection warmup failed for ${url}:`, error);\n }\n });\n\n // Wait for all warmup requests (with timeout)\n await Promise.allSettled(warmupPromises);\n console.log(\"Connection warmup completed\");\n }\n\n /**\n * Resets all connection metrics and statistics to initial state.\n *\n * Useful for clearing metrics after a long-running session or when\n * you want to start fresh measurement without creating a new client instance.\n *\n * @example\n * ```typescript\n * // Reset metrics after a batch of uploads\n * client.reset();\n *\n * // Start fresh measurements\n * const metrics = client.getMetrics(); // All counters back to zero\n * ```\n */\n reset(): void {\n this.connectionTimes = [];\n this.requestCount = 0;\n this.connectionCount = 0;\n this.errorCount = 0;\n this.timeoutCount = 0;\n this.retryCount = 0;\n this.startTime = Date.now();\n this.metrics = {\n activeConnections: 0,\n totalConnections: 0,\n reuseRate: 0,\n averageConnectionTime: 0,\n };\n this.http2Info = this.detectHttp2Support();\n }\n\n /**\n * Gracefully shuts down the HTTP client and logs final statistics.\n *\n * In browser environments, connections are managed by the browser and cannot\n * be explicitly closed. This method waits briefly for pending requests to complete,\n * logs final connection metrics, and resets internal state.\n *\n * @example\n * ```typescript\n * // Clean shutdown with metrics logging\n * await client.close();\n * // Logs: \"Total requests: 150, Connection reuse: 85%, Avg time: 45ms, Health: healthy\"\n * ```\n */\n async close(): Promise<void> {\n console.log(\"Gracefully shutting down HTTP client...\");\n\n // In browser environment, we can't explicitly close connections\n // The browser manages connection pooling automatically\n // But we can clean up our internal state\n\n // Wait a short time for any pending requests to complete\n await new Promise((resolve) => setTimeout(resolve, 100));\n\n // Log final statistics\n const finalMetrics = this.getDetailedMetrics();\n console.log(\"Final connection metrics:\", {\n totalRequests: this.requestCount,\n connectionReuse: `${Math.round(finalMetrics.reuseRate * 100)}%`,\n averageConnectionTime: `${Math.round(finalMetrics.averageConnectionTime)}ms`,\n health: finalMetrics.health.status,\n });\n\n this.reset();\n console.log(\"HTTP client shutdown complete\");\n }\n}\n","import type {\n AbortControllerFactory,\n AbortControllerLike,\n AbortSignalLike,\n} from \"@uploadista/client-core\";\n\n/**\n * Browser implementation of AbortController that wraps the native AbortController API.\n *\n * This class provides a minimal wrapper around the browser's native AbortController\n * to ensure compatibility with the Uploadista client's AbortControllerLike interface.\n * It's used for canceling uploads and HTTP requests.\n *\n * @example\n * ```typescript\n * const controller = new BrowserAbortController();\n *\n * // Start an operation with the signal\n * fetch('https://api.example.com/upload', {\n * signal: controller.signal,\n * method: 'POST',\n * body: formData\n * });\n *\n * // Cancel the operation\n * controller.abort('User canceled');\n * ```\n */\nclass BrowserAbortController implements AbortControllerLike {\n private native: AbortController;\n\n /**\n * Creates a new BrowserAbortController instance.\n *\n * Initializes a new native AbortController from the browser's API.\n */\n constructor() {\n this.native = new AbortController();\n }\n\n /**\n * Gets the AbortSignal associated with this controller.\n *\n * This signal is passed to abortable operations (like fetch or upload)\n * and will be triggered when abort() is called.\n *\n * @returns The abort signal that operations can listen to\n */\n get signal(): AbortSignalLike {\n return this.native.signal;\n }\n\n /**\n * Aborts the operation associated with this controller.\n *\n * When called, this will trigger the abort signal, causing any operations\n * listening to it (such as fetch requests or file uploads) to be canceled.\n *\n * @param reason - Optional reason for the abort, which will be available on the AbortSignal\n *\n * @example\n * ```typescript\n * const controller = new BrowserAbortController();\n *\n * // Start upload\n * const upload = client.upload(file, { signal: controller.signal });\n *\n * // Cancel upload after 5 seconds\n * setTimeout(() => {\n * controller.abort('Timeout exceeded');\n * }, 5000);\n * ```\n */\n abort(reason?: unknown): void {\n this.native.abort(reason);\n }\n}\n\n/**\n * Creates a factory for browser AbortController instances.\n *\n * This factory is used by the Uploadista client to create AbortController\n * instances for canceling uploads and HTTP requests. It wraps the browser's\n * native AbortController API.\n *\n * @returns An AbortControllerFactory that creates browser-compatible abort controllers\n *\n * @example\n * ```typescript\n * import { createBrowserAbortControllerFactory } from '@uploadista/client-browser';\n *\n * const factory = createBrowserAbortControllerFactory();\n *\n * // Create a controller\n * const controller = factory.create();\n *\n * // Use it to cancel operations\n * const upload = client.upload(file, {\n * signal: controller.signal\n * });\n *\n * // Later, cancel the upload\n * controller.abort();\n * ```\n */\nexport const createBrowserAbortControllerFactory =\n (): AbortControllerFactory => ({\n create: (): AbortControllerLike => new BrowserAbortController(),\n });\n","/**\n * Computes the SHA-256 checksum of a Blob using the Web Crypto API.\n *\n * This utility function provides a browser-native way to compute cryptographic\n * hashes of file data. It uses the SubtleCrypto API (part of Web Crypto) which\n * provides hardware-accelerated cryptographic operations when available.\n *\n * The SHA-256 algorithm produces a 256-bit (32-byte) hash value, typically\n * rendered as a 64-character hexadecimal string. SHA-256 is widely used for:\n * - File integrity verification\n * - Content deduplication\n * - File fingerprinting\n * - Checksum validation\n *\n * **Performance note:** For large files (>100MB), this function loads the entire\n * file into memory before hashing. For extremely large files, consider chunked\n * hashing approaches if memory is a concern.\n *\n * @param blob - The Blob or File to hash\n * @returns Promise resolving to the hex-encoded SHA-256 hash\n *\n * @throws {Error} When the hash computation fails (e.g., out of memory, crypto API unavailable)\n *\n * @example\n * ```typescript\n * import { computeblobSha256 } from '@uploadista/client-browser';\n *\n * // Hash a File from input\n * const fileInput = document.querySelector('input[type=\"file\"]');\n * const file = fileInput.files[0];\n * const hash = await computeblobSha256(file);\n * console.log('File SHA-256:', hash);\n * // Output: \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"\n *\n * // Hash a Blob\n * const blob = new Blob(['Hello, World!'], { type: 'text/plain' });\n * const hash = await computeblobSha256(blob);\n * console.log('Blob SHA-256:', hash);\n *\n * // Verify file integrity\n * const expectedHash = 'abc123...';\n * const actualHash = await computeblobSha256(file);\n * if (actualHash === expectedHash) {\n * console.log('File integrity verified');\n * } else {\n * console.error('File has been modified or corrupted');\n * }\n *\n * // Check for duplicate files\n * const file1Hash = await computeblobSha256(file1);\n * const file2Hash = await computeblobSha256(file2);\n * if (file1Hash === file2Hash) {\n * console.log('Files are identical (same content)');\n * }\n * ```\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest} for SubtleCrypto.digest API\n */\nexport async function computeblobSha256(blob: Blob): Promise<string> {\n try {\n // Read blob as ArrayBuffer\n const arrayBuffer = await blob.arrayBuffer();\n\n // Compute SHA-256 hash using Web Crypto API\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", arrayBuffer);\n\n // Convert hash to hex string\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return hashHex;\n } catch (error) {\n throw new Error(\n `Failed to compute file checksum: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n );\n }\n}\n","import type { ChecksumService } from \"@uploadista/client-core\";\nimport { computeblobSha256 } from \"../utils/hash-util\";\n\n/**\n * Creates a checksum service for verifying data integrity using SHA-256 hashing.\n *\n * This service uses the browser's Web Crypto API to compute SHA-256 checksums\n * of data chunks during upload. Checksums ensure that uploaded data hasn't been\n * corrupted in transit and matches what was sent.\n *\n * The service is optimized for browser environments and leverages native crypto\n * APIs for performance.\n *\n * @returns A ChecksumService that computes SHA-256 hashes for data chunks\n *\n * @example\n * ```typescript\n * import { createChecksumService } from '@uploadista/client-browser';\n *\n * const checksumService = createChecksumService();\n *\n * // Compute checksum for a data chunk\n * const data = new Uint8Array([1, 2, 3, 4, 5]);\n * const checksum = await checksumService.computeChecksum(data);\n * console.log('SHA-256 checksum:', checksum);\n * // Output: \"74f81fe167d99b4cb41d6d0ccda82278caee9f3e2f25d5e5a3936ff3dcec60d0\"\n * ```\n *\n * @see {@link computeblobSha256} for the underlying hash implementation\n */\nexport function createChecksumService(): ChecksumService {\n return {\n /**\n * Computes a SHA-256 checksum for the provided data.\n *\n * Converts the Uint8Array to a Blob and uses the Web Crypto API\n * to calculate its SHA-256 hash, returned as a hex string.\n *\n * @param data - The data to checksum\n * @returns Promise resolving to the hex-encoded SHA-256 checksum\n */\n computeChecksum: async (data: Uint8Array<ArrayBuffer>) => {\n return computeblobSha256(new Blob([data]));\n },\n };\n}\n","import type {\n FileSource as CoreFileSource,\n FileReaderService,\n SliceResult,\n} from \"@uploadista/client-core\";\nimport type { BrowserUploadInput } from \"../types/upload-input\";\n\n/**\n * Browser-specific file reader interface for opening and reading file data.\n *\n * Provides methods to open File/Blob objects and create FileSource instances\n * that support chunked reading for upload operations.\n */\nexport type FileReader = {\n /**\n * Opens a file and prepares it for chunked reading.\n *\n * @param input - The File or Blob to open\n * @param chunkSize - Size of chunks to read (in bytes)\n * @returns Promise resolving to a FileSource for reading the file\n */\n openFile: (\n input: BrowserUploadInput,\n chunkSize: number,\n ) => Promise<FileSource>;\n};\n\n/**\n * Represents an opened file that can be read in chunks.\n *\n * This interface provides the core functionality for reading file data\n * in a streaming fashion, which is essential for uploading large files\n * without loading them entirely into memory.\n */\nexport type FileSource = {\n /** The original input File or Blob */\n input: BrowserUploadInput;\n\n /** Total size of the file in bytes, or null if unknown */\n size: number | null;\n\n /**\n * Reads a slice of data from the file.\n *\n * @param start - Starting byte offset\n * @param end - Ending byte offset (exclusive)\n * @returns Promise resolving to the slice result with data and metadata\n */\n slice: (start: number, end: number) => Promise<SliceResult>;\n\n /**\n * Closes the file and releases any resources.\n *\n * For browser File/Blob objects, this is typically a no-op as there are\n * no resources to release, but is included for interface compatibility.\n */\n close: () => void;\n};\n\n/**\n * Re-export SliceResult from core for convenience.\n */\nexport type { SliceResult };\n\n/**\n * Creates a FileSource from a Blob or File object.\n *\n * This function wraps a Blob (or File, which extends Blob) and provides\n * a slice() method for reading specific byte ranges. It uses the Blob.slice()\n * and arrayBuffer() APIs to efficiently read file chunks without loading\n * the entire file into memory.\n *\n * @param blob - The Blob or File to wrap\n * @returns A FileSource that can read chunks from the blob\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'test.txt');\n * const source = blobFileSource(file);\n *\n * // Read first 1024 bytes\n * const chunk = await source.slice(0, 1024);\n * console.log('Chunk size:', chunk.size);\n * console.log('Is complete:', chunk.done);\n * ```\n */\nfunction blobFileSource(blob: Blob): FileSource {\n return {\n input: blob,\n size: blob.size,\n slice: async (start: number, end: number) => {\n const value = blob.slice(start, end);\n const size = value.size;\n const done = end >= blob.size;\n\n return { value: new Uint8Array(await value.arrayBuffer()), size, done };\n },\n close: () => {},\n };\n}\n\n/**\n * Creates a browser-specific file reader service for the Uploadista client.\n *\n * This service provides the ability to open and read File/Blob objects from\n * the browser's File API. It converts browser-native file objects into a\n * format that can be chunked and uploaded efficiently.\n *\n * The service supports:\n * - File objects from `<input type=\"file\">` elements\n * - File objects from drag-and-drop events\n * - Blob objects created programmatically\n *\n * @returns A FileReaderService configured for browser environments\n *\n * @example\n * ```typescript\n * import { createBrowserFileReaderService } from '@uploadista/client-browser';\n *\n * const fileReader = createBrowserFileReaderService();\n *\n * // Open a file from input element\n * const input = document.querySelector('input[type=\"file\"]');\n * const file = input.files[0];\n * const source = await fileReader.openFile(file, 5 * 1024 * 1024); // 5MB chunks\n *\n * console.log('File name:', source.name);\n * console.log('File size:', source.size);\n * console.log('File type:', source.type);\n *\n * // Read first chunk\n * const chunk = await source.slice(0, 5 * 1024 * 1024);\n * console.log('Read', chunk.size, 'bytes');\n * ```\n *\n * @throws {Error} When the input is not a File or Blob\n */\nexport function createBrowserFileReaderService(): FileReaderService<BrowserUploadInput> {\n return {\n openFile: async (\n input: BrowserUploadInput,\n _chunkSize: number,\n ): Promise<CoreFileSource> => {\n // File is a subtype of Blob, so we can check for Blob here.\n if (input instanceof Blob) {\n const source = blobFileSource(input);\n return {\n input: source.input,\n size: source.size,\n slice: source.slice,\n close: source.close,\n name: source.input instanceof File ? source.input.name : null,\n type: source.input instanceof File ? source.input.type : null,\n lastModified:\n source.input instanceof File ? source.input.lastModified : null,\n };\n }\n\n throw new Error(\n \"source object may only be an instance of File, Blob in this environment\",\n );\n },\n };\n}\n","import type { FingerprintService } from \"@uploadista/client-core\";\nimport type { BrowserUploadInput } from \"../types/upload-input\";\nimport { computeblobSha256 } from \"../utils/hash-util\";\n\n/**\n * Creates a fingerprint service for generating unique file identifiers.\n *\n * This service computes SHA-256 fingerprints of files to uniquely identify them.\n * Fingerprints are used for:\n * - Detecting duplicate uploads\n * - Resuming interrupted uploads\n * - Verifying file integrity\n * - Implementing deduplication strategies\n *\n * The fingerprint is computed using the Web Crypto API and represents the\n * SHA-256 hash of the entire file content. Two identical files will always\n * produce the same fingerprint.\n *\n * @returns A FingerprintService that computes SHA-256 fingerprints for browser files\n *\n * @example\n * ```typescript\n * import { createFingerprintService } from '@uploadista/client-browser';\n *\n * const fingerprintService = createFingerprintService();\n *\n * // Generate fingerprint for a file\n * const fileInput = document.querySelector('input[type=\"file\"]');\n * const file = fileInput.files[0];\n *\n * const fingerprint = await fingerprintService.computeFingerprint(\n * file,\n * 'https://api.example.com/upload'\n * );\n *\n * console.log('File fingerprint:', fingerprint);\n * // Can be used to check if file was previously uploaded\n * ```\n *\n * @see {@link computeblobSha256} for the underlying hash implementation\n */\nexport function createFingerprintService(): FingerprintService<BrowserUploadInput> {\n return {\n /**\n * Computes a unique fingerprint for a file.\n *\n * Calculates the SHA-256 hash of the entire file content. The endpoint\n * parameter is currently unused but included for interface compatibility\n * with other platform implementations that might use it for salt or\n * endpoint-specific fingerprinting.\n *\n * @param file - The File or Blob to fingerprint\n * @param _endpoint - Upload endpoint (currently unused, reserved for future use)\n * @returns Promise resolving to the hex-encoded SHA-256 fingerprint\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'example.txt');\n * const fingerprint = await service.computeFingerprint(file, 'https://api.example.com');\n * // fingerprint: \"ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73\"\n * ```\n */\n computeFingerprint: async (file, _endpoint) => {\n return computeblobSha256(file);\n },\n };\n}\n","import type { IdGenerationService } from \"@uploadista/client-core\";\n\n/**\n * Creates a browser-specific ID generation service using the Web Crypto API.\n *\n * This service generates cryptographically secure random UUIDs (v4) using the\n * browser's native `crypto.randomUUID()` method. These UUIDs are used throughout\n * the Uploadista client for:\n * - Upload session identifiers\n * - Chunk identifiers\n * - Request correlation IDs\n * - Internal tracking\n *\n * The generated UUIDs conform to RFC 4122 version 4 (random) and provide\n * strong uniqueness guarantees suitable for distributed systems.\n *\n * Browser compatibility: Requires support for `crypto.randomUUID()` (available\n * in modern browsers). If you need to support older browsers, consider using a\n * polyfill.\n *\n * @returns An IdGenerationService that generates cryptographically secure UUIDs\n *\n * @example\n * ```typescript\n * import { createBrowserIdGenerationService } from '@uploadista/client-browser';\n *\n * const idService = createBrowserIdGenerationService();\n *\n * // Generate a unique ID\n * const id = idService.generate();\n * console.log('Generated ID:', id);\n * // Output: \"550e8400-e29b-41d4-a716-446655440000\" (example UUID v4)\n *\n * // Each call generates a new unique ID\n * const id2 = idService.generate();\n * console.log('Another ID:', id2);\n * // Output: \"7c9e6679-7425-40de-944b-e07fc1f90ae7\" (different UUID)\n * ```\n */\nexport function createBrowserIdGenerationService(): IdGenerationService {\n return {\n /**\n * Generates a cryptographically secure random UUID (v4).\n *\n * Uses the Web Crypto API's `crypto.randomUUID()` method to generate\n * a UUID conforming to RFC 4122 version 4. Each UUID is statistically\n * unique and suitable for use as an identifier in distributed systems.\n *\n * @returns A UUID v4 string in the format \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\"\n *\n * @example\n * ```typescript\n * const service = createBrowserIdGenerationService();\n * const uploadId = service.generate();\n * console.log(uploadId); // \"f47ac10b-58cc-4372-a567-0e02b2c3d479\"\n * ```\n */\n generate: () => crypto.randomUUID(),\n };\n}\n","import type { PlatformService, Timeout } from \"@uploadista/client-core\";\n\n/**\n * Creates a browser-specific platform service that provides environment capabilities.\n *\n * This service abstracts platform-specific functionality and provides a consistent\n * interface for the Uploadista client to interact with browser APIs. It handles:\n * - Timer management (setTimeout/clearTimeout)\n * - Environment detection (browser vs. Node.js)\n * - Network connectivity status\n * - File object detection and metadata extraction\n *\n * This abstraction allows the core upload logic to remain platform-agnostic while\n * still accessing browser-specific features when needed.\n *\n * @returns A PlatformService configured for browser environments\n *\n * @example\n * ```typescript\n * import { createBrowserPlatformService } from '@uploadista/client-browser';\n *\n * const platform = createBrowserPlatformService();\n *\n * // Check if running in browser\n * console.log('Is browser:', platform.isBrowser()); // true\n *\n * // Check network status\n * console.log('Is online:', platform.isOnline()); // true or false\n *\n * // Extract file metadata\n * const fileInput = document.querySelector('input[type=\"file\"]');\n * const file = fileInput.files[0];\n * console.log('File name:', platform.getFileName(file));\n * console.log('File type:', platform.getFileType(file));\n * console.log('File size:', platform.getFileSize(file));\n * ```\n */\nexport function createBrowserPlatformService(): PlatformService {\n return {\n /**\n * Schedules a function to be executed after a specified delay.\n *\n * Wraps the browser's native `setTimeout` function.\n *\n * @param callback - Function to execute after the delay\n * @param ms - Delay in milliseconds before executing the callback\n * @returns A timeout ID that can be passed to clearTimeout\n *\n * @example\n * ```typescript\n * const timeoutId = platform.setTimeout(() => {\n * console.log('Executed after 1 second');\n * }, 1000);\n * ```\n */\n setTimeout: (callback: () => void, ms: number | undefined) => {\n return globalThis.setTimeout(callback, ms);\n },\n\n /**\n * Cancels a timeout previously scheduled with setTimeout.\n *\n * Wraps the browser's native `clearTimeout` function.\n *\n * @param id - The timeout ID returned by setTimeout\n *\n * @example\n * ```typescript\n * const timeoutId = platform.setTimeout(() => { }, 1000);\n * platform.clearTimeout(timeoutId); // Cancel the timeout\n * ```\n */\n clearTimeout: (id: Timeout) => {\n globalThis.clearTimeout(id as number);\n },\n\n /**\n * Checks if the code is running in a browser environment.\n *\n * Detects browser by checking for the existence of the `window` object.\n * This is useful for conditional logic that should only run in browsers.\n *\n * @returns `true` if running in a browser, `false` otherwise\n *\n * @example\n * ```typescript\n * if (platform.isBrowser()) {\n * // Browser-specific code\n * console.log('Running in browser');\n * }\n * ```\n */\n isBrowser: () => {\n return typeof window !== \"undefined\";\n },\n\n /**\n * Checks if the browser is currently online.\n *\n * Uses the Navigator Online Status API (`navigator.onLine`) to determine\n * network connectivity. Note that this only indicates if the device has\n * a network connection, not if it can reach the internet.\n *\n * @returns `true` if online, `false` if offline, defaults to `true` if not in browser\n *\n * @example\n * ```typescript\n * if (platform.isOnline()) {\n * // Proceed with upload\n * await client.upload(file);\n * } else {\n * console.log('Waiting for network connection...');\n * }\n *\n * // Listen for online/offline events\n * window.addEventListener('online', () => {\n * console.log('Back online, resuming upload');\n * });\n * ```\n */\n isOnline: () => {\n if (typeof navigator !== \"undefined\") {\n return navigator.onLine;\n }\n return true;\n },\n\n /**\n * Checks if a value is a File object.\n *\n * Type guard to determine if an unknown value is a browser File object.\n * Useful for validating upload inputs and conditional file handling.\n *\n * @param value - The value to check\n * @returns `true` if the value is a File instance, `false` otherwise\n *\n * @example\n * ```typescript\n * const input = getUploadInput(); // unknown type\n *\n * if (platform.isFileLike(input)) {\n * // TypeScript knows input is a File here\n * console.log('Uploading file:', input.name);\n * }\n * ```\n */\n isFileLike: (value: unknown) => {\n return value instanceof File;\n },\n\n /**\n * Extracts the file name from a File object.\n *\n * @param file - The file to extract the name from\n * @returns The file name string, or `undefined` if not a File\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'document.pdf');\n * const name = platform.getFileName(file);\n * console.log(name); // \"document.pdf\"\n * ```\n */\n getFileName: (file: unknown) => {\n if (file instanceof File) {\n return file.name;\n }\n return undefined;\n },\n\n /**\n * Extracts the MIME type from a File object.\n *\n * @param file - The file to extract the type from\n * @returns The MIME type string, or `undefined` if not a File\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'image.png', { type: 'image/png' });\n * const type = platform.getFileType(file);\n * console.log(type); // \"image/png\"\n * ```\n */\n getFileType: (file: unknown) => {\n if (file instanceof File) {\n return file.type;\n }\n return undefined;\n },\n\n /**\n * Extracts the file size in bytes from a File object.\n *\n * @param file - The file to extract the size from\n * @returns The file size in bytes, or `undefined` if not a File\n *\n * @example\n * ```typescript\n * const file = new File(['Hello'], 'greeting.txt');\n * const size = platform.getFileSize(file);\n * console.log(size); // 5 (bytes)\n * ```\n */\n getFileSize: (file: unknown) => {\n if (file instanceof File) {\n return file.size;\n }\n return undefined;\n },\n\n /**\n * Extracts the last modified timestamp from a File object.\n *\n * @param file - The file to extract the timestamp from\n * @returns The last modified timestamp in milliseconds since epoch, or `undefined` if not a File\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'data.txt');\n * const lastModified = platform.getFileLastModified(file);\n * console.log(new Date(lastModified)); // Date object of when file was last modified\n * ```\n */\n getFileLastModified: (file: unknown) => {\n if (file instanceof File) {\n return file.lastModified;\n }\n return undefined;\n },\n };\n}\n","import type { StorageService } from \"@uploadista/client-core\";\n\n/**\n * Creates a browser storage service using localStorage for persistent data storage.\n *\n * This service provides a key-value storage interface backed by the browser's\n * localStorage API. Data stored with this service persists across browser sessions\n * and page reloads until explicitly deleted or cleared by the user.\n *\n * Use cases include:\n * - Persisting upload state for resumable uploads\n * - Caching file metadata and fingerprints\n * - Storing user preferences and settings\n * - Maintaining upload history\n *\n * **Important notes:**\n * - localStorage has a typical limit of 5-10MB per origin\n * - Data is stored as strings (objects are JSON-serialized)\n * - localStorage is synchronous but this service wraps it in Promises for consistency\n * - Data is scoped to the origin (protocol + domain + port)\n * - Users can clear localStorage through browser settings\n *\n * @returns A StorageService backed by browser localStorage\n *\n * @example\n * ```typescript\n * import { createLocalStorageService } from '@uploadista/client-browser';\n *\n * const storage = createLocalStorageService();\n *\n * // Store upload state\n * await storage.setItem('upload:123', JSON.stringify({\n * fileId: '123',\n * progress: 75,\n * uploadedChunks: [0, 1, 2]\n * }));\n *\n * // Retrieve upload state\n * const data = await storage.getItem('upload:123');\n * if (data) {\n * const state = JSON.parse(data);\n * console.log('Resuming upload at', state.progress, '%');\n * }\n *\n * // Find all uploads\n * const uploads = await storage.find('upload:');\n * console.log('Found', Object.keys(uploads).length, 'uploads');\n *\n * // Clean up completed upload\n * await storage.removeItem('upload:123');\n * ```\n *\n * @see {@link createSessionStorageService} for session-only storage\n */\nexport function createLocalStorageService(): StorageService {\n /**\n * Internal helper to find entries matching a prefix.\n *\n * Iterates through all localStorage keys and returns those that start\n * with the specified prefix along with their values.\n *\n * @param prefix - Key prefix to filter by\n * @returns Object mapping matching keys to their values\n * @private\n */\n const findEntries = (prefix: string): Record<string, string> => {\n const results: Record<string, string> = {};\n\n for (const key in localStorage) {\n if (key.startsWith(prefix)) {\n const item = localStorage.getItem(key);\n if (item) {\n results[key] = item;\n }\n }\n }\n\n return results;\n };\n\n return {\n /**\n * Retrieves a value from localStorage by key.\n *\n * @param key - The key to retrieve\n * @returns Promise resolving to the value, or null if the key doesn't exist\n *\n * @example\n * ```typescript\n * const value = await storage.getItem('user:preferences');\n * if (value) {\n * const prefs = JSON.parse(value);\n * }\n * ```\n */\n async getItem(key: string): Promise<string | null> {\n return localStorage.getItem(key);\n },\n\n /**\n * Stores a value in localStorage.\n *\n * If the key already exists, its value will be overwritten.\n * Values must be strings; use JSON.stringify() for objects.\n *\n * @param key - The key to store under\n * @param value - The string value to store\n *\n * @throws {QuotaExceededError} If localStorage quota is exceeded\n *\n * @example\n * ```typescript\n * // Store string\n * await storage.setItem('upload:status', 'completed');\n *\n * // Store object\n * await storage.setItem('upload:metadata', JSON.stringify({\n * name: 'file.txt',\n * size: 1024\n * }));\n * ```\n */\n async setItem(key: string, value: string): Promise<void> {\n localStorage.setItem(key, value);\n },\n\n /**\n * Removes a value from localStorage by key.\n *\n * If the key doesn't exist, this is a no-op (no error is thrown).\n *\n * @param key - The key to remove\n *\n * @example\n * ```typescript\n * // Clean up completed upload\n * await storage.removeItem('upload:123');\n * ```\n */\n async removeItem(key: string): Promise<void> {\n localStorage.removeItem(key);\n },\n\n /**\n * Retrieves all entries from localStorage.\n *\n * Returns an object mapping every key in localStorage to its value.\n * Use with caution as this can return a large amount of data.\n *\n * @returns Promise resolving to object with all key-value pairs\n *\n * @example\n * ```typescript\n * const all = await storage.findAll();\n * console.log('Total items in storage:', Object.keys(all).length);\n * ```\n */\n async findAll(): Promise<Record<string, string>> {\n return findEntries(\"\");\n },\n\n /**\n * Finds all entries with keys starting with a given prefix.\n *\n * Useful for querying related data or implementing namespacing.\n * For example, use \"upload:\" prefix to find all upload-related entries.\n *\n * @param prefix - The key prefix to search for\n * @returns Promise resolving to object with matching key-value pairs\n *\n * @example\n * ```typescript\n * // Find all uploads\n * const uploads = await storage.find('upload:');\n * for (const [key, value] of Object.entries(uploads)) {\n * console.log('Upload:', key, JSON.parse(value));\n * }\n *\n * // Find user preferences\n * const prefs = await storage.find('pref:');\n * ```\n */\n async find(prefix: string): Promise<Record<string, string>> {\n return findEntries(prefix);\n },\n };\n}\n","import type { WebSocketFactory, WebSocketLike } from \"@uploadista/client-core\";\n\n/**\n * Browser implementation of WebSocket that wraps the native WebSocket API.\n *\n * This class provides a minimal wrapper around the browser's native WebSocket\n * to ensure compatibility with the Uploadista client's WebSocketLike interface.\n * It's used for real-time communication features like:\n * - Upload progress streaming\n * - Flow execution status updates\n * - Real-time error notifications\n * - Live event feeds\n *\n * The wrapper preserves all WebSocket states and properly proxies all events\n * while maintaining the standard WebSocket lifecycle.\n *\n * @example\n * ```typescript\n * const ws = new BrowserWebSocket('wss://api.example.com/ws');\n *\n * ws.onopen = () => {\n * console.log('Connected');\n * ws.send('Hello server');\n * };\n *\n * ws.onmessage = (event) => {\n * console.log('Message:', event.data);\n * };\n *\n * ws.onerror = (event) => {\n * console.error('Error:', event.message);\n * };\n *\n * ws.onclose = (event) => {\n * console.log('Closed:', event.code, event.reason);\n * };\n * ```\n */\nclass BrowserWebSocket implements WebSocketLike {\n /** WebSocket is currently connecting (readyState = 0) */\n readonly CONNECTING = 0;\n /** WebSocket connection is open and ready (readyState = 1) */\n readonly OPEN = 1;\n /** WebSocket is closing (readyState = 2) */\n readonly CLOSING = 2;\n /** WebSocket connection is closed (readyState = 3) */\n readonly CLOSED = 3;\n\n /**\n * Current state of the WebSocket connection.\n *\n * Possible values:\n * - 0 (CONNECTING): Connection is being established\n * - 1 (OPEN): Connection is open and ready for communication\n * - 2 (CLOSING): Connection is closing\n * - 3 (CLOSED): Connection is closed or couldn't be opened\n */\n readyState: number;\n\n /** Event handler called when the connection is established */\n onopen: (() => void) | null = null;\n\n /** Event handler called when the connection is closed */\n onclose: ((event: { code: number; reason: string }) => void) | null = null;\n\n /** Event handler called when an error occurs */\n onerror: ((event: { message: string }) => void) | null = null;\n\n /** Event handler called when a message is received */\n onmessage: ((event: { data: string }) => void) | null = null;\n\n private native: WebSocket;\n\n /**\n * Creates a new BrowserWebSocket instance.\n *\n * Initializes a native WebSocket connection to the specified URL and sets up\n * event handler proxying to convert native events to the WebSocketLike format.\n *\n * @param url - WebSocket URL to connect to (must use ws:// or wss:// protocol)\n *\n * @example\n * ```typescript\n * const ws = new BrowserWebSocket('wss://api.example.com/upload/progress');\n * ```\n */\n constructor(url: string) {\n this.native = new WebSocket(url);\n this.readyState = this.native.readyState;\n\n // Proxy event handlers to convert native events to WebSocketLike format\n this.native.onopen = () => {\n this.readyState = this.native.readyState;\n this.onopen?.();\n };\n\n this.native.onclose = (event) => {\n this.readyState = this.native.readyState;\n const closeEvent = event as CloseEvent;\n this.onclose?.({ code: closeEvent.code, reason: closeEvent.reason });\n };\n\n this.native.onerror = (_event) => {\n this.onerror?.({ message: \"WebSocket error\" });\n };\n\n this.native.onmessage = (event) => {\n const messageEvent = event as MessageEvent;\n this.onmessage?.({ data: messageEvent.data });\n };\n }\n\n /**\n * Sends data through the WebSocket connection.\n *\n * The data can be either a string (text message) or a Uint8Array (binary message).\n * The connection must be in the OPEN state before sending data.\n *\n * @param data - String or binary data to send\n *\n * @throws {Error} If the connection is not open\n *\n * @example\n * ```typescript\n * // Send text message\n * ws.send('{\"type\": \"subscribe\", \"channel\": \"uploads\"}');\n *\n * // Send binary data\n * const buffer = new Uint8Array([1, 2, 3, 4]);\n * ws.send(buffer);\n * ```\n */\n send(data: string | Uint8Array): void {\n this.native.send(data);\n }\n\n /**\n * Closes the WebSocket connection.\n *\n * Optionally accepts a close code and reason that will be sent to the server.\n * Standard close codes include:\n * - 1000: Normal closure\n * - 1001: Going away (e.g., page navigation)\n * - 1002: Protocol error\n * - 1003: Unsupported data\n *\n * @param code - Optional close code (default: 1000 for normal closure)\n * @param reason - Optional human-readable reason for closing\n *\n * @example\n * ```typescript\n * // Normal close\n * ws.close();\n *\n * // Close with reason\n * ws.close(1000, 'Upload completed');\n *\n * // Close due to error\n * ws.close(1011, 'Internal error during upload');\n * ```\n */\n close(code?: number, reason?: string): void {\n this.native.close(code, reason);\n }\n}\n\n/**\n * Creates a factory for browser WebSocket connections.\n *\n * This factory is used by the Uploadista client to create WebSocket connections\n * for real-time features. It wraps the browser's native WebSocket API and provides\n * a consistent interface for the client.\n *\n * The factory creates WebSockets that support:\n * - Real-time upload progress updates\n * - Flow execution status streaming\n * - Live error and event notifications\n * - Bidirectional client-server communication\n *\n * @returns A WebSocketFactory that creates browser-compatible WebSocket connections\n *\n * @example\n * ```typescript\n * import { createBrowserWebSocketFactory } from '@uploadista/client-browser';\n *\n * const factory = createBrowserWebSocketFactory();\n *\n * // Create a WebSocket connection\n * const ws = factory.create('wss://api.example.com/ws/upload/123');\n *\n * // Set up event handlers\n * ws.onmessage = (event) => {\n * const data = JSON.parse(event.data);\n * if (data.type === 'progress') {\n * console.log('Upload progress:', data.progress);\n * }\n * };\n *\n * ws.onopen = () => {\n * console.log('WebSocket connected');\n * };\n *\n * ws.onclose = (event) => {\n * console.log('WebSocket closed:', event.code, event.reason);\n * };\n * ```\n *\n * @see {@link BrowserWebSocket} for the WebSocket implementation details\n */\nexport const createBrowserWebSocketFactory = (): WebSocketFactory => ({\n create: (url: string): WebSocketLike => new BrowserWebSocket(url),\n});\n","import type {\n ConnectionPoolConfig,\n ServiceContainer,\n} from \"@uploadista/client-core\";\nimport { createHttpClient } from \"../http-client\";\nimport type { BrowserUploadInput } from \"../types/upload-input\";\nimport { createBrowserAbortControllerFactory } from \"./abort-controller-factory\";\nimport { createChecksumService } from \"./checksum-service\";\nimport { createBrowserFileReaderService } from \"./file-reader\";\nimport { createFingerprintService } from \"./fingerprint-service\";\nimport { createBrowserIdGenerationService } from \"./id-generation/id-generation\";\nimport { createBrowserPlatformService } from \"./platform-service\";\nimport { createLocalStorageService } from \"./storage/local-storage-service\";\nimport { createBrowserWebSocketFactory } from \"./websocket-factory\";\n\nexport interface BrowserServiceOptions {\n /**\n * HTTP client configuration for connection pooling\n */\n connectionPooling?: ConnectionPoolConfig;\n\n /**\n * Whether to use localStorage for persistence\n * If false, uses in-memory storage\n * @default true\n */\n useLocalStorage?: boolean;\n}\n\n/**\n * Creates a service container with browser-specific implementations\n * of all required services for the upload client\n *\n * @param options - Configuration options for browser services\n * @returns ServiceContainer with browser implementations\n *\n * @example\n * ```typescript\n * import { createBrowserServices } from '@uploadista/browser/services';\n *\n * const services = createBrowserServices({\n * useLocalStorage: true,\n * connectionPooling: {\n * maxConnectionsPerHost: 6,\n * connectionTimeout: 30000,\n * }\n * });\n * ```\n */\nexport function createBrowserServices(\n options: BrowserServiceOptions = {},\n): ServiceContainer<BrowserUploadInput> {\n const { connectionPooling, useLocalStorage = true } = options;\n\n // Create storage service (localStorage or in-memory fallback)\n const storage = useLocalStorage\n ? createLocalStorageService()\n : // Placeholder for in-memory storage - use localStorage as default for browser\n createLocalStorageService();\n\n // Create other services\n const idGeneration = createBrowserIdGenerationService();\n const httpClient = createHttpClient(connectionPooling);\n const fileReader = createBrowserFileReaderService();\n const websocket = createBrowserWebSocketFactory();\n const abortController = createBrowserAbortControllerFactory();\n const checksumService = createChecksumService();\n const fingerprintService = createFingerprintService();\n\n return {\n platform: createBrowserPlatformService(),\n storage,\n idGeneration,\n httpClient,\n fileReader,\n websocket,\n abortController,\n checksumService,\n fingerprintService,\n };\n}\n","import {\n type ConnectionPoolConfig,\n createClientStorage,\n createLogger,\n createUploadistaClient as createUploadistaClientCore,\n type UploadistaClientOptions as UploadistaClientOptionsCore,\n} from \"@uploadista/client-core\";\nimport { createBrowserServices } from \"../services/create-browser-services\";\nimport type { BrowserUploadInput } from \"../types/upload-input\";\n\n/**\n * Configuration options for creating a browser-specific Uploadista client.\n *\n * This interface extends the core client options but omits browser-specific\n * services that are automatically provided by the browser environment.\n * These services include WebSocket factory, AbortController, ID generation,\n * storage, logging, platform detection, fingerprinting, HTTP client, file reader,\n * and checksum calculation.\n *\n * @example\n * ```typescript\n * import { createUploadistaClient } from '@uploadista/client-browser';\n *\n * const client = createUploadistaClient({\n * endpoint: 'https://api.uploadista.com/upload',\n * connectionPooling: {\n * maxConnectionsPerHost: 6,\n * enableHttp2: true\n * }\n * });\n * ```\n */\nexport interface UploadistaClientOptions\n extends Omit<\n UploadistaClientOptionsCore<BrowserUploadInput>,\n | \"webSocketFactory\"\n | \"abortControllerFactory\"\n | \"generateId\"\n | \"clientStorage\"\n | \"logger\"\n | \"platformService\"\n | \"fingerprintService\"\n | \"httpClient\"\n | \"fileReader\"\n | \"checksumService\"\n > {\n /**\n * Connection pooling configuration for the HTTP client.\n *\n * Controls how the browser manages HTTP connections for optimal performance.\n * The browser's native fetch API with keep-alive headers is used under the hood.\n *\n * @default\n * ```typescript\n * {\n * maxConnectionsPerHost: 6,\n * connectionTimeout: 30000,\n * keepAliveTimeout: 60000,\n * enableHttp2: true,\n * retryOnConnectionError: true\n * }\n * ```\n *\n * @example\n * ```typescript\n * connectionPooling: {\n * maxConnectionsPerHost: 10,\n * enableHttp2: true,\n * keepAliveTimeout: 120000\n * }\n * ```\n */\n connectionPooling?: ConnectionPoolConfig;\n}\n\n/**\n * Creates a browser-optimized Uploadista client for file uploads and flow processing.\n *\n * This factory function automatically configures all browser-specific services including:\n * - Fetch-based HTTP client with connection pooling\n * - Native WebSocket support for real-time progress\n * - localStorage for upload state persistence\n * - Web Crypto API for checksums and fingerprints\n * - File API for reading and chunking files\n * - Browser platform detection and capabilities\n *\n * The created client can handle File and Blob objects from file inputs, drag-and-drop,\n * or programmatically created content. It supports resumable uploads, progress tracking,\n * and flow-based file processing.\n *\n * @param options - Configuration options for the browser client\n * @returns A fully configured Uploadista client ready for browser use\n *\n * @example\n * ```typescript\n * import { createUploadistaClient } from '@uploadista/client-browser';\n *\n * // Basic usage\n * const client = createUploadistaClient({\n * endpoint: 'https://api.uploadista.com/upload'\n * });\n *\n * // With custom configuration\n * const client = createUploadistaClient({\n * endpoint: 'https://api.uploadista.com/upload',\n * connectionPooling: {\n * maxConnectionsPerHost: 6,\n * enableHttp2: true,\n * keepAliveTimeout: 60000\n * },\n * chunkSize: 5 * 1024 * 1024, // 5MB chunks\n * retryDelays: [1000, 3000, 5000],\n * allowedMetaFields: ['userId', 'projectId']\n * });\n *\n * // Upload a file\n * const fileInput = document.querySelector('input[type=\"file\"]');\n * const file = fileInput.files[0];\n *\n * const upload = await client.upload(file, {\n * onProgress: (event) => {\n * console.log(`Progress: ${event.progress}%`);\n * }\n * });\n *\n * console.log('Upload complete:', upload.id);\n * ```\n *\n * @see {@link UploadistaClientOptions} for available configuration options\n * @see {@link BrowserUploadInput} for supported file input types\n */\nexport function createUploadistaClient(options: UploadistaClientOptions) {\n const services = createBrowserServices({\n connectionPooling: options.connectionPooling,\n });\n\n return createUploadistaClientCore<BrowserUploadInput>({\n ...options,\n webSocketFactory: services.websocket,\n abortControllerFactory: services.abortController,\n platformService: services.platform,\n httpClient: services.httpClient,\n fileReader: services.fileReader,\n generateId: services.idGeneration,\n fingerprintService: services.fingerprintService,\n checksumService: services.checksumService,\n logger: createLogger(false, () => {}),\n clientStorage: createClientStorage(services.storage),\n });\n}\n","/**\n * Framework Integration Utilities\n *\n * This module provides TypeScript utilities and helper types for building\n * framework-specific wrappers around the Uploadista client.\n *\n * @module framework-utils\n */\n\nimport type { FlowResult, UploadResult } from \"@uploadista/client-core\";\nimport type { FlowEvent } from \"@uploadista/core/flow\";\nimport type { UploadEvent, UploadFile } from \"@uploadista/core/types\";\n\n/**\n * Base upload state that framework wrappers should implement\n */\nexport interface BaseUploadState {\n status: \"idle\" | \"uploading\" | \"success\" | \"error\" | \"aborted\";\n progress: number;\n bytesUploaded: number;\n totalBytes: number;\n error?: Error;\n result?: UploadResult<UploadFile>;\n}\n\n/**\n * Base flow upload state\n */\nexport interface BaseFlowUploadState extends BaseUploadState {\n jobId?: string;\n flowStatus?: \"pending\" | \"processing\" | \"completed\" | \"failed\";\n flowResult?: FlowResult<unknown>;\n}\n\n/**\n * Progress callback signature\n */\nexport type ProgressCallback = (\n uploadId: string,\n bytesUploaded: number,\n totalBytes: number,\n) => void;\n\n/**\n * Complete callback signature\n */\nexport type CompleteCallback = (uploadId: string, result: UploadResult) => void;\n\n/**\n * Error callback signature\n */\nexport type ErrorCallback = (uploadId: string, error: Error) => void;\n\n/**\n * Abort callback signature\n */\nexport type AbortCallback = (uploadId: string) => void;\n\n/**\n * Event handler signature for framework wrappers\n */\nexport type EventHandler<T = unknown> = (event: T) => void;\n\n/**\n * WebSocket event handler signature\n */\nexport type WebSocketEventHandler = (event: UploadEvent | FlowEvent) => void;\n\n/**\n * Framework state updater function signature\n * @template T - The state type\n */\nexport type StateUpdater<T> = (updater: (prevState: T) => T) => void;\n\n/**\n * Cleanup function returned by setup functions\n */\nexport type CleanupFunction = () => void;\n\n/**\n * Upload item for multi-upload tracking\n */\nexport interface UploadItem {\n id: string;\n file: File;\n status: BaseUploadState[\"status\"];\n progress: number;\n bytesUploaded: number;\n totalBytes: number;\n error?: Error;\n result?: UploadResult;\n}\n\n/**\n * Multi-upload aggregate statistics\n */\nexport interface MultiUploadStats {\n totalFiles: number;\n completedFiles: number;\n failedFiles: number;\n totalBytes: number;\n uploadedBytes: number;\n totalProgress: number;\n allComplete: boolean;\n hasErrors: boolean;\n}\n\n/**\n * Drag and drop state\n */\nexport interface DragDropState {\n isDragging: boolean;\n isOver: boolean;\n files: File[];\n}\n\n/**\n * File validation result\n */\nexport interface FileValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * File validation function signature\n */\nexport type FileValidator = (file: File) => FileValidationResult;\n\n/**\n * Utility: Calculate aggregate upload statistics\n */\nexport function calculateMultiUploadStats(\n uploads: UploadItem[],\n): MultiUploadStats {\n const totalFiles = uploads.length;\n const completedFiles = uploads.filter((u) => u.status === \"success\").length;\n const failedFiles = uploads.filter((u) => u.status === \"error\").length;\n const totalBytes = uploads.reduce((sum, u) => sum + u.totalBytes, 0);\n const uploadedBytes = uploads.reduce((sum, u) => sum + u.bytesUploaded, 0);\n const totalProgress = totalBytes > 0 ? (uploadedBytes / totalBytes) * 100 : 0;\n const allComplete = uploads.every((u) => u.status === \"success\");\n const hasErrors = uploads.some((u) => u.status === \"error\");\n\n return {\n totalFiles,\n completedFiles,\n failedFiles,\n totalBytes,\n uploadedBytes,\n totalProgress,\n allComplete,\n hasErrors,\n };\n}\n\n/**\n * Utility: Format file size for display\n */\nexport function formatFileSize(bytes: number): string {\n if (bytes === 0) return \"0 Bytes\";\n\n const k = 1024;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;\n}\n\n/**\n * Utility: Format progress percentage\n */\nexport function formatProgress(progress: number): string {\n return `${Math.round(progress)}%`;\n}\n\n/**\n * Utility: Get file extension\n */\nexport function getFileExtension(filename: string): string {\n const lastDot = filename.lastIndexOf(\".\");\n return lastDot === -1 ? \"\" : filename.slice(lastDot + 1).toLowerCase();\n}\n\n/**\n * Utility: Check if file is an image\n */\nexport function isImageFile(file: File): boolean {\n return file.type.startsWith(\"image/\");\n}\n\n/**\n * Utility: Check if file is a video\n */\nexport function isVideoFile(file: File): boolean {\n return file.type.startsWith(\"video/\");\n}\n\n/**\n * Utility: Create file size validator\n */\nexport function createFileSizeValidator(maxSizeBytes: number): FileValidator {\n return (file: File): FileValidationResult => {\n if (file.size > maxSizeBytes) {\n return {\n valid: false,\n error: `File size exceeds maximum of ${formatFileSize(maxSizeBytes)}`,\n };\n }\n return { valid: true };\n };\n}\n\n/**\n * Utility: Create file type validator\n */\nexport function createFileTypeValidator(allowedTypes: string[]): FileValidator {\n return (file: File): FileValidationResult => {\n const fileType = file.type.toLowerCase();\n const fileExt = getFileExtension(file.name);\n\n const isAllowed = allowedTypes.some((type) => {\n if (type.startsWith(\".\")) {\n return type.slice(1) === fileExt;\n }\n if (type.includes(\"*\")) {\n const pattern = type.replace(\"*\", \"\");\n return fileType.startsWith(pattern);\n }\n return fileType === type;\n });\n\n if (!isAllowed) {\n return {\n valid: false,\n error: `File type not allowed. Allowed types: ${allowedTypes.join(\", \")}`,\n };\n }\n return { valid: true };\n };\n}\n\n/**\n * Utility: Compose multiple validators\n */\nexport function composeValidators(\n ...validators: FileValidator[]\n): FileValidator {\n return (file: File): FileValidationResult => {\n for (const validator of validators) {\n const result = validator(file);\n if (!result.valid) {\n return result;\n }\n }\n return { valid: true };\n };\n}\n\n/**\n * Utility: Generate unique upload ID\n */\nexport function generateUploadId(): string {\n return `upload-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n}\n\n/**\n * Utility: Create delay promise for retry logic\n */\nexport function delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Utility: Calculate exponential backoff delay\n */\nexport function calculateBackoff(\n attempt: number,\n baseDelay = 1000,\n maxDelay = 30000,\n): number {\n const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);\n // Add jitter to prevent thundering herd\n return delay + Math.random() * 1000;\n}\n\n/**\n * Utility: Create retry wrapper for upload function\n */\nexport function createRetryWrapper<T>(\n fn: () => Promise<T>,\n maxAttempts = 3,\n shouldRetry: (error: unknown) => boolean = () => true,\n): () => Promise<T> {\n return async () => {\n let lastError: unknown;\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n if (attempt < maxAttempts - 1 && shouldRetry(error)) {\n const delayMs = calculateBackoff(attempt);\n await delay(delayMs);\n continue;\n }\n break;\n }\n }\n throw lastError;\n };\n}\n\n/**\n * Type guard: Check if error is network-related (should retry)\n */\nexport function isNetworkError(error: unknown): boolean {\n if (error instanceof Error) {\n return (\n error.message.includes(\"network\") ||\n error.message.includes(\"timeout\") ||\n error.message.includes(\"connection\") ||\n error.message.includes(\"ECONNREFUSED\") ||\n error.message.includes(\"ETIMEDOUT\")\n );\n }\n return false;\n}\n\n/**\n * Type guard: Check if error is abort-related (should not retry)\n */\nexport function isAbortError(error: unknown): boolean {\n if (error instanceof Error) {\n return error.name === \"AbortError\" || error.message.includes(\"abort\");\n }\n return false;\n}\n\n/**\n * Format upload speed in human-readable format\n */\nexport function formatSpeed(bytesPerSecond: number): string {\n if (bytesPerSecond === 0) return \"0 B/s\";\n const k = 1024;\n const sizes = [\"B/s\", \"KB/s\", \"MB/s\", \"GB/s\"];\n const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));\n return `${parseFloat((bytesPerSecond / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\n/**\n * Format duration in human-readable format\n */\nexport function formatDuration(milliseconds: number): string {\n if (milliseconds < 1000) {\n return `${Math.round(milliseconds)}ms`;\n }\n\n if (milliseconds < 60000) {\n return `${Math.round(milliseconds / 1000)}s`;\n }\n\n if (milliseconds < 3600000) {\n const minutes = Math.floor(milliseconds / 60000);\n const seconds = Math.round((milliseconds % 60000) / 1000);\n return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;\n }\n\n const hours = Math.floor(milliseconds / 3600000);\n const minutes = Math.round((milliseconds % 3600000) / 60000);\n return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;\n}\n\n/**\n * Validate file type against accepted types\n */\nexport function validateFileType(file: File, accept: string[]): boolean {\n if (!accept || accept.length === 0) return true;\n\n return accept.some((acceptType) => {\n if (acceptType.startsWith(\".\")) {\n // File extension check\n return file.name.toLowerCase().endsWith(acceptType.toLowerCase());\n }\n\n // MIME type check (supports wildcards like image/*)\n if (acceptType.endsWith(\"/*\")) {\n const baseType = acceptType.slice(0, -2);\n return file.type.startsWith(baseType);\n }\n\n return file.type === acceptType;\n });\n}\n\n/**\n * Check if a file is an audio file\n */\nexport function isAudioFile(file: File): boolean {\n return file.type.startsWith(\"audio/\");\n}\n\n/**\n * Check if a file is a document\n */\nexport function isDocumentFile(file: File): boolean {\n const documentTypes = [\n \"application/pdf\",\n \"application/msword\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n \"application/vnd.ms-excel\",\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n \"application/vnd.ms-powerpoint\",\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n \"text/plain\",\n \"text/csv\",\n \"application/rtf\",\n ];\n\n return documentTypes.includes(file.type);\n}\n\n/**\n * Create a preview URL for a file (if supported)\n */\nexport function createFilePreview(file: File): string | null {\n if (isImageFile(file) || isVideoFile(file) || isAudioFile(file)) {\n return URL.createObjectURL(file);\n }\n return null;\n}\n\n/**\n * Clean up a preview URL created with createFilePreview\n */\nexport function revokeFilePreview(previewUrl: string): void {\n URL.revokeObjectURL(previewUrl);\n}\n\n/**\n * Calculate progress percentage\n */\nexport function calculateProgress(current: number, total: number): number {\n if (total === 0) return 0;\n return Math.min(100, Math.max(0, Math.round((current / total) * 100)));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["createUploadistaClientCore","minutes"],"sources":["../src/http-client.ts","../src/services/abort-controller-factory.ts","../src/utils/hash-util.ts","../src/services/checksum-service.ts","../src/services/file-reader.ts","../src/services/fingerprint-service.ts","../src/services/id-generation/id-generation.ts","../src/services/platform-service.ts","../src/services/storage/local-storage-service.ts","../src/services/websocket-factory.ts","../src/services/create-browser-services.ts","../src/client/create-uploadista-client.ts","../src/framework-utils.ts"],"sourcesContent":["import type {\n ConnectionHealth,\n ConnectionMetrics,\n ConnectionPoolConfig,\n DetailedConnectionMetrics,\n Http2Info,\n HttpClient,\n HttpRequestOptions,\n HttpResponse,\n} from \"@uploadista/client-core\";\n\n/**\n * Creates a browser-optimized HTTP client using the Fetch API.\n *\n * This factory function returns an HttpClient implementation that uses the browser's\n * native fetch() API with connection keep-alive headers for optimal performance.\n * The client automatically manages connection pooling, tracks metrics, and provides\n * connection health monitoring.\n *\n * @param config - Optional connection pooling configuration\n * @returns A configured HTTP client ready for making requests\n *\n * @example\n * ```typescript\n * import { createHttpClient } from '@uploadista/client-browser';\n *\n * // Basic usage with defaults\n * const client = createHttpClient();\n *\n * // With custom configuration\n * const client = createHttpClient({\n * maxConnectionsPerHost: 10,\n * connectionTimeout: 60000,\n * keepAliveTimeout: 120000,\n * enableHttp2: true,\n * retryOnConnectionError: true\n * });\n *\n * // Make a request\n * const response = await client.request('https://api.example.com/data', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ key: 'value' })\n * });\n *\n * // Check connection health\n * const metrics = client.getDetailedMetrics();\n * console.log('Connection health:', metrics.health.status);\n * ```\n *\n * @see {@link BrowserHttpClient} for implementation details\n */\nexport function createHttpClient(config?: ConnectionPoolConfig): HttpClient {\n return new BrowserHttpClient(config);\n}\n\n/**\n * Browser-optimized HTTP client implementation using the Fetch API with connection keep-alive.\n *\n * This class implements the HttpClient interface and provides:\n * - Connection pooling via keep-alive headers\n * - Connection metrics tracking (reuse rate, latency, error rates)\n * - HTTP/2 multiplexing detection and support\n * - Connection health monitoring with actionable recommendations\n * - Connection warmup capabilities\n * - Automatic timeout handling\n *\n * The browser manages actual connection pooling, but this client optimizes for\n * reuse through proper HTTP headers and provides visibility into connection performance.\n *\n * @example\n * ```typescript\n * const client = new BrowserHttpClient({\n * maxConnectionsPerHost: 6,\n * keepAliveTimeout: 60000,\n * enableHttp2: true\n * });\n *\n * // Monitor connection health\n * setInterval(() => {\n * const health = client.getDetailedMetrics().health;\n * if (health.status === 'poor') {\n * console.warn('Connection issues:', health.issues);\n * console.log('Recommendations:', health.recommendations);\n * }\n * }, 30000);\n * ```\n */\nclass BrowserHttpClient implements HttpClient {\n private config: Required<ConnectionPoolConfig>;\n private metrics: ConnectionMetrics;\n private connectionTimes: number[] = [];\n private requestCount = 0;\n private connectionCount = 0;\n private errorCount = 0;\n private timeoutCount = 0;\n private retryCount = 0;\n private startTime = Date.now();\n private http2Info: Http2Info;\n\n /**\n * Creates a new browser HTTP client instance.\n *\n * @param config - Connection pooling configuration with optional overrides\n */\n constructor(config: ConnectionPoolConfig = {}) {\n this.config = {\n maxConnectionsPerHost: config.maxConnectionsPerHost ?? 6,\n connectionTimeout: config.connectionTimeout ?? 30000,\n keepAliveTimeout: config.keepAliveTimeout ?? 60000,\n enableHttp2: config.enableHttp2 ?? true,\n retryOnConnectionError: config.retryOnConnectionError ?? true,\n };\n\n this.metrics = {\n activeConnections: 0,\n totalConnections: 0,\n reuseRate: 0,\n averageConnectionTime: 0,\n };\n\n // Initialize HTTP/2 detection\n this.http2Info = this.detectHttp2Support();\n }\n\n /**\n * Detects HTTP/2 support in the current browser environment.\n *\n * This method uses feature detection to determine if the browser supports\n * HTTP/2 features like multiplexing. In browsers, we can't directly query\n * the protocol version, so we infer support based on modern stream APIs.\n *\n * @returns HTTP/2 information with support status and detected features\n * @private\n */\n private detectHttp2Support(): Http2Info {\n // Check if we're in a browser environment\n if (typeof window === \"undefined\" || typeof navigator === \"undefined\") {\n // SSR/Node.js environment - return safe defaults\n return {\n supported: false,\n detected: false,\n version: \"h1.1\",\n multiplexingActive: false,\n };\n }\n\n // Check if the browser supports HTTP/2\n const supported = \"serviceWorker\" in navigator && \"fetch\" in window;\n\n // In browsers, we can't directly detect HTTP/2 protocol version\n // but we can make educated guesses based on browser features\n const hasModernFeatures =\n \"ReadableStream\" in window &&\n \"WritableStream\" in window &&\n \"TransformStream\" in window;\n\n return {\n supported,\n detected: false, // Will be updated during actual requests\n version: hasModernFeatures ? \"h2\" : \"h1.1\",\n multiplexingActive: hasModernFeatures && this.config.enableHttp2,\n };\n }\n\n /**\n * Makes an HTTP request using the Fetch API with optimized connection settings.\n *\n * This method automatically adds keep-alive headers for connection reuse,\n * handles timeouts via AbortController, and tracks connection metrics.\n *\n * @param url - The URL to request\n * @param options - Request options including method, headers, body, credentials, signal, and timeout\n * @returns Promise resolving to the HTTP response\n *\n * @throws {Error} When the request fails or times out\n *\n * @example\n * ```typescript\n * // Simple GET request\n * const response = await client.request('https://api.example.com/data');\n * const data = await response.json();\n *\n * // POST with timeout\n * const response = await client.request('https://api.example.com/upload', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ file: 'data' }),\n * timeout: 10000, // 10 seconds\n * signal: abortController.signal\n * });\n * ```\n */\n async request(\n url: string,\n options: HttpRequestOptions = {},\n ): Promise<HttpResponse> {\n this.requestCount++;\n\n // Create optimized fetch options for connection reuse\n const fetchOptions: RequestInit = {\n method: options.method || \"GET\",\n headers: {\n // Add connection keep-alive headers for better reuse\n Connection: \"keep-alive\",\n \"Keep-Alive\": `timeout=${this.config.keepAliveTimeout / 1000}`,\n ...options.headers,\n },\n body: options.body as BodyInit | null | undefined,\n credentials: options.credentials || \"include\",\n signal: options.signal as AbortSignal | undefined,\n };\n\n // Add timeout if specified\n if (options.timeout) {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), options.timeout);\n\n if (options.signal) {\n options.signal.addEventListener(\"abort\", () => controller.abort());\n }\n\n fetchOptions.signal = controller.signal;\n\n try {\n const response = await this.makeRequest(url, fetchOptions);\n clearTimeout(timeoutId);\n return response;\n } catch (error) {\n clearTimeout(timeoutId);\n throw error;\n }\n }\n\n return this.makeRequest(url, fetchOptions);\n }\n\n /**\n * Internal method to execute the fetch request and track metrics.\n *\n * @param url - The URL to request\n * @param options - Native fetch RequestInit options\n * @returns Promise resolving to the HTTP response\n * @throws {Error} When the fetch fails\n * @private\n */\n private async makeRequest(\n url: string,\n options: RequestInit,\n ): Promise<HttpResponse> {\n const startTime = Date.now();\n\n try {\n const response = await fetch(url, options);\n const connectionTime = Date.now() - startTime;\n\n this.recordConnectionMetrics(connectionTime);\n\n return {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n ok: response.ok,\n json: () => response.json(),\n text: () => response.text(),\n arrayBuffer: () => response.arrayBuffer(),\n };\n } catch (error) {\n // Record failed connection attempt\n this.connectionCount++;\n throw error;\n }\n }\n\n /**\n * Records connection timing metrics and updates statistics.\n *\n * Tracks connection times, calculates reuse rates based on latency patterns,\n * and maintains a rolling window of the last 100 measurements.\n *\n * @param connectionTime - Time in milliseconds for this connection\n * @private\n */\n private recordConnectionMetrics(connectionTime: number): void {\n this.connectionTimes.push(connectionTime);\n this.connectionCount++;\n\n // Keep only last 100 measurements for average calculation\n if (this.connectionTimes.length > 100) {\n this.connectionTimes.shift();\n }\n\n // Update metrics\n this.metrics.totalConnections = this.connectionCount;\n this.metrics.averageConnectionTime =\n this.connectionTimes.reduce((sum, time) => sum + time, 0) /\n this.connectionTimes.length;\n\n // Estimate reuse rate based on connection time patterns\n // Faster connections are likely reused connections\n const fastConnections = this.connectionTimes.filter(\n (time) => time < 100,\n ).length;\n this.metrics.reuseRate = fastConnections / this.connectionTimes.length;\n }\n\n /**\n * Retrieves basic connection metrics.\n *\n * @returns Current connection metrics including total connections, reuse rate, and average connection time\n *\n * @example\n * ```typescript\n * const metrics = client.getMetrics();\n * console.log(`Reuse rate: ${Math.round(metrics.reuseRate * 100)}%`);\n * console.log(`Avg connection time: ${Math.round(metrics.averageConnectionTime)}ms`);\n * ```\n */\n getMetrics(): ConnectionMetrics {\n return { ...this.metrics };\n }\n\n /**\n * Retrieves detailed connection metrics with health assessment.\n *\n * Provides comprehensive metrics including error rates, request throughput,\n * connection health status with score, identified issues, and actionable\n * recommendations for improving connection performance.\n *\n * @returns Detailed metrics with health information, request rates, and HTTP/2 info\n *\n * @example\n * ```typescript\n * const detailed = client.getDetailedMetrics();\n *\n * // Check health\n * if (detailed.health.status === 'degraded') {\n * console.warn('Issues:', detailed.health.issues);\n * console.log('Try:', detailed.health.recommendations);\n * }\n *\n * // Monitor performance\n * console.log(`Requests/sec: ${detailed.requestsPerSecond.toFixed(2)}`);\n * console.log(`Error rate: ${(detailed.errorRate * 100).toFixed(1)}%`);\n * console.log(`Fast connections: ${detailed.fastConnections}/${detailed.fastConnections + detailed.slowConnections}`);\n * ```\n */\n getDetailedMetrics(): DetailedConnectionMetrics {\n const health = this.calculateConnectionHealth();\n const elapsed = (Date.now() - this.startTime) / 1000; // seconds\n const requestsPerSecond = elapsed > 0 ? this.requestCount / elapsed : 0;\n const errorRate =\n this.requestCount > 0 ? this.errorCount / this.requestCount : 0;\n\n const fastConnections = this.connectionTimes.filter(\n (time) => time < 100,\n ).length;\n const slowConnections = this.connectionTimes.length - fastConnections;\n\n return {\n ...this.metrics,\n health,\n requestsPerSecond,\n errorRate,\n timeouts: this.timeoutCount,\n retries: this.retryCount,\n fastConnections,\n slowConnections,\n http2Info: this.http2Info,\n };\n }\n\n /**\n * Calculates connection health status based on current metrics.\n *\n * Analyzes reuse rates, error rates, and connection latency to determine\n * overall health (healthy/degraded/poor), assigns a health score (0-100),\n * and provides specific issues and recommendations.\n *\n * @returns Health assessment with status, score, issues, and recommendations\n * @private\n */\n private calculateConnectionHealth(): ConnectionHealth {\n const issues: string[] = [];\n const recommendations: string[] = [];\n let score = 100;\n\n // Check reuse rate\n if (this.metrics.reuseRate < 0.3) {\n issues.push(\"Low connection reuse rate\");\n recommendations.push(\"Check if keep-alive headers are working\");\n score -= 30;\n } else if (this.metrics.reuseRate < 0.7) {\n issues.push(\"Moderate connection reuse rate\");\n recommendations.push(\"Consider adjusting keep-alive timeout\");\n score -= 15;\n }\n\n // Check error rate\n const errorRate =\n this.requestCount > 0 ? this.errorCount / this.requestCount : 0;\n if (errorRate > 0.1) {\n issues.push(\"High error rate\");\n recommendations.push(\"Check network stability and server configuration\");\n score -= 25;\n } else if (errorRate > 0.05) {\n issues.push(\"Moderate error rate\");\n recommendations.push(\"Monitor network conditions\");\n score -= 10;\n }\n\n // Check average connection time\n if (this.metrics.averageConnectionTime > 1000) {\n issues.push(\"Slow connection establishment\");\n recommendations.push(\"Check network latency and DNS resolution\");\n score -= 20;\n } else if (this.metrics.averageConnectionTime > 500) {\n issues.push(\"Moderate connection latency\");\n recommendations.push(\"Consider connection warming\");\n score -= 10;\n }\n\n // Determine status\n let status: \"healthy\" | \"degraded\" | \"poor\";\n if (score >= 80) {\n status = \"healthy\";\n } else if (score >= 60) {\n status = \"degraded\";\n } else {\n status = \"poor\";\n }\n\n return {\n status,\n score: Math.max(0, score),\n issues,\n recommendations,\n };\n }\n\n /**\n * Warms up HTTP connections to specified URLs by making lightweight HEAD requests.\n *\n * This is useful for establishing connections before actual data transfer,\n * reducing latency for subsequent requests. Particularly beneficial when\n * uploading to multiple endpoints or when connection setup time is critical.\n *\n * @param urls - Array of URLs to warm up connections to\n *\n * @example\n * ```typescript\n * // Warm up connections before uploading\n * await client.warmupConnections([\n * 'https://upload1.example.com',\n * 'https://upload2.example.com',\n * 'https://cdn.example.com'\n * ]);\n *\n * // Now actual uploads will use pre-warmed connections\n * await client.request('https://upload1.example.com/file', {\n * method: 'PUT',\n * body: fileData\n * });\n * ```\n */\n async warmupConnections(urls: string[]): Promise<void> {\n if (urls.length === 0) return;\n\n console.log(`Warming up connections to ${urls.length} hosts...`);\n\n // Make lightweight HEAD requests to warm up connections\n const warmupPromises = urls.map(async (url) => {\n try {\n await this.request(url, {\n method: \"HEAD\",\n timeout: 5000, // 5 second timeout for warmup\n });\n } catch (error) {\n // Ignore warmup failures - they're optional\n console.warn(`Connection warmup failed for ${url}:`, error);\n }\n });\n\n // Wait for all warmup requests (with timeout)\n await Promise.allSettled(warmupPromises);\n console.log(\"Connection warmup completed\");\n }\n\n /**\n * Resets all connection metrics and statistics to initial state.\n *\n * Useful for clearing metrics after a long-running session or when\n * you want to start fresh measurement without creating a new client instance.\n *\n * @example\n * ```typescript\n * // Reset metrics after a batch of uploads\n * client.reset();\n *\n * // Start fresh measurements\n * const metrics = client.getMetrics(); // All counters back to zero\n * ```\n */\n reset(): void {\n this.connectionTimes = [];\n this.requestCount = 0;\n this.connectionCount = 0;\n this.errorCount = 0;\n this.timeoutCount = 0;\n this.retryCount = 0;\n this.startTime = Date.now();\n this.metrics = {\n activeConnections: 0,\n totalConnections: 0,\n reuseRate: 0,\n averageConnectionTime: 0,\n };\n this.http2Info = this.detectHttp2Support();\n }\n\n /**\n * Gracefully shuts down the HTTP client and logs final statistics.\n *\n * In browser environments, connections are managed by the browser and cannot\n * be explicitly closed. This method waits briefly for pending requests to complete,\n * logs final connection metrics, and resets internal state.\n *\n * @example\n * ```typescript\n * // Clean shutdown with metrics logging\n * await client.close();\n * // Logs: \"Total requests: 150, Connection reuse: 85%, Avg time: 45ms, Health: healthy\"\n * ```\n */\n async close(): Promise<void> {\n console.log(\"Gracefully shutting down HTTP client...\");\n\n // In browser environment, we can't explicitly close connections\n // The browser manages connection pooling automatically\n // But we can clean up our internal state\n\n // Wait a short time for any pending requests to complete\n await new Promise((resolve) => setTimeout(resolve, 100));\n\n // Log final statistics\n const finalMetrics = this.getDetailedMetrics();\n console.log(\"Final connection metrics:\", {\n totalRequests: this.requestCount,\n connectionReuse: `${Math.round(finalMetrics.reuseRate * 100)}%`,\n averageConnectionTime: `${Math.round(finalMetrics.averageConnectionTime)}ms`,\n health: finalMetrics.health.status,\n });\n\n this.reset();\n console.log(\"HTTP client shutdown complete\");\n }\n}\n","import type {\n AbortControllerFactory,\n AbortControllerLike,\n AbortSignalLike,\n} from \"@uploadista/client-core\";\n\n/**\n * Browser implementation of AbortController that wraps the native AbortController API.\n *\n * This class provides a minimal wrapper around the browser's native AbortController\n * to ensure compatibility with the Uploadista client's AbortControllerLike interface.\n * It's used for canceling uploads and HTTP requests.\n *\n * @example\n * ```typescript\n * const controller = new BrowserAbortController();\n *\n * // Start an operation with the signal\n * fetch('https://api.example.com/upload', {\n * signal: controller.signal,\n * method: 'POST',\n * body: formData\n * });\n *\n * // Cancel the operation\n * controller.abort('User canceled');\n * ```\n */\nclass BrowserAbortController implements AbortControllerLike {\n private native: AbortController;\n\n /**\n * Creates a new BrowserAbortController instance.\n *\n * Initializes a new native AbortController from the browser's API.\n */\n constructor() {\n this.native = new AbortController();\n }\n\n /**\n * Gets the AbortSignal associated with this controller.\n *\n * This signal is passed to abortable operations (like fetch or upload)\n * and will be triggered when abort() is called.\n *\n * @returns The abort signal that operations can listen to\n */\n get signal(): AbortSignalLike {\n return this.native.signal;\n }\n\n /**\n * Aborts the operation associated with this controller.\n *\n * When called, this will trigger the abort signal, causing any operations\n * listening to it (such as fetch requests or file uploads) to be canceled.\n *\n * @param reason - Optional reason for the abort, which will be available on the AbortSignal\n *\n * @example\n * ```typescript\n * const controller = new BrowserAbortController();\n *\n * // Start upload\n * const upload = client.upload(file, { signal: controller.signal });\n *\n * // Cancel upload after 5 seconds\n * setTimeout(() => {\n * controller.abort('Timeout exceeded');\n * }, 5000);\n * ```\n */\n abort(reason?: unknown): void {\n this.native.abort(reason);\n }\n}\n\n/**\n * Creates a factory for browser AbortController instances.\n *\n * This factory is used by the Uploadista client to create AbortController\n * instances for canceling uploads and HTTP requests. It wraps the browser's\n * native AbortController API.\n *\n * @returns An AbortControllerFactory that creates browser-compatible abort controllers\n *\n * @example\n * ```typescript\n * import { createBrowserAbortControllerFactory } from '@uploadista/client-browser';\n *\n * const factory = createBrowserAbortControllerFactory();\n *\n * // Create a controller\n * const controller = factory.create();\n *\n * // Use it to cancel operations\n * const upload = client.upload(file, {\n * signal: controller.signal\n * });\n *\n * // Later, cancel the upload\n * controller.abort();\n * ```\n */\nexport const createBrowserAbortControllerFactory =\n (): AbortControllerFactory => ({\n create: (): AbortControllerLike => new BrowserAbortController(),\n });\n","/**\n * Computes the SHA-256 checksum of a Blob using the Web Crypto API.\n *\n * This utility function provides a browser-native way to compute cryptographic\n * hashes of file data. It uses the SubtleCrypto API (part of Web Crypto) which\n * provides hardware-accelerated cryptographic operations when available.\n *\n * The SHA-256 algorithm produces a 256-bit (32-byte) hash value, typically\n * rendered as a 64-character hexadecimal string. SHA-256 is widely used for:\n * - File integrity verification\n * - Content deduplication\n * - File fingerprinting\n * - Checksum validation\n *\n * **Performance note:** For large files (>100MB), this function loads the entire\n * file into memory before hashing. For extremely large files, consider chunked\n * hashing approaches if memory is a concern.\n *\n * @param blob - The Blob or File to hash\n * @returns Promise resolving to the hex-encoded SHA-256 hash\n *\n * @throws {Error} When the hash computation fails (e.g., out of memory, crypto API unavailable)\n *\n * @example\n * ```typescript\n * import { computeblobSha256 } from '@uploadista/client-browser';\n *\n * // Hash a File from input\n * const fileInput = document.querySelector('input[type=\"file\"]');\n * const file = fileInput.files[0];\n * const hash = await computeblobSha256(file);\n * console.log('File SHA-256:', hash);\n * // Output: \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"\n *\n * // Hash a Blob\n * const blob = new Blob(['Hello, World!'], { type: 'text/plain' });\n * const hash = await computeblobSha256(blob);\n * console.log('Blob SHA-256:', hash);\n *\n * // Verify file integrity\n * const expectedHash = 'abc123...';\n * const actualHash = await computeblobSha256(file);\n * if (actualHash === expectedHash) {\n * console.log('File integrity verified');\n * } else {\n * console.error('File has been modified or corrupted');\n * }\n *\n * // Check for duplicate files\n * const file1Hash = await computeblobSha256(file1);\n * const file2Hash = await computeblobSha256(file2);\n * if (file1Hash === file2Hash) {\n * console.log('Files are identical (same content)');\n * }\n * ```\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest} for SubtleCrypto.digest API\n */\nexport async function computeblobSha256(blob: Blob): Promise<string> {\n try {\n // Read blob as ArrayBuffer\n const arrayBuffer = await blob.arrayBuffer();\n\n // Compute SHA-256 hash using Web Crypto API\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", arrayBuffer);\n\n // Convert hash to hex string\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return hashHex;\n } catch (error) {\n throw new Error(\n `Failed to compute file checksum: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n );\n }\n}\n","import type { ChecksumService } from \"@uploadista/client-core\";\nimport { computeblobSha256 } from \"../utils/hash-util\";\n\n/**\n * Creates a checksum service for verifying data integrity using SHA-256 hashing.\n *\n * This service uses the browser's Web Crypto API to compute SHA-256 checksums\n * of data chunks during upload. Checksums ensure that uploaded data hasn't been\n * corrupted in transit and matches what was sent.\n *\n * The service is optimized for browser environments and leverages native crypto\n * APIs for performance.\n *\n * @returns A ChecksumService that computes SHA-256 hashes for data chunks\n *\n * @example\n * ```typescript\n * import { createChecksumService } from '@uploadista/client-browser';\n *\n * const checksumService = createChecksumService();\n *\n * // Compute checksum for a data chunk\n * const data = new Uint8Array([1, 2, 3, 4, 5]);\n * const checksum = await checksumService.computeChecksum(data);\n * console.log('SHA-256 checksum:', checksum);\n * // Output: \"74f81fe167d99b4cb41d6d0ccda82278caee9f3e2f25d5e5a3936ff3dcec60d0\"\n * ```\n *\n * @see {@link computeblobSha256} for the underlying hash implementation\n */\nexport function createChecksumService(): ChecksumService {\n return {\n /**\n * Computes a SHA-256 checksum for the provided data.\n *\n * Converts the Uint8Array to a Blob and uses the Web Crypto API\n * to calculate its SHA-256 hash, returned as a hex string.\n *\n * @param data - The data to checksum\n * @returns Promise resolving to the hex-encoded SHA-256 checksum\n */\n computeChecksum: async (data: Uint8Array<ArrayBuffer>) => {\n return computeblobSha256(new Blob([data]));\n },\n };\n}\n","import type {\n FileSource as CoreFileSource,\n FileReaderService,\n SliceResult,\n} from \"@uploadista/client-core\";\nimport type { BrowserUploadInput } from \"../types/upload-input\";\n\n/**\n * Browser-specific file reader interface for opening and reading file data.\n *\n * Provides methods to open File/Blob objects and create FileSource instances\n * that support chunked reading for upload operations.\n */\nexport type FileReader = {\n /**\n * Opens a file and prepares it for chunked reading.\n *\n * @param input - The File or Blob to open\n * @param chunkSize - Size of chunks to read (in bytes)\n * @returns Promise resolving to a FileSource for reading the file\n */\n openFile: (\n input: BrowserUploadInput,\n chunkSize: number,\n ) => Promise<FileSource>;\n};\n\n/**\n * Represents an opened file that can be read in chunks.\n *\n * This interface provides the core functionality for reading file data\n * in a streaming fashion, which is essential for uploading large files\n * without loading them entirely into memory.\n */\nexport type FileSource = {\n /** The original input File or Blob */\n input: BrowserUploadInput;\n\n /** Total size of the file in bytes, or null if unknown */\n size: number | null;\n\n /**\n * Reads a slice of data from the file.\n *\n * @param start - Starting byte offset\n * @param end - Ending byte offset (exclusive)\n * @returns Promise resolving to the slice result with data and metadata\n */\n slice: (start: number, end: number) => Promise<SliceResult>;\n\n /**\n * Closes the file and releases any resources.\n *\n * For browser File/Blob objects, this is typically a no-op as there are\n * no resources to release, but is included for interface compatibility.\n */\n close: () => void;\n};\n\n/**\n * Re-export SliceResult from core for convenience.\n */\nexport type { SliceResult };\n\n/**\n * Creates a FileSource from a Blob or File object.\n *\n * This function wraps a Blob (or File, which extends Blob) and provides\n * a slice() method for reading specific byte ranges. It uses the Blob.slice()\n * and arrayBuffer() APIs to efficiently read file chunks without loading\n * the entire file into memory.\n *\n * @param blob - The Blob or File to wrap\n * @returns A FileSource that can read chunks from the blob\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'test.txt');\n * const source = blobFileSource(file);\n *\n * // Read first 1024 bytes\n * const chunk = await source.slice(0, 1024);\n * console.log('Chunk size:', chunk.size);\n * console.log('Is complete:', chunk.done);\n * ```\n */\nfunction blobFileSource(blob: Blob): FileSource {\n return {\n input: blob,\n size: blob.size,\n slice: async (start: number, end: number) => {\n const value = blob.slice(start, end);\n const size = value.size;\n const done = end >= blob.size;\n\n return { value: new Uint8Array(await value.arrayBuffer()), size, done };\n },\n close: () => {},\n };\n}\n\n/**\n * Creates a browser-specific file reader service for the Uploadista client.\n *\n * This service provides the ability to open and read File/Blob objects from\n * the browser's File API. It converts browser-native file objects into a\n * format that can be chunked and uploaded efficiently.\n *\n * The service supports:\n * - File objects from `<input type=\"file\">` elements\n * - File objects from drag-and-drop events\n * - Blob objects created programmatically\n *\n * @returns A FileReaderService configured for browser environments\n *\n * @example\n * ```typescript\n * import { createBrowserFileReaderService } from '@uploadista/client-browser';\n *\n * const fileReader = createBrowserFileReaderService();\n *\n * // Open a file from input element\n * const input = document.querySelector('input[type=\"file\"]');\n * const file = input.files[0];\n * const source = await fileReader.openFile(file, 5 * 1024 * 1024); // 5MB chunks\n *\n * console.log('File name:', source.name);\n * console.log('File size:', source.size);\n * console.log('File type:', source.type);\n *\n * // Read first chunk\n * const chunk = await source.slice(0, 5 * 1024 * 1024);\n * console.log('Read', chunk.size, 'bytes');\n * ```\n *\n * @throws {Error} When the input is not a File or Blob\n */\nexport function createBrowserFileReaderService(): FileReaderService<BrowserUploadInput> {\n return {\n openFile: async (\n input: BrowserUploadInput,\n _chunkSize: number,\n ): Promise<CoreFileSource> => {\n // File is a subtype of Blob, so we can check for Blob here.\n if (input instanceof Blob) {\n const source = blobFileSource(input);\n return {\n input: source.input,\n size: source.size,\n slice: source.slice,\n close: source.close,\n name: source.input instanceof File ? source.input.name : null,\n type: source.input instanceof File ? source.input.type : null,\n lastModified:\n source.input instanceof File ? source.input.lastModified : null,\n };\n }\n\n throw new Error(\n \"source object may only be an instance of File, Blob in this environment\",\n );\n },\n };\n}\n","import type { FingerprintService } from \"@uploadista/client-core\";\nimport type { BrowserUploadInput } from \"../types/upload-input\";\nimport { computeblobSha256 } from \"../utils/hash-util\";\n\n/**\n * Creates a fingerprint service for generating unique file identifiers.\n *\n * This service computes SHA-256 fingerprints of files to uniquely identify them.\n * Fingerprints are used for:\n * - Detecting duplicate uploads\n * - Resuming interrupted uploads\n * - Verifying file integrity\n * - Implementing deduplication strategies\n *\n * The fingerprint is computed using the Web Crypto API and represents the\n * SHA-256 hash of the entire file content. Two identical files will always\n * produce the same fingerprint.\n *\n * @returns A FingerprintService that computes SHA-256 fingerprints for browser files\n *\n * @example\n * ```typescript\n * import { createFingerprintService } from '@uploadista/client-browser';\n *\n * const fingerprintService = createFingerprintService();\n *\n * // Generate fingerprint for a file\n * const fileInput = document.querySelector('input[type=\"file\"]');\n * const file = fileInput.files[0];\n *\n * const fingerprint = await fingerprintService.computeFingerprint(\n * file,\n * 'https://api.example.com/upload'\n * );\n *\n * console.log('File fingerprint:', fingerprint);\n * // Can be used to check if file was previously uploaded\n * ```\n *\n * @see {@link computeblobSha256} for the underlying hash implementation\n */\nexport function createFingerprintService(): FingerprintService<BrowserUploadInput> {\n return {\n /**\n * Computes a unique fingerprint for a file.\n *\n * Calculates the SHA-256 hash of the entire file content. The endpoint\n * parameter is currently unused but included for interface compatibility\n * with other platform implementations that might use it for salt or\n * endpoint-specific fingerprinting.\n *\n * @param file - The File or Blob to fingerprint\n * @param _endpoint - Upload endpoint (currently unused, reserved for future use)\n * @returns Promise resolving to the hex-encoded SHA-256 fingerprint\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'example.txt');\n * const fingerprint = await service.computeFingerprint(file, 'https://api.example.com');\n * // fingerprint: \"ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73\"\n * ```\n */\n computeFingerprint: async (file, _endpoint) => {\n return computeblobSha256(file);\n },\n };\n}\n","import type { IdGenerationService } from \"@uploadista/client-core\";\n\n/**\n * Creates a browser-specific ID generation service using the Web Crypto API.\n *\n * This service generates cryptographically secure random UUIDs (v4) using the\n * browser's native `crypto.randomUUID()` method. These UUIDs are used throughout\n * the Uploadista client for:\n * - Upload session identifiers\n * - Chunk identifiers\n * - Request correlation IDs\n * - Internal tracking\n *\n * The generated UUIDs conform to RFC 4122 version 4 (random) and provide\n * strong uniqueness guarantees suitable for distributed systems.\n *\n * Browser compatibility: Requires support for `crypto.randomUUID()` (available\n * in modern browsers). If you need to support older browsers, consider using a\n * polyfill.\n *\n * @returns An IdGenerationService that generates cryptographically secure UUIDs\n *\n * @example\n * ```typescript\n * import { createBrowserIdGenerationService } from '@uploadista/client-browser';\n *\n * const idService = createBrowserIdGenerationService();\n *\n * // Generate a unique ID\n * const id = idService.generate();\n * console.log('Generated ID:', id);\n * // Output: \"550e8400-e29b-41d4-a716-446655440000\" (example UUID v4)\n *\n * // Each call generates a new unique ID\n * const id2 = idService.generate();\n * console.log('Another ID:', id2);\n * // Output: \"7c9e6679-7425-40de-944b-e07fc1f90ae7\" (different UUID)\n * ```\n */\nexport function createBrowserIdGenerationService(): IdGenerationService {\n return {\n /**\n * Generates a cryptographically secure random UUID (v4).\n *\n * Uses the Web Crypto API's `crypto.randomUUID()` method to generate\n * a UUID conforming to RFC 4122 version 4. Each UUID is statistically\n * unique and suitable for use as an identifier in distributed systems.\n *\n * @returns A UUID v4 string in the format \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\"\n *\n * @example\n * ```typescript\n * const service = createBrowserIdGenerationService();\n * const uploadId = service.generate();\n * console.log(uploadId); // \"f47ac10b-58cc-4372-a567-0e02b2c3d479\"\n * ```\n */\n generate: () => crypto.randomUUID(),\n };\n}\n","import type { PlatformService, Timeout } from \"@uploadista/client-core\";\n\n/**\n * Creates a browser-specific platform service that provides environment capabilities.\n *\n * This service abstracts platform-specific functionality and provides a consistent\n * interface for the Uploadista client to interact with browser APIs. It handles:\n * - Timer management (setTimeout/clearTimeout)\n * - Environment detection (browser vs. Node.js)\n * - Network connectivity status\n * - File object detection and metadata extraction\n *\n * This abstraction allows the core upload logic to remain platform-agnostic while\n * still accessing browser-specific features when needed.\n *\n * @returns A PlatformService configured for browser environments\n *\n * @example\n * ```typescript\n * import { createBrowserPlatformService } from '@uploadista/client-browser';\n *\n * const platform = createBrowserPlatformService();\n *\n * // Check if running in browser\n * console.log('Is browser:', platform.isBrowser()); // true\n *\n * // Check network status\n * console.log('Is online:', platform.isOnline()); // true or false\n *\n * // Extract file metadata\n * const fileInput = document.querySelector('input[type=\"file\"]');\n * const file = fileInput.files[0];\n * console.log('File name:', platform.getFileName(file));\n * console.log('File type:', platform.getFileType(file));\n * console.log('File size:', platform.getFileSize(file));\n * ```\n */\nexport function createBrowserPlatformService(): PlatformService {\n return {\n /**\n * Schedules a function to be executed after a specified delay.\n *\n * Wraps the browser's native `setTimeout` function.\n *\n * @param callback - Function to execute after the delay\n * @param ms - Delay in milliseconds before executing the callback\n * @returns A timeout ID that can be passed to clearTimeout\n *\n * @example\n * ```typescript\n * const timeoutId = platform.setTimeout(() => {\n * console.log('Executed after 1 second');\n * }, 1000);\n * ```\n */\n setTimeout: (callback: () => void, ms: number | undefined) => {\n return globalThis.setTimeout(callback, ms);\n },\n\n /**\n * Cancels a timeout previously scheduled with setTimeout.\n *\n * Wraps the browser's native `clearTimeout` function.\n *\n * @param id - The timeout ID returned by setTimeout\n *\n * @example\n * ```typescript\n * const timeoutId = platform.setTimeout(() => { }, 1000);\n * platform.clearTimeout(timeoutId); // Cancel the timeout\n * ```\n */\n clearTimeout: (id: Timeout) => {\n globalThis.clearTimeout(id as number);\n },\n\n /**\n * Checks if the code is running in a browser environment.\n *\n * Detects browser by checking for the existence of the `window` object.\n * This is useful for conditional logic that should only run in browsers.\n *\n * @returns `true` if running in a browser, `false` otherwise\n *\n * @example\n * ```typescript\n * if (platform.isBrowser()) {\n * // Browser-specific code\n * console.log('Running in browser');\n * }\n * ```\n */\n isBrowser: () => {\n return typeof window !== \"undefined\";\n },\n\n /**\n * Checks if the browser is currently online.\n *\n * Uses the Navigator Online Status API (`navigator.onLine`) to determine\n * network connectivity. Note that this only indicates if the device has\n * a network connection, not if it can reach the internet.\n *\n * @returns `true` if online, `false` if offline, defaults to `true` if not in browser\n *\n * @example\n * ```typescript\n * if (platform.isOnline()) {\n * // Proceed with upload\n * await client.upload(file);\n * } else {\n * console.log('Waiting for network connection...');\n * }\n *\n * // Listen for online/offline events\n * window.addEventListener('online', () => {\n * console.log('Back online, resuming upload');\n * });\n * ```\n */\n isOnline: () => {\n if (typeof navigator !== \"undefined\") {\n return navigator.onLine;\n }\n return true;\n },\n\n /**\n * Checks if a value is a File object.\n *\n * Type guard to determine if an unknown value is a browser File object.\n * Useful for validating upload inputs and conditional file handling.\n *\n * @param value - The value to check\n * @returns `true` if the value is a File instance, `false` otherwise\n *\n * @example\n * ```typescript\n * const input = getUploadInput(); // unknown type\n *\n * if (platform.isFileLike(input)) {\n * // TypeScript knows input is a File here\n * console.log('Uploading file:', input.name);\n * }\n * ```\n */\n isFileLike: (value: unknown) => {\n return value instanceof File;\n },\n\n /**\n * Extracts the file name from a File object.\n *\n * @param file - The file to extract the name from\n * @returns The file name string, or `undefined` if not a File\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'document.pdf');\n * const name = platform.getFileName(file);\n * console.log(name); // \"document.pdf\"\n * ```\n */\n getFileName: (file: unknown) => {\n if (file instanceof File) {\n return file.name;\n }\n return undefined;\n },\n\n /**\n * Extracts the MIME type from a File object.\n *\n * @param file - The file to extract the type from\n * @returns The MIME type string, or `undefined` if not a File\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'image.png', { type: 'image/png' });\n * const type = platform.getFileType(file);\n * console.log(type); // \"image/png\"\n * ```\n */\n getFileType: (file: unknown) => {\n if (file instanceof File) {\n return file.type;\n }\n return undefined;\n },\n\n /**\n * Extracts the file size in bytes from a File object.\n *\n * @param file - The file to extract the size from\n * @returns The file size in bytes, or `undefined` if not a File\n *\n * @example\n * ```typescript\n * const file = new File(['Hello'], 'greeting.txt');\n * const size = platform.getFileSize(file);\n * console.log(size); // 5 (bytes)\n * ```\n */\n getFileSize: (file: unknown) => {\n if (file instanceof File) {\n return file.size;\n }\n return undefined;\n },\n\n /**\n * Extracts the last modified timestamp from a File object.\n *\n * @param file - The file to extract the timestamp from\n * @returns The last modified timestamp in milliseconds since epoch, or `undefined` if not a File\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'data.txt');\n * const lastModified = platform.getFileLastModified(file);\n * console.log(new Date(lastModified)); // Date object of when file was last modified\n * ```\n */\n getFileLastModified: (file: unknown) => {\n if (file instanceof File) {\n return file.lastModified;\n }\n return undefined;\n },\n };\n}\n","import type { StorageService } from \"@uploadista/client-core\";\n\n/**\n * Creates a browser storage service using localStorage for persistent data storage.\n *\n * This service provides a key-value storage interface backed by the browser's\n * localStorage API. Data stored with this service persists across browser sessions\n * and page reloads until explicitly deleted or cleared by the user.\n *\n * Use cases include:\n * - Persisting upload state for resumable uploads\n * - Caching file metadata and fingerprints\n * - Storing user preferences and settings\n * - Maintaining upload history\n *\n * **Important notes:**\n * - localStorage has a typical limit of 5-10MB per origin\n * - Data is stored as strings (objects are JSON-serialized)\n * - localStorage is synchronous but this service wraps it in Promises for consistency\n * - Data is scoped to the origin (protocol + domain + port)\n * - Users can clear localStorage through browser settings\n *\n * @returns A StorageService backed by browser localStorage\n *\n * @example\n * ```typescript\n * import { createLocalStorageService } from '@uploadista/client-browser';\n *\n * const storage = createLocalStorageService();\n *\n * // Store upload state\n * await storage.setItem('upload:123', JSON.stringify({\n * fileId: '123',\n * progress: 75,\n * uploadedChunks: [0, 1, 2]\n * }));\n *\n * // Retrieve upload state\n * const data = await storage.getItem('upload:123');\n * if (data) {\n * const state = JSON.parse(data);\n * console.log('Resuming upload at', state.progress, '%');\n * }\n *\n * // Find all uploads\n * const uploads = await storage.find('upload:');\n * console.log('Found', Object.keys(uploads).length, 'uploads');\n *\n * // Clean up completed upload\n * await storage.removeItem('upload:123');\n * ```\n *\n * @see {@link createSessionStorageService} for session-only storage\n */\nexport function createLocalStorageService(): StorageService {\n /**\n * Internal helper to find entries matching a prefix.\n *\n * Iterates through all localStorage keys and returns those that start\n * with the specified prefix along with their values.\n *\n * @param prefix - Key prefix to filter by\n * @returns Object mapping matching keys to their values\n * @private\n */\n const findEntries = (prefix: string): Record<string, string> => {\n const results: Record<string, string> = {};\n\n for (const key in localStorage) {\n if (key.startsWith(prefix)) {\n const item = localStorage.getItem(key);\n if (item) {\n results[key] = item;\n }\n }\n }\n\n return results;\n };\n\n return {\n /**\n * Retrieves a value from localStorage by key.\n *\n * @param key - The key to retrieve\n * @returns Promise resolving to the value, or null if the key doesn't exist\n *\n * @example\n * ```typescript\n * const value = await storage.getItem('user:preferences');\n * if (value) {\n * const prefs = JSON.parse(value);\n * }\n * ```\n */\n async getItem(key: string): Promise<string | null> {\n return localStorage.getItem(key);\n },\n\n /**\n * Stores a value in localStorage.\n *\n * If the key already exists, its value will be overwritten.\n * Values must be strings; use JSON.stringify() for objects.\n *\n * @param key - The key to store under\n * @param value - The string value to store\n *\n * @throws {QuotaExceededError} If localStorage quota is exceeded\n *\n * @example\n * ```typescript\n * // Store string\n * await storage.setItem('upload:status', 'completed');\n *\n * // Store object\n * await storage.setItem('upload:metadata', JSON.stringify({\n * name: 'file.txt',\n * size: 1024\n * }));\n * ```\n */\n async setItem(key: string, value: string): Promise<void> {\n localStorage.setItem(key, value);\n },\n\n /**\n * Removes a value from localStorage by key.\n *\n * If the key doesn't exist, this is a no-op (no error is thrown).\n *\n * @param key - The key to remove\n *\n * @example\n * ```typescript\n * // Clean up completed upload\n * await storage.removeItem('upload:123');\n * ```\n */\n async removeItem(key: string): Promise<void> {\n localStorage.removeItem(key);\n },\n\n /**\n * Retrieves all entries from localStorage.\n *\n * Returns an object mapping every key in localStorage to its value.\n * Use with caution as this can return a large amount of data.\n *\n * @returns Promise resolving to object with all key-value pairs\n *\n * @example\n * ```typescript\n * const all = await storage.findAll();\n * console.log('Total items in storage:', Object.keys(all).length);\n * ```\n */\n async findAll(): Promise<Record<string, string>> {\n return findEntries(\"\");\n },\n\n /**\n * Finds all entries with keys starting with a given prefix.\n *\n * Useful for querying related data or implementing namespacing.\n * For example, use \"upload:\" prefix to find all upload-related entries.\n *\n * @param prefix - The key prefix to search for\n * @returns Promise resolving to object with matching key-value pairs\n *\n * @example\n * ```typescript\n * // Find all uploads\n * const uploads = await storage.find('upload:');\n * for (const [key, value] of Object.entries(uploads)) {\n * console.log('Upload:', key, JSON.parse(value));\n * }\n *\n * // Find user preferences\n * const prefs = await storage.find('pref:');\n * ```\n */\n async find(prefix: string): Promise<Record<string, string>> {\n return findEntries(prefix);\n },\n };\n}\n","import type { WebSocketFactory, WebSocketLike } from \"@uploadista/client-core\";\n\n/**\n * Browser implementation of WebSocket that wraps the native WebSocket API.\n *\n * This class provides a minimal wrapper around the browser's native WebSocket\n * to ensure compatibility with the Uploadista client's WebSocketLike interface.\n * It's used for real-time communication features like:\n * - Upload progress streaming\n * - Flow execution status updates\n * - Real-time error notifications\n * - Live event feeds\n *\n * The wrapper preserves all WebSocket states and properly proxies all events\n * while maintaining the standard WebSocket lifecycle.\n *\n * @example\n * ```typescript\n * const ws = new BrowserWebSocket('wss://api.example.com/ws');\n *\n * ws.onopen = () => {\n * console.log('Connected');\n * ws.send('Hello server');\n * };\n *\n * ws.onmessage = (event) => {\n * console.log('Message:', event.data);\n * };\n *\n * ws.onerror = (event) => {\n * console.error('Error:', event.message);\n * };\n *\n * ws.onclose = (event) => {\n * console.log('Closed:', event.code, event.reason);\n * };\n * ```\n */\nclass BrowserWebSocket implements WebSocketLike {\n /** WebSocket is currently connecting (readyState = 0) */\n readonly CONNECTING = 0;\n /** WebSocket connection is open and ready (readyState = 1) */\n readonly OPEN = 1;\n /** WebSocket is closing (readyState = 2) */\n readonly CLOSING = 2;\n /** WebSocket connection is closed (readyState = 3) */\n readonly CLOSED = 3;\n\n /**\n * Current state of the WebSocket connection.\n *\n * Possible values:\n * - 0 (CONNECTING): Connection is being established\n * - 1 (OPEN): Connection is open and ready for communication\n * - 2 (CLOSING): Connection is closing\n * - 3 (CLOSED): Connection is closed or couldn't be opened\n */\n readyState: number;\n\n /** Event handler called when the connection is established */\n onopen: (() => void) | null = null;\n\n /** Event handler called when the connection is closed */\n onclose: ((event: { code: number; reason: string }) => void) | null = null;\n\n /** Event handler called when an error occurs */\n onerror: ((event: { message: string }) => void) | null = null;\n\n /** Event handler called when a message is received */\n onmessage: ((event: { data: string }) => void) | null = null;\n\n private native: WebSocket;\n\n /**\n * Creates a new BrowserWebSocket instance.\n *\n * Initializes a native WebSocket connection to the specified URL and sets up\n * event handler proxying to convert native events to the WebSocketLike format.\n *\n * @param url - WebSocket URL to connect to (must use ws:// or wss:// protocol)\n *\n * @example\n * ```typescript\n * const ws = new BrowserWebSocket('wss://api.example.com/upload/progress');\n * ```\n */\n constructor(url: string) {\n this.native = new WebSocket(url);\n this.readyState = this.native.readyState;\n\n // Proxy event handlers to convert native events to WebSocketLike format\n this.native.onopen = () => {\n this.readyState = this.native.readyState;\n this.onopen?.();\n };\n\n this.native.onclose = (event) => {\n this.readyState = this.native.readyState;\n const closeEvent = event as CloseEvent;\n this.onclose?.({ code: closeEvent.code, reason: closeEvent.reason });\n };\n\n this.native.onerror = (_event) => {\n this.onerror?.({ message: \"WebSocket error\" });\n };\n\n this.native.onmessage = (event) => {\n const messageEvent = event as MessageEvent;\n this.onmessage?.({ data: messageEvent.data });\n };\n }\n\n /**\n * Sends data through the WebSocket connection.\n *\n * The data can be either a string (text message) or a Uint8Array (binary message).\n * The connection must be in the OPEN state before sending data.\n *\n * @param data - String or binary data to send\n *\n * @throws {Error} If the connection is not open\n *\n * @example\n * ```typescript\n * // Send text message\n * ws.send('{\"type\": \"subscribe\", \"channel\": \"uploads\"}');\n *\n * // Send binary data\n * const buffer = new Uint8Array([1, 2, 3, 4]);\n * ws.send(buffer);\n * ```\n */\n send(data: string | Uint8Array): void {\n this.native.send(data);\n }\n\n /**\n * Closes the WebSocket connection.\n *\n * Optionally accepts a close code and reason that will be sent to the server.\n * Standard close codes include:\n * - 1000: Normal closure\n * - 1001: Going away (e.g., page navigation)\n * - 1002: Protocol error\n * - 1003: Unsupported data\n *\n * @param code - Optional close code (default: 1000 for normal closure)\n * @param reason - Optional human-readable reason for closing\n *\n * @example\n * ```typescript\n * // Normal close\n * ws.close();\n *\n * // Close with reason\n * ws.close(1000, 'Upload completed');\n *\n * // Close due to error\n * ws.close(1011, 'Internal error during upload');\n * ```\n */\n close(code?: number, reason?: string): void {\n this.native.close(code, reason);\n }\n}\n\n/**\n * Creates a factory for browser WebSocket connections.\n *\n * This factory is used by the Uploadista client to create WebSocket connections\n * for real-time features. It wraps the browser's native WebSocket API and provides\n * a consistent interface for the client.\n *\n * The factory creates WebSockets that support:\n * - Real-time upload progress updates\n * - Flow execution status streaming\n * - Live error and event notifications\n * - Bidirectional client-server communication\n *\n * @returns A WebSocketFactory that creates browser-compatible WebSocket connections\n *\n * @example\n * ```typescript\n * import { createBrowserWebSocketFactory } from '@uploadista/client-browser';\n *\n * const factory = createBrowserWebSocketFactory();\n *\n * // Create a WebSocket connection\n * const ws = factory.create('wss://api.example.com/ws/upload/123');\n *\n * // Set up event handlers\n * ws.onmessage = (event) => {\n * const data = JSON.parse(event.data);\n * if (data.type === 'progress') {\n * console.log('Upload progress:', data.progress);\n * }\n * };\n *\n * ws.onopen = () => {\n * console.log('WebSocket connected');\n * };\n *\n * ws.onclose = (event) => {\n * console.log('WebSocket closed:', event.code, event.reason);\n * };\n * ```\n *\n * @see {@link BrowserWebSocket} for the WebSocket implementation details\n */\nexport const createBrowserWebSocketFactory = (): WebSocketFactory => ({\n create: (url: string): WebSocketLike => new BrowserWebSocket(url),\n});\n","import type {\n ConnectionPoolConfig,\n ServiceContainer,\n} from \"@uploadista/client-core\";\nimport { createHttpClient } from \"../http-client\";\nimport type { BrowserUploadInput } from \"../types/upload-input\";\nimport { createBrowserAbortControllerFactory } from \"./abort-controller-factory\";\nimport { createChecksumService } from \"./checksum-service\";\nimport { createBrowserFileReaderService } from \"./file-reader\";\nimport { createFingerprintService } from \"./fingerprint-service\";\nimport { createBrowserIdGenerationService } from \"./id-generation/id-generation\";\nimport { createBrowserPlatformService } from \"./platform-service\";\nimport { createLocalStorageService } from \"./storage/local-storage-service\";\nimport { createBrowserWebSocketFactory } from \"./websocket-factory\";\n\nexport interface BrowserServiceOptions {\n /**\n * HTTP client configuration for connection pooling\n */\n connectionPooling?: ConnectionPoolConfig;\n\n /**\n * Whether to use localStorage for persistence\n * If false, uses in-memory storage\n * @default true\n */\n useLocalStorage?: boolean;\n}\n\n/**\n * Creates a service container with browser-specific implementations\n * of all required services for the upload client\n *\n * @param options - Configuration options for browser services\n * @returns ServiceContainer with browser implementations\n *\n * @example\n * ```typescript\n * import { createBrowserServices } from '@uploadista/browser/services';\n *\n * const services = createBrowserServices({\n * useLocalStorage: true,\n * connectionPooling: {\n * maxConnectionsPerHost: 6,\n * connectionTimeout: 30000,\n * }\n * });\n * ```\n */\nexport function createBrowserServices(\n options: BrowserServiceOptions = {},\n): ServiceContainer<BrowserUploadInput> {\n const { connectionPooling, useLocalStorage = true } = options;\n\n // Create storage service (localStorage or in-memory fallback)\n const storage = useLocalStorage\n ? createLocalStorageService()\n : // Placeholder for in-memory storage - use localStorage as default for browser\n createLocalStorageService();\n\n // Create other services\n const idGeneration = createBrowserIdGenerationService();\n const httpClient = createHttpClient(connectionPooling);\n const fileReader = createBrowserFileReaderService();\n const websocket = createBrowserWebSocketFactory();\n const abortController = createBrowserAbortControllerFactory();\n const checksumService = createChecksumService();\n const fingerprintService = createFingerprintService();\n\n return {\n platform: createBrowserPlatformService(),\n storage,\n idGeneration,\n httpClient,\n fileReader,\n websocket,\n abortController,\n checksumService,\n fingerprintService,\n };\n}\n","import {\n type ConnectionPoolConfig,\n createClientStorage,\n createLogger,\n createUploadistaClient as createUploadistaClientCore,\n type UploadistaClientOptions as UploadistaClientOptionsCore,\n} from \"@uploadista/client-core\";\nimport { createBrowserServices } from \"../services/create-browser-services\";\nimport type { BrowserUploadInput } from \"../types/upload-input\";\n\n/**\n * Configuration options for creating a browser-specific Uploadista client.\n *\n * This interface extends the core client options but omits browser-specific\n * services that are automatically provided by the browser environment.\n * These services include WebSocket factory, AbortController, ID generation,\n * storage, logging, platform detection, fingerprinting, HTTP client, file reader,\n * and checksum calculation.\n *\n * @example\n * ```typescript\n * import { createUploadistaClient } from '@uploadista/client-browser';\n *\n * const client = createUploadistaClient({\n * endpoint: 'https://api.uploadista.com/upload',\n * connectionPooling: {\n * maxConnectionsPerHost: 6,\n * enableHttp2: true\n * }\n * });\n * ```\n */\nexport interface UploadistaClientOptions\n extends Omit<\n UploadistaClientOptionsCore<BrowserUploadInput>,\n | \"webSocketFactory\"\n | \"abortControllerFactory\"\n | \"generateId\"\n | \"clientStorage\"\n | \"logger\"\n | \"platformService\"\n | \"fingerprintService\"\n | \"httpClient\"\n | \"fileReader\"\n | \"checksumService\"\n > {\n /**\n * Connection pooling configuration for the HTTP client.\n *\n * Controls how the browser manages HTTP connections for optimal performance.\n * The browser's native fetch API with keep-alive headers is used under the hood.\n *\n * @default\n * ```typescript\n * {\n * maxConnectionsPerHost: 6,\n * connectionTimeout: 30000,\n * keepAliveTimeout: 60000,\n * enableHttp2: true,\n * retryOnConnectionError: true\n * }\n * ```\n *\n * @example\n * ```typescript\n * connectionPooling: {\n * maxConnectionsPerHost: 10,\n * enableHttp2: true,\n * keepAliveTimeout: 120000\n * }\n * ```\n */\n connectionPooling?: ConnectionPoolConfig;\n}\n\n/**\n * Creates a browser-optimized Uploadista client for file uploads and flow processing.\n *\n * This factory function automatically configures all browser-specific services including:\n * - Fetch-based HTTP client with connection pooling\n * - Native WebSocket support for real-time progress\n * - localStorage for upload state persistence\n * - Web Crypto API for checksums and fingerprints\n * - File API for reading and chunking files\n * - Browser platform detection and capabilities\n *\n * The created client can handle File and Blob objects from file inputs, drag-and-drop,\n * or programmatically created content. It supports resumable uploads, progress tracking,\n * and flow-based file processing.\n *\n * @param options - Configuration options for the browser client\n * @returns A fully configured Uploadista client ready for browser use\n *\n * @example\n * ```typescript\n * import { createUploadistaClient } from '@uploadista/client-browser';\n *\n * // Basic usage\n * const client = createUploadistaClient({\n * endpoint: 'https://api.uploadista.com/upload'\n * });\n *\n * // With custom configuration\n * const client = createUploadistaClient({\n * endpoint: 'https://api.uploadista.com/upload',\n * connectionPooling: {\n * maxConnectionsPerHost: 6,\n * enableHttp2: true,\n * keepAliveTimeout: 60000\n * },\n * chunkSize: 5 * 1024 * 1024, // 5MB chunks\n * retryDelays: [1000, 3000, 5000],\n * allowedMetaFields: ['userId', 'projectId']\n * });\n *\n * // Upload a file\n * const fileInput = document.querySelector('input[type=\"file\"]');\n * const file = fileInput.files[0];\n *\n * const upload = await client.upload(file, {\n * onProgress: (event) => {\n * console.log(`Progress: ${event.progress}%`);\n * }\n * });\n *\n * console.log('Upload complete:', upload.id);\n * ```\n *\n * @see {@link UploadistaClientOptions} for available configuration options\n * @see {@link BrowserUploadInput} for supported file input types\n */\nexport function createUploadistaClient(options: UploadistaClientOptions) {\n const services = createBrowserServices({\n connectionPooling: options.connectionPooling,\n });\n\n return createUploadistaClientCore<BrowserUploadInput>({\n ...options,\n webSocketFactory: services.websocket,\n abortControllerFactory: services.abortController,\n platformService: services.platform,\n httpClient: services.httpClient,\n fileReader: services.fileReader,\n generateId: services.idGeneration,\n fingerprintService: services.fingerprintService,\n checksumService: services.checksumService,\n logger: createLogger(false, () => {}),\n clientStorage: createClientStorage(services.storage),\n });\n}\n","/**\n * Framework Integration Utilities\n *\n * This module provides TypeScript utilities and helper types for building\n * framework-specific wrappers around the Uploadista client.\n *\n * @module framework-utils\n */\n\nimport type { FlowResult, UploadResult } from \"@uploadista/client-core\";\nimport type { FlowEvent } from \"@uploadista/core/flow\";\nimport type { UploadEvent, UploadFile } from \"@uploadista/core/types\";\n\n/**\n * Base upload state that framework wrappers should implement\n */\nexport interface BaseUploadState {\n status: \"idle\" | \"uploading\" | \"success\" | \"error\" | \"aborted\";\n progress: number;\n bytesUploaded: number;\n totalBytes: number;\n error?: Error;\n result?: UploadResult<UploadFile>;\n}\n\n/**\n * Base flow upload state\n */\nexport interface BaseFlowUploadState extends BaseUploadState {\n jobId?: string;\n flowStatus?: \"pending\" | \"processing\" | \"completed\" | \"failed\";\n flowResult?: FlowResult<unknown>;\n}\n\n/**\n * Progress callback signature\n */\nexport type ProgressCallback = (\n uploadId: string,\n bytesUploaded: number,\n totalBytes: number,\n) => void;\n\n/**\n * Complete callback signature\n */\nexport type CompleteCallback = (uploadId: string, result: UploadResult) => void;\n\n/**\n * Error callback signature\n */\nexport type ErrorCallback = (uploadId: string, error: Error) => void;\n\n/**\n * Abort callback signature\n */\nexport type AbortCallback = (uploadId: string) => void;\n\n/**\n * Event handler signature for framework wrappers\n */\nexport type EventHandler<T = unknown> = (event: T) => void;\n\n/**\n * WebSocket event handler signature\n */\nexport type WebSocketEventHandler = (event: UploadEvent | FlowEvent) => void;\n\n/**\n * Framework state updater function signature\n * @template T - The state type\n */\nexport type StateUpdater<T> = (updater: (prevState: T) => T) => void;\n\n/**\n * Cleanup function returned by setup functions\n */\nexport type CleanupFunction = () => void;\n\n/**\n * Upload item for multi-upload tracking\n */\nexport interface UploadItem {\n id: string;\n file: File;\n status: BaseUploadState[\"status\"];\n progress: number;\n bytesUploaded: number;\n totalBytes: number;\n error?: Error;\n result?: UploadResult;\n}\n\n/**\n * Multi-upload aggregate statistics\n */\nexport interface MultiUploadStats {\n totalFiles: number;\n completedFiles: number;\n failedFiles: number;\n totalBytes: number;\n uploadedBytes: number;\n totalProgress: number;\n allComplete: boolean;\n hasErrors: boolean;\n}\n\n/**\n * Drag and drop state\n */\nexport interface DragDropState {\n isDragging: boolean;\n isOver: boolean;\n files: File[];\n}\n\n/**\n * File validation result\n */\nexport interface FileValidationResult {\n valid: boolean;\n error?: string;\n}\n\n/**\n * File validation function signature\n */\nexport type FileValidator = (file: File) => FileValidationResult;\n\n/**\n * Utility: Calculate aggregate upload statistics\n */\nexport function calculateMultiUploadStats(\n uploads: UploadItem[],\n): MultiUploadStats {\n const totalFiles = uploads.length;\n const completedFiles = uploads.filter((u) => u.status === \"success\").length;\n const failedFiles = uploads.filter((u) => u.status === \"error\").length;\n const totalBytes = uploads.reduce((sum, u) => sum + u.totalBytes, 0);\n const uploadedBytes = uploads.reduce((sum, u) => sum + u.bytesUploaded, 0);\n const totalProgress = totalBytes > 0 ? (uploadedBytes / totalBytes) * 100 : 0;\n const allComplete = uploads.every((u) => u.status === \"success\");\n const hasErrors = uploads.some((u) => u.status === \"error\");\n\n return {\n totalFiles,\n completedFiles,\n failedFiles,\n totalBytes,\n uploadedBytes,\n totalProgress,\n allComplete,\n hasErrors,\n };\n}\n\n/**\n * Utility: Format file size for display\n */\nexport function formatFileSize(bytes: number): string {\n if (bytes === 0) return \"0 Bytes\";\n\n const k = 1024;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;\n}\n\n/**\n * Utility: Format progress percentage\n */\nexport function formatProgress(progress: number): string {\n return `${Math.round(progress)}%`;\n}\n\n/**\n * Utility: Get file extension\n */\nexport function getFileExtension(filename: string): string {\n const lastDot = filename.lastIndexOf(\".\");\n return lastDot === -1 ? \"\" : filename.slice(lastDot + 1).toLowerCase();\n}\n\n/**\n * Utility: Check if file is an image\n */\nexport function isImageFile(file: File): boolean {\n return file.type.startsWith(\"image/\");\n}\n\n/**\n * Utility: Check if file is a video\n */\nexport function isVideoFile(file: File): boolean {\n return file.type.startsWith(\"video/\");\n}\n\n/**\n * Utility: Create file size validator\n */\nexport function createFileSizeValidator(maxSizeBytes: number): FileValidator {\n return (file: File): FileValidationResult => {\n if (file.size > maxSizeBytes) {\n return {\n valid: false,\n error: `File size exceeds maximum of ${formatFileSize(maxSizeBytes)}`,\n };\n }\n return { valid: true };\n };\n}\n\n/**\n * Utility: Create file type validator\n */\nexport function createFileTypeValidator(allowedTypes: string[]): FileValidator {\n return (file: File): FileValidationResult => {\n const fileType = file.type.toLowerCase();\n const fileExt = getFileExtension(file.name);\n\n const isAllowed = allowedTypes.some((type) => {\n if (type.startsWith(\".\")) {\n return type.slice(1) === fileExt;\n }\n if (type.includes(\"*\")) {\n const pattern = type.replace(\"*\", \"\");\n return fileType.startsWith(pattern);\n }\n return fileType === type;\n });\n\n if (!isAllowed) {\n return {\n valid: false,\n error: `File type not allowed. Allowed types: ${allowedTypes.join(\", \")}`,\n };\n }\n return { valid: true };\n };\n}\n\n/**\n * Utility: Compose multiple validators\n */\nexport function composeValidators(\n ...validators: FileValidator[]\n): FileValidator {\n return (file: File): FileValidationResult => {\n for (const validator of validators) {\n const result = validator(file);\n if (!result.valid) {\n return result;\n }\n }\n return { valid: true };\n };\n}\n\n/**\n * Utility: Generate unique upload ID\n */\nexport function generateUploadId(): string {\n return `upload-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n}\n\n/**\n * Utility: Create delay promise for retry logic\n */\nexport function delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Utility: Calculate exponential backoff delay\n */\nexport function calculateBackoff(\n attempt: number,\n baseDelay = 1000,\n maxDelay = 30000,\n): number {\n const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);\n // Add jitter to prevent thundering herd\n return delay + Math.random() * 1000;\n}\n\n/**\n * Utility: Create retry wrapper for upload function\n */\nexport function createRetryWrapper<T>(\n fn: () => Promise<T>,\n maxAttempts = 3,\n shouldRetry: (error: unknown) => boolean = () => true,\n): () => Promise<T> {\n return async () => {\n let lastError: unknown;\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n if (attempt < maxAttempts - 1 && shouldRetry(error)) {\n const delayMs = calculateBackoff(attempt);\n await delay(delayMs);\n continue;\n }\n break;\n }\n }\n throw lastError;\n };\n}\n\n/**\n * Type guard: Check if error is network-related (should retry)\n */\nexport function isNetworkError(error: unknown): boolean {\n if (error instanceof Error) {\n return (\n error.message.includes(\"network\") ||\n error.message.includes(\"timeout\") ||\n error.message.includes(\"connection\") ||\n error.message.includes(\"ECONNREFUSED\") ||\n error.message.includes(\"ETIMEDOUT\")\n );\n }\n return false;\n}\n\n/**\n * Type guard: Check if error is abort-related (should not retry)\n */\nexport function isAbortError(error: unknown): boolean {\n if (error instanceof Error) {\n return error.name === \"AbortError\" || error.message.includes(\"abort\");\n }\n return false;\n}\n\n/**\n * Format upload speed in human-readable format\n */\nexport function formatSpeed(bytesPerSecond: number): string {\n if (bytesPerSecond === 0) return \"0 B/s\";\n const k = 1024;\n const sizes = [\"B/s\", \"KB/s\", \"MB/s\", \"GB/s\"];\n const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));\n return `${parseFloat((bytesPerSecond / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\n/**\n * Format duration in human-readable format\n */\nexport function formatDuration(milliseconds: number): string {\n if (milliseconds < 1000) {\n return `${Math.round(milliseconds)}ms`;\n }\n\n if (milliseconds < 60000) {\n return `${Math.round(milliseconds / 1000)}s`;\n }\n\n if (milliseconds < 3600000) {\n const minutes = Math.floor(milliseconds / 60000);\n const seconds = Math.round((milliseconds % 60000) / 1000);\n return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;\n }\n\n const hours = Math.floor(milliseconds / 3600000);\n const minutes = Math.round((milliseconds % 3600000) / 60000);\n return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;\n}\n\n/**\n * Validate file type against accepted types\n */\nexport function validateFileType(file: File, accept: string[]): boolean {\n if (!accept || accept.length === 0) return true;\n\n return accept.some((acceptType) => {\n if (acceptType.startsWith(\".\")) {\n // File extension check\n return file.name.toLowerCase().endsWith(acceptType.toLowerCase());\n }\n\n // MIME type check (supports wildcards like image/*)\n if (acceptType.endsWith(\"/*\")) {\n const baseType = acceptType.slice(0, -2);\n return file.type.startsWith(baseType);\n }\n\n return file.type === acceptType;\n });\n}\n\n/**\n * Check if a file is an audio file\n */\nexport function isAudioFile(file: File): boolean {\n return file.type.startsWith(\"audio/\");\n}\n\n/**\n * Check if a file is a document\n */\nexport function isDocumentFile(file: File): boolean {\n const documentTypes = [\n \"application/pdf\",\n \"application/msword\",\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n \"application/vnd.ms-excel\",\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n \"application/vnd.ms-powerpoint\",\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n \"text/plain\",\n \"text/csv\",\n \"application/rtf\",\n ];\n\n return documentTypes.includes(file.type);\n}\n\n/**\n * Create a preview URL for a file (if supported)\n */\nexport function createFilePreview(file: File): string | null {\n if (isImageFile(file) || isVideoFile(file) || isAudioFile(file)) {\n return URL.createObjectURL(file);\n }\n return null;\n}\n\n/**\n * Clean up a preview URL created with createFilePreview\n */\nexport function revokeFilePreview(previewUrl: string): void {\n URL.revokeObjectURL(previewUrl);\n}\n\n/**\n * Calculate progress percentage\n */\nexport function calculateProgress(current: number, total: number): number {\n if (total === 0) return 0;\n return Math.min(100, Math.max(0, Math.round((current / total) * 100)));\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uploadista/client-browser",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.1.0-beta.5",
|
|
5
5
|
"description": "Browser client for Uploadista",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Uploadista",
|
|
@@ -25,20 +25,21 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"js-base64": "3.7.8",
|
|
28
|
-
"@uploadista/core": "0.0.
|
|
29
|
-
"@uploadista/
|
|
28
|
+
"@uploadista/client-core": "0.1.0-beta.5",
|
|
29
|
+
"@uploadista/core": "0.1.0-beta.5"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
32
|
+
"happy-dom": "^20.1.0",
|
|
33
|
+
"tsdown": "0.19.0",
|
|
34
|
+
"vitest": "4.0.17",
|
|
35
|
+
"@uploadista/typescript-config": "0.1.0-beta.5"
|
|
35
36
|
},
|
|
36
37
|
"scripts": {
|
|
37
38
|
"build": "tsc --noEmit && tsdown",
|
|
38
39
|
"format": "biome format --write ./src",
|
|
39
40
|
"lint": "biome lint --write ./src",
|
|
40
41
|
"check": "biome check --write ./src ",
|
|
41
|
-
"test": "vitest",
|
|
42
|
+
"test": "vitest run",
|
|
42
43
|
"test:run": "vitest run",
|
|
43
44
|
"test:watch": "vitest --watch"
|
|
44
45
|
}
|