X-Git-Url: https://git.ladys.computer/Etiquette/blobdiff_plain/ce40db353c27887f4345c88fc70a7251bf688bbb..f8903c5b3bd12d02af174e5043d906d63da0b0d1:/memory.js diff --git a/memory.js b/memory.js index 6773bdd..f4a861b 100644 --- a/memory.js +++ b/memory.js @@ -1,14 +1,19 @@ -// 📧🏷️ É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 . +// SPDX-FileCopyrightText: 2023, 2025 Lady +// SPDX-License-Identifier: MPL-2.0 +/** + * ⁌ 📧🏷️ Étiquette ∷ memory.js + * + * Copyright © 2023, 2025 Lady [@ Ladys 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"; +const ÉTIQUETTE = "📧🏷️ Étiquette"; + /** * A symbol which is used internally to identify the constructor * associated with a stored object. @@ -23,33 +28,32 @@ const constructorSymbol = Symbol("constructor"); * 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 + * 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 - ? (() => { + const { number, buffer } = (() => { + if ($ == 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 { buffer } = values; + return { + number: new DataView(buffer).getUint32(0) >>> 2, + buffer, + }; + } else { + // A value was provided, so a buffer needs to be generated. + const number = $ & -1 >>> 2; + const buffer = new ArrayBuffer(4); + new DataView(buffer).setUint32(0, number << 2, false); + return { number, buffer }; + } + })(); 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; - })(), - ); + const wrmg = wrmgBase32String(buffer); return Object.assign( new String( `${"0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U"[checksum]}${ @@ -61,9 +65,12 @@ const mintID = ($ = null) => { }; /** - * Validates the checksum prefixing the provided W·R·M·G base32 + * Validates the checksum prefixing the provided 30‐bit W·R·M·G base32 * identifier and then returns that same identifier in normalized form. * + * ☡ This function will throw if the provided value is not a 30‐bit + * W·R·M·G base32 value with a leading checksum. + * * ※ This function is not exposed. */ const normalizeID = ($) => { @@ -72,34 +79,36 @@ const normalizeID = ($) => { identifier[0].toUpperCase(), ); if (checksum == -1) { - // The checksum character is invalid. + // ☡ 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. + // ☡ The identifier was of unexpected size. throw new RangeError( - `Expected id to fit within 4 bytes, but got ${byteLength}.`, + `${ÉTIQUETTE}: 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 + // ☡ 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"); + // ※ This should be impossible and indicates something went + // very wrong in base32 decoding. + throw new RangeError( + `${ÉTIQUETTE}: 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. + // ☡ The checksum does not match the number. throw new RangeError( - `Invalid checksum for id: ${identifier} (${number})`, + `${ÉTIQUETTE}: Invalid checksum for id: ${identifier} (${number})`, ); } else { // The checksum matches. Mint a new identifier with the same @@ -115,7 +124,7 @@ const normalizeID = ($) => { * 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`. + * ※ This value is exposed as `Storage.toInstance´. */ const toInstanceSymbol = Symbol("Storage.toInstance"); @@ -124,7 +133,7 @@ const toInstanceSymbol = Symbol("Storage.toInstance"); * instance into an object of enumerable own properties suitable for * persistence. * - * ※ This value is exposed as `Storage.toObject`. + * ※ This value is exposed as `Storage.toObject´. */ const toObjectSymbol = Symbol("Storage.toObject"); @@ -134,7 +143,7 @@ const toObjectSymbol = Symbol("Storage.toObject"); */ export class Storage { static { - // Define `Storage.toInstance` and `Storage.toObject` as + // Define `Storage.toInstance´ and `Storage.toObject´ as // nonconfigurable, non·enumerable, read·only properties with the // appropriate values. Object.defineProperties(this, { @@ -154,12 +163,15 @@ export class Storage { } /** - * A `Set` of deleted identifiers, to ensure they are not + * A `Set´ of deleted identifiers, to ensure they are not * re·assigned. + * + * The identifier `000-0000´ is deleted from the start and can only + * be manually set. */ - #deleted = new Set(); + #deleted = new Set([`${mintID(0)}`]); - /** The `Map` used to actually store the data internally. */ + /** The `Map´ used to actually store the data internally. */ #store = new Map(); /** @@ -172,10 +184,10 @@ export class Storage { ...object } = data; if (!(toInstanceSymbol in constructor)) { - // There is no method on the constructor for generating an + // ☡ There is no method on the constructor for generating an // instance. throw new TypeError( - "Constructor must implement Storage.toInstance for object to be retrieved.", + `${ÉTIQUETTE}: Constructor must implement Storage.toInstance for object to be retrieved.`, ); } else { // Generate an instance and return it. @@ -199,7 +211,7 @@ export class Storage { // 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.", + `${ÉTIQUETTE}: Object must implement Storage.toObject to be stored.`, ); } else { // The provided value has a method for generating a storage @@ -217,12 +229,18 @@ export class Storage { ) { // Generate successive identifiers until an available one // is reached. + // + // ※ Successive identifiers are used in order to guarantee + // an eventual result; continuing to generate random + // identifiers could go on forever. id = mintID(id.value + 1); literalValue = `${id}`; } return literalValue; } else { - // There are no identifiers left to assign. + // ☡ There are no identifiers left to assign. + // + // ※ This is unlikely to ever be possible in practice. throw new TypeError("Out of room."); } })(); @@ -259,9 +277,9 @@ export class Storage { */ delete(id) { const store = this.#store; - const validID = normalizeID(id); - this.#deleted.add(validID); - return store.delete(validID); + const normalized = normalizeID(id); + this.#deleted.add(normalized); + return store.delete(normalized); } /** Yields successive identifier~instance pairs from storage. */ @@ -278,9 +296,9 @@ export class Storage { * constructed from data in storage. * * The callback function will be called with the constructed - * instance, its identifier, and this `Storage` instance. + * instance, its identifier, and this `Storage´ instance. * - * If a second argument is provided, it will be used as the `this` + * If a second argument is provided, it will be used as the `this´ * value. */ forEach(callback, thisArg = undefined) { @@ -293,19 +311,19 @@ export class Storage { /** * Returns an instance constructed from the data stored at the - * provided identifier, or `null` if the identifier has no data. + * 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); + const normalized = normalizeID(id); + const data = store.get(normalized); 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); + return this.#construct(data, normalized); } } @@ -325,7 +343,7 @@ export class Storage { /** * Sets the data for the provided identifier to be that generated - * from the provided instance, then returns this `Storage` object. + * from the provided instance, then returns this `Storage´ object. */ set(id, instance) { this.#persist(instance, normalizeID(id)); @@ -336,7 +354,7 @@ export class Storage { * 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. + * identifiers, as deleted identifiers are ⹐not⹑ freed up for re·use. */ get size() { return this.#store.size;