///|
async test "quick start pipeline" {
@async.with_task_group(root => {
// Create a pair of connected endpoints that speak the Reader/Writer protocols.
// Data written to the write end (`writer`) can be read from the read end (`reader`)
let (reader, writer) = @io.pipe()
// Spawn a background writer that sends a UTF-8 string in three chunks.
root.spawn_bg(() => {
defer writer.close()
writer.write(b"Hello, ")
@async.sleep(10)
writer.write(b"MoonBit")
@async.sleep(10)
writer.write(b"!\n")
})
// Read everything that arrives and print it as text.
defer reader.close()
let message = reader.read_all().text()
inspect(message, content="Hello, MoonBit!\n")
})
}///|
async test "data as binary" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
// Send raw binary bytes.
w.write(b"binary data")
})
let data = r.read_all()
let binary = data.binary()
inspect(@utf8.decode(binary), content="binary data")
})
}
///|
async test "data as text" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
// Send UTF-8 encoded string
w.write("Hello, MoonBit!")
})
let data = r.read_all()
// Decoding happens lazily when `.text()` is invoked.
inspect(data.text(), content="Hello, MoonBit!")
})
}
///|
async test "data as json" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
// send UTF-8 encoded JSON string
let data : Json = { "name": "John", "age": 30 }
w.write(data)
})
let data = r.read_all()
let json = data.json()
// `json_inspect` asserts that the parsed JSON matches the expected structure.
json_inspect(json, content={ "name": "John", "age": 30 })
})
}///|
async test "read from reader" {
@async.with_task_group(root => {
// Create connected endpoints. `r` is a Reader, `w` is a Writer.
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
// Emit the payload in a single chunk.
w.write(b"Hello, World!")
})
let buf = FixedArray::make(13, b'0')
// Read up to 13 bytes into the fixed buffer.
let n = r.read(buf, offset=0, max_len=13)
inspect(n, content="13")
// Convert the binary slice to UTF-8 text for inspection.
inspect(
@utf8.decode(buf.unsafe_reinterpret_as_bytes()),
content="Hello, World!",
)
})
}
///|
async test "read_exactly - read exact number of bytes" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
// Produce a fixed-size frame.
w.write(b"0123456789")
})
// Blocks until exactly 5 bytes are received or ReaderClosed is raised.
let data1 = r.read_exactly(5)
inspect(@utf8.decode(data1), content="01234")
let data2 = r.read_exactly(5)
inspect(@utf8.decode(data2), content="56789")
})
}
///|
async test "read_some - read next chunk of data" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
// the writer supplieds data in two chunks
w.write("abcd")
@async.sleep(200)
w.write("efgh")
@async.sleep(200)
w.write("ijkl")
})
debug_inspect(
r.read_some(),
content=(
#|Some(<Bytes: [0x61, 0x62, 0x63, 0x64]>)
),
)
// `read_some` can optionally accept a length limit
debug_inspect(
r.read_some(max_len=2),
content=(
#|Some(<Bytes: [0x65, 0x66]>)
),
)
// the amount of data returned may be smaller than `max_len`
debug_inspect(
r.read_some(max_len=4),
content=(
#|Some(<Bytes: [0x67, 0x68]>)
),
)
debug_inspect(
r.read_some(),
content=(
#|Some(<Bytes: [0x69, 0x6a, 0x6b, 0x6c]>)
),
)
// on EOF, `None` is returned
debug_inspect(r.read_some(), content="None")
})
}
///|
async test "read_all - read entire content" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
w.write(b"Complete content")
})
// `read_all` accumulates everything into a `&Data` handle.
let data = r.read_all()
// Convert to text on demand.
inspect(data.text(), content="Complete content")
})
}
///|
async test "read_all large data" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
// Large payloads can be streamed without precomputing the size.
w.write(Bytes::make(4097, 0))
})
inspect(r.read_all().binary().length(), content="4097")
})
}
///|
async test "drop - advance stream by discarding data" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
w.write(b"0123456789")
})
// Advance the window by five bytes; subsequent reads start after this point.
// The number of bytes actually dropped would be returned.
inspect(r.drop(5), content="5")
let data = r.read_exactly(5)
inspect(@utf8.decode(data), content="56789")
})
}
///|
async test "read_until - read text from stream until a separator is found" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
w.write("abcd|")
w.write("defg")
})
// read until the separator "|" is met. The separator will be consumed as well
debug_inspect(
r.read_until("|"),
content=(
#|Some("abcd")
),
)
// .. or read until EOF is reached
debug_inspect(
r.read_until("|"),
content=(
#|Some("defg")
),
)
// `None` will be returned when no more data is available
debug_inspect(r.read_until("|"), content="None")
})
}
///|
#cfg(target="native")
async test "read_until - used to read file line by line" {
let file = @fs.open("LICENSE", mode=ReadOnly)
defer file.close()
let lines = []
while file.read_until("\n") is Some(line) {
// Push each decoded line into a growable array.
lines.push(line)
}
inspect(lines.length(), content="202")
assert_eq(lines.join("\n") + "\n", @fs.read_file("LICENSE").text())
}///|
async test "MemoryReader - generate reader content in memory" {
let r = @io.MemoryReader() w => {
w.write("hello, ")
@async.sleep(10)
w.write("MoonBit")
}
defer r.close()
inspect(r.read_all().text(), content="hello, MoonBit")
}
///|
async test "MemoryReader - stream generated chunks" {
let r = @io.MemoryReader() w => {
for part in ["ab", "cd", "ef"] {
w.write(part)
@async.sleep(10)
}
}
defer r.close()
debug_inspect(
r.read_some().map(data => @utf8.decode(data)),
content=(
#|Some("ab")
),
)
debug_inspect(
r.read_some().map(data => @utf8.decode(data)),
content=(
#|Some("cd")
),
)
debug_inspect(
r.read_some().map(data => @utf8.decode(data)),
content=(
#|Some("ef")
),
)
debug_inspect(r.read_some(), content="None")
}///|
async test "write to writer" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
// Each call appends to the outgoing stream.
w.write(b"Hello")
w.write(b", ")
w.write(b"World!")
})
// `read_all` collapses everything for verification.
let data = r.read_all()
inspect(data.text(), content="Hello, World!")
})
}
///|
async test "write_once - single write operation" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
let data : Bytes = b"Test data"
// Manually call `write_once` to demonstrate partial write accounting.
let written = w.write_once(data, offset=0, len=data.length())
inspect(written, content="9")
})
let content = r.read_all()
inspect(content.text(), content="Test data")
})
}
///|
async test "write large data" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
root.spawn_bg(() => {
defer w.close()
let data = Bytes::make(1024 * 16, 0)
// `write` handles chunking internally, avoiding manual loops.
w.write(data)
})
root.spawn_bg(() => {
defer r.close()
inspect(r.read_all().binary().length(), content="16384")
})
})
}
///|
async test "write_reader - copy from reader to writer" {
let log = StringBuilder::new()
@async.with_task_group(root => {
let (r1, w1) = @io.pipe()
let (r2, w2) = @io.pipe()
root.spawn_bg(() => {
defer r2.close()
defer w1.close()
// Stream everything from `r2` into `w1` via `write_reader`.
w1.write_reader(r2)
})
root.spawn_bg(() => {
defer r1.close()
while r1.read_some() is Some(data) {
let data = @utf8.decode(data)
log.write_string("received \{data}\n")
}
})
root.spawn_bg(() => {
defer w2.close()
// Simulate a producer that emits three frames with pauses in between.
log "sending 4 bytes\n"
w2.write(b"abcd")
@async.sleep(300)
log "sending 4 bytes\n"
w2.write(b"efgh")
@async.sleep(300)
log "sending 4 bytes\n"
w2.write(b"ijkl")
})
})
inspect(
log.to_string(),
content=(
#|sending 4 bytes
#|received abcd
#|sending 4 bytes
#|received efgh
#|sending 4 bytes
#|received ijkl
#|
),
)
}
///|
async test "write string" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
// Unicode data is transparently encoded via UTF-8.
w.write("abcd中文☺")
})
inspect(r.read_all().text(), content="abcd中文☺")
})
}///|
async test "BufferedWriter - basic buffering" {
let log = StringBuilder::new()
@async.with_task_group(root => {
let (r, w) = @io.pipe()
root.spawn_bg(() => {
defer w.close()
let w = @io.BufferedWriter::new(w, size=4)
log "2 bytes written\n"
w.write("ab")
@async.sleep(20)
log "2 bytes written\n"
w.write("cd")
@async.sleep(20)
log "2 bytes written\n"
w.write("ef")
// Force the remaining bytes out of the buffer.
w.flush()
})
root.spawn_bg(() => {
defer r.close()
while r.read_some() is Some(data) {
log.write_string("received: \{@utf8.decode(data)}\n")
}
})
})
inspect(
log.to_string(),
content=(
#|2 bytes written
#|2 bytes written
#|2 bytes written
#|received: abcd
#|received: ef
#|
),
)
}
///|
async test "BufferedWriter::new with custom size" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
let w = @io.BufferedWriter::new(w, size=8)
inspect(w.capacity(), content="8")
// Writes remain buffered until an explicit flush.
w.write(b"test")
w.flush()
})
let data = r.read_all()
inspect(data.text(), content="test")
})
}
///|
async test "BufferedWriter::flush - commit buffered data" {
@async.with_task_group(root => {
let (r, w) = @io.pipe()
defer r.close()
root.spawn_bg(() => {
defer w.close()
let w = @io.BufferedWriter::new(w, size=16)
w.write(b"buffer")
w.flush()
// Data written after the flush remains buffered until the next flush.
w.write(b"more")
w.flush()
})
let data = r.read_all()
inspect(data.text(), content="buffermore")
})
}
///|
async test "BufferedWriter::write_reader - buffered copy" {
let log = StringBuilder::new()
@async.with_task_group(root => {
let (r1, w1) = @io.pipe()
let (r2, w2) = @io.pipe()
root.spawn_bg(() => {
defer r2.close()
defer w1.close()
let w1 = @io.BufferedWriter::new(w1, size=6)
w1.write_reader(r2)
// Flush ensures the trailing fragment is committed before closing.
w1.flush()
})
root.spawn_bg(() => {
defer r1.close()
while r1.read_some() is Some(data) {
let data = @utf8.decode(data)
log.write_string("received \{data}\n")
}
})
root.spawn_bg(() => {
defer w2.close()
log "sending 4 bytes\n"
w2.write(b"abcd")
@async.sleep(100)
log "sending 4 bytes\n"
w2.write(b"efgh")
@async.sleep(100)
log "sending 4 bytes\n"
w2.write(b"ijkl")
})
})
inspect(
log.to_string(),
content=(
#|sending 4 bytes
#|sending 4 bytes
#|received abcdef
#|sending 4 bytes
#|received ghijkl
#|
),
)
}trait Dataimpl Data for StringViewfn to_bytes(self : StringView) -> Bytespub(open) trait Reader {
fn _get_internal_buffer(Self) -> ReaderBuffer
async fn _direct_read(Self, FixedArray[Byte], offset~ : Int, max_len~ : Int) -> Int
async fn read(Self, FixedArray[Byte], offset? : Int, max_len? : Int) -> Int = _
async fn drop(Self, Int) -> Int = _
async fn read_exactly(Self, len : Int) -> Bytes = _
async fn read_some(Self, max_len? : Int) -> Bytes? = _
async fn read_all(Self) -> &Data = _
async fn read_until(Self, StringView) -> String? = _
}impl Show for PipeClosedimpl Show for ReaderClosedtype BufferedWriter[W]impl Writer for BufferedWriter[W]async fn[W : Writer] write_once(self : BufferedWriter[W], buf : Bytes, offset~ : Int, len~ : Int) -> Inttype MemoryReaderimpl Reader for MemoryReaderasync fn _direct_read(self : MemoryReader, buf : FixedArray[Byte], offset~ : Int, max_len~ : Int) -> Int#callsite(autofill(loc))
fn MemoryReader::MemoryReader(f : async (&Writer) -> Unit, loc~ : SourceLoc) -> MemoryReadertype PipeReadasync fn _direct_read(self : PipeRead, dst : FixedArray[Byte], offset~ : Int, max_len~ : Int) -> Inttype PipeWrite#internal(internal, "this type is for internal use only")
type ReaderBufferAsynchronous programming library for MoonBit