camofox-browser 2.1.1 → 2.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +150 -0
- package/README.md +310 -34
- package/dist/src/cli/commands/content.d.ts.map +1 -1
- package/dist/src/cli/commands/content.js +37 -0
- package/dist/src/cli/commands/content.js.map +1 -1
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +21 -4
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/interaction.d.ts.map +1 -1
- package/dist/src/cli/commands/interaction.js +5 -14
- package/dist/src/cli/commands/interaction.js.map +1 -1
- package/dist/src/cli/commands/navigation.d.ts.map +1 -1
- package/dist/src/cli/commands/navigation.js +12 -6
- package/dist/src/cli/commands/navigation.js.map +1 -1
- package/dist/src/cli/commands/server.d.ts.map +1 -1
- package/dist/src/cli/commands/server.js +9 -3
- package/dist/src/cli/commands/server.js.map +1 -1
- package/dist/src/cli/commands/session.d.ts.map +1 -1
- package/dist/src/cli/commands/session.js +23 -5
- package/dist/src/cli/commands/session.js.map +1 -1
- package/dist/src/cli/server/manager.d.ts +1 -0
- package/dist/src/cli/server/manager.d.ts.map +1 -1
- package/dist/src/cli/server/manager.js +7 -12
- package/dist/src/cli/server/manager.js.map +1 -1
- package/dist/src/middleware/lifecycle-activity.d.ts +9 -0
- package/dist/src/middleware/lifecycle-activity.d.ts.map +1 -0
- package/dist/src/middleware/lifecycle-activity.js +21 -0
- package/dist/src/middleware/lifecycle-activity.js.map +1 -0
- package/dist/src/openapi/spec.d.ts +4 -0
- package/dist/src/openapi/spec.d.ts.map +1 -0
- package/dist/src/openapi/spec.js +730 -0
- package/dist/src/openapi/spec.js.map +1 -0
- package/dist/src/routes/core.d.ts.map +1 -1
- package/dist/src/routes/core.js +545 -58
- package/dist/src/routes/core.js.map +1 -1
- package/dist/src/routes/docs.d.ts +3 -0
- package/dist/src/routes/docs.d.ts.map +1 -0
- package/dist/src/routes/docs.js +23 -0
- package/dist/src/routes/docs.js.map +1 -0
- package/dist/src/routes/openclaw.d.ts.map +1 -1
- package/dist/src/routes/openclaw.js +317 -90
- package/dist/src/routes/openclaw.js.map +1 -1
- package/dist/src/server.js +55 -4
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/context-pool.d.ts +21 -4
- package/dist/src/services/context-pool.d.ts.map +1 -1
- package/dist/src/services/context-pool.js +290 -71
- package/dist/src/services/context-pool.js.map +1 -1
- package/dist/src/services/download.d.ts +2 -0
- package/dist/src/services/download.d.ts.map +1 -1
- package/dist/src/services/download.js +110 -80
- package/dist/src/services/download.js.map +1 -1
- package/dist/src/services/lifecycle-controller.d.ts +40 -0
- package/dist/src/services/lifecycle-controller.d.ts.map +1 -0
- package/dist/src/services/lifecycle-controller.js +106 -0
- package/dist/src/services/lifecycle-controller.js.map +1 -0
- package/dist/src/services/resource-extractor.d.ts +1 -0
- package/dist/src/services/resource-extractor.d.ts.map +1 -1
- package/dist/src/services/resource-extractor.js +7 -0
- package/dist/src/services/resource-extractor.js.map +1 -1
- package/dist/src/services/session.d.ts +109 -4
- package/dist/src/services/session.d.ts.map +1 -1
- package/dist/src/services/session.js +622 -64
- package/dist/src/services/session.js.map +1 -1
- package/dist/src/services/structured-extractor.d.ts +39 -0
- package/dist/src/services/structured-extractor.d.ts.map +1 -0
- package/dist/src/services/structured-extractor.js +487 -0
- package/dist/src/services/structured-extractor.js.map +1 -0
- package/dist/src/services/tab.d.ts +30 -3
- package/dist/src/services/tab.d.ts.map +1 -1
- package/dist/src/services/tab.js +872 -124
- package/dist/src/services/tab.js.map +1 -1
- package/dist/src/services/tracing.d.ts +7 -0
- package/dist/src/services/tracing.d.ts.map +1 -1
- package/dist/src/services/tracing.js +200 -19
- package/dist/src/services/tracing.js.map +1 -1
- package/dist/src/services/vnc.d.ts.map +1 -1
- package/dist/src/services/vnc.js +5 -3
- package/dist/src/services/vnc.js.map +1 -1
- package/dist/src/services/youtube.js +1 -1
- package/dist/src/services/youtube.js.map +1 -1
- package/dist/src/types.d.ts +71 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/config.d.ts +79 -3
- package/dist/src/utils/config.d.ts.map +1 -1
- package/dist/src/utils/config.js +145 -3
- package/dist/src/utils/config.js.map +1 -1
- package/dist/src/utils/presets.d.ts.map +1 -1
- package/dist/src/utils/presets.js +3 -1
- package/dist/src/utils/presets.js.map +1 -1
- package/dist/src/utils/proxy-profiles.d.ts +18 -0
- package/dist/src/utils/proxy-profiles.d.ts.map +1 -0
- package/dist/src/utils/proxy-profiles.js +197 -0
- package/dist/src/utils/proxy-profiles.js.map +1 -0
- package/dist/src/utils/sidecar-version.d.ts +12 -0
- package/dist/src/utils/sidecar-version.d.ts.map +1 -0
- package/dist/src/utils/sidecar-version.js +63 -0
- package/dist/src/utils/sidecar-version.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/openclaw.plugin.json +39 -0
- package/package.json +16 -4
- package/plugin.ts +949 -0
|
@@ -1,16 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.MAX_TABS_PER_SESSION = exports.MAX_SESSIONS = exports.SESSION_TIMEOUT_MS = void 0;
|
|
4
7
|
exports.__getUserConcurrencyStateForTests = __getUserConcurrencyStateForTests;
|
|
8
|
+
exports.__getSessionsMapForTests = __getSessionsMapForTests;
|
|
5
9
|
exports.withUserLimit = withUserLimit;
|
|
10
|
+
exports.cleanupSessionsForUserId = cleanupSessionsForUserId;
|
|
6
11
|
exports.normalizeUserId = normalizeUserId;
|
|
7
12
|
exports.getSessionMapKey = getSessionMapKey;
|
|
13
|
+
exports.getEstablishedSessionProfile = getEstablishedSessionProfile;
|
|
14
|
+
exports.hasDefaultSessionProfileRuntime = hasDefaultSessionProfileRuntime;
|
|
15
|
+
exports.claimDefaultSessionProfileRuntime = claimDefaultSessionProfileRuntime;
|
|
16
|
+
exports.clearDefaultSessionProfileClaim = clearDefaultSessionProfileClaim;
|
|
17
|
+
exports.getSessionProfileLaunchSettings = getSessionProfileLaunchSettings;
|
|
18
|
+
exports.getCanonicalProfile = getCanonicalProfile;
|
|
19
|
+
exports.hasCanonicalProfile = hasCanonicalProfile;
|
|
20
|
+
exports.acquireFirstCreateMutex = acquireFirstCreateMutex;
|
|
21
|
+
exports.commitCanonicalProfile = commitCanonicalProfile;
|
|
22
|
+
exports.rollbackCanonicalMutex = rollbackCanonicalMutex;
|
|
23
|
+
exports.createCanonicalProfile = createCanonicalProfile;
|
|
24
|
+
exports.clearCanonicalProfile = clearCanonicalProfile;
|
|
25
|
+
exports.establishSessionProfile = establishSessionProfile;
|
|
26
|
+
exports.clearSessionProfile = clearSessionProfile;
|
|
27
|
+
exports.acquireSessionProfileCreateMutex = acquireSessionProfileCreateMutex;
|
|
28
|
+
exports.waitForSessionProfileCreate = waitForSessionProfileCreate;
|
|
29
|
+
exports.rollbackSessionProfileRuntime = rollbackSessionProfileRuntime;
|
|
8
30
|
exports.getSessionsForUser = getSessionsForUser;
|
|
9
31
|
exports.getAllSessions = getAllSessions;
|
|
10
32
|
exports.countTotalTabsForSessions = countTotalTabsForSessions;
|
|
33
|
+
exports.getLifecycleSessionSnapshot = getLifecycleSessionSnapshot;
|
|
34
|
+
exports.getSessionsSnapshot = getSessionsSnapshot;
|
|
11
35
|
exports.getTabGroup = getTabGroup;
|
|
12
36
|
exports.unindexSessionTabs = unindexSessionTabs;
|
|
13
37
|
exports.findTabById = findTabById;
|
|
38
|
+
exports.createStagedSession = createStagedSession;
|
|
39
|
+
exports.commitStagedFirstUse = commitStagedFirstUse;
|
|
40
|
+
exports.rollbackStagedFirstUse = rollbackStagedFirstUse;
|
|
14
41
|
exports.getSession = getSession;
|
|
15
42
|
exports.indexTab = indexTab;
|
|
16
43
|
exports.unindexTab = unindexTab;
|
|
@@ -19,9 +46,12 @@ exports.closeSessionsForUser = closeSessionsForUser;
|
|
|
19
46
|
exports.closeAllSessions = closeAllSessions;
|
|
20
47
|
exports.startCleanupInterval = startCleanupInterval;
|
|
21
48
|
exports.stopCleanupInterval = stopCleanupInterval;
|
|
49
|
+
exports.runLifecycleIdleCleanup = runLifecycleIdleCleanup;
|
|
50
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
22
51
|
const logging_1 = require("../middleware/logging");
|
|
23
52
|
const tab_1 = require("./tab");
|
|
24
53
|
const config_1 = require("../utils/config");
|
|
54
|
+
const presets_1 = require("../utils/presets");
|
|
25
55
|
const context_pool_1 = require("./context-pool");
|
|
26
56
|
const download_1 = require("./download");
|
|
27
57
|
const health_1 = require("./health");
|
|
@@ -31,12 +61,25 @@ const CONFIG = (0, config_1.loadConfig)();
|
|
|
31
61
|
// userId -> { context, tabGroups: Map<sessionKey, Map<tabId, TabState>>, lastAccess }
|
|
32
62
|
// Note: sessionKey was previously called listItemId - both are accepted for backward compatibility
|
|
33
63
|
const sessions = new Map();
|
|
64
|
+
const sessionOwners = new Map();
|
|
34
65
|
// sessionKey -> in-flight session creation promise
|
|
35
66
|
// Avoids storing partially-initialized sessions (e.g., context: null cast) and dedupes concurrent creates.
|
|
36
67
|
const launchingSessions = new Map();
|
|
68
|
+
const launchingSessionOwners = new Map();
|
|
69
|
+
const lifecycleIdleClosures = new Map();
|
|
37
70
|
// tabId -> sessions map key
|
|
38
71
|
// Persistent profiles are keyed only by userId, while tab endpoints only get tabId.
|
|
39
72
|
const tabSessionIndex = new Map();
|
|
73
|
+
// Canonical per-user profile: stores resolved overrides from the first core POST /tabs.
|
|
74
|
+
// Survives passive context eviction; cleared only on explicit session close/cleanup.
|
|
75
|
+
const canonicalProfiles = new Map();
|
|
76
|
+
// Session profiles keyed by userId::sessionKey to track separate proxy/geo profiles per session
|
|
77
|
+
const sessionProfiles = new Map();
|
|
78
|
+
const defaultSessionProfileClaims = new Map();
|
|
79
|
+
// Per-user mutex covering the entire first-create lifecycle (establishment -> tab commit).
|
|
80
|
+
// Prevents sibling requests from observing provisional canonical state.
|
|
81
|
+
const firstCreateMutexes = new Map();
|
|
82
|
+
const sessionProfileCreateMutexes = new Map();
|
|
40
83
|
const userConcurrency = new Map();
|
|
41
84
|
function __getUserConcurrencyStateForTests(userId) {
|
|
42
85
|
const key = String(userId).toLowerCase().trim();
|
|
@@ -45,6 +88,43 @@ function __getUserConcurrencyStateForTests(userId) {
|
|
|
45
88
|
return null;
|
|
46
89
|
return { active: state.active, queueLength: state.queue.length };
|
|
47
90
|
}
|
|
91
|
+
function __getSessionsMapForTests() {
|
|
92
|
+
return sessions;
|
|
93
|
+
}
|
|
94
|
+
function beginLifecycleIdleClosure(userId) {
|
|
95
|
+
const key = normalizeUserId(userId);
|
|
96
|
+
let state = lifecycleIdleClosures.get(key);
|
|
97
|
+
if (!state) {
|
|
98
|
+
let resolve;
|
|
99
|
+
const promise = new Promise((r) => {
|
|
100
|
+
resolve = r;
|
|
101
|
+
});
|
|
102
|
+
state = { pending: 0, promise, resolve };
|
|
103
|
+
lifecycleIdleClosures.set(key, state);
|
|
104
|
+
}
|
|
105
|
+
state.pending++;
|
|
106
|
+
let released = false;
|
|
107
|
+
return () => {
|
|
108
|
+
if (released)
|
|
109
|
+
return;
|
|
110
|
+
released = true;
|
|
111
|
+
const current = lifecycleIdleClosures.get(key);
|
|
112
|
+
if (!current)
|
|
113
|
+
return;
|
|
114
|
+
current.pending--;
|
|
115
|
+
if (current.pending <= 0) {
|
|
116
|
+
lifecycleIdleClosures.delete(key);
|
|
117
|
+
current.resolve();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function waitForLifecycleIdleClosure(userId) {
|
|
122
|
+
const key = normalizeUserId(userId);
|
|
123
|
+
const pending = lifecycleIdleClosures.get(key);
|
|
124
|
+
if (pending) {
|
|
125
|
+
await pending.promise;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
48
128
|
async function withUserLimit(userId, maxConcurrent, operation, operationTimeoutMs) {
|
|
49
129
|
const key = String(userId).toLowerCase().trim();
|
|
50
130
|
let state = userConcurrency.get(key);
|
|
@@ -96,52 +176,371 @@ async function withUserLimit(userId, maxConcurrent, operation, operationTimeoutM
|
|
|
96
176
|
}
|
|
97
177
|
}
|
|
98
178
|
}
|
|
99
|
-
function cleanupSessionsForUserId(userId, reason) {
|
|
100
|
-
const
|
|
179
|
+
function cleanupSessionsForUserId(userId, reason, clearCanonical = true, options = {}) {
|
|
180
|
+
const key = normalizeUserId(userId);
|
|
181
|
+
const allowInternalSessionKey = options.allowInternalSessionKey ?? true;
|
|
182
|
+
const ownerKeys = new Set();
|
|
101
183
|
// If a session is currently being created, drop our reference so callers don't keep a stale placeholder.
|
|
102
|
-
launchingSessions.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
184
|
+
for (const launchKey of Array.from(launchingSessions.keys())) {
|
|
185
|
+
if (isLaunchingSessionKeyForCleanupKey(launchKey, key, allowInternalSessionKey)) {
|
|
186
|
+
ownerKeys.add(launchingSessionOwners.get(launchKey) ?? key);
|
|
187
|
+
launchingSessions.delete(launchKey);
|
|
188
|
+
launchingSessionOwners.delete(launchKey);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const [sessionKey, session] of Array.from(sessions.entries())) {
|
|
192
|
+
if (!isSessionMapKeyForCleanupKey(sessionKey, key, allowInternalSessionKey))
|
|
193
|
+
continue;
|
|
194
|
+
ownerKeys.add(sessionOwners.get(sessionKey) ?? sessionKey);
|
|
195
|
+
unindexSessionTabs(session);
|
|
196
|
+
sessions.delete(sessionKey);
|
|
197
|
+
sessionOwners.delete(sessionKey);
|
|
198
|
+
(0, logging_1.log)('info', 'session cleaned up', { userId: key, sessionKey, reason });
|
|
106
199
|
}
|
|
107
|
-
|
|
108
|
-
|
|
200
|
+
if (ownerKeys.size === 0)
|
|
201
|
+
ownerKeys.add(key);
|
|
202
|
+
for (const ownerKey of ownerKeys) {
|
|
203
|
+
void (0, vnc_1.stopVnc)(ownerKey).catch(() => { });
|
|
204
|
+
try {
|
|
205
|
+
(0, download_1.cleanupUserDownloads)(ownerKey);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// ignore cleanup errors
|
|
209
|
+
}
|
|
210
|
+
(0, tracing_1.cleanupTracing)(ownerKey);
|
|
211
|
+
clearDefaultSessionProfileClaimsForUser(ownerKey);
|
|
109
212
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
213
|
+
if (clearCanonical) {
|
|
214
|
+
canonicalProfiles.delete(key);
|
|
215
|
+
clearDefaultSessionProfileClaimsForUser(key);
|
|
216
|
+
const mutex = firstCreateMutexes.get(key);
|
|
217
|
+
if (mutex) {
|
|
218
|
+
mutex.resolve(false);
|
|
219
|
+
firstCreateMutexes.delete(key);
|
|
220
|
+
}
|
|
221
|
+
// Also clear all session profiles for this user
|
|
222
|
+
const profileKeysToDelete = [];
|
|
223
|
+
for (const [profileKey, profile] of sessionProfiles.entries()) {
|
|
224
|
+
if (profile.userId === key) {
|
|
225
|
+
profileKeysToDelete.push(profileKey);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const profileKey of profileKeysToDelete) {
|
|
229
|
+
sessionProfiles.delete(profileKey);
|
|
230
|
+
}
|
|
231
|
+
for (const [profileKey, mutex] of Array.from(sessionProfileCreateMutexes.entries())) {
|
|
232
|
+
if (mutex.userId === key) {
|
|
233
|
+
mutex.resolve(false);
|
|
234
|
+
sessionProfileCreateMutexes.delete(profileKey);
|
|
235
|
+
}
|
|
116
236
|
}
|
|
117
237
|
}
|
|
118
|
-
userConcurrency.delete(
|
|
238
|
+
userConcurrency.delete(key);
|
|
119
239
|
}
|
|
120
240
|
context_pool_1.contextPool.onEvict((userId) => {
|
|
121
|
-
cleanupSessionsForUserId(userId, 'context_evicted');
|
|
241
|
+
cleanupSessionsForUserId(userId, 'context_evicted', false);
|
|
122
242
|
// Note: the pool will close the context; session cleanup only removes dead Page references.
|
|
123
243
|
});
|
|
124
|
-
exports.SESSION_TIMEOUT_MS =
|
|
125
|
-
exports.MAX_SESSIONS =
|
|
126
|
-
exports.MAX_TABS_PER_SESSION =
|
|
244
|
+
exports.SESSION_TIMEOUT_MS = CONFIG.sessionTimeoutMs;
|
|
245
|
+
exports.MAX_SESSIONS = CONFIG.maxSessions;
|
|
246
|
+
exports.MAX_TABS_PER_SESSION = CONFIG.maxTabsPerSession;
|
|
127
247
|
function normalizeUserId(userId) {
|
|
128
248
|
return String(userId);
|
|
129
249
|
}
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
250
|
+
function encodeKeyComponent(value) {
|
|
251
|
+
return Buffer.from(String(value), 'utf16le').toString('base64url');
|
|
252
|
+
}
|
|
253
|
+
function userSessionMapKey(userId) {
|
|
254
|
+
return `u:${encodeKeyComponent(normalizeUserId(userId))}`;
|
|
255
|
+
}
|
|
256
|
+
function sessionOverlayKey(userId, sessionKey) {
|
|
257
|
+
return `o:${encodeKeyComponent(normalizeUserId(userId))}:${encodeKeyComponent(sessionKey)}`;
|
|
258
|
+
}
|
|
259
|
+
function clearDefaultSessionProfileClaimsForUser(userId) {
|
|
260
|
+
const key = normalizeUserId(userId);
|
|
261
|
+
for (const [claimKey, claim] of Array.from(defaultSessionProfileClaims.entries())) {
|
|
262
|
+
if (claim.userId === key) {
|
|
263
|
+
defaultSessionProfileClaims.delete(claimKey);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function isSessionMapKeyForUser(sessionMapKey, userKey) {
|
|
268
|
+
return sessionMapKey === userSessionMapKey(userKey) || sessionOwners.get(sessionMapKey) === userKey;
|
|
269
|
+
}
|
|
270
|
+
function isSessionMapKeyForCleanupKey(sessionMapKey, cleanupKey, allowInternalSessionKey = true) {
|
|
271
|
+
return (allowInternalSessionKey && sessionMapKey === cleanupKey) || isSessionMapKeyForUser(sessionMapKey, cleanupKey);
|
|
272
|
+
}
|
|
273
|
+
function isLaunchingSessionKeyForCleanupKey(sessionMapKey, cleanupKey, allowInternalSessionKey = true) {
|
|
274
|
+
return ((allowInternalSessionKey && sessionMapKey === cleanupKey) ||
|
|
275
|
+
sessionMapKey === userSessionMapKey(cleanupKey) ||
|
|
276
|
+
launchingSessionOwners.get(sessionMapKey) === cleanupKey);
|
|
277
|
+
}
|
|
278
|
+
// Backward compatible version - takes contextOverrides instead of session profile
|
|
279
|
+
function getSessionMapKey(userId, contextOverridesOrSessionKey, profileSignature) {
|
|
280
|
+
// New signature: (userId, sessionKey, profileSignature)
|
|
281
|
+
if (typeof contextOverridesOrSessionKey === 'string') {
|
|
282
|
+
const sessionKey = contextOverridesOrSessionKey;
|
|
283
|
+
if (profileSignature) {
|
|
284
|
+
return `p:${encodeKeyComponent(normalizeUserId(userId))}:${encodeKeyComponent(sessionKey)}:${encodeKeyComponent(profileSignature)}`;
|
|
285
|
+
}
|
|
286
|
+
return `s:${encodeKeyComponent(normalizeUserId(userId))}:${encodeKeyComponent(sessionKey)}`;
|
|
287
|
+
}
|
|
288
|
+
// Old signature: (userId, contextOverrides) - backward compatibility
|
|
289
|
+
// This maintains the user-scoped behavior for existing routes
|
|
290
|
+
void contextOverridesOrSessionKey;
|
|
291
|
+
return userSessionMapKey(userId);
|
|
292
|
+
}
|
|
293
|
+
function getEstablishedSessionProfile(userId, sessionKey) {
|
|
294
|
+
return sessionProfiles.get(sessionOverlayKey(userId, sessionKey));
|
|
295
|
+
}
|
|
296
|
+
function hasDefaultSessionProfileRuntime(userId, sessionKey) {
|
|
297
|
+
const key = normalizeUserId(userId);
|
|
298
|
+
if (defaultSessionProfileClaims.has(sessionOverlayKey(key, sessionKey)))
|
|
299
|
+
return true;
|
|
300
|
+
const defaultSession = sessions.get(userSessionMapKey(key));
|
|
301
|
+
return defaultSession?.tabGroups.has(sessionKey) ?? false;
|
|
302
|
+
}
|
|
303
|
+
function claimDefaultSessionProfileRuntime(userId, sessionKey) {
|
|
304
|
+
if (getEstablishedSessionProfile(userId, sessionKey))
|
|
305
|
+
return false;
|
|
306
|
+
if (hasDefaultSessionProfileRuntime(userId, sessionKey))
|
|
307
|
+
return false;
|
|
308
|
+
const key = normalizeUserId(userId);
|
|
309
|
+
defaultSessionProfileClaims.set(sessionOverlayKey(key, sessionKey), { userId: key, sessionKey });
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
function clearDefaultSessionProfileClaim(userId, sessionKey) {
|
|
313
|
+
defaultSessionProfileClaims.delete(sessionOverlayKey(userId, sessionKey));
|
|
314
|
+
}
|
|
315
|
+
function contextOverridesFromProfile(profile) {
|
|
316
|
+
const overrides = {};
|
|
317
|
+
if (profile.locale !== undefined)
|
|
318
|
+
overrides.locale = profile.locale;
|
|
319
|
+
if (profile.timezoneId !== undefined)
|
|
320
|
+
overrides.timezoneId = profile.timezoneId;
|
|
321
|
+
if (profile.geolocation !== undefined)
|
|
322
|
+
overrides.geolocation = profile.geolocation;
|
|
323
|
+
if (profile.viewport !== undefined)
|
|
324
|
+
overrides.viewport = profile.viewport;
|
|
325
|
+
return Object.keys(overrides).length > 0 ? overrides : null;
|
|
326
|
+
}
|
|
327
|
+
function getSessionProfileLaunchSettings(userId, profileKey) {
|
|
328
|
+
const key = normalizeUserId(userId);
|
|
329
|
+
if (profileKey === userSessionMapKey(key)) {
|
|
330
|
+
const canonical = canonicalProfiles.get(key);
|
|
331
|
+
return canonical ? { contextOverrides: canonical.resolvedOverrides, proxy: null } : undefined;
|
|
332
|
+
}
|
|
333
|
+
for (const profile of sessionProfiles.values()) {
|
|
334
|
+
if (profile.userId !== key)
|
|
335
|
+
continue;
|
|
336
|
+
if (getSessionMapKey(key, profile.sessionKey, profile.signature) !== profileKey)
|
|
337
|
+
continue;
|
|
338
|
+
return {
|
|
339
|
+
contextOverrides: contextOverridesFromProfile(profile.resolvedProfile),
|
|
340
|
+
proxy: profile.resolvedProfile.proxy,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
function resolveRuntimeSessionProfile(userId, sessionKey, contextOverrides) {
|
|
346
|
+
const userKey = normalizeUserId(userId);
|
|
347
|
+
if (!sessionKey) {
|
|
348
|
+
return {
|
|
349
|
+
sessionMapKey: userSessionMapKey(userKey),
|
|
350
|
+
profileKey: userSessionMapKey(userKey),
|
|
351
|
+
contextOverrides,
|
|
352
|
+
resolvedProxy: null,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const established = getEstablishedSessionProfile(userId, sessionKey);
|
|
356
|
+
if (!established) {
|
|
357
|
+
return {
|
|
358
|
+
sessionMapKey: userSessionMapKey(userKey),
|
|
359
|
+
profileKey: userSessionMapKey(userKey),
|
|
360
|
+
contextOverrides,
|
|
361
|
+
resolvedProxy: null,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const profileKey = getSessionMapKey(userId, sessionKey, established.signature);
|
|
365
|
+
return {
|
|
366
|
+
sessionMapKey: profileKey,
|
|
367
|
+
profileKey,
|
|
368
|
+
contextOverrides: contextOverridesFromProfile(established.resolvedProfile),
|
|
369
|
+
resolvedProxy: established.resolvedProfile.proxy,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function getCanonicalProfile(userId) {
|
|
373
|
+
return canonicalProfiles.get(normalizeUserId(userId));
|
|
374
|
+
}
|
|
375
|
+
function hasCanonicalProfile(userId) {
|
|
376
|
+
return canonicalProfiles.has(normalizeUserId(userId));
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Try to acquire the first-create mutex for a user.
|
|
380
|
+
* Returns { acquired: true } if we are the first creator (mutex acquired).
|
|
381
|
+
* Returns { acquired: false, wait: Promise<boolean> } if another request is first-creating.
|
|
382
|
+
* The promise resolves to true (committed) or false (rolled back).
|
|
383
|
+
* If canonical already exists (committed), returns { acquired: false, wait: resolved-true }.
|
|
384
|
+
*/
|
|
385
|
+
function acquireFirstCreateMutex(userId) {
|
|
386
|
+
const key = normalizeUserId(userId);
|
|
387
|
+
if (canonicalProfiles.has(key)) {
|
|
388
|
+
return { acquired: false, wait: Promise.resolve(true) };
|
|
389
|
+
}
|
|
390
|
+
const existing = firstCreateMutexes.get(key);
|
|
391
|
+
if (existing) {
|
|
392
|
+
return { acquired: false, wait: existing.promise };
|
|
393
|
+
}
|
|
394
|
+
let resolve;
|
|
395
|
+
const promise = new Promise((r) => {
|
|
396
|
+
resolve = r;
|
|
397
|
+
});
|
|
398
|
+
firstCreateMutexes.set(key, { promise, resolve });
|
|
399
|
+
return { acquired: true };
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Commit: store the canonical profile and release the mutex (signaling success to waiters).
|
|
403
|
+
*/
|
|
404
|
+
function commitCanonicalProfile(userId, resolved) {
|
|
405
|
+
const key = normalizeUserId(userId);
|
|
406
|
+
const profile = {
|
|
407
|
+
resolvedOverrides: resolved,
|
|
408
|
+
hash: (0, presets_1.contextHash)(resolved),
|
|
409
|
+
establishedAt: Date.now(),
|
|
410
|
+
};
|
|
411
|
+
canonicalProfiles.set(key, profile);
|
|
412
|
+
const mutex = firstCreateMutexes.get(key);
|
|
413
|
+
if (mutex) {
|
|
414
|
+
mutex.resolve(true);
|
|
415
|
+
firstCreateMutexes.delete(key);
|
|
416
|
+
}
|
|
417
|
+
(0, logging_1.log)('info', 'canonical profile committed', { userId: key, hash: profile.hash });
|
|
418
|
+
return profile;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Rollback: release the mutex (signaling failure to waiters). No canonical is stored.
|
|
422
|
+
*/
|
|
423
|
+
function rollbackCanonicalMutex(userId) {
|
|
424
|
+
const key = normalizeUserId(userId);
|
|
425
|
+
const mutex = firstCreateMutexes.get(key);
|
|
426
|
+
if (mutex) {
|
|
427
|
+
mutex.resolve(false);
|
|
428
|
+
firstCreateMutexes.delete(key);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Create a CanonicalProfile object without storing it (for hash comparison during first-create).
|
|
433
|
+
*/
|
|
434
|
+
function createCanonicalProfile(resolved) {
|
|
435
|
+
return {
|
|
436
|
+
resolvedOverrides: resolved,
|
|
437
|
+
hash: (0, presets_1.contextHash)(resolved),
|
|
438
|
+
establishedAt: Date.now(),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function clearCanonicalProfile(userId) {
|
|
442
|
+
const key = normalizeUserId(userId);
|
|
443
|
+
canonicalProfiles.delete(key);
|
|
444
|
+
const mutex = firstCreateMutexes.get(key);
|
|
445
|
+
if (mutex) {
|
|
446
|
+
mutex.resolve(false);
|
|
447
|
+
firstCreateMutexes.delete(key);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Store or validate a session profile for a specific userId + sessionKey combination.
|
|
452
|
+
* Returns the established profile if successful.
|
|
453
|
+
* Throws if a conflicting profile is already established for the same userId + sessionKey.
|
|
454
|
+
*/
|
|
455
|
+
function establishSessionProfile(userId, sessionKey, profile) {
|
|
456
|
+
const key = sessionOverlayKey(userId, sessionKey);
|
|
457
|
+
const existing = sessionProfiles.get(key);
|
|
458
|
+
if (existing) {
|
|
459
|
+
if (existing.signature !== profile.signature) {
|
|
460
|
+
throw new Error('Session profile conflict');
|
|
461
|
+
}
|
|
462
|
+
return existing;
|
|
463
|
+
}
|
|
464
|
+
const established = {
|
|
465
|
+
userId: normalizeUserId(userId),
|
|
466
|
+
sessionKey,
|
|
467
|
+
signature: profile.signature,
|
|
468
|
+
resolvedProfile: profile,
|
|
469
|
+
establishedAt: Date.now(),
|
|
470
|
+
};
|
|
471
|
+
sessionProfiles.set(key, established);
|
|
472
|
+
(0, logging_1.log)('info', 'session profile established', {
|
|
473
|
+
userId: established.userId,
|
|
474
|
+
sessionKey,
|
|
475
|
+
signature: profile.signature,
|
|
476
|
+
});
|
|
477
|
+
return established;
|
|
478
|
+
}
|
|
479
|
+
function clearSessionProfile(userId, sessionKey) {
|
|
480
|
+
const key = sessionOverlayKey(userId, sessionKey);
|
|
481
|
+
sessionProfiles.delete(key);
|
|
482
|
+
}
|
|
483
|
+
function acquireSessionProfileCreateMutex(userId, sessionKey, signature) {
|
|
484
|
+
const key = sessionOverlayKey(userId, sessionKey);
|
|
485
|
+
const existing = sessionProfileCreateMutexes.get(key);
|
|
486
|
+
if (existing) {
|
|
487
|
+
return { acquired: false, wait: existing.promise };
|
|
488
|
+
}
|
|
489
|
+
let resolve;
|
|
490
|
+
const promise = new Promise((r) => {
|
|
491
|
+
resolve = r;
|
|
492
|
+
});
|
|
493
|
+
const mutex = {
|
|
494
|
+
userId: normalizeUserId(userId),
|
|
495
|
+
sessionKey,
|
|
496
|
+
signature,
|
|
497
|
+
promise,
|
|
498
|
+
resolve,
|
|
499
|
+
};
|
|
500
|
+
sessionProfileCreateMutexes.set(key, mutex);
|
|
501
|
+
let released = false;
|
|
502
|
+
return {
|
|
503
|
+
acquired: true,
|
|
504
|
+
release: (committed) => {
|
|
505
|
+
if (released)
|
|
506
|
+
return;
|
|
507
|
+
released = true;
|
|
508
|
+
const current = sessionProfileCreateMutexes.get(key);
|
|
509
|
+
if (current === mutex) {
|
|
510
|
+
current.resolve(committed);
|
|
511
|
+
sessionProfileCreateMutexes.delete(key);
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
async function waitForSessionProfileCreate(userId, sessionKey) {
|
|
517
|
+
const pending = sessionProfileCreateMutexes.get(sessionOverlayKey(userId, sessionKey));
|
|
518
|
+
if (pending) {
|
|
519
|
+
await pending.promise.catch(() => false);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function rollbackSessionProfileRuntime(userId, sessionKey, profileSignature) {
|
|
523
|
+
const overlayKey = sessionOverlayKey(userId, sessionKey);
|
|
524
|
+
const existingProfile = sessionProfiles.get(overlayKey);
|
|
525
|
+
if (existingProfile?.signature === profileSignature) {
|
|
526
|
+
sessionProfiles.delete(overlayKey);
|
|
527
|
+
}
|
|
528
|
+
const sessionMapKey = getSessionMapKey(userId, sessionKey, profileSignature);
|
|
529
|
+
const session = sessions.get(sessionMapKey);
|
|
530
|
+
if (session) {
|
|
531
|
+
unindexSessionTabs(session);
|
|
532
|
+
sessions.delete(sessionMapKey);
|
|
533
|
+
}
|
|
534
|
+
sessionOwners.delete(sessionMapKey);
|
|
535
|
+
launchingSessions.delete(sessionMapKey);
|
|
536
|
+
launchingSessionOwners.delete(sessionMapKey);
|
|
537
|
+
await context_pool_1.contextPool.closeContext(sessionMapKey).catch(() => { });
|
|
134
538
|
}
|
|
135
539
|
function getSessionsForUser(userId) {
|
|
136
540
|
if (userId === undefined || userId === null)
|
|
137
541
|
return [];
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
for (const [key, session] of sessions) {
|
|
141
|
-
if (key === prefix || key.startsWith(prefix + ':'))
|
|
142
|
-
out.push([key, session]);
|
|
143
|
-
}
|
|
144
|
-
return out;
|
|
542
|
+
const key = normalizeUserId(userId);
|
|
543
|
+
return Array.from(sessions.entries()).filter(([sessionKey]) => isSessionMapKeyForUser(sessionKey, key));
|
|
145
544
|
}
|
|
146
545
|
function getAllSessions() {
|
|
147
546
|
return sessions;
|
|
@@ -155,6 +554,16 @@ function countTotalTabsForSessions(sessionsForUser) {
|
|
|
155
554
|
}
|
|
156
555
|
return totalTabs;
|
|
157
556
|
}
|
|
557
|
+
function getLifecycleSessionSnapshot() {
|
|
558
|
+
return {
|
|
559
|
+
liveSessions: sessions.size,
|
|
560
|
+
liveTabs: countTotalTabsForSessions(),
|
|
561
|
+
stagedCreates: launchingSessions.size,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function getSessionsSnapshot() {
|
|
565
|
+
return new Map(sessions);
|
|
566
|
+
}
|
|
158
567
|
function getTabGroup(session, sessionKey) {
|
|
159
568
|
let group = session.tabGroups.get(sessionKey);
|
|
160
569
|
if (!group) {
|
|
@@ -187,10 +596,10 @@ function unindexSessionTabs(session) {
|
|
|
187
596
|
function findTabById(tabId, userId) {
|
|
188
597
|
if (userId === undefined || userId === null)
|
|
189
598
|
return null;
|
|
190
|
-
const
|
|
599
|
+
const key = normalizeUserId(userId);
|
|
191
600
|
const indexedKey = tabSessionIndex.get(tabId);
|
|
192
601
|
if (indexedKey) {
|
|
193
|
-
if (!(indexedKey
|
|
602
|
+
if (!isSessionMapKeyForUser(indexedKey, key)) {
|
|
194
603
|
return null;
|
|
195
604
|
}
|
|
196
605
|
const session = sessions.get(indexedKey);
|
|
@@ -201,64 +610,125 @@ function findTabById(tabId, userId) {
|
|
|
201
610
|
}
|
|
202
611
|
tabSessionIndex.delete(tabId);
|
|
203
612
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
613
|
+
const defaultSessionKey = userSessionMapKey(key);
|
|
614
|
+
const session = sessions.get(defaultSessionKey);
|
|
615
|
+
if (!session)
|
|
616
|
+
return null;
|
|
617
|
+
const found = findTab(session, tabId);
|
|
618
|
+
if (found) {
|
|
619
|
+
tabSessionIndex.set(tabId, defaultSessionKey);
|
|
620
|
+
return { sessionKey: defaultSessionKey, session, ...found };
|
|
212
621
|
}
|
|
213
622
|
return null;
|
|
214
623
|
}
|
|
215
|
-
|
|
216
|
-
const key = getSessionMapKey(userId, contextOverrides);
|
|
217
|
-
let session = sessions.get(key);
|
|
624
|
+
function buildBrowserContextOptions(contextOverrides, hasSessionProxy = false) {
|
|
218
625
|
const resolved = contextOverrides || {};
|
|
219
626
|
const contextOptions = {
|
|
220
627
|
viewport: resolved.viewport || { width: 1280, height: 720 },
|
|
221
628
|
permissions: ['geolocation'],
|
|
222
629
|
};
|
|
223
630
|
const hasOverrides = !!(contextOverrides &&
|
|
224
|
-
(contextOverrides.locale !== undefined ||
|
|
631
|
+
(contextOverrides.locale !== undefined ||
|
|
632
|
+
contextOverrides.timezoneId !== undefined ||
|
|
633
|
+
contextOverrides.geolocation !== undefined));
|
|
225
634
|
// With proxy+geoip, camoufox auto-configures locale/timezone/geo from proxy IP.
|
|
226
635
|
// If caller explicitly supplies overrides, apply them even when proxy is active.
|
|
227
|
-
if (!CONFIG.proxy.host || hasOverrides) {
|
|
636
|
+
if ((!CONFIG.proxy.host && !hasSessionProxy) || hasOverrides) {
|
|
228
637
|
contextOptions.locale = resolved.locale || 'en-US';
|
|
229
638
|
contextOptions.timezoneId = resolved.timezoneId || 'America/Los_Angeles';
|
|
230
639
|
contextOptions.geolocation = resolved.geolocation || { latitude: 37.7749, longitude: -122.4194 };
|
|
231
640
|
}
|
|
641
|
+
return contextOptions;
|
|
642
|
+
}
|
|
643
|
+
async function createStagedSession(userId, contextOverrides, sessionKey) {
|
|
644
|
+
const runtimeProfile = resolveRuntimeSessionProfile(userId, sessionKey, contextOverrides);
|
|
645
|
+
if (context_pool_1.contextPool.size() >= exports.MAX_SESSIONS) {
|
|
646
|
+
throw new Error('Maximum concurrent sessions reached');
|
|
647
|
+
}
|
|
648
|
+
const generation = node_crypto_1.default.randomUUID();
|
|
649
|
+
const contextOptions = buildBrowserContextOptions(runtimeProfile.contextOverrides, !!runtimeProfile.resolvedProxy);
|
|
650
|
+
const entry = await context_pool_1.contextPool.ensureContext(runtimeProfile.profileKey, normalizeUserId(userId), contextOptions, runtimeProfile.resolvedProxy, true, generation);
|
|
651
|
+
const session = {
|
|
652
|
+
context: entry.context,
|
|
653
|
+
tabGroups: new Map(),
|
|
654
|
+
lastAccess: Date.now(),
|
|
655
|
+
};
|
|
656
|
+
return { session, contextEntry: entry, generation };
|
|
657
|
+
}
|
|
658
|
+
function commitStagedFirstUse(userId, session, contextOverrides, tabInfo, generation) {
|
|
659
|
+
const key = normalizeUserId(userId);
|
|
660
|
+
const entry = context_pool_1.contextPool.getEntry(tabInfo.sessionMapKey);
|
|
661
|
+
if (!entry || entry.stagedGeneration !== generation)
|
|
662
|
+
return false;
|
|
663
|
+
if (!firstCreateMutexes.has(key) || canonicalProfiles.has(key)) {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
session.lastAccess = Date.now();
|
|
667
|
+
const group = getTabGroup(session, tabInfo.sessionKey);
|
|
668
|
+
group.set(tabInfo.tabId, tabInfo.tabState);
|
|
669
|
+
sessions.set(tabInfo.sessionMapKey, session);
|
|
670
|
+
sessionOwners.set(tabInfo.sessionMapKey, key);
|
|
671
|
+
entry.staged = false;
|
|
672
|
+
entry.stagedGeneration = undefined;
|
|
673
|
+
indexTab(tabInfo.tabId, tabInfo.sessionMapKey);
|
|
674
|
+
commitCanonicalProfile(userId, contextOverrides);
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
async function rollbackStagedFirstUse(userId, generation) {
|
|
678
|
+
const key = normalizeUserId(userId);
|
|
679
|
+
try {
|
|
680
|
+
try {
|
|
681
|
+
(0, download_1.cleanupUserDownloads)(key);
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
// ignore cleanup errors
|
|
685
|
+
}
|
|
686
|
+
await context_pool_1.contextPool.closeStagedContextByUserId(key, generation);
|
|
687
|
+
}
|
|
688
|
+
finally {
|
|
689
|
+
rollbackCanonicalMutex(userId);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async function getSession(userId, contextOverrides, sessionKey) {
|
|
693
|
+
const key = normalizeUserId(userId);
|
|
694
|
+
await waitForLifecycleIdleClosure(key);
|
|
695
|
+
const runtimeProfile = resolveRuntimeSessionProfile(userId, sessionKey, contextOverrides);
|
|
696
|
+
let session = sessions.get(runtimeProfile.sessionMapKey);
|
|
697
|
+
const contextOptions = buildBrowserContextOptions(runtimeProfile.contextOverrides, !!runtimeProfile.resolvedProxy);
|
|
232
698
|
if (!session) {
|
|
233
|
-
const existingLaunch = launchingSessions.get(
|
|
699
|
+
const existingLaunch = launchingSessions.get(runtimeProfile.sessionMapKey);
|
|
234
700
|
if (existingLaunch) {
|
|
235
701
|
session = await existingLaunch;
|
|
236
702
|
session.lastAccess = Date.now();
|
|
237
703
|
return session;
|
|
238
704
|
}
|
|
239
|
-
if (
|
|
705
|
+
if (context_pool_1.contextPool.size() >= exports.MAX_SESSIONS) {
|
|
240
706
|
throw new Error('Maximum concurrent sessions reached');
|
|
241
707
|
}
|
|
242
708
|
const launchPromise = (async () => {
|
|
243
|
-
const entry = await context_pool_1.contextPool.ensureContext(
|
|
709
|
+
const entry = await context_pool_1.contextPool.ensureContext(runtimeProfile.profileKey, key, contextOptions, runtimeProfile.resolvedProxy);
|
|
244
710
|
const created = { context: entry.context, tabGroups: new Map(), lastAccess: Date.now() };
|
|
245
|
-
sessions.set(
|
|
246
|
-
(
|
|
711
|
+
sessions.set(runtimeProfile.sessionMapKey, created);
|
|
712
|
+
sessionOwners.set(runtimeProfile.sessionMapKey, key);
|
|
713
|
+
(0, logging_1.log)('info', 'session created', { userId: key, sessionMapKey: runtimeProfile.sessionMapKey });
|
|
247
714
|
return created;
|
|
248
715
|
})();
|
|
249
|
-
|
|
716
|
+
launchingSessionOwners.set(runtimeProfile.sessionMapKey, key);
|
|
717
|
+
launchingSessions.set(runtimeProfile.sessionMapKey, launchPromise);
|
|
250
718
|
try {
|
|
251
719
|
session = await launchPromise;
|
|
252
720
|
}
|
|
253
721
|
finally {
|
|
254
|
-
launchingSessions.delete(
|
|
722
|
+
launchingSessions.delete(runtimeProfile.sessionMapKey);
|
|
723
|
+
launchingSessionOwners.delete(runtimeProfile.sessionMapKey);
|
|
255
724
|
}
|
|
256
725
|
}
|
|
257
726
|
else {
|
|
258
727
|
// Re-resolve context on each access; ContextPool de-dupes launches and detects unexpected closes.
|
|
259
|
-
const entry = await context_pool_1.contextPool.ensureContext(
|
|
728
|
+
const entry = await context_pool_1.contextPool.ensureContext(runtimeProfile.profileKey, key, contextOptions, runtimeProfile.resolvedProxy);
|
|
260
729
|
session.context = entry.context;
|
|
261
730
|
session.lastAccess = Date.now();
|
|
731
|
+
sessionOwners.set(runtimeProfile.sessionMapKey, key);
|
|
262
732
|
}
|
|
263
733
|
// For newly created sessions, lastAccess/context are already set.
|
|
264
734
|
session.lastAccess = Date.now();
|
|
@@ -273,30 +743,59 @@ function unindexTab(tabId) {
|
|
|
273
743
|
}
|
|
274
744
|
function clearAllState() {
|
|
275
745
|
sessions.clear();
|
|
746
|
+
sessionOwners.clear();
|
|
747
|
+
launchingSessions.clear();
|
|
748
|
+
launchingSessionOwners.clear();
|
|
276
749
|
tabSessionIndex.clear();
|
|
750
|
+
canonicalProfiles.clear();
|
|
751
|
+
sessionProfiles.clear();
|
|
752
|
+
defaultSessionProfileClaims.clear();
|
|
753
|
+
for (const [, mutex] of sessionProfileCreateMutexes)
|
|
754
|
+
mutex.resolve(false);
|
|
755
|
+
sessionProfileCreateMutexes.clear();
|
|
756
|
+
for (const [, state] of lifecycleIdleClosures)
|
|
757
|
+
state.resolve();
|
|
758
|
+
lifecycleIdleClosures.clear();
|
|
759
|
+
for (const [, mutex] of firstCreateMutexes)
|
|
760
|
+
mutex.resolve(false);
|
|
761
|
+
firstCreateMutexes.clear();
|
|
277
762
|
(0, tab_1.clearAllTabLocks)();
|
|
278
763
|
userConcurrency.clear();
|
|
279
764
|
}
|
|
280
|
-
async function closeSessionsForUser(userId) {
|
|
281
|
-
const
|
|
282
|
-
await context_pool_1.contextPool.
|
|
283
|
-
|
|
765
|
+
async function closeSessionsForUser(userId, options = {}) {
|
|
766
|
+
const key = normalizeUserId(userId);
|
|
767
|
+
await context_pool_1.contextPool.closeStagedContextByUserId(key).catch(() => { });
|
|
768
|
+
await context_pool_1.contextPool.closeContextByUserId(key).catch(() => { });
|
|
769
|
+
cleanupSessionsForUserId(key, 'explicit_close', options.clearProfiles ?? true, { allowInternalSessionKey: false });
|
|
284
770
|
}
|
|
285
771
|
async function closeAllSessions() {
|
|
286
772
|
await context_pool_1.contextPool.closeAll().catch(() => { });
|
|
287
|
-
for (const [
|
|
288
|
-
|
|
773
|
+
for (const [sessionKey, session] of sessions) {
|
|
774
|
+
const ownerUserId = sessionOwners.get(sessionKey) ?? sessionKey;
|
|
775
|
+
void (0, vnc_1.stopVnc)(ownerUserId).catch(() => { });
|
|
289
776
|
unindexSessionTabs(session);
|
|
290
|
-
sessions.delete(
|
|
291
|
-
|
|
777
|
+
sessions.delete(sessionKey);
|
|
778
|
+
sessionOwners.delete(sessionKey);
|
|
779
|
+
(0, tracing_1.cleanupTracing)(ownerUserId);
|
|
292
780
|
try {
|
|
293
|
-
(0, download_1.cleanupUserDownloads)(
|
|
781
|
+
(0, download_1.cleanupUserDownloads)(ownerUserId);
|
|
294
782
|
}
|
|
295
783
|
catch {
|
|
296
784
|
// ignore
|
|
297
785
|
}
|
|
298
786
|
}
|
|
299
787
|
launchingSessions.clear();
|
|
788
|
+
launchingSessionOwners.clear();
|
|
789
|
+
sessionOwners.clear();
|
|
790
|
+
canonicalProfiles.clear();
|
|
791
|
+
sessionProfiles.clear();
|
|
792
|
+
defaultSessionProfileClaims.clear();
|
|
793
|
+
for (const [, mutex] of sessionProfileCreateMutexes)
|
|
794
|
+
mutex.resolve(false);
|
|
795
|
+
sessionProfileCreateMutexes.clear();
|
|
796
|
+
for (const [, mutex] of firstCreateMutexes)
|
|
797
|
+
mutex.resolve(false);
|
|
798
|
+
firstCreateMutexes.clear();
|
|
300
799
|
}
|
|
301
800
|
let cleanupInterval = null;
|
|
302
801
|
function startCleanupInterval() {
|
|
@@ -306,12 +805,15 @@ function startCleanupInterval() {
|
|
|
306
805
|
const now = Date.now();
|
|
307
806
|
for (const [sessionKey, session] of sessions) {
|
|
308
807
|
if (now - session.lastAccess > exports.SESSION_TIMEOUT_MS) {
|
|
808
|
+
const ownerUserId = sessionOwners.get(sessionKey) ?? sessionKey;
|
|
309
809
|
// Persistent profile is preserved on disk; closing the context frees resources.
|
|
310
810
|
context_pool_1.contextPool.closeContext(sessionKey).catch(() => { });
|
|
311
811
|
unindexSessionTabs(session);
|
|
812
|
+
clearDefaultSessionProfileClaimsForUser(ownerUserId);
|
|
312
813
|
sessions.delete(sessionKey);
|
|
313
|
-
|
|
314
|
-
(0,
|
|
814
|
+
sessionOwners.delete(sessionKey);
|
|
815
|
+
(0, tracing_1.cleanupTracing)(ownerUserId);
|
|
816
|
+
(0, logging_1.log)('info', 'session expired', { userId: ownerUserId, sessionKey });
|
|
315
817
|
}
|
|
316
818
|
}
|
|
317
819
|
}, 60_000);
|
|
@@ -323,4 +825,60 @@ function stopCleanupInterval() {
|
|
|
323
825
|
cleanupInterval = null;
|
|
324
826
|
}
|
|
325
827
|
}
|
|
828
|
+
/**
|
|
829
|
+
* Stage 1 idle cleanup: close runtime state (contexts, session data) for users with no tabs.
|
|
830
|
+
* Does NOT clear stored session profiles - they survive idle cleanup.
|
|
831
|
+
* Does NOT exit the daemon - that is Stage 2 (Task 4).
|
|
832
|
+
* @param sessionSnapshot Snapshot of sessions map taken before cleanup started
|
|
833
|
+
* @param contextSnapshot Snapshot of context pool entries taken before cleanup started
|
|
834
|
+
* @param cleanupStartedMs Timestamp when cleanup was triggered (used to avoid closing newly-created contexts)
|
|
835
|
+
*/
|
|
836
|
+
async function runLifecycleIdleCleanup(sessionSnapshot, contextSnapshot, cleanupStartedMs) {
|
|
837
|
+
const closedUsers = [];
|
|
838
|
+
// Use the provided snapshots to avoid race with new context creation
|
|
839
|
+
const sessionKeysToCleanup = new Set();
|
|
840
|
+
for (const [sessionKey, session] of sessionSnapshot.entries()) {
|
|
841
|
+
const tabCount = countTotalTabsForSessions([[sessionKey, session]]);
|
|
842
|
+
// Only cleanup sessions with zero tabs
|
|
843
|
+
if (tabCount === 0) {
|
|
844
|
+
sessionKeysToCleanup.add(sessionKey);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
// Close only the specific contexts from the snapshot that were created before cleanup started
|
|
848
|
+
const contextsToClose = [];
|
|
849
|
+
for (const [profileKey, entry] of contextSnapshot.entries()) {
|
|
850
|
+
// Skip staged, launching, or newly-created contexts
|
|
851
|
+
if (sessionKeysToCleanup.has(profileKey) && !entry.staged && !entry.launching && entry.createdAt < cleanupStartedMs) {
|
|
852
|
+
contextsToClose.push({ profileKey, createdAt: entry.createdAt, lastAccess: entry.lastAccess });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// Close the specific contexts from the snapshot
|
|
856
|
+
const actuallyClosedSessionKeys = new Set();
|
|
857
|
+
for (const { profileKey, createdAt, lastAccess } of contextsToClose) {
|
|
858
|
+
const entry = contextSnapshot.get(profileKey);
|
|
859
|
+
const releaseIdleClosure = entry ? beginLifecycleIdleClosure(entry.userId) : null;
|
|
860
|
+
try {
|
|
861
|
+
await context_pool_1.contextPool.closeContextIfMatches(profileKey, createdAt, lastAccess);
|
|
862
|
+
// Verify the context was actually closed (not skipped due to reuse)
|
|
863
|
+
const stillExists = context_pool_1.contextPool.getEntry(profileKey);
|
|
864
|
+
if (!stillExists) {
|
|
865
|
+
// Context was actually closed, mark this exact session/profile key for cleanup.
|
|
866
|
+
actuallyClosedSessionKeys.add(profileKey);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
catch (err) {
|
|
870
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
871
|
+
(0, logging_1.log)('error', 'idle cleanup failed to close context', { profileKey, error: message });
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
releaseIdleClosure?.();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// Clean up session data ONLY for session/profile keys whose contexts were actually closed.
|
|
878
|
+
for (const sessionKey of actuallyClosedSessionKeys) {
|
|
879
|
+
cleanupSessionsForUserId(sessionKey, 'idle_cleanup', false);
|
|
880
|
+
closedUsers.push(sessionKey);
|
|
881
|
+
}
|
|
882
|
+
return { closedUsers };
|
|
883
|
+
}
|
|
326
884
|
//# sourceMappingURL=session.js.map
|