2026-07-31 12:54:46 +09:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
function createJsonRepository({fsp,crypto,processId=process.pid}={}){
|
|
|
|
|
if(!fsp?.readFile||!fsp?.writeFile||!fsp?.rename||!crypto?.randomBytes)throw new TypeError('Filesystem and crypto adapters are required');
|
2026-08-01 22:31:04 +09:00
|
|
|
const retryableRenameCodes=new Set(['EACCES','EBUSY','EPERM']),renameRetryDelays=[20,50,100,200,400,800],sleep=milliseconds=>new Promise(resolve=>setTimeout(resolve,milliseconds));
|
2026-07-31 12:54:46 +09:00
|
|
|
const read=async(file,{missing=null}={})=>{
|
|
|
|
|
try{return JSON.parse(await fsp.readFile(file,'utf8'))}
|
|
|
|
|
catch(error){if(error.code==='ENOENT'&&missing!==undefined)return typeof missing==='function'?missing():missing;throw error}
|
|
|
|
|
};
|
|
|
|
|
const write=async(file,value)=>{
|
|
|
|
|
const temporary=`${file}.${processId}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
|
|
|
await fsp.writeFile(temporary,JSON.stringify(value),{encoding:'utf8',mode:0o600});
|
2026-08-01 22:31:04 +09:00
|
|
|
for(let attempt=0;;attempt++)try{await fsp.rename(temporary,file);break}catch(error){if(!retryableRenameCodes.has(error?.code)||attempt>=renameRetryDelays.length){await fsp.unlink?.(temporary).catch(()=>{});throw error}await sleep(renameRetryDelays[attempt])}
|
2026-07-31 12:54:46 +09:00
|
|
|
};
|
|
|
|
|
const remove=async file=>fsp.unlink(file).catch(error=>{if(error.code!=='ENOENT')throw error});
|
|
|
|
|
return Object.freeze({read,write,remove});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports=Object.freeze({createJsonRepository});
|