Rust-like Box and similar objects for TypeScript
npm i @hazae41/box
- 100% TypeScript and ESM
- No external dependencies
- Similar to Rust
- Can hold data
- Composable
- Unit-tested
A movable reference
import { Box } from "@hazae41/box"
class Resource {
[Symbol.dispose]() {
console.log("Disposed")
}
}
async function take(box: Box<Resource>) {
using box2 = box.moveOrThrow()
await doSomethingOrThrow()
}
/**
* Resource will only be disposed after the promise settles
*/
{
using box = new Box(new Resource())
take(box).catch(console.error)
}
A mutable reference
class Pointer {
constructor(
readonly value: number
) {}
[Symbol.dispose]() {
free(this.value)
}
plus(pointer: Pointer) {
return new Pointer(this.value + pointer.value)
}
}
function* getPointersOrThrow() {
yield new Pointer(123)
yield new Pointer(456)
throw new Error()
yield new Pointer(789)
}
{
using result = new Slot(new Pointer(1))
for (const pointer of getPointersOrThrow()) {
using a = pointer
const b = result.get()
result.set(a.plus(b))
using _ = b
}
console.log(result.get().value)
}
Everything is correctly disposed if getPointersOrThrow()
throws in the midst of the loop
A reference that will be disposed when garbage collected
These references are NOT guaranteed to be disposed
class Pointer {
constructor(
readonly value: number
) {}
[Symbol.dispose]() {
free(this.value)
}
}
class MyObject {
constructor(
readonly pointer: Auto<Pointer>
) {}
something() {
something(this.pointer.get().value)
}
}
{
const pointer = new Auto(new Pointer(123))
const object = new MyObject(pointer)
}
The pointer will be freed when the object will be garbage collected
An auto can be disposed to unregister itself from garbage collection
function unwrap<T extends Disposable>(auto: Auto<T>) {
using _ = auto
return auto.get()
}
const raw = new Pointer(123)
const auto = new Auto(raw)
using raw2 = unwrap(auto)
The pointer will need to be manually freed
You can also use .unwrap()
to do this
const raw = new Pointer(123)
const auto = new Auto(raw)
using raw2 = auto.unwrap()
A reference that will be disposed after some delay
These references are guaranteed to be disposed
{
const pointer = new Tick(new Pointer(123))
await doSomethingOrThrow()
// Pointer is guaranteed to be freed here
}
This is useful to prevent WebAssembly memory from growing without using using
A reference that can only be disposed once
class Socket {
constructor(
readonly socket: WebSocket
) {}
[Symbol.dispose]() {
this.socket.close()
}
get() {
return this.socket
}
}
function terminate(socket: Once<Socket>) {
using _ = socket
socket.get().send("closing")
}
{
const socket = new Auto(new Once(new Socket(raw)))
if (something) {
terminate(socket.get())
return
// Will be closed here
}
// Will be closed on garbage collection
}
This can enable mixed behaviour where a resource can be disposed on demand or disposed on garbage collection
A reference to a callback that will be called when disposed
function waitOrThrow(socket: WebSocket) {
const future = new Future<void>()
const onOpen = () => future.resolve()
socket.addEventListener("open", onOpen, { passive: true })
using _0 = new Deferred(() => socket.removeEventListener("open", onOpen))
const onClose = () => future.reject(new Error("Closed"))
socket.addEventListener("close", onClose, { passive: true })
using _1 = new Deferred(() => socket.removeEventListener("close", onClose))
const onError = () => future.reject(new Error("Errored"))
socket.addEventListener("error", onError, { passive: true })
using _2 = new Deferred(() => socket.removeEventListener("error", onError))
return await future.promise
}
A stack of disposable objects
using stack = new Stack()
stack.push(new Resource())
stack.push(new Deferred(() => console.log("Disposed")))
You can also imitate DisposableStack
behaviour with Box<Once<Stack>>
using stack = new Box(new Once(new Stack()))
stack.getOrThrow().get().push(new Resource())
stack.getOrThrow().get().push(new Deferred(() => console.log("Disposed")))
using stack2 = stack.moveOrThrow()
Or just Box<Stack>
using stack = new Box(new Stack())
stack.getOrThrow().push(new Resource())
stack.getOrThrow().push(new Deferred(() => console.log("Disposed")))
using stack2 = stack.moveOrThrow()
DisposableStack already combines many of these concepts.
You can see DisposableStack
as a Box<Once<Stack>>
.
But using DisposableStack
can lead to many issues in your code.
- You can opt-out
Box<T>
behavior
When you use DisposableStack
, you allow the developer to use move()
This makes your code unpredictable if you didn't expect a DisposableStack
to be moved.
When you get passed a DisposableStack
you have to check if it is not moved.
While this may be useful in some situations, it may lead to unnecessary code.
Whereas when using Box<Stack>
, you explicitly allow it to be moved.
And when just using Stack
, you explicitly disallow it to be moved.
- You can opt-out
Once<T>
behavior
When you use DisposableStack
, you can only dispose it once.
While it prevents bugs related to double-dispose, it can prevent other bugs from being discovered.
You're not supposed to dispose a resource multiple times, and doing so is a bug from a logic issue.
When using DisposableStack
, you allow this bug to remain undiscovered because Once<T>
protects you.
When just using Stack
, you can fix your code to avoid double-dispose instead of relying on this protection.
- You can use code that accepts
Box<T>
Suppose we have an external function that accepts any Box<T>
.
When using DisposableStack
as T
, you have to wrap it into Box<DisposableStack>
.
This leads to extra burden because the stack can be moved twice.
Once in Box<T>
and once in DisposableStack
.
Whereas when using Box<Stack>
it can only be moved once.
- You can use code that accepts
Once<T>
Same with Once<DisposableStack>
instead of Once<Stack>
.
You have two different ways of checking if the stack has already been disposed.
This leads to extra code, extra logic burden, and possibly unexpected behavior.
When you use the standardized Once<T>
in all your code, you are safe from this.