@@ -168,36 +168,50 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
168168 return this . entries ( ) ;
169169 }
170170
171+ /**
172+ * Returns an iterator over all [key, value] pairs in the map.
173+ *
174+ * **Concurrency note — snapshot iteration:**
175+ * All entries are collected into a private array while a read lock is held, and the lock is
176+ * released before the first value is yielded. This "snapshot" approach is intentional:
177+ *
178+ * - Holding the lock across `yield` points would block writers on other workers for the
179+ * entire duration of the caller's iteration, including any pauses between yields (e.g.
180+ * async work in a `for...of` body).
181+ * - A generator suspended across a yield cannot reliably release the lock if it is abandoned
182+ * (e.g. the caller breaks early without exhausting the iterator), because V8 does not
183+ * guarantee that a generator's `finally` block runs at GC time.
184+ *
185+ * The trade-off is O(n) extra memory for the snapshot. If you do not need lazy iteration and
186+ * want to avoid the allocation, use `forEach()` instead — it holds the read lock for its full
187+ * duration and calls a callback for each entry without building an intermediate collection.
188+ */
171189 * entries ( ) : MapIterator < [ K , V ] > {
172- for ( let i = 0 ; i < this . buckets ; i ++ ) {
173- let dataPointer = this . indexView . getUint32 ( ShareableMap . INDEX_TABLE_OFFSET + i * ShareableMap . INT_SIZE ) ;
174- while ( dataPointer !== 0 ) {
175- const key = this . readTypedKeyFromDataObject ( dataPointer ) ;
176- const value = this . readValueFromDataObject ( dataPointer ) ;
177- yield [ key , value ] ;
178- dataPointer = this . dataView . getUint32 ( dataPointer ) ;
179- }
180- }
190+ const snapshot : [ K , V ] [ ] = [ ] ;
191+ this . forEach ( ( value , key ) => snapshot . push ( [ key , value ] ) ) ;
192+ yield * snapshot ;
181193 }
182194
195+ /**
196+ * Returns an iterator over all keys in the map.
197+ *
198+ * See `entries()` for a full explanation of the snapshot-based concurrency strategy used here.
199+ */
183200 * keys ( ) : MapIterator < K > {
184- for ( let i = 0 ; i < this . buckets ; i ++ ) {
185- let dataPointer = this . indexView . getUint32 ( ShareableMap . INDEX_TABLE_OFFSET + i * ShareableMap . INT_SIZE ) ;
186- while ( dataPointer !== 0 ) {
187- yield this . readTypedKeyFromDataObject ( dataPointer ) ;
188- dataPointer = this . dataView . getUint32 ( dataPointer ) ;
189- }
190- }
201+ const snapshot : K [ ] = [ ] ;
202+ this . forEach ( ( _ , key ) => snapshot . push ( key ) ) ;
203+ yield * snapshot ;
191204 }
192205
206+ /**
207+ * Returns an iterator over all values in the map.
208+ *
209+ * See `entries()` for a full explanation of the snapshot-based concurrency strategy used here.
210+ */
193211 * values ( ) : MapIterator < V > {
194- for ( let i = 0 ; i < this . buckets ; i ++ ) {
195- let dataPointer = this . indexView . getUint32 ( ShareableMap . INDEX_TABLE_OFFSET + i * 4 ) ;
196- while ( dataPointer !== 0 ) {
197- yield this . readValueFromDataObject ( dataPointer ) ;
198- dataPointer = this . dataView . getUint32 ( dataPointer ) ;
199- }
200- }
212+ const snapshot : V [ ] = [ ] ;
213+ this . forEach ( ( value ) => snapshot . push ( value ) ) ;
214+ yield * snapshot ;
201215 }
202216
203217 clear ( ) : void {
@@ -248,7 +262,11 @@ export class ShareableMap<K, V> extends TransferableDataStructure {
248262 } else {
249263 let previousBlock = bucketLink ;
250264 let currentBlock = this . dataView . getUint32 ( bucketLink ) ;
251- while ( this . dataView . getUint32 ( currentBlock + 16 ) !== hash ) {
265+ // Walk the chain by position, not by hash. Using the stored hash to find the
266+ // predecessor is incorrect when two distinct keys share the same FNV-1a hash
267+ // (a full 32-bit collision): the loop would stop at the first colliding node
268+ // and re-link the wrong predecessor, silently dropping an unrelated entry.
269+ while ( currentBlock !== startPos ) {
252270 previousBlock = currentBlock ;
253271 currentBlock = this . dataView . getUint32 ( currentBlock ) ;
254272 }
0 commit comments