Skip to content

Scope Context

Some values belong to the current scope’s environment rather than to every routine’s business parameters. Context binds those values on the scope chain, so later routines can read the nearest visible binding without threading the value through every call.

Create a key with contextKey(...), then bind a value for that key inside routine code. Any later lookup in the same scope can read that binding.

import { contextKey } from "@shajara/host";
import { bind, lookup } from "@shajara/host/primitives";
const requestIdKey = contextKey<string>();
function* handleRequest(requestId: string) {
yield* bind(requestIdKey, requestId);
return yield* writeAuditLine();
}
function* writeAuditLine() {
const [hasRequestId, requestId] = yield* lookup(requestIdKey);
return hasRequestId ? `request ${requestId}` : "request unknown";
}

The same key is used to bind and read the value. Its type parameter carries the value type, so lookup(requestIdKey) returns Presence<string>.

If the current scope cannot see a binding for that key, lookup(...) returns [false]. If a value is visible, it returns [true, value].

A child scope can see bindings from its ancestors. A nearer binding for the same key shadows the ancestor binding inside that child scope.

import { contextKey } from "@shajara/host";
import { bind, branch, lookup } from "@shajara/host/primitives";
const themeKey = contextKey<"light" | "dark">();
function* readTheme() {
const [hasTheme, theme] = yield* lookup(themeKey);
return hasTheme ? theme : "light";
}
function* renderPage() {
yield* bind(themeKey, "light");
const mainTheme = yield* branch(function* renderMain() {
return yield* readTheme(); // "light"
});
const previewTheme = yield* branch(function* renderPreview() {
yield* bind(themeKey, "dark");
return yield* readTheme(); // "dark"
});
const pageTheme = yield* readTheme(); // "light"
return { mainTheme, pageTheme, previewTheme };
}

The renderMain child scope inherits "light" from renderPage. The renderPreview child scope binds "dark" locally, so its lookup resolves to the nearer value. After that child scope returns, the parent scope still sees its own "light" binding.

unbind(...) removes the current scope’s binding for a key. It does not remove an ancestor binding, so lookup can fall back to the nearest remaining binding.

import { contextKey } from "@shajara/host";
import { bind, branch, lookup, unbind } from "@shajara/host/primitives";
const modeKey = contextKey<"published" | "draft">();
function* readMode() {
const [hasMode, mode] = yield* lookup(modeKey);
return hasMode ? mode : "published";
}
function* renderArticle() {
yield* bind(modeKey, "published");
return yield* branch(function* renderDraftPreview() {
yield* bind(modeKey, "draft");
yield* unbind(modeKey);
return yield* readMode(); // "published"
});
}

The child scope first shadows the parent binding with "draft". After unbind(modeKey), that local binding is gone, and a later lookup sees the parent scope’s "published" binding.

Context lookup reads the currently visible binding along the scope chain. The result is optional because the binding may be absent.

Choose context for values that already belong to the current scope’s environment: request identifiers, tracing handles, local configuration, or another value read by several routines.