X-Git-Url: https://git.ladys.computer/Etiquette/blobdiff_plain/2889a6becfdfadc5b2ddce0b4b4f281a756b0c9e..ce40db353c27887f4345c88fc70a7251bf688bbb:/memory.js diff --git a/memory.js b/memory.js new file mode 100644 index 0000000..6773bdd --- /dev/null +++ b/memory.js @@ -0,0 +1,361 @@ +// đŸ“§đŸ·ïž Étiquette ∷ memory.js +// ==================================================================== +// +// Copyright © 2023 Lady [@ Lady’s Computer]. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at . + +import { wrmgBase32Binary, wrmgBase32String } from "./deps.js"; + +/** + * A symbol which is used internally to identify the constructor + * associated with a stored object. + * + * ※ This value is not exposed. + */ +const constructorSymbol = Symbol("constructor"); + +/** + * Mints a new W·R·M·G base32 identifier with a prefixed checksum. + * + * If an argument is provided, it is used as the underlying numeric + * value for the identifier. + * + * The return value is a `String` *object* with a `.value` own property + * giving the underlying numeric value for the string. + * + * ※ This function is not exposed. + */ +const mintID = ($ = null) => { + const [number, buffer] = $ == null + ? (() => { + // No value was provided; generate a random one and store its + // buffer. + const values = crypto.getRandomValues(new Uint32Array(1)); + const view = new DataView(values.buffer); + return [ + view.getUint32(0) >>> 2, // drop the final 2 bits + view.buffer, + ]; + })() + : [$ >>> 0 & -1 >>> 2, null]; + const checksum = number % 37; + const wrmg = wrmgBase32String( + buffer ?? (() => { + // A value was provided, so a buffer still needs to be generated. + const view = new DataView(new ArrayBuffer(4)); + view.setUint32(0, number << 2, false); + return view.buffer; + })(), + ); + return Object.assign( + new String( + `${"0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U"[checksum]}${ + wrmg.substring(0, 2) + }-${wrmg.substring(2, 6)}`, + ), + { value: number }, + ); +}; + +/** + * Validates the checksum prefixing the provided W·R·M·G base32 + * identifier and then returns that same identifier in normalized form. + * + * ※ This function is not exposed. + */ +const normalizeID = ($) => { + const identifier = `${$}`; + const checksum = "0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U".indexOf( + identifier[0].toUpperCase(), + ); + if (checksum == -1) { + // The checksum character is invalid. + throw new RangeError(`Invalid checksum: "${identifier[0]}".`); + } else { + // There is a valid checksum. + const binary = wrmgBase32Binary`${identifier.substring(1)}0`; + const { byteLength } = binary; + if (byteLength != 4) { + // The identifier was of unexpected size. + throw new RangeError( + `Expected id to fit within 4 bytes, but got ${byteLength}.`, + ); + } else { + // The identifier was correctly‐sized. + const value = new DataView(binary).getUint32(0, false); + if (value & 0b11) { + // The final two bits, which should have been padded with + // zeroes, have a nonzero value. + // + // This should be impossible and indicates something went very + // wrong in base32 decoding. + throw new RangeError("Unexpected values in lower two bits"); + } else { + // The final two bits are zero as expected. + const number = value >>> 2; + if (checksum != number % 37) { + // The checksum does not match the number. + throw new RangeError( + `Invalid checksum for id: ${identifier} (${number})`, + ); + } else { + // The checksum matches. Mint a new identifier with the same + // number to normalize its form. + return `${mintID(number)}`; + } + } + } + } +}; + +/** + * A symbol which is used to identify the method for constructing a new + * instance of a constructor based on stored data. + * + * ※ This value is exposed as `Storage.toInstance`. + */ +const toInstanceSymbol = Symbol("Storage.toInstance"); + +/** + * A symbol which is used to identify the method for converting an + * instance into an object of enumerable own properties suitable for + * persistence. + * + * ※ This value is exposed as `Storage.toObject`. + */ +const toObjectSymbol = Symbol("Storage.toObject"); + +/** + * An in‐memory store of items with checksum‐prefixed 6‐digit W·R·M·G + * base32 identifiers. + */ +export class Storage { + static { + // Define `Storage.toInstance` and `Storage.toObject` as + // nonconfigurable, non·enumerable, read·only properties with the + // appropriate values. + Object.defineProperties(this, { + toInstance: { + configurable: false, + enumerable: false, + value: toInstanceSymbol, + writable: false, + }, + toObject: { + configurable: false, + enumerable: false, + value: toObjectSymbol, + writable: false, + }, + }); + } + + /** + * A `Set` of deleted identifiers, to ensure they are not + * re·assigned. + */ + #deleted = new Set(); + + /** The `Map` used to actually store the data internally. */ + #store = new Map(); + + /** + * Returns a new instance constructed from the provided data as + * retrieved from the provided identifier. + */ + #construct(data, id) { + const { + [constructorSymbol]: constructor, + ...object + } = data; + if (!(toInstanceSymbol in constructor)) { + // There is no method on the constructor for generating an + // instance. + throw new TypeError( + "Constructor must implement Storage.toInstance for object to be retrieved.", + ); + } else { + // Generate an instance and return it. + return constructor[toInstanceSymbol](object, id); + } + } + + /** + * Stores the provided instance and returns its identifier. + * + * If a second argument is given, that identifier will be used; + * otherwise, an unused one will be allocated. + */ + #persist(instance, maybeID = null) { + const store = this.#store; + const deleted = this.#deleted; + if (Object(instance) !== instance) { + // The provided value is not an object. + throw new TypeError("Only objects can be stored."); + } else if (!(toObjectSymbol in instance)) { + // The provided value does not have a method for generating an + // object to store. + throw new TypeError( + "Object must implement Storage.toObject to be stored.", + ); + } else { + // The provided value has a method for generating a storage + // object. + const { constructor } = instance; + const object = instance[toObjectSymbol](); + const id = maybeID ?? (() => { + // No identifier was provided; attempt to generate one. + if (deleted.size + store.size < 1 << 30) { + // There is at least one available identifier. + let id = mintID(); + let literalValue = `${id}`; + while ( + deleted.has(literalValue) || store.has(literalValue) + ) { + // Generate successive identifiers until an available one + // is reached. + id = mintID(id.value + 1); + literalValue = `${id}`; + } + return literalValue; + } else { + // There are no identifiers left to assign. + throw new TypeError("Out of room."); + } + })(); + store.set( + id, + Object.assign( + Object.create(null, { + [constructorSymbol]: { + configurable: false, + enumerable: false, + value: constructor, + writable: false, + }, + }), + object, + ), + ); + return id; + } + } + + /** + * Stores the provided object with a new identifier, and returns the + * assigned identifier. + */ + add(instance) { + return this.#persist(instance); + } + + /** + * Removes the data at the provided identifier. + * + * The identifier will not be re·assigned. + */ + delete(id) { + const store = this.#store; + const validID = normalizeID(id); + this.#deleted.add(validID); + return store.delete(validID); + } + + /** Yields successive identifier~instance pairs from storage. */ + *entries() { + for (const [id, data] of this.#store.entries()) { + // Iterate over the entries, construct instances, and yield them + // with their identifiers. + yield [id, this.#construct(data, id)]; + } + } + + /** + * Call the provided callback function with successive instances + * constructed from data in storage. + * + * The callback function will be called with the constructed + * instance, its identifier, and this `Storage` instance. + * + * If a second argument is provided, it will be used as the `this` + * value. + */ + forEach(callback, thisArg = undefined) { + for (const [id, data] of this.#store.entries()) { + // Iterate over the entries, construct objects, and call the + // callback function with them and their identifiers. + callback.call(thisArg, this.#construct(data, id), id, this); + } + } + + /** + * Returns an instance constructed from the data stored at the + * provided identifier, or `null` if the identifier has no data. + */ + get(id) { + const store = this.#store; + const validID = normalizeID(id); + const data = store.get(validID); + if (data == null) { + // No object was at the provided identifier. + return null; + } else { + // The provided identifier had a stored object; return the + // constructed instance. + return this.#construct(data, validID); + } + } + + /** + * Returns whether the provided identifier currently has data + * associated with it. + */ + has(id) { + const store = this.#store; + return store.has(normalizeID(id)); + } + + /** Yields successive identifiers with data in storage. */ + *keys() { + yield* this.#store.keys(); + } + + /** + * Sets the data for the provided identifier to be that generated + * from the provided instance, then returns this `Storage` object. + */ + set(id, instance) { + this.#persist(instance, normalizeID(id)); + return this; + } + + /** + * Returns the number of identifiers with data in storage. + * + * ☥ This number may be smaller than the actual number of used + * identifiers, as deleted identifiers are *not* freed up for re·use. + */ + get size() { + return this.#store.size; + } + + /** Yields successive instances constructed from data in storage. */ + *values() { + for (const [id, data] of this.#store.entries()) { + // Iterate over the entries, construct instances, and yield them. + yield this.#construct(data, id); + } + } + + /** Yields successive identifier~instance pairs from storage. */ + *[Symbol.iterator]() { + for (const [id, data] of this.#store.entries()) { + // Iterate over the entries, construct instances, and yield them + // with their identifiers. + yield [id, this.#construct(data, id)]; + } + } +}