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.
- package/AGENTS.md +20 -0
- package/README.md +129 -0
- package/base16/index.js +14 -0
- package/base64/index.js +29 -0
- package/content-key/event/index.js +85 -0
- package/content-key/index.js +1 -0
- package/content-key/services/iykc-proof.js +129 -0
- package/double-dh/index.js +166 -0
- package/ecdh/index.js +8 -0
- package/idb/index.js +64 -0
- package/idb-queue/index.js +793 -0
- package/index.js +21 -0
- package/key/index.js +139 -0
- package/network/index.js +68 -0
- package/nip44-v3/index.js +175 -0
- package/nip46/constants/index.js +5 -0
- package/nip46/helpers/frame.js +51 -0
- package/nip46/helpers/url.js +96 -0
- package/nip46/index.js +5 -0
- package/nip46/services/bunker-signer.js +49 -0
- package/nip46/services/client.js +252 -0
- package/nip46/services/server-session.js +225 -0
- package/nip46/services/transport.js +265 -0
- package/package.json +49 -0
- package/private-channel/constants/index.js +5 -0
- package/private-channel/helpers/chunk-size.js +61 -0
- package/private-channel/helpers/chunks.js +282 -0
- package/private-channel/helpers/event.js +68 -0
- package/private-channel/index.js +942 -0
- package/private-channel/services/received-chunks.js +398 -0
- package/private-message/index.js +672 -0
- package/private-messenger/constants/index.js +1 -0
- package/private-messenger/index.js +1431 -0
- package/private-messenger/recovery/index.js +316 -0
- package/relay/constants/index.js +18 -0
- package/relay/helpers/hll.js +62 -0
- package/relay/helpers/publish.js +100 -0
- package/relay/helpers/routing.js +36 -0
- package/relay/helpers/timer.js +4 -0
- package/relay/index.js +4 -0
- package/relay/services/query.js +224 -0
- package/relay/services/relay-connection.js +156 -0
- package/relay/services/relay-pool.js +908 -0
- package/temporary-storage/index.js +85 -0
- package/tests/base16.test.js +19 -0
- package/tests/base64.test.js +18 -0
- package/tests/content-key-event.test.js +48 -0
- package/tests/fixtures/nip44v3-vectors.json +1 -0
- package/tests/helpers/test-signer.js +185 -0
- package/tests/idb-queue.test.js +153 -0
- package/tests/idb.test.js +91 -0
- package/tests/nip44-v3.test.js +73 -0
- package/tests/nip46.test.js +668 -0
- package/tests/private-channel.test.js +1899 -0
- package/tests/private-message.test.js +460 -0
- package/tests/private-messenger.test.js +1715 -0
- package/tests/queries.test.js +101 -0
- package/tests/queue-parity.test.js +105 -0
- package/tests/relay-hll.test.js +32 -0
- package/tests/relay-pool.test.js +2067 -0
- package/tests/temporary-storage.test.js +89 -0
- package/tests/web-storage-queue.test.js +480 -0
- 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
|
+
}
|