@technestinnovations/print-gateway-client 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 ADDED
@@ -0,0 +1,35 @@
1
+ # @technestinnovations/print-gateway-client
2
+
3
+ Web POS client for the Android Print Gateway local HTTP API.
4
+
5
+ ## Usage
6
+
7
+ ```ts
8
+ import { buildReceiptDocument, printWithFallback } from '@technestinnovations/print-gateway-client';
9
+
10
+ const document = buildReceiptDocument({
11
+ storeName: 'MITHO SERVE',
12
+ receiptNumber: '123',
13
+ timestamp: new Date().toISOString(),
14
+ items: [{ name: 'Chicken Momo', quantity: 2, unitPrice: 250, total: 500 }],
15
+ total: 1400,
16
+ paymentMethod: 'Cash',
17
+ });
18
+
19
+ const result = await printWithFallback(document, {
20
+ baseUrl: 'http://127.0.0.1:19643/api/v1',
21
+ role: 'receipt',
22
+ });
23
+ ```
24
+
25
+ ## Mixed content note
26
+
27
+ If your web POS is served over HTTPS, `fetch('http://127.0.0.1:19643')` may be blocked by the browser. Options:
28
+
29
+ 1. Serve POS over HTTP on the Android device during testing
30
+ 2. Configure WebView mixed content for localhost if using a hybrid shell
31
+ 3. Fall back to `window.print()` automatically when the gateway is unavailable
32
+
33
+ ## POS origin
34
+
35
+ Configure the POS website address in the Print Gateway app. Browser requests from that origin are allowed automatically.
@@ -0,0 +1,27 @@
1
+ import type { PrintDocument } from '../schema.js';
2
+ export type ReceiptLineItem = {
3
+ name: string;
4
+ quantity: number;
5
+ unitPrice: number;
6
+ total: number;
7
+ };
8
+ export type ReceiptData = {
9
+ storeName: string;
10
+ storeAddress?: string;
11
+ storePhone?: string;
12
+ receiptNumber: string;
13
+ orderId?: string;
14
+ tableName?: string;
15
+ timestamp: string;
16
+ items: ReceiptLineItem[];
17
+ subtotal?: number;
18
+ discount?: number;
19
+ tax?: number;
20
+ total: number;
21
+ paymentMethod?: string;
22
+ customerName?: string;
23
+ footer?: string;
24
+ qrUrl?: string;
25
+ paperWidthMm?: number;
26
+ };
27
+ export declare function buildReceiptDocument(data: ReceiptData): PrintDocument;
@@ -0,0 +1,97 @@
1
+ export function buildReceiptDocument(data) {
2
+ const blocks = [
3
+ { type: 'text', value: data.storeName, align: 'center', style: { bold: true, width: 2, height: 2 } },
4
+ ];
5
+ if (data.storeAddress) {
6
+ blocks.push({ type: 'text', value: data.storeAddress, align: 'center' });
7
+ }
8
+ if (data.storePhone) {
9
+ blocks.push({ type: 'text', value: data.storePhone, align: 'center' });
10
+ }
11
+ const header = [
12
+ `Bill #${data.receiptNumber}`,
13
+ data.tableName ? `Table ${data.tableName}` : null,
14
+ ].filter(Boolean).join(' · ');
15
+ blocks.push({ type: 'text', value: header, align: 'center' });
16
+ blocks.push({ type: 'text', value: data.timestamp, align: 'center' });
17
+ blocks.push({ type: 'rule' });
18
+ for (const item of data.items) {
19
+ blocks.push({
20
+ type: 'row',
21
+ columns: [
22
+ { text: item.name, width: 60, align: 'left' },
23
+ { text: item.total.toFixed(2), width: 40, align: 'right' },
24
+ ],
25
+ });
26
+ blocks.push({
27
+ type: 'row',
28
+ columns: [
29
+ { text: ` ${item.quantity} x ${item.unitPrice.toFixed(2)}`, width: 60, align: 'left' },
30
+ { text: '', width: 40, align: 'right' },
31
+ ],
32
+ });
33
+ }
34
+ blocks.push({ type: 'rule' });
35
+ if (data.subtotal != null) {
36
+ blocks.push({
37
+ type: 'row',
38
+ columns: [
39
+ { text: 'Subtotal', width: 60, align: 'left' },
40
+ { text: data.subtotal.toFixed(2), width: 40, align: 'right' },
41
+ ],
42
+ });
43
+ }
44
+ if (data.discount != null) {
45
+ blocks.push({
46
+ type: 'row',
47
+ columns: [
48
+ { text: 'Discount', width: 60, align: 'left' },
49
+ { text: data.discount.toFixed(2), width: 40, align: 'right' },
50
+ ],
51
+ });
52
+ }
53
+ if (data.tax != null) {
54
+ blocks.push({
55
+ type: 'row',
56
+ columns: [
57
+ { text: 'Tax', width: 60, align: 'left' },
58
+ { text: data.tax.toFixed(2), width: 40, align: 'right' },
59
+ ],
60
+ });
61
+ }
62
+ blocks.push({
63
+ type: 'row',
64
+ columns: [
65
+ { text: 'GRAND TOTAL', width: 60, align: 'left', style: { bold: true } },
66
+ { text: data.total.toFixed(2), width: 40, align: 'right', style: { bold: true } },
67
+ ],
68
+ });
69
+ if (data.paymentMethod) {
70
+ blocks.push({ type: 'text', value: `PAID · ${data.paymentMethod}`, align: 'left' });
71
+ }
72
+ if (data.customerName) {
73
+ blocks.push({ type: 'text', value: data.customerName, align: 'left' });
74
+ }
75
+ if (data.footer) {
76
+ blocks.push({ type: 'text', value: data.footer, align: 'center' });
77
+ }
78
+ if (data.qrUrl) {
79
+ blocks.push({ type: 'qr', data: data.qrUrl, size: 6, align: 'center' });
80
+ }
81
+ return {
82
+ schema_version: '1.0',
83
+ document_type: 'receipt',
84
+ ...(data.paperWidthMm ? { paper_width_mm: data.paperWidthMm } : {}),
85
+ metadata: {
86
+ receipt_number: data.receiptNumber,
87
+ order_id: data.orderId,
88
+ timestamp: data.timestamp,
89
+ store: {
90
+ name: data.storeName,
91
+ address: data.storeAddress,
92
+ phone: data.storePhone,
93
+ },
94
+ },
95
+ blocks,
96
+ };
97
+ }
@@ -0,0 +1,12 @@
1
+ import type { GatewayHealth } from './schema.js';
2
+ export type DetectOptions = {
3
+ baseUrl?: string;
4
+ timeoutMs?: number;
5
+ };
6
+ export type DetectResult = {
7
+ available: boolean;
8
+ health?: GatewayHealth;
9
+ baseUrl: string;
10
+ };
11
+ export declare function detectPrintGateway(options?: DetectOptions): Promise<DetectResult>;
12
+ export declare function isAndroidDevice(): boolean;
package/dist/detect.js ADDED
@@ -0,0 +1,27 @@
1
+ const DEFAULT_BASE_URL = 'http://127.0.0.1:19643/api/v1';
2
+ export async function detectPrintGateway(options = {}) {
3
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
4
+ const timeoutMs = options.timeoutMs ?? 500;
5
+ try {
6
+ const controller = new AbortController();
7
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
8
+ const response = await fetch(`${baseUrl}/health`, {
9
+ signal: controller.signal,
10
+ credentials: 'include',
11
+ });
12
+ clearTimeout(timer);
13
+ if (!response.ok) {
14
+ return { available: false, baseUrl };
15
+ }
16
+ const health = (await response.json());
17
+ return { available: health.status === 'ok', health, baseUrl };
18
+ }
19
+ catch {
20
+ return { available: false, baseUrl };
21
+ }
22
+ }
23
+ export function isAndroidDevice() {
24
+ if (typeof navigator === 'undefined')
25
+ return false;
26
+ return /Android/i.test(navigator.userAgent);
27
+ }
@@ -0,0 +1,16 @@
1
+ import type { GatewayHealth } from './schema.js';
2
+ export type GatewayDetectHint = 'use_tablet' | 'start_service' | 'add_pos_origin' | 'https_blocked' | 'timeout' | 'unknown' | 'ok';
3
+ export type GatewayDetectDetailed = {
4
+ reachable: boolean;
5
+ authorized: boolean;
6
+ isAndroid: boolean;
7
+ posOrigin: string;
8
+ gatewayBaseUrl: string;
9
+ health?: GatewayHealth;
10
+ hint: GatewayDetectHint;
11
+ };
12
+ export type DetectDetailedOptions = {
13
+ baseUrl?: string;
14
+ timeoutMs?: number;
15
+ };
16
+ export declare function detectPrintGatewayDetailed(options?: DetectDetailedOptions): Promise<GatewayDetectDetailed>;
@@ -0,0 +1,87 @@
1
+ import { detectPrintGateway, isAndroidDevice } from './detect.js';
2
+ async function probePrinters(baseUrl, timeoutMs) {
3
+ try {
4
+ const controller = new AbortController();
5
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
6
+ const response = await fetch(`${baseUrl}/printers`, {
7
+ signal: controller.signal,
8
+ credentials: 'include',
9
+ });
10
+ clearTimeout(timer);
11
+ return response.ok;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ export async function detectPrintGatewayDetailed(options = {}) {
18
+ const gatewayBaseUrl = options.baseUrl ?? 'http://127.0.0.1:19643/api/v1';
19
+ const timeoutMs = options.timeoutMs ?? 3000;
20
+ const isAndroid = isAndroidDevice();
21
+ const posOrigin = typeof window !== 'undefined' ? window.location.origin : '';
22
+ if (!isAndroid) {
23
+ return {
24
+ reachable: false,
25
+ authorized: false,
26
+ isAndroid: false,
27
+ posOrigin,
28
+ gatewayBaseUrl,
29
+ hint: 'use_tablet',
30
+ };
31
+ }
32
+ if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
33
+ // Mixed content may block http://127.0.0.1 — still try detect below.
34
+ }
35
+ let health;
36
+ let reachable = false;
37
+ let hint = 'unknown';
38
+ for (let attempt = 0; attempt < 2; attempt += 1) {
39
+ const detection = await detectPrintGateway({
40
+ baseUrl: gatewayBaseUrl,
41
+ timeoutMs,
42
+ });
43
+ if (detection.available && detection.health) {
44
+ reachable = true;
45
+ health = detection.health;
46
+ break;
47
+ }
48
+ if (attempt === 0) {
49
+ await new Promise((resolve) => setTimeout(resolve, 200));
50
+ }
51
+ }
52
+ if (!reachable) {
53
+ if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
54
+ hint = 'https_blocked';
55
+ }
56
+ else {
57
+ hint = 'start_service';
58
+ }
59
+ return {
60
+ reachable: false,
61
+ authorized: false,
62
+ isAndroid,
63
+ posOrigin,
64
+ gatewayBaseUrl,
65
+ hint,
66
+ };
67
+ }
68
+ const authorized = await probePrinters(gatewayBaseUrl, timeoutMs);
69
+ if (authorized) {
70
+ hint = 'ok';
71
+ }
72
+ else if (health?.pos_origins_configured === false) {
73
+ hint = 'add_pos_origin';
74
+ }
75
+ else {
76
+ hint = 'add_pos_origin';
77
+ }
78
+ return {
79
+ reachable: true,
80
+ authorized,
81
+ isAndroid,
82
+ posOrigin,
83
+ gatewayBaseUrl,
84
+ health,
85
+ hint: authorized ? 'ok' : hint,
86
+ };
87
+ }
@@ -0,0 +1,19 @@
1
+ export declare class PrintGatewayError extends Error {
2
+ readonly status?: number;
3
+ constructor(message: string, status?: number);
4
+ }
5
+ export declare class AuthError extends PrintGatewayError {
6
+ constructor(message?: string);
7
+ }
8
+ export declare class RateLimitError extends PrintGatewayError {
9
+ constructor(message?: string);
10
+ }
11
+ export declare class NetworkError extends PrintGatewayError {
12
+ constructor(message?: string);
13
+ }
14
+ export declare class JobFailedError extends PrintGatewayError {
15
+ readonly jobId: string;
16
+ readonly lastError?: string;
17
+ constructor(jobId: string, lastError?: string);
18
+ }
19
+ export declare function parseApiError(status: number, body: string): PrintGatewayError;
package/dist/errors.js ADDED
@@ -0,0 +1,49 @@
1
+ export class PrintGatewayError extends Error {
2
+ constructor(message, status) {
3
+ super(message);
4
+ this.name = 'PrintGatewayError';
5
+ this.status = status;
6
+ }
7
+ }
8
+ export class AuthError extends PrintGatewayError {
9
+ constructor(message = 'Printing connection lost. Contact support to re-pair this device with your POS.') {
10
+ super(message, 401);
11
+ this.name = 'AuthError';
12
+ }
13
+ }
14
+ export class RateLimitError extends PrintGatewayError {
15
+ constructor(message = 'Print rate limit exceeded. Try again shortly.') {
16
+ super(message, 429);
17
+ this.name = 'RateLimitError';
18
+ }
19
+ }
20
+ export class NetworkError extends PrintGatewayError {
21
+ constructor(message = 'Could not reach the print gateway on this device.') {
22
+ super(message);
23
+ this.name = 'NetworkError';
24
+ }
25
+ }
26
+ export class JobFailedError extends PrintGatewayError {
27
+ constructor(jobId, lastError) {
28
+ super(lastError ?? `Print job ${jobId} failed`);
29
+ this.name = 'JobFailedError';
30
+ this.jobId = jobId;
31
+ this.lastError = lastError;
32
+ }
33
+ }
34
+ export function parseApiError(status, body) {
35
+ let message = body;
36
+ try {
37
+ const parsed = JSON.parse(body);
38
+ if (parsed.error)
39
+ message = parsed.error;
40
+ }
41
+ catch {
42
+ // keep raw body
43
+ }
44
+ if (status === 401)
45
+ return new AuthError(message);
46
+ if (status === 429)
47
+ return new RateLimitError(message);
48
+ return new PrintGatewayError(message || `Request failed with status ${status}`, status);
49
+ }
@@ -0,0 +1,4 @@
1
+ export type FallbackPrintOptions = {
2
+ title?: string;
3
+ };
4
+ export declare function fallbackWindowPrint(options?: FallbackPrintOptions): Promise<void>;
@@ -0,0 +1,13 @@
1
+ export function fallbackWindowPrint(options = {}) {
2
+ return new Promise((resolve, reject) => {
3
+ if (typeof window === 'undefined' || typeof window.print !== 'function') {
4
+ reject(new Error('window.print is not available'));
5
+ return;
6
+ }
7
+ if (options.title && typeof document !== 'undefined') {
8
+ document.title = options.title;
9
+ }
10
+ window.print();
11
+ resolve();
12
+ });
13
+ }
@@ -0,0 +1,35 @@
1
+ import type { PrintDocument } from './schema.js';
2
+ export { buildReceiptDocument } from './adapters/receiptAdapter.js';
3
+ export type { ReceiptData, ReceiptLineItem } from './adapters/receiptAdapter.js';
4
+ export { detectPrintGateway, isAndroidDevice } from './detect.js';
5
+ export { detectPrintGatewayDetailed, type GatewayDetectDetailed, type GatewayDetectHint, } from './detectDetailed.js';
6
+ export { AuthError, JobFailedError, NetworkError, PrintGatewayError, RateLimitError, } from './errors.js';
7
+ export { fallbackWindowPrint } from './fallback.js';
8
+ export { buildPrintIntentUrl, openPrintIntent } from './intent.js';
9
+ export type { IntentPrintPayload } from './intent.js';
10
+ export { PrintGatewayClient } from './print.js';
11
+ export { createPrintRouter, parseSetupBundle, resolvePrintTarget, validateSetup, } from './setup.js';
12
+ export type { PrintRouteOptions, PrintRouterOptions, SetupValidation } from './setup.js';
13
+ export { textToDocument } from './textToDocument.js';
14
+ export type { GatewayDestination, GatewayHealth, GatewayPrinter, PrintDocument, PrintDocumentBlock, PrintJobResponse, PrintOptions, PrintRequest, SetupBundle, } from './schema.js';
15
+ export type PrintWithFallbackOptions = {
16
+ baseUrl?: string;
17
+ printerId?: string;
18
+ role?: string;
19
+ idempotencyKey?: string;
20
+ waitForCompletion?: boolean;
21
+ onlyOnAndroid?: boolean;
22
+ detectTimeoutMs?: number;
23
+ };
24
+ export type PrintResult = {
25
+ method: 'gateway';
26
+ jobId: string;
27
+ status: string;
28
+ } | {
29
+ method: 'intent';
30
+ jobId: string;
31
+ status: string;
32
+ } | {
33
+ method: 'window.print';
34
+ };
35
+ export declare function printWithFallback(document: PrintDocument, options?: PrintWithFallbackOptions): Promise<PrintResult>;
package/dist/index.js ADDED
@@ -0,0 +1,80 @@
1
+ import { detectPrintGateway, isAndroidDevice } from './detect.js';
2
+ import { fallbackWindowPrint } from './fallback.js';
3
+ import { openPrintIntent } from './intent.js';
4
+ import { PrintGatewayClient } from './print.js';
5
+ import { resolvePrintTarget } from './setup.js';
6
+ export { buildReceiptDocument } from './adapters/receiptAdapter.js';
7
+ export { detectPrintGateway, isAndroidDevice } from './detect.js';
8
+ export { detectPrintGatewayDetailed, } from './detectDetailed.js';
9
+ export { AuthError, JobFailedError, NetworkError, PrintGatewayError, RateLimitError, } from './errors.js';
10
+ export { fallbackWindowPrint } from './fallback.js';
11
+ export { buildPrintIntentUrl, openPrintIntent } from './intent.js';
12
+ export { PrintGatewayClient } from './print.js';
13
+ export { createPrintRouter, parseSetupBundle, resolvePrintTarget, validateSetup, } from './setup.js';
14
+ export { textToDocument } from './textToDocument.js';
15
+ async function tryIntentFallback(document, target) {
16
+ if (!isAndroidDevice())
17
+ return null;
18
+ try {
19
+ openPrintIntent({
20
+ token: '',
21
+ document,
22
+ role: target.role,
23
+ printer_id: target.printerId,
24
+ });
25
+ return { method: 'intent', jobId: 'intent', status: 'submitted' };
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ export async function printWithFallback(document, options = {}) {
32
+ const onlyOnAndroid = options.onlyOnAndroid ?? true;
33
+ if (onlyOnAndroid && !isAndroidDevice()) {
34
+ await fallbackWindowPrint();
35
+ return { method: 'window.print' };
36
+ }
37
+ const detection = await detectPrintGateway({
38
+ baseUrl: options.baseUrl,
39
+ timeoutMs: options.detectTimeoutMs ?? 1500,
40
+ });
41
+ const client = new PrintGatewayClient({
42
+ baseUrl: detection.baseUrl,
43
+ });
44
+ const target = detection.available
45
+ ? await resolvePrintTarget(client, {
46
+ printerId: options.printerId,
47
+ role: options.role,
48
+ health: detection.health,
49
+ })
50
+ : {
51
+ printerId: options.printerId,
52
+ role: options.role ?? 'receipt',
53
+ };
54
+ if (!detection.available) {
55
+ const intentResult = await tryIntentFallback(document, target);
56
+ if (intentResult)
57
+ return intentResult;
58
+ await fallbackWindowPrint();
59
+ return { method: 'window.print' };
60
+ }
61
+ const request = {
62
+ document,
63
+ printer_id: target.printerId,
64
+ role: target.printerId ? undefined : target.role,
65
+ };
66
+ try {
67
+ const job = await client.print(request, options.idempotencyKey);
68
+ if (options.waitForCompletion) {
69
+ const finalJob = await client.waitForJob(job.job_id);
70
+ return { method: 'gateway', jobId: finalJob.job_id, status: finalJob.status };
71
+ }
72
+ return { method: 'gateway', jobId: job.job_id, status: job.status };
73
+ }
74
+ catch {
75
+ const intentResult = await tryIntentFallback(document, target);
76
+ if (intentResult)
77
+ return intentResult;
78
+ throw new Error('Print gateway unavailable and intent fallback failed');
79
+ }
80
+ }
@@ -0,0 +1,9 @@
1
+ import type { PrintDocument } from './schema.js';
2
+ export type IntentPrintPayload = {
3
+ token: string;
4
+ document: PrintDocument;
5
+ role?: string;
6
+ printer_id?: string;
7
+ };
8
+ export declare function buildPrintIntentUrl(payload: IntentPrintPayload): string;
9
+ export declare function openPrintIntent(payload: IntentPrintPayload): boolean;
package/dist/intent.js ADDED
@@ -0,0 +1,29 @@
1
+ import { PrintGatewayError } from './errors.js';
2
+ const INTENT_SCHEME = 'printgateway';
3
+ const INTENT_PATH = 'print';
4
+ const MAX_INTENT_PAYLOAD_BYTES = 6144;
5
+ function base64UrlEncode(value) {
6
+ const bytes = new TextEncoder().encode(value);
7
+ let binary = '';
8
+ for (const byte of bytes) {
9
+ binary += String.fromCharCode(byte);
10
+ }
11
+ const base64 = btoa(binary);
12
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
13
+ }
14
+ export function buildPrintIntentUrl(payload) {
15
+ const json = JSON.stringify(payload);
16
+ const byteLength = new TextEncoder().encode(json).byteLength;
17
+ if (byteLength > MAX_INTENT_PAYLOAD_BYTES) {
18
+ throw new PrintGatewayError(`Print payload is too large for intent fallback (${byteLength} bytes, max ${MAX_INTENT_PAYLOAD_BYTES})`);
19
+ }
20
+ const encoded = base64UrlEncode(json);
21
+ return `${INTENT_SCHEME}://${INTENT_PATH}?payload=${encoded}`;
22
+ }
23
+ export function openPrintIntent(payload) {
24
+ if (typeof window === 'undefined')
25
+ return false;
26
+ const url = buildPrintIntentUrl(payload);
27
+ window.location.href = url;
28
+ return true;
29
+ }
@@ -0,0 +1,18 @@
1
+ import type { GatewayHealth, GatewayPrinter, PrintJobResponse, PrintRequest } from './schema.js';
2
+ export type PrintGatewayClientOptions = {
3
+ baseUrl?: string;
4
+ };
5
+ export declare class PrintGatewayClient {
6
+ private baseUrl;
7
+ constructor(options?: PrintGatewayClientOptions);
8
+ private headers;
9
+ listPrinters(): Promise<GatewayPrinter[]>;
10
+ getHealth(): Promise<GatewayHealth>;
11
+ print(request: PrintRequest, idempotencyKey?: string): Promise<PrintJobResponse>;
12
+ getJob(jobId: string): Promise<PrintJobResponse & {
13
+ attempts?: number;
14
+ last_error?: string;
15
+ }>;
16
+ retryJob(jobId: string): Promise<PrintJobResponse>;
17
+ waitForJob(jobId: string, timeoutMs?: number, pollMs?: number): Promise<PrintJobResponse>;
18
+ }
package/dist/print.js ADDED
@@ -0,0 +1,89 @@
1
+ import { AuthError, JobFailedError, NetworkError, parseApiError } from './errors.js';
2
+ export class PrintGatewayClient {
3
+ constructor(options = {}) {
4
+ this.baseUrl = options.baseUrl ?? 'http://127.0.0.1:19643/api/v1';
5
+ }
6
+ headers(idempotencyKey) {
7
+ const headers = {
8
+ 'Content-Type': 'application/json',
9
+ };
10
+ if (idempotencyKey) {
11
+ headers['X-Idempotency-Key'] = idempotencyKey;
12
+ }
13
+ return headers;
14
+ }
15
+ async listPrinters() {
16
+ const response = await fetch(`${this.baseUrl}/printers`, {
17
+ headers: this.headers(),
18
+ credentials: 'include',
19
+ });
20
+ if (!response.ok) {
21
+ throw parseApiError(response.status, await response.text());
22
+ }
23
+ const payload = (await response.json());
24
+ return payload.printers;
25
+ }
26
+ async getHealth() {
27
+ try {
28
+ const response = await fetch(`${this.baseUrl}/health`, {
29
+ credentials: 'include',
30
+ });
31
+ if (!response.ok) {
32
+ throw parseApiError(response.status, await response.text());
33
+ }
34
+ return (await response.json());
35
+ }
36
+ catch (error) {
37
+ if (error instanceof NetworkError || error instanceof AuthError)
38
+ throw error;
39
+ throw new NetworkError(error instanceof Error ? error.message : undefined);
40
+ }
41
+ }
42
+ async print(request, idempotencyKey) {
43
+ const response = await fetch(`${this.baseUrl}/print`, {
44
+ method: 'POST',
45
+ headers: this.headers(idempotencyKey),
46
+ credentials: 'include',
47
+ body: JSON.stringify(request),
48
+ });
49
+ if (!response.ok) {
50
+ throw parseApiError(response.status, await response.text());
51
+ }
52
+ return (await response.json());
53
+ }
54
+ async getJob(jobId) {
55
+ const response = await fetch(`${this.baseUrl}/jobs/${jobId}`, {
56
+ headers: this.headers(),
57
+ credentials: 'include',
58
+ });
59
+ if (!response.ok) {
60
+ throw parseApiError(response.status, await response.text());
61
+ }
62
+ return (await response.json());
63
+ }
64
+ async retryJob(jobId) {
65
+ const response = await fetch(`${this.baseUrl}/jobs/${jobId}/retry`, {
66
+ method: 'POST',
67
+ headers: this.headers(),
68
+ credentials: 'include',
69
+ });
70
+ if (!response.ok) {
71
+ throw parseApiError(response.status, await response.text());
72
+ }
73
+ return (await response.json());
74
+ }
75
+ async waitForJob(jobId, timeoutMs = 30000, pollMs = 500) {
76
+ const deadline = Date.now() + timeoutMs;
77
+ while (Date.now() < deadline) {
78
+ const job = await this.getJob(jobId);
79
+ if (job.status === 'completed') {
80
+ return job;
81
+ }
82
+ if (job.status === 'failed') {
83
+ throw new JobFailedError(job.job_id, job.last_error);
84
+ }
85
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
86
+ }
87
+ throw new NetworkError(`Timed out waiting for job ${jobId}`);
88
+ }
89
+ }