@things-factory/integration-base 9.0.33 → 9.0.34

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.
Files changed (27) hide show
  1. package/dist-server/engine/connector/headless-connector.d.ts +1 -0
  2. package/dist-server/engine/connector/headless-connector.js +117 -61
  3. package/dist-server/engine/connector/headless-connector.js.map +1 -1
  4. package/dist-server/engine/resource-pool/headless-pool.d.ts +29 -1
  5. package/dist-server/engine/resource-pool/headless-pool.js +70 -53
  6. package/dist-server/engine/resource-pool/headless-pool.js.map +1 -1
  7. package/dist-server/engine/task/headless-delete.js +9 -61
  8. package/dist-server/engine/task/headless-delete.js.map +1 -1
  9. package/dist-server/engine/task/headless-get.js +9 -55
  10. package/dist-server/engine/task/headless-get.js.map +1 -1
  11. package/dist-server/engine/task/headless-patch.js +11 -74
  12. package/dist-server/engine/task/headless-patch.js.map +1 -1
  13. package/dist-server/engine/task/headless-post.js +11 -74
  14. package/dist-server/engine/task/headless-post.js.map +1 -1
  15. package/dist-server/engine/task/headless-put.js +11 -74
  16. package/dist-server/engine/task/headless-put.js.map +1 -1
  17. package/dist-server/engine/task/utils/headless-request-with-recovery.d.ts +18 -0
  18. package/dist-server/engine/task/utils/headless-request-with-recovery.js +221 -0
  19. package/dist-server/engine/task/utils/headless-request-with-recovery.js.map +1 -0
  20. package/dist-server/restful/unstable/headless-pool-status.d.ts +1 -0
  21. package/dist-server/restful/unstable/headless-pool-status.js +78 -0
  22. package/dist-server/restful/unstable/headless-pool-status.js.map +1 -0
  23. package/dist-server/restful/unstable/index.d.ts +1 -0
  24. package/dist-server/restful/unstable/index.js +1 -0
  25. package/dist-server/restful/unstable/index.js.map +1 -1
  26. package/dist-server/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +8 -9
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeHeadlessRequestWithRecovery = executeHeadlessRequestWithRecovery;
4
+ const utils_1 = require("@things-factory/utils");
5
+ const connection_manager_1 = require("../../connection-manager");
6
+ /**
7
+ * 세션 회복 기능을 포함한 headless HTTP 요청 함수
8
+ */
9
+ async function executeHeadlessRequestWithRecovery(connectionName, options, context) {
10
+ const { logger, data, domain } = context;
11
+ const { method, path, headers = {}, body, queryParams, maxRetries = 2, accessor, contentType } = options;
12
+ // accessor가 있으면 data에서 body 추출
13
+ let requestBody = body;
14
+ if (accessor && data) {
15
+ requestBody = (0, utils_1.access)(accessor, data);
16
+ }
17
+ const connection = await connection_manager_1.ConnectionManager.getConnectionInstanceByName(domain, connectionName);
18
+ if (!connection) {
19
+ throw new Error(`Connection '${connectionName}' is not established.`);
20
+ }
21
+ const { endpoint, acquireSessionPage, releasePage, reAuthenticateSession, validateSession } = connection;
22
+ let page = null;
23
+ let lastError = null;
24
+ // 재시도 로직
25
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
26
+ try {
27
+ // 페이지 획득
28
+ page = await acquireSessionPage();
29
+ // 페이지가 올바른 도메인에 있는지 확인
30
+ const currentUrl = page.url();
31
+ const targetDomain = new URL(endpoint).origin;
32
+ if (!currentUrl.startsWith(targetDomain)) {
33
+ logger.info(`Navigating to target domain: ${targetDomain}`);
34
+ await page.goto(targetDomain, { waitUntil: 'networkidle2' });
35
+ }
36
+ // 첫 번째 시도가 아니면 세션 검증
37
+ if (attempt > 0) {
38
+ const isSessionValid = await validateSession(page);
39
+ if (!isSessionValid) {
40
+ logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication`);
41
+ await releasePage(page);
42
+ page = await reAuthenticateSession();
43
+ }
44
+ }
45
+ // URL 구성
46
+ const url = new URL(path, endpoint);
47
+ if (queryParams && typeof queryParams === 'object') {
48
+ Object.keys(queryParams).forEach(key => {
49
+ if (queryParams[key] !== null && queryParams[key] !== undefined) {
50
+ url.searchParams.append(key, String(queryParams[key]));
51
+ }
52
+ });
53
+ }
54
+ // 요청 옵션 구성
55
+ const requestOptions = {
56
+ method,
57
+ headers: {
58
+ ...headers
59
+ },
60
+ credentials: 'include'
61
+ };
62
+ // Content-Type과 body 처리
63
+ if (requestBody && ['POST', 'PUT', 'PATCH'].includes(method)) {
64
+ if (contentType) {
65
+ requestOptions.headers['content-type'] = contentType;
66
+ switch (contentType) {
67
+ case 'text/plain':
68
+ requestOptions.body = JSON.stringify(requestBody);
69
+ break;
70
+ case 'application/json':
71
+ default:
72
+ requestOptions.body = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody);
73
+ break;
74
+ }
75
+ }
76
+ else {
77
+ requestOptions.headers['Content-Type'] = 'application/json';
78
+ requestOptions.body = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody);
79
+ }
80
+ }
81
+ // fetch 요청 실행 - try-catch로 네트워크 에러 처리
82
+ const response = await page.evaluate(async (urlString, opts) => {
83
+ try {
84
+ const response = await fetch(urlString, opts);
85
+ const result = {
86
+ ok: response.ok,
87
+ status: response.status,
88
+ statusText: response.statusText,
89
+ headers: Object.fromEntries(response.headers.entries())
90
+ };
91
+ if (!response.ok) {
92
+ return {
93
+ ...result,
94
+ error: `HTTP ${response.status}: ${response.statusText}`,
95
+ data: null
96
+ };
97
+ }
98
+ const contentType = response.headers.get('content-type') || '';
99
+ if (contentType.includes('application/json')) {
100
+ result['data'] = await response.json();
101
+ }
102
+ else if (contentType.includes('text/')) {
103
+ result['data'] = await response.text();
104
+ }
105
+ else {
106
+ // 응답이 없는 경우 (204 No Content 등)
107
+ result['data'] = null;
108
+ }
109
+ return result;
110
+ }
111
+ catch (fetchError) {
112
+ // 네트워크 레벨 에러 처리
113
+ return {
114
+ ok: false,
115
+ status: 0,
116
+ statusText: 'Network Error',
117
+ headers: {},
118
+ error: fetchError.message || 'Failed to fetch',
119
+ data: null,
120
+ networkError: true
121
+ };
122
+ }
123
+ }, url.toString(), requestOptions);
124
+ // 네트워크 에러 체크 (fetch 자체가 실패한 경우)
125
+ if (response.networkError) {
126
+ if (attempt < maxRetries) {
127
+ logger.warn(`Network error detected: ${response.error}, retrying... (${attempt + 1}/${maxRetries + 1})`);
128
+ if (page) {
129
+ await releasePage(page);
130
+ page = null;
131
+ }
132
+ continue;
133
+ }
134
+ else {
135
+ if (page) {
136
+ await releasePage(page);
137
+ }
138
+ throw new Error(`Network error after ${maxRetries + 1} attempts: ${response.error}`);
139
+ }
140
+ }
141
+ // 세션 타임아웃 관련 에러 체크
142
+ if (!response.ok && isSessionTimeoutError(response.status)) {
143
+ if (attempt < maxRetries) {
144
+ logger.warn(`Session timeout detected (${response.status}), retrying... (${attempt + 1}/${maxRetries + 1})`);
145
+ if (page) {
146
+ await releasePage(page);
147
+ page = null;
148
+ }
149
+ continue;
150
+ }
151
+ else {
152
+ if (page) {
153
+ await releasePage(page);
154
+ }
155
+ throw new Error(`Session timeout after ${maxRetries + 1} attempts: ${response.error}`);
156
+ }
157
+ }
158
+ // 기타 HTTP 에러
159
+ if (!response.ok) {
160
+ throw new Error(response.error);
161
+ }
162
+ // 성공 시 페이지 릴리즈 후 결과 반환
163
+ const result = {
164
+ data: response.data,
165
+ status: response.status,
166
+ headers: response.headers
167
+ };
168
+ if (page) {
169
+ await releasePage(page);
170
+ }
171
+ return result;
172
+ }
173
+ catch (error) {
174
+ lastError = error;
175
+ logger.error(`Headless request attempt ${attempt + 1} failed:`, error);
176
+ if (page) {
177
+ await releasePage(page);
178
+ page = null;
179
+ }
180
+ // 세션 관련 에러가 아니거나 마지막 재시도면 에러 발생
181
+ if (!isRecoverableError(error) || attempt === maxRetries) {
182
+ throw error;
183
+ }
184
+ logger.info(`Retrying request... (${attempt + 2}/${maxRetries + 1})`);
185
+ }
186
+ }
187
+ // 모든 재시도가 실패한 경우 - 혹시 남은 페이지가 있으면 정리
188
+ if (page) {
189
+ await releasePage(page);
190
+ }
191
+ throw lastError || new Error('Request failed after all retry attempts');
192
+ }
193
+ /**
194
+ * 세션 타임아웃 관련 HTTP 상태 코드인지 확인
195
+ */
196
+ function isSessionTimeoutError(statusCode) {
197
+ return [401, 403].includes(statusCode);
198
+ }
199
+ /**
200
+ * 복구 가능한 에러인지 확인
201
+ */
202
+ function isRecoverableError(error) {
203
+ const errorMessage = error.message?.toLowerCase() || '';
204
+ // 복구 가능한 에러 키워드 (네트워크, 세션/인증 관련)
205
+ const recoverableErrorKeywords = [
206
+ 'failed to fetch',
207
+ 'network error',
208
+ 'connection failed',
209
+ 'connection reset',
210
+ 'timeout',
211
+ 'timed out',
212
+ 'unauthorized',
213
+ 'forbidden',
214
+ 'session',
215
+ 'authentication',
216
+ 'login',
217
+ 'expired'
218
+ ];
219
+ return recoverableErrorKeywords.some(keyword => errorMessage.includes(keyword));
220
+ }
221
+ //# sourceMappingURL=headless-request-with-recovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless-request-with-recovery.js","sourceRoot":"","sources":["../../../../server/engine/task/utils/headless-request-with-recovery.ts"],"names":[],"mappings":";;AAiBA,gFAmNC;AApOD,iDAA8C;AAC9C,iEAA4D;AAa5D;;GAEG;AACI,KAAK,UAAU,kCAAkC,CACtD,cAAsB,EACtB,OAA+B,EAC/B,OAAgD;IAEhD,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IACxC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,GAAG,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,OAAO,CAAA;IAExG,+BAA+B;IAC/B,IAAI,WAAW,GAAG,IAAI,CAAA;IACtB,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QACrB,WAAW,GAAG,IAAA,cAAM,EAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IACtC,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,sCAAiB,CAAC,2BAA2B,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IAC9F,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,eAAe,cAAc,uBAAuB,CAAC,CAAA;IACvE,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,kBAAkB,EAAE,WAAW,EAAE,qBAAqB,EAAE,eAAe,EAAE,GAAG,UAAU,CAAA;IAExG,IAAI,IAAI,GAAG,IAAI,CAAA;IACf,IAAI,SAAS,GAAG,IAAI,CAAA;IAEpB,SAAS;IACT,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,IAAI,CAAC;YACH,SAAS;YACT,IAAI,GAAG,MAAM,kBAAkB,EAAE,CAAA;YAEjC,uBAAuB;YACvB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YAC7B,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAA;YAC7C,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;gBACzC,MAAM,CAAC,IAAI,CAAC,gCAAgC,YAAY,EAAE,CAAC,CAAA;gBAC3D,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAA;YAC9D,CAAC;YAED,qBAAqB;YACrB,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBAChB,MAAM,cAAc,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,CAAA;gBAClD,IAAI,CAAC,cAAc,EAAE,CAAC;oBACpB,MAAM,CAAC,IAAI,CAAC,mCAAmC,cAAc,iCAAiC,CAAC,CAAA;oBAC/F,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;oBACvB,IAAI,GAAG,MAAM,qBAAqB,EAAE,CAAA;gBACtC,CAAC;YACH,CAAC;YAED,SAAS;YACT,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;YACnC,IAAI,WAAW,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;gBACnD,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;oBACrC,IAAI,WAAW,CAAC,GAAG,CAAC,KAAK,IAAI,IAAI,WAAW,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;wBAChE,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;oBACxD,CAAC;gBACH,CAAC,CAAC,CAAA;YACJ,CAAC;YAED,WAAW;YACX,MAAM,cAAc,GAAQ;gBAC1B,MAAM;gBACN,OAAO,EAAE;oBACP,GAAG,OAAO;iBACX;gBACD,WAAW,EAAE,SAAS;aACvB,CAAA;YAED,wBAAwB;YACxB,IAAI,WAAW,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC7D,IAAI,WAAW,EAAE,CAAC;oBAChB,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,WAAW,CAAA;oBACpD,QAAQ,WAAW,EAAE,CAAC;wBACpB,KAAK,YAAY;4BACf,cAAc,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;4BACjD,MAAK;wBACP,KAAK,kBAAkB,CAAC;wBACxB;4BACE,cAAc,CAAC,IAAI,GAAG,OAAO,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;4BACjG,MAAK;oBACT,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,kBAAkB,CAAA;oBAC3D,cAAc,CAAC,IAAI,GAAG,OAAO,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;gBACnG,CAAC;YACH,CAAC;YAED,sCAAsC;YACtC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAClC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE;gBACxB,IAAI,CAAC;oBACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;oBAE7C,MAAM,MAAM,GAAG;wBACb,EAAE,EAAE,QAAQ,CAAC,EAAE;wBACf,MAAM,EAAE,QAAQ,CAAC,MAAM;wBACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;wBAC/B,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;qBACxD,CAAA;oBAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;wBACjB,OAAO;4BACL,GAAG,MAAM;4BACT,KAAK,EAAE,QAAQ,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,UAAU,EAAE;4BACxD,IAAI,EAAE,IAAI;yBACX,CAAA;oBACH,CAAC;oBAED,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAA;oBAE9D,IAAI,WAAW,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;wBAC7C,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;oBACxC,CAAC;yBAAM,IAAI,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;wBACzC,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;oBACxC,CAAC;yBAAM,CAAC;wBACN,+BAA+B;wBAC/B,MAAM,CAAC,MAAM,CAAC,GAAG,IAAI,CAAA;oBACvB,CAAC;oBAED,OAAO,MAAM,CAAA;gBACf,CAAC;gBAAC,OAAO,UAAU,EAAE,CAAC;oBACpB,gBAAgB;oBAChB,OAAO;wBACL,EAAE,EAAE,KAAK;wBACT,MAAM,EAAE,CAAC;wBACT,UAAU,EAAE,eAAe;wBAC3B,OAAO,EAAE,EAAE;wBACX,KAAK,EAAE,UAAU,CAAC,OAAO,IAAI,iBAAiB;wBAC9C,IAAI,EAAE,IAAI;wBACV,YAAY,EAAE,IAAI;qBACnB,CAAA;gBACH,CAAC;YACH,CAAC,EACD,GAAG,CAAC,QAAQ,EAAE,EACd,cAAc,CACf,CAAA;YAED,gCAAgC;YAChC,IAAI,QAAQ,CAAC,YAAY,EAAE,CAAC;gBAC1B,IAAI,OAAO,GAAG,UAAU,EAAE,CAAC;oBACzB,MAAM,CAAC,IAAI,CAAC,2BAA2B,QAAQ,CAAC,KAAK,kBAAkB,OAAO,GAAG,CAAC,IAAI,UAAU,GAAG,CAAC,GAAG,CAAC,CAAA;oBACxG,IAAI,IAAI,EAAE,CAAC;wBACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;wBACvB,IAAI,GAAG,IAAI,CAAA;oBACb,CAAC;oBACD,SAAQ;gBACV,CAAC;qBAAM,CAAC;oBACN,IAAI,IAAI,EAAE,CAAC;wBACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;oBACzB,CAAC;oBACD,MAAM,IAAI,KAAK,CAAC,uBAAuB,UAAU,GAAG,CAAC,cAAc,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;gBACtF,CAAC;YACH,CAAC;YAED,mBAAmB;YACnB,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,qBAAqB,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3D,IAAI,OAAO,GAAG,UAAU,EAAE,CAAC;oBACzB,MAAM,CAAC,IAAI,CAAC,6BAA6B,QAAQ,CAAC,MAAM,mBAAmB,OAAO,GAAG,CAAC,IAAI,UAAU,GAAG,CAAC,GAAG,CAAC,CAAA;oBAC5G,IAAI,IAAI,EAAE,CAAC;wBACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;wBACvB,IAAI,GAAG,IAAI,CAAA;oBACb,CAAC;oBACD,SAAQ;gBACV,CAAC;qBAAM,CAAC;oBACN,IAAI,IAAI,EAAE,CAAC;wBACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;oBACzB,CAAC;oBACD,MAAM,IAAI,KAAK,CAAC,yBAAyB,UAAU,GAAG,CAAC,cAAc,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;gBACxF,CAAC;YACH,CAAC;YAED,aAAa;YACb,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;YACjC,CAAC;YAED,uBAAuB;YACvB,MAAM,MAAM,GAAG;gBACb,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,OAAO,EAAE,QAAQ,CAAC,OAAO;aAC1B,CAAA;YAED,IAAI,IAAI,EAAE,CAAC;gBACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;YACzB,CAAC;YAED,OAAO,MAAM,CAAA;QAEf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,SAAS,GAAG,KAAK,CAAA;YACjB,MAAM,CAAC,KAAK,CAAC,4BAA4B,OAAO,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;YAEtE,IAAI,IAAI,EAAE,CAAC;gBACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;gBACvB,IAAI,GAAG,IAAI,CAAA;YACb,CAAC;YAED,gCAAgC;YAChC,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;gBACzD,MAAM,KAAK,CAAA;YACb,CAAC;YAED,MAAM,CAAC,IAAI,CAAC,wBAAwB,OAAO,GAAG,CAAC,IAAI,UAAU,GAAG,CAAC,GAAG,CAAC,CAAA;QACvE,CAAC;IACH,CAAC;IAED,qCAAqC;IACrC,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;IACzB,CAAC;IACD,MAAM,SAAS,IAAI,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;AACzE,CAAC;AAED;;GAEG;AACH,SAAS,qBAAqB,CAAC,UAAkB;IAC/C,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;AACxC,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CAAC,KAAU;IACpC,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;IAEvD,iCAAiC;IACjC,MAAM,wBAAwB,GAAG;QAC/B,iBAAiB;QACjB,eAAe;QACf,mBAAmB;QACnB,kBAAkB;QAClB,SAAS;QACT,WAAW;QACX,cAAc;QACd,WAAW;QACX,SAAS;QACT,gBAAgB;QAChB,OAAO;QACP,SAAS;KACV,CAAA;IAED,OAAO,wBAAwB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;AACjF,CAAC","sourcesContent":["import { access } from '@things-factory/utils'\nimport { ConnectionManager } from '../../connection-manager'\n\nexport interface HeadlessRequestOptions {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n path: string\n headers?: Record<string, string>\n body?: any\n queryParams?: Record<string, any>\n maxRetries?: number\n accessor?: string // POST 요청에서 data 접근용\n contentType?: string // POST 요청에서 content-type 지정용\n}\n\n/**\n * 세션 회복 기능을 포함한 headless HTTP 요청 함수\n */\nexport async function executeHeadlessRequestWithRecovery(\n connectionName: string,\n options: HeadlessRequestOptions,\n context: { logger: any; data: any; domain: any }\n): Promise<any> {\n const { logger, data, domain } = context\n const { method, path, headers = {}, body, queryParams, maxRetries = 2, accessor, contentType } = options\n\n // accessor가 있으면 data에서 body 추출\n let requestBody = body\n if (accessor && data) {\n requestBody = access(accessor, data)\n }\n\n const connection = await ConnectionManager.getConnectionInstanceByName(domain, connectionName)\n if (!connection) {\n throw new Error(`Connection '${connectionName}' is not established.`)\n }\n\n const { endpoint, acquireSessionPage, releasePage, reAuthenticateSession, validateSession } = connection\n\n let page = null\n let lastError = null\n\n // 재시도 로직\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n // 페이지 획득\n page = await acquireSessionPage()\n \n // 페이지가 올바른 도메인에 있는지 확인\n const currentUrl = page.url()\n const targetDomain = new URL(endpoint).origin\n if (!currentUrl.startsWith(targetDomain)) {\n logger.info(`Navigating to target domain: ${targetDomain}`)\n await page.goto(targetDomain, { waitUntil: 'networkidle2' })\n }\n \n // 첫 번째 시도가 아니면 세션 검증\n if (attempt > 0) {\n const isSessionValid = await validateSession(page)\n if (!isSessionValid) {\n logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication`)\n await releasePage(page)\n page = await reAuthenticateSession()\n }\n }\n\n // URL 구성\n const url = new URL(path, endpoint)\n if (queryParams && typeof queryParams === 'object') {\n Object.keys(queryParams).forEach(key => {\n if (queryParams[key] !== null && queryParams[key] !== undefined) {\n url.searchParams.append(key, String(queryParams[key]))\n }\n })\n }\n\n // 요청 옵션 구성\n const requestOptions: any = {\n method,\n headers: {\n ...headers\n },\n credentials: 'include'\n }\n\n // Content-Type과 body 처리\n if (requestBody && ['POST', 'PUT', 'PATCH'].includes(method)) {\n if (contentType) {\n requestOptions.headers['content-type'] = contentType\n switch (contentType) {\n case 'text/plain':\n requestOptions.body = JSON.stringify(requestBody)\n break\n case 'application/json':\n default:\n requestOptions.body = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody)\n break\n }\n } else {\n requestOptions.headers['Content-Type'] = 'application/json'\n requestOptions.body = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody)\n }\n }\n\n // fetch 요청 실행 - try-catch로 네트워크 에러 처리\n const response = await page.evaluate(\n async (urlString, opts) => {\n try {\n const response = await fetch(urlString, opts)\n \n const result = {\n ok: response.ok,\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries())\n }\n\n if (!response.ok) {\n return {\n ...result,\n error: `HTTP ${response.status}: ${response.statusText}`,\n data: null\n }\n }\n\n const contentType = response.headers.get('content-type') || ''\n \n if (contentType.includes('application/json')) {\n result['data'] = await response.json()\n } else if (contentType.includes('text/')) {\n result['data'] = await response.text()\n } else {\n // 응답이 없는 경우 (204 No Content 등)\n result['data'] = null\n }\n\n return result\n } catch (fetchError) {\n // 네트워크 레벨 에러 처리\n return {\n ok: false,\n status: 0,\n statusText: 'Network Error',\n headers: {},\n error: fetchError.message || 'Failed to fetch',\n data: null,\n networkError: true\n }\n }\n },\n url.toString(),\n requestOptions\n )\n\n // 네트워크 에러 체크 (fetch 자체가 실패한 경우)\n if (response.networkError) {\n if (attempt < maxRetries) {\n logger.warn(`Network error detected: ${response.error}, retrying... (${attempt + 1}/${maxRetries + 1})`)\n if (page) {\n await releasePage(page)\n page = null\n }\n continue\n } else {\n if (page) {\n await releasePage(page)\n }\n throw new Error(`Network error after ${maxRetries + 1} attempts: ${response.error}`)\n }\n }\n\n // 세션 타임아웃 관련 에러 체크\n if (!response.ok && isSessionTimeoutError(response.status)) {\n if (attempt < maxRetries) {\n logger.warn(`Session timeout detected (${response.status}), retrying... (${attempt + 1}/${maxRetries + 1})`)\n if (page) {\n await releasePage(page)\n page = null\n }\n continue\n } else {\n if (page) {\n await releasePage(page)\n }\n throw new Error(`Session timeout after ${maxRetries + 1} attempts: ${response.error}`)\n }\n }\n\n // 기타 HTTP 에러\n if (!response.ok) {\n throw new Error(response.error)\n }\n\n // 성공 시 페이지 릴리즈 후 결과 반환\n const result = {\n data: response.data,\n status: response.status,\n headers: response.headers\n }\n \n if (page) {\n await releasePage(page)\n }\n \n return result\n\n } catch (error) {\n lastError = error\n logger.error(`Headless request attempt ${attempt + 1} failed:`, error)\n\n if (page) {\n await releasePage(page)\n page = null\n }\n\n // 세션 관련 에러가 아니거나 마지막 재시도면 에러 발생\n if (!isRecoverableError(error) || attempt === maxRetries) {\n throw error\n }\n\n logger.info(`Retrying request... (${attempt + 2}/${maxRetries + 1})`)\n }\n }\n\n // 모든 재시도가 실패한 경우 - 혹시 남은 페이지가 있으면 정리\n if (page) {\n await releasePage(page)\n }\n throw lastError || new Error('Request failed after all retry attempts')\n}\n\n/**\n * 세션 타임아웃 관련 HTTP 상태 코드인지 확인\n */\nfunction isSessionTimeoutError(statusCode: number): boolean {\n return [401, 403].includes(statusCode)\n}\n\n/**\n * 복구 가능한 에러인지 확인\n */\nfunction isRecoverableError(error: any): boolean {\n const errorMessage = error.message?.toLowerCase() || ''\n \n // 복구 가능한 에러 키워드 (네트워크, 세션/인증 관련)\n const recoverableErrorKeywords = [\n 'failed to fetch',\n 'network error',\n 'connection failed',\n 'connection reset',\n 'timeout',\n 'timed out',\n 'unauthorized',\n 'forbidden', \n 'session',\n 'authentication',\n 'login',\n 'expired'\n ]\n\n return recoverableErrorKeywords.some(keyword => errorMessage.includes(keyword))\n}"]}
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const api_1 = require("@things-factory/api");
4
+ const headless_pool_1 = require("../../engine/resource-pool/headless-pool");
5
+ const debug = require('debug')('things-factory:integration-base:restful:unstable:headless-pool-status');
6
+ /**
7
+ * 헤드리스 브라우저 풀의 현재 상태와 통계를 반환
8
+ */
9
+ api_1.restfulApiRouter.get('/unstable/headless-pool/status', async (context, next) => {
10
+ debug('get:/unstable/headless-pool/status');
11
+ try {
12
+ const stats = (0, headless_pool_1.getPoolStats)();
13
+ context.body = {
14
+ success: true,
15
+ data: {
16
+ poolStatus: {
17
+ size: stats.size,
18
+ available: stats.available,
19
+ borrowed: stats.borrowed,
20
+ pending: stats.pending,
21
+ max: stats.max,
22
+ min: stats.min,
23
+ utilizationRate: `${stats.utilizationRate}%`
24
+ },
25
+ statistics: {
26
+ totalCreated: stats.totalCreated,
27
+ totalDestroyed: stats.totalDestroyed,
28
+ totalAcquired: stats.totalAcquired,
29
+ totalReleased: stats.totalReleased,
30
+ currentlyAcquired: stats.currentlyAcquired,
31
+ averageAcquisitionTime: `${stats.averageAcquisitionTime}ms`,
32
+ lastActivity: stats.lastActivity
33
+ },
34
+ health: {
35
+ status: stats.available > 0 ? 'healthy' : 'warning',
36
+ message: stats.available > 0
37
+ ? 'Pool has available resources'
38
+ : 'No available resources in pool',
39
+ leakDetection: stats.totalAcquired - stats.totalReleased > stats.borrowed
40
+ ? 'potential leak detected'
41
+ : 'normal'
42
+ }
43
+ }
44
+ };
45
+ }
46
+ catch (error) {
47
+ debug('Error getting headless pool status:', error);
48
+ context.status = 500;
49
+ context.body = {
50
+ success: false,
51
+ error: error.message
52
+ };
53
+ }
54
+ });
55
+ /**
56
+ * 헤드리스 브라우저 풀을 강제로 정리
57
+ */
58
+ api_1.restfulApiRouter.post('/unstable/headless-pool/cleanup', async (context, next) => {
59
+ debug('post:/unstable/headless-pool/cleanup');
60
+ try {
61
+ await (0, headless_pool_1.forceCleanupPool)();
62
+ context.body = {
63
+ success: true,
64
+ message: 'All headless browsers cleaned up and pool reset successfully',
65
+ details: 'Pool has been reinitialized and is ready for new requests',
66
+ timestamp: new Date().toISOString()
67
+ };
68
+ }
69
+ catch (error) {
70
+ debug('Error during pool cleanup:', error);
71
+ context.status = 500;
72
+ context.body = {
73
+ success: false,
74
+ error: error.message
75
+ };
76
+ }
77
+ });
78
+ //# sourceMappingURL=headless-pool-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless-pool-status.js","sourceRoot":"","sources":["../../../server/restful/unstable/headless-pool-status.ts"],"names":[],"mappings":";;AAAA,6CAAgE;AAChE,4EAAyF;AAEzF,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,uEAAuE,CAAC,CAAA;AAEvG;;GAEG;AACH,sBAAM,CAAC,GAAG,CAAC,gCAAgC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IACnE,KAAK,CAAC,oCAAoC,CAAC,CAAA;IAE3C,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAA,4BAAY,GAAE,CAAA;QAE5B,OAAO,CAAC,IAAI,GAAG;YACb,OAAO,EAAE,IAAI;YACb,IAAI,EAAE;gBACJ,UAAU,EAAE;oBACV,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,SAAS,EAAE,KAAK,CAAC,SAAS;oBAC1B,QAAQ,EAAE,KAAK,CAAC,QAAQ;oBACxB,OAAO,EAAE,KAAK,CAAC,OAAO;oBACtB,GAAG,EAAE,KAAK,CAAC,GAAG;oBACd,GAAG,EAAE,KAAK,CAAC,GAAG;oBACd,eAAe,EAAE,GAAG,KAAK,CAAC,eAAe,GAAG;iBAC7C;gBACD,UAAU,EAAE;oBACV,YAAY,EAAE,KAAK,CAAC,YAAY;oBAChC,cAAc,EAAE,KAAK,CAAC,cAAc;oBACpC,aAAa,EAAE,KAAK,CAAC,aAAa;oBAClC,aAAa,EAAE,KAAK,CAAC,aAAa;oBAClC,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;oBAC1C,sBAAsB,EAAE,GAAG,KAAK,CAAC,sBAAsB,IAAI;oBAC3D,YAAY,EAAE,KAAK,CAAC,YAAY;iBACjC;gBACD,MAAM,EAAE;oBACN,MAAM,EAAE,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;oBACnD,OAAO,EAAE,KAAK,CAAC,SAAS,GAAG,CAAC;wBAC1B,CAAC,CAAC,8BAA8B;wBAChC,CAAC,CAAC,gCAAgC;oBACpC,aAAa,EAAE,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC,aAAa,GAAG,KAAK,CAAC,QAAQ;wBACvE,CAAC,CAAC,yBAAyB;wBAC3B,CAAC,CAAC,QAAQ;iBACb;aACF;SACF,CAAA;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,KAAK,CAAC,qCAAqC,EAAE,KAAK,CAAC,CAAA;QACnD,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;QACpB,OAAO,CAAC,IAAI,GAAG;YACb,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,KAAK,CAAC,OAAO;SACrB,CAAA;IACH,CAAC;AACH,CAAC,CAAC,CAAA;AAEF;;GAEG;AACH,sBAAM,CAAC,IAAI,CAAC,iCAAiC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IACrE,KAAK,CAAC,sCAAsC,CAAC,CAAA;IAE7C,IAAI,CAAC;QACH,MAAM,IAAA,gCAAgB,GAAE,CAAA;QAExB,OAAO,CAAC,IAAI,GAAG;YACb,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,8DAA8D;YACvE,OAAO,EAAE,2DAA2D;YACpE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAA;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAA;QAC1C,OAAO,CAAC,MAAM,GAAG,GAAG,CAAA;QACpB,OAAO,CAAC,IAAI,GAAG;YACb,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,KAAK,CAAC,OAAO;SACrB,CAAA;IACH,CAAC;AACH,CAAC,CAAC,CAAA","sourcesContent":["import { restfulApiRouter as router } from '@things-factory/api'\nimport { getPoolStats, forceCleanupPool } from '../../engine/resource-pool/headless-pool'\n\nconst debug = require('debug')('things-factory:integration-base:restful:unstable:headless-pool-status')\n\n/**\n * 헤드리스 브라우저 풀의 현재 상태와 통계를 반환\n */\nrouter.get('/unstable/headless-pool/status', async (context, next) => {\n debug('get:/unstable/headless-pool/status')\n\n try {\n const stats = getPoolStats()\n \n context.body = {\n success: true,\n data: {\n poolStatus: {\n size: stats.size,\n available: stats.available, \n borrowed: stats.borrowed,\n pending: stats.pending,\n max: stats.max,\n min: stats.min,\n utilizationRate: `${stats.utilizationRate}%`\n },\n statistics: {\n totalCreated: stats.totalCreated,\n totalDestroyed: stats.totalDestroyed,\n totalAcquired: stats.totalAcquired,\n totalReleased: stats.totalReleased,\n currentlyAcquired: stats.currentlyAcquired,\n averageAcquisitionTime: `${stats.averageAcquisitionTime}ms`,\n lastActivity: stats.lastActivity\n },\n health: {\n status: stats.available > 0 ? 'healthy' : 'warning',\n message: stats.available > 0 \n ? 'Pool has available resources' \n : 'No available resources in pool',\n leakDetection: stats.totalAcquired - stats.totalReleased > stats.borrowed \n ? 'potential leak detected' \n : 'normal'\n }\n }\n }\n } catch (error) {\n debug('Error getting headless pool status:', error)\n context.status = 500\n context.body = {\n success: false,\n error: error.message\n }\n }\n})\n\n/**\n * 헤드리스 브라우저 풀을 강제로 정리\n */\nrouter.post('/unstable/headless-pool/cleanup', async (context, next) => {\n debug('post:/unstable/headless-pool/cleanup')\n\n try {\n await forceCleanupPool()\n \n context.body = {\n success: true,\n message: 'All headless browsers cleaned up and pool reset successfully',\n details: 'Pool has been reinitialized and is ready for new requests',\n timestamp: new Date().toISOString()\n }\n } catch (error) {\n debug('Error during pool cleanup:', error)\n context.status = 500\n context.body = {\n success: false,\n error: error.message\n }\n }\n})"]}
@@ -5,3 +5,4 @@ import './scenario-instances';
5
5
  import './run-scenario';
6
6
  import './start-scenario';
7
7
  import './stop-scenario';
8
+ import './headless-pool-status';
@@ -7,4 +7,5 @@ require("./scenario-instances");
7
7
  require("./run-scenario");
8
8
  require("./start-scenario");
9
9
  require("./stop-scenario");
10
+ require("./headless-pool-status");
10
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/restful/unstable/index.ts"],"names":[],"mappings":";;AAAA,sBAAmB;AACnB,uBAAoB;AACpB,+BAA4B;AAC5B,gCAA6B;AAC7B,0BAAuB;AACvB,4BAAyB;AACzB,2BAAwB","sourcesContent":["import './scenario'\nimport './scenarios'\nimport './scenario-instance'\nimport './scenario-instances'\nimport './run-scenario'\nimport './start-scenario'\nimport './stop-scenario'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/restful/unstable/index.ts"],"names":[],"mappings":";;AAAA,sBAAmB;AACnB,uBAAoB;AACpB,+BAA4B;AAC5B,gCAA6B;AAC7B,0BAAuB;AACvB,4BAAyB;AACzB,2BAAwB;AACxB,kCAA+B","sourcesContent":["import './scenario'\nimport './scenarios'\nimport './scenario-instance'\nimport './scenario-instances'\nimport './run-scenario'\nimport './start-scenario'\nimport './stop-scenario'\nimport './headless-pool-status'\n"]}