@solvice/routing 0.17.4-beta.0 → 0.17.6-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -71,7 +71,8 @@ try {
71
71
  | `suggest(request)` | `POST /v3/routing/suggest` |
72
72
  | `validate.solve(request)`, `validate.evaluate(request)`, `validate.suggest(request)` | the same three sent to `/v3/routing/validate/solve`, `/evaluate`, `/suggest`: checked, not run |
73
73
  | `runs.submit(request)` | `POST /v3/routing/runs`: answers at once with a `queued` run |
74
- | `runs.get(id)` | `GET /v3/routing/runs/{id}`: poll the run's `status` |
74
+ | `runs.get(id)` | `GET /v3/routing/runs/{id}`: the run's `status` |
75
+ | `runs.wait(id, { intervalMs, maxIntervalMs, signal })` | polls `get` with a growing pause until the run is final, and returns it (a `failed` or `cancelled` run is returned, not thrown) |
75
76
  | `runs.result(id)` | `GET /v3/routing/runs/{id}/result`: the plan of a `succeeded` run |
76
77
  | `runs.cancel(id)` | `POST /v3/routing/runs/{id}/cancel` |
77
78
 
@@ -85,21 +86,22 @@ import { createSolvice, type SolveRequest } from '@solvice/routing';
85
86
  const solvice = createSolvice({ apiKey: process.env.SOLVICE_API_KEY! });
86
87
 
87
88
  export async function solveInBackground(problem: SolveRequest['problem']) {
88
- let run = await solvice.runs.submit({ problem, options: { time_limit_s: 600 } });
89
- while (run.status === 'queued' || run.status === 'running') {
90
- await new Promise((resolve) => setTimeout(resolve, 5000));
91
- run = await solvice.runs.get(run.id);
92
- }
89
+ const submitted = await solvice.runs.submit({ problem, options: { time_limit_s: 600 } });
90
+ const run = await solvice.runs.wait(submitted.id);
93
91
  if (run.status !== 'succeeded') {
94
- throw new Error(`run ${run.id} ${run.status}: ${run.error?.message ?? 'no plan'}`);
92
+ throw new Error(`run ${run.id} ${run.status}: ${run.status === 'failed' ? run.error.message : 'no plan'}`);
95
93
  }
96
94
  return solvice.runs.result(run.id);
97
95
  }
98
96
  ```
99
97
 
100
98
  The run endpoints are served by the API gateway, so their `404` and `409` errors are
101
- not the API's error body: a `SolviceError` with no `body`, and `raw.message` starting
102
- with a code (`not_found`, `run_not_finished`, `no_result`).
99
+ not the API's error body: a `SolviceError` with no `body`. Branch on `err.code`
100
+ (`not_found`, `run_not_finished`, `no_result`), read from the start of `raw.message`;
101
+ for the API's own errors it is the body's `code`. A run waits about 40 s in `queued`
102
+ while its worker starts, so use runs for searches of a minute or more, and `solve`
103
+ for shorter ones. `Run` narrows on `status`: a `succeeded` run always has
104
+ `started_at`, `finished_at` and `expires_at`, and a `failed` run always has `error`.
103
105
 
104
106
  Any non-2xx response throws a `SolviceError` with the HTTP `status`, the
105
107
  `response`, and, when the response carried one, the API's error `body`. A `401`
@@ -136,6 +138,6 @@ Actions → Run workflow on `main`: it publishes the latest tag, or the one type
136
138
  It runs `npm run publish:beta` through npm trusted publishing; never publish
137
139
  from a branch. The explicit `--tag beta` matters: npm 11 ignores
138
140
  `publishConfig.tag` and would move `latest` to the prerelease. Until a stable version
139
- exists, `latest` still points at the first prerelease (npm gives a package's first
140
- version `latest` whatever the tag), so `npm install @solvice/routing` without
141
- `@beta` installs the beta too.
141
+ exists, `latest` is not moved by a publish and has to be pointed by hand
142
+ (`npm dist-tag add @solvice/routing@<version> latest`, run by a maintainer logged in to npm), or `npm install @solvice/routing` without `@beta` installs
143
+ an old prerelease.
package/dist/index.d.ts CHANGED
@@ -13,6 +13,10 @@ export type RunSchemas = runsComponents["schemas"];
13
13
  export type RunRequest = RunSchemas["RunRequest"];
14
14
  export type Run = RunSchemas["Run"];
15
15
  export type RunStatus = RunSchemas["RunStatus"];
16
+ /** `Run` narrowed by `status`: `succeeded` always has `started_at`, `finished_at` and `expires_at`, `failed` always has `error`. */
17
+ export type RunOf<S extends RunStatus> = Extract<Run, {
18
+ status: S;
19
+ }>;
16
20
  /** The gateway's own error answer on `/v3/routing/runs*`: not an `ErrorBody`. */
17
21
  export type GatewayError = RunSchemas["GatewayError"];
18
22
  export interface SolviceOptions {
@@ -43,9 +47,23 @@ export declare class SolviceError extends Error {
43
47
  constructor(status: number, response: Response, body?: ErrorBody | undefined,
44
48
  /** The parsed response body as received, whatever its shape; `undefined` when there was none. */
45
49
  raw?: unknown | undefined);
50
+ /**
51
+ * What to branch on: the `code` of the API's error body (`invalid_request`),
52
+ * or for a gateway error on `/v3/routing/runs*` the code its `message`
53
+ * starts with (`not_found`, `run_not_finished`, `no_result`). `undefined`
54
+ * when the response carried neither (a `401`, a `429`, a malformed run id).
55
+ */
56
+ get code(): string | undefined;
46
57
  /** Seconds to wait before retrying, from the `Retry-After` header; `undefined` without one (or with an HTTP date). */
47
58
  get retryAfterS(): number | undefined;
48
59
  }
60
+ /** Options for `runs.wait`. `signal` aborts the wait and also bounds each poll: pass `AbortSignal.timeout(ms)` to cap the whole wait. */
61
+ export interface WaitOptions extends RequestOptions {
62
+ /** First pause between polls; it grows by half each poll up to `maxIntervalMs`. Default 2,000. */
63
+ intervalMs?: number;
64
+ /** Longest pause between polls. Default 10,000. */
65
+ maxIntervalMs?: number;
66
+ }
49
67
  /** The client `createSolvice` returns, for naming it in your own signatures. */
50
68
  export type Solvice = ReturnType<typeof createSolvice>;
51
69
  export declare function createSolvice({ apiKey, baseUrl, fetch }: SolviceOptions): {
@@ -61,14 +79,23 @@ export declare function createSolvice({ apiKey, baseUrl, fetch }: SolviceOptions
61
79
  /**
62
80
  * Async runs, served by the API gateway: `submit` answers at once and the
63
81
  * search runs in the background for up to `options.time_limit_s` (3,600 s).
64
- * Poll `get` until `status` is `succeeded`, `failed` or `cancelled`, then
65
- * read the plan with `result`. A gateway error (`404`, `409`) is a
66
- * `SolviceError` with no `body`; its `raw.message` starts with a code such
67
- * as `not_found`, `run_not_finished` or `no_result`.
82
+ * A run waits about 40 s in `queued` while its worker starts, so `wait` is
83
+ * for searches of a minute or more; `solve` is faster for short ones.
84
+ * Poll `get`, or `wait`, until `status` is `succeeded`, `failed` or
85
+ * `cancelled`, then read the plan with `result`. A gateway error (`404`,
86
+ * `409`) is a `SolviceError` with no `body`; branch on its `code`
87
+ * (`not_found`, `run_not_finished`, `no_result`).
68
88
  */
69
89
  runs: {
70
90
  submit: (body: RunRequest, options?: RequestOptions) => Promise<Run>;
71
91
  get: (id: string, options?: RequestOptions) => Promise<Run>;
92
+ /**
93
+ * Polls `get` until the run is `succeeded`, `failed` or `cancelled` and
94
+ * returns it; a `failed` or `cancelled` run is returned, not thrown. A run
95
+ * the gateway never sees start turns `failed` after 30 minutes, so the wait
96
+ * ends; use `signal` for a shorter bound.
97
+ */
98
+ wait: (id: string, { intervalMs, maxIntervalMs, ...options }?: WaitOptions) => Promise<Run>;
72
99
  /** The plan of a `succeeded` run; a run that is not finished, failed or cancelled answers `409`. */
73
100
  result: (id: string, options?: RequestOptions) => Promise<SolveResponse>;
74
101
  /** Cancels a `queued` or `running` run; a finished run comes back in its final state. */
package/dist/index.js CHANGED
@@ -22,6 +22,17 @@ export class SolviceError extends Error {
22
22
  this.raw = raw;
23
23
  this.name = "SolviceError";
24
24
  }
25
+ /**
26
+ * What to branch on: the `code` of the API's error body (`invalid_request`),
27
+ * or for a gateway error on `/v3/routing/runs*` the code its `message`
28
+ * starts with (`not_found`, `run_not_finished`, `no_result`). `undefined`
29
+ * when the response carried neither (a `401`, a `429`, a malformed run id).
30
+ */
31
+ get code() {
32
+ if (this.body)
33
+ return this.body.code;
34
+ return /^([a-z][a-z_]*): /.exec(gatewayMessage(this.raw) ?? "")?.[1];
35
+ }
25
36
  /** Seconds to wait before retrying, from the `Retry-After` header; `undefined` without one (or with an HTTP date). */
26
37
  get retryAfterS() {
27
38
  const value = this.response.headers.get("retry-after");
@@ -51,8 +62,43 @@ function unwrap({ data, error, response }) {
51
62
  }
52
63
  return data;
53
64
  }
65
+ const pause = (ms, signal) => new Promise((resolve, reject) => {
66
+ if (signal?.aborted)
67
+ return reject(signal.reason);
68
+ const timer = setTimeout(() => {
69
+ signal?.removeEventListener("abort", onAbort);
70
+ resolve();
71
+ }, ms);
72
+ const onAbort = () => {
73
+ clearTimeout(timer);
74
+ reject(signal?.reason);
75
+ };
76
+ signal?.addEventListener("abort", onAbort, { once: true });
77
+ });
54
78
  export function createSolvice({ apiKey, baseUrl = "https://api.solvice.io", fetch }) {
55
79
  const client = createClient({ baseUrl, headers: { Authorization: apiKey }, fetch });
80
+ const runs = {
81
+ submit: async (body, options) => unwrap(await client.POST("/v3/routing/runs", { body, ...options })),
82
+ get: async (id, options) => unwrap(await client.GET("/v3/routing/runs/{id}", { params: { path: { id } }, ...options })),
83
+ /**
84
+ * Polls `get` until the run is `succeeded`, `failed` or `cancelled` and
85
+ * returns it; a `failed` or `cancelled` run is returned, not thrown. A run
86
+ * the gateway never sees start turns `failed` after 30 minutes, so the wait
87
+ * ends; use `signal` for a shorter bound.
88
+ */
89
+ wait: async (id, { intervalMs = 2000, maxIntervalMs = 10_000, ...options } = {}) => {
90
+ for (let delay = intervalMs;; delay = Math.min(delay * 1.5, maxIntervalMs)) {
91
+ const run = await runs.get(id, options);
92
+ if (run.status !== "queued" && run.status !== "running")
93
+ return run;
94
+ await pause(delay, options.signal);
95
+ }
96
+ },
97
+ /** The plan of a `succeeded` run; a run that is not finished, failed or cancelled answers `409`. */
98
+ result: async (id, options) => unwrap(await client.GET("/v3/routing/runs/{id}/result", { params: { path: { id } }, ...options })),
99
+ /** Cancels a `queued` or `running` run; a finished run comes back in its final state. */
100
+ cancel: async (id, options) => unwrap(await client.POST("/v3/routing/runs/{id}/cancel", { params: { path: { id } }, ...options })),
101
+ };
56
102
  return {
57
103
  solve: async (body, options) => unwrap(await client.POST("/v3/routing/solve", { body, ...options })),
58
104
  evaluate: async (body, options) => unwrap(await client.POST("/v3/routing/evaluate", { body, ...options })),
@@ -66,19 +112,14 @@ export function createSolvice({ apiKey, baseUrl = "https://api.solvice.io", fetc
66
112
  /**
67
113
  * Async runs, served by the API gateway: `submit` answers at once and the
68
114
  * search runs in the background for up to `options.time_limit_s` (3,600 s).
69
- * Poll `get` until `status` is `succeeded`, `failed` or `cancelled`, then
70
- * read the plan with `result`. A gateway error (`404`, `409`) is a
71
- * `SolviceError` with no `body`; its `raw.message` starts with a code such
72
- * as `not_found`, `run_not_finished` or `no_result`.
115
+ * A run waits about 40 s in `queued` while its worker starts, so `wait` is
116
+ * for searches of a minute or more; `solve` is faster for short ones.
117
+ * Poll `get`, or `wait`, until `status` is `succeeded`, `failed` or
118
+ * `cancelled`, then read the plan with `result`. A gateway error (`404`,
119
+ * `409`) is a `SolviceError` with no `body`; branch on its `code`
120
+ * (`not_found`, `run_not_finished`, `no_result`).
73
121
  */
74
- runs: {
75
- submit: async (body, options) => unwrap(await client.POST("/v3/routing/runs", { body, ...options })),
76
- get: async (id, options) => unwrap(await client.GET("/v3/routing/runs/{id}", { params: { path: { id } }, ...options })),
77
- /** The plan of a `succeeded` run; a run that is not finished, failed or cancelled answers `409`. */
78
- result: async (id, options) => unwrap(await client.GET("/v3/routing/runs/{id}/result", { params: { path: { id } }, ...options })),
79
- /** Cancels a `queued` or `running` run; a finished run comes back in its final state. */
80
- cancel: async (id, options) => unwrap(await client.POST("/v3/routing/runs/{id}/cancel", { params: { path: { id } }, ...options })),
81
- },
122
+ runs,
82
123
  /** The underlying openapi-fetch client, for anything the methods above do not cover. */
83
124
  client,
84
125
  };
package/dist/runs.d.ts CHANGED
@@ -1828,18 +1828,22 @@ export interface components {
1828
1828
  */
1829
1829
  wait_s?: number | null;
1830
1830
  };
1831
- /** @description One async run. */
1832
- Run: {
1831
+ /** @description One async run. `status` selects the variant: which timestamps and `error` are present follows from it. */
1832
+ Run: components["schemas"]["RunQueued"] | components["schemas"]["RunRunning"] | components["schemas"]["RunSucceeded"] | components["schemas"]["RunFailed"] | components["schemas"]["RunCancelled"];
1833
+ /** @description A run cancelled before it finished. `started_at` is absent when it was cancelled while `queued`; it never returns a plan. */
1834
+ RunCancelled: {
1833
1835
  /**
1834
1836
  * Format: uuid
1835
1837
  * @description The run id. Use it in the paths of the other endpoints.
1836
1838
  * @example d28568b1-34cc-4f89-b10b-dc24b26a8275
1837
1839
  */
1838
1840
  id: string;
1839
- status: components["schemas"]["RunStatus"];
1841
+ /**
1842
+ * @description `cancelled`. (enum property replaced by openapi-typescript)
1843
+ * @enum {string}
1844
+ */
1845
+ status: "cancelled";
1840
1846
  links: components["schemas"]["RunLinks"];
1841
- /** @description Present when `status` is `failed`. */
1842
- error?: components["schemas"]["RunError"];
1843
1847
  /**
1844
1848
  * Format: date-time
1845
1849
  * @description When a worker claimed the run, in UTC. Absent while the run is `queued`.
@@ -1851,13 +1855,13 @@ export interface components {
1851
1855
  * @description When the run reached a final state, in UTC. Absent before then.
1852
1856
  * @example 2026-10-15T07:41:00Z
1853
1857
  */
1854
- finished_at?: string;
1858
+ finished_at: string;
1855
1859
  /**
1856
1860
  * Format: date-time
1857
1861
  * @description When the run and its plan are deleted: `finished_at` plus 365 days. Absent before the run is final.
1858
1862
  * @example 2027-10-15T07:41:00Z
1859
1863
  */
1860
- expires_at?: string;
1864
+ expires_at: string;
1861
1865
  };
1862
1866
  /** @description Why a run `failed`, in the `ErrorBody` shape. */
1863
1867
  RunError: {
@@ -1881,6 +1885,41 @@ export interface components {
1881
1885
  /** @description One entry per problem found in the request, as in `ErrorBody`. */
1882
1886
  errors?: components["schemas"]["RequestIssue"][];
1883
1887
  };
1888
+ /** @description A run that stopped with an error. `started_at` is absent when the run never started. */
1889
+ RunFailed: {
1890
+ /**
1891
+ * Format: uuid
1892
+ * @description The run id. Use it in the paths of the other endpoints.
1893
+ * @example d28568b1-34cc-4f89-b10b-dc24b26a8275
1894
+ */
1895
+ id: string;
1896
+ /**
1897
+ * @description `failed`. (enum property replaced by openapi-typescript)
1898
+ * @enum {string}
1899
+ */
1900
+ status: "failed";
1901
+ links: components["schemas"]["RunLinks"];
1902
+ /** @description Present when `status` is `failed`. */
1903
+ error: components["schemas"]["RunError"];
1904
+ /**
1905
+ * Format: date-time
1906
+ * @description When a worker claimed the run, in UTC. Absent while the run is `queued`.
1907
+ * @example 2026-10-15T07:40:59Z
1908
+ */
1909
+ started_at?: string;
1910
+ /**
1911
+ * Format: date-time
1912
+ * @description When the run reached a final state, in UTC. Absent before then.
1913
+ * @example 2026-10-15T07:41:00Z
1914
+ */
1915
+ finished_at: string;
1916
+ /**
1917
+ * Format: date-time
1918
+ * @description When the run and its plan are deleted: `finished_at` plus 365 days. Absent before the run is final.
1919
+ * @example 2027-10-15T07:41:00Z
1920
+ */
1921
+ expires_at: string;
1922
+ };
1884
1923
  /** @description Paths of the run, relative to the API host. */
1885
1924
  RunLinks: {
1886
1925
  /**
@@ -1939,6 +1978,21 @@ export interface components {
1939
1978
  */
1940
1979
  travel_fallback?: null | components["schemas"]["TravelFallback"];
1941
1980
  };
1981
+ /** @description A run no worker has claimed yet. No timestamps. */
1982
+ RunQueued: {
1983
+ /**
1984
+ * Format: uuid
1985
+ * @description The run id. Use it in the paths of the other endpoints.
1986
+ * @example d28568b1-34cc-4f89-b10b-dc24b26a8275
1987
+ */
1988
+ id: string;
1989
+ /**
1990
+ * @description `queued`. (enum property replaced by openapi-typescript)
1991
+ * @enum {string}
1992
+ */
1993
+ status: "queued";
1994
+ links: components["schemas"]["RunLinks"];
1995
+ };
1942
1996
  /**
1943
1997
  * @description Request body for `POST /v3/routing/runs`: the `POST /v3/routing/solve` body, with a longer `options.time_limit_s`.
1944
1998
  * @example {
@@ -2046,11 +2100,65 @@ export interface components {
2046
2100
  /** @description The problem: tasks, fleet, depots, relations, objective, travel. */
2047
2101
  problem: components["schemas"]["Problem"];
2048
2102
  };
2103
+ /** @description A run a worker has claimed. */
2104
+ RunRunning: {
2105
+ /**
2106
+ * Format: uuid
2107
+ * @description The run id. Use it in the paths of the other endpoints.
2108
+ * @example d28568b1-34cc-4f89-b10b-dc24b26a8275
2109
+ */
2110
+ id: string;
2111
+ /**
2112
+ * @description `running`. (enum property replaced by openapi-typescript)
2113
+ * @enum {string}
2114
+ */
2115
+ status: "running";
2116
+ links: components["schemas"]["RunLinks"];
2117
+ /**
2118
+ * Format: date-time
2119
+ * @description When a worker claimed the run, in UTC. Absent while the run is `queued`.
2120
+ * @example 2026-10-15T07:40:59Z
2121
+ */
2122
+ started_at: string;
2123
+ };
2049
2124
  /**
2050
2125
  * @description Where a run is. `queued` until a worker claims it, then `running`. `succeeded`, `failed` and `cancelled` are final.
2051
2126
  * @enum {string}
2052
2127
  */
2053
2128
  RunStatus: "queued" | "running" | "succeeded" | "failed" | "cancelled";
2129
+ /** @description A run that finished. Read the plan with `GET /v3/routing/runs/{id}/result`. */
2130
+ RunSucceeded: {
2131
+ /**
2132
+ * Format: uuid
2133
+ * @description The run id. Use it in the paths of the other endpoints.
2134
+ * @example d28568b1-34cc-4f89-b10b-dc24b26a8275
2135
+ */
2136
+ id: string;
2137
+ /**
2138
+ * @description `succeeded`. (enum property replaced by openapi-typescript)
2139
+ * @enum {string}
2140
+ */
2141
+ status: "succeeded";
2142
+ links: components["schemas"]["RunLinks"];
2143
+ /**
2144
+ * Format: date-time
2145
+ * @description When a worker claimed the run, in UTC. Absent while the run is `queued`.
2146
+ * @example 2026-10-15T07:40:59Z
2147
+ */
2148
+ started_at: string;
2149
+ /**
2150
+ * Format: date-time
2151
+ * @description When the run reached a final state, in UTC. Absent before then.
2152
+ * @example 2026-10-15T07:41:00Z
2153
+ */
2154
+ finished_at: string;
2155
+ /**
2156
+ * Format: date-time
2157
+ * @description When the run and its plan are deleted: `finished_at` plus 365 days. Absent before the run is final.
2158
+ * @example 2027-10-15T07:41:00Z
2159
+ */
2160
+ expires_at: string;
2161
+ };
2054
2162
  /**
2055
2163
  * @description How good a plan is. Plans compare in order: fewer `violation_count`
2056
2164
  * first, then less `unassigned_priority`, then less `cost`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solvice/routing",
3
- "version": "0.17.4-beta.0",
3
+ "version": "0.17.6-beta.0",
4
4
  "description": "TypeScript client for the Solvice V3 routing API, generated from its OpenAPI spec.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://docs.solvice.io/guides/platform/sdks/typescript",