@karmaniverous/jeeves-meta-sdk 0.0.0-stage → 0.1.0-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 +97 -2
- package/dist/MetaError.d.ts +90 -0
- package/dist/createMetaClient.d.ts +39 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +302 -0
- package/dist/options.d.ts +62 -0
- package/dist/toMetaError.d.ts +15 -0
- package/package.json +99 -4
package/README.md
CHANGED
|
@@ -1,3 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @karmaniverous/jeeves-meta-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed TypeScript client for the [jeeves-meta](https://github.com/karmaniverous/jeeves-meta) HTTP API: metas, synthesis, the queue, previews, seeding, unlocking, status and config.
|
|
4
|
+
|
|
5
|
+
The client is generated from jeeves-meta's endpoint catalog (`META_CATALOG` in `@karmaniverous/jeeves-meta-core`), so every method, input and response type matches the routes the service registers. Requests are validated before they are sent and responses are parsed on arrival.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @karmaniverous/jeeves-meta-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node.js 24.22.2 or later.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createMetaClient, isMetaError } from '@karmaniverous/jeeves-meta-sdk';
|
|
19
|
+
|
|
20
|
+
const meta = createMetaClient({ baseUrl: 'http://127.0.0.1:1938' });
|
|
21
|
+
|
|
22
|
+
const { summary } = await meta.listMetas({ hasError: 'true' });
|
|
23
|
+
const detail = await meta.metaDetail({ path: 'j:/domains/projects/x' });
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const queued = await meta.synthesize({ path: 'j:/domains/projects/x' });
|
|
27
|
+
console.log(queued.status, summary.errors, detail.owedPhase);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (isMetaError(error) && error.code === 'META_WATCHER_UNAVAILABLE') {
|
|
30
|
+
console.log(`${error.message} (HTTP ${String(error.status)})`);
|
|
31
|
+
} else throw error;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Every method is named after its catalog entry and takes one input object holding the entry's path parameters, query fields and body fields, flat. Query fields are strings, as on the wire (`hasError: 'true'`):
|
|
36
|
+
|
|
37
|
+
| Method | Route | Input |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `status` | `GET /status` | none |
|
|
40
|
+
| `listMetas` | `GET /metas` | `{ pathPrefix?, hasError?, staleHours?, neverSynthesized?, locked?, disabled?, fields? }` |
|
|
41
|
+
| `metaDetail` | `GET /metas/:path` | `{ path, fields?, includeArchive? }` |
|
|
42
|
+
| `updateMeta` | `PATCH /metas/:path` | `{ path, _steer?, _emphasis?, _depth?, _crossRefs?, _disabled? }` (`null` removes) |
|
|
43
|
+
| `synthesize` | `POST /synthesize` | `{ path? }` |
|
|
44
|
+
| `abort` | `POST /synthesize/abort` | none |
|
|
45
|
+
| `preview` | `GET /preview` | `{ path? }` |
|
|
46
|
+
| `seed` | `POST /seed` | `{ path, crossRefs?, steer? }` |
|
|
47
|
+
| `unlock` | `POST /unlock` | `{ path }` |
|
|
48
|
+
| `config` | `GET /config` | `{ path? }` (JSONPath) |
|
|
49
|
+
| `configApply` | `POST /config/apply` | `{ patch, replace? }` |
|
|
50
|
+
| `queue` | `GET /queue` | none |
|
|
51
|
+
| `queueClear` | `POST /queue/clear` | none |
|
|
52
|
+
|
|
53
|
+
`path` is a meta's owner path or `.meta/` path; the client encodes it once in the URL, so spaces, `/`, `:` and `%` are safe. The request and response types are exported (`MetaInput<'listMetas'>`, `MetaResponse<'synthesize'>`, `MetaHealth`, ...). Responses are parsed against the schemas of the meta-core version the SDK was built with: fields a newer jeeves-meta adds are dropped until you upgrade the SDK.
|
|
54
|
+
|
|
55
|
+
`synthesize` only enqueues: it answers at once (`202` queued, or `200` skipped when nothing is owed). Follow progress with `status` or `queue`.
|
|
56
|
+
|
|
57
|
+
## Timeouts
|
|
58
|
+
|
|
59
|
+
- Every call times out after **120 s** by default (`timeoutMs` option).
|
|
60
|
+
- Any call can override its timeout: `meta.preview({}, { timeoutMs: 10_000 })`.
|
|
61
|
+
|
|
62
|
+
The SDK never retries.
|
|
63
|
+
|
|
64
|
+
## Errors
|
|
65
|
+
|
|
66
|
+
Every failure is a `MetaError`. Branch on `code`, never on `message`:
|
|
67
|
+
|
|
68
|
+
| `code` | `status` | When |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| the body's `code` | the HTTP status | jeeves-meta answered non-2xx with a code (`META_*`, `JEEVES_*`, `FST_ERR_*`, below); `message` is the body's `message`, `body` the whole body and `responseText` the body as received |
|
|
71
|
+
| `http_<status>` | the HTTP status | jeeves-meta answered non-2xx without a code: a masked 500 (an unexpected failure; the cause is in the service log), an unknown route, or a body that isn't jeeves-meta's (a proxy's page) |
|
|
72
|
+
| `invalid_request` | `0` | the input failed the entry's schema; nothing was sent (`issues`) |
|
|
73
|
+
| `invalid_response` | 2xx | the response was not JSON or did not match the entry's schema |
|
|
74
|
+
| `timeout` | `0` | no response within the call's timeout |
|
|
75
|
+
| `network_error` | `0` | the request failed (service not running, DNS, ...) |
|
|
76
|
+
| `invalid_options` | `0` | `createMetaClient` options are invalid |
|
|
77
|
+
|
|
78
|
+
A non-2xx body is always Fastify's standard error body, `{ statusCode, code?, error, message }`. jeeves-meta's own codes (`META_ERROR_CODES` and `META_ERROR_STATUS`, exported here and by `@karmaniverous/jeeves-meta-core`):
|
|
79
|
+
|
|
80
|
+
| `code` | Status | When |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `META_INVALID_JSONPATH` | 400 | `config` with a `path` that is not valid JSONPath |
|
|
83
|
+
| `META_NOT_FOUND` | 404 | no meta at the path given to `metaDetail`, `updateMeta` or `preview` |
|
|
84
|
+
| `META_ALREADY_EXISTS` | 409 | `seed` where a `.meta/` already exists |
|
|
85
|
+
| `META_NOT_LOCKED` | 409 | `unlock` where there is no lock file |
|
|
86
|
+
| `META_CONFIG_UNAVAILABLE` | 500 | `configApply` on a service started without a config file |
|
|
87
|
+
| `META_WATCHER_UNAVAILABLE` | 503 | `listMetas`, `preview` or `synthesize` could not reach jeeves-watcher; retry later |
|
|
88
|
+
| `META_NOT_READY` | 503 | the service is still starting; retry shortly (`config` and `configApply` are served during start-up) |
|
|
89
|
+
|
|
90
|
+
Also: `FST_ERR_VALIDATION` (400) when a request fails the catalog's schemas (the SDK checks input first, so you see `invalid_request` instead), jeeves' `JEEVES_CONFIG_INVALID` from `configApply`, and `JEEVES_RESPONSE_VALIDATION` (500) if jeeves-meta's own response fails its schema. No error carries `Retry-After`.
|
|
91
|
+
|
|
92
|
+
## OpenAPI
|
|
93
|
+
|
|
94
|
+
The service serves its OpenAPI 3.1 document at `GET /openapi.json`; the same document ships as `@karmaniverous/jeeves-meta-core/openapi.json`.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
BSD-3-Clause
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module MetaError
|
|
3
|
+
* The single error class thrown by the SDK: client-side validation, timeouts, network failures, non-2xx responses and unparseable responses. Pure.
|
|
4
|
+
*/
|
|
5
|
+
import type { ErrorBody } from '@karmaniverous/jeeves/catalog';
|
|
6
|
+
import type { MetaErrorCode } from '@karmaniverous/jeeves-meta-core';
|
|
7
|
+
/**
|
|
8
|
+
* Codes the SDK assigns itself (lower case, so they never collide with a
|
|
9
|
+
* server code). An error from a non-2xx response carries the server's code
|
|
10
|
+
* instead: the body's `code` (a `MetaErrorCode` from core
|
|
11
|
+
* `META_ERROR_CODES`, a jeeves `JEEVES_*` code or a Fastify `FST_ERR_*`
|
|
12
|
+
* code), else `http_<status>` (a masked 500, an unknown route, a body that
|
|
13
|
+
* isn't Fastify's).
|
|
14
|
+
*
|
|
15
|
+
* - `invalid_options`: {@link createMetaClient} options are invalid.
|
|
16
|
+
* - `invalid_request`: the input failed the catalog entry's request schema; nothing was sent.
|
|
17
|
+
* - `invalid_response`: a 2xx response was not JSON or failed the entry's response schema.
|
|
18
|
+
* - `timeout`: no response within the call's timeout.
|
|
19
|
+
* - `network_error`: `fetch` rejected (connection refused, DNS failure, ...).
|
|
20
|
+
*/
|
|
21
|
+
export type MetaClientErrorCode = 'invalid_options' | 'invalid_request' | 'invalid_response' | 'timeout' | 'network_error';
|
|
22
|
+
/**
|
|
23
|
+
* What {@link MetaError.code} holds: a meta code, a client code,
|
|
24
|
+
* `http_<status>`, or any other server code (`JEEVES_*`, `FST_ERR_*`); the
|
|
25
|
+
* named ones autocomplete.
|
|
26
|
+
*/
|
|
27
|
+
export type MetaErrorCodeValue = MetaErrorCode | MetaClientErrorCode | `http_${number}` | (string & {});
|
|
28
|
+
/** Constructor fields of a {@link MetaError}. */
|
|
29
|
+
export interface MetaErrorInit {
|
|
30
|
+
/** The server's code, `http_<status>` or a {@link MetaClientErrorCode}. */
|
|
31
|
+
code: MetaErrorCodeValue;
|
|
32
|
+
/** Human-readable message (a response's: the body's `message`). */
|
|
33
|
+
message: string;
|
|
34
|
+
/** HTTP status when a response arrived, else 0. */
|
|
35
|
+
status: number;
|
|
36
|
+
/** Parsed error body of a non-2xx response. */
|
|
37
|
+
body?: ErrorBody;
|
|
38
|
+
/** The non-2xx response body exactly as received. */
|
|
39
|
+
responseText?: string;
|
|
40
|
+
/** Zod issues of an `invalid_request` or `invalid_response`. */
|
|
41
|
+
issues?: unknown;
|
|
42
|
+
/** Underlying error, if any. */
|
|
43
|
+
cause?: unknown;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The one error type thrown by every SDK call. The SDK never retries;
|
|
47
|
+
* branch on `code` (`META_NOT_FOUND`, `META_WATCHER_UNAVAILABLE`,
|
|
48
|
+
* `META_NOT_READY`, ...), never on `message`;
|
|
49
|
+
* `status` is the HTTP status.
|
|
50
|
+
*/
|
|
51
|
+
export declare class MetaError extends Error {
|
|
52
|
+
/**
|
|
53
|
+
* Machine-readable code: the response body's `code` (`META_*`,
|
|
54
|
+
* `JEEVES_*`, `FST_ERR_*`), else `http_<status>`, or a
|
|
55
|
+
* {@link MetaClientErrorCode} when no usable response arrived.
|
|
56
|
+
*/
|
|
57
|
+
readonly code: MetaErrorCodeValue;
|
|
58
|
+
/** HTTP status when a response arrived, else 0. */
|
|
59
|
+
readonly status: number;
|
|
60
|
+
/**
|
|
61
|
+
* Parsed error body of a non-2xx response: Fastify's standard body
|
|
62
|
+
* `{ statusCode, code?, error, message }`.
|
|
63
|
+
*/
|
|
64
|
+
readonly body?: ErrorBody;
|
|
65
|
+
/** The non-2xx response body exactly as received (JSON or not). */
|
|
66
|
+
readonly responseText?: string;
|
|
67
|
+
/** Zod issues of an `invalid_request` or `invalid_response`. */
|
|
68
|
+
readonly issues?: unknown;
|
|
69
|
+
/**
|
|
70
|
+
* Create a meta error.
|
|
71
|
+
*
|
|
72
|
+
* @param init - Code, message, status and optional body, text, issues and cause.
|
|
73
|
+
*/
|
|
74
|
+
constructor(init: MetaErrorInit);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The body of a non-2xx response as text: exactly as received when it was
|
|
78
|
+
* captured, else the parsed body as JSON (`{}` when there is none).
|
|
79
|
+
*
|
|
80
|
+
* @param error - A MetaError with an HTTP status.
|
|
81
|
+
* @returns The body text.
|
|
82
|
+
*/
|
|
83
|
+
export declare function errorBodyText(error: MetaError): string;
|
|
84
|
+
/**
|
|
85
|
+
* Type guard for {@link MetaError}.
|
|
86
|
+
*
|
|
87
|
+
* @param value - Any value (typically a caught error).
|
|
88
|
+
* @returns `true` when `value` is a {@link MetaError}.
|
|
89
|
+
*/
|
|
90
|
+
export declare function isMetaError(value: unknown): value is MetaError;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module createMetaClient
|
|
3
|
+
* Typed meta API client built on jeeves' catalog client (Decision 186): one method per `META_CATALOG` entry, named by the entry, each taking its input and optional per-call options; every failure is a {@link MetaError}. Network I/O via fetch.
|
|
4
|
+
*/
|
|
5
|
+
import { type CatalogInput, type ClientResponse } from '@karmaniverous/jeeves/catalog';
|
|
6
|
+
import { type MetaCatalog } from '@karmaniverous/jeeves-meta-core';
|
|
7
|
+
import { type MetaClientOptions } from './options.js';
|
|
8
|
+
/** Options for one call. */
|
|
9
|
+
export interface CallOptions {
|
|
10
|
+
/** Timeout of this call in milliseconds (overrides the client's). */
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/** The client method for catalog entry `E`: input optional when no field is required. */
|
|
14
|
+
type MethodOf<E extends MetaCatalog['entries'][number]> = Partial<CatalogInput<E>> extends CatalogInput<E> ? (input?: CatalogInput<E>, options?: CallOptions) => Promise<ClientResponse<E>> : (input: CatalogInput<E>, options?: CallOptions) => Promise<ClientResponse<E>>;
|
|
15
|
+
/**
|
|
16
|
+
* A meta API client: one method per `META_CATALOG` entry, named by the
|
|
17
|
+
* entry (`status`, `listMetas`, `metaDetail`, `synthesize`, ...). Every
|
|
18
|
+
* method validates its input against the entry's schemas before sending,
|
|
19
|
+
* parses the response with the entry's response schema, and throws a
|
|
20
|
+
* {@link MetaError} on any failure. No retries.
|
|
21
|
+
*/
|
|
22
|
+
export type MetaClient = {
|
|
23
|
+
[E in MetaCatalog['entries'][number] as E['name']]: MethodOf<E>;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Create a meta API client.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const meta = createMetaClient({ baseUrl: 'http://127.0.0.1:1938' });
|
|
31
|
+
* const { owedPhase } = await meta.synthesize({ path: 'j:/domains/x' });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @param options - Base URL, timeout (default 120 s) and `fetch`.
|
|
35
|
+
* @returns A {@link MetaClient}.
|
|
36
|
+
* @throws {@link MetaError} `invalid_options` when the options are invalid.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createMetaClient(options: MetaClientOptions): MetaClient;
|
|
39
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module jeeves-meta-sdk
|
|
3
|
+
* Typed TypeScript client for the jeeves-meta HTTP API, built from jeeves-meta's endpoint catalog (`META_CATALOG`). Start with {@link createMetaClient}; every failure is a {@link MetaError}.
|
|
4
|
+
*/
|
|
5
|
+
export { type CallOptions, createMetaClient, type MetaClient, } from './createMetaClient.js';
|
|
6
|
+
export { errorBodyText, isMetaError, type MetaClientErrorCode, MetaError, type MetaErrorCodeValue, type MetaErrorInit, } from './MetaError.js';
|
|
7
|
+
export { DEFAULT_TIMEOUT_MS, type FetchLike, type MetaClientOptions, } from './options.js';
|
|
8
|
+
export { META_ERROR_CODES, META_ERROR_STATUS, type MetaErrorCode, } from '@karmaniverous/jeeves-meta-core';
|
|
9
|
+
export type { MetaEntryName, MetaHealth, MetaInput, MetaResponse, } from '@karmaniverous/jeeves-meta-core';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { CatalogClientError, createCatalogClient } from '@karmaniverous/jeeves/catalog';
|
|
2
|
+
import { META_CATALOG } from '@karmaniverous/jeeves-meta-core';
|
|
3
|
+
export { META_ERROR_CODES, META_ERROR_STATUS } from '@karmaniverous/jeeves-meta-core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @module MetaError
|
|
7
|
+
* The single error class thrown by the SDK: client-side validation, timeouts, network failures, non-2xx responses and unparseable responses. Pure.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The one error type thrown by every SDK call. The SDK never retries;
|
|
11
|
+
* branch on `code` (`META_NOT_FOUND`, `META_WATCHER_UNAVAILABLE`,
|
|
12
|
+
* `META_NOT_READY`, ...), never on `message`;
|
|
13
|
+
* `status` is the HTTP status.
|
|
14
|
+
*/
|
|
15
|
+
class MetaError extends Error {
|
|
16
|
+
/**
|
|
17
|
+
* Machine-readable code: the response body's `code` (`META_*`,
|
|
18
|
+
* `JEEVES_*`, `FST_ERR_*`), else `http_<status>`, or a
|
|
19
|
+
* {@link MetaClientErrorCode} when no usable response arrived.
|
|
20
|
+
*/
|
|
21
|
+
code;
|
|
22
|
+
/** HTTP status when a response arrived, else 0. */
|
|
23
|
+
status;
|
|
24
|
+
/**
|
|
25
|
+
* Parsed error body of a non-2xx response: Fastify's standard body
|
|
26
|
+
* `{ statusCode, code?, error, message }`.
|
|
27
|
+
*/
|
|
28
|
+
body;
|
|
29
|
+
/** The non-2xx response body exactly as received (JSON or not). */
|
|
30
|
+
responseText;
|
|
31
|
+
/** Zod issues of an `invalid_request` or `invalid_response`. */
|
|
32
|
+
issues;
|
|
33
|
+
/**
|
|
34
|
+
* Create a meta error.
|
|
35
|
+
*
|
|
36
|
+
* @param init - Code, message, status and optional body, text, issues and cause.
|
|
37
|
+
*/
|
|
38
|
+
constructor(init) {
|
|
39
|
+
super(init.message, init.cause === undefined ? {} : { cause: init.cause });
|
|
40
|
+
this.name = 'MetaError';
|
|
41
|
+
this.code = init.code;
|
|
42
|
+
this.status = init.status;
|
|
43
|
+
this.body = init.body;
|
|
44
|
+
this.responseText = init.responseText;
|
|
45
|
+
this.issues = init.issues;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The body of a non-2xx response as text: exactly as received when it was
|
|
50
|
+
* captured, else the parsed body as JSON (`{}` when there is none).
|
|
51
|
+
*
|
|
52
|
+
* @param error - A MetaError with an HTTP status.
|
|
53
|
+
* @returns The body text.
|
|
54
|
+
*/
|
|
55
|
+
function errorBodyText(error) {
|
|
56
|
+
return error.responseText ?? JSON.stringify(error.body ?? {});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Type guard for {@link MetaError}.
|
|
60
|
+
*
|
|
61
|
+
* @param value - Any value (typically a caught error).
|
|
62
|
+
* @returns `true` when `value` is a {@link MetaError}.
|
|
63
|
+
*/
|
|
64
|
+
function isMetaError(value) {
|
|
65
|
+
return value instanceof MetaError;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @module options
|
|
70
|
+
* Client options for `createMetaClient`, their validation, and the `fetch` adapter that keeps each non-2xx response body as received (for `MetaError.responseText`). Pure apart from wrapping `fetch`.
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Default per-call timeout: 120 s, jeeves' `DEFAULT_FETCH_TIMEOUT_MS`.
|
|
74
|
+
*/
|
|
75
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
76
|
+
const invalid = (message) => new MetaError({ code: 'invalid_options', status: 0, message });
|
|
77
|
+
/** Statuses whose responses carry no body. */
|
|
78
|
+
const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);
|
|
79
|
+
/**
|
|
80
|
+
* Adapt a {@link FetchLike} to the `fetch` signature jeeves' catalog client
|
|
81
|
+
* takes, keeping the body text of every non-2xx response in `errorTexts`.
|
|
82
|
+
*
|
|
83
|
+
* @remarks
|
|
84
|
+
* jeeves' client hands the response's own `Headers` object to the error it
|
|
85
|
+
* throws (`CatalogClientError.headers`), so the text is found again by that
|
|
86
|
+
* object. The client reads the body after this adapter, so the adapter
|
|
87
|
+
* returns a fresh response with the same status, headers and text.
|
|
88
|
+
*
|
|
89
|
+
* @param fetchImpl - The caller's fetch.
|
|
90
|
+
* @param errorTexts - Where non-2xx body texts are kept.
|
|
91
|
+
* @returns A `typeof fetch` that calls `fetchImpl`.
|
|
92
|
+
*/
|
|
93
|
+
function captureErrorText(fetchImpl, errorTexts) {
|
|
94
|
+
return async (input, init) => {
|
|
95
|
+
const url = typeof input === 'string'
|
|
96
|
+
? input
|
|
97
|
+
: input instanceof URL
|
|
98
|
+
? input.href
|
|
99
|
+
: input.url;
|
|
100
|
+
const res = await fetchImpl(url, init ?? {});
|
|
101
|
+
if (res.ok)
|
|
102
|
+
return res;
|
|
103
|
+
if (NULL_BODY_STATUSES.has(res.status)) {
|
|
104
|
+
errorTexts.set(res.headers, '');
|
|
105
|
+
return res;
|
|
106
|
+
}
|
|
107
|
+
const text = await res.text();
|
|
108
|
+
const copy = new Response(text, {
|
|
109
|
+
status: res.status,
|
|
110
|
+
statusText: res.statusText,
|
|
111
|
+
headers: res.headers,
|
|
112
|
+
});
|
|
113
|
+
errorTexts.set(copy.headers, text);
|
|
114
|
+
return copy;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check a timeout option.
|
|
119
|
+
*
|
|
120
|
+
* @param name - Option name (for the message).
|
|
121
|
+
* @param value - The value.
|
|
122
|
+
* @returns The value.
|
|
123
|
+
* @throws {@link MetaError} `invalid_options` unless positive and finite.
|
|
124
|
+
*/
|
|
125
|
+
function checkTimeout(name, value) {
|
|
126
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
127
|
+
throw invalid(`${name} must be a positive number`);
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Validate and normalise client options.
|
|
132
|
+
*
|
|
133
|
+
* @param options - Caller options.
|
|
134
|
+
* @returns Resolved options.
|
|
135
|
+
* @throws {@link MetaError} `invalid_options` on a bad base URL (not http(s), or carrying a query string or fragment), a non-positive timeout or no `fetch`.
|
|
136
|
+
*/
|
|
137
|
+
function resolveClientOptions(options) {
|
|
138
|
+
const { baseUrl } = options;
|
|
139
|
+
if (!URL.canParse(baseUrl) || !/^https?:$/.test(new URL(baseUrl).protocol)) {
|
|
140
|
+
throw invalid(`baseUrl must be an http(s) URL, got "${baseUrl}"`);
|
|
141
|
+
}
|
|
142
|
+
// Entry paths are appended to baseUrl, so a query or fragment would
|
|
143
|
+
// swallow them. Check the raw string: URL drops an empty `?` or `#`.
|
|
144
|
+
if (/[?#]/.test(baseUrl)) {
|
|
145
|
+
throw invalid(`baseUrl must not contain a query string or fragment, got "${baseUrl}"`);
|
|
146
|
+
}
|
|
147
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
148
|
+
if (fetchImpl === undefined) {
|
|
149
|
+
throw invalid('No fetch implementation available; pass options.fetch');
|
|
150
|
+
}
|
|
151
|
+
const errorTexts = new WeakMap();
|
|
152
|
+
return {
|
|
153
|
+
baseUrl: baseUrl.replace(/\/+$/, ''),
|
|
154
|
+
timeoutMs: checkTimeout('timeoutMs', options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
155
|
+
fetch: captureErrorText(fetchImpl, errorTexts),
|
|
156
|
+
errorTexts,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @module toMetaError
|
|
162
|
+
* The one mapping from what jeeves' catalog client throws to {@link MetaError} (Decision 186): a response keeps its HTTP status, its body's `code` (else `http_<status>`) and its body's `message`; client-side failures get the SDK's own codes and status 0. Pure.
|
|
163
|
+
*/
|
|
164
|
+
/**
|
|
165
|
+
* The SDK code of a failure that produced no usable response.
|
|
166
|
+
*
|
|
167
|
+
* @param cause - The underlying error.
|
|
168
|
+
* @returns `timeout`, `invalid_response` (a 2xx body that is not JSON) or `network_error`.
|
|
169
|
+
*/
|
|
170
|
+
function noResponseCode(cause) {
|
|
171
|
+
if (cause instanceof Error && cause.name === 'TimeoutError')
|
|
172
|
+
return 'timeout';
|
|
173
|
+
if (cause instanceof SyntaxError)
|
|
174
|
+
return 'invalid_response';
|
|
175
|
+
return 'network_error';
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Message of an underlying error.
|
|
179
|
+
*
|
|
180
|
+
* @param cause - Anything thrown.
|
|
181
|
+
* @returns Its message, or its string form.
|
|
182
|
+
*/
|
|
183
|
+
const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
|
|
184
|
+
/**
|
|
185
|
+
* A string field of an error body, treating empty as absent.
|
|
186
|
+
*
|
|
187
|
+
* @param value - The field.
|
|
188
|
+
* @returns It, or `undefined` when missing or empty.
|
|
189
|
+
*/
|
|
190
|
+
const nonEmpty = (value) => value === '' ? undefined : value;
|
|
191
|
+
/**
|
|
192
|
+
* Convert a failed catalog call to a {@link MetaError}.
|
|
193
|
+
*
|
|
194
|
+
* @param error - What the catalog client threw.
|
|
195
|
+
* @param operation - Catalog entry name (used in messages).
|
|
196
|
+
* @param timeoutMs - The call's timeout (used in the timeout message).
|
|
197
|
+
* @param responseText - The non-2xx body as received, when captured.
|
|
198
|
+
* @returns The error to throw.
|
|
199
|
+
*/
|
|
200
|
+
function toMetaError(error, operation, timeoutMs, responseText) {
|
|
201
|
+
if (!(error instanceof CatalogClientError)) {
|
|
202
|
+
return new MetaError({
|
|
203
|
+
code: 'network_error',
|
|
204
|
+
status: 0,
|
|
205
|
+
message: `${operation} request failed: ${messageOf(error)}`,
|
|
206
|
+
cause: error,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (error.status >= 300) {
|
|
210
|
+
return new MetaError({
|
|
211
|
+
code: nonEmpty(error.code) ?? `http_${String(error.status)}`,
|
|
212
|
+
status: error.status,
|
|
213
|
+
message: nonEmpty(error.body?.message) ??
|
|
214
|
+
nonEmpty(error.body?.error) ??
|
|
215
|
+
`HTTP ${String(error.status)}`,
|
|
216
|
+
...(error.body === undefined ? {} : { body: error.body }),
|
|
217
|
+
...(responseText === undefined ? {} : { responseText }),
|
|
218
|
+
cause: error,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (error.issues !== undefined) {
|
|
222
|
+
const responded = error.status > 0;
|
|
223
|
+
return new MetaError({
|
|
224
|
+
code: responded ? 'invalid_response' : 'invalid_request',
|
|
225
|
+
status: error.status,
|
|
226
|
+
message: responded
|
|
227
|
+
? `${operation} response did not match the expected shape`
|
|
228
|
+
: error.message,
|
|
229
|
+
issues: error.issues,
|
|
230
|
+
cause: error,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const code = noResponseCode(error.cause);
|
|
234
|
+
const message = code === 'timeout'
|
|
235
|
+
? `${operation} timed out after ${String(timeoutMs)} ms`
|
|
236
|
+
: code === 'invalid_response'
|
|
237
|
+
? `${operation} returned a response that is not JSON`
|
|
238
|
+
: `${operation} request failed: ${messageOf(error.cause)}`;
|
|
239
|
+
return new MetaError({
|
|
240
|
+
code,
|
|
241
|
+
// A non-JSON 2xx did arrive: keep its status.
|
|
242
|
+
status: code === 'invalid_response' ? error.status : 0,
|
|
243
|
+
message,
|
|
244
|
+
cause: error,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @module createMetaClient
|
|
250
|
+
* Typed meta API client built on jeeves' catalog client (Decision 186): one method per `META_CATALOG` entry, named by the entry, each taking its input and optional per-call options; every failure is a {@link MetaError}. Network I/O via fetch.
|
|
251
|
+
*/
|
|
252
|
+
/**
|
|
253
|
+
* Create a meta API client.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* const meta = createMetaClient({ baseUrl: 'http://127.0.0.1:1938' });
|
|
258
|
+
* const { owedPhase } = await meta.synthesize({ path: 'j:/domains/x' });
|
|
259
|
+
* ```
|
|
260
|
+
*
|
|
261
|
+
* @param options - Base URL, timeout (default 120 s) and `fetch`.
|
|
262
|
+
* @returns A {@link MetaClient}.
|
|
263
|
+
* @throws {@link MetaError} `invalid_options` when the options are invalid.
|
|
264
|
+
*/
|
|
265
|
+
function createMetaClient(options) {
|
|
266
|
+
const resolved = resolveClientOptions(options);
|
|
267
|
+
// jeeves' catalog client takes one timeout, so keep one per timeout used.
|
|
268
|
+
const clients = new Map();
|
|
269
|
+
const clientFor = (timeoutMs) => {
|
|
270
|
+
const existing = clients.get(timeoutMs);
|
|
271
|
+
if (existing)
|
|
272
|
+
return existing;
|
|
273
|
+
const created = createCatalogClient(META_CATALOG, {
|
|
274
|
+
baseUrl: resolved.baseUrl,
|
|
275
|
+
timeoutMs,
|
|
276
|
+
fetch: resolved.fetch,
|
|
277
|
+
});
|
|
278
|
+
clients.set(timeoutMs, created);
|
|
279
|
+
return created;
|
|
280
|
+
};
|
|
281
|
+
const methods = META_CATALOG.entries.map(({ name }) => {
|
|
282
|
+
const method = async (input, call = {}) => {
|
|
283
|
+
const timeoutMs = call.timeoutMs === undefined
|
|
284
|
+
? resolved.timeoutMs
|
|
285
|
+
: checkTimeout('timeoutMs', call.timeoutMs);
|
|
286
|
+
const send = clientFor(timeoutMs)[name];
|
|
287
|
+
try {
|
|
288
|
+
return await send(input);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
const text = error instanceof CatalogClientError && error.headers
|
|
292
|
+
? resolved.errorTexts.get(error.headers)
|
|
293
|
+
: undefined;
|
|
294
|
+
throw toMetaError(error, name, timeoutMs, text);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
return [name, method];
|
|
298
|
+
});
|
|
299
|
+
return Object.fromEntries(methods);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export { DEFAULT_TIMEOUT_MS, MetaError, createMetaClient, errorBodyText, isMetaError };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module options
|
|
3
|
+
* Client options for `createMetaClient`, their validation, and the `fetch` adapter that keeps each non-2xx response body as received (for `MetaError.responseText`). Pure apart from wrapping `fetch`.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Default per-call timeout: 120 s, jeeves' `DEFAULT_FETCH_TIMEOUT_MS`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_TIMEOUT_MS = 120000;
|
|
9
|
+
/** A `fetch`-compatible function (the global `fetch` by default). */
|
|
10
|
+
export type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
|
|
11
|
+
/** Options for `createMetaClient`. */
|
|
12
|
+
export interface MetaClientOptions {
|
|
13
|
+
/** Base URL of the meta API, e.g. `http://127.0.0.1:1938`. May carry a path prefix; must not carry a query string or fragment. */
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
/** Per-call timeout in milliseconds. Default: 120000. */
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
/** `fetch` implementation (for tests or custom agents). Default: global `fetch`. */
|
|
18
|
+
fetch?: FetchLike;
|
|
19
|
+
}
|
|
20
|
+
/** Validated, normalised client options. */
|
|
21
|
+
export interface ResolvedClientOptions {
|
|
22
|
+
/** Base URL without trailing slashes. */
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
/** Default timeout. */
|
|
25
|
+
timeoutMs: number;
|
|
26
|
+
/** The caller's fetch, adapted (see {@link captureErrorText}). */
|
|
27
|
+
fetch: typeof fetch;
|
|
28
|
+
/** Non-2xx body texts, keyed by the response's `Headers`. */
|
|
29
|
+
errorTexts: WeakMap<Headers, string>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Adapt a {@link FetchLike} to the `fetch` signature jeeves' catalog client
|
|
33
|
+
* takes, keeping the body text of every non-2xx response in `errorTexts`.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* jeeves' client hands the response's own `Headers` object to the error it
|
|
37
|
+
* throws (`CatalogClientError.headers`), so the text is found again by that
|
|
38
|
+
* object. The client reads the body after this adapter, so the adapter
|
|
39
|
+
* returns a fresh response with the same status, headers and text.
|
|
40
|
+
*
|
|
41
|
+
* @param fetchImpl - The caller's fetch.
|
|
42
|
+
* @param errorTexts - Where non-2xx body texts are kept.
|
|
43
|
+
* @returns A `typeof fetch` that calls `fetchImpl`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function captureErrorText(fetchImpl: FetchLike, errorTexts: WeakMap<Headers, string>): typeof fetch;
|
|
46
|
+
/**
|
|
47
|
+
* Check a timeout option.
|
|
48
|
+
*
|
|
49
|
+
* @param name - Option name (for the message).
|
|
50
|
+
* @param value - The value.
|
|
51
|
+
* @returns The value.
|
|
52
|
+
* @throws {@link MetaError} `invalid_options` unless positive and finite.
|
|
53
|
+
*/
|
|
54
|
+
export declare function checkTimeout(name: string, value: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Validate and normalise client options.
|
|
57
|
+
*
|
|
58
|
+
* @param options - Caller options.
|
|
59
|
+
* @returns Resolved options.
|
|
60
|
+
* @throws {@link MetaError} `invalid_options` on a bad base URL (not http(s), or carrying a query string or fragment), a non-positive timeout or no `fetch`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveClientOptions(options: MetaClientOptions): ResolvedClientOptions;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module toMetaError
|
|
3
|
+
* The one mapping from what jeeves' catalog client throws to {@link MetaError} (Decision 186): a response keeps its HTTP status, its body's `code` (else `http_<status>`) and its body's `message`; client-side failures get the SDK's own codes and status 0. Pure.
|
|
4
|
+
*/
|
|
5
|
+
import { MetaError } from './MetaError.js';
|
|
6
|
+
/**
|
|
7
|
+
* Convert a failed catalog call to a {@link MetaError}.
|
|
8
|
+
*
|
|
9
|
+
* @param error - What the catalog client threw.
|
|
10
|
+
* @param operation - Catalog entry name (used in messages).
|
|
11
|
+
* @param timeoutMs - The call's timeout (used in the timeout message).
|
|
12
|
+
* @param responseText - The non-2xx body as received, when captured.
|
|
13
|
+
* @returns The error to throw.
|
|
14
|
+
*/
|
|
15
|
+
export declare function toMetaError(error: unknown, operation: string, timeoutMs: number, responseText?: string): MetaError;
|
package/package.json
CHANGED
|
@@ -1,6 +1,101 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karmaniverous/jeeves-meta-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-0",
|
|
4
|
+
"author": "Jason Williscroft",
|
|
5
|
+
"description": "Typed TypeScript client for the jeeves-meta HTTP API (metas, synthesis, queue, preview, seed, unlock, status and config), built from jeeves-meta's endpoint catalog.",
|
|
6
|
+
"license": "BSD-3-Clause",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"jeeves-meta",
|
|
25
|
+
"sdk",
|
|
26
|
+
"client",
|
|
27
|
+
"synthesis"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/karmaniverous/jeeves-meta.git",
|
|
32
|
+
"directory": "packages/sdk"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/karmaniverous/jeeves-meta/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/karmaniverous/jeeves-meta#readme",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=24.22.2"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@karmaniverous/jeeves": "0.11.0",
|
|
43
|
+
"@karmaniverous/jeeves-meta-core": "^0.4.0-0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@dotenvx/dotenvx": "^2.38.2",
|
|
47
|
+
"@rollup/plugin-typescript": "^12.3.1",
|
|
48
|
+
"cross-env": "^10.1.0",
|
|
49
|
+
"release-it": "^21.1.2",
|
|
50
|
+
"rollup": "^4.68.3",
|
|
51
|
+
"tslib": "^2.9.1",
|
|
52
|
+
"vitest": "^5.1.0"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "rimraf dist && cross-env NO_COLOR=1 rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
|
|
56
|
+
"changelog": "git-cliff -o CHANGELOG.md",
|
|
57
|
+
"knip": "knip",
|
|
58
|
+
"lint": "eslint .",
|
|
59
|
+
"lint:fix": "eslint --fix .",
|
|
60
|
+
"release": "dotenvx run -f .env.local -- release-it",
|
|
61
|
+
"release:pre": "dotenvx run -f .env.local -- release-it --no-git.requireBranch --github.preRelease --preRelease",
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"typecheck": "tsc --noEmit"
|
|
64
|
+
},
|
|
65
|
+
"release-it": {
|
|
66
|
+
"git": {
|
|
67
|
+
"changelog": "npx git-cliff --unreleased --strip header",
|
|
68
|
+
"commitMessage": "chore: release @karmaniverous/jeeves-meta-sdk v${version}",
|
|
69
|
+
"tagName": "sdk/${version}",
|
|
70
|
+
"requireBranch": "main"
|
|
71
|
+
},
|
|
72
|
+
"github": {
|
|
73
|
+
"release": true,
|
|
74
|
+
"releaseName": "${name.split('/').pop()}@${version}",
|
|
75
|
+
"releaseNotes": "npx git-cliff --tag sdk/${version} --unreleased --strip header"
|
|
76
|
+
},
|
|
77
|
+
"hooks": {
|
|
78
|
+
"after:init": [
|
|
79
|
+
"npm run lint",
|
|
80
|
+
"npm run typecheck",
|
|
81
|
+
"npm run test",
|
|
82
|
+
"npm run build"
|
|
83
|
+
],
|
|
84
|
+
"after:bump": [
|
|
85
|
+
"npm install --package-lock-only --ignore-scripts"
|
|
86
|
+
],
|
|
87
|
+
"before:npm:release": [
|
|
88
|
+
"npx git-cliff --tag sdk/${version} -o CHANGELOG.md",
|
|
89
|
+
"git add -A"
|
|
90
|
+
],
|
|
91
|
+
"after:release": [
|
|
92
|
+
"git switch -c release/sdk/${version}",
|
|
93
|
+
"git push -u origin release/sdk/${version}",
|
|
94
|
+
"git switch ${branchName}"
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
"npm": {
|
|
98
|
+
"publish": true
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|