From: Lady Date: Mon, 29 May 2023 05:09:47 +0000 (-0700) Subject: Small improvements to storage X-Git-Url: https://git.ladys.computer/Etiquette/commitdiff_plain/9f6c704b8d1ebe70e97b0d934d04c71e8ae0ed5f?ds=sidebyside;hp=ce40db353c27887f4345c88fc70a7251bf688bbb Small improvements to storage - Restructuring of I·D minting for readability. - Test to ensure `Storage` must be called as a constructor. - `000-0000` is now reserved (deleted) by default. - Formatting and documentation improvements. --- diff --git a/memory.js b/memory.js index 6773bdd..252418b 100644 --- a/memory.js +++ b/memory.js @@ -29,27 +29,26 @@ const constructorSymbol = Symbol("constructor"); * ※ 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 +60,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,14 +74,14 @@ 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}.`, ); @@ -87,17 +89,17 @@ const normalizeID = ($) => { // 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. + // ※ 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. + // ☡ The checksum does not match the number. throw new RangeError( `Invalid checksum for id: ${identifier} (${number})`, ); @@ -156,8 +158,11 @@ export class Storage { /** * 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. */ #store = new Map(); @@ -172,7 +177,7 @@ 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.", @@ -217,12 +222,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 +270,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. */ @@ -297,15 +308,15 @@ export class Storage { */ 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); } } diff --git a/memory.test.js b/memory.test.js index 8187635..b9217a6 100644 --- a/memory.test.js +++ b/memory.test.js @@ -40,6 +40,12 @@ class Storable { } describe("Storage", () => { + it("[[Call]] throws", () => { + assertThrows(() => { + Storage(); + }); + }); + it("[[Construct]] creates a new Storage", () => { assertStrictEquals( Object.getPrototypeOf(new Storage()), @@ -155,7 +161,7 @@ describe("Storage", () => { assertStrictEquals(instance.has("000-0000"), false); instance.set("000-0000", new Storable()); assertStrictEquals(instance.has("000-0000"), true); - instance.delete("000-0000") + instance.delete("000-0000"); assertStrictEquals(instance.has("000-0000"), false); }); @@ -191,7 +197,7 @@ describe("Storage", () => { const data = { my: "data" }; const storable = new Storable(data); instance.set(newID, storable); - data.my = "new data" + data.my = "new data"; instance.set(newID, storable); assertEquals(instance.get(newID).data, data); });