Skip to content

(store): Improved type-safety of JavaScript functions #2791

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

Open
wants to merge 1 commit into
base: v2
Choose a base branch
from
Open
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
119 changes: 62 additions & 57 deletions plugins/store/guest-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
createNew?: boolean
}

interface StoreEntries {
[key: string]: unknown
}

/**
* Create a new Store or load the existing store with the path.
*
Expand All @@ -48,10 +52,10 @@
* @param path Path to save the store in `app_data_dir`
* @param options Store configuration options
*/
export async function load(
export async function load<S extends StoreEntries>(
path: string,
options?: StoreOptions
): Promise<Store> {
): Promise<Store<S>> {
return await Store.load(path, options)
}

Expand All @@ -71,19 +75,19 @@
*
* @param path Path of the store.
*/
export async function getStore(path: string): Promise<Store | null> {
return await Store.get(path)
export async function getStore<S extends StoreEntries>(path: string): Promise<Store<S> | null> {
return await Store.get<S>(path)
}

/**
* A lazy loaded key-value store persisted by the backend layer.
*/
export class LazyStore implements IStore {
private _store?: Promise<Store>
export class LazyStore <S extends StoreEntries>implements IStore {
private _store?: Promise<Store<S>>

private get store(): Promise<Store> {
private get store(): Promise<Store<S>> {
if (!this._store) {
this._store = load(this.path, this.options)
this._store = load<S>(this.path, this.options)
}
return this._store
}
Expand All @@ -105,20 +109,20 @@
await this.store
}

async set(key: string, value: unknown): Promise<void> {
async set<K extends keyof S>(key: K, value: S[K]): Promise<void> {
return (await this.store).set(key, value)
}

async get<T>(key: string): Promise<T | undefined> {
return (await this.store).get<T>(key)
async get<K extends keyof S>(key: K): Promise<S[K] | undefined> {
return (await this.store).get<K>(key)
}

async has(key: string): Promise<boolean> {
return (await this.store).has(key)
async has<K extends keyof S>(key: K): Promise<boolean> {
return (await this.store).has<K>(key)
}

async delete(key: string): Promise<boolean> {
return (await this.store).delete(key)
async delete<K extends keyof S>(key: K): Promise<boolean> {
return (await this.store).delete<K>(key)
}

async clear(): Promise<void> {
Expand All @@ -129,16 +133,16 @@
await (await this.store).reset()
}

async keys(): Promise<string[]> {
return (await this.store).keys()
async keys<K extends keyof S>(): Promise<K[]> {
return (await this.store).keys<K>()
}

async values<T>(): Promise<T[]> {
async values<T extends S>(): Promise<T[keyof T][]> {
return (await this.store).values<T>()
}

async entries<T>(): Promise<Array<[key: string, value: T]>> {
return (await this.store).entries<T>()
async entries<K extends keyof S>(): Promise<Array<[key: K, value: S[K]]>> {
return (await this.store).entries<K>()
}

async length(): Promise<number> {
Expand All @@ -153,17 +157,17 @@
await (await this.store).save()
}

async onKeyChange<T>(
key: string,
cb: (value: T | undefined) => void
async onKeyChange<K extends keyof S>(
key: K,
cb: (value: S[K] | undefined) => void
): Promise<UnlistenFn> {
return (await this.store).onKeyChange<T>(key, cb)
return (await this.store).onKeyChange<K>(key, cb)
}

async onChange<T>(
cb: (key: string, value: T | undefined) => void
async onChange<K extends keyof S>(
cb: (key: K, value: S[K] | undefined) => void
): Promise<UnlistenFn> {
return (await this.store).onChange<T>(cb)
return (await this.store).onChange<K>(cb)
}

async close(): Promise<void> {
Expand All @@ -176,7 +180,7 @@
/**
* A key-value store persisted by the backend layer.
*/
export class Store extends Resource implements IStore {
export class Store <S extends StoreEntries>extends Resource implements IStore {
private constructor(rid: number) {
super(rid)
}
Expand All @@ -193,7 +197,7 @@
* @param path Path to save the store in `app_data_dir`
* @param options Store configuration options
*/
static async load(path: string, options?: StoreOptions): Promise<Store> {
static async load<S extends StoreEntries>(path: string, options?: StoreOptions): Promise<Store<S>> {
const rid = await invoke<number>('plugin:store|load', {
path,
...options
Expand All @@ -220,36 +224,36 @@
*
* @param path Path of the store.
*/
static async get(path: string): Promise<Store | null> {
static async get<S extends StoreEntries>(path: string): Promise<Store<S> | null> {
return await invoke<number | null>('plugin:store|get_store', { path }).then(
(rid) => (rid ? new Store(rid) : null)
)
}

async set(key: string, value: unknown): Promise<void> {
async set<K extends keyof S>(key: K, value: S[K]): Promise<void> {
await invoke('plugin:store|set', {
rid: this.rid,
key,
value
})
}

async get<T>(key: string): Promise<T | undefined> {
const [value, exists] = await invoke<[T, boolean]>('plugin:store|get', {
async get<K extends keyof S>(key: K): Promise<S[K] | undefined> {
const [value, exists] = await invoke<[S[K], boolean]>('plugin:store|get', {
rid: this.rid,
key
})
return exists ? value : undefined
}

async has(key: string): Promise<boolean> {
async has<K extends keyof S>(key: K): Promise<boolean> {
return await invoke('plugin:store|has', {
rid: this.rid,
key
})
}

async delete(key: string): Promise<boolean> {
async delete<K extends keyof S>(key: K): Promise<boolean> {
return await invoke('plugin:store|delete', {
rid: this.rid,
key
Expand All @@ -264,15 +268,15 @@
await invoke('plugin:store|reset', { rid: this.rid })
}

async keys(): Promise<string[]> {
async keys<K extends keyof S>(): Promise<K[]> {
return await invoke('plugin:store|keys', { rid: this.rid })
}

async values<T>(): Promise<T[]> {
async values<T extends S>(): Promise<T[keyof T][]> {
return await invoke('plugin:store|values', { rid: this.rid })
}

async entries<T>(): Promise<Array<[key: string, value: T]>> {
async entries<K extends keyof S>(): Promise<Array<[key: K, value: S[K]]>> {
return await invoke('plugin:store|entries', { rid: this.rid })
}

Expand All @@ -288,23 +292,24 @@
await invoke('plugin:store|save', { rid: this.rid })
}

async onKeyChange<T>(
key: string,
cb: (value: T | undefined) => void
async onKeyChange<K extends keyof S>(
key: K,
cb: (value: S[K] | undefined) => void
): Promise<UnlistenFn> {
return await listen<ChangePayload<T>>('store://change', (event) => {
return await listen<ChangePayload<S[K]>>('store://change', (event) => {
if (event.payload.resourceId === this.rid && event.payload.key === key) {
cb(event.payload.exists ? event.payload.value : undefined)
}
})
}

async onChange<T>(
cb: (key: string, value: T | undefined) => void
async onChange<K extends keyof S>(
cb: (key: K, value: S[K] | undefined) => void
): Promise<UnlistenFn> {
return await listen<ChangePayload<T>>('store://change', (event) => {
return await listen<ChangePayload<S[K]>>('store://change', (event) => {
if (event.payload.resourceId === this.rid) {
cb(
// @ts-ignore Typescript complains that `key` can be of type `string | number | symbol`. Doesn't affect end-user.

Check failure on line 312 in plugins/store/guest-js/index.ts

View workflow job for this annotation

GitHub Actions / eslint

Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free
event.payload.key,
event.payload.exists ? event.payload.value : undefined
)
Expand All @@ -321,31 +326,31 @@
* @param value
* @returns
*/
set(key: string, value: unknown): Promise<void>
set<K extends keyof StoreEntries>(key: K, value: StoreEntries[K]): Promise<void>

/**
* Returns the value for the given `key` or `undefined` if the key does not exist.
*
* @param key
* @returns
*/
get<T>(key: string): Promise<T | undefined>
get<K extends keyof StoreEntries>(key: K): Promise<StoreEntries[K] | undefined>

/**
* Returns `true` if the given `key` exists in the store.
*
* @param key
* @returns
*/
has(key: string): Promise<boolean>
has<K extends keyof StoreEntries>(key: K): Promise<boolean>

/**
* Removes a key-value pair from the store.
*
* @param key
* @returns
*/
delete(key: string): Promise<boolean>
delete<K extends keyof StoreEntries>(key: K): Promise<boolean>

/**
* Clears the store, removing all key-value pairs.
Expand All @@ -368,21 +373,21 @@
*
* @returns
*/
keys(): Promise<string[]>
keys<K extends keyof StoreEntries>(): Promise<K[]>

/**
* Returns a list of all values in the store.
*
* @returns
*/
values<T>(): Promise<T[]>
values(): Promise<StoreEntries[keyof StoreEntries][]>

/**
* Returns a list of all entries in the store.
*
* @returns
*/
entries<T>(): Promise<Array<[key: string, value: T]>>
entries<K extends keyof StoreEntries>(): Promise<Array<[K, StoreEntries[K]]>>

/**
* Returns the number of key-value pairs in the store.
Expand Down Expand Up @@ -415,9 +420,9 @@
*
* @since 2.0.0
*/
onKeyChange<T>(
key: string,
cb: (value: T | undefined) => void
onKeyChange<K extends keyof StoreEntries>(
key: K,
cb: (value: StoreEntries[K] | undefined) => void
): Promise<UnlistenFn>

/**
Expand All @@ -427,8 +432,8 @@
*
* @since 2.0.0
*/
onChange<T>(
cb: (key: string, value: T | undefined) => void
onChange(
cb: (key: keyof StoreEntries, value: StoreEntries[keyof StoreEntries] | undefined) => void

Check failure on line 436 in plugins/store/guest-js/index.ts

View workflow job for this annotation

GitHub Actions / eslint

'unknown' overrides all other types in this union type
): Promise<UnlistenFn>

/**
Expand Down
Loading