On Your Own: Stock Report

The Course
•
5 min read

A new brief, not another reading queue

Build createStockReport(load, publish), returning an async refresh() function. load returns a promise for unknown data. Valid data is an array of records with a nonnegative integer quantity field. Publish the numeric total for the latest successful refresh, or the message “Invalid stock” for malformed data. A rejected latest load publishes its error message. Older successes and failures must be ignored. Keep input records unchanged.

Do not add a webpage before the core contract works. Write a pure total decoder, then the async ownership wrapper. Use injected functions and deferred promises to test the wrapper. You may consult explanations; record which hints you needed and retry a changed brief later.

For this first attempt, assume rejected operations use Error values. The required cases are a normal total, empty success, a malformed record, a rejected current load, and an older response finishing after a newer one. Cancellation and disposal are deliberate extensions, not hidden requirements.

Use the checkpoint block to make a first independent attempt and test one case. Finishing all variations may take longer; do not mark readiness merely because you copied the worked solution. You are checking whether you can choose and connect the tools yourself.

Predict and trace

Write the output before running. Follow the values through each call; if your prediction differs, identify the first step that surprised you.

Predict, then run JavaScript
const records = [{ quantity: 2 }, { quantity: 3 }];
let total = 0;

for (const record of records) {
  total += record.quantity;
}

console.log(total);
Check your prediction
5

This only demonstrates the data shape. It does not validate unknown input or protect async ownership.

Build it yourself

Exercise

On Your Own: Stock Report: practise

Implement the pure decoder and createStockReport. Print 5 for valid data and Invalid stock for a numeric string. Add the required schedule tests yourself before opening the solution. The worked answer includes both old success and old failure schedules.

Your working space JavaScript
// Start with decodeTotal(value), then wrap a load dependency.
Hint 1: the idea

Keep validation separate from the current-generation decision. A throw in the decoder belongs to the same guarded catch as a rejected load.

Hint 2: a first step

Capture mine before await; validate after fulfillment; only publish when mine still matches.

Show one solution
async function main() {
  function decodeTotal(value) {
    if (!Array.isArray(value)) {
      throw new Error("Invalid stock");
    }

    let total = 0;

    for (const item of value) {
      if (
        item === null ||
        typeof item !== "object" ||
        Array.isArray(item) ||
        !Number.isInteger(item.quantity) ||
        item.quantity < 0
      ) {
        throw new Error("Invalid stock");
      }

      total += item.quantity;
    }

    return total;
  }

  function createStockReport(load, publish) {
    let generation = 0;

    return async function refresh() {
      const mine = ++generation;

      try {
        const total = decodeTotal(await load());

        if (mine === generation) {
          publish(total);
        }
      } catch (error) {
        if (mine === generation) {
          publish(error.message);
        }
      }
    };
  }

  const values = [];

  await createStockReport(
    async () => [{ quantity: 2 }, { quantity: 3 }],
    value => values.push(value)
  )();
  await createStockReport(
    async () => [{ quantity: "2" }],
    value => values.push(value)
  )();
  console.log(values[0]);
  console.log(values[1]);

  if (decodeTotal([]) !== 0) {
    throw new Error("empty total");
  }

  const input = [{ quantity: 2 }];

  decodeTotal(input);

  if (input[0].quantity !== 2) {
    throw new Error("input changed");
  }

  const failures = [];

  await createStockReport(
    async () => {
      throw new Error("Offline");
    },
    value => failures.push(value)
  )();

  if (failures[0] !== "Offline") {
    throw new Error("current rejection");
  }

  function deferred() {
    let resolve;
    let reject;
    const promise = new Promise((yes, no) => {
      resolve = yes;
      reject = no;
    });

    return { promise, resolve, reject };
  }

  for (const olderFails of [false, true]) {
    const old = deferred();
    const recent = deferred();
    const tasks = [old.promise, recent.promise];
    const published = [];
    const refresh = createStockReport(
      () => tasks.shift(),
      value => published.push(value)
    );
    const first = refresh();
    const second = refresh();

    recent.resolve([{ quantity: 7 }]);
    await second;

    if (olderFails) {
      old.reject(new Error("Old failure"));
    } else {
      old.resolve([{ quantity: 1 }]);
    }

    await first;

    if (published.length !== 1 || published[0] !== 7) {
      throw new Error("stale publication");
    }
  }

  console.log("Empty, rejection, and latest-result checks passed");
}

main();
5
Invalid stock
Empty, rejection, and latest-result checks passed

Try an empty array and null record. Use deferred loads in the same controller instance to prove latest wins; two separate factories would not test shared generation ownership. Then add cancellation as a new contract.

Test a changed requirement

Try an empty array and null record. Use deferred loads in the same controller instance to prove latest wins; two separate factories would not test shared generation ownership. Then add cancellation as a new contract.

Explain before moving on

Can you explain the wrapper without reading its source? Which test would fail if the catch forgot its token guard?

Was this article helpful?

Your response is saved on this device.