Skip to content

@actuallab/core

Core primitives used across all other Fusion TypeScript packages. This package has zero dependencies and corresponds to parts of ActualLab.Core in .NET.

Result<T>

Immutable value-or-error container — the TypeScript equivalent of Result<T> in .NET.

ts
import { Result, result, errorResult, resultFrom, resultFromAsync } from "@actuallab/core";

const ok = result(42);            // Result<number> with value
const err = errorResult<number>(new Error("fail"));  // Result<number> with error

ok.hasValue;        // true
ok.value;           // 42
ok.error;           // undefined
ok.valueOrUndefined // 42

err.hasError;       // true
err.error;          // Error("fail")
err.value;          // throws Error("fail")

// Wrap a function call in a Result
const r1 = resultFrom(() => JSON.parse("{}"));          // Result<any>
const r2 = await resultFromAsync(() => fetch("/api"));   // Result<Response>
MemberDescription
result(value, error?)Create a Result from value and optional error
errorResult(error)Create an error Result
resultFrom(fn)Wrap a sync function — catches exceptions into error result
resultFromAsync(fn)Wrap an async function — catches rejections into error result
.hasValuetrue if the result holds a value
.hasErrortrue if the result holds an error
.valueReturns value or throws the stored error
.valueOrUndefinedReturns value or undefined (never throws)
.errorReturns the error or undefined

PromiseSource<T>

Externally-resolvable promise — the TypeScript equivalent of TaskCompletionSource<T>.

ts
import { PromiseSource } from "@actuallab/core";

const ps = new PromiseSource<string>();
ps.isCompleted;  // false

// Somewhere else:
ps.resolve("done");
// or: ps.reject(new Error("fail"));

const value = await ps.promise;  // "done"
ps.isCompleted;  // true
MemberDescription
.promiseThe underlying Promise<T>
.isCompletedtrue after resolve or reject
.resolve(value)Resolve the promise
.reject(error)Reject the promise

There is also a pre-resolved constant: resolvedVoidPromise.

AsyncContext

General-purpose typed context container — provides AsyncLocal<T>-like functionality for JavaScript's single-threaded environment. Named for forward-compatibility with the TC39 AsyncContext proposal.

ts
import { AsyncContext, AsyncContextKey } from "@actuallab/core";

// Define a typed key
const userKey = new AsyncContextKey<string>("user", "anonymous");

// Create context with a value
const ctx = AsyncContext.empty.with(userKey, "Alice");

// Run code within the context
ctx.run(() => {
  AsyncContext.current!.get(userKey);  // "Alice"
});

// Or activate/deactivate manually
const guard = ctx.activate();
AsyncContext.current!.get(userKey);  // "Alice"
guard.dispose();
MemberDescription
AsyncContext.currentStatic: the currently active context (or undefined)
AsyncContext.emptyStatic: immutable empty singleton
AsyncContext.from(ctx)Returns ctx if defined, otherwise .current
AsyncContext.fromArgs(args)Extract context from last argument, or fall back to .current
.get(key)Read a value by key (returns key.defaultValue if not set)
.with(key, value)Create a new context with the key set (immutable)
.run(fn)Execute fn with this context as .current
.activate()Set as .current, return a Disposable that restores the previous one
.stripFromArgs(args)Remove this context from the end of an args array

WARNING

AsyncContext.current does not automatically flow across await boundaries. When calling compute methods from within other compute methods, pass the context explicitly as the last argument. See AsyncContext: Why It Matters.

AsyncLock

Promise-based mutual exclusion lock — the TypeScript equivalent of AsyncLock.

ts
import { AsyncLock } from "@actuallab/core";

const lock = new AsyncLock();

// Option 1: acquire/release
const release = await lock.acquire();
try {
  // critical section
} finally {
  release();
}

// Option 2: run (acquires, runs fn, releases)
await lock.run(async () => {
  // critical section
});

EventHandlerSet<T>

Typed pub/sub event system — similar to .NET's multicast delegates.

ts
import { EventHandlerSet } from "@actuallab/core";

const onChanged = new EventHandlerSet<string>();

const handler = (msg: string) => console.log(msg);
onChanged.add(handler);

onChanged.trigger("hello");  // logs "hello"
onChanged.remove(handler);

// Await the next event
const next = await onChanged.whenNext();
MemberDescription
.add(handler)Subscribe
.remove(handler)Unsubscribe
.trigger(value?)Fire all handlers synchronously
.clear()Remove all handlers
.whenNext()Returns a Promise that resolves on the next trigger

RetryDelaySeq and RetryDelayer

Retry delay sequences with exponential backoff, similar to .NET's RetryDelaySeq.

ts
import { RetryDelaySeq, RetryDelayer } from "@actuallab/core";

// Fixed 1-second delays
const fixed = RetryDelaySeq.fixed(1000);

// Exponential: 1s, 2s, 4s, ... capped at 30s
const exp = RetryDelaySeq.exp(1000, 30000);

// RetryDelayer wraps a sequence with cancellation support
const delayer = new RetryDelayer(exp, 10);  // max 10 retries
const delay = delayer.getDelay(3);  // 3rd retry
if (!delay.isLimitExceeded) {
  await delay.promise;  // wait for the delay
}

DisposableBag

Aggregates multiple Disposable objects and disposes them in LIFO order.

ts
import { DisposableBag } from "@actuallab/core";

const bag = new DisposableBag();
bag.add({ dispose: () => console.log("first") });
bag.add({ dispose: () => console.log("second") });
bag.dispose();  // logs "second", then "first"