]> Lady’s Gitweb - Etiquette/blob - memory.js
Apply some reformatting and make Reuse‐compliant
[Etiquette] / memory.js
1 // SPDX-FileCopyrightText: 2023, 2025 Lady <https://www.ladys.computer/about/#lady>
2 // SPDX-License-Identifier: MPL-2.0
3 /**
4 * ⁌ 📧🏷️ Étiquette ∷ memory.js
5 *
6 * Copyright © 2023, 2025 Lady [@ Ladys Computer].
7 *
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/>.
11 */
12
13 import { wrmgBase32Binary, wrmgBase32String } from "./deps.js";
14
15 const ÉTIQUETTE = "📧🏷️ Étiquette";
16
17 /**
18 * A symbol which is used internally to identify the constructor
19 * associated with a stored object.
20 *
21 * ※ This value is not exposed.
22 */
23 const constructorSymbol = Symbol("constructor");
24
25 /**
26 * Mints a new W·R·M·G base32 identifier with a prefixed checksum.
27 *
28 * If an argument is provided, it is used as the underlying numeric
29 * value for the identifier.
30 *
31 * The return value is a `String´ ⹐object⹑ with a `.value´ own property
32 * giving the underlying numeric value for the string.
33 *
34 * ※ This function is not exposed.
35 */
36 const mintID = ($ = null) => {
37 const { number, buffer } = (() => {
38 if ($ == null) {
39 // No value was provided; generate a random one and store its
40 // buffer.
41 const values = crypto.getRandomValues(new Uint32Array(1));
42 const { buffer } = values;
43 return {
44 number: new DataView(buffer).getUint32(0) >>> 2,
45 buffer,
46 };
47 } else {
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 };
53 }
54 })();
55 const checksum = number % 37;
56 const wrmg = wrmgBase32String(buffer);
57 return Object.assign(
58 new String(
59 `${"0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U"[checksum]}${
60 wrmg.substring(0, 2)
61 }-${wrmg.substring(2, 6)}`,
62 ),
63 { value: number },
64 );
65 };
66
67 /**
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.
70 *
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.
73 *
74 * ※ This function is not exposed.
75 */
76 const normalizeID = ($) => {
77 const identifier = `${$}`;
78 const checksum = "0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U".indexOf(
79 identifier[0].toUpperCase(),
80 );
81 if (checksum == -1) {
82 // ☡ The checksum character is invalid.
83 throw new RangeError(`Invalid checksum: "${identifier[0]}".`);
84 } else {
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.
90 throw new RangeError(
91 `${ÉTIQUETTE}: Expected id to fit within 4 bytes, but got ${byteLength}.`,
92 );
93 } else {
94 // The identifier was correctly‐sized.
95 const value = new DataView(binary).getUint32(0, false);
96 if (value & 0b11) {
97 // ☡ The final two bits, which should have been padded with
98 // zeroes, have a nonzero value.
99 //
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`,
104 );
105 } else {
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})`,
112 );
113 } else {
114 // The checksum matches. Mint a new identifier with the same
115 // number to normalize its form.
116 return `${mintID(number)}`;
117 }
118 }
119 }
120 }
121 };
122
123 /**
124 * A symbol which is used to identify the method for constructing a new
125 * instance of a constructor based on stored data.
126 *
127 * ※ This value is exposed as `Storage.toInstance´.
128 */
129 const toInstanceSymbol = Symbol("Storage.toInstance");
130
131 /**
132 * A symbol which is used to identify the method for converting an
133 * instance into an object of enumerable own properties suitable for
134 * persistence.
135 *
136 * ※ This value is exposed as `Storage.toObject´.
137 */
138 const toObjectSymbol = Symbol("Storage.toObject");
139
140 /**
141 * An in‐memory store of items with checksum‐prefixed 6‐digit W·R·M·G
142 * base32 identifiers.
143 */
144 export class Storage {
145 static {
146 // Define `Storage.toInstance´ and `Storage.toObject´ as
147 // nonconfigurable, non·enumerable, read·only properties with the
148 // appropriate values.
149 Object.defineProperties(this, {
150 toInstance: {
151 configurable: false,
152 enumerable: false,
153 value: toInstanceSymbol,
154 writable: false,
155 },
156 toObject: {
157 configurable: false,
158 enumerable: false,
159 value: toObjectSymbol,
160 writable: false,
161 },
162 });
163 }
164
165 /**
166 * A `Set´ of deleted identifiers, to ensure they are not
167 * re·assigned.
168 *
169 * The identifier `000-0000´ is deleted from the start and can only
170 * be manually set.
171 */
172 #deleted = new Set([`${mintID(0)}`]);
173
174 /** The `Map´ used to actually store the data internally. */
175 #store = new Map();
176
177 /**
178 * Returns a new instance constructed from the provided data as
179 * retrieved from the provided identifier.
180 */
181 #construct(data, id) {
182 const {
183 [constructorSymbol]: constructor,
184 ...object
185 } = data;
186 if (!(toInstanceSymbol in constructor)) {
187 // ☡ There is no method on the constructor for generating an
188 // instance.
189 throw new TypeError(
190 `${ÉTIQUETTE}: Constructor must implement Storage.toInstance for object to be retrieved.`,
191 );
192 } else {
193 // Generate an instance and return it.
194 return constructor[toInstanceSymbol](object, id);
195 }
196 }
197
198 /**
199 * Stores the provided instance and returns its identifier.
200 *
201 * If a second argument is given, that identifier will be used;
202 * otherwise, an unused one will be allocated.
203 */
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
212 // object to store.
213 throw new TypeError(
214 `${ÉTIQUETTE}: Object must implement Storage.toObject to be stored.`,
215 );
216 } else {
217 // The provided value has a method for generating a storage
218 // object.
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.
225 let id = mintID();
226 let literalValue = `${id}`;
227 while (
228 deleted.has(literalValue) || store.has(literalValue)
229 ) {
230 // Generate successive identifiers until an available one
231 // is reached.
232 //
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}`;
238 }
239 return literalValue;
240 } else {
241 // ☡ There are no identifiers left to assign.
242 //
243 // ※ This is unlikely to ever be possible in practice.
244 throw new TypeError("Out of room.");
245 }
246 })();
247 store.set(
248 id,
249 Object.assign(
250 Object.create(null, {
251 [constructorSymbol]: {
252 configurable: false,
253 enumerable: false,
254 value: constructor,
255 writable: false,
256 },
257 }),
258 object,
259 ),
260 );
261 return id;
262 }
263 }
264
265 /**
266 * Stores the provided object with a new identifier, and returns the
267 * assigned identifier.
268 */
269 add(instance) {
270 return this.#persist(instance);
271 }
272
273 /**
274 * Removes the data at the provided identifier.
275 *
276 * The identifier will not be re·assigned.
277 */
278 delete(id) {
279 const store = this.#store;
280 const normalized = normalizeID(id);
281 this.#deleted.add(normalized);
282 return store.delete(normalized);
283 }
284
285 /** Yields successive identifier~instance pairs from storage. */
286 *entries() {
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)];
291 }
292 }
293
294 /**
295 * Call the provided callback function with successive instances
296 * constructed from data in storage.
297 *
298 * The callback function will be called with the constructed
299 * instance, its identifier, and this `Storage´ instance.
300 *
301 * If a second argument is provided, it will be used as the `this´
302 * value.
303 */
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);
309 }
310 }
311
312 /**
313 * Returns an instance constructed from the data stored at the
314 * provided identifier, or `null´ if the identifier has no data.
315 */
316 get(id) {
317 const store = this.#store;
318 const normalized = normalizeID(id);
319 const data = store.get(normalized);
320 if (data == null) {
321 // No object was at the provided identifier.
322 return null;
323 } else {
324 // The provided identifier had a stored object; return the
325 // constructed instance.
326 return this.#construct(data, normalized);
327 }
328 }
329
330 /**
331 * Returns whether the provided identifier currently has data
332 * associated with it.
333 */
334 has(id) {
335 const store = this.#store;
336 return store.has(normalizeID(id));
337 }
338
339 /** Yields successive identifiers with data in storage. */
340 *keys() {
341 yield* this.#store.keys();
342 }
343
344 /**
345 * Sets the data for the provided identifier to be that generated
346 * from the provided instance, then returns this `Storage´ object.
347 */
348 set(id, instance) {
349 this.#persist(instance, normalizeID(id));
350 return this;
351 }
352
353 /**
354 * Returns the number of identifiers with data in storage.
355 *
356 * ☡ This number may be smaller than the actual number of used
357 * identifiers, as deleted identifiers are ⹐not⹑ freed up for re·use.
358 */
359 get size() {
360 return this.#store.size;
361 }
362
363 /** Yields successive instances constructed from data in storage. */
364 *values() {
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);
368 }
369 }
370
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)];
377 }
378 }
379 }
This page took 0.074307 seconds and 5 git commands to generate.