numbering
| Subpath | exported from @noy-db/hub root (no dedicated subpath) |
| Factory | withDeferredNumbering() |
| Layer | with-commit |
What it does
Deferred numbering hands out gap-free, duplicate-free serial numbers — invoice numbers, fiscal receipt numbers, anything a regulator expects to increment by exactly one with no reuse — to records in a collection. The sequence service does the same job immediately, but it needs the store to support an atomic compare-and-swap (CAS) write: check the current counter and write the next value in one race-free operation. Not every backend can do that — @noy-db/to-file (local disk / USB storage), for example, has no CAS and would make sequence() throw SequenceOfflineError.
Deferred numbering works around the missing CAS. Instead of allocating a number the instant you ask, it records that a number is wanted for a given record, and later — when you explicitly run a numbering pass — it assigns numbers to every record whose write is provably finished, in the order the store actually committed them. The tradeoff: numbers are not available immediately, and you are responsible for triggering passes.
Prerequisites
- A store clock — the store must implement
getStoreTime()and advertisecapabilities.serverWriteTime, ornext()/runNumberingPass()throwNumberingUncertaintyError. BothtoMemory()andtoFile()provide it; check other adapters' docs. - A scheduled numbering pass —
runNumberingPass()is never called automatically; your application triggers it (cron, queue worker, end of batch).
Opt in
Unlike most with*() capabilities, deferred numbering has no strategy option on createNoydb(). Instead, pass one withDeferredNumbering() descriptor per series in the numbering array:
import { createNoydb, withDeferredNumbering } from '@noy-db/hub'
import { toMemory } from '@noy-db/to-memory'
const db = await createNoydb({
user: 'alice',
secret: 's3cret',
store: toMemory(), // any store with capabilities.serverWriteTime — see Limits below
numbering: [
withDeferredNumbering({
series: 'invoices',
collection: 'sales',
field: 'fiscalNumber',
}),
],
})
Usage
1. Enqueue a record for numbering
const vault = await db.openVault('acme')
const sales = vault.collection('sales')
await sales.put('order-42', { customer: 'Ada', total: 100 })
const pending = vault.sequence('invoices').next({ for: 'order-42' })
// pending is a Promise<number> — it does not resolve until a numbering pass runs
2. Run a numbering pass
const assignments = await vault.runNumberingPass('invoices')
// → [{ recordId: 'order-42', serial: 1 }]
const numbered = await sales.get('order-42')
// → { customer: 'Ada', total: 100, fiscalNumber: 1 }
await pending // 1 — the next() promise from step 1 now resolves
3. Peek at the last assigned serial
const last = await vault.sequence('invoices').peek()
// → 1
API reference
withDeferredNumbering(config)
Declares one deferred-numbering series. Pass one or more results in the numbering array of createNoydb().
function withDeferredNumbering(config: {
series: string
collection: string
field: string
settleWindowMs?: number
}): DeferredNumberingConfig
| Option | Type | Default | Meaning |
|---|---|---|---|
series | string | — | The name passed to vault.sequence(series). |
collection | string | — | The collection holding the records to number. |
field | string | — | The field on each record where the assigned serial is written. |
settleWindowMs | number | 0 | Accepted and stored on the config, but not currently read by runNumberingPass() — eligibility today is governed only by the store-clock commit-wait interval described in Limits below. |
vault.sequence(series).next({ for: recordId })
On a series declared via withDeferredNumbering, next() requires a for (the id of the record to number) and returns Promise<number> that resolves once a numbering pass processes that record. Calling next() without for throws ValidationError.
vault.sequence(series).peek()
Returns Promise<number> — the last serial assigned to the series so far, or 0 if no pass has run yet. Read-only; never allocates a number.
vault.runNumberingPass(series)
function runNumberingPass(series: string): Promise<Assignment[]>
interface Assignment {
recordId: string
serial: number
}
Runs one numbering pass for series: finds every enqueued record whose commit-time interval is fully in the past relative to the store's current time, orders them by commit time (then by record id), assigns the next serials in order, writes the assigned field on each record through the collection layer (so caches, indexes, and materialized views stay coherent), and advances the series' last-serial marker with one atomic write. Returns the assignments made in this pass — [] if nothing was ready yet, or if a concurrent pass won the race (the next pass reconciles; no serial is lost or duplicated).
runNumberingPass is never called automatically — trigger it on whatever schedule your application needs (a cron job, a queue worker, the end of a batch import).
Limits & gotchas
- Requires a store clock. The store must implement
getStoreTime()and advertisecapabilities.serverWriteTime; otherwisenext()andrunNumberingPass()throwNumberingUncertaintyError. BothtoMemory()(@noy-db/to-memory) andtoFile()(@noy-db/to-file) provide it; check a given adapter's own docs for others. - Passes are manual. noy-db never runs a numbering pass on its own.
- No formatted serials, no seeding. The
formatrender template andseedTo()thatsequence()supports for CAS-backed series apply to CAS sequences only — calling either on a deferred series throwsValidationError. - A settle delay is inherent. An entry becomes eligible only once the store's current time interval starts after the entry's own commit interval ended, so there is a short, store-clock-uncertainty-bounded delay between enqueueing and eligibility. The
settleWindowMsconfig option exists but is not read by the current pass logic — it has no effect on timing. - Deleted records don't burn a number. If a record is deleted before its pass runs, the pass skips it without assigning it a serial.
See also
- sequence — the CAS-backed, immediate alternative for stores that support atomic compare-and-swap.
- store capabilities — per-adapter store capabilities.
- Services overview · Orientation map
- Proven by showcase
132-deferred-numbering.