src/ids/clientOrderIndex.ts
v0.2.1 · 1.8 KB
// `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;
},
};
}