Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(pinia-orm): add optional caching for same get requests #272

Merged
merged 5 commits into from
Sep 15, 2022
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
45 changes: 45 additions & 0 deletions docs/content/1.guide/4.repository/2.retrieving-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,51 @@ const users = useRepo(User)
const users = useRepo(User).groupBy('name', 'age').get()
```

## Caching

The `useCache` method allows you to cache the results if your store requests are more frequent.
The initial retrieve query will be a bit smaller but all next are very fast.


For caching a custom [Weakref](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef) provider is used. That way garbage collection is included.


```js
// Generate a cache with a auto generated key.
const users = useRepo(User).useCache().get()

// Generate a cache with a manual key (recommanded).
const users = useRepo(User).useCache('key').get()

// Generate a cache with a manual key and dynamic params (recommanded).
const users = useRepo(User).useCache('key', { id: idProperty }).get()
```

You can access the current cache instance with the `cache` method

```js
// Get the repos cache instance
const cache = useRepo(User).cache()

// Getting the size of the current cache
useRepo(User).cache().size

// Checking if a specific key exist
useRepo(User).cache().has('key')
```

As a default configuration the cache is shared between the repos. If you don't want it
to be shared you can set it in the config.

```js
// Setting the cache not to be shared for every repo
createORM({
cache: {
shared: false,
},
})
```


## Limit & Offset

Expand Down
26 changes: 26 additions & 0 deletions docs/content/2.api/3.query/use-cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: 'useCache()'
description: 'Cache the query result.'
---

## Usage

````ts
import { useRepo } from 'pinia-orm'
import User from './models/User'

// Generate a cache with a auto generated key.
useRepo(User).useCache().get()

// Generate a cache with a manual key (recommanded).
useRepo(User).useCache('key').get()

// Generate a cache with a manual key and dynamic params (recommanded).
useRepo(User).useCache('key', { id: idProperty }).get()
````

## Typescript Declarations

````ts
function useCache(key?: string, params?: Record<string, any>): Query
````
20 changes: 20 additions & 0 deletions docs/content/2.api/4.repository/cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: 'cache'
description: 'Returns the cache instance of the repository'
---

## Usage

````js
import { useRepo } from 'pinia-orm'
import User from './models/User'

// Returns the cache instance
useRepo(User).cache()
````

## Typescript Declarations

````ts
function cache(): WeakCache
````
13 changes: 13 additions & 0 deletions docs/content/2.api/5.configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ icon: heroicons-outline:adjustments
| `visible` | `[*]` | Sets default visible fields for every model |
| `hidden` | `[]` | Sets default hidden fields for every model |

## `cache`

| Option | Default | Description |
|------------|:-----------:|:----------------------------------------------------------|
| `provider` | `Weakcache` | Defines which cache provider should be used |
| `shared` | `true` | Activates the cache to be shared between all repositories |

## Typescript Declarations

````ts
Expand All @@ -21,8 +28,14 @@ export interface ModelConfigOptions {
visible?: string[]
}

export interface CacheConfigOptions {
shared?: boolean
provider?: typeof WeakCache<string, Model[]>
}

export interface InstallOptions {
model?: ModelConfigOptions
cache?: CacheConfigOptions | boolean
}
const options: InstallOptions
````
4 changes: 4 additions & 0 deletions packages/pinia-orm/src/cache/SharedWeakCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Model } from '../model/Model'
import { WeakCache } from './WeakCache'

export const cache = new WeakCache<string, Model[]>()
48 changes: 48 additions & 0 deletions packages/pinia-orm/src/cache/SimpleCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export interface StoredData<T> {
key: string
data: T
expiration: number
}

// By default data will expire in 5 minutes.
const DEFAULT_EXPIRATION_SECONDS = 5 * 60

export class SimpleCache {
constructor(private cache = new Map()) {}

clear(): void {
this.cache = new Map()
}

size(): number {
return this.cache.size
}

private computeExpirationTime(expiresInSeconds: number): number {
return new Date().getTime() + expiresInSeconds * 1000
}

// Store the data in memory and attach to the object expiration containing the
// expiration time.
set<T>({ key, data, expiration = DEFAULT_EXPIRATION_SECONDS }: StoredData<T>): T {
this.cache.set(key, { data, expiration: this.computeExpirationTime(expiration) })

return data
}

// Will get specific data from the Map object based on a key and return null if
// the data has expired.
get<T>(key: string): T | null {
if (this.cache.has(key)) {
const { data, expiration } = this.cache.get(key) as StoredData<T>

return this.hasExpired(expiration) ? null : data
}

return null
}

private hasExpired(expiration: number): boolean {
return expiration < new Date().getTime()
}
}
73 changes: 73 additions & 0 deletions packages/pinia-orm/src/cache/WeakCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
export class WeakCache<K, V extends object> implements Map<K, V> {
// @ts-expect-error dont know
readonly [Symbol.toStringTag]: string

#map = new Map<K, WeakRef<V>>()

has(key: K) {
return !!(this.#map.has(key) && this.#map.get(key)?.deref())
}

get(key: K): V {
const weakRef = this.#map.get(key)
if (!weakRef)
// @ts-expect-error object has no undefined
return undefined

const value = weakRef.deref()
if (value)
return value

// If it cant be dereference, remove the key
this.#map.delete(key)
// @ts-expect-error object has no undefined
return undefined
}

set(key: K, value: V) {
this.#map.set(key, new WeakRef<V>(value))
return this
}

get size(): number {
return this.#map.size
}

clear(): void {
this.#map.clear()
}

delete(key: K): boolean {
this.#map.delete(key)
return false
}

forEach(cb: (value: V, key: K, map: Map<K, V>) => void): void {
for (const [key, value] of this) cb(value, key, this)
}

* [Symbol.iterator](): IterableIterator<[K, V]> {
for (const [key, weakRef] of this.#map) {
const ref = weakRef.deref()

// If it cant be dereference, remove the key
if (!ref) {
this.#map.delete(key)
continue
}
yield [key, ref]
}
}

* entries(): IterableIterator<[K, V]> {
for (const [key, value] of this) yield [key, value]
}

* keys(): IterableIterator<K> {
for (const [key] of this) yield key
}

* values(): IterableIterator<V> {
for (const [, value] of this) yield value
}
}
4 changes: 1 addition & 3 deletions packages/pinia-orm/src/composables/useDataStore.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { defineStore } from 'pinia'
import type { Model } from '../model/Model'
import type { FilledInstallOptions } from '../store/Store'
import { useStoreActions } from './useStoreActions'

export function useDataStore<M extends Model = Model>(
id: string,
options: Record<string, any> | null = null,
) {
return defineStore(id, {
state: (): DataStoreState<M> => ({ data: {}, config: {} as FilledInstallOptions }),
state: (): DataStoreState<M> => ({ data: {} }),
actions: useStoreActions(),
...options,
})
}

export interface DataStoreState<M extends Model = Model> {
data: Record<string, M>
config: FilledInstallOptions
}

export type DataStore = ReturnType<typeof import('@/composables')['useDataStore']>
14 changes: 8 additions & 6 deletions packages/pinia-orm/src/model/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Collection, Element, Item } from '../data/Data'
import type { MutatorFunctions, Mutators } from '../types'
import type { DataStore, DataStoreState } from '../composables/useDataStore'
import type { ModelConfigOptions } from '../store/Store'
import { config } from '../store/Config'
import type { Attribute } from './attributes/Attribute'
import { Attr } from './attributes/types/Attr'
import { String as Str } from './attributes/types/String'
Expand Down Expand Up @@ -613,11 +614,12 @@ export class Model {
*/
$fill(attributes: Element = {}, options: ModelOptions = {}): this {
const operation = options.operation ?? 'get'
const config = {
...options.config,

const modelConfig = {
...config.model,
...this.$config(),
}
config.withMeta && (this.$self().schemas[this.$entity()][this.$self().metaKey] = this.$self().attr({}))
modelConfig.withMeta && (this.$self().schemas[this.$entity()][this.$self().metaKey] = this.$self().attr({}))

const fields = this.$fields()
const fillRelation = options.relations ?? true
Expand Down Expand Up @@ -658,7 +660,7 @@ export class Model {
this[key] = this[key] ?? keyValue
}

config.withMeta && operation === 'set' && this.$fillMeta(options.action)
modelConfig.withMeta && operation === 'set' && this.$fillMeta(options.action)

return this
}
Expand Down Expand Up @@ -690,8 +692,8 @@ export class Model {
}

protected isFieldVisible(key: string, modelHidden: string[], modelVisible: string[], options: ModelOptions): boolean {
const hidden = modelHidden.length > 0 ? modelHidden : options.config?.hidden ?? []
const visible = [...(modelVisible.length > 0 ? modelVisible : options.config?.visible ?? ['*']), String(this.$primaryKey())]
const hidden = modelHidden.length > 0 ? modelHidden : config.model.hidden ?? []
const visible = [...(modelVisible.length > 0 ? modelVisible : config.model.visible ?? ['*']), String(this.$primaryKey())]
const optionsVisible = options.visible ?? []
const optionsHidden = options.hidden ?? []
if (((hidden.includes('*') || hidden.includes(key)) && !optionsVisible.includes(key)) || optionsHidden.includes(key))
Expand Down
Loading