@vibexnpm/talkx 8.13.0 → 9.1.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.
@@ -104,58 +104,62 @@ class ApiClient {
104
104
  }
105
105
  }
106
106
 
107
- // AbortController for timeout
107
+ // 시간 상한은 본문을 다 읽을 때까지다 — 헤더만 오고 본문이 멈춰도 끝나게(AuthSession._request 와 같다). 끊으면 본문 읽기도 AbortError 로
108
+ // 끝난다.
108
109
  const controller = new AbortController();
109
110
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
110
111
 
111
- let response;
112
112
  try {
113
- this.logger.debug(`${method} ${url}`);
114
-
115
- response = await fetch(url, {
116
- method,
117
- headers: { ...this._getHeaders(), ...headers },
118
- credentials: this.authProvider ? 'omit' : 'same-origin',
119
- body: body ? JSON.stringify(body) : undefined,
120
- signal: controller.signal
121
- });
122
- } catch (error) {
123
- clearTimeout(timeoutId);
124
-
125
- if (error.name === 'AbortError') {
126
- const timeoutError = new Error('Request timeout');
127
- timeoutError.code = ErrorTypes.API_TIMEOUT;
128
- throw timeoutError;
113
+ let response;
114
+ try {
115
+ this.logger.debug(`${method} ${url}`);
116
+
117
+ response = await fetch(url, {
118
+ method,
119
+ headers: { ...this._getHeaders(), ...headers },
120
+ credentials: this.authProvider ? 'omit' : 'same-origin',
121
+ body: body ? JSON.stringify(body) : undefined,
122
+ signal: controller.signal
123
+ });
124
+ } catch (error) {
125
+ if (error.name !== 'AbortError') {
126
+ this.logger.error(`API Error: ${method} ${url}`, error);
127
+ }
128
+ throw error;
129
129
  }
130
130
 
131
- this.logger.error(`API Error: ${method} ${url}`, error);
132
- throw error;
133
- }
134
-
135
- clearTimeout(timeoutId);
131
+ // 응답 파싱
132
+ const contentType = response.headers.get('content-type');
133
+ let data;
136
134
 
137
- // 응답 파싱
138
- const contentType = response.headers.get('content-type');
139
- let data;
135
+ if (contentType && contentType.includes('application/json')) {
136
+ data = await response.json();
137
+ } else {
138
+ data = await response.text();
139
+ }
140
140
 
141
- if (contentType && contentType.includes('application/json')) {
142
- data = await response.json();
143
- } else {
144
- data = await response.text();
145
- }
141
+ // 에러 처리
142
+ if (!response.ok) {
143
+ const error = new Error(data?.message || `HTTP ${response.status}`);
144
+ error.code = ErrorTypes.API_ERROR;
145
+ error.status = response.status;
146
+ error.response = data;
147
+ this.logger.error(`API Error: ${method} ${url}`, error);
148
+ throw error;
149
+ }
146
150
 
147
- // 에러 처리
148
- if (!response.ok) {
149
- const error = new Error(data?.message || `HTTP ${response.status}`);
150
- error.code = ErrorTypes.API_ERROR;
151
- error.status = response.status;
152
- error.response = data;
153
- this.logger.error(`API Error: ${method} ${url}`, error);
151
+ this.logger.debug(`Response ${response.status}`);
152
+ return data;
153
+ } catch (error) {
154
+ if (error?.name === 'AbortError') {
155
+ const timeoutError = new Error('Request timeout');
156
+ timeoutError.code = ErrorTypes.API_TIMEOUT;
157
+ throw timeoutError;
158
+ }
154
159
  throw error;
160
+ } finally {
161
+ clearTimeout(timeoutId);
155
162
  }
156
-
157
- this.logger.debug(`Response ${response.status}`);
158
- return data;
159
163
  }
160
164
 
161
165
  // HTTP 메서드 헬퍼
@@ -0,0 +1,135 @@
1
+ /**
2
+ * CallLedger
3
+ * 통화 이벤트 장부 — 브라우저·LiveKit 없이 판정만 한다(화상통화 SFU 설계 v0.7 I8·§5.3).
4
+ *
5
+ * <ul>
6
+ * <li>중복 전달은 버린다 — 같은 (version, 종류) 는 한 번만.</li>
7
+ * <li>«지금 상태» 를 뜻하는 이벤트(초대)는 본 것보다 낡은 version 이면 버린다 — 통화가 이미 그 뒤로 나아갔다. 거절·응답·취소·종료처럼
8
+ * «일어난 일» 을 알리는 이벤트는 늦게 와도 버리지 않는다(두 사람의 거절이 역순으로 와도 둘 다 방장에게 간다).</li>
9
+ * <li>서버가 끝냈다고 알린 통화는 묘비로 기억한다 — 늦게 온 초대가 다시 울리지 않게. 이 기기만 나간 것(나가기·재접속 실패·다른 기기로
10
+ * 옮김)은 묘비가 아니다 — 다시 들어갈 수 있다.</li>
11
+ * <li>울림 정지 표식은 version 과 따로 둔다 — «응답함»·«취소» 를 한 번 받으면 그 통화의 울림을 되살리지 않는다.</li>
12
+ * </ul>
13
+ */
14
+
15
+ /** 기억하는 통화 수 상한 — 오래된 것부터 잊는다(초대·종료는 짧게만 의미가 있다). */
16
+ const DEFAULT_MAX_CALLS = 64;
17
+ /** 통화 하나에서 기억하는 (version, 종류) 수 상한. */
18
+ const MAX_SEEN_PER_CALL = 64;
19
+
20
+ class CallLedger {
21
+ /**
22
+ * @param {Object} [options]
23
+ * @param {number} [options.maxCalls=64]
24
+ */
25
+ constructor({ maxCalls = DEFAULT_MAX_CALLS } = {}) {
26
+ this._maxCalls = maxCalls;
27
+ /** @type {Map<string, {version: number|null, seen: Set<string>, ended: {reason: string}|null, ringStopped: boolean}>} */
28
+ this._calls = new Map();
29
+ }
30
+
31
+ /**
32
+ * 본 version 보다 낡았나 — 초대처럼 «지금 상태» 를 뜻하는 이벤트에만 쓴다.
33
+ * @param {string} callRoomId
34
+ * @param {number} version
35
+ * @returns {boolean}
36
+ */
37
+ isStale(callRoomId, version) {
38
+ const entry = this._calls.get(callRoomId);
39
+ return Boolean(entry && Number.isFinite(version) && entry.version !== null && version < entry.version);
40
+ }
41
+
42
+ /**
43
+ * 이벤트를 기억한다 — 처음 보는 (version, 종류) 면 true, 이미 본 것(중복 전달)이면 false. 최신 version 을 올린다.
44
+ * @param {string} callRoomId
45
+ * @param {number} version
46
+ * @param {string} type
47
+ * @returns {boolean}
48
+ */
49
+ record(callRoomId, version, type) {
50
+ const entry = this._entry(callRoomId);
51
+ if (!Number.isFinite(version)) {
52
+ return true;
53
+ }
54
+ const key = `${version}:${type}`;
55
+ if (entry.seen.has(key)) {
56
+ return false;
57
+ }
58
+ entry.seen.add(key);
59
+ if (entry.seen.size > MAX_SEEN_PER_CALL) {
60
+ entry.seen.delete(entry.seen.values().next().value);
61
+ }
62
+ if (entry.version === null || version > entry.version) {
63
+ entry.version = version;
64
+ }
65
+ return true;
66
+ }
67
+
68
+ /**
69
+ * REST 응답 등에서 본 version 을 바닥으로 올린다 — 그보다 낡은 초대는 이후 버린다.
70
+ * @param {string} callRoomId
71
+ * @param {number} version
72
+ */
73
+ observe(callRoomId, version) {
74
+ if (!Number.isFinite(version)) {
75
+ return;
76
+ }
77
+ const entry = this._entry(callRoomId);
78
+ if (entry.version === null || version > entry.version) {
79
+ entry.version = version;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 서버가 끝냈다고 알린 통화로 기억한다(묘비). 처음 사유가 남는다.
85
+ * @param {string} callRoomId
86
+ * @param {string} reason
87
+ */
88
+ markEnded(callRoomId, reason) {
89
+ const entry = this._entry(callRoomId);
90
+ if (!entry.ended) {
91
+ entry.ended = { reason };
92
+ }
93
+ entry.ringStopped = true;
94
+ }
95
+
96
+ /**
97
+ * @param {string} callRoomId
98
+ * @returns {string|null} 서버에서 끝났으면 그 사유, 아니면 null
99
+ */
100
+ endedReason(callRoomId) {
101
+ return this._calls.get(callRoomId)?.ended?.reason ?? null;
102
+ }
103
+
104
+ /** @param {string} callRoomId */
105
+ stopRinging(callRoomId) {
106
+ this._entry(callRoomId).ringStopped = true;
107
+ }
108
+
109
+ /**
110
+ * @param {string} callRoomId
111
+ * @returns {boolean} 울림을 되살리면 안 되는가(정지 표식 또는 묘비)
112
+ */
113
+ isRingStopped(callRoomId) {
114
+ const entry = this._calls.get(callRoomId);
115
+ return Boolean(entry && (entry.ringStopped || entry.ended));
116
+ }
117
+
118
+ _entry(callRoomId) {
119
+ let entry = this._calls.get(callRoomId);
120
+ if (entry) {
121
+ // 최근에 쓴 통화를 뒤로 옮겨 오래된 것부터 잊게 한다.
122
+ this._calls.delete(callRoomId);
123
+ this._calls.set(callRoomId, entry);
124
+ return entry;
125
+ }
126
+ entry = { version: null, seen: new Set(), ended: null, ringStopped: false };
127
+ this._calls.set(callRoomId, entry);
128
+ while (this._calls.size > this._maxCalls) {
129
+ this._calls.delete(this._calls.keys().next().value);
130
+ }
131
+ return entry;
132
+ }
133
+ }
134
+
135
+ export default CallLedger;