]>
Lady’s Gitweb - Etiquette/blob - memory.js
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
} = (() => {
34 // No value was provided; generate a random one and store its
36 const values
= crypto
.getRandomValues(new Uint32Array(1));
37 const { buffer
} = values
;
39 number
: new DataView(buffer
).getUint32(0) >>> 2,
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
};
50 const checksum
= number
% 37;
51 const wrmg
= wrmgBase32String(buffer
);
54 `${"0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U"[checksum]}${
56 }-${wrmg.substring(2, 6)}`,
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.
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.
69 * ※ This function is not exposed.
71 const normalizeID
= ($) => {
72 const identifier
= `${$}`;
73 const checksum
= "0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U".indexOf(
74 identifier
[0].toUpperCase(),
77 // ☡ The checksum character is invalid.
78 throw new RangeError(`Invalid checksum: "${identifier[0]}".`);
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.
86 `Expected id to fit within 4 bytes, but got ${byteLength}.`,
89 // The identifier was correctly‐sized.
90 const value
= new DataView(binary
).getUint32(0, false);
92 // ☡ The final two bits, which should have been padded with
93 // zeroes, have a nonzero value.
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");
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})`,
107 // The checksum matches. Mint a new identifier with the same
108 // number to normalize its form.
109 return `${mintID(number)}`;
117 * A symbol which is used to identify the method for constructing a new
118 * instance of a constructor based on stored data.
120 * ※ This value is exposed as `Storage.toInstance`.
122 const toInstanceSymbol
= Symbol("Storage.toInstance");
125 * A symbol which is used to identify the method for converting an
126 * instance into an object of enumerable own properties suitable for
129 * ※ This value is exposed as `Storage.toObject`.
131 const toObjectSymbol
= Symbol("Storage.toObject");
134 * An in‐memory store of items with checksum‐prefixed 6‐digit W·R·M·G
135 * base32 identifiers.
137 export class Storage
{
139 // Define `Storage.toInstance` and `Storage.toObject` as
140 // nonconfigurable, non·enumerable, read·only properties with the
141 // appropriate values.
142 Object
.defineProperties(this, {
146 value
: toInstanceSymbol
,
152 value
: toObjectSymbol
,
159 * A `Set` of deleted identifiers, to ensure they are not
162 * The identifier `000-0000` is deleted from the start and can only
165 #deleted
= new Set([`${mintID(0)}`]);
167 /** The `Map` used to actually store the data internally. */
171 * Returns a new instance constructed from the provided data as
172 * retrieved from the provided identifier.
174 #construct(data
, id
) {
176 [constructorSymbol
]: constructor,
179 if (!(toInstanceSymbol
in constructor)) {
180 // ☡ There is no method on the constructor for generating an
183 "Constructor must implement Storage.toInstance for object to be retrieved.",
186 // Generate an instance and return it.
187 return constructor[toInstanceSymbol
](object
, id
);
192 * Stores the provided instance and returns its identifier.
194 * If a second argument is given, that identifier will be used;
195 * otherwise, an unused one will be allocated.
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
207 "Object must implement Storage.toObject to be stored.",
210 // The provided value has a method for generating a storage
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.
219 let literalValue
= `${id}`;
221 deleted
.has(literalValue
) || store
.has(literalValue
)
223 // Generate successive identifiers until an available one
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}`;
234 // ☡ There are no identifiers left to assign.
236 // ※ This is unlikely to ever be possible in practice.
237 throw new TypeError("Out of room.");
243 Object
.create(null, {
244 [constructorSymbol
]: {
259 * Stores the provided object with a new identifier, and returns the
260 * assigned identifier.
263 return this.#persist(instance
);
267 * Removes the data at the provided identifier.
269 * The identifier will not be re·assigned.
272 const store
= this.#store
;
273 const normalized
= normalizeID(id
);
274 this.#deleted
.add(normalized
);
275 return store
.delete(normalized
);
278 /** Yields successive identifier~instance pairs from storage. */
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
)];
288 * Call the provided callback function with successive instances
289 * constructed from data in storage.
291 * The callback function will be called with the constructed
292 * instance, its identifier, and this `Storage` instance.
294 * If a second argument is provided, it will be used as the `this`
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);
306 * Returns an instance constructed from the data stored at the
307 * provided identifier, or `null` if the identifier has no data.
310 const store
= this.#store
;
311 const normalized
= normalizeID(id
);
312 const data
= store
.get(normalized
);
314 // No object was at the provided identifier.
317 // The provided identifier had a stored object; return the
318 // constructed instance.
319 return this.#construct(data
, normalized
);
324 * Returns whether the provided identifier currently has data
325 * associated with it.
328 const store
= this.#store
;
329 return store
.has(normalizeID(id
));
332 /** Yields successive identifiers with data in storage. */
334 yield* this.#store
.keys();
338 * Sets the data for the provided identifier to be that generated
339 * from the provided instance, then returns this `Storage` object.
342 this.#persist(instance
, normalizeID(id
));
347 * Returns the number of identifiers with data in storage.
349 * ☡ This number may be smaller than the actual number of used
350 * identifiers, as deleted identifiers are *not* freed up for re·use.
353 return this.#store
.size
;
356 /** Yields successive instances constructed from data in storage. */
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
);
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
)];
This page took 0.082733 seconds and 5 git commands to generate.