DEV Community

Tom
Tom

Posted on

Prove an Inventory Cursor Walk Finished Before Replacing the Export

A store connector can fetch the first inventory page successfully and still produce an incomplete export. Publishing those rows as the complete stock view makes a transport problem look like missing stock.

The boundary worth testing is when a collected page set becomes eligible to replace the previous export. The following example keeps the candidate private until the cursor walk reaches the contract's explicit end marker.

Warehouse workstations with monitors, printers and handling equipment

Warehouse workstations at YX Fulfillment.

Give the adapter a precise page contract

In this small model, each page has an items array and a next field. next: null means the walk is complete. A nonempty string is an opaque cursor. Missing next, an empty string and an unexpected type are errors. These are deliberately chosen adapter rules, not a claim about every warehouse API.

An empty page with a next cursor is allowed: emptiness alone does not prove completion. A repeated cursor is rejected. A page limit also stops a server that keeps issuing new cursors without terminating.

const assert = require('node:assert/strict');

async function collectPages(fetchPage, maxPages = 100) {
  if (!Number.isSafeInteger(maxPages) || maxPages < 1) {
    throw new Error('invalid page limit');
  }
  const requested = new Set();
  const candidate = [];
  let cursor = null;
  for (let count = 0; count < maxPages; count++) {
    if (requested.has(cursor)) throw new Error('cursor cycle');
    requested.add(cursor);
    const page = await fetchPage(cursor);
    if (!page || !Array.isArray(page.items) ||
        !Object.hasOwn(page, 'next')) {
      throw new Error('invalid page');
    }
    const next = page.next;
    if (next !== null && (typeof next !== 'string' || next.length === 0)) {
      throw new Error('invalid cursor');
    }
    candidate.push(...page.items);
    if (next === null) return candidate;
    cursor = next;
  }
  throw new Error('page limit reached');
}

async function main() {
  const pages = new Map([
    [null, { items: [{ sku: 'CASE-BLUE' }], next: 'A' }],
    ['A', { items: [], next: 'B' }],
    ['B', { items: [{ sku: 'CASE-GREEN' }], next: null }]
  ]);
  const calls = [];
  const rows = await collectPages(async cursor => {
    calls.push(cursor);
    return pages.get(cursor);
  });
  assert.deepEqual(calls, [null, 'A', 'B']);
  assert.deepEqual(rows.map(row => row.sku), ['CASE-BLUE', 'CASE-GREEN']);
  assert.deepEqual(await collectPages(async () => ({ items: [], next: null })), []);
  await assert.rejects(collectPages(async () => ({ items: [] })), /invalid page/);
  await assert.rejects(collectPages(async () => ({ items: [], next: '' })), /invalid cursor/);
  await assert.rejects(collectPages(async () => ({ items: [], next: 7 })), /invalid cursor/);
  await assert.rejects(collectPages(async () => ({ items: [], next: 'A' })), /cursor cycle/);
  await assert.rejects(collectPages(async () => ({ items: [], next: 'A' }), 1), /page limit/);
  await assert.rejects(collectPages(async () => null), /invalid page/);
  await assert.rejects(collectPages(async () => ({}), 0), /invalid page limit/);

  let currentExport = ['previous complete export'];
  let pageCount = 0;
  await assert.rejects((async () => {
    const candidate = await collectPages(async () => {
      if (++pageCount === 1) return { items: ['partial'], next: 'later' };
      throw new Error('connection lost');
    });
    currentExport = candidate;
  })(), /connection lost/);
  assert.deepEqual(currentExport, ['previous complete export']);
  console.log('cursor collection checks passed');
}
main().catch(error => { console.error(error); process.exitCode = 1; });
Enter fullscreen mode Exit fullscreen mode

Save the block as inventory-pages.cjs and run node inventory-pages.cjs. The assertions cover a three-page walk with an empty middle page, a valid empty result, malformed pages, invalid cursors, a cursor cycle, the page bound and an interrupted walk that leaves the previous export unchanged. The Node.js strict assertion documentation describes the assertion interface. The Set reference explains the unique-value collection used to remember requested cursors.

Completion is one check, not the whole inventory guarantee

This collector does not validate row identities or quantities, freeze an upstream snapshot, or make a database replacement atomic. If stock changes between pages, a completed walk can still mix different moments in time. The real API's documented snapshot or consistency behavior needs separate handling. Duplicate rows, account scope, resource limits, timeouts and cancellation also belong in the production adapter.

The example buffers all rows in memory. A larger export can stage rows in a separate run and make that run visible only after completion and validation. An incomplete run must remain identifiable as incomplete; it should not overwrite the last known complete view by accident.

YX Fulfillment's public integration scope includes store and API connections. For an actual connection, page semantics and the point at which stock becomes usable should be verified against that connector. This is an executable design example, not a claim that the collector is deployed in a warehouse system.

Top comments (0)