Describe the feature in detail (code, mocks, or screenshots encouraged)
The issue is when you want have access to a non-proxy value of your persistedState (to serialise on your own, to clone, etc.).
It's impossible for the moment.
export const saveSession = () => {
const ongoing = sessionState.current;
// const ongoing2 = structuredClone(ongoing); <--- Fails!
const endedSession: Session = {
...ongoing,
questions: sessionState.current.questions.map((q) => q.qid),
duration_s: sessionDuration.current || 0
};
// try to save to IDB, fail! because `sessionState.current` is a proxy
};
Solution, something like
currentRaw(): T {
this.#subscribe?.();
let root: T | undefined;
if (this.#connected) {
// when we're connected to storage, we use storage as the source of truth
const storageItem = this.#storage?.getItem(this.#key);
root = storageItem ? this.#deserialize(storageItem) : this.#current;
} else {
// when we're not connected to storage, we use the current value in memory
root = this.#current;
}
return root;
}
Full repro, a bit lengthy due to no external lib for IDB.
Details
import { writable } from 'svelte/store';
import { PersistedState } from 'runed';
// TODO, switch to `idb` package?
export type IDbValue = Blob | string | ArrayBuffer | Uint8Array | string[] | number | object;
// bump version when touching this
const storeKeys = ['session'] as const;
type StoreKeys = (typeof storeKeys)[number];
export class Db {
private inner: IDBDatabase;
stores: Record<StoreKeys, Store>;
constructor(inner: IDBDatabase, stores: Record<StoreKeys, Store>) {
this.inner = inner;
this.stores = stores;
}
static async open(): Promise<Db> {
const openReq = indexedDB.open(prefix, 2);
return new Promise<Db>((resolve) => {
openReq.onupgradeneeded = () => {
let db = openReq.result;
// create all stores
for (const storeKey of storeKeys) {
if (!db.objectStoreNames.contains(storeKey)) {
console.log(`Creating object store: ${storeKey}`);
db.createObjectStore(storeKey, { keyPath: 'id' });
}
}
};
openReq.onerror = (event: any) => console.error(`DB open error: ${JSON.stringify(event)}`);
openReq.onblocked = function () {
// this event shouldn't trigger if we handle onversionchange correctly
const blocked = 'Database is blocked, close all other tabs of this page';
console.error(blocked);
alert(blocked);
};
openReq.onsuccess = function () {
let db = openReq.result;
db.onversionchange = function () {
db.close();
const outdated = 'Database is outdated, please reload the page.';
console.error(outdated);
alert(outdated);
};
const stores = {
session: new Store(db, 'session')
};
resolve(new Db(db, stores));
};
});
}
}
// IDB store, based on lila/ui/lib/src/objectStorage.ts, AGPL
export class Store {
private db: IDBDatabase;
private storeKey: StoreKeys;
constructor(db: IDBDatabase, storeKey: StoreKeys) {
this.db = db;
this.storeKey = storeKey;
}
objectStore = (mode: IDBTransactionMode) => {
return this.db.transaction(this.storeKey, mode).objectStore(this.storeKey);
};
// ArrayBuffer, Blob, File, and typed arrays like Uint8Array
async put(key: IDbValue, value: IDbValue): Promise<void> {
console.log(`putting in store ${this.storeKey}: key {${key}} value {${JSON.stringify(value)}}`);
return promise(() => this.objectStore('readwrite').put({ id: key, data: value }));
}
async getMany<T extends IDbValue>(keys?: IDBKeyRange): Promise<(T | null)[]> {
return promise(() => this.objectStore('readonly').getAll(keys));
}
}
let theGlobalDb: Db;
let init: (db: Db) => void;
export const getDb = new Promise<Db>((resolve) => {
init = (db: Db) => {
resolve(db);
};
});
export const initDb = async (): Promise<void> => {
theGlobalDb = await Db.open();
init(theGlobalDb);
};
interface SessionBase<T> {
id: SessionId;
name: string;
kind: { is: 'exam'; year: number; initial_time: number } | { is: 'study' };
created_at: Timestamp;
questions: T[];
}
export type Session = SessionBase<Qid> & {
duration_s: number;
};
export interface QuestionWip {
qid: Qid;
duration_s: number;
selected_choice?: number;
correct_choice?: number; // if correct is set, then the question is no longer editable
}
// duration_s is notset until the end of the session, so that we can display a timer during the session
// without having to (de)serialise the session object on every tick.
export type OngoingSession = SessionBase<QuestionWip>;
const sessionKey = 'ongoingSession' as LocalStorageKey;
const durationKey = 'sessionDuration' as LocalStorageKey;
export const sessionState = new PersistedState<OngoingSession | undefined>(sessionKey, undefined);
// if above is defined, assume below belongs to it
export const sessionDuration = new PersistedState<number>(durationKey, 0);
export const pastSessions = writable<Session[]>([],
(set) => {
getDb.then((db: Db) => {
db.stores.attempt.getMany().then((idbArray) => {
// sort by most recent first
const sessions = (idbArray as {id: SessionId, data: Session}[]).map((obj) => obj.data);
(sessions).sort((a, b) => b.created_at - a.created_at);
set(sessions);
});
});
});
// always keep the object in sync with IndexedDB
pastSessions.subscribe((value: Session[]) => {
getDb.then((db: Db) => {
for (const session of value) {
db.stores.attempt.put(session.id, session);
}
});
});
export const saveSession = () => {
const ongoing = sessionState.current;
const ongoing2 = structuredClone(ongoing);
const endedSession: Session = {
...ongoing,
questions: sessionState.current.questions.map((q) => q.qid),
duration_s: sessionDuration.current || 0
};
pastSessions.update((current) => {
current.unshift(endedSession);
return current;
});
};
initDb();
saveSession()
What type of pull request would this be?
None
Provide relevant links or additional information.
No response
Describe the feature in detail (code, mocks, or screenshots encouraged)
The issue is when you want have access to a non-proxy value of your persistedState (to serialise on your own, to clone, etc.).
It's impossible for the moment.
Solution, something like
Full repro, a bit lengthy due to no external lib for IDB.
Details
What type of pull request would this be?
None
Provide relevant links or additional information.
No response