Skip to content

Instantly share code, notes, and snippets.

@nberlette
Last active April 17, 2025 07:02
Show Gist options
  • Save nberlette/1a8d7b6d805b7aa3e5d365bb7d2b6773 to your computer and use it in GitHub Desktop.
Save nberlette/1a8d7b6d805b7aa3e5d365bb7d2b6773 to your computer and use it in GitHub Desktop.
Rc, Weak, Cell, OnceCell in TypeScript
import { IterableWeakMap } from "jsr:@iter/[email protected]";
export const _rc: unique symbol = Symbol("rc");
/**
* Internal structure representing the shared state for an Rc pointer.
*/
export interface RcBox<T = any> {
[_rc]: number;
value: T;
}
export function isRcBox<T>(it: any): it is RcBox<T> {
return typeof it === "object" && it !== null && typeof it[_rc] === "number";
}
/**
* A WeakMap to hold the internal RcBox for each Rc instance.
* This simulates private storage and leverages WeakMap for automatic cleanup.
*/
export const bm = new IterableWeakMap<Rc<any>, RcBox>();
/**
* This module provides a simple cell implementation for interior mutability,
* mimicking Rust's `Cell`. It allows mutation of its inner value, even if the
* cell itself is immutable.
*
* @license MIT
* @copyright 2025 Nicholas Berlette. All rights reserved.
* @author Nicholas Berlette <https://github.com/nberlette>
* @module cell
*/
/**
* Simple cell implementation for interior mutability. Mimics Rust's `Cell` by
* allowing mutation of its inner value, even if the cell itself is immutable.
*
* @example
* ```ts
* import { Cell } from "jsr:@type/rc";
*
* const cell = new Cell(5);
*
* console.log(cell.get()); // 5
*
* cell.set(10);
*
* console.log(cell.get()); // 10
* ```
*/
export class Cell<T> {
#disposed = false;
#value: T;
/**
* Creates a new Cell with the given initial value.
* @param value - The initial value.
*/
constructor(value: T) {
this.#value = value;
}
/**
* Retrieves the current inner value.
* @returns The contained value.
*/
get(): T {
this.#disposedCheck();
return this.#value;
}
/**
* Sets the cell's inner value.
*
* @param value The new value.
* @returns The Cell instance for method chaining.
*/
set(value: T): this {
this.#disposedCheck();
this.#value = value;
return this;
}
/**
* Returns the inner value.
*
* @returns The contained value.
*/
valueOf(): T {
this.#disposedCheck();
return this.#value;
}
/**
* Disposes of the cell, setting its value to `null` and marking it as
* disposed. Attempting to access the disposed cell will throw an error.
*
* This method is called internally by the semantics of the `using` and
* `await using` statements, introduced in ES2024 by the TC39 Proposal for
* [Explicit Resource Management][ERM].
*
* [ERM]: https://github.com/tc39/proposal-explicit-resource-management
*
* @internal
*/
[Symbol.dispose](): void {
this.#disposedCheck();
this.#value = null!;
this.#disposed = true;
}
#disposedCheck = () => {
if (this.#disposed) {
const error = new ReferenceError("Cell already disposed");
error.name = "CellDisposedError";
Error.captureStackTrace?.(error, this.#disposedCheck);
error.stack; // ensure stack trace is captured
throw error;
}
};
}
The MIT License (MIT)
Copyright (c) 2025+ Nicholas Berlette (https://github.com/nberlette)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
export * from "./rc.ts";
export * from "./weak.ts";
export * from "./cell.ts";
export * from "./once_cell.ts";
/**
* This module provides a `OnceCell` class that allows for a single assignment
* of a value. Once a value is set, it cannot be changed. This is useful for
* creating immutable cells that can be set only once.
*
* The {@linkcode OnceCell} class extends the {@linkcode Cell} API, which is a
* generic container for creating and managing single values in a type-safe
* manner, mimicking the behavior of Rust's `Cell` and `RefCell` types.
*
* @license MIT
* @copyright 2025 Nicholas Berlette. All rights reserved.
* @author Nicholas Berlette <https://github.com/nberlette>
* @module once-cell
*/
import { Cell } from "./cell.ts";
/**
* A single-assignment cell that can be set only once.
*
* @example
* ```ts
* import { OnceCell } from "jsr:@type/rc";
*
* const onceCell = new OnceCell<number>();
*
* onceCell.set(42);
* console.log(onceCell.get()); // 42
*
* // Throws an error if you try to set it again
* onceCell.set(100); // Error: Cell already set
* ```
*/
export class OnceCell<T> extends Cell<T | undefined> {
#set = false;
/**
* Constructs a new `OnceCell` instance, optionally with an initial value to
*
* @param [value] The initial value (optional).
* @returns A new `OnceCell` instance.
*/
constructor(value?: T) {
super(value);
}
/**
* Sets the cell's inner value.
*
* @param value The new value.
* @returns The `OnceCell` instance for method chaining.
* @throws {ReferenceError} If the cell has already been set.
*/
override set(value: T): this {
this.#setCheck(false);
this.#set = true as never;
return super.set(value);
}
/**
* Retrieves the current inner value.
*
* @returns The contained value of type `T`.
* @throws {ReferenceError} If the cell has not been set.
*/
override get(): T {
this.#setCheck(true);
return super.get()!;
}
/**
* Checks if the cell has been set.
*
* @returns `true` if the cell has been set; otherwise, `false`.
*/
isSet(): boolean {
return this.#set;
}
/**
* Returns the inner value.
*
* @returns The contained value of type `T`.
* @throws {ReferenceError} If the cell has not been set.
*/
override valueOf(): T {
this.#setCheck(true);
this.#set;
return super.valueOf()!;
}
/**
* Disposes of the cell, setting its value to `null` and marking it as
* disposed. Attempting to access the disposed cell will throw an error.
*
* This method is called internally by the semantics of the `using` and
* `await using` statements, introduced in ES2024 by the TC39 Proposal for
* [Explicit Resource Management][ERM].
*
* [ERM]: https://github.com/tc39/proposal-explicit-resource-management
*
* @internal
*/
override [Symbol.dispose](): void {
super[Symbol.dispose]();
this.#set = false;
}
#setCheck<E extends boolean = false>(
expected: E = false as E,
// @ts-ignore -- but this _is_ in the class body :p
): asserts this is this & { #set: E } {
if (this.#set !== expected) {
const error = new ReferenceError(
`OnceCell has ${expected ? "not" : "already"} been set`,
);
error.name = "OnceCellSetError";
Error.captureStackTrace?.(error, this.#setCheck);
error.stack; // ensure stack trace is captured
throw error;
}
}
}
/**
* This module provides experimental reference-counted types.
*
* It implements:
*
* - `Rc<T>`: A reference-counted pointer. Each clone increments a count,
* and a FinalizationRegistry decrements the count when an instance is GC’ed.
* - `Weak<T>`: A weak pointer that can be upgraded to a strong Rc if the
* underlying value is still alive.
* - `Cell<T>`: A simple container for interior mutability.
*
* Note: Due to the non-deterministic nature of garbage collection,
* the reference count is only best-effort.
*
* @module rc
* @example
* ```ts
* import { Rc, Weak, Cell } from "@type/rc";
*
* // Rc example
* const a = Rc.new({ text: "hello" });
* const b = a.clone();
* console.log(a.refCount()); // Should print 2 (best-effort)
* console.log(b.get().text); // "hello"
*
* // Weak example
* const weak = new Weak(a);
* const strong = weak.upgrade();
* if (strong) {
* console.log("Upgraded value:", strong.get());
* }
*
* // Cell example
* const cell = new Cell(42);
* console.log(cell.get()); // 42
* cell.set(100);
* console.log(cell.get()); // 100
* ```
* @license MIT
* @author Nicholas Berlette <https://github.com/nberlette>
* @copyright 2025 Nicholas Berlette. All rights reserved.
*/
import { bm, isRcBox, type RcBox, _rc } from "./_internal.ts";
import { Weak } from "./weak.ts";
/**
* A reference-counted pointer that mimics some behaviors of Rust's `Rc`.
*
* Each Rc instance shares an internal RcBox with the wrapped value and
* a reference count. When an instance is cloned, the count is incremented.
* When an instance is garbage collected, a FinalizationRegistry callback
* decrements the count.
*
* @example
* ```ts
* const a = Rc.new({ data: "hello" });
* const b = a.clone();
* console.log(a.refCount()); // 2 (best-effort)
* ```
*/
export class Rc<T> {
// #region private
/**
* Global FinalizationRegistry that decrements the reference count
* when an `Rc` instance is garbage collected.
*/
static readonly #finalizer = new FinalizationRegistry<RcBox>((box) => {
box[_rc]--;
// If the reference count reaches zero, we can clean up the box.
// This is a best-effort cleanup, as the garbage collector may not
// run immediately. there is also no guarantee this callback will ever be
// called, so the reference count _could_ never reach zero.
if (box[_rc] <= 0) {
const key = bm.entries().toArray().find(([_, v]) => v === box)?.[0];
if (key) bm.delete(key);
// Optionally, you can also clear the value if needed.
box.value = null;
box[_rc] = 0;
}
});
#get = () => {
const box = bm.get(this);
if (!box) throw new ReferenceError("Stale Rc pointer");
return box;
};
/**
* Private constructor. Use Rc.new() to create a instance.
* @param box - The internal RcBox to share.
*/
protected constructor(box: RcBox<T>) {
// Store the shared box in our private map.
bm.set(this, box);
// Increment the reference count.
box[_rc]++;
// Register this instance with the finalizer.
Rc.#finalizer.register(this, box);
}
// #endregion private
// #region public
/**
* Creates a new Rc instance from a given value.
*
* @param value - The value to wrap.
* @returns A new Rc instance.
*/
static new<T>(value: T | Rc<T> | RcBox<T>): Rc<T> {
if (value instanceof Rc) value = value.#get();
if (!isRcBox(value)) value = { __proto__: null, value, [_rc]: 0 } as RcBox;
return new Rc(value);
}
/**
* Clones this Rc pointer, incrementing the reference count.
* @returns A new Rc instance sharing the same value.
*/
clone(): Rc<T> {
const box = this.#get();
return new Rc(box);
}
/**
* Returns the wrapped value.
* @returns The inner value.
*/
get(): T {
const box = this.#get();
return box.value;
}
/**
* Downgrades this Rc pointer to a {@linkcode Weak} reference.
* @returns A new Weak instance.
*/
downgrade(): Weak<T> {
const box = this.#get();
return new Weak(box);
}
/**
* Returns the current reference count.
*
* Note: The count is updated asynchronously by the garbage collector,
* so this is only a best-effort approximation.
* @returns The number of strong references.
*/
refCount(): number {
const box = this.#get();
return box[_rc];
}
/**
* Checks if this Rc pointer is still valid.
* @returns True if the pointer is valid, false otherwise.
*/
isAlive(): boolean {
const box = this.#get();
return box[_rc] > 0;
}
/**
* Destroys this Rc pointer, decrementing the reference count.
* This is a no-op if the reference count is already zero.
*/
destroy(): void {
const box = this.#get();
box[_rc]--;
// If the reference count reaches zero, we can clean up the box.
// This is a best-effort cleanup, as the garbage collector may not
// run immediately. there is also no guarantee this callback will ever be
// called, so the reference count _could_ never reach zero.
if (box[_rc] <= 0) {
const key = bm.entries().toArray().find(([_, v]) => v === box)?.[0];
if (key) bm.delete(key);
// Optionally, you can also clear the value if needed.
box.value = null;
box[_rc] = 0;
}
// Unregister this instance from the finalizer.
Rc.#finalizer.unregister(this);
}
valueOf(): T {
const box = this.#get();
return box.value;
}
[Symbol.for("nodejs.util.inspect.custom")](
depth: number | null,
// deno-lint-ignore no-explicit-any
options: Record<string, any>,
): string {
const s = options.stylize ?? ((s: string) => s);
const box = this.#get();
const value = box.value;
const type = typeof value;
let tag = "Rc";
depth ??= 0;
if (depth > 0) tag = `Rc<${type}>`;
if (depth === 0) tag = `Rc<...>`;
if (depth >= 1) {
return `${s(tag, "special")} { ${s("value", "name")}: ${
s(
type === "object" ? value : String(value),
type === "string" ? "string" : "special",
)
} }`;
}
return s(`[${tag}]`, "special");
}
// #endregion public
}
/**
* TypeScript implementation of Rust's `Weak` pointers.
*
* @license MIT
* @copyright 2025 Nicholas Berlette. All rights reserved.
* @author Nicholas Berlette <https://github.com/nberlette>
* @module weak
*/
import { type RcBox, bm, isRcBox } from "./_internal.ts";
import { Rc } from "./rc.ts";
/**
* A weak reference to a value managed by {@linkcode Rc}.
*
* Similar to Rust's `Weak` pointer, does not contribute to the ref count.
*
* > [!TIP]
* >
* > Use `Weak.upgrade()` to obtain a strong `Rc` pointer, as long as the
* > value is still alive (has not been garbage collected).
*
* @example
* ```ts
* import { Rc, Weak } from "@type/rc";
*
* const a = Rc.new({ data: "hello" });
*
* const weak = new Weak(a);
*
* const strong = weak.upgrade();
*
* if (strong) console.log("Value:", strong.get());
* ```
*/
export class Weak<T> {
#ref: WeakRef<RcBox<T>>;
/**
* Constructs a Weak from a strong {@linkcode Rc} pointer.
*
* @param rc A strong {@linkcode Rc} pointer.
*/
constructor(rc: Rc<T>);
constructor(box: RcBox<T>);
constructor(rc: Rc<T> | RcBox<T>) {
const box = isRcBox(rc) ? rc : bm.get(rc);
if (!isRcBox<T>(box)) {
throw new TypeError("Invalid Rc pointer provided to Weak constructor");
}
this.#ref = new WeakRef(box);
}
/**
* Attempts to dereference the weak pointer.
* @returns The underlying value if it is still alive; otherwise, undefined.
*/
deref(): RcBox<T> | undefined {
return this.#ref.deref();
}
/**
* Attempts to upgrade this weak pointer to a strong {@linkcode Rc} pointer.
*
* @returns A new {@linkcode Rc} instance if the underlying value is still
* alive; otherwise, undefined.
*/
upgrade(): Rc<T> | undefined {
const box = this.#ref.deref();
if (box) return Rc.new(box.value);
}
/** @internal */
[Symbol.dispose](): void {
this.#ref = undefined!;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment