@things-factory/integration-base 9.0.32 → 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.
- package/dist-server/engine/connector/headless-connector.d.ts +1 -0
- package/dist-server/engine/connector/headless-connector.js +117 -61
- package/dist-server/engine/connector/headless-connector.js.map +1 -1
- package/dist-server/engine/resource-pool/headless-pool.d.ts +29 -1
- package/dist-server/engine/resource-pool/headless-pool.js +70 -53
- package/dist-server/engine/resource-pool/headless-pool.js.map +1 -1
- package/dist-server/engine/task/headless-delete.js +9 -61
- package/dist-server/engine/task/headless-delete.js.map +1 -1
- package/dist-server/engine/task/headless-get.js +9 -62
- package/dist-server/engine/task/headless-get.js.map +1 -1
- package/dist-server/engine/task/headless-patch.js +11 -83
- package/dist-server/engine/task/headless-patch.js.map +1 -1
- package/dist-server/engine/task/headless-post.js +11 -83
- package/dist-server/engine/task/headless-post.js.map +1 -1
- package/dist-server/engine/task/headless-put.js +11 -83
- package/dist-server/engine/task/headless-put.js.map +1 -1
- package/dist-server/engine/task/utils/headless-request-with-recovery.d.ts +18 -0
- package/dist-server/engine/task/utils/headless-request-with-recovery.js +221 -0
- package/dist-server/engine/task/utils/headless-request-with-recovery.js.map +1 -0
- package/dist-server/restful/unstable/headless-pool-status.d.ts +1 -0
- package/dist-server/restful/unstable/headless-pool-status.js +78 -0
- package/dist-server/restful/unstable/headless-pool-status.js.map +1 -0
- package/dist-server/restful/unstable/index.d.ts +1 -0
- package/dist-server/restful/unstable/index.js +1 -0
- package/dist-server/restful/unstable/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- 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":"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","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 @@
|
|
1
|
+
export {};
|
@@ -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})"]}
|
@@ -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"]}
|