logroller 2.1.1 → 3.0.0

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/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- /*! logroller v2.1.1 | MIT License */
2
- "use strict";var t=require("node:crypto"),e=require("node:fs"),i=require("node:path"),s=require("node:stream"),n=require("node:zlib"),a=require("node:stream/promises");const r=t=>{try{return"string"==typeof t?e.statSync(t).size:0}catch{return 0}},h=t=>{try{return e.existsSync(t)}catch{return!1}};async function o(t,i,s,r=6){try{return await a.pipeline(e.createReadStream(t),n.createGzip({level:r}),e.createWriteStream(i)),await e.promises.unlink(t),!0}catch(t){return await e.promises.unlink(i).catch(()=>{}),s?.(t),!1}}const d=t=>t?new Promise(e=>t.end(()=>e())):Promise.resolve(),c={b:1,k:1024,kb:1024,m:1048576,mb:1048576,g:1073741824,gb:1073741824},u={ms:1,s:1e3,m:6e4,h:36e5,hour:36e5,hours:36e5};function l(t,e,i){if("number"==typeof t)return Math.round(t*u[e]);const s=/^\s*(-?\d+(?:\.\d+)?)\s*(h(?:our)?s?|ms|s|m)?\s*$/i.exec(t);if(!s)throw new Error(`Invalid ${i} "${t}"`);const n=(s[2]??e).toLowerCase(),a=u[n];if(void 0===a)throw new Error(`Invalid ${i} unit "${s[2]}" in "${t}"`);return Math.round(Number.parseFloat(s[1])*a)}function _(t){if(null==t||0===t)return 0;if("number"==typeof t)return t;const e=/^\s*(\d+(?:\.\d+)?)\s*(b|kb|k|mb|m|gb|g)?\s*$/i.exec(t);if(!e)throw new Error(`Invalid maxSize "${t}"`);return Math.round(Number.parseFloat(e[1])*(c[e[2]?.toLowerCase()]??1))}const m={ms:1,s:1e3,m:6e4,h:36e5};const p=new Map;function f(t){let e=p.get(t);if(!e){try{e=new Intl.DateTimeFormat("en-US",{timeZone:t,hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"})}catch{throw new Error(`Invalid timeZone "${t}"`)}p.set(t,e)}return e}function w(t,e){const i={};for(const s of f(t).formatToParts(e))i[s.type]=s.value;return i}const v=t=>t.padStart(2,"0");function y(t){return t.replace(/[.*+?^${}()|[\]\\]/g,String.raw`\$&`)}function b(t,e){const i=w(t,e);return Date.UTC(+i.year,+i.month-1,+i.day,+i.hour,+i.minute,+i.second)-(e.getTime()-e.getTime()%1e3)}class x extends s.Writable{tz;template;dir;datePattern;zippedArchive;maxSize;maxFiles;freq;unrefTimers;maxTotalSize;_tL;_tMid;_tExt;_rxFor;_rxActive;_auditEnabled;_auditPath;_bootId=t.randomUUID();_onSeal;_sealHookTimeoutMs;_compressionLevel;_symlinkPath=null;_offsetMs;_stepMs;_bumpEvery;_curStamp;_index;_ws=null;_path=null;_bytes=0;_timer=null;_bumpTimer=null;_ended=!1;_tail=Promise.resolve();_warnedBackward=!1;_audit;constructor(t={}){super(t.highWaterMark?{highWaterMark:t.highWaterMark}:{});const s=t.filename;if("string"!=typeof s||!s.includes("%DATE%"))throw new Error("opts.filename must contain '%DATE%' (e.g. 'logs/app-%DATE%.log')");var n;this.tz=t.tz||"UTC",f(this.tz),this.template=i.basename(s),this.dir=i.dirname(s),this.datePattern=t.datePattern||"YYYY-MM-DD",this.zippedArchive=!!t.zippedArchive,this.maxSize=_(t.maxSize??0),this.maxFiles=function(t){if(null==t||""===t||0===t)return null;if("number"==typeof t){if(t<0)throw new Error(`Invalid maxFiles "${t}"`);return{count:t}}const e=/^\s*(\d+)\s*d\s*$/i.exec(t);if(e)return{days:Number.parseInt(e[1],10)};const i=/^\s*(\d+)\s*$/.exec(t);if(i)return{count:Number.parseInt(i[1],10)};throw new Error(`Invalid maxFiles "${t}"`)}(t.maxFiles),this.maxTotalSize=_(t.maxTotalSize??0),this.freq=function(t){if(null==t)return{type:"daily"};if("daily"===t)return{type:"daily"};if("number"==typeof t){if(t<=0)throw new Error(`Invalid frequency "${t}"`);return{type:"interval",ms:t}}const e=/^\s*(\d+)\s*(ms|s|m|h)\s*$/i.exec(t);if(!e)throw new Error(`Invalid frequency "${t}"`);return{type:"interval",ms:Number.parseInt(e[1],10)*m[e[2].toLowerCase()]}}(t.frequency),this.unrefTimers=!!t.unrefTimers,this._offsetMs=null==(n=t.addHours)||""===n?0:l(n,"h","addHours"),this._stepMs=function(t){return null==t||""===t?0:l(t,"h","addHoursEveryMin")}(t.addHoursEveryMin),this._bumpEvery=Math.max(50,t.clockStepIntervalMs??6e4),this._compressionLevel=Math.min(9,Math.max(0,t.compressionLevel??6)),this._onSeal=t.onSeal,this._sealHookTimeoutMs=Math.max(0,t.sealHookTimeoutMs??3e4);const a=this.template.indexOf("%DATE%"),r=this.template.slice(a+6),h=r.lastIndexOf(".");this._tL=this.template.slice(0,a),this._tMid=h>=0?r.slice(0,h):"",this._tExt=h>=0?r.slice(h):"",this._rxFor=t=>new RegExp("^"+y(this._tL)+t+y(this._tMid)+"(?:\\.(\\d+))?"+y(this._tExt)+"(?:\\.gz)?$"),this._rxActive=this._rxFor("[^.]+"),this._auditEnabled=!1!==t.audit;const o=(this._tL.replace(/[^\w.-]+/g,"")||"log").replace(/^[-.]+|[-.]+$/g,"");var d,c,u,p;this._auditPath=i.join(this.dir,t.auditFile||`${o}_audit.json`),this._symlinkPath=t.symlink?i.join(this.dir,"string"==typeof t.symlink?t.symlink:`${o}-current.log`):null,e.mkdirSync(this.dir,{recursive:!0}),this._curStamp=this._stampNow(),this._audit=(d=this._tL,c=this._tExt,u=this.datePattern,p=this.tz,{version:1,prefix:d,ext:c,datePattern:u,tz:p,createdAt:null,updatedAt:null,activeStamp:null,activeIndex:null,writer:null,files:{},events:[]});const w=this._auditEnabled&&this._loadAudit();this._index=this._recoverIndexFor(this._curStamp);const v=this._audit.activeIndex;w&&null!==v&&this._audit.activeStamp===this._curStamp&&(this._index=Math.max(this._index,v)),0===this._offsetMs&&0===this._stepMs||process.nextTick(()=>this.emit("clock",{addHours:t.addHours??void 0,addHoursEveryMin:t.addHoursEveryMin??void 0,offsetMs:this._offsetMs})),process.nextTick(()=>{const e=t=>this.emit("warn",new Error(t));"interval"===this.freq.type?(this.freq.ms<864e5&&!this.datePattern.includes("HH")&&e(`frequency "${t.frequency}" needs HH in datePattern — segments would collapse onto one filename`),this.freq.ms<6e4&&!this.datePattern.includes("mm")&&e(`frequency "${t.frequency}" needs mm in datePattern`)):this.datePattern.includes("DD")||e("daily frequency needs DD in datePattern")}),this._job(async()=>{await this._sealOrphanedPastStamps(),this._auditEnabled&&(this._demoteZombieActives(),this._reconcileWithDisk(),await this._repairInterruptedGzips(),this._aevent("boot",{stamp:this._curStamp,index:this._index}),this._saveAudit()),(this.maxFiles||this.maxTotalSize>0)&&this._retire()}).catch(()=>{}),this._armTimer(),this._armBump(),this.once("close",()=>{this._stopTimer(),this._stopBump()})}on(t,e){return super.on(t,e)}once(t,e){return super.once(t,e)}emit(t,...e){return super.emit(t,...e)}get filename(){return this._path}get currentIndex(){return this._index}rotateNow(){return this._job(()=>this._rotate("manual"))}advanceClock(){return this._job(()=>this._applyBump())}listSegments(){const t=new RegExp("^"+y(this._tL)+"(.+?)"+y(this._tMid)+"(?:\\.(\\d+))?"+y(this._tExt)+"(?:\\.gz)?$"),s=this._symlinkPath&&i.basename(this._symlinkPath),n=[];let a=[];try{a=e.readdirSync(this.dir)}catch{return n}for(const e of a){if(e===s)continue;const a=t.exec(e);if(!a)continue;const h=this._audit.files[e];n.push({file:i.join(this.dir,e),stamp:a[1],index:a[2]?Number.parseInt(a[2],10):0,gzipped:e.endsWith(".gz"),bytes:r(i.join(this.dir,e)),state:h?.state??"untracked",openedAt:h?.openedAt,archivedAt:h?.archivedAt})}return n.sort((t,e)=>t.stamp.localeCompare(e.stamp)||t.index-e.index),n}stats(){const t=this.listSegments();return{dir:this.dir,currentStamp:this._curStamp,activeFile:this._path,activeIndex:this._index,segments:t.length,archived:t.filter(t=>t.gzipped).length,totalBytes:t.reduce((t,e)=>t+e.bytes,0),clockOffsetMs:this._offsetMs,symlink:this._symlinkPath}}_write(t,e,i){if(this._ended)return void process.nextTick(i,new Error("write after end"));const s=t.length;this._job(async()=>{try{this._stampAdvanced(this._stampNow())&&await this._rotate("time"),await this._ensureOpen(),this.maxSize>0&&this._bytes>0&&this._bytes+s>this.maxSize&&(await this._rotate("size"),await this._ensureOpen());const n=this._ws;if(!n)throw new Error("segment write stream not open");await new Promise((i,s)=>n.write(t,e,t=>t?s(t):i())),this._bytes+=s,i()}catch(t){this.emit("error",t),i(t)}}).catch(()=>{})}_final(t){this._ended=!0,this._stopTimer(),this._stopBump(),this._job(async()=>{await d(this._ws);const t=this._path&&i.basename(this._path);if(t&&this._audit.files[t]){const e=this._audit.files[t];e.state="closed",e.closedAt=(new Date).toISOString(),e.bytes=r(this._path),this._aevent("close",{file:t}),this._saveAudit()}this._ws=null,this._path=null}).catch(()=>{}).finally(()=>t())}destroy(...t){return this._stopTimer(),this._stopBump(),super.destroy(...t)}_job(t){const e=this._tail.then(t,t);return this._tail=e.catch(()=>{}),e}_now(){return Date.now()+this._offsetMs}_stampNow(t=new Date(this._now())){return e=this.datePattern,i=w(this.tz,t),e.replace(/YYYY|YY|MM|DD|HH|mm|ss/g,t=>{switch(t){case"YYYY":return i.year;case"YY":return i.year.slice(-2);case"MM":return v(i.month);case"DD":return v(i.day);case"HH":return v(i.hour);case"mm":return v(i.minute);case"ss":return v(i.second);default:return t}});var e,i}_resolveName(t,e){const s=i.join(this.dir,this._tL+t+this._tMid+this._tExt);return e?/\.[^.]+$/.test(s)?s.replace(/\.[^.]+$/,`.${e}$&`):`${s}.${e}`:s}_stampAdvanced(t){return t>this._curStamp||(t<this._curStamp&&!this._warnedBackward&&(this._warnedBackward=!0,this.emit("warn",new Error(`clock moved backward (stamp ${t} < ${this._curStamp}); keeping current segment until the clock catches up`))),!1)}_recoverIndexFor(t){const i=this._rxFor(y(t));let s=-1,n=-1,a=[];try{a=e.readdirSync(this.dir)}catch{}for(const t of a){const e=i.exec(t);if(!e)continue;const a=e[1]?Number.parseInt(e[1],10):0;a>s&&(s=a),!t.endsWith(".gz")&&a>n&&(n=a)}let r=n>=0?n:s>=0?s+1:0;const h=this._audit.activeIndex;return this._auditEnabled&&null!==h&&this._audit.activeStamp===t&&(r=Math.max(r,h)),r}_updateSymlink(){const t=this._symlinkPath;if(!t||!this._path)return;const s=`${t}.tmp`;try{try{e.unlinkSync(s)}catch{}e.symlinkSync(i.basename(this._path),s,"file"),e.renameSync(s,t)}catch(e){this._symlinkPath=null,this.emit("warn",new Error(`symlink unavailable ("${t}"): ${e.message} — continuing without it`))}}async _runSealHook(t,e){const s=this._onSeal;if(!s)return;let n;try{await Promise.race([Promise.resolve().then(()=>s({raw:t,gz:e})),new Promise((s,a)=>{n=setTimeout(()=>a(new Error(`onSeal hook timed out after ${this._sealHookTimeoutMs}ms (${i.basename(e??t)})`)),this._sealHookTimeoutMs)})])}catch(t){this.emit("warn",t)}finally{n&&clearTimeout(n)}}async _ensureOpen(){if(this._ws)return;const t=this._resolveName(this._curStamp,this._index);this._bytes=r(t);const s=e.createWriteStream(t,{flags:"a"});if(s.on("error",t=>this.emit("error",t)),this._ws=s,this._path=t,this._updateSymlink(),this._auditEnabled){const e=i.basename(t);this._audit.files[e]={index:this._index,state:"active",gzipPending:!1,openedAt:(new Date).toISOString()},this._setActivePointer(this._index),this._aevent("open",{file:e,index:this._index,resumedBytes:this._bytes}),this._saveAudit()}this.emit("open",t)}async _rotate(t){const s=this._path,n=this._ws;this._ws=null,this._path=null,this._bytes=0;let a=null;if(n&&s){await d(n);const t=r(s),c=i.basename(s),u=this._auditEnabled?this._audit.files[c]:void 0;if(this.zippedArchive&&t>0){a=`${s}.gz`,u&&(u.gzipPending=!0,u.bytes=t,this._saveAudit());await o(s,a,t=>this.emit("warn",t),this._compressionLevel)?(this._finishArchivedRecord(c),await this._runSealHook(s,a)):(u&&(u.gzipPending=!1,u.state="kept-raw"),this._aevent("gzip-failed",{file:c}),a=null,await this._runSealHook(s,null))}else if(0===t){const t=`${s}.gz`;if(h(t))this._finishArchivedRecord(c),a=t,await this._runSealHook(s,t);else{try{e.unlinkSync(s)}catch{}u&&delete this._audit.files[c],this._aevent("drop-empty",{file:c})}}else t>0&&(u&&(u.state="sealed",u.bytes=t),await this._runSealHook(s,null))}let c=this._stampNow();c<this._curStamp&&(this._warnedBackward||(this._warnedBackward=!0,this.emit("warn",new Error(`clock moved backward (stamp ${c} < ${this._curStamp}); rotation clamped to the current period`))),c=this._curStamp);const u=c!==this._curStamp;this._curStamp=c,this._index=u||"time"===t?this._recoverIndexFor(c):this._index+1,this._bytes=0,this._auditEnabled&&(this._setActivePointer(this._index),this._aevent("rotate",{reason:t,nextIndex:this._index,stamp:c}),this._saveAudit()),"time"!==t&&"manual"!==t||this._armTimer(),s&&this.emit("rotated",{reason:t,oldFile:s,archive:a,newFile:this._resolveName(c,this._index)}),this._retire()}_hasContent(){return this._ws?this._bytes>0||r(this._path)>0:!!(this._path&&r(this._path)>0)}_armTimer(){if(this._stopTimer(),this._ended)return;const t=this._now();var e,i;const s=("daily"===this.freq.type?function(t,e){const i=w(t,new Date(e)),s=Date.UTC(+i.year,+i.month-1,+i.day+1);let n=s-b(t,new Date(s));for(let e=0;e<3;e++){const e=s-b(t,new Date(n));if(e===n)break;n=e}return n<=e&&(n+=864e5),n}(this.tz,t):(e=t,i=this.freq.ms,Math.floor(e/i)*i+i))-this._now(),n=s<0?1e3:Math.max(100,s);this._timer=setTimeout(()=>{this._job(async()=>{const t=this._stampNow();"daily"!==this.freq.type?this._hasContent()&&await this._rotate("time"):this._stampAdvanced(t)&&(this._hasContent()?await this._rotate("time"):(this._curStamp=t,this._index=this._recoverIndexFor(t),this._auditEnabled&&(this._setActivePointer(this._index),this._aevent("period",{stamp:t}),this._saveAudit()),this.emit("period",t))),this._armTimer()}).catch(()=>{})},n),this.unrefTimers&&this._timer.unref()}_armBump(){this._stopBump(),this._stepMs&&!this._ended&&(this._bumpTimer=setInterval(()=>this._job(()=>this._applyBump()).catch(()=>{}),this._bumpEvery),this.unrefTimers&&this._bumpTimer.unref())}_stopBump(){this._bumpTimer&&(clearInterval(this._bumpTimer),this._bumpTimer=null)}async _applyBump(){if(this._ended)return;this._offsetMs+=this._stepMs,this._armTimer();const t=this._stampNow();this._stampAdvanced(t)&&(this._hasContent()?await this._rotate("time"):(this._curStamp=t,this._index=this._recoverIndexFor(t),this._auditEnabled&&(this._setActivePointer(this._index),this._aevent("period",{stamp:t,virtual:!0})),this.emit("period",t))),this._auditEnabled&&(this._aevent("clock-step",{addedMsPerMin:this._stepMs,offsetMs:this._offsetMs,stamp:t}),this._saveAudit())}_stopTimer(){this._timer&&(clearTimeout(this._timer),this._timer=null)}_loadAudit(){let i;try{i=JSON.parse(e.readFileSync(this._auditPath,"utf8"))}catch{try{e.renameSync(this._auditPath,`${this._auditPath}.corrupt`)}catch{}return!1}const s=i;if(!s||1!==s.version||s.prefix!==this._tL||s.ext!==this._tExt){try{e.renameSync(this._auditPath,`${this._auditPath}.corrupt`)}catch{}return!1}this._audit.createdAt=s.createdAt??(new Date).toISOString(),this._audit.activeStamp="string"==typeof s.activeStamp?s.activeStamp:null,this._audit.activeIndex=Number.isInteger(s.activeIndex)?s.activeIndex:null,this._audit.files=s.files&&"object"==typeof s.files?s.files:{},this._audit.events=Array.isArray(s.events)?s.events.slice(-200):[];const n=s.writer;if(n&&"string"==typeof n.bootId&&n.bootId!==this._bootId&&"number"==typeof n.heartbeat&&Date.now()-n.heartbeat<3e4){const e=new Error(`concurrent writer detected (pid ${n.pid}, boot ${n.bootId}); two processes share this log family and will race. If running 'node --watch', exclude the logs dir via --watch-path.`);this.emit("warn",e),this._audit.events.push({id:t.randomUUID(),t:(new Date).toISOString(),type:"concurrent-writer",otherPid:n.pid,otherBoot:n.bootId})}return!0}_saveAudit(){if(this._auditEnabled)try{this._pruneAuditFiles(),this._audit.updatedAt=(new Date).toISOString(),this._audit.writer={pid:process.pid,bootId:this._bootId,heartbeat:Date.now()};const t=`${this._auditPath}.tmp`;e.writeFileSync(t,JSON.stringify(this._audit,null,1)),e.renameSync(t,this._auditPath)}catch(t){this.emit("warn",t)}}_pruneAuditFiles(){const t=this._audit.files,e=Object.keys(t);if(e.length<=600)return;const s=this._path&&i.basename(this._path),n=t=>t.archivedAt??t.closedAt??t.abandonedAt??t.openedAt??"",a=e.filter(e=>e!==s&&"active"!==t[e].state).sort((e,i)=>n(t[e]).localeCompare(n(t[i]))),r=e.length-600+50;for(const e of a.slice(0,r))delete t[e]}_aevent(e,i={}){if(!this._auditEnabled)return;const s=this._audit.events;s.push({id:t.randomUUID(),t:(new Date).toISOString(),type:e,...i}),s.length>200&&s.splice(0,s.length-200)}_setActivePointer(t){this._audit.activeStamp=this._curStamp,this._audit.activeIndex=t}_demoteZombieActives(){const t=this._path&&i.basename(this._path);for(const[e,i]of Object.entries(this._audit.files))"active"===i.state&&e!==t&&(i.state="abandoned",i.abandonedAt=(new Date).toISOString(),this._aevent("abandon",{file:e}))}_reconcileWithDisk(){for(const t of Object.keys(this._audit.files)){const e=this._audit.files[t];if(e.gzipPending)continue;if(t.endsWith(".gz")){const s=h(i.join(this.dir,t)),n=t.slice(0,-3),a=h(i.join(this.dir,n));if(s)continue;if(a){delete this._audit.files[t],this._audit.files[n]={...e,state:"sealed"},this._aevent("archive-regressed",{file:t,backTo:n}),this.emit("warn",new Error(`archive ${t} missing but raw ${n} exists`));continue}this._aevent("vanished",{file:t}),delete this._audit.files[t];continue}const s=h(i.join(this.dir,t)),n=h(i.join(this.dir,`${t}.gz`));s||(n?this._finishArchivedRecord(t,!0):(this._aevent("vanished",{file:t}),delete this._audit.files[t]))}}async _sealOrphanedPastStamps(){if(!this.zippedArchive)return;const t=new RegExp("^"+y(this._tL)+"(.+?)"+y(this._tMid)+"(?:\\.\\d+)?"+y(this._tExt)+"(?:\\.gz)?$");let s=[];try{s=e.readdirSync(this.dir)}catch{return}for(const e of s){if(e.endsWith(".gz"))continue;if(e===(this._path&&i.basename(this._path)))continue;const s=t.exec(e)?.[1];if(!s||s>=this._curStamp)continue;const n=i.join(this.dir,e);if(!h(n)||0===r(n))continue;const a=`${n}.gz`;await o(n,a,t=>this.emit("warn",t),this._compressionLevel)&&(this._auditEnabled&&this._audit.files[e]?this._finishArchivedRecord(e,!0):(this._auditEnabled&&(this._audit.files[a]={state:"archived",gzipPending:!1,archivedAt:(new Date).toISOString()},this._aevent("seal-past",{file:a})),this.emit("archive",a)),await this._runSealHook(n,a))}}async _repairInterruptedGzips(){for(const[t,s]of Object.entries(this._audit.files)){if(!s.gzipPending)continue;const n=i.join(this.dir,t),a=`${n}.gz`;if(!h(n)){s.gzipPending=!1,s.state="lost",this._aevent("gzip-lost",{file:t});continue}if(h(a)){try{e.unlinkSync(n)}catch{}this._finishArchivedRecord(t,!0),await this._runSealHook(n,a);continue}await o(n,a,t=>this.emit("warn",t),this._compressionLevel)?(this._finishArchivedRecord(t,!0),await this._runSealHook(n,a)):(s.gzipPending=!1,s.state="kept-raw",this._aevent("gzip-failed",{file:t}))}}_finishArchivedRecord(t,e=!1){const s=this._audit.files[t];if(!s)return;if(s.gzipPending=!1,s.state="archived",s.archivedAt=(new Date).toISOString(),t.endsWith(".gz"))return this.emit("archive",i.join(this.dir,t)),void this._aevent("archive",{file:t,...e&&{repaired:e}});const n=`${t}.gz`;delete this._audit.files[t],this._audit.files[n]=s,this.emit("archive",i.join(this.dir,n)),this._aevent("archive",{file:n,...e&&{repaired:e}})}async _retire(){const t=this.maxFiles;if(!t&&this.maxTotalSize<=0)return;const s=this._path&&i.basename(this._path),n=s?.replace(/(?:\.\d+)?(?:\.log|)(?:\.gz)?$/,""),a=[i.basename(this._auditPath),`${i.basename(this._auditPath)}.tmp`,`${i.basename(this._auditPath)}.corrupt`,...this._symlinkPath?[i.basename(this._symlinkPath),`${i.basename(this._symlinkPath)}.tmp`]:[]];try{const r=(await e.promises.readdir(this.dir)).filter(t=>this._rxActive.test(t)&&t!==s&&!(n&&t.startsWith(n))&&!a.includes(t)),h=[];for(const t of r){const s=i.join(this.dir,t);try{const t=await e.promises.stat(s);h.push({full:s,mtimeMs:t.mtimeMs,size:t.size})}catch{}}h.sort((t,e)=>t.mtimeMs-e.mtimeMs);const o=new Set;if(null!=t?.days){const e=this._now()-864e5*t.days;for(const t of h)t.mtimeMs<e&&o.add(t.full)}if(null!=t?.count){const e=h.filter(t=>!o.has(t.full)),i=e.length-t.count;for(let t=0;t<i;t++)o.add(e[t].full)}if(this.maxTotalSize>0){const t=h.filter(t=>!o.has(t.full));let e=t.reduce((t,e)=>t+e.size,0),i=0;for(;e>this.maxTotalSize&&i<t.length;)o.add(t[i].full),e-=t[i].size,i++}if(0===o.size)return;for(const t of o)await e.promises.unlink(t).catch(()=>{}),this._auditEnabled&&(delete this._audit.files[i.basename(t)],this._aevent("deleted",{file:i.basename(t)})),this.emit("deleted",t);this._saveAudit()}catch(t){this.emit("warn",t)}}}const g=new Set;let S=!1,k=!1;async function z(t=5e3){if(k)return;k=!0;const e=setTimeout(()=>process.exit(1),t);e.unref();try{await Promise.allSettled([...g].map(t=>new Promise(e=>t.end(()=>e()))))}finally{clearTimeout(e),k=!1}}exports.RotateFileStream=x,exports.createStream=function(t){return new x(t)},exports.flushAll=z,exports.installShutdown=function(t,e={}){const i=Array.isArray(t)?t:[t];for(const t of i)g.add(t);if(!S){S=!0;const t=e.timeoutMs??5e3,i=e.code??0;for(const s of e.signals??["SIGINT","SIGTERM"])process.on(s,()=>{z(t).finally(()=>process.exit(i))})}return async()=>{await z(e.timeoutMs??5e3);for(const t of i)g.delete(t)}};
1
+ /*! logroller v3.0.0 | MIT License */
2
+ "use strict";var e=require("./rotate-file-stream.cjs");require("node:crypto"),require("node:fs"),require("node:path"),require("node:stream"),require("node:zlib"),require("node:stream/promises");const t=new Set;let r=!1,o=null;function s(e=5e3){return o??=(async()=>{let r;try{const o=Promise.allSettled([...t].map(e=>new Promise(t=>{e.once("close",t),e.end(()=>t()),e.destroyed&&t()}))).then(()=>!0),s=new Promise(t=>{r=setTimeout(()=>t(!1),e),r.unref()});return await Promise.race([o,s])}finally{clearTimeout(r);for(const e of t)(e.writableFinished||e.destroyed)&&t.delete(e);o=null}})(),o}exports.RotateFileStream=e.RotateFileStream,exports.createStream=e.createStream,exports.flushAll=s,exports.installShutdown=function(e,o={}){const n=Array.isArray(e)?e:[e];for(const e of n)t.add(e);if(!r){r=!0;const e=o.timeoutMs??5e3,t=o.code??0;for(const r of o.signals??["SIGINT","SIGTERM"])process.on(r,()=>{s(e).then(e=>process.exit(e?t:1))})}return async()=>{await s(o.timeoutMs??5e3);for(const e of n)t.delete(e)}};
@@ -1,5 +1,8 @@
1
- import type { RotateFileStream } from "./rotate-file-stream.js";
2
- export interface ShutdownOptions {
1
+ import { R as RotateFileStream } from './rotate-file-stream.cjs';
2
+ export { C as ClockInfo, a as RotateFileStreamEventMap, b as RotateFileStreamOptions, c as RotatedInfo, d as RotationReason, S as SealInfo, e as SegmentInfo, f as StreamStats, g as createStream } from './rotate-file-stream.cjs';
3
+ import 'node:stream';
4
+
5
+ interface ShutdownOptions {
3
6
  /** Signals to listen on. @default ["SIGINT", "SIGTERM"] */
4
7
  signals?: NodeJS.Signals[];
5
8
  /** Hard-exit deadline (ms) if a stream refuses to end.
@@ -23,10 +26,14 @@ export interface ShutdownOptions {
23
26
  * const flush = installShutdown(logStream);
24
27
  * process.on("exit", () => { void flush(); });
25
28
  */
26
- export declare function installShutdown(streams: RotateFileStream | RotateFileStream[], opts?: ShutdownOptions): () => Promise<void>;
29
+ declare function installShutdown(streams: RotateFileStream | RotateFileStream[], opts?: ShutdownOptions): () => Promise<void>;
27
30
  /**
28
31
  * 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.
32
+ * Does NOT exit the process. Resolves `true` when every stream finished,
33
+ * `false` when the deadline hit first. Concurrent calls share one flush.
34
+ * Ended/destroyed streams leave the registry.
31
35
  */
32
- export declare function flushAll(timeoutMs?: number): Promise<void>;
36
+ declare function flushAll(timeoutMs?: number): Promise<boolean>;
37
+
38
+ export { RotateFileStream, flushAll, installShutdown };
39
+ export type { ShutdownOptions };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,39 @@
1
+ import { R as RotateFileStream } from './rotate-file-stream.js';
2
+ export { C as ClockInfo, a as RotateFileStreamEventMap, b as RotateFileStreamOptions, c as RotatedInfo, d as RotationReason, S as SealInfo, e as SegmentInfo, f as StreamStats, g as createStream } from './rotate-file-stream.js';
3
+ import 'node:stream';
4
+
5
+ interface ShutdownOptions {
6
+ /** Signals to listen on. @default ["SIGINT", "SIGTERM"] */
7
+ signals?: NodeJS.Signals[];
8
+ /** Hard-exit deadline (ms) if a stream refuses to end.
9
+ * @default 5000 */
10
+ timeoutMs?: number;
11
+ /** Process exit code. @default 0 */
12
+ code?: number;
13
+ }
1
14
  /**
2
- * logroller — zero-dependency rotating file streams for Node.js.
15
+ * Register streams for graceful shutdown: on SIGINT/SIGTERM every
16
+ * registered stream is ended — flushing buffered writes and finishing the
17
+ * audit manifest — before the process exits, with a hard deadline.
3
18
  *
4
- * @packageDocumentation
19
+ * Safe to call multiple times: process listeners install once, streams
20
+ * accumulate in the registry.
21
+ *
22
+ * Returns `flush()`, the same graceful end WITHOUT exiting — for tests
23
+ * and custom signal handling.
24
+ *
25
+ * @example
26
+ * const flush = installShutdown(logStream);
27
+ * process.on("exit", () => { void flush(); });
28
+ */
29
+ declare function installShutdown(streams: RotateFileStream | RotateFileStream[], opts?: ShutdownOptions): () => Promise<void>;
30
+ /**
31
+ * End every registered stream (flush buffered writes) with a deadline.
32
+ * Does NOT exit the process. Resolves `true` when every stream finished,
33
+ * `false` when the deadline hit first. Concurrent calls share one flush.
34
+ * Ended/destroyed streams leave the registry.
5
35
  */
6
- export { createStream, RotateFileStream } from "./rotate-file-stream.js";
7
- export { flushAll, installShutdown } from "./shutdown.js";
8
- export type { ShutdownOptions } from "./shutdown.js";
9
- export type { ClockInfo, RotatedInfo, RotateFileStreamEventMap, RotateFileStreamOptions, RotationReason, SealInfo, SegmentInfo, StreamStats } from "./types.js";
36
+ declare function flushAll(timeoutMs?: number): Promise<boolean>;
37
+
38
+ export { RotateFileStream, flushAll, installShutdown };
39
+ export type { ShutdownOptions };
package/dist/index.esm.js CHANGED
@@ -1,2 +1,2 @@
1
- /*! logroller v2.1.1 | MIT License */
2
- import{randomUUID as t}from"node:crypto";import e from"node:fs";import i from"node:path";import{Writable as s}from"node:stream";import n from"node:zlib";import{pipeline as a}from"node:stream/promises";const r=t=>{try{return"string"==typeof t?e.statSync(t).size:0}catch{return 0}},h=t=>{try{return e.existsSync(t)}catch{return!1}};async function o(t,i,s,r=6){try{return await a(e.createReadStream(t),n.createGzip({level:r}),e.createWriteStream(i)),await e.promises.unlink(t),!0}catch(t){return await e.promises.unlink(i).catch(()=>{}),s?.(t),!1}}const d=t=>t?new Promise(e=>t.end(()=>e())):Promise.resolve(),c={b:1,k:1024,kb:1024,m:1048576,mb:1048576,g:1073741824,gb:1073741824},u={ms:1,s:1e3,m:6e4,h:36e5,hour:36e5,hours:36e5};function l(t,e,i){if("number"==typeof t)return Math.round(t*u[e]);const s=/^\s*(-?\d+(?:\.\d+)?)\s*(h(?:our)?s?|ms|s|m)?\s*$/i.exec(t);if(!s)throw new Error(`Invalid ${i} "${t}"`);const n=(s[2]??e).toLowerCase(),a=u[n];if(void 0===a)throw new Error(`Invalid ${i} unit "${s[2]}" in "${t}"`);return Math.round(Number.parseFloat(s[1])*a)}function _(t){if(null==t||0===t)return 0;if("number"==typeof t)return t;const e=/^\s*(\d+(?:\.\d+)?)\s*(b|kb|k|mb|m|gb|g)?\s*$/i.exec(t);if(!e)throw new Error(`Invalid maxSize "${t}"`);return Math.round(Number.parseFloat(e[1])*(c[e[2]?.toLowerCase()]??1))}const m={ms:1,s:1e3,m:6e4,h:36e5};const p=new Map;function f(t){let e=p.get(t);if(!e){try{e=new Intl.DateTimeFormat("en-US",{timeZone:t,hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"})}catch{throw new Error(`Invalid timeZone "${t}"`)}p.set(t,e)}return e}function w(t,e){const i={};for(const s of f(t).formatToParts(e))i[s.type]=s.value;return i}const y=t=>t.padStart(2,"0");function v(t){return t.replace(/[.*+?^${}()|[\]\\]/g,String.raw`\$&`)}function b(t,e){const i=w(t,e);return Date.UTC(+i.year,+i.month-1,+i.day,+i.hour,+i.minute,+i.second)-(e.getTime()-e.getTime()%1e3)}class g extends s{tz;template;dir;datePattern;zippedArchive;maxSize;maxFiles;freq;unrefTimers;maxTotalSize;_tL;_tMid;_tExt;_rxFor;_rxActive;_auditEnabled;_auditPath;_bootId=t();_onSeal;_sealHookTimeoutMs;_compressionLevel;_symlinkPath=null;_offsetMs;_stepMs;_bumpEvery;_curStamp;_index;_ws=null;_path=null;_bytes=0;_timer=null;_bumpTimer=null;_ended=!1;_tail=Promise.resolve();_warnedBackward=!1;_audit;constructor(t={}){super(t.highWaterMark?{highWaterMark:t.highWaterMark}:{});const s=t.filename;if("string"!=typeof s||!s.includes("%DATE%"))throw new Error("opts.filename must contain '%DATE%' (e.g. 'logs/app-%DATE%.log')");var n;this.tz=t.tz||"UTC",f(this.tz),this.template=i.basename(s),this.dir=i.dirname(s),this.datePattern=t.datePattern||"YYYY-MM-DD",this.zippedArchive=!!t.zippedArchive,this.maxSize=_(t.maxSize??0),this.maxFiles=function(t){if(null==t||""===t||0===t)return null;if("number"==typeof t){if(t<0)throw new Error(`Invalid maxFiles "${t}"`);return{count:t}}const e=/^\s*(\d+)\s*d\s*$/i.exec(t);if(e)return{days:Number.parseInt(e[1],10)};const i=/^\s*(\d+)\s*$/.exec(t);if(i)return{count:Number.parseInt(i[1],10)};throw new Error(`Invalid maxFiles "${t}"`)}(t.maxFiles),this.maxTotalSize=_(t.maxTotalSize??0),this.freq=function(t){if(null==t)return{type:"daily"};if("daily"===t)return{type:"daily"};if("number"==typeof t){if(t<=0)throw new Error(`Invalid frequency "${t}"`);return{type:"interval",ms:t}}const e=/^\s*(\d+)\s*(ms|s|m|h)\s*$/i.exec(t);if(!e)throw new Error(`Invalid frequency "${t}"`);return{type:"interval",ms:Number.parseInt(e[1],10)*m[e[2].toLowerCase()]}}(t.frequency),this.unrefTimers=!!t.unrefTimers,this._offsetMs=null==(n=t.addHours)||""===n?0:l(n,"h","addHours"),this._stepMs=function(t){return null==t||""===t?0:l(t,"h","addHoursEveryMin")}(t.addHoursEveryMin),this._bumpEvery=Math.max(50,t.clockStepIntervalMs??6e4),this._compressionLevel=Math.min(9,Math.max(0,t.compressionLevel??6)),this._onSeal=t.onSeal,this._sealHookTimeoutMs=Math.max(0,t.sealHookTimeoutMs??3e4);const a=this.template.indexOf("%DATE%"),r=this.template.slice(a+6),h=r.lastIndexOf(".");this._tL=this.template.slice(0,a),this._tMid=h>=0?r.slice(0,h):"",this._tExt=h>=0?r.slice(h):"",this._rxFor=t=>new RegExp("^"+v(this._tL)+t+v(this._tMid)+"(?:\\.(\\d+))?"+v(this._tExt)+"(?:\\.gz)?$"),this._rxActive=this._rxFor("[^.]+"),this._auditEnabled=!1!==t.audit;const o=(this._tL.replace(/[^\w.-]+/g,"")||"log").replace(/^[-.]+|[-.]+$/g,"");var d,c,u,p;this._auditPath=i.join(this.dir,t.auditFile||`${o}_audit.json`),this._symlinkPath=t.symlink?i.join(this.dir,"string"==typeof t.symlink?t.symlink:`${o}-current.log`):null,e.mkdirSync(this.dir,{recursive:!0}),this._curStamp=this._stampNow(),this._audit=(d=this._tL,c=this._tExt,u=this.datePattern,p=this.tz,{version:1,prefix:d,ext:c,datePattern:u,tz:p,createdAt:null,updatedAt:null,activeStamp:null,activeIndex:null,writer:null,files:{},events:[]});const w=this._auditEnabled&&this._loadAudit();this._index=this._recoverIndexFor(this._curStamp);const y=this._audit.activeIndex;w&&null!==y&&this._audit.activeStamp===this._curStamp&&(this._index=Math.max(this._index,y)),0===this._offsetMs&&0===this._stepMs||process.nextTick(()=>this.emit("clock",{addHours:t.addHours??void 0,addHoursEveryMin:t.addHoursEveryMin??void 0,offsetMs:this._offsetMs})),process.nextTick(()=>{const e=t=>this.emit("warn",new Error(t));"interval"===this.freq.type?(this.freq.ms<864e5&&!this.datePattern.includes("HH")&&e(`frequency "${t.frequency}" needs HH in datePattern — segments would collapse onto one filename`),this.freq.ms<6e4&&!this.datePattern.includes("mm")&&e(`frequency "${t.frequency}" needs mm in datePattern`)):this.datePattern.includes("DD")||e("daily frequency needs DD in datePattern")}),this._job(async()=>{await this._sealOrphanedPastStamps(),this._auditEnabled&&(this._demoteZombieActives(),this._reconcileWithDisk(),await this._repairInterruptedGzips(),this._aevent("boot",{stamp:this._curStamp,index:this._index}),this._saveAudit()),(this.maxFiles||this.maxTotalSize>0)&&this._retire()}).catch(()=>{}),this._armTimer(),this._armBump(),this.once("close",()=>{this._stopTimer(),this._stopBump()})}on(t,e){return super.on(t,e)}once(t,e){return super.once(t,e)}emit(t,...e){return super.emit(t,...e)}get filename(){return this._path}get currentIndex(){return this._index}rotateNow(){return this._job(()=>this._rotate("manual"))}advanceClock(){return this._job(()=>this._applyBump())}listSegments(){const t=new RegExp("^"+v(this._tL)+"(.+?)"+v(this._tMid)+"(?:\\.(\\d+))?"+v(this._tExt)+"(?:\\.gz)?$"),s=this._symlinkPath&&i.basename(this._symlinkPath),n=[];let a=[];try{a=e.readdirSync(this.dir)}catch{return n}for(const e of a){if(e===s)continue;const a=t.exec(e);if(!a)continue;const h=this._audit.files[e];n.push({file:i.join(this.dir,e),stamp:a[1],index:a[2]?Number.parseInt(a[2],10):0,gzipped:e.endsWith(".gz"),bytes:r(i.join(this.dir,e)),state:h?.state??"untracked",openedAt:h?.openedAt,archivedAt:h?.archivedAt})}return n.sort((t,e)=>t.stamp.localeCompare(e.stamp)||t.index-e.index),n}stats(){const t=this.listSegments();return{dir:this.dir,currentStamp:this._curStamp,activeFile:this._path,activeIndex:this._index,segments:t.length,archived:t.filter(t=>t.gzipped).length,totalBytes:t.reduce((t,e)=>t+e.bytes,0),clockOffsetMs:this._offsetMs,symlink:this._symlinkPath}}_write(t,e,i){if(this._ended)return void process.nextTick(i,new Error("write after end"));const s=t.length;this._job(async()=>{try{this._stampAdvanced(this._stampNow())&&await this._rotate("time"),await this._ensureOpen(),this.maxSize>0&&this._bytes>0&&this._bytes+s>this.maxSize&&(await this._rotate("size"),await this._ensureOpen());const n=this._ws;if(!n)throw new Error("segment write stream not open");await new Promise((i,s)=>n.write(t,e,t=>t?s(t):i())),this._bytes+=s,i()}catch(t){this.emit("error",t),i(t)}}).catch(()=>{})}_final(t){this._ended=!0,this._stopTimer(),this._stopBump(),this._job(async()=>{await d(this._ws);const t=this._path&&i.basename(this._path);if(t&&this._audit.files[t]){const e=this._audit.files[t];e.state="closed",e.closedAt=(new Date).toISOString(),e.bytes=r(this._path),this._aevent("close",{file:t}),this._saveAudit()}this._ws=null,this._path=null}).catch(()=>{}).finally(()=>t())}destroy(...t){return this._stopTimer(),this._stopBump(),super.destroy(...t)}_job(t){const e=this._tail.then(t,t);return this._tail=e.catch(()=>{}),e}_now(){return Date.now()+this._offsetMs}_stampNow(t=new Date(this._now())){return e=this.datePattern,i=w(this.tz,t),e.replace(/YYYY|YY|MM|DD|HH|mm|ss/g,t=>{switch(t){case"YYYY":return i.year;case"YY":return i.year.slice(-2);case"MM":return y(i.month);case"DD":return y(i.day);case"HH":return y(i.hour);case"mm":return y(i.minute);case"ss":return y(i.second);default:return t}});var e,i}_resolveName(t,e){const s=i.join(this.dir,this._tL+t+this._tMid+this._tExt);return e?/\.[^.]+$/.test(s)?s.replace(/\.[^.]+$/,`.${e}$&`):`${s}.${e}`:s}_stampAdvanced(t){return t>this._curStamp||(t<this._curStamp&&!this._warnedBackward&&(this._warnedBackward=!0,this.emit("warn",new Error(`clock moved backward (stamp ${t} < ${this._curStamp}); keeping current segment until the clock catches up`))),!1)}_recoverIndexFor(t){const i=this._rxFor(v(t));let s=-1,n=-1,a=[];try{a=e.readdirSync(this.dir)}catch{}for(const t of a){const e=i.exec(t);if(!e)continue;const a=e[1]?Number.parseInt(e[1],10):0;a>s&&(s=a),!t.endsWith(".gz")&&a>n&&(n=a)}let r=n>=0?n:s>=0?s+1:0;const h=this._audit.activeIndex;return this._auditEnabled&&null!==h&&this._audit.activeStamp===t&&(r=Math.max(r,h)),r}_updateSymlink(){const t=this._symlinkPath;if(!t||!this._path)return;const s=`${t}.tmp`;try{try{e.unlinkSync(s)}catch{}e.symlinkSync(i.basename(this._path),s,"file"),e.renameSync(s,t)}catch(e){this._symlinkPath=null,this.emit("warn",new Error(`symlink unavailable ("${t}"): ${e.message} — continuing without it`))}}async _runSealHook(t,e){const s=this._onSeal;if(!s)return;let n;try{await Promise.race([Promise.resolve().then(()=>s({raw:t,gz:e})),new Promise((s,a)=>{n=setTimeout(()=>a(new Error(`onSeal hook timed out after ${this._sealHookTimeoutMs}ms (${i.basename(e??t)})`)),this._sealHookTimeoutMs)})])}catch(t){this.emit("warn",t)}finally{n&&clearTimeout(n)}}async _ensureOpen(){if(this._ws)return;const t=this._resolveName(this._curStamp,this._index);this._bytes=r(t);const s=e.createWriteStream(t,{flags:"a"});if(s.on("error",t=>this.emit("error",t)),this._ws=s,this._path=t,this._updateSymlink(),this._auditEnabled){const e=i.basename(t);this._audit.files[e]={index:this._index,state:"active",gzipPending:!1,openedAt:(new Date).toISOString()},this._setActivePointer(this._index),this._aevent("open",{file:e,index:this._index,resumedBytes:this._bytes}),this._saveAudit()}this.emit("open",t)}async _rotate(t){const s=this._path,n=this._ws;this._ws=null,this._path=null,this._bytes=0;let a=null;if(n&&s){await d(n);const t=r(s),c=i.basename(s),u=this._auditEnabled?this._audit.files[c]:void 0;if(this.zippedArchive&&t>0){a=`${s}.gz`,u&&(u.gzipPending=!0,u.bytes=t,this._saveAudit());await o(s,a,t=>this.emit("warn",t),this._compressionLevel)?(this._finishArchivedRecord(c),await this._runSealHook(s,a)):(u&&(u.gzipPending=!1,u.state="kept-raw"),this._aevent("gzip-failed",{file:c}),a=null,await this._runSealHook(s,null))}else if(0===t){const t=`${s}.gz`;if(h(t))this._finishArchivedRecord(c),a=t,await this._runSealHook(s,t);else{try{e.unlinkSync(s)}catch{}u&&delete this._audit.files[c],this._aevent("drop-empty",{file:c})}}else t>0&&(u&&(u.state="sealed",u.bytes=t),await this._runSealHook(s,null))}let c=this._stampNow();c<this._curStamp&&(this._warnedBackward||(this._warnedBackward=!0,this.emit("warn",new Error(`clock moved backward (stamp ${c} < ${this._curStamp}); rotation clamped to the current period`))),c=this._curStamp);const u=c!==this._curStamp;this._curStamp=c,this._index=u||"time"===t?this._recoverIndexFor(c):this._index+1,this._bytes=0,this._auditEnabled&&(this._setActivePointer(this._index),this._aevent("rotate",{reason:t,nextIndex:this._index,stamp:c}),this._saveAudit()),"time"!==t&&"manual"!==t||this._armTimer(),s&&this.emit("rotated",{reason:t,oldFile:s,archive:a,newFile:this._resolveName(c,this._index)}),this._retire()}_hasContent(){return this._ws?this._bytes>0||r(this._path)>0:!!(this._path&&r(this._path)>0)}_armTimer(){if(this._stopTimer(),this._ended)return;const t=this._now();var e,i;const s=("daily"===this.freq.type?function(t,e){const i=w(t,new Date(e)),s=Date.UTC(+i.year,+i.month-1,+i.day+1);let n=s-b(t,new Date(s));for(let e=0;e<3;e++){const e=s-b(t,new Date(n));if(e===n)break;n=e}return n<=e&&(n+=864e5),n}(this.tz,t):(e=t,i=this.freq.ms,Math.floor(e/i)*i+i))-this._now(),n=s<0?1e3:Math.max(100,s);this._timer=setTimeout(()=>{this._job(async()=>{const t=this._stampNow();"daily"!==this.freq.type?this._hasContent()&&await this._rotate("time"):this._stampAdvanced(t)&&(this._hasContent()?await this._rotate("time"):(this._curStamp=t,this._index=this._recoverIndexFor(t),this._auditEnabled&&(this._setActivePointer(this._index),this._aevent("period",{stamp:t}),this._saveAudit()),this.emit("period",t))),this._armTimer()}).catch(()=>{})},n),this.unrefTimers&&this._timer.unref()}_armBump(){this._stopBump(),this._stepMs&&!this._ended&&(this._bumpTimer=setInterval(()=>this._job(()=>this._applyBump()).catch(()=>{}),this._bumpEvery),this.unrefTimers&&this._bumpTimer.unref())}_stopBump(){this._bumpTimer&&(clearInterval(this._bumpTimer),this._bumpTimer=null)}async _applyBump(){if(this._ended)return;this._offsetMs+=this._stepMs,this._armTimer();const t=this._stampNow();this._stampAdvanced(t)&&(this._hasContent()?await this._rotate("time"):(this._curStamp=t,this._index=this._recoverIndexFor(t),this._auditEnabled&&(this._setActivePointer(this._index),this._aevent("period",{stamp:t,virtual:!0})),this.emit("period",t))),this._auditEnabled&&(this._aevent("clock-step",{addedMsPerMin:this._stepMs,offsetMs:this._offsetMs,stamp:t}),this._saveAudit())}_stopTimer(){this._timer&&(clearTimeout(this._timer),this._timer=null)}_loadAudit(){let i;try{i=JSON.parse(e.readFileSync(this._auditPath,"utf8"))}catch{try{e.renameSync(this._auditPath,`${this._auditPath}.corrupt`)}catch{}return!1}const s=i;if(!s||1!==s.version||s.prefix!==this._tL||s.ext!==this._tExt){try{e.renameSync(this._auditPath,`${this._auditPath}.corrupt`)}catch{}return!1}this._audit.createdAt=s.createdAt??(new Date).toISOString(),this._audit.activeStamp="string"==typeof s.activeStamp?s.activeStamp:null,this._audit.activeIndex=Number.isInteger(s.activeIndex)?s.activeIndex:null,this._audit.files=s.files&&"object"==typeof s.files?s.files:{},this._audit.events=Array.isArray(s.events)?s.events.slice(-200):[];const n=s.writer;if(n&&"string"==typeof n.bootId&&n.bootId!==this._bootId&&"number"==typeof n.heartbeat&&Date.now()-n.heartbeat<3e4){const e=new Error(`concurrent writer detected (pid ${n.pid}, boot ${n.bootId}); two processes share this log family and will race. If running 'node --watch', exclude the logs dir via --watch-path.`);this.emit("warn",e),this._audit.events.push({id:t(),t:(new Date).toISOString(),type:"concurrent-writer",otherPid:n.pid,otherBoot:n.bootId})}return!0}_saveAudit(){if(this._auditEnabled)try{this._pruneAuditFiles(),this._audit.updatedAt=(new Date).toISOString(),this._audit.writer={pid:process.pid,bootId:this._bootId,heartbeat:Date.now()};const t=`${this._auditPath}.tmp`;e.writeFileSync(t,JSON.stringify(this._audit,null,1)),e.renameSync(t,this._auditPath)}catch(t){this.emit("warn",t)}}_pruneAuditFiles(){const t=this._audit.files,e=Object.keys(t);if(e.length<=600)return;const s=this._path&&i.basename(this._path),n=t=>t.archivedAt??t.closedAt??t.abandonedAt??t.openedAt??"",a=e.filter(e=>e!==s&&"active"!==t[e].state).sort((e,i)=>n(t[e]).localeCompare(n(t[i]))),r=e.length-600+50;for(const e of a.slice(0,r))delete t[e]}_aevent(e,i={}){if(!this._auditEnabled)return;const s=this._audit.events;s.push({id:t(),t:(new Date).toISOString(),type:e,...i}),s.length>200&&s.splice(0,s.length-200)}_setActivePointer(t){this._audit.activeStamp=this._curStamp,this._audit.activeIndex=t}_demoteZombieActives(){const t=this._path&&i.basename(this._path);for(const[e,i]of Object.entries(this._audit.files))"active"===i.state&&e!==t&&(i.state="abandoned",i.abandonedAt=(new Date).toISOString(),this._aevent("abandon",{file:e}))}_reconcileWithDisk(){for(const t of Object.keys(this._audit.files)){const e=this._audit.files[t];if(e.gzipPending)continue;if(t.endsWith(".gz")){const s=h(i.join(this.dir,t)),n=t.slice(0,-3),a=h(i.join(this.dir,n));if(s)continue;if(a){delete this._audit.files[t],this._audit.files[n]={...e,state:"sealed"},this._aevent("archive-regressed",{file:t,backTo:n}),this.emit("warn",new Error(`archive ${t} missing but raw ${n} exists`));continue}this._aevent("vanished",{file:t}),delete this._audit.files[t];continue}const s=h(i.join(this.dir,t)),n=h(i.join(this.dir,`${t}.gz`));s||(n?this._finishArchivedRecord(t,!0):(this._aevent("vanished",{file:t}),delete this._audit.files[t]))}}async _sealOrphanedPastStamps(){if(!this.zippedArchive)return;const t=new RegExp("^"+v(this._tL)+"(.+?)"+v(this._tMid)+"(?:\\.\\d+)?"+v(this._tExt)+"(?:\\.gz)?$");let s=[];try{s=e.readdirSync(this.dir)}catch{return}for(const e of s){if(e.endsWith(".gz"))continue;if(e===(this._path&&i.basename(this._path)))continue;const s=t.exec(e)?.[1];if(!s||s>=this._curStamp)continue;const n=i.join(this.dir,e);if(!h(n)||0===r(n))continue;const a=`${n}.gz`;await o(n,a,t=>this.emit("warn",t),this._compressionLevel)&&(this._auditEnabled&&this._audit.files[e]?this._finishArchivedRecord(e,!0):(this._auditEnabled&&(this._audit.files[a]={state:"archived",gzipPending:!1,archivedAt:(new Date).toISOString()},this._aevent("seal-past",{file:a})),this.emit("archive",a)),await this._runSealHook(n,a))}}async _repairInterruptedGzips(){for(const[t,s]of Object.entries(this._audit.files)){if(!s.gzipPending)continue;const n=i.join(this.dir,t),a=`${n}.gz`;if(!h(n)){s.gzipPending=!1,s.state="lost",this._aevent("gzip-lost",{file:t});continue}if(h(a)){try{e.unlinkSync(n)}catch{}this._finishArchivedRecord(t,!0),await this._runSealHook(n,a);continue}await o(n,a,t=>this.emit("warn",t),this._compressionLevel)?(this._finishArchivedRecord(t,!0),await this._runSealHook(n,a)):(s.gzipPending=!1,s.state="kept-raw",this._aevent("gzip-failed",{file:t}))}}_finishArchivedRecord(t,e=!1){const s=this._audit.files[t];if(!s)return;if(s.gzipPending=!1,s.state="archived",s.archivedAt=(new Date).toISOString(),t.endsWith(".gz"))return this.emit("archive",i.join(this.dir,t)),void this._aevent("archive",{file:t,...e&&{repaired:e}});const n=`${t}.gz`;delete this._audit.files[t],this._audit.files[n]=s,this.emit("archive",i.join(this.dir,n)),this._aevent("archive",{file:n,...e&&{repaired:e}})}async _retire(){const t=this.maxFiles;if(!t&&this.maxTotalSize<=0)return;const s=this._path&&i.basename(this._path),n=s?.replace(/(?:\.\d+)?(?:\.log|)(?:\.gz)?$/,""),a=[i.basename(this._auditPath),`${i.basename(this._auditPath)}.tmp`,`${i.basename(this._auditPath)}.corrupt`,...this._symlinkPath?[i.basename(this._symlinkPath),`${i.basename(this._symlinkPath)}.tmp`]:[]];try{const r=(await e.promises.readdir(this.dir)).filter(t=>this._rxActive.test(t)&&t!==s&&!(n&&t.startsWith(n))&&!a.includes(t)),h=[];for(const t of r){const s=i.join(this.dir,t);try{const t=await e.promises.stat(s);h.push({full:s,mtimeMs:t.mtimeMs,size:t.size})}catch{}}h.sort((t,e)=>t.mtimeMs-e.mtimeMs);const o=new Set;if(null!=t?.days){const e=this._now()-864e5*t.days;for(const t of h)t.mtimeMs<e&&o.add(t.full)}if(null!=t?.count){const e=h.filter(t=>!o.has(t.full)),i=e.length-t.count;for(let t=0;t<i;t++)o.add(e[t].full)}if(this.maxTotalSize>0){const t=h.filter(t=>!o.has(t.full));let e=t.reduce((t,e)=>t+e.size,0),i=0;for(;e>this.maxTotalSize&&i<t.length;)o.add(t[i].full),e-=t[i].size,i++}if(0===o.size)return;for(const t of o)await e.promises.unlink(t).catch(()=>{}),this._auditEnabled&&(delete this._audit.files[i.basename(t)],this._aevent("deleted",{file:i.basename(t)})),this.emit("deleted",t);this._saveAudit()}catch(t){this.emit("warn",t)}}}function x(t){return new g(t)}const S=new Set;let k=!1,z=!1;function A(t,e={}){const i=Array.isArray(t)?t:[t];for(const t of i)S.add(t);if(!k){k=!0;const t=e.timeoutMs??5e3,i=e.code??0;for(const s of e.signals??["SIGINT","SIGTERM"])process.on(s,()=>{M(t).finally(()=>process.exit(i))})}return async()=>{await M(e.timeoutMs??5e3);for(const t of i)S.delete(t)}}async function M(t=5e3){if(z)return;z=!0;const e=setTimeout(()=>process.exit(1),t);e.unref();try{await Promise.allSettled([...S].map(t=>new Promise(e=>t.end(()=>e()))))}finally{clearTimeout(e),z=!1}}export{g as RotateFileStream,x as createStream,M as flushAll,A as installShutdown};
1
+ /*! logroller v3.0.0 | MIT License */
2
+ export{R as RotateFileStream,c as createStream}from"./rotate-file-stream.esm.js";import"node:crypto";import"node:fs";import"node:path";import"node:stream";import"node:zlib";import"node:stream/promises";const e=new Set;let t=!1,o=null;function r(o,r={}){const n=Array.isArray(o)?o:[o];for(const t of n)e.add(t);if(!t){t=!0;const e=r.timeoutMs??5e3,o=r.code??0;for(const t of r.signals??["SIGINT","SIGTERM"])process.on(t,()=>{s(e).then(e=>process.exit(e?o:1))})}return async()=>{await s(r.timeoutMs??5e3);for(const t of n)e.delete(t)}}function s(t=5e3){return o??=(async()=>{let r;try{const o=Promise.allSettled([...e].map(e=>new Promise(t=>{e.once("close",t),e.end(()=>t()),e.destroyed&&t()}))).then(()=>!0),s=new Promise(e=>{r=setTimeout(()=>e(!1),t),r.unref()});return await Promise.race([o,s])}finally{clearTimeout(r);for(const t of e)(t.writableFinished||t.destroyed)&&e.delete(t);o=null}})(),o}export{s as flushAll,r as installShutdown};
package/dist/pino.cjs ADDED
@@ -0,0 +1,2 @@
1
+ /*! logroller v3.0.0 | MIT License */
2
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("./rotate-file-stream.cjs");require("node:crypto"),require("node:fs"),require("node:path"),require("node:stream"),require("node:zlib"),require("node:stream/promises"),exports.default=function(r){return e.createStream(r)};
@@ -0,0 +1,6 @@
1
+ import { RotateFileStreamOptions, RotateFileStream } from './index.cjs';
2
+ import 'node:stream';
3
+
4
+ declare function pinoTransport(options: RotateFileStreamOptions): RotateFileStream;
5
+
6
+ export { pinoTransport as default };
package/dist/pino.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { RotateFileStreamOptions, RotateFileStream } from './index.js';
2
+ import 'node:stream';
3
+
4
+ declare function pinoTransport(options: RotateFileStreamOptions): RotateFileStream;
5
+
6
+ export { pinoTransport as default };
@@ -0,0 +1,2 @@
1
+ /*! logroller v3.0.0 | MIT License */
2
+ import{c as o}from"./rotate-file-stream.esm.js";import"node:crypto";import"node:fs";import"node:path";import"node:stream";import"node:zlib";import"node:stream/promises";function t(t){return o(t)}export{t as default};
@@ -0,0 +1,2 @@
1
+ /*! logroller v3.0.0 | MIT License */
2
+ "use strict";var t=require("node:crypto"),e=require("node:fs"),i=require("node:path"),s=require("node:stream"),a=require("node:zlib"),n=require("node:stream/promises");const r=t=>{try{return"string"==typeof t?e.statSync(t).size:0}catch{return 0}},h=t=>{try{return e.existsSync(t)}catch{return!1}};async function o(t,i,s,r=6){try{return await n.pipeline(e.createReadStream(t),a.createGzip({level:r}),e.createWriteStream(i)),await e.promises.unlink(t),!0}catch(t){return await e.promises.unlink(i).catch(()=>{}),s?.(t),!1}}const d=t=>t?new Promise(e=>t.end(()=>e())):Promise.resolve(),c={b:1,k:1024,kb:1024,m:1048576,mb:1048576,g:1073741824,gb:1073741824},u={ms:1,s:1e3,m:6e4,h:36e5,hour:36e5,hours:36e5};function l(t,e,i){if("number"==typeof t)return Math.round(t*u[e]);const s=/^\s*(-?\d+(?:\.\d+)?)\s*(h(?:our)?s?|ms|s|m)?\s*$/i.exec(t);if(!s)throw new Error(`Invalid ${i} "${t}"`);const a=(s[2]??e).toLowerCase(),n=u[a];if(void 0===n)throw new Error(`Invalid ${i} unit "${s[2]}" in "${t}"`);return Math.round(Number.parseFloat(s[1])*n)}function _(t){if(null==t||0===t)return 0;if("number"==typeof t)return t;const e=/^\s*(\d+(?:\.\d+)?)\s*(b|kb|k|mb|m|gb|g)?\s*$/i.exec(t);if(!e)throw new Error(`Invalid maxSize "${t}"`);return Math.round(Number.parseFloat(e[1])*(c[e[2]?.toLowerCase()]??1))}const m={ms:1,s:1e3,m:6e4,h:36e5};const p=new Map;function f(t){let e=p.get(t);if(!e){try{e=new Intl.DateTimeFormat("en-US",{timeZone:t,hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"})}catch{throw new Error(`Invalid timeZone "${t}"`)}p.set(t,e)}return e}function w(t,e){const i={};for(const s of f(t).formatToParts(e))i[s.type]=s.value;return i}const v=t=>t.padStart(2,"0");function y(t){return t.replace(/[.*+?^${}()|[\]\\]/g,String.raw`\$&`)}function b(t,e){const i=w(t,e);return Date.UTC(+i.year,+i.month-1,+i.day,+i.hour,+i.minute,+i.second)-(e.getTime()-e.getTime()%1e3)}class S extends s.Writable{tz;template;dir;datePattern;zippedArchive;maxSize;maxFiles;freq;unrefTimers;maxTotalSize;_tL;_tMid;_tExt;_rxFor;_rxActive;_auditEnabled;_auditPath;_bootId=t.randomUUID();_onSeal;_sealHookTimeoutMs;_compressionLevel;_symlinkPath=null;_offsetMs;_stepMs;_bumpEvery;_curStamp;_index;_ws=null;_path=null;_bytes=0;_timer=null;_bumpTimer=null;_beatTimer;_ended=!1;_tail=Promise.resolve();_warnedBackward=!1;_stampSec=Number.NaN;_stampVal="";_audit;constructor(t={}){super(t.highWaterMark?{highWaterMark:t.highWaterMark}:{});const s=t.filename;if("string"!=typeof s||!s.includes("%DATE%"))throw new Error("opts.filename must contain '%DATE%' (e.g. 'logs/app-%DATE%.log')");var a;this.tz=t.tz||"UTC",f(this.tz),this.template=i.basename(s),this.dir=i.dirname(s),this.datePattern=t.datePattern||"YYYY-MM-DD",this.zippedArchive=!!t.zippedArchive,this.maxSize=_(t.maxSize??0),this.maxFiles=function(t){if(null==t||""===t||0===t)return null;if("number"==typeof t){if(t<0)throw new Error(`Invalid maxFiles "${t}"`);return{count:t}}const e=/^\s*(\d+)\s*d\s*$/i.exec(t);if(e)return 0===Number(e[1])?null:{days:Number.parseInt(e[1],10)};const i=/^\s*(\d+)\s*$/.exec(t);if(i)return 0===Number(i[1])?null:{count:Number.parseInt(i[1],10)};throw new Error(`Invalid maxFiles "${t}"`)}(t.maxFiles),this.maxTotalSize=_(t.maxTotalSize??0),this.freq=function(t){if(null==t)return{type:"daily"};if("daily"===t)return{type:"daily"};if("number"==typeof t){if(t<=0)throw new Error(`Invalid frequency "${t}"`);return{type:"interval",ms:t}}const e=/^\s*(\d+)\s*(ms|s|m|h)\s*$/i.exec(t);if(!e)throw new Error(`Invalid frequency "${t}"`);return{type:"interval",ms:Number.parseInt(e[1],10)*m[e[2].toLowerCase()]}}(t.frequency),this.unrefTimers=!!t.unrefTimers,this._offsetMs=null==(a=t.addHours)||""===a?0:l(a,"h","addHours"),this._stepMs=function(t){return null==t||""===t?0:l(t,"h","addHoursEveryMin")}(t.addHoursEveryMin),this._bumpEvery=Math.max(50,t.clockStepIntervalMs??6e4),this._compressionLevel=Math.min(9,Math.max(0,t.compressionLevel??6)),this._onSeal=t.onSeal,this._sealHookTimeoutMs=Math.max(0,t.sealHookTimeoutMs??3e4);const n=this.template.indexOf("%DATE%"),r=this.template.slice(n+6),h=r.lastIndexOf(".");this._tL=this.template.slice(0,n),this._tMid=h>=0?r.slice(0,h):"",this._tExt=h>=0?r.slice(h):"",this._rxFor=t=>new RegExp("^"+y(this._tL)+t+y(this._tMid)+"(?:\\.(\\d+))?"+y(this._tExt)+"(?:\\.gz)?$"),this._rxActive=this._rxFor("("+this.datePattern.split(/(YYYY|YY|MM|DD|HH|mm|ss)/).map((t,e)=>e%2?"YYYY"===t?"\\d{4}":"\\d{2}":y(t)).join("")+")"),this._auditEnabled=!1!==t.audit;const o=(this._tL.replace(/[^\w.-]+/g,"")||"log").replace(/^[-.]+|[-.]+$/g,"");var d,c,u,p;this._auditPath=i.join(this.dir,t.auditFile||`${o}_audit.json`),this._symlinkPath=t.symlink?i.join(this.dir,"string"==typeof t.symlink?t.symlink:`${o}-current.log`):null,e.mkdirSync(this.dir,{recursive:!0}),this._curStamp=this._stampNow(),this._audit=(d=this._tL,c=this._tExt,u=this.datePattern,p=this.tz,{version:1,prefix:d,ext:c,datePattern:u,tz:p,createdAt:null,updatedAt:null,activeStamp:null,activeIndex:null,writer:null,files:{},events:[]});const w=this._auditEnabled&&this._loadAudit();this._index=this._recoverIndexFor(this._curStamp);const v=this._audit.activeIndex;w&&null!==v&&this._audit.activeStamp===this._curStamp&&(this._index=Math.max(this._index,v)),0===this._offsetMs&&0===this._stepMs||process.nextTick(()=>this.emit("clock",{addHours:t.addHours??void 0,addHoursEveryMin:t.addHoursEveryMin??void 0,offsetMs:this._offsetMs})),process.nextTick(()=>{const e=t=>this.emit("warn",new Error(t));"interval"===this.freq.type?(this.freq.ms<864e5&&!this.datePattern.includes("HH")&&e(`frequency "${t.frequency}" needs HH in datePattern — segments would collapse onto one filename`),this.freq.ms<6e4&&!this.datePattern.includes("mm")&&e(`frequency "${t.frequency}" needs mm in datePattern`)):this.datePattern.includes("DD")||e("daily frequency needs DD in datePattern")}),this._job(async()=>{await this._sealOrphanedPastStamps(),this._auditEnabled&&(this._demoteZombieActives(),this._reconcileWithDisk(),await this._repairInterruptedGzips(),this._aevent("boot",{stamp:this._curStamp,index:this._index}),this._saveAudit()),await this._retire()}).catch(()=>{}),this._armTimer(),this._armBump(),this._auditEnabled&&(this._beatTimer=setInterval(()=>this._job(()=>this._saveAudit()).catch(()=>{}),1e4),this._beatTimer.unref()),this.once("close",()=>{this._stopTimer(),this._stopBump(),clearInterval(this._beatTimer)})}on(t,e){return super.on(t,e)}once(t,e){return super.once(t,e)}emit(t,...e){return super.emit(t,...e)}get filename(){return this._path}get currentIndex(){return this._index}rotateNow(){return this._job(()=>this._rotate("manual"))}advanceClock(){return this._job(()=>this._applyBump())}listSegments(){const t=this._rxActive,s=this._symlinkPath&&i.basename(this._symlinkPath),a=[];let n=[];try{n=e.readdirSync(this.dir)}catch{return a}for(const e of n){if(e===s)continue;const n=t.exec(e);if(!n)continue;const h=this._audit.files[e];a.push({file:i.join(this.dir,e),stamp:n[1],index:n[2]?Number.parseInt(n[2],10):0,gzipped:e.endsWith(".gz"),bytes:r(i.join(this.dir,e)),state:h?.state??"untracked",openedAt:h?.openedAt,archivedAt:h?.archivedAt})}return a.sort((t,e)=>t.stamp.localeCompare(e.stamp)||t.index-e.index),a}stats(){const t=this.listSegments();return{dir:this.dir,currentStamp:this._curStamp,activeFile:this._path,activeIndex:this._index,segments:t.length,archived:t.filter(t=>t.gzipped).length,totalBytes:t.reduce((t,e)=>t+e.bytes,0),clockOffsetMs:this._offsetMs,symlink:this._symlinkPath}}_write(t,e,i){this._writeBatch([t],i)}_writev(t,e){this._writeBatch(t.map(t=>t.chunk),e)}_writeBatch(t,e){this._ended?process.nextTick(e,new Error("write after end")):this._job(async()=>{try{this._stampAdvanced(this._stampNow())&&await this._rotate("time"),await this._ensureOpen();let i=[],s=0;for(const e of t){const t=this._bytes+s;this.maxSize>0&&t>0&&t+e.length>this.maxSize&&(await this._flush(i,s),i=[],s=0,await this._rotate("size"),await this._ensureOpen()),i.push(e),s+=e.length}await this._flush(i,s),e()}catch(t){e(t)}}).catch(()=>{})}async _flush(t,e){if(!e)return;const i=this._ws;if(!i)throw new Error("segment write stream not open");const s=1===t.length?t[0]:Buffer.concat(t,e);await new Promise((t,e)=>i.write(s,i=>i?e(i):t())),this._bytes+=e}_final(t){this._ended=!0,this._stopTimer(),this._stopBump(),clearInterval(this._beatTimer),this._job(async()=>{await d(this._ws);const t=this._path&&i.basename(this._path);if(t&&this._audit.files[t]){const e=this._audit.files[t];e.state="closed",e.closedAt=(new Date).toISOString(),e.bytes=r(this._path),this._aevent("close",{file:t}),this._saveAudit()}this._ws=null,this._path=null}).catch(()=>{}).finally(()=>t())}_destroy(t,e){this._ended=!0,this._stopTimer(),this._stopBump(),clearInterval(this._beatTimer),this._ws?.destroy(),this._ws=null,e(t)}async[Symbol.asyncDispose](){this.destroyed||this.writableFinished||await new Promise(t=>this.end(()=>t()))}_job(t){const e=this._tail.then(t,t);return this._tail=e.catch(()=>{}),e}_now(){return Date.now()+this._offsetMs}_stampNow(){const t=Math.floor(this._now()/1e3);var e,i;return t!==this._stampSec&&(this._stampSec=t,this._stampVal=(e=this.datePattern,i=w(this.tz,new Date(1e3*t)),e.replace(/YYYY|YY|MM|DD|HH|mm|ss/g,t=>{switch(t){case"YYYY":return i.year;case"YY":return i.year.slice(-2);case"MM":return v(i.month);case"DD":return v(i.day);case"HH":return v(i.hour);case"mm":return v(i.minute);case"ss":return v(i.second);default:return t}}))),this._stampVal}_resolveName(t,e){return i.join(this.dir,this._tL+t+this._tMid+(e?`.${e}`:"")+this._tExt)}_stampAdvanced(t){return t>this._curStamp||(t<this._curStamp&&!this._warnedBackward&&(this._warnedBackward=!0,this.emit("warn",new Error(`clock moved backward (stamp ${t} < ${this._curStamp}); keeping current segment until the clock catches up`))),!1)}_recoverIndexFor(t){const i=this._rxFor(y(t));let s=-1,a=-1,n=[];try{n=e.readdirSync(this.dir)}catch{}for(const t of n){const e=i.exec(t);if(!e)continue;const n=e[1]?Number.parseInt(e[1],10):0;n>s&&(s=n),!t.endsWith(".gz")&&n>a&&(a=n)}let r=a>=0?a:s>=0?s+1:0;const h=this._audit.activeIndex;return this._auditEnabled&&null!==h&&this._audit.activeStamp===t&&(r=Math.max(r,h)),r}_updateSymlink(){const t=this._symlinkPath;if(!t||!this._path)return;const s=`${t}.tmp`;try{try{e.unlinkSync(s)}catch{}e.symlinkSync(i.basename(this._path),s,"file"),e.renameSync(s,t)}catch(e){this._symlinkPath=null,this.emit("warn",new Error(`symlink unavailable ("${t}"): ${e.message} — continuing without it`))}}async _runSealHook(t,e){const s=this._onSeal;if(!s)return;let a;try{await Promise.race([Promise.resolve().then(()=>s({raw:t,gz:e})),new Promise((s,n)=>{a=setTimeout(()=>n(new Error(`onSeal hook timed out after ${this._sealHookTimeoutMs}ms (${i.basename(e??t)})`)),this._sealHookTimeoutMs)})])}catch(t){this.emit("warn",t)}finally{a&&clearTimeout(a)}}async _ensureOpen(){if(this._ws)return;const t=this._resolveName(this._curStamp,this._index);this._bytes=r(t);const s=e.createWriteStream(t,{flags:"a"});if(s.on("error",t=>this.destroy(t)),this._ws=s,this._path=t,this._updateSymlink(),this._auditEnabled){const e=i.basename(t);this._audit.files[e]={index:this._index,state:"active",gzipPending:!1,openedAt:(new Date).toISOString()},this._setActivePointer(this._index),this._aevent("open",{file:e,index:this._index,resumedBytes:this._bytes}),this._saveAudit()}this.emit("open",t)}async _rotate(t){const s=this._path,a=this._ws;this._ws=null,this._path=null,this._bytes=0;let n=null;if(a&&s){await d(a);const t=r(s),c=i.basename(s),u=this._auditEnabled?this._audit.files[c]:void 0;if(this.zippedArchive&&t>0){n=`${s}.gz`,u&&(u.gzipPending=!0,u.bytes=t,this._saveAudit());await o(s,n,t=>this.emit("warn",t),this._compressionLevel)?(this._finishArchivedRecord(c),await this._runSealHook(s,n)):(u&&(u.gzipPending=!1,u.state="kept-raw"),this._aevent("gzip-failed",{file:c}),n=null,await this._runSealHook(s,null))}else if(0===t){const t=`${s}.gz`;if(h(t))this._finishArchivedRecord(c),n=t,await this._runSealHook(s,t);else{try{e.unlinkSync(s)}catch{}u&&delete this._audit.files[c],this._aevent("drop-empty",{file:c})}}else t>0&&(u&&(u.state="sealed",u.bytes=t),await this._runSealHook(s,null))}let c=this._stampNow();c<this._curStamp&&(this._warnedBackward||(this._warnedBackward=!0,this.emit("warn",new Error(`clock moved backward (stamp ${c} < ${this._curStamp}); rotation clamped to the current period`))),c=this._curStamp);const u=c!==this._curStamp;this._curStamp=c,this._index=u||"time"===t?this._recoverIndexFor(c):this._index+1,this._bytes=0,this._auditEnabled&&(this._setActivePointer(this._index),this._aevent("rotate",{reason:t,nextIndex:this._index,stamp:c}),this._saveAudit()),"time"!==t&&"manual"!==t||this._armTimer(),s&&this.emit("rotated",{reason:t,oldFile:s,archive:n,newFile:this._resolveName(c,this._index)}),await this._retire()}_hasContent(){return this._ws?this._bytes>0||r(this._path)>0:!!(this._path&&r(this._path)>0)}_armTimer(){if(this._stopTimer(),this._ended)return;const t=this._now();var e,i;const s=("daily"===this.freq.type?function(t,e){const i=w(t,new Date(e)),s=Date.UTC(+i.year,+i.month-1,+i.day+1);let a=s-b(t,new Date(s));for(let e=0;e<3;e++){const e=s-b(t,new Date(a));if(e===a)break;a=e}return a<=e&&(a+=864e5),a}(this.tz,t):(e=t,i=this.freq.ms,Math.floor(e/i)*i+i))-this._now(),a=s<0?1e3:Math.max(100,s);this._timer=setTimeout(()=>{this._job(async()=>{const t=this._stampNow();"daily"!==this.freq.type?this._hasContent()&&await this._rotate("time"):await this._crossPeriod(t),this._armTimer()}).catch(()=>{})},a),this.unrefTimers&&this._timer.unref()}async _crossPeriod(t,e=!1){this._stampAdvanced(t)&&(this._hasContent()?await this._rotate("time"):(this._curStamp=t,this._index=this._recoverIndexFor(t),this._auditEnabled&&(this._setActivePointer(this._index),this._aevent("period",e?{stamp:t,virtual:e}:{stamp:t}),this._saveAudit()),this.emit("period",t)))}_armBump(){this._stopBump(),this._stepMs&&!this._ended&&(this._bumpTimer=setInterval(()=>this._job(()=>this._applyBump()).catch(()=>{}),this._bumpEvery),this.unrefTimers&&this._bumpTimer.unref())}_stopBump(){this._bumpTimer&&(clearInterval(this._bumpTimer),this._bumpTimer=null)}async _applyBump(){if(this._ended)return;this._offsetMs+=this._stepMs,this._armTimer();const t=this._stampNow();await this._crossPeriod(t,!0),this._auditEnabled&&(this._aevent("clock-step",{addedMsPerMin:this._stepMs,offsetMs:this._offsetMs,stamp:t}),this._saveAudit())}_stopTimer(){this._timer&&(clearTimeout(this._timer),this._timer=null)}_loadAudit(){let i;try{i=JSON.parse(e.readFileSync(this._auditPath,"utf8"))}catch{try{e.renameSync(this._auditPath,`${this._auditPath}.corrupt`)}catch{}return!1}const s=i;if(!s||1!==s.version||s.prefix!==this._tL||s.ext!==this._tExt){try{e.renameSync(this._auditPath,`${this._auditPath}.corrupt`)}catch{}return!1}this._audit.createdAt=s.createdAt??(new Date).toISOString(),this._audit.activeStamp="string"==typeof s.activeStamp?s.activeStamp:null,this._audit.activeIndex=Number.isInteger(s.activeIndex)?s.activeIndex:null,this._audit.files=s.files&&"object"==typeof s.files?s.files:{},this._audit.events=Array.isArray(s.events)?s.events.slice(-200):[];const a=s.writer;if(a&&"string"==typeof a.bootId&&a.bootId!==this._bootId&&"number"==typeof a.heartbeat&&Date.now()-a.heartbeat<3e4){const e=new Error(`concurrent writer detected (pid ${a.pid}, boot ${a.bootId}); two processes share this log family and will race. If running 'node --watch', exclude the logs dir via --watch-path.`);this.emit("warn",e),this._audit.events.push({id:t.randomUUID(),t:(new Date).toISOString(),type:"concurrent-writer",otherPid:a.pid,otherBoot:a.bootId})}return!0}_saveAudit(){if(this._auditEnabled)try{this._pruneAuditFiles(),this._audit.updatedAt=(new Date).toISOString(),this._audit.writer={pid:process.pid,bootId:this._bootId,heartbeat:Date.now()};const t=`${this._auditPath}.tmp`;e.writeFileSync(t,JSON.stringify(this._audit,null,1)),e.renameSync(t,this._auditPath)}catch(t){this.emit("warn",t)}}_pruneAuditFiles(){const t=this._audit.files,e=Object.keys(t);if(e.length<=600)return;const s=this._path&&i.basename(this._path),a=t=>t.archivedAt??t.closedAt??t.abandonedAt??t.openedAt??"",n=e.filter(e=>e!==s&&"active"!==t[e].state).sort((e,i)=>a(t[e]).localeCompare(a(t[i]))),r=e.length-600+50;for(const e of n.slice(0,r))delete t[e]}_aevent(e,i={}){if(!this._auditEnabled)return;const s=this._audit.events;s.push({id:t.randomUUID(),t:(new Date).toISOString(),type:e,...i}),s.length>200&&s.splice(0,s.length-200)}_setActivePointer(t){this._audit.activeStamp=this._curStamp,this._audit.activeIndex=t}_demoteZombieActives(){const t=this._path&&i.basename(this._path);for(const[e,i]of Object.entries(this._audit.files))"active"===i.state&&e!==t&&(i.state="abandoned",i.abandonedAt=(new Date).toISOString(),this._aevent("abandon",{file:e}))}_reconcileWithDisk(){for(const t of Object.keys(this._audit.files)){const e=this._audit.files[t];if(e.gzipPending)continue;if(t.endsWith(".gz")){const s=h(i.join(this.dir,t)),a=t.slice(0,-3),n=h(i.join(this.dir,a));if(s)continue;if(n){delete this._audit.files[t],this._audit.files[a]={...e,state:"sealed"},this._aevent("archive-regressed",{file:t,backTo:a}),this.emit("warn",new Error(`archive ${t} missing but raw ${a} exists`));continue}this._aevent("vanished",{file:t}),delete this._audit.files[t];continue}const s=h(i.join(this.dir,t)),a=h(i.join(this.dir,`${t}.gz`));s||(a?this._finishArchivedRecord(t,!0):(this._aevent("vanished",{file:t}),delete this._audit.files[t]))}}async _sealOrphanedPastStamps(){if(!this.zippedArchive)return;const t=this._rxActive;let s=[];try{s=e.readdirSync(this.dir)}catch{return}for(const e of s){if(e.endsWith(".gz"))continue;if(e===(this._path&&i.basename(this._path)))continue;const s=t.exec(e)?.[1];if(!s||s>=this._curStamp)continue;const a=i.join(this.dir,e);if(!h(a)||0===r(a))continue;const n=`${a}.gz`;await o(a,n,t=>this.emit("warn",t),this._compressionLevel)&&(this._auditEnabled&&this._audit.files[e]?this._finishArchivedRecord(e,!0):(this._auditEnabled&&(this._audit.files[n]={state:"archived",gzipPending:!1,archivedAt:(new Date).toISOString()},this._aevent("seal-past",{file:n})),this.emit("archive",n)),await this._runSealHook(a,n))}}async _repairInterruptedGzips(){for(const[t,s]of Object.entries(this._audit.files)){if(!s.gzipPending)continue;const a=i.join(this.dir,t),n=`${a}.gz`;if(!h(a)){s.gzipPending=!1,s.state="lost",this._aevent("gzip-lost",{file:t});continue}if(h(n)){try{e.unlinkSync(a)}catch{}this._finishArchivedRecord(t,!0),await this._runSealHook(a,n);continue}await o(a,n,t=>this.emit("warn",t),this._compressionLevel)?(this._finishArchivedRecord(t,!0),await this._runSealHook(a,n)):(s.gzipPending=!1,s.state="kept-raw",this._aevent("gzip-failed",{file:t}))}}_finishArchivedRecord(t,e=!1){const s=this._audit.files[t];if(!s)return;if(s.gzipPending=!1,s.state="archived",s.archivedAt=(new Date).toISOString(),t.endsWith(".gz"))return this.emit("archive",i.join(this.dir,t)),void this._aevent("archive",{file:t,...e&&{repaired:e}});const a=`${t}.gz`;delete this._audit.files[t],this._audit.files[a]=s,this.emit("archive",i.join(this.dir,a)),this._aevent("archive",{file:a,...e&&{repaired:e}})}async _retire(){const t=this.maxFiles;if(!t&&this.maxTotalSize<=0)return;const s=i.basename(this._resolveName(this._curStamp,this._index)),a=[i.basename(this._auditPath),`${i.basename(this._auditPath)}.tmp`,`${i.basename(this._auditPath)}.corrupt`,...this._symlinkPath?[i.basename(this._symlinkPath),`${i.basename(this._symlinkPath)}.tmp`]:[]];try{const n=(await e.promises.readdir(this.dir)).filter(t=>this._rxActive.test(t)&&t!==s&&!a.includes(t)),r=[];for(const t of n){const s=i.join(this.dir,t);try{const t=await e.promises.stat(s);r.push({full:s,mtimeMs:t.mtimeMs,size:t.size})}catch{}}r.sort((t,e)=>t.mtimeMs-e.mtimeMs);const h=new Set;if(null!=t?.days){const e=this._now()-864e5*t.days;for(const t of r)t.mtimeMs<e&&h.add(t.full)}if(null!=t?.count){const e=r.filter(t=>!h.has(t.full)),i=e.length-t.count;for(let t=0;t<i;t++)h.add(e[t].full)}if(this.maxTotalSize>0){const t=r.filter(t=>!h.has(t.full));let e=t.reduce((t,e)=>t+e.size,0),i=0;for(;e>this.maxTotalSize&&i<t.length;)h.add(t[i].full),e-=t[i].size,i++}if(0===h.size)return;for(const t of h)await e.promises.unlink(t).catch(()=>{}),this._auditEnabled&&(delete this._audit.files[i.basename(t)],this._aevent("deleted",{file:i.basename(t)})),this.emit("deleted",t);this._saveAudit()}catch(t){this.emit("warn",t)}}}exports.RotateFileStream=S,exports.createStream=function(t){return new S(t)};
@@ -0,0 +1,350 @@
1
+ import { Writable } from 'node:stream';
2
+
3
+ /**
4
+ * logroller — public types.
5
+ *
6
+ * Everything exported here is part of the stable API surface.
7
+ */
8
+ type RotationReason = "time" | "size" | "manual";
9
+ /** Options accepted by {@linkcode RotateFileStream} and `createStream`. */
10
+ interface RotateFileStreamOptions {
11
+ /**
12
+ * Destination filename pattern. REQUIRED and must contain the `%DATE%`
13
+ * token.
14
+ * @example "logs/app-%DATE%.log"
15
+ */
16
+ filename: string;
17
+ /** Tokens: `YYYY YY MM DD HH mm ss`. Must be coarse-or-equal to your
18
+ * rotation frequency, or segments collapse onto one name.
19
+ * @default "YYYY-MM-DD" */
20
+ datePattern?: string;
21
+ /** Gzip closed segments (`name.log` → `name.log.gz`).
22
+ * @default false */
23
+ zippedArchive?: boolean;
24
+ /**
25
+ * gzip level 0–9. 0 = stored (fastest, no ratio), 9 = best ratio,
26
+ * 6 = default library default. Ignored when `zippedArchive` is false.
27
+ * @default 6
28
+ */
29
+ compressionLevel?: number;
30
+ /**
31
+ * Roll when the next write would exceed this size per segment.
32
+ * `"20m"`, `"512k"`, `"2g"`, or raw bytes. `0` disables.
33
+ * @default 0 (unlimited)
34
+ */
35
+ maxSize?: number | string;
36
+ /**
37
+ * Retention by age or count:
38
+ * - `"90d"` — delete archived files older than 90 days (mtime)
39
+ * - `500` — keep at most the newest 500 archived files
40
+ * Unset, `0`, `"0"` or `"0d"` means never auto-delete.
41
+ */
42
+ maxFiles?: number | string;
43
+ /**
44
+ * Retention by total byte budget across archived files (the active
45
+ * segment is never counted). When archives exceed the budget, the
46
+ * OLDEST are deleted until under budget. Composes with `maxFiles`
47
+ * (either policy may delete). `"5g"`, `"500m"`, or raw bytes.
48
+ * Unset/0 = no byte budget.
49
+ */
50
+ maxTotalSize?: number | string;
51
+ /** IANA timezone for stamps and the daily boundary.
52
+ * @default "UTC" */
53
+ tz?: string;
54
+ /** `"daily"` (local midnight in `tz`) or interval `"30s"`/`"5m"`/`"2h"`
55
+ * /ms. Interval boundaries align to epoch multiples.
56
+ * @default "daily" */
57
+ frequency?: number | string;
58
+ /**
59
+ * TEST ONLY. Static shift of the internal clock. `"-6h"`, `"24h"`,
60
+ * plain/fractional number, or `"30m"`/`"90s"`.
61
+ */
62
+ addHours?: number | string;
63
+ /**
64
+ * TEST ONLY. Virtual time added per tick (see `clockStepIntervalMs`).
65
+ * Negative values exercise the backward-clock guard (no rotations).
66
+ */
67
+ addHoursEveryMin?: number | string;
68
+ /** Real interval between virtual-clock ticks.
69
+ * @default 60_000 */
70
+ clockStepIntervalMs?: number;
71
+ /**
72
+ * Maintain a stable "current" pointer for log collectors
73
+ * (`tail -f`, Filebeat, promtail). `true` derives the name from the
74
+ * prefix (`"app-"` → `app-current.log`); a string names it exactly.
75
+ * The link is updated atomically each time a segment opens. Best-effort:
76
+ * on filesystems denying symlinks (Windows without Developer Mode) it
77
+ * warns once and disables itself.
78
+ * @default false */
79
+ symlink?: boolean | string;
80
+ /**
81
+ * Awaited after a segment is sealed/archived and BEFORE retention can
82
+ * delete it — the extension point for uploading to S3/GCS. Errors and
83
+ * timeouts degrade to a `warn`; they never break rotation.
84
+ * @default undefined */
85
+ onSeal?: (info: SealInfo) => Promise<void> | void;
86
+ /** Hard deadline for one `onSeal` invocation.
87
+ * @default 30_000 */
88
+ sealHookTimeoutMs?: number;
89
+ /** Detach rotation timers from the event loop.
90
+ * @default false */
91
+ unrefTimers?: boolean;
92
+ /** Maintain `<stem>_audit.json` (library-owned; treat as read-only).
93
+ * @default true */
94
+ audit?: boolean;
95
+ /** Override the audit manifest filename. */
96
+ auditFile?: string;
97
+ /** Passed through to the underlying `Writable`. */
98
+ highWaterMark?: number;
99
+ }
100
+ /** Payload delivered to `onSeal` when a segment completes its lifecycle. */
101
+ interface SealInfo {
102
+ /** Closed segment path. May already be unlinked when `gz` exists. */
103
+ raw: string;
104
+ /** Compressed artifact; `null` when compression is off or failed. */
105
+ gz: string | null;
106
+ }
107
+ /** One file of the stream family, as reported by `listSegments()`. */
108
+ interface SegmentInfo {
109
+ file: string;
110
+ /** Date stamp embedded in the filename. */
111
+ stamp: string;
112
+ /** Segment index within the stamp (0 = base file). */
113
+ index: number;
114
+ gzipped: boolean;
115
+ bytes: number;
116
+ /** Audit state, or `"untracked"` for files unknown to the manifest. */
117
+ state: string;
118
+ openedAt?: string;
119
+ archivedAt?: string;
120
+ }
121
+ /** Snapshot for dashboards/health checks — see `stats()`. */
122
+ interface StreamStats {
123
+ dir: string;
124
+ currentStamp: string;
125
+ activeFile: string | null;
126
+ activeIndex: number;
127
+ /** All family files on disk (active included). */
128
+ segments: number;
129
+ /** Of those, how many are compressed archives. */
130
+ archived: number;
131
+ totalBytes: number;
132
+ /** Active virtual-clock shift (non-zero only under test knobs). */
133
+ clockOffsetMs: number;
134
+ /** Active symlink path, or `null`. */
135
+ symlink: string | null;
136
+ }
137
+ /** Payload for the `rotated` event. */
138
+ interface RotatedInfo {
139
+ reason: RotationReason;
140
+ oldFile: string | null;
141
+ archive: string | null;
142
+ newFile: string;
143
+ }
144
+ /** Payload for the `clock` event. */
145
+ interface ClockInfo {
146
+ addHours?: number | string;
147
+ addHoursEveryMin?: number | string;
148
+ offsetMs: number;
149
+ }
150
+ /** Tuple-style event map typing `on` / `once` / `emit`. */
151
+ interface RotateFileStreamEventMap {
152
+ open: [file: string];
153
+ rotated: [info: RotatedInfo];
154
+ archive: [gzFile: string];
155
+ deleted: [file: string];
156
+ period: [stamp: string];
157
+ clock: [info: ClockInfo];
158
+ warn: [err: Error];
159
+ error: [err: Error];
160
+ close: [];
161
+ }
162
+
163
+ interface ParsedMaxFiles {
164
+ days?: number;
165
+ count?: number;
166
+ }
167
+ type ParsedFrequency = {
168
+ readonly type: "daily";
169
+ } | {
170
+ readonly type: "interval";
171
+ ms: number;
172
+ };
173
+
174
+ /**
175
+ * A `Writable` that appends to dated log files, rolling segments on time
176
+ * and/or size, optionally gzipping sealed segments, enforcing retention
177
+ * (count, age, and total-byte budget), maintaining a stable "current"
178
+ * symlink for log collectors, running an awaited seal hook (upload
179
+ * pipeline), and resuming an unbroken series across restarts via disk
180
+ * scan + audit manifest.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * const log = createStream({
185
+ * filename: "logs/app-%DATE%.log",
186
+ * maxSize: "20m",
187
+ * maxTotalSize: "5g",
188
+ * maxFiles: "90d",
189
+ * zippedArchive: true,
190
+ * symlink: true,
191
+ * onSeal: async ({ gz }) => uploadToS3(gz),
192
+ * });
193
+ * log.write("hello\n"); // or: readable.pipe(log)
194
+ * ```
195
+ */
196
+ declare class RotateFileStream extends Writable {
197
+ readonly tz: string;
198
+ readonly template: string;
199
+ readonly dir: string;
200
+ readonly datePattern: string;
201
+ readonly zippedArchive: boolean;
202
+ readonly maxSize: number;
203
+ readonly maxFiles: Readonly<ParsedMaxFiles> | null;
204
+ readonly freq: ParsedFrequency;
205
+ readonly unrefTimers: boolean;
206
+ readonly maxTotalSize: number;
207
+ private readonly _tL;
208
+ private readonly _tMid;
209
+ private readonly _tExt;
210
+ private readonly _rxFor;
211
+ private readonly _rxActive;
212
+ private readonly _auditEnabled;
213
+ private readonly _auditPath;
214
+ private readonly _bootId;
215
+ private readonly _onSeal?;
216
+ private readonly _sealHookTimeoutMs;
217
+ private readonly _compressionLevel;
218
+ private _symlinkPath;
219
+ private _offsetMs;
220
+ private _stepMs;
221
+ private _bumpEvery;
222
+ private _curStamp;
223
+ private _index;
224
+ private _ws;
225
+ private _path;
226
+ private _bytes;
227
+ private _timer;
228
+ private _bumpTimer;
229
+ private _beatTimer;
230
+ private _ended;
231
+ private _tail;
232
+ private _warnedBackward;
233
+ private _stampSec;
234
+ private _stampVal;
235
+ private _audit;
236
+ constructor(opts?: Partial<RotateFileStreamOptions>);
237
+ on<E extends keyof RotateFileStreamEventMap>(event: E, listener: (...args: RotateFileStreamEventMap[E]) => void): this;
238
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
239
+ once<E extends keyof RotateFileStreamEventMap>(event: E, listener: (...args: RotateFileStreamEventMap[E]) => void): this;
240
+ once(event: string | symbol, listener: (...args: any[]) => void): this;
241
+ emit<E extends keyof RotateFileStreamEventMap>(event: E, ...args: RotateFileStreamEventMap[E]): boolean;
242
+ emit(event: string | symbol, ...args: unknown[]): boolean;
243
+ /** Absolute path of the segment currently written; `null` until first write. */
244
+ get filename(): string | null;
245
+ /** Index of the active segment within the current period. */
246
+ get currentIndex(): number;
247
+ /** Force an immediate rotation and resolve when the next slot is staged. */
248
+ rotateNow(): Promise<void>;
249
+ /**
250
+ * Apply ONE virtual-clock tick immediately (bypasses the interval timer
251
+ * for deterministic tests). Crossing into a new period triggers the same
252
+ * archive flow as a real midnight roll.
253
+ */
254
+ advanceClock(): Promise<void>;
255
+ /**
256
+ * Every family file on disk, oldest stamp first.
257
+ * Disk is truth: untracked files are reported with state "untracked".
258
+ */
259
+ listSegments(): SegmentInfo[];
260
+ /**
261
+ * Health-check snapshot for dashboards and support triage.
262
+ */
263
+ stats(): StreamStats;
264
+ _write(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
265
+ /** Lines buffered while the previous write was in flight arrive here as one batch. */
266
+ _writev(chunks: {
267
+ chunk: Buffer;
268
+ encoding: BufferEncoding;
269
+ }[], callback: (error?: Error | null) => void): void;
270
+ private _writeBatch;
271
+ private _flush;
272
+ _final(callback: (error?: Error | null) => void): void;
273
+ _destroy(err: Error | null, callback: (error?: Error | null) => void): void;
274
+ /**
275
+ * `await using log = createStream(...)` — ends (flushes) the stream on scope exit.
276
+ * Node's default destroys instead, dropping buffered lines.
277
+ */
278
+ [Symbol.asyncDispose](): Promise<void>;
279
+ private _job;
280
+ private _now;
281
+ /** Stamps change at most once per second (ss is the finest token), so cache per second. */
282
+ private _stampNow;
283
+ private _resolveName;
284
+ /**
285
+ * true only when the period legally advanced. A regressed
286
+ * clock (NTP correction, manual change) never reopens a past family:
287
+ * writes continue in the current segment until real time catches up.
288
+ * Requires zero-padded datePattern tokens (documented assumption).
289
+ */
290
+ private _stampAdvanced;
291
+ /**
292
+ * Highest viable segment index for `stamp`.
293
+ * Disk is truth: resume a raw tail; start after sealed ones.
294
+ * The audit pointer (same stamp only) may only RAISE the result.
295
+ */
296
+ private _recoverIndexFor;
297
+ /**
298
+ * Atomically flip the "current" symlink to the active segment.
299
+ * tmp + rename so followers (`tail -f`, Filebeat) never observe a gap.
300
+ * Relative target keeps the link portable. Best-effort: filesystems
301
+ * denying symlinks warn once and disable the feature permanently.
302
+ */
303
+ private _updateSymlink;
304
+ /**
305
+ * Await the user's onSeal hook with a hard timeout. Result or
306
+ * failure degrades to `warn` — a wedged upload must never stall rotation.
307
+ * Called BEFORE retention so uploads finish before deletion.
308
+ */
309
+ private _runSealHook;
310
+ private _ensureOpen;
311
+ private _rotate;
312
+ private _hasContent;
313
+ private _armTimer;
314
+ /** Guarded period crossing: rotate if the segment has data, else slide with no file. */
315
+ private _crossPeriod;
316
+ private _armBump;
317
+ private _stopBump;
318
+ private _applyBump;
319
+ private _stopTimer;
320
+ private _loadAudit;
321
+ private _saveAudit;
322
+ private _pruneAuditFiles;
323
+ private _aevent;
324
+ private _setActivePointer;
325
+ /** Records left `active` by dead processes → `abandoned`. */
326
+ private _demoteZombieActives;
327
+ /** Heal audit-vs-disk divergence after crashes / external tampering. */
328
+ private _reconcileWithDisk;
329
+ /**
330
+ * DISK-DRIVEN seal. Seal raw segments whose period has
331
+ * already passed (crash near midnight, virtual-clock leftovers,
332
+ * untracked/pruned files). Iterates the DIRECTORY, not the manifest:
333
+ * the filesystem is the source of truth; the manifest is an index.
334
+ */
335
+ private _sealOrphanedPastStamps;
336
+ private _repairInterruptedGzips;
337
+ /** Seal a record; re-key `name` → `name.gz` so scans stop ghosting. */
338
+ private _finishArchivedRecord;
339
+ private _retire;
340
+ }
341
+ /**
342
+ * Convenience factory mirroring common rotator APIs.
343
+ *
344
+ * @example
345
+ * const log = createStream({ filename: "logs/app-%DATE%.log" });
346
+ */
347
+ declare function createStream(options: RotateFileStreamOptions): RotateFileStream;
348
+
349
+ export { RotateFileStream as R, createStream as g };
350
+ export type { ClockInfo as C, SealInfo as S, RotateFileStreamEventMap as a, RotateFileStreamOptions as b, RotatedInfo as c, RotationReason as d, SegmentInfo as e, StreamStats as f };