logroller 2.0.2 → 2.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/README.md +232 -46
- package/dist/fsio.d.ts +3 -3
- package/dist/index.cjs +2 -1080
- package/dist/index.d.ts +4 -2
- package/dist/index.esm.js +2 -1077
- package/dist/parsers.d.ts +2 -0
- package/dist/rotate-file-stream.d.ts +36 -4
- package/dist/shutdown.d.ts +32 -0
- package/dist/types.d.ts +96 -66
- package/package.json +4 -4
- package/CHANGELOG.md +0 -133
package/dist/parsers.d.ts
CHANGED
|
@@ -12,5 +12,7 @@ export type ParsedFrequency = {
|
|
|
12
12
|
export declare function parseSize(v: RotateFileStreamOptions["maxSize"]): number;
|
|
13
13
|
export declare function parseMaxFiles(v: RotateFileStreamOptions["maxFiles"]): ParsedMaxFiles | null;
|
|
14
14
|
export declare function parseFrequency(v: RotateFileStreamOptions["frequency"]): ParsedFrequency;
|
|
15
|
+
/** Static virtual-clock shift. Accepts hours by default; h/hours/m/s/ms. */
|
|
15
16
|
export declare function parseClockOffset(v: RotateFileStreamOptions["addHours"]): number;
|
|
17
|
+
/** Per-tick virtual-clock step. Accepts hours by default; h/hours/m/s/ms. */
|
|
16
18
|
export declare function parseClockStep(v: RotateFileStreamOptions["addHoursEveryMin"]): number;
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { Writable } from "node:stream";
|
|
2
2
|
import { type ParsedFrequency, type ParsedMaxFiles } from "./parsers.js";
|
|
3
|
-
import type { RotateFileStreamEventMap, RotateFileStreamOptions } from "./types.js";
|
|
3
|
+
import type { RotateFileStreamEventMap, RotateFileStreamOptions, SegmentInfo, StreamStats } from "./types.js";
|
|
4
4
|
/**
|
|
5
5
|
* A `Writable` that appends to dated log files, rolling segments on time
|
|
6
|
-
* and/or size, optionally gzipping sealed segments, enforcing retention
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* and/or size, optionally gzipping sealed segments, enforcing retention
|
|
7
|
+
* (count, age, and total-byte budget), maintaining a stable "current"
|
|
8
|
+
* symlink for log collectors, running an awaited seal hook (upload
|
|
9
|
+
* pipeline), and resuming an unbroken series across restarts via disk
|
|
10
|
+
* scan + audit manifest.
|
|
9
11
|
*
|
|
10
12
|
* @example
|
|
11
13
|
* ```ts
|
|
12
14
|
* const log = createStream({
|
|
13
15
|
* filename: "logs/app-%DATE%.log",
|
|
14
16
|
* maxSize: "20m",
|
|
17
|
+
* maxTotalSize: "5g",
|
|
15
18
|
* maxFiles: "90d",
|
|
16
19
|
* zippedArchive: true,
|
|
20
|
+
* symlink: true,
|
|
21
|
+
* onSeal: async ({ gz }) => uploadToS3(gz),
|
|
17
22
|
* });
|
|
18
23
|
* log.write("hello\n"); // or: readable.pipe(log)
|
|
19
24
|
* ```
|
|
@@ -28,6 +33,7 @@ export declare class RotateFileStream extends Writable {
|
|
|
28
33
|
readonly maxFiles: Readonly<ParsedMaxFiles> | null;
|
|
29
34
|
readonly freq: ParsedFrequency;
|
|
30
35
|
readonly unrefTimers: boolean;
|
|
36
|
+
readonly maxTotalSize: number;
|
|
31
37
|
private readonly _tL;
|
|
32
38
|
private readonly _tMid;
|
|
33
39
|
private readonly _tExt;
|
|
@@ -36,6 +42,10 @@ export declare class RotateFileStream extends Writable {
|
|
|
36
42
|
private readonly _auditEnabled;
|
|
37
43
|
private readonly _auditPath;
|
|
38
44
|
private readonly _bootId;
|
|
45
|
+
private readonly _onSeal?;
|
|
46
|
+
private readonly _sealHookTimeoutMs;
|
|
47
|
+
private readonly _compressionLevel;
|
|
48
|
+
private _symlinkPath;
|
|
39
49
|
private _offsetMs;
|
|
40
50
|
private _stepMs;
|
|
41
51
|
private _bumpEvery;
|
|
@@ -69,6 +79,15 @@ export declare class RotateFileStream extends Writable {
|
|
|
69
79
|
* archive flow as a real midnight roll.
|
|
70
80
|
*/
|
|
71
81
|
advanceClock(): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Every family file on disk, oldest stamp first.
|
|
84
|
+
* Disk is truth: untracked files are reported with state "untracked".
|
|
85
|
+
*/
|
|
86
|
+
listSegments(): SegmentInfo[];
|
|
87
|
+
/**
|
|
88
|
+
* Health-check snapshot for dashboards and support triage.
|
|
89
|
+
*/
|
|
90
|
+
stats(): StreamStats;
|
|
72
91
|
_write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
|
|
73
92
|
_final(callback: (error?: Error | null) => void): void;
|
|
74
93
|
destroy(...args: Parameters<Writable["destroy"]>): this;
|
|
@@ -89,6 +108,19 @@ export declare class RotateFileStream extends Writable {
|
|
|
89
108
|
* The audit pointer (same stamp only) may only RAISE the result.
|
|
90
109
|
*/
|
|
91
110
|
private _recoverIndexFor;
|
|
111
|
+
/**
|
|
112
|
+
* Atomically flip the "current" symlink to the active segment.
|
|
113
|
+
* tmp + rename so followers (`tail -f`, Filebeat) never observe a gap.
|
|
114
|
+
* Relative target keeps the link portable. Best-effort: filesystems
|
|
115
|
+
* denying symlinks warn once and disable the feature permanently.
|
|
116
|
+
*/
|
|
117
|
+
private _updateSymlink;
|
|
118
|
+
/**
|
|
119
|
+
* Await the user's onSeal hook with a hard timeout. Result or
|
|
120
|
+
* failure degrades to `warn` — a wedged upload must never stall rotation.
|
|
121
|
+
* Called BEFORE retention so uploads finish before deletion.
|
|
122
|
+
*/
|
|
123
|
+
private _runSealHook;
|
|
92
124
|
private _ensureOpen;
|
|
93
125
|
private _rotate;
|
|
94
126
|
private _hasContent;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RotateFileStream } from "./rotate-file-stream.js";
|
|
2
|
+
export interface ShutdownOptions {
|
|
3
|
+
/** Signals to listen on. @default ["SIGINT", "SIGTERM"] */
|
|
4
|
+
signals?: NodeJS.Signals[];
|
|
5
|
+
/** Hard-exit deadline (ms) if a stream refuses to end.
|
|
6
|
+
* @default 5000 */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
/** Process exit code. @default 0 */
|
|
9
|
+
code?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Register streams for graceful shutdown: on SIGINT/SIGTERM every
|
|
13
|
+
* registered stream is ended — flushing buffered writes and finishing the
|
|
14
|
+
* audit manifest — before the process exits, with a hard deadline.
|
|
15
|
+
*
|
|
16
|
+
* Safe to call multiple times: process listeners install once, streams
|
|
17
|
+
* accumulate in the registry.
|
|
18
|
+
*
|
|
19
|
+
* Returns `flush()`, the same graceful end WITHOUT exiting — for tests
|
|
20
|
+
* and custom signal handling.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const flush = installShutdown(logStream);
|
|
24
|
+
* process.on("exit", () => { void flush(); });
|
|
25
|
+
*/
|
|
26
|
+
export declare function installShutdown(streams: RotateFileStream | RotateFileStream[], opts?: ShutdownOptions): () => Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* End every registered stream (flush buffered writes) with a deadline.
|
|
29
|
+
* Does NOT exit the process. Double-ending an already-ended stream is
|
|
30
|
+
* harmless — the error lands in the ignored callback.
|
|
31
|
+
*/
|
|
32
|
+
export declare function flushAll(timeoutMs?: number): Promise<void>;
|
package/dist/types.d.ts
CHANGED
|
@@ -8,122 +8,152 @@ export type RotationReason = "time" | "size" | "manual";
|
|
|
8
8
|
export interface RotateFileStreamOptions {
|
|
9
9
|
/**
|
|
10
10
|
* Destination filename pattern. REQUIRED and must contain the `%DATE%`
|
|
11
|
-
* token
|
|
12
|
-
*
|
|
11
|
+
* token.
|
|
13
12
|
* @example "logs/app-%DATE%.log"
|
|
14
13
|
*/
|
|
15
14
|
filename: string;
|
|
16
|
-
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* Supported tokens: `YYYY YY MM DD HH mm ss`.
|
|
20
|
-
* Use tokens coarser-or-equal to your rotation frequency — hourly
|
|
21
|
-
* rotation with a day-only pattern collapses multiple files onto one name.
|
|
22
|
-
*
|
|
23
|
-
* @default "YYYY-MM-DD"
|
|
24
|
-
*/
|
|
15
|
+
/** Tokens: `YYYY YY MM DD HH mm ss`. Must be coarse-or-equal to your
|
|
16
|
+
* rotation frequency, or segments collapse onto one name.
|
|
17
|
+
* @default "YYYY-MM-DD" */
|
|
25
18
|
datePattern?: string;
|
|
26
|
-
/** Gzip closed segments (`name.log` → `name.log.gz`).
|
|
27
|
-
|
|
19
|
+
/** Gzip closed segments (`name.log` → `name.log.gz`).
|
|
20
|
+
* @default false */
|
|
28
21
|
zippedArchive?: boolean;
|
|
29
22
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
23
|
+
* gzip level 0–9. 0 = stored (fastest, no ratio), 9 = best ratio,
|
|
24
|
+
* 6 = default library default. Ignored when `zippedArchive` is false.
|
|
25
|
+
* @default 6
|
|
26
|
+
*/
|
|
27
|
+
compressionLevel?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Roll when the next write would exceed this size per segment.
|
|
30
|
+
* `"20m"`, `"512k"`, `"2g"`, or raw bytes. `0` disables.
|
|
33
31
|
* @default 0 (unlimited)
|
|
34
32
|
*/
|
|
35
33
|
maxSize?: number | string;
|
|
36
34
|
/**
|
|
37
|
-
* Retention
|
|
38
|
-
* - `"90d"` — delete archived files older than 90 days (
|
|
35
|
+
* Retention by age or count:
|
|
36
|
+
* - `"90d"` — delete archived files older than 90 days (mtime)
|
|
39
37
|
* - `500` — keep at most the newest 500 archived files
|
|
40
|
-
* Unset means
|
|
38
|
+
* Unset means never auto-delete.
|
|
41
39
|
*/
|
|
42
40
|
maxFiles?: number | string;
|
|
43
41
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* Retention by total byte budget across archived files (the active
|
|
43
|
+
* segment is never counted). When archives exceed the budget, the
|
|
44
|
+
* OLDEST are deleted until under budget. Composes with `maxFiles`
|
|
45
|
+
* (either policy may delete). `"5g"`, `"500m"`, or raw bytes.
|
|
46
|
+
* Unset/0 = no byte budget.
|
|
47
47
|
*/
|
|
48
|
+
maxTotalSize?: number | string;
|
|
49
|
+
/** IANA timezone for stamps and the daily boundary.
|
|
50
|
+
* @default "UTC" */
|
|
48
51
|
tz?: string;
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* Interval boundaries align to epoch multiples (hh:00:00 for `"1h"`).
|
|
53
|
-
* @default "daily"
|
|
54
|
-
*/
|
|
52
|
+
/** `"daily"` (local midnight in `tz`) or interval `"30s"`/`"5m"`/`"2h"`
|
|
53
|
+
* /ms. Interval boundaries align to epoch multiples.
|
|
54
|
+
* @default "daily" */
|
|
55
55
|
frequency?: number | string;
|
|
56
56
|
/**
|
|
57
|
-
* TEST ONLY. Static shift of the internal clock
|
|
58
|
-
* `"
|
|
59
|
-
* Affects naming, timers, and retention cutoffs — never production data.
|
|
57
|
+
* TEST ONLY. Static shift of the internal clock. `"-6h"`, `"24h"`,
|
|
58
|
+
* plain/fractional number, or `"30m"`/`"90s"`.
|
|
60
59
|
*/
|
|
61
60
|
addHours?: number | string;
|
|
62
61
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* simulating passing days: crossing into a new period archives the old
|
|
66
|
-
* segment exactly like a real midnight roll.
|
|
62
|
+
* TEST ONLY. Virtual time added per tick (see `clockStepIntervalMs`).
|
|
63
|
+
* Negative values exercise the backward-clock guard (no rotations).
|
|
67
64
|
*/
|
|
68
65
|
addHoursEveryMin?: number | string;
|
|
69
|
-
/**
|
|
70
|
-
*
|
|
71
|
-
* Lower it to fast-forward faster in integration tests.
|
|
72
|
-
* @default 60_000
|
|
73
|
-
*/
|
|
66
|
+
/** Real interval between virtual-clock ticks.
|
|
67
|
+
* @default 60_000 */
|
|
74
68
|
clockStepIntervalMs?: number;
|
|
75
|
-
/** Detach rotation timers from the event loop (`timer.unref()`). */
|
|
76
|
-
/** @default false */
|
|
77
|
-
unrefTimers?: boolean;
|
|
78
69
|
/**
|
|
79
|
-
* Maintain
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
70
|
+
* Maintain a stable "current" pointer for log collectors
|
|
71
|
+
* (`tail -f`, Filebeat, promtail). `true` derives the name from the
|
|
72
|
+
* prefix (`"app-"` → `app-current.log`); a string names it exactly.
|
|
73
|
+
* The link is updated atomically each time a segment opens. Best-effort:
|
|
74
|
+
* on filesystems denying symlinks (Windows without Developer Mode) it
|
|
75
|
+
* warns once and disables itself.
|
|
76
|
+
* @default false */
|
|
77
|
+
symlink?: boolean | string;
|
|
78
|
+
/**
|
|
79
|
+
* Awaited after a segment is sealed/archived and BEFORE retention can
|
|
80
|
+
* delete it — the extension point for uploading to S3/GCS. Errors and
|
|
81
|
+
* timeouts degrade to a `warn`; they never break rotation.
|
|
82
|
+
* @default undefined */
|
|
83
|
+
onSeal?: (info: SealInfo) => Promise<void> | void;
|
|
84
|
+
/** Hard deadline for one `onSeal` invocation.
|
|
85
|
+
* @default 30_000 */
|
|
86
|
+
sealHookTimeoutMs?: number;
|
|
87
|
+
/** Detach rotation timers from the event loop.
|
|
88
|
+
* @default false */
|
|
89
|
+
unrefTimers?: boolean;
|
|
90
|
+
/** Maintain `<stem>_audit.json` (library-owned; treat as read-only).
|
|
91
|
+
* @default true */
|
|
84
92
|
audit?: boolean;
|
|
85
|
-
/** Override the audit manifest filename.
|
|
93
|
+
/** Override the audit manifest filename. */
|
|
86
94
|
auditFile?: string;
|
|
87
95
|
/** Passed through to the underlying `Writable`. */
|
|
88
96
|
highWaterMark?: number;
|
|
89
97
|
}
|
|
98
|
+
/** Payload delivered to `onSeal` when a segment completes its lifecycle. */
|
|
99
|
+
export interface SealInfo {
|
|
100
|
+
/** Closed segment path. May already be unlinked when `gz` exists. */
|
|
101
|
+
raw: string;
|
|
102
|
+
/** Compressed artifact; `null` when compression is off or failed. */
|
|
103
|
+
gz: string | null;
|
|
104
|
+
}
|
|
105
|
+
/** One file of the stream family, as reported by `listSegments()`. */
|
|
106
|
+
export interface SegmentInfo {
|
|
107
|
+
file: string;
|
|
108
|
+
/** Date stamp embedded in the filename. */
|
|
109
|
+
stamp: string;
|
|
110
|
+
/** Segment index within the stamp (0 = base file). */
|
|
111
|
+
index: number;
|
|
112
|
+
gzipped: boolean;
|
|
113
|
+
bytes: number;
|
|
114
|
+
/** Audit state, or `"untracked"` for files unknown to the manifest. */
|
|
115
|
+
state: string;
|
|
116
|
+
openedAt?: string;
|
|
117
|
+
archivedAt?: string;
|
|
118
|
+
}
|
|
119
|
+
/** Snapshot for dashboards/health checks — see `stats()`. */
|
|
120
|
+
export interface StreamStats {
|
|
121
|
+
dir: string;
|
|
122
|
+
currentStamp: string;
|
|
123
|
+
activeFile: string | null;
|
|
124
|
+
activeIndex: number;
|
|
125
|
+
/** All family files on disk (active included). */
|
|
126
|
+
segments: number;
|
|
127
|
+
/** Of those, how many are compressed archives. */
|
|
128
|
+
archived: number;
|
|
129
|
+
totalBytes: number;
|
|
130
|
+
/** Active virtual-clock shift (non-zero only under test knobs). */
|
|
131
|
+
clockOffsetMs: number;
|
|
132
|
+
/** Active symlink path, or `null`. */
|
|
133
|
+
symlink: string | null;
|
|
134
|
+
}
|
|
90
135
|
/** Payload for the `rotated` event. */
|
|
91
136
|
export interface RotatedInfo {
|
|
92
137
|
reason: RotationReason;
|
|
93
|
-
/** Segment that was closed; `null` when nothing was open. */
|
|
94
138
|
oldFile: string | null;
|
|
95
|
-
/** Gzip artifact created for it; `null` when unzipped, empty, or failed. */
|
|
96
139
|
archive: string | null;
|
|
97
|
-
/** Segment subsequent writes go to (opened lazily). */
|
|
98
140
|
newFile: string;
|
|
99
141
|
}
|
|
100
|
-
/** Payload for the `clock` event
|
|
142
|
+
/** Payload for the `clock` event. */
|
|
101
143
|
export interface ClockInfo {
|
|
102
144
|
addHours?: number | string;
|
|
103
145
|
addHoursEveryMin?: number | string;
|
|
104
146
|
offsetMs: number;
|
|
105
147
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Tuple-style event map used to type `on` / `once` / `emit`.
|
|
108
|
-
* Keys mirror Node core lifecycle events where relevant.
|
|
109
|
-
*/
|
|
148
|
+
/** Tuple-style event map typing `on` / `once` / `emit`. */
|
|
110
149
|
export interface RotateFileStreamEventMap {
|
|
111
|
-
/** A segment file was opened for appending. */
|
|
112
150
|
open: [file: string];
|
|
113
|
-
/** A segment completed its lifecycle; see {@linkcode RotatedInfo}. */
|
|
114
151
|
rotated: [info: RotatedInfo];
|
|
115
|
-
/** A gzip archive finished successfully. */
|
|
116
152
|
archive: [gzFile: string];
|
|
117
|
-
/** Retention removed a file. */
|
|
118
153
|
deleted: [file: string];
|
|
119
|
-
/** Period changed with nothing to archive (idle boundary slide). */
|
|
120
154
|
period: [stamp: string];
|
|
121
|
-
/** Virtual clock engaged (test knobs). */
|
|
122
155
|
clock: [info: ClockInfo];
|
|
123
|
-
/** Non-fatal problem: gzip failure, divergent state, stray writer… */
|
|
124
156
|
warn: [err: Error];
|
|
125
|
-
/** Fatal problem mid-write; mirrors `Writable#close` semantics. */
|
|
126
157
|
error: [err: Error];
|
|
127
|
-
/** Stream fully closed. */
|
|
128
158
|
close: [];
|
|
129
159
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "logroller",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Zero-dependency rotating file stream for Node.js — daily/interval/size rotation, gzip archives, retention, timezone-aware, crash-safe resume",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Vinau",
|
|
@@ -48,8 +48,7 @@
|
|
|
48
48
|
"files": [
|
|
49
49
|
"dist",
|
|
50
50
|
"README.md",
|
|
51
|
-
"LICENSE"
|
|
52
|
-
"CHANGELOG.md"
|
|
51
|
+
"LICENSE"
|
|
53
52
|
],
|
|
54
53
|
"engines": {
|
|
55
54
|
"node": ">=16.14"
|
|
@@ -59,12 +58,13 @@
|
|
|
59
58
|
"build": "tsc -p tsconfig.json && rollup -c",
|
|
60
59
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
61
60
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.js\"",
|
|
62
|
-
"test": "node --test --experimental-strip-types test/rotate.test.ts",
|
|
61
|
+
"test": "npm run build && node --test --experimental-strip-types test/rotate.test.ts test/features.test.ts",
|
|
63
62
|
"verify": "npm run typecheck && npm test",
|
|
64
63
|
"prepublishOnly": "npm run verify",
|
|
65
64
|
"release": "npm run verify && npm pack --dry-run"
|
|
66
65
|
},
|
|
67
66
|
"devDependencies": {
|
|
67
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
68
68
|
"@rollup/plugin-typescript": "^11.1.6",
|
|
69
69
|
"@types/node": "^26.4.0",
|
|
70
70
|
"prettier": "^3.4.2",
|
package/CHANGELOG.md
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
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.2] — 2026-08-28
|
|
9
|
-
|
|
10
|
-
### Fixed
|
|
11
|
-
|
|
12
|
-
- **Corrupt audit manifest quarantine**: an unparseable `<stem>_audit.json`
|
|
13
|
-
(partial write, bad edit) is now renamed to `.corrupt` and preserved,
|
|
14
|
-
matching existing behaviour for version/family mismatches. Previously it
|
|
15
|
-
was silently overwritten by the first manifest save, destroying the
|
|
16
|
-
event journal.
|
|
17
|
-
- **Backward-clock safety**: a clock moving backward (NTP correction,
|
|
18
|
-
manual change) no longer reopens a past log family or forks the segment
|
|
19
|
-
series. The stream keeps the current segment, warns once, and resumes
|
|
20
|
-
normal rotation when the clock catches up.
|
|
21
|
-
- **Timer anti-spin**: with the rotation boundary in the past (backward
|
|
22
|
-
clock), the timer now polls at 1 s instead of firing every 100 ms,
|
|
23
|
-
which previously could keep processes from exiting cleanly.
|
|
24
|
-
|
|
25
|
-
### Added
|
|
26
|
-
|
|
27
|
-
- **Construction-time pattern warning**: warns when `frequency` is finer
|
|
28
|
-
than the `datePattern` tokens allow (`daily`→`DD`, sub-daily→`HH`,
|
|
29
|
-
sub-minute→`mm`), preventing silent filename collisions where many
|
|
30
|
-
rotations overwrite a single file.
|
|
31
|
-
|
|
32
|
-
### Tests
|
|
33
|
-
|
|
34
|
-
- Suite expanded to 23 tests: interrupted-gzip repair, manifest divergence
|
|
35
|
-
healing (`archive-regressed`, `vanished`), manifest quarantine,
|
|
36
|
-
concurrent-writer detection, IST-midnight boundary, idle `period` slide,
|
|
37
|
-
`rotateNow()`, over-max resume, backward-clock guard, and coarse-pattern
|
|
38
|
-
warning.
|
|
39
|
-
|
|
40
|
-
## [2.0.1] — 2026-08-27
|
|
41
|
-
|
|
42
|
-
### Fixed
|
|
43
|
-
|
|
44
|
-
- Reactivated segments no longer inherit stale lifecycle timestamps
|
|
45
|
-
(`abandonedAt` could predate `openedAt`).
|
|
46
|
-
- Raw tails of already-passed periods are now gzipped at boot
|
|
47
|
-
(crash-near-midnight no longer leaves yesterday's file uncompressed).
|
|
48
|
-
- Rotation adopts a `.gz` produced by a (warned-about) concurrent writer
|
|
49
|
-
instead of recording a false `drop-empty` that orphaned the archive.
|
|
50
|
-
|
|
51
|
-
## [2.0.0] — 2026-08-28
|
|
52
|
-
|
|
53
|
-
Architecture release: the library moved from a single-file JavaScript SDK to a
|
|
54
|
-
modular **TypeScript** codebase with strict typings, first-class DX tooling,
|
|
55
|
-
and CI-gated releases. The public runtime API (`createStream`,
|
|
56
|
-
`RotateFileStream`, all options and events) remains compatible with 1.x;
|
|
57
|
-
TypeScript consumers gain full type coverage.
|
|
58
|
-
|
|
59
|
-
> ⚠️ Consumers on 1.x should re-run their smoke/integration tests after
|
|
60
|
-
> upgrading — mostly due to the deterministic boot-order fix below.
|
|
61
|
-
|
|
62
|
-
### Added
|
|
63
|
-
|
|
64
|
-
- **Full TypeScript source with shipped declarations** (`dist/index.d.ts`);
|
|
65
|
-
strict-mode compile (`noImplicitOverride`, `noUnusedLocals`,
|
|
66
|
-
`verbatimModuleSyntax`, …).
|
|
67
|
-
- **Tuple-typed events** via `RotateFileStreamEventMap` — `on("rotated", …)`
|
|
68
|
-
autocompletes and payload types are checked (`RotatedInfo`, `ClockInfo`),
|
|
69
|
-
while Node-core fallback overloads keep `.pipe()` structurally valid.
|
|
70
|
-
- `advanceClock()` — apply one virtual-clock tick immediately (previously the
|
|
71
|
-
step timer fired only every minute); makes day-crossing tests fully
|
|
72
|
-
deterministic.
|
|
73
|
-
- `clockStepIntervalMs` option — control how often `addHoursEveryMin` ticks.
|
|
74
|
-
- Modular source layout (`src/types.ts`, `parsers.ts`, `time.ts`, `fsio.ts`,
|
|
75
|
-
`rotate-file-stream.ts`) with `.js` extension imports so emitted
|
|
76
|
-
declarations resolve under `nodenext` consumers.
|
|
77
|
-
- Developer toolchain: Prettier, `npm run verify` gate (typecheck + build + tests).
|
|
78
|
-
- GitHub Actions: CI matrix on Node 18/20/22, release-triggered publish with
|
|
79
|
-
npm provenance (`id-token: write`, `--provenance`).
|
|
80
|
-
- Expanded README: comparison table, FAQ, restart-resume matrix, pino recipe.
|
|
81
|
-
|
|
82
|
-
### Fixed
|
|
83
|
-
|
|
84
|
-
- **Deterministic boot sequencing**: housekeeping (zombie demotion, disk
|
|
85
|
-
reconciliation, interrupted-gzip repair) is chained as job #1 of the
|
|
86
|
-
serial operation queue in the constructor instead of `process.nextTick`,
|
|
87
|
-
so a rapid first `write()` can no longer race a boot-time reopen or skip
|
|
88
|
-
zombie cleanup.
|
|
89
|
-
- Retention sweep skips `<audit>.corrupt` artifacts alongside `.tmp`.
|
|
90
|
-
|
|
91
|
-
### Changed
|
|
92
|
-
|
|
93
|
-
- Internals restructured behind the same public exports; barrels re-export
|
|
94
|
-
types explicitly (`import type { RotatedInfo } from "logroller"`).
|
|
95
|
-
- Stricter package hygiene metadata: `sideEffects: false`, explicit
|
|
96
|
-
`./package.json` export, `files` whitelist (`dist`, docs, license,
|
|
97
|
-
changelog).
|
|
98
|
-
- Test suite runs against the built bundle and covers: base writing +
|
|
99
|
-
manifest creation, size rotation/gzip/indexing, restart resume,
|
|
100
|
-
`addHours` naming, virtual midnight crossing, count-retention ordering
|
|
101
|
-
(mtime-deterministic), zombie-record lifecycle (`abandon → active →
|
|
102
|
-
closed`), and CJS loadability through the exports map.
|
|
103
|
-
|
|
104
|
-
## [1.0.0] — 2026-08-26
|
|
105
|
-
|
|
106
|
-
Initial release. Single-file zero-dependency JavaScript SDK
|
|
107
|
-
(`file-rotation-sdk.js`) providing a Writable stream with scheduled file
|
|
108
|
-
rotation.
|
|
109
|
-
|
|
110
|
-
### Added
|
|
111
|
-
|
|
112
|
-
- Time-based rotation: `frequency: "daily"` (local midnight in any IANA
|
|
113
|
-
timezone via `Intl`) or epoch-aligned intervals (`"30s"`, `"5m"`, `"2h"`).
|
|
114
|
-
- Size-based rotation: `maxSize` (`"20m"` / `"512k"` / raw bytes) producing
|
|
115
|
-
numbered segments — `app-D.log`, `app-D.1.log`, … — inserted between the
|
|
116
|
-
date stamp and the extension.
|
|
117
|
-
- Optional gzip archiving of sealed segments (`zippedArchive`), atomic tmp+
|
|
118
|
-
rename writes, source deleted only after verified compression success.
|
|
119
|
-
- Retention via `maxFiles`: age policy (`"90d"`) or count policy (`500`);
|
|
120
|
-
unset means files are never auto-deleted.
|
|
121
|
-
- Crash-safe restart resume from disk scan: raw tails are appended to,
|
|
122
|
-
sealed chains continue at the next free index; lazy period-roll catches
|
|
123
|
-
boundaries missed while suspended or shut down.
|
|
124
|
-
- Audit manifest (`<stem>_audit.json`): lifecycle records per file, capped
|
|
125
|
-
uuid-tagged event journal, `gzipPending` intent markers enabling automatic
|
|
126
|
-
repair of gzips interrupted by a crash, zombie active-record demotion,
|
|
127
|
-
disk-divergence healing (`archive-regressed`, `vanished`),
|
|
128
|
-
concurrent-writer detection via pid/bootId heartbeat.
|
|
129
|
-
- Virtual test clock: static shift (`addHours`) and per-minute stepping
|
|
130
|
-
(`addHoursEveryMin`).
|
|
131
|
-
- Dual-format distribution (ESM + CJS) bundled with Rollup; named exports
|
|
132
|
-
`createStream` and `RotateFileStream`; factory options mirroring common
|
|
133
|
-
rotator APIs; streams pipe-ready for pino multistreams.
|