curfew 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,184 @@
1
- # Temporary Holding Version
1
+ # curfew
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Run batches from an (async) iterable and stop **before** a time budget runs out.
4
+
5
+ Built for AWS Lambda (15 min hard limit) and similar time-limited runtimes: instead of being killed mid-batch, `curfew` won't start a batch that probably can't finish, and tells you to reschedule the rest.
6
+
7
+ - No runtime dependencies
8
+ - Pluggable time budget and duration estimator
9
+ - In-flight batches are never killed; they get an `AbortSignal` instead
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install curfew
15
+ ```
16
+
17
+ Requires Node.js 22+. Ships ESM and CommonJS.
18
+
19
+ ## Quick start (AWS Lambda)
20
+
21
+ ```ts
22
+ import { LambdaBudget, runBatches } from 'curfew'
23
+
24
+ export const handler = async (event, context) => {
25
+ let offset = event.offset
26
+
27
+ try {
28
+ const { batchesProcessed, mayHaveMore } = await runBatches(
29
+ fetchPages(offset),
30
+ async (page, { signal }) => {
31
+ await processPage(page, { signal })
32
+ offset++
33
+ },
34
+ { budget: new LambdaBudget(context, 60_000) },
35
+ )
36
+
37
+ if (mayHaveMore) {
38
+ await reenqueue({ offset: event.offset + batchesProcessed })
39
+ }
40
+ } catch (error) {
41
+ // The signal aborted mid-page: an expected timeout, not a failure.
42
+ // `curfew` passes handler errors through, so catch it here and resume from the interrupted page.
43
+ if (!(error instanceof Error && error.name === 'AbortError')) throw error
44
+ await reenqueue({ offset })
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Example
50
+
51
+ [`examples/basic.ts`](examples/basic.ts) processes 10 pages with a 1-second budget per run. It shows the three ways a run ends, and how to resume after each:
52
+
53
+ - **Budget runs out mid-page.** `saveItem` passes `ctx.signal` to its async work, so it rejects with an `AbortError`. The example's `runOnce` catches that error (`curfew` passes handler errors through) and resumes from the interrupted page, which is why page processing must be idempotent.
54
+ - **The next page won't fit.** `reason` is `estimate-exceeds-budget`, and the next run starts at the following page.
55
+ - **All pages done.** `reason` is `done`.
56
+
57
+ ```sh
58
+ npm run build && node examples/basic.ts
59
+ ```
60
+
61
+ ```
62
+ run 1: starting at page 0
63
+ stopped: budget ran out during page 4
64
+ run 2: starting at page 4
65
+ stopped: estimate-exceeds-budget
66
+ run 3: starting at page 5
67
+ stopped: done
68
+ all pages processed
69
+ ```
70
+
71
+ Requires Node.js 22.18+ to run `.ts` files directly.
72
+
73
+ ## How it works
74
+
75
+ Before pulling each batch, `runBatches` checks the budget. It stops and returns `mayHaveMore: true` with a `reason` if:
76
+
77
+ - the budget is used up (`StopReason.BUDGET_EXHAUSTED`), or
78
+ - the estimated batch duration is longer than the time remaining (`StopReason.ESTIMATE_EXCEEDS_BUDGET`).
79
+
80
+ When the source runs out, it returns `mayHaveMore: false` with `StopReason.DONE`.
81
+
82
+ ```ts
83
+ import { StopReason } from 'curfew'
84
+
85
+ if (result.reason === StopReason.ESTIMATE_EXCEEDS_BUDGET) {
86
+ console.log('stopped with time left; consider a smaller batch size')
87
+ }
88
+ ```
89
+
90
+ The duration of a batch covers both fetching it from the source (`iterator.next()`) and running the handler, so slow async sources such as DB cursors or paginated APIs are counted. Durations are measured with `performance.now()`, so system clock changes don't affect the estimate.
91
+
92
+ Batches run one at a time. Items from a sync iterable are passed to the handler as is: a sync iterable of promises hands the handler the promises themselves, not their values. A batch that has started is never interrupted: when the budget runs out, `ctx.signal` aborts with a `TimeoutError` reason (the same as `AbortSignal.timeout()`), so cooperative code such as `fetch(url, { signal })` can stop early.
93
+
94
+ - If fetching a batch overruns the budget, the handler still runs, but `ctx.signal` is already aborted. Check `signal.aborted` first if that matters.
95
+ - The abort timer is armed once, from `budget.remainingMs()` at the start of the run. With a custom budget that isn't based on the clock, the signal and the decision to stop can disagree.
96
+ - For budgets longer than about 24.8 days (the `setTimeout` limit), including `Infinity`, the signal never aborts. The budget check before each batch still applies.
97
+
98
+ Handler errors propagate unchanged.
99
+
100
+ The budget is checked before pulling the next item, so no item is ever pulled and then dropped. As a result, `mayHaveMore: true` means "there may be more work", not "there is more work": if the budget runs out just as the source is exhausted, the next run simply processes 0 batches and returns `mayHaveMore: false`.
101
+
102
+ ### The source is not closed
103
+
104
+ `runBatches` never calls `iterator.return()`, whether it finishes, stops early or the handler throws. You own the source's cleanup. You can resume either by offset (`batchesProcessed`) in a new invocation, or by passing the same iterator or generator to `runBatches` again. An array (or any iterable that creates a fresh iterator each time) starts again from the beginning, so resume it by offset instead:
105
+
106
+ ```ts
107
+ const source = generateWork()
108
+ const first = await runBatches(source, handle, { budget })
109
+ if (first.mayHaveMore) await runBatches(source, handle, { budget: nextBudget })
110
+ ```
111
+
112
+ ## Budgets
113
+
114
+ ```ts
115
+ import { DeadlineBudget, FixedBudget, LambdaBudget } from 'curfew'
116
+
117
+ new FixedBudget(13 * 60_000) // 13 minutes from now
118
+ new DeadlineBudget(new Date('2026-12-01')) // until an absolute time (Date or epoch ms)
119
+ new LambdaBudget(context, 60_000) // Lambda remaining time minus a 60 s margin
120
+ ```
121
+
122
+ ### Custom budget
123
+
124
+ Implement `TimeBudget`:
125
+
126
+ ```ts
127
+ import type { TimeBudget } from 'curfew'
128
+
129
+ class JobBudget implements TimeBudget {
130
+ constructor(private job: { expiresAt: number }) {}
131
+ remainingMs() {
132
+ return this.job.expiresAt - Date.now()
133
+ }
134
+ }
135
+ ```
136
+
137
+ ## Estimators
138
+
139
+ By default, the estimate is the longest batch seen so far (`MaxEstimator`), so `runBatches` errs toward stopping early. Before any batch has run, the estimate is `0`, so the first batch runs as long as any time remains. Pass `new MaxEstimator(initialMs)` to require that much time for the first batch:
140
+
141
+ ```ts
142
+ import { MaxEstimator, runBatches } from 'curfew'
143
+
144
+ await runBatches(source, handle, { budget, estimator: new MaxEstimator(30_000) })
145
+ ```
146
+
147
+ ### Custom estimator
148
+
149
+ Implement `Estimator`. `record` is called with each batch's duration, and `estimateMs` is consulted before each batch:
150
+
151
+ ```ts
152
+ import type { Estimator } from 'curfew'
153
+
154
+ class EwmaEstimator implements Estimator {
155
+ private value = 0
156
+ constructor(private alpha = 0.3) {}
157
+ record(ms: number) {
158
+ this.value = this.value === 0 ? ms : this.alpha * ms + (1 - this.alpha) * this.value
159
+ }
160
+ estimateMs() {
161
+ return this.value
162
+ }
163
+ }
164
+ ```
165
+
166
+ ## API
167
+
168
+ ```ts
169
+ runBatches<T>(
170
+ source: Iterable<T> | AsyncIterable<T>,
171
+ handler: (batch: T, ctx: { signal: AbortSignal; index: number }) => Promise<void> | void,
172
+ options: { budget: TimeBudget; estimator?: Estimator },
173
+ ): Promise<{ batchesProcessed: number; mayHaveMore: boolean; reason: StopReason }>
174
+
175
+ enum StopReason {
176
+ DONE = 'done',
177
+ BUDGET_EXHAUSTED = 'budget-exhausted',
178
+ ESTIMATE_EXCEEDS_BUDGET = 'estimate-exceeds-budget',
179
+ }
180
+ ```
181
+
182
+ ## License
183
+
184
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,114 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ // src/budgets/DeadlineBudget.ts
3
+ var DeadlineBudget = class {
4
+ deadline;
5
+ constructor(deadline) {
6
+ this.deadline = deadline instanceof Date ? deadline.getTime() : deadline;
7
+ }
8
+ remainingMs() {
9
+ return this.deadline - Date.now();
10
+ }
11
+ };
12
+ // src/budgets/FixedBudget.ts
13
+ var FixedBudget = class extends DeadlineBudget {
14
+ constructor(durationMs) {
15
+ super(Date.now() + durationMs);
16
+ }
17
+ };
18
+ // src/budgets/LambdaBudget.ts
19
+ var LambdaBudget = class {
20
+ context;
21
+ safetyMarginMs;
22
+ constructor(context, safetyMarginMs) {
23
+ this.context = context;
24
+ this.safetyMarginMs = safetyMarginMs;
25
+ }
26
+ remainingMs() {
27
+ return this.context.getRemainingTimeInMillis() - this.safetyMarginMs;
28
+ }
29
+ };
30
+ // src/estimators/MaxEstimator.ts
31
+ var MaxEstimator = class {
32
+ max;
33
+ constructor(initialMs = 0) {
34
+ this.max = initialMs;
35
+ }
36
+ record(ms) {
37
+ this.max = Math.max(this.max, ms);
38
+ }
39
+ estimateMs() {
40
+ return this.max;
41
+ }
42
+ };
43
+ // src/runner/StopReason.ts
44
+ let StopReason = /* @__PURE__ */ function(StopReason) {
45
+ StopReason["DONE"] = "done";
46
+ StopReason["BUDGET_EXHAUSTED"] = "budget-exhausted";
47
+ StopReason["ESTIMATE_EXCEEDS_BUDGET"] = "estimate-exceeds-budget";
48
+ return StopReason;
49
+ }({});
50
+ // src/runner/RunBatches.ts
51
+ const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
52
+ async function runBatches(source, handler, { budget, estimator = new MaxEstimator() }) {
53
+ const batches = getIterator(source);
54
+ const deadline = abortWhenBudgetRunsOut(budget);
55
+ let batchesProcessed = 0;
56
+ try {
57
+ while (true) {
58
+ const reason = reasonToStopBeforeNextBatch(budget, estimator);
59
+ if (reason) return {
60
+ batchesProcessed,
61
+ mayHaveMore: true,
62
+ reason
63
+ };
64
+ const startedAt = performance.now();
65
+ const next = await batches.next();
66
+ if (next.done) return {
67
+ batchesProcessed,
68
+ mayHaveMore: false,
69
+ reason: "done"
70
+ };
71
+ await handler(next.value, {
72
+ signal: deadline.signal,
73
+ index: batchesProcessed
74
+ });
75
+ estimator.record(performance.now() - startedAt);
76
+ batchesProcessed++;
77
+ }
78
+ } finally {
79
+ deadline.cancel();
80
+ }
81
+ }
82
+ function reasonToStopBeforeNextBatch(budget, estimator) {
83
+ const remainingMs = budget.remainingMs();
84
+ if (!(remainingMs > 0)) return "budget-exhausted";
85
+ if (!(estimator.estimateMs() <= remainingMs)) return "estimate-exceeds-budget";
86
+ return null;
87
+ }
88
+ function abortWhenBudgetRunsOut(budget) {
89
+ const controller = new AbortController();
90
+ const remainingMs = budget.remainingMs();
91
+ const delayMs = remainingMs > 0 ? remainingMs : 0;
92
+ if (delayMs > MAX_TIMER_DELAY_MS) return {
93
+ signal: controller.signal,
94
+ cancel: () => {}
95
+ };
96
+ const timer = setTimeout(() => controller.abort(new DOMException("Budget exhausted", "TimeoutError")), delayMs);
97
+ timer.unref();
98
+ return {
99
+ signal: controller.signal,
100
+ cancel: () => clearTimeout(timer)
101
+ };
102
+ }
103
+ function getIterator(source) {
104
+ return isAsyncIterable(source) ? source[Symbol.asyncIterator]() : source[Symbol.iterator]();
105
+ }
106
+ function isAsyncIterable(source) {
107
+ return typeof source[Symbol.asyncIterator] === "function";
108
+ }
109
+ exports.DeadlineBudget = DeadlineBudget;
110
+ exports.FixedBudget = FixedBudget;
111
+ exports.LambdaBudget = LambdaBudget;
112
+ exports.MaxEstimator = MaxEstimator;
113
+ exports.StopReason = StopReason;
114
+ exports.runBatches = runBatches;
@@ -0,0 +1,52 @@
1
+ interface TimeBudget {
2
+ remainingMs(): number;
3
+ }
4
+ export declare class DeadlineBudget implements TimeBudget {
5
+ private readonly deadline;
6
+ constructor(deadline: Date | number);
7
+ remainingMs(): number;
8
+ }
9
+ export declare class FixedBudget extends DeadlineBudget {
10
+ constructor(durationMs: number);
11
+ }
12
+ interface LambdaContextLike {
13
+ getRemainingTimeInMillis(): number;
14
+ }
15
+ export declare class LambdaBudget implements TimeBudget {
16
+ private readonly context;
17
+ private readonly safetyMarginMs;
18
+ constructor(context: LambdaContextLike, safetyMarginMs: number);
19
+ remainingMs(): number;
20
+ }
21
+ interface Estimator {
22
+ record(ms: number): void;
23
+ estimateMs(): number;
24
+ }
25
+ export declare class MaxEstimator implements Estimator {
26
+ private max;
27
+ constructor(initialMs?: number);
28
+ record(ms: number): void;
29
+ estimateMs(): number;
30
+ }
31
+ interface BatchContext {
32
+ signal: AbortSignal;
33
+ index: number;
34
+ }
35
+ type BatchHandler<T> = (batch: T, ctx: BatchContext) => Promise<void> | void;
36
+ type BatchSource<T> = Iterable<T> | AsyncIterable<T>;
37
+ interface RunOptions {
38
+ budget: TimeBudget;
39
+ estimator?: Estimator;
40
+ }
41
+ export declare enum StopReason {
42
+ DONE = "done",
43
+ BUDGET_EXHAUSTED = "budget-exhausted",
44
+ ESTIMATE_EXCEEDS_BUDGET = "estimate-exceeds-budget"
45
+ }
46
+ interface RunResult {
47
+ batchesProcessed: number;
48
+ mayHaveMore: boolean;
49
+ reason: StopReason;
50
+ }
51
+ export declare function runBatches<T>(source: BatchSource<T>, handler: BatchHandler<T>, { budget, estimator }: RunOptions): Promise<RunResult>;
52
+ export type { BatchContext, BatchHandler, BatchSource, Estimator, LambdaContextLike, RunOptions, RunResult, TimeBudget };
@@ -0,0 +1,52 @@
1
+ interface TimeBudget {
2
+ remainingMs(): number;
3
+ }
4
+ export declare class DeadlineBudget implements TimeBudget {
5
+ private readonly deadline;
6
+ constructor(deadline: Date | number);
7
+ remainingMs(): number;
8
+ }
9
+ export declare class FixedBudget extends DeadlineBudget {
10
+ constructor(durationMs: number);
11
+ }
12
+ interface LambdaContextLike {
13
+ getRemainingTimeInMillis(): number;
14
+ }
15
+ export declare class LambdaBudget implements TimeBudget {
16
+ private readonly context;
17
+ private readonly safetyMarginMs;
18
+ constructor(context: LambdaContextLike, safetyMarginMs: number);
19
+ remainingMs(): number;
20
+ }
21
+ interface Estimator {
22
+ record(ms: number): void;
23
+ estimateMs(): number;
24
+ }
25
+ export declare class MaxEstimator implements Estimator {
26
+ private max;
27
+ constructor(initialMs?: number);
28
+ record(ms: number): void;
29
+ estimateMs(): number;
30
+ }
31
+ interface BatchContext {
32
+ signal: AbortSignal;
33
+ index: number;
34
+ }
35
+ type BatchHandler<T> = (batch: T, ctx: BatchContext) => Promise<void> | void;
36
+ type BatchSource<T> = Iterable<T> | AsyncIterable<T>;
37
+ interface RunOptions {
38
+ budget: TimeBudget;
39
+ estimator?: Estimator;
40
+ }
41
+ export declare enum StopReason {
42
+ DONE = "done",
43
+ BUDGET_EXHAUSTED = "budget-exhausted",
44
+ ESTIMATE_EXCEEDS_BUDGET = "estimate-exceeds-budget"
45
+ }
46
+ interface RunResult {
47
+ batchesProcessed: number;
48
+ mayHaveMore: boolean;
49
+ reason: StopReason;
50
+ }
51
+ export declare function runBatches<T>(source: BatchSource<T>, handler: BatchHandler<T>, { budget, estimator }: RunOptions): Promise<RunResult>;
52
+ export type { BatchContext, BatchHandler, BatchSource, Estimator, LambdaContextLike, RunOptions, RunResult, TimeBudget };
package/dist/index.mjs ADDED
@@ -0,0 +1,108 @@
1
+ // src/budgets/DeadlineBudget.ts
2
+ var DeadlineBudget = class {
3
+ deadline;
4
+ constructor(deadline) {
5
+ this.deadline = deadline instanceof Date ? deadline.getTime() : deadline;
6
+ }
7
+ remainingMs() {
8
+ return this.deadline - Date.now();
9
+ }
10
+ };
11
+ // src/budgets/FixedBudget.ts
12
+ var FixedBudget = class extends DeadlineBudget {
13
+ constructor(durationMs) {
14
+ super(Date.now() + durationMs);
15
+ }
16
+ };
17
+ // src/budgets/LambdaBudget.ts
18
+ var LambdaBudget = class {
19
+ context;
20
+ safetyMarginMs;
21
+ constructor(context, safetyMarginMs) {
22
+ this.context = context;
23
+ this.safetyMarginMs = safetyMarginMs;
24
+ }
25
+ remainingMs() {
26
+ return this.context.getRemainingTimeInMillis() - this.safetyMarginMs;
27
+ }
28
+ };
29
+ // src/estimators/MaxEstimator.ts
30
+ var MaxEstimator = class {
31
+ max;
32
+ constructor(initialMs = 0) {
33
+ this.max = initialMs;
34
+ }
35
+ record(ms) {
36
+ this.max = Math.max(this.max, ms);
37
+ }
38
+ estimateMs() {
39
+ return this.max;
40
+ }
41
+ };
42
+ // src/runner/StopReason.ts
43
+ let StopReason = /* @__PURE__ */ function(StopReason) {
44
+ StopReason["DONE"] = "done";
45
+ StopReason["BUDGET_EXHAUSTED"] = "budget-exhausted";
46
+ StopReason["ESTIMATE_EXCEEDS_BUDGET"] = "estimate-exceeds-budget";
47
+ return StopReason;
48
+ }({});
49
+ // src/runner/RunBatches.ts
50
+ const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
51
+ async function runBatches(source, handler, { budget, estimator = new MaxEstimator() }) {
52
+ const batches = getIterator(source);
53
+ const deadline = abortWhenBudgetRunsOut(budget);
54
+ let batchesProcessed = 0;
55
+ try {
56
+ while (true) {
57
+ const reason = reasonToStopBeforeNextBatch(budget, estimator);
58
+ if (reason) return {
59
+ batchesProcessed,
60
+ mayHaveMore: true,
61
+ reason
62
+ };
63
+ const startedAt = performance.now();
64
+ const next = await batches.next();
65
+ if (next.done) return {
66
+ batchesProcessed,
67
+ mayHaveMore: false,
68
+ reason: "done"
69
+ };
70
+ await handler(next.value, {
71
+ signal: deadline.signal,
72
+ index: batchesProcessed
73
+ });
74
+ estimator.record(performance.now() - startedAt);
75
+ batchesProcessed++;
76
+ }
77
+ } finally {
78
+ deadline.cancel();
79
+ }
80
+ }
81
+ function reasonToStopBeforeNextBatch(budget, estimator) {
82
+ const remainingMs = budget.remainingMs();
83
+ if (!(remainingMs > 0)) return "budget-exhausted";
84
+ if (!(estimator.estimateMs() <= remainingMs)) return "estimate-exceeds-budget";
85
+ return null;
86
+ }
87
+ function abortWhenBudgetRunsOut(budget) {
88
+ const controller = new AbortController();
89
+ const remainingMs = budget.remainingMs();
90
+ const delayMs = remainingMs > 0 ? remainingMs : 0;
91
+ if (delayMs > MAX_TIMER_DELAY_MS) return {
92
+ signal: controller.signal,
93
+ cancel: () => {}
94
+ };
95
+ const timer = setTimeout(() => controller.abort(new DOMException("Budget exhausted", "TimeoutError")), delayMs);
96
+ timer.unref();
97
+ return {
98
+ signal: controller.signal,
99
+ cancel: () => clearTimeout(timer)
100
+ };
101
+ }
102
+ function getIterator(source) {
103
+ return isAsyncIterable(source) ? source[Symbol.asyncIterator]() : source[Symbol.iterator]();
104
+ }
105
+ function isAsyncIterable(source) {
106
+ return typeof source[Symbol.asyncIterator] === "function";
107
+ }
108
+ export { DeadlineBudget, FixedBudget, LambdaBudget, MaxEstimator, StopReason, runBatches };
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "curfew",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Run batches from an (async) iterable and stop before a time budget runs out.",
5
+ "keywords": [
6
+ "batch",
7
+ "deadline",
8
+ "timeout",
9
+ "time-budget",
10
+ "lambda",
11
+ "aws-lambda",
12
+ "abortsignal",
13
+ "graceful-shutdown"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Pushkal Guha Choudhury",
17
+ "homepage": "https://github.com/Maganlal-Meghraj/curfew#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Maganlal-Meghraj/curfew.git"
21
+ },
22
+ "type": "module",
23
+ "sideEffects": false,
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "types": "./dist/index.d.cts",
32
+ "exports": {
33
+ ".": {
34
+ "import": {
35
+ "types": "./dist/index.d.mts",
36
+ "default": "./dist/index.mjs"
37
+ },
38
+ "require": {
39
+ "types": "./dist/index.d.cts",
40
+ "default": "./dist/index.cjs"
41
+ }
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
45
+ "scripts": {
46
+ "build": "tsdown",
47
+ "test": "vitest run",
48
+ "typecheck": "tsc",
49
+ "lint": "eslint .",
50
+ "lint:pkg": "publint && attw --pack .",
51
+ "prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build && npm run lint:pkg"
52
+ },
53
+ "devDependencies": {
54
+ "@arethetypeswrong/cli": "^0.18.5",
55
+ "@eslint/js": "^10.0.1",
56
+ "@types/node": "^22.20.5",
57
+ "eslint": "^10.14.0",
58
+ "publint": "^0.3.25",
59
+ "tsdown": "0.23.7",
60
+ "typescript": "^6.0.3",
61
+ "typescript-eslint": "^8.72.1",
62
+ "vitest": "^5.1.0"
63
+ }
64
+ }