chore: type-aware lint, and the four things it found - #34

Merged
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint
Aug 22, 2026
Merged

chore: type-aware lint, and the four things it found#34
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #738.

Four type-aware rules on src (not tests): no-floating-promises, no-misused-promises, await-thenable, require-await.

The broad recommendedTypeChecked preset is deliberately not used. Most of it duplicates what strict already enforces, at the cost of a much slower lint and a large backlog of findings that are style rather than defects. These four catch things tsc cannot.

It found five errors. One is a bug, two are traps, two are fine and now say so.

The bug: express/captcha.ts hung on any throw

returnasync(req,res,next): Promise<void>=>{constresult=awaitendpoints.handle({ ... });

Express 4 does not catch a rejected promise from a handler. Anything thrown inside endpoints.handle() — crypto, JSON parsing, a store access — left the request hanging until the client timed out, and logged an unhandled rejection instead of returning a 500. Now synchronous, with the rejection routed to next() explicitly.

The trap: an async submit listener

document.addEventListener('submit',async(e)=>{
...
e.preventDefault();constresult=awaitthis.execute(...);

This workedpreventDefault() is reached before the first await, so the cancel lands. But it only works for that reason, and it was one added await above line 124 away from silently breaking every protected form: once the handler yields, the browser has already submitted and cancelling is a no-op. Nothing in the test suite would have caught it.

The listener is now synchronous and the async half is its own method, kicked off with void after the cancel. The invariant is in a comment rather than in someone's memory.

The trap: a floating audioCtx.close()

Inside a try whose catch does not cover it — the promise escapes the block, so a rejection surfaced as an unhandled rejection in the user's console. void alone wouldn't fix that; it needs the .catch().

The two that were fine

violation-reporter's flush timer (flush catches everything internally and never rejects) and the fastify plugin's async signature (that's the plugin contract). Both now carry a void/disable and a sentence saying why, so they read as decisions rather than oversights. A timer whose callback rejects keeps firing and adds an unhandled rejection every tick — worth stating even when it isn't happening.

Verification

0 errors across all six packages. Warning budgets unchanged (9/25/10/0/2/0). 446 tests pass. Lint runtime went from ~0.5s to ~4s for the whole workspace, which is why the rule set stays small.

Four rules, on src only: no-floating-promises, no-misused-promises,
await-thenable, require-await. The broad recommendedTypeChecked preset is
deliberately not used -- most of it duplicates what strict already
enforces, at the cost of a much slower lint and a backlog of style
findings.
Five errors, of which one is a bug and two are traps:
- express/captcha.ts was an async RequestHandler. Express 4 does not catch
a rejected promise from a handler, so anything thrown inside
endpoints.handle() -- crypto, JSON, a store -- left the request hanging
until the client timed out, and logged an unhandled rejection instead of
returning 500. Now synchronous, with the rejection routed to next().
- invisible.ts attached an ASYNC submit listener. e.preventDefault() only
works because nothing had awaited yet; once the handler yields the
browser has already submitted and cancelling is a no-op. It worked, but
it was one added `await` away from silently breaking every protected
form, with no test that would notice. The async half is now its own
method, kicked off after the cancel.
- environment.ts let audioCtx.close() float. The surrounding try/catch does
not cover it, so a rejection surfaced as an unhandled rejection in the
user's console.
- violation-reporter's flush timer and the fastify plugin signature are
both fine as they were; they now say so rather than reading as
oversights.
ClosesWebDecoy/app#738
@cport1
cport1 merged commit 03cb3a8 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the chore/type-aware-lint branch August 22, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

chore: type-aware lint, and the four things it found - #34

Merged
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint
Aug 22, 2026
Merged

chore: type-aware lint, and the four things it found#34
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #738.

Four type-aware rules on src (not tests): no-floating-promises, no-misused-promises, await-thenable, require-await.

The broad recommendedTypeChecked preset is deliberately not used. Most of it duplicates what strict already enforces, at the cost of a much slower lint and a large backlog of findings that are style rather than defects. These four catch things tsc cannot.

It found five errors. One is a bug, two are traps, two are fine and now say so.

The bug: express/captcha.ts hung on any throw

returnasync(req,res,next): Promise<void>=>{constresult=awaitendpoints.handle({ ... });

Express 4 does not catch a rejected promise from a handler. Anything thrown inside endpoints.handle() — crypto, JSON parsing, a store access — left the request hanging until the client timed out, and logged an unhandled rejection instead of returning a 500. Now synchronous, with the rejection routed to next() explicitly.

The trap: an async submit listener

document.addEventListener('submit',async(e)=>{
...
e.preventDefault();constresult=awaitthis.execute(...);

This workedpreventDefault() is reached before the first await, so the cancel lands. But it only works for that reason, and it was one added await above line 124 away from silently breaking every protected form: once the handler yields, the browser has already submitted and cancelling is a no-op. Nothing in the test suite would have caught it.

The listener is now synchronous and the async half is its own method, kicked off with void after the cancel. The invariant is in a comment rather than in someone's memory.

The trap: a floating audioCtx.close()

Inside a try whose catch does not cover it — the promise escapes the block, so a rejection surfaced as an unhandled rejection in the user's console. void alone wouldn't fix that; it needs the .catch().

The two that were fine

violation-reporter's flush timer (flush catches everything internally and never rejects) and the fastify plugin's async signature (that's the plugin contract). Both now carry a void/disable and a sentence saying why, so they read as decisions rather than oversights. A timer whose callback rejects keeps firing and adds an unhandled rejection every tick — worth stating even when it isn't happening.

Verification

0 errors across all six packages. Warning budgets unchanged (9/25/10/0/2/0). 446 tests pass. Lint runtime went from ~0.5s to ~4s for the whole workspace, which is why the rule set stays small.

Four rules, on src only: no-floating-promises, no-misused-promises,
await-thenable, require-await. The broad recommendedTypeChecked preset is
deliberately not used -- most of it duplicates what strict already
enforces, at the cost of a much slower lint and a backlog of style
findings.
Five errors, of which one is a bug and two are traps:
- express/captcha.ts was an async RequestHandler. Express 4 does not catch
a rejected promise from a handler, so anything thrown inside
endpoints.handle() -- crypto, JSON, a store -- left the request hanging
until the client timed out, and logged an unhandled rejection instead of
returning 500. Now synchronous, with the rejection routed to next().
- invisible.ts attached an ASYNC submit listener. e.preventDefault() only
works because nothing had awaited yet; once the handler yields the
browser has already submitted and cancelling is a no-op. It worked, but
it was one added `await` away from silently breaking every protected
form, with no test that would notice. The async half is now its own
method, kicked off after the cancel.
- environment.ts let audioCtx.close() float. The surrounding try/catch does
not cover it, so a rejection surfaced as an unhandled rejection in the
user's console.
- violation-reporter's flush timer and the fastify plugin signature are
both fine as they were; they now say so rather than reading as
oversights.
ClosesWebDecoy/app#738
@cport1
cport1 merged commit 03cb3a8 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the chore/type-aware-lint branch August 22, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

chore: type-aware lint, and the four things it found - #34

Merged
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint
Aug 22, 2026
Merged

chore: type-aware lint, and the four things it found#34
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #738.

Four type-aware rules on src (not tests): no-floating-promises, no-misused-promises, await-thenable, require-await.

The broad recommendedTypeChecked preset is deliberately not used. Most of it duplicates what strict already enforces, at the cost of a much slower lint and a large backlog of findings that are style rather than defects. These four catch things tsc cannot.

It found five errors. One is a bug, two are traps, two are fine and now say so.

The bug: express/captcha.ts hung on any throw

returnasync(req,res,next): Promise<void>=>{constresult=awaitendpoints.handle({ ... });

Express 4 does not catch a rejected promise from a handler. Anything thrown inside endpoints.handle() — crypto, JSON parsing, a store access — left the request hanging until the client timed out, and logged an unhandled rejection instead of returning a 500. Now synchronous, with the rejection routed to next() explicitly.

The trap: an async submit listener

document.addEventListener('submit',async(e)=>{
...
e.preventDefault();constresult=awaitthis.execute(...);

This workedpreventDefault() is reached before the first await, so the cancel lands. But it only works for that reason, and it was one added await above line 124 away from silently breaking every protected form: once the handler yields, the browser has already submitted and cancelling is a no-op. Nothing in the test suite would have caught it.

The listener is now synchronous and the async half is its own method, kicked off with void after the cancel. The invariant is in a comment rather than in someone's memory.

The trap: a floating audioCtx.close()

Inside a try whose catch does not cover it — the promise escapes the block, so a rejection surfaced as an unhandled rejection in the user's console. void alone wouldn't fix that; it needs the .catch().

The two that were fine

violation-reporter's flush timer (flush catches everything internally and never rejects) and the fastify plugin's async signature (that's the plugin contract). Both now carry a void/disable and a sentence saying why, so they read as decisions rather than oversights. A timer whose callback rejects keeps firing and adds an unhandled rejection every tick — worth stating even when it isn't happening.

Verification

0 errors across all six packages. Warning budgets unchanged (9/25/10/0/2/0). 446 tests pass. Lint runtime went from ~0.5s to ~4s for the whole workspace, which is why the rule set stays small.

Four rules, on src only: no-floating-promises, no-misused-promises,
await-thenable, require-await. The broad recommendedTypeChecked preset is
deliberately not used -- most of it duplicates what strict already
enforces, at the cost of a much slower lint and a backlog of style
findings.
Five errors, of which one is a bug and two are traps:
- express/captcha.ts was an async RequestHandler. Express 4 does not catch
a rejected promise from a handler, so anything thrown inside
endpoints.handle() -- crypto, JSON, a store -- left the request hanging
until the client timed out, and logged an unhandled rejection instead of
returning 500. Now synchronous, with the rejection routed to next().
- invisible.ts attached an ASYNC submit listener. e.preventDefault() only
works because nothing had awaited yet; once the handler yields the
browser has already submitted and cancelling is a no-op. It worked, but
it was one added `await` away from silently breaking every protected
form, with no test that would notice. The async half is now its own
method, kicked off after the cancel.
- environment.ts let audioCtx.close() float. The surrounding try/catch does
not cover it, so a rejection surfaced as an unhandled rejection in the
user's console.
- violation-reporter's flush timer and the fastify plugin signature are
both fine as they were; they now say so rather than reading as
oversights.
ClosesWebDecoy/app#738
@cport1
cport1 merged commit 03cb3a8 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the chore/type-aware-lint branch August 22, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

chore: type-aware lint, and the four things it found - #34

Merged
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint
Aug 22, 2026
Merged

chore: type-aware lint, and the four things it found#34
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #738.

Four type-aware rules on src (not tests): no-floating-promises, no-misused-promises, await-thenable, require-await.

The broad recommendedTypeChecked preset is deliberately not used. Most of it duplicates what strict already enforces, at the cost of a much slower lint and a large backlog of findings that are style rather than defects. These four catch things tsc cannot.

It found five errors. One is a bug, two are traps, two are fine and now say so.

The bug: express/captcha.ts hung on any throw

returnasync(req,res,next): Promise<void>=>{constresult=awaitendpoints.handle({ ... });

Express 4 does not catch a rejected promise from a handler. Anything thrown inside endpoints.handle() — crypto, JSON parsing, a store access — left the request hanging until the client timed out, and logged an unhandled rejection instead of returning a 500. Now synchronous, with the rejection routed to next() explicitly.

The trap: an async submit listener

document.addEventListener('submit',async(e)=>{
...
e.preventDefault();constresult=awaitthis.execute(...);

This workedpreventDefault() is reached before the first await, so the cancel lands. But it only works for that reason, and it was one added await above line 124 away from silently breaking every protected form: once the handler yields, the browser has already submitted and cancelling is a no-op. Nothing in the test suite would have caught it.

The listener is now synchronous and the async half is its own method, kicked off with void after the cancel. The invariant is in a comment rather than in someone's memory.

The trap: a floating audioCtx.close()

Inside a try whose catch does not cover it — the promise escapes the block, so a rejection surfaced as an unhandled rejection in the user's console. void alone wouldn't fix that; it needs the .catch().

The two that were fine

violation-reporter's flush timer (flush catches everything internally and never rejects) and the fastify plugin's async signature (that's the plugin contract). Both now carry a void/disable and a sentence saying why, so they read as decisions rather than oversights. A timer whose callback rejects keeps firing and adds an unhandled rejection every tick — worth stating even when it isn't happening.

Verification

0 errors across all six packages. Warning budgets unchanged (9/25/10/0/2/0). 446 tests pass. Lint runtime went from ~0.5s to ~4s for the whole workspace, which is why the rule set stays small.

Four rules, on src only: no-floating-promises, no-misused-promises,
await-thenable, require-await. The broad recommendedTypeChecked preset is
deliberately not used -- most of it duplicates what strict already
enforces, at the cost of a much slower lint and a backlog of style
findings.
Five errors, of which one is a bug and two are traps:
- express/captcha.ts was an async RequestHandler. Express 4 does not catch
a rejected promise from a handler, so anything thrown inside
endpoints.handle() -- crypto, JSON, a store -- left the request hanging
until the client timed out, and logged an unhandled rejection instead of
returning 500. Now synchronous, with the rejection routed to next().
- invisible.ts attached an ASYNC submit listener. e.preventDefault() only
works because nothing had awaited yet; once the handler yields the
browser has already submitted and cancelling is a no-op. It worked, but
it was one added `await` away from silently breaking every protected
form, with no test that would notice. The async half is now its own
method, kicked off after the cancel.
- environment.ts let audioCtx.close() float. The surrounding try/catch does
not cover it, so a rejection surfaced as an unhandled rejection in the
user's console.
- violation-reporter's flush timer and the fastify plugin signature are
both fine as they were; they now say so rather than reading as
oversights.
ClosesWebDecoy/app#738
@cport1
cport1 merged commit 03cb3a8 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the chore/type-aware-lint branch August 22, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

chore: type-aware lint, and the four things it found - #34

Merged
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint
Aug 22, 2026
Merged

chore: type-aware lint, and the four things it found#34
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #738.

Four type-aware rules on src (not tests): no-floating-promises, no-misused-promises, await-thenable, require-await.

The broad recommendedTypeChecked preset is deliberately not used. Most of it duplicates what strict already enforces, at the cost of a much slower lint and a large backlog of findings that are style rather than defects. These four catch things tsc cannot.

It found five errors. One is a bug, two are traps, two are fine and now say so.

The bug: express/captcha.ts hung on any throw

returnasync(req,res,next): Promise<void>=>{constresult=awaitendpoints.handle({ ... });

Express 4 does not catch a rejected promise from a handler. Anything thrown inside endpoints.handle() — crypto, JSON parsing, a store access — left the request hanging until the client timed out, and logged an unhandled rejection instead of returning a 500. Now synchronous, with the rejection routed to next() explicitly.

The trap: an async submit listener

document.addEventListener('submit',async(e)=>{
...
e.preventDefault();constresult=awaitthis.execute(...);

This workedpreventDefault() is reached before the first await, so the cancel lands. But it only works for that reason, and it was one added await above line 124 away from silently breaking every protected form: once the handler yields, the browser has already submitted and cancelling is a no-op. Nothing in the test suite would have caught it.

The listener is now synchronous and the async half is its own method, kicked off with void after the cancel. The invariant is in a comment rather than in someone's memory.

The trap: a floating audioCtx.close()

Inside a try whose catch does not cover it — the promise escapes the block, so a rejection surfaced as an unhandled rejection in the user's console. void alone wouldn't fix that; it needs the .catch().

The two that were fine

violation-reporter's flush timer (flush catches everything internally and never rejects) and the fastify plugin's async signature (that's the plugin contract). Both now carry a void/disable and a sentence saying why, so they read as decisions rather than oversights. A timer whose callback rejects keeps firing and adds an unhandled rejection every tick — worth stating even when it isn't happening.

Verification

0 errors across all six packages. Warning budgets unchanged (9/25/10/0/2/0). 446 tests pass. Lint runtime went from ~0.5s to ~4s for the whole workspace, which is why the rule set stays small.

Four rules, on src only: no-floating-promises, no-misused-promises,
await-thenable, require-await. The broad recommendedTypeChecked preset is
deliberately not used -- most of it duplicates what strict already
enforces, at the cost of a much slower lint and a backlog of style
findings.
Five errors, of which one is a bug and two are traps:
- express/captcha.ts was an async RequestHandler. Express 4 does not catch
a rejected promise from a handler, so anything thrown inside
endpoints.handle() -- crypto, JSON, a store -- left the request hanging
until the client timed out, and logged an unhandled rejection instead of
returning 500. Now synchronous, with the rejection routed to next().
- invisible.ts attached an ASYNC submit listener. e.preventDefault() only
works because nothing had awaited yet; once the handler yields the
browser has already submitted and cancelling is a no-op. It worked, but
it was one added `await` away from silently breaking every protected
form, with no test that would notice. The async half is now its own
method, kicked off after the cancel.
- environment.ts let audioCtx.close() float. The surrounding try/catch does
not cover it, so a rejection surfaced as an unhandled rejection in the
user's console.
- violation-reporter's flush timer and the fastify plugin signature are
both fine as they were; they now say so rather than reading as
oversights.
ClosesWebDecoy/app#738
@cport1
cport1 merged commit 03cb3a8 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the chore/type-aware-lint branch August 22, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

chore: type-aware lint, and the four things it found - #34

Merged
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint
Aug 22, 2026
Merged

chore: type-aware lint, and the four things it found#34
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #738.

Four type-aware rules on src (not tests): no-floating-promises, no-misused-promises, await-thenable, require-await.

The broad recommendedTypeChecked preset is deliberately not used. Most of it duplicates what strict already enforces, at the cost of a much slower lint and a large backlog of findings that are style rather than defects. These four catch things tsc cannot.

It found five errors. One is a bug, two are traps, two are fine and now say so.

The bug: express/captcha.ts hung on any throw

returnasync(req,res,next): Promise<void>=>{constresult=awaitendpoints.handle({ ... });

Express 4 does not catch a rejected promise from a handler. Anything thrown inside endpoints.handle() — crypto, JSON parsing, a store access — left the request hanging until the client timed out, and logged an unhandled rejection instead of returning a 500. Now synchronous, with the rejection routed to next() explicitly.

The trap: an async submit listener

document.addEventListener('submit',async(e)=>{
...
e.preventDefault();constresult=awaitthis.execute(...);

This workedpreventDefault() is reached before the first await, so the cancel lands. But it only works for that reason, and it was one added await above line 124 away from silently breaking every protected form: once the handler yields, the browser has already submitted and cancelling is a no-op. Nothing in the test suite would have caught it.

The listener is now synchronous and the async half is its own method, kicked off with void after the cancel. The invariant is in a comment rather than in someone's memory.

The trap: a floating audioCtx.close()

Inside a try whose catch does not cover it — the promise escapes the block, so a rejection surfaced as an unhandled rejection in the user's console. void alone wouldn't fix that; it needs the .catch().

The two that were fine

violation-reporter's flush timer (flush catches everything internally and never rejects) and the fastify plugin's async signature (that's the plugin contract). Both now carry a void/disable and a sentence saying why, so they read as decisions rather than oversights. A timer whose callback rejects keeps firing and adds an unhandled rejection every tick — worth stating even when it isn't happening.

Verification

0 errors across all six packages. Warning budgets unchanged (9/25/10/0/2/0). 446 tests pass. Lint runtime went from ~0.5s to ~4s for the whole workspace, which is why the rule set stays small.

Four rules, on src only: no-floating-promises, no-misused-promises,
await-thenable, require-await. The broad recommendedTypeChecked preset is
deliberately not used -- most of it duplicates what strict already
enforces, at the cost of a much slower lint and a backlog of style
findings.
Five errors, of which one is a bug and two are traps:
- express/captcha.ts was an async RequestHandler. Express 4 does not catch
a rejected promise from a handler, so anything thrown inside
endpoints.handle() -- crypto, JSON, a store -- left the request hanging
until the client timed out, and logged an unhandled rejection instead of
returning 500. Now synchronous, with the rejection routed to next().
- invisible.ts attached an ASYNC submit listener. e.preventDefault() only
works because nothing had awaited yet; once the handler yields the
browser has already submitted and cancelling is a no-op. It worked, but
it was one added `await` away from silently breaking every protected
form, with no test that would notice. The async half is now its own
method, kicked off after the cancel.
- environment.ts let audioCtx.close() float. The surrounding try/catch does
not cover it, so a rejection surfaced as an unhandled rejection in the
user's console.
- violation-reporter's flush timer and the fastify plugin signature are
both fine as they were; they now say so rather than reading as
oversights.
ClosesWebDecoy/app#738
@cport1
cport1 merged commit 03cb3a8 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the chore/type-aware-lint branch August 22, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

chore: type-aware lint, and the four things it found - #34

Merged
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint
Aug 22, 2026
Merged

chore: type-aware lint, and the four things it found#34
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #738.

Four type-aware rules on src (not tests): no-floating-promises, no-misused-promises, await-thenable, require-await.

The broad recommendedTypeChecked preset is deliberately not used. Most of it duplicates what strict already enforces, at the cost of a much slower lint and a large backlog of findings that are style rather than defects. These four catch things tsc cannot.

It found five errors. One is a bug, two are traps, two are fine and now say so.

The bug: express/captcha.ts hung on any throw

returnasync(req,res,next): Promise<void>=>{constresult=awaitendpoints.handle({ ... });

Express 4 does not catch a rejected promise from a handler. Anything thrown inside endpoints.handle() — crypto, JSON parsing, a store access — left the request hanging until the client timed out, and logged an unhandled rejection instead of returning a 500. Now synchronous, with the rejection routed to next() explicitly.

The trap: an async submit listener

document.addEventListener('submit',async(e)=>{
...
e.preventDefault();constresult=awaitthis.execute(...);

This workedpreventDefault() is reached before the first await, so the cancel lands. But it only works for that reason, and it was one added await above line 124 away from silently breaking every protected form: once the handler yields, the browser has already submitted and cancelling is a no-op. Nothing in the test suite would have caught it.

The listener is now synchronous and the async half is its own method, kicked off with void after the cancel. The invariant is in a comment rather than in someone's memory.

The trap: a floating audioCtx.close()

Inside a try whose catch does not cover it — the promise escapes the block, so a rejection surfaced as an unhandled rejection in the user's console. void alone wouldn't fix that; it needs the .catch().

The two that were fine

violation-reporter's flush timer (flush catches everything internally and never rejects) and the fastify plugin's async signature (that's the plugin contract). Both now carry a void/disable and a sentence saying why, so they read as decisions rather than oversights. A timer whose callback rejects keeps firing and adds an unhandled rejection every tick — worth stating even when it isn't happening.

Verification

0 errors across all six packages. Warning budgets unchanged (9/25/10/0/2/0). 446 tests pass. Lint runtime went from ~0.5s to ~4s for the whole workspace, which is why the rule set stays small.

Four rules, on src only: no-floating-promises, no-misused-promises,
await-thenable, require-await. The broad recommendedTypeChecked preset is
deliberately not used -- most of it duplicates what strict already
enforces, at the cost of a much slower lint and a backlog of style
findings.
Five errors, of which one is a bug and two are traps:
- express/captcha.ts was an async RequestHandler. Express 4 does not catch
a rejected promise from a handler, so anything thrown inside
endpoints.handle() -- crypto, JSON, a store -- left the request hanging
until the client timed out, and logged an unhandled rejection instead of
returning 500. Now synchronous, with the rejection routed to next().
- invisible.ts attached an ASYNC submit listener. e.preventDefault() only
works because nothing had awaited yet; once the handler yields the
browser has already submitted and cancelling is a no-op. It worked, but
it was one added `await` away from silently breaking every protected
form, with no test that would notice. The async half is now its own
method, kicked off after the cancel.
- environment.ts let audioCtx.close() float. The surrounding try/catch does
not cover it, so a rejection surfaced as an unhandled rejection in the
user's console.
- violation-reporter's flush timer and the fastify plugin signature are
both fine as they were; they now say so rather than reading as
oversights.
ClosesWebDecoy/app#738
@cport1
cport1 merged commit 03cb3a8 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the chore/type-aware-lint branch August 22, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@cport1
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

chore: type-aware lint, and the four things it found - #34

Merged
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint
Aug 22, 2026
Merged

chore: type-aware lint, and the four things it found#34
cport1 merged 1 commit into
mainfrom
chore/type-aware-lint

Conversation

@cport1

Copy link
Copy Markdown
Contributor

Closes #738.

Four type-aware rules on src (not tests): no-floating-promises, no-misused-promises, await-thenable, require-await.

The broad recommendedTypeChecked preset is deliberately not used. Most of it duplicates what strict already enforces, at the cost of a much slower lint and a large backlog of findings that are style rather than defects. These four catch things tsc cannot.

It found five errors. One is a bug, two are traps, two are fine and now say so.

The bug: express/captcha.ts hung on any throw

returnasync(req,res,next): Promise<void>=>{constresult=awaitendpoints.handle({ ... });

Express 4 does not catch a rejected promise from a handler. Anything thrown inside endpoints.handle() — crypto, JSON parsing, a store access — left the request hanging until the client timed out, and logged an unhandled rejection instead of returning a 500. Now synchronous, with the rejection routed to next() explicitly.

The trap: an async submit listener

document.addEventListener('submit',async(e)=>{
...
e.preventDefault();constresult=awaitthis.execute(...);

This workedpreventDefault() is reached before the first await, so the cancel lands. But it only works for that reason, and it was one added await above line 124 away from silently breaking every protected form: once the handler yields, the browser has already submitted and cancelling is a no-op. Nothing in the test suite would have caught it.

The listener is now synchronous and the async half is its own method, kicked off with void after the cancel. The invariant is in a comment rather than in someone's memory.

The trap: a floating audioCtx.close()

Inside a try whose catch does not cover it — the promise escapes the block, so a rejection surfaced as an unhandled rejection in the user's console. void alone wouldn't fix that; it needs the .catch().

The two that were fine

violation-reporter's flush timer (flush catches everything internally and never rejects) and the fastify plugin's async signature (that's the plugin contract). Both now carry a void/disable and a sentence saying why, so they read as decisions rather than oversights. A timer whose callback rejects keeps firing and adds an unhandled rejection every tick — worth stating even when it isn't happening.

Verification

0 errors across all six packages. Warning budgets unchanged (9/25/10/0/2/0). 446 tests pass. Lint runtime went from ~0.5s to ~4s for the whole workspace, which is why the rule set stays small.

Four rules, on src only: no-floating-promises, no-misused-promises,
await-thenable, require-await. The broad recommendedTypeChecked preset is
deliberately not used -- most of it duplicates what strict already
enforces, at the cost of a much slower lint and a backlog of style
findings.
Five errors, of which one is a bug and two are traps:
- express/captcha.ts was an async RequestHandler. Express 4 does not catch
a rejected promise from a handler, so anything thrown inside
endpoints.handle() -- crypto, JSON, a store -- left the request hanging
until the client timed out, and logged an unhandled rejection instead of
returning 500. Now synchronous, with the rejection routed to next().
- invisible.ts attached an ASYNC submit listener. e.preventDefault() only
works because nothing had awaited yet; once the handler yields the
browser has already submitted and cancelling is a no-op. It worked, but
it was one added `await` away from silently breaking every protected
form, with no test that would notice. The async half is now its own
method, kicked off after the cancel.
- environment.ts let audioCtx.close() float. The surrounding try/catch does
not cover it, so a rejection surfaced as an unhandled rejection in the
user's console.
- violation-reporter's flush timer and the fastify plugin signature are
both fine as they were; they now say so rather than reading as
oversights.
ClosesWebDecoy/app#738
@cport1
cport1 merged commit 03cb3a8 into mainAug 22, 2026
2 checks passed
@cport1
cport1 deleted the chore/type-aware-lint branch August 22, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@cport1