Skip to content

Commit

Permalink
feat: IndexedDB support
Browse files Browse the repository at this point in the history
- The persisted function is now deprecated, but remains functional
- Instead of a storage option, there are now three seperate functions;
 localState, sessionState and indexedDBState
- The localState and sessionState functions remain the same, aside from the storage parameter
- indexedDBState is asynchronous and uses async/await (or callback)
- Some refactoring was done, specifically with types

PS: persisted() remains functional with the same API
  • Loading branch information
intercepted16 committed Jun 9, 2024
1 parent 605799a commit 0b3f4d7
Show file tree
Hide file tree
Showing 12 changed files with 6,241 additions and 3,730 deletions.
49 changes: 49 additions & 0 deletions drivers/idb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export default class indexedDB<T> {
private defaultDb;
private defaultVersion;
private defaultObjectStore;
constructor() {
this.defaultDb = "svelte-persisted-store";
this.defaultVersion = 1;
this.defaultObjectStore = "keyvaluepairs";
}
public getItem(key: string): Promise<string | null> {
return new Promise((resolve, reject) => {
const request = window.indexedDB.open(
this.defaultDb,
this.defaultVersion
);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction(this.defaultObjectStore, "readonly");
const store = tx.objectStore(this.defaultObjectStore);
const getRequest = store.get(key);
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => resolve(getRequest.result);
};
});
}
public setItem(key: string, value: T): Promise<void> {
return new Promise((resolve, reject) => {
const request = window.indexedDB.open(
this.defaultDb,
this.defaultVersion
);
request.onerror = () => reject(request.error);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.defaultObjectStore))
db.createObjectStore(this.defaultObjectStore);
};
request.onsuccess = () => {
const db = request.result;
const tx = db.transaction(this.defaultObjectStore, "readwrite");
const store = tx.objectStore(this.defaultObjectStore);
const putRequest = store.put(value, key);
putRequest.onerror = () => reject(putRequest.error);
putRequest.onsuccess = () => resolve();
};
});
}
}
210 changes: 84 additions & 126 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,139 +1,97 @@
import { writable as internal, type Writable } from 'svelte/store'

declare type Updater<T> = (value: T) => T;
declare type StoreDict<T> = { [key: string]: Persisted<T> }

interface Persisted<T> extends Writable<T> {
reset: () => void
}

/* eslint-disable @typescript-eslint/no-explicit-any */
interface Stores {
local: StoreDict<any>,
session: StoreDict<any>,
import { get, writable as internal } from "svelte/store";
import indexedDB from "./drivers/idb";
import { AsyncPersisted, Persisted, Stores } from "./types/store";
import { Options, DeprecatedOptions } from "./types/options";
import createState from "./state";
if (process.env.NODE_ENV === "test" || process.env.NODE_ENV === "development") {
// structured-clone is not supported in jsdom
require("core-js/stable/structured-clone");
require("fake-indexeddb/auto");
}

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

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

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

export type StorageType = 'local' | 'session'

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
/** @deprecated `persisted()` has been deprecated. */
export function persisted<StoreType, SerializerType = StoreType>(
key: string,
initialValue: StoreType,
options?: DeprecatedOptions<StoreType, SerializerType>
): Persisted<StoreType> {
console.warn(
"persisted() has been deprecated. Please use localState() or sessionState()) instead."
);
const storageType = options?.storage || "local";
switch (storageType) {
case "local":
return localState(key, initialValue, options);
case "session":
return sessionState(key, initialValue, options);
default:
throw new Error("Invalid storage type. Please use 'local' or 'session'");
}
}

function getStorage(type: StorageType) {
return type === 'local' ? localStorage : sessionStorage
export function localState<StoreType, SerializerType = StoreType>(
key: string,
initialValue: StoreType,
options?: Options<StoreType, SerializerType>
): Persisted<StoreType> {
return createState(key, initialValue, stores, {
...options,
storage: "local",
});
}

/** @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 function sessionState<StoreType, SerializerType = StoreType>(
key: string,
initialValue: StoreType,
options?: Options<StoreType, SerializerType>
): Persisted<StoreType> {
return createState(key, initialValue, stores, {
...options,
storage: "session",
});
}
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)

try {
storage?.setItem(key, serializer.stringify(newVal))
} catch (e) {
onWriteError(e)
}
}

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

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

const newVal = beforeRead(serialized)
return newVal
}

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

set(processedVal)
}
}

window.addEventListener("storage", handleStorage)

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

const { subscribe, set } = store

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

updateStorage(key, value)

return value
})
},
reset() {
this.set(initialValue)
},
subscribe
}
}
return stores[storageType][key]
export async function indexedDBState<T>(
key: string,
initialValue: T
): Promise<Persisted<T> | AsyncPersisted<T>> {
const store = internal(initialValue);
const { subscribe } = store;
const storage = new indexedDB();

stores["indexedDB"][key] = {
async set(value: T): Promise<void> {
store.set(value);
await storage.setItem(key, value);
},

async update(updater: (value: T) => T): Promise<void> {
const updatedValue = updater(get(store));
await this.set(updatedValue);
},

async reset(): Promise<void> {
this.set(initialValue);
},
subscribe,
};
return stores["indexedDB"][key];
}
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"main": "dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"types": "dist/index.d.ts",
Expand All @@ -30,11 +30,15 @@
"devDependencies": {
"@babel/core": "^7.24.6",
"@babel/preset-env": "^7.24.5",
"@types/core-js": "^2.5.8",
"@types/node": "^20.13.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vitest/coverage-v8": "^1.6.0",
"codecov": "^3.8.3",
"core-js": "^3.37.1",
"eslint": "^8.57.0",
"fake-indexeddb": "^6.0.0",
"jsdom": "^24.0.0",
"publint": "^0.2.8",
"rollup": "^4.18.0",
Expand All @@ -53,5 +57,6 @@
],
"files": [
"dist"
]
],
"packageManager": "[email protected]+sha1.648f6014eb363abb36618f2ba59282a9eeb3e879"
}
Loading

0 comments on commit 0b3f4d7

Please sign in to comment.