jupyterlab-ipyflow 0.0.228 → 0.0.230

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.
@@ -7,7 +7,7 @@ import { INotebookTracker, Notebook } from '@jupyterlab/notebook';
7
7
  * dispatcher, and open the comm. Returns a disconnect handler that tears the
8
8
  * connection down.
9
9
  */
10
- export declare function connectToComm(session: ISessionContext, notebooks: INotebookTracker, notebook: Notebook, docManager: IDocumentManager): () => void;
10
+ export declare function connectToComm(session: ISessionContext, notebooks: INotebookTracker, notebook: Notebook, docManager: IDocumentManager, allowSelfEstablish?: boolean): () => void;
11
11
  /**
12
12
  * Activate-time wiring: track the foreground notebook for the debug hook and
13
13
  * register the `ipyflow-client` comm target (plus kernel-restart handling) for
@@ -3,16 +3,70 @@ import { updateUI } from '../ui/decorations';
3
3
  import { clearCellState } from '../ui/dom';
4
4
  import { createMessageHandler } from './messageHandlers';
5
5
  import { createNotebookEventHandlers } from './notebookEvents';
6
+ /**
7
+ * Pre-install (but do NOT activate) ipyflow under JupyterLite/Pyodide, so the
8
+ * user can `%load_ext ipyflow` without a manual `%pip install` first. This only
9
+ * `pip install`s `ipyflow-core` (resolved offline from the bundled piplite
10
+ * index) -- it does not run `load_ipython_extension`, so nothing activates until
11
+ * the user opts in. On a regular Jupyter kernel the `sys.platform` gate makes it
12
+ * a no-op.
13
+ *
14
+ * Awaited before the comm is set up so it lands as the first execute on the
15
+ * fresh kernel; `import ipyflow` is then ready by the time the user runs
16
+ * `%load_ext ipyflow` (even from a "Run All").
17
+ */
18
+ async function ensureIpyflowInstalled(session) {
19
+ const kernel = session.session?.kernel;
20
+ if (kernel == null) {
21
+ return;
22
+ }
23
+ const code = [
24
+ 'import sys as _sys',
25
+ "if _sys.platform == 'emscripten':",
26
+ ' try:',
27
+ ' import piplite as _piplite',
28
+ // keep_going=True mirrors what the `%pip install` magic does: skip deps that
29
+ // can't be resolved in-browser (e.g. ipykernel/pyzmq, ipywidgets) rather
30
+ // than failing the whole install. ipyflow imports fine without them.
31
+ " await _piplite.install('ipyflow-core', keep_going=True)",
32
+ ' except Exception:',
33
+ ' pass',
34
+ ].join('\n');
35
+ try {
36
+ await kernel.requestExecute({
37
+ code,
38
+ silent: true,
39
+ store_history: false,
40
+ stop_on_error: false,
41
+ }).done;
42
+ }
43
+ catch {
44
+ // Expected if the kernel is restarted during the heavy first load; the
45
+ // install re-runs on reconnect (kernelChanged handler in setupComm).
46
+ }
47
+ }
48
+ /**
49
+ * Create the `ipyflow` data comm. We let the kernel comm manager assign the
50
+ * comm id instead of pinning it to a fixed `'ipyflow'`: on a JupyterLite/Pyodide
51
+ * kernel reconnect (e.g. the kernel stalling during the one-time in-browser
52
+ * install) the previous comm may not be disposed yet, and re-creating it under a
53
+ * fixed id throws "Comm is already created", leaving the session with a dead
54
+ * comm. The kernel side keys off the target name, not the id, so a fresh id each
55
+ * time is safe and reconnect-resilient.
56
+ */
57
+ function createIpyflowComm(session) {
58
+ return session.session.kernel.createComm('ipyflow');
59
+ }
6
60
  /**
7
61
  * Establish the ipyflow comm for a notebook session: create the store, build
8
62
  * the connection context, wire the notebook event handlers and message
9
63
  * dispatcher, and open the comm. Returns a disconnect handler that tears the
10
64
  * connection down.
11
65
  */
12
- export function connectToComm(session, notebooks, notebook, docManager) {
66
+ export function connectToComm(session, notebooks, notebook, docManager, allowSelfEstablish = false) {
13
67
  const store = initStore(session.session.id);
14
68
  store.activeCell = notebook.activeCell;
15
- store.comm = session.session.kernel.createComm('ipyflow', 'ipyflow');
69
+ store.comm = createIpyflowComm(session);
16
70
  store.notebook = notebook;
17
71
  store.session = session;
18
72
  const ipyflowMetadata = notebook.model.getMetadata?.('ipyflow') ?? {};
@@ -42,7 +96,7 @@ export function connectToComm(session, notebooks, notebook, docManager) {
42
96
  else if (store.comm.isDisposed) {
43
97
  ctx.onEstablishPayload = data;
44
98
  const oldComm = store.comm;
45
- store.comm = session.session.kernel.createComm('ipyflow', 'ipyflow');
99
+ store.comm = createIpyflowComm(session);
46
100
  store.comm.onMsg = oldComm.onMsg;
47
101
  store.comm.open({
48
102
  interface: 'jupyterlab',
@@ -73,6 +127,29 @@ export function connectToComm(session, notebooks, notebook, docManager) {
73
127
  cell_parents: ctx.ipyflowMetadata?.cell_parents ?? {},
74
128
  cell_children: ctx.ipyflowMetadata?.cell_children ?? {},
75
129
  });
130
+ // Fallback for kernels that cannot deliver the kernel-side `establish` reply
131
+ // sent from inside their comm_open handler (JupyterLite/Pyodide: the worker is
132
+ // mid-RPC and blocks its event loop between requests, so the reply never
133
+ // lands). comm_open runs synchronously kernel-side there, so by now the kernel
134
+ // has already initialized the comm target; if we still haven't been
135
+ // established shortly after opening, drive the establish handling ourselves.
136
+ //
137
+ // Only do this when the connect was triggered by the `ipyflow-client`
138
+ // establish (i.e. `%load_ext ipyflow` actually ran and registered the comm
139
+ // target) -- NOT for the speculative connect on session-ready. Otherwise a
140
+ // kernel with ipyflow not loaded (e.g. before `%load_ext`) would falsely
141
+ // appear ipyflow-connected. On a normal kernel the real `establish` arrives
142
+ // first and this no-ops regardless.
143
+ if (allowSelfEstablish) {
144
+ setTimeout(() => {
145
+ if (ctx.disconnected || store.isIpyflowCommConnected) {
146
+ return;
147
+ }
148
+ store.comm.onMsg({
149
+ content: { data: { type: 'establish', success: true } },
150
+ });
151
+ }, 2000);
152
+ }
76
153
  return commDisconnectHandler;
77
154
  }
78
155
  /**
@@ -108,15 +185,19 @@ export function setupComm(notebooks, docManager) {
108
185
  }
109
186
  else if (payload.type === 'establish') {
110
187
  commDisconnectHandler();
111
- commDisconnectHandler = connectToComm(session, notebooks, nbPanel.content, docManager);
188
+ // `%load_ext ipyflow` ran and registered the comm target, so allow
189
+ // the self-establish fallback (the JupyterLite data-comm establish
190
+ // is not delivered on its own).
191
+ commDisconnectHandler = connectToComm(session, notebooks, nbPanel.content, docManager, true);
112
192
  }
113
193
  };
114
194
  commDisconnectHandler();
115
- commDisconnectHandler = connectToComm(session, notebooks, nbPanel.content, docManager);
195
+ commDisconnectHandler = connectToComm(session, notebooks, nbPanel.content, docManager, true);
116
196
  });
117
197
  };
118
- session.ready.then(() => {
198
+ session.ready.then(async () => {
119
199
  clearCellState(nbPanel.content);
200
+ await ensureIpyflowInstalled(session);
120
201
  registerCommTarget();
121
202
  commDisconnectHandler();
122
203
  commDisconnectHandler = connectToComm(session, notebooks, nbPanel.content, docManager);
@@ -128,7 +209,8 @@ export function setupComm(notebooks, docManager) {
128
209
  commDisconnectHandler();
129
210
  resetStore(session.session.id);
130
211
  commDisconnectHandler = () => resetStore(session.session.id);
131
- session.ready.then(() => {
212
+ session.ready.then(async () => {
213
+ await ensureIpyflowInstalled(session);
132
214
  registerCommTarget();
133
215
  commDisconnectHandler = connectToComm(session, notebooks, nbPanel.content, docManager);
134
216
  });
@@ -31,7 +31,13 @@ export function patchRunCommands(app, notebooks) {
31
31
  };
32
32
  const isBatchReactive = () => {
33
33
  const state = getIpyflowState();
34
- return state.isBatchReactive();
34
+ // `getIpyflowState` returns an empty `{}` when the store is missing (e.g.
35
+ // during a JupyterLite kernel reconnect, when the store was reset). Guard so
36
+ // running a cell / pressing a shortcut doesn't throw
37
+ // "isBatchReactive is not a function" before the cell is ever submitted.
38
+ return typeof state.isBatchReactive === 'function'
39
+ ? state.isBatchReactive()
40
+ : false;
35
41
  };
36
42
  const executeBatchReactive = (skipFirst = false) => {
37
43
  const state = getIpyflowState();
package/package.json CHANGED
@@ -73,5 +73,5 @@
73
73
  "extension": true,
74
74
  "outputDir": "../../core/ipyflow/resources/labextension/"
75
75
  },
76
- "version": "0.0.228"
76
+ "version": "0.0.230"
77
77
  }