Skip to content

Commit

Permalink
add iterator on etcd adapter (#857)
Browse files Browse the repository at this point in the history
  • Loading branch information
Christian Llontop committed Jul 1, 2023
1 parent 294661a commit 3d1c6ba
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 22 deletions.
37 changes: 24 additions & 13 deletions packages/etcd/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {EventEmitter} from 'events';
import type {Lease} from 'etcd3';
import {Etcd3} from 'etcd3';
import {ExponentialBackoff, handleAll, retry} from 'cockatiel';
import type {Store, StoredData} from 'keyv';
import type {StoredData} from 'keyv';
import type {ClearOutput, DeleteManyOutput, DeleteOutput, GetOutput, HasOutput, SetOutput} from './types';

type KeyvEtcdOptions = {
url?: string;
Expand All @@ -11,9 +12,7 @@ type KeyvEtcdOptions = {
busyTimeout?: number;
};

type GetOutput<Value> = Value | Promise<Value | undefined> | undefined;

class KeyvEtcd<Value = any> extends EventEmitter implements Store<Value> {
class KeyvEtcd<Value = any> extends EventEmitter {
public ttlSupport: boolean;
public opts: KeyvEtcdOptions;
public client: Etcd3;
Expand Down Expand Up @@ -72,11 +71,11 @@ class KeyvEtcd<Value = any> extends EventEmitter implements Store<Value> {
}
}

get(key: string): GetOutput<Value> {
async get(key: string): GetOutput<Value> {
return this.client.get(key) as unknown as GetOutput<Value>;
}

getMany(keys: string[]): Promise<Array<StoredData<Value>>> {
async getMany(keys: string[]): Promise<Array<StoredData<Value>>> {
const promises = [];
for (const key of keys) {
promises.push(this.get(key));
Expand All @@ -99,7 +98,7 @@ class KeyvEtcd<Value = any> extends EventEmitter implements Store<Value> {
});
}

set(key: string, value: Value) {
async set(key: string, value: Value): SetOutput {
let client: 'lease' | 'client' = 'client';

if (this.opts.ttl) {
Expand All @@ -110,15 +109,15 @@ class KeyvEtcd<Value = any> extends EventEmitter implements Store<Value> {
return this[client]!.put(key).value(value);
}

delete(key: string): Promise<boolean> {
async delete(key: string): DeleteOutput {
if (typeof key !== 'string') {
return Promise.resolve(false);
return false;
}

return this.client.delete().key(key).then(key => key.deleted !== '0');
}

deleteMany(keys: string[]): Promise<boolean> {
async deleteMany(keys: string[]): DeleteManyOutput {
const promises = [];
for (const key of keys) {
promises.push(this.delete(key));
Expand All @@ -128,18 +127,30 @@ class KeyvEtcd<Value = any> extends EventEmitter implements Store<Value> {
return Promise.allSettled(promises).then(values => values.every(x => x.value === true));
}

clear(): Promise<void> {
async clear(): ClearOutput {
const promise = this.namespace
? this.client.delete().prefix(this.namespace)
: this.client.delete().all();
return promise.then(() => undefined);
}

has(key: string): Promise<boolean> {
async * iterator(namespace?: string) {
const iterator = await this.client
.getAll()
.prefix(namespace ? namespace + ':' : '')
.keys();

for await (const key of iterator) {
const value = await this.get(key);
yield [key, value];
}
}

async has(key: string): HasOutput {
return this.client.get(key).exists();
}

disconnect() {
async disconnect() {
return this.client.close();
}
}
Expand Down
15 changes: 15 additions & 0 deletions packages/etcd/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {type StoredData} from 'keyv';

export type GetOutput<Value> = Promise<Value | undefined>;

export type GetManyOutput<Value> = Promise<Array<StoredData<Value | undefined> | undefined>>;

export type SetOutput = Promise<any>;

export type DeleteOutput = Promise<boolean>;

export type DeleteManyOutput = Promise<boolean>;

export type ClearOutput = Promise<void>;

export type HasOutput = Promise<boolean>;
49 changes: 40 additions & 9 deletions packages/etcd/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ const store = () => new KeyvEtcd({uri: etcdUrl, busyTimeout: 3000});

keyvTestSuite(test, Keyv, store);

test('default options', t => {
test.serial('default options', t => {
const store = new KeyvEtcd();
t.deepEqual(store.opts, {
url: '127.0.0.1:2379',
});
});

test('enable ttl using default url', t => {
test.serial('enable ttl using default url', t => {
const store = new KeyvEtcd({ttl: 1000});
t.deepEqual(store.opts, {
url: '127.0.0.1:2379',
Expand All @@ -27,7 +27,7 @@ test('enable ttl using default url', t => {
t.is(store.ttlSupport, true);
});

test('disable ttl using default url', t => {
test.serial('disable ttl using default url', t => {
// @ts-expect-error - ttl is not a number, just for test
const store = new KeyvEtcd({ttl: true});
t.deepEqual(store.opts, {
Expand All @@ -37,7 +37,7 @@ test('disable ttl using default url', t => {
t.is(store.ttlSupport, false);
});

test('enable ttl using url and options', t => {
test.serial('enable ttl using url and options', t => {
const store = new KeyvEtcd('127.0.0.1:2379', {ttl: 1000});
t.deepEqual(store.opts, {
url: '127.0.0.1:2379',
Expand All @@ -46,7 +46,7 @@ test('enable ttl using url and options', t => {
t.is(store.ttlSupport, true);
});

test('disable ttl using url and options', t => {
test.serial('disable ttl using url and options', t => {
// @ts-expect-error - ttl is not a number, just for test
const store = new KeyvEtcd('127.0.0.1:2379', {ttl: true});
t.deepEqual(store.opts, {
Expand All @@ -70,18 +70,18 @@ test.serial('KeyvEtcd respects default tll option', async t => {
t.is(await keyv.get('foo'), null);
});

test('.delete() with key as number', async t => {
test.serial('.delete() with key as number', async t => {
const store = new KeyvEtcd({uri: etcdUrl});
// @ts-expect-error - key needs be a string, just for test
t.false(await store.delete(123));
});

test('.clear() with default namespace', async t => {
test.serial('.clear() with default namespace', async t => {
const store = new KeyvEtcd(etcdUrl);
t.is(await store.clear(), undefined);
});

test('.clear() with namespace', async t => {
test.serial('.clear() with namespace', async t => {
const store = new KeyvEtcd(etcdUrl);
store.namespace = 'key1';
await store.set(`${store.namespace}:key`, 'bar');
Expand All @@ -92,7 +92,7 @@ test('.clear() with namespace', async t => {
test.serial('close connection successfully', async t => {
const keyv = new KeyvEtcd(etcdUrl);
t.is(await keyv.get('foo'), null);
keyv.disconnect();
await keyv.disconnect();
try {
await keyv.get('foo');
t.fail();
Expand All @@ -101,3 +101,34 @@ test.serial('close connection successfully', async t => {
}
});

test.serial('iterator with namespace', async t => {
const store = new KeyvEtcd(etcdUrl);
store.namespace = 'key1';
await store.set('key1:foo', 'bar');
await store.set('key1:foo2', 'bar2');
const iterator = store.iterator('key1');
let entry = await iterator.next();
// @ts-expect-error - test iterator
t.is(entry.value[0], 'key1:foo');
// @ts-expect-error - test iterator
t.is(entry.value[1], 'bar');
entry = await iterator.next();
// @ts-expect-error - test iterator
t.is(entry.value[0], 'key1:foo2');
// @ts-expect-error - test iterator
t.is(entry.value[1], 'bar2');
entry = await iterator.next();
t.is(entry.value, undefined);
});

test.serial('iterator without namespace', async t => {
const store = new KeyvEtcd(etcdUrl);
await store.set('foo', 'bar');
const iterator = store.iterator();
const entry = await iterator.next();
// @ts-expect-error - test iterator
t.is(entry.value[0], 'foo');
// @ts-expect-error - test iterator
t.is(entry.value[1], 'bar');
});

0 comments on commit 3d1c6ba

Please sign in to comment.