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.
@@ -0,0 +1,16 @@
1
+ import type { RotateFileStreamOptions } from "./types.js";
2
+ export interface ParsedMaxFiles {
3
+ days?: number;
4
+ count?: number;
5
+ }
6
+ export type ParsedFrequency = {
7
+ readonly type: "daily";
8
+ } | {
9
+ readonly type: "interval";
10
+ ms: number;
11
+ };
12
+ export declare function parseSize(v: RotateFileStreamOptions["maxSize"]): number;
13
+ export declare function parseMaxFiles(v: RotateFileStreamOptions["maxFiles"]): ParsedMaxFiles | null;
14
+ export declare function parseFrequency(v: RotateFileStreamOptions["frequency"]): ParsedFrequency;
15
+ export declare function parseClockOffset(v: RotateFileStreamOptions["addHours"]): number;
16
+ export declare function parseClockStep(v: RotateFileStreamOptions["addHoursEveryMin"]): number;
@@ -0,0 +1,121 @@
1
+ import { Writable } from "node:stream";
2
+ import { type ParsedFrequency, type ParsedMaxFiles } from "./parsers.js";
3
+ import type { RotateFileStreamEventMap, RotateFileStreamOptions } from "./types";
4
+ /**
5
+ * A `Writable` that appends to dated log files, rolling segments on time
6
+ * and/or size, optionally gzipping sealed segments, enforcing retention,
7
+ * and resuming an unbroken series across restarts via disk scan + audit
8
+ * manifest.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const log = createStream({
13
+ * filename: "logs/app-%DATE%.log",
14
+ * maxSize: "20m",
15
+ * maxFiles: "90d",
16
+ * zippedArchive: true,
17
+ * });
18
+ * log.write("hello\n"); // or: readable.pipe(log)
19
+ * ```
20
+ */
21
+ export declare class RotateFileStream extends Writable {
22
+ readonly tz: string;
23
+ readonly template: string;
24
+ readonly dir: string;
25
+ readonly datePattern: string;
26
+ readonly zippedArchive: boolean;
27
+ readonly maxSize: number;
28
+ readonly maxFiles: Readonly<ParsedMaxFiles> | null;
29
+ readonly freq: ParsedFrequency;
30
+ readonly unrefTimers: boolean;
31
+ private readonly _tL;
32
+ private readonly _tMid;
33
+ private readonly _tExt;
34
+ private readonly _rxFor;
35
+ private readonly _rxActive;
36
+ private readonly _auditEnabled;
37
+ private readonly _auditPath;
38
+ private readonly _bootId;
39
+ private _offsetMs;
40
+ private readonly _stepMs;
41
+ private readonly _bumpEvery;
42
+ private _curStamp;
43
+ private _index;
44
+ private _ws;
45
+ private _path;
46
+ private _bytes;
47
+ private _timer;
48
+ private _bumpTimer;
49
+ private _ended;
50
+ private _tail;
51
+ private _audit;
52
+ constructor(opts?: Partial<RotateFileStreamOptions>);
53
+ on<E extends keyof RotateFileStreamEventMap>(event: E, listener: (...args: RotateFileStreamEventMap[E]) => void): this;
54
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
55
+ once<E extends keyof RotateFileStreamEventMap>(event: E, listener: (...args: RotateFileStreamEventMap[E]) => void): this;
56
+ once(event: string | symbol, listener: (...args: any[]) => void): this;
57
+ emit<E extends keyof RotateFileStreamEventMap>(event: E, ...args: RotateFileStreamEventMap[E]): boolean;
58
+ emit(event: string | symbol, ...args: unknown[]): boolean;
59
+ /** Absolute path of the segment currently written; `null` until first write. */
60
+ get filename(): string | null;
61
+ /** Index of the active segment within the current period. */
62
+ get currentIndex(): number;
63
+ /**
64
+ * Force an immediate rotation and resolve when the next slot is staged.
65
+ */
66
+ rotateNow(): Promise<void>;
67
+ /**
68
+ * Apply ONE virtual-clock tick immediately (bypasses the interval timer
69
+ * for deterministic tests). Crossing into a new period triggers the same
70
+ * archive flow as a real midnight roll.
71
+ */
72
+ advanceClock(): Promise<void>;
73
+ _write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
74
+ _final(callback: (error?: Error | null) => void): void;
75
+ destroy(...args: Parameters<Writable["destroy"]>): this;
76
+ private _job;
77
+ private _now;
78
+ private _stampNow;
79
+ private _resolveName;
80
+ /**
81
+ * Highest viable segment index for `stamp`.
82
+ * Disk is truth: resume a raw tail; start after sealed ones.
83
+ * The audit pointer (same stamp only) may only RAISE the result.
84
+ */
85
+ private _recoverIndexFor;
86
+ private _ensureOpen;
87
+ private _rotate;
88
+ private _hasContent;
89
+ private _armTimer;
90
+ private _armBump;
91
+ private _stopBump;
92
+ private _applyBump;
93
+ private _stopTimer;
94
+ private _loadAudit;
95
+ private _saveAudit;
96
+ private _pruneAuditFiles;
97
+ private _aevent;
98
+ private _setActivePointer;
99
+ /** Records left `active` by dead processes → `abandoned`. */
100
+ private _demoteZombieActives;
101
+ /** Heal audit-vs-disk divergence after crashes / external tampering. */
102
+ private _reconcileWithDisk;
103
+ private _repairInterruptedGzips;
104
+ /** Seal a record; re-key `name` → `name.gz` so scans stop ghosting. */
105
+ private _finishArchivedRecord;
106
+ /**
107
+ * Seal raw segments whose period has already passed (crash near midnight,
108
+ * virtual-clock leftovers, untracked files). Disk is the source of truth —
109
+ * manifest-tracked or not, a raw tail nothing will ever write to again
110
+ * must still honour zippedArchive.
111
+ */
112
+ private _sealOrphanedPastStamps;
113
+ private _retire;
114
+ }
115
+ /**
116
+ * Convenience factory mirroring common rotator APIs.
117
+ *
118
+ * @example
119
+ * const log = createStream({ filename: "logs/app-%DATE%.log" });
120
+ */
121
+ export declare function createStream(options: RotateFileStreamOptions): RotateFileStream;
package/dist/time.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /** Cached `Intl.DateTimeFormat`; throws synchronously for invalid zones. */
2
+ export declare function getDtf(tz: string): Intl.DateTimeFormat;
3
+ export declare function partsIn(tz: string, date: Date): Record<string, string>;
4
+ /** Escape a literal string for embedding inside a RegExp. */
5
+ export declare function escRe(s: string): string;
6
+ /** Substitute YYYY|YY|MM|DD|HH|mm|ss tokens in `pattern`. */
7
+ export declare function formatStamp(pattern: string, parts: Record<string, string>): string;
8
+ /** Offset of `tz` vs UTC at instant `date`, milliseconds. */
9
+ export declare function tzOffsetMs(tz: string, date: Date): number;
10
+ /** Next local midnight in `tz`, DST-corrected by fixed-point iteration. */
11
+ export declare function nextMidnight(tz: string, fromMs: number): number;
12
+ /** Next epoch-aligned boundary (e.g. hh:00 for step = 1h). */
13
+ export declare const nextAligned: (from: number, step: number) => number;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * logroller — public types.
3
+ *
4
+ * Everything exported here is part of the stable API surface.
5
+ */
6
+ export type RotationReason = "time" | "size" | "manual";
7
+ /** Options accepted by {@linkcode RotateFileStream} and `createStream`. */
8
+ export interface RotateFileStreamOptions {
9
+ /**
10
+ * Destination filename pattern. REQUIRED and must contain the `%DATE%`
11
+ * token, which is replaced by the formatted current date/time.
12
+ *
13
+ * @example "logs/app-%DATE%.log"
14
+ */
15
+ filename: string;
16
+ /**
17
+ * Date token pattern substituted for `%DATE%`.
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
+ */
25
+ datePattern?: string;
26
+ /** Gzip closed segments (`name.log` → `name.log.gz`). */
27
+ /** @default false */
28
+ zippedArchive?: boolean;
29
+ /**
30
+ * Maximum bytes per segment before rolling. Accepts raw numbers (bytes)
31
+ * or human strings: `"20m"`, `"512k"`, `"2g"`, `"100b"`.
32
+ * `0` disables size-based rotation.
33
+ * @default 0 (unlimited)
34
+ */
35
+ maxSize?: number | string;
36
+ /**
37
+ * Retention policy for this stream family.
38
+ * - `"90d"` — delete archived files older than 90 days (by mtime)
39
+ * - `500` — keep at most the newest 500 archived files
40
+ * Unset means **never** auto-delete.
41
+ */
42
+ maxFiles?: number | string;
43
+ /**
44
+ * IANA timezone used for date stamps and the daily boundary.
45
+ * Resolved via `Intl.DateTimeFormat`; throws early if invalid.
46
+ * @default "UTC"
47
+ */
48
+ tz?: string;
49
+ /**
50
+ * Rotation cadence: `"daily"` (local midnight in `tz`) or an interval
51
+ * like `"30s"`, `"5m"`, `"2h"`, or a millisecond number.
52
+ * Interval boundaries align to epoch multiples (hh:00:00 for `"1h"`).
53
+ * @default "daily"
54
+ */
55
+ frequency?: number | string;
56
+ /**
57
+ * TEST ONLY. Static shift of the internal clock, in hours.
58
+ * `"-6h"`, `"24h"`, or a plain number (fractional allowed).
59
+ * Affects naming, timers, and retention cutoffs — never production data.
60
+ */
61
+ addHours?: number | string;
62
+ /**
63
+ * TEST ONLY. Amount of virtual time added to the internal clock at
64
+ * every tick ({@linkcode RotateFileStreamOptions.clockStepIntervalMs}),
65
+ * simulating passing days: crossing into a new period archives the old
66
+ * segment exactly like a real midnight roll.
67
+ */
68
+ addHoursEveryMin?: number | string;
69
+ /**
70
+ * Real interval between virtual-clock ticks (see `addHoursEveryMin`).
71
+ * Lower it to fast-forward faster in integration tests.
72
+ * @default 60_000
73
+ */
74
+ clockStepIntervalMs?: number;
75
+ /** Detach rotation timers from the event loop (`timer.unref()`). */
76
+ /** @default false */
77
+ unrefTimers?: boolean;
78
+ /**
79
+ * Maintain `<stem>_audit.json` beside the logs: lifecycle manifest,
80
+ * uuid-tagged journal, restart recovery, interrupted-gzip repair,
81
+ * zombie-process detection. Library-owned; treat as read-only.
82
+ * @default true
83
+ */
84
+ audit?: boolean;
85
+ /** Override the audit manifest filename. Default derives from the prefix. */
86
+ auditFile?: string;
87
+ /** Passed through to the underlying `Writable`. */
88
+ highWaterMark?: number;
89
+ }
90
+ /** Payload for the `rotated` event. */
91
+ export interface RotatedInfo {
92
+ reason: RotationReason;
93
+ /** Segment that was closed; `null` when nothing was open. */
94
+ oldFile: string | null;
95
+ /** Gzip artifact created for it; `null` when unzipped, empty, or failed. */
96
+ archive: string | null;
97
+ /** Segment subsequent writes go to (opened lazily). */
98
+ newFile: string;
99
+ }
100
+ /** Payload for the `clock` event (emitted when a virtual clock is active). */
101
+ export interface ClockInfo {
102
+ addHours?: number | string;
103
+ addHoursEveryMin?: number | string;
104
+ offsetMs: number;
105
+ }
106
+ /**
107
+ * Tuple-style event map used to type `on` / `once` / `emit`.
108
+ * Keys mirror Node core lifecycle events where relevant.
109
+ */
110
+ export interface RotateFileStreamEventMap {
111
+ /** A segment file was opened for appending. */
112
+ open: [file: string];
113
+ /** A segment completed its lifecycle; see {@linkcode RotatedInfo}. */
114
+ rotated: [info: RotatedInfo];
115
+ /** A gzip archive finished successfully. */
116
+ archive: [gzFile: string];
117
+ /** Retention removed a file. */
118
+ deleted: [file: string];
119
+ /** Period changed with nothing to archive (idle boundary slide). */
120
+ period: [stamp: string];
121
+ /** Virtual clock engaged (test knobs). */
122
+ clock: [info: ClockInfo];
123
+ /** Non-fatal problem: gzip failure, divergent state, stray writer… */
124
+ warn: [err: Error];
125
+ /** Fatal problem mid-write; mirrors `Writable#close` semantics. */
126
+ error: [err: Error];
127
+ /** Stream fully closed. */
128
+ close: [];
129
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "logroller",
3
- "version": "1.0.0",
3
+ "version": "2.0.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
- "author": "Vinau <>",
6
+ "author": "Vinau",
7
7
  "homepage": "https://github.com/VinniWin/logroller#readme",
8
8
  "repository": {
9
9
  "type": "git",
@@ -13,47 +13,63 @@
13
13
  "keywords": [
14
14
  "log",
15
15
  "logger",
16
+ "logging",
16
17
  "rotate",
17
18
  "rotation",
18
19
  "rotating-file",
20
+ "rolling",
19
21
  "gzip",
22
+ "compress",
23
+ "retention",
20
24
  "stream",
25
+ "writable",
21
26
  "daily-rotate",
22
27
  "logrotate",
23
- "writable"
28
+ "timezone",
29
+ "utc",
30
+ "pino",
31
+ "pipeline",
32
+ "crash-safe",
33
+ "resilient",
34
+ "zero-dependency"
24
35
  ],
25
36
  "type": "module",
26
37
  "main": "./dist/index.cjs",
27
38
  "module": "./dist/index.esm.js",
39
+ "types": "./dist/index.d.ts",
28
40
  "exports": {
29
41
  ".": {
30
42
  "types": "./dist/index.d.ts",
31
43
  "import": "./dist/index.esm.js",
32
44
  "require": "./dist/index.cjs"
33
- }
45
+ },
46
+ "./package.json": "./package.json"
34
47
  },
35
48
  "files": [
36
49
  "dist",
37
50
  "README.md",
38
- "LICENSE"
51
+ "LICENSE",
52
+ "CHANGELOG.md"
39
53
  ],
40
54
  "engines": {
41
- "node": ">=16.4"
55
+ "node": ">=16.14"
42
56
  },
43
57
  "sideEffects": false,
44
58
  "scripts": {
45
59
  "build": "tsc -p tsconfig.json && rollup -c",
46
- "test": "npm run build && node --test --experimental-strip-types test/rotate.test.ts",
47
- "prepublishOnly": "npm test && npm run build",
48
- "prepare": "npm run build"
60
+ "typecheck": "tsc --noEmit -p tsconfig.json",
61
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.js\"",
62
+ "test": "node --test --experimental-strip-types test/rotate.test.ts",
63
+ "verify": "npm run typecheck && npm test",
64
+ "prepublishOnly": "npm run verify",
65
+ "release": "npm run verify && npm pack --dry-run"
49
66
  },
50
67
  "devDependencies": {
51
68
  "@rollup/plugin-typescript": "^11.1.6",
52
- "@types/node": "^22.10.0",
53
- "rollup": "^4.27.0",
54
- "typescript": "^5.6.3"
55
- },
56
- "dependencies": {
57
- "tslib": "^2.8.1"
69
+ "@types/node": "^26.4.0",
70
+ "prettier": "^3.4.2",
71
+ "rollup": "^4.28.1",
72
+ "tslib": "^2.8.1",
73
+ "typescript": "^5.7.2"
58
74
  }
59
75
  }