wenmar-open 0.0.1 → 0.2.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.
@@ -0,0 +1,57 @@
1
+ /** The codes the API answers with. */
2
+ export type ApiErrorCode = "invalid_vin" | "validation_failed" | "not_found" | "method_not_allowed" | "payload_too_large" | "uri_too_long" | "rate_limited" | "unavailable" | "internal_error";
3
+ /**
4
+ * The codes this client adds for failures that never reached the API or
5
+ * did not come from it.
6
+ *
7
+ * - `network`: the request could not be sent or the answer could not be read.
8
+ * - `timeout`: no complete answer within `timeoutMs`.
9
+ * - `aborted`: the caller's `signal` was aborted.
10
+ * - `bad_response`: something answered, but not with the API's JSON. A proxy's
11
+ * error page and a sign-in page from a captive portal both end up here.
12
+ */
13
+ export type ClientErrorCode = "network" | "timeout" | "aborted" | "bad_response";
14
+ /**
15
+ * The codes of `wenmar-open/offline`, for failures of the data and not of
16
+ * the question. The command-line tool uses `no_data` and `data_invalid` for
17
+ * the same things.
18
+ *
19
+ * - `no_data`: there is no data file where one was looked for.
20
+ * - `data_invalid`: the database is not a data file this version reads: it
21
+ * has another schema version, or a row is not what the layout says.
22
+ * - `store_error`: the database failed while it was being read.
23
+ */
24
+ export type OfflineErrorCode = "no_data" | "data_invalid" | "store_error";
25
+ /**
26
+ * Every code known when this version was published. The API may add codes,
27
+ * so `code` is typed to accept any string while still completing these.
28
+ */
29
+ export type ErrorCode = ApiErrorCode | ClientErrorCode | OfflineErrorCode | (string & {});
30
+ export interface WenmarOpenErrorInit {
31
+ code: ErrorCode;
32
+ message: string;
33
+ details?: Record<string, unknown> | undefined;
34
+ status?: number | undefined;
35
+ retryAfter?: number | undefined;
36
+ cause?: unknown;
37
+ }
38
+ /** The one error every method of the client throws. */
39
+ export declare class WenmarOpenError extends Error {
40
+ /** A stable code to branch on. */
41
+ readonly code: ErrorCode;
42
+ /** The HTTP status, or `undefined` when there was no HTTP answer. */
43
+ readonly status: number | undefined;
44
+ /** More about the error, as the API sent it. Empty when there is nothing. */
45
+ readonly details: Record<string, unknown>;
46
+ /**
47
+ * Seconds to wait before trying again, from the `Retry-After` header of a
48
+ * `429` or `503`. `undefined` when the answer did not say.
49
+ */
50
+ readonly retryAfter: number | undefined;
51
+ constructor(init: WenmarOpenErrorInit);
52
+ }
53
+ /**
54
+ * Reads a `Retry-After` header: a number of seconds, or an HTTP date.
55
+ * Returns whole seconds, never negative, or `undefined` if it is neither.
56
+ */
57
+ export declare function parseRetryAfter(header: string | null, now?: number): number | undefined;
package/dist/errors.js ADDED
@@ -0,0 +1,37 @@
1
+ /** The one error every method of the client throws. */
2
+ export class WenmarOpenError extends Error {
3
+ /** A stable code to branch on. */
4
+ code;
5
+ /** The HTTP status, or `undefined` when there was no HTTP answer. */
6
+ status;
7
+ /** More about the error, as the API sent it. Empty when there is nothing. */
8
+ details;
9
+ /**
10
+ * Seconds to wait before trying again, from the `Retry-After` header of a
11
+ * `429` or `503`. `undefined` when the answer did not say.
12
+ */
13
+ retryAfter;
14
+ constructor(init) {
15
+ super(init.message, init.cause === undefined ? undefined : { cause: init.cause });
16
+ this.name = "WenmarOpenError";
17
+ this.code = init.code;
18
+ this.status = init.status;
19
+ this.details = init.details ?? {};
20
+ this.retryAfter = init.retryAfter;
21
+ }
22
+ }
23
+ /**
24
+ * Reads a `Retry-After` header: a number of seconds, or an HTTP date.
25
+ * Returns whole seconds, never negative, or `undefined` if it is neither.
26
+ */
27
+ export function parseRetryAfter(header, now = Date.now()) {
28
+ if (header === null)
29
+ return undefined;
30
+ const text = header.trim();
31
+ if (/^\d+$/.test(text))
32
+ return Number(text);
33
+ const date = Date.parse(text);
34
+ if (Number.isNaN(date))
35
+ return undefined;
36
+ return Math.max(0, Math.ceil((date - now) / 1000));
37
+ }
@@ -0,0 +1,7 @@
1
+ export { MAX_BATCH, isBatchError } from "./batch.js";
2
+ export { DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, WenmarOpen } from "./client.js";
3
+ export type { ClientOptions, DecodeOptions, FetchInit, FetchLike, FetchResponse, RequestOptions, } from "./client.js";
4
+ export { WenmarOpenError } from "./errors.js";
5
+ export type { ApiErrorCode, ClientErrorCode, ErrorCode, OfflineErrorCode, WenmarOpenErrorInit, } from "./errors.js";
6
+ export type { BatchItem, CheckDigit, Engine, EngineOption, EnginesQuery, Entry, ErrorBody, Make, MakesQuery, Manufacturer, Meta, Model, ModelsQuery, Plant, Safety, SearchQuery, Selection, Submodel, SubmodelsQuery, VinDecode, Warning, YearsQuery, } from "./types.js";
7
+ export type { components, operations, paths } from "./schema.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { MAX_BATCH, isBatchError } from "./batch.js";
2
+ export { DEFAULT_BASE_URL, DEFAULT_TIMEOUT_MS, WenmarOpen } from "./client.js";
3
+ export { WenmarOpenError } from "./errors.js";
@@ -0,0 +1,70 @@
1
+ import type { DecodeOptions, RequestOptions } from "../client.js";
2
+ import type { BatchItem, EngineOption, EnginesQuery, Entry, Make, MakesQuery, Meta, Model, ModelsQuery, SearchQuery, Submodel, SubmodelsQuery, VinDecode, YearsQuery } from "../types.js";
3
+ import type { WasmSource } from "./engine.js";
4
+ import type { Store } from "./store.js";
5
+ /**
6
+ * The most times one question, or the opening of the data, may go back to
7
+ * the store. Opening takes 2; over the fixture of the tests a decode takes
8
+ * at most 7 and a search at most 4. Reaching this means something is wrong.
9
+ */
10
+ export declare const MAX_STEPS = 32;
11
+ export interface OfflineOptions {
12
+ /** Where the data is: `syncStore(...)`, `d1Store(...)`, or your own. */
13
+ store: Store;
14
+ /**
15
+ * The decoder. Leave it out everywhere but Cloudflare Workers, where it
16
+ * is the module `import wasm from "wenmar-open/offline.wasm"` gives.
17
+ */
18
+ wasm?: WasmSource;
19
+ /**
20
+ * The current calendar year, which bounds the model years a VIN can
21
+ * have. Default: the year of the device's clock, in UTC.
22
+ */
23
+ currentYear?: number;
24
+ }
25
+ /**
26
+ * VIN decoding and the vehicle catalog with no network: the methods of
27
+ * `WenmarOpen`, answered from a data file by the decoder inside the package.
28
+ * Every method gives what the hosted API gives for the same question and
29
+ * fails with the same `WenmarOpenError` codes.
30
+ */
31
+ export declare class WenmarOpenOffline {
32
+ #private;
33
+ constructor(options: OfflineOptions);
34
+ /** Decode one VIN. Spaces, dashes and lowercase are accepted. */
35
+ decodeVin(vin: string, options?: DecodeOptions): Promise<VinDecode>;
36
+ /**
37
+ * Decode up to 50 VINs. The answer has one item per VIN, in order: a
38
+ * decode, or the error a single call for that VIN would have thrown.
39
+ * Tell them apart with `isBatchError`.
40
+ */
41
+ decodeVins(vins: readonly string[], options?: RequestOptions): Promise<BatchItem[]>;
42
+ /** Model years, newest first. */
43
+ years(query?: YearsQuery, options?: RequestOptions): Promise<number[]>;
44
+ /** Makes, popular ones first and the rest by name. */
45
+ makes(query?: MakesQuery, options?: RequestOptions): Promise<Make[]>;
46
+ /** Models of a make, by name. An unknown make has no models. */
47
+ models(query: ModelsQuery, options?: RequestOptions): Promise<Model[]>;
48
+ /** Submodels of a model year: trims, or series where there are no trims. */
49
+ submodels(query: SubmodelsQuery, options?: RequestOptions): Promise<Submodel[]>;
50
+ /** The same as `submodels`, under the name most people use. */
51
+ trims(query: SubmodelsQuery, options?: RequestOptions): Promise<Submodel[]>;
52
+ /** Engines of a model year, in short form. */
53
+ engines(query: EnginesQuery, options?: RequestOptions): Promise<EngineOption[]>;
54
+ /**
55
+ * Catalog entries for free text such as `2019 civic si`, best first. The
56
+ * hosted API also finds words in any order with an index of its own;
57
+ * here, text the catalog cannot read as year, make, model and submodel
58
+ * finds nothing.
59
+ */
60
+ search(query: SearchQuery, options?: RequestOptions): Promise<Entry[]>;
61
+ /** One catalog entry by its id, such as `2019_honda_civic_si`. */
62
+ vehicle(id: string, options?: RequestOptions): Promise<Entry>;
63
+ /**
64
+ * The data version, the vPIC release it came from, and the build time.
65
+ * `server_version` is the version of this package.
66
+ */
67
+ meta(options?: RequestOptions): Promise<Meta>;
68
+ /** Closes the store, if the store was opened for this client. */
69
+ close(): void;
70
+ }
@@ -0,0 +1,245 @@
1
+ import { MAX_BATCH } from "../batch.js";
2
+ import { WenmarOpenError } from "../errors.js";
3
+ import { Engine, compile } from "./engine.js";
4
+ import { cleanRows } from "./store.js";
5
+ /**
6
+ * The most times one question, or the opening of the data, may go back to
7
+ * the store. Opening takes 2; over the fixture of the tests a decode takes
8
+ * at most 7 and a search at most 4. Reaching this means something is wrong.
9
+ */
10
+ export const MAX_STEPS = 32;
11
+ const keyOf = (statement) => `${statement.sql}\u0000${JSON.stringify(statement.params)}`;
12
+ function thrown(failure) {
13
+ return new WenmarOpenError({ code: failure.code, message: failure.message, details: failure.details });
14
+ }
15
+ /**
16
+ * The rows of every statement, from the store. A store that throws, and one
17
+ * that does not return one list for each statement, are `store_error`: the
18
+ * same whether the data is being opened or a question answered.
19
+ */
20
+ async function read(store, statements) {
21
+ let rows;
22
+ try {
23
+ rows = await store.query(statements);
24
+ }
25
+ catch (cause) {
26
+ if (cause instanceof WenmarOpenError)
27
+ throw cause;
28
+ throw new WenmarOpenError({
29
+ code: "store_error",
30
+ message: "The database failed while it was being read.",
31
+ cause,
32
+ });
33
+ }
34
+ if (!Array.isArray(rows) || rows.length !== statements.length) {
35
+ throw new WenmarOpenError({
36
+ code: "store_error",
37
+ message: "The store must return one list of rows for each statement.",
38
+ });
39
+ }
40
+ return rows;
41
+ }
42
+ /**
43
+ * VIN decoding and the vehicle catalog with no network: the methods of
44
+ * `WenmarOpen`, answered from a data file by the decoder inside the package.
45
+ * Every method gives what the hosted API gives for the same question and
46
+ * fails with the same `WenmarOpenError` codes.
47
+ */
48
+ export class WenmarOpenOffline {
49
+ #store;
50
+ #wasm;
51
+ #currentYear;
52
+ #module;
53
+ #engine;
54
+ constructor(options) {
55
+ this.#store = options.store;
56
+ this.#wasm = options.wasm;
57
+ this.#currentYear = options.currentYear;
58
+ }
59
+ /** Decode one VIN. Spaces, dashes and lowercase are accepted. */
60
+ async decodeVin(vin, options = {}) {
61
+ return this.#one({ op: "decode", args: { vin: String(vin), year: options.year } }, options);
62
+ }
63
+ /**
64
+ * Decode up to 50 VINs. The answer has one item per VIN, in order: a
65
+ * decode, or the error a single call for that VIN would have thrown.
66
+ * Tell them apart with `isBatchError`.
67
+ */
68
+ async decodeVins(vins, options = {}) {
69
+ if (vins.length > MAX_BATCH) {
70
+ throw new WenmarOpenError({
71
+ code: "validation_failed",
72
+ message: `a batch holds at most ${MAX_BATCH} VINs`,
73
+ details: { field: "vins", max: MAX_BATCH, received: vins.length },
74
+ });
75
+ }
76
+ const questions = vins.map((vin) => ({ op: "decode", args: { vin: String(vin) } }));
77
+ const outcomes = await this.#ask(questions, options);
78
+ return outcomes.map((outcome) => ("ok" in outcome ? outcome.ok : { error: outcome.error }));
79
+ }
80
+ /** Model years, newest first. */
81
+ async years(query = {}, options = {}) {
82
+ return this.#one({ op: "years", args: query }, options);
83
+ }
84
+ /** Makes, popular ones first and the rest by name. */
85
+ async makes(query = {}, options = {}) {
86
+ return this.#one({ op: "makes", args: query }, options);
87
+ }
88
+ /** Models of a make, by name. An unknown make has no models. */
89
+ async models(query, options = {}) {
90
+ return this.#one({ op: "models", args: query }, options);
91
+ }
92
+ /** Submodels of a model year: trims, or series where there are no trims. */
93
+ async submodels(query, options = {}) {
94
+ return this.#one({ op: "submodels", args: query }, options);
95
+ }
96
+ /** The same as `submodels`, under the name most people use. */
97
+ async trims(query, options = {}) {
98
+ return this.#one({ op: "submodels", args: query }, options);
99
+ }
100
+ /** Engines of a model year, in short form. */
101
+ async engines(query, options = {}) {
102
+ return this.#one({ op: "engines", args: query }, options);
103
+ }
104
+ /**
105
+ * Catalog entries for free text such as `2019 civic si`, best first. The
106
+ * hosted API also finds words in any order with an index of its own;
107
+ * here, text the catalog cannot read as year, make, model and submodel
108
+ * finds nothing.
109
+ */
110
+ async search(query, options = {}) {
111
+ return this.#one({ op: "search", args: query }, options);
112
+ }
113
+ /** One catalog entry by its id, such as `2019_honda_civic_si`. */
114
+ async vehicle(id, options = {}) {
115
+ return this.#one({ op: "vehicle", args: { id: String(id) } }, options);
116
+ }
117
+ /**
118
+ * The data version, the vPIC release it came from, and the build time.
119
+ * `server_version` is the version of this package.
120
+ */
121
+ async meta(options = {}) {
122
+ return this.#one({ op: "meta", args: {} }, options);
123
+ }
124
+ /** Closes the store, if the store was opened for this client. */
125
+ close() {
126
+ this.#store.close?.();
127
+ }
128
+ async #one(question, options) {
129
+ const [outcome] = await this.#ask([question], options);
130
+ if (outcome === undefined) {
131
+ throw new WenmarOpenError({ code: "internal_error", message: "The decoder gave no answer." });
132
+ }
133
+ if ("error" in outcome)
134
+ throw thrown(outcome.error);
135
+ return outcome.ok;
136
+ }
137
+ /** The decoder, started and with the data opened. Started again after a stop. */
138
+ async #ready() {
139
+ const current = this.#engine;
140
+ if (current !== undefined) {
141
+ const engine = await current;
142
+ if (!engine.stopped)
143
+ return engine;
144
+ if (this.#engine === current)
145
+ this.#engine = undefined;
146
+ }
147
+ this.#engine ??= this.#start();
148
+ try {
149
+ return await this.#engine;
150
+ }
151
+ catch (error) {
152
+ this.#engine = undefined;
153
+ throw error;
154
+ }
155
+ }
156
+ async #start() {
157
+ this.#module ??= compile(this.#wasm);
158
+ let module;
159
+ try {
160
+ module = await this.#module;
161
+ }
162
+ catch (error) {
163
+ this.#module = undefined;
164
+ throw error;
165
+ }
166
+ const engine = await Engine.start(module);
167
+ const answers = [];
168
+ for (let step = 0; step < MAX_STEPS; step += 1) {
169
+ const answer = engine.call({ op: "open", answers });
170
+ if ("ok" in answer)
171
+ return engine;
172
+ if ("error" in answer)
173
+ throw thrown(answer.error);
174
+ const rows = await read(this.#store, answer.need);
175
+ answer.need.forEach((statement, index) => {
176
+ answers.push({ ...statement, rows: cleanRows(rows[index], statement.sql) });
177
+ });
178
+ }
179
+ throw new WenmarOpenError({ code: "internal_error", message: "Opening the data did not finish." });
180
+ }
181
+ /**
182
+ * Answers every question, going back to the store as often as the
183
+ * decoder asks. What several questions need in the same step is read
184
+ * together, each statement once.
185
+ */
186
+ async #ask(questions, options) {
187
+ const signal = options.signal;
188
+ const timeoutMs = options.timeoutMs ?? 0;
189
+ const started = Date.now();
190
+ const currentYear = this.#currentYear ?? new Date().getUTCFullYear();
191
+ const answers = questions.map(() => []);
192
+ const outcomes = questions.map(() => undefined);
193
+ for (let step = 0;; step += 1) {
194
+ if (signal?.aborted) {
195
+ throw new WenmarOpenError({ code: "aborted", message: "The request was aborted.", cause: signal.reason });
196
+ }
197
+ if (timeoutMs > 0 && Date.now() - started > timeoutMs) {
198
+ throw new WenmarOpenError({
199
+ code: "timeout",
200
+ message: `No answer within ${timeoutMs} ms.`,
201
+ details: { timeout_ms: timeoutMs },
202
+ });
203
+ }
204
+ const engine = await this.#ready();
205
+ const wanted = new Map();
206
+ const waiting = [];
207
+ questions.forEach((question, index) => {
208
+ if (outcomes[index] !== undefined)
209
+ return;
210
+ const answer = engine.call({
211
+ op: question.op,
212
+ args: question.args,
213
+ current_year: currentYear,
214
+ answers: answers[index] ?? [],
215
+ });
216
+ if ("need" in answer) {
217
+ waiting.push({ index, need: answer.need });
218
+ for (const statement of answer.need)
219
+ wanted.set(keyOf(statement), statement);
220
+ }
221
+ else {
222
+ outcomes[index] = answer;
223
+ }
224
+ });
225
+ if (waiting.length === 0)
226
+ return outcomes;
227
+ if (step >= MAX_STEPS) {
228
+ throw new WenmarOpenError({ code: "internal_error", message: "The decoder kept asking for more data." });
229
+ }
230
+ const statements = [...wanted.values()];
231
+ const rows = await read(this.#store, statements);
232
+ const found = new Map();
233
+ statements.forEach((statement, index) => {
234
+ found.set(keyOf(statement), { ...statement, rows: cleanRows(rows[index], statement.sql) });
235
+ });
236
+ for (const { index, need } of waiting) {
237
+ for (const statement of need) {
238
+ const answered = found.get(keyOf(statement));
239
+ if (answered !== undefined)
240
+ answers[index]?.push(answered);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
@@ -0,0 +1,53 @@
1
+ import type { Statement } from "./store.js";
2
+ /**
3
+ * A compiled `WebAssembly.Module`. It is written as `object` so that these
4
+ * declarations name nothing a project without the DOM library lacks.
5
+ */
6
+ export type CompiledWasm = object;
7
+ /**
8
+ * The decoder: a compiled module, or its bytes. Cloudflare Workers do not
9
+ * compile bytes, so there it is the module `import wasm from
10
+ * "wenmar-open/offline.wasm"` gives.
11
+ */
12
+ export type WasmSource = CompiledWasm | ArrayBuffer | ArrayBufferView;
13
+ /** A statement with the rows a store gave for it. */
14
+ export interface Answered extends Statement {
15
+ readonly rows: readonly (readonly (string | number | null)[])[];
16
+ }
17
+ export interface EngineRequest {
18
+ op: string;
19
+ args?: object;
20
+ current_year?: number;
21
+ answers?: readonly Answered[];
22
+ }
23
+ export type EngineAnswer = {
24
+ need: Statement[];
25
+ } | {
26
+ ok: unknown;
27
+ } | {
28
+ error: {
29
+ code: string;
30
+ message: string;
31
+ details: Record<string, unknown>;
32
+ };
33
+ };
34
+ /**
35
+ * Compiles the decoder. With no source it is the copy inside the package,
36
+ * which is read as text and never fetched, so nothing here needs a file
37
+ * system or a network.
38
+ */
39
+ export declare function compile(source?: WasmSource): Promise<CompiledWasm>;
40
+ /** One running copy of the decoder. */
41
+ export declare class Engine {
42
+ #private;
43
+ private constructor();
44
+ static start(module: CompiledWasm): Promise<Engine>;
45
+ /** Whether a call stopped this copy. A stopped copy is never used again. */
46
+ get stopped(): boolean;
47
+ /**
48
+ * One request, one answer. A request that cannot be written as JSON is
49
+ * refused before the decoder sees it. A stop inside the decoder is thrown
50
+ * as an error, and this copy is not used again.
51
+ */
52
+ call(request: EngineRequest): EngineAnswer;
53
+ }
@@ -0,0 +1,129 @@
1
+ import { WenmarOpenError } from "../errors.js";
2
+ const EXPORTS = ["memory", "wo_alloc", "wo_call", "wo_result", "wo_result_len"];
3
+ function bytesOf(base64) {
4
+ const binary = atob(base64);
5
+ const bytes = new Uint8Array(binary.length);
6
+ for (let index = 0; index < binary.length; index += 1)
7
+ bytes[index] = binary.charCodeAt(index);
8
+ return bytes;
9
+ }
10
+ /**
11
+ * Compiles the decoder. With no source it is the copy inside the package,
12
+ * which is read as text and never fetched, so nothing here needs a file
13
+ * system or a network.
14
+ */
15
+ export async function compile(source) {
16
+ if (source instanceof WebAssembly.Module)
17
+ return source;
18
+ try {
19
+ const bytes = source ?? bytesOf((await import("./wasm-inline.js")).WASM_BASE64);
20
+ return await WebAssembly.compile(bytes);
21
+ }
22
+ catch (cause) {
23
+ throw new WenmarOpenError({
24
+ code: "internal_error",
25
+ message: "The decoder could not be loaded. Where bytes may not be compiled, as in Cloudflare Workers, pass { wasm } from: import wasm from \"wenmar-open/offline.wasm\".",
26
+ cause,
27
+ });
28
+ }
29
+ }
30
+ /** One running copy of the decoder. */
31
+ export class Engine {
32
+ #exports;
33
+ #stopped = false;
34
+ constructor(exports) {
35
+ this.#exports = exports;
36
+ }
37
+ static async start(module) {
38
+ let instance;
39
+ try {
40
+ instance = await WebAssembly.instantiate(module, {});
41
+ }
42
+ catch (cause) {
43
+ throw new WenmarOpenError({
44
+ code: "internal_error",
45
+ message: "The decoder could not be started: the wasm given is not the one of this package.",
46
+ cause,
47
+ });
48
+ }
49
+ const exports = instance.exports;
50
+ for (const name of EXPORTS) {
51
+ if (exports[name] === undefined) {
52
+ throw new WenmarOpenError({
53
+ code: "internal_error",
54
+ message: "The decoder could not be started: the wasm given is not the one of this package.",
55
+ details: { missing: name },
56
+ });
57
+ }
58
+ }
59
+ return new Engine(exports);
60
+ }
61
+ /** Whether a call stopped this copy. A stopped copy is never used again. */
62
+ get stopped() {
63
+ return this.#stopped;
64
+ }
65
+ #read() {
66
+ const { memory, wo_result, wo_result_len } = this.#exports;
67
+ // The memory may have grown during the call, so it is looked at afresh.
68
+ return new TextDecoder().decode(new Uint8Array(memory.buffer, wo_result(), wo_result_len()));
69
+ }
70
+ /**
71
+ * One request, one answer. A request that cannot be written as JSON is
72
+ * refused before the decoder sees it. A stop inside the decoder is thrown
73
+ * as an error, and this copy is not used again.
74
+ */
75
+ call(request) {
76
+ if (this.#stopped) {
77
+ throw new WenmarOpenError({ code: "internal_error", message: "The decoder has stopped." });
78
+ }
79
+ let input;
80
+ try {
81
+ input = new TextEncoder().encode(JSON.stringify(request));
82
+ }
83
+ catch (cause) {
84
+ throw new WenmarOpenError({
85
+ code: "validation_failed",
86
+ message: "The question could not be written as JSON.",
87
+ cause,
88
+ });
89
+ }
90
+ const { memory, wo_alloc, wo_call } = this.#exports;
91
+ try {
92
+ const pointer = wo_alloc(input.length);
93
+ new Uint8Array(memory.buffer, pointer, input.length).set(input);
94
+ wo_call(pointer, input.length);
95
+ }
96
+ catch (cause) {
97
+ // WebAssembly cannot unwind, so a failure inside it ends the call
98
+ // here. What it was is left where the answer would have been.
99
+ this.#stopped = true;
100
+ let panic;
101
+ try {
102
+ panic = this.#read();
103
+ }
104
+ catch {
105
+ panic = undefined;
106
+ }
107
+ // An answer is always a JSON object and a panic's message never is:
108
+ // text that starts as an object is an answer left from before.
109
+ if (panic === "" || panic?.startsWith("{"))
110
+ panic = undefined;
111
+ throw new WenmarOpenError({
112
+ code: "internal_error",
113
+ message: "The decoder stopped unexpectedly. Please report the VIN or the question that caused it.",
114
+ details: panic === undefined ? {} : { panic },
115
+ cause,
116
+ });
117
+ }
118
+ try {
119
+ return JSON.parse(this.#read());
120
+ }
121
+ catch (cause) {
122
+ throw new WenmarOpenError({
123
+ code: "internal_error",
124
+ message: "The decoder gave an answer that could not be read.",
125
+ cause,
126
+ });
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,10 @@
1
+ export { MAX_BATCH, isBatchError } from "../batch.js";
2
+ export { WenmarOpenError } from "../errors.js";
3
+ export type { ApiErrorCode, ClientErrorCode, ErrorCode, OfflineErrorCode, WenmarOpenErrorInit, } from "../errors.js";
4
+ export type { DecodeOptions, RequestOptions } from "../client.js";
5
+ export type { BatchItem, CheckDigit, Engine, EngineOption, EnginesQuery, Entry, ErrorBody, Make, MakesQuery, Manufacturer, Meta, Model, ModelsQuery, Plant, Safety, SearchQuery, Selection, Submodel, SubmodelsQuery, VinDecode, Warning, YearsQuery, } from "../types.js";
6
+ export { MAX_STEPS, WenmarOpenOffline } from "./client.js";
7
+ export type { OfflineOptions } from "./client.js";
8
+ export type { CompiledWasm, WasmSource } from "./engine.js";
9
+ export { d1Store, syncStore } from "./store.js";
10
+ export type { D1Like, D1Statement, Param, Statement, Store, SyncDatabase, SyncStatement } from "./store.js";
@@ -0,0 +1,4 @@
1
+ export { MAX_BATCH, isBatchError } from "../batch.js";
2
+ export { WenmarOpenError } from "../errors.js";
3
+ export { MAX_STEPS, WenmarOpenOffline } from "./client.js";
4
+ export { d1Store, syncStore } from "./store.js";
@@ -0,0 +1,20 @@
1
+ import { WenmarOpenOffline } from "./client.js";
2
+ import type { WasmSource } from "./engine.js";
3
+ export interface NodeOptions {
4
+ /**
5
+ * The data file. Default: the one in the `wenmar-open-data` package,
6
+ * which must then be installed.
7
+ */
8
+ path?: string;
9
+ wasm?: WasmSource;
10
+ currentYear?: number;
11
+ }
12
+ /**
13
+ * Opens a data file with Node's own SQLite (Node 22.16 or later) and
14
+ * returns a client that reads it. The file is opened read-only and checked:
15
+ * a file of another schema version fails here, not at the first decode.
16
+ *
17
+ * On Node 20, open the file with `better-sqlite3` and give it to
18
+ * `syncStore` instead.
19
+ */
20
+ export declare function openOffline(options?: NodeOptions): Promise<WenmarOpenOffline>;