Skip to content

Commit

Permalink
feat: default to no TTL and add keepIdentifierBirthTTL option (OpenAl…
Browse files Browse the repository at this point in the history
  • Loading branch information
fraxken authored Jan 11, 2023
1 parent 4b9cd1c commit 63726e7
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 14 deletions.
20 changes: 15 additions & 5 deletions src/timestore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ import { TimeStore } from "@openally/timestore";

const store = new TimeStore({ ttl: 10_000 })
.add("foo")
.add("bar", { ttl: 500 });
.add("bar", { ttl: 500 })
.add("bar", { ttl: 200, keepIdentifierBirthTTL: true }); // will be ignored!

console.log(store.ttl); // 10000

Expand All @@ -61,15 +62,15 @@ Identifier are often described with the following type:
export type TimeStoreIdentifier = string | symbol | number | boolean | bigint | object | null;
```

### constructor(options: ITimeStoreConstructorOptions)
### constructor(options?: ITimeStoreConstructorOptions)
The constructor `options` payload is described by the following TS interface:

```ts
interface ITimeStoreConstructorOptions {
/**
* Time To Live (Lifetime of stored identifiers).
*/
ttl: number;
ttl?: number;
/**
* Automatically expire identifiers when Node.js process "exit" event is triggered.
*
Expand All @@ -92,6 +93,8 @@ interface ITimeStoreConstructorOptions {
}
```

If the `ttl` option is not provided all identifiers will remain active. The default class `ttl` will be equal **zero**.

### add(identifier: TimeStoreIdentifier, options?: ITimeStoreAddOptions): this
The `options` payload is described by the following TS interface:

Expand All @@ -102,10 +105,17 @@ interface ITimeStoreAddOptions {
* If no value provided it will take the class TTL value.
*/
ttl?: number;

/**
* If identifier exist then keep is original timestamp and ttl.
*
* @default false
*/
keepIdentifierBirthTTL?: boolean;
}
```

> **Note** Adding an existing ID will reset its previous TTL/timestamp
> **Note** Adding an existing ID will reset its previous TTL/timestamp except if the `keepIdentifierBirthTTL` option is set to **true**.
### addTsv(data: tSvResponse): this
Add a value using a TimeStoreValue:
Expand All @@ -131,7 +141,7 @@ Remove a given identifier from the store.
Calling this method will remove all stored identifiers and clear the internal Node.js Timeout. The instance basically returns to its initial state.

### get ttl(): number
Read-only TTL.
Read-only TTL. Return `0` if the class has no ttl.

## Events

Expand Down
35 changes: 26 additions & 9 deletions src/timestore/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface ITimeStoreConstructorOptions {
/**
* Time To Live (Lifetime of stored identifiers).
*/
ttl: number;
ttl?: number;
/**
* Automatically expire identifiers when Node.js process "exit" event is triggered.
*
Expand Down Expand Up @@ -41,6 +41,13 @@ export interface ITimeStoreAddOptions {
* If no value provided it will take the class TTL value.
*/
ttl?: number;

/**
* If identifier exist then keep is original timestamp and ttl.
*
* @default false
*/
keepIdentifierBirthTTL?: boolean;
}

export type TimeStoreIdentifier = string | symbol | number | boolean | bigint | object | null;
Expand All @@ -61,7 +68,7 @@ export class TimeStore extends EventEmitter {
#timer: NodeJS.Timeout | null = null;
#customEventEmitter: EventEmitter | null = null;

constructor(options: ITimeStoreConstructorOptions) {
constructor(options: ITimeStoreConstructorOptions = {}) {
super();
const {
ttl,
Expand All @@ -70,7 +77,7 @@ export class TimeStore extends EventEmitter {
eventEmitter = null
} = options;

this.#ttl = ttl;
this.#ttl = ttl ?? 0;
this.#customEventEmitter = eventEmitter;
this.#keepEventLoopAlive = keepEventLoopAlive;

Expand Down Expand Up @@ -101,17 +108,25 @@ export class TimeStore extends EventEmitter {
identifier: TimeStoreIdentifier,
options: ITimeStoreAddOptions = {}
) {
const { ttl = this.#ttl } = options;
const { ttl = this.#ttl, keepIdentifierBirthTTL = false } = options;

const hasIdentifier = this.#identifiers.has(identifier);
const timestamp = Date.now();
const originalIdentifier = this.#identifiers.get(identifier);
const hasIdentifier = typeof originalIdentifier !== "undefined";

const timestamp = hasIdentifier && keepIdentifierBirthTTL ? originalIdentifier.timestamp : Date.now();
if (!keepIdentifierBirthTTL) {
this.#identifiers.set(identifier, { timestamp, ttl });
}

this.#identifiers.set(identifier, { timestamp, ttl });
if (hasIdentifier) {
this.emit(TimeStore.Renewed, identifier);
this.#customEventEmitter?.emit(TimeStore.Renewed, identifier);
}

if (ttl === 0) {
return this;
}

if (this.#timer === null) {
this.#setNewUpfrontIdentifier(identifier, ttl);
}
Expand Down Expand Up @@ -159,9 +174,11 @@ export class TimeStore extends EventEmitter {

while (sortedIdentifiers.length > 0) {
const [identifier, value] = sortedIdentifiers.pop()!;
const delta = Date.now() - value.timestamp;
if (value.ttl === 0) {
continue;
}

// If current key is expired
const delta = Date.now() - value.timestamp;
if (delta >= value.ttl) {
this.emit(TimeStore.Expired, identifier);
}
Expand Down
58 changes: 58 additions & 0 deletions src/timestore/test/TimeStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ describe("TimeStore", () => {

expect(store.ttl).to.equal(ttl);
});

it("should return ttl zero if no ttl is provided in the constructor", () => {
const store = new TimeStore();

expect(store.ttl).to.equal(0);
});
});

describe("add", () => {
Expand Down Expand Up @@ -174,6 +180,58 @@ describe("TimeStore", () => {
.execute(counter.events(), { allowNoMatchingValues: false });
expect(isMatching).to.equal(true);
});

it("should not expire if no ttl is provided in the constructor", async() => {
const store = new TimeStore();
const counter = new utils.EventEmitterCounter(store, [
TimeStore.Renewed
]);

store.add("foo");
await timers.setTimeout(100);
store.add("foo");

const { isMatching } = new IteratorMatcher()
.expect(TimeStore.Renewed)
.execute(counter.events(), { allowNoMatchingValues: false });
expect(isMatching).to.equal(true);
});

it("should renew and make expire a given identifier that didn't had any ttl", async() => {
const store = new TimeStore();
const counter = new utils.EventEmitterCounter(store, [
TimeStore.Renewed,
TimeStore.Expired
]);

store.add("foo");
store.add("foo", { ttl: 10 });
await timers.setTimeout(100);

const { isMatching } = new IteratorMatcher()
.expect(TimeStore.Renewed)
.expect(TimeStore.Expired)
.execute(counter.events(), { allowNoMatchingValues: false });
expect(isMatching).to.equal(true);
});

it("should keep the original identifier TTL when we renew it with keepIdentifierBirthTTL equal true", async() => {
const store = new TimeStore({ ttl: 50 });
const counter = new utils.EventEmitterCounter(store, [
TimeStore.Renewed,
TimeStore.Expired
]);

store.add("foo");
store.add("foo", { ttl: 500, keepIdentifierBirthTTL: true });
await timers.setTimeout(100);

const { isMatching } = new IteratorMatcher()
.expect(TimeStore.Renewed)
.expect(TimeStore.Expired)
.execute(counter.events(), { allowNoMatchingValues: false });
expect(isMatching).to.equal(true);
});
});

describe("addTsv", () => {
Expand Down

0 comments on commit 63726e7

Please sign in to comment.