logroller 1.0.0 → 2.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,101 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [2.0.1] — 2026-09-0X
9
+
10
+ ### Fixed
11
+
12
+ - Reactivated segments no longer inherit stale lifecycle timestamps
13
+ (`abandonedAt` could predate `openedAt`).
14
+ - Raw tails of already-passed periods are now gzipped at boot
15
+ (crash-near-midnight no longer leaves yesterday's file uncompressed).
16
+ - Rotation adopts a `.gz` produced by a (warned-about) concurrent writer
17
+ instead of recording a false `drop-empty` that orphaned the archive.
18
+
19
+ ## [2.0.0] — 2026-08-28
20
+
21
+ Architecture release: the library moved from a single-file JavaScript SDK to a
22
+ modular **TypeScript** codebase with strict typings, first-class DX tooling,
23
+ and CI-gated releases. The public runtime API (`createStream`,
24
+ `RotateFileStream`, all options and events) remains compatible with 1.x;
25
+ TypeScript consumers gain full type coverage.
26
+
27
+ > ⚠️ Consumers on 1.x should re-run their smoke/integration tests after
28
+ > upgrading — mostly due to the deterministic boot-order fix below.
29
+
30
+ ### Added
31
+
32
+ - **Full TypeScript source with shipped declarations** (`dist/index.d.ts`);
33
+ strict-mode compile (`noImplicitOverride`, `noUnusedLocals`,
34
+ `verbatimModuleSyntax`, …).
35
+ - **Tuple-typed events** via `RotateFileStreamEventMap` — `on("rotated", …)`
36
+ autocompletes and payload types are checked (`RotatedInfo`, `ClockInfo`),
37
+ while Node-core fallback overloads keep `.pipe()` structurally valid.
38
+ - `advanceClock()` — apply one virtual-clock tick immediately (previously the
39
+ step timer fired only every minute); makes day-crossing tests fully
40
+ deterministic.
41
+ - `clockStepIntervalMs` option — control how often `addHoursEveryMin` ticks.
42
+ - Modular source layout (`src/types.ts`, `parsers.ts`, `time.ts`, `fsio.ts`,
43
+ `rotate-file-stream.ts`) with `.js` extension imports so emitted
44
+ declarations resolve under `nodenext` consumers.
45
+ - Developer toolchain: Prettier, `npm run verify` gate (typecheck + build + tests).
46
+ - GitHub Actions: CI matrix on Node 18/20/22, release-triggered publish with
47
+ npm provenance (`id-token: write`, `--provenance`).
48
+ - Expanded README: comparison table, FAQ, restart-resume matrix, pino recipe.
49
+
50
+ ### Fixed
51
+
52
+ - **Deterministic boot sequencing**: housekeeping (zombie demotion, disk
53
+ reconciliation, interrupted-gzip repair) is chained as job #1 of the
54
+ serial operation queue in the constructor instead of `process.nextTick`,
55
+ so a rapid first `write()` can no longer race a boot-time reopen or skip
56
+ zombie cleanup.
57
+ - Retention sweep skips `<audit>.corrupt` artifacts alongside `.tmp`.
58
+
59
+ ### Changed
60
+
61
+ - Internals restructured behind the same public exports; barrels re-export
62
+ types explicitly (`import type { RotatedInfo } from "logroller"`).
63
+ - Stricter package hygiene metadata: `sideEffects: false`, explicit
64
+ `./package.json` export, `files` whitelist (`dist`, docs, license,
65
+ changelog).
66
+ - Test suite runs against the built bundle and covers: base writing +
67
+ manifest creation, size rotation/gzip/indexing, restart resume,
68
+ `addHours` naming, virtual midnight crossing, count-retention ordering
69
+ (mtime-deterministic), zombie-record lifecycle (`abandon → active →
70
+ closed`), and CJS loadability through the exports map.
71
+
72
+ ## [1.0.0] — 2026-08-26
73
+
74
+ Initial release. Single-file zero-dependency JavaScript SDK
75
+ (`file-rotation-sdk.js`) providing a Writable stream with scheduled file
76
+ rotation.
77
+
78
+ ### Added
79
+
80
+ - Time-based rotation: `frequency: "daily"` (local midnight in any IANA
81
+ timezone via `Intl`) or epoch-aligned intervals (`"30s"`, `"5m"`, `"2h"`).
82
+ - Size-based rotation: `maxSize` (`"20m"` / `"512k"` / raw bytes) producing
83
+ numbered segments — `app-D.log`, `app-D.1.log`, … — inserted between the
84
+ date stamp and the extension.
85
+ - Optional gzip archiving of sealed segments (`zippedArchive`), atomic tmp+
86
+ rename writes, source deleted only after verified compression success.
87
+ - Retention via `maxFiles`: age policy (`"90d"`) or count policy (`500`);
88
+ unset means files are never auto-deleted.
89
+ - Crash-safe restart resume from disk scan: raw tails are appended to,
90
+ sealed chains continue at the next free index; lazy period-roll catches
91
+ boundaries missed while suspended or shut down.
92
+ - Audit manifest (`<stem>_audit.json`): lifecycle records per file, capped
93
+ uuid-tagged event journal, `gzipPending` intent markers enabling automatic
94
+ repair of gzips interrupted by a crash, zombie active-record demotion,
95
+ disk-divergence healing (`archive-regressed`, `vanished`),
96
+ concurrent-writer detection via pid/bootId heartbeat.
97
+ - Virtual test clock: static shift (`addHours`) and per-minute stepping
98
+ (`addHoursEveryMin`).
99
+ - Dual-format distribution (ESM + CJS) bundled with Rollup; named exports
100
+ `createStream` and `RotateFileStream`; factory options mirroring common
101
+ rotator APIs; streams pipe-ready for pino multistreams.
package/README.md CHANGED
@@ -1,19 +1,48 @@
1
- # logroller
1
+ ## README.md
2
2
 
3
- Zero-dependency rotating file stream for Node.js. Daily or interval rotation,
4
- size limits, gzip archives, retention policies, timezone-aware naming and a
5
- crash-safe resume — in one small Writable stream you can `pipe()` into or hand
6
- to pino.
3
+ <p align="center"><strong>logroller</strong></p>
4
+ <h3 align="center">Zero-dependency rotating file streams for Node.js</h3>
5
+
6
+ <div align="center">
7
+
8
+ ![npm](https://img.shields.io/npm/v/logroller)
9
+ ![node](https://img.shields.io/node/v/logroller)
10
+ ![license](https://img.shields.io/npm/l/logroller)
11
+ ![types](https://shields.io/badge/types-included-blue)
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ `logroller` turns a plain append destination into a resilient, observable
18
+ log sink: **it is a Writable**, so `readable.pipe(log)` works, pino multistreams
19
+ work, anything that consumes streams works.
7
20
 
8
21
  ```bash
9
22
  npm install logroller
10
23
  ```
11
24
 
25
+ ## Features
26
+
27
+ - **Daily & interval rotation** — exact local midnight in any IANA zone,
28
+ or epoch-aligned intervals (`30s`, `5m`, `2h`)
29
+ - **Size caps** — `"20m"`-style limits roll numbered segments
30
+ - **Gzip archives** — sealed segments compressed atomically
31
+ - **Retention** — by age or count; unset means files live forever
32
+ - **Timezone-correct** — `Asia/Kolkata` means IST filenames, on a UTC box
33
+ - **Crash-safe** — restarts resume mid-series byte-exactly;
34
+ gzips interrupted by a kill are completed on boot
35
+ - **Observability** — `<stem>_audit.json` manifest with a uuid-tagged
36
+ event journal for every file's lifecycle
37
+ - **Testable time** — virtual clocks (`addHours`, `addHoursEveryMin`,
38
+ `advanceClock()`) let a day pass per minute, or instantly
39
+ - **Types first** — tuple-typed events, strict mode, dual ESM/CJS build
40
+
12
41
  ## Quick start
13
42
 
14
43
  ### ESM
15
44
 
16
- ```js
45
+ ```ts
17
46
  import { createStream } from "logroller";
18
47
 
19
48
  const log = createStream({
@@ -27,7 +56,10 @@ const log = createStream({
27
56
  });
28
57
 
29
58
  log.write("hello\n");
30
- log.on("rotated", (r) => console.log("rotated:", r));
59
+ log.on("rotated", ({ reason, archive, newFile }) =>
60
+ console.log(`${reason}: ${archive ?? "-"} → ${newFile}`),
61
+ );
62
+ process.on("exit", () => log.end());
31
63
  ```
32
64
 
33
65
  ### CommonJS
@@ -36,112 +68,129 @@ log.on("rotated", (r) => console.log("rotated:", r));
36
68
  const { createStream } = require("logroller");
37
69
  ```
38
70
 
39
- ### Pipe anything into it
71
+ ### pino destination
40
72
 
41
73
  ```js
42
- source.pipe(createStream({ filename: "logs/data-%DATE%.log" }));
74
+ const logger = pino(
75
+ pino.multistream([
76
+ { level: "info", stream: createStream({ filename: "logs/general-%DATE%.log" }) },
77
+ { level: "error", stream: createStream({ filename: "logs/error-%DATE%.log" }) },
78
+ ]),
79
+ );
43
80
  ```
44
81
 
45
- ### With pino
46
-
47
- ```js
48
- import pino from "pino";
49
- import { createStream } from "logroller";
50
-
51
- const stream = createStream({
52
- filename: "logs/general-%DATE%.log",
53
- zippedArchive: true,
54
- maxSize: "20m",
55
- maxFiles: "90d",
56
- tz: "Asia/Kolkata",
57
- });
58
-
59
- const logger = pino(pino.multistream([
60
- { level: "info", stream: pino.transport({ target: "./pretty.js" }) },
61
- { level: "info", stream },
62
- ]));
63
- ```
64
-
65
- ## Options
66
-
67
- | Option | Values | Default |
68
- |---|---|---|
69
- | `filename` | must contain `%DATE%` | required |
70
- | `datePattern` | `YYYY YY MM DD HH mm ss` | `YYYY-MM-DD` |
71
- | `zippedArchive` | boolean | `false` |
72
- | `maxSize` | `20m`, `1k`, `5g`, number of bytes, `0` = off | off |
73
- | `maxFiles` | `"90d"` (age), `500` (count), unset = never delete | off |
74
- | `frequency` | `"daily"`, `"1s"`, `"5m"`, `"2h"`, ms number | `"daily"` |
75
- | `tz` | any IANA zone (`Intl`-based) | `"UTC"` |
76
- | `audit` | write `<stem>_audit.json` manifest | `true` |
77
- | `auditFile` | custom manifest name | `<stem>_audit.json` |
78
- | `unrefTimers` | don't hold the process open | `false` |
79
- | `addHours` | static clock shift (`"-6h"`) — **test only** | off |
80
- | `addHoursEveryMin` | hours added per step (`"24h"`) — **test only** | off |
81
- | `clockStepIntervalMs` | step interval for `addHoursEveryMin` | `60000` |
82
-
83
- ## File naming
82
+ ## What lands on disk
84
83
 
85
84
  ```
86
85
  app-2026-08-27.log ← active
87
- app-2026-08-27.log.gz ← hit maxSize → gzipped
88
- app-2026-08-27.1.log ← next segment, writing…
89
- app-2026-08-27.1.log.gz ← that one sealed too
90
- app-2026-08-28.log ← new day (in tz), fresh series
86
+ app-2026-08-27.log.gz ← sealed (hit 20 MB)
87
+ app-2026-08-27.1.log ← next segment mid-day
88
+ app-2026-08-27.1.log.gz ← sealed too
89
+ app-2026-08-28.log ← fresh day (in your tz), clean series
90
+ app_audit.json ← library-owned manifest
91
91
  ```
92
92
 
93
- ## Restart behaviour
94
-
95
- | Disk at crash | Resumes into |
96
- |---|---|
97
- | `app-D.log` | `app-D.log` (append) |
98
- | `app-D.log.gz` + `app-D.1.log` | `app-D.1.log` (append) |
99
- | `app-D.1.log.gz` only | `app-D.2.log` |
100
- | crossed midnight while down | new day base file on first write |
101
- | killed mid-gzip | boot finishes the interrupted gzip |
93
+ A killed process resumes without gaps or duplicates:
94
+
95
+ | Disk at crash | Restart continues into |
96
+ | ------------------------------- | ------------------------------------- |
97
+ | `app-D.log` | `app-D.log` (append) |
98
+ | `app-D.log.gz` + `app-D.1.log` | `app-D.1.log` |
99
+ | `app-D.1.log.gz` (chain sealed) | `app-D.2.log` |
100
+ | died during a gzip | boot finishes the gzip, then proceeds |
101
+
102
+ ## API
103
+
104
+ ### `createStream(options)` → `RotateFileStream`
105
+
106
+ ### Options
107
+
108
+ | Option | Values | Default |
109
+ | --------------------- | ------------------------------------- | ------------------ |
110
+ | `filename` | **required**, must contain `%DATE%` | — |
111
+ | `datePattern` | `YYYY YY MM DD HH mm ss` | `YYYY-MM-DD` |
112
+ | `zippedArchive` | boolean | `false` |
113
+ | `maxSize` | `20m`, `512k`, `2g`, bytes, `0`=off | off |
114
+ | `maxFiles` | `"90d"` age, `500` count, unset=never | — |
115
+ | `frequency` | `daily`, `30s`, `5m`, `2h`, ms | `daily` |
116
+ | `tz` | any IANA zone | `UTC` |
117
+ | `audit` / `auditFile` | manifest control | `true` / auto-name |
118
+ | `unrefTimers` | detach from event loop | `false` |
119
+ | `addHours` | ⚠️ static clock shift, test only | off |
120
+ | `addHoursEveryMin` | ⚠️ virtual time per tick, test only | off |
121
+ | `clockStepIntervalMs` | tick interval | `60000` |
122
+
123
+ ### Methods
124
+
125
+ | Method | Returns | Purpose |
126
+ | ----------------------- | ---------------- | -------------------------- |
127
+ | `write / end / destroy` | stream semantics | normal `Writable` |
128
+ | `rotateNow()` | `Promise<void>` | force a rotation |
129
+ | `advanceClock()` | `Promise<void>` | apply one virtual tick now |
130
+
131
+ ### Events (typed)
132
+
133
+ | Event | Payload | Meaning |
134
+ | --------- | ------------------------------------- | ---------------------------- |
135
+ | `open` | `file` | segment opened for appending |
136
+ | `rotated` | `{reason, oldFile, archive, newFile}` | segment closed |
137
+ | `archive` | `gzFile` | gzip finished |
138
+ | `deleted` | `file` | retention removed it |
139
+ | `period` | `stamp` | boundary crossed while idle |
140
+ | `clock` | `{offsetMs, …}` | virtual clock engaged |
141
+ | `warn` | `Error` | non-fatal problems |
142
+ | `error` | `Error` | fatal write problems |
143
+
144
+ Errors must be handled — attach a listener on every instance.
145
+
146
+ ## Testing rotation without waiting a week
147
+
148
+ ```ts
149
+ const s = createStream({
150
+ filename: "logs/demo-%DATE%.log",
151
+ zippedArchive: true,
152
+ addHoursEveryMin: "24h", // one day per real minute
153
+ clockStepIntervalMs: 1000, // or faster
154
+ });
155
+ await s.advanceClock(); // jump instantly when useful
156
+ ```
102
157
 
103
- ## Events
158
+ ## FAQ
104
159
 
105
- | Event | Payload |
106
- |---|---|
107
- | `open` | `(file: string)` |
108
- | `rotated` | `({ reason, oldFile, archive, newFile })` |
109
- | `archive` | `(gzFile: string)` |
110
- | `deleted` | `(file: string)` — retention |
111
- | `period` | `(stamp: string)` — new period, nothing to archive |
112
- | `clock` | `({ addHours, addHoursEveryMin, offsetMs })` — test clock active |
113
- | `warn` | `(err: Error)` — non-fatal (gzip failure, concurrent writer, …) |
160
+ **Why is `%DATE%` mandatory?**
161
+ It is the anchor that decouples _rotation scheduling_ from _filenames_.
162
+ Coarser patterns than your frequency collapse distinct segments onto one
163
+ name — match tokens to cadence (`HH` for hourly, etc.).
114
164
 
115
- Always attach an `error` handler — this is a real Writable stream.
165
+ **Can two processes share one directory?**
166
+ Different prefixes/families: yes. The _same_ family: no — the audit
167
+ manifest detects a live second writer (`writer.heartbeat`) and warns loudly.
116
168
 
117
- ## Audit manifest
169
+ **Is the audit JSON required?**
170
+ No — `audit: false` reduces behaviour to pure disk-scan recovery. Recommended
171
+ to leave on: it repairs interrupted gzips and demotes zombies automatically.
118
172
 
119
- Each stream family keeps a `*_audit.json` beside the logs (library-owned —
120
- treat it as read-only). It records every file's lifecycle with a capped,
121
- uuid-tagged event journal, restores the series index across restarts, repairs
122
- gzip interrupted by a crash, and demotes records left behind by dead
123
- processes. Writes are atomic (tmp + rename); a corrupt/foreign file is
124
- quarantined as `*.corrupt` and logging continues via disk scan.
173
+ **Windows / macOS / Linux?**
174
+ Pure Node APIs; path handling via `node:path`. All three supported
175
+ (Node `>=16.14`).
125
176
 
126
- ## Testing rotation without waiting
177
+ ## Compare
127
178
 
128
- ```js
129
- const s = createStream({
130
- filename: "logs/test-%DATE%.log",
131
- zippedArchive: true,
132
- addHoursEveryMin: "24h", // a day passes every minute
133
- clockStepIntervalMs: 1000, // optional: step every second instead
134
- tz: "UTC",
135
- });
136
- await s.advanceClock(); // or trigger one step manually
137
- ```
179
+ | | logroller | file-stream-rotator | rotating-file-stream | winston-daily-rotate-file |
180
+ | ------------------------- | ----------------- | ------------------- | -------------------- | ------------------------- |
181
+ | Dependencies | **0** | 4+ | 2+ | winston-bound |
182
+ | Crash-safe resume | ✅ audit-assisted | partial | partial | ✖ |
183
+ | Interrupted-gzip repair | ✅ | ✖ | ✖ | ✖ |
184
+ | Event journal | ✅ | ✖ | ✖ | ✖ |
185
+ | TZ-correct daily rollover | ✅ | partial | ✅ | ✅ |
186
+ | Virtual test clock | ✅ | ✖ | ✖ | ✖ |
187
+ | Winston required | no | no | no | yes |
138
188
 
139
- ## Retention
189
+ ## Contributing
140
190
 
141
- - `maxFiles: "90d"` — delete family files older than 90 days (mtime)
142
- - `maxFiles: 500` — keep newest 500 archives
143
- - unset — never auto-delete; remove manually
191
+ PRs welcome. `npm run verify` gates every change (typecheck + tests).
192
+ Please file issues before large refactors.
144
193
 
145
194
  ## License
146
195
 
147
- MIT
196
+ [MIT](./LICENSE)
package/dist/fsio.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import fs from "node:fs";
2
+ /** `stat().size` guarded to 0; string paths only. */
3
+ export declare const statSizeSafe: (p: string | null | undefined) => number;
4
+ export declare const existsSafe: (p: string) => boolean;
5
+ /**
6
+ * Compress `src` → `dst`, deleting `src` ONLY on verified success.
7
+ * On failure removes the partial `dst` and reports via callback —
8
+ * logs degrade to uncompressed rather than losing data.
9
+ */
10
+ export declare function gzipMove(src: string, dst: string, onWarn?: (err: Error) => void): Promise<boolean>;
11
+ export declare const endStream: (ws: fs.WriteStream | null) => Promise<void>;