Last active
September 29, 2024 20:49
-
-
Save amal/1605ba93651f9d2e0a2efd2ea44192df to your computer and use it in GitHub Desktop.
Reatom LazyStoreItem for WebExt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Returns `false` if the value is `null`, `undefined`, `NaN`, or `Infinity`. | |
*/ | |
export const isV = <T>(v: T | undefined | null | typeof NaN | typeof Infinity): v is T => | |
v !== null && v !== undefined && !Number.isNaN(v) && v !== Infinity && v !== -Infinity; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type {Ctx, LensAtom} from "@reatom/framework"; | |
/** | |
* Read-only Atom with a `set` method to update the value. | |
*/ | |
export interface RItemAtom<TValue = unknown> extends LensAtom<TValue> { | |
readonly set: (ctx: Ctx, value: TValue) => any | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {StorageItemKey, WxtStorageItem } from 'wxt/storage'; | |
export type LazyStoreItem<TValue, TMetadata extends Record<string, unknown> = {}> = | |
(() => WxtStorageItem<TValue, TMetadata>) | |
& { key?: StorageItemKey, fallback?: TValue }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type {LazyStoreItem} from "./storage.ts"; | |
import type {RItemAtom} from "./RItemAtom.ts"; | |
import { | |
atom, | |
Ctx, | |
isInit, | |
onConnect, | |
onDisconnect, | |
readonly, | |
reatomAsync, | |
withAssign, | |
withDataAtom | |
} from "@reatom/framework"; | |
const _DEBUG = false; | |
const resolve = Promise.resolve; | |
/** | |
* A real update of the storage should be throttled. | |
* | |
* MAX_WRITE_OPERATIONS_PER_MINUTE is 120 write ops per minute. | |
* An op every 8-9 ms is allowed. | |
* But we want to allow concurrent store items, so let's take 50 ms. | |
*/ | |
const OP_THROTTLE_MS = 50; | |
// TODO: Allow readonly storeItemReatom? | |
/** | |
* Returns an atom that wraps a value in the storage. | |
* | |
* @param lazyItem A function that returns a storage item. | |
* @param fallback The fallback/default value. | |
* @param filter An optional function that filters or maps the value from the storage, sync or async. | |
* @param encDec An optional object with the encoder and decoder functions. | |
*/ | |
export function storeItemReatom< | |
// Force TValue to be a non-nullable non-undefined type. | |
TValue extends unknown, | |
TFilteredValue extends TValue = TValue, | |
StoreValue = TValue, | |
>( | |
lazyItem: LazyStoreItem<StoreValue>, | |
fallback: TFilteredValue = lazyItem.fallback! as TFilteredValue, | |
filter?: (value?: TValue | TFilteredValue) => Promise<TFilteredValue | undefined> | TFilteredValue | undefined, | |
encDec?: { | |
enc: (value: TFilteredValue) => Promise<StoreValue>, | |
dec: (value: StoreValue) => Promise<TValue>, | |
}, | |
): RItemAtom<TFilteredValue> { | |
let key: string | undefined; | |
if (DEV) { | |
key = lazyItem.key; | |
key = key ? `${key}::` : ''; | |
} | |
const filtered: (v?: TValue | TFilteredValue) => Promise<TFilteredValue | undefined> = | |
filter ? (v) => resolve(filter(v)) : resolve; | |
const atomAwareFilter = async (value?: TValue | TFilteredValue, ctx?: Ctx): Promise<TFilteredValue> => { | |
let v: TFilteredValue | undefined = await filtered(value); | |
if (!isV(v) && v !== null) { | |
if (ctx) { | |
v = ctx.get(dataAtom); | |
if (!isV(v) && v !== null) { | |
v = fallback | |
} | |
} else { | |
v = fallback | |
} | |
} | |
return v; | |
}; | |
/** | |
* Basic async + dataAtom. | |
* | |
* @TODO: Can it be simplified with reatomResource? | |
*/ | |
const loader = reatomAsync( | |
async (ctx) => { | |
const storeValue: StoreValue = await lazyItem().getValue(); | |
const rawValue = encDec | |
? await encDec.dec(storeValue) | |
: storeValue as unknown as TValue; | |
return atomAwareFilter(rawValue, ctx); | |
}, | |
rn(`${key}Loader`), | |
).pipe(withDataAtom(fallback)); | |
const dataAtom = loader.dataAtom; | |
if (DEV && _DEBUG) { | |
loader.onChange((_ctx, v) => { | |
console.debug(`${key}loader onChange`, v); | |
}); | |
dataAtom.onChange((_ctx, v) => { | |
console.debug(`${key}dataAtom onChange`, v); | |
}); | |
} | |
// Watch for sync storage changes | |
// and update the data atom accordingly. | |
onConnect(dataAtom, (ctx) => { | |
if (DEV && _DEBUG) console.debug(`on ${key}dataAtom connect`); | |
if (isInit(ctx)) { | |
if (DEV && _DEBUG) console.debug(`on ${key}dataAtom connect, isInit, calling loader`); | |
loader(ctx); | |
} | |
return lazyItem().watch(async (storeValue, oldValue) => { | |
const rawValue = encDec | |
? await encDec.dec(storeValue) | |
: storeValue as unknown as TValue; | |
const v = await filtered(rawValue); | |
if (DEV && _DEBUG) { | |
console.debug(`on ${key}dataAtom change from store (was: ${oldValue}):`, storeValue, v); | |
} | |
if (isV(v) || v === null) { | |
dataAtom(ctx, v); | |
} else if (DEV && _DEBUG) { | |
console.warn(`Invalid ${key} value from store. was: ${oldValue}, new:`, storeValue); | |
} | |
}) | |
}); | |
if (DEV && _DEBUG) onDisconnect(dataAtom, () => console.debug(`on ${key}dataAtom disconnect`)); | |
let timer: any; | |
const updater = reatomAsync( | |
async (ctx, value: TFilteredValue | TValue) => { | |
const v: TFilteredValue | undefined = await filtered(value); | |
if (DEV && _DEBUG) console.debug(`${key}updater`, value, v, isV(v) || v === null); | |
if (isV(v) || v === null) { | |
// An optimistic update of the data atom. | |
dataAtom(ctx, v); | |
if (DEV && _DEBUG) console.debug(`${key}updater optimistic done`, value, v); | |
clearTimeout(timer); | |
timer = setTimeout(async () => { | |
const item = lazyItem(); | |
await (v !== null | |
? item.setValue(encDec ? await encDec.enc(v) : v as unknown as StoreValue) | |
: item.removeValue({removeMeta: true})); | |
if (DEV && _DEBUG) console.debug(`${key}updater done`, value, v); | |
}, OP_THROTTLE_MS); | |
} | |
}, | |
rn(`${key}updater`), | |
// 1. onEffect is called BEFORE the effect is executed. | |
// 2. Effect is executed. | |
// 3. onFulfill is called AFTER the effect is fulfilled. | |
// 4. onSettle is called AFTER the effect is settled. | |
); | |
return atom((ctx) => { | |
// FIXME: Somehow the onConnect is not called sometimes. | |
if (isInit(ctx)) { | |
if (DEV && _DEBUG) console.debug(`on ${key}proxy init, calling loader`); | |
loader(ctx); | |
} | |
return ctx.spy(dataAtom); | |
}, rn(`${key}proxy`)).pipe( | |
readonly, | |
withAssign(() => ({ | |
set: updater, | |
})), | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment