@mblaney/holster 2.0.1 → 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 -28
- package/src/store.js +30 -17
- package/src/user.js +92 -101
- package/src/wire.js +325 -125
- 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 -51
- 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 +76 -58
- package/ts/src/user.ts +136 -113
- package/ts/src/utils.ts +36 -11
- package/ts/src/wire.ts +477 -248
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
|
|
@@ -206,10 +207,111 @@ const Wire = opt => {
|
|
|
206
207
|
|
|
207
208
|
// Initialize rate limiting and connection management
|
|
208
209
|
// Check if test environment (mock-socket usage)
|
|
209
|
-
const isTestEnv =
|
|
210
|
+
const isTestEnv = isNode && globalThis.WebSocket !== wsModule?.WebSocket
|
|
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
|
|
|
@@ -270,28 +368,39 @@ const Wire = opt => {
|
|
|
270
368
|
}),
|
|
271
369
|
)
|
|
272
370
|
} else {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
err: err,
|
|
284
|
-
}),
|
|
285
|
-
)
|
|
286
|
-
},
|
|
287
|
-
{secure: true},
|
|
288
|
-
)
|
|
371
|
+
store.get(msg.get, (err, ack) => {
|
|
372
|
+
send(
|
|
373
|
+
JSON.stringify({
|
|
374
|
+
"#": dup.track(utils.text.random(9)),
|
|
375
|
+
"@": msg["#"],
|
|
376
|
+
put: ack,
|
|
377
|
+
err: err,
|
|
378
|
+
}),
|
|
379
|
+
)
|
|
380
|
+
})
|
|
289
381
|
}
|
|
290
382
|
}
|
|
291
383
|
|
|
292
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
|
+
|
|
293
395
|
// Store updates returned from Ham.mix and defer updates if required.
|
|
294
|
-
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
|
+
)
|
|
295
404
|
if (Object.keys(update.now).length === 0) {
|
|
296
405
|
// No updates to store, check deferred.
|
|
297
406
|
if (Object.keys(update.defer).length !== 0) {
|
|
@@ -305,7 +414,7 @@ const Wire = opt => {
|
|
|
305
414
|
|
|
306
415
|
if (!(await check(update.now, send))) return
|
|
307
416
|
|
|
308
|
-
|
|
417
|
+
const finish = err => {
|
|
309
418
|
if (err) console.warn("store.put", err)
|
|
310
419
|
send(
|
|
311
420
|
JSON.stringify({
|
|
@@ -316,7 +425,35 @@ const Wire = opt => {
|
|
|
316
425
|
)
|
|
317
426
|
// Fire listeners after data is stored
|
|
318
427
|
update.listeners.forEach(cb => cb())
|
|
319
|
-
}
|
|
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
|
+
}
|
|
320
457
|
|
|
321
458
|
if (Object.keys(update.defer).length !== 0) {
|
|
322
459
|
setTimeout(
|
|
@@ -371,105 +508,105 @@ const Wire = opt => {
|
|
|
371
508
|
return
|
|
372
509
|
}
|
|
373
510
|
|
|
374
|
-
store.get(
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
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]
|
|
401
539
|
}
|
|
402
540
|
}
|
|
403
541
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (sendResult && sendResult.err) {
|
|
424
|
-
cb({err: sendResult.err})
|
|
425
|
-
return
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
} 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)
|
|
429
561
|
const sendResult = send(request)
|
|
430
562
|
if (sendResult && sendResult.err) {
|
|
431
563
|
cb({err: sendResult.err})
|
|
432
564
|
return
|
|
433
565
|
}
|
|
434
566
|
}
|
|
567
|
+
} else {
|
|
568
|
+
const sendResult = send(request)
|
|
569
|
+
if (sendResult && sendResult.err) {
|
|
570
|
+
cb({err: sendResult.err})
|
|
571
|
+
return
|
|
572
|
+
}
|
|
435
573
|
}
|
|
436
|
-
cb(err ? {put: ack, err: err} : {put: ack})
|
|
437
|
-
return
|
|
438
574
|
}
|
|
575
|
+
cb(err ? {put: ack, err: err} : {put: ack})
|
|
576
|
+
return
|
|
439
577
|
}
|
|
578
|
+
}
|
|
440
579
|
|
|
441
|
-
|
|
442
|
-
|
|
580
|
+
// Data wasn't found locally, log errors and call back after a timeout.
|
|
581
|
+
if (err) console.log(err)
|
|
443
582
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
cb({put: ack})
|
|
452
|
-
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}
|
|
453
589
|
}
|
|
590
|
+
cb({put: ack})
|
|
591
|
+
return
|
|
592
|
+
}
|
|
454
593
|
|
|
455
|
-
|
|
594
|
+
queue[track] = cb
|
|
456
595
|
|
|
457
|
-
|
|
458
|
-
|
|
596
|
+
// Store lex to construct the null ack when the timeout fires
|
|
597
|
+
pendingTimeouts.set(track, lex)
|
|
459
598
|
|
|
460
|
-
|
|
461
|
-
|
|
599
|
+
// Ensure the requested soul is stored when the response arrives.
|
|
600
|
+
pendingReferences.add(lex["#"])
|
|
462
601
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
_opt,
|
|
472
|
-
)
|
|
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
|
+
})
|
|
473
610
|
}
|
|
474
611
|
|
|
475
612
|
const api = send => {
|
|
@@ -489,7 +626,22 @@ const Wire = opt => {
|
|
|
489
626
|
// here using the api. This is ok because correct timestamps should be
|
|
490
627
|
// used whereas wire spec needs to handle clock skew for updates
|
|
491
628
|
// across the network.
|
|
492
|
-
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
|
+
)
|
|
493
645
|
if (Object.keys(update.now).length === 0) {
|
|
494
646
|
// No updates, still respond to callback.
|
|
495
647
|
if (cb) cb(null)
|
|
@@ -500,6 +652,24 @@ const Wire = opt => {
|
|
|
500
652
|
return
|
|
501
653
|
}
|
|
502
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
|
+
|
|
503
673
|
// Seed pendingReferences with any new references from API calls
|
|
504
674
|
for (const [soul, node] of Object.entries(update.now)) {
|
|
505
675
|
if (node && typeof node === "object") {
|
|
@@ -569,9 +739,7 @@ const Wire = opt => {
|
|
|
569
739
|
// mock-sockets provides clients as a function that returns an array.
|
|
570
740
|
let clients = () => wss.clients()
|
|
571
741
|
if (!wss) {
|
|
572
|
-
const config = opt.server
|
|
573
|
-
? {server: opt.server}
|
|
574
|
-
: {port: opt.port}
|
|
742
|
+
const config = opt.server ? {server: opt.server} : {port: opt.port}
|
|
575
743
|
wss = new wsModule.WebSocketServer(config)
|
|
576
744
|
clients = () => wss.clients
|
|
577
745
|
}
|
|
@@ -584,18 +752,21 @@ const Wire = opt => {
|
|
|
584
752
|
const lex = pendingTimeouts.get(trackId)
|
|
585
753
|
pendingTimeouts.delete(trackId)
|
|
586
754
|
|
|
587
|
-
setTimeout(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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]
|
|
594
766
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}, isTestEnv ? 100 : 10000)
|
|
767
|
+
},
|
|
768
|
+
isTestEnv ? 100 : 10000,
|
|
769
|
+
)
|
|
599
770
|
}
|
|
600
771
|
|
|
601
772
|
clients().forEach(client => {
|
|
@@ -619,6 +790,11 @@ const Wire = opt => {
|
|
|
619
790
|
}
|
|
620
791
|
})
|
|
621
792
|
}
|
|
793
|
+
if (opt.userLimit) {
|
|
794
|
+
loadUserStorage(() => loadUserLimits(() => logStorageSummary()))
|
|
795
|
+
setInterval(loadUserLimits, 60000).unref()
|
|
796
|
+
}
|
|
797
|
+
|
|
622
798
|
wss.on("connection", ws => {
|
|
623
799
|
// Check connection limit
|
|
624
800
|
if (!connectionManager.add(ws)) {
|
|
@@ -726,10 +902,15 @@ const Wire = opt => {
|
|
|
726
902
|
let clientThrottled = false
|
|
727
903
|
let throttleUntil = 0
|
|
728
904
|
let messageQueue = []
|
|
905
|
+
let offlinePuts = []
|
|
729
906
|
let queueProcessor = null
|
|
730
907
|
const maxQueueLength = opt.maxQueueLength || 10000
|
|
731
908
|
|
|
732
|
-
const startResponseTimeout = (
|
|
909
|
+
const startResponseTimeout = (
|
|
910
|
+
trackId,
|
|
911
|
+
lex,
|
|
912
|
+
delay = isTestEnv ? 100 : 10000,
|
|
913
|
+
) => {
|
|
733
914
|
return setTimeout(() => {
|
|
734
915
|
offlineTimeouts.delete(trackId)
|
|
735
916
|
pendingTimeouts.delete(trackId)
|
|
@@ -779,10 +960,16 @@ const Wire = opt => {
|
|
|
779
960
|
|
|
780
961
|
// Drop GET messages with no pending callback — these were already served
|
|
781
962
|
// from local store so no caller is waiting for the wire response.
|
|
782
|
-
const isStaleCheckMessage =
|
|
963
|
+
const isStaleCheckMessage =
|
|
964
|
+
!sent && msg.get && !pendingTimeouts.has(trackId)
|
|
783
965
|
|
|
784
966
|
if (sent || isStaleCheckMessage) {
|
|
785
967
|
messageQueue.shift()
|
|
968
|
+
} else if (!sent && msg.put) {
|
|
969
|
+
// Hold offline PUTs separately so they don't block GET messages behind
|
|
970
|
+
// them. They'll be flushed back to the front of the queue on reconnect.
|
|
971
|
+
messageQueue.shift()
|
|
972
|
+
offlinePuts.push(data)
|
|
786
973
|
}
|
|
787
974
|
|
|
788
975
|
if (trackId && pendingTimeouts.has(trackId)) {
|
|
@@ -864,14 +1051,22 @@ const Wire = opt => {
|
|
|
864
1051
|
ws = null
|
|
865
1052
|
|
|
866
1053
|
if (retryHandler.shouldRetry()) {
|
|
867
|
-
const delay = retryHandler.getDelay()
|
|
1054
|
+
const delay = isTestEnv ? 50 : retryHandler.getDelay()
|
|
868
1055
|
retryHandler.increment()
|
|
869
|
-
setTimeout(start, delay)
|
|
1056
|
+
const t = setTimeout(start, delay)
|
|
1057
|
+
if (t.unref) t.unref()
|
|
870
1058
|
}
|
|
871
1059
|
}
|
|
872
1060
|
|
|
873
1061
|
ws.onopen = () => {
|
|
874
1062
|
retryHandler.reset()
|
|
1063
|
+
if (offlinePuts.length > 0) {
|
|
1064
|
+
messageQueue = offlinePuts.concat(messageQueue)
|
|
1065
|
+
offlinePuts = []
|
|
1066
|
+
if (!queueProcessor) {
|
|
1067
|
+
queueProcessor = setTimeout(processQueue, 50)
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
875
1070
|
}
|
|
876
1071
|
|
|
877
1072
|
ws.onerror = e => {
|
|
@@ -902,7 +1097,12 @@ const Wire = opt => {
|
|
|
902
1097
|
for (const [soul, node] of Object.entries(msg.put)) {
|
|
903
1098
|
const known = await hasSoul(soul)
|
|
904
1099
|
hasSoulMap.set(soul, known)
|
|
905
|
-
if (
|
|
1100
|
+
if (
|
|
1101
|
+
(!known && !pendingReferences.has(soul)) ||
|
|
1102
|
+
!node ||
|
|
1103
|
+
typeof node !== "object"
|
|
1104
|
+
)
|
|
1105
|
+
continue
|
|
906
1106
|
for (const [, value] of Object.entries(node)) {
|
|
907
1107
|
const soulId = utils.rel.is(value)
|
|
908
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
|
-
|