Skip to content
This repository was archived by the owner on May 25, 2025. It is now read-only.

Commit 9dec99d

Browse files
authored
feat: use hash fields as table namespace (#18)
1 parent d85ff72 commit 9dec99d

File tree

4 files changed

+110
-116
lines changed

4 files changed

+110
-116
lines changed

src/redisClient.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ export class RedisClient implements CommonClient {
271271
Convenient type-safe wrapper.
272272
Returns BATCHES of keys in each iteration (as-is).
273273
*/
274-
scanStream(opt: ScanStreamOptions): ReadableTyped<string[]> {
274+
scanStream(opt?: ScanStreamOptions): ReadableTyped<string[]> {
275275
return this.redis().scanStream(opt)
276276
}
277277

@@ -294,11 +294,11 @@ export class RedisClient implements CommonClient {
294294
return count
295295
}
296296

297-
hscanStream(key: string, opt: ScanStreamOptions): ReadableTyped<string[]> {
297+
hscanStream(key: string, opt?: ScanStreamOptions): ReadableTyped<string[]> {
298298
return this.redis().hscanStream(key, opt)
299299
}
300300

301-
async hscanCount(key: string, opt: ScanStreamOptions): Promise<number> {
301+
async hscanCount(key: string, opt?: ScanStreamOptions): Promise<number> {
302302
let count = 0
303303

304304
const stream = this.redis().hscanStream(key, opt)

src/redisHashKeyValueDB.ts

Lines changed: 48 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,46 @@ import {
66
IncrementTuple,
77
KeyValueDBTuple,
88
} from '@naturalcycles/db-lib'
9-
import { _chunk, StringMap } from '@naturalcycles/js-lib'
109
import { ReadableTyped } from '@naturalcycles/nodejs-lib'
11-
import { RedisClient } from './redisClient'
1210
import { RedisKeyValueDBCfg } from './redisKeyValueDB'
1311

14-
export interface RedisHashKeyValueDBCfg extends RedisKeyValueDBCfg {
15-
hashKey: string
16-
}
17-
18-
export class RedisHashKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
19-
client: RedisClient
20-
keyOfHashField: string
21-
22-
constructor(cfg: RedisHashKeyValueDBCfg) {
23-
this.client = cfg.client
24-
this.keyOfHashField = cfg.hashKey
25-
}
12+
/**
13+
* RedisHashKeyValueDB is a KeyValueDB implementation that uses hash fields to simulate tables.
14+
* The value in the `table` arguments points to a hash field in Redis.
15+
*
16+
* The reason for having this approach and also the traditional RedisKeyValueDB is that
17+
* the currently available Redis versions (in Memorystore, or on MacOs) do not support
18+
* expiring hash properties.
19+
* The expiring fields feature is important, and only available via RedisKeyValueDB.
20+
*
21+
* Once the available Redis version reaches 7.4.0+,
22+
* this implementation can take over for RedisKeyValueDB.
23+
*/
24+
export class RedishHashKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
25+
constructor(public cfg: RedisKeyValueDBCfg) {}
2626

2727
support = {
2828
...commonKeyValueDBFullSupport,
2929
}
3030

3131
async ping(): Promise<void> {
32-
await this.client.ping()
32+
await this.cfg.client.ping()
3333
}
3434

3535
async [Symbol.asyncDispose](): Promise<void> {
36-
await this.client.disconnect()
36+
await this.cfg.client.disconnect()
3737
}
3838

3939
async getByIds(table: string, ids: string[]): Promise<KeyValueDBTuple[]> {
4040
if (!ids.length) return []
4141
// we assume that the order of returned values is the same as order of input ids
42-
const bufs = await this.client.hmgetBuffer(this.keyOfHashField, this.idsToKeys(table, ids))
42+
const bufs = await this.cfg.client.hmgetBuffer(table, ids)
4343
return bufs.map((buf, i) => [ids[i], buf] as KeyValueDBTuple).filter(([_k, v]) => v !== null)
4444
}
4545

4646
async deleteByIds(table: string, ids: string[]): Promise<void> {
4747
if (!ids.length) return
48-
await this.client.hdel(this.keyOfHashField, this.idsToKeys(table, ids))
48+
await this.cfg.client.hdel(table, ids)
4949
}
5050

5151
async saveBatch(
@@ -55,106 +55,70 @@ export class RedisHashKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
5555
): Promise<void> {
5656
if (!entries.length) return
5757

58-
const entriesWithKey = entries.map(([k, v]) => [this.idToKey(table, k), v])
59-
const map: StringMap<any> = Object.fromEntries(entriesWithKey)
58+
const record = Object.fromEntries(entries)
6059

6160
if (opt?.expireAt) {
62-
await this.client.hsetWithTTL(this.keyOfHashField, map, opt.expireAt)
61+
await this.cfg.client.hsetWithTTL(table, record, opt.expireAt)
6362
} else {
64-
await this.client.hset(this.keyOfHashField, map)
63+
await this.cfg.client.hset(table, record)
6564
}
6665
}
6766

6867
streamIds(table: string, limit?: number): ReadableTyped<string> {
69-
let stream = this.client
70-
.hscanStream(this.keyOfHashField, {
71-
match: `${table}:*`,
72-
})
68+
const stream = this.cfg.client
69+
.hscanStream(table)
7370
.flatMap(keyValueList => {
7471
const keys: string[] = []
75-
keyValueList.forEach((keyOrValue, index) => {
76-
if (index % 2 !== 0) return
77-
keys.push(keyOrValue)
78-
})
79-
return this.keysToIds(table, keys)
72+
for (let i = 0; i < keyValueList.length; i += 2) {
73+
keys.push(keyValueList[i]!)
74+
}
75+
return keys
8076
})
81-
82-
if (limit) {
83-
stream = stream.take(limit)
84-
}
77+
.take(limit || Infinity)
8578

8679
return stream
8780
}
8881

8982
streamValues(table: string, limit?: number): ReadableTyped<Buffer> {
90-
return this.client
91-
.hscanStream(this.keyOfHashField, {
92-
match: `${table}:*`,
93-
})
83+
return this.cfg.client
84+
.hscanStream(table)
9485
.flatMap(keyValueList => {
95-
const values: string[] = []
96-
keyValueList.forEach((keyOrValue, index) => {
97-
if (index % 2 !== 1) return
98-
values.push(keyOrValue)
99-
})
100-
return values.map(v => Buffer.from(v))
86+
const values: Buffer[] = []
87+
for (let i = 0; i < keyValueList.length; i += 2) {
88+
const value = Buffer.from(keyValueList[i + 1]!)
89+
values.push(value)
90+
}
91+
return values
10192
})
10293
.take(limit || Infinity)
10394
}
10495

10596
streamEntries(table: string, limit?: number): ReadableTyped<KeyValueDBTuple> {
106-
return this.client
107-
.hscanStream(this.keyOfHashField, {
108-
match: `${table}:*`,
109-
})
97+
return this.cfg.client
98+
.hscanStream(table)
11099
.flatMap(keyValueList => {
111-
const entries = _chunk(keyValueList, 2)
112-
return entries.map(([k, v]) => {
113-
return [this.keyToId(table, String(k)), Buffer.from(String(v))] satisfies KeyValueDBTuple
114-
})
100+
const entries: [string, Buffer][] = []
101+
for (let i = 0; i < keyValueList.length; i += 2) {
102+
const key = keyValueList[i]!
103+
const value = Buffer.from(keyValueList[i + 1]!)
104+
entries.push([key, value])
105+
}
106+
return entries
115107
})
116108
.take(limit || Infinity)
117109
}
118110

119111
async count(table: string): Promise<number> {
120-
return await this.client.hscanCount(this.keyOfHashField, {
121-
match: `${table}:*`,
122-
})
112+
return await this.cfg.client.hscanCount(table)
123113
}
124114

125115
async incrementBatch(table: string, increments: IncrementTuple[]): Promise<IncrementTuple[]> {
126-
const incrementTuplesWithInternalKeys = increments.map(
127-
([id, v]) => [this.idToKey(table, id), v] as [string, number],
128-
)
129-
const resultsWithInternalKeys = await this.client.hincrBatch(
130-
this.keyOfHashField,
131-
incrementTuplesWithInternalKeys,
132-
)
133-
const results = resultsWithInternalKeys.map(
134-
([k, v]) => [this.keyToId(table, k), v] as IncrementTuple,
135-
)
136-
return results
116+
return await this.cfg.client.hincrBatch(table, increments)
137117
}
138118

139119
async createTable(table: string, opt?: CommonDBCreateOptions): Promise<void> {
140120
if (!opt?.dropIfExists) return
141121

142-
await this.client.dropTable(table)
143-
}
144-
145-
private idsToKeys(table: string, ids: string[]): string[] {
146-
return ids.map(id => this.idToKey(table, id))
147-
}
148-
149-
private idToKey(table: string, id: string): string {
150-
return `${table}:${id}`
151-
}
152-
153-
private keysToIds(table: string, keys: string[]): string[] {
154-
return keys.map(key => this.keyToId(table, key))
155-
}
156-
157-
private keyToId(table: string, key: string): string {
158-
return key.slice(table.length + 1)
122+
await this.cfg.client.del([table])
159123
}
160124
}

src/redisKeyValueDB.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,30 @@ export interface RedisKeyValueDBCfg {
1515
}
1616

1717
export class RedisKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
18-
constructor(cfg: RedisKeyValueDBCfg) {
19-
this.client = cfg.client
20-
}
21-
22-
client: RedisClient
18+
constructor(public cfg: RedisKeyValueDBCfg) {}
2319

2420
support = {
2521
...commonKeyValueDBFullSupport,
2622
}
2723

2824
async ping(): Promise<void> {
29-
await this.client.ping()
25+
await this.cfg.client.ping()
3026
}
3127

3228
async [Symbol.asyncDispose](): Promise<void> {
33-
await this.client.disconnect()
29+
await this.cfg.client.disconnect()
3430
}
3531

3632
async getByIds(table: string, ids: string[]): Promise<KeyValueDBTuple[]> {
3733
if (!ids.length) return []
3834
// we assume that the order of returned values is the same as order of input ids
39-
const bufs = await this.client.mgetBuffer(this.idsToKeys(table, ids))
35+
const bufs = await this.cfg.client.mgetBuffer(this.idsToKeys(table, ids))
4036
return bufs.map((buf, i) => [ids[i], buf] as KeyValueDBTuple).filter(([_k, v]) => v !== null)
4137
}
4238

4339
async deleteByIds(table: string, ids: string[]): Promise<void> {
4440
if (!ids.length) return
45-
await this.client.del(this.idsToKeys(table, ids))
41+
await this.cfg.client.del(this.idsToKeys(table, ids))
4642
}
4743

4844
async saveBatch(
@@ -55,7 +51,7 @@ export class RedisKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
5551
if (opt?.expireAt) {
5652
// There's no supported mset with TTL: https://stackoverflow.com/questions/16423342/redis-multi-set-with-a-ttl
5753
// so we gonna use a pipeline instead
58-
await this.client.withPipeline(pipeline => {
54+
await this.cfg.client.withPipeline(pipeline => {
5955
for (const [k, v] of entries) {
6056
pipeline.set(this.idToKey(table, k), v, 'EXAT', opt.expireAt!)
6157
}
@@ -64,12 +60,12 @@ export class RedisKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
6460
const obj: Record<string, Buffer> = Object.fromEntries(
6561
entries.map(([k, v]) => [this.idToKey(table, k), v]) as KeyValueDBTuple[],
6662
)
67-
await this.client.msetBuffer(obj)
63+
await this.cfg.client.msetBuffer(obj)
6864
}
6965
}
7066

7167
streamIds(table: string, limit?: number): ReadableTyped<string> {
72-
let stream = this.client
68+
let stream = this.cfg.client
7369
.scanStream({
7470
match: `${table}:*`,
7571
// count: limit, // count is actually a "batchSize", not a limit
@@ -84,13 +80,13 @@ export class RedisKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
8480
}
8581

8682
streamValues(table: string, limit?: number): ReadableTyped<Buffer> {
87-
return this.client
83+
return this.cfg.client
8884
.scanStream({
8985
match: `${table}:*`,
9086
})
9187
.flatMap(
9288
async keys => {
93-
return (await this.client.mgetBuffer(keys)).filter(_isTruthy)
89+
return (await this.cfg.client.mgetBuffer(keys)).filter(_isTruthy)
9490
},
9591
{
9692
concurrency: 16,
@@ -100,14 +96,14 @@ export class RedisKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
10096
}
10197

10298
streamEntries(table: string, limit?: number): ReadableTyped<KeyValueDBTuple> {
103-
return this.client
99+
return this.cfg.client
104100
.scanStream({
105101
match: `${table}:*`,
106102
})
107103
.flatMap(
108104
async keys => {
109105
// casting as Buffer[], because values are expected to exist for given keys
110-
const bufs = (await this.client.mgetBuffer(keys)) as Buffer[]
106+
const bufs = (await this.cfg.client.mgetBuffer(keys)) as Buffer[]
111107
return _zip(this.keysToIds(table, keys), bufs)
112108
},
113109
{
@@ -119,7 +115,7 @@ export class RedisKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
119115

120116
async count(table: string): Promise<number> {
121117
// todo: implement more efficiently, e.g via LUA?
122-
return await this.client.scanCount({
118+
return await this.cfg.client.scanCount({
123119
match: `${table}:*`,
124120
})
125121
}
@@ -128,7 +124,7 @@ export class RedisKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
128124
const incrementTuplesWithInternalKeys = increments.map(
129125
([id, v]) => [this.idToKey(table, id), v] as [string, number],
130126
)
131-
const resultsWithInternalKeys = await this.client.incrBatch(incrementTuplesWithInternalKeys)
127+
const resultsWithInternalKeys = await this.cfg.client.incrBatch(incrementTuplesWithInternalKeys)
132128
const results = resultsWithInternalKeys.map(
133129
([k, v]) => [this.keyToId(table, k), v] as IncrementTuple,
134130
)
@@ -138,7 +134,7 @@ export class RedisKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
138134
async createTable(table: string, opt?: CommonDBCreateOptions): Promise<void> {
139135
if (!opt?.dropIfExists) return
140136

141-
await this.client.dropTable(table)
137+
await this.cfg.client.dropTable(table)
142138
}
143139

144140
private idsToKeys(table: string, ids: string[]): string[] {

0 commit comments

Comments
 (0)