]>
Lady’s Gitweb - Etiquette/blob - memory.js
1 // SPDX-FileCopyrightText: 2023, 2025 Lady <https://www.ladys.computer/about/#lady>
2 // SPDX-License-Identifier: MPL-2.0
4 * ⁌ 📧🏷️ Étiquette ∷ memory.js
6 * Copyright © 2023, 2025 Lady [@ Ladys Computer].
8 * This Source Code Form is subject to the terms of the Mozilla Public
9 * License, v. 2.0. If a copy of the MPL was not distributed with this
10 * file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
13 import { wrmgBase32Binary
, wrmgBase32String
} from "./deps.js";
15 const ÉTIQUETTE
= "📧🏷️ Étiquette";
18 * A symbol which is used internally to identify the constructor
19 * associated with a stored object.
21 * ※ This value is not exposed.
23 const constructorSymbol
= Symbol("constructor");
26 * Mints a new W·R·M·G base32 identifier with a prefixed checksum.
28 * If an argument is provided, it is used as the underlying numeric
29 * value for the identifier.
31 * The return value is a `String´ ⹐object⹑ with a `.value´ own property
32 * giving the underlying numeric value for the string.
34 * ※ This function is not exposed.
36 const mintID
= ($ = null) => {
37 const { number
, buffer
} = (() => {
39 // No value was provided; generate a random one and store its
41 const values
= crypto
.getRandomValues(new Uint32Array(1));
42 const { buffer
} = values
;
44 number: new DataView(buffer
).getUint32(0) >>> 2,
48 // A value was provided, so a buffer needs to be generated.
49 const number
= $ & -1 >>> 2;
50 const buffer
= new ArrayBuffer(4);
51 new DataView(buffer
).setUint32(0, number
<< 2, false);
52 return { number
, buffer
};
55 const checksum
= number
% 37;
56 const wrmg
= wrmgBase32String(buffer
);
59 `${"0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U"[checksum]}${
61 }-${wrmg.substring(2, 6)}`,
68 * Validates the checksum prefixing the provided 30‐bit W·R·M·G base32
69 * identifier and then returns that same identifier in normalized form.
71 * ☡ This function will throw if the provided value is not a 30‐bit
72 * W·R·M·G base32 value with a leading checksum.
74 * ※ This function is not exposed.
76 const normalizeID
= ($) => {
77 const identifier
= `${$}`;
78 const checksum
= "0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U".indexOf(
79 identifier
[0].toUpperCase(),
82 // ☡ The checksum character is invalid.
83 throw new RangeError(`Invalid checksum: "${identifier[0]}".`);
85 // There is a valid checksum.
86 const binary
= wrmgBase32Binary
`${identifier.substring(1)}0`;
87 const { byteLength
} = binary
;
88 if (byteLength
!= 4) {
89 // ☡ The identifier was of unexpected size.
91 `${ÉTIQUETTE}: Expected id to fit within 4 bytes, but got ${byteLength}.`,
94 // The identifier was correctly‐sized.
95 const value
= new DataView(binary
).getUint32(0, false);
97 // ☡ The final two bits, which should have been padded with
98 // zeroes, have a nonzero value.
100 // ※ This should be impossible and indicates something went
101 // very wrong in base32 decoding.
102 throw new RangeError(
103 `${ÉTIQUETTE}: Unexpected values in lower two bits`,
106 // The final two bits are zero as expected.
107 const number
= value
>>> 2;
108 if (checksum
!= number
% 37) {
109 // ☡ The checksum does not match the number.
110 throw new RangeError(
111 `${ÉTIQUETTE}: Invalid checksum for id: ${identifier} (${number})`,
114 // The checksum matches. Mint a new identifier with the same
115 // number to normalize its form.
116 return `${mintID(number)}`;
124 * A symbol which is used to identify the method for constructing a new
125 * instance of a constructor based on stored data.
127 * ※ This value is exposed as `Storage.toInstance´.
129 const toInstanceSymbol
= Symbol("Storage.toInstance");
132 * A symbol which is used to identify the method for converting an
133 * instance into an object of enumerable own properties suitable for
136 * ※ This value is exposed as `Storage.toObject´.
138 const toObjectSymbol
= Symbol("Storage.toObject");
141 * An in‐memory store of items with checksum‐prefixed 6‐digit W·R·M·G
142 * base32 identifiers.
144 export class Storage
{
146 // Define `Storage.toInstance´ and `Storage.toObject´ as
147 // nonconfigurable, non·enumerable, read·only properties with the
148 // appropriate values.
149 Object
.defineProperties(this, {
153 value: toInstanceSymbol
,
159 value: toObjectSymbol
,
166 * A `Set´ of deleted identifiers, to ensure they are not
169 * The identifier `000-0000´ is deleted from the start and can only
172 #deleted
= new Set([`${mintID(0)}`]);
174 /** The `Map´ used to actually store the data internally. */
178 * Returns a new instance constructed from the provided data as
179 * retrieved from the provided identifier.
181 #construct(data
, id
) {
183 [constructorSymbol
]: constructor,
186 if (!(toInstanceSymbol
in constructor)) {
187 // ☡ There is no method on the constructor for generating an
190 `${ÉTIQUETTE}: Constructor must implement Storage.toInstance for object to be retrieved.`,
193 // Generate an instance and return it.
194 return constructor[toInstanceSymbol
](object
, id
);
199 * Stores the provided instance and returns its identifier.
201 * If a second argument is given, that identifier will be used;
202 * otherwise, an unused one will be allocated.
204 #persist(instance
, maybeID
= null) {
205 const store
= this.#store
;
206 const deleted
= this.#deleted
;
207 if (Object(instance
) !== instance
) {
208 // The provided value is not an object.
209 throw new TypeError("Only objects can be stored.");
210 } else if (!(toObjectSymbol
in instance
)) {
211 // The provided value does not have a method for generating an
214 `${ÉTIQUETTE}: Object must implement Storage.toObject to be stored.`,
217 // The provided value has a method for generating a storage
219 const { constructor } = instance
;
220 const object
= instance
[toObjectSymbol
]();
221 const id
= maybeID
?? (() => {
222 // No identifier was provided; attempt to generate one.
223 if (deleted
.size
+ store
.size
< 1 << 30) {
224 // There is at least one available identifier.
226 let literalValue
= `${id}`;
228 deleted
.has(literalValue
) || store
.has(literalValue
)
230 // Generate successive identifiers until an available one
233 // ※ Successive identifiers are used in order to guarantee
234 // an eventual result; continuing to generate random
235 // identifiers could go on forever.
236 id
= mintID(id
.value
+ 1);
237 literalValue
= `${id}`;
241 // ☡ There are no identifiers left to assign.
243 // ※ This is unlikely to ever be possible in practice.
244 throw new TypeError("Out of room.");
250 Object
.create(null, {
251 [constructorSymbol
]: {
266 * Stores the provided object with a new identifier, and returns the
267 * assigned identifier.
270 return this.#persist(instance
);
274 * Removes the data at the provided identifier.
276 * The identifier will not be re·assigned.
279 const store
= this.#store
;
280 const normalized
= normalizeID(id
);
281 this.#deleted
.add(normalized
);
282 return store
.delete(normalized
);
285 /** Yields successive identifier~instance pairs from storage. */
287 for (const [id
, data
] of this.#store
.entries()) {
288 // Iterate over the entries, construct instances, and yield them
289 // with their identifiers.
290 yield [id
, this.#construct(data
, id
)];
295 * Call the provided callback function with successive instances
296 * constructed from data in storage.
298 * The callback function will be called with the constructed
299 * instance, its identifier, and this `Storage´ instance.
301 * If a second argument is provided, it will be used as the `this´
304 forEach(callback
, thisArg
= undefined) {
305 for (const [id
, data
] of this.#store
.entries()) {
306 // Iterate over the entries, construct objects, and call the
307 // callback function with them and their identifiers.
308 callback
.call(thisArg
, this.#construct(data
, id
), id
, this);
313 * Returns an instance constructed from the data stored at the
314 * provided identifier, or `null´ if the identifier has no data.
317 const store
= this.#store
;
318 const normalized
= normalizeID(id
);
319 const data
= store
.get(normalized
);
321 // No object was at the provided identifier.
324 // The provided identifier had a stored object; return the
325 // constructed instance.
326 return this.#construct(data
, normalized
);
331 * Returns whether the provided identifier currently has data
332 * associated with it.
335 const store
= this.#store
;
336 return store
.has(normalizeID(id
));
339 /** Yields successive identifiers with data in storage. */
341 yield* this.#store
.keys();
345 * Sets the data for the provided identifier to be that generated
346 * from the provided instance, then returns this `Storage´ object.
349 this.#persist(instance
, normalizeID(id
));
354 * Returns the number of identifiers with data in storage.
356 * ☡ This number may be smaller than the actual number of used
357 * identifiers, as deleted identifiers are ⹐not⹑ freed up for re·use.
360 return this.#store
.size
;
363 /** Yields successive instances constructed from data in storage. */
365 for (const [id
, data
] of this.#store
.entries()) {
366 // Iterate over the entries, construct instances, and yield them.
367 yield this.#construct(data
, id
);
371 /** Yields successive identifier~instance pairs from storage. */
372 *[Symbol
.iterator
]() {
373 for (const [id
, data
] of this.#store
.entries()) {
374 // Iterate over the entries, construct instances, and yield them
375 // with their identifiers.
376 yield [id
, this.#construct(data
, id
)];
This page took 0.074307 seconds and 5 git commands to generate.