Skip to content

Commit

Permalink
Add ttl "clamping" to avoid false positives
Browse files Browse the repository at this point in the history
  • Loading branch information
cgatno committed May 2, 2018
1 parent 1e03ba2 commit a7ff594
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 23 deletions.
59 changes: 41 additions & 18 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ class SnapMap extends Map {
// Pass any and all arguments to parent constructor
super(...args);

// Use another Map to track key updates. This allows scheduled deletes to abort if their key
// has been updated after the original scheduling.
// Necessary to use Map here to support any key type
this.updatedKeys = new Map();
// Use another Map to track timestamps. This will effectively clamp expired values in case their timer
// tick is delayed due to event loop inconsistencies. This also allows tracking of key updates for aborting
// deletes on updated keys.
this.timestamps = new Map();
}

/**
Expand All @@ -26,31 +26,54 @@ class SnapMap extends Map {
* @returns {SnapMap} The SnapMap object.
*/
set(key, value, ttl) {
// If key already exists, this is an update. Make sure scheduled deletion is aborted.
let timestamp;
if (super.has(key)) {
timestamp = Date.now() * Math.random();
this.updatedKeys.set(key, timestamp);
}

// Store new data in parent Map
super.set(key, value);
// Calculate future timestamp now
const timestamp = Date.now() + ttl || 0;

if (typeof ttl !== "undefined") {
// Set/update timestamp
this.timestamps.set(key, timestamp);
// TTL specified, schedule deletion
(async (key, delay, timestamp) => {
await sleep(delay);
if (
this.updatedKeys.has(key) &&
this.updatedKeys.get(key) !== timestamp
) {
if (this.timestamps.get(key) !== timestamp) {
return;
}
this.timestamps.delete(key);
super.delete(key);
})(key, ttl, timestamp);
} else {
// No ttl defined - make sure timestamps excludes this key
this.timestamps.delete(key);
}

return super.set(key, value); // mimics default Map API
}

/**
* Retrieve value for a given key.
* @param {*} key
*/
get(key) {
// If there is a timestamp for this value and it has passed, "clamp" deletion by returning undefined
if (this.timestamps.has(key) && this.timestamps.get(key) < Date.now()) {
return undefined;
}

// Otherwise, return normal Map get
return super.get(key);
}

/**
* Check if parent map has a given key.
* @param {*} key
*/
has(key) {
// Also do a "clamping" deletion check here
if (this.timestamps.has(key) && this.timestamps.get(key) < Date.now()) {
return false;
}

return this; // mimics default Map API
return super.has(key);
}
}

Expand Down
10 changes: 5 additions & 5 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ test("expires data after a given time period", done => {
setTimeout(() => {
expect(sm.has(key)).toBe(false);
done();
}, expirationTime + 1);
}, expirationTime);
});

test("expires data added consecutively with same expiration time", done => {
Expand Down Expand Up @@ -101,7 +101,7 @@ test("resets expiration time on update via set() call", done => {
setTimeout(() => {
expect(sm.has(key)).toBe(false);
done();
}, expirationTime3 + 1);
}, expirationTime3);
});

test("does not delete keys that have been updated with undefined ttl", done => {
Expand Down Expand Up @@ -142,14 +142,14 @@ test("expires data in correct order regardless of set order", done => {
setTimeout(() => {
expect(sm.has(key1)).toBe(false);
expect(sm.get(key2)).toBe(val2);
}, expirationTime1 + 1);
}, expirationTime1);

// After second expiration, both key1 and key2 should be gone
setTimeout(() => {
expect(sm.has(key1)).toBe(false);
expect(sm.has(key2)).toBe(false);
done();
}, expirationTime2 + 1);
}, expirationTime2);

// Both keys should exist
expect(sm.get(key1)).toBe(val1);
Expand All @@ -170,7 +170,7 @@ test("does not block execution", done => {
expect(doing).toBe(100);
expect(sm.has("nonblocking")).toBe(false);
done();
}, 2000 + 1);
}, 2000);

// Increment while timer is waiting
for (let i = 0; i < 100; i++) {
Expand Down

0 comments on commit a7ff594

Please sign in to comment.