Skip to content
Lucent
SEARCH LUCENT

Search guides, APIs, and examples.

GitHub

Very early and experimental. The language, the generated native code and every package API change without notice. Do not use Lucent in production.

The lucent:* modules

The declarations of lucent:core, lucent:platform, lucent:thread, lucent:ios and lucent:android.

Helpers for any module: delays, errors with codes, UTF-8, a monotonic clock, and promises and subscriptions over callback APIs such as native listeners. compute runs work on worker threads (Run heavy work in parallel). Its JavaScript version, @lucent-lang/lucent/core, lets tests run modules as TypeScript (Test a module).

core.d.ts
/** Resolves after `ms` milliseconds; rejects with the signal's reason if it aborts first. */
export declare function delay(ms: number, signal?: AbortSignal): Promise<void>;
/** An Error with a machine-readable `code`, visible to JavaScript as `error.code`. */
export declare function error(code: string, message: string): Error;
/** The `code` of an error created with `error()`, or undefined. */
export declare function errorCode(e: Error): string | undefined;
/** UTF-8 encoding of a string (like `new TextEncoder().encode(s)`). */
export declare function utf8Encode(s: string): Uint8Array;
/** Decodes UTF-8 bytes (like `new TextDecoder().decode(b)`), replacing invalid sequences. */
export declare function utf8Decode(bytes: Uint8Array): string;
/** Milliseconds from a monotonic clock, for measuring durations. */
export declare function now(): number;
/**
* A promise of what a callback API reports once, such as a native listener.
* `register` starts listening, at once, and may return the cleanup that
* stops it:
*
* ```ts
* fromCallback<string>((resolve, reject) => {
* const subscription = source.listen(resolve, reject);
* return () => subscription.cancel();
* }, signal);
* ```
*
* The first of `resolve`, `reject` and the signal aborting settles the
* promise; later calls do nothing. The cleanup runs exactly once, as soon as
* the promise settles, or right after `register` returns if it settled
* during registration. A throw from `register` rejects the promise. With an
* aborted signal, the promise rejects with the signal's reason and
* `register` is not called. A cleanup that throws is reported as uncaught;
* the promise keeps its outcome. `resolve` takes a value, not a promise.
*
* Called from another thread (a `main` block, say), `resolve` and `reject`
* take effect on the thread `fromCallback` was called on, in the order
* they were called.
*/
export declare function fromCallback<T>(
register: (resolve: (value: T) => void, reject: (reason: Error) => void) => (() => void) | void,
signal?: AbortSignal,
): Promise<T>;
/**
* Passes what a listener reports to `onValue` until the subscription ends,
* and resolves when it does. `register` starts listening, at once, and may
* return the cleanup that stops it.
*
* `next(value)` calls `onValue(value)` while the subscription is open, and
* does nothing after. The first of `end()`, `fail(error)`, `onValue`
* throwing and the signal aborting ends it: the promise resolves (`end`) or
* rejects (with the error, or the signal's reason), and the cleanup runs
* exactly once. Registration, an aborted signal, a throwing cleanup and
* calls from other threads behave as in `fromCallback`.
*/
export declare function subscribe<T>(
register: (
next: (value: T) => void,
end: () => void,
fail: (error: Error) => void,
) => (() => void) | void,
onValue: (value: T) => void,
signal?: AbortSignal,
): Promise<void>;
/** How a compute task runs. */
export interface ComputeOptions {
/** Aborting it cancels the task: the promise rejects with its reason at once. */
signal?: AbortSignal;
}
/**
* Runs `task(input)` on a pool of worker threads and resolves with its
* result, on the thread that called `compute`. `task` is a function declared
* at the top level of a module, taking one parameter: the compiler checks
* everything it runs can run on a worker (no module state, no main-thread
* or unknown-thread native code, nothing asynchronous) and that `input` and
* its result are data.
*
* `input` is copied when `compute` is called: later changes by the caller
* do not reach the task. Objects reached twice are copied once, and cycles
* survive. The result comes back as it is.
*
* A task that throws rejects the promise with its error. With `signal`,
* aborting it rejects the promise at once with the signal's reason: a
* queued task never starts, and a running one stops at the next iteration
* of a loop in the task or in a module function it calls. Loops in
* closures, methods and generic functions, and native calls, run to their
* end first; the result is then dropped. The pool
* holds a bounded number of waiting tasks; beyond that, `compute` rejects
* with a QuotaExceededError.
*
* ```ts
* function edgePositions(bytes: Uint8Array): number[] {
* const positions: number[] = [];
* for (let i = 1; i < bytes.length; i++)
* if (Math.abs(bytes[i]! - bytes[i - 1]!) > 40) positions.push(i);
* return positions;
* }
*
* export async function edges(bytes: Uint8Array, signal: AbortSignal): Promise<number[]> {
* return await compute(edgePositions, bytes, { signal });
* }
* ```
*/
export declare function compute<T, R>(
task: (input: T) => R,
input: T,
options?: ComputeOptions,
): Promise<Awaited<R>>;
/**
* Bytes native code owns, handed to compute tasks and JavaScript without
* copying them. A `Uint8Array` is copied whenever it crosses; a
* NativeBuffer moves.
*
* The bytes are reached through a borrow, for the length of one call:
* `withRead` lends them to its callback as a `ByteSpan`, `withWrite` as a
* `MutableByteSpan`. Reads share the buffer; a write needs it to itself.
* A borrow that would conflict with one in progress (a write inside a
* read, a close or transfer inside either) throws an InvalidStateError at
* once. The compiler keeps a span inside its callback: it cannot be
* returned, stored, captured by a closure that outlives the call, passed
* to code that keeps it, or held across `await`.
*
* `transfer()` moves the bytes to a new buffer, uncopied: every reference
* to the old one then refuses them. Passing a buffer to `compute`, alone
* or inside the input, moves it the same way, and a task's buffer comes
* back as it is. Copies are explicit: `NativeBuffer.from(bytes)` and
* `toUint8Array()`, which `NativeBuffer.stats()` counts.
*
* ```ts
* function scan(buffer: NativeBuffer): number {
* using owned = buffer;
* return owned.withRead((bytes) => {
* let total = 0;
* for (let i = 0; i < bytes.length; i++) total += bytes[i]!;
* return total;
* });
* }
*
* export async function sample(): Promise<number> {
* const buffer = NativeBuffer.allocate(4096);
* buffer.withWrite((bytes) => bytes.fill(7));
* return await compute(scan, buffer.transfer());
* }
* ```
*
* JavaScript sees a buffer as an opaque object with the same methods; its
* borrows lend it a copy of the bytes (copied back after `withWrite`), so
* JavaScript never holds memory a worker may be writing.
*/
export declare class NativeBuffer {
private constructor();
/** A buffer of `size` zeroed bytes: a RangeError unless `size` is a whole number from 0. */
static allocate(size: number): NativeBuffer;
/** A new buffer holding a copy of `bytes`. */
static from(bytes: Uint8Array): NativeBuffer;
/** What every buffer has done since the app started. */
static stats(): NativeBufferStats;
/** How many bytes it holds: 0 once closed or transferred. */
readonly byteLength: number;
/** Calls `read` with the bytes, while no one writes them, and returns what it returns. */
withRead<R>(read: (bytes: ByteSpan) => R): R;
/** Calls `write` with the bytes, while nothing else borrows them, and returns what it returns. */
withWrite<R>(write: (bytes: MutableByteSpan) => R): R;
/** A copy of the bytes, independent of the buffer from then on. */
toUint8Array(): Uint8Array;
/** Moves the bytes, uncopied, to a new buffer: every reference to this one refuses them from now on. */
transfer(): NativeBuffer;
/** Releases the bytes. Closing a closed or transferred buffer does nothing. */
close(): void;
[Symbol.dispose](): void;
}
/** The bytes `withRead` lends, read like a `Uint8Array`'s: `bytes[i]` is undefined out of range. */
export interface ByteSpan {
readonly length: number;
readonly [index: number]: number;
}
/** The bytes `withWrite` lends, written like a `Uint8Array`'s: values wrap to 0–255, writes out of range do nothing. */
export interface MutableByteSpan extends ByteSpan {
[index: number]: number;
/** Sets the bytes from `start` to `end` (counted from the end when negative) to `value`. */
fill(value: number, start?: number, end?: number): void;
/** Copies `source` in at `offset`: a RangeError if it does not fit. */
set(source: Uint8Array, offset?: number): void;
}
/** Counts of what native buffers have done, for checking that a path does not copy. */
export interface NativeBufferStats {
/** Buffers allocated, including those `from` made. */
readonly allocated: number;
/** Buffers native code handed over with its own memory. */
readonly adopted: number;
/** Moves of a buffer's bytes to a new buffer: `transfer()` and handoffs to tasks. */
readonly transfers: number;
/** Copies in and out (`from`, `toUint8Array`, and JavaScript's borrows), and their bytes. */
readonly copies: number;
readonly bytesCopied: number;
}

Which platform the code runs on, for platform branches.

platform.d.ts
/**
* The platform: `if (PLATFORM === "ios") { … } else { … }` and
* `PLATFORM === "ios" ? … : …` compile each platform's branch only, so a
* shared module can use `lucent:ios/…` and `lucent:android/…` in them.
*/
export declare const PLATFORM: "ios" | "android";

The main thread, for platform code.

thread.d.ts
/**
* Runs `f` on the platform's main thread (the main queue on iOS, the main
* Looper on Android) and resolves with its result. Main-thread-only SDK
* APIs may only be used inside `f`.
*/
export declare function main<T>(f: () => T): Promise<T>;

iOS helpers for platform code. The SDK itself is lucent:ios/<Framework>.

ios.d.ts
import type { UIViewController } from "lucent:ios/UIKit";
/** Whether the OS is at least `major.minor` (Swift's `#available`). */
export declare function available(platform: "ios", major: number, minor?: number): boolean;
/** The root of Objective-C objects. Values of type `Any` (`id`) arrive as NSObjects. */
export declare class NSObject {
private readonly __lucent_NSObject: never;
protected constructor();
}
/** What can go where Objective-C takes `Any` (`id`), CoreFoundation values included. */
export type ObjCValue = string | number | boolean | Uint8Array | Date | NSObject | null;
/** The main dispatch queue (dispatch_get_main_queue()), for APIs that take a queue. */
export declare function mainQueue(): NSObject;
/** Swift's `as? String`: the string an `Any` holds, or null. */
export declare function asString(value: NSObject | null): string | null;
/** Swift's `as? Double` (an NSNumber). */
export declare function asNumber(value: NSObject | null): number | null;
/** Swift's `as? Bool` (an NSNumber). */
export declare function asBoolean(value: NSObject | null): boolean | null;
/** Swift's `as? Data`. */
export declare function asData(value: NSObject | null): Uint8Array | null;
/** Swift's `as? Date`. */
export declare function asDate(value: NSObject | null): Date | null;
/**
* What a method writes through a pointer (`CGFloat *`, `NSRange *`,
* `NSDate **`, `CFTypeRef *`): pass it, then read `value`. For a pointer the
* method also reads (a number or a struct), set `value` first.
*/
export declare class Out<T> {
constructor();
value: T | null;
}
/**
* UIApplication's lifecycle notifications, by name: `"didBecomeActive"` is
* `UIApplication.didBecomeActiveNotification`.
*/
export type AppEvent =
| "didBecomeActive"
| "willResignActive"
| "didEnterBackground"
| "willEnterForeground"
| "didReceiveMemoryWarning"
| "willTerminate";
/**
* UIScene's lifecycle notifications, by name: `"willConnect"` is
* `UIScene.willConnectNotification`.
*/
export type SceneEvent =
| "willConnect"
| "didDisconnect"
| "didActivate"
| "willDeactivate"
| "willEnterForeground"
| "didEnterBackground";
/**
* Calls `listener` each time the app posts `event`, until the returned
* function is called or `signal` aborts. It runs on the main thread, as
* UIKit posts it, so main-thread APIs work there without `main()`. Lucent
* observes the notifications: the app's delegate, and other modules', stay
* as they are. What `listener` throws is logged; the app goes on.
*/
export declare function onAppEvent(
event: AppEvent,
listener: () => void,
signal?: AbortSignal,
): () => void;
/**
* Like `onAppEvent`, for each scene's `event`: `listener` gets the scene's
* session's `persistentIdentifier`.
*/
export declare function onSceneEvent(
event: SceneEvent,
listener: (scene: string) => void,
signal?: AbortSignal,
): () => void;
/**
* Presents the view controller `build` returns from the scene the person is
* using (the top view controller of its key window), and resolves with the
* value given to `resolve`, or rejects with the error given to `reject`.
* `build` runs on the main thread, like `main()`'s function: make the view
* controller there, and call `resolve` or `reject` from its delegate or
* completion handler.
*
* It settles once, and whatever settles it dismisses the view controller if
* it is still shown. It rejects with an `AbortError` when `signal` aborts,
* when the person dismisses the view controller (swiping a sheet down), when
* the view controller goes before settling, or when its scene disconnects;
* with an `InvalidStateError` when no scene is in the foreground or UIKit
* does not present it.
*/
export declare function present<T>(
build: (resolve: (value: T) => void, reject: (reason: Error) => void) => UIViewController,
signal?: AbortSignal,
): Promise<T>;

Android helpers for platform code. The SDK itself is lucent:android/<package>.

android.d.ts
import type { Activity, Instrumentation_ActivityResult } from "lucent:android/android.app";
import type { Context, Intent } from "lucent:android/android.content";
import type { Throwable } from "lucent:android/java.lang";
/** The application Context. */
export declare function appContext(): Context;
/**
* The Error Lucent makes of `throwable` when a call throws it: its message
* the exception's (or its class name, without one) and its `code` the class
* name (`java.lang.IllegalStateException`). An exception carrying a Lucent
* error (one a Lucent suspend function ended a Kotlin call with) gives
* that error.
* For an adapter whose callback API reports failure with a Throwable:
* `reject(errorOf(e))` rejects as the call would have thrown.
*/
export declare function errorOf(throwable: Throwable): Error;
/** Whether the device runs at least API level `api` (`Build.VERSION.SDK_INT >= api`). */
export declare function available(platform: "android", api: number): boolean;
/**
* The Activity in front: the one last created, started or resumed and not
* destroyed, or null (before the first, between two, after the last).
* Lucent holds Activities weakly: use this one now, on the main thread
* (inside `main()`), and ask again later rather than keeping it.
*/
export declare function currentActivity(): Activity | null;
/**
* Starts `intent` from the Activity in front and resolves with its result
* (`getResultCode()`, `getResultData()`). The request survives the app's
* Activity being recreated meanwhile. Rejects with `ERR_NO_ACTIVITY` without
* an Activity, `ERR_ACTIVITY_NOT_FOUND` when no app handles the intent, and
* the signal's reason if it aborts first: the answer, if it still comes, is
* dropped (Lucent closes what it started when Android lets it).
*/
export declare function startActivityForResult(
intent: Intent,
signal?: AbortSignal,
): Promise<Instrumentation_ActivityResult>;
/**
* Asks for runtime permissions (`"android.permission.CAMERA"`), and
* resolves with whether each was granted, in order. Requests wait for the
* one before them, as Android asks one at a time. Rejects like
* `startActivityForResult`.
*/
export declare function requestPermissions(
permissions: string[],
signal?: AbortSignal,
): Promise<boolean[]>;
/** The lifecycle events of the app's Activities, and a new intent sent to one. */
export type ActivityEvent =
| "created"
| "started"
| "resumed"
| "paused"
| "stopped"
| "destroyed"
| "newIntent";
/**
* Runs `handler` after each `event` of the app's Activities, with the
* Activity (and the intent, for "newIntent"), in a turn of the calling
* context. Returns the function that stops it.
*/
export declare function onActivityEvent(
event: ActivityEvent,
handler: (activity: Activity, intent: Intent | null) => void,
): () => void;