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

@winglet/data-loader

v0.14.0

Published

Batching and caching utility for asynchronous data fetching inspired by GraphQL DataLoader, providing efficient request batching and built-in cache system with TypeScript support

Downloads

818

Readme

@winglet/data-loader

TypeScript Batching Caching


Overview

@winglet/data-loader is a batching and caching utility for asynchronous data fetching.

This implementation is inspired by the original "Loader" API developed by @schrockn at Facebook in 2010, which was designed to simplify and consolidate various key-value store back-end APIs.

While conceptually based on GraphQL DataLoader, this version is a ground-up rewrite focused on performance optimizations, type safety, and adaptation to specific runtime requirements.

Key Features

  • Batching: Automatically groups multiple individual requests into efficient batches
  • Caching: Built-in cache system that prevents duplicate requests
  • Type Safety: Full TypeScript support with compile-time type validation
  • Flexibility: Support for custom cache implementations and batch scheduling
  • Performance: Optimized algorithms for high throughput processing

Installation

# Using npm
npm install @winglet/data-loader

# Using yarn
yarn add @winglet/data-loader

# Using pnpm
pnpm add @winglet/data-loader

Compatibility

Supported Environments:

  • Node.js 14.0.0 or higher
  • Modern browsers (with ES2020 support)

For Legacy Environment Support: Use transpilers like Babel to convert the code to match your target environment.


Basic Usage

Simple Example

import { DataLoader } from '@winglet/data-loader';

// Function to batch load user information
const userBatchLoader = async (userIds: ReadonlyArray<string>) => {
  // In practice, fetch data from database or API
  const users = await fetchUsersFromDatabase(userIds);
  return users;
};

// Create DataLoader instance
const userLoader = new DataLoader(userBatchLoader);

// Load individual users (automatically batched)
const user1Promise = userLoader.load('user1');
const user2Promise = userLoader.load('user2');
const user3Promise = userLoader.load('user3');

// All requests are processed in a single batch
const [user1, user2, user3] = await Promise.all([
  user1Promise,
  user2Promise,
  user3Promise,
]);

Loading Multiple Keys Simultaneously

const userIds = ['user1', 'user2', 'user3', 'user4'];
const usersOrErrors = await userLoader.loadMany(userIds);

// Each result is either a value or an error
usersOrErrors.forEach((userOrError, index) => {
  if (userOrError instanceof Error) {
    console.error(`Failed to load user ${userIds[index]}:`, userOrError);
  } else {
    console.log(`User data:`, userOrError);
  }
});

Advanced Configuration

Cache Configuration

// Default caching with Map (default behavior)
const userLoader = new DataLoader(userBatchLoader);

// Custom cache implementation
const customCache = new Map<string, Promise<User>>();
const userLoaderWithCustomCache = new DataLoader(userBatchLoader, {
  cache: customCache,
});

// Disable caching
const userLoaderNoCache = new DataLoader(userBatchLoader, {
  cache: false,
});

Batch Size Limitation

const userLoader = new DataLoader(userBatchLoader, {
  maxBatchSize: 50, // Process maximum 50 items per batch
});

Custom Cache Key Function

interface UserKey {
  id: string;
  version: number;
}

const userLoader = new DataLoader<UserKey, User, string>(userBatchLoader, {
  // Convert composite key to string
  cacheKeyFn: (key: UserKey) => `${key.id}:${key.version}`,
});

Custom Batch Scheduler

const userLoader = new DataLoader(userBatchLoader, {
  // Use setTimeout for delayed execution
  batchScheduler: (callback) => {
    setTimeout(callback, 10);
  },
});

API Reference

DataLoader Class

Constructor

constructor(
  batchLoader: BatchLoader<Key, Value>,
  options?: DataLoaderOptions<Key, Value, CacheKey>
)
  • batchLoader: Async function that takes an array of keys and returns an array of values
  • options: Optional configuration object

Methods

load(key: Key): Promise<Value>

Loads a value for a single key. Automatically batched and cached.

const user = await userLoader.load('user123');
loadMany(keys: ReadonlyArray<Key>): Promise<Array<Value | Error>>

Loads values for multiple keys simultaneously. Other values are collected normally even if one key fails.

const results = await userLoader.loadMany(['user1', 'user2', 'user3']);
clear(key: Key): this

Removes a specific key from the cache.

userLoader.clear('user123'); // Remove 'user123' from cache
clearAll(): this

Removes all keys from the cache.

userLoader.clearAll(); // Clear entire cache
prime(key: Key, value: Value | Promise<Value> | Error): this

Programmatically adds a value to the cache for a given key.

// Cache a known value
userLoader.prime('user123', userData);

// Cache with Promise
userLoader.prime('user456', fetchUserPromise);

// Cache error state
userLoader.prime('invalidUser', new Error('User not found'));

Configuration Options

DataLoaderOptions

interface DataLoaderOptions<Key, Value, CacheKey = Key> {
  /** Name of the loader (for debugging) */
  name?: string;

  /** Cache Map object or false (disabled) */
  cache?: MapLike<CacheKey, Promise<Value>> | false;

  /** Function for scheduling batch execution */
  batchScheduler?: (task: () => void) => void;

  /** Function that converts loader keys to cache keys */
  cacheKeyFn?: (key: Key) => CacheKey;

  /** Maximum batch size to process at once */
  maxBatchSize?: number;
}

BatchLoader Function

type BatchLoader<Key, Value> = (
  keys: ReadonlyArray<Key>,
) => Promise<ReadonlyArray<Value | Error>>;

The batch loader function must guarantee:

  • Return an array of results with the same length as the input key array
  • Return either a value or Error object for each key
  • Maintain order consistency between keys and results

Usage Examples

Database Query Optimization

import { DataLoader } from '@winglet/data-loader';

// Solving N+1 query problem
class UserService {
  private userLoader: DataLoader<string, User>;
  private postLoader: DataLoader<string, Post[]>;

  constructor() {
    this.userLoader = new DataLoader(this.batchLoadUsers.bind(this));
    this.postLoader = new DataLoader(this.batchLoadPostsByUserId.bind(this));
  }

  private async batchLoadUsers(userIds: ReadonlyArray<string>) {
    const users = await db.users.findMany({
      where: { id: { in: [...userIds] } },
    });

    // Sort results to match key order
    return userIds.map(
      (id) =>
        users.find((user) => user.id === id) ||
        new Error(`User ${id} not found`),
    );
  }

  private async batchLoadPostsByUserId(userIds: ReadonlyArray<string>) {
    const posts = await db.posts.findMany({
      where: { authorId: { in: [...userIds] } },
    });

    return userIds.map((userId) =>
      posts.filter((post) => post.authorId === userId),
    );
  }

  async getUser(id: string): Promise<User> {
    return this.userLoader.load(id);
  }

  async getUserPosts(userId: string): Promise<Post[]> {
    return this.postLoader.load(userId);
  }
}

Usage in GraphQL Resolvers

// Add DataLoader to GraphQL context
interface Context {
  loaders: {
    user: DataLoader<string, User>;
    posts: DataLoader<string, Post[]>;
  };
}

// Use in resolvers
const resolvers = {
  Post: {
    author: async (post: Post, args: any, { loaders }: Context) => {
      return loaders.user.load(post.authorId);
    },
  },
  User: {
    posts: async (user: User, args: any, { loaders }: Context) => {
      return loaders.posts.load(user.id);
    },
  },
};

Error Handling and Retry

const userLoader = new DataLoader(async (userIds: ReadonlyArray<string>) => {
  try {
    const users = await fetchUsersWithRetry(userIds);
    return users;
  } catch (error) {
    // If entire batch fails, return same error for each key
    return userIds.map(() => error);
  }
});

// When only specific users fail
const userLoaderPartialFailure = new DataLoader(
  async (userIds: ReadonlyArray<string>) => {
    const results = await Promise.allSettled(
      userIds.map((id) => fetchSingleUser(id)),
    );

    return results.map((result, index) => {
      if (result.status === 'fulfilled') {
        return result.value;
      } else {
        return new Error(
          `Failed to load user ${userIds[index]}: ${result.reason}`,
        );
      }
    });
  },
);

Performance Considerations

  • Batch Size: Set maxBatchSize appropriately to balance memory usage and processing efficiency
  • Cache Strategy: For long-running applications, call clearAll() periodically or implement TTL-based cache to prevent memory leaks
  • Batch Scheduler: Use custom schedulers instead of the default process.nextTick to control batch timing

Acknowledgments

This implementation is inspired by the original "Loader" API developed by @schrockn at Facebook in 2010 and the GraphQL DataLoader project. We express our deep gratitude for the concepts and ideas provided by these excellent open source projects.


License

This repository is provided under the MIT License. See the LICENSE file for details.


Contact

For questions or suggestions about this project, please create a GitHub issue.