mirror of
https://github.com/hanzoai/sign.git
synced 2026-10-21 01:21:51 +00:00
ESLint was raised to 10, which reads eslint.config.* and nothing else. This repo still said it all in .eslintrc.cjs and .eslintignore, so every lint run ended at "couldn't find an eslint.config file" — and the pre-commit hook runs eslint over staged sources, so no commit touching a .ts file could be made. The config is the same config in the flat spelling: every rule, every ignore and the __tests__ override carried over, and the two legacy files are gone rather than left beside it to disagree. The transaction rule (inside $transaction, only `tx`) keeps both its callers. transaction.cjs is a whole flat config, so `lint:tx` still hands it to eslint alone — with --no-config-lookup, since --no-eslintrc and --ext no longer exist — and the shared config spreads it in. Its test now gives the linter the array AS SHIPPED instead of re-stating the parser beside a copy of the rules: a copy keeps passing after the original stops selecting files. 10 of 10, and `lint:tx` is clean over packages and apps. eslint-plugin-react finds the React version by asking the rule context for a filename through an API ESLint 10 removed, so detection threw on the first file. The version is stated, which never takes that path. Co-authored-by: Hanzo Dev <[email protected]>
95 lines
4.2 KiB
JavaScript
95 lines
4.2 KiB
JavaScript
/**
|
|
* Inside a transaction, only `tx`.
|
|
*
|
|
* Base SQLite serves one write connection. `prisma.$transaction(async (tx) =>
|
|
* …)` holds that connection for as long as the callback runs. A call in the
|
|
* callback that reaches the global client asks for the same connection, so it
|
|
* queues behind a lock the transaction cannot release until the call it is
|
|
* waiting on returns. It waits out the busy timeout and the transaction dies:
|
|
* `P2028 Transaction already closed`.
|
|
*
|
|
* A call that leaves the process — mail, SES, an HTTP request — does not
|
|
* deadlock, but it holds that one connection open across the network. Every
|
|
* other write in the process waits on a mail server, and a slow send reaches
|
|
* the same timeout by a longer road.
|
|
*
|
|
* So the callback waits on `tx` and on nothing else. Jobs, webhooks,
|
|
* notifications and mail are side effects of committed state: they read what
|
|
* the transaction wrote, so they belong after the commit — return what they
|
|
* need from the callback and fire them on the way out. This deliberately
|
|
* changes what a failed send means. A bounced notification no longer rolls
|
|
* back the record it announces; the record stands and the send is reported
|
|
* where it happened.
|
|
*
|
|
* Three questions, one each:
|
|
*
|
|
* 1. Is the transaction client called `tx`? One name, so 2 and 3 can read
|
|
* the callback by that name and be exact.
|
|
* 2. Does the callback name a client that is not `tx`? That is the deadlock.
|
|
* 3. Does the callback wait on a call it did not hand `tx`? That is
|
|
* everything else — jobs, webhooks, mail, any I/O — under one question
|
|
* rather than a list of the callees anyone has hit so far.
|
|
*
|
|
* Read by two callers: `./index.cjs`, so an editor says it while you type, and
|
|
* the `lint:tx` script, which runs this file alone over every package and app.
|
|
* It takes seconds, so it sweeps the whole tree rather than a list of the
|
|
* directories that open a transaction today — a new one is covered by being
|
|
* written, not by being remembered here.
|
|
*/
|
|
|
|
const TRANSACTION = 'CallExpression[callee.property.name="$transaction"]';
|
|
const CALLBACK = `${TRANSACTION} > :function`;
|
|
|
|
const because =
|
|
'Base SQLite serves one write connection: the transaction holds it, so a second client ' +
|
|
'waits on a lock that is only released once the waiting call returns (P2028), and a call ' +
|
|
'that leaves the process holds it open across the network. ' +
|
|
'Return what you need from the callback and run the side effect after the commit.';
|
|
|
|
/** @type {{ selector: string, message: string }[]} */
|
|
const restricted = [
|
|
{
|
|
selector: `${TRANSACTION} > :function:not([params.0.name="tx"])`,
|
|
message:
|
|
'name the transaction client tx — every transaction in the tree does, and the rest of ' +
|
|
'this rule reads the callback by that name.',
|
|
},
|
|
{
|
|
selector: `${CALLBACK} Identifier[name=/^(prisma|kyselyPrisma)$/]`,
|
|
message: `no global prisma inside $transaction — use tx, or move the side effect after commit. ${because}`,
|
|
},
|
|
{
|
|
// Handing `tx` on is how a call says it joins the transaction; a call that
|
|
// mentions it anywhere — `tx.envelope.create(…)`, `Promise.all([tx…])`,
|
|
// `helper({ tx })` — has said so. Waiting on one that does not is waiting
|
|
// on the world outside, and the transaction is holding the door.
|
|
selector: `${CALLBACK} AwaitExpression > CallExpression:not(:has(Identifier[name="tx"]))`,
|
|
message: `no awaiting a call you did not hand tx inside $transaction — a job, a webhook, an email or any other I/O belongs after the commit. ${because}`,
|
|
},
|
|
];
|
|
|
|
const parser = require('@typescript-eslint/parser');
|
|
|
|
// A flat config, and a whole one: `lint:tx` hands this file to eslint alone, so
|
|
// it says for itself which files it reads and which it never does.
|
|
module.exports = [
|
|
{ ignores: ['**/build/**', '**/dist/**', '**/.react-router/**', '**/node_modules/**'] },
|
|
{
|
|
name: 'esign/transaction',
|
|
files: ['**/*.ts', '**/*.tsx'],
|
|
|
|
languageOptions: {
|
|
parser,
|
|
parserOptions: {
|
|
ecmaVersion: 2022,
|
|
sourceType: 'module',
|
|
ecmaFeatures: { jsx: true },
|
|
},
|
|
},
|
|
|
|
rules: {
|
|
'no-restricted-syntax': ['error', ...restricted],
|
|
},
|
|
},
|
|
];
|