connectbase-client 3.0.1 → 3.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/dist/index.mjs CHANGED
@@ -7632,6 +7632,176 @@ var AIAPI = class {
7632
7632
  }
7633
7633
  };
7634
7634
 
7635
+ // src/api/endpoint.ts
7636
+ var EndpointAPI = class {
7637
+ constructor(http) {
7638
+ this.http = http;
7639
+ }
7640
+ /**
7641
+ * 라벨 + path 로 사용자 PC 모델 호출. fetch() 시그니처 호환.
7642
+ *
7643
+ * 동작:
7644
+ * - URL 조립: `${baseUrl}/v1/proxy/${label}${path}`
7645
+ * - X-Public-Key 헤더 자동 주입 (호출자가 명시하면 그 값 우선)
7646
+ * - body / method / 추가 헤더 / signal 그대로 전달
7647
+ * - 응답 그대로 반환 (Response 객체) — 스트리밍은 res.body 로 read
7648
+ *
7649
+ * @param label - 콘솔에서 등록한 endpoint 라벨 (예: "comfyui-main")
7650
+ * @param init - fetch() 의 RequestInit + path. path 는 사용자 모델 서버의 엔드포인트 경로 (예: "/prompt", "/v1/chat/completions").
7651
+ */
7652
+ async call(label, init) {
7653
+ if (!label) {
7654
+ throw new Error("EndpointAPI.call: label required");
7655
+ }
7656
+ if (!init.path || !init.path.startsWith("/")) {
7657
+ throw new Error(
7658
+ `EndpointAPI.call: path must start with '/', got ${JSON.stringify(init.path)}`
7659
+ );
7660
+ }
7661
+ const url = this.url(label, init.path);
7662
+ const headers = new Headers(init.headers ?? {});
7663
+ if (!headers.has("X-Public-Key")) {
7664
+ const pk = this.http.getPublicKey();
7665
+ if (!pk) {
7666
+ throw new Error(
7667
+ "EndpointAPI.call: publicKey not configured. Pass `publicKey` to ConnectBase constructor."
7668
+ );
7669
+ }
7670
+ headers.set("X-Public-Key", pk);
7671
+ }
7672
+ return fetch(url, {
7673
+ method: init.method ?? "GET",
7674
+ headers,
7675
+ body: init.body,
7676
+ signal: init.signal,
7677
+ // streaming 응답 (SSE / chunked) 을 바로 reader 로 받기 위해 'manual' redirect 회피.
7678
+ redirect: "follow"
7679
+ });
7680
+ }
7681
+ /**
7682
+ * 라벨 + path 의 최종 호출 URL 을 조립해서 반환. fetch / WebSocket / new Image()
7683
+ * 같이 SDK 의 `call()` 이 아닌 직접 호출이 필요할 때 사용.
7684
+ *
7685
+ * 주의: WebSocket 같이 `X-Public-Key` 헤더를 못 보내는 경로면, 모델 서버가
7686
+ * 자체 토큰으로 별도 인증을 해야 합니다 (ConnectBase 는 인증 게이트가 아닌
7687
+ * dumb pipe 라 path 만으로 인증하지 않음).
7688
+ *
7689
+ * @example
7690
+ * ```typescript
7691
+ * // 결과 이미지를 <img> 로 바로 그리기
7692
+ * img.src = `${cb.endpoint.url("comfyui-main", `/view?filename=${name}`)}` +
7693
+ * `&__pk=${publicKey}` // 모델 서버가 직접 검증하는 토큰
7694
+ * ```
7695
+ */
7696
+ url(label, path) {
7697
+ if (!label) throw new Error("EndpointAPI.url: label required");
7698
+ if (!path || !path.startsWith("/")) {
7699
+ throw new Error(
7700
+ `EndpointAPI.url: path must start with '/', got ${JSON.stringify(path)}`
7701
+ );
7702
+ }
7703
+ const baseUrl = this.http.getBaseUrl().replace(/\/+$/, "");
7704
+ return `${baseUrl}/v1/proxy/${encodeURIComponent(label)}${path}`;
7705
+ }
7706
+ /**
7707
+ * 같은 endpoint 호출을 `predicate` 가 값을 반환할 때까지 주기적으로 반복.
7708
+ * ComfyUI `/history/{id}`, A1111 `/sdapi/v1/progress`, 자체 큐 API 처럼
7709
+ * "작업 제출 → 폴링" 패턴을 한 줄로 처리하기 위한 헬퍼.
7710
+ *
7711
+ * 동작:
7712
+ * - `call(label, init)` → predicate(response) 호출
7713
+ * - predicate 가 `undefined` 면 `intervalMs` 만큼 대기 후 재시도
7714
+ * - predicate 가 값을 반환하면 그 값을 즉시 resolve
7715
+ * - `timeoutMs` 초과 또는 `signal` abort 시 reject
7716
+ * - HTTP 5xx/네트워크 오류는 재시도, 4xx 는 즉시 reject (작업 자체가 잘못된 경우)
7717
+ *
7718
+ * predicate 는 같은 Response 를 한 번만 읽을 수 있으므로, 헬퍼 내부에서
7719
+ * `res.clone().json()` 형태로 안전하게 파싱한 뒤 호출자에게 전달.
7720
+ *
7721
+ * @example ComfyUI 결과 폴링
7722
+ * ```typescript
7723
+ * type Hist = Record<string, { outputs: Record<string, { images?: { filename: string }[] }> }>
7724
+ *
7725
+ * const filename = await cb.endpoint.pollUntil<string>(
7726
+ * "comfyui-main",
7727
+ * { path: `/history/${promptId}` },
7728
+ * (data: Hist) => {
7729
+ * const entry = data[promptId]
7730
+ * if (!entry) return undefined // 아직 큐에 있음
7731
+ * for (const out of Object.values(entry.outputs)) {
7732
+ * const img = out.images?.[0]
7733
+ * if (img) return img.filename
7734
+ * }
7735
+ * return undefined
7736
+ * },
7737
+ * { intervalMs: 1000, timeoutMs: 5 * 60_000 },
7738
+ * )
7739
+ * ```
7740
+ */
7741
+ async pollUntil(label, init, predicate, opts = {}) {
7742
+ const intervalMs = opts.intervalMs ?? 1500;
7743
+ const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
7744
+ const parser = opts.parse ?? "json";
7745
+ const start = Date.now();
7746
+ while (true) {
7747
+ if (opts.signal?.aborted) {
7748
+ throw new DOMException("aborted", "AbortError");
7749
+ }
7750
+ if (Date.now() - start > timeoutMs) {
7751
+ throw new Error(
7752
+ `EndpointAPI.pollUntil: timeout after ${timeoutMs}ms (label=${label}, path=${init.path})`
7753
+ );
7754
+ }
7755
+ let res;
7756
+ try {
7757
+ res = await this.call(label, { ...init, signal: opts.signal });
7758
+ } catch (err) {
7759
+ if (err.name === "AbortError") throw err;
7760
+ await sleep(intervalMs, opts.signal);
7761
+ continue;
7762
+ }
7763
+ if (res.status >= 500) {
7764
+ await drainBody(res);
7765
+ await sleep(intervalMs, opts.signal);
7766
+ continue;
7767
+ }
7768
+ if (res.status >= 400) {
7769
+ const text = await res.text().catch(() => "");
7770
+ throw new Error(
7771
+ `EndpointAPI.pollUntil: ${res.status} ${res.statusText} (label=${label}, path=${init.path})${text ? ` \u2014 ${text.slice(0, 200)}` : ""}`
7772
+ );
7773
+ }
7774
+ let body;
7775
+ if (parser === "text") body = await res.text();
7776
+ else if (parser === "none") body = void 0;
7777
+ else body = await res.json().catch(() => void 0);
7778
+ const result = await predicate(body, res);
7779
+ if (result !== void 0) return result;
7780
+ await sleep(intervalMs, opts.signal);
7781
+ }
7782
+ }
7783
+ };
7784
+ async function sleep(ms, signal) {
7785
+ if (signal?.aborted) throw new DOMException("aborted", "AbortError");
7786
+ return new Promise((resolve, reject) => {
7787
+ const t = setTimeout(() => {
7788
+ signal?.removeEventListener("abort", onAbort);
7789
+ resolve();
7790
+ }, ms);
7791
+ const onAbort = () => {
7792
+ clearTimeout(t);
7793
+ reject(new DOMException("aborted", "AbortError"));
7794
+ };
7795
+ signal?.addEventListener("abort", onAbort, { once: true });
7796
+ });
7797
+ }
7798
+ async function drainBody(res) {
7799
+ try {
7800
+ await res.text();
7801
+ } catch {
7802
+ }
7803
+ }
7804
+
7635
7805
  // src/api/queue.ts
7636
7806
  var QueueAPI = class {
7637
7807
  constructor(http) {
@@ -9170,6 +9340,7 @@ var ConnectBase = class {
9170
9340
  this.ai = new AIAPI(this.http);
9171
9341
  this.queue = new QueueAPI(this.http);
9172
9342
  this.analytics = new AnalyticsAPI(this.http);
9343
+ this.endpoint = new EndpointAPI(this.http);
9173
9344
  this.auth._attachAnalytics(this.analytics);
9174
9345
  }
9175
9346
  /**
@@ -9198,6 +9369,7 @@ export {
9198
9369
  ApiError,
9199
9370
  AuthError,
9200
9371
  ConnectBase,
9372
+ EndpointAPI,
9201
9373
  GameAPI,
9202
9374
  GameRoom,
9203
9375
  GameRoomTransport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "3.0.1",
3
+ "version": "3.2.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",