@tscomfy/client 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +76 -2
- package/dist/channel.d.ts +54 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +307 -0
- package/dist/channel.js.map +1 -0
- package/dist/client-error.d.ts +15 -0
- package/dist/client-error.d.ts.map +1 -0
- package/dist/client-error.js +16 -0
- package/dist/client-error.js.map +1 -0
- package/dist/connection.d.ts +33 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +35 -0
- package/dist/connection.js.map +1 -0
- package/dist/event-queue.d.ts +16 -0
- package/dist/event-queue.d.ts.map +1 -0
- package/dist/event-queue.js +66 -0
- package/dist/event-queue.js.map +1 -0
- package/dist/events.d.ts +60 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/files.d.ts +55 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +69 -0
- package/dist/files.js.map +1 -0
- package/dist/frames.d.ts +24 -0
- package/dist/frames.d.ts.map +1 -0
- package/dist/frames.js +71 -0
- package/dist/frames.js.map +1 -0
- package/dist/http.d.ts +28 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +96 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/job.d.ts +59 -0
- package/dist/job.d.ts.map +1 -0
- package/dist/job.js +481 -0
- package/dist/job.js.map +1 -0
- package/dist/outcome.d.ts +47 -0
- package/dist/outcome.d.ts.map +1 -0
- package/dist/outcome.js +2 -0
- package/dist/outcome.js.map +1 -0
- package/dist/outputs.d.ts +53 -0
- package/dist/outputs.d.ts.map +1 -0
- package/dist/outputs.js +21 -0
- package/dist/outputs.js.map +1 -0
- package/dist/read-outputs.d.ts +11 -0
- package/dist/read-outputs.d.ts.map +1 -0
- package/dist/read-outputs.js +142 -0
- package/dist/read-outputs.js.map +1 -0
- package/dist/server-errors.d.ts +7 -0
- package/dist/server-errors.d.ts.map +1 -0
- package/dist/server-errors.js +46 -0
- package/dist/server-errors.js.map +1 -0
- package/dist/server.d.ts +30 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -0
- package/dist/timers.d.ts +11 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +38 -0
- package/dist/timers.js.map +1 -0
- package/dist/url.d.ts +14 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +39 -0
- package/dist/url.js.map +1 -0
- package/dist/uuid.d.ts +7 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +13 -0
- package/dist/uuid.js.map +1 -0
- package/package.json +46 -4
- package/src/channel.ts +334 -0
- package/src/client-error.ts +19 -0
- package/src/connection.ts +59 -0
- package/src/event-queue.ts +70 -0
- package/src/events.ts +76 -0
- package/src/files.ts +119 -0
- package/src/frames.ts +87 -0
- package/src/http.ts +113 -0
- package/src/index.ts +53 -0
- package/src/job.ts +562 -0
- package/src/outcome.ts +52 -0
- package/src/outputs.ts +69 -0
- package/src/read-outputs.ts +171 -0
- package/src/server-errors.ts +61 -0
- package/src/server.ts +29 -0
- package/src/timers.ts +35 -0
- package/src/url.ts +41 -0
- package/src/uuid.ts +12 -0
package/src/frames.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type NodeId, parse } from "@tscomfy/core";
|
|
2
|
+
import { isObject } from "./http.ts";
|
|
3
|
+
|
|
4
|
+
/** A preview image: binary type 4, which carries the node and prompt it belongs to. */
|
|
5
|
+
export interface PreviewFrame {
|
|
6
|
+
readonly kind: "preview";
|
|
7
|
+
readonly promptId: string;
|
|
8
|
+
readonly node: NodeId;
|
|
9
|
+
readonly mime: string;
|
|
10
|
+
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Progress text: binary type 3. Only newer servers say which prompt it belongs to. */
|
|
14
|
+
export interface TextFrame {
|
|
15
|
+
readonly kind: "text";
|
|
16
|
+
readonly promptId: string | undefined;
|
|
17
|
+
readonly node: NodeId;
|
|
18
|
+
readonly text: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const TEXT = 3;
|
|
22
|
+
const PREVIEW_WITH_METADATA = 4;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Decodes a binary websocket frame: a big-endian `uint32` type, then the payload. Type 1 (a
|
|
26
|
+
* preview without ids) isn't decoded: the client asks for type 4 instead, and an unattributed
|
|
27
|
+
* image can't be given to a job. Unknown types and malformed frames give `undefined`.
|
|
28
|
+
* `textHasPromptId` is the server's `supports_progress_text_metadata` flag.
|
|
29
|
+
*/
|
|
30
|
+
export function decodeFrame(
|
|
31
|
+
data: ArrayBuffer,
|
|
32
|
+
textHasPromptId: boolean,
|
|
33
|
+
): PreviewFrame | TextFrame | undefined {
|
|
34
|
+
if (data.byteLength < 4) return undefined;
|
|
35
|
+
const view = new DataView(data);
|
|
36
|
+
const type = view.getUint32(0);
|
|
37
|
+
if (type === PREVIEW_WITH_METADATA) return preview(data, view);
|
|
38
|
+
if (type === TEXT) return text(data, view, textHasPromptId);
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function preview(data: ArrayBuffer, view: DataView): PreviewFrame | undefined {
|
|
43
|
+
const length = readLength(view, 4);
|
|
44
|
+
if (length === undefined) return undefined;
|
|
45
|
+
const metadata = parse(decoder.decode(new Uint8Array(data, 8, length))).value;
|
|
46
|
+
if (!isObject(metadata)) return undefined;
|
|
47
|
+
const promptId = metadata["prompt_id"];
|
|
48
|
+
const node = metadata["display_node_id"] ?? metadata["node_id"];
|
|
49
|
+
const mime = metadata["image_type"];
|
|
50
|
+
if (typeof promptId !== "string" || typeof node !== "string") return undefined;
|
|
51
|
+
return {
|
|
52
|
+
kind: "preview",
|
|
53
|
+
promptId,
|
|
54
|
+
node,
|
|
55
|
+
mime: typeof mime === "string" ? mime : "image/jpeg",
|
|
56
|
+
bytes: new Uint8Array(data.slice(8 + length)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function text(data: ArrayBuffer, view: DataView, hasPromptId: boolean): TextFrame | undefined {
|
|
61
|
+
let offset = 4;
|
|
62
|
+
let promptId: string | undefined;
|
|
63
|
+
if (hasPromptId) {
|
|
64
|
+
const length = readLength(view, offset);
|
|
65
|
+
if (length === undefined) return undefined;
|
|
66
|
+
promptId = decoder.decode(new Uint8Array(data, offset + 4, length));
|
|
67
|
+
offset += 4 + length;
|
|
68
|
+
}
|
|
69
|
+
const length = readLength(view, offset);
|
|
70
|
+
if (length === undefined) return undefined;
|
|
71
|
+
const node = decoder.decode(new Uint8Array(data, offset + 4, length));
|
|
72
|
+
return {
|
|
73
|
+
kind: "text",
|
|
74
|
+
promptId,
|
|
75
|
+
node,
|
|
76
|
+
text: decoder.decode(new Uint8Array(data, offset + 4 + length)),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A `uint32` length at `offset` whose bytes fit in the frame after it. */
|
|
81
|
+
function readLength(view: DataView, offset: number): number | undefined {
|
|
82
|
+
if (view.byteLength < offset + 4) return undefined;
|
|
83
|
+
const length = view.getUint32(offset);
|
|
84
|
+
return offset + 4 + length <= view.byteLength ? length : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const decoder = new TextDecoder();
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type JsonObject, type JsonValue, parse } from "@tscomfy/core";
|
|
2
|
+
import { ClientError } from "./client-error.ts";
|
|
3
|
+
import type { Server } from "./server.ts";
|
|
4
|
+
import { baseUrl, endpoint, shownUrl } from "./url.ts";
|
|
5
|
+
|
|
6
|
+
/** One HTTP request to a server. */
|
|
7
|
+
export interface Request {
|
|
8
|
+
readonly method: "GET" | "POST";
|
|
9
|
+
/** Under the server's path: `prompt`, `api/jobs/<id>`. */
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly query?: Readonly<Record<string, string>>;
|
|
12
|
+
readonly body?: string | FormData;
|
|
13
|
+
readonly signal?: AbortSignal | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sends a request and returns the response, whatever its status. A failure to reach the server
|
|
18
|
+
* is a {@link ClientError}; an aborted signal rejects with the signal's reason, as `fetch` does.
|
|
19
|
+
*/
|
|
20
|
+
export async function send(server: Server, request: Request): Promise<Response> {
|
|
21
|
+
const url = endpoint(baseUrl(server), request.path, request.query);
|
|
22
|
+
const headers = new Headers(server.headers);
|
|
23
|
+
if (typeof request.body === "string") headers.set("content-type", "application/json");
|
|
24
|
+
const fetcher = server.fetch ?? globalThis.fetch;
|
|
25
|
+
try {
|
|
26
|
+
return await fetcher(url, {
|
|
27
|
+
method: request.method,
|
|
28
|
+
headers,
|
|
29
|
+
body: request.body ?? null,
|
|
30
|
+
signal: request.signal ?? null,
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (request.signal?.aborted === true) throw request.signal.reason;
|
|
34
|
+
// The runtime's message may quote the URL, query and all.
|
|
35
|
+
const why = describe(error).replaceAll(url.href, shownUrl(url));
|
|
36
|
+
throw new ClientError(`Could not reach ${shownUrl(url)}: ${why}`, { cause: error });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The response's body as JSON, read losslessly (big integers stay exact). */
|
|
41
|
+
export async function readJson(response: Response): Promise<JsonValue> {
|
|
42
|
+
const text = await response.text();
|
|
43
|
+
const { value } = parse(text);
|
|
44
|
+
if (value === undefined) {
|
|
45
|
+
throw new ClientError(`${where(response)} answered with a body that isn't JSON`, {
|
|
46
|
+
status: response.status,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The response's body as a JSON object; anything else is a {@link ClientError}. */
|
|
53
|
+
export async function readObject(response: Response): Promise<JsonObject> {
|
|
54
|
+
const value = await readJson(response);
|
|
55
|
+
if (!isObject(value)) {
|
|
56
|
+
throw new ClientError(`${where(response)} answered with JSON that isn't an object`, {
|
|
57
|
+
status: response.status,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A {@link ClientError} for an answer the protocol doesn't expect, with the body's message. */
|
|
64
|
+
export async function unexpected(response: Response): Promise<ClientError> {
|
|
65
|
+
const answer = `${String(response.status)} ${response.statusText}`.trimEnd();
|
|
66
|
+
// Best effort: the status says what went wrong; the body may say why.
|
|
67
|
+
const body = await response.text().catch(() => "");
|
|
68
|
+
const detail = bodyMessage(body);
|
|
69
|
+
return new ClientError(
|
|
70
|
+
`${where(response)} answered ${answer}${detail === undefined ? "" : `: ${detail}`}`,
|
|
71
|
+
{ status: response.status },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A JSON list. */
|
|
76
|
+
export function isList(value: JsonValue | undefined): value is readonly JsonValue[] {
|
|
77
|
+
return Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A JSON object, not an array or `null`. */
|
|
81
|
+
export function isObject(value: JsonValue | undefined): value is JsonObject {
|
|
82
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The response's URL without its query, or a fallback when the runtime gives none. */
|
|
86
|
+
function where(response: Response): string {
|
|
87
|
+
return URL.canParse(response.url) ? shownUrl(new URL(response.url)) : "The server";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The message of an error body (`{"error": …}`, `{"detail": …}`), at most 200 characters. */
|
|
91
|
+
function bodyMessage(text: string): string | undefined {
|
|
92
|
+
const value = parse(text).value;
|
|
93
|
+
if (!isObject(value)) return undefined;
|
|
94
|
+
const message = value["error"] ?? value["detail"] ?? value["message"];
|
|
95
|
+
const shown =
|
|
96
|
+
typeof message === "string"
|
|
97
|
+
? message
|
|
98
|
+
: isObject(message) && typeof message["message"] === "string"
|
|
99
|
+
? message["message"]
|
|
100
|
+
: undefined;
|
|
101
|
+
if (shown === undefined) return undefined;
|
|
102
|
+
// oxlint-disable-next-line no-control-regex -- control characters are what it removes
|
|
103
|
+
const printable = shown.replaceAll(/[\u0000-\u001f\u007f]/g, " ");
|
|
104
|
+
return printable.length <= 200 ? printable : `${printable.slice(0, 200)}…`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Why something failed. Node's `fetch` says only "fetch failed"; the reason is in its `cause`. */
|
|
108
|
+
function describe(error: unknown): string {
|
|
109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
+
const cause: unknown = error instanceof Error ? error.cause : undefined;
|
|
111
|
+
if (!(cause instanceof Error) || message.includes(cause.message)) return message;
|
|
112
|
+
return `${message} (${cause.message})`;
|
|
113
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @tscomfy/client
|
|
3
|
+
*
|
|
4
|
+
* Runs tscomfy workflows on a ComfyUI server: `connect` opens a websocket, `submit` queues a
|
|
5
|
+
* workflow and returns a job whose events stream progress, live previews and outputs, and whose
|
|
6
|
+
* outcome the server's record of the job decides. `uploadFile` and `fetchFile` move files.
|
|
7
|
+
* Runtime-agnostic: it uses only `fetch`, `WebSocket` and their companions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { ClientError } from "./client-error.ts";
|
|
11
|
+
export { type Connection, type ConnectOptions, connect } from "./connection.ts";
|
|
12
|
+
export type {
|
|
13
|
+
BlockedEvent,
|
|
14
|
+
CachedEvent,
|
|
15
|
+
ExecutingEvent,
|
|
16
|
+
JobEvent,
|
|
17
|
+
OutputEvent,
|
|
18
|
+
PreviewEvent,
|
|
19
|
+
ProgressEvent,
|
|
20
|
+
StartedEvent,
|
|
21
|
+
TextEvent,
|
|
22
|
+
} from "./events.ts";
|
|
23
|
+
export {
|
|
24
|
+
type FetchFileOptions,
|
|
25
|
+
fetchFile,
|
|
26
|
+
type FileData,
|
|
27
|
+
fileUrl,
|
|
28
|
+
type UploadedFile,
|
|
29
|
+
uploadFile,
|
|
30
|
+
type UploadOptions,
|
|
31
|
+
type UploadSource,
|
|
32
|
+
} from "./files.ts";
|
|
33
|
+
export { type Job, type PreviewMethod, submit, type SubmitOptions } from "./job.ts";
|
|
34
|
+
export type {
|
|
35
|
+
CancelledOutcome,
|
|
36
|
+
ExecutionError,
|
|
37
|
+
FailedOutcome,
|
|
38
|
+
Outcome,
|
|
39
|
+
RejectedOutcome,
|
|
40
|
+
SuccessOutcome,
|
|
41
|
+
} from "./outcome.ts";
|
|
42
|
+
export {
|
|
43
|
+
type FileKind,
|
|
44
|
+
type FileRef,
|
|
45
|
+
type FolderType,
|
|
46
|
+
type NodeOutput,
|
|
47
|
+
type NodeRef,
|
|
48
|
+
type OutputFile,
|
|
49
|
+
outputFiles,
|
|
50
|
+
outputOf,
|
|
51
|
+
type Outputs,
|
|
52
|
+
} from "./outputs.ts";
|
|
53
|
+
export type { Server } from "./server.ts";
|