npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jengo/storage

v1.0.1

Published

Universal client library for Jengo Storage featuring chunked uploads, resumability, direct-to-cloud S3/R2 transfers, and framework adapters.

Readme

@jengo/storage

Universal TypeScript client library for Jengo Storage. Provides chunked multipart uploading, concurrent streaming, pause/resume/abort capabilities, transfer speed & ETA estimators, instant client-side previews, direct-to-cloud S3/R2 transfers, and first-class adapters for React, Vue, and Svelte.

Designed to pair seamlessly with the backend jengo/storage package.


Features

  • Chunked File Uploads: Automatically slices large files into configurable chunk sizes (e.g. 2 MB, 5 MB) and streams them concurrently.
  • Resumability and Concurrency: Configurable concurrency pool (default 3 streams) with automatic retry backoff and pause/resume/abort controls.
  • Memory-Safe Backend Assembly: Pairs with Jengo\Storage\Controllers\ChunkUploadController for low-memory buffered stream concatenation.
  • Web Crypto SHA-256 Integrity: Computes chunk-level and total-file SHA-256 hashes in the browser for end-to-end verification.
  • Zero Boilerplate CSRF: Automatically detects and injects CodeIgniter 4 CSRF tokens from meta tags, cookies (csrf_cookie_name), or window globals.
  • Instant Client Previews: Generates client-side thumbnail previews, dimensions, and metadata before transmission begins.
  • Framework Adapters: Zero-friction hooks and stores for React (useChunkedUpload, useUpload), Vue 3 (useChunkedUpload, useUpload), and Svelte (createChunkedUpload, createUpload).
  • Direct Cloud Transfers: Presigned PUT/POST support for S3, Cloudflare R2, and Google Cloud Storage.

Installation

# npm
npm install @jengo/storage

# pnpm
pnpm add @jengo/storage

# yarn
yarn add @jengo/storage

Quick Start (Vanilla TypeScript / JavaScript)

1. Chunked Upload

import { ChunkedUploader } from '@jengo/storage';

const fileInput = document.querySelector<HTMLInputElement>('#fileInput')!;

fileInput.addEventListener('change', async () => {
    const file = fileInput.files?.[0];
    if (!file) return;

    const uploader = new ChunkedUploader(file, {
        chunkSize: 2 * 1024 * 1024, // 2 MB parts
        concurrency: 3,             // 3 parallel streams
        disk: 'public',             // destination disk
        folder: 'uploads/videos',
        computeChecksums: true,
        onProgress: (progress) => {
            console.log(`Progress: ${progress.percent}% | Speed: ${progress.speed} | ETA: ${progress.remainingSeconds}s`);
        },
        onStatusChange: (status) => {
            console.log(`Status changed to: ${status}`);
        },
        onSuccess: (result) => {
            console.log('File uploaded and assembled:', result.url);
        },
        onError: (err) => {
            console.error('Upload failed:', err);
        },
    });

    // Control upload lifecycle
    await uploader.start();

    // To pause or resume:
    // uploader.pause();
    // await uploader.resume();

    // To abort and clean staging on server:
    // await uploader.abort();
});

2. Instant Client-Side Previews

import { createFilePreview } from '@jengo/storage';

const preview = await createFilePreview(file);
console.log(`Name: ${preview.name}, Size: ${preview.sizeFormatted}`);

if (preview.isImage) {
    const imgElement = document.createElement('img');
    imgElement.src = preview.url!;
    console.log(`Dimensions: ${preview.width}x${preview.height}`);
    document.body.appendChild(imgElement);
}

// When done, free the browser object URL
preview.revoke();

Framework Adapters

React

import React, { useState } from 'react';
import { useChunkedUpload } from '@jengo/storage/react';

export function VideoUploader() {
    const [file, setFile] = useState<File | null>(null);
    const {
        status,
        progress,
        result,
        error,
        isUploading,
        isPaused,
        start,
        pause,
        resume,
        abort,
    } = useChunkedUpload({
        chunkSize: 2 * 1024 * 1024,
        concurrency: 3,
        disk: 'public',
        folder: 'videos',
    });

    const handleUpload = async () => {
        if (!file) return;
        try {
            await start(file);
        } catch (e) {
            // Handled via error state
        }
    };

    return (
        <div>
            <input type="file" onChange={(e) => setFile(e.target.files?.[0] || null)} />
            <button onClick={handleUpload} disabled={!file || isUploading}>
                Upload
            </button>

            {isUploading && (
                <>
                    <button onClick={pause}>Pause</button>
                    <button onClick={abort}>Abort</button>
                </>
            )}

            {isPaused && <button onClick={resume}>Resume</button>}

            {isUploading && (
                <div>
                    <div>{progress.percent}% ({progress.speed})</div>
                    <div>Remaining: {progress.remainingSeconds}s</div>
                    <div style={{ width: `${progress.percent}%`, height: '8px', background: 'blue' }} />
                </div>
            )}

            {result && <div>Uploaded successfully: <a href={result.url}>{result.filename}</a></div>}
            {error && <div>Error: {error.message}</div>}
        </div>
    );
}

Vue 3

<script setup lang="ts">
import { ref } from 'vue';
import { useChunkedUpload } from '@jengo/storage/vue';

const selectedFile = ref<File | null>(null);

const {
    status,
    progress,
    result,
    error,
    isUploading,
    isPaused,
    start,
    pause,
    resume,
    abort,
} = useChunkedUpload({
    disk: 'public',
    folder: 'assets',
});

function onFileSelected(event: Event) {
    const target = event.target as HTMLInputElement;
    selectedFile.value = target.files?.[0] || null;
}

async function upload() {
    if (selectedFile.value) {
        await start(selectedFile.value);
    }
}
</script>

<template>
    <div>
        <input type="file" @change="onFileSelected" />
        <button :disabled="!selectedFile || isUploading" @click="upload">Upload</button>
        <button v-if="isUploading" @click="pause">Pause</button>
        <button v-if="isPaused" @click="resume">Resume</button>
        <button v-if="isUploading || isPaused" @click="abort">Abort</button>

        <div v-if="isUploading || isPaused">
            <p>Progress: {{ progress.percent }}% ({{ progress.speed }})</p>
            <progress :value="progress.percent" max="100"></progress>
        </div>

        <div v-if="result">
            <p>Complete: <a :href="result.url">{{ result.filename }}</a></p>
        </div>
    </div>
</template>

Svelte

<script lang="ts">
import { createChunkedUpload } from '@jengo/storage/svelte';

let file: File | null = null;
const uploadStore = createChunkedUpload({
    disk: 'public',
    folder: 'docs',
});

function onFileChange(e: Event) {
    const input = e.target as HTMLInputElement;
    file = input.files?.[0] || null;
}

function handleStart() {
    if (file) {
        uploadStore.start(file);
    }
}
</script>

<input type="file" on:change={onFileChange} />
<button on:click={handleStart} disabled={!file || $uploadStore.isUploading}>Upload</button>

{#if $uploadStore.isUploading}
    <button on:click={uploadStore.pause}>Pause</button>
    <button on:click={uploadStore.abort}>Abort</button>
{/if}

{#if $uploadStore.isPaused}
    <button on:click={uploadStore.resume}>Resume</button>
{/if}

{#if $uploadStore.isUploading || $uploadStore.isPaused}
    <div>
        <p>{$uploadStore.progress.percent}% - {$uploadStore.progress.speed}</p>
        <div style="width: {$uploadStore.progress.percent}%; height: 6px; background: indigo;"></div>
    </div>
{/if}

{#if $uploadStore.result}
    <p>Uploaded: {$uploadStore.result.filename}</p>
{/if}

Universal Upload UI Component

@jengo/storage includes an all-in-one, highly customizable upload component with full support for Vanilla HTML/JS, React, Vue 3, and Svelte.

Features

  • Zero-Dependency Styling: Ships with clean, embedded scoped CSS.
  • Theme Manipulation: 5 built-in theme presets (light, dark, minimal, corporate, glass) plus custom CSS variables.
  • Multiple Layout Variants: dropzone (full card with drag-and-drop), compact (single row), minimal (sleek borders, no shadows), and button (trigger button).
  • Modal or Inline: Render inline anywhere in your UI or as a modal dialog (modal: true or <UploadModal />).
  • Single & Multiple Uploads: Toggle multiple: true or multiple: false. In multi-file mode, files are queued, managed, and uploaded concurrently.
  • Automatic Chunking: Automatically slices files larger than 5 MB into concurrent chunks; standard files are uploaded via multipart streams.
  • Queue Controls: Previews, speed meters, ETA, pause/resume, cancel, retry, and remove buttons per item.
  • Granular Developer Control: Toggle any section (showDropzone, showFileList, showProgress, showDetails, showThumbnails, allowPause, allowCancel) or supply custom render hooks (renderDropzone, renderFileItem, renderHeader, renderFooter).

1. Vanilla HTML & Web Component (<jengo-uploader>)

<script type="module">
  import { registerJengoUploader } from '@jengo/storage';
  registerJengoUploader();
</script>

<!-- Render inline -->
<jengo-uploader
  disk="public"
  folder="documents"
  theme="corporate"
  variant="dropzone"
  multiple="true"
></jengo-uploader>

<!-- Or as an imperative DOM component -->
<div id="uploaderContainer"></div>
<script type="module">
  import { createUploaderUI } from '@jengo/storage';

  const uploader = createUploaderUI('#uploaderContainer', {
    disk: 'public',
    theme: 'dark',
    multiple: true,
    autoUpload: true,
    onComplete: (results) => console.log('Finished:', results),
  });
</script>

2. React / Next.js (<JengoUploader /> & <UploadModal />)

import { useState } from 'react';
import { JengoUploader, UploadModal } from '@jengo/storage/react';

export function DocumentManager() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <div>
      {/* Inline Dropzone with custom theme variables */}
      <JengoUploader
        disk="public"
        folder="projects"
        theme="minimal"
        variant="dropzone"
        multiple={true}
        themeVariables={{
          primary: '#4f46e5',
          radius: '0.5rem',
        }}
        onComplete={(results) => console.log('Uploaded files:', results)}
      />

      {/* Standalone Modal Uploader */}
      <button onClick={() => setIsModalOpen(true)}>Open Upload Modal</button>

      <UploadModal
        isOpen={isModalOpen}
        disk="public"
        theme="glass"
        modalTitle="Attach Media"
        onClose={() => setIsModalOpen(false)}
        onComplete={(results) => {
          console.log('Saved:', results);
          setIsModalOpen(false);
        }}
      />
    </div>
  );
}

3. Vue 3 (<JengoUploader /> & <UploadModal />)

<script setup>
import { ref } from 'vue';
import { JengoUploader, UploadModal } from '@jengo/storage/vue';

const isModalOpen = ref(false);

function handleComplete(results) {
  console.log('Uploads complete:', results);
}
</script>

<template>
  <!-- Inline Compact variant -->
  <JengoUploader
    disk="public"
    folder="profiles"
    variant="compact"
    theme="dark"
    :multiple="false"
    @complete="handleComplete"
  />

  <!-- Modal Dialog -->
  <button @click="isModalOpen = true">Upload Files</button>
  <UploadModal
    :is-open="isModalOpen"
    disk="public"
    theme="corporate"
    @close="isModalOpen = false"
    @complete="handleComplete"
  />
</template>

4. Svelte (Svelte 4 & 5 Action use:uploader)

<script>
  import { uploader } from '@jengo/storage/svelte';

  let uploaderOptions = {
    disk: 'public',
    folder: 'gallery',
    theme: 'light',
    variant: 'dropzone',
    multiple: true,
    onComplete: (results) => console.log('Uploaded:', results),
  };
</script>

<!-- Attach directly to any DOM element via Svelte action -->
<div use:uploader={uploaderOptions} />

Complete Customization Options

| Option | Type | Default | Description | |---|---|---|---| | disk | string | 'public' | Target filesystem disk. | | folder | string | 'uploads' | Subfolder on destination disk. | | theme | 'light' \| 'dark' \| 'minimal' \| 'corporate' \| 'glass' \| 'custom' | 'light' | Preset theme appearance. | | themeVariables | Record<string, string> | undefined | Custom CSS variable overrides (e.g. { primary: '#0066cc', radius: '4px' }). | | variant | 'dropzone' \| 'compact' \| 'minimal' \| 'button' | 'dropzone' | Layout presentation mode. | | modal | boolean | false | Whether to render inside a backdrop overlay modal. | | modalTitle | string | 'Upload Files' | Dialog title when in modal mode. | | multiple | boolean | true | Allow selecting/dropping multiple files vs single file. | | autoUpload | boolean | true | Immediately start upload on file selection. | | chunked | boolean \| 'auto' | 'auto' | Automatic chunking for files above chunkThreshold. | | chunkThreshold| number | 5242880 (5 MB) | Threshold in bytes to trigger chunking. | | chunkSize | number | 2097152 (2 MB) | Byte size per chunk. | | concurrency | number | 2 | Number of simultaneous file uploads. | | maxFiles | number | undefined | Maximum allowed files in queue. | | maxFileSize | number | undefined | Maximum allowed file size in bytes. | | allowedTypes | string[] | undefined | Allowed MIME types or file extensions (e.g. ['image/*', '.pdf']). | | showDropzone | boolean | true | Show drag-and-drop zone. | | showFileList | boolean | true | Show queued/uploaded file items list. | | showProgress | boolean | true | Show progress indicators. | | showDetails | boolean | true | Show transfer rate and ETA metrics. | | showThumbnails| boolean | true | Show client-generated image/file thumbnails. | | allowPause | boolean | true | Allow pausing active chunked transfers. | | allowCancel | boolean | true | Allow canceling uploads. | | allowRemove | boolean | true | Allow removing items from queue. | | labels | Partial<UploaderLabels> | DEFAULT_LABELS | Internationalization labels. | | renderDropzone| (ctx) => HTMLElement \| string | undefined | Custom renderer for dropzone. | | renderFileItem| (item, ctx) => HTMLElement \| string | undefined | Custom renderer for queue items. | | renderHeader | (ctx) => HTMLElement \| string | undefined | Custom renderer for component header. | | renderFooter | (ctx) => HTMLElement \| string | undefined | Custom renderer for component footer. |


Direct Cloud Transfers

When uploading directly to Amazon S3, Cloudflare R2, or Google Cloud Storage via presigned URLs:

import { DirectCloudUploader } from '@jengo/storage';

const uploader = new DirectCloudUploader(file, {
    ticketEndpoint: '/storage/direct/ticket',   // Endpoint issuing presigned PUT URL
    confirmEndpoint: '/storage/direct/confirm', // Endpoint saving metadata after completion
    onProgress: (prog) => console.log(`${prog.percent}%`),
});

const result = await uploader.start();

Backend Integration

The backend endpoints are provided out of the box by jengo/storage:

  • POST /storage/chunks/upload: Receives individual chunk .part files.
  • POST /storage/chunks/assemble: Concatenates staged parts via memory-safe streams and copies to destination disk (public, local, s3).
  • POST /storage/chunks/abort: Purges staging directory if user cancels.

Cleanup of orphaned chunk sessions is handled automatically via:

php spark storage:clean --chunks

License

MIT License. Copyright Ian Otieno.