Skip to content
markpaper

src/ids/clientOrderIndex.ts

v0.2.1 · 1.8 KB

Download file
// `client_order_index` counter (knowledge base: orders.md §1).
//
// The index must be unique per account; it is YOUR identifier for matching "what I placed" with
// "what is in the book". A monotonic counter seeded from the process start time, modulo 2e9, is
// enough. Deduplication by the exchange on this index is NOT confirmed: it is for matching,
// not a protection against double sends.

/** Default counter modulus (fits comfortably in an int32-sized field). */
export const DEFAULT_CLIENT_ORDER_INDEX_MODULO = 2_000_000_000;

export interface ClientOrderIndexOptions {
  /** Clock used for the seed. Default `Date.now`. */
  now?: () => number;
  /** Explicit seed (e.g. restored from a journal); overrides the clock-derived one. */
  seed?: number;
  /** Default {@link DEFAULT_CLIENT_ORDER_INDEX_MODULO}. */
  modulo?: number;
}

export interface ClientOrderIndexCounter {
  /** Next index (increments). */
  next(): number;
  /** Last issued index without incrementing. */
  current(): number;
}

/** Seed from a clock: `floor(nowMs / 1000) % 1e9`, so two processes started at different seconds do not overlap soon. */
export function clientOrderIndexSeed(nowMs: number): number {
  return Math.floor(nowMs / 1000) % 1_000_000_000;
}

export function createClientOrderIndex(opts: ClientOrderIndexOptions = {}): ClientOrderIndexCounter {
  const modulo = Math.floor(opts.modulo ?? DEFAULT_CLIENT_ORDER_INDEX_MODULO);
  if (!(modulo > 1)) throw new RangeError(`modulo must be > 1, got ${String(opts.modulo)}`);
  let seq = (opts.seed ?? clientOrderIndexSeed((opts.now ?? Date.now)())) % modulo;
  if (!Number.isInteger(seq) || seq < 0)
    throw new RangeError(`seed must be a non-negative integer, got ${String(opts.seed)}`);
  return {
    next() {
      seq = (seq + 1) % modulo;
      return seq;
    },
    current() {
      return seq;
    },
  };
}
All files