]>
Lady’s Gitweb - Etiquette/blob - memory.js
6773bddfc91b3ee533f7cb5bef46fd864a753534
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
] = $ == null
34 // No value was provided; generate a random one and store its
36 const values
= crypto
.getRandomValues(new Uint32Array(1));
37 const view
= new DataView(values
.buffer
);
39 view
.getUint32(0) >>> 2, // drop the final 2 bits
43 : [$ >>> 0 & -1 >>> 2, null];
44 const checksum
= number
% 37;
45 const wrmg
= wrmgBase32String(
47 // A value was provided, so a buffer still needs to be generated.
48 const view
= new DataView(new ArrayBuffer(4));
49 view
.setUint32(0, number
<< 2, false);
55 `${"0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U"[checksum]}${
57 }-${wrmg.substring(2, 6)}`,
64 * Validates the checksum prefixing the provided W·R·M·G base32
65 * identifier and then returns that same identifier in normalized form.
67 * ※ This function is not exposed.
69 const normalizeID
= ($) => {
70 const identifier
= `${$}`;
71 const checksum
= "0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U".indexOf(
72 identifier
[0].toUpperCase(),
75 // The checksum character is invalid.
76 throw new RangeError(`Invalid checksum: "${identifier[0]}".`);
78 // There is a valid checksum.
79 const binary
= wrmgBase32Binary
`${identifier.substring(1)}0`;
80 const { byteLength
} = binary
;
81 if (byteLength
!= 4) {
82 // The identifier was of unexpected size.
84 `Expected id to fit within 4 bytes, but got ${byteLength}.`,
87 // The identifier was correctly‐sized.
88 const value
= new DataView(binary
).getUint32(0, false);
90 // The final two bits, which should have been padded with
91 // zeroes, have a nonzero value.
93 // This should be impossible and indicates something went very
94 // wrong in base32 decoding.
95 throw new RangeError("Unexpected values in lower two bits");
97 // The final two bits are zero as expected.
98 const number
= value
>>> 2;
99 if (checksum
!= number
% 37) {
100 // The checksum does not match the number.
101 throw new RangeError(
102 `Invalid checksum for id: ${identifier} (${number})`,
105 // The checksum matches. Mint a new identifier with the same
106 // number to normalize its form.
107 return `${mintID(number)}`;
115 * A symbol which is used to identify the method for constructing a new
116 * instance of a constructor based on stored data.
118 * ※ This value is exposed as `Storage.toInstance`.
120 const toInstanceSymbol
= Symbol("Storage.toInstance");
123 * A symbol which is used to identify the method for converting an
124 * instance into an object of enumerable own properties suitable for
127 * ※ This value is exposed as `Storage.toObject`.
129 const toObjectSymbol
= Symbol("Storage.toObject");
132 * An in‐memory store of items with checksum‐prefixed 6‐digit W·R·M·G
133 * base32 identifiers.
135 export class Storage
{
137 // Define `Storage.toInstance` and `Storage.toObject` as
138 // nonconfigurable, non·enumerable, read·only properties with the
139 // appropriate values.
140 Object
.defineProperties(this, {
144 value
: toInstanceSymbol
,
150 value
: toObjectSymbol
,
157 * A `Set` of deleted identifiers, to ensure they are not
160 #deleted
= new Set();
162 /** The `Map` used to actually store the data internally. */
166 * Returns a new instance constructed from the provided data as
167 * retrieved from the provided identifier.
169 #construct(data
, id
) {
171 [constructorSymbol
]: constructor,
174 if (!(toInstanceSymbol
in constructor)) {
175 // There is no method on the constructor for generating an
178 "Constructor must implement Storage.toInstance for object to be retrieved.",
181 // Generate an instance and return it.
182 return constructor[toInstanceSymbol
](object
, id
);
187 * Stores the provided instance and returns its identifier.
189 * If a second argument is given, that identifier will be used;
190 * otherwise, an unused one will be allocated.
192 #persist(instance
, maybeID
= null) {
193 const store
= this.#store
;
194 const deleted
= this.#deleted
;
195 if (Object(instance
) !== instance
) {
196 // The provided value is not an object.
197 throw new TypeError("Only objects can be stored.");
198 } else if (!(toObjectSymbol
in instance
)) {
199 // The provided value does not have a method for generating an
202 "Object must implement Storage.toObject to be stored.",
205 // The provided value has a method for generating a storage
207 const { constructor } = instance
;
208 const object
= instance
[toObjectSymbol
]();
209 const id
= maybeID
?? (() => {
210 // No identifier was provided; attempt to generate one.
211 if (deleted
.size
+ store
.size
< 1 << 30) {
212 // There is at least one available identifier.
214 let literalValue
= `${id}`;
216 deleted
.has(literalValue
) || store
.has(literalValue
)
218 // Generate successive identifiers until an available one
220 id
= mintID(id
.value
+ 1);
221 literalValue
= `${id}`;
225 // There are no identifiers left to assign.
226 throw new TypeError("Out of room.");
232 Object
.create(null, {
233 [constructorSymbol
]: {
248 * Stores the provided object with a new identifier, and returns the
249 * assigned identifier.
252 return this.#persist(instance
);
256 * Removes the data at the provided identifier.
258 * The identifier will not be re·assigned.
261 const store
= this.#store
;
262 const validID
= normalizeID(id
);
263 this.#deleted
.add(validID
);
264 return store
.delete(validID
);
267 /** Yields successive identifier~instance pairs from storage. */
269 for (const [id
, data
] of this.#store
.entries()) {
270 // Iterate over the entries, construct instances, and yield them
271 // with their identifiers.
272 yield [id
, this.#construct(data
, id
)];
277 * Call the provided callback function with successive instances
278 * constructed from data in storage.
280 * The callback function will be called with the constructed
281 * instance, its identifier, and this `Storage` instance.
283 * If a second argument is provided, it will be used as the `this`
286 forEach(callback
, thisArg
= undefined) {
287 for (const [id
, data
] of this.#store
.entries()) {
288 // Iterate over the entries, construct objects, and call the
289 // callback function with them and their identifiers.
290 callback
.call(thisArg
, this.#construct(data
, id
), id
, this);
295 * Returns an instance constructed from the data stored at the
296 * provided identifier, or `null` if the identifier has no data.
299 const store
= this.#store
;
300 const validID
= normalizeID(id
);
301 const data
= store
.get(validID
);
303 // No object was at the provided identifier.
306 // The provided identifier had a stored object; return the
307 // constructed instance.
308 return this.#construct(data
, validID
);
313 * Returns whether the provided identifier currently has data
314 * associated with it.
317 const store
= this.#store
;
318 return store
.has(normalizeID(id
));
321 /** Yields successive identifiers with data in storage. */
323 yield* this.#store
.keys();
327 * Sets the data for the provided identifier to be that generated
328 * from the provided instance, then returns this `Storage` object.
331 this.#persist(instance
, normalizeID(id
));
336 * Returns the number of identifiers with data in storage.
338 * ☡ This number may be smaller than the actual number of used
339 * identifiers, as deleted identifiers are *not* freed up for re·use.
342 return this.#store
.size
;
345 /** Yields successive instances constructed from data in storage. */
347 for (const [id
, data
] of this.#store
.entries()) {
348 // Iterate over the entries, construct instances, and yield them.
349 yield this.#construct(data
, id
);
353 /** Yields successive identifier~instance pairs from storage. */
354 *[Symbol
.iterator
]() {
355 for (const [id
, data
] of this.#store
.entries()) {
356 // Iterate over the entries, construct instances, and yield them
357 // with their identifiers.
358 yield [id
, this.#construct(data
, id
)];
This page took 0.076979 seconds and 3 git commands to generate.