Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nip98-redis-nx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostream": patch
---

feat(redis): add setKeyIfNotExists for one-time claims
2 changes: 1 addition & 1 deletion src/@types/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface ICacheAdapter {
getKey(key: string): Promise<string>
hasKey(key: string): Promise<boolean>
setKey(key: string, value: string, expirySeconds?: number): Promise<boolean>
setKeyIfNotExists(key: string, value: string, expirySeconds?: number): Promise<boolean>
addToSortedSet(key: string, set: Record<string, string> | Record<string, string>[]): Promise<number>
removeRangeByScoreFromSortedSet(key: string, min: number, max: number): Promise<number>
getRangeFromSortedSet(key: string, start: number, stop: number): Promise<string[]>
Expand All @@ -33,6 +34,5 @@ export interface ICacheAdapter {
getHKey(key: string, field: string): Promise<string>
setHKey(key: string, fields: Record<string, string>): Promise<boolean>


eval(script: string, keys: string[], args: string[]): Promise<unknown>
}
19 changes: 12 additions & 7 deletions src/adapters/redis-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { ICacheAdapter } from '../@types/adapters'
import { CacheClient } from '../@types/cache'
import { createLogger } from '../factories/logger-factory'
import { ICacheAdapter } from '../@types/adapters'

const logger = createLogger('redis-adapter')

export class RedisAdapter implements ICacheAdapter {

private connection: Promise<void>

private scriptShas: Map<string, string> = new Map()
Expand Down Expand Up @@ -70,6 +69,15 @@ export class RedisAdapter implements ICacheAdapter {
return 'OK' === (await this.client.set(key, value))
}

public async setKeyIfNotExists(key: string, value: string, expirySeconds?: number): Promise<boolean> {
await this.connection
logger('set nx %s key', key)
if (typeof expirySeconds === 'number') {
return 'OK' === (await this.client.set(key, value, { EX: expirySeconds, NX: true }))
}
return 'OK' === (await this.client.set(key, value, { NX: true }))
}

public async removeRangeByScoreFromSortedSet(key: string, min: number, max: number): Promise<number> {
await this.connection
logger('remove %d..%d range from sorted set %s', min, max, key)
Expand All @@ -96,7 +104,6 @@ export class RedisAdapter implements ICacheAdapter {
return this.client.zAdd(key, members)
}


public async deleteKey(key: string): Promise<number> {
await this.connection
logger('delete %s key', key)
Expand All @@ -106,13 +113,13 @@ export class RedisAdapter implements ICacheAdapter {
public async getHKey(key: string, field: string): Promise<string> {
await this.connection
logger('get %s field for key %s', field, key)
return await this.client.hGet(key, field) ?? ''
return (await this.client.hGet(key, field)) ?? ''
}

public async setHKey(key: string, fields: Record<string, string>): Promise<boolean> {
await this.connection
logger('set %s key', key)
return await this.client.hSet(key, fields) >= 0
return (await this.client.hSet(key, fields)) >= 0
}

public async eval(script: string, keys: string[], args: string[]): Promise<unknown> {
Expand All @@ -123,6 +130,4 @@ export class RedisAdapter implements ICacheAdapter {
}
return await this.client.evalSha(this.scriptShas.get(script)!, { keys, arguments: args })
}


}
19 changes: 19 additions & 0 deletions test/unit/adapters/redis-adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,25 @@ describe('RedisAdapter', () => {
})
})

describe('setKeyIfNotExists', () => {
it('returns true when NX set creates the key', async () => {
client.set.resolves('OK')

const result = await adapter.setKeyIfNotExists('key', 'value', 60)

expect(client.set).to.have.been.calledOnceWithExactly('key', 'value', { EX: 60, NX: true })
expect(result).to.be.true
})

it('returns false when the key already exists', async () => {
client.set.resolves(null)

const result = await adapter.setKeyIfNotExists('key', 'value', 60)

expect(result).to.be.false
})
})

describe('removeRangeByScoreFromSortedSet', () => {
it('calls client.zRemRangeByScore with correct arguments', async () => {
client.zRemRangeByScore.resolves(3)
Expand Down
Loading