char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport webstorage extension to typescriptcbc8039

main
6.2 KiB183 linesraw
1import {
2  ObjectCreate,
3  ObjectGetOwnPropertyDescriptor as getDescriptor,
4  Proxy,
5  ReflectDefineProperty as defineReflect,
6  ReflectDeleteProperty as deleteReflect,
7  ReflectGet as reflectGet,
8  ReflectHas as reflectHas,
9  ReflectOwnKeys as reflectOwnKeys,
10  ReflectSet as reflectSet,
11  StringPrototypeIsWellFormed as isWellFormed,
12  TypeError,
13  WeakMap,
14  WeakMapPrototypeGet as getState,
15  WeakMapPrototypeSet as setState,
16} from "ext:core/primordials";
17import {
18  op_webstorage_clear as clearOp,
19  op_webstorage_get as getOp,
20  op_webstorage_key as keyOp,
21  op_webstorage_keys as keysOp,
22  op_webstorage_length as lengthOp,
23  op_webstorage_remove as removeOp,
24  op_webstorage_set as setOp,
25} from "ext:core/ops";
26import { createException } from "ext:web/events.js";
27import { required, state, string } from "ext:web/idl.js";
28import { registerUncloneable } from "ext:web/serialization.js";
29
30const states = new WeakMap<object, { persistent: boolean }>();
31registerUncloneable(states);
32const secret: object = ObjectCreate(null);
33
34// Strings cross into Rust as UTF-8; refuse lone surrogates on writes
35// rather than letting that conversion silently replace them. Ill-formed
36// names can never have been stored, so reads simply report them absent.
37function wellFormed(value: unknown): string {
38  const result = string(value);
39  if (!isWellFormed(result)) {
40    throw new TypeError("Storage keys and values must be well-formed strings");
41  }
42  return result;
43}
44
45function readStored(persistent: boolean, key: string): string | null {
46  if (!isWellFormed(key)) return null;
47  return getOp([persistent, key]);
48}
49
50function writeStored(persistent: boolean, key: string, value: unknown): void {
51  if (!isWellFormed(key)) {
52    throw new TypeError("Storage keys and values must be well-formed strings");
53  }
54  const stored = setOp([persistent, key, wellFormed(value)]);
55  if (!stored) {
56    throw createException({
57      message: "Exceeded maximum storage size",
58      name: "QuotaExceededError",
59    });
60  }
61}
62
63function removeStored(persistent: boolean, key: string): void {
64  if (isWellFormed(key)) removeOp([persistent, key]);
65}
66
67class Storage {
68  [key: string | symbol]: unknown;
69  constructor();
70  constructor(token: object, persistent: boolean);
71  constructor() {
72    if (arguments[0] !== secret) throw new TypeError("Illegal constructor");
73    // Only createStorage holds the token and supplies the persistence flag.
74    setState(states, this, { persistent: arguments[1] as boolean });
75  }
76  get length(): number {
77    return lengthOp(state(states, this).persistent);
78  }
79  key(index: unknown): string | null {
80    const current = state(states, this);
81    required(arguments.length, 1);
82    // Keep ToUint32 coercion: Number() would accept BigInt.
83    return keyOp([current.persistent, (index as number) >>> 0]);
84  }
85  getItem(key: unknown): string | null {
86    const current = state(states, this);
87    required(arguments.length, 1);
88    return readStored(current.persistent, string(key));
89  }
90  setItem(key: unknown, value: unknown): void {
91    const current = state(states, this);
92    required(arguments.length, 2);
93    writeStored(current.persistent, string(key), value);
94  }
95  removeItem(key: unknown): void {
96    const current = state(states, this);
97    required(arguments.length, 1);
98    removeStored(current.persistent, string(key));
99  }
100  clear(): void {
101    clearOp(state(states, this).persistent);
102  }
103}
104
105function createStorage(persistent: boolean): Storage {
106  const storage = new Storage(secret, persistent);
107  const proxy: Storage = new Proxy(storage, {
108    deleteProperty(target, key) {
109      if (typeof key === "symbol" || reflectHas(target, key)) {
110        return deleteReflect(target, key);
111      }
112      removeStored(persistent, key);
113      return true;
114    },
115    defineProperty(target, key, descriptor) {
116      if (typeof key === "symbol") return defineReflect(target, key, descriptor);
117      // Accessor, generic, and non-configurable descriptors have no
118      // named-property equivalent; WebIDL rejects them.
119      if (!("value" in descriptor) || descriptor.configurable === false) {
120        return false;
121      }
122      writeStored(persistent, key, descriptor.value);
123      return true;
124    },
125    get(target, key, receiver: unknown): unknown {
126      if (typeof key !== "symbol" && !reflectHas(target, key)) {
127        const value = readStored(persistent, key);
128        return value === null ? undefined : value;
129      }
130      return reflectGet(target, key, receiver);
131    },
132    set(target, key, value: unknown, receiver: unknown): boolean {
133      if (typeof key === "symbol") {
134        return defineReflect(target, key, {
135          value,
136          writable: true,
137          enumerable: true,
138          configurable: true,
139        });
140      }
141      if (receiver !== proxy) return reflectSet(target, key, value, receiver);
142      writeStored(persistent, key, value);
143      return true;
144    },
145    has(target, key) {
146      if (reflectHas(target, key)) return true;
147      return typeof key === "string" && readStored(persistent, key) !== null;
148    },
149    ownKeys(target) {
150      // Named properties shadowed by a prototype member are not visible
151      // as own properties (and have no descriptor below), so they are not
152      // listed either.
153      const stored = keysOp(persistent);
154      const keys: (string | symbol)[] = [];
155      for (let i = 0; i < stored.length; i++) {
156        if (!reflectHas(target, stored[i])) keys[keys.length] = stored[i];
157      }
158      const own = reflectOwnKeys(target);
159      for (let i = 0; i < own.length; i++) {
160        if (typeof own[i] === "symbol") keys[keys.length] = own[i];
161      }
162      return keys;
163    },
164    getOwnPropertyDescriptor(target, key) {
165      if (typeof key === "symbol") return getDescriptor(target, key);
166      if (reflectHas(target, key)) return undefined;
167      const value = readStored(persistent, key);
168      if (value === null) return undefined;
169      return { value, writable: true, enumerable: true, configurable: true };
170    },
171    preventExtensions() {
172      return false;
173    },
174  });
175  // The proxy, not the hidden target, is the branded object guests hold.
176  setState(states, proxy, getState(states, storage)!);
177  return proxy;
178}
179
180const localStorage = createStorage(true);
181const sessionStorage = createStorage(false);
182
183export { localStorage, sessionStorage, Storage };