Skip to content

Commit

Permalink
Implement function loaders (#290)
Browse files Browse the repository at this point in the history
  • Loading branch information
kibertoad committed Dec 7, 2023
1 parent 67d9498 commit f7da2c3
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 3 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,45 @@ const loader = new Loader<string>({
const classifier = await loader.get('1')
```

### Simplified loader syntax

It is also possible to inline datasource definition:

```ts
const loader = new Loader<string>({
// this cache will be checked first
inMemoryCache: {
cacheType: 'lru-object', // you can choose between lru and fifo caches, fifo being 10% slightly faster
ttlInMsecs: 1000 * 60,
maxItems: 100,
},

// this cache will be checked if in-memory one returns undefined
asyncCache: new RedisCache(ioRedis, {
json: true, // this instructs loader to serialize passed objects as string and deserialize them back to objects
ttlInMsecs: 1000 * 60 * 10,
}),

// data source will be generated from one or both provided data loading functions
dataSourceGetOneFn: async (key: string) => {
const results = await this.db('classifiers')
.select('*')
.where({
id: parseInt(key),
})
return results[0]
},
dataSourceGetManyFn: (keys: string[]) => {
return this.db('classifiers')
.select('*')
.whereIn('id', keys.map(parseInt))
}
})

// If cache is empty, but there is data in the DB, after this operation is completed, both caches will be populated
const classifier = await loader.get('1')
```

## Loader API

Loader has the following config parameters:
Expand Down
35 changes: 35 additions & 0 deletions lib/GeneratedDataSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { DataSource } from './types/DataSources'

export type GeneratedDataSourceParams<LoadedValue, LoaderParams = undefined> = {
name?: string
dataSourceGetOneFn?: (key: string, loadParams?: LoaderParams) => Promise<LoadedValue | undefined | null>
dataSourceGetManyFn?: (keys: string[], loadParams?: LoaderParams) => Promise<LoadedValue[]>
}

export class GeneratedDataSource<LoadedValue, LoadParams = undefined> implements DataSource<LoadedValue, LoadParams> {
private readonly getOneFn: (key: string, loadParams?: LoadParams) => Promise<LoadedValue | undefined | null>
private readonly getManyFn: (keys: string[], loadParams?: LoadParams) => Promise<LoadedValue[]>
public readonly name: string
constructor(params: GeneratedDataSourceParams<LoadedValue, LoadParams>) {
this.name = params.name ?? 'Generated loader'
this.getOneFn =
params.dataSourceGetOneFn ??
function () {
throw new Error('Retrieval of a single entity is not implemented')
}

this.getManyFn =
params.dataSourceGetManyFn ??
function () {
throw new Error('Retrieval of multiple entities is not implemented')
}
}

get(key: string, loadParams: LoadParams | undefined): Promise<LoadedValue | undefined | null> {
return this.getOneFn(key, loadParams)
}

getMany(keys: string[], loadParams: LoadParams | undefined): Promise<LoadedValue[]> {
return this.getManyFn(keys, loadParams)
}
}
29 changes: 28 additions & 1 deletion lib/Loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { InMemoryGroupCacheConfiguration } from './memory/InMemoryGroupCach
import type { SynchronousCache, SynchronousGroupCache, GetManyResult } from './types/SyncDataSources'
import type { NotificationPublisher } from './notifications/NotificationPublisher'
import type { GroupNotificationPublisher } from './notifications/GroupNotificationPublisher'
import { GeneratedDataSource } from './GeneratedDataSource'

export type LoaderConfig<
LoadedValue,
Expand All @@ -23,6 +24,9 @@ export type LoaderConfig<
| GroupNotificationPublisher<LoadedValue> = NotificationPublisher<LoadedValue>,
> = {
dataSources?: readonly DataSourceType[]
dataSourceGetOneFn?: (key: string, loadParams?: LoaderParams) => Promise<LoadedValue | undefined | null>
dataSourceGetManyFn?: (keys: string[], loadParams?: LoaderParams) => Promise<LoadedValue[]>
dataSourceName?: string
throwIfLoadError?: boolean
throwIfUnresolved?: boolean
} & CommonCacheConfig<LoadedValue, CacheType, InMemoryCacheConfigType, InMemoryCacheType, NotificationPublisherType>
Expand All @@ -35,7 +39,30 @@ export class Loader<LoadedValue, LoaderParams = undefined> extends AbstractFlatC

constructor(config: LoaderConfig<LoadedValue, Cache<LoadedValue>, LoaderParams>) {
super(config)
this.dataSources = config.dataSources ?? []

// generated datasource
if (config.dataSourceGetManyFn || config.dataSourceGetOneFn) {
if (config.dataSources) {
throw new Error('Cannot set both "dataSources" and "dataSourceGetManyFn"/"dataSourceGetOneFn" parameters.')
}

this.dataSources = [
new GeneratedDataSource({
dataSourceGetOneFn: config.dataSourceGetOneFn,
dataSourceGetManyFn: config.dataSourceGetManyFn,
name: config.dataSourceName,
}),
]
}
// defined datasource
else if (config.dataSources) {
this.dataSources = config.dataSources
}
// no datasource
else {
this.dataSources = []
}

this.throwIfLoadError = config.throwIfLoadError ?? true
this.throwIfUnresolved = config.throwIfUnresolved ?? false
this.isKeyRefreshing = new Set()
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@
"toad-cache": "^3.3.1"
},
"devDependencies": {
"@types/node": "^20.10.1",
"@types/node": "^20.10.2",
"@typescript-eslint/eslint-plugin": "^6.13.1",
"@typescript-eslint/parser": "^6.13.1",
"@vitest/coverage-v8": "0.34.6",
"del-cli": "^5.1.0",
"eslint": "^8.54.0",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.1",
Expand Down
74 changes: 74 additions & 0 deletions test/Loader-main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,19 @@ describe('Loader Main', () => {
})
})

describe('constructor', () => {
it('throws an error if both datasource and datasource fns are provided', () => {
expect(() => {
new Loader({
dataSources: [],
dataSourceGetOneFn: () => {
return Promise.resolve('x')
},
})
}).toThrow(/Cannot set both/)
})
})

describe('get', () => {
it('returns undefined when fails to resolve value', async () => {
const operation = new Loader({})
Expand Down Expand Up @@ -461,6 +474,36 @@ describe('Loader Main', () => {
expect(result).toBe('value')
})

it('returns value when resolved via generated loader', async () => {
const operation = new Loader<string>({
inMemoryCache: IN_MEMORY_CACHE_CONFIG,
dataSourceGetOneFn: (key) => {
if (key === 'key') {
return Promise.resolve('value')
}
throw new Error('Not found')
},
})

const result = await operation.get('key')

expect(result).toBe('value')
})

it('throws an error if requested generated loader is not set', async () => {
const operation = new Loader<string>({
inMemoryCache: IN_MEMORY_CACHE_CONFIG,
dataSourceGetManyFn: (keys) => {
if (keys[0] === 'key') {
return Promise.resolve(['value'])
}
throw new Error('Not found')
},
})

await expect(operation.get('key')).rejects.toThrow(/Retrieval of a single entity is not/)
})

it('returns value when resolved via multiple loaders', async () => {
const asyncCache = new DummyCache(undefined)

Expand Down Expand Up @@ -664,6 +707,37 @@ describe('Loader Main', () => {
expect(result).toEqual(['value'])
})

it('returns value when resolved via generated loader', async () => {
const operation = new Loader<string>({
inMemoryCache: IN_MEMORY_CACHE_CONFIG,
dataSourceGetManyFn: (keys: string[]) => {
if (keys.includes('key')) {
return Promise.resolve(['value'])
}

throw new Error('Not found')
},
})

const result = await operation.getMany(['key'], idResolver)

expect(result).toEqual(['value'])
})

it('throws an error if requested generated loader is not set', async () => {
const operation = new Loader<string>({
inMemoryCache: IN_MEMORY_CACHE_CONFIG,
dataSourceGetOneFn: (key) => {
if (key === 'key') {
return Promise.resolve('value')
}
throw new Error('Not found')
},
})

await expect(operation.getMany(['key'], idResolver)).rejects.toThrow(/Retrieval of multiple entities/)
})

it('returns value when resolved via multiple caches', async () => {
const asyncCache = new DummyCache(undefined)

Expand Down

0 comments on commit f7da2c3

Please sign in to comment.