@tscomfy/client 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/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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { isOutputRef, type JsonObject, type JsonValue, type NodeId } from "@tscomfy/core";
|
|
2
|
+
import { isList, isObject } from "./http.ts";
|
|
3
|
+
import type { FileKind, FolderType, NodeOutput, NodeRef, OutputFile, Outputs } from "./outputs.ts";
|
|
4
|
+
|
|
5
|
+
/** The id of a node given by id, handle or output. */
|
|
6
|
+
export function nodeIdOf(node: NodeRef): NodeId {
|
|
7
|
+
if (typeof node === "string") return node;
|
|
8
|
+
if (isOutputRef(node)) return node.node;
|
|
9
|
+
return node.id;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A node's UI output read leniently: anything unrecognized stays in `ui` only. */
|
|
13
|
+
export function readNodeOutput(node: NodeId, ui: JsonObject): NodeOutput {
|
|
14
|
+
const flags = ui["animated"];
|
|
15
|
+
const animated = isList(flags) && flags.includes(true);
|
|
16
|
+
const files: OutputFile[] = [];
|
|
17
|
+
for (const [key, value] of Object.entries(ui)) {
|
|
18
|
+
if (!isList(value)) continue;
|
|
19
|
+
for (const item of value) {
|
|
20
|
+
const file = readFile(key, item, animated);
|
|
21
|
+
if (file !== undefined) files.push(file);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const texts = ui["text"];
|
|
25
|
+
const text = isList(texts)
|
|
26
|
+
? texts.filter((item): item is string => typeof item === "string")
|
|
27
|
+
: [];
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
node,
|
|
30
|
+
files: Object.freeze(files),
|
|
31
|
+
text: Object.freeze(text),
|
|
32
|
+
ui: deepFreeze(ui),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Builds the outputs map, in the order of `order` (the prompt's node order), unknown ids last. */
|
|
37
|
+
export function outputsInOrder(
|
|
38
|
+
outputs: ReadonlyMap<NodeId, NodeOutput>,
|
|
39
|
+
order: readonly NodeId[],
|
|
40
|
+
): Outputs {
|
|
41
|
+
const position = new Map(order.map((id, index) => [id, index]));
|
|
42
|
+
const sorted = [...outputs.values()].toSorted(
|
|
43
|
+
(a, b) =>
|
|
44
|
+
(position.get(a.node) ?? Number.MAX_SAFE_INTEGER) -
|
|
45
|
+
(position.get(b.node) ?? Number.MAX_SAFE_INTEGER),
|
|
46
|
+
);
|
|
47
|
+
return new FrozenMap(sorted.map((output) => [output.node, output]));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A map nobody can change: what `outcome.outputs` is. */
|
|
51
|
+
class FrozenMap<K, V> implements ReadonlyMap<K, V> {
|
|
52
|
+
readonly #map: ReadonlyMap<K, V>;
|
|
53
|
+
|
|
54
|
+
constructor(entries: Iterable<readonly [K, V]>) {
|
|
55
|
+
this.#map = new Map(entries);
|
|
56
|
+
Object.freeze(this);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get size(): number {
|
|
60
|
+
return this.#map.size;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get(key: K): V | undefined {
|
|
64
|
+
return this.#map.get(key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
has(key: K): boolean {
|
|
68
|
+
return this.#map.has(key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
forEach(callback: (value: V, key: K, map: ReadonlyMap<K, V>) => void, thisArg?: unknown): void {
|
|
72
|
+
for (const [key, value] of this.#map) callback.call(thisArg, value, key, this);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
entries(): MapIterator<[K, V]> {
|
|
76
|
+
return this.#map.entries();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
keys(): MapIterator<K> {
|
|
80
|
+
return this.#map.keys();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
values(): MapIterator<V> {
|
|
84
|
+
return this.#map.values();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
[Symbol.iterator](): MapIterator<[K, V]> {
|
|
88
|
+
return this.#map[Symbol.iterator]();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Freezes a JSON value and everything in it. */
|
|
93
|
+
function deepFreeze<T extends JsonValue>(value: T): T {
|
|
94
|
+
if (typeof value === "object" && value !== null) {
|
|
95
|
+
for (const item of Object.values(value)) deepFreeze(item);
|
|
96
|
+
Object.freeze(value);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** One of the server's folder names. */
|
|
102
|
+
export function isFolderType(value: JsonValue | undefined): value is FolderType {
|
|
103
|
+
return value === "output" || value === "temp" || value === "input";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A file entry: `{ filename, subfolder, type }` and maybe `metadata`. */
|
|
107
|
+
function readFile(key: string, item: JsonValue, animated: boolean): OutputFile | undefined {
|
|
108
|
+
if (!isObject(item)) return undefined;
|
|
109
|
+
const { filename, subfolder, type } = item;
|
|
110
|
+
if (typeof filename !== "string" || filename === "") return undefined;
|
|
111
|
+
if (!isFolderType(type)) return undefined;
|
|
112
|
+
const metadata = isObject(item["metadata"]) ? item["metadata"] : {};
|
|
113
|
+
const kind = kindOf(key, filename, metadata["kind"], animated);
|
|
114
|
+
const file: OutputFile = {
|
|
115
|
+
filename,
|
|
116
|
+
subfolder: typeof subfolder === "string" ? subfolder : "",
|
|
117
|
+
type,
|
|
118
|
+
kind,
|
|
119
|
+
animated: animated && (kind === "image" || kind === "video"),
|
|
120
|
+
key,
|
|
121
|
+
...dimension("width", metadata["width"]),
|
|
122
|
+
...dimension("height", metadata["height"]),
|
|
123
|
+
};
|
|
124
|
+
return Object.freeze(file);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function dimension(
|
|
128
|
+
name: "width" | "height",
|
|
129
|
+
value: JsonValue | undefined,
|
|
130
|
+
): { readonly width?: number; readonly height?: number } {
|
|
131
|
+
return typeof value === "number" && Number.isFinite(value) ? { [name]: value } : {};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const EXTENSIONS: ReadonlyMap<string, FileKind> = new Map([
|
|
135
|
+
...["png", "jpg", "jpeg", "webp", "gif", "bmp", "tif", "tiff", "avif", "svg"].map(
|
|
136
|
+
(extension) => [extension, "image"] as const,
|
|
137
|
+
),
|
|
138
|
+
...["mp4", "webm", "mov", "mkv", "avi", "m4v"].map((extension) => [extension, "video"] as const),
|
|
139
|
+
...["flac", "mp3", "wav", "ogg", "opus", "m4a", "aac"].map(
|
|
140
|
+
(extension) => [extension, "audio"] as const,
|
|
141
|
+
),
|
|
142
|
+
...["glb", "gltf", "obj", "fbx", "stl", "ply", "splat", "spz", "usdz"].map(
|
|
143
|
+
(extension) => [extension, "3d"] as const,
|
|
144
|
+
),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
const KEYS: ReadonlyMap<string, FileKind> = new Map([
|
|
148
|
+
["images", "image"],
|
|
149
|
+
["audio", "audio"],
|
|
150
|
+
["3d", "3d"],
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
/** The server's own `metadata.kind` first, then the extension, then the key. */
|
|
154
|
+
function kindOf(
|
|
155
|
+
key: string,
|
|
156
|
+
filename: string,
|
|
157
|
+
metadataKind: JsonValue | undefined,
|
|
158
|
+
animated: boolean,
|
|
159
|
+
): FileKind {
|
|
160
|
+
if (metadataKind === "image" || metadataKind === "video" || metadataKind === "audio") {
|
|
161
|
+
return metadataKind;
|
|
162
|
+
}
|
|
163
|
+
const dot = filename.lastIndexOf(".");
|
|
164
|
+
const extension = dot === -1 ? "" : filename.slice(dot + 1).toLowerCase();
|
|
165
|
+
const byExtension = EXTENSIONS.get(extension);
|
|
166
|
+
if (byExtension !== undefined) return byExtension;
|
|
167
|
+
const byKey = KEYS.get(key);
|
|
168
|
+
// An animated entry with an unknown extension under `images` is a video (core's video nodes).
|
|
169
|
+
if (byKey === "image" && animated) return "video";
|
|
170
|
+
return byKey ?? "file";
|
|
171
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createDiagnostic, type Diagnostic, type JsonValue } from "@tscomfy/core";
|
|
2
|
+
import { isList, isObject } from "./http.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The diagnostics of the server's answer to a prompt: its top-level `error` (only on a
|
|
6
|
+
* rejection) as `E_PROMPT_REJECTED`, then each node error as `E_SERVER_VALIDATION`, by node id.
|
|
7
|
+
*/
|
|
8
|
+
export function serverDiagnostics(
|
|
9
|
+
error: JsonValue | undefined,
|
|
10
|
+
nodeErrors: JsonValue | undefined,
|
|
11
|
+
): readonly Diagnostic[] {
|
|
12
|
+
const diagnostics: Diagnostic[] = [];
|
|
13
|
+
if (error !== undefined) diagnostics.push(rejection(error));
|
|
14
|
+
if (isObject(nodeErrors)) {
|
|
15
|
+
for (const [node, entry] of Object.entries(nodeErrors)) {
|
|
16
|
+
const listed = isObject(entry) ? entry["errors"] : undefined;
|
|
17
|
+
const errors = isList(listed) ? listed : [entry];
|
|
18
|
+
for (const each of errors) {
|
|
19
|
+
const inputName = isObject(each) ? inputOf(each["extra_info"]) : undefined;
|
|
20
|
+
diagnostics.push(
|
|
21
|
+
createDiagnostic(
|
|
22
|
+
"E_SERVER_VALIDATION",
|
|
23
|
+
inputName === undefined ? [node] : [node, "inputs", inputName],
|
|
24
|
+
describe(each),
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze(diagnostics);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The top-level error: at the node it names (an unknown class), else at the prompt. */
|
|
34
|
+
function rejection(error: JsonValue): Diagnostic {
|
|
35
|
+
const extra = isObject(error) && isObject(error["extra_info"]) ? error["extra_info"] : {};
|
|
36
|
+
const node = extra["node_id"];
|
|
37
|
+
return createDiagnostic(
|
|
38
|
+
"E_PROMPT_REJECTED",
|
|
39
|
+
typeof node === "string" && node !== "" ? [node] : [],
|
|
40
|
+
describe(error),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function inputOf(extra: JsonValue | undefined): string | undefined {
|
|
45
|
+
const name = isObject(extra) ? extra["input_name"] : undefined;
|
|
46
|
+
return typeof name === "string" && name !== "" ? name : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `type: message: details`, leaving out the parts the server didn't send. */
|
|
50
|
+
function describe(error: JsonValue): string {
|
|
51
|
+
if (typeof error === "string") return error;
|
|
52
|
+
if (!isObject(error)) return "The server reported an error it didn't describe.";
|
|
53
|
+
const parts = [error["type"], error["message"], error["details"]].filter(
|
|
54
|
+
(part): part is string => typeof part === "string" && part !== "",
|
|
55
|
+
);
|
|
56
|
+
// The server repeats the message as the details for some errors.
|
|
57
|
+
const unique = parts.filter((part, index) => parts.indexOf(part) === index);
|
|
58
|
+
return unique.length === 0
|
|
59
|
+
? "The server reported an error it didn't describe."
|
|
60
|
+
: unique.join(": ");
|
|
61
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A ComfyUI server: where it is and how to talk to it. Plain data; every function that needs no
|
|
3
|
+
* websocket takes one, and a {@link Connection} is one too.
|
|
4
|
+
*/
|
|
5
|
+
export interface Server {
|
|
6
|
+
/**
|
|
7
|
+
* The server's http(s) URL. It may have a path prefix and a query (a token, say), which every
|
|
8
|
+
* request and the websocket URL keep: `http://host/comfy/?token=t` gives
|
|
9
|
+
* `http://host/comfy/prompt?token=t`. Credentials (`user:password@`) are refused, because
|
|
10
|
+
* runtimes disagree on them; send them in `headers`.
|
|
11
|
+
*/
|
|
12
|
+
readonly url: string;
|
|
13
|
+
/**
|
|
14
|
+
* Headers for every HTTP request, such as `Authorization`. Not sent on the websocket
|
|
15
|
+
* handshake, which browsers can't give headers; see {@link Server.webSocket}.
|
|
16
|
+
*/
|
|
17
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
18
|
+
/**
|
|
19
|
+
* Replaces the global `fetch`: the global itself, or a function wrapping it (to add a proxy, a
|
|
20
|
+
* cookie, logging). It is called with a URL and an init.
|
|
21
|
+
*/
|
|
22
|
+
readonly fetch?: (url: URL, init: RequestInit) => Promise<Response>;
|
|
23
|
+
/**
|
|
24
|
+
* Opens the websocket instead of `new WebSocket(url)`. Bun, Node and Deno accept handshake
|
|
25
|
+
* headers this way: `(url) => new WebSocket(url, { headers })` (not standard, so not in the
|
|
26
|
+
* types; browsers throw).
|
|
27
|
+
*/
|
|
28
|
+
readonly webSocket?: (url: string) => WebSocket;
|
|
29
|
+
}
|
package/src/timers.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waits `ms` milliseconds, or less if `signal` aborts; never rejects. Its timer goes with it, so
|
|
3
|
+
* nothing outlives an abort and keeps a process running.
|
|
4
|
+
*/
|
|
5
|
+
export function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
6
|
+
if (signal.aborted) return Promise.resolve();
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const done = (): void => {
|
|
9
|
+
clearTimeout(timer);
|
|
10
|
+
signal.removeEventListener("abort", done);
|
|
11
|
+
resolve();
|
|
12
|
+
};
|
|
13
|
+
const timer = setTimeout(done, ms);
|
|
14
|
+
signal.addEventListener("abort", done, { once: true });
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `promise`, or the signal's reason as soon as it aborts. The promise keeps running either way
|
|
20
|
+
* and is marked handled, so a rejection after the abort goes unreported.
|
|
21
|
+
*/
|
|
22
|
+
export function abortable<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
|
|
23
|
+
if (signal === undefined) return promise;
|
|
24
|
+
promise.catch(() => undefined);
|
|
25
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const abort = (): void => {
|
|
28
|
+
reject(signal.reason);
|
|
29
|
+
};
|
|
30
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
31
|
+
promise.then(resolve, reject).finally(() => {
|
|
32
|
+
signal.removeEventListener("abort", abort);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
package/src/url.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Server } from "./server.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The server's base URL, checked. Throws a `TypeError` for a URL that isn't http(s), has
|
|
5
|
+
* credentials or a fragment. The URL isn't echoed: it may hold a token.
|
|
6
|
+
*/
|
|
7
|
+
export function baseUrl(server: Server): URL {
|
|
8
|
+
if (typeof server.url !== "string" || !URL.canParse(server.url)) {
|
|
9
|
+
throw new TypeError("server.url is not a valid URL");
|
|
10
|
+
}
|
|
11
|
+
const url = new URL(server.url);
|
|
12
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
13
|
+
throw new TypeError("server.url must be an http(s) URL");
|
|
14
|
+
}
|
|
15
|
+
if (url.username !== "" || url.password !== "") {
|
|
16
|
+
throw new TypeError("server.url can't hold credentials; send them in server.headers");
|
|
17
|
+
}
|
|
18
|
+
if (url.hash !== "") {
|
|
19
|
+
throw new TypeError("server.url can't have a #fragment: it never reaches the server");
|
|
20
|
+
}
|
|
21
|
+
return url;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** An endpoint under the server's path, keeping its query: `http://h/api/?t=1` → `…/api/x?t=1`. */
|
|
25
|
+
export function endpoint(base: URL, path: string, query?: Readonly<Record<string, string>>): URL {
|
|
26
|
+
const url = new URL(base.href);
|
|
27
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/${path}`;
|
|
28
|
+
for (const [key, value] of Object.entries(query ?? {})) url.searchParams.set(key, value);
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A URL fit for messages: without query or fragment, which is where servers expect tokens.
|
|
34
|
+
* `http://host/api?token=t` → `http://host/api`.
|
|
35
|
+
*/
|
|
36
|
+
export function shownUrl(url: URL): string {
|
|
37
|
+
const shown = new URL(url.href);
|
|
38
|
+
shown.search = "";
|
|
39
|
+
shown.hash = "";
|
|
40
|
+
return shown.href;
|
|
41
|
+
}
|
package/src/uuid.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A random UUID (version 4) in the canonical lowercase form ComfyUI requires of a prompt id.
|
|
3
|
+
* Built from `crypto.getRandomValues`, which, unlike `crypto.randomUUID`, browsers also offer on
|
|
4
|
+
* pages that aren't a secure context (`http://192.168.…`).
|
|
5
|
+
*/
|
|
6
|
+
export function randomUuid(): string {
|
|
7
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
8
|
+
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
|
|
9
|
+
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
|
10
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
11
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
12
|
+
}
|