import { assertNever } from './assert-never'

type CacheEntry<V> =
	| { type: 'pending'; promise: Promise<V> }
	| { type: 'done'; result: V }

export class AsyncQueue<K, V> {
	private readonly cache = new Map<K, CacheEntry<V>>()

	constructor(private readonly create: (key: K) => Promise<V>) {}

	public set(key: K, value: V) {
		this.cache.set(key, { type: 'done', result: value })
	}

	public preload(key: K) {
		this.get(key).catch(() => {
			return
		})
	}

	public get(key: K) {
		const cache = this.cache.get(key)

		if (cache) {
			if (cache.type === 'pending') {
				return cache.promise
			} else if (cache.type === 'done') {
				return Promise.resolve(cache.result)
			} else {
				return assertNever(cache)
			}
		}

		const promise = this.create(key)
			.then((result) => {
				this.cache.set(key, { type: 'done', result })

				return result
			})
			.catch((error) => {
				this.cache.delete(key)

				throw error
			})

		this.cache.set(key, { type: 'pending', promise })

		return promise
	}
}

export default class AsyncCache<K, V> {
	private readonly cache = new Map<K, CacheEntry<V>>()

	constructor(private readonly create: (key: K) => Promise<V>) {}

	public set(key: K, value: V) {
		this.cache.set(key, { type: 'done', result: value })
	}

	public preload(key: K) {
		this.get(key).catch(() => {
			return
		})
	}

	public get(key: K) {
		const cache = this.cache.get(key)

		if (cache) {
			if (cache.type === 'pending') {
				return cache.promise
			} else if (cache.type === 'done') {
				return Promise.resolve(cache.result)
			} else {
				return assertNever(cache)
			}
		}

		const promise = this.create(key)
			.then((result) => {
				this.cache.set(key, { type: 'done', result })

				return result
			})
			.catch((error) => {
				this.cache.delete(key)

				throw error
			})

		this.cache.set(key, { type: 'pending', promise })

		return promise
	}
}
