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

@perdieminc/order-pacing

v0.1.0

Published

A library for managing order pacing

Readme

Order Pacing Engine

CI

Installation

npm install @perdieminc/order-pacing

Usage

import Redis from 'ioredis';
import { Engine, TimeframeMode, OrderSource } from '@perdieminc/order-pacing';

const redis = new Redis({
  host: 'localhost',
  port: 6379
});

const engine = new Engine({
  redis,
  bucket: 'storeId:locationId',
  timeframeMode: TimeframeMode.BEFORE_ONLY,
  timeZone: 'UTC',
  rules: [{
    ruleId: 'rule-1',
    timeFrameMinutes: 30,
    busyTimeMinutes: 15,
    categoryIds: [],
    weekDays: [],
    tags: ['delivery'],
    maxOrders: 10,
    maxItems: 100,
    maxAmountCents: 100000
  }]
});

await engine.add({
  orderId: '123',
  orderTime: new Date(),
  totalAmountCents: 5000,
  source: OrderSource.PERDIEM,
  tags: ['delivery'],
  items: [{
    itemId: 'item-1',
    quantity: 2,
    totalAmountCents: 2000,
    categoryId: 'cat-1'
  }]
});

const busyTimes = await engine.getBusyTimes();
const orders = await engine.getOrders();
const validation = await engine.validateOrderTime(new Date(), ['delivery']);
const stats = await engine.getOrdersStats(
  new Date(Date.now() - 24 * 60 * 60 * 1000),
  new Date(Date.now() + 24 * 60 * 60 * 1000)
);

API

new Engine({ redis, bucket, timeframeMode?, timeZone?, rules?, logger? })

Creates a new Engine instance.

  • redis: Redis instance from ioredis

  • bucket: Bucket identifier (e.g., storeId:locationId)

  • timeframeMode: Optional timeframe calculation mode. Options:

    • TimeframeMode.BEFORE_ONLY (default): Look back timeFrameMinutes from order time
    • TimeframeMode.CENTERED: Half of timeFrameMinutes on each side of order time (~timeFrameMinutes total)
    • TimeframeMode.AFTER_ONLY: Look forward timeFrameMinutes from order time
    • TimeframeMode.BEFORE_AND_AFTER: timeFrameMinutes on each side of order time (~2x timeFrameMinutes total)
  • timeZone: Optional IANA timezone name, e.g. 'America/New_York' (defaults to 'UTC'). Used only to evaluate rule weekDays/startTime/endTime against the order time; all timestamps are stored as UTC epoch seconds.

  • rules: Optional array of rules (defaults to []). Rules determine when to apply busy time based on order volume in a time window:

    rules: [{
      ruleId: 'rule-1',              // Unique rule identifier
      timeFrameMinutes: 30,        // Time window in minutes
      busyTimeMinutes: 15,         // Busy time to apply in minutes (minimum 5)
      categoryIds: [],      // Optional: Filter by category IDs (omitted or empty = all categories)
      weekDays: [],         // Optional: Filter by week days 0-6, 0 = Sunday (omitted or empty = all days)
      startTime: '09:00',   // Optional: Start time for rule (HH:mm or HH:mm:ss, seconds are ignored)
      endTime: '17:00',     // Optional: End time for rule (HH:mm or HH:mm:ss, seconds are ignored)
      tags: ['delivery'],   // Optional: Filter by order tags (omitted or empty = all orders)
      maxOrders: 10,        // Optional: Max orders threshold
      maxItems: 100,        // Optional: Max items threshold
      maxAmountCents: 100000 // Optional: Max total amount in cents threshold
    }]

    At least one threshold (maxOrders, maxItems, or maxAmountCents) must be set. When any threshold is reached (>=) within the time window — the order being added counts toward its own window — the busy time is applied. Multiple rules can be set to handle different scenarios; each matching rule creates its own busy time. The constructor throws if any rule is invalid, naming the offending rule.

    A rule scoped by tags only counts matching orders toward its thresholds, only trips on matching orders, and its busy times only delay matching orders — a ['delivery'] cap never blocks pickups. An order matches when it carries every rule tag; tag vocabulary is the consumer's to define (e.g. pickup, delivery, catering). All rule filters combine with AND.

  • logger: Optional logger instance (defaults to a noop logger; a consoleLogger is also exported)

add(inputOrder)

Adds an order to the engine. If a rule's threshold is reached, a busy time period of exactly busyTimeMinutes is created, ending at the order's scheduled time or busyTimeMinutes from now, whichever is later. Throws a TypeError if orderTime is not a valid Date, orderId is empty, or tags is not an array of non-empty strings.

await engine.add({
  orderId: '123',
  orderTime: new Date(),
  totalAmountCents: 5000, // Amount in cents ($50.00)
  source: OrderSource.PERDIEM, // or OrderSource.OTHER
  tags: ['pickup'],
  items: [{
    itemId: 'item-1',
    quantity: 2,
    totalAmountCents: 2000,
    categoryId: 'cat-1'
  }]
});

Note: Only orders with source: OrderSource.PERDIEM will trigger busy time calculations.

getBusyTimes()

Returns an array of busy time entries:

[
  {
    busyTimeId: string,           // Busy time unique identifier
    ruleId: string,               // Rule identifier that triggered this busy time
    startTime: Date,              // Start of busy period
    endTime: Date,                // End of busy period
    orderTimeSeconds: number,     // Order time in seconds
    currentTimeSeconds: number,   // Current time in seconds when busy time was created
    busyTimeSeconds: number,      // Duration in seconds
    scope: string[],              // The tripping rule's tags; only matching orders are delayed ([] = all)
    busyTimeContext: {
      totalAmountCents: number,   // Total amount in cents from all orders in the time window
      totalItems: number,         // Total items from all orders in the time window
      totalOrders: number,        // Total number of orders in the time window
      categoryIds: string[]       // All category IDs from all orders in the time window
    },
    threshold: {
      type: 'orders' | 'items' | 'amount', // Type of threshold that was reached
      value: number,              // Actual value that reached the threshold
      limit: number,              // Threshold limit that was reached
      categoryIds: string[]       // The rule's configured categoryIds filter (empty if unfiltered)
    }
  }
]

getOrders()

Returns an array of order entries:

[
  {
    orderId: '123',
    items: [{
      itemId: 'item-1',
      quantity: 2,
      totalAmountCents: 2000,
      categoryId: 'cat-1'
    }],
    totalAmountCents: 5000,
    source: OrderSource.PERDIEM, // or OrderSource.OTHER
    orderTime: Date,
    orderTimeSeconds: number,
    currentTimeSeconds: number
  }
]

validateOrderTime(orderTime, tags?)

Checks if an order placed at the given time would fall within a busy period that applies to its tags (tags defaults to []). Only busy periods whose scope matches are considered — a ['delivery']-scoped busy period never delays a pickup order. Consecutive matching busy periods chain: if pushing the order past one period lands it inside the next, the wait extends until the first free moment. Already-ended busy periods are ignored. Throws a TypeError if orderTime is not a valid Date or tags is not an array of non-empty strings. Returns wait period information:

const validation = await engine.validateOrderTime(new Date(), ['delivery']);

{
  waitPeriodSeconds: number,  // Seconds to wait until one second past the last delaying busy period (0 if not busy)
  ordersInWindow: number      // totalOrders recorded by the busy period that delayed the order (0 if not busy)
}

getOrdersStats(startTime, endTime)

Retrieves order statistics for a specific time range. Note that orders are only retained for 7 days (see Data retention below), so older ranges return partial or no data. Returns an array of orders sorted by order time:

[
  {
    orderId: '123',
    orderTime: Date,
    source: OrderSource.PERDIEM // or OrderSource.OTHER
  }
]

Example:

const stats = await engine.getOrdersStats(
  new Date(Date.now() - 24 * 60 * 60 * 1000),
  new Date(Date.now() + 24 * 60 * 60 * 1000)
);

Data retention

  • Orders are kept for 7 days past their order time; busy times for 1 day past their triggering order's time.
  • Pruning happens lazily on engine calls (add() and the read methods) — there is no TTL on the Redis keys, so buckets that stop receiving traffic keep their last entries until the next call.
  • validateOrderTime() ignores busy periods that have already ended, so lingering expired entries do not affect results.

Redis requirements

  • ioredis (v5) is a peer dependency; the engine uses an injected client and never opens its own connection.
  • Each bucket uses two sorted sets: orders:{bucket} and busytimes:{bucket}.
  • Every method, including the get* reads, may prune expired entries, so the engine needs a writable Redis connection.