@curium.rocks/json-chronicler
Advanced tools
Comparing version 0.1.1-1 to 0.1.1-2
@@ -1,1 +0,2 @@ | ||
export { JsonChronicler, JsonChroniclerOptions, getMsFromRotationOptions, RotationOptions } from './jsonChronicler'; | ||
export { JsonChronicler, JsonChroniclerOptions, getMsFromRotationOptions, RotationOptions, isRotationOptions } from './jsonChronicler'; | ||
export { JsonChroniclerFactory } from './jsonChroniclerFactory'; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getMsFromRotationOptions = exports.JsonChronicler = void 0; | ||
exports.JsonChroniclerFactory = exports.isRotationOptions = exports.getMsFromRotationOptions = exports.JsonChronicler = void 0; | ||
var jsonChronicler_1 = require("./jsonChronicler"); | ||
Object.defineProperty(exports, "JsonChronicler", { enumerable: true, get: function () { return jsonChronicler_1.JsonChronicler; } }); | ||
Object.defineProperty(exports, "getMsFromRotationOptions", { enumerable: true, get: function () { return jsonChronicler_1.getMsFromRotationOptions; } }); | ||
Object.defineProperty(exports, "isRotationOptions", { enumerable: true, get: function () { return jsonChronicler_1.isRotationOptions; } }); | ||
var jsonChroniclerFactory_1 = require("./jsonChroniclerFactory"); | ||
Object.defineProperty(exports, "JsonChroniclerFactory", { enumerable: true, get: function () { return jsonChroniclerFactory_1.JsonChroniclerFactory; } }); | ||
//# sourceMappingURL=index.js.map |
@@ -1,7 +0,7 @@ | ||
import { LoggerFacade, IRotatingFileChronicler, IJsonSerializable } from '@curium.rocks/data-emitter-base'; | ||
export interface JsonChroniclerOptions { | ||
import { LoggerFacade, IRotatingFileChronicler, IJsonSerializable, IClassifier } from '@curium.rocks/data-emitter-base'; | ||
export interface JsonChroniclerOptions extends IClassifier { | ||
logger?: LoggerFacade; | ||
rotationSettings: RotationOptions; | ||
logDirectory: string; | ||
name: string; | ||
logName: string; | ||
} | ||
@@ -24,5 +24,12 @@ export interface RotationOptions { | ||
/** | ||
* Check if an object conforms to the RotationOptions interface | ||
* @param {unknown} options | ||
* @return {boolean} | ||
*/ | ||
export declare function isRotationOptions(options: unknown): boolean; | ||
/** | ||
* Persist events to a rolling JSON file | ||
*/ | ||
export declare class JsonChronicler implements IRotatingFileChronicler { | ||
static readonly TYPE: string; | ||
private firstWrite; | ||
@@ -38,4 +45,19 @@ private fileHandle; | ||
private disposed; | ||
private _id; | ||
private _name; | ||
private _description; | ||
/** | ||
* | ||
*/ | ||
get id(): string; | ||
/** | ||
* | ||
*/ | ||
get description(): string; | ||
/** | ||
* | ||
*/ | ||
get name(): string; | ||
/** | ||
* | ||
* @param {JsonChroniclerOptions} options | ||
@@ -42,0 +64,0 @@ */ |
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [op[0] & 2, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
@@ -42,8 +6,8 @@ return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.JsonChronicler = exports.getMsFromRotationOptions = void 0; | ||
var promises_1 = __importDefault(require("fs/promises")); | ||
var promises_2 = require("stream/promises"); | ||
var fs_1 = require("fs"); | ||
var zlib_1 = __importDefault(require("zlib")); | ||
var path_1 = __importDefault(require("path")); | ||
exports.JsonChronicler = exports.isRotationOptions = exports.getMsFromRotationOptions = void 0; | ||
const promises_1 = __importDefault(require("fs/promises")); | ||
const promises_2 = require("stream/promises"); | ||
const fs_1 = require("fs"); | ||
const zlib_1 = __importDefault(require("zlib")); | ||
const path_1 = __importDefault(require("path")); | ||
/** | ||
@@ -55,3 +19,3 @@ * | ||
function getMsFromRotationOptions(options) { | ||
var total = 0; | ||
let total = 0; | ||
if (options.days) | ||
@@ -71,5 +35,20 @@ total += options.days * 86400000; | ||
/** | ||
* Check if an object conforms to the RotationOptions interface | ||
* @param {unknown} options | ||
* @return {boolean} | ||
*/ | ||
function isRotationOptions(options) { | ||
if (options == null) | ||
return false; | ||
if (typeof options != "object") | ||
return false; | ||
// all properties for rotation options are nullable soooo as long as it's an object and not null | ||
// it conforms to the contract for now. | ||
return true; | ||
} | ||
exports.isRotationOptions = isRotationOptions; | ||
/** | ||
* Persist events to a rolling JSON file | ||
*/ | ||
var JsonChronicler = /** @class */ (function () { | ||
class JsonChronicler { | ||
/** | ||
@@ -79,3 +58,3 @@ * | ||
*/ | ||
function JsonChronicler(options) { | ||
constructor(options) { | ||
this.firstWrite = true; | ||
@@ -86,12 +65,33 @@ this.disposed = false; | ||
this.logDirectory = options.logDirectory; | ||
this.logName = options.name; | ||
this.logName = options.logName; | ||
this.rotationIntervalMs = getMsFromRotationOptions(options.rotationSettings); | ||
this._name = options.name; | ||
this._description = options.description; | ||
this._id = options.id; | ||
} | ||
/** | ||
* | ||
*/ | ||
get id() { | ||
return this._id; | ||
} | ||
/** | ||
* | ||
*/ | ||
get description() { | ||
return this._description; | ||
} | ||
/** | ||
* | ||
*/ | ||
get name() { | ||
return this._name; | ||
} | ||
/** | ||
* @return {string} filename for next archive | ||
* @private | ||
*/ | ||
JsonChronicler.prototype.generateFilename = function () { | ||
return this.logName + "." + new Date().getTime() + ".json"; | ||
}; | ||
generateFilename() { | ||
return `${this.logName}.${new Date().getTime()}.json`; | ||
} | ||
/** | ||
@@ -102,37 +102,17 @@ * | ||
*/ | ||
JsonChronicler.prototype.saveRecord = function (record) { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var string, fileName, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
if (this.disposed) | ||
throw new Error("Object Disposed!"); | ||
string = JSON.stringify(record); | ||
return [4 /*yield*/, this.shouldCreateFile()]; | ||
case 1: | ||
if (!_c.sent()) return [3 /*break*/, 3]; | ||
fileName = this.generateFilename(); | ||
_b = this; | ||
return [4 /*yield*/, this.createLogFile(fileName)]; | ||
case 2: | ||
_b.fileHandle = _c.sent(); | ||
this.currentFile = fileName; | ||
this.lastRotationMs = new Date().getTime(); | ||
_c.label = 3; | ||
case 3: | ||
if (!this.shouldRotate()) return [3 /*break*/, 5]; | ||
return [4 /*yield*/, this.rotateLog()]; | ||
case 4: | ||
_c.sent(); | ||
_c.label = 5; | ||
case 5: return [4 /*yield*/, ((_a = this.fileHandle) === null || _a === void 0 ? void 0 : _a.write("" + (!this.firstWrite ? ',\n' : '') + string))]; | ||
case 6: | ||
_c.sent(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
async saveRecord(record) { | ||
if (this.disposed) | ||
throw new Error("Object Disposed!"); | ||
const string = JSON.stringify(record); | ||
if (await this.shouldCreateFile()) { | ||
const fileName = this.generateFilename(); | ||
this.fileHandle = await this.createLogFile(fileName); | ||
this.currentFile = fileName; | ||
this.lastRotationMs = new Date().getTime(); | ||
} | ||
if (this.shouldRotate()) { | ||
await this.rotateLog(); | ||
} | ||
await this.fileHandle?.write(`${!this.firstWrite ? ',\n' : ''}${string}`); | ||
} | ||
/** | ||
@@ -142,25 +122,11 @@ * @param {string} fileName of new log file | ||
*/ | ||
JsonChronicler.prototype.createLogFile = function (fileName) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var filePath, fileHandle; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
filePath = path_1.default.join(this.logDirectory, fileName); | ||
return [4 /*yield*/, promises_1.default.mkdir(this.logDirectory, { | ||
recursive: true | ||
})]; | ||
case 1: | ||
_a.sent(); | ||
return [4 /*yield*/, promises_1.default.open(filePath, 'w')]; | ||
case 2: | ||
fileHandle = _a.sent(); | ||
return [4 /*yield*/, fileHandle.write('[')]; | ||
case 3: | ||
_a.sent(); | ||
return [2 /*return*/, fileHandle]; | ||
} | ||
}); | ||
async createLogFile(fileName) { | ||
const filePath = path_1.default.join(this.logDirectory, fileName); | ||
await promises_1.default.mkdir(this.logDirectory, { | ||
recursive: true | ||
}); | ||
}; | ||
const fileHandle = await promises_1.default.open(filePath, 'w'); | ||
await fileHandle.write('['); | ||
return fileHandle; | ||
} | ||
/** | ||
@@ -170,21 +136,11 @@ * | ||
*/ | ||
JsonChronicler.prototype.shouldCreateFile = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var _a; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
_b.trys.push([0, 2, , 3]); | ||
return [4 /*yield*/, promises_1.default.access(path_1.default.join(this.logDirectory, this.getCurrentFilename()))]; | ||
case 1: | ||
_b.sent(); | ||
return [2 /*return*/, false]; | ||
case 2: | ||
_a = _b.sent(); | ||
return [2 /*return*/, true]; | ||
case 3: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
async shouldCreateFile() { | ||
try { | ||
await promises_1.default.access(path_1.default.join(this.logDirectory, this.getCurrentFilename())); | ||
return false; | ||
} | ||
catch { | ||
return true; | ||
} | ||
} | ||
/** | ||
@@ -195,7 +151,7 @@ * | ||
*/ | ||
JsonChronicler.prototype.shouldRotate = function () { | ||
shouldRotate() { | ||
// check the data extracted from the current file name and compare against | ||
// rotation rules | ||
return this.timeTillRotation() < 0; | ||
}; | ||
} | ||
/** | ||
@@ -205,11 +161,11 @@ * Gets the time since the last log file rotation in seconds | ||
*/ | ||
JsonChronicler.prototype.timeSinceRotation = function () { | ||
var now = new Date().getTime(); | ||
timeSinceRotation() { | ||
const now = new Date().getTime(); | ||
if (this.lastRotationMs == null) | ||
return now / 1000; | ||
var timeSinceMs = now - this.lastRotationMs; | ||
const timeSinceMs = now - this.lastRotationMs; | ||
if (timeSinceMs == 0) | ||
return 0; | ||
return Math.floor(timeSinceMs / 1000); | ||
}; | ||
} | ||
/** | ||
@@ -219,12 +175,12 @@ * Gets the time till the next log file rotation in seconds | ||
*/ | ||
JsonChronicler.prototype.timeTillRotation = function () { | ||
timeTillRotation() { | ||
if (this.lastRotationMs == null) | ||
return Math.floor(this.rotationIntervalMs / 1000); | ||
var now = new Date().getTime(); | ||
var rotationTime = this.lastRotationMs + this.rotationIntervalMs; | ||
var timeTillRotation = rotationTime - now; | ||
const now = new Date().getTime(); | ||
const rotationTime = this.lastRotationMs + this.rotationIntervalMs; | ||
const timeTillRotation = rotationTime - now; | ||
if (timeTillRotation == 0) | ||
return 0; | ||
return Math.floor(timeTillRotation / 1000); | ||
}; | ||
} | ||
/** | ||
@@ -234,32 +190,16 @@ * Rotates the logs and returns the new file name | ||
*/ | ||
JsonChronicler.prototype.rotateLog = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var oldHandle, name, _a; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
oldHandle = this.fileHandle; | ||
name = this.generateFilename(); | ||
_a = this; | ||
return [4 /*yield*/, this.createLogFile(name)]; | ||
case 1: | ||
_a.fileHandle = _b.sent(); | ||
this.currentFile = name; | ||
return [4 /*yield*/, (oldHandle === null || oldHandle === void 0 ? void 0 : oldHandle.write(']'))]; | ||
case 2: | ||
_b.sent(); | ||
// close the old file handle | ||
return [4 /*yield*/, (oldHandle === null || oldHandle === void 0 ? void 0 : oldHandle.close())]; | ||
case 3: | ||
// close the old file handle | ||
_b.sent(); | ||
return [4 /*yield*/, this.compactLogs()]; | ||
case 4: | ||
_b.sent(); | ||
this.lastRotationMs = new Date().getTime(); | ||
return [2 /*return*/, name]; | ||
} | ||
}); | ||
}); | ||
}; | ||
async rotateLog() { | ||
// create a new file | ||
// swap the file handles | ||
const oldHandle = this.fileHandle; | ||
const name = this.generateFilename(); | ||
this.fileHandle = await this.createLogFile(name); | ||
this.currentFile = name; | ||
await oldHandle?.write(']'); | ||
// close the old file handle | ||
await oldHandle?.close(); | ||
await this.compactLogs(); | ||
this.lastRotationMs = new Date().getTime(); | ||
return name; | ||
} | ||
/** | ||
@@ -271,90 +211,54 @@ * | ||
*/ | ||
JsonChronicler.prototype.compressLog = function (logName) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var gzip, source, destination; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
gzip = zlib_1.default.createGzip(); | ||
source = fs_1.createReadStream(path_1.default.join(this.logDirectory, logName)); | ||
destination = fs_1.createWriteStream(path_1.default.join(this.logDirectory, logName + ".gz")); | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, , 4, 5]); | ||
return [4 /*yield*/, promises_2.pipeline(source, gzip, destination)]; | ||
case 2: | ||
_a.sent(); | ||
return [4 /*yield*/, promises_1.default.unlink(path_1.default.join(this.logDirectory, logName))]; | ||
case 3: | ||
_a.sent(); | ||
return [3 /*break*/, 5]; | ||
case 4: | ||
source.close(); | ||
gzip.close(); | ||
destination.close(); | ||
return [7 /*endfinally*/]; | ||
case 5: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
async compressLog(logName) { | ||
const gzip = zlib_1.default.createGzip(); | ||
const source = fs_1.createReadStream(path_1.default.join(this.logDirectory, logName)); | ||
const destination = fs_1.createWriteStream(path_1.default.join(this.logDirectory, `${logName}.gz`)); | ||
try { | ||
await promises_2.pipeline(source, gzip, destination); | ||
await promises_1.default.unlink(path_1.default.join(this.logDirectory, logName)); | ||
} | ||
finally { | ||
source.close(); | ||
gzip.close(); | ||
destination.close(); | ||
} | ||
} | ||
/** | ||
* @return {Promise<void>} | ||
*/ | ||
JsonChronicler.prototype.compactLogs = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var logMatch, compressMatch, files, _i, files_1, fileName; | ||
var _this = this; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
logMatch = new RegExp(this.logName + ".*\\.json"); | ||
compressMatch = new RegExp('.*\\.json.gz'); | ||
return [4 /*yield*/, promises_1.default.readdir(this.logDirectory)]; | ||
case 1: | ||
files = (_a.sent()) | ||
// filter that list by the regex of matching names | ||
.filter(function (fileName) { return fileName.match(logMatch); }) | ||
.filter(function (fileName) { return !fileName.match(compressMatch); }) | ||
// exclude the current log from that list | ||
.filter(function (fileName) { return fileName != _this.currentFile; }); | ||
_i = 0, files_1 = files; | ||
_a.label = 2; | ||
case 2: | ||
if (!(_i < files_1.length)) return [3 /*break*/, 5]; | ||
fileName = files_1[_i]; | ||
return [4 /*yield*/, this.compressLog(fileName)]; | ||
case 3: | ||
_a.sent(); | ||
_a.label = 4; | ||
case 4: | ||
_i++; | ||
return [3 /*break*/, 2]; | ||
case 5: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
async compactLogs() { | ||
const logMatch = `${this.logName}.*\\.json`; | ||
const compressMatch = '.*\\.json.gz'; | ||
// get the list of files in the log directory without .gz | ||
const files = (await promises_1.default.readdir(this.logDirectory)) | ||
// filter that list by the regex of matching names | ||
.filter((fileName) => fileName.match(logMatch)) | ||
.filter((fileName) => !fileName.match(compressMatch)) | ||
// exclude the current log from that list | ||
.filter((fileName) => fileName != this.currentFile); | ||
// call compressLog on each one of the uncompressed files that match this loggers scheme | ||
for (const fileName of files) { | ||
await this.compressLog(fileName); | ||
} | ||
} | ||
/** | ||
* @return {string} | ||
*/ | ||
JsonChronicler.prototype.getCurrentFilename = function () { | ||
getCurrentFilename() { | ||
return this.currentFile || "N/A"; | ||
}; | ||
} | ||
/** | ||
* | ||
*/ | ||
JsonChronicler.prototype.dispose = function () { | ||
var _this = this; | ||
dispose() { | ||
this.disposed = true; | ||
if (this.fileHandle) { | ||
this.fileHandle.write(']').then(function () { | ||
var _a; | ||
return (_a = _this.fileHandle) === null || _a === void 0 ? void 0 : _a.close(); | ||
this.fileHandle.write(']').then(() => { | ||
return this.fileHandle?.close(); | ||
}); | ||
} | ||
}; | ||
return JsonChronicler; | ||
}()); | ||
} | ||
} | ||
exports.JsonChronicler = JsonChronicler; | ||
JsonChronicler.TYPE = "JSON-CHRONICLER"; | ||
//# sourceMappingURL=jsonChronicler.js.map |
{ | ||
"name": "@curium.rocks/json-chronicler", | ||
"version": "0.1.1-1", | ||
"version": "0.1.1-2", | ||
"description": "Persists objects in JSON format to rolling archive files", | ||
@@ -29,4 +29,2 @@ "main": "build/src/index.js", | ||
"devDependencies": { | ||
"@curium.rocks/data-emitter-base": "^0.1.1-alpha.20", | ||
"@curium.rocks/ping-pong-emitter": "^0.1.1-4", | ||
"@types/chai": "^4.2.21", | ||
@@ -55,6 +53,10 @@ "@types/mocha": "^9.0.0", | ||
"exclude": [ | ||
"**/*.d.ts" | ||
"**/*.d.ts", | ||
".eslintrc.js", | ||
"coverage/**/*", | ||
"test/**/*" | ||
], | ||
"reporter": [ | ||
"lcov" | ||
"lcov", | ||
"text" | ||
], | ||
@@ -70,4 +72,4 @@ "all": true | ||
"dependencies": { | ||
"@curium.rocks/data-emitter-base": "^0.1.1-alpha.20" | ||
"@curium.rocks/data-emitter-base": "^0.1.1-alpha.31" | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
16
14
30622
436