Skip to content
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

feat: indexedDB support #271

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
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
207 changes: 124 additions & 83 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,139 +1,180 @@
import { writable as internal, type Writable } from 'svelte/store'

import { get, writable as internal, type Writable } from "svelte/store";
if (process.env.NODE_ENV === "test" || process.env.NODE_ENV === "development") {
require("fake-indexeddb/auto");
}
import localforage from "localforage";
declare type StoreDict<T> = { [key: string]: Persisted<T> };
declare type Updater<T> = (value: T) => T;
declare type StoreDict<T> = { [key: string]: Persisted<T> }

interface Persisted<T> extends Writable<T> {
reset: () => void
set: (this: void, value: T) => Promise<void>;
reset: () => Promise<void>;
update: (callback: Updater<T>) => Promise<void>;
}

/* eslint-disable @typescript-eslint/no-explicit-any */
interface Stores {
local: StoreDict<any>,
session: StoreDict<any>,
local: StoreDict<any>;
session: StoreDict<any>;
indexedDB: StoreDict<any>;
}

const stores: Stores = {
local: {},
session: {}
}
session: {},
indexedDB: {},
};

export interface Serializer<T> {
parse(text: string): T
stringify(object: T): string
parse(text: string): T;
stringify(object: T): string;
}

export type StorageType = 'local' | 'session'
export type StorageType = "local" | "session" | "indexedDB";

export interface Options<StoreType, SerializerType> {
serializer?: Serializer<SerializerType>
storage?: StorageType,
syncTabs?: boolean,
onError?: (e: unknown) => void
onWriteError?: (e: unknown) => void
onParseError?: (newValue: string | null, e: unknown) => void
beforeRead?: (val: SerializerType) => StoreType
beforeWrite?: (val: StoreType) => SerializerType
serializer?: Serializer<SerializerType>;
storage?: StorageType;
syncTabs?: boolean;
onError?: (e: unknown) => void;
onWriteError?: (e: unknown) => void;
onParseError?: (newValue: string | null, e: unknown) => void;
beforeRead?: (val: SerializerType) => StoreType;
beforeWrite?: (val: StoreType) => SerializerType;
}

function getStorage(type: StorageType) {
return type === 'local' ? localStorage : sessionStorage
async function getStorage(type: StorageType) {
let storage: LocalForage | Storage | null;
try {
storage = type === "session" ? window.sessionStorage : localforage;
if (type === "local") await storage.setDriver(localforage.LOCALSTORAGE);
if (type === "indexedDB") await storage.setDriver(localforage.INDEXEDDB);
} catch (error) {
storage = null;
}
return storage;
}

/** @deprecated `writable()` has been renamed to `persisted()` */
export function writable<StoreType, SerializerType = StoreType>(key: string, initialValue: StoreType, options?: Options<StoreType, SerializerType>): Persisted<StoreType> {
console.warn("writable() has been deprecated. Please use persisted() instead.\n\nchange:\n\nimport { writable } from 'svelte-persisted-store'\n\nto:\n\nimport { persisted } from 'svelte-persisted-store'")
return persisted<StoreType, SerializerType>(key, initialValue, options)
export async function writable<StoreType, SerializerType = StoreType>(
key: string,
initialValue: StoreType,
options?: Options<StoreType, SerializerType>
): Promise<Persisted<StoreType>> {
console.warn(
"writable() has been deprecated. Please use persisted() instead.\n\nchange:\n\nimport { writable } from 'svelte-persisted-store'\n\nto:\n\nimport { persisted } from 'svelte-persisted-store'"
);
return await persisted<StoreType, SerializerType>(key, initialValue, options);
}
export function persisted<StoreType, SerializerType = StoreType>(key: string, initialValue: StoreType, options?: Options<StoreType, SerializerType>): Persisted<StoreType> {
if (options?.onError) console.warn("onError has been deprecated. Please use onWriteError instead")

const serializer = options?.serializer ?? JSON
const storageType = options?.storage ?? 'local'
const syncTabs = options?.syncTabs ?? true
const onWriteError = options?.onWriteError ?? options?.onError ?? ((e) => console.error(`Error when writing value from persisted store "${key}" to ${storageType}`, e))
const onParseError = options?.onParseError ?? ((newVal, e) => console.error(`Error when parsing ${newVal ? '"' + newVal + '"' : "value"} from persisted store "${key}"`, e))

const beforeRead = options?.beforeRead ?? ((val) => val as unknown as StoreType)
const beforeWrite = options?.beforeWrite ?? ((val) => val as unknown as SerializerType)

const browser = typeof (window) !== 'undefined' && typeof (document) !== 'undefined'
const storage = browser ? getStorage(storageType) : null

function updateStorage(key: string, value: StoreType) {
const newVal = beforeWrite(value)

export async function persisted<StoreType, SerializerType = StoreType>(
key: string,
initialValue: StoreType,
options?: Options<StoreType, SerializerType>
): Promise<Persisted<StoreType>> {
if (options?.onError)
console.warn(
"onError has been deprecated. Please use onWriteError instead"
);

const serializer = options?.serializer ?? JSON;
const storageType = options?.storage ?? "local";
const syncTabs = options?.syncTabs ?? true;
const onWriteError =
options?.onWriteError ??
options?.onError ??
((e) =>
console.error(
`Error when writing value from persisted store "${key}" to ${storageType}`,
e
));
const onParseError =
options?.onParseError ??
((newVal, e) =>
console.error(
`Error when parsing ${
newVal ? '"' + newVal + '"' : "value"
} from persisted store "${key}"`,
e
));

const beforeRead =
options?.beforeRead ?? ((val) => val as unknown as StoreType);
const beforeWrite =
options?.beforeWrite ?? ((val) => val as unknown as SerializerType);

const browser =
typeof window !== "undefined" && typeof document !== "undefined";
const storage: Storage | LocalForage | null = browser
? await getStorage(storageType)
: null;
async function updateStorage(key: string, value: StoreType) {
const newVal = beforeWrite(value);
try {
storage?.setItem(key, serializer.stringify(newVal))
await storage?.setItem(key, serializer.stringify(newVal));
} catch (e) {
onWriteError(e)
onWriteError(e);
}
}

function maybeLoadInitial(): StoreType {
async function maybeLoadInitial(): Promise<StoreType> {
function serialize(json: any) {
try {
return <SerializerType>serializer.parse(json)
return <SerializerType>serializer.parse(json);
} catch (e) {
onParseError(json, e)
onParseError(json, e);
}
}
const json = storage?.getItem(key)
if (json == null) return initialValue
const json = await storage?.getItem(key);
if (json == null) return initialValue;

const serialized = serialize(json)
if (serialized == null) return initialValue
const serialized = serialize(json);
if (serialized == null) return initialValue;

const newVal = beforeRead(serialized)
return newVal
const newVal = beforeRead(serialized);
return newVal;
}

if (!stores[storageType][key]) {
const initial = maybeLoadInitial()
const initial: StoreType = await maybeLoadInitial();
const store = internal(initial, (set) => {
if (browser && storageType == 'local' && syncTabs) {
const handleStorage = (event: StorageEvent) => {
if (browser && storageType == "local" && syncTabs) {
const handleStorage = async (event: StorageEvent) => {
if (event.key === key && event.newValue) {
let newVal: any
let newVal: any;
try {
newVal = serializer.parse(event.newValue)
newVal = serializer.parse(event.newValue);
} catch (e) {
onParseError(event.newValue, e)
return
onParseError(event.newValue, e);
return;
}
const processedVal = beforeRead(newVal)
const processedVal = beforeRead(newVal);

set(processedVal)
set(processedVal);
}
}
};

window.addEventListener("storage", handleStorage)
window.addEventListener("storage", handleStorage);

return () => window.removeEventListener("storage", handleStorage)
return () => window.removeEventListener("storage", handleStorage);
}
})
});

const { subscribe, set } = store
const { subscribe, set } = store;

stores[storageType][key] = {
set(value: StoreType) {
set(value)
updateStorage(key, value)
async set(value: StoreType) {
set(value);
await updateStorage(key, value);
},
update(callback: Updater<StoreType>) {
return store.update((last) => {
const value = callback(last)

updateStorage(key, value)

return value
})
async update(callback: Updater<StoreType>) {
// this is more concise than store.update
await this.set(callback(get(store)));
},
reset() {
this.set(initialValue)
async reset() {
await this.set(initialValue);
},
subscribe
}
subscribe,
};
}
return stores[storageType][key]
return stores[storageType][key];
}
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,10 @@
],
"files": [
"dist"
]
],
"dependencies": {
"@types/node": "^20.13.0",
"fake-indexeddb": "^6.0.0",
"localforage": "^1.10.0"
}
}
Loading
Loading