]>
Lady’s Gitweb - Etiquette/blob - memory.js
   1 // 📧🏷️ Étiquette ∷ memory.js 
   2 // ==================================================================== 
   4 // Copyright © 2023 Lady [@ Lady’s Computer]. 
   6 // This Source Code Form is subject to the terms of the Mozilla Public 
   7 // License, v. 2.0. If a copy of the MPL was not distributed with this 
   8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>. 
  10 import { wrmgBase32Binary
, wrmgBase32String 
} from "./deps.js"; 
  13  * A symbol which is used internally to identify the constructor 
  14  * associated with a stored object. 
  16  * ※ This value is not exposed. 
  18 const constructorSymbol 
= Symbol("constructor"); 
  21  * Mints a new W·R·M·G base32 identifier with a prefixed checksum. 
  23  * If an argument is provided, it is used as the underlying numeric 
  24  * value for the identifier. 
  26  * The return value is a `String` *object* with a `.value` own property 
  27  * giving the underlying numeric value for the string. 
  29  * ※ This function is not exposed. 
  31 const mintID 
= ($ = null) => { 
  32   const { number
, buffer 
} = (() => { 
  34       // No value was provided; generate a random one and store its 
  36       const values 
= crypto
.getRandomValues(new Uint32Array(1)); 
  37       const { buffer 
} = values
; 
  39         number: new DataView(buffer
).getUint32(0) >>> 2, 
  43       // A value was provided, so a buffer needs to be generated. 
  44       const number 
= $ & -1 >>> 2; 
  45       const buffer 
= new ArrayBuffer(4); 
  46       new DataView(buffer
).setUint32(0, number 
<< 2, false); 
  47       return { number
, buffer 
}; 
  50   const checksum 
= number 
% 37; 
  51   const wrmg 
= wrmgBase32String(buffer
); 
  54       `${"0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U"[checksum]}${ 
  56       }-${wrmg.substring(2, 6)}`, 
  63  * Validates the checksum prefixing the provided 30‐bit W·R·M·G base32 
  64  * identifier and then returns that same identifier in normalized form. 
  66  * ☡ This function will throw if the provided value is not a 30‐bit 
  67  * W·R·M·G base32 value with a leading checksum. 
  69  * ※ This function is not exposed. 
  71 const normalizeID 
= ($) => { 
  72   const identifier 
= `${$}`; 
  73   const checksum 
= "0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U".indexOf( 
  74     identifier
[0].toUpperCase(), 
  77     // ☡ The checksum character is invalid. 
  78     throw new RangeError(`Invalid checksum: "${identifier[0]}".`); 
  80     // There is a valid checksum. 
  81     const binary 
= wrmgBase32Binary
`${identifier.substring(1)}0`; 
  82     const { byteLength 
} = binary
; 
  83     if (byteLength 
!= 4) { 
  84       // ☡ The identifier was of unexpected size. 
  86         `Expected id to fit within 4 bytes, but got ${byteLength}.`, 
  89       // The identifier was correctly‐sized. 
  90       const value 
= new DataView(binary
).getUint32(0, false); 
  92         // ☡ The final two bits, which should have been padded with 
  93         // zeroes, have a nonzero value. 
  95         // ※ This should be impossible and indicates something went 
  96         // very wrong in base32 decoding. 
  97         throw new RangeError("Unexpected values in lower two bits"); 
  99         // The final two bits are zero as expected. 
 100         const number 
= value 
>>> 2; 
 101         if (checksum 
!= number 
% 37) { 
 102           // ☡ The checksum does not match the number. 
 103           throw new RangeError( 
 104             `Invalid checksum for id: ${identifier} (${number})`, 
 107           // The checksum matches. Mint a new identifier with the same 
 108           // number to normalize its form. 
 109           return `${mintID(number)}`; 
 117  * A symbol which is used to identify the method for constructing a new 
 118  * instance of a constructor based on stored data. 
 120  * ※ This value is exposed as `Storage.toInstance`. 
 122 const toInstanceSymbol 
= Symbol("Storage.toInstance"); 
 125  * A symbol which is used to identify the method for converting an 
 126  * instance into an object of enumerable own properties suitable for 
 129  * ※ This value is exposed as `Storage.toObject`. 
 131 const toObjectSymbol 
= Symbol("Storage.toObject"); 
 134  * An in‐memory store of items with checksum‐prefixed 6‐digit W·R·M·G 
 135  * base32 identifiers. 
 137 export class Storage 
{ 
 139     // Define `Storage.toInstance` and `Storage.toObject` as 
 140     // nonconfigurable, non·enumerable, read·only properties with the 
 141     // appropriate values. 
 142     Object
.defineProperties(this, { 
 146         value: toInstanceSymbol
, 
 152         value: toObjectSymbol
, 
 159    * A `Set` of deleted identifiers, to ensure they are not 
 162    * The identifier `000-0000` is deleted from the start and can only 
 165   #deleted 
= new Set([`${mintID(0)}`]); 
 167   /** The `Map` used to actually store the data internally. */ 
 171    * Returns a new instance constructed from the provided data as 
 172    * retrieved from the provided identifier. 
 174   #construct(data
, id
) { 
 176       [constructorSymbol
]: constructor, 
 179     if (!(toInstanceSymbol 
in constructor)) { 
 180       // ☡ There is no method on the constructor for generating an 
 183         "Constructor must implement Storage.toInstance for object to be retrieved.", 
 186       // Generate an instance and return it. 
 187       return constructor[toInstanceSymbol
](object
, id
); 
 192    * Stores the provided instance and returns its identifier. 
 194    * If a second argument is given, that identifier will be used; 
 195    * otherwise, an unused one will be allocated. 
 197   #persist(instance
, maybeID 
= null) { 
 198     const store 
= this.#store
; 
 199     const deleted 
= this.#deleted
; 
 200     if (Object(instance
) !== instance
) { 
 201       // The provided value is not an object. 
 202       throw new TypeError("Only objects can be stored."); 
 203     } else if (!(toObjectSymbol 
in instance
)) { 
 204       // The provided value does not have a method for generating an 
 207         "Object must implement Storage.toObject to be stored.", 
 210       // The provided value has a method for generating a storage 
 212       const { constructor } = instance
; 
 213       const object 
= instance
[toObjectSymbol
](); 
 214       const id 
= maybeID 
?? (() => { 
 215         // No identifier was provided; attempt to generate one. 
 216         if (deleted
.size 
+ store
.size 
< 1 << 30) { 
 217           // There is at least one available identifier. 
 219           let literalValue 
= `${id}`; 
 221             deleted
.has(literalValue
) || store
.has(literalValue
) 
 223             // Generate successive identifiers until an available one 
 226             // ※ Successive identifiers are used in order to guarantee 
 227             // an eventual result; continuing to generate random 
 228             // identifiers could go on forever. 
 229             id 
= mintID(id
.value 
+ 1); 
 230             literalValue 
= `${id}`; 
 234           // ☡ There are no identifiers left to assign. 
 236           // ※ This is unlikely to ever be possible in practice. 
 237           throw new TypeError("Out of room."); 
 243           Object
.create(null, { 
 244             [constructorSymbol
]: { 
 259    * Stores the provided object with a new identifier, and returns the 
 260    * assigned identifier. 
 263     return this.#persist(instance
); 
 267    * Removes the data at the provided identifier. 
 269    * The identifier will not be re·assigned. 
 272     const store 
= this.#store
; 
 273     const normalized 
= normalizeID(id
); 
 274     this.#deleted
.add(normalized
); 
 275     return store
.delete(normalized
); 
 278   /** Yields successive identifier~instance pairs from storage. */ 
 280     for (const [id
, data
] of this.#store
.entries()) { 
 281       // Iterate over the entries, construct instances, and yield them 
 282       // with their identifiers. 
 283       yield [id
, this.#construct(data
, id
)]; 
 288    * Call the provided callback function with successive instances 
 289    * constructed from data in storage. 
 291    * The callback function will be called with the constructed 
 292    * instance, its identifier, and this `Storage` instance. 
 294    * If a second argument is provided, it will be used as the `this` 
 297   forEach(callback
, thisArg 
= undefined) { 
 298     for (const [id
, data
] of this.#store
.entries()) { 
 299       // Iterate over the entries, construct objects, and call the 
 300       // callback function with them and their identifiers. 
 301       callback
.call(thisArg
, this.#construct(data
, id
), id
, this); 
 306    * Returns an instance constructed from the data stored at the 
 307    * provided identifier, or `null` if the identifier has no data. 
 310     const store 
= this.#store
; 
 311     const normalized 
= normalizeID(id
); 
 312     const data 
= store
.get(normalized
); 
 314       // No object was at the provided identifier. 
 317       // The provided identifier had a stored object; return the 
 318       // constructed instance. 
 319       return this.#construct(data
, normalized
); 
 324    * Returns whether the provided identifier currently has data 
 325    * associated with it. 
 328     const store 
= this.#store
; 
 329     return store
.has(normalizeID(id
)); 
 332   /** Yields successive identifiers with data in storage. */ 
 334     yield* this.#store
.keys(); 
 338    * Sets the data for the provided identifier to be that generated 
 339    * from the provided instance, then returns this `Storage` object. 
 342     this.#persist(instance
, normalizeID(id
)); 
 347    * Returns the number of identifiers with data in storage. 
 349    * ☡ This number may be smaller than the actual number of used 
 350    * identifiers, as deleted identifiers are *not* freed up for re·use. 
 353     return this.#store
.size
; 
 356   /** Yields successive instances constructed from data in storage. */ 
 358     for (const [id
, data
] of this.#store
.entries()) { 
 359       // Iterate over the entries, construct instances, and yield them. 
 360       yield this.#construct(data
, id
); 
 364   /** Yields successive identifier~instance pairs from storage. */ 
 365   *[Symbol
.iterator
]() { 
 366     for (const [id
, data
] of this.#store
.entries()) { 
 367       // Iterate over the entries, construct instances, and yield them 
 368       // with their identifiers. 
 369       yield [id
, this.#construct(data
, id
)]; 
 
This page took 0.133486 seconds  and 5 git commands  to generate.