-
Notifications
You must be signed in to change notification settings - Fork 7
/
index.js
115 lines (105 loc) · 3.3 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
"use strict";
/// <reference types="./index.d.ts"/>
/** @typedef {{ moreDebugs: number }} PulseCall */
/** @typedef {{ isOpenBeat: boolean }} PulseAck */
(() => {
/** @type {DevtoolsDetectorConfig} */
const config = {
pollingIntervalSeconds: 0.25,
maxMillisBeforeAckWhenClosed: 100,
moreAnnoyingDebuggerStatements: 1,
onDetectOpen: undefined,
onDetectClose: undefined,
startup: "asap",
onCheckOpennessWhilePaused: "returnStaleValue",
};
Object.seal(config);
const heart = new Worker(URL.createObjectURL(new Blob([
// Note: putting everything before the first debugger on the same line as the
// opening callback brace prevents a user from placing their own debugger on
// a line before the first debugger and taking control in that way.
`"use strict";
onmessage = (ev) => { postMessage({isOpenBeat:true});
debugger; for (let i = 0; i < ev.data.moreDebugs; i++) { debugger; }
postMessage({isOpenBeat:false});
};`
], { type: "text/javascript" })));
let _isDevtoolsOpen = false;
let _isDetectorPaused = true;
// @ts-expect-error
// note: leverages that promises can only resolve once.
/**@type {function (boolean | null): void}*/ let resolveVerdict = undefined;
/**@type {number}*/ let nextPulse$ = NaN;
const onHeartMsg = (/** @type {MessageEvent<PulseAck>}*/ msg) => {
if (msg.data.isOpenBeat) {
/** @type {Promise<boolean | null>} */
let p = new Promise((_resolveVerdict) => {
resolveVerdict = _resolveVerdict;
let wait$ = setTimeout(
() => { wait$ = NaN; resolveVerdict(true); },
config.maxMillisBeforeAckWhenClosed + 1,
);
});
p.then((verdict) => {
if (verdict === null) return;
if (verdict !== _isDevtoolsOpen) {
_isDevtoolsOpen = verdict;
const cb = { true: config.onDetectOpen, false: config.onDetectClose }[verdict+""];
if (cb) cb();
}
nextPulse$ = setTimeout(
() => { nextPulse$ = NaN; doOnePulse(); },
config.pollingIntervalSeconds * 1000,
);
});
} else {
resolveVerdict(false);
}
};
const doOnePulse = () => {
heart.postMessage({ moreDebugs: config.moreAnnoyingDebuggerStatements });
}
/** @type {DevtoolsDetector} */
const detector = {
config,
get isOpen() {
if (_isDetectorPaused && config.onCheckOpennessWhilePaused === "throw") {
throw new Error("`onCheckOpennessWhilePaused` is set to `\"throw\"`.")
}
return _isDevtoolsOpen;
},
get paused() { return _isDetectorPaused; },
set paused(pause) {
// Note: a simpler implementation is to skip updating results in the
// ack callback. The current implementation conserves resources when
// paused.
if (_isDetectorPaused === pause) { return; }
_isDetectorPaused = pause;
if (pause) {
heart.removeEventListener("message", onHeartMsg);
clearTimeout(nextPulse$); nextPulse$ = NaN;
resolveVerdict(null);
} else {
heart.addEventListener("message", onHeartMsg);
doOnePulse();
}
}
};
Object.freeze(detector);
// @ts-expect-error
globalThis.devtoolsDetector = detector;
switch (config.startup) {
case "manual": break;
case "asap": detector.paused = false; break;
case "domContentLoaded": {
if (document.readyState !== "loading") {
detector.paused = false;
} else {
document.addEventListener("DOMContentLoaded", (ev) => {
detector.paused = false;
}, { once: true });
}
break;
}
}
})();