libp2r2p 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/AGENTS.md +20 -0
  2. package/README.md +129 -0
  3. package/base16/index.js +14 -0
  4. package/base64/index.js +29 -0
  5. package/content-key/event/index.js +85 -0
  6. package/content-key/index.js +1 -0
  7. package/content-key/services/iykc-proof.js +129 -0
  8. package/double-dh/index.js +166 -0
  9. package/ecdh/index.js +8 -0
  10. package/idb/index.js +64 -0
  11. package/idb-queue/index.js +793 -0
  12. package/index.js +21 -0
  13. package/key/index.js +139 -0
  14. package/network/index.js +68 -0
  15. package/nip44-v3/index.js +175 -0
  16. package/nip46/constants/index.js +5 -0
  17. package/nip46/helpers/frame.js +51 -0
  18. package/nip46/helpers/url.js +96 -0
  19. package/nip46/index.js +5 -0
  20. package/nip46/services/bunker-signer.js +49 -0
  21. package/nip46/services/client.js +252 -0
  22. package/nip46/services/server-session.js +225 -0
  23. package/nip46/services/transport.js +265 -0
  24. package/package.json +49 -0
  25. package/private-channel/constants/index.js +5 -0
  26. package/private-channel/helpers/chunk-size.js +61 -0
  27. package/private-channel/helpers/chunks.js +282 -0
  28. package/private-channel/helpers/event.js +68 -0
  29. package/private-channel/index.js +942 -0
  30. package/private-channel/services/received-chunks.js +398 -0
  31. package/private-message/index.js +672 -0
  32. package/private-messenger/constants/index.js +1 -0
  33. package/private-messenger/index.js +1431 -0
  34. package/private-messenger/recovery/index.js +316 -0
  35. package/relay/constants/index.js +18 -0
  36. package/relay/helpers/hll.js +62 -0
  37. package/relay/helpers/publish.js +100 -0
  38. package/relay/helpers/routing.js +36 -0
  39. package/relay/helpers/timer.js +4 -0
  40. package/relay/index.js +4 -0
  41. package/relay/services/query.js +224 -0
  42. package/relay/services/relay-connection.js +156 -0
  43. package/relay/services/relay-pool.js +908 -0
  44. package/temporary-storage/index.js +85 -0
  45. package/tests/base16.test.js +19 -0
  46. package/tests/base64.test.js +18 -0
  47. package/tests/content-key-event.test.js +48 -0
  48. package/tests/fixtures/nip44v3-vectors.json +1 -0
  49. package/tests/helpers/test-signer.js +185 -0
  50. package/tests/idb-queue.test.js +153 -0
  51. package/tests/idb.test.js +91 -0
  52. package/tests/nip44-v3.test.js +73 -0
  53. package/tests/nip46.test.js +668 -0
  54. package/tests/private-channel.test.js +1899 -0
  55. package/tests/private-message.test.js +460 -0
  56. package/tests/private-messenger.test.js +1715 -0
  57. package/tests/queries.test.js +101 -0
  58. package/tests/queue-parity.test.js +105 -0
  59. package/tests/relay-hll.test.js +32 -0
  60. package/tests/relay-pool.test.js +2067 -0
  61. package/tests/temporary-storage.test.js +89 -0
  62. package/tests/web-storage-queue.test.js +480 -0
  63. package/web-storage-queue/index.js +652 -0
@@ -0,0 +1,652 @@
1
+ const encoder = new TextEncoder()
2
+ const DEFAULT_EVICTION_HEADROOM_RATIO = 0.1
3
+ const MAX_EVICTION_HEADROOM_BYTES = 64 * 1024 // 64 KiB
4
+
5
+ export function createQueue ({
6
+ prefix,
7
+ storageArea = globalThis.localStorage,
8
+ maxBytes,
9
+ evictionPolicy = 'opposite-end' // 'opposite-end' = push evicts from head, unshift evicts from tail
10
+ } = {}) {
11
+ if (!prefix) throw new Error('QUEUE_PREFIX_REQUIRED')
12
+ const stateKey = `${prefix}:queue`
13
+ const operationKey = `${prefix}:queue:operation`
14
+ const itemPrefix = `${prefix}:queue:item:`
15
+ const waiters = new Set()
16
+ const configuredMaxBytes = Number.isSafeInteger(maxBytes) && maxBytes > 0 ? maxBytes : Infinity
17
+ const configuredEvictionPolicy = normalizeEvictionPolicy(evictionPolicy)
18
+ let sessionMaxBytes = configuredMaxBytes
19
+
20
+ function storage () {
21
+ return storageArea
22
+ }
23
+
24
+ function itemKey (id) {
25
+ return `${itemPrefix}${id}`
26
+ }
27
+
28
+ function normalizeState (state) {
29
+ const head = Number.isSafeInteger(state.head) ? state.head : 0
30
+ const tail = Number.isSafeInteger(state.tail) && state.tail >= head ? state.tail : head
31
+ const usedBytes = Number.isSafeInteger(state.usedBytes) && state.usedBytes >= 0 ? state.usedBytes : 0
32
+ return { head, tail, usedBytes }
33
+ }
34
+
35
+ function byteLength (value) {
36
+ return encoder.encode(String(value)).length
37
+ }
38
+
39
+ function hasByteLimit () {
40
+ return Number.isFinite(sessionMaxBytes)
41
+ }
42
+
43
+ function normalizeEvictionPolicy (policy) {
44
+ if (policy === 'opposite-end' || policy === undefined || policy === null) return 'opposite-end'
45
+ if (policy === 'fifo' || policy === 'head') return 'head'
46
+ if (policy === 'lifo' || policy === 'tail') return 'tail'
47
+ throw new Error('QUEUE_INVALID_EVICTION_POLICY')
48
+ }
49
+
50
+ function evictionDirectionFor (operation, { index = 0, length = 0 } = {}) {
51
+ if (configuredEvictionPolicy === 'head') return 'head'
52
+ if (configuredEvictionPolicy === 'tail') return 'tail'
53
+ if (operation === 'unshift') return 'tail'
54
+ if (operation === 'setAt' || operation === 'insertAt') {
55
+ return index <= length / 2 ? 'tail' : 'head'
56
+ }
57
+ return 'head'
58
+ }
59
+
60
+ function evictionHeadroomBytes () {
61
+ if (!hasByteLimit()) return 0
62
+ // Evict a little below the limit so the next write has room for estimate overhead.
63
+ return Math.min(Math.max(1, Math.floor(sessionMaxBytes * DEFAULT_EVICTION_HEADROOM_RATIO)), MAX_EVICTION_HEADROOM_BYTES)
64
+ }
65
+
66
+ function targetBytesAfterWrite (requiredBytes) {
67
+ if (!hasByteLimit()) return Infinity
68
+ return Math.max(requiredBytes, sessionMaxBytes - evictionHeadroomBytes())
69
+ }
70
+
71
+ function isQuotaExceeded (err) {
72
+ return err?.name === 'QuotaExceededError' ||
73
+ err?.name === 'NS_ERROR_DOM_QUOTA_REACHED' ||
74
+ err?.code === 22 ||
75
+ err?.code === 1014 ||
76
+ /quota/i.test(err?.message || '')
77
+ }
78
+
79
+ function lowerSessionMaxBytes (requiredBytes) {
80
+ if (!hasByteLimit()) return
81
+ const next = Math.max(requiredBytes, Math.floor(sessionMaxBytes * 0.8))
82
+ if (next < sessionMaxBytes) sessionMaxBytes = next
83
+ }
84
+
85
+ function recoverHead (state) {
86
+ // Reconcile head reservations and left-behind items after interrupted writes.
87
+ let recovered = state
88
+ while (storage().getItem(itemKey(recovered.head - 1))) {
89
+ recovered = { ...recovered, head: recovered.head - 1 }
90
+ }
91
+ while (recovered.head < recovered.tail && !storage().getItem(itemKey(recovered.head))) {
92
+ recovered = { ...recovered, head: recovered.head + 1 }
93
+ }
94
+ if (recovered.head !== state.head || recovered.tail !== state.tail) writeState(recovered)
95
+ return recovered
96
+ }
97
+
98
+ function recoverTail (state) {
99
+ // Reconcile tail reservations and left-behind items after interrupted writes.
100
+ let recovered = state
101
+ while (storage().getItem(itemKey(recovered.tail))) {
102
+ recovered = { ...recovered, tail: recovered.tail + 1 }
103
+ }
104
+ while (recovered.tail > recovered.head && !storage().getItem(itemKey(recovered.tail - 1))) {
105
+ recovered = { ...recovered, tail: recovered.tail - 1 }
106
+ }
107
+ if (recovered.head !== state.head || recovered.tail !== state.tail) writeState(recovered)
108
+ return recovered
109
+ }
110
+
111
+ function readStoredItemFromRaw (raw) {
112
+ if (!raw) return null
113
+ // Invalid or pre-envelope rows are holes, never caller payloads.
114
+ try {
115
+ const parsed = JSON.parse(raw)
116
+ if (parsed?.i && Number.isSafeInteger(parsed.b)) return { item: parsed.i, byteSize: parsed.b }
117
+ return { item: null, byteSize: byteLength(raw) }
118
+ } catch {
119
+ return { item: null, byteSize: byteLength(raw) }
120
+ }
121
+ }
122
+
123
+ function readStoredItem (id) {
124
+ return readStoredItemFromRaw(storage().getItem(itemKey(id)))
125
+ }
126
+
127
+ function recoverUsage (state) {
128
+ // Rebuild the cached total because item and state writes are separate.
129
+ let usedBytes = 0
130
+ for (let id = state.head; id < state.tail; id++) {
131
+ const stored = readStoredItem(id)
132
+ if (stored) usedBytes += stored.byteSize
133
+ }
134
+ const recovered = { ...state, usedBytes }
135
+ if (usedBytes !== state.usedBytes) writeState(recovered)
136
+ return recovered
137
+ }
138
+
139
+ function protectedIdsFrom (options) {
140
+ if (!options?.protectedIds) return new Set()
141
+ if (options.protectedIds instanceof Set) return options.protectedIds
142
+ return new Set(options.protectedIds)
143
+ }
144
+
145
+ function trimHead (state, protectedIds = new Set()) {
146
+ while (
147
+ state.head < state.tail &&
148
+ !protectedIds.has(state.head) &&
149
+ !storage().getItem(itemKey(state.head))
150
+ ) {
151
+ state.head++
152
+ }
153
+ }
154
+
155
+ function trimTail (state, protectedIds = new Set()) {
156
+ while (
157
+ state.tail > state.head &&
158
+ !protectedIds.has(state.tail - 1) &&
159
+ !storage().getItem(itemKey(state.tail - 1))
160
+ ) {
161
+ state.tail--
162
+ }
163
+ }
164
+
165
+ function evictOneFromHead (state, options = {}) {
166
+ // Keep a pending positional-write slot out of eviction while freeing capacity.
167
+ const protectedIds = protectedIdsFrom(options)
168
+ trimHead(state, protectedIds)
169
+ for (let id = state.head; id < state.tail; id++) {
170
+ if (protectedIds.has(id)) continue
171
+ const key = itemKey(id)
172
+ const stored = readStoredItem(id)
173
+ if (!stored) continue
174
+ storage().removeItem(key)
175
+ state.usedBytes = Math.max(0, state.usedBytes - stored.byteSize)
176
+ if (id === state.head) trimHead(state, protectedIds)
177
+ writeState(state)
178
+ return true
179
+ }
180
+ return false
181
+ }
182
+
183
+ function evictOneFromTail (state, options = {}) {
184
+ // Keep a pending positional-write slot out of eviction while freeing capacity.
185
+ const protectedIds = protectedIdsFrom(options)
186
+ trimTail(state, protectedIds)
187
+ for (let id = state.tail - 1; id >= state.head; id--) {
188
+ if (protectedIds.has(id)) continue
189
+ const key = itemKey(id)
190
+ const stored = readStoredItem(id)
191
+ if (!stored) continue
192
+ storage().removeItem(key)
193
+ state.usedBytes = Math.max(0, state.usedBytes - stored.byteSize)
194
+ if (id === state.tail - 1) trimTail(state, protectedIds)
195
+ writeState(state)
196
+ return true
197
+ }
198
+ return false
199
+ }
200
+
201
+ function evictToBytes (state, targetBytes, options = {}) {
202
+ if (!hasByteLimit()) return state
203
+ const { direction = 'head' } = options
204
+ const evictOne = direction === 'tail' ? evictOneFromTail : evictOneFromHead
205
+ while (state.usedBytes > targetBytes) {
206
+ if (!evictOne(state, options)) break
207
+ }
208
+ return state
209
+ }
210
+
211
+ function evictToFit (state, requiredBytes, options = {}) {
212
+ if (!hasByteLimit()) return state
213
+ const { direction = 'head' } = options
214
+ if (requiredBytes > sessionMaxBytes) throw new Error('QUEUE_ITEM_TOO_LARGE')
215
+ // Make room for the write and retain eviction headroom for the next one.
216
+ const targetBytes = targetBytesAfterWrite(requiredBytes)
217
+ while (state.usedBytes + requiredBytes > targetBytes) {
218
+ const evicted = direction === 'tail'
219
+ ? evictOneFromTail(state, options)
220
+ : evictOneFromHead(state, options)
221
+ if (!evicted) break
222
+ }
223
+ if (state.usedBytes + requiredBytes > sessionMaxBytes) throw new Error('QUEUE_CAPACITY_EXCEEDED')
224
+ return state
225
+ }
226
+
227
+ function recoverByteLimit (state) {
228
+ if (!hasByteLimit()) return state
229
+ return evictToBytes(state, Math.min(sessionMaxBytes, targetBytesAfterWrite(0)), { direction: evictionDirectionFor('recover') })
230
+ }
231
+
232
+ function readState () {
233
+ // localStorage has no transactions, so repair durable invariants before use.
234
+ let parsed = {}
235
+ try {
236
+ parsed = JSON.parse(storage().getItem(stateKey) || '{}')
237
+ } catch {
238
+ parsed = {}
239
+ }
240
+ return recoverByteLimit(recoverUsage(recoverTail(recoverHead(recoverOperation(normalizeState(parsed))))))
241
+ }
242
+
243
+ function writeState (state, { keepEmpty = false } = {}) {
244
+ if (!keepEmpty && state.head >= state.tail) storage().removeItem(stateKey)
245
+ else storage().setItem(stateKey, JSON.stringify(state))
246
+ }
247
+
248
+ function lengthFromState (state) {
249
+ return Math.max(0, state.tail - state.head)
250
+ }
251
+
252
+ function assertIndex (index, length, { allowEnd = false } = {}) {
253
+ const max = allowEnd ? length : length - 1
254
+ if (!Number.isSafeInteger(index) || index < 0 || index > max) throw new Error('QUEUE_INDEX_OUT_OF_RANGE')
255
+ }
256
+
257
+ function itemForStorage (item) {
258
+ // The storage key controls queue order; this compact envelope caches size.
259
+ const storedItem = { ...item }
260
+ let byteSize = 0
261
+ let raw = ''
262
+ while (true) {
263
+ raw = JSON.stringify({ b: byteSize, i: storedItem })
264
+ const nextByteSize = byteLength(raw)
265
+ if (nextByteSize === byteSize) break
266
+ byteSize = nextByteSize
267
+ }
268
+ return { raw, byteSize, item: storedItem }
269
+ }
270
+
271
+ function setItemRaw (key, raw, state, requiredBytes, options) {
272
+ try {
273
+ storage().setItem(key, raw)
274
+ } catch (err) {
275
+ if (!isQuotaExceeded(err) || !hasByteLimit()) throw err
276
+ // Actual browser quota can be lower than the configured logical budget.
277
+ lowerSessionMaxBytes(requiredBytes)
278
+ if (options.evict === false) throw err
279
+ evictToFit(state, requiredBytes, options)
280
+ storage().setItem(key, raw)
281
+ }
282
+ }
283
+
284
+ function writeItem (id, item, state, options = {}) {
285
+ const previous = readStoredItem(id)
286
+ const stored = itemForStorage(item)
287
+ const previousByteSize = previous?.byteSize || 0
288
+ const delta = stored.byteSize - previousByteSize
289
+ if (hasByteLimit() && stored.byteSize > sessionMaxBytes) throw new Error('QUEUE_ITEM_TOO_LARGE')
290
+ if (delta > 0) {
291
+ if (options.evict === false) {
292
+ if (hasByteLimit() && state.usedBytes + delta > sessionMaxBytes) throw new Error('QUEUE_CAPACITY_EXCEEDED')
293
+ } else {
294
+ evictToFit(state, delta, options)
295
+ }
296
+ }
297
+ setItemRaw(itemKey(id), stored.raw, state, Math.max(delta, stored.byteSize), options)
298
+ state.usedBytes = Math.max(0, state.usedBytes - previousByteSize + stored.byteSize)
299
+ writeState(state, { keepEmpty: true })
300
+ }
301
+
302
+ function moveItem (from, to) {
303
+ const raw = storage().getItem(itemKey(from))
304
+ if (!raw) return
305
+ storage().setItem(itemKey(to), raw)
306
+ storage().removeItem(itemKey(from))
307
+ }
308
+
309
+ function readItem (id) {
310
+ return readStoredItem(id)?.item || null
311
+ }
312
+
313
+ function readOperation () {
314
+ try {
315
+ return JSON.parse(storage().getItem(operationKey) || 'null')
316
+ } catch {
317
+ storage().removeItem(operationKey)
318
+ return null
319
+ }
320
+ }
321
+
322
+ function writeOperation (operation) {
323
+ storage().setItem(operationKey, JSON.stringify(operation))
324
+ }
325
+
326
+ function clearOperation () {
327
+ storage().removeItem(operationKey)
328
+ }
329
+
330
+ function normalizedOperation (operation) {
331
+ if (!operation || (operation.type !== 'insert' && operation.type !== 'remove')) return null
332
+ if (!Number.isSafeInteger(operation.head) || !Number.isSafeInteger(operation.tail) || operation.tail < operation.head) return null
333
+ if (!Number.isSafeInteger(operation.slot) || !Number.isSafeInteger(operation.cursor)) return null
334
+ return operation
335
+ }
336
+
337
+ function recoverOperation (state) {
338
+ // Positional moves span keys, so resume their journal before using the queue.
339
+ const operation = normalizedOperation(readOperation())
340
+ if (!operation) {
341
+ clearOperation()
342
+ return state
343
+ }
344
+
345
+ if (operation.type === 'insert') return finishInsert(operation)
346
+ return finishRemove(operation)
347
+ }
348
+
349
+ function finishInsert (operation) {
350
+ let current = operation
351
+ const state = { head: current.head, tail: current.tail, usedBytes: current.usedBytes || 0 }
352
+ writeState(state)
353
+
354
+ while (current.cursor > current.slot) {
355
+ // Persist progress after each move so a reload can resume at this cursor.
356
+ moveItem(current.cursor - 1, current.cursor)
357
+ current = { ...current, cursor: current.cursor - 1 }
358
+ writeOperation(current)
359
+ }
360
+
361
+ writeItem(current.slot, current.item, state, { evict: false })
362
+ clearOperation()
363
+ writeState(state)
364
+ return state
365
+ }
366
+
367
+ function finishRemove (operation) {
368
+ let current = operation
369
+ const state = { head: current.head, tail: current.tail, usedBytes: current.usedBytes || 0 }
370
+ writeState(state, { keepEmpty: true })
371
+
372
+ while (current.cursor < current.tail) {
373
+ // Persist progress after each move so a reload can resume at this cursor.
374
+ moveItem(current.cursor + 1, current.cursor)
375
+ current = { ...current, cursor: current.cursor + 1 }
376
+ writeOperation(current)
377
+ }
378
+
379
+ storage().removeItem(itemKey(current.tail))
380
+ clearOperation()
381
+ writeState(state)
382
+ return state
383
+ }
384
+
385
+ function wake () {
386
+ for (const resolve of waiters) resolve()
387
+ waiters.clear()
388
+ }
389
+
390
+ function push (item) {
391
+ const state = readState()
392
+ const direction = evictionDirectionFor('push')
393
+ let id = state.tail
394
+ let stored = itemForStorage(item)
395
+ if (hasByteLimit() && stored.byteSize > sessionMaxBytes) throw new Error('QUEUE_ITEM_TOO_LARGE')
396
+ if (direction === 'tail') {
397
+ evictToFit(state, stored.byteSize, { direction })
398
+ id = state.tail
399
+ stored = itemForStorage(item)
400
+ if (hasByteLimit() && stored.byteSize > sessionMaxBytes) throw new Error('QUEUE_ITEM_TOO_LARGE')
401
+ }
402
+ // Reserve the tail first; recoverTail trims it if writing the item fails.
403
+ state.tail = id + 1
404
+ writeState(state)
405
+ writeItem(id, item, state, {
406
+ direction,
407
+ protectedIds: new Set([id])
408
+ })
409
+ wake()
410
+ return lengthFromState(state)
411
+ }
412
+
413
+ function unshift (item) {
414
+ const state = readState()
415
+ const direction = evictionDirectionFor('unshift')
416
+ let id = state.head - 1
417
+ let stored = itemForStorage(item)
418
+ if (hasByteLimit() && stored.byteSize > sessionMaxBytes) throw new Error('QUEUE_ITEM_TOO_LARGE')
419
+ if (direction === 'head') {
420
+ evictToFit(state, stored.byteSize, { direction })
421
+ id = state.head - 1
422
+ stored = itemForStorage(item)
423
+ if (hasByteLimit() && stored.byteSize > sessionMaxBytes) throw new Error('QUEUE_ITEM_TOO_LARGE')
424
+ }
425
+ // Reserve the head first; recoverHead trims it if writing the item fails.
426
+ state.head = id
427
+ writeState(state)
428
+ writeItem(id, item, state, {
429
+ direction,
430
+ protectedIds: new Set([id])
431
+ })
432
+ wake()
433
+ return lengthFromState(state)
434
+ }
435
+
436
+ function shift () {
437
+ const state = readState()
438
+ while (state.head < state.tail) {
439
+ const key = itemKey(state.head)
440
+ const stored = readStoredItem(state.head)
441
+ // Advance first; recoverHead restores the item if its deletion fails.
442
+ state.head++
443
+ writeState(state, { keepEmpty: true })
444
+ storage().removeItem(key)
445
+ if (stored) state.usedBytes = Math.max(0, state.usedBytes - stored.byteSize)
446
+ writeState(state)
447
+ if (!stored?.item) continue
448
+ return stored.item
449
+ }
450
+ return null
451
+ }
452
+
453
+ function pop () {
454
+ const state = readState()
455
+ while (state.tail > state.head) {
456
+ const id = state.tail - 1
457
+ const key = itemKey(id)
458
+ const stored = readStoredItem(id)
459
+ // Retreat first; recoverTail restores the item if its deletion fails.
460
+ state.tail--
461
+ writeState(state, { keepEmpty: true })
462
+ storage().removeItem(key)
463
+ if (stored) state.usedBytes = Math.max(0, state.usedBytes - stored.byteSize)
464
+ writeState(state)
465
+ if (!stored?.item) continue
466
+ return stored.item
467
+ }
468
+ return null
469
+ }
470
+
471
+ function setAt (index, item) {
472
+ const state = readState()
473
+ const length = lengthFromState(state)
474
+ assertIndex(index, length)
475
+ const id = state.head + index
476
+ writeItem(id, item, state, {
477
+ direction: evictionDirectionFor('setAt', { index, length }),
478
+ protectedIds: new Set([id])
479
+ })
480
+ wake()
481
+ return index
482
+ }
483
+
484
+ function insertAt (index, item) {
485
+ const state = readState()
486
+ const length = lengthFromState(state)
487
+ assertIndex(index, length, { allowEnd: true })
488
+
489
+ let slot = state.head + index
490
+ let stored = itemForStorage(item)
491
+ if (hasByteLimit() && stored.byteSize > sessionMaxBytes) throw new Error('QUEUE_ITEM_TOO_LARGE')
492
+ evictToFit(state, stored.byteSize, { direction: evictionDirectionFor('insertAt', { index, length }) })
493
+ const nextLength = lengthFromState(state)
494
+ const nextIndex = Math.min(index, nextLength)
495
+ slot = state.head + nextIndex
496
+ stored = itemForStorage(item)
497
+ if (hasByteLimit() && stored.byteSize > sessionMaxBytes) throw new Error('QUEUE_ITEM_TOO_LARGE')
498
+ evictToFit(state, stored.byteSize, { direction: evictionDirectionFor('insertAt', { index: nextIndex, length: nextLength }) })
499
+ state.tail++
500
+ writeState(state)
501
+ // Store a resume cursor before shifting rows across multiple keys.
502
+ writeOperation({
503
+ type: 'insert',
504
+ head: state.head,
505
+ tail: state.tail,
506
+ usedBytes: state.usedBytes,
507
+ slot,
508
+ cursor: state.tail - 1,
509
+ item
510
+ })
511
+ finishInsert(readOperation())
512
+ wake()
513
+ return nextIndex
514
+ }
515
+
516
+ function insertWhere (predicate, item, { appendIfMissing = false } = {}) {
517
+ if (typeof predicate !== 'function') throw new Error('QUEUE_PREDICATE_REQUIRED')
518
+ const state = readState()
519
+ const length = lengthFromState(state)
520
+
521
+ for (let index = 0; index < length; index++) {
522
+ const value = readItem(state.head + index)
523
+ if (value && predicate(value, index)) return insertAt(index, item)
524
+ }
525
+
526
+ if (appendIfMissing) return insertAt(length, item)
527
+ return null
528
+ }
529
+
530
+ function removeAt (index) {
531
+ const state = readState()
532
+ const length = lengthFromState(state)
533
+ assertIndex(index, length)
534
+
535
+ const slot = state.head + index
536
+ const stored = readStoredItem(slot)
537
+ state.tail--
538
+ if (stored) state.usedBytes = Math.max(0, state.usedBytes - stored.byteSize)
539
+ writeState(state, { keepEmpty: true })
540
+ // Store a resume cursor before compacting rows across multiple keys.
541
+ writeOperation({
542
+ type: 'remove',
543
+ head: state.head,
544
+ tail: state.tail,
545
+ usedBytes: state.usedBytes,
546
+ slot,
547
+ cursor: slot
548
+ })
549
+ finishRemove(readOperation())
550
+ wake()
551
+
552
+ return stored?.item || null
553
+ }
554
+
555
+ async function * items () {
556
+ while (true) {
557
+ const item = shift()
558
+ if (item) {
559
+ yield item
560
+ } else {
561
+ await new Promise(resolve => waiters.add(resolve))
562
+ }
563
+ }
564
+ }
565
+
566
+ async function * reverseItems () {
567
+ while (true) {
568
+ const item = pop()
569
+ if (item) {
570
+ yield item
571
+ } else {
572
+ await new Promise(resolve => waiters.add(resolve))
573
+ }
574
+ }
575
+ }
576
+
577
+ async function * storedItems () {
578
+ const state = readState()
579
+ for (let id = state.head; id < state.tail; id++) {
580
+ const item = readItem(id)
581
+ if (item) yield item
582
+ }
583
+ }
584
+
585
+ async function * reverseStoredItems () {
586
+ const state = readState()
587
+ for (let id = state.tail - 1; id >= state.head; id--) {
588
+ const item = readItem(id)
589
+ if (item) yield item
590
+ }
591
+ }
592
+
593
+ function removeWhere (predicate) {
594
+ if (typeof predicate !== 'function') throw new Error('QUEUE_PREDICATE_REQUIRED')
595
+ const state = readState()
596
+ // Bulk predicate removal leaves holes on purpose; shift/pop skip them, and
597
+ // callers that need contiguous positions can use removeAt for compaction.
598
+ for (let id = state.head; id < state.tail; id++) {
599
+ const key = itemKey(id)
600
+ const stored = readStoredItem(id)
601
+ if (!stored) continue
602
+ try {
603
+ if (predicate(stored.item)) {
604
+ storage().removeItem(key)
605
+ state.usedBytes = Math.max(0, state.usedBytes - stored.byteSize)
606
+ }
607
+ } catch {
608
+ storage().removeItem(key)
609
+ state.usedBytes = Math.max(0, state.usedBytes - stored.byteSize)
610
+ }
611
+ }
612
+ writeState(state)
613
+ }
614
+
615
+ function some (predicate) {
616
+ if (typeof predicate !== 'function') throw new Error('QUEUE_PREDICATE_REQUIRED')
617
+ const state = readState()
618
+ for (let id = state.head; id < state.tail; id++) {
619
+ const item = readItem(id)
620
+ if (item && predicate(item)) return true
621
+ }
622
+ return false
623
+ }
624
+
625
+ function clear () {
626
+ const state = readState()
627
+ for (let id = state.head; id < state.tail; id++) storage().removeItem(itemKey(id))
628
+ storage().removeItem(stateKey)
629
+ storage().removeItem(operationKey)
630
+ }
631
+
632
+ readState()
633
+
634
+ return {
635
+ enqueue: push,
636
+ push,
637
+ pop,
638
+ unshift,
639
+ shift,
640
+ items,
641
+ reverseItems,
642
+ storedItems,
643
+ reverseStoredItems,
644
+ setAt,
645
+ insertAt,
646
+ insertWhere,
647
+ removeAt,
648
+ removeWhere,
649
+ some,
650
+ clear
651
+ }
652
+ }