Browse recipes

Retry queued work after connectivity returns

Keep an order visible while delivery waits for connectivity.

Table of contents

APIs used

mount
Starts a Valyrian.js application on a DOM target.
onRemove
Runs after the component subtree detaches from the rendered tree.
update
Requests a render pass for the mounted view.
v
Creates the vnode description that Valyrian.js renders as part of a view.
OfflineQueue
Stores operations, processes them when the network is available and exposes pending and failed counts plus syncing state.
NetworkManager
Observes browser connectivity and signal quality.

Run this recipe

Starting project
Use the local TSX and ESM project from Stage 1 with Valyrian.js installed and its ESM browser build configured.
File
Replace src/client-entry.tsx with this recipe snippet.
Run
Run npm run build, serve public with npx --yes serve@14.2.5 public --listen 8000, open http://localhost:8000 and use the example's visible controls.
Observable result
Choose Retry first failed order to call retryOne(id), Retry all failed orders to call retryAll(), or Discard failed orders to call discardFailed(). Retries switch the delivery response to 204.

Starting point

Create an order queue with a network manager and an asynchronous delivery handler.

Steps

Choose Create failed order. It enqueues an order and runs two sync attempts. Reproducible 503 responses, maxRetries: 1 and a 10 ms linear backoff move its ID to failed.

ts
import { mount, onRemove, update, v } from "valyrian.js";
import { OfflineQueue } from "valyrian.js/offline";
import { NetworkManager } from "valyrian.js/network";

const exampleState = {
  nextOrder: 1,
  rejectDelivery: true,
  lastAction: "Ready",
};
const network = new NetworkManager();
const queue = new OfflineQueue({
  id: "orders",
  network,
  maxRetries: 1,
  backoff: { strategy: "linear", baseMs: 10, maxMs: 10 },
  handler: async (operation) => {
    const response = exampleState.rejectDelivery
      ? new Response(JSON.stringify({ message: "Delivery unavailable" }), {
          status: 503,
          statusText: "Service Unavailable",
          headers: { "Content-Type": "application/json" },
        })
      : new Response(null, { status: 204 });

    if (response.ok === false) {
      throw new Error(`Order delivery failed (${response.status})`);
    }
    exampleState.lastAction = `Delivered ${operation.id}`;
  },
});

const stop = queue.on("change", () => update());

async function createFailedOrder() {
  exampleState.rejectDelivery = true;
  queue.enqueue({
    type: "create-order",
    payload: { sku: `A-${exampleState.nextOrder}` },
  });
  exampleState.nextOrder += 1;
  await queue.sync();
  await queue.sync();
  exampleState.lastAction = "Created one failed order";
  update();
}

async function retryOneFailed() {
  const operation = queue.failed()[0];
  if (!operation) {
    exampleState.lastAction = "No failed order to retry";
    update();
    return;
  }
  exampleState.rejectDelivery = false;
  queue.retryOne(operation.id);
  await queue.sync();
  exampleState.lastAction = `Retried ${operation.id}`;
  update();
}

async function retryAllFailed() {
  exampleState.rejectDelivery = false;
  queue.retryAll();
  await queue.sync();
  exampleState.lastAction = "Retried all failed orders";
  update();
}

function discardAllFailed() {
  queue.discardFailed();
  exampleState.lastAction = "Discarded all failed orders";
  update();
}

function QueueStatus() {
  onRemove(() => {
    stop();
    queue.destroy();
    network.destroy();
  });
  const state = queue.state();
  const summary = `Pending: ${state.pending}; failed: ${state.failed}; syncing: ${state.syncing}`;
  if (typeof v !== "function") {
    return summary;
  }
  const failedIds =
    queue
      .failed()
      .map((operation) => operation.id)
      .join(", ") || "none";
  return v(
    "main",
    {},
    v("p", {}, summary),
    v("p", {}, `Failed IDs: ${failedIds}`),
    v("p", {}, exampleState.lastAction),
    v("button", { onclick: createFailedOrder }, "Create failed order"),
    v("button", { onclick: retryOneFailed }, "Retry first failed order"),
    v("button", { onclick: retryAllFailed }, "Retry all failed orders"),
    v("button", { onclick: discardAllFailed }, "Discard failed orders"),
  );
}

mount("body", QueueStatus);

Result

Choose Retry first failed order to call retryOne(id), Retry all failed orders to call retryAll(), or Discard failed orders to call discardFailed(). Retries switch the delivery response to 204.

Limits

After every action, the mounted view shows pending, failed, syncing, failed IDs and the last action. Cleanup removes the subscription and destroys the queue and network manager.

Continue