Skip to content

Commit 00dd695

Browse files
authored
itterator support and a test suite update according to latst keyv testing lib. (#21)
* chore(ROLAND-001): updated to be green on latest test suite tslinted and more code readabillity * feature(ROLAND-002): adding itterator support * chore(ROLAND-003): cleaning up for pull request * chore(ROLAND-004): Exporting constructor to stay backwards compattible * fix(ROLAND-005): preventing overwrites of defaultconfig
1 parent 0119aea commit 00dd695

File tree

6 files changed

+3435
-237
lines changed

6 files changed

+3435
-237
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2017 Zack Young
3+
Copyright (c) 2024 Zack Young
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

index.ts

Lines changed: 188 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -2,191 +2,211 @@
22

33
import * as os from 'os'
44
import * as fs from 'fs-extra'
5-
import Debug from 'debug'
5+
import EventEmitter from 'events';
6+
import {type KeyvStoreAdapter, type StoredData} from 'keyv';
7+
8+
export interface Options {
9+
deserialize: (val: any) => any;
10+
dialect: string
11+
expiredCheckDelay: number; // milliseconds
12+
filename: string;
13+
serialize: (val: any) => any;
14+
writeDelay: number; // milliseconds
15+
}
616

7-
const debug = Debug('keyv-file')
17+
export const defaultOpts: Options = {
18+
deserialize: JSON.parse as any as (val: any) => any,
19+
dialect: 'redis',
20+
expiredCheckDelay: 24 * 3600 * 1000, // ms
21+
filename: `${os.tmpdir()}/keyv-file/default-rnd-${Math.random().toString(36).slice(2)}.json`,
22+
serialize: JSON.stringify as any as (val: any) => any,
23+
writeDelay: 100, // ms
24+
}
825

926
function isNumber(val: any): val is number {
10-
return typeof val === 'number'
11-
}
12-
export interface Data<V> {
13-
expire?: number
14-
value: V
27+
return typeof val === 'number'
1528
}
1629

17-
export const defaultOpts = {
18-
filename: `${os.tmpdir()}/keyv-file/default-rnd-${Math.random().toString(36).slice(2)}.json`,
19-
expiredCheckDelay: 24 * 3600 * 1000, // ms
20-
writeDelay: 100, // ms
21-
encode: JSON.stringify as any as (val: any) => any,
22-
decode: JSON.parse as any as (val: any) => any,
23-
}
30+
export class KeyvFile extends EventEmitter implements KeyvStoreAdapter {
31+
public ttlSupport = true;
32+
public namespace?: string;
33+
public opts: Options;
34+
private _cache: Map<string, any>;
35+
private _lastExpire: number;
36+
37+
constructor(options?: Options) {
38+
super();
39+
this.opts = Object.assign({}, defaultOpts, options);
40+
41+
try {
42+
const data = this.opts.deserialize(fs.readFileSync(this.opts.filename, 'utf8'))
43+
if (!Array.isArray(data.cache)) {
44+
const _cache = data.cache
45+
data.cache = [];
46+
for (const key in _cache) {
47+
if (_cache.hasOwnProperty(key)) {
48+
data.cache.push([key, _cache[key]])
49+
}
50+
}
51+
}
52+
this._cache = new Map(data.cache)
53+
this._lastExpire = data.lastExpire
54+
} catch (e) {
55+
this._cache = new Map()
56+
this._lastExpire = Date.now()
57+
}
58+
}
2459

25-
export class Field<T, D extends T|void=T|void> {
26-
constructor(protected kv: KeyvFile, protected key: string, protected defaults?: D) {}
27-
28-
get(): D
29-
get(def: D): D
30-
get(def = this.defaults) {
31-
return this.kv.get(this.key, def)
32-
}
33-
set(val: T, ttl?: number) {
34-
return this.kv.set(this.key, val, ttl)
35-
}
36-
delete() {
37-
return this.kv.delete(this.key)
38-
}
39-
}
60+
private _getKeyName = (key: string): string => {
61+
if (this.namespace) {
62+
return `${this.namespace}:${key}`;
63+
}
64+
return key;
65+
};
4066

41-
export function makeField<T = any, D=T>(kv: KeyvFile, key: string, defaults: T): Field<T, T>
42-
export function makeField<T = any, D extends T|void = T|void>(kv: KeyvFile, key: string, defaults?: D) {
43-
return new Field<T, D>(kv, key, defaults)
44-
}
45-
export class KeyvFile<V = any> {
46-
ttlSupport = true
47-
private _opts = defaultOpts
48-
private _cache: Map<string, Data<V>>
49-
private _lastExpire: number
50-
51-
constructor(opts?: Partial<typeof defaultOpts>) {
52-
this._opts = {
53-
...this._opts,
54-
...opts,
67+
private _removeNamespaceFromKey = (key: string): string => {
68+
if (this.namespace) {
69+
return key.replace(`${this.namespace}:`, '');
70+
}
71+
return key;
5572
}
56-
try {
57-
const data = this._opts.decode(fs.readFileSync(this._opts.filename, 'utf8'))
58-
if (!Array.isArray(data.cache)) {
59-
const _cache = data.cache
60-
data.cache = []
61-
for (const key in _cache) {
62-
data.cache.push([key, _cache[key]])
73+
74+
public async get<Value>(key: string): Promise<StoredData<Value> | undefined> {
75+
try {
76+
const data = this._cache.get(this._getKeyName(key));
77+
if (!data) {
78+
return undefined;
79+
} else if (this.isExpired(data)) {
80+
await this.delete(this._getKeyName(key))
81+
return undefined;
82+
} else {
83+
return data.value as StoredData<Value>
84+
}
85+
} catch (error) {
86+
// do nothing;
6387
}
64-
}
65-
this._cache = new Map(data.cache)
66-
this._lastExpire = data.lastExpire
67-
} catch (e) {
68-
debug(e)
69-
this._cache = new Map()
70-
this._lastExpire = Date.now()
7188
}
72-
}
73-
74-
isExpired(data: Data<V>) {
75-
return isNumber(data.expire) && data.expire <= Date.now()
76-
}
77-
78-
get<T=V>(key: string, defaults: T): T
79-
get<T=V>(key: string): T | void
80-
get<T=V>(key: string, defaults?: T): T | void {
81-
try {
82-
const data = this._cache.get(key)
83-
if (!data) {
84-
return defaults
85-
} else if (this.isExpired(data)) {
86-
this.delete(key)
87-
return defaults
88-
} else {
89-
return data.value as any as T
90-
}
91-
} catch (error) {
92-
console.error(error)
89+
90+
public async getMany<Value>(keys: string[]): Promise<Array<StoredData<Value | undefined>>> {
91+
const results = await Promise.all(
92+
keys.map(async (key) => {
93+
const value = await this.get(key);
94+
return value as StoredData<Value | undefined>;
95+
})
96+
);
97+
return results;
9398
}
94-
}
95-
96-
has(key: string) {
97-
return typeof this.get(key) !== 'undefined'
98-
}
99-
100-
keys() {
101-
let keys = [] as string[]
102-
for (const key of this._cache.keys()) {
103-
if (!this.isExpired(this._cache.get(key)!)) {
104-
keys.push(key)
105-
}
99+
100+
public async set(key: string, value: any, ttl?: number) {
101+
if (ttl === 0) {
102+
ttl = undefined
103+
}
104+
this._cache.set(this._getKeyName(key), {
105+
expire: isNumber(ttl)
106+
? Date.now() + ttl
107+
: undefined,
108+
value: value as any
109+
})
110+
return this.save()
106111
}
107-
return keys
108-
}
109-
/**
110-
*
111-
* @param key
112-
* @param value
113-
* @param ttl time-to-live, seconds
114-
*/
115-
set<T = V>(key: string, value: T, ttl?: number) {
116-
if (ttl === 0) {
117-
ttl = undefined
112+
113+
public async delete(key: string) {
114+
const ret = this._cache.delete(this._getKeyName(key));
115+
await this.save();
116+
return ret;
118117
}
119-
this._cache.set(key, {
120-
value: value as any,
121-
expire: isNumber(ttl)
122-
? Date.now() + ttl
123-
: undefined
124-
})
125-
return this.save()
126-
}
127-
128-
delete(key: string): boolean {
129-
let ret = this._cache.delete(key)
130-
this.save()
131-
return ret
132-
}
133-
134-
clear() {
135-
this._cache = new Map()
136-
this._lastExpire = Date.now()
137-
return this.save()
138-
}
139-
140-
clearExpire() {
141-
const now = Date.now()
142-
if (now - this._lastExpire <= this._opts.expiredCheckDelay) {
143-
return
118+
119+
public async deleteMany(keys: string[]): Promise<boolean> {
120+
const deletePromises:Promise<boolean>[] = keys.map((key) => this.delete(key));
121+
const results = await Promise.all(deletePromises);
122+
return results.every((result) => result);
144123
}
145-
for (const key of this._cache.keys()) {
146-
const data = this._cache.get(key)
147-
if (this.isExpired(data!)) {
148-
this._cache.delete(key)
149-
}
124+
125+
public async clear() {
126+
this._cache = new Map()
127+
this._lastExpire = Date.now()
128+
return this.save()
150129
}
151-
this._lastExpire = now
152-
}
153130

154-
saveToDisk() {
155-
const cache = [] as [string, Data<V>][]
156-
for (const [key, val] of this._cache) {
157-
cache.push([key, val])
131+
public async * iterator(namespace?: string) {
132+
for (const [key, data] of this._cache) {
133+
// Filter by namespace if provided
134+
if (key === undefined) {
135+
continue;
136+
}
137+
if (!namespace || key.includes(namespace)) {
138+
const resolvedValue = data.value;
139+
yield [this._removeNamespaceFromKey(key), resolvedValue];
140+
}
141+
}
158142
}
159-
const data = this._opts.encode({
160-
cache,
161-
lastExpire: this._lastExpire,
162-
})
163-
return new Promise<void>((resolve, reject) => {
164-
fs.outputFile(this._opts.filename, data, err => {
165-
if (err) {
166-
reject(err)
167-
} else {
168-
resolve()
143+
144+
public async has(key: string): Promise<boolean> {
145+
return this._cache.has(this._getKeyName(key));
146+
}
147+
148+
private isExpired(data: any) {
149+
return isNumber(data.expire) && data.expire <= Date.now()
150+
}
151+
152+
private clearExpire() {
153+
const now = Date.now()
154+
if (now - this._lastExpire <= this.opts.expiredCheckDelay) {
155+
return
156+
}
157+
for (const key of this._cache.keys()) {
158+
const data = this._cache.get(key)
159+
if (this.isExpired(data!)) {
160+
this._cache.delete(key)
161+
}
162+
}
163+
this._lastExpire = now
164+
}
165+
166+
private saveToDisk() {
167+
const cache = [] as [string, any][];
168+
for (const [key, val] of this._cache) {
169+
cache.push([key, val]);
169170
}
170-
})
171-
})
172-
}
173-
private _savePromise?: Promise<any>
174-
save() {
175-
this.clearExpire()
176-
if (this._savePromise) {
177-
return this._savePromise
171+
const data = this.opts.serialize({
172+
cache,
173+
lastExpire: this._lastExpire,
174+
})
175+
return new Promise<void>((resolve, reject) => {
176+
fs.outputFile(this.opts.filename, data, (err) => {
177+
if (err) {
178+
reject(err);
179+
} else {
180+
resolve();
181+
}
182+
});
183+
});
178184
}
179-
this._savePromise = new Promise<void>((resolve, reject) => {
180-
setTimeout(
181-
() => {
182-
this.saveToDisk().then(() => {
183-
this._savePromise = void 0
184-
}).then(resolve, reject)
185-
},
186-
this._opts.writeDelay
187-
)
188-
})
189-
return this._savePromise
190-
}
185+
186+
private _savePromise?: Promise<any>
187+
188+
private save() {
189+
this.clearExpire();
190+
if (this._savePromise) {
191+
return this._savePromise;
192+
}
193+
this._savePromise = new Promise<void>((resolve, reject) => {
194+
setTimeout(
195+
() => {
196+
this.saveToDisk().then(() => {
197+
this._savePromise = void 0;
198+
}).then(resolve, reject);
199+
},
200+
this.opts.writeDelay
201+
)
202+
})
203+
return this._savePromise;
204+
}
205+
206+
public disconnect(): Promise<void> {
207+
return Promise.resolve();
208+
}
209+
191210
}
192-
export default KeyvFile
211+
212+
export default KeyvFile;

0 commit comments

Comments
 (0)