]> Lady’s Gitweb - Etiquette/blob - memory.js
Small improvements to storage
[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 } = (() => {
33 if ($ == null) {
34 // No value was provided; generate a random one and store its
35 // buffer.
36 const values = crypto.getRandomValues(new Uint32Array(1));
37 const { buffer } = values;
38 return {
39 number: new DataView(buffer).getUint32(0) >>> 2,
40 buffer,
41 };
42 } else {
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 };
48 }
49 })();
50 const checksum = number % 37;
51 const wrmg = wrmgBase32String(buffer);
52 return Object.assign(
53 new String(
54 `${"0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U"[checksum]}${
55 wrmg.substring(0, 2)
56 }-${wrmg.substring(2, 6)}`,
57 ),
58 { value: number },
59 );
60 };
61
62 /**
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.
65 *
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.
68 *
69 * ※ This function is not exposed.
70 */
71 const normalizeID = ($) => {
72 const identifier = `${$}`;
73 const checksum = "0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U".indexOf(
74 identifier[0].toUpperCase(),
75 );
76 if (checksum == -1) {
77 // ☡ The checksum character is invalid.
78 throw new RangeError(`Invalid checksum: "${identifier[0]}".`);
79 } else {
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.
85 throw new RangeError(
86 `Expected id to fit within 4 bytes, but got ${byteLength}.`,
87 );
88 } else {
89 // The identifier was correctly‐sized.
90 const value = new DataView(binary).getUint32(0, false);
91 if (value & 0b11) {
92 // ☡ The final two bits, which should have been padded with
93 // zeroes, have a nonzero value.
94 //
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");
98 } else {
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})`,
105 );
106 } else {
107 // The checksum matches. Mint a new identifier with the same
108 // number to normalize its form.
109 return `${mintID(number)}`;
110 }
111 }
112 }
113 }
114 };
115
116 /**
117 * A symbol which is used to identify the method for constructing a new
118 * instance of a constructor based on stored data.
119 *
120 * ※ This value is exposed as `Storage.toInstance`.
121 */
122 const toInstanceSymbol = Symbol("Storage.toInstance");
123
124 /**
125 * A symbol which is used to identify the method for converting an
126 * instance into an object of enumerable own properties suitable for
127 * persistence.
128 *
129 * ※ This value is exposed as `Storage.toObject`.
130 */
131 const toObjectSymbol = Symbol("Storage.toObject");
132
133 /**
134 * An in‐memory store of items with checksum‐prefixed 6‐digit W·R·M·G
135 * base32 identifiers.
136 */
137 export class Storage {
138 static {
139 // Define `Storage.toInstance` and `Storage.toObject` as
140 // nonconfigurable, non·enumerable, read·only properties with the
141 // appropriate values.
142 Object.defineProperties(this, {
143 toInstance: {
144 configurable: false,
145 enumerable: false,
146 value: toInstanceSymbol,
147 writable: false,
148 },
149 toObject: {
150 configurable: false,
151 enumerable: false,
152 value: toObjectSymbol,
153 writable: false,
154 },
155 });
156 }
157
158 /**
159 * A `Set` of deleted identifiers, to ensure they are not
160 * re·assigned.
161 *
162 * The identifier `000-0000` is deleted from the start and can only
163 * be manually set.
164 */
165 #deleted = new Set([`${mintID(0)}`]);
166
167 /** The `Map` used to actually store the data internally. */
168 #store = new Map();
169
170 /**
171 * Returns a new instance constructed from the provided data as
172 * retrieved from the provided identifier.
173 */
174 #construct(data, id) {
175 const {
176 [constructorSymbol]: constructor,
177 ...object
178 } = data;
179 if (!(toInstanceSymbol in constructor)) {
180 // ☡ There is no method on the constructor for generating an
181 // instance.
182 throw new TypeError(
183 "Constructor must implement Storage.toInstance for object to be retrieved.",
184 );
185 } else {
186 // Generate an instance and return it.
187 return constructor[toInstanceSymbol](object, id);
188 }
189 }
190
191 /**
192 * Stores the provided instance and returns its identifier.
193 *
194 * If a second argument is given, that identifier will be used;
195 * otherwise, an unused one will be allocated.
196 */
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
205 // object to store.
206 throw new TypeError(
207 "Object must implement Storage.toObject to be stored.",
208 );
209 } else {
210 // The provided value has a method for generating a storage
211 // object.
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.
218 let id = mintID();
219 let literalValue = `${id}`;
220 while (
221 deleted.has(literalValue) || store.has(literalValue)
222 ) {
223 // Generate successive identifiers until an available one
224 // is reached.
225 //
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}`;
231 }
232 return literalValue;
233 } else {
234 // ☡ There are no identifiers left to assign.
235 //
236 // ※ This is unlikely to ever be possible in practice.
237 throw new TypeError("Out of room.");
238 }
239 })();
240 store.set(
241 id,
242 Object.assign(
243 Object.create(null, {
244 [constructorSymbol]: {
245 configurable: false,
246 enumerable: false,
247 value: constructor,
248 writable: false,
249 },
250 }),
251 object,
252 ),
253 );
254 return id;
255 }
256 }
257
258 /**
259 * Stores the provided object with a new identifier, and returns the
260 * assigned identifier.
261 */
262 add(instance) {
263 return this.#persist(instance);
264 }
265
266 /**
267 * Removes the data at the provided identifier.
268 *
269 * The identifier will not be re·assigned.
270 */
271 delete(id) {
272 const store = this.#store;
273 const normalized = normalizeID(id);
274 this.#deleted.add(normalized);
275 return store.delete(normalized);
276 }
277
278 /** Yields successive identifier~instance pairs from storage. */
279 *entries() {
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)];
284 }
285 }
286
287 /**
288 * Call the provided callback function with successive instances
289 * constructed from data in storage.
290 *
291 * The callback function will be called with the constructed
292 * instance, its identifier, and this `Storage` instance.
293 *
294 * If a second argument is provided, it will be used as the `this`
295 * value.
296 */
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);
302 }
303 }
304
305 /**
306 * Returns an instance constructed from the data stored at the
307 * provided identifier, or `null` if the identifier has no data.
308 */
309 get(id) {
310 const store = this.#store;
311 const normalized = normalizeID(id);
312 const data = store.get(normalized);
313 if (data == null) {
314 // No object was at the provided identifier.
315 return null;
316 } else {
317 // The provided identifier had a stored object; return the
318 // constructed instance.
319 return this.#construct(data, normalized);
320 }
321 }
322
323 /**
324 * Returns whether the provided identifier currently has data
325 * associated with it.
326 */
327 has(id) {
328 const store = this.#store;
329 return store.has(normalizeID(id));
330 }
331
332 /** Yields successive identifiers with data in storage. */
333 *keys() {
334 yield* this.#store.keys();
335 }
336
337 /**
338 * Sets the data for the provided identifier to be that generated
339 * from the provided instance, then returns this `Storage` object.
340 */
341 set(id, instance) {
342 this.#persist(instance, normalizeID(id));
343 return this;
344 }
345
346 /**
347 * Returns the number of identifiers with data in storage.
348 *
349 * ☡ This number may be smaller than the actual number of used
350 * identifiers, as deleted identifiers are *not* freed up for re·use.
351 */
352 get size() {
353 return this.#store.size;
354 }
355
356 /** Yields successive instances constructed from data in storage. */
357 *values() {
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);
361 }
362 }
363
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)];
370 }
371 }
372 }
This page took 0.086043 seconds and 5 git commands to generate.