@printhive/api 0.0.0-stage → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,237 @@
1
+ import {
2
+ PrintHiveApiError,
3
+ PrintHiveNetworkError,
4
+ apiError,
5
+ apiKey,
6
+ model,
7
+ page,
8
+ source
9
+ } from "./chunk-TGPYH7CV.js";
10
+
11
+ // src/transport.ts
12
+ var MAX_RETRY_AFTER_MS = 6e4;
13
+ function positive(value, name) {
14
+ if (!Number.isFinite(value) || value <= 0)
15
+ throw new RangeError(`${name} must be positive`);
16
+ return value;
17
+ }
18
+ async function pause(ms, signal) {
19
+ if (signal?.aborted) throw new PrintHiveNetworkError("aborted");
20
+ await new Promise((resolve, reject) => {
21
+ const abort = () => {
22
+ clearTimeout(timer);
23
+ reject(new PrintHiveNetworkError("aborted"));
24
+ };
25
+ const timer = setTimeout(() => {
26
+ signal?.removeEventListener("abort", abort);
27
+ resolve();
28
+ }, ms);
29
+ signal?.addEventListener("abort", abort, { once: true });
30
+ });
31
+ }
32
+ var Transport = class {
33
+ options;
34
+ constructor(options) {
35
+ if (!apiKey(options.apiKey))
36
+ throw new TypeError("apiKey must be a non-empty valid HTTP header value");
37
+ const baseUrl = new URL(options.baseUrl ?? "https://api.printhiv3d.com");
38
+ if (!["http:", "https:"].includes(baseUrl.protocol) || baseUrl.username || baseUrl.password) {
39
+ throw new TypeError("baseUrl must be an HTTP URL without credentials");
40
+ }
41
+ const maxRetries = options.maxRetries ?? 2;
42
+ if (!Number.isInteger(maxRetries) || maxRetries < 0)
43
+ throw new RangeError("maxRetries must be a nonnegative integer");
44
+ this.options = {
45
+ ...options,
46
+ baseUrl: baseUrl.href.replace(/\/$/, ""),
47
+ fetch: options.fetch ?? globalThis.fetch,
48
+ timeoutMs: positive(options.timeoutMs ?? 3e4, "timeoutMs"),
49
+ maxRetries
50
+ };
51
+ }
52
+ async request(request) {
53
+ const url = new URL(this.options.baseUrl + request.path);
54
+ url.search = request.query?.toString() ?? "";
55
+ const replayable = request.method === "GET" || request.replayable;
56
+ for (let attempt = 0; ; attempt += 1) {
57
+ if (request.signal?.aborted) throw new PrintHiveNetworkError("aborted");
58
+ const controller = new AbortController();
59
+ let timedOut = false;
60
+ const timer = setTimeout(() => {
61
+ timedOut = true;
62
+ controller.abort();
63
+ }, this.options.timeoutMs);
64
+ const abort = () => controller.abort();
65
+ request.signal?.addEventListener("abort", abort, { once: true });
66
+ let failure;
67
+ try {
68
+ const headers = new Headers({
69
+ "x-api-key": this.options.apiKey,
70
+ accept: "application/json"
71
+ });
72
+ const init = {
73
+ method: request.method,
74
+ headers,
75
+ signal: controller.signal,
76
+ redirect: "error"
77
+ };
78
+ if (request.method === "POST") {
79
+ headers.set("content-type", "model/3mf");
80
+ headers.set("content-length", String(request.size));
81
+ headers.set("idempotency-key", request.idempotencyKey);
82
+ init.body = await request.body();
83
+ init.duplex = "half";
84
+ }
85
+ const response = await this.options.fetch(url, init);
86
+ if (!response.ok) throw await apiError(response);
87
+ return await request.decode(response);
88
+ } catch (error) {
89
+ if (error instanceof TypeError && error.message === "Invalid Print Hive API response")
90
+ throw error;
91
+ if (request.signal?.aborted) throw new PrintHiveNetworkError("aborted");
92
+ if (timedOut) throw new PrintHiveNetworkError("timeout");
93
+ failure = error instanceof PrintHiveApiError ? error : new PrintHiveNetworkError("network");
94
+ } finally {
95
+ clearTimeout(timer);
96
+ request.signal?.removeEventListener("abort", abort);
97
+ }
98
+ const delay = retryDelay(
99
+ failure,
100
+ replayable,
101
+ attempt,
102
+ this.options.maxRetries
103
+ );
104
+ await pause(delay, request.signal);
105
+ }
106
+ }
107
+ };
108
+ function retryDelay(failure, replayable, attempt, maxRetries) {
109
+ const retryable = failure instanceof PrintHiveApiError ? failure.status === 429 || failure.status === 503 : failure.reason === "network";
110
+ if (!retryable || !replayable || attempt >= maxRetries) throw failure;
111
+ const retryAfterMs = failure instanceof PrintHiveApiError ? failure.retryAfterMs ?? 0 : 0;
112
+ if (retryAfterMs > MAX_RETRY_AFTER_MS) throw failure;
113
+ const backoff = Math.min(3e4, 250 * 2 ** attempt) * (0.5 + Math.random());
114
+ return Math.max(backoff, retryAfterMs);
115
+ }
116
+
117
+ // src/index.ts
118
+ async function decode(response, validate) {
119
+ const value = await response.json().catch((error) => {
120
+ if (error instanceof SyntaxError)
121
+ throw new TypeError("Invalid Print Hive API response");
122
+ throw error;
123
+ });
124
+ if (!validate(value)) throw new TypeError("Invalid Print Hive API response");
125
+ return value;
126
+ }
127
+ function isPath(file) {
128
+ return !(file instanceof Uint8Array) && !(file instanceof ReadableStream);
129
+ }
130
+ var prefix = "/v1/partner-production";
131
+ var PrintHive = class {
132
+ partnerSources;
133
+ models;
134
+ constructor(options) {
135
+ const transport = new Transport(options);
136
+ const get = (sourceId, signal) => transport.request({
137
+ decode: (response) => decode(response, source),
138
+ method: "GET",
139
+ path: `${prefix}/uploads/${encodeURIComponent(sourceId)}`,
140
+ signal
141
+ });
142
+ this.partnerSources = {
143
+ get,
144
+ async upload(input) {
145
+ const idempotencyKey = input.idempotencyKey ?? crypto.randomUUID();
146
+ const query = new URLSearchParams({ filename: input.filename });
147
+ if (input.externalId !== void 0)
148
+ query.set("externalId", input.externalId);
149
+ if (input.modelId !== void 0) query.set("modelId", input.modelId);
150
+ const file = input.file;
151
+ let body;
152
+ let size;
153
+ let replayable;
154
+ if (isPath(file)) {
155
+ const { readFile } = await import("fs/promises");
156
+ const bytes = new Uint8Array(await readFile(file));
157
+ size = bytes.byteLength;
158
+ body = async () => new Blob([bytes]);
159
+ replayable = true;
160
+ } else if (file instanceof Uint8Array) {
161
+ const bytes = new Uint8Array(file);
162
+ size = bytes.byteLength;
163
+ body = async () => new Blob([bytes]);
164
+ replayable = true;
165
+ } else {
166
+ if (input.size === void 0)
167
+ throw new TypeError("Stream uploads require size");
168
+ size = input.size;
169
+ body = async () => file;
170
+ replayable = false;
171
+ }
172
+ if (!Number.isSafeInteger(size) || size < 0)
173
+ throw new RangeError("size must be a nonnegative safe integer");
174
+ const source2 = await transport.request({
175
+ decode: (response) => decode(response, source),
176
+ method: "POST",
177
+ path: `${prefix}/uploads`,
178
+ query,
179
+ body,
180
+ size,
181
+ idempotencyKey,
182
+ replayable
183
+ });
184
+ return { ...source2, idempotencyKey };
185
+ },
186
+ async waitUntilSettled(sourceId, options2 = {}) {
187
+ const interval = positive(options2.intervalMs ?? 1e3, "intervalMs");
188
+ const deadline = AbortSignal.timeout(
189
+ positive(options2.timeoutMs ?? 12e4, "timeoutMs")
190
+ );
191
+ const signal = options2.signal ? AbortSignal.any([options2.signal, deadline]) : deadline;
192
+ try {
193
+ for (; ; ) {
194
+ const source2 = await get(sourceId, signal);
195
+ if (source2.status === "ready" || source2.status === "failed")
196
+ return source2;
197
+ await pause(interval, signal);
198
+ }
199
+ } catch (error) {
200
+ if (deadline.aborted && !options2.signal?.aborted) {
201
+ const { PrintHiveNetworkError: PrintHiveNetworkError2 } = await import("./errors-LHFABGGG.js");
202
+ throw new PrintHiveNetworkError2("timeout");
203
+ }
204
+ throw error;
205
+ }
206
+ }
207
+ };
208
+ this.models = {
209
+ findByExternalId: (externalId) => transport.request({
210
+ decode: (response) => decode(response, model),
211
+ method: "GET",
212
+ path: `${prefix}/models`,
213
+ query: new URLSearchParams({ externalId })
214
+ }),
215
+ async *listSourceFiles(modelId) {
216
+ let cursor = null;
217
+ do {
218
+ const query = new URLSearchParams();
219
+ if (cursor !== null) query.set("cursor", cursor);
220
+ const page2 = await transport.request({
221
+ decode: (response) => decode(response, page),
222
+ method: "GET",
223
+ path: `${prefix}/models/${encodeURIComponent(modelId)}/files`,
224
+ query
225
+ });
226
+ yield* page2.sources;
227
+ cursor = page2.nextCursor;
228
+ } while (cursor !== null);
229
+ }
230
+ };
231
+ }
232
+ };
233
+ export {
234
+ PrintHive,
235
+ PrintHiveApiError,
236
+ PrintHiveNetworkError
237
+ };
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@printhive/api",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Print Hive partner-production REST API client",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "CHANGELOG.md",
26
+ "LICENSE"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "scripts": {
35
+ "build": "bun run check:drift && tsup src/index.ts --format cjs,esm --dts --clean",
36
+ "check-types": "bun run build && tsc --noEmit",
37
+ "lint": "eslint src test --ext .ts",
38
+ "generate": "node scripts/contracts.mjs --write",
39
+ "check:drift": "node scripts/contracts.mjs",
40
+ "test": "bun run build && vitest run"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^24.10.1",
44
+ "openapi-typescript": "^7.13.0",
45
+ "tsup": "^8.5.0",
46
+ "typescript": "^5.9.3",
47
+ "vitest": "^4.1.0",
48
+ "@hiv3d/eslint-config": "*",
49
+ "eslint": "^9.39.1",
50
+ "ajv": "^8.17.1",
51
+ "prettier": "^3.8.5"
52
+ },
53
+ "license": "MIT"
54
+ }