gridrunner
v0.13.0
Published
Producer SDK for GRIDRUNNER — fire-and-forget job progress events
Downloads
1,184
Readme
GRIDRUNNER Node SDK
Zero-dependency Node 18+ client for reporting jobs, service pings, operator alerts and simulation runs to GRIDRUNNER. Calls never block or throw into your code. The SDK overview explains when to use a job, a ping, an alert or a run.
Setup
import * as gr from "gridrunner";
gr.init({ token: readSecret("gridrunner_produce_token"),
url: "https://gridrunner.example.com" });
gr.init(); // local dev core at http://127.0.0.1:7077A token without url throws. The SDK reads no environment variables or
config files.
Jobs
Declare the chunk plan, then run each chunk inside withChunk. A resolved
callback completes the chunk or job; a rejection fails it and is rethrown.
const chunks = Object.entries(monthlyEstimates).map(([month, units]) => ({
chunk_id: `orders:${month}`, item: "orders", label: month, units,
}));
await gr.withJob("etl.orders.monthly.v1",
{ chunks, label: "Export orders", heartbeat_s: 30 },
async (job) => {
for (const spec of chunks) {
await job.withChunk(spec.chunk_id, () => exportMonth(spec.label),
{ worker: workerName });
}
});Chunk plans can also be an object of id to units, or an array of ids (one unit each):
const chunks = { "users:0": 50_000, "users:1": 50_000 };
const chunks = ["load", "fit", "write"];Manual lifecycle
When the helpers do not fit your framework:
const job = gr.job("model.forecast.v3", {
chunks: [
{ chunk_id: "load", item: "prepare", units: 1 },
{ chunk_id: "fit", item: "model", units: 10 },
{ chunk_id: "write", item: "output", units: 1 },
],
expected_silence_s: 300,
}).register();
try {
const fit = job.chunk("fit").start();
await fitModel();
fit.complete();
job.complete();
} catch (error) {
job.fail(error);
throw error;
}Indivisible steps
For one step that cannot be split, report a measured fraction:
await job.withChunk("fit", async (chunk) => {
for await (const { completed, total } of train()) {
chunk.progress(completed / total);
}
});Job options
| Option | Meaning |
|---|---|
| chunks | The chunk plan |
| label | Name shown on the card (defaults to the job type id) |
| meta | Free-form JSON attached to the run |
| jobId | Your own stable run id (default: generated) |
| heartbeat_s | Send heartbeats at this interval; a silent process is failed |
| expected_silence_s | How long the job may legitimately stay quiet |
| totalUnits | Override the sum of chunk units |
job.chunk(chunkId, { units, worker, item }) and
job.withChunk(chunkId, fn, { units, worker, item }) accept chunks not in
the plan and add them on the fly.
Pings
One call per cycle of a recurring process. Report errors with a description; declare the next due time when the schedule is too sparse to learn (weekly, monthly).
async function dailyCycle() {
try {
await runExport();
gr.ping("daily-export");
} catch (error) {
gr.ping("daily-export", { status: "error", description: String(error) });
throw error;
}
}
gr.ping("monthly-report", { expectedNextTs: nextRunAt }); // epoch ms or DateAlerts
A one-line message for operators under a stable category. status is
info (default), warning, or error. short is an optional few-word
version — the least a reader needs to know what happened.
gr.alert("storage", "Disk at 91% on db-1", { status: "warning" });
gr.alert("billing", "Stripe webhook rejected 3 events",
{ status: "error", short: "stripe: 3 rejected" });One-shot calls for scripts
Cron scripts can send a single ping or alert without init. Both return a
Promise<boolean> that never rejects; ignore it to fire and forget, or
await it to learn whether delivery succeeded. The process stays alive
until the request settles.
gr.pingOnce("daily-export", { token, url });
gr.alertOnce("deploy", "v2.3.1 live", { token, url });
const ok = await gr.pingOnce("daily-export", { token, url });Simulation runs
Publish the manifest once, then open one run per candidate. The SDK overview explains the manifest and what the console makes of each field.
gr.manifest(MANIFEST);
await gr.withRun("inventory.reorder.v1", { levers, seed: 7, foundBy: "grid" },
async (run) => {
for (const [t, values] of engine.simulate(levers)) run.tick(t, values);
run.done({ metrics: { cost: total, fill_rate: filled / demanded } });
});A resolved callback without done completes the run with no metrics; a
rejection fails it and is rethrown.
Manual lifecycle
const run = gr.run("inventory.reorder.v1", { levers });
try {
for (const [t, values] of engine.simulate(levers)) run.tick(t, values);
if (stockFloorBroken) {
run.done({ metrics: { cost: total }, feasible: false, violated: ["stock"] });
} else {
run.done({ metrics: { cost: total }, objective: total });
}
} catch (error) {
run.fail(error);
throw error;
}Run options
| Option | Meaning |
|---|---|
| levers | The candidate's lever values, keyed by lever id |
| seed | Random seed recorded for reproduction |
| foundBy | Who proposed the candidate: operator (default), search, grid, any label |
| runId | Your own stable run id (default: generated) |
run.done({ metrics, feasible = true, objective, violated }) and
run.fail(error) are final; a second call is ignored. objective
defaults to the objective metric's value in metrics.
gr.manifest(manifest) warns naming the field when the manifest has no
sim_id, no objective metric, a lever without bounds or options, a
trajectory_id naming no trajectory metric, or a duplicate id; the
manifest is sent anyway.
gr.searchStatus(simId, { method, round, evaluated, budget, coverage,
bestObjective }) reports an optimizer's progress.
Advanced
new gr.Client(url, { token }) is an independent client for applications
that cannot use module-level state; await client.close() on shutdown
flushes pending events. gr.emit(event) queues a raw event for lifecycles
the structured API does not cover. init also accepts flushIntervalMs,
queueSize, and retryIntervalMs; the defaults suit nearly everyone.
