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,89 @@
1
+ import { afterEach, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {
4
+ cleanupTemporaryStorage,
5
+ createTemporaryStorage,
6
+ getTemporaryItem,
7
+ removeTemporaryItems,
8
+ setTemporaryItem,
9
+ TEMPORARY_STORAGE_KEYS_KEY
10
+ } from '../temporary-storage/index.js'
11
+
12
+ const localData = new Map()
13
+ const sessionData = new Map()
14
+ let failOnSetKey = ''
15
+
16
+ function storageFor (data) {
17
+ return {
18
+ clear: () => data.clear(),
19
+ getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
20
+ removeItem: key => { data.delete(String(key)) },
21
+ setItem: (key, value) => {
22
+ if (key === failOnSetKey) throw new Error('set failed')
23
+ data.set(String(key), String(value))
24
+ }
25
+ }
26
+ }
27
+
28
+ globalThis.localStorage = storageFor(localData)
29
+ globalThis.sessionStorage = storageFor(sessionData)
30
+
31
+ afterEach(() => {
32
+ failOnSetKey = ''
33
+ globalThis.localStorage.clear()
34
+ globalThis.sessionStorage.clear()
35
+ })
36
+
37
+ function trackedKeys () {
38
+ const raw = globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY)
39
+ return raw ? JSON.parse(raw) : []
40
+ }
41
+
42
+ test('setTemporaryItem tracks a key before storing the value', () => {
43
+ failOnSetKey = 'tmp.fail'
44
+
45
+ assert.throws(() => setTemporaryItem('tmp.fail', 'secret'), /set failed/)
46
+
47
+ assert.deepEqual(trackedKeys(), ['tmp.fail'])
48
+ assert.equal(globalThis.sessionStorage.getItem('tmp.fail'), null)
49
+ assert.equal(globalThis.localStorage.getItem('tmp.fail'), null)
50
+ })
51
+
52
+ test('cleanupTemporaryStorage removes tracked fields and the tracker field', () => {
53
+ setTemporaryItem('tmp.one', 'a')
54
+ setTemporaryItem('tmp.two', 'b')
55
+ globalThis.sessionStorage.setItem('permanent', 'keep')
56
+
57
+ cleanupTemporaryStorage()
58
+
59
+ assert.equal(getTemporaryItem('tmp.one'), null)
60
+ assert.equal(getTemporaryItem('tmp.two'), null)
61
+ assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
62
+ assert.equal(globalThis.sessionStorage.getItem('permanent'), 'keep')
63
+ })
64
+
65
+ test('removeTemporaryItems untracks fields cleaned during normal operation', () => {
66
+ setTemporaryItem('tmp.one', 'a')
67
+ setTemporaryItem('tmp.two', 'b')
68
+
69
+ removeTemporaryItems(['tmp.one'])
70
+
71
+ assert.equal(getTemporaryItem('tmp.one'), null)
72
+ assert.equal(getTemporaryItem('tmp.two'), 'b')
73
+ assert.deepEqual(trackedKeys(), ['tmp.two'])
74
+ })
75
+
76
+ test('createTemporaryStorage isolates a caller-supplied area', () => {
77
+ const storage = createTemporaryStorage({ storageArea: globalThis.localStorage })
78
+
79
+ storage.setItem('tmp.local', 'value')
80
+
81
+ assert.equal(globalThis.localStorage.getItem('tmp.local'), 'value')
82
+ assert.equal(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), JSON.stringify(['tmp.local']))
83
+ assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
84
+
85
+ storage.cleanup()
86
+
87
+ assert.equal(globalThis.localStorage.getItem('tmp.local'), null)
88
+ assert.equal(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
89
+ })
@@ -0,0 +1,480 @@
1
+ import { afterEach, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { createQueue } from '../web-storage-queue/index.js'
4
+
5
+ const data = new Map()
6
+ let failNextItemWrite = false
7
+ let failNextItemRemove = false
8
+ globalThis.localStorage = {
9
+ clear: () => data.clear(),
10
+ getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
11
+ key: index => [...data.keys()][index] ?? null,
12
+ removeItem: key => {
13
+ if (failNextItemRemove && String(key).startsWith('test:queue:item:')) {
14
+ failNextItemRemove = false
15
+ throw new Error('ITEM_REMOVE_FAILED')
16
+ }
17
+ data.delete(String(key))
18
+ },
19
+ setItem: (key, value) => {
20
+ if (failNextItemWrite && String(key).startsWith('test:queue:item:')) {
21
+ failNextItemWrite = false
22
+ throw new Error('ITEM_WRITE_FAILED')
23
+ }
24
+ data.set(String(key), String(value))
25
+ },
26
+ get length () { return data.size }
27
+ }
28
+
29
+ afterEach(() => {
30
+ failNextItemWrite = false
31
+ failNextItemRemove = false
32
+ globalThis.localStorage.clear()
33
+ })
34
+
35
+ function setJson (key, value) {
36
+ globalThis.localStorage.setItem(key, JSON.stringify(value))
37
+ }
38
+
39
+ function setStoredItem (key, item) {
40
+ let byteSize = 0
41
+ let raw = ''
42
+ while (true) {
43
+ raw = JSON.stringify({ b: byteSize, i: item })
44
+ const nextByteSize = new TextEncoder().encode(raw).length
45
+ if (nextByteSize === byteSize) break
46
+ byteSize = nextByteSize
47
+ }
48
+ globalThis.localStorage.setItem(key, raw)
49
+ }
50
+
51
+ function assertQueueState (expected) {
52
+ const state = JSON.parse(globalThis.localStorage.getItem('test:queue'))
53
+ for (const [key, value] of Object.entries(expected)) assert.equal(state[key], value)
54
+ }
55
+
56
+ function createStorageArea () {
57
+ const store = new Map()
58
+ return {
59
+ clear: () => store.clear(),
60
+ getItem: key => store.has(String(key)) ? store.get(String(key)) : null,
61
+ removeItem: key => store.delete(String(key)),
62
+ setItem: (key, value) => store.set(String(key), String(value)),
63
+ get length () { return store.size }
64
+ }
65
+ }
66
+
67
+ function createQuotaStorageArea ({ failOnItemWrite = 1 } = {}) {
68
+ const storageArea = createStorageArea()
69
+ let itemWrites = 0
70
+ return {
71
+ ...storageArea,
72
+ setItem: (key, value) => {
73
+ if (String(key).startsWith('test:queue:item:')) itemWrites++
74
+ if (itemWrites === failOnItemWrite && String(key).startsWith('test:queue:item:')) {
75
+ const err = new Error('Quota exceeded')
76
+ err.name = 'QuotaExceededError'
77
+ throw err
78
+ }
79
+ storageArea.setItem(key, value)
80
+ }
81
+ }
82
+ }
83
+
84
+ test('queue can use an explicit storage area instead of localStorage', () => {
85
+ const storageArea = createStorageArea()
86
+ const queue = createQueue({ prefix: 'test', storageArea })
87
+
88
+ queue.push({ value: 'custom' })
89
+
90
+ assert.equal(globalThis.localStorage.getItem('test:queue'), null)
91
+ assert.notEqual(storageArea.getItem('test:queue'), null)
92
+ assert.deepEqual(queue.shift().value, 'custom')
93
+ })
94
+
95
+ test('queue trims a tail reserved before its item was persisted', () => {
96
+ setJson('test:queue', { head: 0, tail: 1 })
97
+
98
+ const queue = createQueue({ prefix: 'test' })
99
+
100
+ assert.equal(queue.shift(), null)
101
+ assert.equal(globalThis.localStorage.getItem('test:queue'), null)
102
+ })
103
+
104
+ test('queue repairs stale tail and skips missing head items', () => {
105
+ setJson('test:queue', { head: 0, tail: 2 })
106
+ setStoredItem('test:queue:item:1', { value: 'second' })
107
+
108
+ const queue = createQueue({ prefix: 'test' })
109
+
110
+ assert.deepEqual(queue.shift(), { value: 'second' })
111
+ assert.equal(queue.shift(), null)
112
+ })
113
+
114
+ test('queue drops persisted rows outside the compact envelope format', () => {
115
+ setJson('test:queue', { head: 0, tail: 1 })
116
+ setJson('test:queue:item:0', { value: 'old-direct-row' })
117
+
118
+ const queue = createQueue({ prefix: 'test' })
119
+
120
+ assert.equal(queue.shift(), null)
121
+ assert.equal(globalThis.localStorage.getItem('test:queue'), null)
122
+ })
123
+
124
+ test('queue repairs a failed enqueue that only persisted the tail reservation', () => {
125
+ const queue = createQueue({ prefix: 'test' })
126
+ failNextItemWrite = true
127
+
128
+ assert.throws(() => queue.enqueue({ value: 'failed' }), /ITEM_WRITE_FAILED/)
129
+ assertQueueState({ head: 0, tail: 1 })
130
+
131
+ const recovered = createQueue({ prefix: 'test' })
132
+
133
+ assert.equal(recovered.shift(), null)
134
+ assert.equal(globalThis.localStorage.getItem('test:queue'), null)
135
+ })
136
+
137
+ test('queue repairs a failed unshift that only persisted the head reservation', () => {
138
+ const queue = createQueue({ prefix: 'test' })
139
+ failNextItemWrite = true
140
+
141
+ assert.throws(() => queue.unshift({ value: 'failed' }), /ITEM_WRITE_FAILED/)
142
+ assertQueueState({ head: -1, tail: 0 })
143
+
144
+ const recovered = createQueue({ prefix: 'test' })
145
+
146
+ assert.equal(recovered.shift(), null)
147
+ assert.equal(globalThis.localStorage.getItem('test:queue'), null)
148
+ })
149
+
150
+ test('queue repairs a failed shift that only persisted the head advance', () => {
151
+ const queue = createQueue({ prefix: 'test' })
152
+ queue.enqueue({ value: 'first' })
153
+ failNextItemRemove = true
154
+
155
+ assert.throws(() => queue.shift(), /ITEM_REMOVE_FAILED/)
156
+ assertQueueState({ head: 1, tail: 1 })
157
+ assert.notEqual(globalThis.localStorage.getItem('test:queue:item:0'), null)
158
+
159
+ const recovered = createQueue({ prefix: 'test' })
160
+
161
+ assert.deepEqual(recovered.shift(), { value: 'first' })
162
+ assert.equal(recovered.shift(), null)
163
+ })
164
+
165
+ test('queue supports push unshift shift and pop', () => {
166
+ const queue = createQueue({ prefix: 'test' })
167
+
168
+ assert.equal(queue.push({ value: 'middle' }), 1)
169
+ assert.equal(queue.unshift({ value: 'first' }), 2)
170
+ assert.equal(queue.push({ value: 'last' }), 3)
171
+
172
+ assert.deepEqual(queue.shift().value, 'first')
173
+ assert.deepEqual(queue.pop().value, 'last')
174
+ assert.deepEqual(queue.shift().value, 'middle')
175
+ assert.equal(queue.shift(), null)
176
+ })
177
+
178
+ test('enqueue is an alias of push', () => {
179
+ const queue = createQueue({ prefix: 'test' })
180
+
181
+ queue.enqueue({ value: 'pushed' })
182
+
183
+ assert.deepEqual(queue.shift().value, 'pushed')
184
+ })
185
+
186
+ test('queue repairs a failed pop that only persisted the tail retreat', () => {
187
+ const queue = createQueue({ prefix: 'test' })
188
+ queue.push({ value: 'last' })
189
+ failNextItemRemove = true
190
+
191
+ assert.throws(() => queue.pop(), /ITEM_REMOVE_FAILED/)
192
+ assertQueueState({ head: 0, tail: 0 })
193
+ assert.notEqual(globalThis.localStorage.getItem('test:queue:item:0'), null)
194
+
195
+ const recovered = createQueue({ prefix: 'test' })
196
+
197
+ assert.deepEqual(recovered.pop(), { value: 'last' })
198
+ assert.equal(recovered.pop(), null)
199
+ })
200
+
201
+ test('queue can setAt by logical index', () => {
202
+ const queue = createQueue({ prefix: 'test' })
203
+ queue.push({ value: 'a' })
204
+ queue.push({ value: 'b' })
205
+ queue.push({ value: 'c' })
206
+
207
+ assert.equal(queue.setAt(1, { value: 'B' }), 1)
208
+
209
+ assert.deepEqual(queue.shift().value, 'a')
210
+ assert.deepEqual(queue.shift().value, 'B')
211
+ assert.deepEqual(queue.shift().value, 'c')
212
+ })
213
+
214
+ test('queue can insertAt by logical index moving current and later items forward', () => {
215
+ const queue = createQueue({ prefix: 'test' })
216
+ queue.push({ value: 'a' })
217
+ queue.push({ value: 'c' })
218
+
219
+ assert.equal(queue.insertAt(1, { value: 'b' }), 1)
220
+
221
+ assert.deepEqual(queue.shift().value, 'a')
222
+ assert.deepEqual(queue.shift().value, 'b')
223
+ assert.deepEqual(queue.shift().value, 'c')
224
+ })
225
+
226
+ test('queue can insert before the first item matching a predicate', () => {
227
+ const queue = createQueue({ prefix: 'test' })
228
+ queue.push({ value: 'a' })
229
+ queue.push({ value: 'c' })
230
+
231
+ assert.equal(queue.insertWhere(item => item.value === 'c', { value: 'b' }), 1)
232
+
233
+ assert.deepEqual(queue.shift().value, 'a')
234
+ assert.deepEqual(queue.shift().value, 'b')
235
+ assert.deepEqual(queue.shift().value, 'c')
236
+ })
237
+
238
+ test('queue leaves the queue unchanged when insertWhere finds no match by default', () => {
239
+ const queue = createQueue({ prefix: 'test' })
240
+ queue.push({ value: 'a' })
241
+ queue.push({ value: 'b' })
242
+
243
+ assert.equal(queue.insertWhere(item => item.value === 'missing', { value: 'c' }), null)
244
+
245
+ assert.deepEqual(queue.shift().value, 'a')
246
+ assert.deepEqual(queue.shift().value, 'b')
247
+ assert.equal(queue.shift(), null)
248
+ })
249
+
250
+ test('queue can append when insertWhere finds no match if configured', () => {
251
+ const queue = createQueue({ prefix: 'test' })
252
+ queue.push({ value: 'a' })
253
+ queue.push({ value: 'b' })
254
+
255
+ assert.equal(queue.insertWhere(item => item.value === 'missing', { value: 'c' }, { appendIfMissing: true }), 2)
256
+
257
+ assert.deepEqual(queue.shift().value, 'a')
258
+ assert.deepEqual(queue.shift().value, 'b')
259
+ assert.deepEqual(queue.shift().value, 'c')
260
+ })
261
+
262
+ test('queue can remove by logical index moving later items backward', () => {
263
+ const queue = createQueue({ prefix: 'test' })
264
+ queue.push({ value: 'a' })
265
+ queue.push({ value: 'b' })
266
+ queue.push({ value: 'c' })
267
+
268
+ assert.deepEqual(queue.removeAt(1).value, 'b')
269
+
270
+ assert.deepEqual(queue.shift().value, 'a')
271
+ assert.deepEqual(queue.shift().value, 'c')
272
+ assert.equal(queue.shift(), null)
273
+ })
274
+
275
+ test('queue resumes a removeAt that failed while moving items', () => {
276
+ const queue = createQueue({ prefix: 'test' })
277
+ queue.push({ value: 'a' })
278
+ queue.push({ value: 'b' })
279
+ queue.push({ value: 'c' })
280
+ queue.push({ value: 'd' })
281
+ failNextItemRemove = true
282
+
283
+ assert.throws(() => queue.removeAt(1), /ITEM_REMOVE_FAILED/)
284
+ assert.notEqual(globalThis.localStorage.getItem('test:queue:operation'), null)
285
+
286
+ const recovered = createQueue({ prefix: 'test' })
287
+
288
+ assert.deepEqual(recovered.shift().value, 'a')
289
+ assert.deepEqual(recovered.shift().value, 'c')
290
+ assert.deepEqual(recovered.shift().value, 'd')
291
+ assert.equal(recovered.shift(), null)
292
+ assert.equal(globalThis.localStorage.getItem('test:queue:operation'), null)
293
+ })
294
+
295
+ test('removeWhere removes matching items sparsely and readers skip the holes', () => {
296
+ const queue = createQueue({ prefix: 'test' })
297
+ queue.push({ value: 'a' })
298
+ queue.push({ value: 'b' })
299
+ queue.push({ value: 'c' })
300
+
301
+ queue.removeWhere(item => item.value === 'b')
302
+
303
+ assert.deepEqual(queue.shift().value, 'a')
304
+ assert.deepEqual(queue.shift().value, 'c')
305
+ assert.equal(queue.shift(), null)
306
+ })
307
+
308
+ test('queue resumes a forward insertAt that failed while moving items', () => {
309
+ const queue = createQueue({ prefix: 'test' })
310
+ queue.push({ value: 'a' })
311
+ queue.push({ value: 'c' })
312
+ queue.push({ value: 'd' })
313
+ failNextItemRemove = true
314
+
315
+ assert.throws(() => queue.insertAt(1, { value: 'b' }), /ITEM_REMOVE_FAILED/)
316
+ assert.notEqual(globalThis.localStorage.getItem('test:queue:operation'), null)
317
+
318
+ const recovered = createQueue({ prefix: 'test' })
319
+
320
+ assert.deepEqual(recovered.shift().value, 'a')
321
+ assert.deepEqual(recovered.shift().value, 'b')
322
+ assert.deepEqual(recovered.shift().value, 'c')
323
+ assert.deepEqual(recovered.shift().value, 'd')
324
+ assert.equal(globalThis.localStorage.getItem('test:queue:operation'), null)
325
+ })
326
+
327
+ test('queue insertAt inserts after an item by using the next logical index', () => {
328
+ const queue = createQueue({ prefix: 'test' })
329
+ queue.push({ value: 'a' })
330
+ queue.push({ value: 'b' })
331
+
332
+ assert.equal(queue.insertAt(2, { value: 'c' }), 2)
333
+
334
+ assert.deepEqual(queue.shift().value, 'a')
335
+ assert.deepEqual(queue.shift().value, 'b')
336
+ assert.deepEqual(queue.shift().value, 'c')
337
+ })
338
+
339
+ test('items consumes from the front', async () => {
340
+ const queue = createQueue({ prefix: 'test' })
341
+ queue.push({ value: 'first' })
342
+ queue.push({ value: 'second' })
343
+ const iterator = queue.items()
344
+
345
+ assert.deepEqual((await iterator.next()).value.value, 'first')
346
+ assert.deepEqual((await iterator.next()).value.value, 'second')
347
+ await iterator.return()
348
+ })
349
+
350
+ test('reverseItems consumes from the end', async () => {
351
+ const queue = createQueue({ prefix: 'test' })
352
+ queue.push({ value: 'first' })
353
+ queue.push({ value: 'second' })
354
+ const iterator = queue.reverseItems()
355
+
356
+ assert.deepEqual((await iterator.next()).value.value, 'second')
357
+ assert.deepEqual((await iterator.next()).value.value, 'first')
358
+ await iterator.return()
359
+ })
360
+
361
+ test('storedItems scans stored items without consuming them', async () => {
362
+ const queue = createQueue({ prefix: 'test' })
363
+ queue.push({ value: 'first' })
364
+ queue.push({ value: 'second' })
365
+
366
+ const values = []
367
+ for await (const item of queue.storedItems()) values.push(item.value)
368
+
369
+ assert.deepEqual(values, ['first', 'second'])
370
+ assert.deepEqual(queue.shift().value, 'first')
371
+ assert.deepEqual(queue.shift().value, 'second')
372
+ })
373
+
374
+ test('reverseStoredItems scans stored items from the end without consuming them', async () => {
375
+ const queue = createQueue({ prefix: 'test' })
376
+ queue.push({ value: 'first' })
377
+ queue.push({ value: 'second' })
378
+
379
+ const values = []
380
+ for await (const item of queue.reverseStoredItems()) values.push(item.value)
381
+
382
+ assert.deepEqual(values, ['second', 'first'])
383
+ assert.deepEqual(queue.shift().value, 'first')
384
+ assert.deepEqual(queue.shift().value, 'second')
385
+ })
386
+
387
+ test('queue evicts older items when maxBytes would be exceeded', () => {
388
+ const queue = createQueue({ prefix: 'test', maxBytes: 450 })
389
+ queue.push({ value: 'a'.repeat(120) })
390
+ queue.push({ value: 'b'.repeat(120) })
391
+ queue.push({ value: 'c'.repeat(120) })
392
+
393
+ assert.deepEqual(queue.shift().value, 'b'.repeat(120))
394
+ assert.deepEqual(queue.shift().value, 'c'.repeat(120))
395
+ assert.equal(queue.shift(), null)
396
+ })
397
+
398
+ test('queue can evict from the tail when configured', () => {
399
+ const queue = createQueue({ prefix: 'test', maxBytes: 450, evictionPolicy: 'lifo' })
400
+ queue.push({ value: 'a'.repeat(120) })
401
+ queue.push({ value: 'b'.repeat(120) })
402
+ queue.push({ value: 'c'.repeat(120) })
403
+
404
+ assert.deepEqual(queue.shift().value, 'a'.repeat(120))
405
+ assert.deepEqual(queue.shift().value, 'c'.repeat(120))
406
+ assert.equal(queue.shift(), null)
407
+ })
408
+
409
+ test('queue rejects unknown eviction policies', () => {
410
+ assert.throws(() => createQueue({ prefix: 'test', evictionPolicy: 'random' }), /QUEUE_INVALID_EVICTION_POLICY/)
411
+ })
412
+
413
+ test('setAt evicts to fit without evicting the item being replaced', () => {
414
+ const queue = createQueue({ prefix: 'test', maxBytes: 400, evictionPolicy: 'fifo' })
415
+ queue.push({ value: 'a'.repeat(120) })
416
+ queue.push({ value: 'b'.repeat(120) })
417
+
418
+ assert.equal(queue.setAt(0, { value: 'A'.repeat(200) }), 0)
419
+
420
+ assert.deepEqual(queue.shift().value, 'A'.repeat(200))
421
+ assert.equal(queue.shift(), null)
422
+ })
423
+
424
+ test('insertAt evicts to fit before inserting the new item', () => {
425
+ const queue = createQueue({ prefix: 'test', maxBytes: 540, evictionPolicy: 'fifo' })
426
+ queue.push({ value: 'a'.repeat(120) })
427
+ queue.push({ value: 'b'.repeat(120) })
428
+
429
+ assert.equal(queue.insertAt(1, { value: 'X'.repeat(200) }), 1)
430
+
431
+ assert.deepEqual(queue.shift().value, 'b'.repeat(120))
432
+ assert.deepEqual(queue.shift().value, 'X'.repeat(200))
433
+ assert.equal(queue.shift(), null)
434
+ })
435
+
436
+ test('queue rejects items larger than maxBytes', () => {
437
+ const queue = createQueue({ prefix: 'test', maxBytes: 80 })
438
+
439
+ assert.throws(() => queue.push({ value: 'too-large'.repeat(20) }), /QUEUE_ITEM_TOO_LARGE/)
440
+ assert.equal(queue.shift(), null)
441
+ })
442
+
443
+ test('queue enforces maxBytes when opening an existing queue', () => {
444
+ const queue = createQueue({ prefix: 'test' })
445
+ queue.push({ value: 'a'.repeat(120) })
446
+ queue.push({ value: 'b'.repeat(120) })
447
+ queue.push({ value: 'c'.repeat(120) })
448
+
449
+ const bounded = createQueue({ prefix: 'test', maxBytes: 450 })
450
+
451
+ assert.deepEqual(bounded.shift().value, 'b'.repeat(120))
452
+ assert.deepEqual(bounded.shift().value, 'c'.repeat(120))
453
+ assert.equal(bounded.shift(), null)
454
+ })
455
+
456
+ test('queue retries once with a lower session byte limit after quota errors', () => {
457
+ const storageArea = createQuotaStorageArea({ failOnItemWrite: 2 })
458
+ const queue = createQueue({ prefix: 'test', storageArea, maxBytes: 300 })
459
+
460
+ queue.push({ value: 'evicted-after-quota'.repeat(5) })
461
+ queue.push({ value: 'survives-quota-retry'.repeat(5) })
462
+
463
+ assert.deepEqual(queue.shift().value, 'survives-quota-retry'.repeat(5))
464
+ assert.equal(queue.shift(), null)
465
+ })
466
+
467
+ test('clear removes queued items state and pending operation metadata', () => {
468
+ const queue = createQueue({ prefix: 'test' })
469
+ queue.push({ value: 'first' })
470
+ queue.push({ value: 'second' })
471
+ setJson('test:queue:operation', { type: 'unknown' })
472
+
473
+ queue.clear()
474
+
475
+ assert.equal(globalThis.localStorage.getItem('test:queue'), null)
476
+ assert.equal(globalThis.localStorage.getItem('test:queue:item:0'), null)
477
+ assert.equal(globalThis.localStorage.getItem('test:queue:item:1'), null)
478
+ assert.equal(globalThis.localStorage.getItem('test:queue:operation'), null)
479
+ assert.equal(queue.shift(), null)
480
+ })