import { ObjectCreate, ObjectGetOwnPropertyDescriptor as getDescriptor, Proxy, ReflectDefineProperty as defineReflect, ReflectDeleteProperty as deleteReflect, ReflectGet as reflectGet, ReflectHas as reflectHas, ReflectOwnKeys as reflectOwnKeys, ReflectSet as reflectSet, StringPrototypeIsWellFormed as isWellFormed, TypeError, WeakMap, WeakMapPrototypeGet as getState, WeakMapPrototypeSet as setState, } from "ext:core/primordials"; import { op_webstorage_clear as clearOp, op_webstorage_get as getOp, op_webstorage_key as keyOp, op_webstorage_keys as keysOp, op_webstorage_length as lengthOp, op_webstorage_remove as removeOp, op_webstorage_set as setOp, } from "ext:core/ops"; import { createException } from "ext:web/events.js"; import { required, state, string } from "ext:web/idl.js"; import { registerUncloneable } from "ext:web/serialization.js"; const states = new WeakMap(); registerUncloneable(states); const secret: object = ObjectCreate(null); // Strings cross into Rust as UTF-8; refuse lone surrogates on writes // rather than letting that conversion silently replace them. Ill-formed // names can never have been stored, so reads simply report them absent. function wellFormed(value: unknown): string { const result = string(value); if (!isWellFormed(result)) { throw new TypeError("Storage keys and values must be well-formed strings"); } return result; } function readStored(persistent: boolean, key: string): string | null { if (!isWellFormed(key)) return null; return getOp([persistent, key]); } function writeStored(persistent: boolean, key: string, value: unknown): void { if (!isWellFormed(key)) { throw new TypeError("Storage keys and values must be well-formed strings"); } const stored = setOp([persistent, key, wellFormed(value)]); if (!stored) { throw createException({ message: "Exceeded maximum storage size", name: "QuotaExceededError", }); } } function removeStored(persistent: boolean, key: string): void { if (isWellFormed(key)) removeOp([persistent, key]); } class Storage { [key: string | symbol]: unknown; constructor(); constructor(token: object, persistent: boolean); constructor() { if (arguments[0] !== secret) throw new TypeError("Illegal constructor"); // Only createStorage holds the token and supplies the persistence flag. setState(states, this, { persistent: arguments[1] as boolean }); } get length(): number { return lengthOp(state(states, this).persistent); } key(index: unknown): string | null { const current = state(states, this); required(arguments.length, 1); // Keep ToUint32 coercion: Number() would accept BigInt. return keyOp([current.persistent, (index as number) >>> 0]); } getItem(key: unknown): string | null { const current = state(states, this); required(arguments.length, 1); return readStored(current.persistent, string(key)); } setItem(key: unknown, value: unknown): void { const current = state(states, this); required(arguments.length, 2); writeStored(current.persistent, string(key), value); } removeItem(key: unknown): void { const current = state(states, this); required(arguments.length, 1); removeStored(current.persistent, string(key)); } clear(): void { clearOp(state(states, this).persistent); } } function createStorage(persistent: boolean): Storage { const storage = new Storage(secret, persistent); const proxy: Storage = new Proxy(storage, { deleteProperty(target, key) { if (typeof key === "symbol" || reflectHas(target, key)) { return deleteReflect(target, key); } removeStored(persistent, key); return true; }, defineProperty(target, key, descriptor) { if (typeof key === "symbol") return defineReflect(target, key, descriptor); // Accessor, generic, and non-configurable descriptors have no // named-property equivalent; WebIDL rejects them. if (!("value" in descriptor) || descriptor.configurable === false) { return false; } writeStored(persistent, key, descriptor.value); return true; }, get(target, key, receiver: unknown): unknown { if (typeof key !== "symbol" && !reflectHas(target, key)) { const value = readStored(persistent, key); return value === null ? undefined : value; } return reflectGet(target, key, receiver); }, set(target, key, value: unknown, receiver: unknown): boolean { if (typeof key === "symbol") { return defineReflect(target, key, { value, writable: true, enumerable: true, configurable: true, }); } if (receiver !== proxy) return reflectSet(target, key, value, receiver); writeStored(persistent, key, value); return true; }, has(target, key) { if (reflectHas(target, key)) return true; return typeof key === "string" && readStored(persistent, key) !== null; }, ownKeys(target) { // Named properties shadowed by a prototype member are not visible // as own properties (and have no descriptor below), so they are not // listed either. const stored = keysOp(persistent); const keys: (string | symbol)[] = []; for (let i = 0; i < stored.length; i++) { if (!reflectHas(target, stored[i])) keys[keys.length] = stored[i]; } const own = reflectOwnKeys(target); for (let i = 0; i < own.length; i++) { if (typeof own[i] === "symbol") keys[keys.length] = own[i]; } return keys; }, getOwnPropertyDescriptor(target, key) { if (typeof key === "symbol") return getDescriptor(target, key); if (reflectHas(target, key)) return undefined; const value = readStored(persistent, key); if (value === null) return undefined; return { value, writable: true, enumerable: true, configurable: true }; }, preventExtensions() { return false; }, }); // The proxy, not the hidden target, is the branded object guests hold. setState(states, proxy, getState(states, storage)!); return proxy; } const localStorage = createStorage(true); const sessionStorage = createStorage(false); export { localStorage, sessionStorage, Storage };