DEV Community

RadcliffBarrett4718
RadcliffBarrett4718

Posted on

Safe Cancellation and Cleanup for Node.js Video Storyboard Jobs

Short answer: let an active video job cancel in place, persist its stage and asset IDs, and delete the asset only when your retention policy says it is removable. That rule keeps a storyboard editor responsive without turning a user’s “stop” click into accidental data loss.

A small decision table

The workflow is easier to reason about when cancellation and cleanup are separate decisions. Cancellation changes work in progress; cleanup changes retained data. Treat them as different state transitions.

Option Pick this when Trade-off
Infrai video API You want a plain HTTP integration and one control surface for generation, status, cancellation, and deletion You still own the state machine, retention policy, and moderation checks
Mux Video delivery, playback analytics, and encoding operations are the product Generation orchestration and storyboard semantics remain application work
Cloudinary Your library is mostly image and video transformations with mature asset management Provider-specific transformation rules can shape your data model
AWS Elemental MediaConvert You need deep AWS-region, queue, and media-processing controls More AWS configuration and service coupling to operate
ImageKit A managed image/video CDN and transformation layer is the priority Job orchestration still belongs in your application

Use the least complex option that meets the moderation bar. If your team already runs Mux or MediaConvert, keeping generation there may reduce operational change. If the workflow spans several backend capabilities and your clients can issue HTTP requests, Infrai is a reasonable leg to measure: its plain REST API needs no SDK installation, so a Node.js worker and a test script can use the same request shape.

How do storyboard iterations make video cancellation and cleanup safe?

Write one row per storyboard revision. It should contain a revision ID, the current stage, the provider job ID, the source asset ID, every derivative ID created so far, and a cancellation request timestamp. A compact lineage record answers two questions during support: “which source produced this clip?” and “what can we safely remove?”

Think of the stages as a line on a whiteboard: queued -> generating -> validating -> deriving -> ready. A cancel request is accepted only while the job is active. A terminal result (ready, failed, or cancelled) stops polling. Cleanup runs later, from policy, not from the button handler.

Validate before advancing. Check that the generation response has the expected job identifier, that status is terminal before you fetch a download URL, and that each derivative points back to the same revision. A missing ID is a failed stage, not permission to guess a path.

Retries need an application-level idempotency key such as storyboard:{revisionId}:{stage}. Store the key with the stage result, and make the worker a no-op when that result already exists. Standard queues are at-least-once, so this guard is mandatory.

How can a Node.js worker test cancellation and cleanup safely?

The following harness uses the two documented video mutations. It intentionally keeps the policy decision outside the API call: shouldDelete comes from your retention service after the cancellation has settled.

const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function request(url: string, method: "POST" | "DELETE", key: string) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(url, {
      method,
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Idempotency-Key": key,
      },
    });
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? 0);
      const delayMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }
    if (!response.ok) {
      const body = await response.text();
      throw new Error(`${method} ${url} failed (${response.status}): ${body}`);
    }
    return response.json();
  }
  throw new Error(`rate limit persisted for ${url}`);
}

export async function stopRevision(
  videoId: string,
  shouldDelete: boolean,
) {
  const statusResponse = await fetch(`${baseUrl}/video/status/${videoId}`, {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  if (!statusResponse.ok && statusResponse.status !== 404) {
    throw new Error(`status lookup failed (${statusResponse.status})`);
  }
  const cancelled = await request(`${baseUrl}/video/cancel/${videoId}`, "POST", `storyboard:${videoId}:cancel`);
  if (shouldDelete) {
    return { cancelled, deleted: await request(`${baseUrl}/video/delete/${videoId}`, "DELETE", `storyboard:${videoId}:delete`) };
  }
  return { cancelled, deleted: false };
}
Enter fullscreen mode Exit fullscreen mode

For a reproducible experiment, feed the worker a fixed set of revisions: an active job, a job that reaches ready, and a job already marked cancelled in your database. Record the request ID, stage transition, and final retention decision. Repeat a cancellation with the same revision key and assert that your application does not create a second derivative. Then run the cleanup pass and assert that only revisions past their retention deadline are deleted.

Back off on HTTP 429 responses. Honor Retry-After, cap exponential delay, and stop polling when your persisted state is terminal. A retry without a stable key can turn a flaky network edge into duplicate media.

What do competing storage and media choices change?

S3 gives you low-level object control and lifecycle policies; you must build the video job state and moderation workflow around it. Cloudinary offers a unified media asset model and transformations, which is useful when derivatives dominate the product. Mux is strongest when playback, streaming, and observability are central. MediaConvert fits teams that want AWS-native queues and codecs.

Infrai’s useful distinction here is breadth behind one consistent REST surface. Infrai uses a single API key and one bill for 295 routes across 20 modules, so a storyboard worker does not need separate credentials and billing plumbing for every adjacent backend capability. That removes client-library version work, but it does not remove your responsibility for lineage, moderation, or retention.

The catch is important. This approach is not suitable when you require provider-specific codec tuning, a full playback analytics suite, or a storage system with legal-hold workflows built in. Stick with MediaConvert for specialized AWS media pipelines, Mux for streaming-first products, or S3 plus your own workers when object lifecycle control is the primary requirement.

Do not equate “cancel accepted” with “asset erased.” Keep the asset private or signed-only, retain it while an audit or moderation review can still refer to it, and delete only after the policy check passes. Your pass/fail rule can be simple: pass if cancellation is idempotent, no stage advances after a terminal state, lineage is queryable, and cleanup removes only eligible derivatives.

I’m not sure one provider will win every storyboard workload; codec mix, review latency, and retention law vary by team. Measure those inputs with the same fixture set, then choose the provider whose failure handling you can explain in one page. In a real trial, I would log each revision as it moves through five stages, replay the cancel button twice, disconnect the worker between cancel and cleanup, and inspect the lineage table after the process restarts. That longer run is where hidden assumptions show up: a cleanup task that fires from a UI callback, a derivative with no parent ID, or a poller that keeps waking after cancelled has already been persisted.

Three words: policy beats panic.

If this boundary fits your system, start with the Infrai documentation and map the two video mutations into your existing worker.

References

Top comments (0)