Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: kwertop/persistent-node-cache
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.1.1
Choose a base ref
...
head repository: kwertop/persistent-node-cache
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
  • 5 commits
  • 6 files changed
  • 2 contributors

Commits on May 10, 2024

  1. Copy the full SHA
    df12893 View commit details

Commits on May 15, 2024

  1. Copy the full SHA
    bbb9b5c View commit details
  2. Merge pull request #3 from jasonshugart/main

    Added better file checks and automatic recovery
    kwertop authored May 15, 2024
    Copy the full SHA
    b7b8f8e View commit details
  3. update README

    kwertop committed May 15, 2024
    Copy the full SHA
    d9e988e View commit details
  4. typo fix in README

    kwertop committed May 15, 2024
    Copy the full SHA
    71b1e8b View commit details
Showing with 241 additions and 377 deletions.
  1. +11 −2 README.md
  2. +1 −1 lib/persistentNodeCache.d.ts
  3. +34 −71 lib/persistentNodeCache.js
  4. +143 −269 package-lock.json
  5. +24 −19 src/persistentNodeCache.ts
  6. +28 −15 test/persistentNodeCache.test.ts
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -73,8 +73,17 @@ cache.del("mykey"); //1

### Restore/Recover Cache

The `cacheName` field should be passed to tell which cache to restore. If `dir` field was passed during cache initialization
previously, it should be passed during recovery as well to locate the backup files.
#### Version 1.2.0 onwards
The cache is restored automatically when the cache is initialized (again). Parameter is `cacheName` should be passed. `dir` will have to be passed if the cache was created specifying a custom directory.

```typescript
const cache = new PersistentNodeCache("mycache");

cache.get("mykey"); //myval
```

#### Version 1.1.1 and before
The `cacheName` field should be passed to tell which cache to restore. If `dir` field was passed during cache initialization previously, it should be passed during recovery as well to locate the backup files.

```typescript
const cache = new PersistentNodeCache("mycache");
2 changes: 1 addition & 1 deletion lib/persistentNodeCache.d.ts
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ export default class PersistentNodeCache extends NodeCache {
ttl(key: Key, ttl?: number): boolean;
flushAll(): void;
close(): void;
recover(): Promise<void>;
recover(): void;
private appendExpiredEvent;
private saveToDisk;
private appendToFile;
105 changes: 34 additions & 71 deletions lib/persistentNodeCache.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,4 @@
"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 __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
@@ -23,23 +7,18 @@ const node_cache_1 = __importDefault(require("node-cache"));
const events_1 = require("events");
const waitFor_1 = require("./waitFor");
const fs_1 = __importDefault(require("fs"));
const readline_1 = __importDefault(require("readline"));
const os_1 = __importDefault(require("os"));
class PersistentNodeCache extends node_cache_1.default {
constructor(cacheName, period, dir, opts, serializer) {
super(opts);
this.cacheName = cacheName;
this.interval = setInterval(() => { this.saveToDisk(); }, period || 1000);
this.emitter = new events_1.EventEmitter();
this.flushingToDisk = false;
if (dir === null || dir === void 0 ? void 0 : dir.endsWith('/')) {
dir = dir.slice(0, -1);
}
this.backupFilePath = (dir || os_1.default.homedir()) + `/${this.cacheName}.backup`;
this.appendFilePath = (dir || os_1.default.homedir()) + `/${this.cacheName}.append`;
//create files
fs_1.default.writeFileSync(this.backupFilePath, '');
fs_1.default.writeFileSync(this.appendFilePath, '');
if (serializer) {
this.serializer = serializer;
}
@@ -56,6 +35,18 @@ class PersistentNodeCache extends node_cache_1.default {
}
this.changesSinceLastBackup = false;
super.on("expired", (key, _) => { this.appendExpiredEvent(key); });
// Look for backup and append files and recover if found. Otherwise create them
try {
fs_1.default.accessSync(this.backupFilePath, fs_1.default.constants.R_OK | fs_1.default.constants.W_OK);
fs_1.default.accessSync(this.appendFilePath, fs_1.default.constants.R_OK | fs_1.default.constants.W_OK);
this.recover();
}
catch (err) {
fs_1.default.writeFileSync(this.backupFilePath, '');
fs_1.default.writeFileSync(this.appendFilePath, '');
}
// Need to start the interval after we recover the files otherwise they get erased
this.interval = setInterval(() => { this.saveToDisk(); }, period || 1000);
}
set(key, value, ttl) {
if (this.flushingToDisk) {
@@ -130,57 +121,29 @@ class PersistentNodeCache extends node_cache_1.default {
clearInterval(this.interval);
}
recover() {
const _super = Object.create(null, {
mset: { get: () => super.mset },
keys: { get: () => super.keys },
set: { get: () => super.set },
del: { get: () => super.del },
ttl: { get: () => super.ttl }
});
var _a, e_1, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
const backup = fs_1.default.readFileSync(this.backupFilePath);
if (backup.length === 0) {
return;
}
const backup = fs_1.default.readFileSync(this.backupFilePath);
if (backup.length > 0) {
let data = this.serializer.deserialize(backup);
_super.mset.call(this, data);
const fileStream = fs_1.default.createReadStream(this.appendFilePath);
const rl = readline_1.default.createInterface({
input: fileStream,
crlfDelay: Infinity
});
try {
for (var _d = true, rl_1 = __asyncValues(rl), rl_1_1; rl_1_1 = yield rl_1.next(), _a = rl_1_1.done, !_a; _d = true) {
_c = rl_1_1.value;
_d = false;
const line = _c;
let m = _super.keys.call(this);
let data = this.serializer.deserialize(line);
switch (data['cmd']) {
case 'set':
_super.set.call(this, data['key'], data['val'], data['ttl']);
break;
case 'mset':
_super.mset.call(this, data['keyValue']);
break;
case 'del':
_super.del.call(this, data['key']);
break;
case 'ttl':
_super.ttl.call(this, data['key'], data['ttl']);
break;
default:
break;
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = rl_1.return)) yield _b.call(rl_1);
}
finally { if (e_1) throw e_1.error; }
super.mset(data);
}
const appendData = fs_1.default.readFileSync(this.appendFilePath, 'utf-8');
appendData.split(/\r?\n/).forEach((line) => {
let data = this.serializer.deserialize(line);
switch (data['cmd']) {
case 'set':
super.set(data['key'], data['val'], data['ttl']);
break;
case 'mset':
super.mset(data['keyValue']);
break;
case 'del':
super.del(data['key']);
break;
case 'ttl':
super.ttl(data['key'], data['ttl']);
break;
default:
break;
}
});
}
Loading