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

jolly-donny

v1.1.0

Published

A TypeScript library for powerful offline data management. Supports pluggable storage providers (LocalStorage, IndexedDB), LINQ-style query API, async CRUD operations, change tracking, and enhanced QueryableArray utilities.

Downloads

19

Readme

Offline Storage Library

A TypeScript library for powerful offline data management. Supports pluggable storage providers (LocalStorage, IndexedDB, SQLite via WASM), LINQ-style query API, async CRUD operations, change tracking, and enhanced QueryableArray utilities.

✨ Features

  • Generic Collections – Create type-safe collections for structured data management.

  • CRUD Operations – Perform standard Create, Read, Update, and Delete actions with ease.

  • Flexible Querying – Use intuitive query functions to filter and transform data.

  • Asynchronous Execution – All operations are async to keep your app responsive and non-blocking.

  • Pluggable Storage Providers – Works with various backends: LocalStorage, IndexedDB, File System API, and SQLite (WASM).

  • Data Synchronization – Easily sync local data with remote APIs using built-in helpers.

    • Transformable Fetch – Fetch method supports optional transformation logic or returns raw JSON.

  • Timeout & Cancellation Support – Use AbortController to handle request timeouts and cancellations.

  • Built with TypeScript – Enjoy full type safety and enhanced developer tooling.

    Easily Extensible – Add your own custom storage providers or extend existing features effortlessly.

Installation


npm  i  jolly-donny

Usage

  1. Initialize OfflineStorage


import { OfflineStorage, IndexedDBProvider,LocalStorageProvider, PersistedEntity } from  'your-package-name';

  

class  User  extends  PersistedEntity<User> {

constructor(public  name: string, public  age: number) {

super();

}

}

  

const  provider = new  IndexedDBProvider();

// const provider = new LocalStorageProvider();

  

const  storage = new  OfflineStorage(provider, 'userStorage');

  

async  function  initStorage() {

await  storage.init();

console.log('Storage initialized');

}

  

initStorage();

  
  1. Add and Retrieve Data


  

async  function  addAndRetrieveUsers() {

const  users = storage.getCollection<User>('users');

  

const  newUser = new  User('John Doe', 25);

await  users.insert(newUser);

  

const  allUsers = await  users.all();

console.log('All users:', allUsers);

  

const  filteredUsers = await  users.find(user  =>  user.age > 25);

console.log('Filtered users:', filteredUsers);

  

const  firstUser = await  users.first();

console.log('First user:', firstUser);

}

  

addAndRetrieveUsers();

  
  
  1. Update and Delete Data


  

async  function  updateAndDeleteUsers() {

const  users = storage.getCollection<User>('users');

  

const  firstUser = await  users.first();

if (firstUser) {

firstUser.age = 35;

await  users.update(firstUser);

console.log('User updated:', firstUser);

  

await  users.delete(firstUser);

console.log('User deleted:', firstUser);

}

}

  

updateAndDeleteUsers();

  
  1. Using QueryableArray


  

async  function  useQueryableArray() {

const  users = storage.getCollection<User>('users');

  

const  usersArray = await  users.all();

const  filteredUsers = usersArray

.where(user  =>  user.age > 20)

.orderBy(user  =>  user.name)

.take(5);

  

console.log('Filtered and ordered users:', filteredUsers);

}

  

useQueryableArray();

  
  1. Change Tracking


  

storage.onChange = (change) => {

console.log('Data changed:', change);

};

  

async  function  insertUserWithChangeTracking() {

const  users = storage.getCollection<User>('users');

const  newUser = new  User('Jane Smith', 25);

await  users.insert(newUser);

}

  

insertUserWithChangeTracking();

  
  1. Using the fetch Helper for Data Synchronization


interface  IMenu {

dishes: IDish[];

categories: ICategory[];

}

  

interface  IDish {

id: number;

category: number;

title: string;

priceString: string;

sku: number;

description: string;

uuid: string;

showInLimited: boolean;

}

  

interface  ICategory {

id: number;

name: string;

}

  

class  ExtendedDish {

id: number;

category: number;

title: string;

priceString: string;

sku: number;

description: string;

uuid: string;

showInLimited: boolean;

categoryName: string;

  

get  price(): number {

const  parsedPrice = parseFloat(this.priceString);

return  isNaN(parsedPrice) ? 0 : parsedPrice;

}

  

set  price(value: number) {

this.priceString = value.toFixed(2);

}

}

  

async  function  syncAndStoreDishes() {

try {

const  extendedDishes = await  OfflineStorage.fetch<IMenu, ExtendedDish[]>('fake-api/data.json', (result) => {

const  dishes = result.dishes.map((dish) => {

const  category = result.categories.find((cat) =>  cat.id === dish.category);

const  extendedDish = new  ExtendedDish();

Object.assign(extendedDish, dish);

extendedDish.categoryName = category ? category.name : 'Unknown';

return  extendedDish;

});

return  dishes;

});

  

for (const  dish  of  extendedDishes) {

const  existingDish = await  storage.getCollection<ExtendedDish>('dishStorage').find((d) =>  d.uuid === dish.uuid);

if (!existingDish || existingDish.length === 0) {

await  storage.getCollection<ExtendedDish>('dishStorage').insert(dish);

}

}

  

const  storedDishes = await  storage.getCollection<ExtendedDish>('dishStorage').all();

console.log('Stored dishes:', storedDishes);

} catch (error) {

console.error('Synchronization failed:', error);

}

}

  

syncAndStoreDishes();

  
  1. Using Formatters with PersistedEntityBuilder


import { PersistedEntity, PersistedEntityBuilder, IFormatter } from  'your-package-name';

  

class  CustomStringFormater  implements  IFormatter<string> {

format(value: string): string {

return  value.toLowerCase(); // always store the value in lower case

}

parse(value: string): string {

return  value.toUpperCase(); // always return the value in upper case

}

}

  

class  CustomDateFormater  implements  IFormatter<Date | null> {

format(value: Date | null): Date | null {

// Store the Date object as is

return  value;

}

parse(value: Date | null): Date | null {

if (!value) {

return  null;

}

value.setHours(0, 0, 0, 0); // set the time to 00:00:00.000

return  value;

}

}

  

class  User  extends  PersistedEntity<User> {

id: string;

created: number;

lastModified: number;

name: string;

age: number;

birthDate: Date | null = null;

  

constructor(name: string, age: number) {

super(new  PersistedEntityBuilder<User>()

.addFormatter('name', new  CustomStringFormater())

.addFormatter('birthDate', new  CustomDateFormater())

);

  

this.id = crypto.randomUUID();

this.created = Date.now();

this.lastModified = Date.now();

this.name = name;

this.age = age;

this.birthDate = new  Date();

}

}

  

// Example usage:

async  function  exampleFormatters() {

const  users = storage.getCollection<User>('users');

const  newUser = new  User('Example User', 30);

await  users.insert(newUser);

  

const  allUsers = await  users.all();

console.log('Users with formatters:', allUsers);

}

  

exampleFormatters();

  
  1. Using the FileSystemProvider for File-Based Storage

The FileSystemProvider enables storing and loading collections using the File System Access API (in supported browsers).


import {

FileSystemProvider,

OfflineStorage,

IProviderConfig,

} from  'jolly-donny';

import { IMenu } from  './types'; // Your data interfaces

  

// Optional: define a parser to extract collections from a loaded file

const  dishFileParser = (content: string): Map<string, any> => {

const  parsed: IMenu = JSON.parse(content);

const  collections = new  Map<string, any>();

if (Array.isArray(parsed.dishes)) collections.set('dishes', parsed.dishes);

if (Array.isArray(parsed.categories)) collections.set('categories', parsed.categories);

return  collections;

};

  

async  function  useFileSystemProvider() {

const  providerConfig: IProviderConfig = {

parser:  dishFileParser,

};

  

const  provider = new  FileSystemProvider(providerConfig);

const  storage = new  OfflineStorage(provider, 'menu');

await  storage.init();

  

const  dishes = await  storage.getCollection('dishes').all();

console.log('Loaded dishes from file:', dishes);

  

// Modify and save

if (dishes.length > 0) {

dishes[0].price = 99;

await  storage.update('dishes', dishes[0]);

await  storage.save(); // Save back to file

}

}

Note: The File System API is available in Chromium-based browsers like Chrome and Edge. When init() is called, the user will be prompted to select a file.

  1. Using the SQLiteSchemeProvider

This example demonstrates how to use the SQLiteSchemeProvider for storing and retrieving data in an SQLite database.


  

import { SQLiteSchemeProvider } from  'jolly-donny';

  

// Create a new instance of the SQLiteProvider

const  provider = new  SQLiteSchemeProvider();

  

// Initialize the provider with a storage name

provider.init('notes-db').then(async () => {

  

console.log('SQLiteProvider initialized');

  

// Create a new note object

const  newNote = { title:  'First Note', content:  'This is the content of the first note.' };

  

// Insert the note into the database

await  provider.update('notes', newNote);

  

// Retrieve all notes from the database

const  allNotes = await  provider.all('notes');

console.log('All notes:', allNotes);

}).catch((error) => {

console.error('Error initializing SQLite provider:', error);

});