@mblaney/holster 1.0.35 → 2.0.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.
Files changed (72) hide show
  1. package/build/holster.js +3 -3
  2. package/build/holster.js.map +3 -3
  3. package/dist/holster.js +4 -0
  4. package/dist/holster.js.map +7 -0
  5. package/package.json +22 -5
  6. package/src/dup.js +4 -5
  7. package/src/get.js +10 -19
  8. package/src/ham.js +22 -49
  9. package/src/holster.js +172 -58
  10. package/src/index.js +1 -1
  11. package/src/radisk.js +89 -86
  12. package/src/sea-utils.js +1 -1
  13. package/src/sea.js +0 -52
  14. package/src/store.js +8 -8
  15. package/src/user.js +0 -4
  16. package/src/wire.js +209 -79
  17. package/ts/src/array.ts +78 -0
  18. package/ts/src/buffer.ts +441 -0
  19. package/ts/src/dup.ts +38 -0
  20. package/ts/src/get.ts +83 -0
  21. package/ts/src/ham.ts +258 -0
  22. package/ts/src/holster.ts +968 -0
  23. package/ts/src/index.ts +35 -0
  24. package/ts/src/radisk.ts +591 -0
  25. package/ts/src/radix.ts +157 -0
  26. package/ts/src/schemas.ts +515 -0
  27. package/ts/src/sea-utils.ts +103 -0
  28. package/ts/src/sea.ts +388 -0
  29. package/ts/src/store.ts +321 -0
  30. package/ts/src/user.ts +377 -0
  31. package/ts/src/utils.ts +232 -0
  32. package/ts/src/wire.ts +961 -0
  33. package/.dockerignore +0 -9
  34. package/.github/workflows/node.js.yml +0 -31
  35. package/.github/workflows/npm-publish.yml +0 -33
  36. package/Dockerfile +0 -20
  37. package/examples/index.html +0 -33
  38. package/examples/index.js +0 -49
  39. package/examples/user.html +0 -31
  40. package/examples/user.js +0 -93
  41. package/examples/wire.html +0 -27
  42. package/examples/wire.js +0 -55
  43. package/test/holster.get.test.js +0 -94
  44. package/test/holster.lex.test.js +0 -196
  45. package/test/holster.off.test.js +0 -53
  46. package/test/holster.on.test.js +0 -264
  47. package/test/holster.put.test.js +0 -358
  48. package/test/holster.secure.test.js +0 -202
  49. package/test/holster.user.get.test.js +0 -114
  50. package/test/holster.user.lex.test.js +0 -149
  51. package/test/holster.user.off.test.js +0 -73
  52. package/test/holster.user.on.test.js +0 -300
  53. package/test/holster.user.put.test.js +0 -463
  54. package/test/kitty/! +0 -9
  55. package/test/names.js +0 -225
  56. package/test/radisk.cache.test.js +0 -213
  57. package/test/radisk.test.js +0 -331
  58. package/test/radix.test.js +0 -117
  59. package/test/sea.test.js +0 -284
  60. package/test/split.test.js +0 -317
  61. package/test/store.test.js +0 -316
  62. package/test/system/basic.test.js +0 -29
  63. package/test/system/chained-on.test.js +0 -41
  64. package/test/system/concurrent-listeners.test.js +0 -125
  65. package/test/system/immediate-read.test.js +0 -30
  66. package/test/system/listener-before-data.test.js +0 -51
  67. package/test/system/promise-wrapper.test.js +0 -47
  68. package/test/system/user-chained-on.test.js +0 -60
  69. package/test/system/user-concurrent-listeners.test.js +0 -142
  70. package/test/system/user-listener-before-data.test.js +0 -66
  71. package/test/user.test.js +0 -173
  72. package/test/wire.test.js +0 -158
package/src/wire.js CHANGED
@@ -4,7 +4,7 @@ import Ham from "./ham.js"
4
4
  import Store from "./store.js"
5
5
  import * as utils from "./utils.js"
6
6
 
7
- const isNode = typeof document === "undefined"
7
+ const isNode = typeof process !== "undefined" && process.versions?.node != null
8
8
 
9
9
  const wsModule = isNode ? await import("ws") : undefined
10
10
 
@@ -169,18 +169,31 @@ const createRetryHandler = (maxRetries = 5) => {
169
169
  // Wire starts a websocket client or server and returns get and put methods
170
170
  // for access to the wire spec and storage.
171
171
  const Wire = opt => {
172
- const dup = Dup(opt.maxAge)
172
+ const dup = Dup()
173
173
  const store = Store(opt)
174
174
  const graph = {}
175
175
  const queue = {}
176
176
  const listen = {}
177
+ // Tracks souls that have had a complete full-node store fetch, so subsequent
178
+ // full-node requests can read directly from the in-memory graph without a
179
+ // redundant store round-trip. Kept in sync with graph eviction via Ham.mix.
180
+ const hasNode = new Set()
177
181
 
178
182
  // Track references we want but don't have yet
179
183
  const pendingReferences = new Set()
180
184
 
185
+ // Debounce client-side wire update checks: skip if the same lex was checked
186
+ // recently. Keyed by "soul" or "soul.property".
187
+ const recentChecks = new Map()
188
+ const recentCheckTTL = 10000 // 10 seconds
189
+
181
190
  // Track pending timeouts that should start when message is sent
182
191
  const pendingTimeouts = new Map()
183
192
 
193
+ // Track offline null-response timeouts so they can be cancelled if the WS
194
+ // connects before they fire. Keyed by the same trackId as pendingTimeouts.
195
+ const offlineTimeouts = new Map()
196
+
184
197
  // Helper to check if we have a soul in memory or storage
185
198
  const hasSoul = async soul => {
186
199
  if (graph[soul]) return true
@@ -206,25 +219,26 @@ const Wire = opt => {
206
219
  const key = utils.userPublicKey
207
220
 
208
221
  for (const soul of Object.keys(data)) {
222
+ const node = data[soul]
223
+ // If the incoming update doesn't include a userPublicKey property at all,
224
+ // allow it - the update is not trying to change the public key.
225
+ if (node[key] === undefined) continue
226
+
209
227
  const msg = await new Promise(res => {
210
- getWithCallback({"#": soul, ".": key}, res, send)
228
+ getWithCallback({"#": soul, ".": key}, res, send, {put: true})
211
229
  })
212
230
  if (msg.err) {
213
231
  if (cb) cb(msg.err)
214
232
  return false
215
233
  }
216
234
 
217
- const node = data[soul]
218
235
  // If there is no current node then the data is ok to write without
219
236
  // matching public keys, as the provided soul also needs a rel on the
220
237
  // parent node which then also requires checking. Otherwise public keys
221
- // need to match for existing data. If the incoming update doesn't
222
- // include a userPublicKey property at all, allow it - the update is
223
- // not trying to change the public key.
238
+ // need to match for existing data.
224
239
  if (
225
240
  !msg.put ||
226
241
  !msg.put[soul] ||
227
- node[key] === undefined ||
228
242
  msg.put[soul][key] === node[key]
229
243
  ) {
230
244
  continue
@@ -277,7 +291,7 @@ const Wire = opt => {
277
291
 
278
292
  const put = async (msg, send) => {
279
293
  // Store updates returned from Ham.mix and defer updates if required.
280
- const update = await Ham.mix(msg.put, graph, opt.secure, listen)
294
+ const update = await Ham.mix(msg.put, graph, hasNode, opt.secure, listen)
281
295
  if (Object.keys(update.now).length === 0) {
282
296
  // No updates to store, check deferred.
283
297
  if (Object.keys(update.defer).length !== 0) {
@@ -292,6 +306,7 @@ const Wire = opt => {
292
306
  if (!(await check(update.now, send))) return
293
307
 
294
308
  store.put(update.now, err => {
309
+ if (err) console.warn("store.put", err)
295
310
  send(
296
311
  JSON.stringify({
297
312
  "#": dup.track(utils.text.random(9)),
@@ -316,7 +331,10 @@ const Wire = opt => {
316
331
 
317
332
  if (!utils.obj.is(_opt)) _opt = {}
318
333
 
319
- const ack = Get(lex, graph, _opt.fast)
334
+ // For full-node requests on souls that have been fully fetched from store,
335
+ // treat the graph as authoritative — skip the redundant store round-trip.
336
+ const fast = _opt.fast || (!lex["."] && hasNode.has(lex["#"]))
337
+ const ack = Get(lex, graph, fast)
320
338
  const track = utils.text.random(9)
321
339
  const request = JSON.stringify({
322
340
  "#": dup.track(track),
@@ -324,11 +342,30 @@ const Wire = opt => {
324
342
  })
325
343
 
326
344
  if (ack) {
327
- // Also send request on the wire to check for updates.
328
- const sendResult = send(request)
329
- if (sendResult && sendResult.err) {
330
- cb({err: sendResult.err})
331
- return
345
+ if (!isNode) {
346
+ // Client: send request on the wire to check for updates,
347
+ // since it may have missed puts while disconnected.
348
+ // Skip if the same lex was checked recently to avoid flooding.
349
+ if (!lex["."] || typeof lex["."] === "string") {
350
+ const checkKey = lex["#"] + (lex["."] ? "." + lex["."] : "")
351
+ const now = Date.now()
352
+ const lastCheck = recentChecks.get(checkKey)
353
+ if (!lastCheck || now - lastCheck > recentCheckTTL) {
354
+ recentChecks.set(checkKey, now)
355
+ const sendResult = send(request)
356
+ if (sendResult && sendResult.err) {
357
+ cb({err: sendResult.err})
358
+ return
359
+ }
360
+ }
361
+ } else {
362
+ // Don't skip checks for other types of lex queries.
363
+ const sendResult = send(request)
364
+ if (sendResult && sendResult.err) {
365
+ cb({err: sendResult.err})
366
+ return
367
+ }
368
+ }
332
369
  }
333
370
  cb({put: ack})
334
371
  return
@@ -338,25 +375,90 @@ const Wire = opt => {
338
375
  lex,
339
376
  (err, ack) => {
340
377
  if (ack) {
341
- // Also send request on the wire to check for updates.
342
- const sendResult = send(request)
343
- if (sendResult && sendResult.err) {
344
- cb({err: sendResult.err})
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
+ }
401
+ }
402
+ }
403
+ }
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 {
429
+ const sendResult = send(request)
430
+ if (sendResult && sendResult.err) {
431
+ cb({err: sendResult.err})
432
+ return
433
+ }
434
+ }
435
+ }
436
+ cb(err ? {put: ack, err: err} : {put: ack})
345
437
  return
346
438
  }
347
- cb({put: ack, err: err})
348
- return
349
439
  }
350
440
 
441
+ // Data wasn't found locally, log errors and call back after a timeout.
351
442
  if (err) console.log(err)
352
443
 
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
453
+ }
454
+
353
455
  queue[track] = cb
354
456
 
355
- // Store timeout config to start after message is sent from queue
356
- pendingTimeouts.set(track, {
357
- lex: lex,
358
- wait: _opt.wait || 100,
359
- })
457
+ // Store lex to construct the null ack when the timeout fires
458
+ pendingTimeouts.set(track, lex)
459
+
460
+ // Ensure the requested soul is stored when the response arrives.
461
+ pendingReferences.add(lex["#"])
360
462
 
361
463
  const sendResult = send(request)
362
464
  if (sendResult && sendResult.err) {
@@ -387,7 +489,7 @@ const Wire = opt => {
387
489
  // here using the api. This is ok because correct timestamps should be
388
490
  // used whereas wire spec needs to handle clock skew for updates
389
491
  // across the network.
390
- const update = await Ham.mix(data, graph, opt.secure, listen)
492
+ const update = await Ham.mix(data, graph, hasNode, opt.secure, listen)
391
493
  if (Object.keys(update.now).length === 0) {
392
494
  // No updates, still respond to callback.
393
495
  if (cb) cb(null)
@@ -461,7 +563,7 @@ const Wire = opt => {
461
563
  }
462
564
  }
463
565
 
464
- if (isNode) {
566
+ if (isNode && (opt.wss || opt.server || opt.port != null)) {
465
567
  let wss = opt.wss
466
568
  // Node's websocket server provides clients as an array, whereas
467
569
  // mock-sockets provides clients as a function that returns an array.
@@ -469,7 +571,7 @@ const Wire = opt => {
469
571
  if (!wss) {
470
572
  const config = opt.server
471
573
  ? {server: opt.server}
472
- : {port: opt.port || 8765}
574
+ : {port: opt.port}
473
575
  wss = new wsModule.WebSocketServer(config)
474
576
  clients = () => wss.clients
475
577
  }
@@ -479,22 +581,21 @@ const Wire = opt => {
479
581
  const msg = JSON.parse(data)
480
582
  const trackId = msg["#"]
481
583
  if (trackId && pendingTimeouts.has(trackId)) {
482
- const timeoutConfig = pendingTimeouts.get(trackId)
584
+ const lex = pendingTimeouts.get(trackId)
483
585
  pendingTimeouts.delete(trackId)
484
586
 
485
- // Respond to callback with null if no response.
486
- timeoutConfig.timeoutId = setTimeout(() => {
587
+ setTimeout(() => {
487
588
  const cb = queue[trackId]
488
589
  if (cb) {
489
- const id = timeoutConfig.lex["#"]
590
+ const id = lex["#"]
490
591
  const ack = {[id]: null}
491
- if (typeof timeoutConfig.lex["."] === "string") {
492
- ack[id] = {[timeoutConfig.lex["."]]: null}
592
+ if (typeof lex["."] === "string") {
593
+ ack[id] = {[lex["."]]: null}
493
594
  }
494
595
  cb({put: ack})
495
596
  delete queue[trackId]
496
597
  }
497
- }, timeoutConfig.wait)
598
+ }, isTestEnv ? 100 : 10000)
498
599
  }
499
600
 
500
601
  clients().forEach(client => {
@@ -628,6 +729,24 @@ const Wire = opt => {
628
729
  let queueProcessor = null
629
730
  const maxQueueLength = opt.maxQueueLength || 10000
630
731
 
732
+ const startResponseTimeout = (trackId, lex, delay = isTestEnv ? 100 : 10000) => {
733
+ return setTimeout(() => {
734
+ offlineTimeouts.delete(trackId)
735
+ pendingTimeouts.delete(trackId)
736
+ const cb = queue[trackId]
737
+ // No-op on successful send as callback is removed from the queue.
738
+ if (cb) {
739
+ const id = lex["#"]
740
+ const ack = {[id]: null}
741
+ if (typeof lex["."] === "string") {
742
+ ack[id] = {[lex["."]]: null}
743
+ }
744
+ cb({put: ack})
745
+ delete queue[trackId]
746
+ }
747
+ }, delay)
748
+ }
749
+
631
750
  const processQueue = () => {
632
751
  if (messageQueue.length === 0) {
633
752
  queueProcessor = null
@@ -655,30 +774,27 @@ const Wire = opt => {
655
774
  const data = messageQueue[0]
656
775
  const sent = sendToPeers(data)
657
776
 
658
- if (sent) {
659
- // Only remove from queue if successfully sent
660
- messageQueue.shift()
777
+ const msg = JSON.parse(data)
778
+ const trackId = msg["#"]
661
779
 
662
- // Start timeout for this message now that it's been sent
663
- const msg = JSON.parse(data)
664
- const trackId = msg["#"]
665
- if (trackId && pendingTimeouts.has(trackId)) {
666
- const timeoutConfig = pendingTimeouts.get(trackId)
667
- pendingTimeouts.delete(trackId)
780
+ // Drop GET messages with no pending callback — these were already served
781
+ // from local store so no caller is waiting for the wire response.
782
+ const isStaleCheckMessage = !sent && msg.get && !pendingTimeouts.has(trackId)
668
783
 
669
- // Respond to callback with null if no response.
670
- timeoutConfig.timeoutId = setTimeout(() => {
671
- const cb = queue[trackId]
672
- if (cb) {
673
- const id = timeoutConfig.lex["#"]
674
- const ack = {[id]: null}
675
- if (typeof timeoutConfig.lex["."] === "string") {
676
- ack[id] = {[timeoutConfig.lex["."]]: null}
677
- }
678
- cb({put: ack})
679
- delete queue[trackId]
680
- }
681
- }, timeoutConfig.wait)
784
+ if (sent || isStaleCheckMessage) {
785
+ messageQueue.shift()
786
+ }
787
+
788
+ if (trackId && pendingTimeouts.has(trackId)) {
789
+ const lex = pendingTimeouts.get(trackId)
790
+ if (sent) {
791
+ if (offlineTimeouts.has(trackId)) {
792
+ clearTimeout(offlineTimeouts.get(trackId))
793
+ offlineTimeouts.delete(trackId)
794
+ }
795
+ startResponseTimeout(trackId, lex)
796
+ } else if (!offlineTimeouts.has(trackId)) {
797
+ offlineTimeouts.set(trackId, startResponseTimeout(trackId, lex, 0))
682
798
  }
683
799
  }
684
800
 
@@ -776,39 +892,53 @@ const Wire = opt => {
776
892
  dup.track(msg["#"])
777
893
  if (msg.get) get(msg, send)
778
894
  if (msg.put) {
779
- // Handle selective storage for client WebSocket messages
780
- const filteredPut = {}
895
+ // Pre-pass: expand pendingReferences to include rels of every soul
896
+ // that will be stored in this batch. This handles the case where a
897
+ // server batch contains both a parent soul (e.g. day_soul with item
898
+ // rels) and its child souls (item_souls), in any order — without the
899
+ // pre-pass, a child soul that appears before its parent in the batch
900
+ // would be missed because pendingReferences hasn't been updated yet.
901
+ const hasSoulMap = new Map()
781
902
  for (const [soul, node] of Object.entries(msg.put)) {
782
- let shouldStore = false
783
- // Case 1: We already have this soul - always update
784
- if (await hasSoul(soul)) {
785
- shouldStore = true
786
- // Check if this update adds new references
787
- if (node && typeof node === "object") {
788
- for (const [key, value] of Object.entries(node)) {
789
- const soulId = utils.rel.is(value)
790
- if (soulId) {
791
- // Add referenced soul to pending list
792
- pendingReferences.add(soulId)
793
- }
794
- }
795
- }
903
+ const known = await hasSoul(soul)
904
+ hasSoulMap.set(soul, known)
905
+ if ((!known && !pendingReferences.has(soul)) || !node || typeof node !== "object") continue
906
+ for (const [, value] of Object.entries(node)) {
907
+ const soulId = utils.rel.is(value)
908
+ if (soulId) pendingReferences.add(soulId)
796
909
  }
797
- // Case 2: This soul was referenced by something we have
910
+ }
911
+
912
+ // Main pass: filter using cached hasSoul results and the now-complete
913
+ // pendingReferences set.
914
+ const filteredPut = {}
915
+ const pendingToDelete = []
916
+ for (const [soul, node] of Object.entries(msg.put)) {
917
+ let shouldStore = hasSoulMap.get(soul)
798
918
  if (pendingReferences.has(soul)) {
799
919
  shouldStore = true
800
- pendingReferences.delete(soul) // Got it, remove from pending
801
- }
802
- if (shouldStore) {
803
- filteredPut[soul] = node
920
+ // Don't delete from pendingReferences yet — concurrent onmessage
921
+ // handlers for the same soul may have yielded at await hasSoul()
922
+ // before this pass ran. Deleting here would cause those handlers
923
+ // to see shouldStore=false and skip storing the soul's data.
924
+ // Instead, defer the delete until after put() updates the graph,
925
+ // so any concurrent handler can find the soul via hasSoul(graph).
926
+ pendingToDelete.push(soul)
804
927
  }
928
+ if (shouldStore) filteredPut[soul] = node
805
929
  }
930
+
806
931
  // Store filtered data if any
807
932
  if (Object.keys(filteredPut).length > 0) {
808
933
  // Create filtered message for Ham.mix with original message ID
809
934
  const filteredMsg = {put: filteredPut, "#": msg["#"]}
810
935
  await put(filteredMsg, send)
811
936
  }
937
+
938
+ // Safe to delete now — put() has run Ham.mix which updates graph
939
+ // in-place, so hasSoul() will return true for these souls going
940
+ // forward, making pendingReferences no longer needed for them.
941
+ pendingToDelete.forEach(soul => pendingReferences.delete(soul))
812
942
  }
813
943
 
814
944
  const id = msg["@"]
@@ -0,0 +1,78 @@
1
+ /**
2
+ * SeaArray - Extended Array with custom toString for encoding support
3
+ * Supports utf8, hex, and base64 encoding
4
+ */
5
+
6
+ if (typeof btoa === "undefined") {
7
+ globalThis.btoa = (data: string): string =>
8
+ Buffer.from(data, "binary").toString("base64")
9
+ globalThis.atob = (data: string): string =>
10
+ Buffer.from(data, "base64").toString("binary")
11
+ }
12
+
13
+ type Encoding = "utf8" | "hex" | "base64"
14
+
15
+ /**
16
+ * SeaArray extends Array to provide encoding capabilities
17
+ */
18
+ class SeaArray extends Array<number> {
19
+ /**
20
+ * Convert array to string with specified encoding
21
+ * @param enc - Encoding type (utf8, hex, or base64)
22
+ * @param start - Start index (default: 0)
23
+ * @param end - End index (default: array length)
24
+ */
25
+ override toString(enc?: Encoding, start?: number, end?: number): string {
26
+ const encoding = enc || "utf8"
27
+ const startIdx = start || 0
28
+ const endIdx = end ? Math.min(Math.max(end, startIdx), this.length) : this.length
29
+
30
+ if (encoding === "hex") {
31
+ const buf = new Uint8Array(this.slice(startIdx, endIdx))
32
+ return Array.from(buf, byte => byte.toString(16).padStart(2, "0")).join("")
33
+ }
34
+
35
+ if (encoding === "utf8") {
36
+ return Array.from({ length: endIdx - startIdx }, (_, i) => {
37
+ const charCode = this[i + startIdx]
38
+ // Use unicode replacement character for invalid character codes
39
+ if (
40
+ charCode === undefined ||
41
+ charCode < 0 ||
42
+ charCode > 0x10ffff
43
+ ) {
44
+ return String.fromCharCode(0xfffd)
45
+ }
46
+ return String.fromCharCode(charCode)
47
+ }).join("")
48
+ }
49
+
50
+ if (encoding === "base64") {
51
+ const utf8String = Array.from({ length: endIdx - startIdx }, (_, i) => {
52
+ const charCode = this[i + startIdx]
53
+ if (charCode === undefined || charCode < 0 || charCode > 255) {
54
+ return String.fromCharCode(0xfffd)
55
+ }
56
+ return String.fromCharCode(charCode)
57
+ }).join("")
58
+ return btoa(utf8String)
59
+ }
60
+
61
+ // Default to utf8
62
+ return this.toString("utf8", startIdx, endIdx)
63
+ }
64
+
65
+ /**
66
+ * Create SeaArray from iterable or array-like
67
+ */
68
+ static override from<T>(
69
+ arrayLike: ArrayLike<T> | Iterable<T>
70
+ ): SeaArray {
71
+ const arr = Array.from(arrayLike) as number[]
72
+ const seaArray = new SeaArray(...arr)
73
+ return seaArray
74
+ }
75
+ }
76
+
77
+ export default SeaArray
78
+