kernel-checkpoint 0.1.7__tar.gz → 0.1.9__tar.gz
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.
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/PKG-INFO +1 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/jupyter-notebook-image/Dockerfile +1 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/kernel_checkpoint/_version.py +1 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/kernel_checkpoint/labextension/build_log.json +1 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/kernel_checkpoint/labextension/package.json +2 -2
- kernel_checkpoint-0.1.7/kernel_checkpoint/labextension/static/lib_index_js.3571ff1d8cbf7ea37b2e.js → kernel_checkpoint-0.1.9/kernel_checkpoint/labextension/static/lib_index_js.b36bed60f63c982f956c.js +439 -11
- kernel_checkpoint-0.1.9/kernel_checkpoint/labextension/static/lib_index_js.b36bed60f63c982f956c.js.map +1 -0
- kernel_checkpoint-0.1.7/kernel_checkpoint/labextension/static/remoteEntry.900312eadd6aa5546ed7.js → kernel_checkpoint-0.1.9/kernel_checkpoint/labextension/static/remoteEntry.e1cc534db6759a5ed255.js +3 -3
- kernel_checkpoint-0.1.7/kernel_checkpoint/labextension/static/remoteEntry.900312eadd6aa5546ed7.js.map → kernel_checkpoint-0.1.9/kernel_checkpoint/labextension/static/remoteEntry.e1cc534db6759a5ed255.js.map +1 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/package.json +1 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/src/checkpoint-panel.tsx +19 -3
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/src/index.ts +107 -5
- kernel_checkpoint-0.1.9/src/restore-handler.ts +418 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/src/types.ts +24 -1
- kernel_checkpoint-0.1.7/kernel_checkpoint/labextension/static/lib_index_js.3571ff1d8cbf7ea37b2e.js.map +0 -1
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/.copier-answers.yml +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/.gitignore +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/.prettierignore +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/.yarnrc.yml +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/4 +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/=4 +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/CHANGELOG.md +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/LICENSE +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/README.md +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/RELEASE.md +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/babel.config.js +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/cursor_jupyterlab_extension_for_checkpo.md +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/install.json +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/jest.config.js +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/jupyter-config/jupyter_server_config.d/kernel_checkpoint.json +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/kernel_checkpoint/Untitled.ipynb +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/kernel_checkpoint/__init__.py +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/kernel_checkpoint/handlers.py +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/kernel_checkpoint/labextension/static/style.js +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/kernel_checkpoint/labextension/static/style_index_js.c3e72438ed03e9dee0fc.js +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/kernel_checkpoint/labextension/static/style_index_js.c3e72438ed03e9dee0fc.js.map +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/pyproject.toml +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/setup.py +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/src/__tests__/kernel_checkpoint.spec.ts +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/src/api.ts +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/style/base.css +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/style/index.css +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/style/index.js +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/tsconfig.json +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/tsconfig.test.json +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/ui-tests/README.md +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/ui-tests/jupyter_server_test_config.py +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/ui-tests/package.json +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/ui-tests/playwright.config.js +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/ui-tests/tests/kernel_checkpoint.spec.ts +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/ui-tests/yarn.lock +0 -0
- {kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/yarn.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kernel_checkpoint
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: An extension for saving and restoring kernel pod state with jupyter enterprise gateway
|
|
5
5
|
Project-URL: Homepage, https://github.com/lehuannhatrang/kernel-checkpoint-extension.git
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/lehuannhatrang/kernel-checkpoint-extension.git/issues
|
{kernel_checkpoint-0.1.7 → kernel_checkpoint-0.1.9}/kernel_checkpoint/labextension/package.json
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kernel-checkpoint",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "An extension for saving and restoring kernel pod state with jupyter enterprise gateway",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
},
|
|
115
115
|
"outputDir": "kernel_checkpoint/labextension",
|
|
116
116
|
"_build": {
|
|
117
|
-
"load": "static/remoteEntry.
|
|
117
|
+
"load": "static/remoteEntry.e1cc534db6759a5ed255.js",
|
|
118
118
|
"extension": "./extension",
|
|
119
119
|
"style": "./style"
|
|
120
120
|
}
|
|
@@ -119,7 +119,7 @@ function formatDate(iso) {
|
|
|
119
119
|
return new Date(iso).toLocaleString();
|
|
120
120
|
}
|
|
121
121
|
function CheckpointPanel(props) {
|
|
122
|
-
const { namespace, kernelId, kernelSpecName, notebookName, onRestore } = props;
|
|
122
|
+
const { namespace, kernelId, kernelSpecName, notebookName, sessionId, onRestore, onBeforeCreate } = props;
|
|
123
123
|
const [checkpoints, setCheckpoints] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
|
|
124
124
|
const [loading, setLoading] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(true);
|
|
125
125
|
const [error, setError] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
|
|
@@ -170,6 +170,7 @@ function CheckpointPanel(props) {
|
|
|
170
170
|
setCreating(true);
|
|
171
171
|
setFlash(null);
|
|
172
172
|
try {
|
|
173
|
+
const busyCells = onBeforeCreate ? onBeforeCreate() : [];
|
|
173
174
|
await _api__WEBPACK_IMPORTED_MODULE_1__.CheckpointAPI.createCheckpoint({
|
|
174
175
|
name: newName.trim(),
|
|
175
176
|
namespace,
|
|
@@ -178,7 +179,9 @@ function CheckpointPanel(props) {
|
|
|
178
179
|
metadata: {
|
|
179
180
|
kernelId,
|
|
180
181
|
kernelName: kernelSpecName,
|
|
181
|
-
notebookName
|
|
182
|
+
notebookName,
|
|
183
|
+
sessionId,
|
|
184
|
+
busyCells
|
|
182
185
|
}
|
|
183
186
|
});
|
|
184
187
|
setNewName('');
|
|
@@ -236,7 +239,7 @@ function CheckpointPanel(props) {
|
|
|
236
239
|
}
|
|
237
240
|
};
|
|
238
241
|
const handleRestore = async (checkpointName) => {
|
|
239
|
-
var _a, _b, _c, _d;
|
|
242
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
240
243
|
setConfirmRestore(null);
|
|
241
244
|
setRestoring(checkpointName);
|
|
242
245
|
setFlash(null);
|
|
@@ -250,7 +253,12 @@ function CheckpointPanel(props) {
|
|
|
250
253
|
if (!cpKernelId) {
|
|
251
254
|
throw new Error('No kernel ID found in checkpoint metadata');
|
|
252
255
|
}
|
|
253
|
-
|
|
256
|
+
const cpSessionId = (_e = (_d = detail.metadata) === null || _d === void 0 ? void 0 : _d.sessionId) !== null && _e !== void 0 ? _e : '';
|
|
257
|
+
if (!cpSessionId) {
|
|
258
|
+
throw new Error('No session ID found in checkpoint metadata');
|
|
259
|
+
}
|
|
260
|
+
const busyCells = (_g = (_f = detail.metadata) === null || _f === void 0 ? void 0 : _f.busyCells) !== null && _g !== void 0 ? _g : [];
|
|
261
|
+
await onRestore(checkpointName, checkpointFile.storagePath, checkpointFile.containerName, cpKernelId, cpSessionId, busyCells);
|
|
254
262
|
setFlash({
|
|
255
263
|
type: 'success',
|
|
256
264
|
text: `Kernel restored from "${checkpointName}". The kernel is restarting.`
|
|
@@ -259,7 +267,7 @@ function CheckpointPanel(props) {
|
|
|
259
267
|
catch (err) {
|
|
260
268
|
setFlash({
|
|
261
269
|
type: 'error',
|
|
262
|
-
text: (
|
|
270
|
+
text: (_h = err.message) !== null && _h !== void 0 ? _h : 'Failed to restore checkpoint'
|
|
263
271
|
});
|
|
264
272
|
}
|
|
265
273
|
finally {
|
|
@@ -389,6 +397,8 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
389
397
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_4__);
|
|
390
398
|
/* harmony import */ var _checkpoint_panel__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./checkpoint-panel */ "./lib/checkpoint-panel.js");
|
|
391
399
|
/* harmony import */ var _api__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./api */ "./lib/api.js");
|
|
400
|
+
/* harmony import */ var _restore_handler__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./restore-handler */ "./lib/restore-handler.js");
|
|
401
|
+
|
|
392
402
|
|
|
393
403
|
|
|
394
404
|
|
|
@@ -418,11 +428,34 @@ const plugin = {
|
|
|
418
428
|
requires: [_jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__.INotebookTracker],
|
|
419
429
|
activate: (app, notebookTracker) => {
|
|
420
430
|
console.log('JupyterLab extension kernel-checkpoint is activated!');
|
|
431
|
+
// ── Per-notebook execution tracking ──────────────────────────────
|
|
432
|
+
//
|
|
433
|
+
// Each open notebook gets its own CellExecutionTracker that
|
|
434
|
+
// continuously records in-flight msg_id → execution_count pairs
|
|
435
|
+
// from the kernel's iopub channel. At checkpoint time this map
|
|
436
|
+
// is consulted to capture the execution counts for busy cells.
|
|
437
|
+
const trackerMap = new Map();
|
|
438
|
+
function ensureTracker(panel) {
|
|
439
|
+
let cellTracker = trackerMap.get(panel.id);
|
|
440
|
+
if (!cellTracker) {
|
|
441
|
+
cellTracker = new _restore_handler__WEBPACK_IMPORTED_MODULE_7__.CellExecutionTracker();
|
|
442
|
+
trackerMap.set(panel.id, cellTracker);
|
|
443
|
+
}
|
|
444
|
+
return cellTracker;
|
|
445
|
+
}
|
|
446
|
+
function connectTrackerToKernel(panel) {
|
|
447
|
+
var _a;
|
|
448
|
+
const kernel = (_a = panel.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel;
|
|
449
|
+
if (kernel) {
|
|
450
|
+
ensureTracker(panel).connectToKernel(kernel);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ── Command ──────────────────────────────────────────────────────
|
|
421
454
|
app.commands.addCommand(COMMAND_ID, {
|
|
422
455
|
label: 'Saving Points',
|
|
423
456
|
caption: 'Manage kernel checkpoints',
|
|
424
457
|
execute: async () => {
|
|
425
|
-
var _a;
|
|
458
|
+
var _a, _b;
|
|
426
459
|
const panel = notebookTracker.currentWidget;
|
|
427
460
|
if (!panel) {
|
|
428
461
|
return;
|
|
@@ -461,7 +494,14 @@ const plugin = {
|
|
|
461
494
|
const kernelId = kernel.id;
|
|
462
495
|
const kernelSpecName = kernel.name || 'python_kubernetes';
|
|
463
496
|
const notebookName = panel.title.label || '';
|
|
464
|
-
const
|
|
497
|
+
const sessionId = ((_b = sessionContext.session) === null || _b === void 0 ? void 0 : _b.id) || '';
|
|
498
|
+
// ── Restore callback ─────────────────────────────────────────
|
|
499
|
+
//
|
|
500
|
+
// Called by the CheckpointPanel when the user confirms a restore.
|
|
501
|
+
// Creates a new kernel seeded with the checkpoint environment
|
|
502
|
+
// variables, switches the notebook session to it, and then
|
|
503
|
+
// activates the RestoreHandler to catch orphaned iopub messages.
|
|
504
|
+
const onRestore = async (checkpointName, checkpointFilePath, containerName, cpKernelId, cpSessionId, busyCells) => {
|
|
465
505
|
const settings = _jupyterlab_services__WEBPACK_IMPORTED_MODULE_3__.ServerConnection.makeSettings();
|
|
466
506
|
const kernelUrl = _jupyterlab_coreutils__WEBPACK_IMPORTED_MODULE_2__.URLExt.join(settings.baseUrl, 'api', 'kernels');
|
|
467
507
|
const response = await _jupyterlab_services__WEBPACK_IMPORTED_MODULE_3__.ServerConnection.makeRequest(kernelUrl, {
|
|
@@ -473,7 +513,8 @@ const plugin = {
|
|
|
473
513
|
KERNEL_CHECKPOINT_NAME: checkpointName,
|
|
474
514
|
KERNEL_CHECKPOINT_FILE_PATH: checkpointFilePath,
|
|
475
515
|
KERNEL_CHECKPOINT_CONTAINER_NAME: containerName,
|
|
476
|
-
KERNEL_ID: cpKernelId
|
|
516
|
+
KERNEL_ID: cpKernelId,
|
|
517
|
+
KERNEL_SESSION_ID: cpSessionId
|
|
477
518
|
}
|
|
478
519
|
})
|
|
479
520
|
}, settings);
|
|
@@ -483,13 +524,38 @@ const plugin = {
|
|
|
483
524
|
}
|
|
484
525
|
const kernelData = await response.json();
|
|
485
526
|
await sessionContext.changeKernel({ id: kernelData.id });
|
|
527
|
+
// ── Activate Catch-and-Render (Steps 3-6) ──────────────────
|
|
528
|
+
//
|
|
529
|
+
// The restored kernel is now connected. If there were busy
|
|
530
|
+
// cells at checkpoint time, spin up a RestoreHandler that
|
|
531
|
+
// hooks iopub and intercepts orphaned messages.
|
|
532
|
+
if (busyCells.length > 0) {
|
|
533
|
+
console.log(`[kernel-checkpoint] Restore detected ${busyCells.length} ` +
|
|
534
|
+
'busy cell(s). Activating RestoreHandler.');
|
|
535
|
+
const handler = new _restore_handler__WEBPACK_IMPORTED_MODULE_7__.RestoreHandler();
|
|
536
|
+
handler.hookRestoredKernel(panel, busyCells);
|
|
537
|
+
}
|
|
486
538
|
};
|
|
539
|
+
// ── Dialog construction ──────────────────────────────────────
|
|
487
540
|
const body = new CheckpointDialogBody({
|
|
488
541
|
namespace: config.namespace,
|
|
489
542
|
kernelId,
|
|
490
543
|
kernelSpecName,
|
|
491
544
|
notebookName,
|
|
492
|
-
|
|
545
|
+
sessionId,
|
|
546
|
+
onRestore,
|
|
547
|
+
onBeforeCreate: () => {
|
|
548
|
+
const cellTracker = trackerMap.get(panel.id);
|
|
549
|
+
if (!cellTracker) {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
const busyCells = (0,_restore_handler__WEBPACK_IMPORTED_MODULE_7__.scanBusyCells)(panel, cellTracker);
|
|
553
|
+
if (busyCells.length > 0) {
|
|
554
|
+
console.log(`[kernel-checkpoint] Scanned ${busyCells.length} busy cell(s) ` +
|
|
555
|
+
'before checkpoint:', busyCells);
|
|
556
|
+
}
|
|
557
|
+
return busyCells;
|
|
558
|
+
}
|
|
493
559
|
});
|
|
494
560
|
await (0,_jupyterlab_apputils__WEBPACK_IMPORTED_MODULE_1__.showDialog)({
|
|
495
561
|
title: 'Kernel Checkpoints',
|
|
@@ -499,8 +565,9 @@ const plugin = {
|
|
|
499
565
|
body.dispose();
|
|
500
566
|
}
|
|
501
567
|
});
|
|
502
|
-
//
|
|
568
|
+
// ── Notebook lifecycle wiring ────────────────────────────────────
|
|
503
569
|
notebookTracker.widgetAdded.connect((_sender, panel) => {
|
|
570
|
+
// Toolbar button
|
|
504
571
|
const button = new _jupyterlab_apputils__WEBPACK_IMPORTED_MODULE_1__.ToolbarButton({
|
|
505
572
|
label: 'Saving Points',
|
|
506
573
|
tooltip: 'Open kernel checkpoint manager',
|
|
@@ -509,13 +576,374 @@ const plugin = {
|
|
|
509
576
|
}
|
|
510
577
|
});
|
|
511
578
|
panel.toolbar.insertItem(10, 'kernel-checkpoint', button);
|
|
579
|
+
// Execution tracker setup — connect once the session is ready,
|
|
580
|
+
// and reconnect whenever the kernel is swapped.
|
|
581
|
+
panel.sessionContext.ready.then(() => {
|
|
582
|
+
connectTrackerToKernel(panel);
|
|
583
|
+
});
|
|
584
|
+
panel.sessionContext.kernelChanged.connect(() => {
|
|
585
|
+
connectTrackerToKernel(panel);
|
|
586
|
+
});
|
|
587
|
+
panel.disposed.connect(() => {
|
|
588
|
+
const cellTracker = trackerMap.get(panel.id);
|
|
589
|
+
if (cellTracker) {
|
|
590
|
+
cellTracker.disconnectFromKernel();
|
|
591
|
+
trackerMap.delete(panel.id);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
512
594
|
});
|
|
513
595
|
}
|
|
514
596
|
};
|
|
515
597
|
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (plugin);
|
|
516
598
|
|
|
517
599
|
|
|
600
|
+
/***/ },
|
|
601
|
+
|
|
602
|
+
/***/ "./lib/restore-handler.js"
|
|
603
|
+
/*!********************************!*\
|
|
604
|
+
!*** ./lib/restore-handler.js ***!
|
|
605
|
+
\********************************/
|
|
606
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
607
|
+
|
|
608
|
+
__webpack_require__.r(__webpack_exports__);
|
|
609
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
610
|
+
/* harmony export */ CellExecutionTracker: () => (/* binding */ CellExecutionTracker),
|
|
611
|
+
/* harmony export */ RestoreHandler: () => (/* binding */ RestoreHandler),
|
|
612
|
+
/* harmony export */ loadCheckpointCellMetadata: () => (/* binding */ loadCheckpointCellMetadata),
|
|
613
|
+
/* harmony export */ saveCheckpointCellMetadata: () => (/* binding */ saveCheckpointCellMetadata),
|
|
614
|
+
/* harmony export */ scanBusyCells: () => (/* binding */ scanBusyCells)
|
|
615
|
+
/* harmony export */ });
|
|
616
|
+
/**
|
|
617
|
+
* Safely extracts `msg_id` from a kernel message's `parent_header`.
|
|
618
|
+
* Returns undefined when the parent_header is empty (e.g. unsolicited
|
|
619
|
+
* kernel-status broadcasts at startup).
|
|
620
|
+
*/
|
|
621
|
+
function getParentMsgId(msg) {
|
|
622
|
+
const ph = msg.parent_header;
|
|
623
|
+
return ph && 'msg_id' in ph
|
|
624
|
+
? ph.msg_id
|
|
625
|
+
: undefined;
|
|
626
|
+
}
|
|
627
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
628
|
+
// Mock save / load helpers.
|
|
629
|
+
// These demonstrate the checkpoint-metadata persistence contract.
|
|
630
|
+
// Replace the bodies with real API calls (e.g. CheckpointAPI.updateCheckpoint)
|
|
631
|
+
// when wiring to your backend.
|
|
632
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
633
|
+
async function saveCheckpointCellMetadata(_checkpointName, _namespace, busyCells) {
|
|
634
|
+
console.log(`[RestoreHandler] Would persist ${busyCells.length} busy-cell record(s)`, busyCells);
|
|
635
|
+
}
|
|
636
|
+
async function loadCheckpointCellMetadata(_checkpointName, _namespace) {
|
|
637
|
+
console.log('[RestoreHandler] Would load busy-cell records from backend');
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
640
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
641
|
+
// CellExecutionTracker
|
|
642
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
643
|
+
/**
|
|
644
|
+
* Continuously monitors a kernel's iopub channel to maintain a live map of
|
|
645
|
+
* **in-flight** execute requests: `msg_id → execution_count`.
|
|
646
|
+
*
|
|
647
|
+
* When the kernel broadcasts `execute_input`, the tracker records the
|
|
648
|
+
* execution count assigned by the kernel (this is the number that will
|
|
649
|
+
* eventually be shown as `[N]` in the cell prompt). When `status: idle`
|
|
650
|
+
* arrives for that same `msg_id`, the entry is removed.
|
|
651
|
+
*
|
|
652
|
+
* The tracker must be connected to the kernel **before** any cells are
|
|
653
|
+
* executed so that the execution count is available at checkpoint time.
|
|
654
|
+
*/
|
|
655
|
+
class CellExecutionTracker {
|
|
656
|
+
constructor() {
|
|
657
|
+
this._pendingExecutions = new Map();
|
|
658
|
+
this._kernel = null;
|
|
659
|
+
}
|
|
660
|
+
get pendingExecutions() {
|
|
661
|
+
return this._pendingExecutions;
|
|
662
|
+
}
|
|
663
|
+
connectToKernel(kernel) {
|
|
664
|
+
this.disconnectFromKernel();
|
|
665
|
+
this._kernel = kernel;
|
|
666
|
+
kernel.iopubMessage.connect(this._onIopubMessage, this);
|
|
667
|
+
}
|
|
668
|
+
disconnectFromKernel() {
|
|
669
|
+
if (this._kernel) {
|
|
670
|
+
this._kernel.iopubMessage.disconnect(this._onIopubMessage, this);
|
|
671
|
+
this._kernel = null;
|
|
672
|
+
}
|
|
673
|
+
this._pendingExecutions.clear();
|
|
674
|
+
}
|
|
675
|
+
_onIopubMessage(_sender, msg) {
|
|
676
|
+
const parentMsgId = getParentMsgId(msg);
|
|
677
|
+
if (!parentMsgId) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (msg.header.msg_type === 'execute_input') {
|
|
681
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
682
|
+
const execCount = msg.content.execution_count;
|
|
683
|
+
this._pendingExecutions.set(parentMsgId, execCount);
|
|
684
|
+
}
|
|
685
|
+
if (msg.header.msg_type === 'status' &&
|
|
686
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
687
|
+
msg.content.execution_state === 'idle') {
|
|
688
|
+
this._pendingExecutions.delete(parentMsgId);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
693
|
+
// scanBusyCells (Step 1 of the Catch-and-Render architecture)
|
|
694
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
695
|
+
/**
|
|
696
|
+
* Scans the active notebook for cells that are currently executing (`[*]`
|
|
697
|
+
* prompt) and extracts the `msg_id` of each in-flight execution request.
|
|
698
|
+
*
|
|
699
|
+
* ### How the msg_id is obtained
|
|
700
|
+
*
|
|
701
|
+
* When JupyterLab executes a cell it calls `kernel.requestExecute()` and
|
|
702
|
+
* stores the resulting `IShellFuture` on the cell's `OutputArea`:
|
|
703
|
+
*
|
|
704
|
+
* `codeCell.outputArea.future = kernel.requestExecute({ code })`
|
|
705
|
+
*
|
|
706
|
+
* JupyterLab exposes `future` as a **write-only** setter on `OutputArea`.
|
|
707
|
+
* The underlying field is `OutputArea._future` (private). We read it here
|
|
708
|
+
* because no public getter exists, and the `msg_id` it contains is the only
|
|
709
|
+
* reliable link between the cell widget and the kernel message stream.
|
|
710
|
+
*
|
|
711
|
+
* ### Why we also need the CellExecutionTracker
|
|
712
|
+
*
|
|
713
|
+
* The `_future.msg.header.msg_id` gives us the execute-request id, but not
|
|
714
|
+
* the **execution count** the kernel assigned (the `[N]` prompt number).
|
|
715
|
+
* During execution the cell's `model.executionCount` is `null` (that is
|
|
716
|
+
* what produces the `[*]` display). The actual count was broadcast earlier
|
|
717
|
+
* in an `execute_input` iopub message which the `CellExecutionTracker`
|
|
718
|
+
* recorded. We look it up here so that the restore phase can set the
|
|
719
|
+
* correct prompt after the cell finishes.
|
|
720
|
+
*/
|
|
721
|
+
function scanBusyCells(panel, tracker) {
|
|
722
|
+
var _a, _b, _c;
|
|
723
|
+
const records = [];
|
|
724
|
+
const cells = panel.content.widgets;
|
|
725
|
+
for (let i = 0; i < cells.length; i++) {
|
|
726
|
+
const cell = cells[i];
|
|
727
|
+
if (cell.model.type !== 'code') {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const codeCell = cell;
|
|
731
|
+
// `executionCount === null` is the canonical indicator for a busy cell
|
|
732
|
+
// (the prompt renders `[*]`). Fresh cells also have `null`, so we
|
|
733
|
+
// additionally require an active future below.
|
|
734
|
+
if (codeCell.model.executionCount !== null) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
// Read the private `_future` field. This is an `IShellFuture` whose
|
|
738
|
+
// `.msg.header.msg_id` is the id the kernel uses in `parent_header`
|
|
739
|
+
// for every message belonging to this execution.
|
|
740
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
741
|
+
const future = codeCell.outputArea._future;
|
|
742
|
+
if (!((_b = (_a = future === null || future === void 0 ? void 0 : future.msg) === null || _a === void 0 ? void 0 : _a.header) === null || _b === void 0 ? void 0 : _b.msg_id)) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const msgId = future.msg.header.msg_id;
|
|
746
|
+
const executionCount = (_c = tracker.pendingExecutions.get(msgId)) !== null && _c !== void 0 ? _c : null;
|
|
747
|
+
records.push({
|
|
748
|
+
cellIndex: i,
|
|
749
|
+
cellId: cell.model.id,
|
|
750
|
+
msgId,
|
|
751
|
+
executionCount
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return records;
|
|
755
|
+
}
|
|
756
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
757
|
+
// RestoreHandler (Steps 3–6 of the Catch-and-Render architecture)
|
|
758
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
759
|
+
/**
|
|
760
|
+
* Hooks into a **restored** kernel's iopub channel and intercepts "orphaned"
|
|
761
|
+
* messages whose `parent_header.msg_id` matches a cell that was busy at
|
|
762
|
+
* checkpoint time.
|
|
763
|
+
*
|
|
764
|
+
* For each intercepted message the handler manually injects the output into
|
|
765
|
+
* the cell's `OutputArea`, replicating what JupyterLab would normally do
|
|
766
|
+
* if the original execution future were still alive.
|
|
767
|
+
*
|
|
768
|
+
* ### Output injection mechanism
|
|
769
|
+
*
|
|
770
|
+
* `codeCell.outputArea.model` implements `IOutputAreaModel`. Calling
|
|
771
|
+
* `.add(output)` with an `nbformat.IOutput`-shaped object triggers the
|
|
772
|
+
* following internal chain:
|
|
773
|
+
*
|
|
774
|
+
* 1. The model wraps the raw JSON in an `OutputModel` instance.
|
|
775
|
+
* 2. It appends the model to its internal list.
|
|
776
|
+
* – For `stream` outputs with the same `name` (stdout / stderr) as the
|
|
777
|
+
* preceding entry, the text is **merged** instead of creating a new
|
|
778
|
+
* item. This matches standard notebook behavior.
|
|
779
|
+
* 3. A Lumino `changed` signal fires.
|
|
780
|
+
* 4. The `OutputArea` widget reacts and creates the visual renderer
|
|
781
|
+
* (e.g. `<pre>` for streams, a MIME renderer for display_data).
|
|
782
|
+
*
|
|
783
|
+
* Because all of this happens through the model's public API the rendered
|
|
784
|
+
* output is indistinguishable from one produced by the normal future-based
|
|
785
|
+
* routing.
|
|
786
|
+
*/
|
|
787
|
+
class RestoreHandler {
|
|
788
|
+
constructor() {
|
|
789
|
+
this._activeMsgIds = new Map();
|
|
790
|
+
this._panel = null;
|
|
791
|
+
this._kernel = null;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Begin intercepting orphaned messages for the given busy cells.
|
|
795
|
+
* Call immediately after `sessionContext.changeKernel()` resolves.
|
|
796
|
+
*/
|
|
797
|
+
hookRestoredKernel(panel, busyCells) {
|
|
798
|
+
var _a;
|
|
799
|
+
if (busyCells.length === 0) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
this.dispose();
|
|
803
|
+
this._panel = panel;
|
|
804
|
+
for (const record of busyCells) {
|
|
805
|
+
this._activeMsgIds.set(record.msgId, record);
|
|
806
|
+
}
|
|
807
|
+
// Re-apply the busy indicator ([*]) so the user sees the cells as
|
|
808
|
+
// still running while output continues to stream in.
|
|
809
|
+
for (const record of busyCells) {
|
|
810
|
+
const cell = this._findCell(panel, record);
|
|
811
|
+
if (cell) {
|
|
812
|
+
cell.model.executionCount = null;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const kernel = (_a = panel.sessionContext.session) === null || _a === void 0 ? void 0 : _a.kernel;
|
|
816
|
+
if (!kernel) {
|
|
817
|
+
console.warn('[RestoreHandler] No kernel on panel after changeKernel(); ' +
|
|
818
|
+
'cannot hook iopub. Orphaned outputs will be lost.');
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
this._kernel = kernel;
|
|
822
|
+
kernel.iopubMessage.connect(this._onIopubMessage, this);
|
|
823
|
+
console.log(`[RestoreHandler] Hooked into restored kernel iopub. ` +
|
|
824
|
+
`Tracking ${busyCells.length} busy cell(s):`, busyCells.map(r => `cell[${r.cellIndex}] (${r.cellId}) → ${r.msgId}`));
|
|
825
|
+
}
|
|
826
|
+
dispose() {
|
|
827
|
+
if (this._kernel) {
|
|
828
|
+
this._kernel.iopubMessage.disconnect(this._onIopubMessage, this);
|
|
829
|
+
this._kernel = null;
|
|
830
|
+
}
|
|
831
|
+
this._activeMsgIds.clear();
|
|
832
|
+
this._panel = null;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Locate the CodeCell for a given record. We first try the saved index
|
|
836
|
+
* (fast path) and verify by cell ID, then fall back to a linear scan.
|
|
837
|
+
* This handles the case where the user inserted or deleted cells between
|
|
838
|
+
* checkpoint and restore.
|
|
839
|
+
*/
|
|
840
|
+
_findCell(panel, record) {
|
|
841
|
+
const cells = panel.content.widgets;
|
|
842
|
+
if (record.cellIndex < cells.length) {
|
|
843
|
+
const candidate = cells[record.cellIndex];
|
|
844
|
+
if (candidate.model.type === 'code' &&
|
|
845
|
+
candidate.model.id === record.cellId) {
|
|
846
|
+
return candidate;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
for (let i = 0; i < cells.length; i++) {
|
|
850
|
+
const c = cells[i];
|
|
851
|
+
if (c.model.id === record.cellId && c.model.type === 'code') {
|
|
852
|
+
return c;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
// ── iopub signal handler ──────────────────────────────────────────────
|
|
858
|
+
_onIopubMessage(_sender, msg) {
|
|
859
|
+
const parentMsgId = getParentMsgId(msg);
|
|
860
|
+
if (!parentMsgId) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const record = this._activeMsgIds.get(parentMsgId);
|
|
864
|
+
if (!record) {
|
|
865
|
+
// Not one of our tracked orphaned executions — let JupyterLab's
|
|
866
|
+
// normal future-based routing handle this message.
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (!this._panel) {
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const codeCell = this._findCell(this._panel, record);
|
|
873
|
+
if (!codeCell) {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const msgType = msg.header.msg_type;
|
|
877
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
878
|
+
const content = msg.content;
|
|
879
|
+
// ── Step 5: Inject output into the cell's OutputArea ────────────
|
|
880
|
+
//
|
|
881
|
+
// We construct an nbformat-compatible output object and pass it to
|
|
882
|
+
// `outputArea.model.add()`. The model validates the shape, wraps it
|
|
883
|
+
// in an OutputModel, and emits a `changed` signal that causes the
|
|
884
|
+
// OutputArea widget to render the new entry.
|
|
885
|
+
switch (msgType) {
|
|
886
|
+
case 'stream':
|
|
887
|
+
codeCell.outputArea.model.add({
|
|
888
|
+
output_type: 'stream',
|
|
889
|
+
name: content.name,
|
|
890
|
+
text: content.text
|
|
891
|
+
});
|
|
892
|
+
break;
|
|
893
|
+
case 'error':
|
|
894
|
+
codeCell.outputArea.model.add({
|
|
895
|
+
output_type: 'error',
|
|
896
|
+
ename: content.ename,
|
|
897
|
+
evalue: content.evalue,
|
|
898
|
+
traceback: content.traceback
|
|
899
|
+
});
|
|
900
|
+
break;
|
|
901
|
+
case 'execute_result':
|
|
902
|
+
codeCell.outputArea.model.add({
|
|
903
|
+
output_type: 'execute_result',
|
|
904
|
+
data: content.data,
|
|
905
|
+
metadata: content.metadata,
|
|
906
|
+
execution_count: content.execution_count
|
|
907
|
+
});
|
|
908
|
+
break;
|
|
909
|
+
case 'display_data':
|
|
910
|
+
codeCell.outputArea.model.add({
|
|
911
|
+
output_type: 'display_data',
|
|
912
|
+
data: content.data,
|
|
913
|
+
metadata: content.metadata
|
|
914
|
+
});
|
|
915
|
+
break;
|
|
916
|
+
// ── Step 6: Lifecycle Cleanup ─────────────────────────────────
|
|
917
|
+
//
|
|
918
|
+
// A `status: idle` message with our tracked msg_id means the
|
|
919
|
+
// kernel finished executing this cell. We:
|
|
920
|
+
// a) Set the cell's execution count → prompt changes [*] → [N]
|
|
921
|
+
// b) Remove the msg_id from the tracking map
|
|
922
|
+
// c) Disconnect from iopub when no busy cells remain
|
|
923
|
+
case 'status':
|
|
924
|
+
if (content.execution_state === 'idle') {
|
|
925
|
+
if (record.executionCount !== null) {
|
|
926
|
+
codeCell.model.executionCount = record.executionCount;
|
|
927
|
+
}
|
|
928
|
+
this._activeMsgIds.delete(parentMsgId);
|
|
929
|
+
console.log(`[RestoreHandler] Cell[${record.cellIndex}] execution ` +
|
|
930
|
+
`complete (exec count: ${record.executionCount}). ` +
|
|
931
|
+
`${this._activeMsgIds.size} cell(s) still pending.`);
|
|
932
|
+
if (this._activeMsgIds.size === 0) {
|
|
933
|
+
console.log('[RestoreHandler] All restored cells complete. ' +
|
|
934
|
+
'Disconnecting iopub hook.');
|
|
935
|
+
this.dispose();
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
break;
|
|
939
|
+
default:
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
|
|
518
946
|
/***/ }
|
|
519
947
|
|
|
520
948
|
}]);
|
|
521
|
-
//# sourceMappingURL=lib_index_js.
|
|
949
|
+
//# sourceMappingURL=lib_index_js.b36bed60f63c982f956c.js.map
|