connectbase-client 6.3.4 → 6.5.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/CHANGELOG.md CHANGED
@@ -3,6 +3,73 @@
3
3
  본 SDK 의 모든 주요 변경사항을 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) 형식으로 기록합니다.
4
4
  버전은 [Semantic Versioning](https://semver.org/lang/ko/) 을 따릅니다.
5
5
 
6
+ ## [6.5.0] - 2026-10-21
7
+
8
+ ### Added — 실시간 연결 거부 사유: `RealtimeConnectionError`
9
+
10
+ 서버가 실시간 연결을 받지 않으면 `cb.realtime.connect()` 가 이유를 담은 `RealtimeConnectionError` 를 던집니다.
11
+ `code`(`connection_limit_exceeded`, `quota_exceeded`, `server_at_capacity`, `auth_unavailable`, `unauthorized`,
12
+ `forbidden`, `invalid_user_id`, `internal_error`), `retryable`, `closeCode`(4000 + HTTP 상태), `details`
13
+ (예: 동시접속 초과의 `current`/`limit`)가 들어 있습니다.
14
+
15
+ 브라우저의 WebSocket API 는 실패한 핸드셰이크의 HTTP 상태를 읽지 못해, 지금까지는 동시접속 초과도 인증 실패도
16
+ "Connection timeout"/"Failed to connect" 로만 보였습니다. SDK 가 `reject_mode=close` 로 협상하면 서버가 연결을
17
+ 받은 뒤 이유를 보내고 4xxx 로 닫습니다(서버가 구버전이면 예전과 같게 동작합니다).
18
+
19
+ ```ts
20
+ try {
21
+ await cb.realtime.connect()
22
+ } catch (e) {
23
+ if (e instanceof RealtimeConnectionError && e.code === 'connection_limit_exceeded') {
24
+ showFull(e.details?.limit)
25
+ }
26
+ }
27
+ ```
28
+
29
+ ### Changed — 서버가 거부한 연결은 SSE 폴백으로 넘어가지 않습니다
30
+
31
+ 예전에는 거부도 네트워크 실패처럼 처리돼 SSE degraded 모드로 넘어갔고, `connect()` 는 성공했는데 `subscribe`/
32
+ `send` 가 나중에 이유 없이 실패했습니다. 이제 거부는 위 에러로 던집니다. 네트워크/프록시가 WebSocket 을 막는
33
+ 경우(사유 없는 실패)는 종전처럼 SSE 로 넘어갑니다.
34
+
35
+ 자동 재연결 중에 거부되면 `retryable` 이면 지수 백오프로 계속 시도하고, 아니면(`unauthorized`, `quota_exceeded`
36
+ 등) 재연결을 멈추고 `onError` 로 같은 에러를 보냅니다.
37
+
38
+ ### Changed — 첫 연결 전에 저지연 원본을 묻습니다
39
+
40
+ realtime 만 쓰는 페이지는 WS 를 열기 전에 REST 를 부르지 않아 direct 원본(`ws-direct`)을 배우지 못했고,
41
+ 첫 연결이 늘 Cloudflare 를 거쳐 한국 사용자는 미국(LAX)을 도는 ~270ms 로 붙었습니다. 이제 direct 원본을 아직
42
+ 모르면 `connect()` 가 `GET /v1/public/edge-hints` 를 한 번(최대 1.5초) 묻고 연결합니다. 해외 사용자와 킬 스위치가
43
+ 켜진 경우엔 빈 응답이라 원래 소켓 주소로 갑니다. 세션당 한 번만 묻습니다.
44
+
45
+ ### Fixed — 재연결 시도가 한 번이라도 실패하면 구독이 복구되지 않던 문제
46
+
47
+ 연결이 끊긴 뒤 첫 재연결 시도가 실패하면(서버 롤링 재시작 등) 다음 시도에서 연결에 성공해도 카테고리 구독,
48
+ 프레전스 구독, 타이핑 핸들러가 하나도 복구되지 않았습니다. 실패한 시도마다 이미 비워진 구독 맵으로 복구
49
+ 스냅샷을 덮었기 때문입니다. 이제 끊긴 순간의 스냅샷을 재연결에 성공할 때까지 유지합니다.
50
+
51
+ ### Fixed — 재연결 대기 중 `disconnect()` 해도 다시 연결되던 문제
52
+
53
+ 백오프 대기 중에 `disconnect()` 를 부르면 예약된 재연결이 그대로 실행돼 다시 연결됐습니다. 이제 대기가 끝났을 때
54
+ 상태가 재연결 중이 아니면 연결하지 않습니다.
55
+
56
+ ## [6.4.0] - 2026-10-01
57
+
58
+ ### Added — 웹 배포에 브라우저 ML 모델 확장자 허용 (`.onnx` `.tflite` `.task` `.safetensors`)
59
+
60
+ onnxruntime-web / TensorFlow Lite / MediaPipe Tasks / transformers.js 가 fetch 로 읽는 모델 파일을
61
+ `deploy` 가 허용목록에 없다는 이유로 빠뜨렸습니다. 배포는 성공으로 끝나는데 사이트에서는 404 였습니다.
62
+ 이제 네 확장자 모두 바이너리로 수집되며, 웹 스토리지 파일당 한도(약 6 MB)를 넘는 모델은 다른 대용량
63
+ 바이너리와 똑같이 presigned URL 로 오브젝트 스토리지에 직접 올라갑니다. `.bin` 으로 이름을 바꾸는
64
+ 우회는 더 이상 필요 없습니다.
65
+
66
+ ### Changed — 허용되지 않는 파일이 있으면 배포 때 알려줍니다
67
+
68
+ 허용되지 않는 확장자의 파일은 여전히 배포에서 제외되지만, 이제 조용히 넘어가지 않습니다. 파일 수집
69
+ 직후 제외된 파일 목록(앞 10개 + 외 N개), 확장자별 개수, 허용 확장자 목록을 경고로 보여주고, 배포 성공
70
+ 메시지 뒤에도 "N개는 포함되지 않았습니다" 를 한 번 더 찍습니다. 숨김 파일/폴더(`.env`, `.git/` 등)는
71
+ 의도적 제외라 목록에 넣지 않습니다. 배포 결과(종료 코드)는 바뀌지 않습니다.
72
+
6
73
  ## [6.3.4] - 2026-09-08
7
74
 
8
75
  ### Fixed — deploy 스크립트가 `npx` 로 CLI 를 부르면 6.3.3 의 "스크립트 안" 판정이 빠지던 문제
package/README.md CHANGED
@@ -294,7 +294,9 @@ In CI, read the Secret Key from a repository secret:
294
294
  ### Requirements
295
295
 
296
296
  - `index.html` must exist in the root of the deploy directory
297
- - Supported file types: `.html`, `.css`, `.js`, `.json`, `.svg`, `.png`, `.jpg`, `.gif`, `.webp`, `.woff`, `.woff2`, `.ttf`, `.mp3`, `.mp4`, `.pdf`, `.wasm`, etc.
297
+ - Supported file types: `.html`, `.css`, `.js`, `.json`, `.svg`, `.png`, `.jpg`, `.gif`, `.webp`, `.woff`, `.woff2`, `.ttf`, `.mp3`, `.mp4`, `.pdf`, `.wasm`, 3D assets (`.glb`, `.gltf`, `.bin`), browser ML models (`.onnx`, `.tflite`, `.task`, `.safetensors`), etc.
298
+ - Files with other extensions are skipped, and `deploy` prints the skipped list as a warning. Hidden files and folders (`.env`, `.git/`) are skipped silently.
299
+ - Binary files over the per-file limit (~6 MB) are uploaded directly to object storage via presigned URLs.
298
300
 
299
301
  ### SPA Mode
300
302
 
@@ -1004,6 +1006,29 @@ cb.realtime.transport // 'ws' | 'sse' | null
1004
1006
  > **v5 → v6 BREAKING**: v5 까지 `isConnected()` 는 SSE 폴백에서도 true 였습니다.
1005
1007
  > 스트리밍 게이팅에 쓰고 있었다면 `isStreamReady()` 로 바꾸세요.
1006
1008
 
1009
+ #### Connection rejected (v6.5.0)
1010
+
1011
+ 서버가 연결을 받지 않으면(동시접속 한도, 할당량, 인증 실패 등) `connect()` 가 `RealtimeConnectionError` 를
1012
+ 던집니다. 이 경우는 SSE 폴백으로 넘어가지 않습니다 — 네트워크는 정상이고 서버가 거부한 것이기 때문입니다.
1013
+
1014
+ ```typescript
1015
+ import { RealtimeConnectionError } from 'connectbase-client'
1016
+
1017
+ try {
1018
+ await cb.realtime.connect()
1019
+ } catch (e) {
1020
+ if (e instanceof RealtimeConnectionError) {
1021
+ e.code // 'connection_limit_exceeded' | 'quota_exceeded' | 'server_at_capacity' | 'unauthorized' | ...
1022
+ e.retryable // 나중에 다시 연결하면 성공할 수 있는지
1023
+ e.details // 예: { current: 100, limit: 100 }
1024
+ }
1025
+ }
1026
+ ```
1027
+
1028
+ 자동 재연결 중의 거부는 `retryable` 이면 백오프로 계속 시도하고, 아니면 재연결을 멈추고 `onError` 로 같은 에러를
1029
+ 보냅니다. 서버 코드(함수)에서 realtime 을 쓴다면 끝날 때 `cb.realtime.disconnect()` 를 부르세요 — 함수 컨테이너는
1030
+ 호출 사이에 재사용되므로 닫지 않은 소켓이 호출마다 쌓입니다(`unsubscribe()` 는 소켓을 닫지 않습니다).
1031
+
1007
1032
  #### Presence / Typing
1008
1033
 
1009
1034
  Presence(온라인 상태) 와 typing(입력 중 표시) 은 `cb.realtime.*` 가 단일 SoT 입니다.
package/dist/cli.js CHANGED
@@ -35,8 +35,10 @@ __export(cli_exports, {
35
35
  ancestorCommandLines: () => ancestorCommandLines,
36
36
  apiErrorMessage: () => apiErrorMessage,
37
37
  buildFirstHint: () => buildFirstHint,
38
+ collectDeployFiles: () => collectDeployFiles,
38
39
  computeDeployDiff: () => computeDeployDiff,
39
40
  detectPackageManager: () => detectPackageManager,
41
+ formatSkippedFilesReport: () => formatSkippedFilesReport,
40
42
  getTunnelConnectCandidates: () => getTunnelConnectCandidates,
41
43
  guessMimeType: () => guessMimeType,
42
44
  isAllowedDeployExt: () => isAllowedDeployExt,
@@ -154,7 +156,7 @@ function handleTunnelError(msg, appId, localPort) {
154
156
  }
155
157
 
156
158
  // src/cli.ts
157
- var VERSION = "6.3.4" ? "6.3.4" : getPackageVersion();
159
+ var VERSION = "6.5.0" ? "6.5.0" : getPackageVersion();
158
160
  var DEFAULT_BASE_URL = "https://api.connectbase.world";
159
161
  var colors = {
160
162
  reset: "\x1B[0m",
@@ -215,7 +217,17 @@ var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
215
217
  // 3D 에셋: .glb/.bin 은 바이너리, .gltf 는 JSON(텍스트)
216
218
  ".glb",
217
219
  ".gltf",
218
- ".bin"
220
+ ".bin",
221
+ // 브라우저 추론용 ML 모델 (platform-issue 01a0f660). 전부 바이너리다.
222
+ // .onnx — onnxruntime-web
223
+ // .tflite — TensorFlow Lite (tfjs-tflite 등)
224
+ // .task — MediaPipe Tasks 번들
225
+ // .safetensors — transformers.js 등의 가중치
226
+ // 수 MB~수십 MB 가 흔하므로 BINARY_EXTENSIONS 에도 넣어 대용량 오프로드(presign) 대상으로 만든다.
227
+ ".onnx",
228
+ ".tflite",
229
+ ".task",
230
+ ".safetensors"
219
231
  ]);
220
232
  var ALLOWED_EXTENSIONLESS_FILES = /* @__PURE__ */ new Set(["_redirects", "_headers"]);
221
233
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
@@ -240,7 +252,11 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
240
252
  ".zip",
241
253
  ".wasm",
242
254
  ".glb",
243
- ".bin"
255
+ ".bin",
256
+ ".onnx",
257
+ ".tflite",
258
+ ".task",
259
+ ".safetensors"
244
260
  ]);
245
261
  var MIME_BY_EXT = {
246
262
  ".png": "image/png",
@@ -266,7 +282,13 @@ var MIME_BY_EXT = {
266
282
  ".wasm": "application/wasm",
267
283
  ".glb": "model/gltf-binary",
268
284
  ".gltf": "model/gltf+json",
269
- ".bin": "application/octet-stream"
285
+ ".bin": "application/octet-stream",
286
+ // ML 모델은 등록된 표준 MIME 이 없다. 로더(onnxruntime-web/MediaPipe/transformers.js)는
287
+ // fetch → ArrayBuffer 로 읽으므로 octet-stream 이면 충분하다.
288
+ ".onnx": "application/octet-stream",
289
+ ".tflite": "application/octet-stream",
290
+ ".task": "application/octet-stream",
291
+ ".safetensors": "application/octet-stream"
270
292
  };
271
293
  function guessMimeType(filePath) {
272
294
  if (ALLOWED_EXTENSIONLESS_FILES.has(path2.basename(filePath))) {
@@ -349,7 +371,13 @@ function loadConfig() {
349
371
  }
350
372
  return config;
351
373
  }
352
- function collectFiles(dir, baseDir = dir) {
374
+ function collectDeployFiles(dir) {
375
+ const skipped = [];
376
+ const files = collectFiles(dir, dir, skipped);
377
+ skipped.sort();
378
+ return { files, skipped };
379
+ }
380
+ function collectFiles(dir, baseDir, skipped) {
353
381
  const files = [];
354
382
  const entries = fs2.readdirSync(dir, { withFileTypes: true });
355
383
  for (const entry of entries) {
@@ -357,14 +385,15 @@ function collectFiles(dir, baseDir = dir) {
357
385
  const relativePath = path2.relative(baseDir, fullPath);
358
386
  if (entry.isDirectory()) {
359
387
  if (!entry.name.startsWith(".")) {
360
- files.push(...collectFiles(fullPath, baseDir));
388
+ files.push(...collectFiles(fullPath, baseDir, skipped));
361
389
  }
362
390
  } else if (entry.isFile()) {
363
391
  const ext = path2.extname(entry.name).toLowerCase();
364
- if (!isAllowedDeployExt(entry.name)) {
392
+ if (entry.name.startsWith(".")) {
365
393
  continue;
366
394
  }
367
- if (entry.name.startsWith(".")) {
395
+ if (!isAllowedDeployExt(entry.name)) {
396
+ skipped.push(normalizeRelativePath(relativePath));
368
397
  continue;
369
398
  }
370
399
  const isBinary = BINARY_EXTENSIONS.has(ext);
@@ -385,6 +414,44 @@ function collectFiles(dir, baseDir = dir) {
385
414
  }
386
415
  return files;
387
416
  }
417
+ var SKIPPED_FILES_LIST_LIMIT = 10;
418
+ function formatSkippedFilesReport(skipped, limit = SKIPPED_FILES_LIST_LIMIT) {
419
+ if (skipped.length === 0) return [];
420
+ const lines = [];
421
+ lines.push(
422
+ `\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD655\uC7A5\uC790\uB77C \uBC30\uD3EC\uC5D0\uC11C \uC81C\uC678\uB41C \uD30C\uC77C ${skipped.length}\uAC1C \u2014 \uC0AC\uC774\uD2B8\uC5D0\uC11C 404 \uAC00 \uB429\uB2C8\uB2E4:`
423
+ );
424
+ for (const p of skipped.slice(0, limit)) lines.push(` \u2022 ${p}`);
425
+ if (skipped.length > limit) {
426
+ lines.push(` \u2026 \uC678 ${skipped.length - limit}\uAC1C`);
427
+ }
428
+ const byExt = /* @__PURE__ */ new Map();
429
+ for (const p of skipped) {
430
+ const ext = path2.extname(p).toLowerCase() || "(\uD655\uC7A5\uC790 \uC5C6\uC74C)";
431
+ byExt.set(ext, (byExt.get(ext) ?? 0) + 1);
432
+ }
433
+ const extSummary = [...byExt.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([ext, n]) => `${ext} ${n}\uAC1C`).join(", ");
434
+ lines.push(` \uD655\uC7A5\uC790\uBCC4: ${extSummary}`);
435
+ lines.push(
436
+ ` \uD5C8\uC6A9 \uD655\uC7A5\uC790: ${[...ALLOWED_EXTENSIONS].join(" ")} (+ \uD655\uC7A5\uC790 \uC5C6\uB294 ${[...ALLOWED_EXTENSIONLESS_FILES].join(", ")})`
437
+ );
438
+ lines.push(
439
+ " \uD544\uC694\uD55C \uD30C\uC77C\uC774\uBA74 \uD5C8\uC6A9 \uD655\uC7A5\uC790\uB85C \uBC14\uAFB8\uAC70\uB098, \uD30C\uC77C \uC2A4\uD1A0\uB9AC\uC9C0(cb.storage)\uC5D0 \uC62C\uB824 URL \uB85C \uCC38\uC870\uD558\uC138\uC694."
440
+ );
441
+ return lines;
442
+ }
443
+ function reportSkippedFiles(skipped) {
444
+ const lines = formatSkippedFilesReport(skipped);
445
+ if (lines.length === 0) return;
446
+ warn(`${colors.yellow}${lines[0]}${colors.reset}`);
447
+ for (const line of lines.slice(1)) log(line);
448
+ }
449
+ function remindSkippedFiles(skipped) {
450
+ if (skipped.length === 0) return;
451
+ warn(
452
+ `\uB2E8, \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD655\uC7A5\uC790 \uD30C\uC77C ${skipped.length}\uAC1C\uB294 \uBC30\uD3EC\uC5D0 \uD3EC\uD568\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 (\uC704 \uBAA9\uB85D \uCC38\uACE0).`
453
+ );
454
+ }
388
455
  function formatSize(bytes) {
389
456
  if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
390
457
  return `${(bytes / 1024).toFixed(1)} KB`;
@@ -699,17 +766,19 @@ async function deploy(directory, config, isDev = false, deployOpts = {}) {
699
766
  }
700
767
  info(`\uBC30\uD3EC \uC900\uBE44 \uC911... ${dir}`);
701
768
  process.stdout.write(`${colors.dim}\uD30C\uC77C \uC218\uC9D1 \uC911...${colors.reset}`);
702
- const files = collectFiles(dir);
769
+ const { files, skipped } = collectDeployFiles(dir);
703
770
  process.stdout.write(`\r${colors.dim} \r${colors.reset}`);
704
771
  if (files.length === 0) {
772
+ reportSkippedFiles(skipped);
705
773
  error("\uBC30\uD3EC\uD560 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4");
706
774
  process.exit(1);
707
775
  }
708
776
  const totalSize = files.reduce((sum, f) => sum + f.content.length, 0);
709
777
  const sizeKB = (totalSize / 1024).toFixed(1);
710
778
  log(
711
- `${colors.green}\u2713${colors.reset} ${files.length}\uAC1C \uD30C\uC77C \uBC1C\uACAC (${sizeKB} KB)`
779
+ `${colors.green}\u2713${colors.reset} ${files.length}\uAC1C \uD30C\uC77C \uBC1C\uACAC (${sizeKB} KB)${skipped.length > 0 ? ` ${colors.yellow}\u2014 ${skipped.length}\uAC1C \uC81C\uC678${colors.reset}` : ""}`
712
780
  );
781
+ reportSkippedFiles(skipped);
713
782
  const target = isDev ? "qa" : void 0;
714
783
  const envLabel = isDev ? "QA" : "Prod";
715
784
  if (isDev) {
@@ -736,6 +805,7 @@ async function deploy(directory, config, isDev = false, deployOpts = {}) {
736
805
  timeoutMs,
737
806
  target
738
807
  );
808
+ remindSkippedFiles(skipped);
739
809
  return;
740
810
  }
741
811
  const diff = computeDeployDiff(files, manifest);
@@ -754,6 +824,7 @@ ${colors.cyan}URL: https://${config.storageId}.web.connectbase.world${colors.res
754
824
  `
755
825
  );
756
826
  }
827
+ remindSkippedFiles(skipped);
757
828
  return;
758
829
  }
759
830
  await offloadLargeBinaries(
@@ -777,6 +848,7 @@ ${colors.cyan}URL: https://${config.storageId}.web.connectbase.world${colors.res
777
848
  timeoutMs,
778
849
  target
779
850
  );
851
+ remindSkippedFiles(skipped);
780
852
  } catch (err) {
781
853
  process.stdout.write("\r \r");
782
854
  error(`\uB124\uD2B8\uC6CC\uD06C \uC624\uB958: ${err instanceof Error ? err.message : err}`);
@@ -3880,8 +3952,10 @@ main().catch((err) => {
3880
3952
  ancestorCommandLines,
3881
3953
  apiErrorMessage,
3882
3954
  buildFirstHint,
3955
+ collectDeployFiles,
3883
3956
  computeDeployDiff,
3884
3957
  detectPackageManager,
3958
+ formatSkippedFilesReport,
3885
3959
  getTunnelConnectCandidates,
3886
3960
  guessMimeType,
3887
3961
  isAllowedDeployExt,