@mblaney/holster 2.0.2 → 2.1.0
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/README.md +37 -6
- package/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/dist/holster.js +3 -3
- package/dist/holster.js.map +3 -3
- package/package.json +1 -1
- package/src/ham.js +10 -3
- package/src/holster.js +36 -25
- package/src/radisk.js +26 -12
- package/src/user.js +92 -101
- package/src/wire.js +298 -104
- package/ts/src/array.ts +10 -13
- package/ts/src/buffer.ts +40 -20
- package/ts/src/get.ts +5 -6
- package/ts/src/ham.ts +47 -28
- package/ts/src/holster.ts +277 -132
- package/ts/src/index.ts +14 -15
- package/ts/src/radisk.ts +122 -37
- package/ts/src/radix.ts +50 -34
- package/ts/src/schemas.ts +44 -24
- package/ts/src/sea-utils.ts +7 -11
- package/ts/src/sea.ts +51 -54
- package/ts/src/store.ts +48 -42
- package/ts/src/user.ts +136 -113
- package/ts/src/utils.ts +36 -11
- package/ts/src/wire.ts +453 -232
package/src/wire.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as utils from "./utils.js"
|
|
|
7
7
|
const isNode = typeof process !== "undefined" && process.versions?.node != null
|
|
8
8
|
|
|
9
9
|
const wsModule = isNode ? await import("ws") : undefined
|
|
10
|
+
const fs = isNode ? await import(/*webpackIgnore: true*/ "node:fs") : undefined
|
|
10
11
|
|
|
11
12
|
if (typeof globalThis.WebSocket === "undefined") {
|
|
12
13
|
globalThis.WebSocket = wsModule?.WebSocket
|
|
@@ -210,6 +211,107 @@ const Wire = opt => {
|
|
|
210
211
|
const rateLimiter = createRateLimiter(isTestEnv)
|
|
211
212
|
const connectionManager = createConnectionManager(opt.maxConnections || 1000)
|
|
212
213
|
|
|
214
|
+
// User storage limiting — server-side only, requires opt.userLimit: true.
|
|
215
|
+
const DEFAULT_STORAGE_LIMIT = 1 // 1 MB
|
|
216
|
+
const defaultLimit =
|
|
217
|
+
(typeof opt.defaultLimit === "number"
|
|
218
|
+
? opt.defaultLimit
|
|
219
|
+
: DEFAULT_STORAGE_LIMIT) * 1048576
|
|
220
|
+
const userStorage = new Map() // Map<pubkey, totalBytes>
|
|
221
|
+
const userLimits = new Map() // Map<pubkey, maxBytes> converted from MB in .user_limit.json
|
|
222
|
+
let saveStorageTimer = null
|
|
223
|
+
|
|
224
|
+
const loadUserStorage = cb => {
|
|
225
|
+
fs.readFile(`${opt.file}/../.user_storage.json`, "utf8", (err, raw) => {
|
|
226
|
+
if (err) {
|
|
227
|
+
if (cb) cb()
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const data = JSON.parse(raw)
|
|
233
|
+
userStorage.clear()
|
|
234
|
+
for (const [pub, total] of Object.entries(data)) {
|
|
235
|
+
if (typeof total === "number") userStorage.set(pub, total)
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
// Invalid JSON — start with empty map.
|
|
239
|
+
}
|
|
240
|
+
if (cb) cb()
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const saveUserStorage = () => {
|
|
245
|
+
if (saveStorageTimer) return
|
|
246
|
+
|
|
247
|
+
const t = setTimeout(() => {
|
|
248
|
+
saveStorageTimer = null
|
|
249
|
+
const data = {}
|
|
250
|
+
for (const [pub, total] of userStorage) {
|
|
251
|
+
data[pub] = total
|
|
252
|
+
}
|
|
253
|
+
fs.writeFile(
|
|
254
|
+
`${opt.file}/../.user_storage.json`,
|
|
255
|
+
JSON.stringify(data),
|
|
256
|
+
err => {
|
|
257
|
+
if (err) console.warn("saveUserStorage error:", err)
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
}, 5000)
|
|
261
|
+
if (t.unref) t.unref()
|
|
262
|
+
saveStorageTimer = t
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const loadUserLimits = cb => {
|
|
266
|
+
fs.readFile(`${opt.file}/../.user_limit.json`, "utf8", (err, raw) => {
|
|
267
|
+
if (err) {
|
|
268
|
+
if (cb) cb()
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const data = JSON.parse(raw)
|
|
273
|
+
userLimits.clear()
|
|
274
|
+
for (const [pub, limit] of Object.entries(data)) {
|
|
275
|
+
if (typeof limit === "number") userLimits.set(pub, limit * 1048576)
|
|
276
|
+
// Non-numeric values ignored, falls back to defaultLimit.
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
// Invalid JSON — userLimits stays empty.
|
|
280
|
+
}
|
|
281
|
+
if (cb) cb()
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const formatBytes = bytes => {
|
|
286
|
+
if (bytes < 1024) return `${bytes} B`
|
|
287
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`
|
|
288
|
+
if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`
|
|
289
|
+
return `${(bytes / 1073741824).toFixed(1)} GB`
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const logStorageSummary = () => {
|
|
293
|
+
const userCount = userStorage.size
|
|
294
|
+
const defLimitStr =
|
|
295
|
+
defaultLimit === 0 ? "unlisted users blocked" : formatBytes(defaultLimit)
|
|
296
|
+
console.log(
|
|
297
|
+
`[HOLSTER] Per-user storage limits enabled (${userCount} user${userCount !== 1 ? "s" : ""}, default: ${defLimitStr})`,
|
|
298
|
+
)
|
|
299
|
+
const sorted = [...userStorage.entries()].sort(([, a], [, b]) => b - a)
|
|
300
|
+
const formatUser = pub => {
|
|
301
|
+
const total = userStorage.get(pub) ?? 0
|
|
302
|
+
const limit = userLimits.get(pub) ?? defaultLimit
|
|
303
|
+
const limitStr = limit === 0 ? "blocked" : formatBytes(limit)
|
|
304
|
+
return ` ${pub} — used: ${formatBytes(total)} / limit: ${limitStr}`
|
|
305
|
+
}
|
|
306
|
+
if (userCount <= 5) {
|
|
307
|
+
sorted.forEach(([pub]) => console.log(formatUser(pub)))
|
|
308
|
+
} else {
|
|
309
|
+
console.log(" Top 5 by storage used:")
|
|
310
|
+
sorted.slice(0, 5).forEach(([pub]) => console.log(formatUser(pub)))
|
|
311
|
+
console.log(` Run \`node examples/user-storage.js\` to view all users`)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
213
315
|
// The check function is required because user data must provide a public key
|
|
214
316
|
// so that it can be verified. The public key might verify the provided
|
|
215
317
|
// signature but not actually match the user under which the data is being
|
|
@@ -236,11 +338,7 @@ const Wire = opt => {
|
|
|
236
338
|
// matching public keys, as the provided soul also needs a rel on the
|
|
237
339
|
// parent node which then also requires checking. Otherwise public keys
|
|
238
340
|
// need to match for existing data.
|
|
239
|
-
if (
|
|
240
|
-
!msg.put ||
|
|
241
|
-
!msg.put[soul] ||
|
|
242
|
-
msg.put[soul][key] === node[key]
|
|
243
|
-
) {
|
|
341
|
+
if (!msg.put || !msg.put[soul] || msg.put[soul][key] === node[key]) {
|
|
244
342
|
continue
|
|
245
343
|
}
|
|
246
344
|
|
|
@@ -284,8 +382,25 @@ const Wire = opt => {
|
|
|
284
382
|
}
|
|
285
383
|
|
|
286
384
|
const put = async (msg, send) => {
|
|
385
|
+
// Capture old graph sizes before Ham.mix updates graph in place.
|
|
386
|
+
const oldGraphBytes = {}
|
|
387
|
+
if (opt.userLimit) {
|
|
388
|
+
for (const [soul, node] of Object.entries(msg.put)) {
|
|
389
|
+
if (node && node[utils.userPublicKey] && graph[soul]) {
|
|
390
|
+
oldGraphBytes[soul] = JSON.stringify(graph[soul]).length
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
287
395
|
// Store updates returned from Ham.mix and defer updates if required.
|
|
288
|
-
const update = await Ham.mix(
|
|
396
|
+
const update = await Ham.mix(
|
|
397
|
+
msg.put,
|
|
398
|
+
graph,
|
|
399
|
+
hasNode,
|
|
400
|
+
opt.secure,
|
|
401
|
+
listen,
|
|
402
|
+
opt.maxGraphSize,
|
|
403
|
+
)
|
|
289
404
|
if (Object.keys(update.now).length === 0) {
|
|
290
405
|
// No updates to store, check deferred.
|
|
291
406
|
if (Object.keys(update.defer).length !== 0) {
|
|
@@ -299,7 +414,7 @@ const Wire = opt => {
|
|
|
299
414
|
|
|
300
415
|
if (!(await check(update.now, send))) return
|
|
301
416
|
|
|
302
|
-
|
|
417
|
+
const finish = err => {
|
|
303
418
|
if (err) console.warn("store.put", err)
|
|
304
419
|
send(
|
|
305
420
|
JSON.stringify({
|
|
@@ -310,7 +425,35 @@ const Wire = opt => {
|
|
|
310
425
|
)
|
|
311
426
|
// Fire listeners after data is stored
|
|
312
427
|
update.listeners.forEach(cb => cb())
|
|
313
|
-
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!opt.userLimit) {
|
|
431
|
+
store.put(update.now, finish)
|
|
432
|
+
} else {
|
|
433
|
+
const toStore = {}
|
|
434
|
+
for (const [soul, node] of Object.entries(update.now)) {
|
|
435
|
+
const pub = node[utils.userPublicKey]
|
|
436
|
+
if (!pub) {
|
|
437
|
+
toStore[soul] = node
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
const limit = userLimits.get(pub) ?? defaultLimit
|
|
441
|
+
if ((userStorage.get(pub) ?? 0) >= limit) continue
|
|
442
|
+
toStore[soul] = node
|
|
443
|
+
const oldBytes = oldGraphBytes[soul] ?? 0
|
|
444
|
+
const newBytes = JSON.stringify(node).length
|
|
445
|
+
userStorage.set(
|
|
446
|
+
pub,
|
|
447
|
+
Math.max(0, (userStorage.get(pub) ?? 0) - oldBytes + newBytes),
|
|
448
|
+
)
|
|
449
|
+
saveUserStorage()
|
|
450
|
+
}
|
|
451
|
+
if (Object.keys(toStore).length > 0) {
|
|
452
|
+
store.put(toStore, finish)
|
|
453
|
+
} else {
|
|
454
|
+
finish(null)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
314
457
|
|
|
315
458
|
if (Object.keys(update.defer).length !== 0) {
|
|
316
459
|
setTimeout(
|
|
@@ -365,103 +508,105 @@ const Wire = opt => {
|
|
|
365
508
|
return
|
|
366
509
|
}
|
|
367
510
|
|
|
368
|
-
store.get(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
511
|
+
store.get(lex, (err, ack) => {
|
|
512
|
+
if (ack) {
|
|
513
|
+
// Populate in-memory graph from store data before sending the server
|
|
514
|
+
// request, so stale server responses with older state are correctly
|
|
515
|
+
// rejected by ham.mix.
|
|
516
|
+
for (const [soul, node] of Object.entries(ack)) {
|
|
517
|
+
if (!node) continue
|
|
518
|
+
if (!graph[soul]) {
|
|
519
|
+
graph[soul] = {...node}
|
|
520
|
+
continue
|
|
521
|
+
}
|
|
522
|
+
// Merge properties absent from the current graph entry. Only add
|
|
523
|
+
// missing keys — existing entries were set by ham.mix and represent
|
|
524
|
+
// the authoritative merged state for those properties.
|
|
525
|
+
for (const [key, value] of Object.entries(node)) {
|
|
526
|
+
if (key === "_") continue
|
|
527
|
+
if (typeof graph[soul][key] !== "undefined") continue
|
|
528
|
+
graph[soul][key] = value
|
|
529
|
+
if (
|
|
530
|
+
node._ &&
|
|
531
|
+
node._[">"] &&
|
|
532
|
+
typeof node._[">"][key] !== "undefined"
|
|
533
|
+
) {
|
|
534
|
+
const state = node._[">"][key]
|
|
535
|
+
graph[soul]._[">"][key] = state
|
|
536
|
+
if (node._["s"] && node._["s"][state]) {
|
|
537
|
+
if (!graph[soul]._["s"]) graph[soul]._["s"] = {}
|
|
538
|
+
graph[soul]._["s"][state] = node._["s"][state]
|
|
395
539
|
}
|
|
396
540
|
}
|
|
397
541
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (sendResult && sendResult.err) {
|
|
418
|
-
cb({err: sendResult.err})
|
|
419
|
-
return
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
} else {
|
|
542
|
+
}
|
|
543
|
+
// Full-node fetch: the graph is now complete for this soul.
|
|
544
|
+
if (!lex["."] && ack[lex["#"]]) hasNode.add(lex["#"])
|
|
545
|
+
// Only serve from store if the requested property is present.
|
|
546
|
+
const node = ack[lex["#"]]
|
|
547
|
+
const hasProperty =
|
|
548
|
+
node &&
|
|
549
|
+
Object.keys(node).some(
|
|
550
|
+
key => key !== "_" && utils.match(lex["."], key),
|
|
551
|
+
)
|
|
552
|
+
if (hasProperty) {
|
|
553
|
+
// Public keys are immutable once written — never need a wire check.
|
|
554
|
+
if (!isNode && lex["."] !== utils.userPublicKey) {
|
|
555
|
+
if (!lex["."] || typeof lex["."] === "string") {
|
|
556
|
+
const checkKey = lex["#"] + (lex["."] ? "." + lex["."] : "")
|
|
557
|
+
const now = Date.now()
|
|
558
|
+
const lastCheck = recentChecks.get(checkKey)
|
|
559
|
+
if (!lastCheck || now - lastCheck > recentCheckTTL) {
|
|
560
|
+
recentChecks.set(checkKey, now)
|
|
423
561
|
const sendResult = send(request)
|
|
424
562
|
if (sendResult && sendResult.err) {
|
|
425
563
|
cb({err: sendResult.err})
|
|
426
564
|
return
|
|
427
565
|
}
|
|
428
566
|
}
|
|
567
|
+
} else {
|
|
568
|
+
const sendResult = send(request)
|
|
569
|
+
if (sendResult && sendResult.err) {
|
|
570
|
+
cb({err: sendResult.err})
|
|
571
|
+
return
|
|
572
|
+
}
|
|
429
573
|
}
|
|
430
|
-
cb(err ? {put: ack, err: err} : {put: ack})
|
|
431
|
-
return
|
|
432
574
|
}
|
|
575
|
+
cb(err ? {put: ack, err: err} : {put: ack})
|
|
576
|
+
return
|
|
433
577
|
}
|
|
578
|
+
}
|
|
434
579
|
|
|
435
|
-
|
|
436
|
-
|
|
580
|
+
// Data wasn't found locally, log errors and call back after a timeout.
|
|
581
|
+
if (err) console.log(err)
|
|
437
582
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
cb({put: ack})
|
|
446
|
-
return
|
|
583
|
+
// Put requests only need a local answer — skip the wire round-trip.
|
|
584
|
+
if (_opt && _opt.put) {
|
|
585
|
+
const id = lex["#"]
|
|
586
|
+
const ack = {[id]: null}
|
|
587
|
+
if (typeof lex["."] === "string") {
|
|
588
|
+
ack[id] = {[lex["."]]: null}
|
|
447
589
|
}
|
|
590
|
+
cb({put: ack})
|
|
591
|
+
return
|
|
592
|
+
}
|
|
448
593
|
|
|
449
|
-
|
|
594
|
+
queue[track] = cb
|
|
450
595
|
|
|
451
|
-
|
|
452
|
-
|
|
596
|
+
// Store lex to construct the null ack when the timeout fires
|
|
597
|
+
pendingTimeouts.set(track, lex)
|
|
453
598
|
|
|
454
|
-
|
|
455
|
-
|
|
599
|
+
// Ensure the requested soul is stored when the response arrives.
|
|
600
|
+
pendingReferences.add(lex["#"])
|
|
456
601
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
602
|
+
const sendResult = send(request)
|
|
603
|
+
if (sendResult && sendResult.err) {
|
|
604
|
+
cb({err: sendResult.err})
|
|
605
|
+
delete queue[track]
|
|
606
|
+
pendingTimeouts.delete(track)
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
})
|
|
465
610
|
}
|
|
466
611
|
|
|
467
612
|
const api = send => {
|
|
@@ -481,7 +626,22 @@ const Wire = opt => {
|
|
|
481
626
|
// here using the api. This is ok because correct timestamps should be
|
|
482
627
|
// used whereas wire spec needs to handle clock skew for updates
|
|
483
628
|
// across the network.
|
|
484
|
-
const
|
|
629
|
+
const oldGraphBytes = {}
|
|
630
|
+
if (opt.userLimit) {
|
|
631
|
+
for (const [soul, node] of Object.entries(data)) {
|
|
632
|
+
if (node && node[utils.userPublicKey] && graph[soul]) {
|
|
633
|
+
oldGraphBytes[soul] = JSON.stringify(graph[soul]).length
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const update = await Ham.mix(
|
|
638
|
+
data,
|
|
639
|
+
graph,
|
|
640
|
+
hasNode,
|
|
641
|
+
opt.secure,
|
|
642
|
+
listen,
|
|
643
|
+
opt.maxGraphSize,
|
|
644
|
+
)
|
|
485
645
|
if (Object.keys(update.now).length === 0) {
|
|
486
646
|
// No updates, still respond to callback.
|
|
487
647
|
if (cb) cb(null)
|
|
@@ -492,6 +652,24 @@ const Wire = opt => {
|
|
|
492
652
|
return
|
|
493
653
|
}
|
|
494
654
|
|
|
655
|
+
if (opt.userLimit) {
|
|
656
|
+
for (const [soul, node] of Object.entries(update.now)) {
|
|
657
|
+
const pub = node[utils.userPublicKey]
|
|
658
|
+
if (!pub) continue
|
|
659
|
+
const limit = userLimits.get(pub) ?? defaultLimit
|
|
660
|
+
const current = userStorage.get(pub) ?? 0
|
|
661
|
+
if (current >= limit) {
|
|
662
|
+
if (cb) cb("storage limit exceeded")
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const oldBytes = oldGraphBytes[soul] ?? 0
|
|
667
|
+
const newBytes = JSON.stringify(node).length
|
|
668
|
+
userStorage.set(pub, Math.max(0, current - oldBytes + newBytes))
|
|
669
|
+
saveUserStorage()
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
495
673
|
// Seed pendingReferences with any new references from API calls
|
|
496
674
|
for (const [soul, node] of Object.entries(update.now)) {
|
|
497
675
|
if (node && typeof node === "object") {
|
|
@@ -561,9 +739,7 @@ const Wire = opt => {
|
|
|
561
739
|
// mock-sockets provides clients as a function that returns an array.
|
|
562
740
|
let clients = () => wss.clients()
|
|
563
741
|
if (!wss) {
|
|
564
|
-
const config = opt.server
|
|
565
|
-
? {server: opt.server}
|
|
566
|
-
: {port: opt.port}
|
|
742
|
+
const config = opt.server ? {server: opt.server} : {port: opt.port}
|
|
567
743
|
wss = new wsModule.WebSocketServer(config)
|
|
568
744
|
clients = () => wss.clients
|
|
569
745
|
}
|
|
@@ -576,18 +752,21 @@ const Wire = opt => {
|
|
|
576
752
|
const lex = pendingTimeouts.get(trackId)
|
|
577
753
|
pendingTimeouts.delete(trackId)
|
|
578
754
|
|
|
579
|
-
setTimeout(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
755
|
+
setTimeout(
|
|
756
|
+
() => {
|
|
757
|
+
const cb = queue[trackId]
|
|
758
|
+
if (cb) {
|
|
759
|
+
const id = lex["#"]
|
|
760
|
+
const ack = {[id]: null}
|
|
761
|
+
if (typeof lex["."] === "string") {
|
|
762
|
+
ack[id] = {[lex["."]]: null}
|
|
763
|
+
}
|
|
764
|
+
cb({put: ack})
|
|
765
|
+
delete queue[trackId]
|
|
586
766
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}, isTestEnv ? 100 : 10000)
|
|
767
|
+
},
|
|
768
|
+
isTestEnv ? 100 : 10000,
|
|
769
|
+
)
|
|
591
770
|
}
|
|
592
771
|
|
|
593
772
|
clients().forEach(client => {
|
|
@@ -611,6 +790,11 @@ const Wire = opt => {
|
|
|
611
790
|
}
|
|
612
791
|
})
|
|
613
792
|
}
|
|
793
|
+
if (opt.userLimit) {
|
|
794
|
+
loadUserStorage(() => loadUserLimits(() => logStorageSummary()))
|
|
795
|
+
setInterval(loadUserLimits, 60000).unref()
|
|
796
|
+
}
|
|
797
|
+
|
|
614
798
|
wss.on("connection", ws => {
|
|
615
799
|
// Check connection limit
|
|
616
800
|
if (!connectionManager.add(ws)) {
|
|
@@ -722,7 +906,11 @@ const Wire = opt => {
|
|
|
722
906
|
let queueProcessor = null
|
|
723
907
|
const maxQueueLength = opt.maxQueueLength || 10000
|
|
724
908
|
|
|
725
|
-
const startResponseTimeout = (
|
|
909
|
+
const startResponseTimeout = (
|
|
910
|
+
trackId,
|
|
911
|
+
lex,
|
|
912
|
+
delay = isTestEnv ? 100 : 10000,
|
|
913
|
+
) => {
|
|
726
914
|
return setTimeout(() => {
|
|
727
915
|
offlineTimeouts.delete(trackId)
|
|
728
916
|
pendingTimeouts.delete(trackId)
|
|
@@ -772,7 +960,8 @@ const Wire = opt => {
|
|
|
772
960
|
|
|
773
961
|
// Drop GET messages with no pending callback — these were already served
|
|
774
962
|
// from local store so no caller is waiting for the wire response.
|
|
775
|
-
const isStaleCheckMessage =
|
|
963
|
+
const isStaleCheckMessage =
|
|
964
|
+
!sent && msg.get && !pendingTimeouts.has(trackId)
|
|
776
965
|
|
|
777
966
|
if (sent || isStaleCheckMessage) {
|
|
778
967
|
messageQueue.shift()
|
|
@@ -908,7 +1097,12 @@ const Wire = opt => {
|
|
|
908
1097
|
for (const [soul, node] of Object.entries(msg.put)) {
|
|
909
1098
|
const known = await hasSoul(soul)
|
|
910
1099
|
hasSoulMap.set(soul, known)
|
|
911
|
-
if (
|
|
1100
|
+
if (
|
|
1101
|
+
(!known && !pendingReferences.has(soul)) ||
|
|
1102
|
+
!node ||
|
|
1103
|
+
typeof node !== "object"
|
|
1104
|
+
)
|
|
1105
|
+
continue
|
|
912
1106
|
for (const [, value] of Object.entries(node)) {
|
|
913
1107
|
const soulId = utils.rel.is(value)
|
|
914
1108
|
if (soulId) pendingReferences.add(soulId)
|
package/ts/src/array.ts
CHANGED
|
@@ -25,22 +25,22 @@ class SeaArray extends Array<number> {
|
|
|
25
25
|
override toString(enc?: Encoding, start?: number, end?: number): string {
|
|
26
26
|
const encoding = enc || "utf8"
|
|
27
27
|
const startIdx = start || 0
|
|
28
|
-
const endIdx = end
|
|
28
|
+
const endIdx = end
|
|
29
|
+
? Math.min(Math.max(end, startIdx), this.length)
|
|
30
|
+
: this.length
|
|
29
31
|
|
|
30
32
|
if (encoding === "hex") {
|
|
31
33
|
const buf = new Uint8Array(this.slice(startIdx, endIdx))
|
|
32
|
-
return Array.from(buf, byte => byte.toString(16).padStart(2, "0")).join(
|
|
34
|
+
return Array.from(buf, byte => byte.toString(16).padStart(2, "0")).join(
|
|
35
|
+
"",
|
|
36
|
+
)
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
if (encoding === "utf8") {
|
|
36
|
-
return Array.from({
|
|
40
|
+
return Array.from({length: endIdx - startIdx}, (_, i) => {
|
|
37
41
|
const charCode = this[i + startIdx]
|
|
38
42
|
// Use unicode replacement character for invalid character codes
|
|
39
|
-
if (
|
|
40
|
-
charCode === undefined ||
|
|
41
|
-
charCode < 0 ||
|
|
42
|
-
charCode > 0x10ffff
|
|
43
|
-
) {
|
|
43
|
+
if (charCode === undefined || charCode < 0 || charCode > 0x10ffff) {
|
|
44
44
|
return String.fromCharCode(0xfffd)
|
|
45
45
|
}
|
|
46
46
|
return String.fromCharCode(charCode)
|
|
@@ -48,7 +48,7 @@ class SeaArray extends Array<number> {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
if (encoding === "base64") {
|
|
51
|
-
const utf8String = Array.from({
|
|
51
|
+
const utf8String = Array.from({length: endIdx - startIdx}, (_, i) => {
|
|
52
52
|
const charCode = this[i + startIdx]
|
|
53
53
|
if (charCode === undefined || charCode < 0 || charCode > 255) {
|
|
54
54
|
return String.fromCharCode(0xfffd)
|
|
@@ -65,9 +65,7 @@ class SeaArray extends Array<number> {
|
|
|
65
65
|
/**
|
|
66
66
|
* Create SeaArray from iterable or array-like
|
|
67
67
|
*/
|
|
68
|
-
static override from<T>(
|
|
69
|
-
arrayLike: ArrayLike<T> | Iterable<T>
|
|
70
|
-
): SeaArray {
|
|
68
|
+
static override from<T>(arrayLike: ArrayLike<T> | Iterable<T>): SeaArray {
|
|
71
69
|
const arr = Array.from(arrayLike) as number[]
|
|
72
70
|
const seaArray = new SeaArray(...arr)
|
|
73
71
|
return seaArray
|
|
@@ -75,4 +73,3 @@ class SeaArray extends Array<number> {
|
|
|
75
73
|
}
|
|
76
74
|
|
|
77
75
|
export default SeaArray
|
|
78
|
-
|