@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/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 = opt.wss && opt.wss.constructor.name === "Server"
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
- // Always pass secure to ensure public key is fetched. User data may be
274
- // requested that requires the public key for verification.
275
- store.get(
276
- msg.get,
277
- (err, ack) => {
278
- send(
279
- JSON.stringify({
280
- "#": dup.track(utils.text.random(9)),
281
- "@": msg["#"],
282
- put: ack,
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(msg.put, graph, hasNode, opt.secure, listen)
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
- store.put(update.now, err => {
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
- lex,
376
- (err, ack) => {
377
- if (ack) {
378
- // Populate in-memory graph from store data before sending the server
379
- // request, so stale server responses with older state are correctly
380
- // rejected by ham.mix.
381
- for (const [soul, node] of Object.entries(ack)) {
382
- if (!node) continue
383
- if (!graph[soul]) {
384
- graph[soul] = {...node}
385
- continue
386
- }
387
- // Merge properties absent from the current graph entry. Only add
388
- // missing keys — existing entries were set by ham.mix and represent
389
- // the authoritative merged state for those properties.
390
- for (const [key, value] of Object.entries(node)) {
391
- if (key === "_") continue
392
- if (typeof graph[soul][key] !== "undefined") continue
393
- graph[soul][key] = value
394
- if (node._ && node._[">"] && typeof node._[">"][key] !== "undefined") {
395
- const state = node._[">"][key]
396
- graph[soul]._[">"][key] = state
397
- if (node._["s"] && node._["s"][state]) {
398
- if (!graph[soul]._["s"]) graph[soul]._["s"] = {}
399
- graph[soul]._["s"][state] = node._["s"][state]
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
- // Full-node fetch: the graph is now complete for this soul.
405
- if (!lex["."] && ack[lex["#"]]) hasNode.add(lex["#"])
406
- // Only serve from store if the requested property is present.
407
- const node = ack[lex["#"]]
408
- const hasProperty =
409
- node &&
410
- Object.keys(node).some(
411
- key => key !== "_" && utils.match(lex["."], key),
412
- )
413
- if (hasProperty) {
414
- // Public keys are immutable once written — never need a wire check.
415
- if (!isNode && lex["."] !== utils.userPublicKey) {
416
- if (!lex["."] || typeof lex["."] === "string") {
417
- const checkKey = lex["#"] + (lex["."] ? "." + lex["."] : "")
418
- const now = Date.now()
419
- const lastCheck = recentChecks.get(checkKey)
420
- if (!lastCheck || now - lastCheck > recentCheckTTL) {
421
- recentChecks.set(checkKey, now)
422
- const sendResult = send(request)
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
- // Data wasn't found locally, log errors and call back after a timeout.
442
- if (err) console.log(err)
580
+ // Data wasn't found locally, log errors and call back after a timeout.
581
+ if (err) console.log(err)
443
582
 
444
- // Put requests only need a local answer — skip the wire round-trip.
445
- if (_opt && _opt.put) {
446
- const id = lex["#"]
447
- const ack = {[id]: null}
448
- if (typeof lex["."] === "string") {
449
- ack[id] = {[lex["."]]: null}
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
- queue[track] = cb
594
+ queue[track] = cb
456
595
 
457
- // Store lex to construct the null ack when the timeout fires
458
- pendingTimeouts.set(track, lex)
596
+ // Store lex to construct the null ack when the timeout fires
597
+ pendingTimeouts.set(track, lex)
459
598
 
460
- // Ensure the requested soul is stored when the response arrives.
461
- pendingReferences.add(lex["#"])
599
+ // Ensure the requested soul is stored when the response arrives.
600
+ pendingReferences.add(lex["#"])
462
601
 
463
- const sendResult = send(request)
464
- if (sendResult && sendResult.err) {
465
- cb({err: sendResult.err})
466
- delete queue[track]
467
- pendingTimeouts.delete(track)
468
- return
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 update = await Ham.mix(data, graph, hasNode, opt.secure, listen)
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
- const cb = queue[trackId]
589
- if (cb) {
590
- const id = lex["#"]
591
- const ack = {[id]: null}
592
- if (typeof lex["."] === "string") {
593
- ack[id] = {[lex["."]]: null}
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
- cb({put: ack})
596
- delete queue[trackId]
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 = (trackId, lex, delay = isTestEnv ? 100 : 10000) => {
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 = !sent && msg.get && !pendingTimeouts.has(trackId)
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 ((!known && !pendingReferences.has(soul)) || !node || typeof node !== "object") continue
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 ? Math.min(Math.max(end, startIdx), this.length) : this.length
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({ length: endIdx - startIdx }, (_, i) => {
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({ length: endIdx - startIdx }, (_, i) => {
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
-