offline-first, p2p synced, atproto enabled, feed reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 116 lines 3.1 kB view raw
1import {Semaphore} from './async/semaphore' 2 3/** extension of readonly maps that has a throwing require */ 4export interface ReadonlyStrictMap<K, V> extends ReadonlyMap<K, V> { 5 require(key: K): V 6} 7 8/** a map with methods to ensure key presence and safe update */ 9export class StrictMap<K, V> extends Map<K, V> { 10 #lock = new Semaphore(1) 11 12 /** 13 * get a value from the map, throwing if missing 14 * @throws Error if the key is not present in the map 15 */ 16 require(key: K): V { 17 if (!this.has(key)) throw Error(`key is required but not in the map`) 18 19 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 20 const value = this.get(key)! 21 return value 22 } 23 24 /** 25 * remove a value from the map, returning it if it was present. 26 * @param key - the key to delete 27 * @returns the value at the given key before removal 28 */ 29 remove(key: K): V | undefined { 30 const peer = this.get(key) 31 this.delete(key) 32 return peer 33 } 34 35 /** get a value from the map, creating it if not present */ 36 ensure(key: K, maker: () => V): V { 37 if (!this.has(key)) { 38 this.set(key, maker()) 39 } 40 41 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 42 return this.get(key)! 43 } 44 45 /** get a value from the map, creating it (async) if not present */ 46 async ensureAsync(key: K, maker: (signal?: AbortSignal) => Promise<V>, signal?: AbortSignal): Promise<V> { 47 signal?.throwIfAborted() 48 49 if (!(await this.#lock.take(signal))) { 50 throw new Error('lock take aborted') 51 } 52 53 try { 54 if (!this.has(key)) { 55 const value = await maker(signal) 56 if (!this.has(key)) { 57 this.set(key, value) 58 } else { 59 console.warn('ensureasync was out-raced!', this, key) 60 } 61 } 62 } finally { 63 this.#lock.free() 64 } 65 66 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 67 return this.get(key)! 68 } 69 70 /** update a value in the map, removing if undefined is returned */ 71 update(key: K, update: (prev?: V) => V | undefined): V | undefined { 72 const prev = this.get(key) 73 const next = update(prev) 74 75 if (next === undefined) { 76 this.delete(key) 77 } else { 78 this.set(key, next) 79 } 80 81 return this.get(key) 82 } 83 84 /** update a value in the map, removing if undefined is returned */ 85 async updateAsync( 86 key: K, 87 update: (prev?: V, signal?: AbortSignal) => Promise<V | undefined>, 88 signal?: AbortSignal, 89 ): Promise<V | undefined> { 90 signal?.throwIfAborted() 91 92 if (!(await this.#lock.take(signal))) { 93 throw new Error('lock take aborted') 94 } 95 96 try { 97 const prev = this.get(key) 98 const next = await update(prev, signal) 99 100 if (prev === this.get(key)) { 101 if (next === undefined) { 102 this.delete(key) 103 } else { 104 this.set(key, next) 105 } 106 } else { 107 // current value !== value we updated 108 console.warn('we were out raced!', this, key) 109 } 110 } finally { 111 this.#lock.free() 112 } 113 114 return this.get(key) 115 } 116}