Skip to content

Commit 71d61e2

Browse files
authored
Rely on sending and receiving messages instead of direct DOM access (#195)
* Send error as message when inside worker context * Add/use src-runtime/crossContextPostMessage.js for context independent message posting (works from inside <iframe> and `Worker` at the same time) * crossContextPostMessage: fix lint * src-runtime/inspectType.js: Update UI through messages * Manage addition/deletion of breakpoints between UI and Worker/IFrame * REPL: Install worker-with-import-map for testing RTI inside a `Worker` * Refactor `warnedTable` into `TypePanel` * Move event handling code into src-runtime/TypePanel.js * Warning: get correct source when event came from Worker * Sending enabledness state to worker whenever it changed with a cache, since we don't know when it finally started Remove enabledness check from `validateType`, add it directly to `inspectType` (which is called first) * Fix types and handle Warning's without events (restored from URL state) * Simplify some code and add message handling into TypePanel TypePanel: add show/hide method * Add repl/divide.worker.js (test file for `Worker`'s) * Fix `validateDivision` to work inside `Worker`'s * Use `options.enabled` instead of top level boolean `enabled` and `isEnabled()` * Add `repl/test-add.worker.js` and `repl/test-divide.worker.js` for easy worker testing
1 parent 740bf5f commit 71d61e2

14 files changed

+370
-65
lines changed

repl/importmap.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const imports = {
2929
"@runtime-type-inspector/transpiler": '../src-transpiler/index.js',
3030
"@babel/parser" : "./babel-parser.js",
3131
"display-anything" : "./node_modules/display-anything/src/index.js",
32+
"worker-with-import-map" : "./node_modules/worker-with-import-map/src/index.js",
3233
"test-import-validation-b" : "../test/typechecking/import-validation/b.js",
3334
//"@babel/helper-plugin-utils" : "./babel-helper-plugin-utils.js",
3435
//"@babel/plugin-syntax-typescript" : "./babel-plugin-syntax-typescript.js",

repl/package-lock.json

Lines changed: 40 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

repl/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"ace-builds": "^1.35.2",
1414
"display-anything": "^1.1.0",
1515
"express": "^4.18.2",
16-
"react-es6": "^1.0.0"
16+
"react-es6": "^1.0.0",
17+
"worker-with-import-map": "^1.0.4"
1718
}
1819
}

repl/test-add.worker.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {inspectType, youCanAddABreakpointHere} from '@runtime-type-inspector/runtime';
2+
/**
3+
* @param {number} a
4+
* @param {number} b
5+
*/
6+
function add(a, b) {
7+
if (!inspectType(a, "number", 'add', 'a')) {
8+
youCanAddABreakpointHere();
9+
}
10+
if (!inspectType(b, "number", 'add', 'b')) {
11+
youCanAddABreakpointHere();
12+
}
13+
return a + b;
14+
}
15+
function f() {
16+
/** @type {number[]} */
17+
const arr = [10_20];
18+
const [a, b] = arr;
19+
const ret = add(a, b);
20+
self.postMessage(`ret ${ret}`);
21+
}
22+
f();
23+
setInterval(f, 2000); // Test spam mode

repl/test-divide.worker.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {inspectType, youCanAddABreakpointHere, validateDivision} from '@runtime-type-inspector/runtime';
2+
/**
3+
* @param {number} a
4+
* @param {number} b
5+
*/
6+
function testDivide(a, b) {
7+
if (!inspectType(a, "number", 'testDivide', 'a')) {
8+
youCanAddABreakpointHere();
9+
}
10+
if (!inspectType(b, "number", 'testDivide', 'b')) {
11+
youCanAddABreakpointHere();
12+
}
13+
return validateDivision(a, b, "testDivide");
14+
}
15+
function f() {
16+
/** @type {number[]} */
17+
const arr = [10_20];
18+
const [a, b] = arr;
19+
const ret = testDivide(a, b);
20+
self.postMessage(`ret ${ret}`);
21+
}
22+
f();
23+
setInterval(f, 2000); // Test spam mode
24+
25+
/*
26+
import {typePanel} from '@runtime-type-inspector/runtime';
27+
import {WorkerWithImportMapViaBedfordsShim} from 'worker-with-import-map';
28+
// console.log("WorkerWithImportMapViaBedfordsShim", WorkerWithImportMapViaBedfordsShim);
29+
// const url = './test-add.worker.js';
30+
const url = './test-divide.worker.js';
31+
const worker = new WorkerWithImportMapViaBedfordsShim(url, {
32+
importMap: 'inherit'
33+
});
34+
console.log("worker", worker);
35+
worker.addEventListener('message', (e) => {
36+
// console.log('addEventListener message', e.data);
37+
if (e.data.type !== 'rti') {
38+
return;
39+
}
40+
e.preventDefault();
41+
e.stopPropagation();
42+
// Forward message to window listener:
43+
// window.postMessage(e.data);
44+
// Forward event directly to typePanel:
45+
typePanel.handleEvent(e);
46+
});
47+
*/

src-runtime/TypePanel.js

Lines changed: 127 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import {assertMode } from "./assertMode.js";
2-
import {options } from "./options.js";
3-
import {disableTypeChecking} from "./validateType.js";
4-
import {enableTypeChecking } from "./validateType.js";
5-
import {warnedTable } from "./warnedTable.js";
6-
import {Warning } from "./Warning.js";
1+
import {assertMode } from "./assertMode.js";
2+
import {options } from "./options.js";
3+
import {createTable} from "./warnedTable.js";
4+
import {Warning } from "./Warning.js";
5+
/**
6+
* @typedef {MessageEvent<{action: string}>} MessageEventRTI
7+
*/
78
/**
89
* @param {HTMLDivElement} div - The <div>.
910
*/
@@ -58,10 +59,11 @@ class TypePanel {
5859
buttonLoadState = document.createElement('button');
5960
buttonSaveState = document.createElement('button');
6061
buttonClear = document.createElement('button');
62+
warnedTable = createTable();
6163
constructor() {
6264
const {
6365
div, inputEnable, spanErrors, span, select, option_spam, option_once, option_never,
64-
buttonHide, buttonLoadState, buttonSaveState, buttonClear
66+
buttonHide, buttonLoadState, buttonSaveState, buttonClear, warnedTable,
6567
} = this;
6668
div.style.position = "absolute";
6769
div.style.bottom = "0px";
@@ -72,9 +74,9 @@ class TypePanel {
7274
inputEnable.type = "checkbox";
7375
inputEnable.onchange = (e) => {
7476
if (inputEnable.checked) {
75-
enableTypeChecking();
77+
this.enableTypeChecking();
7678
} else {
77-
disableTypeChecking();
79+
this.disableTypeChecking();
7880
}
7981
};
8082
inputEnable.onchange();
@@ -98,7 +100,7 @@ class TypePanel {
98100
onchange(); // set mode in options
99101
buttonHide.textContent = 'Hide';
100102
buttonHide.onclick = () => {
101-
div.style.display = 'none';
103+
this.hide();
102104
};
103105
buttonLoadState.textContent = 'Load state';
104106
buttonLoadState.onclick = () => this.loadState();
@@ -118,6 +120,55 @@ class TypePanel {
118120
document.addEventListener("DOMContentLoaded", finalFunc);
119121
}
120122
this.loadState();
123+
// In the simplest case RTI sends its errors onto `window` to update UI state.
124+
// If you start a Worker, you have to attach RTI yourself.
125+
window.addEventListener('message', (e) => {
126+
const {data} = e;
127+
const {type, destination} = data;
128+
// console.log("TypePanel Message event", e);
129+
// console.log("TypePanel Message data", data);
130+
if (type !== 'rti') {
131+
return;
132+
}
133+
if (destination !== 'ui') {
134+
return;
135+
}
136+
this.handleEvent(e);
137+
});
138+
}
139+
hide() {
140+
this.div.style.display = 'none';
141+
}
142+
show() {
143+
this.div.style.display = '';
144+
}
145+
disableTypeChecking() {
146+
localStorage.setItem('rti-enabled', 'false');
147+
this.sendEnabledDisabledStateToWorker();
148+
}
149+
enableTypeChecking() {
150+
localStorage.setItem('rti-enabled', 'true');
151+
this.sendEnabledDisabledStateToWorker();
152+
}
153+
lastKnownCountWithStatus = '0-true';
154+
sendEnabledDisabledStateToWorker() {
155+
// Problem: First time the worker may not even have started and `this.eventSources.size === 0`
156+
// So we first know a RTI worker started after receiving the first message from it.
157+
const {eventSources} = this;
158+
const key = `${eventSources.size}-${this.inputEnable.checked}`;
159+
// Only update when either changed.
160+
if (key === this.lastKnownCountWithStatus) {
161+
return;
162+
}
163+
this.lastKnownCountWithStatus = key;
164+
// console.log("Update state to eventSources", eventSources, "key", key);
165+
this.eventSources.forEach(eventSource => {
166+
eventSource.postMessage({
167+
type: 'rti',
168+
action: this.inputEnable.checked ? 'enable' : 'disable',
169+
destination: 'worker',
170+
});
171+
});
121172
}
122173
clear() {
123174
const {warned} = options;
@@ -173,7 +224,7 @@ class TypePanel {
173224
// If we didn't find it, create it.
174225
if (!foundWarning) {
175226
foundWarning = new Warning('msg', 'value', 'expect', loc, name);
176-
warnedTable?.append(foundWarning.tr);
227+
this.warnedTable?.append(foundWarning.tr);
177228
options.warned[`${loc}-${name}`] = foundWarning;
178229
}
179230
foundWarning.state = state;
@@ -190,6 +241,70 @@ class TypePanel {
190241
updateErrorCount() {
191242
this.spanErrors.innerText = `Type validation errors: ${options.count}`;
192243
}
244+
get eventSources() {
245+
/** @type {Set<EventTarget | MessageEventSource>} */
246+
const eventSources = new Set();
247+
for (const key in options.warned) {
248+
const warning = options.warned[key];
249+
if (warning.eventSource) {
250+
eventSources.add(warning.eventSource);
251+
}
252+
}
253+
return eventSources;
254+
}
255+
/**
256+
* @param {MessageEventRTI} event - The event from Worker, IFrame or own window.
257+
*/
258+
addError(event) {
259+
const {value, expect, loc, name, valueToString, strings, extras = [], key} = event.data;
260+
const msg = `${loc}> The '${name}' argument has an invalid type. ${strings.join(' ')}`.trim();
261+
this.updateErrorCount();
262+
let warnObj = options.warned[key];
263+
if (!warnObj) {
264+
warnObj = new Warning(msg, value, expect, loc, name);
265+
this.warnedTable?.append(warnObj.tr);
266+
options.warned[key] = warnObj;
267+
}
268+
warnObj.event = event;
269+
warnObj.hits++;
270+
warnObj.warn(msg, {expect, value, valueToString}, ...extras);
271+
// The value may change and we only show the latest wrong value
272+
warnObj.value = value;
273+
// Message may change aswell, especially after loading state.
274+
warnObj.msg = msg;
275+
}
276+
/**
277+
* @param {MessageEventRTI} event - The event from Worker, IFrame or own window.
278+
*/
279+
deleteBreakpoint(event) {
280+
const {key} = event.data;
281+
const warnObj = options.warned[key];
282+
if (!warnObj) {
283+
console.warn("warnObj doesn't exist", {key});
284+
return;
285+
}
286+
warnObj.dbg = false;
287+
}
288+
/**
289+
* @param {MessageEventRTI} event - The event from Worker, IFrame or own window.
290+
*/
291+
addBreakpoint(event) {
292+
console.warn('TypePanel#addBreakpoint> Not adding breakpoints for UI via messages, event', event);
293+
}
294+
/**
295+
* @param {MessageEventRTI} event - The event from Worker, IFrame or own window.
296+
*/
297+
handleEvent(event) {
298+
const {action} = event.data;
299+
this[action](event);
300+
// Could be anywhere we know that a new worker is sending RTI messages.
301+
this.sendEnabledDisabledStateToWorker();
302+
}
303+
}
304+
/** @type {TypePanel | undefined} */
305+
let typePanel;
306+
// @todo create UI explicitly programmatically inside e.g. src/index.rti.js of the projects using it.
307+
if (typeof importScripts === 'undefined') {
308+
typePanel = new TypePanel();
193309
}
194-
const typePanel = new TypePanel();
195310
export {niceDiv, TypePanel, typePanel};

0 commit comments

Comments
 (0)