e617981e59
Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
85 lines
2.2 KiB
TypeScript
85 lines
2.2 KiB
TypeScript
// Copyright 2024 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import type { RequireAtLeastOne } from 'type-fest';
|
|
|
|
export type MapKey<T extends Map<unknown, unknown>> =
|
|
T extends Map<infer Key, unknown> ? Key : never;
|
|
|
|
export type MapValue<T extends Map<unknown, unknown>> =
|
|
T extends Map<unknown, infer Value> ? Value : never;
|
|
|
|
export type MapEmplaceOptions<T extends Map<unknown, unknown>> =
|
|
RequireAtLeastOne<{
|
|
insert?: (key: MapKey<T>, map: T) => MapValue<T>;
|
|
update?: (existing: MapValue<T>, key: MapKey<T>, map: T) => MapValue<T>;
|
|
}>;
|
|
|
|
/**
|
|
* Lightweight polyfill of the `Map.prototype.emplace` TC39 Proposal:
|
|
* @see https://github.com/tc39/proposal-upsert
|
|
*
|
|
* @throws If no `insert()` is provided and key is not present
|
|
*
|
|
* @example Get or Insert:
|
|
* ```ts
|
|
* let pagesByBook = new Map<BookId, Map<PageId, Page>>()
|
|
* for (let page of pages) {
|
|
* let bookPages = mapEmplace(pagesByBook, page.bookId, {
|
|
* insert: () => new Map(),
|
|
* })
|
|
* bookPages.set(page.id, page)
|
|
* }
|
|
* ```
|
|
*
|
|
* @example Get+Update or Insert:
|
|
* ```ts
|
|
* let unreadPages = new Map<BookId, number>()
|
|
* for (let page of pages) {
|
|
* if (page.readAt == null) {
|
|
* mapEmplace(unreadPages, page.bookId, {
|
|
* insert: () => 1,
|
|
* update: (prev) => prev + 1,
|
|
* })
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @example Get+Update or Throw
|
|
* ```ts
|
|
* let PagesCache = new Map<PageId, Page>()
|
|
*
|
|
* function onPageReadEvent(pageId: PageId, readAt: number) {
|
|
* if (PagesCache.has(pageId)) {
|
|
* mapEmplace(PagesCache, pageId, {
|
|
* update(page) {
|
|
* return { ...page, readAt }
|
|
* },
|
|
* })
|
|
* } else {
|
|
* // save for later
|
|
* onEarlyPageReadEvent(pageId, readAt)
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
export function mapEmplace<T extends Map<unknown, unknown>>(
|
|
map: T,
|
|
key: MapKey<T>,
|
|
options: MapEmplaceOptions<T>
|
|
): MapValue<T> {
|
|
if (map.has(key)) {
|
|
let value = map.get(key) as MapValue<T>;
|
|
if (options.update != null) {
|
|
value = options.update(value, key, map);
|
|
map.set(key, value);
|
|
}
|
|
return value;
|
|
}
|
|
if (options.insert != null) {
|
|
const value = options.insert(key, map);
|
|
map.set(key, value);
|
|
return value;
|
|
}
|
|
throw new Error('Key was not present in map, and insert() was not provided');
|
|
}
|