@mblaney/holster 2.0.2 → 2.1.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/README.md +43 -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 +2 -2
- 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 +300 -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 +455 -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 = graph[soul] ? JSON.stringify(graph[soul]).length : 0
|
|
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,26 @@ 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 = graph[soul]
|
|
668
|
+
? JSON.stringify(graph[soul]).length
|
|
669
|
+
: 0
|
|
670
|
+
userStorage.set(pub, Math.max(0, current - oldBytes + newBytes))
|
|
671
|
+
saveUserStorage()
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
495
675
|
// Seed pendingReferences with any new references from API calls
|
|
496
676
|
for (const [soul, node] of Object.entries(update.now)) {
|
|
497
677
|
if (node && typeof node === "object") {
|
|
@@ -561,9 +741,7 @@ const Wire = opt => {
|
|
|
561
741
|
// mock-sockets provides clients as a function that returns an array.
|
|
562
742
|
let clients = () => wss.clients()
|
|
563
743
|
if (!wss) {
|
|
564
|
-
const config = opt.server
|
|
565
|
-
? {server: opt.server}
|
|
566
|
-
: {port: opt.port}
|
|
744
|
+
const config = opt.server ? {server: opt.server} : {port: opt.port}
|
|
567
745
|
wss = new wsModule.WebSocketServer(config)
|
|
568
746
|
clients = () => wss.clients
|
|
569
747
|
}
|
|
@@ -576,18 +754,21 @@ const Wire = opt => {
|
|
|
576
754
|
const lex = pendingTimeouts.get(trackId)
|
|
577
755
|
pendingTimeouts.delete(trackId)
|
|
578
756
|
|
|
579
|
-
setTimeout(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
757
|
+
setTimeout(
|
|
758
|
+
() => {
|
|
759
|
+
const cb = queue[trackId]
|
|
760
|
+
if (cb) {
|
|
761
|
+
const id = lex["#"]
|
|
762
|
+
const ack = {[id]: null}
|
|
763
|
+
if (typeof lex["."] === "string") {
|
|
764
|
+
ack[id] = {[lex["."]]: null}
|
|
765
|
+
}
|
|
766
|
+
cb({put: ack})
|
|
767
|
+
delete queue[trackId]
|
|
586
768
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}, isTestEnv ? 100 : 10000)
|
|
769
|
+
},
|
|
770
|
+
isTestEnv ? 100 : 10000,
|
|
771
|
+
)
|
|
591
772
|
}
|
|
592
773
|
|
|
593
774
|
clients().forEach(client => {
|
|
@@ -611,6 +792,11 @@ const Wire = opt => {
|
|
|
611
792
|
}
|
|
612
793
|
})
|
|
613
794
|
}
|
|
795
|
+
if (opt.userLimit) {
|
|
796
|
+
loadUserStorage(() => loadUserLimits(() => logStorageSummary()))
|
|
797
|
+
setInterval(loadUserLimits, 60000).unref()
|
|
798
|
+
}
|
|
799
|
+
|
|
614
800
|
wss.on("connection", ws => {
|
|
615
801
|
// Check connection limit
|
|
616
802
|
if (!connectionManager.add(ws)) {
|
|
@@ -722,7 +908,11 @@ const Wire = opt => {
|
|
|
722
908
|
let queueProcessor = null
|
|
723
909
|
const maxQueueLength = opt.maxQueueLength || 10000
|
|
724
910
|
|
|
725
|
-
const startResponseTimeout = (
|
|
911
|
+
const startResponseTimeout = (
|
|
912
|
+
trackId,
|
|
913
|
+
lex,
|
|
914
|
+
delay = isTestEnv ? 100 : 10000,
|
|
915
|
+
) => {
|
|
726
916
|
return setTimeout(() => {
|
|
727
917
|
offlineTimeouts.delete(trackId)
|
|
728
918
|
pendingTimeouts.delete(trackId)
|
|
@@ -772,7 +962,8 @@ const Wire = opt => {
|
|
|
772
962
|
|
|
773
963
|
// Drop GET messages with no pending callback — these were already served
|
|
774
964
|
// from local store so no caller is waiting for the wire response.
|
|
775
|
-
const isStaleCheckMessage =
|
|
965
|
+
const isStaleCheckMessage =
|
|
966
|
+
!sent && msg.get && !pendingTimeouts.has(trackId)
|
|
776
967
|
|
|
777
968
|
if (sent || isStaleCheckMessage) {
|
|
778
969
|
messageQueue.shift()
|
|
@@ -908,7 +1099,12 @@ const Wire = opt => {
|
|
|
908
1099
|
for (const [soul, node] of Object.entries(msg.put)) {
|
|
909
1100
|
const known = await hasSoul(soul)
|
|
910
1101
|
hasSoulMap.set(soul, known)
|
|
911
|
-
if (
|
|
1102
|
+
if (
|
|
1103
|
+
(!known && !pendingReferences.has(soul)) ||
|
|
1104
|
+
!node ||
|
|
1105
|
+
typeof node !== "object"
|
|
1106
|
+
)
|
|
1107
|
+
continue
|
|
912
1108
|
for (const [, value] of Object.entries(node)) {
|
|
913
1109
|
const soulId = utils.rel.is(value)
|
|
914
1110
|
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
|
-
|