promptfoo 0.119.13 → 0.119.14
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/package.json +28 -26
- package/dist/src/app/assets/index-eJ2lMe94.js +51 -0
- package/dist/src/app/assets/{source-map-support-Bnh0UQ2S.js → source-map-support-1v4oeb7P.js} +1 -1
- package/dist/src/app/assets/sync-CtLQRuC1.js +1 -0
- package/dist/src/app/assets/{vendor-charts-T60Uk0Z3.js → vendor-charts-DnVv66VV.js} +1 -1
- package/dist/src/app/assets/{vendor-markdown-DLig-KJh.js → vendor-markdown-DCpQIyMA.js} +1 -1
- package/dist/src/app/assets/{vendor-mui-core-5BLaiG3c.js → vendor-mui-core-Boqnpf9f.js} +1 -1
- package/dist/src/app/assets/{vendor-mui-icons-fn39Fu2e.js → vendor-mui-icons-B8MqoVbj.js} +1 -1
- package/dist/src/app/assets/vendor-mui-x-CGSS6QHF.js +45 -0
- package/dist/src/app/assets/{vendor-utils-DYBMEuwX.js → vendor-utils-DdfHIEy8.js} +1 -1
- package/dist/src/app/index.html +7 -7
- package/dist/src/assertions/guardrails.d.ts +1 -1
- package/dist/src/assertions/guardrails.js +18 -9
- package/dist/src/assertions/index.d.ts +1 -1
- package/dist/src/assertions/index.js +9 -3
- package/dist/src/assertions/searchRubric.d.ts +3 -0
- package/dist/src/assertions/searchRubric.js +18 -0
- package/dist/src/commands/eval.js +1 -1
- package/dist/src/commands/modelScan.d.ts +7 -1
- package/dist/src/commands/modelScan.js +121 -59
- package/dist/src/database/index.d.ts +6 -0
- package/dist/src/database/index.js +11 -0
- package/dist/src/database/tables.d.ts +46 -24
- package/dist/src/envars.d.ts +17 -0
- package/dist/src/generated/constants.js +1 -1
- package/dist/src/logger.d.ts +5 -0
- package/dist/src/logger.js +28 -0
- package/dist/src/main.js +17 -6
- package/dist/src/matchers.d.ts +1 -0
- package/dist/src/matchers.js +80 -0
- package/dist/src/models/eval.d.ts +2 -1
- package/dist/src/models/eval.js +44 -2
- package/dist/src/prompts/grading.d.ts +1 -0
- package/dist/src/prompts/grading.js +26 -1
- package/dist/src/prompts/index.d.ts +1 -0
- package/dist/src/prompts/index.js +4 -1
- package/dist/src/providers/adaline.gateway.js +2 -2
- package/dist/src/providers/anthropic/defaults.d.ts +1 -1
- package/dist/src/providers/anthropic/defaults.js +15 -0
- package/dist/src/providers/azure/chat.d.ts +3 -1
- package/dist/src/providers/azure/chat.js +16 -3
- package/dist/src/providers/azure/defaults.js +660 -141
- package/dist/src/providers/azure/responses.d.ts +5 -0
- package/dist/src/providers/azure/responses.js +33 -4
- package/dist/src/providers/azure/types.d.ts +4 -0
- package/dist/src/providers/bedrock/agents.d.ts +1 -1
- package/dist/src/providers/bedrock/agents.js +2 -2
- package/dist/src/providers/bedrock/base.d.ts +40 -0
- package/dist/src/providers/bedrock/base.js +171 -0
- package/dist/src/providers/bedrock/converse.d.ts +146 -0
- package/dist/src/providers/bedrock/converse.js +1044 -0
- package/dist/src/providers/bedrock/index.d.ts +1 -34
- package/dist/src/providers/bedrock/index.js +4 -159
- package/dist/src/providers/bedrock/knowledgeBase.d.ts +1 -1
- package/dist/src/providers/bedrock/knowledgeBase.js +2 -2
- package/dist/src/providers/bedrock/nova-sonic.d.ts +2 -1
- package/dist/src/providers/bedrock/nova-sonic.js +2 -2
- package/dist/src/providers/claude-agent-sdk.d.ts +58 -1
- package/dist/src/providers/claude-agent-sdk.js +22 -1
- package/dist/src/providers/defaults.js +4 -0
- package/dist/src/providers/github/defaults.js +6 -6
- package/dist/src/providers/google/types.d.ts +25 -0
- package/dist/src/providers/google/util.d.ts +2 -0
- package/dist/src/providers/google/vertex.js +78 -22
- package/dist/src/providers/{groq.d.ts → groq/chat.d.ts} +26 -20
- package/dist/src/providers/groq/chat.js +79 -0
- package/dist/src/providers/groq/index.d.ts +5 -0
- package/dist/src/providers/groq/index.js +24 -0
- package/dist/src/providers/groq/responses.d.ts +106 -0
- package/dist/src/providers/groq/responses.js +64 -0
- package/dist/src/providers/groq/types.d.ts +44 -0
- package/dist/src/providers/groq/types.js +3 -0
- package/dist/src/providers/groq/util.d.ts +15 -0
- package/dist/src/providers/groq/util.js +28 -0
- package/dist/src/providers/mcp/client.d.ts +8 -0
- package/dist/src/providers/mcp/client.js +60 -10
- package/dist/src/providers/mcp/types.d.ts +21 -0
- package/dist/src/providers/openai/chatkit-pool.d.ts +114 -0
- package/dist/src/providers/openai/chatkit-pool.js +548 -0
- package/dist/src/providers/openai/chatkit-types.d.ts +73 -0
- package/dist/src/providers/openai/chatkit-types.js +3 -0
- package/dist/src/providers/openai/chatkit.d.ts +76 -0
- package/dist/src/providers/openai/chatkit.js +879 -0
- package/dist/src/providers/openai/codex-sdk.d.ts +109 -0
- package/dist/src/providers/openai/codex-sdk.js +346 -0
- package/dist/src/providers/openai/defaults.d.ts +2 -0
- package/dist/src/providers/openai/defaults.js +10 -4
- package/dist/src/providers/registry.js +48 -9
- package/dist/src/providers/responses/types.d.ts +1 -1
- package/dist/src/providers/sagemaker.d.ts +2 -2
- package/dist/src/providers/webSearchUtils.d.ts +17 -0
- package/dist/src/providers/webSearchUtils.js +169 -0
- package/dist/src/providers/xai/chat.d.ts +61 -0
- package/dist/src/providers/xai/chat.js +68 -3
- package/dist/src/providers/xai/responses.d.ts +189 -0
- package/dist/src/providers/xai/responses.js +268 -0
- package/dist/src/redteam/constants/plugins.d.ts +1 -1
- package/dist/src/redteam/constants/plugins.js +1 -1
- package/dist/src/redteam/constants/strategies.d.ts +1 -1
- package/dist/src/redteam/constants/strategies.js +1 -0
- package/dist/src/redteam/plugins/vlguard.d.ts +53 -4
- package/dist/src/redteam/plugins/vlguard.js +362 -46
- package/dist/src/redteam/providers/constants.d.ts +2 -2
- package/dist/src/redteam/providers/constants.js +2 -2
- package/dist/src/redteam/providers/crescendo/index.d.ts +1 -1
- package/dist/src/redteam/providers/crescendo/index.js +5 -3
- package/dist/src/redteam/providers/hydra/index.js +1 -1
- package/dist/src/server/routes/modelAudit.js +4 -4
- package/dist/src/share.js +4 -2
- package/dist/src/telemetry.js +44 -8
- package/dist/src/types/env.d.ts +3 -0
- package/dist/src/types/env.js +1 -0
- package/dist/src/types/index.d.ts +896 -615
- package/dist/src/types/index.js +1 -0
- package/dist/src/types/providers.d.ts +1 -0
- package/dist/src/types/tracing.d.ts +3 -0
- package/dist/src/util/database.d.ts +6 -4
- package/dist/src/util/file.js +6 -4
- package/dist/src/util/modelAuditCliParser.d.ts +4 -4
- package/dist/src/util/xlsx.js +52 -26
- package/dist/src/validators/providers.d.ts +142 -122
- package/dist/src/validators/providers.js +4 -6
- package/dist/src/validators/redteam.d.ts +36 -28
- package/dist/src/validators/redteam.js +9 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +28 -26
- package/dist/drizzle/CLAUDE.md +0 -65
- package/dist/src/app/assets/index-DifT6VGT.js +0 -51
- package/dist/src/app/assets/sync-Oo-W_Rbj.js +0 -1
- package/dist/src/app/assets/vendor-mui-x-C2xF-yiO.js +0 -45
- package/dist/src/providers/groq.js +0 -48
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatKit Browser Pool
|
|
3
|
+
*
|
|
4
|
+
* Manages a pool of browser contexts for concurrent ChatKit evaluations.
|
|
5
|
+
* This significantly reduces resource usage compared to spawning separate
|
|
6
|
+
* browsers for each test.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* - Single browser process (shared across all tests)
|
|
10
|
+
* - Multiple browser contexts (isolated like incognito windows)
|
|
11
|
+
* - Shared HTTP server with per-workflow template routing
|
|
12
|
+
* - Pages are workflow-specific (different workflows get different pages)
|
|
13
|
+
*/
|
|
14
|
+
import { type BrowserContext, type Page } from 'playwright';
|
|
15
|
+
interface PooledPage {
|
|
16
|
+
context: BrowserContext;
|
|
17
|
+
page: Page;
|
|
18
|
+
ready: boolean;
|
|
19
|
+
inUse: boolean;
|
|
20
|
+
templateKey: string;
|
|
21
|
+
}
|
|
22
|
+
interface ChatKitPoolConfig {
|
|
23
|
+
maxConcurrency: number;
|
|
24
|
+
headless: boolean;
|
|
25
|
+
serverPort?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Singleton browser pool for ChatKit evaluations.
|
|
29
|
+
* Supports high concurrency by reusing browser contexts.
|
|
30
|
+
* Each workflow gets its own isolated pages via template routing.
|
|
31
|
+
*/
|
|
32
|
+
export declare class ChatKitBrowserPool {
|
|
33
|
+
private static instance;
|
|
34
|
+
private static cleanupRegistered;
|
|
35
|
+
private browser;
|
|
36
|
+
private server;
|
|
37
|
+
private serverPort;
|
|
38
|
+
private pages;
|
|
39
|
+
private waitQueue;
|
|
40
|
+
private config;
|
|
41
|
+
private templates;
|
|
42
|
+
private initialized;
|
|
43
|
+
private initPromise;
|
|
44
|
+
private idleTimer;
|
|
45
|
+
private constructor();
|
|
46
|
+
/**
|
|
47
|
+
* Register process exit handlers to clean up browser resources
|
|
48
|
+
*/
|
|
49
|
+
private static registerCleanupHandlers;
|
|
50
|
+
/**
|
|
51
|
+
* Get the singleton pool instance
|
|
52
|
+
*/
|
|
53
|
+
static getInstance(config?: Partial<ChatKitPoolConfig>): ChatKitBrowserPool;
|
|
54
|
+
/**
|
|
55
|
+
* Reset the singleton (for testing)
|
|
56
|
+
*/
|
|
57
|
+
static resetInstance(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Generate a template key from workflow configuration.
|
|
60
|
+
* This ensures different workflows get isolated pages.
|
|
61
|
+
*/
|
|
62
|
+
static generateTemplateKey(workflowId: string, version?: string, userId?: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Register a template for a workflow configuration
|
|
65
|
+
*/
|
|
66
|
+
setTemplate(templateKey: string, html: string): void;
|
|
67
|
+
/**
|
|
68
|
+
* Initialize the pool - launches browser and creates server
|
|
69
|
+
*/
|
|
70
|
+
initialize(): Promise<void>;
|
|
71
|
+
private doInitialize;
|
|
72
|
+
/**
|
|
73
|
+
* Acquire a page from the pool for a specific template.
|
|
74
|
+
* Only returns pages configured for the requested template.
|
|
75
|
+
* Blocks if all pages are in use.
|
|
76
|
+
*/
|
|
77
|
+
acquirePage(templateKey: string): Promise<PooledPage>;
|
|
78
|
+
/**
|
|
79
|
+
* Release a page back to the pool
|
|
80
|
+
*/
|
|
81
|
+
releasePage(pooledPage: PooledPage): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Try to serve waiting requests by creating new pages if we have capacity
|
|
84
|
+
*/
|
|
85
|
+
private tryServeWaiters;
|
|
86
|
+
/**
|
|
87
|
+
* Schedule automatic shutdown if pool remains idle
|
|
88
|
+
*/
|
|
89
|
+
private scheduleIdleShutdown;
|
|
90
|
+
/**
|
|
91
|
+
* Cancel scheduled idle shutdown
|
|
92
|
+
*/
|
|
93
|
+
private cancelIdleTimer;
|
|
94
|
+
/**
|
|
95
|
+
* Create a new pooled page with ChatKit initialized for a specific template
|
|
96
|
+
*/
|
|
97
|
+
private createPooledPage;
|
|
98
|
+
private refreshPooledPage;
|
|
99
|
+
/**
|
|
100
|
+
* Get pool statistics
|
|
101
|
+
*/
|
|
102
|
+
getStats(): {
|
|
103
|
+
total: number;
|
|
104
|
+
inUse: number;
|
|
105
|
+
waiting: number;
|
|
106
|
+
templates: number;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Shutdown the pool and release all resources
|
|
110
|
+
*/
|
|
111
|
+
shutdown(): Promise<void>;
|
|
112
|
+
}
|
|
113
|
+
export {};
|
|
114
|
+
//# sourceMappingURL=chatkit-pool.d.ts.map
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ChatKit Browser Pool
|
|
4
|
+
*
|
|
5
|
+
* Manages a pool of browser contexts for concurrent ChatKit evaluations.
|
|
6
|
+
* This significantly reduces resource usage compared to spawning separate
|
|
7
|
+
* browsers for each test.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* - Single browser process (shared across all tests)
|
|
11
|
+
* - Multiple browser contexts (isolated like incognito windows)
|
|
12
|
+
* - Shared HTTP server with per-workflow template routing
|
|
13
|
+
* - Pages are workflow-specific (different workflows get different pages)
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
49
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
50
|
+
};
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.ChatKitBrowserPool = void 0;
|
|
53
|
+
const playwright_1 = require("playwright");
|
|
54
|
+
const http = __importStar(require("http"));
|
|
55
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
56
|
+
const providerRegistry_1 = require("../providerRegistry");
|
|
57
|
+
// Pool configuration constants
|
|
58
|
+
const CHATKIT_READY_TIMEOUT_MS = 60000;
|
|
59
|
+
const PAGE_REFRESH_TIMEOUT_MS = 60000;
|
|
60
|
+
const PAGE_ACQUIRE_TIMEOUT_MS = 120000;
|
|
61
|
+
const IDLE_SHUTDOWN_DELAY_MS = 5000; // Shutdown pool if idle for this long
|
|
62
|
+
/**
|
|
63
|
+
* Singleton browser pool for ChatKit evaluations.
|
|
64
|
+
* Supports high concurrency by reusing browser contexts.
|
|
65
|
+
* Each workflow gets its own isolated pages via template routing.
|
|
66
|
+
*/
|
|
67
|
+
class ChatKitBrowserPool {
|
|
68
|
+
constructor(config) {
|
|
69
|
+
this.browser = null;
|
|
70
|
+
this.server = null;
|
|
71
|
+
this.serverPort = 0;
|
|
72
|
+
this.pages = [];
|
|
73
|
+
this.waitQueue = [];
|
|
74
|
+
this.templates = new Map(); // templateKey -> HTML
|
|
75
|
+
this.initialized = false;
|
|
76
|
+
this.initPromise = null;
|
|
77
|
+
this.idleTimer = null;
|
|
78
|
+
this.config = config;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Register process exit handlers to clean up browser resources
|
|
82
|
+
*/
|
|
83
|
+
static registerCleanupHandlers() {
|
|
84
|
+
if (ChatKitBrowserPool.cleanupRegistered) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
ChatKitBrowserPool.cleanupRegistered = true;
|
|
88
|
+
const cleanup = () => {
|
|
89
|
+
if (ChatKitBrowserPool.instance) {
|
|
90
|
+
// Synchronous cleanup - close browser immediately
|
|
91
|
+
ChatKitBrowserPool.instance.shutdown().catch(() => { });
|
|
92
|
+
ChatKitBrowserPool.instance = null;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
// beforeExit fires when event loop is empty - allows cleanup of browser
|
|
96
|
+
// which otherwise keeps the event loop alive
|
|
97
|
+
process.on('beforeExit', () => {
|
|
98
|
+
if (ChatKitBrowserPool.instance) {
|
|
99
|
+
ChatKitBrowserPool.instance.shutdown().catch(() => { });
|
|
100
|
+
ChatKitBrowserPool.instance = null;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
process.on('exit', cleanup);
|
|
104
|
+
process.on('SIGINT', () => {
|
|
105
|
+
cleanup();
|
|
106
|
+
process.exit(130);
|
|
107
|
+
});
|
|
108
|
+
process.on('SIGTERM', () => {
|
|
109
|
+
cleanup();
|
|
110
|
+
process.exit(143);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get the singleton pool instance
|
|
115
|
+
*/
|
|
116
|
+
static getInstance(config) {
|
|
117
|
+
if (!ChatKitBrowserPool.instance) {
|
|
118
|
+
ChatKitBrowserPool.instance = new ChatKitBrowserPool({
|
|
119
|
+
maxConcurrency: config?.maxConcurrency ?? 4,
|
|
120
|
+
headless: config?.headless ?? true,
|
|
121
|
+
serverPort: config?.serverPort ?? 0,
|
|
122
|
+
});
|
|
123
|
+
ChatKitBrowserPool.registerCleanupHandlers();
|
|
124
|
+
// Register with providerRegistry for cleanup at end of evaluation
|
|
125
|
+
// This is cleaner than relying only on process exit handlers
|
|
126
|
+
const instance = ChatKitBrowserPool.instance;
|
|
127
|
+
providerRegistry_1.providerRegistry.register({
|
|
128
|
+
async shutdown() {
|
|
129
|
+
if (instance) {
|
|
130
|
+
await instance.shutdown();
|
|
131
|
+
ChatKitBrowserPool.instance = null;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else if (config) {
|
|
137
|
+
// Warn if different config is requested for existing instance
|
|
138
|
+
const existing = ChatKitBrowserPool.instance.config;
|
|
139
|
+
if ((config.maxConcurrency !== undefined &&
|
|
140
|
+
config.maxConcurrency !== existing.maxConcurrency) ||
|
|
141
|
+
(config.headless !== undefined && config.headless !== existing.headless)) {
|
|
142
|
+
logger_1.default.warn('[ChatKitPool] Pool already exists with different config, ignoring new config', {
|
|
143
|
+
existing: { maxConcurrency: existing.maxConcurrency, headless: existing.headless },
|
|
144
|
+
requested: { maxConcurrency: config.maxConcurrency, headless: config.headless },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return ChatKitBrowserPool.instance;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Reset the singleton (for testing)
|
|
152
|
+
*/
|
|
153
|
+
static resetInstance() {
|
|
154
|
+
if (ChatKitBrowserPool.instance) {
|
|
155
|
+
ChatKitBrowserPool.instance.shutdown().catch((err) => {
|
|
156
|
+
logger_1.default.debug('[ChatKitPool] Error during shutdown:', { error: String(err) });
|
|
157
|
+
});
|
|
158
|
+
ChatKitBrowserPool.instance = null;
|
|
159
|
+
}
|
|
160
|
+
// Don't reset cleanupRegistered - process handlers should only be registered once
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Generate a template key from workflow configuration.
|
|
164
|
+
* This ensures different workflows get isolated pages.
|
|
165
|
+
*/
|
|
166
|
+
static generateTemplateKey(workflowId, version, userId) {
|
|
167
|
+
// Use a simple concatenation - workflowId is the primary differentiator
|
|
168
|
+
// version and userId are included for completeness but workflowId is key
|
|
169
|
+
return `${workflowId}:${version || 'default'}:${userId || 'default'}`;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Register a template for a workflow configuration
|
|
173
|
+
*/
|
|
174
|
+
setTemplate(templateKey, html) {
|
|
175
|
+
const existing = this.templates.get(templateKey);
|
|
176
|
+
if (existing !== html) {
|
|
177
|
+
this.templates.set(templateKey, html);
|
|
178
|
+
logger_1.default.debug('[ChatKitPool] Registered template', { templateKey });
|
|
179
|
+
// Mark pages with this template as needing refresh if template changed
|
|
180
|
+
for (const page of this.pages) {
|
|
181
|
+
if (page.templateKey === templateKey) {
|
|
182
|
+
page.ready = false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Initialize the pool - launches browser and creates server
|
|
189
|
+
*/
|
|
190
|
+
async initialize() {
|
|
191
|
+
if (this.initialized) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Prevent multiple concurrent initializations
|
|
195
|
+
if (this.initPromise) {
|
|
196
|
+
return this.initPromise;
|
|
197
|
+
}
|
|
198
|
+
this.initPromise = this.doInitialize();
|
|
199
|
+
await this.initPromise;
|
|
200
|
+
this.initPromise = null;
|
|
201
|
+
}
|
|
202
|
+
async doInitialize() {
|
|
203
|
+
logger_1.default.debug('[ChatKitPool] Initializing browser pool', {
|
|
204
|
+
maxConcurrency: this.config.maxConcurrency,
|
|
205
|
+
});
|
|
206
|
+
// Create shared HTTP server with per-template routing
|
|
207
|
+
this.server = http.createServer((req, res) => {
|
|
208
|
+
// Extract template key from URL path: /template/<key>
|
|
209
|
+
const url = new URL(req.url || '/', `http://localhost`);
|
|
210
|
+
const pathParts = url.pathname.split('/').filter(Boolean);
|
|
211
|
+
if (pathParts[0] === 'template' && pathParts[1]) {
|
|
212
|
+
const templateKey = decodeURIComponent(pathParts[1]);
|
|
213
|
+
const template = this.templates.get(templateKey);
|
|
214
|
+
if (template) {
|
|
215
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
216
|
+
res.end(template);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Fallback: 404 for unknown templates
|
|
221
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
222
|
+
res.end('Template not found');
|
|
223
|
+
});
|
|
224
|
+
await new Promise((resolve, reject) => {
|
|
225
|
+
this.server.once('error', (err) => {
|
|
226
|
+
reject(new Error(`Failed to start ChatKit pool server: ${err.message}`));
|
|
227
|
+
});
|
|
228
|
+
this.server.listen(this.config.serverPort, () => {
|
|
229
|
+
const address = this.server.address();
|
|
230
|
+
this.serverPort = typeof address === 'object' ? address?.port || 0 : 0;
|
|
231
|
+
logger_1.default.debug('[ChatKitPool] Server started', { port: this.serverPort });
|
|
232
|
+
resolve();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
// Launch single browser
|
|
236
|
+
try {
|
|
237
|
+
this.browser = await playwright_1.chromium.launch({
|
|
238
|
+
headless: this.config.headless,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
243
|
+
if (msg.includes("Executable doesn't exist")) {
|
|
244
|
+
throw new Error('Playwright browser not installed. Run: npx playwright install chromium');
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
this.initialized = true;
|
|
249
|
+
logger_1.default.debug('[ChatKitPool] Browser pool initialized');
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Acquire a page from the pool for a specific template.
|
|
253
|
+
* Only returns pages configured for the requested template.
|
|
254
|
+
* Blocks if all pages are in use.
|
|
255
|
+
*/
|
|
256
|
+
async acquirePage(templateKey) {
|
|
257
|
+
// Cancel any pending idle shutdown since we're being used
|
|
258
|
+
this.cancelIdleTimer();
|
|
259
|
+
await this.initialize();
|
|
260
|
+
// Ensure template is registered
|
|
261
|
+
if (!this.templates.has(templateKey)) {
|
|
262
|
+
throw new Error(`Template not registered: ${templateKey}. Call setTemplate first.`);
|
|
263
|
+
}
|
|
264
|
+
// Try to find an available ready page with matching template
|
|
265
|
+
const available = this.pages.find((p) => !p.inUse && p.ready && p.templateKey === templateKey);
|
|
266
|
+
if (available) {
|
|
267
|
+
available.inUse = true;
|
|
268
|
+
logger_1.default.debug('[ChatKitPool] Acquired existing page', {
|
|
269
|
+
templateKey,
|
|
270
|
+
poolSize: this.pages.length,
|
|
271
|
+
});
|
|
272
|
+
return available;
|
|
273
|
+
}
|
|
274
|
+
// Try to find an idle page with matching template that needs refresh
|
|
275
|
+
const needsRefresh = this.pages.find((p) => !p.inUse && !p.ready && p.templateKey === templateKey);
|
|
276
|
+
if (needsRefresh) {
|
|
277
|
+
await this.refreshPooledPage(needsRefresh);
|
|
278
|
+
needsRefresh.inUse = true;
|
|
279
|
+
logger_1.default.debug('[ChatKitPool] Acquired and refreshed page', {
|
|
280
|
+
templateKey,
|
|
281
|
+
poolSize: this.pages.length,
|
|
282
|
+
});
|
|
283
|
+
return needsRefresh;
|
|
284
|
+
}
|
|
285
|
+
// Create new page if under limit
|
|
286
|
+
if (this.pages.length < this.config.maxConcurrency) {
|
|
287
|
+
const pooledPage = await this.createPooledPage(templateKey);
|
|
288
|
+
pooledPage.inUse = true;
|
|
289
|
+
this.pages.push(pooledPage);
|
|
290
|
+
logger_1.default.debug('[ChatKitPool] Created new page', {
|
|
291
|
+
templateKey,
|
|
292
|
+
poolSize: this.pages.length,
|
|
293
|
+
});
|
|
294
|
+
return pooledPage;
|
|
295
|
+
}
|
|
296
|
+
// Wait for a page with matching template to become available
|
|
297
|
+
logger_1.default.debug('[ChatKitPool] Waiting for available page', {
|
|
298
|
+
templateKey,
|
|
299
|
+
poolSize: this.pages.length,
|
|
300
|
+
waiting: this.waitQueue.length + 1,
|
|
301
|
+
});
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
const timeoutId = setTimeout(() => {
|
|
304
|
+
const index = this.waitQueue.findIndex((w) => w.resolve === wrappedResolve);
|
|
305
|
+
if (index >= 0) {
|
|
306
|
+
this.waitQueue.splice(index, 1);
|
|
307
|
+
}
|
|
308
|
+
reject(new Error(`Timeout waiting for available page after ${PAGE_ACQUIRE_TIMEOUT_MS}ms. ` +
|
|
309
|
+
`Pool has ${this.pages.length} pages, ${this.pages.filter((p) => p.inUse).length} in use.`));
|
|
310
|
+
}, PAGE_ACQUIRE_TIMEOUT_MS);
|
|
311
|
+
const wrappedResolve = (page) => {
|
|
312
|
+
clearTimeout(timeoutId);
|
|
313
|
+
resolve(page);
|
|
314
|
+
};
|
|
315
|
+
this.waitQueue.push({ templateKey, resolve: wrappedResolve });
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Release a page back to the pool
|
|
320
|
+
*/
|
|
321
|
+
async releasePage(pooledPage) {
|
|
322
|
+
const originalTemplateKey = pooledPage.templateKey;
|
|
323
|
+
// Keep inUse=true during refresh to prevent race conditions
|
|
324
|
+
// Reset the page for next use by reloading
|
|
325
|
+
try {
|
|
326
|
+
await this.refreshPooledPage(pooledPage);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
logger_1.default.warn('[ChatKitPool] Failed to reset page, recreating', { error });
|
|
330
|
+
// Page is broken, remove it from the pool
|
|
331
|
+
const index = this.pages.indexOf(pooledPage);
|
|
332
|
+
if (index >= 0) {
|
|
333
|
+
this.pages.splice(index, 1);
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
await pooledPage.context.close();
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// Ignore close errors
|
|
340
|
+
}
|
|
341
|
+
// Create replacement - if this fails, we just reduce pool size
|
|
342
|
+
// The pool will recover by creating new pages on demand
|
|
343
|
+
try {
|
|
344
|
+
const newPage = await this.createPooledPage(originalTemplateKey);
|
|
345
|
+
this.pages.push(newPage);
|
|
346
|
+
pooledPage = newPage;
|
|
347
|
+
}
|
|
348
|
+
catch (createError) {
|
|
349
|
+
logger_1.default.warn('[ChatKitPool] Failed to create replacement page', { error: createError });
|
|
350
|
+
// Pool size is now reduced - try to serve any waiting requests by creating pages for them
|
|
351
|
+
// This prevents deadlock when all pages fail
|
|
352
|
+
await this.tryServeWaiters();
|
|
353
|
+
this.scheduleIdleShutdown();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// If someone is waiting for this template, give them the page directly
|
|
358
|
+
const waiterIndex = this.waitQueue.findIndex((w) => w.templateKey === pooledPage.templateKey);
|
|
359
|
+
if (waiterIndex >= 0) {
|
|
360
|
+
const waiter = this.waitQueue.splice(waiterIndex, 1)[0];
|
|
361
|
+
pooledPage.inUse = true;
|
|
362
|
+
waiter.resolve(pooledPage);
|
|
363
|
+
this.cancelIdleTimer();
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
// No one waiting for this template, mark as available
|
|
367
|
+
pooledPage.inUse = false;
|
|
368
|
+
// Check if we can serve waiters for other templates now that we have capacity
|
|
369
|
+
await this.tryServeWaiters();
|
|
370
|
+
this.scheduleIdleShutdown();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Try to serve waiting requests by creating new pages if we have capacity
|
|
375
|
+
*/
|
|
376
|
+
async tryServeWaiters() {
|
|
377
|
+
// Process waiters while we have capacity and waiters exist
|
|
378
|
+
while (this.waitQueue.length > 0 && this.pages.length < this.config.maxConcurrency) {
|
|
379
|
+
const waiter = this.waitQueue.shift();
|
|
380
|
+
if (!waiter) {
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const newPage = await this.createPooledPage(waiter.templateKey);
|
|
385
|
+
newPage.inUse = true;
|
|
386
|
+
this.pages.push(newPage);
|
|
387
|
+
waiter.resolve(newPage);
|
|
388
|
+
logger_1.default.debug('[ChatKitPool] Created page for waiting request', {
|
|
389
|
+
templateKey: waiter.templateKey,
|
|
390
|
+
poolSize: this.pages.length,
|
|
391
|
+
remainingWaiters: this.waitQueue.length,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
logger_1.default.warn('[ChatKitPool] Failed to create page for waiter', {
|
|
396
|
+
templateKey: waiter.templateKey,
|
|
397
|
+
error,
|
|
398
|
+
});
|
|
399
|
+
// Put waiter back at the front of the queue to retry later
|
|
400
|
+
this.waitQueue.unshift(waiter);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Schedule automatic shutdown if pool remains idle
|
|
407
|
+
*/
|
|
408
|
+
scheduleIdleShutdown() {
|
|
409
|
+
// Cancel any existing timer
|
|
410
|
+
this.cancelIdleTimer();
|
|
411
|
+
// Check if pool is completely idle (no pages in use, no waiters)
|
|
412
|
+
const inUseCount = this.pages.filter((p) => p.inUse).length;
|
|
413
|
+
if (inUseCount === 0 && this.waitQueue.length === 0 && this.pages.length > 0) {
|
|
414
|
+
logger_1.default.debug('[ChatKitPool] Pool idle, scheduling shutdown', {
|
|
415
|
+
delay: IDLE_SHUTDOWN_DELAY_MS,
|
|
416
|
+
});
|
|
417
|
+
this.idleTimer = setTimeout(() => {
|
|
418
|
+
// Double-check still idle
|
|
419
|
+
const stillInUse = this.pages.filter((p) => p.inUse).length;
|
|
420
|
+
if (stillInUse === 0 && this.waitQueue.length === 0) {
|
|
421
|
+
logger_1.default.debug('[ChatKitPool] Auto-shutting down idle pool');
|
|
422
|
+
this.shutdown().catch((err) => {
|
|
423
|
+
logger_1.default.debug('[ChatKitPool] Error during idle shutdown', { error: String(err) });
|
|
424
|
+
});
|
|
425
|
+
ChatKitBrowserPool.instance = null;
|
|
426
|
+
}
|
|
427
|
+
}, IDLE_SHUTDOWN_DELAY_MS);
|
|
428
|
+
// Don't let the timer prevent process exit
|
|
429
|
+
if (this.idleTimer.unref) {
|
|
430
|
+
this.idleTimer.unref();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Cancel scheduled idle shutdown
|
|
436
|
+
*/
|
|
437
|
+
cancelIdleTimer() {
|
|
438
|
+
if (this.idleTimer) {
|
|
439
|
+
clearTimeout(this.idleTimer);
|
|
440
|
+
this.idleTimer = null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Create a new pooled page with ChatKit initialized for a specific template
|
|
445
|
+
*/
|
|
446
|
+
async createPooledPage(templateKey) {
|
|
447
|
+
if (!this.browser) {
|
|
448
|
+
throw new Error('Browser not initialized');
|
|
449
|
+
}
|
|
450
|
+
const context = await this.browser.newContext({
|
|
451
|
+
viewport: { width: 800, height: 600 },
|
|
452
|
+
});
|
|
453
|
+
// Set longer default timeout to prevent Playwright's 30s default from interfering
|
|
454
|
+
context.setDefaultTimeout(120000);
|
|
455
|
+
try {
|
|
456
|
+
const page = await context.newPage();
|
|
457
|
+
// Navigate to the template-specific URL
|
|
458
|
+
const templateUrl = `http://localhost:${this.serverPort}/template/${encodeURIComponent(templateKey)}`;
|
|
459
|
+
await page.goto(templateUrl, {
|
|
460
|
+
waitUntil: 'domcontentloaded',
|
|
461
|
+
});
|
|
462
|
+
await page.waitForFunction(() => window.__state?.ready === true, {
|
|
463
|
+
timeout: CHATKIT_READY_TIMEOUT_MS,
|
|
464
|
+
});
|
|
465
|
+
return {
|
|
466
|
+
context,
|
|
467
|
+
page,
|
|
468
|
+
ready: true,
|
|
469
|
+
inUse: false,
|
|
470
|
+
templateKey,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
// Clean up context if page creation/initialization fails
|
|
475
|
+
try {
|
|
476
|
+
await context.close();
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// Ignore close errors
|
|
480
|
+
}
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async refreshPooledPage(pooledPage) {
|
|
485
|
+
logger_1.default.debug('[ChatKitPool] Refreshing page', { timeout: PAGE_REFRESH_TIMEOUT_MS });
|
|
486
|
+
await pooledPage.page.reload({ waitUntil: 'domcontentloaded' });
|
|
487
|
+
await pooledPage.page.waitForFunction(() => window.__state?.ready === true, {
|
|
488
|
+
timeout: PAGE_REFRESH_TIMEOUT_MS,
|
|
489
|
+
});
|
|
490
|
+
pooledPage.ready = true;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Get pool statistics
|
|
494
|
+
*/
|
|
495
|
+
getStats() {
|
|
496
|
+
return {
|
|
497
|
+
total: this.pages.length,
|
|
498
|
+
inUse: this.pages.filter((p) => p.inUse).length,
|
|
499
|
+
waiting: this.waitQueue.length,
|
|
500
|
+
templates: this.templates.size,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Shutdown the pool and release all resources
|
|
505
|
+
*/
|
|
506
|
+
async shutdown() {
|
|
507
|
+
logger_1.default.debug('[ChatKitPool] Shutting down');
|
|
508
|
+
// Cancel any pending idle timer
|
|
509
|
+
this.cancelIdleTimer();
|
|
510
|
+
// Clear pending waiters - they will timeout via PAGE_ACQUIRE_TIMEOUT_MS
|
|
511
|
+
if (this.waitQueue.length > 0) {
|
|
512
|
+
logger_1.default.debug('[ChatKitPool] Clearing pending waiters', { count: this.waitQueue.length });
|
|
513
|
+
this.waitQueue = [];
|
|
514
|
+
}
|
|
515
|
+
// Close all contexts
|
|
516
|
+
for (const pooledPage of this.pages) {
|
|
517
|
+
try {
|
|
518
|
+
await pooledPage.context.close();
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
// Ignore errors during shutdown
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
this.pages = [];
|
|
525
|
+
// Close browser
|
|
526
|
+
if (this.browser) {
|
|
527
|
+
try {
|
|
528
|
+
await this.browser.close();
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
// Ignore errors
|
|
532
|
+
}
|
|
533
|
+
this.browser = null;
|
|
534
|
+
}
|
|
535
|
+
// Close server
|
|
536
|
+
if (this.server) {
|
|
537
|
+
this.server.close();
|
|
538
|
+
this.server = null;
|
|
539
|
+
}
|
|
540
|
+
this.initialized = false;
|
|
541
|
+
this.templates.clear();
|
|
542
|
+
logger_1.default.debug('[ChatKitPool] Shutdown complete');
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
exports.ChatKitBrowserPool = ChatKitBrowserPool;
|
|
546
|
+
ChatKitBrowserPool.instance = null;
|
|
547
|
+
ChatKitBrowserPool.cleanupRegistered = false;
|
|
548
|
+
//# sourceMappingURL=chatkit-pool.js.map
|