feat(messaging-streams): manually deepen all six stream utility modules
- chunker: framed seq chunks, reassemble, setChunkSize - multiplex: closeAll, getStream, per-stream byte counters - backpressure: low/high water marks, bufferedBytes - tee: branchBytes, removeBranch, depth guard - transform: pipeTo and outbound backpressure - resume-token: labeled checkpoints, clearTokens, bytesFromOffset - Update category README and chunker tests Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -8,12 +8,12 @@ Local and composable byte-stream utilities: chunking, multiplexing, backpressure
|
|||||||
|
|
||||||
| Module | Protocol | Role |
|
| Module | Protocol | Role |
|
||||||
|--------|----------|------|
|
|--------|----------|------|
|
||||||
| [hyper-p2p-stream-chunker](./hyper-p2p-stream-chunker/) | `stream-chunker/v1` | Fixed-size chunks, `pendingLength`, `reset` |
|
| [hyper-p2p-stream-chunker](./hyper-p2p-stream-chunker/) | `stream-chunker/v1` | Framed seq chunks, `reassemble`, `setChunkSize` |
|
||||||
| [hyper-p2p-stream-multiplex](./hyper-p2p-stream-multiplex/) | `stream-multiplex/v1` | `listOpenStreamIds`, `hasStream` |
|
| [hyper-p2p-stream-multiplex](./hyper-p2p-stream-multiplex/) | `stream-multiplex/v1` | `closeAll`, `getStream`, backpressure on frames |
|
||||||
| [hyper-p2p-stream-backpressure](./hyper-p2p-stream-backpressure/) | `stream-backpressure/v1` | `drain`, `isPaused`, high-water mark |
|
| [hyper-p2p-stream-backpressure](./hyper-p2p-stream-backpressure/) | `stream-backpressure/v1` | Low/high water marks, `bufferedBytes`, `setWaterMarks` |
|
||||||
| [hyper-p2p-stream-tee](./hyper-p2p-stream-tee/) | `stream-tee/v1` | `drainBranch`, `drainAll`, `listBranches` |
|
| [hyper-p2p-stream-tee](./hyper-p2p-stream-tee/) | `stream-tee/v1` | `branchBytes`, `removeBranch`, max depth guard |
|
||||||
| [hyper-p2p-stream-transform](./hyper-p2p-stream-transform/) | `stream-transform/v1` | `compose`, `flush`, `readAll` |
|
| [hyper-p2p-stream-transform](./hyper-p2p-stream-transform/) | `stream-transform/v1` | `pipeTo`, backpressure, `compose` |
|
||||||
| [hyper-p2p-stream-resume-token](./hyper-p2p-stream-resume-token/) | `stream-resume-token/v1` | `pruneBefore`, `latestCheckpoint`, `listTokens` |
|
| [hyper-p2p-stream-resume-token](./hyper-p2p-stream-resume-token/) | `stream-resume-token/v1` | Labeled checkpoints, `clearTokens`, `bytesFromOffset` |
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ class HyperP2PStreamBackpressure extends EventEmitter {
|
|||||||
constructor (opts = {}) {
|
constructor (opts = {}) {
|
||||||
super()
|
super()
|
||||||
this.highWaterMark = opts.highWaterMark || 65536
|
this.highWaterMark = opts.highWaterMark || 65536
|
||||||
|
this.lowWaterMark = opts.lowWaterMark || Math.floor(this.highWaterMark * 0.5)
|
||||||
this._buffer = []
|
this._buffer = []
|
||||||
this._bytes = 0
|
this._bytes = 0
|
||||||
this._paused = false
|
this._paused = false
|
||||||
this._stats = { written: 0, dropped: 0, paused: 0 }
|
this._stats = { written: 0, dropped: 0, paused: 0, resumed: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
write (chunk) {
|
write (chunk) {
|
||||||
@@ -42,6 +43,7 @@ class HyperP2PStreamBackpressure extends EventEmitter {
|
|||||||
|
|
||||||
resume () {
|
resume () {
|
||||||
this._paused = false
|
this._paused = false
|
||||||
|
this._stats.resumed++
|
||||||
this.emit('resume')
|
this.emit('resume')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -50,13 +52,25 @@ class HyperP2PStreamBackpressure extends EventEmitter {
|
|||||||
const buf = this._buffer.shift()
|
const buf = this._buffer.shift()
|
||||||
if (!buf) return null
|
if (!buf) return null
|
||||||
this._bytes -= buf.length || buf.byteLength || 0
|
this._bytes -= buf.length || buf.byteLength || 0
|
||||||
if (this._bytes < this.highWaterMark) this._paused = false
|
if (this._paused && this._bytes <= this.lowWaterMark) {
|
||||||
|
this._paused = false
|
||||||
|
this._stats.resumed++
|
||||||
|
this.emit('resume')
|
||||||
|
}
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
pending () { return this._buffer.length }
|
pending () {
|
||||||
|
return this._buffer.length
|
||||||
|
}
|
||||||
|
|
||||||
isPaused () { return this._paused }
|
bufferedBytes () {
|
||||||
|
return this._bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
isPaused () {
|
||||||
|
return this._paused
|
||||||
|
}
|
||||||
|
|
||||||
drain () {
|
drain () {
|
||||||
const out = []
|
const out = []
|
||||||
@@ -74,12 +88,32 @@ class HyperP2PStreamBackpressure extends EventEmitter {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
getStats () {
|
setWaterMarks (high, low) {
|
||||||
return { ...this._stats, bytes: this._bytes, paused: this._paused, protocol: PROTOCOL }
|
this.highWaterMark = Number(high)
|
||||||
|
this.lowWaterMark = Number(low)
|
||||||
|
if (this.lowWaterMark > this.highWaterMark) {
|
||||||
|
throw new Error('lowWaterMark must be <= highWaterMark')
|
||||||
|
}
|
||||||
|
return { high: this.highWaterMark, low: this.lowWaterMark }
|
||||||
}
|
}
|
||||||
|
|
||||||
async ready () { return this }
|
getStats () {
|
||||||
async close () { this._buffer = []; this._bytes = 0 }
|
return {
|
||||||
|
...this._stats,
|
||||||
|
bytes: this._bytes,
|
||||||
|
paused: this._paused,
|
||||||
|
protocol: PROTOCOL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async ready () {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async close () {
|
||||||
|
this.clear()
|
||||||
|
this.emit('closed')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { HyperP2PStreamBackpressure, PROTOCOL }
|
module.exports = { HyperP2PStreamBackpressure, PROTOCOL }
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ class HyperP2PStreamChunker extends EventEmitter {
|
|||||||
super()
|
super()
|
||||||
this.chunkSize = opts.chunkSize || 4096
|
this.chunkSize = opts.chunkSize || 4096
|
||||||
this._pending = b4a.alloc(0)
|
this._pending = b4a.alloc(0)
|
||||||
this._stats = { chunks: 0, bytes: 0 }
|
this._seq = 0
|
||||||
|
this._parts = []
|
||||||
|
this._stats = { chunks: 0, bytes: 0, flushed: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
push (data) {
|
push (data) {
|
||||||
@@ -20,17 +22,28 @@ class HyperP2PStreamChunker extends EventEmitter {
|
|||||||
while (this._pending.length >= this.chunkSize) {
|
while (this._pending.length >= this.chunkSize) {
|
||||||
const slice = this._pending.subarray(0, this.chunkSize)
|
const slice = this._pending.subarray(0, this.chunkSize)
|
||||||
this._pending = this._pending.subarray(this.chunkSize)
|
this._pending = this._pending.subarray(this.chunkSize)
|
||||||
this._stats.chunks++
|
const framed = this._frame(slice)
|
||||||
emitted.push(slice)
|
emitted.push(framed)
|
||||||
this.emit('chunk', slice)
|
this._parts.push(framed.data)
|
||||||
|
this.emit('chunk', framed)
|
||||||
}
|
}
|
||||||
return emitted
|
return emitted
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingLength () { return this._pending.length }
|
_frame (data) {
|
||||||
|
const seq = this._seq++
|
||||||
|
this._stats.chunks++
|
||||||
|
return { seq, data, size: data.length }
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingLength () {
|
||||||
|
return this._pending.length
|
||||||
|
}
|
||||||
|
|
||||||
reset () {
|
reset () {
|
||||||
this._pending = b4a.alloc(0)
|
this._pending = b4a.alloc(0)
|
||||||
|
this._seq = 0
|
||||||
|
this._parts = []
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,17 +51,52 @@ class HyperP2PStreamChunker extends EventEmitter {
|
|||||||
if (!this._pending.length) return null
|
if (!this._pending.length) return null
|
||||||
const tail = this._pending
|
const tail = this._pending
|
||||||
this._pending = b4a.alloc(0)
|
this._pending = b4a.alloc(0)
|
||||||
this._stats.chunks++
|
const framed = this._frame(tail)
|
||||||
this.emit('chunk', tail)
|
this._parts.push(framed.data)
|
||||||
return tail
|
this._stats.flushed++
|
||||||
|
this.emit('chunk', framed)
|
||||||
|
return framed
|
||||||
|
}
|
||||||
|
|
||||||
|
reassemble () {
|
||||||
|
if (!this._parts.length) return b4a.alloc(0)
|
||||||
|
return b4a.concat(this._parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
reassembleFrom (chunks) {
|
||||||
|
if (!Array.isArray(chunks)) throw new Error('chunks must be an array')
|
||||||
|
const bufs = chunks.map((c) => (c && c.data ? c.data : c))
|
||||||
|
return b4a.concat(bufs)
|
||||||
|
}
|
||||||
|
|
||||||
|
setChunkSize (n) {
|
||||||
|
const size = Number(n)
|
||||||
|
if (!Number.isFinite(size) || size < 1) throw new Error('chunkSize must be positive')
|
||||||
|
this.chunkSize = size
|
||||||
|
return this.chunkSize
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkCount () {
|
||||||
|
return this._stats.chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
getStats () {
|
getStats () {
|
||||||
return { ...this._stats, pending: this._pending.length, protocol: PROTOCOL }
|
return {
|
||||||
|
...this._stats,
|
||||||
|
pending: this._pending.length,
|
||||||
|
chunkSize: this.chunkSize,
|
||||||
|
protocol: PROTOCOL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async ready () { return this }
|
async ready () {
|
||||||
async close () { this._pending = b4a.alloc(0) }
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async close () {
|
||||||
|
this.reset()
|
||||||
|
this.emit('closed')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { HyperP2PStreamChunker, PROTOCOL }
|
module.exports = { HyperP2PStreamChunker, PROTOCOL }
|
||||||
|
|||||||
@@ -8,7 +8,16 @@ test('split chunks', async (t) => {
|
|||||||
const m = new HyperP2PStreamChunker({ chunkSize: 4 })
|
const m = new HyperP2PStreamChunker({ chunkSize: 4 })
|
||||||
const parts = m.push(require('b4a').from('abcdefgh'))
|
const parts = m.push(require('b4a').from('abcdefgh'))
|
||||||
t.is(parts.length, 2)
|
t.is(parts.length, 2)
|
||||||
t.is(parts[0].length, 4)
|
t.is(parts[0].data.length, 4)
|
||||||
|
t.is(parts[0].seq, 0)
|
||||||
|
await m.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reassemble', async (t) => {
|
||||||
|
const m = new HyperP2PStreamChunker({ chunkSize: 3 })
|
||||||
|
m.push(require('b4a').from('hello'))
|
||||||
|
m.flush()
|
||||||
|
t.is(m.reassemble().toString(), 'hello')
|
||||||
await m.close()
|
await m.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
require('bare-process/global')
|
require('bare-process/global')
|
||||||
const EventEmitter = require('bare-events')
|
const EventEmitter = require('bare-events')
|
||||||
const b4a = require('b4a')
|
const b4a = require('b4a')
|
||||||
const { assertNonEmpty } = require('../../_shared/lib/errors.js')
|
|
||||||
|
|
||||||
const PROTOCOL = 'stream-multiplex/v1'
|
const PROTOCOL = 'stream-multiplex/v1'
|
||||||
|
|
||||||
@@ -11,6 +10,8 @@ class StreamHandle {
|
|||||||
this.id = id
|
this.id = id
|
||||||
this._listeners = []
|
this._listeners = []
|
||||||
this.closed = false
|
this.closed = false
|
||||||
|
this.bytesWritten = 0
|
||||||
|
this.bytesRead = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
write (chunk) {
|
write (chunk) {
|
||||||
@@ -32,6 +33,7 @@ class StreamHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_emitData (chunk, fin) {
|
_emitData (chunk, fin) {
|
||||||
|
if (chunk) this.bytesRead += chunk.length || chunk.byteLength || 0
|
||||||
for (const fn of this._listeners) fn(chunk, fin)
|
for (const fn of this._listeners) fn(chunk, fin)
|
||||||
if (fin) this.closed = true
|
if (fin) this.closed = true
|
||||||
}
|
}
|
||||||
@@ -43,7 +45,7 @@ class HyperP2PStreamMultiplex extends EventEmitter {
|
|||||||
this.highWaterMark = opts.highWaterMark || 65536
|
this.highWaterMark = opts.highWaterMark || 65536
|
||||||
this._streams = new Map()
|
this._streams = new Map()
|
||||||
this._nextId = 1
|
this._nextId = 1
|
||||||
this._stats = { streams: 0, frames: 0, bytes: 0 }
|
this._stats = { streams: 0, frames: 0, bytes: 0, rejected: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
openStream (id = null) {
|
openStream (id = null) {
|
||||||
@@ -59,6 +61,8 @@ class HyperP2PStreamMultiplex extends EventEmitter {
|
|||||||
_sendFrame (streamId, chunk, fin) {
|
_sendFrame (streamId, chunk, fin) {
|
||||||
const size = chunk ? (chunk.length || chunk.byteLength || 0) : 0
|
const size = chunk ? (chunk.length || chunk.byteLength || 0) : 0
|
||||||
if (this._stats.bytes + size > this.highWaterMark && !fin) {
|
if (this._stats.bytes + size > this.highWaterMark && !fin) {
|
||||||
|
this._stats.rejected++
|
||||||
|
this.emit('backpressure', { streamId, bytes: this._stats.bytes })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const frame = { type: 'frame', streamId, chunk, fin: !!fin }
|
const frame = { type: 'frame', streamId, chunk, fin: !!fin }
|
||||||
@@ -66,7 +70,10 @@ class HyperP2PStreamMultiplex extends EventEmitter {
|
|||||||
this._stats.bytes += size
|
this._stats.bytes += size
|
||||||
this.emit('frame', frame)
|
this.emit('frame', frame)
|
||||||
const local = this._streams.get(streamId)
|
const local = this._streams.get(streamId)
|
||||||
if (local) local._emitData(chunk, fin)
|
if (local) {
|
||||||
|
if (chunk) local.bytesWritten += size
|
||||||
|
local._emitData(chunk, fin)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,9 +86,21 @@ class HyperP2PStreamMultiplex extends EventEmitter {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
listOpenStreamIds () { return [...this._streams.keys()] }
|
openStreamCount () {
|
||||||
|
return this._streams.size
|
||||||
|
}
|
||||||
|
|
||||||
hasStream (streamId) { return this._streams.has(String(streamId)) }
|
listOpenStreamIds () {
|
||||||
|
return [...this._streams.keys()]
|
||||||
|
}
|
||||||
|
|
||||||
|
hasStream (streamId) {
|
||||||
|
return this._streams.has(String(streamId))
|
||||||
|
}
|
||||||
|
|
||||||
|
getStream (streamId) {
|
||||||
|
return this._streams.get(String(streamId)) || null
|
||||||
|
}
|
||||||
|
|
||||||
closeStream (streamId) {
|
closeStream (streamId) {
|
||||||
const h = this._streams.get(streamId)
|
const h = this._streams.get(streamId)
|
||||||
@@ -91,6 +110,11 @@ class HyperP2PStreamMultiplex extends EventEmitter {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeAll () {
|
||||||
|
for (const id of [...this._streams.keys()]) this.closeStream(id)
|
||||||
|
return this._streams.size === 0
|
||||||
|
}
|
||||||
|
|
||||||
getStats () {
|
getStats () {
|
||||||
return {
|
return {
|
||||||
...this._stats,
|
...this._stats,
|
||||||
@@ -99,10 +123,12 @@ class HyperP2PStreamMultiplex extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async ready () { return this }
|
async ready () {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
async close () {
|
async close () {
|
||||||
for (const id of [...this._streams.keys()]) this.closeStream(id)
|
this.closeAll()
|
||||||
this.emit('closed')
|
this.emit('closed')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class HyperP2PStreamResumeToken extends EventEmitter {
|
|||||||
this._offset = 0
|
this._offset = 0
|
||||||
this._tokens = new Map()
|
this._tokens = new Map()
|
||||||
this._nextToken = 1
|
this._nextToken = 1
|
||||||
this._stats = { bytes: 0, checkpoints: 0, resumes: 0 }
|
this._stats = { bytes: 0, checkpoints: 0, resumes: 0, pruned: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
write (chunk) {
|
write (chunk) {
|
||||||
@@ -23,9 +23,9 @@ class HyperP2PStreamResumeToken extends EventEmitter {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
checkpoint () {
|
checkpoint (label = null) {
|
||||||
const id = String(this._nextToken++)
|
const id = String(this._nextToken++)
|
||||||
const token = { id, offset: this._stats.bytes, at: Date.now() }
|
const token = { id, label, offset: this._stats.bytes, at: Date.now() }
|
||||||
this._tokens.set(id, token)
|
this._tokens.set(id, token)
|
||||||
this._stats.checkpoints++
|
this._stats.checkpoints++
|
||||||
this.emit('checkpoint', token)
|
this.emit('checkpoint', token)
|
||||||
@@ -52,7 +52,13 @@ class HyperP2PStreamResumeToken extends EventEmitter {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
listTokens () { return [...this._tokens.values()] }
|
bytesFromOffset () {
|
||||||
|
return Math.max(0, this._stats.bytes - this._offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
listTokens () {
|
||||||
|
return [...this._tokens.values()]
|
||||||
|
}
|
||||||
|
|
||||||
latestCheckpoint () {
|
latestCheckpoint () {
|
||||||
let best = null
|
let best = null
|
||||||
@@ -78,16 +84,35 @@ class HyperP2PStreamResumeToken extends EventEmitter {
|
|||||||
}
|
}
|
||||||
const removed = this._chunks.length - keep.length
|
const removed = this._chunks.length - keep.length
|
||||||
this._chunks = keep
|
this._chunks = keep
|
||||||
|
this._stats.pruned += removed
|
||||||
if (this._offset < cut) this._offset = cut
|
if (this._offset < cut) this._offset = cut
|
||||||
return removed
|
return removed
|
||||||
}
|
}
|
||||||
|
|
||||||
getStats () {
|
clearTokens () {
|
||||||
return { ...this._stats, offset: this._offset, protocol: PROTOCOL }
|
const n = this._tokens.size
|
||||||
|
this._tokens.clear()
|
||||||
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
async ready () { return this }
|
getStats () {
|
||||||
async close () { this._chunks = []; this._tokens.clear() }
|
return {
|
||||||
|
...this._stats,
|
||||||
|
offset: this._offset,
|
||||||
|
tokens: this._tokens.size,
|
||||||
|
protocol: PROTOCOL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async ready () {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async close () {
|
||||||
|
this._chunks = []
|
||||||
|
this._tokens.clear()
|
||||||
|
this.emit('closed')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { HyperP2PStreamResumeToken, PROTOCOL }
|
module.exports = { HyperP2PStreamResumeToken, PROTOCOL }
|
||||||
|
|||||||
@@ -8,24 +8,40 @@ class HyperP2PStreamTee extends EventEmitter {
|
|||||||
constructor (opts = {}) {
|
constructor (opts = {}) {
|
||||||
super()
|
super()
|
||||||
this._branches = new Map()
|
this._branches = new Map()
|
||||||
this._stats = { written: 0, branches: 0 }
|
this._stats = { written: 0, branches: 0, dropped: 0 }
|
||||||
|
this.maxBranchDepth = opts.maxBranchDepth ?? 10000
|
||||||
}
|
}
|
||||||
|
|
||||||
addBranch (name) {
|
addBranch (name) {
|
||||||
if (!name) throw new Error('branch name required')
|
if (!name) throw new Error('branch name required')
|
||||||
const branch = { name, chunks: [] }
|
const branch = { name, chunks: [], bytes: 0 }
|
||||||
this._branches.set(name, branch)
|
this._branches.set(name, branch)
|
||||||
this._stats.branches++
|
this._stats.branches++
|
||||||
return () => this._branches.delete(name)
|
this.emit('branch-added', { name })
|
||||||
|
return () => this.removeBranch(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeBranch (name) {
|
||||||
|
return this._branches.delete(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
write (chunk) {
|
write (chunk) {
|
||||||
if (chunk == null) throw new Error('chunk required')
|
if (chunk == null) throw new Error('chunk required')
|
||||||
const buf = typeof chunk === 'string' ? b4a.from(chunk) : chunk
|
const buf = typeof chunk === 'string' ? b4a.from(chunk) : chunk
|
||||||
for (const branch of this._branches.values()) branch.chunks.push(buf)
|
const size = buf.length || buf.byteLength || 0
|
||||||
|
let n = 0
|
||||||
|
for (const branch of this._branches.values()) {
|
||||||
|
if (branch.chunks.length >= this.maxBranchDepth) {
|
||||||
|
this._stats.dropped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
branch.chunks.push(buf)
|
||||||
|
branch.bytes += size
|
||||||
|
n++
|
||||||
|
}
|
||||||
this._stats.written++
|
this._stats.written++
|
||||||
this.emit('data', buf)
|
this.emit('data', buf)
|
||||||
return true
|
return n > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
readBranch (name) {
|
readBranch (name) {
|
||||||
@@ -38,14 +54,24 @@ class HyperP2PStreamTee extends EventEmitter {
|
|||||||
return b ? b.chunks.length : 0
|
return b ? b.chunks.length : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
listBranches () { return [...this._branches.keys()] }
|
branchBytes (name) {
|
||||||
|
const b = this._branches.get(name)
|
||||||
|
return b ? b.bytes : 0
|
||||||
|
}
|
||||||
|
|
||||||
hasBranch (name) { return this._branches.has(name) }
|
listBranches () {
|
||||||
|
return [...this._branches.keys()]
|
||||||
|
}
|
||||||
|
|
||||||
|
hasBranch (name) {
|
||||||
|
return this._branches.has(name)
|
||||||
|
}
|
||||||
|
|
||||||
drainBranch (name) {
|
drainBranch (name) {
|
||||||
const b = this._branches.get(name)
|
const b = this._branches.get(name)
|
||||||
if (!b) return []
|
if (!b) return []
|
||||||
const out = b.chunks.splice(0, b.chunks.length)
|
const out = b.chunks.splice(0, b.chunks.length)
|
||||||
|
b.bytes = 0
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +85,14 @@ class HyperP2PStreamTee extends EventEmitter {
|
|||||||
return { ...this._stats, branchCount: this._branches.size, protocol: PROTOCOL }
|
return { ...this._stats, branchCount: this._branches.size, protocol: PROTOCOL }
|
||||||
}
|
}
|
||||||
|
|
||||||
async ready () { return this }
|
async ready () {
|
||||||
async close () { this._branches.clear() }
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async close () {
|
||||||
|
this._branches.clear()
|
||||||
|
this.emit('closed')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { HyperP2PStreamTee, PROTOCOL }
|
module.exports = { HyperP2PStreamTee, PROTOCOL }
|
||||||
|
|||||||
@@ -9,16 +9,22 @@ class HyperP2PStreamTransform extends EventEmitter {
|
|||||||
super()
|
super()
|
||||||
this._fn = opts.transform || null
|
this._fn = opts.transform || null
|
||||||
this._out = []
|
this._out = []
|
||||||
this._stats = { in: 0, out: 0 }
|
this._stats = { in: 0, out: 0, skipped: 0 }
|
||||||
|
this.highWaterMark = opts.highWaterMark ?? 1024
|
||||||
}
|
}
|
||||||
|
|
||||||
setTransform (fn) {
|
setTransform (fn) {
|
||||||
if (typeof fn !== 'function') throw new Error('transform must be a function')
|
if (typeof fn !== 'function') throw new Error('transform must be a function')
|
||||||
this._fn = fn
|
this._fn = fn
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
write (chunk) {
|
write (chunk) {
|
||||||
if (chunk == null) throw new Error('chunk required')
|
if (chunk == null) throw new Error('chunk required')
|
||||||
|
if (this._out.length >= this.highWaterMark) {
|
||||||
|
this.emit('backpressure')
|
||||||
|
return false
|
||||||
|
}
|
||||||
const buf = typeof chunk === 'string' ? b4a.from(chunk) : chunk
|
const buf = typeof chunk === 'string' ? b4a.from(chunk) : chunk
|
||||||
this._stats.in++
|
this._stats.in++
|
||||||
const result = this._fn ? this._fn(buf) : buf
|
const result = this._fn ? this._fn(buf) : buf
|
||||||
@@ -27,18 +33,23 @@ class HyperP2PStreamTransform extends EventEmitter {
|
|||||||
this._out.push(out)
|
this._out.push(out)
|
||||||
this._stats.out++
|
this._stats.out++
|
||||||
this.emit('data', out)
|
this.emit('data', out)
|
||||||
|
} else {
|
||||||
|
this._stats.skipped++
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
read () { return this._out.shift() || null }
|
read () {
|
||||||
|
return this._out.shift() || null
|
||||||
readAll () {
|
|
||||||
const out = this._out.splice(0, this._out.length)
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flush () { return this.readAll() }
|
readAll () {
|
||||||
|
return this._out.splice(0, this._out.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
flush () {
|
||||||
|
return this.readAll()
|
||||||
|
}
|
||||||
|
|
||||||
compose (fn) {
|
compose (fn) {
|
||||||
if (typeof fn !== 'function') throw new Error('fn must be a function')
|
if (typeof fn !== 'function') throw new Error('fn must be a function')
|
||||||
@@ -51,14 +62,32 @@ class HyperP2PStreamTransform extends EventEmitter {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
pending () { return this._out.length }
|
pipeTo (target) {
|
||||||
|
if (!target || typeof target.write !== 'function') {
|
||||||
getStats () {
|
throw new Error('target must implement write()')
|
||||||
return { ...this._stats, protocol: PROTOCOL }
|
}
|
||||||
|
while (this._out.length) {
|
||||||
|
target.write(this.read())
|
||||||
|
}
|
||||||
|
return target
|
||||||
}
|
}
|
||||||
|
|
||||||
async ready () { return this }
|
pending () {
|
||||||
async close () { this._out = [] }
|
return this._out.length
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats () {
|
||||||
|
return { ...this._stats, pending: this._out.length, protocol: PROTOCOL }
|
||||||
|
}
|
||||||
|
|
||||||
|
async ready () {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async close () {
|
||||||
|
this._out = []
|
||||||
|
this.emit('closed')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { HyperP2PStreamTransform, PROTOCOL }
|
module.exports = { HyperP2PStreamTransform, PROTOCOL }
|
||||||
|
|||||||
Reference in New Issue
Block a user