@printhive/api 0.0.0-stage → 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.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/dist/chunk-7UULMDS5.js +13947 -0
- package/dist/errors-TJFN2M7S.js +10 -0
- package/dist/index.cjs +14356 -0
- package/dist/index.d.cts +2728 -0
- package/dist/index.d.ts +2728 -0
- package/dist/index.js +368 -0
- package/package.json +57 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PrintHiveApiError,
|
|
3
|
+
PrintHiveNetworkError,
|
|
4
|
+
apiError,
|
|
5
|
+
apiKey,
|
|
6
|
+
createRequest,
|
|
7
|
+
model,
|
|
8
|
+
page,
|
|
9
|
+
printJob,
|
|
10
|
+
printer,
|
|
11
|
+
printers,
|
|
12
|
+
source
|
|
13
|
+
} from "./chunk-7UULMDS5.js";
|
|
14
|
+
|
|
15
|
+
// src/transport.ts
|
|
16
|
+
var MAX_RETRY_AFTER_MS = 6e4;
|
|
17
|
+
function positive(value, name) {
|
|
18
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
19
|
+
throw new RangeError(`${name} must be positive`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
async function pause(ms, signal) {
|
|
23
|
+
if (signal?.aborted) throw new PrintHiveNetworkError("aborted");
|
|
24
|
+
await new Promise((resolve, reject) => {
|
|
25
|
+
const abort = () => {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
reject(new PrintHiveNetworkError("aborted"));
|
|
28
|
+
};
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
signal?.removeEventListener("abort", abort);
|
|
31
|
+
resolve();
|
|
32
|
+
}, ms);
|
|
33
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function postHeaders(request) {
|
|
37
|
+
const headers = new Headers({ "idempotency-key": request.idempotencyKey });
|
|
38
|
+
if (request.contentType !== null)
|
|
39
|
+
headers.set("content-type", request.contentType ?? "model/3mf");
|
|
40
|
+
if (request.size !== void 0)
|
|
41
|
+
headers.set("content-length", String(request.size));
|
|
42
|
+
return headers;
|
|
43
|
+
}
|
|
44
|
+
var Transport = class {
|
|
45
|
+
options;
|
|
46
|
+
constructor(options) {
|
|
47
|
+
if (!apiKey(options.apiKey))
|
|
48
|
+
throw new TypeError("apiKey must be a non-empty valid HTTP header value");
|
|
49
|
+
const baseUrl = new URL(options.baseUrl ?? "https://api.printhiv3d.com");
|
|
50
|
+
if (!["http:", "https:"].includes(baseUrl.protocol) || baseUrl.username || baseUrl.password) {
|
|
51
|
+
throw new TypeError("baseUrl must be an HTTP URL without credentials");
|
|
52
|
+
}
|
|
53
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
54
|
+
if (!Number.isInteger(maxRetries) || maxRetries < 0)
|
|
55
|
+
throw new RangeError("maxRetries must be a nonnegative integer");
|
|
56
|
+
this.options = {
|
|
57
|
+
...options,
|
|
58
|
+
baseUrl: baseUrl.href.replace(/\/$/, ""),
|
|
59
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
60
|
+
timeoutMs: positive(options.timeoutMs ?? 3e4, "timeoutMs"),
|
|
61
|
+
maxRetries
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async request(request) {
|
|
65
|
+
const url = new URL(this.options.baseUrl + request.path);
|
|
66
|
+
url.search = request.query?.toString() ?? "";
|
|
67
|
+
const replayable = request.method === "GET" || request.replayable;
|
|
68
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
69
|
+
if (request.signal?.aborted) throw new PrintHiveNetworkError("aborted");
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
let timedOut = false;
|
|
72
|
+
const timer = setTimeout(() => {
|
|
73
|
+
timedOut = true;
|
|
74
|
+
controller.abort();
|
|
75
|
+
}, this.options.timeoutMs);
|
|
76
|
+
const abort = () => controller.abort();
|
|
77
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
78
|
+
let failure;
|
|
79
|
+
try {
|
|
80
|
+
const headers = new Headers({
|
|
81
|
+
"x-api-key": this.options.apiKey,
|
|
82
|
+
accept: "application/json"
|
|
83
|
+
});
|
|
84
|
+
const init = {
|
|
85
|
+
method: request.method,
|
|
86
|
+
headers,
|
|
87
|
+
signal: controller.signal,
|
|
88
|
+
redirect: "error"
|
|
89
|
+
};
|
|
90
|
+
if (request.method === "POST") {
|
|
91
|
+
for (const [name, value] of postHeaders(request))
|
|
92
|
+
headers.set(name, value);
|
|
93
|
+
init.body = await request.body();
|
|
94
|
+
init.duplex = "half";
|
|
95
|
+
}
|
|
96
|
+
const response = await this.options.fetch(url, init);
|
|
97
|
+
if (!response.ok) throw await apiError(response);
|
|
98
|
+
return await request.decode(response);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error instanceof TypeError && error.message === "Invalid Print Hive API response")
|
|
101
|
+
throw error;
|
|
102
|
+
if (request.signal?.aborted) throw new PrintHiveNetworkError("aborted");
|
|
103
|
+
if (timedOut) throw new PrintHiveNetworkError("timeout");
|
|
104
|
+
failure = error instanceof PrintHiveApiError ? error : new PrintHiveNetworkError("network");
|
|
105
|
+
} finally {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
request.signal?.removeEventListener("abort", abort);
|
|
108
|
+
}
|
|
109
|
+
const delay = retryDelay(
|
|
110
|
+
failure,
|
|
111
|
+
replayable,
|
|
112
|
+
attempt,
|
|
113
|
+
this.options.maxRetries
|
|
114
|
+
);
|
|
115
|
+
await pause(delay, request.signal);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
function retryDelay(failure, replayable, attempt, maxRetries) {
|
|
120
|
+
const retryable = failure instanceof PrintHiveApiError ? failure.status === 429 || failure.status === 503 : failure.reason === "network";
|
|
121
|
+
if (!retryable || !replayable || attempt >= maxRetries) throw failure;
|
|
122
|
+
const retryAfterMs = failure instanceof PrintHiveApiError ? failure.retryAfterMs ?? 0 : 0;
|
|
123
|
+
if (retryAfterMs > MAX_RETRY_AFTER_MS) throw failure;
|
|
124
|
+
const backoff = Math.min(3e4, 250 * 2 ** attempt) * (0.5 + Math.random());
|
|
125
|
+
return Math.max(backoff, retryAfterMs);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/print-jobs.ts
|
|
129
|
+
var PrintJobCreateStoppedError = class extends Error {
|
|
130
|
+
constructor(request) {
|
|
131
|
+
super(`Print job creation ${request.status}`);
|
|
132
|
+
this.request = request;
|
|
133
|
+
this.name = "PrintJobCreateStoppedError";
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
async function decode(response, validate) {
|
|
137
|
+
const value = await response.json().catch((error) => {
|
|
138
|
+
if (error instanceof SyntaxError)
|
|
139
|
+
throw new TypeError("Invalid Print Hive API response");
|
|
140
|
+
throw error;
|
|
141
|
+
});
|
|
142
|
+
if (!validate(value)) throw new TypeError("Invalid Print Hive API response");
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
var PrintJobs = class {
|
|
146
|
+
constructor(transport) {
|
|
147
|
+
this.transport = transport;
|
|
148
|
+
}
|
|
149
|
+
async create(input) {
|
|
150
|
+
const { idempotencyKey: suppliedKey, ...bodyInput } = input;
|
|
151
|
+
const idempotencyKey = suppliedKey ?? crypto.randomUUID();
|
|
152
|
+
if ("file" in bodyInput) {
|
|
153
|
+
const { file, filename, ...metadata } = bodyInput;
|
|
154
|
+
const bytes = file instanceof Uint8Array ? new Uint8Array(file) : new Uint8Array(
|
|
155
|
+
await (await import("fs/promises")).readFile(file)
|
|
156
|
+
);
|
|
157
|
+
const response2 = await this.transport.request({
|
|
158
|
+
method: "POST",
|
|
159
|
+
path: "/v1/print-jobs/create",
|
|
160
|
+
idempotencyKey,
|
|
161
|
+
replayable: true,
|
|
162
|
+
contentType: null,
|
|
163
|
+
body: async () => {
|
|
164
|
+
const form = new FormData();
|
|
165
|
+
for (const [field, value] of Object.entries({
|
|
166
|
+
name: metadata.name,
|
|
167
|
+
printer_id: metadata.printer_id,
|
|
168
|
+
on_filament_mismatch: metadata.on_filament_mismatch,
|
|
169
|
+
externalId: metadata.externalId,
|
|
170
|
+
release: metadata.release
|
|
171
|
+
})) {
|
|
172
|
+
if (value !== void 0) form.set(field, value);
|
|
173
|
+
}
|
|
174
|
+
form.set("plate", String(metadata.plate));
|
|
175
|
+
form.set("filaments", JSON.stringify(metadata.filaments));
|
|
176
|
+
if (metadata.print_options !== void 0)
|
|
177
|
+
form.set("print_options", JSON.stringify(metadata.print_options));
|
|
178
|
+
form.set("file", new Blob([bytes]), filename);
|
|
179
|
+
return form;
|
|
180
|
+
},
|
|
181
|
+
decode: (response3) => decode(response3, createRequest)
|
|
182
|
+
});
|
|
183
|
+
return { ...response2, idempotencyKey };
|
|
184
|
+
}
|
|
185
|
+
const response = await this.transport.request({
|
|
186
|
+
method: "POST",
|
|
187
|
+
path: "/v1/print-jobs/create",
|
|
188
|
+
idempotencyKey,
|
|
189
|
+
replayable: true,
|
|
190
|
+
contentType: "application/json",
|
|
191
|
+
body: async () => JSON.stringify(bodyInput),
|
|
192
|
+
decode: (response2) => decode(response2, printJob)
|
|
193
|
+
});
|
|
194
|
+
return { ...response, idempotencyKey };
|
|
195
|
+
}
|
|
196
|
+
getCreateRequest(requestId, signal) {
|
|
197
|
+
return this.transport.request({
|
|
198
|
+
method: "GET",
|
|
199
|
+
path: `/v1/print-jobs/create-requests/${encodeURIComponent(requestId)}`,
|
|
200
|
+
signal,
|
|
201
|
+
decode: (response) => decode(response, createRequest)
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async waitUntilCreated(requestId, options = {}) {
|
|
205
|
+
const interval = positive(options.intervalMs ?? 1e3, "intervalMs");
|
|
206
|
+
const deadline = AbortSignal.timeout(
|
|
207
|
+
positive(options.timeoutMs ?? 12e4, "timeoutMs")
|
|
208
|
+
);
|
|
209
|
+
const signal = options.signal ? AbortSignal.any([options.signal, deadline]) : deadline;
|
|
210
|
+
try {
|
|
211
|
+
for (; ; ) {
|
|
212
|
+
const request = await this.getCreateRequest(requestId, signal);
|
|
213
|
+
if (request.status === "created") return request;
|
|
214
|
+
if (request.status !== "preparing")
|
|
215
|
+
throw new PrintJobCreateStoppedError(request);
|
|
216
|
+
await pause(interval, signal);
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (deadline.aborted && !options.signal?.aborted)
|
|
220
|
+
throw new PrintHiveNetworkError("timeout");
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// src/index.ts
|
|
227
|
+
async function decode2(response, validate) {
|
|
228
|
+
const value = await response.json().catch((error) => {
|
|
229
|
+
if (error instanceof SyntaxError)
|
|
230
|
+
throw new TypeError("Invalid Print Hive API response");
|
|
231
|
+
throw error;
|
|
232
|
+
});
|
|
233
|
+
if (!validate(value)) throw new TypeError("Invalid Print Hive API response");
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
function isPath(file) {
|
|
237
|
+
return !(file instanceof Uint8Array) && !(file instanceof ReadableStream);
|
|
238
|
+
}
|
|
239
|
+
var prefix = "/v1/partner-production";
|
|
240
|
+
var PrintHive = class {
|
|
241
|
+
partnerSources;
|
|
242
|
+
models;
|
|
243
|
+
printers;
|
|
244
|
+
printJobs;
|
|
245
|
+
constructor(options) {
|
|
246
|
+
const transport = new Transport(options);
|
|
247
|
+
this.printJobs = new PrintJobs(transport);
|
|
248
|
+
const get = (sourceId, signal) => transport.request({
|
|
249
|
+
decode: (response) => decode2(response, source),
|
|
250
|
+
method: "GET",
|
|
251
|
+
path: `${prefix}/uploads/${encodeURIComponent(sourceId)}`,
|
|
252
|
+
signal
|
|
253
|
+
});
|
|
254
|
+
this.printers = {
|
|
255
|
+
list: (input = {}) => {
|
|
256
|
+
const query = new URLSearchParams();
|
|
257
|
+
if (input.limit !== void 0) query.set("limit", String(input.limit));
|
|
258
|
+
if (input.cursor !== void 0) query.set("cursor", input.cursor);
|
|
259
|
+
return transport.request({
|
|
260
|
+
decode: (response) => decode2(response, printers),
|
|
261
|
+
method: "GET",
|
|
262
|
+
path: "/v1/printers",
|
|
263
|
+
query
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
get: (id) => transport.request({
|
|
267
|
+
decode: (response) => decode2(response, printer),
|
|
268
|
+
method: "GET",
|
|
269
|
+
path: `/v1/printers/${encodeURIComponent(id)}`
|
|
270
|
+
})
|
|
271
|
+
};
|
|
272
|
+
this.partnerSources = {
|
|
273
|
+
get,
|
|
274
|
+
async upload(input) {
|
|
275
|
+
const idempotencyKey = input.idempotencyKey ?? crypto.randomUUID();
|
|
276
|
+
const query = new URLSearchParams({ filename: input.filename });
|
|
277
|
+
if (input.externalId !== void 0)
|
|
278
|
+
query.set("externalId", input.externalId);
|
|
279
|
+
if (input.modelId !== void 0) query.set("modelId", input.modelId);
|
|
280
|
+
const file = input.file;
|
|
281
|
+
let body;
|
|
282
|
+
let size;
|
|
283
|
+
let replayable;
|
|
284
|
+
if (isPath(file)) {
|
|
285
|
+
const { readFile } = await import("fs/promises");
|
|
286
|
+
const bytes = new Uint8Array(await readFile(file));
|
|
287
|
+
size = bytes.byteLength;
|
|
288
|
+
body = async () => new Blob([bytes]);
|
|
289
|
+
replayable = true;
|
|
290
|
+
} else if (file instanceof Uint8Array) {
|
|
291
|
+
const bytes = new Uint8Array(file);
|
|
292
|
+
size = bytes.byteLength;
|
|
293
|
+
body = async () => new Blob([bytes]);
|
|
294
|
+
replayable = true;
|
|
295
|
+
} else {
|
|
296
|
+
if (input.size === void 0)
|
|
297
|
+
throw new TypeError("Stream uploads require size");
|
|
298
|
+
size = input.size;
|
|
299
|
+
body = async () => file;
|
|
300
|
+
replayable = false;
|
|
301
|
+
}
|
|
302
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
303
|
+
throw new RangeError("size must be a nonnegative safe integer");
|
|
304
|
+
const source2 = await transport.request({
|
|
305
|
+
decode: (response) => decode2(response, source),
|
|
306
|
+
method: "POST",
|
|
307
|
+
path: `${prefix}/uploads`,
|
|
308
|
+
query,
|
|
309
|
+
body,
|
|
310
|
+
size,
|
|
311
|
+
idempotencyKey,
|
|
312
|
+
replayable
|
|
313
|
+
});
|
|
314
|
+
return { ...source2, idempotencyKey };
|
|
315
|
+
},
|
|
316
|
+
async waitUntilSettled(sourceId, options2 = {}) {
|
|
317
|
+
const interval = positive(options2.intervalMs ?? 1e3, "intervalMs");
|
|
318
|
+
const deadline = AbortSignal.timeout(
|
|
319
|
+
positive(options2.timeoutMs ?? 12e4, "timeoutMs")
|
|
320
|
+
);
|
|
321
|
+
const signal = options2.signal ? AbortSignal.any([options2.signal, deadline]) : deadline;
|
|
322
|
+
try {
|
|
323
|
+
for (; ; ) {
|
|
324
|
+
const source2 = await get(sourceId, signal);
|
|
325
|
+
if (source2.status === "ready" || source2.status === "failed")
|
|
326
|
+
return source2;
|
|
327
|
+
await pause(interval, signal);
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (deadline.aborted && !options2.signal?.aborted) {
|
|
331
|
+
const { PrintHiveNetworkError: PrintHiveNetworkError2 } = await import("./errors-TJFN2M7S.js");
|
|
332
|
+
throw new PrintHiveNetworkError2("timeout");
|
|
333
|
+
}
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
this.models = {
|
|
339
|
+
findByExternalId: (externalId) => transport.request({
|
|
340
|
+
decode: (response) => decode2(response, model),
|
|
341
|
+
method: "GET",
|
|
342
|
+
path: `${prefix}/models`,
|
|
343
|
+
query: new URLSearchParams({ externalId })
|
|
344
|
+
}),
|
|
345
|
+
async *listSourceFiles(modelId) {
|
|
346
|
+
let cursor = null;
|
|
347
|
+
do {
|
|
348
|
+
const query = new URLSearchParams();
|
|
349
|
+
if (cursor !== null) query.set("cursor", cursor);
|
|
350
|
+
const page2 = await transport.request({
|
|
351
|
+
decode: (response) => decode2(response, page),
|
|
352
|
+
method: "GET",
|
|
353
|
+
path: `${prefix}/models/${encodeURIComponent(modelId)}/files`,
|
|
354
|
+
query
|
|
355
|
+
});
|
|
356
|
+
yield* page2.sources;
|
|
357
|
+
cursor = page2.nextCursor;
|
|
358
|
+
} while (cursor !== null);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
export {
|
|
364
|
+
PrintHive,
|
|
365
|
+
PrintHiveApiError,
|
|
366
|
+
PrintHiveNetworkError,
|
|
367
|
+
PrintJobCreateStoppedError
|
|
368
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@printhive/api",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Print Hive REST API client",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/PrintHive/hive.git",
|
|
10
|
+
"directory": "packages/api-client"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.cjs",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"require": {
|
|
22
|
+
"types": "./dist/index.d.cts",
|
|
23
|
+
"default": "./dist/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"CHANGELOG.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "bun run check:drift && tsup src/index.ts --format cjs,esm --dts --clean",
|
|
41
|
+
"check-types": "bun run build && tsc --noEmit",
|
|
42
|
+
"lint": "eslint src test --ext .ts",
|
|
43
|
+
"generate": "node scripts/contracts.mjs --write",
|
|
44
|
+
"check:drift": "node scripts/contracts.mjs",
|
|
45
|
+
"test": "bun run build && vitest run"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^24.10.1",
|
|
49
|
+
"openapi-typescript": "^7.13.0",
|
|
50
|
+
"tsup": "^8.5.0",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
52
|
+
"vitest": "^4.1.0",
|
|
53
|
+
"@hiv3d/eslint-config": "*",
|
|
54
|
+
"eslint": "^9.39.1",
|
|
55
|
+
"ajv": "^8.17.1",
|
|
56
|
+
"prettier": "^3.8.5"
|
|
57
|
+
},
|
|
58
|
+
"license": "MIT"
|
|
59
|
+
}
|