Skip to content

Commit

Permalink
feat(cache): add Subscribable (#1197)
Browse files Browse the repository at this point in the history
# Overview
- add `Subscribable`
<!--
    A clear and concise description of what this pr is about.
 -->

## PR Checklist

- [x] I did below actions if need

1. I read the [Contributing
Guide](https://github.com/toss/suspensive/blob/main/CONTRIBUTING.md)
2. I added documents and tests.

---------

Co-authored-by: Jonghyeon Ko <[email protected]>
  • Loading branch information
SEOKKAMONI and manudeli authored Aug 17, 2024
1 parent 0720992 commit 0bb493c
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changeset/twelve-apricots-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@suspensive/cache': minor
---

feat(cache): add `Subscribable`
24 changes: 12 additions & 12 deletions packages/cache/src/CacheStore.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ describe('CacheStore', () => {

describe('reset', () => {
it('should delete all cached and notify to subscribers', async () => {
const mockSync1 = vitest.fn()
const mockSync2 = vitest.fn()
cacheStore.subscribe(successCache(1), mockSync1)
cacheStore.subscribe(successCache(2), mockSync2)
const mockListener1 = vitest.fn()
const mockListener2 = vitest.fn()
cacheStore.subscribe(mockListener1)
cacheStore.subscribe(mockListener2)
try {
cacheStore.suspend(successCache(1))
} catch (promiseToSuspense) {
Expand All @@ -117,28 +117,28 @@ describe('CacheStore', () => {
}
expect(cacheStore.getData(successCache(1))).toBe(TEXT)
expect(cacheStore.getData(successCache(2))).toBe(TEXT)
expect(mockSync1).not.toHaveBeenCalled()
expect(mockSync2).not.toHaveBeenCalled()
expect(mockListener1).not.toHaveBeenCalled()
expect(mockListener2).not.toHaveBeenCalled()
cacheStore.reset()
expect(cacheStore.getData(successCache(1))).toBeUndefined()
expect(cacheStore.getData(successCache(2))).toBeUndefined()
expect(mockSync1).toHaveBeenCalledOnce()
expect(mockSync2).toHaveBeenCalledOnce()
expect(mockListener1).toHaveBeenCalledOnce()
expect(mockListener2).toHaveBeenCalledOnce()
})

it('should delete specific cached and notify to subscriber', async () => {
const mockSync = vitest.fn()
cacheStore.subscribe(successCache(1), mockSync)
const mockListener = vitest.fn()
cacheStore.subscribe(mockListener)
try {
cacheStore.suspend(successCache(1))
} catch (promiseToSuspense) {
await promiseToSuspense
}
expect(cacheStore.getData(successCache(1))).toBe(TEXT)
expect(mockSync).not.toHaveBeenCalled()
expect(mockListener).not.toHaveBeenCalled()
cacheStore.reset(successCache(1))
expect(cacheStore.getData(successCache(1))).toBeUndefined()
expect(mockSync).toHaveBeenCalledOnce()
expect(mockListener).toHaveBeenCalledOnce()
})
})

Expand Down
34 changes: 4 additions & 30 deletions packages/cache/src/CacheStore.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Subscribable } from './models/Subscribable'
import type { CacheKey, CacheOptions } from './types'
import type { ExtractPartial } from './utility-types/ExtractPartial'
import { hashCacheKey } from './utils'
Expand All @@ -9,8 +10,6 @@ const enum CacheStatus {
Rejected = 'rejected',
}

type Sync = (...args: unknown[]) => unknown

export type AllStatusCached<TData, TCacheKey extends CacheKey = CacheKey> =
| {
status: CacheStatus.Idle
Expand Down Expand Up @@ -83,14 +82,13 @@ export type Cached<TData, TCacheKey extends CacheKey = CacheKey> =
/**
* @experimental This is experimental feature.
*/
export class CacheStore {
export class CacheStore extends Subscribable<() => void> {
private cacheStore = new Map<ReturnType<typeof hashCacheKey>, Cached<unknown>>()
private syncsMap = new Map<ReturnType<typeof hashCacheKey>, Array<Sync>>()

public reset = (options?: Pick<CacheOptions<unknown, CacheKey>, 'cacheKey'>) => {
if (typeof options?.cacheKey === 'undefined' || options.cacheKey.length === 0) {
this.cacheStore.clear()
this.syncSubscribers()
this.notify()
return
}

Expand All @@ -100,7 +98,7 @@ export class CacheStore {
this.cacheStore.delete(hashedCacheKey)
}

this.syncSubscribers(options.cacheKey)
this.notify()
}

public remove = (options: Pick<CacheOptions<unknown, CacheKey>, 'cacheKey'>) => {
Expand Down Expand Up @@ -193,28 +191,4 @@ export class CacheStore {
this.cacheStore.get(hashCacheKey(options.cacheKey))?.state.data
public getError = (options: Pick<CacheOptions<unknown, CacheKey>, 'cacheKey'>) =>
this.cacheStore.get(hashCacheKey(options.cacheKey))?.state.error

public subscribe(options: Pick<CacheOptions<unknown, CacheKey>, 'cacheKey'>, syncSubscriber: Sync) {
const hashedCacheKey = hashCacheKey(options.cacheKey)
const syncs = this.syncsMap.get(hashedCacheKey)
this.syncsMap.set(hashedCacheKey, [...(syncs ?? []), syncSubscriber])

const unsubscribe = () => {
if (syncs) {
this.syncsMap.set(
hashedCacheKey,
syncs.filter((sync) => sync !== syncSubscriber)
)
}
}
return unsubscribe
}

private syncSubscribers = (cacheKey?: CacheKey) => {
const hashedCacheKey = cacheKey ? hashCacheKey(cacheKey) : undefined

return hashedCacheKey
? this.syncsMap.get(hashedCacheKey)?.forEach((sync) => sync())
: this.syncsMap.forEach((syncs) => syncs.forEach((sync) => sync()))
}
}
19 changes: 19 additions & 0 deletions packages/cache/src/models/Subscribable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
type Listener = (...args: any[]) => unknown

type Unsubscribe = () => void
type Subscribe<TListener extends Listener> = (listener: TListener) => Unsubscribe

export class Subscribable<TListener extends Listener> {
protected listeners = new Set<TListener>()

public subscribe: Subscribe<TListener> = (listener) => {
this.listeners.add(listener)

const unsubscribe = (): void => {
this.listeners.delete(listener)
}
return unsubscribe
}

protected notify = (): void => this.listeners.forEach((listener) => listener())
}
2 changes: 1 addition & 1 deletion packages/cache/src/useCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function useCache<TData, TCacheKey extends CacheKey>(
): ResolvedCached<TData, TCacheKey>['state'] {
const cacheStore = useCacheStore()
return useSyncExternalStore<ResolvedCached<TData, TCacheKey>>(
(sync) => cacheStore.subscribe(options, sync),
cacheStore.subscribe,
() => cacheStore.suspend<TData, TCacheKey>(options),
() => cacheStore.suspend<TData, TCacheKey>(options)
).state
Expand Down

0 comments on commit 0bb493c

Please sign in to comment.