Define the first implementable portable standard-library surface for Soundscript across:
js-browserjs-nodewasm-browserwasm-nodewasm-wasi- future native/LLVM standalone
This plan is the API catalog companion to:
docs/plans/runtime-target-platform-and-interop.mdfor targets, providers, and host boundariesdocs/plans/structured-concurrency-and-parallelism.mdfor structured concurrency, parallelism, cancellation, runtime setup,Send, andShare
The goal is not to copy Node, Deno, WASI, or the Web platform wholesale. The goal is a small, consistent, provider-backed API surface that preserves Web-style familiarity where honest, exposes target limitations explicitly, and still gives performance-oriented code low-level tools.
sts:*modules are Soundscript-owned and do not require// #[interop].- Raw host/app imports remain explicit
// #[interop]boundaries:web:*,node:*,native:*,extern:*, and ordinary foreign package imports. - Web-standard APIs keep Web semantics when they are exposed directly as globals.
- Soundscript-owned async APIs return
AsyncResult<T, E>, which isPromise<Result<T, E>>. Task<T, E>remains a cold/lazy recipe type, not the default shape for hot IO.Taskshould also be a value helper object so users writeTask.succeed(...),Task.fromPromise(...),Task.all(...), and similar helpers instead of importing a flat bag of task functions.- Expected host errors normalize to
Failuresubclasses or structuredFailuredata atsts:*boundaries. - Cancellation uses
AbortSignal/AbortController. - Stdlib IO should consult
TaskGroup.currentSignal()when an explicit signal is not passed. - Resources with lifetimes should implement
DisposableorAsyncDisposable. - True parallelism crosses
ThreadPool/Threadand requiresSend. - Shared mutable state requires explicit
Share, atomics, channels, mutexes, or provider-declared synchronized handles. - Capability absence should be a checker diagnostic when statically known, or an
UnsupportedCapabilityFailurewhen a single build can run under multiple provider configurations.
Capabilities should be named narrowly enough for audits and target diagnostics:
platform.urlplatform.fetchplatform.streamsplatform.textplatform.crypto.randomplatform.crypto.subtleplatform.consoletime.clock.walltime.clock.monotonictime.timerconcurrency.taskconcurrency.asyncContextconcurrency.parallel.threadconcurrency.parallel.sharedMemoryconcurrency.sync.atomicconcurrency.sync.channelconcurrency.sync.mutexfs.readfs.writefs.metadatafs.watchenv.readenv.writecli.argscli.stdioprocess.infoprocess.cwdprocess.signalprocess.spawnnet.dnsnet.tcpnet.udpnet.tlsnet.unixhttp.clienthttp.servertransport.websockettransport.webtransportnative.system
The checker and generated wrappers should report these names in diagnostics and provider manifests.
These types are shared across capability modules.
import type { AsyncResult } from 'sts:concurrency';
import { Failure } from 'sts:failures';
type ResourceId = string;
interface CapabilityInfo {
readonly name: string;
readonly available: boolean;
readonly provider?: string;
readonly reason?: string;
}
class UnsupportedCapabilityFailure extends Failure {
readonly capability: string;
}
class PermissionDeniedFailure extends Failure {
readonly capability?: string;
}
class CancellationFailure extends Failure {}
class DeadlineFailure extends Failure {}
class TimeoutFailure extends Failure {}
interface OperationOptions {
readonly signal?: AbortSignal;
readonly deadline?: Instant;
readonly timeout?: Duration;
}OperationOptions is a shape convention, not a required base type for every API. APIs should accept
only the options they actually support.
Keep the checked .sts prelude small. It should remain focused on core language ergonomics:
Result,Option,Ok,Err,Some,Noneok,err,some,noneisOk,isErr,isSome,isNoneTry,Match,whereFailureDefer,todo,unreachable
Web-style platform values are target-provided globals rather than prelude re-exports. They are available when the active target/provider supports them:
URLURLSearchParamsfetchRequestResponseHeadersReadableStreamWritableStreamTransformStreamTextEncoderTextDecoderAbortSignalAbortControllerBlobFileFormDataEventEventTargetcryptostructuredCloneconsole
setTimeout, clearTimeout, setInterval, clearInterval, queueMicrotask, performance,
WebSocket, and WebTransport are also Web-style platform values where available, but portable
Soundscript code should prefer sts:time, sts:concurrency, and focused transport modules for
owned semantics.
These modules are already in the stable core shape and should stay portable with no provider:
sts:preludests:resultsts:matchsts:failuressts:jsonsts:decodests:encodests:codecsts:comparests:hashsts:derivests:hktsts:typeclasses
Additional foundational modules that belong in this plan:
sts:pathsts:bytes
Use root modules as the normal teaching path and submodules for lower-level, capability-gated, provider-heavy, or large optional surfaces.
The preferred pattern is:
- root module: common re-export and beginner-facing imports
- submodule: focused ownership for a coherent optional slice
- no
advancedsubmodule names; name the thing being controlled
Apply this first to concurrency:
sts:concurrencysts:concurrency/tasksts:concurrency/parallelsts:concurrency/syncsts:concurrency/atomicssts:concurrency/runtime
Apply it selectively elsewhere:
sts:net/tcpsts:net/udpsts:net/dnssts:net/tlssts:net/unixsts:crypto/digeststs:crypto/hmacsts:crypto/keyssts:process/commandsts:process/signalssts:bytes/transfersts:bytes/shared
Do not create submodules only for symmetry. sts:console, sts:time, sts:path, sts:fetch,
sts:streams, and sts:fs should stay simple at the root until their surfaces become large enough
to justify a split.
Capability queries are useful for diagnostics, optional features, and test skips. They should not be used to hide semantically different behavior behind the same API call.
export type CapabilityName = string;
export interface CapabilityInfo {
readonly name: CapabilityName;
readonly available: boolean;
readonly provider?: string;
readonly reason?: string;
}
export function list(): readonly CapabilityInfo[];
export function get(name: CapabilityName): Option<CapabilityInfo>;
export function has(name: CapabilityName): boolean;
export function require(name: CapabilityName): Result<void, UnsupportedCapabilityFailure>;The checker should still reject statically unavailable APIs when the target profile proves they cannot exist. Runtime capability queries are for provider-dependent profiles and libraries that can honestly offer optional behavior.
The JS provider manifest should report the narrow audit names from the capability list, such as
fs.read, process.spawn, time.timer, and http.server. It may also expose coarse module
aliases such as fs, process, net, bytes, and crypto during the pre-v1 buildout so simple
feature checks stay ergonomic.
URL and URLSearchParams are Web-standard globals where available. sts:url is the explicit
portable import surface and the place for small result-oriented helpers.
export { URL, URLSearchParams };
export function parseUrl(input: string, base?: string | URL): Result<URL, Failure>;
export function canParseUrl(input: string, base?: string | URL): boolean;
export function fileUrlToPath(url: URL): Result<string, Failure>;
export function pathToFileUrl(path: string): Result<URL, Failure>;fileUrlToPath and pathToFileUrl are target-aware because path interpretation is provider
specific. The JS-neutral slice exports them but returns UnsupportedCapabilityFailure until a
target path provider owns the platform rules. Pure path manipulation belongs in sts:path.
The global fetch keeps ordinary Web semantics. sts:fetch should expose the same Web classes and
also provide result-oriented helpers for Soundscript-owned code.
export { fetch, Headers, Request, Response };
export type FetchFailure = Failure;
export function request(
input: RequestInfo,
init?: RequestInit,
): AsyncResult<Response, FetchFailure>;
export function readJson<T>(
response: Response,
decoder: Decoder<T>,
options?: OperationOptions,
): AsyncResult<T, Failure>;
export function readText(
response: Response,
options?: OperationOptions,
): AsyncResult<string, Failure>;
export function readBytes(
response: Response,
options?: OperationOptions,
): AsyncResult<Bytes, Failure>;request(...) normalizes thrown/rejected host errors to Failure. The global fetch(...) remains
available for code that wants exact Web behavior.
sts:concurrency/task replaces the current sts:async plan. This is an intentional breaking pre-v1
cleanup: the async/concurrency surface should have one conceptual home, and task helpers should live
under a Task.* value object rather than as bare module-level functions.
import type { Result } from 'sts:result';
export type Task<T, E = Failure> = () => AsyncResult<T, E>;
export type TaskAllResult<T> = {
readonly [K in keyof T]: T[K] extends Task<infer V, unknown> ? V : never;
};
export interface TaskModule {
succeed<T>(value: T): Task<T, never>;
fail<E>(error: E): Task<never, E>;
fromResult<T, E>(result: Result<T, E>): Task<T, E>;
fromAsyncResult<T, E>(work: () => AsyncResult<T, E>): Task<T, E>;
fromPromise<T>(
body: () => Promise<T>,
mapFailure?: (error: unknown) => Failure,
): Task<T, Failure>;
map<A, B, E>(task: Task<A, E>, fn: (value: A) => B): Task<B, E>;
flatMap<A, B, E1, E2>(
task: Task<A, E1>,
fn: (value: A) => Task<B, E2>,
): Task<B, E1 | E2>;
recover<A, B, E>(
task: Task<A, E>,
fn: (error: E) => B | AsyncResult<B, Failure>,
): Task<A | B, Failure>;
all<T extends Record<string, Task<unknown, E>>, E>(
tasks: T,
): Task<TaskAllResult<T>, E>;
race<T, E>(tasks: readonly [Task<T, E>, ...Task<T, E>[]]): Task<T, E>;
timeout<T, E>(
task: Task<T, E>,
duration: Duration,
): Task<T, E | TimeoutFailure>;
}
export const Task: TaskModule;Migration note: remove the current top-level sts:async.parallel(...) helper before this surface
stabilizes. Promise fanout is Task.all(...); true parallelism is ThreadPool.
The detailed API lives in docs/plans/structured-concurrency-and-parallelism.md. This module is
included here so the full stdlib catalog has one portable entry point. The root stays pay-for-play:
it re-exports task and cancellation primitives only, and does not import provider-backed runtime,
parallel, sync, or atomics modules.
export type AsyncResult<T, E = Failure> = Promise<Result<T, E>>;
export { Task } from 'sts:concurrency/task';
export type { Task, TaskAllResult } from 'sts:concurrency/task';
export { CancellationFailure, DeadlineFailure, TimeoutFailure } from 'sts:concurrency/task';Provider-backed APIs are imported from the descriptive submodules directly: TaskGroup,
TaskHandle, AsyncContext, and Runtime from sts:concurrency/runtime; ThreadPool, Thread,
Send, and Share from sts:concurrency/parallel; synchronization from sts:concurrency/sync;
and atomic shared-memory helpers from sts:concurrency/atomics.
sts:concurrency/parallel owns true parallel execution and sendability rules.
export type Send<T> = T;
export type Share<T> = T;
export type ThreadEntry<I, O, E = Failure> = (input: I) => Result<O, E> | AsyncResult<O, E>;
export interface ThreadPoolOptions {
readonly workers: number | 'available';
readonly name?: string;
readonly queueLimit?: number;
}
export class ThreadPool implements AsyncDisposable {
static get default(): ThreadPool;
static fixed(options: ThreadPoolOptions): ThreadPool;
run<I, O, E = Failure>(
entrypoint: ThreadEntry<Send<I>, Send<O>, E>,
input: Send<I>,
options?: { name?: string },
): AsyncResult<Send<O>, E>;
map<I, O, E = Failure>(
entrypoint: ThreadEntry<Send<I>, Send<O>, E>,
inputs: readonly Send<I>[],
options?: { name?: string },
): AsyncResult<Send<O>[], E>;
}
export class Thread<I, O, E = Failure> {
static spawn<I, O, E = Failure>(
entrypoint: ThreadEntry<Send<I>, Send<O>, E>,
input: Send<I>,
options?: ThreadOptions,
): Thread<Send<I>, Send<O>, E>;
join(): AsyncResult<Send<O>, E>;
cancel(reason?: Failure): void;
static blockOn<T, E = Failure>(work: () => AsyncResult<T, E>): Result<T, E>;
}Runtime/provider override surface:
export interface RuntimeOptions {
readonly threadPool?: ThreadPool;
readonly deadline?: Instant | Duration;
readonly signal?: AbortSignal;
readonly scheduler?: SchedulerPolicy;
readonly tracing?: TracingHooks;
readonly providers?: ProviderOverrides;
}
export namespace Runtime {
export function with<T, E = Failure>(
options: RuntimeOptions,
body: () => AsyncResult<T, E>,
): AsyncResult<T, E>;
export function capabilities(): readonly CapabilityInfo[];
export function hasCapability(name: string): boolean;
export function requireCapability(name: string): Result<void, UnsupportedCapabilityFailure>;
}Normal applications should configure providers at the launcher/adapter boundary instead of calling
Runtime.with(...) at every use site.
Portable time should distinguish wall clock, monotonic clock, and timers.
export class Duration {
static milliseconds(value: number): Duration;
static seconds(value: number): Duration;
static minutes(value: number): Duration;
static nanoseconds(value: bigint): Duration;
readonly milliseconds: number;
readonly nanoseconds: bigint;
}
export class Instant {
durationSince(other: Instant): Duration;
add(duration: Duration): Instant;
subtract(duration: Duration): Instant;
}
export class WallDateTime {
static now(): Result<WallDateTime, Failure>;
toIsoString(): string;
}
export namespace monotonic {
export function now(): Result<Instant, Failure>;
}
export namespace wall {
export function now(): Result<WallDateTime, Failure>;
}
export function sleep(duration: Duration, options?: OperationOptions): AsyncResult<void, Failure>;
export function deadline(
at: Instant,
options?: { signal?: AbortSignal },
): AsyncResult<void, DeadlineFailure | CancellationFailure>;
export function timeoutSignal(duration: Duration): AbortSignal;Date may remain available as a JS/Web global where supported, but portable runtime scheduling
should use Instant/Duration.
console is a Web-style global. sts:console is the explicit importable portable surface and the
hook point for runtimes that do not expose a real host console.
export type ConsoleValue =
| null
| undefined
| boolean
| number
| bigint
| string
| JsonValue
| Error
| Failure;
export interface Console {
debug(...values: readonly ConsoleValue[]): void;
info(...values: readonly ConsoleValue[]): void;
log(...values: readonly ConsoleValue[]): void;
warn(...values: readonly ConsoleValue[]): void;
error(...values: readonly ConsoleValue[]): void;
trace(...values: readonly ConsoleValue[]): void;
group(label?: string): void;
groupEnd(): void;
time(label?: string): void;
timeEnd(label?: string): void;
}
export const console: Console;
export const debug: Console['debug'];
export const info: Console['info'];
export const log: Console['log'];
export const warn: Console['warn'];
export const error: Console['error'];This is diagnostic output, not structured application logging. A future sts:log can provide
records, sinks, levels, redaction, and trace-context integration.
The global stream classes should follow Web Streams where available. sts:streams owns helpers and
portable failure/cancellation behavior.
export type ByteStream = ReadableStream<Uint8Array<ArrayBufferLike>>;
export interface PipeOptions extends OperationOptions {
readonly preventClose?: boolean;
readonly preventAbort?: boolean;
readonly preventCancel?: boolean;
}
export function readAllBytes(
stream: ByteStream,
options?: OperationOptions,
): AsyncResult<Bytes, Failure>;
export function readAllText(
stream: ByteStream,
options?: OperationOptions & { encoding?: string },
): AsyncResult<string, Failure>;
export function writeAllBytes(
stream: WritableStream<Uint8Array<ArrayBufferLike>>,
bytes: Bytes | ByteView,
options?: OperationOptions,
): AsyncResult<void, Failure>;
export function pipe(
source: ReadableStream<unknown>,
sink: WritableStream<unknown>,
options?: PipeOptions,
): AsyncResult<void, Failure>;
export function fromBytes(bytes: Bytes | ByteView): ByteStream;
export function fromIterable<T>(values: Iterable<T>): ReadableStream<T>;Async iteration helpers should wait for the async-iteration runtime slice before becoming stable.
The bytes module is the common low-level data API for IO, crypto, networking, workers, and Wasm.
The initial JS implementation keeps Bytes as a Uint8Array alias and exposes allocation-free
views, explicit copy helpers, lexicographic compare, shared-buffer detection, and ArrayBuffer
conversion for host boundaries:
export type Bytes = Uint8Array;
export type BytesCompareResult = -1 | 0 | 1;
export interface BytesViewOptions {
readonly byteOffset?: number;
readonly byteLength?: number;
}
export interface BytesArrayBufferOptions {
readonly copy?: boolean;
}
export function empty(): Bytes;
export function from(values: ArrayLike<number> | ArrayBufferLike): Bytes;
export function isBytes(value: unknown): value is Bytes;
export function view(buffer: ArrayBufferLike, options?: BytesViewOptions): Bytes;
export function fromString(text: string, options?: BytesFromOptions): Bytes;
export function toString(bytes: Bytes, options?: BytesFromOptions): string;
export function concat(chunks: readonly Bytes[]): Bytes;
export function equals(left: Bytes, right: Bytes): boolean;
export function compare(left: Bytes, right: Bytes): BytesCompareResult;
export function slice(bytes: Bytes, start?: number, end?: number): Bytes;
export function copy(bytes: Bytes): Bytes;
export function copyTo(source: Bytes, target: Bytes, targetOffset?: number): void;
export function isShared(bytes: Bytes): boolean;
export function toArrayBuffer(bytes: Bytes, options?: BytesArrayBufferOptions): ArrayBuffer;The deeper value-typed design below remains the direction once the checker/runtime has first-class support for immutable byte values, transferability, and shared-buffer capabilities:
export interface ByteView {
readonly byteLength: number;
slice(start?: number, end?: number): Bytes;
copyTo(target: MutableBytes, targetOffset?: number): Result<void, Failure>;
}
export interface Bytes extends ByteView {
readonly buffer: ArrayBuffer;
readonly byteOffset: number;
}
export interface MutableBytes extends ByteView {
readonly buffer: ArrayBuffer;
readonly byteOffset: number;
set(index: number, value: number): Result<void, Failure>;
fill(value: number, start?: number, end?: number): Result<void, Failure>;
freeze(): Bytes;
}
export interface TransferBuffer {
readonly byteLength: number;
transfer(): Send<Bytes>;
}
export interface SharedBytes extends Share<ByteView> {
readonly byteLength: number;
}
export function alloc(length: number): Result<MutableBytes, Failure>;
export function copy(bytes: ByteView): Result<MutableBytes, Failure>;
export function fromUint8Array(bytes: Uint8Array<ArrayBufferLike>): Bytes;
export function toUint8Array(bytes: ByteView): Uint8Array<ArrayBufferLike>;
export function concat(chunks: readonly ByteView[]): Result<Bytes, Failure>;The checker should treat ordinary Uint8Array as mutable and not deeply immutable. Sendability of
buffers depends on copy, transfer, or shared-buffer rules.
Text encoding remains Web-compatible but should expose explicit result helpers.
export function encodeUtf8(text: string): Result<Bytes, Failure>;
export function decodeUtf8(bytes: ByteView, options?: { fatal?: boolean }): Result<string, Failure>;
export { TextDecoder, TextEncoder };sts:random is cryptographic random by default. Deterministic PRNGs should live in a separate
testing or simulation module later. It should expose portable result-returning helpers, not raw
crypto or getRandomValues exports; code that needs the Web Crypto object can use the Web global
surface or an explicit interop import.
export function randomBytes(length: number): Result<Bytes, Failure>;
export function fillRandom(bytes: MutableBytes): Result<void, Failure>;
export function uuidV4(): Result<string, Failure>;crypto.getRandomValues is already covered by sts:random. The first crypto slice follows Web
Crypto digest/HMAC semantics and uses a value-object helper instead of TypeScript namespaces.
export type DigestAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
export type HmacAlgorithm = DigestAlgorithm;
export function digest(algorithm: DigestAlgorithm, data: Bytes): AsyncResult<Bytes, Failure>;
export function hmac(
algorithm: HmacAlgorithm,
key: Bytes,
data: Bytes,
): AsyncResult<Bytes, Failure>;
export function timingSafeEqual(left: Bytes, right: Bytes): Result<boolean, Failure>;
export const Crypto: {
digest: typeof digest;
hmac: typeof hmac;
randomBytes: typeof randomBytes;
timingSafeEqual: typeof timingSafeEqual;
};Subtle crypto should stay compatible with Web Crypto where supported. Provider-backed native/WASI implementations must match semantics before exposing the same names.
Path manipulation is pure. Filesystem access is not.
export type PathStyle = 'posix' | 'windows';
export interface ParsedPath {
readonly root: string;
readonly dir: string;
readonly base: string;
readonly ext: string;
readonly name: string;
}
export interface PathApi {
join(...segments: readonly string[]): string;
normalize(path: string): string;
dirname(path: string): string;
basename(path: string, suffix?: string): string;
extname(path: string): string;
parse(path: string): ParsedPath;
format(path: ParsedPath): string;
isAbsolute(path: string): boolean;
relative(from: string, to: string): string;
}
export const posix: PathApi;
export const windows: PathApi;Avoid a magical native path namespace in portable code. APIs that need provider-native paths
should accept strings and define whether they are interpreted by the active provider.
Filesystem APIs are provider-backed and unavailable in browser-family targets unless a provider can honestly implement the semantics.
export type PathLike = string | URL;
export interface FileInfo {
readonly type: 'file' | 'directory' | 'symlink' | 'other';
readonly size: bigint;
readonly modifiedAt?: WallDateTime;
readonly accessedAt?: WallDateTime;
readonly createdAt?: WallDateTime;
readonly readonly?: boolean;
}
export interface DirectoryEntry {
readonly name: string;
readonly type: FileInfo['type'];
}
export interface ReadFileOptions extends OperationOptions {}
export interface WriteFileOptions extends OperationOptions {
readonly create?: boolean;
readonly append?: boolean;
readonly truncate?: boolean;
readonly mode?: number;
}
export function readFile(path: PathLike, options?: ReadFileOptions): AsyncResult<Bytes, Failure>;
export function readTextFile(
path: PathLike,
options?: ReadFileOptions & { encoding?: string },
): AsyncResult<string, Failure>;
export function writeFile(
path: PathLike,
bytes: ByteView,
options?: WriteFileOptions,
): AsyncResult<void, Failure>;
export function writeTextFile(
path: PathLike,
text: string,
options?: WriteFileOptions & { encoding?: string },
): AsyncResult<void, Failure>;
export function stat(path: PathLike, options?: OperationOptions): AsyncResult<FileInfo, Failure>;
export function lstat(path: PathLike, options?: OperationOptions): AsyncResult<FileInfo, Failure>;
export function readDir(
path: PathLike,
options?: OperationOptions,
): AsyncResult<readonly DirectoryEntry[], Failure>;
export function mkdir(
path: PathLike,
options?: OperationOptions & { recursive?: boolean; mode?: number },
): AsyncResult<void, Failure>;
export function remove(
path: PathLike,
options?: OperationOptions & { recursive?: boolean },
): AsyncResult<void, Failure>;
export function rename(
oldPath: PathLike,
newPath: PathLike,
options?: OperationOptions,
): AsyncResult<void, Failure>;
export function copyFile(
from: PathLike,
to: PathLike,
options?: OperationOptions,
): AsyncResult<void, Failure>;
export function realPath(path: PathLike, options?: OperationOptions): AsyncResult<string, Failure>;Lower-level streaming file handles should be the second slice:
export class File implements AsyncDisposable {
readonly readable: ReadableStream<Uint8Array<ArrayBufferLike>>;
readonly writable: WritableStream<Uint8Array<ArrayBufferLike>>;
stat(options?: OperationOptions): AsyncResult<FileInfo, Failure>;
sync(options?: OperationOptions): AsyncResult<void, Failure>;
}
export function open(path: PathLike, options?: OpenOptions): AsyncResult<File, Failure>;File watching is useful but should be deferred until provider semantics are clearer:
export function watch(path: PathLike, options?: WatchOptions): AsyncResult<WatchHandle, Failure>;Environment access is separate from process and CLI.
export function get(name: string): Result<Option<string>, Failure>;
export function required(name: string): Result<string, Failure>;
export function has(name: string): Result<boolean, Failure>;
export function toRecord(): Result<Readonly<Record<string, string>>, Failure>;
export function set(name: string, value: string): Result<void, Failure>;
export function remove(name: string): Result<void, Failure>;set and remove require env.write; most browser-family targets should reject them.
CLI APIs are for command-line entrypoints and terminal IO. They should not own child-process
creation; that belongs to sts:process.
export function args(): Result<readonly string[], Failure>;
export interface Stdio {
readonly stdin: ReadableStream<Uint8Array<ArrayBufferLike>>;
readonly stdout: WritableStream<Uint8Array<ArrayBufferLike>>;
readonly stderr: WritableStream<Uint8Array<ArrayBufferLike>>;
}
export function stdio(): Result<Stdio, Failure>;
export function isTerminal(stream: 'stdin' | 'stdout' | 'stderr'): Result<boolean, Failure>;
export function terminalSize(): Result<Option<{ columns: number; rows: number }>, Failure>;
export function readLine(
options?: OperationOptions & { prompt?: string },
): AsyncResult<string, Failure>;
export function write(
text: string,
options?: { stream?: 'stdout' | 'stderr' },
): AsyncResult<void, Failure>;
export function writeLine(
text: string,
options?: { stream?: 'stdout' | 'stderr' },
): AsyncResult<void, Failure>;Process APIs are provider-backed and usually server/native only.
export interface ProcessInfo {
readonly pid?: number;
readonly ppid?: number;
readonly executable?: string;
readonly platform?: string;
readonly arch?: string;
}
export function info(): Result<ProcessInfo, Failure>;
export function cwd(): Result<string, Failure>;
export function chdir(path: string): Result<void, Failure>;
export function exit(code?: number): never;
export type SignalName =
| 'SIGINT'
| 'SIGTERM'
| 'SIGHUP'
| 'SIGQUIT'
| 'SIGKILL';
export function onSignal(
signal: SignalName,
handler: () => void,
): Result<Disposable, Failure>;Child processes:
export interface CommandOptions {
readonly args?: readonly string[];
readonly cwd?: string;
readonly env?: Readonly<Record<string, string>>;
readonly stdin?: 'inherit' | 'null' | 'piped';
readonly stdout?: 'inherit' | 'null' | 'piped';
readonly stderr?: 'inherit' | 'null' | 'piped';
readonly signal?: AbortSignal;
}
export interface CommandOutput {
readonly code: number;
readonly success: boolean;
readonly stdout: Bytes;
readonly stderr: Bytes;
}
export class Child implements AsyncDisposable {
readonly pid?: number;
readonly stdin?: WritableStream<Uint8Array<ArrayBufferLike>>;
readonly stdout?: ReadableStream<Uint8Array<ArrayBufferLike>>;
readonly stderr?: ReadableStream<Uint8Array<ArrayBufferLike>>;
status(): AsyncResult<{ code: number; success: boolean }, Failure>;
kill(signal?: SignalName): Result<void, Failure>;
}
export function spawn(command: string, options?: CommandOptions): AsyncResult<Child, Failure>;
export function output(
command: string,
options?: CommandOptions,
): AsyncResult<CommandOutput, Failure>;Raw networking is not a browser capability. Browser networking should use fetch, WebSocket, and
WebTransport where available.
export type IpAddress = string;
export interface SocketAddress {
readonly hostname: string;
readonly port: number;
}
export interface TcpConnectOptions extends OperationOptions {
readonly hostname: string;
readonly port: number;
readonly nodelay?: boolean;
readonly keepAlive?: boolean;
}
export class TcpStream implements AsyncDisposable {
readonly readable: ReadableStream<Uint8Array<ArrayBufferLike>>;
readonly writable: WritableStream<Uint8Array<ArrayBufferLike>>;
readonly localAddress: SocketAddress;
readonly remoteAddress: SocketAddress;
close(): AsyncResult<void, Failure>;
}
export function connectTcp(options: TcpConnectOptions): AsyncResult<TcpStream, Failure>;
export interface TcpListenOptions {
readonly hostname?: string;
readonly port: number;
readonly backlog?: number;
readonly signal?: AbortSignal;
}
export class TcpListener implements AsyncDisposable {
readonly address: SocketAddress;
accept(options?: OperationOptions): AsyncResult<TcpStream, Failure>;
close(): AsyncResult<void, Failure>;
}
export function listenTcp(options: TcpListenOptions): AsyncResult<TcpListener, Failure>;UDP and DNS:
export class UdpSocket implements AsyncDisposable {
readonly address: SocketAddress;
receive(options?: OperationOptions): AsyncResult<{ data: Bytes; from: SocketAddress }, Failure>;
send(data: ByteView, to: SocketAddress, options?: OperationOptions): AsyncResult<number, Failure>;
close(): AsyncResult<void, Failure>;
}
export function bindUdp(
options: { hostname?: string; port: number },
): AsyncResult<UdpSocket, Failure>;
export function lookupHost(
hostname: string,
options?: OperationOptions,
): AsyncResult<readonly IpAddress[], Failure>;TLS should either be a submodule or a separate module once certificate and trust-store semantics are clear:
export function connectTls(options: TlsConnectOptions): AsyncResult<TcpStream, Failure>;
export function startTls(stream: TcpStream, options: TlsOptions): AsyncResult<TcpStream, Failure>;Unix domain sockets are net.unix and target-gated.
fetch is the portable HTTP client baseline. sts:http owns server APIs and advanced provider
integration. It should use Web Request / Response objects where practical. Raw host
request/response handler shapes, such as Node's IncomingMessage / ServerResponse, stay behind
explicit #[interop] host imports instead of being re-exported from sts:http.
export type Handler = (request: Request) => Response | AsyncResult<Response, Failure>;
export interface ServeOptions {
readonly hostname?: string;
readonly port: number;
readonly signal?: AbortSignal;
readonly name?: string;
readonly maxRequestBodyBytes?: number;
readonly headersTimeout?: Duration;
readonly requestTimeout?: Duration;
readonly keepAliveTimeout?: Duration;
}
export interface CloseOptions {
readonly forceAfter?: Duration;
}
export class Server implements AsyncDisposable {
readonly address: SocketAddress;
listen(): AsyncResult<Server, Failure>;
serve(): AsyncResult<void, Failure>;
closed(): AsyncResult<void, Failure>;
close(options?: CloseOptions): AsyncResult<void, Failure>;
}
export function server(options: ServeOptions & { handle: Handler }): Result<Server, Failure>;
export function listen(options: ServeOptions & { handle: Handler }): AsyncResult<Server, Failure>;
export function serve(options: ServeOptions & { handle: Handler }): AsyncResult<void, Failure>;maxRequestBodyBytes should reject known oversized Content-Length requests before invoking
handlers and enforce streamed Web-handler bodies as they are read. Timeout options map to the
provider's HTTP server lifecycle controls.
Advanced server options can be added after the first slice:
- TLS
- HTTP/2
- WebSocket upgrade
- handler execution through
ThreadPool - connection limits
This module should be deferred until fetch, streams, networking, and concurrency settle.
The intended role is a portable message/datagram transport abstraction over providers such as:
- WebSocket
- WebTransport
- Node sockets
- WASI sockets
- native sockets
Sketch:
export class MessageTransport implements AsyncDisposable {
readonly incoming: ReadableStream<Bytes>;
readonly outgoing: WritableStream<ByteView>;
close(): AsyncResult<void, Failure>;
}
export class DatagramTransport implements AsyncDisposable {
receive(options?: OperationOptions): AsyncResult<Bytes, Failure>;
send(bytes: ByteView, options?: OperationOptions): AsyncResult<void, Failure>;
}Do not model WebTransport as generic TCP or UDP. It can be a provider for transport shapes that match its actual stream/datagram semantics.
Synchronization APIs are for true parallelism and resource coordination. They should be pay-for-play and capability-gated.
export class Mutex<T> implements Share<Mutex<T>> {
static create<T>(value: Send<T>): Result<Mutex<T>, Failure>;
withLock<R, E = Failure>(
body: (value: T) => Result<R, E> | AsyncResult<R, E>,
options?: OperationOptions,
): AsyncResult<R, E | Failure>;
}
export class Semaphore implements Share<Semaphore> {
static create(permits: number): Result<Semaphore, Failure>;
acquire(options?: OperationOptions): AsyncResult<Permit, Failure>;
}
export class Permit implements AsyncDisposable {
release(): void;
}
export class Channel<T> implements Share<Channel<T>> {
static bounded<T>(capacity: number): Result<Channel<T>, Failure>;
static unbounded<T>(): Result<Channel<T>, Failure>;
send(value: Send<T>, options?: OperationOptions): AsyncResult<void, Failure>;
receive(options?: OperationOptions): AsyncResult<Option<Send<T>>, Failure>;
close(): void;
}Channels are both a concurrency primitive and a low-level runtime feature. They should not be
required for ordinary request fanout; use TaskGroup first.
Atomic and shared-memory APIs should be explicit and low-level.
export class SharedArray<T extends AtomicElement> implements Share<SharedArray<T>> {
readonly length: number;
load(index: number): T;
store(index: number, value: T): void;
add(index: number, value: T): T;
compareExchange(index: number, expected: T, replacement: T): T;
}
export namespace SharedArray {
export function i32(length: number): Result<SharedArray<i32>, Failure>;
export function u32(length: number): Result<SharedArray<u32>, Failure>;
export function i64(length: number): Result<SharedArray<i64>, Failure>;
export function u64(length: number): Result<SharedArray<u64>, Failure>;
}Blocking atomic waits are target-gated. Browser main-thread waits must be unavailable.
Native OS APIs are not part of sts:* by default. They are raw interop:
// #[interop]
import { mmap } from 'native:posix/memory';Use native:* when:
- the API is inherently OS-specific
- the type surface cannot be made portable without lying
- the caller accepts platform-specific build constraints
- the operation needs lower-level access than an
sts:*provider exposes
If a native API proves broadly useful and portable enough, wrap it later behind an sts:* provider
module.
Legend:
yes: expected portable supportprovider: support depends on configured host/provider capabilitypartial: useful subset onlyno: not part of that target profilelater: intentionally deferred
| Surface | js-browser | js-node | wasm-browser | wasm-node | wasm-wasi | native |
|---|---|---|---|---|---|---|
| core pure modules | yes | yes | yes | yes | yes | yes |
| capabilities query | yes | yes | yes | yes | yes | yes |
| path/bytes | yes | yes | yes | yes | yes | yes |
| Web URL/text | yes | yes | yes | yes | provider | yes |
| fetch/client HTTP | yes | yes | provider | provider | provider | yes |
| streams | yes | yes | provider | provider | provider | yes |
| console | yes | yes | provider | provider | provider | yes |
| crypto random/hash/HMAC | yes | yes | provider | provider | provider | yes |
| time clocks/timers | yes | yes | provider | provider | provider | yes |
| AsyncResult/Task | yes | yes | yes | yes | yes | yes |
| TaskGroup/AsyncContext | provider | yes | provider | provider | provider | yes |
| ThreadPool/Thread | provider | provider | provider | provider | provider | yes |
| shared memory/atomics | provider | yes | provider | yes | provider | yes |
| fs | no | yes | provider | yes | provider | yes |
| env read | no | yes | provider | yes | provider | yes |
| env write | no | provider | no | provider | provider | yes |
| cli stdio/args | no | yes | no | yes | provider | yes |
| process info/cwd | no | yes | no | yes | provider | yes |
| child process | no | yes | no | yes | no | yes |
| DNS/TCP/TLS | no | yes | no | yes | provider | yes |
| UDP | no | later | no | later | provider | yes |
| HTTP server | no | yes | no | yes | provider | yes |
| WebSocket | yes | provider | provider | provider | no | later |
| WebTransport | provider | no | provider | no | no | later |
raw web:* |
yes | no | yes | no | no | no |
raw node:* |
no | yes | no | yes | no | no |
raw native:* |
no | no | no | no | no | yes |
raw extern:* |
yes | yes | yes | yes | no | later |
The current implementation intentionally starts with JS targets and leaves Wasm provider work gated until the Wasm runtime/compiler path is ready.
| Module/API | js-browser today | js-node today |
|---|---|---|
sts:concurrency/task |
implemented | implemented |
sts:concurrency/runtime |
gated | TaskGroup, TaskHandle, AsyncContext, Runtime |
sts:capabilities, sts:time, sts:console, sts:path, sts:bytes |
implemented | implemented |
sts:url, sts:fetch, sts:streams, sts:text, sts:random, sts:crypto, sts:crypto/digest, sts:crypto/hmac |
implemented | implemented |
sts:fs |
gated | file read/write, stat/lstat, directories, copy/rename/remove, real path |
sts:env |
gated | read, required, set, remove, record snapshot |
sts:cli |
gated | args, stdio streams, terminal checks, terminal size, readLine/write/writeLine |
sts:process, sts:process/command, sts:process/signals |
gated | process info/cwd/platform/uptime/signals/exit code and child process spawn/output |
sts:http |
gated | Web Request/Response server, ready listen, run-until-closed serve, body limits, server timeouts, force-deadline close |
sts:net, sts:net/dns, sts:net/tcp, sts:net/tls |
gated | DNS lookup, TCP connect/listen, TLS connect/listen |
sts:concurrency/parallel, sts:concurrency/sync, sts:concurrency/atomics |
gated/unsupported placeholders | gated/unsupported placeholders |
Browser code should continue to use Web-platform APIs for browser-native capabilities. sts:fetch
is the portable HTTP client surface; WebSocket and WebTransport remain Web-platform APIs until a
future sts:transport abstraction is justified. sts:net is deliberately raw DNS/TCP/TLS and is
not a browser networking facade.
- Add this plan and link it from the plans index.
- Update
docs/reference/builtin-modules.mdwith planned modules. - Replace
sts:asyncwithsts:concurrency/task. - Remove ambiguous
sts:async.parallel. - Replace bare task helpers with the
Task.*value helper surface. - Add
AsyncResultand sharedAbortSignaldeclarations. - Add provider capability metadata names.
- Harden
sts:url,sts:fetch,sts:streams,sts:text,sts:random, andsts:console. - Make target global injection explicit by profile.
- Normalize
AbortSignalacross modules. - Add
sts:timetimers and monotonic clock facade.
- Define provider manifest shape.
- Teach checker/package recheck to read target provider capabilities.
- Route provider-backed APIs through JS and Wasm wrappers.
- Add
UnsupportedCapabilityFailure.
- Implement
sts:fs,sts:env,sts:cli, andsts:processfor Node-family targets first, withsts:process/commandandsts:process/signalsas narrower provider entry points. - Add Wasm-hosted provider shims where semantics are honest.
- Keep browser unsupported diagnostics precise.
- Stabilize
sts:httpserver shape. - Add
sts:netDNS/TCP/TLS first, withsts:net/dns,sts:net/tcp, andsts:net/tlsas narrower provider entry points. - Add UDP after stream/resource semantics settle.
- Keep WebSocket/WebTransport as Web-platform/provider surfaces until
sts:transportis justified.
- Add
Send/Sharechecker rules. - Add transfer/shared byte buffers.
- Add
sts:concurrency/syncandsts:concurrency/atomics. - Add worker/thread-backed providers and target-gated diagnostics.
AbortSignal/AbortControllershould be Web-compatible globals and re-exported fromsts:concurrency. Do not addsts:abortyet.sts:pathshould expose explicitposixandwindowsAPIs. Do not add a magical provider-native namespace.fetchis the v1 portable HTTP client.sts:httpshould own servers, upgrades, and later provider-heavy HTTP features.sts:consoleshould acceptunknownlike JavaScript console and document best-effort diagnostic formatting. Structured application logging belongs in a futurests:log.sts:cryptoshould start with random bytes, digest helpers, HMAC helpers, and timing-safe equality, withsts:crypto/digestandsts:crypto/hmacas narrower entry points. Defer broad WebCrypto key/subtle APIs.sts:transportshould be deferred. Keep WebSocket/WebTransport as Web platform APIs and raw sockets insts:netuntil a common transport abstraction is justified.Taskhelpers should be exposed asTask.*. Declaration files can use a type/namespace merge; runtime modules can implement that surface with an equivalent value object.- Keep
sts:capabilitiesas the simple public capability-query module.Runtime.capabilities()may exist as a scoped runtime-context view, but normal code should prefersts:capabilities. - The first
Sendproof should be conservative: primitives, strings, readonly arrays/tuples/records ofSend, deep#[value]classes, frozenBytes, transfer buffers, explicit shared buffers, and provider-declared handles only.
- Which
sts:concurrency/*submodules should ship first versus existing only as re-export paths? - What is the minimal provider manifest shape needed by the checker without overfitting the first JS providers?