- 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.
* ※ This function is not exposed.
*/
const mintID = ($ = null) => {
* ※ 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));
// 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 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]}${
return Object.assign(
new String(
`${"0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U"[checksum]}${
- * 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.
*
* 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 = ($) => {
* ※ This function is not exposed.
*/
const normalizeID = ($) => {
identifier[0].toUpperCase(),
);
if (checksum == -1) {
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) {
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}.`,
);
throw new RangeError(
`Expected id to fit within 4 bytes, but got ${byteLength}.`,
);
// The identifier was correctly‐sized.
const value = new DataView(binary).getUint32(0, false);
if (value & 0b11) {
// 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.
//
// 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) {
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})`,
);
throw new RangeError(
`Invalid checksum for id: ${identifier} (${number})`,
);
/**
* A `Set` of deleted identifiers, to ensure they are not
* re·assigned.
/**
* 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([`${mintID(0)}`]);
/** The `Map` used to actually store the data internally. */
#store = new Map();
/** The `Map` used to actually store the data internally. */
#store = new Map();
...object
} = data;
if (!(toInstanceSymbol in constructor)) {
...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.",
// instance.
throw new TypeError(
"Constructor must implement Storage.toInstance for object to be retrieved.",
) {
// Generate successive identifiers until an available one
// is reached.
) {
// 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 {
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.");
}
})();
throw new TypeError("Out of room.");
}
})();
*/
delete(id) {
const store = this.#store;
*/
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. */
}
/** Yields successive identifier~instance pairs from storage. */
*/
get(id) {
const store = this.#store;
*/
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.
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);
}
describe("Storage", () => {
}
describe("Storage", () => {
+ it("[[Call]] throws", () => {
+ assertThrows(() => {
+ Storage();
+ });
+ });
+
it("[[Construct]] creates a new Storage", () => {
assertStrictEquals(
Object.getPrototypeOf(new Storage()),
it("[[Construct]] creates a new Storage", () => {
assertStrictEquals(
Object.getPrototypeOf(new Storage()),
assertStrictEquals(instance.has("000-0000"), false);
instance.set("000-0000", new Storable());
assertStrictEquals(instance.has("000-0000"), true);
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);
});
assertStrictEquals(instance.has("000-0000"), false);
});
const data = { my: "data" };
const storable = new Storable(data);
instance.set(newID, storable);
const data = { my: "data" };
const storable = new Storable(data);
instance.set(newID, storable);
instance.set(newID, storable);
assertEquals(instance.get(newID).data, data);
});
instance.set(newID, storable);
assertEquals(instance.get(newID).data, data);
});