mrmd-js 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +842 -0
- package/dist/index.cjs +7613 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +7530 -0
- package/dist/index.js.map +1 -0
- package/dist/mrmd-js.iife.js +7618 -0
- package/dist/mrmd-js.iife.js.map +1 -0
- package/package.json +47 -0
- package/src/analysis/format.js +371 -0
- package/src/analysis/index.js +18 -0
- package/src/analysis/is-complete.js +394 -0
- package/src/constants.js +44 -0
- package/src/execute/css.js +205 -0
- package/src/execute/html.js +162 -0
- package/src/execute/index.js +41 -0
- package/src/execute/interface.js +144 -0
- package/src/execute/javascript.js +197 -0
- package/src/execute/registry.js +245 -0
- package/src/index.js +136 -0
- package/src/lsp/complete.js +353 -0
- package/src/lsp/format.js +310 -0
- package/src/lsp/hover.js +126 -0
- package/src/lsp/index.js +55 -0
- package/src/lsp/inspect.js +466 -0
- package/src/lsp/parse.js +455 -0
- package/src/lsp/variables.js +283 -0
- package/src/runtime.js +518 -0
- package/src/session/console-capture.js +181 -0
- package/src/session/context/iframe.js +407 -0
- package/src/session/context/index.js +12 -0
- package/src/session/context/interface.js +38 -0
- package/src/session/context/main.js +357 -0
- package/src/session/index.js +16 -0
- package/src/session/manager.js +327 -0
- package/src/session/session.js +678 -0
- package/src/transform/async.js +133 -0
- package/src/transform/extract.js +251 -0
- package/src/transform/index.js +10 -0
- package/src/transform/persistence.js +176 -0
- package/src/types/analysis.js +24 -0
- package/src/types/capabilities.js +44 -0
- package/src/types/completion.js +47 -0
- package/src/types/execution.js +62 -0
- package/src/types/index.js +16 -0
- package/src/types/inspection.js +39 -0
- package/src/types/session.js +32 -0
- package/src/types/streaming.js +74 -0
- package/src/types/variables.js +54 -0
- package/src/utils/ansi-renderer.js +301 -0
- package/src/utils/css-applicator.js +149 -0
- package/src/utils/html-renderer.js +355 -0
- package/src/utils/index.js +25 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main Execution Context
|
|
3
|
+
*
|
|
4
|
+
* Executes JavaScript in the main window context (no isolation).
|
|
5
|
+
* Provides full access to the page's DOM and state.
|
|
6
|
+
*
|
|
7
|
+
* @module session/context/main
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ConsoleCapture } from '../console-capture.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {import('./interface.js').ExecutionContext} ExecutionContext
|
|
14
|
+
* @typedef {import('./interface.js').RawExecutionResult} RawExecutionResult
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} StdinRequest
|
|
19
|
+
* @property {string} prompt - Prompt text to display
|
|
20
|
+
* @property {boolean} password - Whether to hide input
|
|
21
|
+
* @property {string} execId - Execution ID for this request
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @callback OnStdinRequestCallback
|
|
26
|
+
* @param {StdinRequest} request - The stdin request
|
|
27
|
+
* @returns {Promise<string>} - Resolves with user input
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} MainContextOptions
|
|
32
|
+
* @property {Record<string, *>} [utilities] - Custom utilities to inject
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Main window execution context (no isolation)
|
|
37
|
+
* @implements {ExecutionContext}
|
|
38
|
+
*/
|
|
39
|
+
export class MainContext {
|
|
40
|
+
/** @type {Set<string>} */
|
|
41
|
+
#trackedVars = new Set();
|
|
42
|
+
|
|
43
|
+
/** @type {ConsoleCapture | null} */
|
|
44
|
+
#consoleCapture = null;
|
|
45
|
+
|
|
46
|
+
/** @type {MainContextOptions} */
|
|
47
|
+
#options;
|
|
48
|
+
|
|
49
|
+
/** @type {boolean} */
|
|
50
|
+
#initialized = false;
|
|
51
|
+
|
|
52
|
+
/** @type {Array<{data: Record<string, string>, metadata: Record<string, *>}>} */
|
|
53
|
+
#displayQueue = [];
|
|
54
|
+
|
|
55
|
+
/** @type {OnStdinRequestCallback | null} */
|
|
56
|
+
#onStdinRequest = null;
|
|
57
|
+
|
|
58
|
+
/** @type {string} */
|
|
59
|
+
#currentExecId = '';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {MainContextOptions} [options]
|
|
63
|
+
*/
|
|
64
|
+
constructor(options = {}) {
|
|
65
|
+
this.#options = options;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize the context
|
|
70
|
+
*/
|
|
71
|
+
#initialize() {
|
|
72
|
+
if (this.#initialized) return;
|
|
73
|
+
|
|
74
|
+
// Set up utilities on window
|
|
75
|
+
this.#setupUtilities();
|
|
76
|
+
|
|
77
|
+
// Set up console capture
|
|
78
|
+
this.#consoleCapture = new ConsoleCapture(window);
|
|
79
|
+
this.#consoleCapture.start();
|
|
80
|
+
|
|
81
|
+
this.#initialized = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Set up utility functions
|
|
86
|
+
*/
|
|
87
|
+
#setupUtilities() {
|
|
88
|
+
const self = this;
|
|
89
|
+
|
|
90
|
+
// Sleep helper (if not already defined)
|
|
91
|
+
if (!('sleep' in window)) {
|
|
92
|
+
/** @type {*} */ (window).sleep = (ms) =>
|
|
93
|
+
new Promise((resolve) => setTimeout(resolve, ms));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Print helper
|
|
97
|
+
if (!('print' in window) || typeof window.print !== 'function') {
|
|
98
|
+
/** @type {*} */ (window).print = (...args) => {
|
|
99
|
+
console.log(...args);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Input helper - prompts for user input (like Python's input())
|
|
104
|
+
/** @type {*} */ (window).__mrmd_input__ = async (prompt = '', options = {}) => {
|
|
105
|
+
// Print prompt to console (like Python does)
|
|
106
|
+
if (prompt) {
|
|
107
|
+
console.log(prompt);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If no stdin handler is set, fall back to browser prompt()
|
|
111
|
+
if (!self.#onStdinRequest) {
|
|
112
|
+
const result = window.prompt(prompt) ?? '';
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Request input from the external handler
|
|
117
|
+
const request = {
|
|
118
|
+
prompt: prompt,
|
|
119
|
+
password: options.password ?? false,
|
|
120
|
+
execId: self.#currentExecId,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const response = await self.#onStdinRequest(request);
|
|
125
|
+
// Remove trailing newline if present (input() in Python strips it)
|
|
126
|
+
return response.replace(/\n$/, '');
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// If cancelled, throw an error like Python's KeyboardInterrupt
|
|
129
|
+
throw new Error('Input cancelled');
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Expose as 'input' if not already defined
|
|
134
|
+
if (!('input' in window)) {
|
|
135
|
+
/** @type {*} */ (window).input = /** @type {*} */ (window).__mrmd_input__;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Display helper
|
|
139
|
+
/** @type {*} */ (window).__mrmd_display__ = (data, mimeType = 'text/plain') => {
|
|
140
|
+
let content;
|
|
141
|
+
if (typeof data === 'string') {
|
|
142
|
+
content = data;
|
|
143
|
+
} else if (data instanceof HTMLElement) {
|
|
144
|
+
content = data.outerHTML;
|
|
145
|
+
mimeType = 'text/html';
|
|
146
|
+
} else {
|
|
147
|
+
try {
|
|
148
|
+
content = JSON.stringify(data, null, 2);
|
|
149
|
+
mimeType = 'application/json';
|
|
150
|
+
} catch {
|
|
151
|
+
content = String(data);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.#displayQueue.push({ data: { [mimeType]: content }, metadata: {} });
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Inject custom utilities
|
|
159
|
+
if (this.#options.utilities) {
|
|
160
|
+
for (const [key, value] of Object.entries(this.#options.utilities)) {
|
|
161
|
+
/** @type {*} */ (window)[key] = value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Set the stdin request handler
|
|
168
|
+
* @param {OnStdinRequestCallback | null} handler
|
|
169
|
+
*/
|
|
170
|
+
setStdinHandler(handler) {
|
|
171
|
+
this.#onStdinRequest = handler;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get the current stdin request handler
|
|
176
|
+
* @returns {OnStdinRequestCallback | null}
|
|
177
|
+
*/
|
|
178
|
+
getStdinHandler() {
|
|
179
|
+
return this.#onStdinRequest;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Execute code in main context
|
|
184
|
+
* @param {string} code - Already transformed/wrapped code from executor
|
|
185
|
+
* @param {{ execId?: string }} [options] - Execution options
|
|
186
|
+
* @returns {Promise<RawExecutionResult>}
|
|
187
|
+
*/
|
|
188
|
+
async execute(code, options = {}) {
|
|
189
|
+
this.#initialize();
|
|
190
|
+
|
|
191
|
+
// Set current execution ID for input() calls
|
|
192
|
+
this.#currentExecId = options.execId || '';
|
|
193
|
+
|
|
194
|
+
// Clear display queue
|
|
195
|
+
this.#displayQueue = [];
|
|
196
|
+
|
|
197
|
+
// Clear console capture
|
|
198
|
+
this.#consoleCapture?.clear();
|
|
199
|
+
|
|
200
|
+
const startTime = performance.now();
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// Execute - code is already transformed/wrapped by the executor
|
|
204
|
+
const result = await eval(code);
|
|
205
|
+
const duration = performance.now() - startTime;
|
|
206
|
+
|
|
207
|
+
// Get logs
|
|
208
|
+
const logs = this.#consoleCapture?.flush() || [];
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
result,
|
|
212
|
+
logs,
|
|
213
|
+
duration,
|
|
214
|
+
};
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const duration = performance.now() - startTime;
|
|
217
|
+
const logs = this.#consoleCapture?.flush() || [];
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
result: undefined,
|
|
221
|
+
logs,
|
|
222
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
223
|
+
duration,
|
|
224
|
+
};
|
|
225
|
+
} finally {
|
|
226
|
+
// Clear current exec ID
|
|
227
|
+
this.#currentExecId = '';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get all user-defined variables
|
|
233
|
+
* @returns {Record<string, *>}
|
|
234
|
+
*/
|
|
235
|
+
getVariables() {
|
|
236
|
+
const vars = {};
|
|
237
|
+
for (const name of this.#trackedVars) {
|
|
238
|
+
try {
|
|
239
|
+
vars[name] = /** @type {*} */ (window)[name];
|
|
240
|
+
} catch {
|
|
241
|
+
// Skip inaccessible
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return vars;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get a specific variable
|
|
249
|
+
* @param {string} name
|
|
250
|
+
* @returns {*}
|
|
251
|
+
*/
|
|
252
|
+
getVariable(name) {
|
|
253
|
+
return /** @type {*} */ (window)[name];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Check if variable exists
|
|
258
|
+
* @param {string} name
|
|
259
|
+
* @returns {boolean}
|
|
260
|
+
*/
|
|
261
|
+
hasVariable(name) {
|
|
262
|
+
return name in window;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get the global object
|
|
267
|
+
* @returns {Window}
|
|
268
|
+
*/
|
|
269
|
+
getGlobal() {
|
|
270
|
+
return window;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Track a declared variable
|
|
275
|
+
* @param {string} name
|
|
276
|
+
*/
|
|
277
|
+
trackVariable(name) {
|
|
278
|
+
this.#trackedVars.add(name);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get tracked variable names
|
|
283
|
+
* @returns {Set<string>}
|
|
284
|
+
*/
|
|
285
|
+
getTrackedVariables() {
|
|
286
|
+
return this.#trackedVars;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Reset the context
|
|
291
|
+
*/
|
|
292
|
+
reset() {
|
|
293
|
+
// Delete tracked variables from window
|
|
294
|
+
for (const name of this.#trackedVars) {
|
|
295
|
+
try {
|
|
296
|
+
delete /** @type {*} */ (window)[name];
|
|
297
|
+
} catch {
|
|
298
|
+
// Some properties can't be deleted
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
this.#trackedVars = new Set();
|
|
302
|
+
this.#displayQueue = [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Destroy the context
|
|
307
|
+
*/
|
|
308
|
+
destroy() {
|
|
309
|
+
this.#consoleCapture?.stop();
|
|
310
|
+
this.#consoleCapture = null;
|
|
311
|
+
|
|
312
|
+
// Clean up tracked variables
|
|
313
|
+
this.reset();
|
|
314
|
+
|
|
315
|
+
// Clean up utilities we added
|
|
316
|
+
try {
|
|
317
|
+
delete /** @type {*} */ (window).__mrmd_display__;
|
|
318
|
+
} catch {
|
|
319
|
+
// Ignore
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this.#initialized = false;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Check if this is main context
|
|
327
|
+
* @returns {boolean}
|
|
328
|
+
*/
|
|
329
|
+
isMainContext() {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get the iframe element
|
|
335
|
+
* @returns {null}
|
|
336
|
+
*/
|
|
337
|
+
getIframe() {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get display data queue
|
|
343
|
+
* @returns {Array<{data: Record<string, string>, metadata: Record<string, *>}>}
|
|
344
|
+
*/
|
|
345
|
+
getDisplayQueue() {
|
|
346
|
+
return this.#displayQueue;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Create a main context
|
|
352
|
+
* @param {MainContextOptions} [options]
|
|
353
|
+
* @returns {MainContext}
|
|
354
|
+
*/
|
|
355
|
+
export function createMainContext(options) {
|
|
356
|
+
return new MainContext(options);
|
|
357
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Management
|
|
3
|
+
*
|
|
4
|
+
* Session and context management for mrmd-js.
|
|
5
|
+
* @module session
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Session classes
|
|
9
|
+
export { Session, createSession } from './session.js';
|
|
10
|
+
export { SessionManager, createSessionManager } from './manager.js';
|
|
11
|
+
|
|
12
|
+
// Context infrastructure
|
|
13
|
+
export { ConsoleCapture, createConsoleCapture } from './console-capture.js';
|
|
14
|
+
export { IframeContext, createIframeContext } from './context/iframe.js';
|
|
15
|
+
export { MainContext, createMainContext } from './context/main.js';
|
|
16
|
+
export * from './context/interface.js';
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages multiple sessions, handles creation/destruction,
|
|
5
|
+
* and enforces limits.
|
|
6
|
+
*
|
|
7
|
+
* @module session/manager
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Session } from './session.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {import('../types/session.js').SessionInfo} SessionInfo
|
|
14
|
+
* @typedef {import('../types/session.js').CreateSessionOptions} CreateSessionOptions
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} SessionManagerOptions
|
|
19
|
+
* @property {number} [maxSessions=10] - Maximum number of concurrent sessions
|
|
20
|
+
* @property {string} [defaultLanguage='javascript'] - Default language for new sessions
|
|
21
|
+
* @property {import('../types/session.js').IsolationMode} [defaultIsolation='iframe'] - Default isolation mode
|
|
22
|
+
* @property {boolean} [defaultAllowMainAccess=false] - Default main access setting
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate a unique session ID
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function generateSessionId() {
|
|
30
|
+
return `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Session Manager - manages multiple execution sessions
|
|
35
|
+
*/
|
|
36
|
+
export class SessionManager {
|
|
37
|
+
/** @type {Map<string, Session>} */
|
|
38
|
+
#sessions = new Map();
|
|
39
|
+
|
|
40
|
+
/** @type {SessionManagerOptions} */
|
|
41
|
+
#options;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {SessionManagerOptions} [options]
|
|
45
|
+
*/
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
this.#options = {
|
|
48
|
+
maxSessions: 10,
|
|
49
|
+
defaultLanguage: 'javascript',
|
|
50
|
+
defaultIsolation: 'iframe',
|
|
51
|
+
defaultAllowMainAccess: false,
|
|
52
|
+
...options,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Session Lifecycle
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a new session
|
|
62
|
+
* @param {CreateSessionOptions} [options]
|
|
63
|
+
* @returns {Session}
|
|
64
|
+
* @throws {Error} If max sessions reached
|
|
65
|
+
*/
|
|
66
|
+
create(options = {}) {
|
|
67
|
+
// Check limits
|
|
68
|
+
if (this.#sessions.size >= this.#options.maxSessions) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Maximum sessions (${this.#options.maxSessions}) reached. ` +
|
|
71
|
+
'Destroy existing sessions before creating new ones.'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Generate ID if not provided
|
|
76
|
+
const id = options.id || generateSessionId();
|
|
77
|
+
|
|
78
|
+
// Check for duplicate ID
|
|
79
|
+
if (this.#sessions.has(id)) {
|
|
80
|
+
throw new Error(`Session with ID '${id}' already exists`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Merge with defaults
|
|
84
|
+
const sessionOptions = {
|
|
85
|
+
language: options.language || this.#options.defaultLanguage,
|
|
86
|
+
isolation: options.isolation || this.#options.defaultIsolation,
|
|
87
|
+
allowMainAccess: options.allowMainAccess ?? this.#options.defaultAllowMainAccess,
|
|
88
|
+
utilities: options.utilities,
|
|
89
|
+
executorRegistry: options.executorRegistry,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Create session
|
|
93
|
+
const session = new Session(id, sessionOptions);
|
|
94
|
+
this.#sessions.set(id, session);
|
|
95
|
+
|
|
96
|
+
return session;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get an existing session by ID
|
|
101
|
+
* @param {string} id
|
|
102
|
+
* @returns {Session | undefined}
|
|
103
|
+
*/
|
|
104
|
+
get(id) {
|
|
105
|
+
return this.#sessions.get(id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get a session, creating it if it doesn't exist
|
|
110
|
+
* @param {string} id
|
|
111
|
+
* @param {CreateSessionOptions} [options]
|
|
112
|
+
* @returns {Session}
|
|
113
|
+
*/
|
|
114
|
+
getOrCreate(id, options = {}) {
|
|
115
|
+
const existing = this.#sessions.get(id);
|
|
116
|
+
if (existing) {
|
|
117
|
+
return existing;
|
|
118
|
+
}
|
|
119
|
+
return this.create({ ...options, id });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if a session exists
|
|
124
|
+
* @param {string} id
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
has(id) {
|
|
128
|
+
return this.#sessions.has(id);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Destroy a session by ID
|
|
133
|
+
* @param {string} id
|
|
134
|
+
* @returns {boolean} Whether a session was destroyed
|
|
135
|
+
*/
|
|
136
|
+
destroy(id) {
|
|
137
|
+
const session = this.#sessions.get(id);
|
|
138
|
+
if (session) {
|
|
139
|
+
session.destroy();
|
|
140
|
+
this.#sessions.delete(id);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Destroy all sessions
|
|
148
|
+
*/
|
|
149
|
+
destroyAll() {
|
|
150
|
+
for (const session of this.#sessions.values()) {
|
|
151
|
+
session.destroy();
|
|
152
|
+
}
|
|
153
|
+
this.#sessions.clear();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Session Queries
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* List all sessions
|
|
162
|
+
* @returns {SessionInfo[]}
|
|
163
|
+
*/
|
|
164
|
+
list() {
|
|
165
|
+
return Array.from(this.#sessions.values()).map((s) => s.getInfo());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get number of active sessions
|
|
170
|
+
* @returns {number}
|
|
171
|
+
*/
|
|
172
|
+
get size() {
|
|
173
|
+
return this.#sessions.size;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get all session IDs
|
|
178
|
+
* @returns {string[]}
|
|
179
|
+
*/
|
|
180
|
+
get ids() {
|
|
181
|
+
return Array.from(this.#sessions.keys());
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get the maximum number of sessions allowed
|
|
186
|
+
* @returns {number}
|
|
187
|
+
*/
|
|
188
|
+
get maxSessions() {
|
|
189
|
+
return this.#options.maxSessions;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Iterate over sessions
|
|
194
|
+
* @returns {IterableIterator<Session>}
|
|
195
|
+
*/
|
|
196
|
+
[Symbol.iterator]() {
|
|
197
|
+
return this.#sessions.values();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Iterate over session entries
|
|
202
|
+
* @returns {IterableIterator<[string, Session]>}
|
|
203
|
+
*/
|
|
204
|
+
entries() {
|
|
205
|
+
return this.#sessions.entries();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// Bulk Operations
|
|
210
|
+
// ============================================================================
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Reset all sessions (clear variables but keep sessions)
|
|
214
|
+
*/
|
|
215
|
+
resetAll() {
|
|
216
|
+
for (const session of this.#sessions.values()) {
|
|
217
|
+
session.reset();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Interrupt all running executions across all sessions
|
|
223
|
+
* @returns {number} Number of sessions that had executions interrupted
|
|
224
|
+
*/
|
|
225
|
+
interruptAll() {
|
|
226
|
+
let count = 0;
|
|
227
|
+
for (const session of this.#sessions.values()) {
|
|
228
|
+
if (session.interrupt()) {
|
|
229
|
+
count++;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return count;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get sessions by language
|
|
237
|
+
* @param {string} language
|
|
238
|
+
* @returns {Session[]}
|
|
239
|
+
*/
|
|
240
|
+
getByLanguage(language) {
|
|
241
|
+
const results = [];
|
|
242
|
+
for (const session of this.#sessions.values()) {
|
|
243
|
+
if (session.language === language) {
|
|
244
|
+
results.push(session);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return results;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get the most recently active session
|
|
252
|
+
* @returns {Session | undefined}
|
|
253
|
+
*/
|
|
254
|
+
getMostRecent() {
|
|
255
|
+
let mostRecent = undefined;
|
|
256
|
+
let latestTime = 0;
|
|
257
|
+
|
|
258
|
+
for (const session of this.#sessions.values()) {
|
|
259
|
+
const time = session.lastActivity.getTime();
|
|
260
|
+
if (time > latestTime) {
|
|
261
|
+
latestTime = time;
|
|
262
|
+
mostRecent = session;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return mostRecent;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Cleanup
|
|
271
|
+
// ============================================================================
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Destroy sessions that have been inactive for a certain time
|
|
275
|
+
* @param {number} maxIdleMs - Maximum idle time in milliseconds
|
|
276
|
+
* @returns {number} Number of sessions destroyed
|
|
277
|
+
*/
|
|
278
|
+
cleanupIdle(maxIdleMs) {
|
|
279
|
+
const now = Date.now();
|
|
280
|
+
const toDestroy = [];
|
|
281
|
+
|
|
282
|
+
for (const [id, session] of this.#sessions) {
|
|
283
|
+
if (now - session.lastActivity.getTime() > maxIdleMs) {
|
|
284
|
+
toDestroy.push(id);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const id of toDestroy) {
|
|
289
|
+
this.destroy(id);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return toDestroy.length;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Destroy oldest sessions to get under a certain count
|
|
297
|
+
* @param {number} targetCount - Target number of sessions
|
|
298
|
+
* @returns {number} Number of sessions destroyed
|
|
299
|
+
*/
|
|
300
|
+
trimToCount(targetCount) {
|
|
301
|
+
if (this.#sessions.size <= targetCount) {
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Sort by last activity (oldest first)
|
|
306
|
+
const sorted = Array.from(this.#sessions.entries()).sort(
|
|
307
|
+
([, a], [, b]) => a.lastActivity.getTime() - b.lastActivity.getTime()
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const toDestroy = sorted.slice(0, this.#sessions.size - targetCount);
|
|
311
|
+
|
|
312
|
+
for (const [id] of toDestroy) {
|
|
313
|
+
this.destroy(id);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return toDestroy.length;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Create a session manager
|
|
322
|
+
* @param {SessionManagerOptions} [options]
|
|
323
|
+
* @returns {SessionManager}
|
|
324
|
+
*/
|
|
325
|
+
export function createSessionManager(options) {
|
|
326
|
+
return new SessionManager(options);
|
|
327
|
+
}
|