mrmd-monitor 0.1.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 +517 -0
- package/bin/cli.js +178 -0
- package/package.json +32 -0
- package/src/coordination.js +358 -0
- package/src/document.js +187 -0
- package/src/execution.js +261 -0
- package/src/index.js +14 -0
- package/src/monitor.js +420 -0
- package/src/terminal.js +294 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mrmd-monitor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless Yjs peer for monitoring and executing code in mrmd notebooks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mrmd-monitor": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/cli.js",
|
|
12
|
+
"dev": "node --watch bin/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mrmd",
|
|
16
|
+
"yjs",
|
|
17
|
+
"collaborative",
|
|
18
|
+
"notebook",
|
|
19
|
+
"execution",
|
|
20
|
+
"monitor"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"yjs": "^13.6.0",
|
|
26
|
+
"y-websocket": "^2.0.0",
|
|
27
|
+
"lib0": "^0.2.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordination Protocol
|
|
3
|
+
*
|
|
4
|
+
* Defines the Y.Map schema and protocol for browser/monitor coordination.
|
|
5
|
+
*
|
|
6
|
+
* @module mrmd-monitor/coordination
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as Y from 'yjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execution status values
|
|
13
|
+
*/
|
|
14
|
+
export const EXECUTION_STATUS = {
|
|
15
|
+
/** Browser has requested execution */
|
|
16
|
+
REQUESTED: 'requested',
|
|
17
|
+
/** Monitor has claimed the execution */
|
|
18
|
+
CLAIMED: 'claimed',
|
|
19
|
+
/** Browser has created output block, ready to start */
|
|
20
|
+
READY: 'ready',
|
|
21
|
+
/** Monitor is streaming output from runtime */
|
|
22
|
+
RUNNING: 'running',
|
|
23
|
+
/** Execution completed successfully */
|
|
24
|
+
COMPLETED: 'completed',
|
|
25
|
+
/** Execution failed with error */
|
|
26
|
+
ERROR: 'error',
|
|
27
|
+
/** Execution was cancelled */
|
|
28
|
+
CANCELLED: 'cancelled',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} ExecutionRequest
|
|
33
|
+
* @property {string} id - Unique execution ID
|
|
34
|
+
* @property {string} [cellId] - Cell ID this execution is for
|
|
35
|
+
* @property {string} code - Code to execute
|
|
36
|
+
* @property {string} language - Language identifier
|
|
37
|
+
* @property {string} runtimeUrl - MRP runtime URL
|
|
38
|
+
* @property {string} [session] - MRP session ID
|
|
39
|
+
* @property {string} status - Current status
|
|
40
|
+
* @property {number} requestedBy - Client ID that requested
|
|
41
|
+
* @property {number} requestedAt - Timestamp
|
|
42
|
+
* @property {number} [claimedBy] - Client ID that claimed
|
|
43
|
+
* @property {number} [claimedAt] - Timestamp
|
|
44
|
+
* @property {boolean} outputBlockReady - Whether output block exists in Y.Text
|
|
45
|
+
* @property {Object} [outputPosition] - Yjs RelativePosition JSON
|
|
46
|
+
* @property {number} [startedAt] - Timestamp
|
|
47
|
+
* @property {number} [completedAt] - Timestamp
|
|
48
|
+
* @property {Object} [stdinRequest] - Pending stdin request
|
|
49
|
+
* @property {Object} [stdinResponse] - Response to stdin request
|
|
50
|
+
* @property {*} [result] - Execution result
|
|
51
|
+
* @property {Object} [error] - Execution error
|
|
52
|
+
* @property {Array} [displayData] - Rich outputs
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Coordination protocol for browser/monitor communication via Y.Map
|
|
57
|
+
*/
|
|
58
|
+
export class CoordinationProtocol {
|
|
59
|
+
/**
|
|
60
|
+
* @param {Y.Doc} ydoc - Yjs document
|
|
61
|
+
* @param {number} clientId - This client's ID
|
|
62
|
+
*/
|
|
63
|
+
constructor(ydoc, clientId) {
|
|
64
|
+
/** @type {Y.Doc} */
|
|
65
|
+
this.ydoc = ydoc;
|
|
66
|
+
|
|
67
|
+
/** @type {number} */
|
|
68
|
+
this.clientId = clientId;
|
|
69
|
+
|
|
70
|
+
/** @type {Y.Map} */
|
|
71
|
+
this.executions = ydoc.getMap('executions');
|
|
72
|
+
|
|
73
|
+
/** @type {Set<Function>} */
|
|
74
|
+
this._observers = new Set();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Generate unique execution ID
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
static generateExecId() {
|
|
82
|
+
return `exec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Request execution (called by browser)
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} options
|
|
89
|
+
* @param {string} options.code
|
|
90
|
+
* @param {string} options.language
|
|
91
|
+
* @param {string} options.runtimeUrl
|
|
92
|
+
* @param {string} [options.session]
|
|
93
|
+
* @param {string} [options.cellId]
|
|
94
|
+
* @returns {string} execId
|
|
95
|
+
*/
|
|
96
|
+
requestExecution({ code, language, runtimeUrl, session = 'default', cellId }) {
|
|
97
|
+
const execId = CoordinationProtocol.generateExecId();
|
|
98
|
+
|
|
99
|
+
this.executions.set(execId, {
|
|
100
|
+
id: execId,
|
|
101
|
+
cellId,
|
|
102
|
+
code,
|
|
103
|
+
language,
|
|
104
|
+
runtimeUrl,
|
|
105
|
+
session,
|
|
106
|
+
status: EXECUTION_STATUS.REQUESTED,
|
|
107
|
+
requestedBy: this.clientId,
|
|
108
|
+
requestedAt: Date.now(),
|
|
109
|
+
claimedBy: null,
|
|
110
|
+
claimedAt: null,
|
|
111
|
+
outputBlockReady: false,
|
|
112
|
+
outputPosition: null,
|
|
113
|
+
startedAt: null,
|
|
114
|
+
completedAt: null,
|
|
115
|
+
stdinRequest: null,
|
|
116
|
+
stdinResponse: null,
|
|
117
|
+
result: null,
|
|
118
|
+
error: null,
|
|
119
|
+
displayData: [],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return execId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Claim execution (called by monitor)
|
|
127
|
+
*
|
|
128
|
+
* @param {string} execId
|
|
129
|
+
* @returns {boolean} true if claimed successfully
|
|
130
|
+
*/
|
|
131
|
+
claimExecution(execId) {
|
|
132
|
+
const exec = this.executions.get(execId);
|
|
133
|
+
if (!exec) return false;
|
|
134
|
+
|
|
135
|
+
// Only claim if status is REQUESTED and not already claimed
|
|
136
|
+
if (exec.status !== EXECUTION_STATUS.REQUESTED) return false;
|
|
137
|
+
if (exec.claimedBy !== null) return false;
|
|
138
|
+
|
|
139
|
+
// Claim it
|
|
140
|
+
this.executions.set(execId, {
|
|
141
|
+
...exec,
|
|
142
|
+
status: EXECUTION_STATUS.CLAIMED,
|
|
143
|
+
claimedBy: this.clientId,
|
|
144
|
+
claimedAt: Date.now(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Mark output block as ready (called by browser)
|
|
152
|
+
*
|
|
153
|
+
* @param {string} execId
|
|
154
|
+
* @param {Object} outputPosition - Yjs RelativePosition as JSON
|
|
155
|
+
*/
|
|
156
|
+
setOutputBlockReady(execId, outputPosition) {
|
|
157
|
+
const exec = this.executions.get(execId);
|
|
158
|
+
if (!exec) return;
|
|
159
|
+
|
|
160
|
+
this.executions.set(execId, {
|
|
161
|
+
...exec,
|
|
162
|
+
status: EXECUTION_STATUS.READY,
|
|
163
|
+
outputBlockReady: true,
|
|
164
|
+
outputPosition,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Mark execution as running (called by monitor)
|
|
170
|
+
*
|
|
171
|
+
* @param {string} execId
|
|
172
|
+
*/
|
|
173
|
+
setRunning(execId) {
|
|
174
|
+
const exec = this.executions.get(execId);
|
|
175
|
+
if (!exec) return;
|
|
176
|
+
|
|
177
|
+
this.executions.set(execId, {
|
|
178
|
+
...exec,
|
|
179
|
+
status: EXECUTION_STATUS.RUNNING,
|
|
180
|
+
startedAt: Date.now(),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Mark execution as completed (called by monitor)
|
|
186
|
+
*
|
|
187
|
+
* @param {string} execId
|
|
188
|
+
* @param {Object} options
|
|
189
|
+
* @param {*} [options.result]
|
|
190
|
+
* @param {Array} [options.displayData]
|
|
191
|
+
*/
|
|
192
|
+
setCompleted(execId, { result, displayData } = {}) {
|
|
193
|
+
const exec = this.executions.get(execId);
|
|
194
|
+
if (!exec) return;
|
|
195
|
+
|
|
196
|
+
this.executions.set(execId, {
|
|
197
|
+
...exec,
|
|
198
|
+
status: EXECUTION_STATUS.COMPLETED,
|
|
199
|
+
completedAt: Date.now(),
|
|
200
|
+
result: result ?? null,
|
|
201
|
+
displayData: displayData ?? exec.displayData,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Mark execution as error (called by monitor)
|
|
207
|
+
*
|
|
208
|
+
* @param {string} execId
|
|
209
|
+
* @param {Object} error
|
|
210
|
+
*/
|
|
211
|
+
setError(execId, error) {
|
|
212
|
+
const exec = this.executions.get(execId);
|
|
213
|
+
if (!exec) return;
|
|
214
|
+
|
|
215
|
+
this.executions.set(execId, {
|
|
216
|
+
...exec,
|
|
217
|
+
status: EXECUTION_STATUS.ERROR,
|
|
218
|
+
completedAt: Date.now(),
|
|
219
|
+
error,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Request stdin input (called by monitor)
|
|
225
|
+
*
|
|
226
|
+
* @param {string} execId
|
|
227
|
+
* @param {Object} request
|
|
228
|
+
* @param {string} request.prompt
|
|
229
|
+
* @param {boolean} [request.password]
|
|
230
|
+
*/
|
|
231
|
+
requestStdin(execId, { prompt, password = false }) {
|
|
232
|
+
const exec = this.executions.get(execId);
|
|
233
|
+
if (!exec) return;
|
|
234
|
+
|
|
235
|
+
this.executions.set(execId, {
|
|
236
|
+
...exec,
|
|
237
|
+
stdinRequest: {
|
|
238
|
+
prompt,
|
|
239
|
+
password,
|
|
240
|
+
requestedAt: Date.now(),
|
|
241
|
+
},
|
|
242
|
+
stdinResponse: null,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Respond to stdin request (called by browser)
|
|
248
|
+
*
|
|
249
|
+
* @param {string} execId
|
|
250
|
+
* @param {string} text
|
|
251
|
+
*/
|
|
252
|
+
respondStdin(execId, text) {
|
|
253
|
+
const exec = this.executions.get(execId);
|
|
254
|
+
if (!exec) return;
|
|
255
|
+
|
|
256
|
+
this.executions.set(execId, {
|
|
257
|
+
...exec,
|
|
258
|
+
stdinResponse: {
|
|
259
|
+
text,
|
|
260
|
+
respondedAt: Date.now(),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Clear stdin request after processing (called by monitor)
|
|
267
|
+
*
|
|
268
|
+
* @param {string} execId
|
|
269
|
+
*/
|
|
270
|
+
clearStdinRequest(execId) {
|
|
271
|
+
const exec = this.executions.get(execId);
|
|
272
|
+
if (!exec) return;
|
|
273
|
+
|
|
274
|
+
this.executions.set(execId, {
|
|
275
|
+
...exec,
|
|
276
|
+
stdinRequest: null,
|
|
277
|
+
stdinResponse: null,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Add display data (called by monitor)
|
|
283
|
+
*
|
|
284
|
+
* @param {string} execId
|
|
285
|
+
* @param {Object} display
|
|
286
|
+
* @param {string} display.mimeType
|
|
287
|
+
* @param {string} [display.data]
|
|
288
|
+
* @param {string} [display.assetId]
|
|
289
|
+
*/
|
|
290
|
+
addDisplayData(execId, display) {
|
|
291
|
+
const exec = this.executions.get(execId);
|
|
292
|
+
if (!exec) return;
|
|
293
|
+
|
|
294
|
+
this.executions.set(execId, {
|
|
295
|
+
...exec,
|
|
296
|
+
displayData: [...(exec.displayData || []), display],
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get execution by ID
|
|
302
|
+
*
|
|
303
|
+
* @param {string} execId
|
|
304
|
+
* @returns {ExecutionRequest|undefined}
|
|
305
|
+
*/
|
|
306
|
+
getExecution(execId) {
|
|
307
|
+
return this.executions.get(execId);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get all executions with a specific status
|
|
312
|
+
*
|
|
313
|
+
* @param {string} status
|
|
314
|
+
* @returns {ExecutionRequest[]}
|
|
315
|
+
*/
|
|
316
|
+
getExecutionsByStatus(status) {
|
|
317
|
+
const results = [];
|
|
318
|
+
this.executions.forEach((exec, id) => {
|
|
319
|
+
if (exec.status === status) {
|
|
320
|
+
results.push(exec);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
return results;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Observe execution changes
|
|
328
|
+
*
|
|
329
|
+
* @param {Function} callback - Called with (execId, execution, changeType)
|
|
330
|
+
* @returns {Function} Unsubscribe function
|
|
331
|
+
*/
|
|
332
|
+
observe(callback) {
|
|
333
|
+
const observer = (event) => {
|
|
334
|
+
event.changes.keys.forEach((change, key) => {
|
|
335
|
+
const exec = this.executions.get(key);
|
|
336
|
+
callback(key, exec, change.action);
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
this.executions.observe(observer);
|
|
341
|
+
this._observers.add(observer);
|
|
342
|
+
|
|
343
|
+
return () => {
|
|
344
|
+
this.executions.unobserve(observer);
|
|
345
|
+
this._observers.delete(observer);
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Clean up observers
|
|
351
|
+
*/
|
|
352
|
+
destroy() {
|
|
353
|
+
for (const observer of this._observers) {
|
|
354
|
+
this.executions.unobserve(observer);
|
|
355
|
+
}
|
|
356
|
+
this._observers.clear();
|
|
357
|
+
}
|
|
358
|
+
}
|
package/src/document.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Writer
|
|
3
|
+
*
|
|
4
|
+
* Handles Y.Text manipulation for writing execution output.
|
|
5
|
+
*
|
|
6
|
+
* @module mrmd-monitor/document
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as Y from 'yjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Writer for execution output to Y.Text
|
|
13
|
+
*/
|
|
14
|
+
export class DocumentWriter {
|
|
15
|
+
/**
|
|
16
|
+
* @param {Y.Doc} ydoc - Yjs document
|
|
17
|
+
* @param {string} [textName='content'] - Name of the Y.Text
|
|
18
|
+
*/
|
|
19
|
+
constructor(ydoc, textName = 'content') {
|
|
20
|
+
/** @type {Y.Doc} */
|
|
21
|
+
this.ydoc = ydoc;
|
|
22
|
+
|
|
23
|
+
/** @type {Y.Text} */
|
|
24
|
+
this.ytext = ydoc.getText(textName);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find output block by execId
|
|
29
|
+
*
|
|
30
|
+
* Searches for ```output:<execId> marker in the document.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} execId
|
|
33
|
+
* @returns {{markerStart: number, contentStart: number, contentEnd: number}|null}
|
|
34
|
+
*/
|
|
35
|
+
findOutputBlock(execId) {
|
|
36
|
+
const text = this.ytext.toString();
|
|
37
|
+
const marker = '```output:' + execId;
|
|
38
|
+
const markerStart = text.indexOf(marker);
|
|
39
|
+
|
|
40
|
+
if (markerStart === -1) return null;
|
|
41
|
+
|
|
42
|
+
// Find the newline after marker
|
|
43
|
+
const newlineAfterMarker = text.indexOf('\n', markerStart);
|
|
44
|
+
if (newlineAfterMarker === -1) return null;
|
|
45
|
+
|
|
46
|
+
const contentStart = newlineAfterMarker + 1;
|
|
47
|
+
|
|
48
|
+
// Find the closing ``` - it's the first ``` after contentStart
|
|
49
|
+
// Output blocks don't contain nested code, so this is safe
|
|
50
|
+
const closingPos = text.indexOf('```', contentStart);
|
|
51
|
+
const contentEnd = closingPos === -1 ? text.length : closingPos;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
markerStart,
|
|
55
|
+
contentStart,
|
|
56
|
+
contentEnd,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Append content to output block
|
|
62
|
+
*
|
|
63
|
+
* @param {string} execId
|
|
64
|
+
* @param {string} content
|
|
65
|
+
* @returns {boolean} true if successful
|
|
66
|
+
*/
|
|
67
|
+
appendOutput(execId, content) {
|
|
68
|
+
const block = this.findOutputBlock(execId);
|
|
69
|
+
if (!block) {
|
|
70
|
+
console.warn(`[DocumentWriter] Output block not found for ${execId}`);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Insert just before the closing ```
|
|
75
|
+
this.ytext.insert(block.contentEnd, content);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Replace all content in output block
|
|
81
|
+
*
|
|
82
|
+
* @param {string} execId
|
|
83
|
+
* @param {string} content
|
|
84
|
+
* @returns {boolean} true if successful
|
|
85
|
+
*/
|
|
86
|
+
replaceOutput(execId, content) {
|
|
87
|
+
const block = this.findOutputBlock(execId);
|
|
88
|
+
if (!block) {
|
|
89
|
+
console.warn(`[DocumentWriter] Output block not found for ${execId}`);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Ensure content ends with newline so closing ``` stays on its own line
|
|
94
|
+
const normalizedContent = content && !content.endsWith('\n') ? content + '\n' : content;
|
|
95
|
+
|
|
96
|
+
// Use transaction for atomic delete+insert
|
|
97
|
+
this.ydoc.transact(() => {
|
|
98
|
+
// Delete existing content
|
|
99
|
+
const existingLength = block.contentEnd - block.contentStart;
|
|
100
|
+
if (existingLength > 0) {
|
|
101
|
+
this.ytext.delete(block.contentStart, existingLength);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Insert new content
|
|
105
|
+
if (normalizedContent) {
|
|
106
|
+
this.ytext.insert(block.contentStart, normalizedContent);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create relative position for output block insertion point
|
|
115
|
+
*
|
|
116
|
+
* @param {string} execId
|
|
117
|
+
* @returns {Object|null} RelativePosition as JSON, or null if not found
|
|
118
|
+
*/
|
|
119
|
+
createOutputPosition(execId) {
|
|
120
|
+
const block = this.findOutputBlock(execId);
|
|
121
|
+
if (!block) return null;
|
|
122
|
+
|
|
123
|
+
const relPos = Y.createRelativePositionFromTypeIndex(this.ytext, block.contentStart);
|
|
124
|
+
return Y.relativePositionToJSON(relPos);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get absolute position from stored relative position
|
|
129
|
+
*
|
|
130
|
+
* @param {Object} relPosJson - RelativePosition as JSON
|
|
131
|
+
* @returns {number|null} Absolute position, or null if invalid
|
|
132
|
+
*/
|
|
133
|
+
getAbsolutePosition(relPosJson) {
|
|
134
|
+
if (!relPosJson) return null;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const relPos = Y.createRelativePositionFromJSON(relPosJson);
|
|
138
|
+
const absPos = Y.createAbsolutePositionFromRelativePosition(relPos, this.ydoc);
|
|
139
|
+
return absPos?.index ?? null;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.warn('[DocumentWriter] Failed to resolve relative position:', err.message);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Insert at relative position
|
|
148
|
+
*
|
|
149
|
+
* @param {Object} relPosJson - RelativePosition as JSON
|
|
150
|
+
* @param {string} content
|
|
151
|
+
* @returns {boolean} true if successful
|
|
152
|
+
*/
|
|
153
|
+
insertAtPosition(relPosJson, content) {
|
|
154
|
+
const absPos = this.getAbsolutePosition(relPosJson);
|
|
155
|
+
if (absPos === null) {
|
|
156
|
+
console.warn('[DocumentWriter] Could not resolve position');
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.ytext.insert(absPos, content);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get current output block content
|
|
166
|
+
*
|
|
167
|
+
* @param {string} execId
|
|
168
|
+
* @returns {string|null}
|
|
169
|
+
*/
|
|
170
|
+
getOutputContent(execId) {
|
|
171
|
+
const block = this.findOutputBlock(execId);
|
|
172
|
+
if (!block) return null;
|
|
173
|
+
|
|
174
|
+
const text = this.ytext.toString();
|
|
175
|
+
return text.slice(block.contentStart, block.contentEnd);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check if output block exists
|
|
180
|
+
*
|
|
181
|
+
* @param {string} execId
|
|
182
|
+
* @returns {boolean}
|
|
183
|
+
*/
|
|
184
|
+
hasOutputBlock(execId) {
|
|
185
|
+
return this.findOutputBlock(execId) !== null;
|
|
186
|
+
}
|
|
187
|
+
}
|