This file is Claude Code's working brief for the mail-time repository. It complements AGENTS.md (general agent guidelines) and skills/mail-time/SKILL.md (the distributable user-facing skill).
mail-time is an NPM package that queues and sends emails (via nodemailer transports) across horizontally scaled Node.js / Bun apps. It uses Mongo / Redis / Postgres (or a custom adapter) for queue storage and the josk library for cluster-aware scheduling.
- Editing core behavior? →
index.js. Everything else (CJS bundle, types) is regenerated from it. - Adding / fixing an adapter? →
adapters/<store>.jsplusadapters/blank-example.jsas scaffold +docs/queue-api.mdfor the contract. - Tuning a preset / adding one? →
presets.js. Test intest/jest/presets.test.js. The re-export fromindex.jsis already wired. - Testing? →
test/jest/*.test.jsis the fast unit suite (no live DB).test/npm-*.jsis the integration suite that needsREDIS_URL/MONGO_URL/PG_URL. - TS types? → JSDoc lives in
index.js,presets.js, andadapters/*.js. The .d.ts is generated. To strip private members,scripts/strip-internal-dts.mjsruns as part ofprepublishOnly. - User docs? →
README.md(public),docs/{multi-instance,dedicated-mail-host,tuning,queue-api,meteor,migration-v3-v4}.md(dive-in examples), andskills/mail-time/SKILL.md(Claude-facing). - AI instructions and guidelines? →
AGENTS.mdand this fileCLAUDE.md(Claude-facing).
- Read the JoSk skill (
/joskor~/.claude/skills/josk/) when changing scheduler-related code. MailTime is a thin layer over JoSk for an email-shaped workload — most "should this happen?" questions resolve through JoSk's contract. - Public methods on
MailTimeand the three queues are stable contract. Internal helpers (___send,___compileMailOpts,__getKey, etc.) are explicitly marked internal and stripped from .d.ts — they're free to change. - The only runtime dep is
josk. Don't add another without a written reason in the PR description.
- Adding a "convenience" dep: explicit no-go. We removed
deepmergeto get to a single runtime dep. - Editing
index.cjs,index.d.ts,index.d.cts, oradapters/*.d.tsdirectly: they're generated. Runnpm run prepublishOnlyto refresh. - Lowering Jest coverage threshold: don't. Add tests instead.
- Bypassing the atomic claim guard in a queue adapter's
update: this is what prevents two servers from sending the same email. Claim updates carryupdateObj = { isSending: true, sendingAt: <ms>, tries: N }. The guard predicate isisSent=false AND isFailed=false AND isCancelled=false AND tries=task.tries AND (isSending=false OR sendingAt <= now - sendingTimeout).isSendingis the per-row lock;sendingAtis when it was taken (for stale-lock recovery). - Routing iterate dispatches through
___sendinstead of___dispatch: adapters must callawait mailTimeInstance.___dispatch(row)for each due row.___dispatchacquires a slot from the bounded send pool (concurrencyoption) and starts___senddetached so the JoSk lease can be released as soon as the scan completes. - Setting
zombieTimebelow 60s: SMTP send + retries can legitimately take ~30s. JoSk's docs explicitly call this out. Because___iteratereturns as soon as the scan completes (not after SMTP), only a stuck storage scan can blowzombieTime. - Setting
sendingTimeoutbelow the worst-case SMTP roundtrip: a healthy still-mid-SMTP worker can lose itsisSendinglock to a "recovery" worker, causing a duplicate send. Since v5___startClaimRenewalre-stampssendingAtmid-send (renewClaim, bounded bymaxRenewals), so this is survivable — butsendingTimeoutstill sets crash-recovery latency, and the constructor warns below120000. Do not remove the renewal's lease guard: it must reuse__withLease(task, …)so it succeeds only while this worker still owns the row. - Skipping
mailTime.destroy()(orawait mailTime.drain()) in tests: the scheduler timer keeps the test process alive. Tests that await___senddirectly still run the full lifecycle synchronously, but tests that drive iterate (via___iterateorqueue.iterate()) must callawait mailTime.drain()to let the in-process send pool finish.
# Jest unit suite (no live DB needed)
npm run test:jest
# Type-checks the .d.ts against fixture .ts/.cts files
npm run test:types
# Mocha integration suite (needs all three DBs)
REDIS_URL=... MONGO_URL=... PG_URL=... npm run test:mocha
# Bun runner (Jest-shape tests only)
bun test ./test/jest
# Refresh .cjs + .d.ts after editing index.js or adapters
npm run prepublishOnlyskills/mail-time/SKILL.md is the distributable Claude-facing knowledge bundle. The layout follows the npx skills convention: a top-level skills/ directory with each skill in its own subfolder. Keep SKILL.md under ~500 lines; push deep detail into skills/mail-time/references/{api,adapters,recipes,tuning}.md. The frontmatter description is the trigger — it should be specific enough that Claude reaches for the skill without the user naming MailTime explicitly. When user-visible API changes, update both README.md and skills/mail-time/references/api.md so the skill stays accurate.