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.js
CHANGED
|
@@ -5355,6 +5355,15 @@ var NativeAPI = class {
|
|
|
5355
5355
|
return window.NativeBridge.clipboard.readImage();
|
|
5356
5356
|
}
|
|
5357
5357
|
return null;
|
|
5358
|
+
},
|
|
5359
|
+
/**
|
|
5360
|
+
* 클립보드에 특정 형식(MIME 타입, 예: 'text/plain', 'image/png')의 데이터가 있는지 확인 (데스크톱 전용)
|
|
5361
|
+
*/
|
|
5362
|
+
has: async (format) => {
|
|
5363
|
+
if (window.NativeBridge?.clipboard?.has) {
|
|
5364
|
+
return window.NativeBridge.clipboard.has(format);
|
|
5365
|
+
}
|
|
5366
|
+
return false;
|
|
5358
5367
|
}
|
|
5359
5368
|
};
|
|
5360
5369
|
/**
|
|
@@ -5393,25 +5402,33 @@ var NativeAPI = class {
|
|
|
5393
5402
|
saveFile: async (content, filename, options) => {
|
|
5394
5403
|
const platform = this.getPlatform();
|
|
5395
5404
|
if (platform === "desktop" && window.NativeBridge?.filesystem) {
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5405
|
+
try {
|
|
5406
|
+
const result = await window.NativeBridge.filesystem.showSaveDialog?.({
|
|
5407
|
+
defaultPath: filename,
|
|
5408
|
+
filters: options?.filters
|
|
5409
|
+
});
|
|
5410
|
+
if (result?.canceled || !result?.filePath) return false;
|
|
5411
|
+
const textContent = content instanceof Blob ? await content.text() : content;
|
|
5412
|
+
const writeResult = await window.NativeBridge.filesystem.writeFile(
|
|
5413
|
+
result.filePath,
|
|
5414
|
+
textContent
|
|
5415
|
+
);
|
|
5416
|
+
return writeResult.success;
|
|
5417
|
+
} catch {
|
|
5418
|
+
return false;
|
|
5419
|
+
}
|
|
5407
5420
|
}
|
|
5408
5421
|
if (platform === "mobile" && window.NativeBridge?.filesystem) {
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5422
|
+
try {
|
|
5423
|
+
const textContent = content instanceof Blob ? await content.text() : content;
|
|
5424
|
+
const result = await window.NativeBridge.filesystem.writeFile(
|
|
5425
|
+
filename,
|
|
5426
|
+
textContent
|
|
5427
|
+
);
|
|
5428
|
+
return result.success;
|
|
5429
|
+
} catch {
|
|
5430
|
+
return false;
|
|
5431
|
+
}
|
|
5415
5432
|
}
|
|
5416
5433
|
const blob = content instanceof Blob ? content : new Blob([content], { type: "text/plain" });
|
|
5417
5434
|
const url = URL.createObjectURL(blob);
|
|
@@ -5427,8 +5444,12 @@ var NativeAPI = class {
|
|
|
5427
5444
|
*/
|
|
5428
5445
|
readFile: async (path) => {
|
|
5429
5446
|
if (window.NativeBridge?.filesystem?.readFile) {
|
|
5430
|
-
|
|
5431
|
-
|
|
5447
|
+
try {
|
|
5448
|
+
const result = await window.NativeBridge.filesystem.readFile(path);
|
|
5449
|
+
return result.success ? result.content ?? null : null;
|
|
5450
|
+
} catch {
|
|
5451
|
+
return null;
|
|
5452
|
+
}
|
|
5432
5453
|
}
|
|
5433
5454
|
return null;
|
|
5434
5455
|
},
|
|
@@ -5452,7 +5473,11 @@ var NativeAPI = class {
|
|
|
5452
5473
|
takePicture: async (options) => {
|
|
5453
5474
|
const platform = this.getPlatform();
|
|
5454
5475
|
if (platform === "mobile" && window.NativeBridge?.camera) {
|
|
5455
|
-
|
|
5476
|
+
try {
|
|
5477
|
+
return await window.NativeBridge.camera.takePicture(options);
|
|
5478
|
+
} catch {
|
|
5479
|
+
return null;
|
|
5480
|
+
}
|
|
5456
5481
|
}
|
|
5457
5482
|
return new Promise((resolve) => {
|
|
5458
5483
|
const input = document.createElement("input");
|
|
@@ -5489,7 +5514,11 @@ var NativeAPI = class {
|
|
|
5489
5514
|
pickImage: async (options) => {
|
|
5490
5515
|
const platform = this.getPlatform();
|
|
5491
5516
|
if (platform === "mobile" && window.NativeBridge?.camera) {
|
|
5492
|
-
|
|
5517
|
+
try {
|
|
5518
|
+
return await window.NativeBridge.camera.pickImage(options);
|
|
5519
|
+
} catch {
|
|
5520
|
+
return null;
|
|
5521
|
+
}
|
|
5493
5522
|
}
|
|
5494
5523
|
return new Promise((resolve) => {
|
|
5495
5524
|
const input = document.createElement("input");
|
|
@@ -5632,7 +5661,11 @@ var NativeAPI = class {
|
|
|
5632
5661
|
*/
|
|
5633
5662
|
getToken: async () => {
|
|
5634
5663
|
if (this.getPlatform() === "mobile" && window.NativeBridge?.push) {
|
|
5635
|
-
|
|
5664
|
+
try {
|
|
5665
|
+
return await window.NativeBridge.push.getToken();
|
|
5666
|
+
} catch {
|
|
5667
|
+
return null;
|
|
5668
|
+
}
|
|
5636
5669
|
}
|
|
5637
5670
|
return null;
|
|
5638
5671
|
},
|
|
@@ -5643,8 +5676,12 @@ var NativeAPI = class {
|
|
|
5643
5676
|
/** 로컬 알림 예약 (패키징 앱 전용) */
|
|
5644
5677
|
scheduleLocal: async (options) => {
|
|
5645
5678
|
if (this.getPlatform() === "mobile" && window.NativeBridge?.push) {
|
|
5646
|
-
|
|
5647
|
-
|
|
5679
|
+
try {
|
|
5680
|
+
const result = await window.NativeBridge.push.scheduleLocal(options);
|
|
5681
|
+
return result.notificationId;
|
|
5682
|
+
} catch {
|
|
5683
|
+
return null;
|
|
5684
|
+
}
|
|
5648
5685
|
}
|
|
5649
5686
|
return null;
|
|
5650
5687
|
},
|
|
@@ -5652,8 +5689,12 @@ var NativeAPI = class {
|
|
|
5652
5689
|
setBadgeCount: async (count) => {
|
|
5653
5690
|
const setter = window.NativeBridge?.push?.setBadgeCount;
|
|
5654
5691
|
if (!setter) return false;
|
|
5655
|
-
|
|
5656
|
-
|
|
5692
|
+
try {
|
|
5693
|
+
const result = await setter(count);
|
|
5694
|
+
return result.success;
|
|
5695
|
+
} catch {
|
|
5696
|
+
return false;
|
|
5697
|
+
}
|
|
5657
5698
|
}
|
|
5658
5699
|
};
|
|
5659
5700
|
/**
|
|
@@ -5672,12 +5713,12 @@ var NativeAPI = class {
|
|
|
5672
5713
|
if (platform === "mobile" && window.NativeBridge?.push?.scheduleLocal) {
|
|
5673
5714
|
const granted = await this.notification.requestPermission();
|
|
5674
5715
|
if (!granted) return false;
|
|
5675
|
-
await
|
|
5716
|
+
const notificationId = await this.push.scheduleLocal({
|
|
5676
5717
|
title: options.title,
|
|
5677
5718
|
body: options.body,
|
|
5678
5719
|
trigger: null
|
|
5679
5720
|
});
|
|
5680
|
-
return
|
|
5721
|
+
return notificationId !== null;
|
|
5681
5722
|
}
|
|
5682
5723
|
if (!("Notification" in window)) return false;
|
|
5683
5724
|
if (Notification.permission !== "granted") {
|
|
@@ -5697,8 +5738,12 @@ var NativeAPI = class {
|
|
|
5697
5738
|
requestPermission: async () => {
|
|
5698
5739
|
const platform = this.getPlatform();
|
|
5699
5740
|
if (platform === "mobile" && window.NativeBridge?.push) {
|
|
5700
|
-
|
|
5701
|
-
|
|
5741
|
+
try {
|
|
5742
|
+
const result = await window.NativeBridge.push.requestPermission();
|
|
5743
|
+
return result.granted;
|
|
5744
|
+
} catch {
|
|
5745
|
+
return false;
|
|
5746
|
+
}
|
|
5702
5747
|
}
|
|
5703
5748
|
if (!("Notification" in window)) return false;
|
|
5704
5749
|
const permission = await Notification.requestPermission();
|
|
@@ -5719,13 +5764,77 @@ var NativeAPI = class {
|
|
|
5719
5764
|
return result.success;
|
|
5720
5765
|
}
|
|
5721
5766
|
if (platform === "mobile" && window.NativeBridge?.browser) {
|
|
5722
|
-
|
|
5723
|
-
|
|
5767
|
+
try {
|
|
5768
|
+
const result = await window.NativeBridge.browser.openExternal(url);
|
|
5769
|
+
return result.success;
|
|
5770
|
+
} catch {
|
|
5771
|
+
return false;
|
|
5772
|
+
}
|
|
5724
5773
|
}
|
|
5725
5774
|
window.open(url, "_blank");
|
|
5726
5775
|
return true;
|
|
5727
5776
|
}
|
|
5728
5777
|
};
|
|
5778
|
+
/**
|
|
5779
|
+
* 모바일 전용: 네이티브 인앱 브라우저 세션으로 여는 OAuth
|
|
5780
|
+
*
|
|
5781
|
+
* WebView 안에서 소셜 로그인 팝업을 열면 세션/쿠키가 격리돼 실패하는 경우가 많다.
|
|
5782
|
+
* 패키징 앱(RN)에서는 시스템 브라우저 세션(iOS ASWebAuthenticationSession /
|
|
5783
|
+
* Android Custom Tabs)으로 열고 앱 URL 스킴 콜백으로 결과를 받는다.
|
|
5784
|
+
* 웹/데스크톱에서는 지원하지 않는다 — 일반 팝업/리다이렉트 OAuth 플로우를 쓸 것.
|
|
5785
|
+
*
|
|
5786
|
+
* @example
|
|
5787
|
+
* ```typescript
|
|
5788
|
+
* if (cb.native.getPlatform() === 'mobile') {
|
|
5789
|
+
* const result = await cb.native.oauth.signIn(authorizeUrl)
|
|
5790
|
+
* if (result) window.location.href = result.url // 콜백 URL 로 계속 진행
|
|
5791
|
+
* }
|
|
5792
|
+
* ```
|
|
5793
|
+
*/
|
|
5794
|
+
this.oauth = {
|
|
5795
|
+
/**
|
|
5796
|
+
* OAuth 인가 URL 을 네이티브 인앱 브라우저 세션으로 열고 콜백 URL 을 기다린다.
|
|
5797
|
+
* callbackScheme 을 생략하면 이 패키징 앱의 스킴(getCallbackScheme())을 사용한다.
|
|
5798
|
+
* 사용자가 취소했거나 미지원 플랫폼이면 null.
|
|
5799
|
+
*/
|
|
5800
|
+
signIn: async (authUrl, callbackScheme) => {
|
|
5801
|
+
const bridge = window.NativeBridge;
|
|
5802
|
+
if (this.getPlatform() !== "mobile" || !bridge?.oauth) return null;
|
|
5803
|
+
const scheme = callbackScheme ?? bridge.oauth.getCallbackScheme();
|
|
5804
|
+
try {
|
|
5805
|
+
return await bridge.oauth.signIn(authUrl, scheme);
|
|
5806
|
+
} catch {
|
|
5807
|
+
return null;
|
|
5808
|
+
}
|
|
5809
|
+
},
|
|
5810
|
+
/** 이 패키징 앱의 OAuth 콜백 URL 스킴(`<scheme>://oauth/callback`). 미지원 플랫폼은 null (동기) */
|
|
5811
|
+
getCallbackScheme: () => {
|
|
5812
|
+
if (this.getPlatform() !== "mobile" || !window.NativeBridge?.oauth) {
|
|
5813
|
+
return null;
|
|
5814
|
+
}
|
|
5815
|
+
return window.NativeBridge.oauth.getCallbackScheme();
|
|
5816
|
+
}
|
|
5817
|
+
};
|
|
5818
|
+
/**
|
|
5819
|
+
* 모바일 전용: 패키징 앱에 연결된 Connect Base Storage 로 파일 업로드.
|
|
5820
|
+
* 콘솔 패키징 설정에서 "파일 저장 위치" 를 Connect Base Storage 로 선택한 경우에만 지원한다.
|
|
5821
|
+
*/
|
|
5822
|
+
this.connectbase = {
|
|
5823
|
+
uploadFile: async (uri, filename, mimeType) => {
|
|
5824
|
+
if (this.getPlatform() !== "mobile" || !window.NativeBridge?.connectbase) {
|
|
5825
|
+
return null;
|
|
5826
|
+
}
|
|
5827
|
+
try {
|
|
5828
|
+
return await window.NativeBridge.connectbase.uploadFile(
|
|
5829
|
+
uri,
|
|
5830
|
+
filename,
|
|
5831
|
+
mimeType
|
|
5832
|
+
);
|
|
5833
|
+
} catch {
|
|
5834
|
+
return null;
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
};
|
|
5729
5838
|
/**
|
|
5730
5839
|
* 데스크톱 전용 창 제어 API
|
|
5731
5840
|
*/
|
|
@@ -5762,6 +5871,36 @@ var NativeAPI = class {
|
|
|
5762
5871
|
await document.exitFullscreen();
|
|
5763
5872
|
}
|
|
5764
5873
|
}
|
|
5874
|
+
},
|
|
5875
|
+
isFullScreen: async () => {
|
|
5876
|
+
if (window.NativeBridge?.window) {
|
|
5877
|
+
return await window.NativeBridge.window.isFullScreen() ?? false;
|
|
5878
|
+
}
|
|
5879
|
+
return !!document.fullscreenElement;
|
|
5880
|
+
},
|
|
5881
|
+
/** 창 크기 설정 (데스크톱 전용) */
|
|
5882
|
+
setSize: async (width, height) => {
|
|
5883
|
+
await window.NativeBridge?.window?.setSize(width, height);
|
|
5884
|
+
},
|
|
5885
|
+
/** 창 크기 조회 (데스크톱 전용, 미지원 플랫폼은 null) */
|
|
5886
|
+
getSize: async () => {
|
|
5887
|
+
return await window.NativeBridge?.window?.getSize() ?? null;
|
|
5888
|
+
},
|
|
5889
|
+
/** 창 위치 설정 (데스크톱 전용) */
|
|
5890
|
+
setPosition: async (x, y) => {
|
|
5891
|
+
await window.NativeBridge?.window?.setPosition(x, y);
|
|
5892
|
+
},
|
|
5893
|
+
/** 창 위치 조회 (데스크톱 전용, 미지원 플랫폼은 null) */
|
|
5894
|
+
getPosition: async () => {
|
|
5895
|
+
return await window.NativeBridge?.window?.getPosition() ?? null;
|
|
5896
|
+
},
|
|
5897
|
+
/** 화면 중앙으로 이동 (데스크톱 전용) */
|
|
5898
|
+
center: async () => {
|
|
5899
|
+
await window.NativeBridge?.window?.center();
|
|
5900
|
+
},
|
|
5901
|
+
/** 항상 위에 표시 설정 (데스크톱 전용) */
|
|
5902
|
+
setAlwaysOnTop: async (flag) => {
|
|
5903
|
+
await window.NativeBridge?.window?.setAlwaysOnTop(flag);
|
|
5765
5904
|
}
|
|
5766
5905
|
};
|
|
5767
5906
|
/**
|
|
@@ -5779,6 +5918,12 @@ var NativeAPI = class {
|
|
|
5779
5918
|
return window.NativeBridge.system.getMemory();
|
|
5780
5919
|
}
|
|
5781
5920
|
return null;
|
|
5921
|
+
},
|
|
5922
|
+
getCPU: async () => {
|
|
5923
|
+
if (window.NativeBridge?.system) {
|
|
5924
|
+
return window.NativeBridge.system.getCPU();
|
|
5925
|
+
}
|
|
5926
|
+
return null;
|
|
5782
5927
|
}
|
|
5783
5928
|
};
|
|
5784
5929
|
/**
|
|
@@ -5787,13 +5932,21 @@ var NativeAPI = class {
|
|
|
5787
5932
|
this.biometric = {
|
|
5788
5933
|
isAvailable: async () => {
|
|
5789
5934
|
if (window.NativeBridge?.biometric) {
|
|
5790
|
-
|
|
5935
|
+
try {
|
|
5936
|
+
return await window.NativeBridge.biometric.isAvailable();
|
|
5937
|
+
} catch {
|
|
5938
|
+
return null;
|
|
5939
|
+
}
|
|
5791
5940
|
}
|
|
5792
5941
|
return null;
|
|
5793
5942
|
},
|
|
5794
5943
|
authenticate: async (options) => {
|
|
5795
5944
|
if (window.NativeBridge?.biometric) {
|
|
5796
|
-
|
|
5945
|
+
try {
|
|
5946
|
+
return await window.NativeBridge.biometric.authenticate(options);
|
|
5947
|
+
} catch {
|
|
5948
|
+
return null;
|
|
5949
|
+
}
|
|
5797
5950
|
}
|
|
5798
5951
|
return null;
|
|
5799
5952
|
}
|
|
@@ -5804,26 +5957,38 @@ var NativeAPI = class {
|
|
|
5804
5957
|
this.secureStore = {
|
|
5805
5958
|
setItem: async (key, value) => {
|
|
5806
5959
|
if (window.NativeBridge?.secureStore) {
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5960
|
+
try {
|
|
5961
|
+
const result = await window.NativeBridge.secureStore.setItem(
|
|
5962
|
+
key,
|
|
5963
|
+
value
|
|
5964
|
+
);
|
|
5965
|
+
return result.success;
|
|
5966
|
+
} catch {
|
|
5967
|
+
return false;
|
|
5968
|
+
}
|
|
5812
5969
|
}
|
|
5813
5970
|
localStorage.setItem(key, value);
|
|
5814
5971
|
return true;
|
|
5815
5972
|
},
|
|
5816
5973
|
getItem: async (key) => {
|
|
5817
5974
|
if (window.NativeBridge?.secureStore) {
|
|
5818
|
-
|
|
5819
|
-
|
|
5975
|
+
try {
|
|
5976
|
+
const result = await window.NativeBridge.secureStore.getItem(key);
|
|
5977
|
+
return result.value;
|
|
5978
|
+
} catch {
|
|
5979
|
+
return null;
|
|
5980
|
+
}
|
|
5820
5981
|
}
|
|
5821
5982
|
return localStorage.getItem(key);
|
|
5822
5983
|
},
|
|
5823
5984
|
deleteItem: async (key) => {
|
|
5824
5985
|
if (window.NativeBridge?.secureStore) {
|
|
5825
|
-
|
|
5826
|
-
|
|
5986
|
+
try {
|
|
5987
|
+
const result = await window.NativeBridge.secureStore.deleteItem(key);
|
|
5988
|
+
return result.success;
|
|
5989
|
+
} catch {
|
|
5990
|
+
return false;
|
|
5991
|
+
}
|
|
5827
5992
|
}
|
|
5828
5993
|
localStorage.removeItem(key);
|
|
5829
5994
|
return true;
|
|
@@ -5835,15 +6000,23 @@ var NativeAPI = class {
|
|
|
5835
6000
|
this.admob = {
|
|
5836
6001
|
showInterstitial: async () => {
|
|
5837
6002
|
if (window.NativeBridge?.admob) {
|
|
5838
|
-
|
|
5839
|
-
|
|
6003
|
+
try {
|
|
6004
|
+
const result = await window.NativeBridge.admob.showInterstitial();
|
|
6005
|
+
return result.shown;
|
|
6006
|
+
} catch {
|
|
6007
|
+
return false;
|
|
6008
|
+
}
|
|
5840
6009
|
}
|
|
5841
6010
|
return false;
|
|
5842
6011
|
},
|
|
5843
6012
|
showRewarded: async () => {
|
|
5844
6013
|
if (window.NativeBridge?.admob) {
|
|
5845
|
-
|
|
5846
|
-
|
|
6014
|
+
try {
|
|
6015
|
+
const result = await window.NativeBridge.admob.showRewarded();
|
|
6016
|
+
return result.rewarded;
|
|
6017
|
+
} catch {
|
|
6018
|
+
return false;
|
|
6019
|
+
}
|
|
5847
6020
|
}
|
|
5848
6021
|
return false;
|
|
5849
6022
|
}
|
|
@@ -7763,9 +7936,35 @@ var RealtimeAPI = class {
|
|
|
7763
7936
|
return this.state;
|
|
7764
7937
|
}
|
|
7765
7938
|
/**
|
|
7766
|
-
*
|
|
7939
|
+
* pub/sub 사용 가능 여부.
|
|
7940
|
+
*
|
|
7941
|
+
* WebSocket 으로 연결됐을 때만 true 다. SSE fallback 모드에서는 `subscribe` /
|
|
7942
|
+
* `sendMessage` / `setPresence` / typing 이 전부 예외를 던지므로, 여기서 true 를
|
|
7943
|
+
* 반환하면 앱이 "연결됨"으로 오판하고 아무것도 오가지 않는 채 조용히 멈춘다
|
|
7944
|
+
* (platform-issue 01a01e6a-3cb1).
|
|
7945
|
+
*
|
|
7946
|
+
* AI 스트리밍만 필요하다면 {@link isStreamReady} 를, 다운그레이드 여부를 알고 싶다면
|
|
7947
|
+
* {@link isDegraded} 를 쓴다.
|
|
7767
7948
|
*/
|
|
7768
7949
|
isConnected() {
|
|
7950
|
+
return this.state === "connected" && this.activeTransport === "ws";
|
|
7951
|
+
}
|
|
7952
|
+
/**
|
|
7953
|
+
* SSE fallback 으로 다운그레이드된 상태인지 여부.
|
|
7954
|
+
*
|
|
7955
|
+
* true 면 연결은 살아 있지만 단방향이라 `realtime.stream()` 만 동작한다. 사용자에게
|
|
7956
|
+
* "제한된 네트워크라 일부 기능을 쓸 수 없다" 고 안내할 지점이다.
|
|
7957
|
+
*/
|
|
7958
|
+
isDegraded() {
|
|
7959
|
+
return this.sseFallbackActive;
|
|
7960
|
+
}
|
|
7961
|
+
/**
|
|
7962
|
+
* AI 스트리밍(`realtime.stream()`) 사용 가능 여부.
|
|
7963
|
+
*
|
|
7964
|
+
* WS 든 SSE fallback 이든 연결만 서 있으면 true 다. 종전 `isConnected()` 와 같은
|
|
7965
|
+
* 판정이므로, 스트리밍 게이팅에 `isConnected()` 를 쓰던 코드는 이쪽으로 옮긴다.
|
|
7966
|
+
*/
|
|
7967
|
+
isStreamReady() {
|
|
7769
7968
|
return this.state === "connected";
|
|
7770
7969
|
}
|
|
7771
7970
|
/**
|
|
@@ -10896,7 +11095,7 @@ var VideoAPI = class {
|
|
|
10896
11095
|
};
|
|
10897
11096
|
|
|
10898
11097
|
// src/api/webrtc.ts
|
|
10899
|
-
var WebRTCAPI = class {
|
|
11098
|
+
var WebRTCAPI = class _WebRTCAPI {
|
|
10900
11099
|
constructor(http, webrtcUrl, appId) {
|
|
10901
11100
|
this.ws = null;
|
|
10902
11101
|
this.state = "disconnected";
|
|
@@ -10908,6 +11107,16 @@ var WebRTCAPI = class {
|
|
|
10908
11107
|
this.reconnectAttempts = 0;
|
|
10909
11108
|
this.maxReconnectAttempts = 5;
|
|
10910
11109
|
this.reconnectTimeout = null;
|
|
11110
|
+
/**
|
|
11111
|
+
* 시그널링 소켓의 세대(generation) 토큰.
|
|
11112
|
+
*
|
|
11113
|
+
* 소켓을 열 때마다 1 증가하며, 각 핸들러는 자기가 열릴 때의 세대를 클로저에 담아둔다.
|
|
11114
|
+
* 세대가 밀린 핸들러는 "옛 소켓의 지연 이벤트" 이므로 인스턴스 상태를 건드리지 않는다.
|
|
11115
|
+
* 참조(`this.ws`)만 끊는 것으로는 부족하다 — 핸들러 클로저가 인스턴스를 캡처하고 있어
|
|
11116
|
+
* close 이벤트가 뒤늦게 도착하면 새 연결을 'disconnected' 로 덮어쓰고 새로 만든 피어
|
|
11117
|
+
* 연결까지 정리해버린다.
|
|
11118
|
+
*/
|
|
11119
|
+
this.wsGeneration = 0;
|
|
10911
11120
|
// 현재 연결 정보
|
|
10912
11121
|
this.currentRoomId = null;
|
|
10913
11122
|
this.currentPeerId = null;
|
|
@@ -10980,15 +11189,23 @@ var WebRTCAPI = class {
|
|
|
10980
11189
|
connectWebSocket() {
|
|
10981
11190
|
return new Promise((resolve, reject) => {
|
|
10982
11191
|
const wsUrl = this.buildWebSocketUrl();
|
|
10983
|
-
|
|
11192
|
+
const generation = ++this.wsGeneration;
|
|
11193
|
+
const ws = new WebSocket(wsUrl);
|
|
11194
|
+
this.ws = ws;
|
|
11195
|
+
const isStale = () => this.wsGeneration !== generation;
|
|
10984
11196
|
const timeout = setTimeout(() => {
|
|
11197
|
+
if (isStale()) return;
|
|
10985
11198
|
if (this.state === "connecting") {
|
|
10986
|
-
|
|
11199
|
+
ws.close();
|
|
10987
11200
|
reject(new Error("\uC5F0\uACB0 \uC2DC\uAC04 \uCD08\uACFC"));
|
|
10988
11201
|
}
|
|
10989
11202
|
}, 1e4);
|
|
10990
|
-
|
|
11203
|
+
ws.onopen = () => {
|
|
10991
11204
|
clearTimeout(timeout);
|
|
11205
|
+
if (isStale()) {
|
|
11206
|
+
ws.close(1e3, "superseded");
|
|
11207
|
+
return;
|
|
11208
|
+
}
|
|
10992
11209
|
this.reconnectAttempts = 0;
|
|
10993
11210
|
this.sendSignaling({
|
|
10994
11211
|
type: "join",
|
|
@@ -10999,7 +11216,8 @@ var WebRTCAPI = class {
|
|
|
10999
11216
|
}
|
|
11000
11217
|
});
|
|
11001
11218
|
};
|
|
11002
|
-
|
|
11219
|
+
ws.onmessage = async (event) => {
|
|
11220
|
+
if (isStale()) return;
|
|
11003
11221
|
try {
|
|
11004
11222
|
const msg = JSON.parse(event.data);
|
|
11005
11223
|
await this.handleSignalingMessage(msg, resolve, reject);
|
|
@@ -11007,13 +11225,15 @@ var WebRTCAPI = class {
|
|
|
11007
11225
|
console.error("Failed to parse signaling message:", error);
|
|
11008
11226
|
}
|
|
11009
11227
|
};
|
|
11010
|
-
|
|
11228
|
+
ws.onerror = (event) => {
|
|
11011
11229
|
clearTimeout(timeout);
|
|
11230
|
+
if (isStale()) return;
|
|
11012
11231
|
console.error("WebSocket error:", event);
|
|
11013
11232
|
this.emitError(new Error("WebSocket \uC5F0\uACB0 \uC624\uB958"));
|
|
11014
11233
|
};
|
|
11015
|
-
|
|
11234
|
+
ws.onclose = (event) => {
|
|
11016
11235
|
clearTimeout(timeout);
|
|
11236
|
+
if (isStale()) return;
|
|
11017
11237
|
if (this.state === "connecting") {
|
|
11018
11238
|
reject(new Error("\uC5F0\uACB0\uC774 \uC885\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4"));
|
|
11019
11239
|
}
|
|
@@ -11021,6 +11241,19 @@ var WebRTCAPI = class {
|
|
|
11021
11241
|
};
|
|
11022
11242
|
});
|
|
11023
11243
|
}
|
|
11244
|
+
/**
|
|
11245
|
+
* 소켓에 붙은 핸들러를 전부 떼어낸다.
|
|
11246
|
+
*
|
|
11247
|
+
* `this.ws = null` 로 참조만 끊으면 클로저가 살아남아 close 이벤트가 계속 들어온다.
|
|
11248
|
+
* 세대 토큰이 그 이벤트를 무시해주긴 하지만, 핸들러 자체를 떼면 인스턴스 참조가 끊겨
|
|
11249
|
+
* 소켓이 GC 될 때까지 인스턴스를 붙잡고 있지 않는다.
|
|
11250
|
+
*/
|
|
11251
|
+
static detachSocket(ws) {
|
|
11252
|
+
ws.onopen = null;
|
|
11253
|
+
ws.onmessage = null;
|
|
11254
|
+
ws.onerror = null;
|
|
11255
|
+
ws.onclose = null;
|
|
11256
|
+
}
|
|
11024
11257
|
buildWebSocketUrl() {
|
|
11025
11258
|
const wsBase = this.webrtcUrl.replace("https://", "wss://").replace("http://", "ws://");
|
|
11026
11259
|
const publicKey = this.http.getPublicKey();
|
|
@@ -11231,19 +11464,76 @@ var WebRTCAPI = class {
|
|
|
11231
11464
|
clearTimeout(this.reconnectTimeout);
|
|
11232
11465
|
this.reconnectTimeout = null;
|
|
11233
11466
|
}
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11467
|
+
this.reconnectAttempts = 0;
|
|
11468
|
+
this.wsGeneration++;
|
|
11469
|
+
const ws = this.ws;
|
|
11470
|
+
this.ws = null;
|
|
11471
|
+
if (ws) {
|
|
11472
|
+
_WebRTCAPI.detachSocket(ws);
|
|
11473
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
11474
|
+
try {
|
|
11475
|
+
ws.send(JSON.stringify({ type: "leave" }));
|
|
11476
|
+
} catch {
|
|
11477
|
+
}
|
|
11478
|
+
ws.close(1e3, "User disconnected");
|
|
11479
|
+
} else if (ws.readyState === WebSocket.CONNECTING) {
|
|
11480
|
+
ws.close();
|
|
11481
|
+
}
|
|
11237
11482
|
}
|
|
11238
11483
|
this.peerConnections.forEach((pc) => pc.close());
|
|
11239
11484
|
this.peerConnections.clear();
|
|
11240
11485
|
this.remoteStreams.clear();
|
|
11241
|
-
this.ws = null;
|
|
11242
11486
|
this.currentRoomId = null;
|
|
11243
11487
|
this.currentPeerId = null;
|
|
11244
11488
|
this.localStream = null;
|
|
11245
11489
|
this.setState("disconnected");
|
|
11246
11490
|
}
|
|
11491
|
+
/**
|
|
11492
|
+
* 현재 세션의 룸을 교체한다.
|
|
11493
|
+
*
|
|
11494
|
+
* `disconnect()` 후 곧바로 `connect()` 하는 패턴을 안전하게 감싼 것이다. 옛 소켓의
|
|
11495
|
+
* close 이벤트가 새 연결의 'connecting' 구간에 떨어져도 세대 토큰이 걸러낸다.
|
|
11496
|
+
* 생략한 옵션은 현재 세션 값을 그대로 이어받는다.
|
|
11497
|
+
*
|
|
11498
|
+
* @example
|
|
11499
|
+
* ```typescript
|
|
11500
|
+
* await cb.webrtc.switchRoom('channel:room-b')
|
|
11501
|
+
* ```
|
|
11502
|
+
*/
|
|
11503
|
+
async switchRoom(roomId, options = {}) {
|
|
11504
|
+
const localStream = options.localStream ?? this.localStream ?? void 0;
|
|
11505
|
+
const userId = options.userId ?? this.currentUserId ?? void 0;
|
|
11506
|
+
const isBroadcaster = options.isBroadcaster ?? this.isBroadcaster;
|
|
11507
|
+
this.disconnect();
|
|
11508
|
+
await this.connect({ roomId, userId, isBroadcaster, localStream });
|
|
11509
|
+
}
|
|
11510
|
+
/**
|
|
11511
|
+
* 소켓, 룸, 이벤트 리스너, 피어 연결을 **독립적으로** 갖는 새 WebRTC 세션을 만든다.
|
|
11512
|
+
*
|
|
11513
|
+
* `cb.webrtc` 는 인스턴스 하나에 소켓 하나/룸 하나라, 한 앱에 WebRTC 기능이 둘 이상이면
|
|
11514
|
+
* (예: 공간 음성채팅 + 1:1 통화) 서로 연결을 뺏고 이벤트가 교차한다. 기능마다 세션을
|
|
11515
|
+
* 하나씩 만들면 각자의 소켓과 핸들러를 갖는다.
|
|
11516
|
+
*
|
|
11517
|
+
* 인증 정보(HttpClient)와 서버 주소, appId 는 부모와 공유한다.
|
|
11518
|
+
*
|
|
11519
|
+
* @example
|
|
11520
|
+
* ```typescript
|
|
11521
|
+
* const voice = cb.webrtc.createSession()
|
|
11522
|
+
* const call = cb.webrtc.createSession()
|
|
11523
|
+
*
|
|
11524
|
+
* voice.onRemoteStream((peerId, stream) => attachToSpatialAudio(peerId, stream))
|
|
11525
|
+
* call.onRemoteStream((peerId, stream) => showCallScreen(stream))
|
|
11526
|
+
*
|
|
11527
|
+
* await voice.connect({ roomId: 'voice:lobby', localStream: mic })
|
|
11528
|
+
* await call.connect({ roomId: 'call:alice-bob', localStream: mic })
|
|
11529
|
+
*
|
|
11530
|
+
* // 각 세션은 독립적으로 끊는다
|
|
11531
|
+
* call.disconnect()
|
|
11532
|
+
* ```
|
|
11533
|
+
*/
|
|
11534
|
+
createSession() {
|
|
11535
|
+
return new _WebRTCAPI(this.http, this.webrtcUrl, this.appId);
|
|
11536
|
+
}
|
|
11247
11537
|
/**
|
|
11248
11538
|
* 현재 연결 상태 조회
|
|
11249
11539
|
*/
|