@johngw/stream
Version:
Reactive programming tools using the WHATWG Streams API.
145 lines (132 loc) • 3.11 kB
text/typescript
import { timeout } from '@johngw/stream-common/Async'
import { StorageCache } from '@johngw/stream/storages/StorageCache'
import { Clearable } from '@johngw/stream/types/Clearable'
/**
* Describes a function to pull data.
*
* @group Sources
*/
export interface CachePuller<T> {
(): CachePullerResult<T> | Promise<CachePullerResult<T>>
}
/**
* The result of a CachableStream pull.
*
* @group Sources
*/
export type CachePullerResult<T> = IteratorResult<T>
/**
* The source of a `ReadableStream` that queues items
* only when a timed cache becomes invalid.
*
* @group Sources
* @see {@link StorageCache}
* @see {@link CachableStream:class}
* @example
* Caching for 30 minutes.
*
* ```
* const cache = new StorageCache(
* localStorage,
* 'my-cache',
* 30 * 60_000
* )
*
* let i = 0
* const stream = new ReadableStream(
* new CachableSource<number>(
* cache,
* ['numbers'],
* () => ++i
* )
* )
*
* stream.pipeTo(write(console.info))
* // 1
*
* await setTimeout(30 * 60_000))
* // 2
* ```
* @example
* Manually clearing cache.
*
* ```
* const cache = new StorageCache(
* localStorage,
* 'my-cache',
* 30 * 60_000
* )
*
* let i = 0
* const source = new CachableSource<number>(
* cache,
* ['numbers'],
* () => ++i
* )
*
* const stream = new ReadableStream(source)
*
* stream.pipeTo(write(console.info))
* // 1
*
* source.clear()
* // 2
* ```
*/
export class CachableSource<T>
implements UnderlyingDefaultSource<T>, Clearable
{
#abortController = new AbortController()
#sourceHasFinished = false
readonly #cache: StorageCache
readonly #ms: number
readonly #path: string[]
readonly #pullResult: CachePuller<T>
constructor(
cache: StorageCache,
path: string[],
pullResult: CachePuller<T>,
ms: number = cache.ms
) {
this.#cache = cache
this.#ms = ms
this.#path = path
this.#pullResult = pullResult
}
async pull(controller: ReadableStreamDefaultController<T>): Promise<void> {
if (this.#sourceHasFinished) {
const item = this.#cache.get(this.#path)
return item === undefined
? controller.close()
: controller.enqueue(item as T)
} else if (
await this.#cache.updateIfStale(this.#path, this.#pullItem, this.#ms)
)
controller.enqueue(this.#cache.get(this.#path) as T)
else await this.#wait(this.#cache.timeLeft(this.#path))
if (controller.desiredSize) return this.pull(controller)
}
cancel(reason: unknown) {
this.#abortController.abort(reason)
}
readonly #pullItem = async (): Promise<T | void> => {
const result = await this.#pullResult()
if (result.done) this.#sourceHasFinished = true
else return result.value
}
async #wait(ms: number) {
try {
await timeout(ms, undefined, this.#abortController.signal)
} catch (error) {
//
}
}
clear() {
this.#cache.unset(this.#path)
this.#abortController.abort('clear')
this.#abortController = new AbortController()
}
get sourceHasFinished() {
return this.#sourceHasFinished
}
}