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