connectbase-client 5.12.6 → 6.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/CHANGELOG.md +98 -0
- package/README.md +38 -0
- package/dist/connect-base.umd.js +5 -5
- package/dist/index.d.mts +171 -1
- package/dist/index.d.ts +171 -1
- package/dist/index.js +349 -59
- package/dist/index.mjs +349 -59
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -5306,6 +5306,15 @@ var NativeAPI = class {
|
|
|
5306
5306
|
return window.NativeBridge.clipboard.readImage();
|
|
5307
5307
|
}
|
|
5308
5308
|
return null;
|
|
5309
|
+
},
|
|
5310
|
+
/**
|
|
5311
|
+
* 클립보드에 특정 형식(MIME 타입, 예: 'text/plain', 'image/png')의 데이터가 있는지 확인 (데스크톱 전용)
|
|
5312
|
+
*/
|
|
5313
|
+
has: async (format) => {
|
|
5314
|
+
if (window.NativeBridge?.clipboard?.has) {
|
|
5315
|
+
return window.NativeBridge.clipboard.has(format);
|
|
5316
|
+
}
|
|
5317
|
+
return false;
|
|
5309
5318
|
}
|
|
5310
5319
|
};
|
|
5311
5320
|
/**
|
|
@@ -5344,25 +5353,33 @@ var NativeAPI = class {
|
|
|
5344
5353
|
saveFile: async (content, filename, options) => {
|
|
5345
5354
|
const platform = this.getPlatform();
|
|
5346
5355
|
if (platform === "desktop" && window.NativeBridge?.filesystem) {
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5356
|
+
try {
|
|
5357
|
+
const result = await window.NativeBridge.filesystem.showSaveDialog?.({
|
|
5358
|
+
defaultPath: filename,
|
|
5359
|
+
filters: options?.filters
|
|
5360
|
+
});
|
|
5361
|
+
if (result?.canceled || !result?.filePath) return false;
|
|
5362
|
+
const textContent = content instanceof Blob ? await content.text() : content;
|
|
5363
|
+
const writeResult = await window.NativeBridge.filesystem.writeFile(
|
|
5364
|
+
result.filePath,
|
|
5365
|
+
textContent
|
|
5366
|
+
);
|
|
5367
|
+
return writeResult.success;
|
|
5368
|
+
} catch {
|
|
5369
|
+
return false;
|
|
5370
|
+
}
|
|
5358
5371
|
}
|
|
5359
5372
|
if (platform === "mobile" && window.NativeBridge?.filesystem) {
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5373
|
+
try {
|
|
5374
|
+
const textContent = content instanceof Blob ? await content.text() : content;
|
|
5375
|
+
const result = await window.NativeBridge.filesystem.writeFile(
|
|
5376
|
+
filename,
|
|
5377
|
+
textContent
|
|
5378
|
+
);
|
|
5379
|
+
return result.success;
|
|
5380
|
+
} catch {
|
|
5381
|
+
return false;
|
|
5382
|
+
}
|
|
5366
5383
|
}
|
|
5367
5384
|
const blob = content instanceof Blob ? content : new Blob([content], { type: "text/plain" });
|
|
5368
5385
|
const url = URL.createObjectURL(blob);
|
|
@@ -5378,8 +5395,12 @@ var NativeAPI = class {
|
|
|
5378
5395
|
*/
|
|
5379
5396
|
readFile: async (path) => {
|
|
5380
5397
|
if (window.NativeBridge?.filesystem?.readFile) {
|
|
5381
|
-
|
|
5382
|
-
|
|
5398
|
+
try {
|
|
5399
|
+
const result = await window.NativeBridge.filesystem.readFile(path);
|
|
5400
|
+
return result.success ? result.content ?? null : null;
|
|
5401
|
+
} catch {
|
|
5402
|
+
return null;
|
|
5403
|
+
}
|
|
5383
5404
|
}
|
|
5384
5405
|
return null;
|
|
5385
5406
|
},
|
|
@@ -5403,7 +5424,11 @@ var NativeAPI = class {
|
|
|
5403
5424
|
takePicture: async (options) => {
|
|
5404
5425
|
const platform = this.getPlatform();
|
|
5405
5426
|
if (platform === "mobile" && window.NativeBridge?.camera) {
|
|
5406
|
-
|
|
5427
|
+
try {
|
|
5428
|
+
return await window.NativeBridge.camera.takePicture(options);
|
|
5429
|
+
} catch {
|
|
5430
|
+
return null;
|
|
5431
|
+
}
|
|
5407
5432
|
}
|
|
5408
5433
|
return new Promise((resolve) => {
|
|
5409
5434
|
const input = document.createElement("input");
|
|
@@ -5440,7 +5465,11 @@ var NativeAPI = class {
|
|
|
5440
5465
|
pickImage: async (options) => {
|
|
5441
5466
|
const platform = this.getPlatform();
|
|
5442
5467
|
if (platform === "mobile" && window.NativeBridge?.camera) {
|
|
5443
|
-
|
|
5468
|
+
try {
|
|
5469
|
+
return await window.NativeBridge.camera.pickImage(options);
|
|
5470
|
+
} catch {
|
|
5471
|
+
return null;
|
|
5472
|
+
}
|
|
5444
5473
|
}
|
|
5445
5474
|
return new Promise((resolve) => {
|
|
5446
5475
|
const input = document.createElement("input");
|
|
@@ -5583,7 +5612,11 @@ var NativeAPI = class {
|
|
|
5583
5612
|
*/
|
|
5584
5613
|
getToken: async () => {
|
|
5585
5614
|
if (this.getPlatform() === "mobile" && window.NativeBridge?.push) {
|
|
5586
|
-
|
|
5615
|
+
try {
|
|
5616
|
+
return await window.NativeBridge.push.getToken();
|
|
5617
|
+
} catch {
|
|
5618
|
+
return null;
|
|
5619
|
+
}
|
|
5587
5620
|
}
|
|
5588
5621
|
return null;
|
|
5589
5622
|
},
|
|
@@ -5594,8 +5627,12 @@ var NativeAPI = class {
|
|
|
5594
5627
|
/** 로컬 알림 예약 (패키징 앱 전용) */
|
|
5595
5628
|
scheduleLocal: async (options) => {
|
|
5596
5629
|
if (this.getPlatform() === "mobile" && window.NativeBridge?.push) {
|
|
5597
|
-
|
|
5598
|
-
|
|
5630
|
+
try {
|
|
5631
|
+
const result = await window.NativeBridge.push.scheduleLocal(options);
|
|
5632
|
+
return result.notificationId;
|
|
5633
|
+
} catch {
|
|
5634
|
+
return null;
|
|
5635
|
+
}
|
|
5599
5636
|
}
|
|
5600
5637
|
return null;
|
|
5601
5638
|
},
|
|
@@ -5603,8 +5640,12 @@ var NativeAPI = class {
|
|
|
5603
5640
|
setBadgeCount: async (count) => {
|
|
5604
5641
|
const setter = window.NativeBridge?.push?.setBadgeCount;
|
|
5605
5642
|
if (!setter) return false;
|
|
5606
|
-
|
|
5607
|
-
|
|
5643
|
+
try {
|
|
5644
|
+
const result = await setter(count);
|
|
5645
|
+
return result.success;
|
|
5646
|
+
} catch {
|
|
5647
|
+
return false;
|
|
5648
|
+
}
|
|
5608
5649
|
}
|
|
5609
5650
|
};
|
|
5610
5651
|
/**
|
|
@@ -5623,12 +5664,12 @@ var NativeAPI = class {
|
|
|
5623
5664
|
if (platform === "mobile" && window.NativeBridge?.push?.scheduleLocal) {
|
|
5624
5665
|
const granted = await this.notification.requestPermission();
|
|
5625
5666
|
if (!granted) return false;
|
|
5626
|
-
await
|
|
5667
|
+
const notificationId = await this.push.scheduleLocal({
|
|
5627
5668
|
title: options.title,
|
|
5628
5669
|
body: options.body,
|
|
5629
5670
|
trigger: null
|
|
5630
5671
|
});
|
|
5631
|
-
return
|
|
5672
|
+
return notificationId !== null;
|
|
5632
5673
|
}
|
|
5633
5674
|
if (!("Notification" in window)) return false;
|
|
5634
5675
|
if (Notification.permission !== "granted") {
|
|
@@ -5648,8 +5689,12 @@ var NativeAPI = class {
|
|
|
5648
5689
|
requestPermission: async () => {
|
|
5649
5690
|
const platform = this.getPlatform();
|
|
5650
5691
|
if (platform === "mobile" && window.NativeBridge?.push) {
|
|
5651
|
-
|
|
5652
|
-
|
|
5692
|
+
try {
|
|
5693
|
+
const result = await window.NativeBridge.push.requestPermission();
|
|
5694
|
+
return result.granted;
|
|
5695
|
+
} catch {
|
|
5696
|
+
return false;
|
|
5697
|
+
}
|
|
5653
5698
|
}
|
|
5654
5699
|
if (!("Notification" in window)) return false;
|
|
5655
5700
|
const permission = await Notification.requestPermission();
|
|
@@ -5670,13 +5715,77 @@ var NativeAPI = class {
|
|
|
5670
5715
|
return result.success;
|
|
5671
5716
|
}
|
|
5672
5717
|
if (platform === "mobile" && window.NativeBridge?.browser) {
|
|
5673
|
-
|
|
5674
|
-
|
|
5718
|
+
try {
|
|
5719
|
+
const result = await window.NativeBridge.browser.openExternal(url);
|
|
5720
|
+
return result.success;
|
|
5721
|
+
} catch {
|
|
5722
|
+
return false;
|
|
5723
|
+
}
|
|
5675
5724
|
}
|
|
5676
5725
|
window.open(url, "_blank");
|
|
5677
5726
|
return true;
|
|
5678
5727
|
}
|
|
5679
5728
|
};
|
|
5729
|
+
/**
|
|
5730
|
+
* 모바일 전용: 네이티브 인앱 브라우저 세션으로 여는 OAuth
|
|
5731
|
+
*
|
|
5732
|
+
* WebView 안에서 소셜 로그인 팝업을 열면 세션/쿠키가 격리돼 실패하는 경우가 많다.
|
|
5733
|
+
* 패키징 앱(RN)에서는 시스템 브라우저 세션(iOS ASWebAuthenticationSession /
|
|
5734
|
+
* Android Custom Tabs)으로 열고 앱 URL 스킴 콜백으로 결과를 받는다.
|
|
5735
|
+
* 웹/데스크톱에서는 지원하지 않는다 — 일반 팝업/리다이렉트 OAuth 플로우를 쓸 것.
|
|
5736
|
+
*
|
|
5737
|
+
* @example
|
|
5738
|
+
* ```typescript
|
|
5739
|
+
* if (cb.native.getPlatform() === 'mobile') {
|
|
5740
|
+
* const result = await cb.native.oauth.signIn(authorizeUrl)
|
|
5741
|
+
* if (result) window.location.href = result.url // 콜백 URL 로 계속 진행
|
|
5742
|
+
* }
|
|
5743
|
+
* ```
|
|
5744
|
+
*/
|
|
5745
|
+
this.oauth = {
|
|
5746
|
+
/**
|
|
5747
|
+
* OAuth 인가 URL 을 네이티브 인앱 브라우저 세션으로 열고 콜백 URL 을 기다린다.
|
|
5748
|
+
* callbackScheme 을 생략하면 이 패키징 앱의 스킴(getCallbackScheme())을 사용한다.
|
|
5749
|
+
* 사용자가 취소했거나 미지원 플랫폼이면 null.
|
|
5750
|
+
*/
|
|
5751
|
+
signIn: async (authUrl, callbackScheme) => {
|
|
5752
|
+
const bridge = window.NativeBridge;
|
|
5753
|
+
if (this.getPlatform() !== "mobile" || !bridge?.oauth) return null;
|
|
5754
|
+
const scheme = callbackScheme ?? bridge.oauth.getCallbackScheme();
|
|
5755
|
+
try {
|
|
5756
|
+
return await bridge.oauth.signIn(authUrl, scheme);
|
|
5757
|
+
} catch {
|
|
5758
|
+
return null;
|
|
5759
|
+
}
|
|
5760
|
+
},
|
|
5761
|
+
/** 이 패키징 앱의 OAuth 콜백 URL 스킴(`<scheme>://oauth/callback`). 미지원 플랫폼은 null (동기) */
|
|
5762
|
+
getCallbackScheme: () => {
|
|
5763
|
+
if (this.getPlatform() !== "mobile" || !window.NativeBridge?.oauth) {
|
|
5764
|
+
return null;
|
|
5765
|
+
}
|
|
5766
|
+
return window.NativeBridge.oauth.getCallbackScheme();
|
|
5767
|
+
}
|
|
5768
|
+
};
|
|
5769
|
+
/**
|
|
5770
|
+
* 모바일 전용: 패키징 앱에 연결된 Connect Base Storage 로 파일 업로드.
|
|
5771
|
+
* 콘솔 패키징 설정에서 "파일 저장 위치" 를 Connect Base Storage 로 선택한 경우에만 지원한다.
|
|
5772
|
+
*/
|
|
5773
|
+
this.connectbase = {
|
|
5774
|
+
uploadFile: async (uri, filename, mimeType) => {
|
|
5775
|
+
if (this.getPlatform() !== "mobile" || !window.NativeBridge?.connectbase) {
|
|
5776
|
+
return null;
|
|
5777
|
+
}
|
|
5778
|
+
try {
|
|
5779
|
+
return await window.NativeBridge.connectbase.uploadFile(
|
|
5780
|
+
uri,
|
|
5781
|
+
filename,
|
|
5782
|
+
mimeType
|
|
5783
|
+
);
|
|
5784
|
+
} catch {
|
|
5785
|
+
return null;
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
};
|
|
5680
5789
|
/**
|
|
5681
5790
|
* 데스크톱 전용 창 제어 API
|
|
5682
5791
|
*/
|
|
@@ -5713,6 +5822,36 @@ var NativeAPI = class {
|
|
|
5713
5822
|
await document.exitFullscreen();
|
|
5714
5823
|
}
|
|
5715
5824
|
}
|
|
5825
|
+
},
|
|
5826
|
+
isFullScreen: async () => {
|
|
5827
|
+
if (window.NativeBridge?.window) {
|
|
5828
|
+
return await window.NativeBridge.window.isFullScreen() ?? false;
|
|
5829
|
+
}
|
|
5830
|
+
return !!document.fullscreenElement;
|
|
5831
|
+
},
|
|
5832
|
+
/** 창 크기 설정 (데스크톱 전용) */
|
|
5833
|
+
setSize: async (width, height) => {
|
|
5834
|
+
await window.NativeBridge?.window?.setSize(width, height);
|
|
5835
|
+
},
|
|
5836
|
+
/** 창 크기 조회 (데스크톱 전용, 미지원 플랫폼은 null) */
|
|
5837
|
+
getSize: async () => {
|
|
5838
|
+
return await window.NativeBridge?.window?.getSize() ?? null;
|
|
5839
|
+
},
|
|
5840
|
+
/** 창 위치 설정 (데스크톱 전용) */
|
|
5841
|
+
setPosition: async (x, y) => {
|
|
5842
|
+
await window.NativeBridge?.window?.setPosition(x, y);
|
|
5843
|
+
},
|
|
5844
|
+
/** 창 위치 조회 (데스크톱 전용, 미지원 플랫폼은 null) */
|
|
5845
|
+
getPosition: async () => {
|
|
5846
|
+
return await window.NativeBridge?.window?.getPosition() ?? null;
|
|
5847
|
+
},
|
|
5848
|
+
/** 화면 중앙으로 이동 (데스크톱 전용) */
|
|
5849
|
+
center: async () => {
|
|
5850
|
+
await window.NativeBridge?.window?.center();
|
|
5851
|
+
},
|
|
5852
|
+
/** 항상 위에 표시 설정 (데스크톱 전용) */
|
|
5853
|
+
setAlwaysOnTop: async (flag) => {
|
|
5854
|
+
await window.NativeBridge?.window?.setAlwaysOnTop(flag);
|
|
5716
5855
|
}
|
|
5717
5856
|
};
|
|
5718
5857
|
/**
|
|
@@ -5730,6 +5869,12 @@ var NativeAPI = class {
|
|
|
5730
5869
|
return window.NativeBridge.system.getMemory();
|
|
5731
5870
|
}
|
|
5732
5871
|
return null;
|
|
5872
|
+
},
|
|
5873
|
+
getCPU: async () => {
|
|
5874
|
+
if (window.NativeBridge?.system) {
|
|
5875
|
+
return window.NativeBridge.system.getCPU();
|
|
5876
|
+
}
|
|
5877
|
+
return null;
|
|
5733
5878
|
}
|
|
5734
5879
|
};
|
|
5735
5880
|
/**
|
|
@@ -5738,13 +5883,21 @@ var NativeAPI = class {
|
|
|
5738
5883
|
this.biometric = {
|
|
5739
5884
|
isAvailable: async () => {
|
|
5740
5885
|
if (window.NativeBridge?.biometric) {
|
|
5741
|
-
|
|
5886
|
+
try {
|
|
5887
|
+
return await window.NativeBridge.biometric.isAvailable();
|
|
5888
|
+
} catch {
|
|
5889
|
+
return null;
|
|
5890
|
+
}
|
|
5742
5891
|
}
|
|
5743
5892
|
return null;
|
|
5744
5893
|
},
|
|
5745
5894
|
authenticate: async (options) => {
|
|
5746
5895
|
if (window.NativeBridge?.biometric) {
|
|
5747
|
-
|
|
5896
|
+
try {
|
|
5897
|
+
return await window.NativeBridge.biometric.authenticate(options);
|
|
5898
|
+
} catch {
|
|
5899
|
+
return null;
|
|
5900
|
+
}
|
|
5748
5901
|
}
|
|
5749
5902
|
return null;
|
|
5750
5903
|
}
|
|
@@ -5755,26 +5908,38 @@ var NativeAPI = class {
|
|
|
5755
5908
|
this.secureStore = {
|
|
5756
5909
|
setItem: async (key, value) => {
|
|
5757
5910
|
if (window.NativeBridge?.secureStore) {
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5911
|
+
try {
|
|
5912
|
+
const result = await window.NativeBridge.secureStore.setItem(
|
|
5913
|
+
key,
|
|
5914
|
+
value
|
|
5915
|
+
);
|
|
5916
|
+
return result.success;
|
|
5917
|
+
} catch {
|
|
5918
|
+
return false;
|
|
5919
|
+
}
|
|
5763
5920
|
}
|
|
5764
5921
|
localStorage.setItem(key, value);
|
|
5765
5922
|
return true;
|
|
5766
5923
|
},
|
|
5767
5924
|
getItem: async (key) => {
|
|
5768
5925
|
if (window.NativeBridge?.secureStore) {
|
|
5769
|
-
|
|
5770
|
-
|
|
5926
|
+
try {
|
|
5927
|
+
const result = await window.NativeBridge.secureStore.getItem(key);
|
|
5928
|
+
return result.value;
|
|
5929
|
+
} catch {
|
|
5930
|
+
return null;
|
|
5931
|
+
}
|
|
5771
5932
|
}
|
|
5772
5933
|
return localStorage.getItem(key);
|
|
5773
5934
|
},
|
|
5774
5935
|
deleteItem: async (key) => {
|
|
5775
5936
|
if (window.NativeBridge?.secureStore) {
|
|
5776
|
-
|
|
5777
|
-
|
|
5937
|
+
try {
|
|
5938
|
+
const result = await window.NativeBridge.secureStore.deleteItem(key);
|
|
5939
|
+
return result.success;
|
|
5940
|
+
} catch {
|
|
5941
|
+
return false;
|
|
5942
|
+
}
|
|
5778
5943
|
}
|
|
5779
5944
|
localStorage.removeItem(key);
|
|
5780
5945
|
return true;
|
|
@@ -5786,15 +5951,23 @@ var NativeAPI = class {
|
|
|
5786
5951
|
this.admob = {
|
|
5787
5952
|
showInterstitial: async () => {
|
|
5788
5953
|
if (window.NativeBridge?.admob) {
|
|
5789
|
-
|
|
5790
|
-
|
|
5954
|
+
try {
|
|
5955
|
+
const result = await window.NativeBridge.admob.showInterstitial();
|
|
5956
|
+
return result.shown;
|
|
5957
|
+
} catch {
|
|
5958
|
+
return false;
|
|
5959
|
+
}
|
|
5791
5960
|
}
|
|
5792
5961
|
return false;
|
|
5793
5962
|
},
|
|
5794
5963
|
showRewarded: async () => {
|
|
5795
5964
|
if (window.NativeBridge?.admob) {
|
|
5796
|
-
|
|
5797
|
-
|
|
5965
|
+
try {
|
|
5966
|
+
const result = await window.NativeBridge.admob.showRewarded();
|
|
5967
|
+
return result.rewarded;
|
|
5968
|
+
} catch {
|
|
5969
|
+
return false;
|
|
5970
|
+
}
|
|
5798
5971
|
}
|
|
5799
5972
|
return false;
|
|
5800
5973
|
}
|
|
@@ -7714,9 +7887,35 @@ var RealtimeAPI = class {
|
|
|
7714
7887
|
return this.state;
|
|
7715
7888
|
}
|
|
7716
7889
|
/**
|
|
7717
|
-
*
|
|
7890
|
+
* pub/sub 사용 가능 여부.
|
|
7891
|
+
*
|
|
7892
|
+
* WebSocket 으로 연결됐을 때만 true 다. SSE fallback 모드에서는 `subscribe` /
|
|
7893
|
+
* `sendMessage` / `setPresence` / typing 이 전부 예외를 던지므로, 여기서 true 를
|
|
7894
|
+
* 반환하면 앱이 "연결됨"으로 오판하고 아무것도 오가지 않는 채 조용히 멈춘다
|
|
7895
|
+
* (platform-issue 01a01e6a-3cb1).
|
|
7896
|
+
*
|
|
7897
|
+
* AI 스트리밍만 필요하다면 {@link isStreamReady} 를, 다운그레이드 여부를 알고 싶다면
|
|
7898
|
+
* {@link isDegraded} 를 쓴다.
|
|
7718
7899
|
*/
|
|
7719
7900
|
isConnected() {
|
|
7901
|
+
return this.state === "connected" && this.activeTransport === "ws";
|
|
7902
|
+
}
|
|
7903
|
+
/**
|
|
7904
|
+
* SSE fallback 으로 다운그레이드된 상태인지 여부.
|
|
7905
|
+
*
|
|
7906
|
+
* true 면 연결은 살아 있지만 단방향이라 `realtime.stream()` 만 동작한다. 사용자에게
|
|
7907
|
+
* "제한된 네트워크라 일부 기능을 쓸 수 없다" 고 안내할 지점이다.
|
|
7908
|
+
*/
|
|
7909
|
+
isDegraded() {
|
|
7910
|
+
return this.sseFallbackActive;
|
|
7911
|
+
}
|
|
7912
|
+
/**
|
|
7913
|
+
* AI 스트리밍(`realtime.stream()`) 사용 가능 여부.
|
|
7914
|
+
*
|
|
7915
|
+
* WS 든 SSE fallback 이든 연결만 서 있으면 true 다. 종전 `isConnected()` 와 같은
|
|
7916
|
+
* 판정이므로, 스트리밍 게이팅에 `isConnected()` 를 쓰던 코드는 이쪽으로 옮긴다.
|
|
7917
|
+
*/
|
|
7918
|
+
isStreamReady() {
|
|
7720
7919
|
return this.state === "connected";
|
|
7721
7920
|
}
|
|
7722
7921
|
/**
|
|
@@ -10847,7 +11046,7 @@ var VideoAPI = class {
|
|
|
10847
11046
|
};
|
|
10848
11047
|
|
|
10849
11048
|
// src/api/webrtc.ts
|
|
10850
|
-
var WebRTCAPI = class {
|
|
11049
|
+
var WebRTCAPI = class _WebRTCAPI {
|
|
10851
11050
|
constructor(http, webrtcUrl, appId) {
|
|
10852
11051
|
this.ws = null;
|
|
10853
11052
|
this.state = "disconnected";
|
|
@@ -10859,6 +11058,16 @@ var WebRTCAPI = class {
|
|
|
10859
11058
|
this.reconnectAttempts = 0;
|
|
10860
11059
|
this.maxReconnectAttempts = 5;
|
|
10861
11060
|
this.reconnectTimeout = null;
|
|
11061
|
+
/**
|
|
11062
|
+
* 시그널링 소켓의 세대(generation) 토큰.
|
|
11063
|
+
*
|
|
11064
|
+
* 소켓을 열 때마다 1 증가하며, 각 핸들러는 자기가 열릴 때의 세대를 클로저에 담아둔다.
|
|
11065
|
+
* 세대가 밀린 핸들러는 "옛 소켓의 지연 이벤트" 이므로 인스턴스 상태를 건드리지 않는다.
|
|
11066
|
+
* 참조(`this.ws`)만 끊는 것으로는 부족하다 — 핸들러 클로저가 인스턴스를 캡처하고 있어
|
|
11067
|
+
* close 이벤트가 뒤늦게 도착하면 새 연결을 'disconnected' 로 덮어쓰고 새로 만든 피어
|
|
11068
|
+
* 연결까지 정리해버린다.
|
|
11069
|
+
*/
|
|
11070
|
+
this.wsGeneration = 0;
|
|
10862
11071
|
// 현재 연결 정보
|
|
10863
11072
|
this.currentRoomId = null;
|
|
10864
11073
|
this.currentPeerId = null;
|
|
@@ -10931,15 +11140,23 @@ var WebRTCAPI = class {
|
|
|
10931
11140
|
connectWebSocket() {
|
|
10932
11141
|
return new Promise((resolve, reject) => {
|
|
10933
11142
|
const wsUrl = this.buildWebSocketUrl();
|
|
10934
|
-
|
|
11143
|
+
const generation = ++this.wsGeneration;
|
|
11144
|
+
const ws = new WebSocket(wsUrl);
|
|
11145
|
+
this.ws = ws;
|
|
11146
|
+
const isStale = () => this.wsGeneration !== generation;
|
|
10935
11147
|
const timeout = setTimeout(() => {
|
|
11148
|
+
if (isStale()) return;
|
|
10936
11149
|
if (this.state === "connecting") {
|
|
10937
|
-
|
|
11150
|
+
ws.close();
|
|
10938
11151
|
reject(new Error("\uC5F0\uACB0 \uC2DC\uAC04 \uCD08\uACFC"));
|
|
10939
11152
|
}
|
|
10940
11153
|
}, 1e4);
|
|
10941
|
-
|
|
11154
|
+
ws.onopen = () => {
|
|
10942
11155
|
clearTimeout(timeout);
|
|
11156
|
+
if (isStale()) {
|
|
11157
|
+
ws.close(1e3, "superseded");
|
|
11158
|
+
return;
|
|
11159
|
+
}
|
|
10943
11160
|
this.reconnectAttempts = 0;
|
|
10944
11161
|
this.sendSignaling({
|
|
10945
11162
|
type: "join",
|
|
@@ -10950,7 +11167,8 @@ var WebRTCAPI = class {
|
|
|
10950
11167
|
}
|
|
10951
11168
|
});
|
|
10952
11169
|
};
|
|
10953
|
-
|
|
11170
|
+
ws.onmessage = async (event) => {
|
|
11171
|
+
if (isStale()) return;
|
|
10954
11172
|
try {
|
|
10955
11173
|
const msg = JSON.parse(event.data);
|
|
10956
11174
|
await this.handleSignalingMessage(msg, resolve, reject);
|
|
@@ -10958,13 +11176,15 @@ var WebRTCAPI = class {
|
|
|
10958
11176
|
console.error("Failed to parse signaling message:", error);
|
|
10959
11177
|
}
|
|
10960
11178
|
};
|
|
10961
|
-
|
|
11179
|
+
ws.onerror = (event) => {
|
|
10962
11180
|
clearTimeout(timeout);
|
|
11181
|
+
if (isStale()) return;
|
|
10963
11182
|
console.error("WebSocket error:", event);
|
|
10964
11183
|
this.emitError(new Error("WebSocket \uC5F0\uACB0 \uC624\uB958"));
|
|
10965
11184
|
};
|
|
10966
|
-
|
|
11185
|
+
ws.onclose = (event) => {
|
|
10967
11186
|
clearTimeout(timeout);
|
|
11187
|
+
if (isStale()) return;
|
|
10968
11188
|
if (this.state === "connecting") {
|
|
10969
11189
|
reject(new Error("\uC5F0\uACB0\uC774 \uC885\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4"));
|
|
10970
11190
|
}
|
|
@@ -10972,6 +11192,19 @@ var WebRTCAPI = class {
|
|
|
10972
11192
|
};
|
|
10973
11193
|
});
|
|
10974
11194
|
}
|
|
11195
|
+
/**
|
|
11196
|
+
* 소켓에 붙은 핸들러를 전부 떼어낸다.
|
|
11197
|
+
*
|
|
11198
|
+
* `this.ws = null` 로 참조만 끊으면 클로저가 살아남아 close 이벤트가 계속 들어온다.
|
|
11199
|
+
* 세대 토큰이 그 이벤트를 무시해주긴 하지만, 핸들러 자체를 떼면 인스턴스 참조가 끊겨
|
|
11200
|
+
* 소켓이 GC 될 때까지 인스턴스를 붙잡고 있지 않는다.
|
|
11201
|
+
*/
|
|
11202
|
+
static detachSocket(ws) {
|
|
11203
|
+
ws.onopen = null;
|
|
11204
|
+
ws.onmessage = null;
|
|
11205
|
+
ws.onerror = null;
|
|
11206
|
+
ws.onclose = null;
|
|
11207
|
+
}
|
|
10975
11208
|
buildWebSocketUrl() {
|
|
10976
11209
|
const wsBase = this.webrtcUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
10977
11210
|
const publicKey = this.http.getPublicKey();
|
|
@@ -11182,19 +11415,76 @@ var WebRTCAPI = class {
|
|
|
11182
11415
|
clearTimeout(this.reconnectTimeout);
|
|
11183
11416
|
this.reconnectTimeout = null;
|
|
11184
11417
|
}
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11418
|
+
this.reconnectAttempts = 0;
|
|
11419
|
+
this.wsGeneration++;
|
|
11420
|
+
const ws = this.ws;
|
|
11421
|
+
this.ws = null;
|
|
11422
|
+
if (ws) {
|
|
11423
|
+
_WebRTCAPI.detachSocket(ws);
|
|
11424
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
11425
|
+
try {
|
|
11426
|
+
ws.send(JSON.stringify({ type: "leave" }));
|
|
11427
|
+
} catch {
|
|
11428
|
+
}
|
|
11429
|
+
ws.close(1e3, "User disconnected");
|
|
11430
|
+
} else if (ws.readyState === WebSocket.CONNECTING) {
|
|
11431
|
+
ws.close();
|
|
11432
|
+
}
|
|
11188
11433
|
}
|
|
11189
11434
|
this.peerConnections.forEach((pc) => pc.close());
|
|
11190
11435
|
this.peerConnections.clear();
|
|
11191
11436
|
this.remoteStreams.clear();
|
|
11192
|
-
this.ws = null;
|
|
11193
11437
|
this.currentRoomId = null;
|
|
11194
11438
|
this.currentPeerId = null;
|
|
11195
11439
|
this.localStream = null;
|
|
11196
11440
|
this.setState("disconnected");
|
|
11197
11441
|
}
|
|
11442
|
+
/**
|
|
11443
|
+
* 현재 세션의 룸을 교체한다.
|
|
11444
|
+
*
|
|
11445
|
+
* `disconnect()` 후 곧바로 `connect()` 하는 패턴을 안전하게 감싼 것이다. 옛 소켓의
|
|
11446
|
+
* close 이벤트가 새 연결의 'connecting' 구간에 떨어져도 세대 토큰이 걸러낸다.
|
|
11447
|
+
* 생략한 옵션은 현재 세션 값을 그대로 이어받는다.
|
|
11448
|
+
*
|
|
11449
|
+
* @example
|
|
11450
|
+
* ```typescript
|
|
11451
|
+
* await cb.webrtc.switchRoom('channel:room-b')
|
|
11452
|
+
* ```
|
|
11453
|
+
*/
|
|
11454
|
+
async switchRoom(roomId, options = {}) {
|
|
11455
|
+
const localStream = options.localStream ?? this.localStream ?? void 0;
|
|
11456
|
+
const userId = options.userId ?? this.currentUserId ?? void 0;
|
|
11457
|
+
const isBroadcaster = options.isBroadcaster ?? this.isBroadcaster;
|
|
11458
|
+
this.disconnect();
|
|
11459
|
+
await this.connect({ roomId, userId, isBroadcaster, localStream });
|
|
11460
|
+
}
|
|
11461
|
+
/**
|
|
11462
|
+
* 소켓, 룸, 이벤트 리스너, 피어 연결을 **독립적으로** 갖는 새 WebRTC 세션을 만든다.
|
|
11463
|
+
*
|
|
11464
|
+
* `cb.webrtc` 는 인스턴스 하나에 소켓 하나/룸 하나라, 한 앱에 WebRTC 기능이 둘 이상이면
|
|
11465
|
+
* (예: 공간 음성채팅 + 1:1 통화) 서로 연결을 뺏고 이벤트가 교차한다. 기능마다 세션을
|
|
11466
|
+
* 하나씩 만들면 각자의 소켓과 핸들러를 갖는다.
|
|
11467
|
+
*
|
|
11468
|
+
* 인증 정보(HttpClient)와 서버 주소, appId 는 부모와 공유한다.
|
|
11469
|
+
*
|
|
11470
|
+
* @example
|
|
11471
|
+
* ```typescript
|
|
11472
|
+
* const voice = cb.webrtc.createSession()
|
|
11473
|
+
* const call = cb.webrtc.createSession()
|
|
11474
|
+
*
|
|
11475
|
+
* voice.onRemoteStream((peerId, stream) => attachToSpatialAudio(peerId, stream))
|
|
11476
|
+
* call.onRemoteStream((peerId, stream) => showCallScreen(stream))
|
|
11477
|
+
*
|
|
11478
|
+
* await voice.connect({ roomId: 'voice:lobby', localStream: mic })
|
|
11479
|
+
* await call.connect({ roomId: 'call:alice-bob', localStream: mic })
|
|
11480
|
+
*
|
|
11481
|
+
* // 각 세션은 독립적으로 끊는다
|
|
11482
|
+
* call.disconnect()
|
|
11483
|
+
* ```
|
|
11484
|
+
*/
|
|
11485
|
+
createSession() {
|
|
11486
|
+
return new _WebRTCAPI(this.http, this.webrtcUrl, this.appId);
|
|
11487
|
+
}
|
|
11198
11488
|
/**
|
|
11199
11489
|
* 현재 연결 상태 조회
|
|
11200
11490
|
*/
|