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

@classic-homes/offline-svelte

v0.1.3

Published

Svelte bindings for offline persistence and caching

Downloads

66

Readme

@classic-homes/offline-svelte

Svelte 5 composables for offline persistence and caching. Provides reactive file storage, form draft persistence, and data caching using Svelte's runes.

Features

  • useFileStorage: Reactive file storage with validation and previews
  • usePersistedForm: Automatic form draft persistence
  • useOfflineCache: Data caching with TTL and stale-while-revalidate
  • Svelte 5 Runes: Built with $state and $effect for optimal reactivity
  • TypeScript: Full type safety with generics

Installation

npm install @classic-homes/offline-svelte @classic-homes/offline-core

Quick Start

<script lang="ts">
  import { onMount } from 'svelte';
  import { initOfflineStorage } from '@classic-homes/offline-core';
  import { useFileStorage, usePersistedForm, useOfflineCache } from '@classic-homes/offline-svelte';

  // Initialize storage at app startup (e.g., in +layout.svelte)
  onMount(async () => {
    await initOfflineStorage();
  });
</script>

Composables

useFileStorage

Manage file attachments with validation, preview generation, and persistence.

<script lang="ts">
  import { useFileStorage } from '@classic-homes/offline-svelte';

  const documents = useFileStorage({
    formKey: 'contact-form',
    fieldName: 'documents',
    maxFiles: 5,
    maxSizeBytes: 10 * 1024 * 1024, // 10MB
    acceptedTypes: ['application/pdf', 'image/*'],
    onValidationError: (errors) => {
      errors.forEach((e) => toast.error(e.message));
    },
  });

  async function handleDrop(files: File[]) {
    await documents.addFiles(files);
  }
</script>

{#if documents.isLoading}
  <Spinner />
{:else}
  <ul>
    {#each documents.files as file (file.id)}
      <li>
        {#if file.previewUrl}
          <img src={file.previewUrl} alt={file.filename} />
        {/if}
        <span>{file.filename}</span>
        <button onclick={() => documents.removeFile(file.id)}>Remove</button>
      </li>
    {/each}
  </ul>
{/if}

<DropZone ondrop={handleDrop} />

Options

| Option | Type | Description | | ------------------- | ---------- | ------------------------------------------------ | | formKey | string | Form identifier | | fieldName | string | Field name within the form | | maxFiles | number | Maximum number of files | | maxSizeBytes | number | Maximum file size in bytes | | acceptedTypes | string[] | Accepted MIME types (supports wildcards) | | expirationMs | number | File expiration time | | generatePreviews | boolean | Generate preview URLs for images (default: true) | | onValidationError | function | Callback for validation errors |

Return Value

| Property | Type | Description | | ------------------- | ----------------- | -------------------------------- | | files | PersistedFile[] | Current files (reactive) | | fileIds | string[] | File IDs for form submission | | isLoading | boolean | Loading state | | error | Error \| null | Current error | | addFiles | function | Add files (validates and stores) | | removeFile | function | Remove a file by ID | | clearFiles | function | Remove all files | | reload | function | Reload from storage | | getFilesForSubmit | function | Get File objects for FormData |

usePersistedForm

Automatically persist form state across page reloads.

<script lang="ts">
  import { usePersistedForm } from '@classic-homes/offline-svelte';
  import { z } from 'zod';

  const schema = z.object({
    name: z.string().min(1),
    email: z.string().email(),
    message: z.string(),
  });

  const form = usePersistedForm({
    formKey: 'contact-form',
    defaultValues: { name: '', email: '', message: '' },
    schema,
    debounceMs: 500,
    excludeFields: ['password'],
  });

  async function handleSubmit() {
    if (!form.isValid) return;
    await submitForm(form.values);
    await form.clearDraft();
  }
</script>

<form onsubmit|preventDefault={handleSubmit}>
  {#if form.hasDraft}
    <button type="button" onclick={form.restoreDraft}> Restore draft </button>
  {/if}

  <input
    name="name"
    value={form.values.name}
    oninput={(e) => form.setValue('name', e.currentTarget.value)}
  />
  {#if form.errors.name}
    <span class="error">{form.errors.name}</span>
  {/if}

  <input
    name="email"
    value={form.values.email}
    oninput={(e) => form.setValue('email', e.currentTarget.value)}
  />
  {#if form.errors.email}
    <span class="error">{form.errors.email}</span>
  {/if}

  <textarea
    name="message"
    value={form.values.message}
    oninput={(e) => form.setValue('message', e.currentTarget.value)}
  ></textarea>

  <button type="submit" disabled={!form.isValid || form.isSubmitting}> Submit </button>
</form>

Options

| Option | Type | Description | | --------------- | ----------- | ------------------------------------------ | | formKey | string | Unique form identifier | | defaultValues | T | Default form values | | schema | ZodSchema | Optional Zod validation schema | | debounceMs | number | Debounce time for auto-save (default: 500) | | excludeFields | string[] | Fields to exclude from persistence | | expirationMs | number | Draft expiration time | | autoRestore | boolean | Auto-restore on mount (default: false) |

Return Value

| Property | Type | Description | | -------------- | ---------------- | -------------------------------- | | values | T | Current form values (reactive) | | errors | FieldErrors<T> | Validation errors by field | | isValid | boolean | Whether form passes validation | | isDirty | boolean | Whether form has unsaved changes | | hasDraft | boolean | Whether a draft exists | | draftInfo | object | Draft metadata | | setValue | function | Set a field value | | setValues | function | Set multiple values | | reset | function | Reset to default values | | saveDraft | function | Manually save draft | | restoreDraft | function | Restore from draft | | clearDraft | function | Delete the draft |

useOfflineCache

Cache data with TTL and stale-while-revalidate support.

<script lang="ts">
  import { useOfflineCache } from '@classic-homes/offline-svelte';

  interface User {
    id: string;
    name: string;
    email: string;
  }

  let { userId } = $props<{ userId: string }>();

  const userCache = useOfflineCache<User>({
    key: `user:${userId}`,
    ttl: 5 * 60 * 1000, // 5 minutes
    tags: ['user'],
    staleWhileRevalidate: true,
    fetcher: () => fetchUser(userId), // Auto-fetch on mount
  });

  // Or manually fetch
  $effect(() => {
    userCache.get(() => fetchUser(userId));
  });
</script>

{#if userCache.isLoading}
  <Spinner />
{:else if userCache.error}
  <Error error={userCache.error} />
{:else if userCache.data}
  <div>
    <h1>{userCache.data.name}</h1>
    {#if userCache.isStale}
      <span>Updating...</span>
    {/if}
    <button onclick={() => userCache.refresh(() => fetchUser(userId))}> Refresh </button>
  </div>
{/if}

Options

| Option | Type | Description | | ---------------------- | ---------- | ------------------------------------- | | key | string | Cache key | | ttl | number | Time to live in milliseconds | | tags | string[] | Tags for bulk invalidation | | staleWhileRevalidate | boolean | Return stale data while fetching | | staleTime | number | How long stale data is acceptable | | fetcher | function | Auto-fetch function on initialization |

Return Value

| Property | Type | Description | | -------------- | ---------------- | ---------------------- | | data | T \| null | Cached data (reactive) | | isLoading | boolean | Loading state | | isStale | boolean | Whether data is stale | | error | Error \| null | Current error | | ttlRemaining | number \| null | Time until expiration | | get | function | Get cached or fetch | | set | function | Set cache value | | invalidate | function | Invalidate this key | | refresh | function | Force re-fetch | | has | function | Check if cached |

Utility Functions

<script lang="ts">
  import { invalidateCacheByTag, invalidateAllCache } from '@classic-homes/offline-svelte';

  async function handleLogout() {
    // Invalidate all user-related cache entries
    await invalidateCacheByTag('user');
  }

  async function clearAllCache() {
    await invalidateAllCache();
  }
</script>

Subpath Exports

Import individual composables for better tree-shaking:

// Import specific composable
import { useFileStorage } from '@classic-homes/offline-svelte/useFileStorage';
import { usePersistedForm } from '@classic-homes/offline-svelte/usePersistedForm';
import { useOfflineCache } from '@classic-homes/offline-svelte/useOfflineCache';

TypeScript

All composables are fully typed with generics:

<script lang="ts">
  interface User {
    id: string;
    name: string;
    email: string;
  }

  const userCache = useOfflineCache<User>({ key: 'user' });
  // userCache.data is User | null

  const form = usePersistedForm<User>({
    formKey: 'user',
    defaultValues: { id: '', name: '', email: '' },
  });
  // form.values is User
  // form.setValue('name', value) - 'name' is typed
</script>

Svelte 5 Runes

The composables use Svelte 5's runes for reactivity:

  • $state for reactive values
  • $effect for side effects and cleanup
  • Getter functions for derived values

This means the returned properties are reactive without needing stores:

<script lang="ts">
  const cache = useOfflineCache({ key: 'data' });

  // cache.data, cache.isLoading, etc. are reactive
  // No need for $: reactive statements
</script>

<!-- Automatically updates when data changes -->
{#if cache.data}
  <pre>{JSON.stringify(cache.data, null, 2)}</pre>
{/if}

Testing

npm test           # Run tests
npm run test:watch # Watch mode
npm run test:coverage # With coverage

License

MIT