@textmode/runner-client 0.1.0 → 0.2.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 +10 -24
- package/dist/IframeTextmodeRuntime.d.ts +4 -68
- package/dist/IframeTextmodeRuntime.js +42 -195
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -3
- package/dist/internal/heartbeat.d.ts +8 -0
- package/dist/internal/heartbeat.js +38 -4
- package/dist/internal/messageRouter.d.ts +1 -6
- package/dist/internal/messageRouter.js +0 -15
- package/dist/internal/requestRegistry.d.ts +5 -2
- package/dist/internal/requestRegistry.js +59 -16
- package/dist/internal/visibility.d.ts +6 -0
- package/dist/internal/visibility.js +65 -0
- package/dist/options.d.ts +2 -6
- package/package.json +2 -2
- package/dist/font.d.ts +0 -11
- package/dist/font.js +0 -1
package/README.md
CHANGED
|
@@ -4,9 +4,8 @@ Browser iframe runtime client for the hosted textmode runner.
|
|
|
4
4
|
|
|
5
5
|
This package gives any browser host app a typed runtime API for mounting the
|
|
6
6
|
runner iframe, performing the current generic protocol handshake, routing
|
|
7
|
-
request/response messages, monitoring heartbeat status,
|
|
8
|
-
|
|
9
|
-
disposing the transport.
|
|
7
|
+
request/response messages, monitoring heartbeat status, running code,
|
|
8
|
+
soft-resetting sketches, reconnecting, and disposing the transport.
|
|
10
9
|
|
|
11
10
|
It does not execute textmode.js sketches directly. It controls a runner app at
|
|
12
11
|
the `runnerUrl` you provide.
|
|
@@ -40,17 +39,9 @@ const runtime = new IframeTextmodeRuntime({
|
|
|
40
39
|
onStatusChange(status: RunnerRuntimeStatus, reason) {
|
|
41
40
|
console.info('runner status changed', status, reason);
|
|
42
41
|
},
|
|
43
|
-
onExportProgress(requestId, format, progress) {
|
|
44
|
-
console.info('export progress', requestId, format, progress);
|
|
45
|
-
},
|
|
46
42
|
});
|
|
47
43
|
|
|
48
|
-
await runtime.init(container
|
|
49
|
-
width: 640,
|
|
50
|
-
height: 640,
|
|
51
|
-
fontSize: 16,
|
|
52
|
-
frameRate: 60,
|
|
53
|
-
});
|
|
44
|
+
await runtime.init(container);
|
|
54
45
|
|
|
55
46
|
try {
|
|
56
47
|
await runtime.runCode(`
|
|
@@ -64,12 +55,7 @@ try {
|
|
|
64
55
|
}
|
|
65
56
|
}
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
format: 'png',
|
|
69
|
-
scale: 2,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
console.info(image.filename, image.mimeType, image.blob);
|
|
58
|
+
await runtime.runCode('t.frameCount = 0;', { softReset: true });
|
|
73
59
|
runtime.dispose();
|
|
74
60
|
```
|
|
75
61
|
|
|
@@ -91,7 +77,6 @@ The main exports are:
|
|
|
91
77
|
- `RunnerRuntimeStatus`
|
|
92
78
|
- `RunnerExecutionError`
|
|
93
79
|
- `RunnerRequestError`
|
|
94
|
-
- `FontLoadResult`
|
|
95
80
|
- `IframeTextmodeRuntimeOptions`
|
|
96
81
|
- `IframeMountMode`
|
|
97
82
|
- `IframeSandboxToken`
|
|
@@ -102,9 +87,9 @@ The main exports are:
|
|
|
102
87
|
Typical host apps follow this lifecycle:
|
|
103
88
|
|
|
104
89
|
1. Create an `IframeTextmodeRuntime` with a trusted `runnerUrl`.
|
|
105
|
-
2. Call `init(container
|
|
106
|
-
3. Use `runCode
|
|
107
|
-
|
|
90
|
+
2. Call `init(container)` from a browser context.
|
|
91
|
+
3. Use `runCode` for normal execution and `runCode(code, { softReset: true })`
|
|
92
|
+
when the host should reset sketch time before rerunning code.
|
|
108
93
|
4. Call `reconnect` after a recoverable runner failure.
|
|
109
94
|
5. Call `dispose` when the host view is unmounted.
|
|
110
95
|
|
|
@@ -118,8 +103,9 @@ The default iframe sandbox tokens are:
|
|
|
118
103
|
['allow-scripts', 'allow-same-origin']
|
|
119
104
|
```
|
|
120
105
|
|
|
121
|
-
`allow-downloads` is not included by default.
|
|
122
|
-
|
|
106
|
+
`allow-downloads` is not included by default. Sketches can use the installed
|
|
107
|
+
`textmode.export.js` helpers inside the sandboxed runtime, but the host client
|
|
108
|
+
does not expose a parent-controlled export channel.
|
|
123
109
|
|
|
124
110
|
The runtime refuses to start a runner that combines `allow-scripts` and
|
|
125
111
|
`allow-same-origin` on the same origin as the parent page.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import type { FontLoadResult } from './font';
|
|
1
|
+
import { type RunnerCapabilities } from '@textmode/runner-protocol';
|
|
3
2
|
import { type IframeTextmodeRuntimeOptions } from './options';
|
|
4
3
|
import type { RunnerRuntimeStatus } from './status';
|
|
5
4
|
/**
|
|
@@ -17,11 +16,11 @@ export declare class IframeTextmodeRuntime {
|
|
|
17
16
|
private readonly mountMode;
|
|
18
17
|
private readonly pending;
|
|
19
18
|
private readonly heartbeat;
|
|
19
|
+
private readonly visibility;
|
|
20
20
|
private iframe;
|
|
21
21
|
private channel;
|
|
22
22
|
private port;
|
|
23
23
|
private container;
|
|
24
|
-
private settings;
|
|
25
24
|
private capabilities;
|
|
26
25
|
private ready;
|
|
27
26
|
private currentStatus;
|
|
@@ -64,11 +63,10 @@ export declare class IframeTextmodeRuntime {
|
|
|
64
63
|
* Mounts the runner iframe and performs the current protocol handshake.
|
|
65
64
|
*
|
|
66
65
|
* @param container - DOM element that should contain the runner iframe.
|
|
67
|
-
* @param settings - Optional fixed runtime settings to configure after ready.
|
|
68
66
|
* @returns `true` when the runner is ready.
|
|
69
67
|
* @category Runtime
|
|
70
68
|
*/
|
|
71
|
-
init(container: HTMLElement
|
|
69
|
+
init(container: HTMLElement): Promise<boolean>;
|
|
72
70
|
/**
|
|
73
71
|
* Disposes the iframe connection and rejects pending requests.
|
|
74
72
|
*
|
|
@@ -90,18 +88,6 @@ export declare class IframeTextmodeRuntime {
|
|
|
90
88
|
* @category Runtime
|
|
91
89
|
*/
|
|
92
90
|
activateFromUserGesture(): void;
|
|
93
|
-
/**
|
|
94
|
-
* Configures complete fixed runtime settings.
|
|
95
|
-
*
|
|
96
|
-
* @category Runtime
|
|
97
|
-
*/
|
|
98
|
-
configure(settings: RuntimeSettings): Promise<PlaybackState | null>;
|
|
99
|
-
/**
|
|
100
|
-
* Applies a partial runtime settings update.
|
|
101
|
-
*
|
|
102
|
-
* @category Runtime
|
|
103
|
-
*/
|
|
104
|
-
setSettings(settings: Partial<RuntimeSettings>): Promise<PlaybackState | null>;
|
|
105
91
|
/**
|
|
106
92
|
* Executes code in the runner.
|
|
107
93
|
*
|
|
@@ -110,57 +96,6 @@ export declare class IframeTextmodeRuntime {
|
|
|
110
96
|
runCode(code: string, options?: {
|
|
111
97
|
softReset?: boolean;
|
|
112
98
|
}): Promise<boolean>;
|
|
113
|
-
/**
|
|
114
|
-
* Exports the current runner output in any supported format.
|
|
115
|
-
*
|
|
116
|
-
* @category Exports
|
|
117
|
-
*/
|
|
118
|
-
export(format: ExportMessage['format'], options?: ImageExportOptions | SvgExportOptions | TxtExportOptions | GifExportOptions | WebmExportOptions, timeoutMs?: number): Promise<ExportResultMessage>;
|
|
119
|
-
/**
|
|
120
|
-
* Exports the current runner output as a raster image.
|
|
121
|
-
*
|
|
122
|
-
* @category Exports
|
|
123
|
-
*/
|
|
124
|
-
exportImage(options: ImageExportOptions): Promise<ExportResultMessage>;
|
|
125
|
-
/**
|
|
126
|
-
* Exports the current runner output as SVG.
|
|
127
|
-
*
|
|
128
|
-
* @category Exports
|
|
129
|
-
*/
|
|
130
|
-
exportSvg(options: SvgExportOptions): Promise<ExportResultMessage>;
|
|
131
|
-
/**
|
|
132
|
-
* Exports the current runner output as plain text.
|
|
133
|
-
*
|
|
134
|
-
* @category Exports
|
|
135
|
-
*/
|
|
136
|
-
exportTxt(options: TxtExportOptions): Promise<ExportResultMessage>;
|
|
137
|
-
/**
|
|
138
|
-
* Records an animated GIF export.
|
|
139
|
-
*
|
|
140
|
-
* @category Exports
|
|
141
|
-
*/
|
|
142
|
-
exportGif(options: GifExportOptions): Promise<ExportResultMessage>;
|
|
143
|
-
/**
|
|
144
|
-
* Records a WebM export.
|
|
145
|
-
*
|
|
146
|
-
* @category Exports
|
|
147
|
-
*/
|
|
148
|
-
exportWebm(options: WebmExportOptions): Promise<ExportResultMessage>;
|
|
149
|
-
/**
|
|
150
|
-
* Loads a font file into the runner.
|
|
151
|
-
*
|
|
152
|
-
* @category Fonts
|
|
153
|
-
*/
|
|
154
|
-
loadFont(file: File): Promise<FontLoadResult>;
|
|
155
|
-
/**
|
|
156
|
-
* Sends a playback command and resolves with the resulting playback state.
|
|
157
|
-
*
|
|
158
|
-
* @category Playback
|
|
159
|
-
*/
|
|
160
|
-
playback(action: PlaybackAction, options?: {
|
|
161
|
-
frame?: number;
|
|
162
|
-
maxFrames?: number;
|
|
163
|
-
}): Promise<PlaybackState>;
|
|
164
99
|
private connectPort;
|
|
165
100
|
private handlePortMessage;
|
|
166
101
|
private handleReady;
|
|
@@ -169,6 +104,7 @@ export declare class IframeTextmodeRuntime {
|
|
|
169
104
|
private request;
|
|
170
105
|
private postMessage;
|
|
171
106
|
private handleUnavailable;
|
|
107
|
+
private forceDisposeFrameForReconnect;
|
|
172
108
|
private validateReadyMessage;
|
|
173
109
|
private assertSandboxOriginPolicy;
|
|
174
110
|
private setStatus;
|
|
@@ -6,15 +6,7 @@ import { createRunnerIframe, focusElement, mountRunnerIframe } from './internal/
|
|
|
6
6
|
import { routeRunnerMessage } from './internal/messageRouter';
|
|
7
7
|
import { RequestRegistry, requestKindForMessage } from './internal/requestRegistry';
|
|
8
8
|
import { assertSandboxOriginPolicy } from './internal/sandboxPolicy';
|
|
9
|
-
|
|
10
|
-
if (error instanceof Error && error.message) {
|
|
11
|
-
return error.message;
|
|
12
|
-
}
|
|
13
|
-
if (typeof error === 'string' && error.trim()) {
|
|
14
|
-
return error;
|
|
15
|
-
}
|
|
16
|
-
return fallback;
|
|
17
|
-
};
|
|
9
|
+
import { createDocumentVisibilityApi } from './internal/visibility';
|
|
18
10
|
/**
|
|
19
11
|
* Browser iframe runtime for communicating with the hosted textmode runner.
|
|
20
12
|
*
|
|
@@ -28,13 +20,13 @@ export class IframeTextmodeRuntime {
|
|
|
28
20
|
requestTimeoutMs;
|
|
29
21
|
options;
|
|
30
22
|
mountMode;
|
|
31
|
-
pending
|
|
23
|
+
pending;
|
|
32
24
|
heartbeat;
|
|
25
|
+
visibility;
|
|
33
26
|
iframe = null;
|
|
34
27
|
channel = null;
|
|
35
28
|
port = null;
|
|
36
29
|
container = null;
|
|
37
|
-
settings = null;
|
|
38
30
|
capabilities = null;
|
|
39
31
|
ready = false;
|
|
40
32
|
currentStatus = 'idle';
|
|
@@ -51,9 +43,12 @@ export class IframeTextmodeRuntime {
|
|
|
51
43
|
this.sandboxTokens = [...(options.sandboxTokens ?? DEFAULT_IFRAME_SANDBOX_TOKENS)];
|
|
52
44
|
this.handshakeTimeoutMs = options.handshakeTimeoutMs ?? 5000;
|
|
53
45
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 12000;
|
|
46
|
+
this.visibility = createDocumentVisibilityApi();
|
|
47
|
+
this.pending = new RequestRegistry(undefined, this.visibility);
|
|
54
48
|
this.heartbeat = new HeartbeatController({
|
|
55
49
|
intervalMs: options.heartbeatIntervalMs ?? 2000,
|
|
56
50
|
timeoutMs: options.heartbeatTimeoutMs ?? 10000,
|
|
51
|
+
visibilityApi: this.visibility,
|
|
57
52
|
onPing: () => {
|
|
58
53
|
this.postMessage({
|
|
59
54
|
type: 'PING',
|
|
@@ -109,18 +104,13 @@ export class IframeTextmodeRuntime {
|
|
|
109
104
|
* Mounts the runner iframe and performs the current protocol handshake.
|
|
110
105
|
*
|
|
111
106
|
* @param container - DOM element that should contain the runner iframe.
|
|
112
|
-
* @param settings - Optional fixed runtime settings to configure after ready.
|
|
113
107
|
* @returns `true` when the runner is ready.
|
|
114
108
|
* @category Runtime
|
|
115
109
|
*/
|
|
116
|
-
async init(container
|
|
110
|
+
async init(container) {
|
|
117
111
|
this.container = container;
|
|
118
|
-
this.settings = settings ? { ...settings } : null;
|
|
119
112
|
this.assertSandboxOriginPolicy();
|
|
120
113
|
if (this.isReady && this.iframe?.isConnected) {
|
|
121
|
-
if (settings) {
|
|
122
|
-
await this.configure(settings);
|
|
123
|
-
}
|
|
124
114
|
return true;
|
|
125
115
|
}
|
|
126
116
|
this.disposeFrame();
|
|
@@ -177,7 +167,8 @@ export class IframeTextmodeRuntime {
|
|
|
177
167
|
}
|
|
178
168
|
const code = this.lastRequestedCode;
|
|
179
169
|
this.setStatus('recovering');
|
|
180
|
-
|
|
170
|
+
this.forceDisposeFrameForReconnect();
|
|
171
|
+
const initialized = await this.init(this.container);
|
|
181
172
|
if (initialized && code) {
|
|
182
173
|
void this.runCode(code);
|
|
183
174
|
}
|
|
@@ -202,42 +193,6 @@ export class IframeTextmodeRuntime {
|
|
|
202
193
|
// Element focus still helps browsers that gate iframe animation cadence.
|
|
203
194
|
}
|
|
204
195
|
}
|
|
205
|
-
/**
|
|
206
|
-
* Configures complete fixed runtime settings.
|
|
207
|
-
*
|
|
208
|
-
* @category Runtime
|
|
209
|
-
*/
|
|
210
|
-
async configure(settings) {
|
|
211
|
-
this.settings = { ...settings };
|
|
212
|
-
const wasReady = this.ready;
|
|
213
|
-
this.setStatus('configuring');
|
|
214
|
-
return this.request({
|
|
215
|
-
type: 'CONFIGURE_RUNTIME',
|
|
216
|
-
requestId: this.createRequestId('settings'),
|
|
217
|
-
settings,
|
|
218
|
-
}).then((message) => {
|
|
219
|
-
if (wasReady) {
|
|
220
|
-
this.setStatus('ready');
|
|
221
|
-
}
|
|
222
|
-
return message.state;
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Applies a partial runtime settings update.
|
|
227
|
-
*
|
|
228
|
-
* @category Runtime
|
|
229
|
-
*/
|
|
230
|
-
async setSettings(settings) {
|
|
231
|
-
this.settings = {
|
|
232
|
-
...(this.settings ?? settings),
|
|
233
|
-
...settings,
|
|
234
|
-
};
|
|
235
|
-
return this.request({
|
|
236
|
-
type: 'SET_SETTINGS',
|
|
237
|
-
requestId: this.createRequestId('settings'),
|
|
238
|
-
settings,
|
|
239
|
-
}).then((message) => message.state);
|
|
240
|
-
}
|
|
241
196
|
/**
|
|
242
197
|
* Executes code in the runner.
|
|
243
198
|
*
|
|
@@ -252,95 +207,6 @@ export class IframeTextmodeRuntime {
|
|
|
252
207
|
await this.request(message);
|
|
253
208
|
return true;
|
|
254
209
|
}
|
|
255
|
-
/**
|
|
256
|
-
* Exports the current runner output in any supported format.
|
|
257
|
-
*
|
|
258
|
-
* @category Exports
|
|
259
|
-
*/
|
|
260
|
-
async export(format, options, timeoutMs) {
|
|
261
|
-
const message = {
|
|
262
|
-
type: 'EXPORT',
|
|
263
|
-
requestId: this.createRequestId('export'),
|
|
264
|
-
format,
|
|
265
|
-
options,
|
|
266
|
-
};
|
|
267
|
-
return this.request(message, timeoutMs);
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Exports the current runner output as a raster image.
|
|
271
|
-
*
|
|
272
|
-
* @category Exports
|
|
273
|
-
*/
|
|
274
|
-
async exportImage(options) {
|
|
275
|
-
return this.export('image', options);
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Exports the current runner output as SVG.
|
|
279
|
-
*
|
|
280
|
-
* @category Exports
|
|
281
|
-
*/
|
|
282
|
-
async exportSvg(options) {
|
|
283
|
-
return this.export('svg', options);
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Exports the current runner output as plain text.
|
|
287
|
-
*
|
|
288
|
-
* @category Exports
|
|
289
|
-
*/
|
|
290
|
-
async exportTxt(options) {
|
|
291
|
-
return this.export('txt', options);
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Records an animated GIF export.
|
|
295
|
-
*
|
|
296
|
-
* @category Exports
|
|
297
|
-
*/
|
|
298
|
-
async exportGif(options) {
|
|
299
|
-
return this.export('gif', options, Math.max(this.requestTimeoutMs, 120000));
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Records a WebM export.
|
|
303
|
-
*
|
|
304
|
-
* @category Exports
|
|
305
|
-
*/
|
|
306
|
-
async exportWebm(options) {
|
|
307
|
-
return this.export('webm', options, Math.max(this.requestTimeoutMs, 120000));
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Loads a font file into the runner.
|
|
311
|
-
*
|
|
312
|
-
* @category Fonts
|
|
313
|
-
*/
|
|
314
|
-
async loadFont(file) {
|
|
315
|
-
const requestId = this.createRequestId('font');
|
|
316
|
-
const buffer = await file.arrayBuffer();
|
|
317
|
-
const message = {
|
|
318
|
-
type: 'LOAD_FONT',
|
|
319
|
-
requestId,
|
|
320
|
-
fileName: file.name,
|
|
321
|
-
mimeType: file.type || undefined,
|
|
322
|
-
buffer,
|
|
323
|
-
};
|
|
324
|
-
return this.request(message, undefined, [buffer]).then((result) => ({
|
|
325
|
-
familyName: result.familyName,
|
|
326
|
-
characters: result.characters,
|
|
327
|
-
}));
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Sends a playback command and resolves with the resulting playback state.
|
|
331
|
-
*
|
|
332
|
-
* @category Playback
|
|
333
|
-
*/
|
|
334
|
-
async playback(action, options = {}) {
|
|
335
|
-
const message = {
|
|
336
|
-
type: 'PLAYBACK',
|
|
337
|
-
requestId: this.createRequestId('playback'),
|
|
338
|
-
action,
|
|
339
|
-
frame: options.frame,
|
|
340
|
-
maxFrames: options.maxFrames,
|
|
341
|
-
};
|
|
342
|
-
return this.request(message).then((result) => result.state);
|
|
343
|
-
}
|
|
344
210
|
connectPort() {
|
|
345
211
|
if (!this.iframe?.contentWindow) {
|
|
346
212
|
this.handleUnavailable('runner frame is unavailable');
|
|
@@ -361,8 +227,10 @@ export class IframeTextmodeRuntime {
|
|
|
361
227
|
routeRunnerMessage(message, {
|
|
362
228
|
onReady: (readyMessage) => this.handleReady(readyMessage),
|
|
363
229
|
onRunOk: (runOkMessage) => {
|
|
364
|
-
this.pending.resolve(runOkMessage.requestId, runOkMessage);
|
|
365
|
-
|
|
230
|
+
const resolved = this.pending.resolve(runOkMessage.requestId, runOkMessage);
|
|
231
|
+
if (!runOkMessage.requestId || resolved) {
|
|
232
|
+
this.options.onRunOk?.(runOkMessage);
|
|
233
|
+
}
|
|
366
234
|
},
|
|
367
235
|
onRunError: (runErrorMessage) => this.handleRunError(runErrorMessage),
|
|
368
236
|
onSynthError: (synthErrorMessage) => {
|
|
@@ -374,24 +242,6 @@ export class IframeTextmodeRuntime {
|
|
|
374
242
|
onUserInteraction: () => {
|
|
375
243
|
this.options.onUserInteraction?.();
|
|
376
244
|
},
|
|
377
|
-
onExportProgress: (exportProgressMessage) => {
|
|
378
|
-
this.options.onExportProgress?.(exportProgressMessage.requestId, exportProgressMessage.format, exportProgressMessage.progress);
|
|
379
|
-
},
|
|
380
|
-
onExportResult: (exportResultMessage) => {
|
|
381
|
-
this.pending.resolve(exportResultMessage.requestId, exportResultMessage);
|
|
382
|
-
},
|
|
383
|
-
onFontLoaded: (fontLoadedMessage) => {
|
|
384
|
-
this.pending.resolve(fontLoadedMessage.requestId, fontLoadedMessage);
|
|
385
|
-
},
|
|
386
|
-
onFontError: (fontErrorMessage) => {
|
|
387
|
-
this.pending.reject(fontErrorMessage.requestId, new Error(fontErrorMessage.message));
|
|
388
|
-
},
|
|
389
|
-
onPlaybackState: (playbackStateMessage) => {
|
|
390
|
-
this.options.onPlaybackState?.(playbackStateMessage.state);
|
|
391
|
-
if (playbackStateMessage.requestId) {
|
|
392
|
-
this.pending.resolve(playbackStateMessage.requestId, playbackStateMessage);
|
|
393
|
-
}
|
|
394
|
-
},
|
|
395
245
|
onPong: () => {
|
|
396
246
|
this.heartbeat.markPong();
|
|
397
247
|
},
|
|
@@ -409,23 +259,7 @@ export class IframeTextmodeRuntime {
|
|
|
409
259
|
this.readyTimeoutId = null;
|
|
410
260
|
}
|
|
411
261
|
this.options.onConnected?.();
|
|
412
|
-
|
|
413
|
-
if (settings) {
|
|
414
|
-
void this.configure(settings)
|
|
415
|
-
.then(() => {
|
|
416
|
-
this.markReady();
|
|
417
|
-
})
|
|
418
|
-
.catch((error) => {
|
|
419
|
-
this.ready = false;
|
|
420
|
-
this.setStatus('unavailable', getRuntimeErrorMessage(error, 'runner configuration failed'));
|
|
421
|
-
this.readyRejecter?.(error);
|
|
422
|
-
this.readyResolver = null;
|
|
423
|
-
this.readyRejecter = null;
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
this.markReady();
|
|
428
|
-
}
|
|
262
|
+
this.markReady();
|
|
429
263
|
}
|
|
430
264
|
markReady() {
|
|
431
265
|
this.ready = true;
|
|
@@ -449,37 +283,36 @@ export class IframeTextmodeRuntime {
|
|
|
449
283
|
column: message.column,
|
|
450
284
|
});
|
|
451
285
|
}
|
|
452
|
-
request(message, timeoutMs = this.requestTimeoutMs
|
|
453
|
-
if (!this.port ||
|
|
286
|
+
request(message, timeoutMs = this.requestTimeoutMs) {
|
|
287
|
+
if (!this.port || !this.ready) {
|
|
454
288
|
return Promise.reject(new Error('runner is not ready'));
|
|
455
289
|
}
|
|
456
290
|
const requestId = 'requestId' in message ? message.requestId : undefined;
|
|
457
291
|
if (!requestId) {
|
|
458
|
-
this.postMessage(message
|
|
292
|
+
this.postMessage(message);
|
|
459
293
|
return Promise.resolve(undefined);
|
|
460
294
|
}
|
|
295
|
+
const kind = requestKindForMessage(message.type);
|
|
461
296
|
const promise = this.pending.register({
|
|
462
297
|
requestId,
|
|
463
|
-
kind
|
|
298
|
+
kind,
|
|
464
299
|
messageType: message.type,
|
|
465
300
|
timeoutMs,
|
|
466
301
|
onTimeout: (error) => {
|
|
302
|
+
if (kind === 'run') {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
467
305
|
this.handleUnavailable(error.message);
|
|
468
306
|
},
|
|
469
307
|
});
|
|
470
|
-
this.postMessage(message
|
|
308
|
+
this.postMessage(message);
|
|
471
309
|
return promise;
|
|
472
310
|
}
|
|
473
|
-
postMessage(message
|
|
311
|
+
postMessage(message) {
|
|
474
312
|
if (!this.port) {
|
|
475
313
|
throw new Error('runner port is not connected');
|
|
476
314
|
}
|
|
477
|
-
|
|
478
|
-
this.port.postMessage(message, transfer);
|
|
479
|
-
}
|
|
480
|
-
else {
|
|
481
|
-
this.port.postMessage(message);
|
|
482
|
-
}
|
|
315
|
+
this.port.postMessage(message);
|
|
483
316
|
}
|
|
484
317
|
handleUnavailable(reason) {
|
|
485
318
|
const error = new Error(reason);
|
|
@@ -490,17 +323,31 @@ export class IframeTextmodeRuntime {
|
|
|
490
323
|
this.readyResolver = null;
|
|
491
324
|
this.readyRejecter = null;
|
|
492
325
|
this.disposeFrame();
|
|
493
|
-
const status = reason
|
|
326
|
+
const status = reason === 'runner heartbeat timed out' ? 'hung' : 'unavailable';
|
|
494
327
|
this.setStatus(status, reason);
|
|
495
328
|
this.options.onUnavailable?.(reason, status);
|
|
496
329
|
}
|
|
330
|
+
forceDisposeFrameForReconnect() {
|
|
331
|
+
if (this.port) {
|
|
332
|
+
try {
|
|
333
|
+
this.postMessage({ type: 'DISPOSE' });
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// The existing connection may already be unavailable.
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
this.heartbeat.stop();
|
|
340
|
+
this.pending.rejectAll(new Error('runner reconnecting'));
|
|
341
|
+
this.ready = false;
|
|
342
|
+
this.disposeFrame();
|
|
343
|
+
}
|
|
497
344
|
validateReadyMessage(message) {
|
|
498
345
|
const capabilities = message.capabilities;
|
|
499
346
|
if (!isRunnerCapabilities(capabilities)) {
|
|
500
347
|
return 'runner did not advertise a valid current capability set';
|
|
501
348
|
}
|
|
502
|
-
if (!capabilities.
|
|
503
|
-
return 'runner does not support
|
|
349
|
+
if (!capabilities.heartbeat) {
|
|
350
|
+
return 'runner does not support heartbeat monitoring';
|
|
504
351
|
}
|
|
505
352
|
return null;
|
|
506
353
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,14 +4,12 @@
|
|
|
4
4
|
* Browser iframe runtime client for the hosted textmode runner.
|
|
5
5
|
*
|
|
6
6
|
* `@textmode/runner-client` manages the runner iframe lifecycle, current
|
|
7
|
-
* protocol handshake, request/response routing, heartbeat monitoring,
|
|
8
|
-
*
|
|
9
|
-
* apps.
|
|
7
|
+
* protocol handshake, request/response routing, heartbeat monitoring, reconnect,
|
|
8
|
+
* and disposal for host apps.
|
|
10
9
|
*
|
|
11
10
|
* @module @textmode/runner-client
|
|
12
11
|
*/
|
|
13
12
|
export { IframeTextmodeRuntime } from './IframeTextmodeRuntime';
|
|
14
13
|
export { RunnerRequestError, type RunnerExecutionError } from './errors';
|
|
15
|
-
export { type FontLoadResult } from './font';
|
|
16
14
|
export { DEFAULT_IFRAME_SANDBOX_TOKENS, type IframeMountMode, type IframeSandboxToken, type IframeTextmodeRuntimeOptions, } from './options';
|
|
17
15
|
export { type RunnerRuntimeStatus } from './status';
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* Browser iframe runtime client for the hosted textmode runner.
|
|
5
5
|
*
|
|
6
6
|
* `@textmode/runner-client` manages the runner iframe lifecycle, current
|
|
7
|
-
* protocol handshake, request/response routing, heartbeat monitoring,
|
|
8
|
-
*
|
|
9
|
-
* apps.
|
|
7
|
+
* protocol handshake, request/response routing, heartbeat monitoring, reconnect,
|
|
8
|
+
* and disposal for host apps.
|
|
10
9
|
*
|
|
11
10
|
* @module @textmode/runner-client
|
|
12
11
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PageVisibilityApi } from './visibility';
|
|
1
2
|
export interface HeartbeatTimerApi {
|
|
2
3
|
setInterval: (handler: () => void, intervalMs: number) => number;
|
|
3
4
|
clearInterval: (intervalId: number) => void;
|
|
@@ -7,6 +8,7 @@ export interface HeartbeatControllerOptions {
|
|
|
7
8
|
timeoutMs: number;
|
|
8
9
|
now?: () => number;
|
|
9
10
|
timerApi?: HeartbeatTimerApi;
|
|
11
|
+
visibilityApi?: PageVisibilityApi;
|
|
10
12
|
onPing: () => void;
|
|
11
13
|
onTimeout: () => void;
|
|
12
14
|
}
|
|
@@ -15,12 +17,18 @@ export declare class HeartbeatController {
|
|
|
15
17
|
private readonly timeoutMs;
|
|
16
18
|
private readonly now;
|
|
17
19
|
private readonly timerApi;
|
|
20
|
+
private readonly visibilityApi;
|
|
18
21
|
private readonly onPing;
|
|
19
22
|
private readonly onTimeout;
|
|
20
23
|
private intervalId;
|
|
24
|
+
private cleanupVisibility;
|
|
25
|
+
private isActive;
|
|
21
26
|
private lastPongAt;
|
|
22
27
|
constructor(options: HeartbeatControllerOptions);
|
|
23
28
|
start(): void;
|
|
24
29
|
stop(): void;
|
|
25
30
|
markPong(): void;
|
|
31
|
+
private readonly handleVisibilityChange;
|
|
32
|
+
private startInterval;
|
|
33
|
+
private stopInterval;
|
|
26
34
|
}
|
|
@@ -1,24 +1,61 @@
|
|
|
1
|
+
import { createDocumentVisibilityApi, isPageVisible } from './visibility';
|
|
1
2
|
export class HeartbeatController {
|
|
2
3
|
intervalMs;
|
|
3
4
|
timeoutMs;
|
|
4
5
|
now;
|
|
5
6
|
timerApi;
|
|
7
|
+
visibilityApi;
|
|
6
8
|
onPing;
|
|
7
9
|
onTimeout;
|
|
8
10
|
intervalId = null;
|
|
11
|
+
cleanupVisibility = null;
|
|
12
|
+
isActive = false;
|
|
9
13
|
lastPongAt = 0;
|
|
10
14
|
constructor(options) {
|
|
11
15
|
this.intervalMs = options.intervalMs;
|
|
12
16
|
this.timeoutMs = options.timeoutMs;
|
|
13
17
|
this.now = options.now ?? Date.now;
|
|
14
18
|
this.timerApi = options.timerApi ?? createWindowTimerApi();
|
|
19
|
+
this.visibilityApi = options.visibilityApi ?? createDocumentVisibilityApi();
|
|
15
20
|
this.onPing = options.onPing;
|
|
16
21
|
this.onTimeout = options.onTimeout;
|
|
17
22
|
}
|
|
18
23
|
start() {
|
|
19
24
|
this.stop();
|
|
25
|
+
this.isActive = true;
|
|
20
26
|
this.lastPongAt = this.now();
|
|
27
|
+
this.cleanupVisibility = this.visibilityApi.addChangeListener(this.handleVisibilityChange);
|
|
28
|
+
if (isPageVisible(this.visibilityApi)) {
|
|
29
|
+
this.startInterval();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
stop() {
|
|
33
|
+
this.isActive = false;
|
|
34
|
+
this.stopInterval();
|
|
35
|
+
this.cleanupVisibility?.();
|
|
36
|
+
this.cleanupVisibility = null;
|
|
37
|
+
}
|
|
38
|
+
markPong() {
|
|
39
|
+
this.lastPongAt = this.now();
|
|
40
|
+
}
|
|
41
|
+
handleVisibilityChange = () => {
|
|
42
|
+
if (!this.isActive)
|
|
43
|
+
return;
|
|
44
|
+
if (!isPageVisible(this.visibilityApi)) {
|
|
45
|
+
this.stopInterval();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.lastPongAt = this.now();
|
|
49
|
+
this.startInterval();
|
|
50
|
+
};
|
|
51
|
+
startInterval() {
|
|
52
|
+
if (this.intervalId !== null)
|
|
53
|
+
return;
|
|
21
54
|
this.intervalId = this.timerApi.setInterval(() => {
|
|
55
|
+
if (!isPageVisible(this.visibilityApi)) {
|
|
56
|
+
this.stopInterval();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
22
59
|
if (this.now() - this.lastPongAt > this.timeoutMs) {
|
|
23
60
|
this.onTimeout();
|
|
24
61
|
return;
|
|
@@ -26,15 +63,12 @@ export class HeartbeatController {
|
|
|
26
63
|
this.onPing();
|
|
27
64
|
}, this.intervalMs);
|
|
28
65
|
}
|
|
29
|
-
|
|
66
|
+
stopInterval() {
|
|
30
67
|
if (this.intervalId !== null) {
|
|
31
68
|
this.timerApi.clearInterval(this.intervalId);
|
|
32
69
|
this.intervalId = null;
|
|
33
70
|
}
|
|
34
71
|
}
|
|
35
|
-
markPong() {
|
|
36
|
-
this.lastPongAt = this.now();
|
|
37
|
-
}
|
|
38
72
|
}
|
|
39
73
|
function createWindowTimerApi() {
|
|
40
74
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type PongMessage, type ReadyMessage, type RunErrorMessage, type RunOkMessage, type SynthErrorMessage } from '@textmode/runner-protocol';
|
|
2
2
|
export interface RunnerMessageHandlers {
|
|
3
3
|
onReady: (message: ReadyMessage) => void;
|
|
4
4
|
onRunOk: (message: RunOkMessage) => void;
|
|
@@ -6,11 +6,6 @@ export interface RunnerMessageHandlers {
|
|
|
6
6
|
onSynthError: (message: SynthErrorMessage) => void;
|
|
7
7
|
onToggleUI: () => void;
|
|
8
8
|
onUserInteraction: () => void;
|
|
9
|
-
onExportProgress: (message: ExportProgressMessage) => void;
|
|
10
|
-
onExportResult: (message: ExportResultMessage) => void;
|
|
11
|
-
onFontLoaded: (message: FontLoadedMessage) => void;
|
|
12
|
-
onFontError: (message: FontErrorMessage) => void;
|
|
13
|
-
onPlaybackState: (message: PlaybackStateMessage) => void;
|
|
14
9
|
onPong: (message: PongMessage) => void;
|
|
15
10
|
}
|
|
16
11
|
export declare function routeRunnerMessage(message: unknown, handlers: RunnerMessageHandlers): boolean;
|
|
@@ -22,21 +22,6 @@ export function routeRunnerMessage(message, handlers) {
|
|
|
22
22
|
case 'USER_INTERACTION':
|
|
23
23
|
handlers.onUserInteraction();
|
|
24
24
|
break;
|
|
25
|
-
case 'EXPORT_PROGRESS':
|
|
26
|
-
handlers.onExportProgress(message);
|
|
27
|
-
break;
|
|
28
|
-
case 'EXPORT_RESULT':
|
|
29
|
-
handlers.onExportResult(message);
|
|
30
|
-
break;
|
|
31
|
-
case 'FONT_LOADED':
|
|
32
|
-
handlers.onFontLoaded(message);
|
|
33
|
-
break;
|
|
34
|
-
case 'FONT_ERROR':
|
|
35
|
-
handlers.onFontError(message);
|
|
36
|
-
break;
|
|
37
|
-
case 'PLAYBACK_STATE':
|
|
38
|
-
handlers.onPlaybackState(message);
|
|
39
|
-
break;
|
|
40
25
|
case 'PONG':
|
|
41
26
|
handlers.onPong(message);
|
|
42
27
|
break;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ParentToRunnerMessage } from '@textmode/runner-protocol';
|
|
2
|
-
|
|
2
|
+
import { type PageVisibilityApi } from './visibility';
|
|
3
|
+
export type RequestKind = 'run' | 'lifecycle';
|
|
3
4
|
export interface RequestTimerApi {
|
|
4
5
|
setTimeout: (handler: () => void, timeoutMs: number) => number;
|
|
5
6
|
clearTimeout: (timeoutId: number) => void;
|
|
7
|
+
now: () => number;
|
|
6
8
|
}
|
|
7
9
|
interface RegisterRequestOptions {
|
|
8
10
|
requestId: string;
|
|
@@ -14,7 +16,8 @@ interface RegisterRequestOptions {
|
|
|
14
16
|
export declare class RequestRegistry {
|
|
15
17
|
private readonly pending;
|
|
16
18
|
private readonly timerApi;
|
|
17
|
-
|
|
19
|
+
private readonly visibilityApi;
|
|
20
|
+
constructor(timerApi?: RequestTimerApi, visibilityApi?: PageVisibilityApi);
|
|
18
21
|
get size(): number;
|
|
19
22
|
register<T>(options: RegisterRequestOptions): Promise<T>;
|
|
20
23
|
resolve(requestId: string | undefined, value: unknown): boolean;
|
|
@@ -1,25 +1,75 @@
|
|
|
1
|
+
import { createDocumentVisibilityApi, isPageVisible } from './visibility';
|
|
1
2
|
export class RequestRegistry {
|
|
2
3
|
pending = new Map();
|
|
3
4
|
timerApi;
|
|
4
|
-
|
|
5
|
+
visibilityApi;
|
|
6
|
+
constructor(timerApi = createWindowTimerApi(), visibilityApi = createDocumentVisibilityApi()) {
|
|
5
7
|
this.timerApi = timerApi;
|
|
8
|
+
this.visibilityApi = visibilityApi;
|
|
6
9
|
}
|
|
7
10
|
get size() {
|
|
8
11
|
return this.pending.size;
|
|
9
12
|
}
|
|
10
13
|
register(options) {
|
|
11
14
|
return new Promise((resolve, reject) => {
|
|
12
|
-
|
|
15
|
+
let remainingMs = options.timeoutMs;
|
|
16
|
+
let timeoutId = null;
|
|
17
|
+
let startedAt = this.timerApi.now();
|
|
18
|
+
const timerApi = this.timerApi;
|
|
19
|
+
let cleanupVisibility = () => { };
|
|
20
|
+
function pauseTimer() {
|
|
21
|
+
if (timeoutId === null)
|
|
22
|
+
return;
|
|
23
|
+
const elapsedMs = Math.max(0, timerApi.now() - startedAt);
|
|
24
|
+
remainingMs = Math.max(0, remainingMs - elapsedMs);
|
|
25
|
+
timerApi.clearTimeout(timeoutId);
|
|
26
|
+
timeoutId = null;
|
|
27
|
+
}
|
|
28
|
+
const cleanup = () => {
|
|
29
|
+
if (timeoutId !== null) {
|
|
30
|
+
timerApi.clearTimeout(timeoutId);
|
|
31
|
+
timeoutId = null;
|
|
32
|
+
}
|
|
33
|
+
cleanupVisibility();
|
|
34
|
+
};
|
|
35
|
+
const rejectTimedOut = () => {
|
|
36
|
+
cleanup();
|
|
13
37
|
this.pending.delete(options.requestId);
|
|
14
38
|
const error = new Error(`runner request timed out: ${options.messageType}`);
|
|
15
39
|
reject(error);
|
|
16
40
|
options.onTimeout(error);
|
|
17
|
-
}
|
|
41
|
+
};
|
|
42
|
+
const scheduleTimer = () => {
|
|
43
|
+
startedAt = timerApi.now();
|
|
44
|
+
timeoutId = timerApi.setTimeout(() => {
|
|
45
|
+
timeoutId = null;
|
|
46
|
+
if (!isPageVisible(this.visibilityApi)) {
|
|
47
|
+
remainingMs = 0;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
rejectTimedOut();
|
|
51
|
+
}, Math.max(0, remainingMs));
|
|
52
|
+
};
|
|
53
|
+
function resumeTimer() {
|
|
54
|
+
if (timeoutId !== null)
|
|
55
|
+
return;
|
|
56
|
+
scheduleTimer();
|
|
57
|
+
}
|
|
58
|
+
cleanupVisibility = this.visibilityApi.addChangeListener(() => {
|
|
59
|
+
if (isPageVisible(this.visibilityApi)) {
|
|
60
|
+
resumeTimer();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
pauseTimer();
|
|
64
|
+
});
|
|
65
|
+
if (isPageVisible(this.visibilityApi)) {
|
|
66
|
+
scheduleTimer();
|
|
67
|
+
}
|
|
18
68
|
this.pending.set(options.requestId, {
|
|
19
69
|
kind: options.kind,
|
|
20
70
|
resolve: resolve,
|
|
21
71
|
reject,
|
|
22
|
-
|
|
72
|
+
cleanup,
|
|
23
73
|
});
|
|
24
74
|
});
|
|
25
75
|
}
|
|
@@ -29,7 +79,7 @@ export class RequestRegistry {
|
|
|
29
79
|
const pending = this.pending.get(requestId);
|
|
30
80
|
if (!pending)
|
|
31
81
|
return false;
|
|
32
|
-
|
|
82
|
+
pending.cleanup();
|
|
33
83
|
this.pending.delete(requestId);
|
|
34
84
|
pending.resolve(value);
|
|
35
85
|
return true;
|
|
@@ -38,14 +88,14 @@ export class RequestRegistry {
|
|
|
38
88
|
const pending = this.pending.get(requestId);
|
|
39
89
|
if (!pending)
|
|
40
90
|
return false;
|
|
41
|
-
|
|
91
|
+
pending.cleanup();
|
|
42
92
|
this.pending.delete(requestId);
|
|
43
93
|
pending.reject(error);
|
|
44
94
|
return true;
|
|
45
95
|
}
|
|
46
96
|
rejectAll(error) {
|
|
47
97
|
for (const [requestId, pending] of this.pending) {
|
|
48
|
-
|
|
98
|
+
pending.cleanup();
|
|
49
99
|
pending.reject(error);
|
|
50
100
|
this.pending.delete(requestId);
|
|
51
101
|
}
|
|
@@ -56,22 +106,15 @@ export function requestKindForMessage(type) {
|
|
|
56
106
|
case 'RUN_CODE':
|
|
57
107
|
case 'SOFT_RESET':
|
|
58
108
|
return 'run';
|
|
59
|
-
case 'EXPORT':
|
|
60
|
-
return 'export';
|
|
61
|
-
case 'LOAD_FONT':
|
|
62
|
-
return 'font';
|
|
63
|
-
case 'PLAYBACK':
|
|
64
|
-
return 'playback';
|
|
65
|
-
case 'CONFIGURE_RUNTIME':
|
|
66
|
-
case 'SET_SETTINGS':
|
|
67
109
|
case 'PING':
|
|
68
110
|
case 'DISPOSE':
|
|
69
|
-
return '
|
|
111
|
+
return 'lifecycle';
|
|
70
112
|
}
|
|
71
113
|
}
|
|
72
114
|
function createWindowTimerApi() {
|
|
73
115
|
return {
|
|
74
116
|
setTimeout: (handler, timeoutMs) => window.setTimeout(handler, timeoutMs),
|
|
75
117
|
clearTimeout: (timeoutId) => window.clearTimeout(timeoutId),
|
|
118
|
+
now: () => Date.now(),
|
|
76
119
|
};
|
|
77
120
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface PageVisibilityApi {
|
|
2
|
+
getState: () => string;
|
|
3
|
+
addChangeListener: (listener: () => void) => () => void;
|
|
4
|
+
}
|
|
5
|
+
export declare function isPageVisible(visibility: PageVisibilityApi): boolean;
|
|
6
|
+
export declare function createDocumentVisibilityApi(): PageVisibilityApi;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function isPageVisible(visibility) {
|
|
2
|
+
return visibility.getState() === 'visible';
|
|
3
|
+
}
|
|
4
|
+
export function createDocumentVisibilityApi() {
|
|
5
|
+
const listeners = new Set();
|
|
6
|
+
let forcedHidden = false;
|
|
7
|
+
const notify = () => {
|
|
8
|
+
for (const listener of listeners) {
|
|
9
|
+
listener();
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const getState = () => {
|
|
13
|
+
if (forcedHidden)
|
|
14
|
+
return 'hidden';
|
|
15
|
+
const documentState = getDocument()?.visibilityState;
|
|
16
|
+
return typeof documentState === 'string' ? documentState : 'visible';
|
|
17
|
+
};
|
|
18
|
+
const addChangeListener = (listener) => {
|
|
19
|
+
listeners.add(listener);
|
|
20
|
+
if (listeners.size === 1) {
|
|
21
|
+
attachGlobalListeners();
|
|
22
|
+
}
|
|
23
|
+
return () => {
|
|
24
|
+
listeners.delete(listener);
|
|
25
|
+
if (listeners.size === 0) {
|
|
26
|
+
detachGlobalListeners();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
const handleVisibilityChange = () => {
|
|
31
|
+
notify();
|
|
32
|
+
};
|
|
33
|
+
const handlePageHide = () => {
|
|
34
|
+
forcedHidden = true;
|
|
35
|
+
notify();
|
|
36
|
+
};
|
|
37
|
+
const handlePageShow = () => {
|
|
38
|
+
forcedHidden = false;
|
|
39
|
+
notify();
|
|
40
|
+
};
|
|
41
|
+
const attachGlobalListeners = () => {
|
|
42
|
+
getDocument()?.addEventListener?.('visibilitychange', handleVisibilityChange);
|
|
43
|
+
getDocument()?.addEventListener?.('freeze', handlePageHide);
|
|
44
|
+
getDocument()?.addEventListener?.('resume', handlePageShow);
|
|
45
|
+
getWindow()?.addEventListener?.('pagehide', handlePageHide);
|
|
46
|
+
getWindow()?.addEventListener?.('pageshow', handlePageShow);
|
|
47
|
+
};
|
|
48
|
+
const detachGlobalListeners = () => {
|
|
49
|
+
getDocument()?.removeEventListener?.('visibilitychange', handleVisibilityChange);
|
|
50
|
+
getDocument()?.removeEventListener?.('freeze', handlePageHide);
|
|
51
|
+
getDocument()?.removeEventListener?.('resume', handlePageShow);
|
|
52
|
+
getWindow()?.removeEventListener?.('pagehide', handlePageHide);
|
|
53
|
+
getWindow()?.removeEventListener?.('pageshow', handlePageShow);
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
getState,
|
|
57
|
+
addChangeListener,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function getDocument() {
|
|
61
|
+
return typeof document === 'undefined' ? null : document;
|
|
62
|
+
}
|
|
63
|
+
function getWindow() {
|
|
64
|
+
return typeof window === 'undefined' ? null : window;
|
|
65
|
+
}
|
package/dist/options.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RunnerCapabilities, RunOkMessage } from '@textmode/runner-protocol';
|
|
2
2
|
import type { RunnerExecutionError } from './errors';
|
|
3
3
|
import type { RunnerRuntimeStatus } from './status';
|
|
4
4
|
/**
|
|
@@ -42,7 +42,7 @@ export interface IframeTextmodeRuntimeOptions {
|
|
|
42
42
|
heartbeatIntervalMs?: number;
|
|
43
43
|
/** Maximum time without a heartbeat pong before the runner is marked hung. */
|
|
44
44
|
heartbeatTimeoutMs?: number;
|
|
45
|
-
/** Called after the runner is ready
|
|
45
|
+
/** Called after the runner is ready. */
|
|
46
46
|
onReady?: (capabilities: RunnerCapabilities) => void;
|
|
47
47
|
/** Called when code execution succeeds. */
|
|
48
48
|
onRunOk?: (message: RunOkMessage) => void;
|
|
@@ -54,10 +54,6 @@ export interface IframeTextmodeRuntimeOptions {
|
|
|
54
54
|
onToggleUI?: () => void;
|
|
55
55
|
/** Called when the runner reports user interaction. */
|
|
56
56
|
onUserInteraction?: () => void;
|
|
57
|
-
/** Called as multi-frame exports report progress. */
|
|
58
|
-
onExportProgress?: (requestId: string, format: 'gif' | 'webm', progress: ExportProgress) => void;
|
|
59
|
-
/** Called whenever the runner reports playback state. */
|
|
60
|
-
onPlaybackState?: (state: PlaybackState) => void;
|
|
61
57
|
/** Called when the runner becomes unavailable or hung. */
|
|
62
58
|
onUnavailable?: (reason: string, status: RunnerRuntimeStatus) => void;
|
|
63
59
|
/** Called after the MessagePort connection is established. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@textmode/runner-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Browser iframe runtime client for the hosted textmode runner.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
"prepare": "npm run build"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@textmode/runner-protocol": "^0.
|
|
33
|
+
"@textmode/runner-protocol": "^0.2.0"
|
|
34
34
|
}
|
|
35
35
|
}
|
package/dist/font.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Metadata returned after the runner successfully loads a font file.
|
|
3
|
-
*
|
|
4
|
-
* @category Fonts
|
|
5
|
-
*/
|
|
6
|
-
export interface FontLoadResult {
|
|
7
|
-
/** Font family name detected by the runner. */
|
|
8
|
-
familyName: string | null;
|
|
9
|
-
/** Characters available in the loaded font. */
|
|
10
|
-
characters: string[];
|
|
11
|
-
}
|
package/dist/font.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|