Runtime APIs
This page covers the Fickl.Core bindings and compiler support for JavaScript runtime features: DOM and Node.js events as F# events, WeakRef and FinalizationRegistry, Proxy and Reflect, copying F# values through structuredClone and postMessage, typed-array views, and import attributes. Each section is independent, so read the ones you need.
Events
JsEvent.ofTarget target "click" is an F# IEvent over addEventListener. The Event and Observable functions apply to it, and disposing a subscription or removing a handler calls removeEventListener. Nothing listens while the event has no handlers.
open Fable.Core
open Fickl.Core
[<Global("document.body")>]
let body: JS.EventTarget = jsNative
let clicks: IEvent<JS.Event> = JsEvent.ofTarget body "click"
let watch () =
clicks
|> Observable.filter (fun e -> not e.defaultPrevented)
|> Observable.subscribe (fun e -> printfn "clicked %s" e.``type``)
import { fromEventTarget } from "@fable-org/fable-library-js/Event.js";
import { filter, subscribe } from "@fable-org/fable-library-js/Observable.js";
import { printf, toConsole } from "@fable-org/fable-library-js/String.js";
export const clicks = fromEventTarget(document.body, "click");
export function watch() {
return subscribe((e_1) => {
const arg = e_1.type;
toConsole(printf("clicked %s"))(arg);
}, filter((e) => !e.defaultPrevented, clicks));
}
import { type IEvent$2, fromEventTarget } from "@fable-org/fable-library-ts/Event.ts";
import { filter, subscribe } from "@fable-org/fable-library-ts/Observable.ts";
import { printf, toConsole } from "@fable-org/fable-library-ts/String.ts";
import type { IDisposable } from "@fable-org/fable-library-ts/Util.ts";
export const clicks: IEvent$2<((arg0: any | null, arg1: Event) => void), Event> = fromEventTarget(document.body, "click");
export function watch(): IDisposable {
return subscribe<Event>((e_1: Event): void => {
const arg: string = e_1.type;
toConsole(printf("clicked %s"))(arg);
}, filter<Event>((e: Event): boolean => !e.defaultPrevented, clicks));
}
The type argument of the event, here JS.Event, is the type of the event object, and nothing checks it. JsEvent.ofTargetWith options target "scroll" listens with addEventListener options such as passive or capture, built with jsOptions<JS.AddEventListenerOptions>. A JS.AbortSignal is an EventTarget too, so JsEvent.ofTarget signal "abort" is its abort event.
Node.js emitters
JsEvent.ofEmitter does the same over an object with on and off (or removeListener) methods, such as a Node.js EventEmitter, stream or socket.
open Fickl.Core
let lines (stream: obj) : IEvent<string> = JsEvent.ofEmitter stream "data"
let print (stream: obj) =
(lines stream).Add(fun line -> printfn "%s" line)
import { fromEmitter } from "@fable-org/fable-library-js/Event.js";
import { add } from "@fable-org/fable-library-js/Observable.js";
import { printf, toConsole } from "@fable-org/fable-library-js/String.js";
export function lines(stream) {
return fromEmitter(stream, "data");
}
export function print(stream) {
add((line) => {
toConsole(printf("%s"))(line);
}, lines(stream));
}
import { type IEvent$2, fromEmitter } from "@fable-org/fable-library-ts/Event.ts";
import { add } from "@fable-org/fable-library-ts/Observable.ts";
import { printf, toConsole } from "@fable-org/fable-library-ts/String.ts";
export function lines(stream: any): IEvent$2<((arg0: any | null, arg1: string) => void), string> {
return fromEmitter(stream, "data");
}
export function print(stream: any): void {
add((line: string): void => {
toConsole(printf("%s"))(line);
}, lines(stream));
}
The F# event's value is the first argument the emitter passes; the others are dropped.
Dispatching events from an F# class
An F# class that inherits JS.EventTarget is a JavaScript EventTarget, so JavaScript code subscribes to it with addEventListener. JsEvent.dispatch this "change" value dispatches a CustomEvent with the value as its detail, and returns false if a listener cancelled the event.
open Fickl.Core
type Store() =
inherit JS.EventTarget()
let mutable value = 0
member this.Changed: IEvent<JS.CustomEvent<int>> = JsEvent.ofTarget this "change"
member this.Set(next: int) =
value <- next
JsEvent.dispatch this "change" next |> ignore
import { dispatchCustomEvent, fromEventTarget } from "@fable-org/fable-library-js/Event.js";
import { class_type } from "@fable-org/fable-library-js/Reflection.js";
export class Store extends EventTarget {
#value;
constructor() {
super();
this.#value = 0;
}
get Changed() {
const this$ = this;
return fromEventTarget(this$, "change");
}
Set(next) {
const this$ = this;
this$.#value = (next | 0);
dispatchCustomEvent(this$, "change", next);
}
}
export function Store_$reflection() {
return class_type("Example3.Store", undefined, Store, class_type("Fickl.Core.JS.EventTarget", undefined, EventTarget));
}
export function Store_$ctor() {
return new Store();
}
import { dispatchCustomEvent, type IEvent$2, fromEventTarget } from "@fable-org/fable-library-ts/Event.ts";
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import { class_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";
export class Store extends EventTarget {
#value: int32;
constructor() {
super();
this.#value = 0;
}
get Changed(): IEvent$2<((arg0: any | null, arg1: CustomEvent<int32>) => void), CustomEvent<int32>> {
const this$: Store = this;
return fromEventTarget(this$, "change");
}
Set(next: int32): void {
const this$: Store = this;
this$.#value = (next | 0);
dispatchCustomEvent(this$, "change", next);
}
}
export function Store_$reflection(): TypeInfo {
return class_type("Example3.Store", undefined, Store, class_type("Fickl.Core.JS.EventTarget", undefined, EventTarget));
}
export function Store_$ctor(): Store {
return new Store();
}
Store compiles to class Store extends EventTarget. Because its base is a JavaScript class, its let fields are ES #private fields, here #value. JS.Event, JS.CustomEvent<'T> and the listener options bind the web types.
Weak references
System.WeakReference<'T> compiles to a native WeakRef, typed WeakRef<T> in TypeScript, and TryGetTarget works as on .NET. JS.FinalizationRegistry calls back with a held value some time after a registered object is collected.
open System
open Fickl.Core
type Node(name: string) =
member _.Name = name
let track (node: Node) = WeakReference<Node>(node)
let tryName (reference: WeakReference<Node>) =
match reference.TryGetTarget() with
| true, node -> Some node.Name
| _ -> None
let registry = JS.FinalizationRegistry(fun (held: string) -> printfn "collected %s" held)
let watch (node: Node) = registry.register (node, node.Name)
import { class_type, string_type } from "@fable-org/fable-library-js/Reflection.js";
import { defaultOf } from "@fable-org/fable-library-js/Util.js";
import { tryGetTarget } from "@fable-org/fable-library-js/WeakRef.js";
import { FSharpRef } from "@fable-org/fable-library-js/Types.js";
import { printf, toConsole } from "@fable-org/fable-library-js/String.js";
export class Node$ {
constructor(name) {
this.name = name;
}
}
export function Node$_$reflection() {
return class_type("Example4.Node", undefined, Node$, undefined, () => [[[["name", string_type]]]]);
}
export function Node_$ctor_Z721C83C5(name) {
return new Node$(name);
}
export function Node__get_Name(_) {
return _.name;
}
export function track(node) {
return new WeakRef(node);
}
export function tryName(reference) {
let matchValue;
let outArg = defaultOf();
matchValue = [tryGetTarget(reference, new FSharpRef(() => outArg, (v) => {
outArg = v;
})), outArg];
if (matchValue[0]) {
return Node__get_Name(matchValue[1]);
}
else {
return undefined;
}
}
export const registry = new FinalizationRegistry((held) => {
toConsole(printf("collected %s"))(held);
});
export function watch(node) {
registry.register(node, Node__get_Name(node));
}
import { class_type, string_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";
import { defaultOf } from "@fable-org/fable-library-ts/Util.ts";
import { tryGetTarget } from "@fable-org/fable-library-ts/WeakRef.ts";
import { FSharpRef } from "@fable-org/fable-library-ts/Types.ts";
import type { Option } from "@fable-org/fable-library-ts/Option.ts";
import { printf, toConsole } from "@fable-org/fable-library-ts/String.ts";
export class Node$ {
readonly name: string;
constructor(name: string) {
this.name = name;
}
}
export function Node$_$reflection(): TypeInfo {
return class_type("Example4.Node", undefined, Node$, undefined, () => [[[["name", string_type]]]]);
}
export function Node_$ctor_Z721C83C5(name: string): Node$ {
return new Node$(name);
}
export function Node__get_Name(_: Node$): string {
return _.name;
}
export function track(node: Node$): WeakRef<Node$> {
return new WeakRef<Node$>(node);
}
export function tryName(reference: WeakRef<Node$>): Option<string> {
let matchValue: [boolean, Node$];
let outArg: Node$ = defaultOf();
matchValue = ([tryGetTarget(reference, new FSharpRef<Node$>((): Node$ => outArg, (v: Node$): void => {
outArg = v;
})), outArg] as [boolean, Node$]);
if (matchValue[0]) {
return Node__get_Name(matchValue[1]);
}
else {
return undefined;
}
}
export const registry: FinalizationRegistry<string> = new FinalizationRegistry<string>((held: string): void => {
toConsole(printf("collected %s"))(held);
});
export function watch(node: Node$): void {
registry.register(node, Node__get_Name(node));
}
SetTarget is a compile error, as a WeakRef cannot be retargeted. JS.WeakRef<'T> binds the JavaScript type directly, with deref returning an option.
Proxies
JS.Proxy.create target handler is new Proxy(target, handler), typed as its target. JS.ProxyHandler.create builds the handler as an object literal with only the traps you set, checked against ProxyHandler<T> in TypeScript. In each trap, the JS.Reflect member of the same name performs the default operation.
open Fable.Core
open Fickl.Core
type Point = { X: int; Y: int }
let logged (point: Point) : Point =
JS.Proxy.create
point
(JS.ProxyHandler.create (fun h ->
h.get <-
fun target key receiver ->
match key with
| U3.Case1 name -> printfn "read %s" name
| _ -> ()
JS.Reflect.get (target, key, receiver)))
import { Record } from "@fable-org/fable-library-js/Types.js";
import { record_type, int32_type } from "@fable-org/fable-library-js/Reflection.js";
import { printf, toConsole } from "@fable-org/fable-library-js/String.js";
export class Point extends Record {
constructor(X, Y) {
super();
this.X = (X | 0);
this.Y = (Y | 0);
}
}
export function Point_$reflection() {
return record_type("Example5.Point", [], Point, () => [["X", int32_type], ["Y", int32_type]]);
}
export function logged(point) {
return new Proxy(point, {
get: (target, key, receiver) => {
if (typeof key === "string") {
toConsole(printf("read %s"))(key);
}
return Reflect.get(target, key, receiver);
},
});
}
import { Record } from "@fable-org/fable-library-ts/Types.ts";
import type { float64, int32 } from "@fable-org/fable-library-ts/Int32.ts";
import type { IComparable, IEquatable } from "@fable-org/fable-library-ts/Util.ts";
import { record_type, int32_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";
import { printf, toConsole } from "@fable-org/fable-library-ts/String.ts";
export class Point extends Record implements IEquatable<Point>, IComparable<Point> {
readonly X: int32;
readonly Y: int32;
constructor(X: int32, Y: int32) {
super();
this.X = (X | 0);
this.Y = (Y | 0);
}
}
export function Point_$reflection(): TypeInfo {
return record_type("Example5.Point", [], Point, () => [["X", int32_type], ["Y", int32_type]]);
}
export function logged(point: Point): Point {
return new Proxy(point, ({
get: (target: Point, key: string | float64 | (symbol), receiver: any): any => {
if (typeof key === "string") {
const name: string = key;
toConsole(printf("read %s"))(name);
}
return Reflect.get(target, key, receiver);
},
} satisfies Partial<ProxyHandler<Point>> as ProxyHandler<Point>));
}
A key is a JS.PropertyKey, the TypeScript string | number | symbol. A trap receives a string or a symbol; a number key arrives as its string. JS.Proxy.revocable returns a proxy with a revoke function, and JS.Symbol binds Symbol() (as JS.Symbol.create) and Symbol.for.
Nothing checks that the traps keep to the target's type: a get trap can return anything, and F# still sees a Point. A set trap that returns false makes the write throw a TypeError, as ES modules run in strict mode.
Copying F# values with structuredClone
structuredClone, postMessage, BroadcastChannel and IndexedDB copy the data of a value but drop its prototypes, so an F# record, union, list, map or set is not a working F# value after the copy. An F# map or set, which holds its comparer function, throws a DataCloneError.
Transfer.toPlain value copies an F# value into plain objects and arrays by its type, and Transfer.ofPlain<'T> data rebuilds it with its prototypes. Transfer.clone does both around structuredClone.
open Fickl.Core
type Shape =
| Circle of radius: float
| Empty
type Model = { Name: string; Shapes: Shape list }
let send (model: Model) : obj = Transfer.toPlain model
let receive (data: obj) : Model = Transfer.ofPlain data
let copy (model: Model) = Transfer.clone model
let rawCopy (values: string[]) = JS.structuredClone values
import { Record, Union } from "@fable-org/fable-library-js/Types.js";
import { record_type, list_type, string_type, union_type, float64_type } from "@fable-org/fable-library-js/Reflection.js";
import { clone, ofPlain, toPlain } from "@fable-org/fable-library-js/Transfer.js";
export class Shape extends Union {
constructor(tag, fields) {
super();
this.tag = tag;
this.fields = fields;
}
cases() {
return ["Circle", "Empty"];
}
static Empty = new Shape(1, []);
}
export function Shape_$reflection() {
return union_type("Example6.Shape", [], Shape, () => [[["radius", float64_type]], []]);
}
export class Model extends Record {
constructor(Name, Shapes) {
super();
this.Name = Name;
this.Shapes = Shapes;
}
}
export function Model_$reflection() {
return record_type("Example6.Model", [], Model, () => [["Name", string_type], ["Shapes", list_type(Shape_$reflection())]]);
}
export function send(model) {
return toPlain(model, Model_$reflection());
}
export function receive(data) {
return ofPlain(data, Model_$reflection());
}
export function copy(model) {
return clone(model, Model_$reflection());
}
export function rawCopy(values) {
return structuredClone(values);
}
import { Record, Union } from "@fable-org/fable-library-ts/Types.ts";
import type { float64 } from "@fable-org/fable-library-ts/Int32.ts";
import { record_type, list_type, string_type, union_type, float64_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";
import type { FSharpList } from "@fable-org/fable-library-ts/List.ts";
import type { MutableArray, IComparable, IEquatable } from "@fable-org/fable-library-ts/Util.ts";
import { clone, ofPlain, toPlain } from "@fable-org/fable-library-ts/Transfer.ts";
export type Shape_$union =
| Shape<0>
| Shape<1>
export type Shape_$cases = {
0: ["Circle", [float64]],
1: ["Empty", []]
}
export function Shape_Circle(radius: float64) {
return new Shape<0>(0, [radius]);
}
export function Shape_Empty() {
return Shape.Empty;
}
export class Shape<Tag extends keyof Shape_$cases> extends Union<Tag, Shape_$cases[Tag][0]> {
constructor(tag: Tag, fields: Shape_$cases[Tag][1]) {
super();
this.tag = tag;
this.fields = fields;
}
readonly tag: Tag;
readonly fields: Shape_$cases[Tag][1];
cases() {
return ["Circle", "Empty"];
}
static readonly Empty: Shape_$union = new Shape<1>(1, []);
}
export function Shape_$reflection(): TypeInfo {
return union_type("Example6.Shape", [], Shape, () => [[["radius", float64_type]], []]);
}
export class Model extends Record implements IEquatable<Model>, IComparable<Model> {
readonly Name: string;
readonly Shapes: FSharpList<Shape_$union>;
constructor(Name: string, Shapes: FSharpList<Shape_$union>) {
super();
this.Name = Name;
this.Shapes = Shapes;
}
}
export function Model_$reflection(): TypeInfo {
return record_type("Example6.Model", [], Model, () => [["Name", string_type], ["Shapes", list_type(Shape_$reflection())]]);
}
export function send(model: Model): any {
return toPlain(model, Model_$reflection());
}
export function receive(data: any): Model {
return ofPlain(data, Model_$reflection());
}
export function copy(model: Model): Model {
return clone(model, Model_$reflection());
}
export function rawCopy(values: MutableArray<string>): MutableArray<string> {
return structuredClone(values);
}
The conversion follows the static type, which the compiler passes as reflection info: toPlain(model, Model_$reflection()). In the plain form:
- a record or anonymous record is an object with its field names;
- a union case is
{ case: "Name", fields: [...] }; - a list, set, array or tuple is an array, and a map an array of
[key, value]pairs; Noneisnull, andSomeis the value, or{ value }where the value is itself an option,unitorobj;- a decimal is its string.
Numbers, strings, dates and class instances are kept as they are, so a class instance still loses its prototype in a clone. Where 'T is a type parameter, call Transfer from an inline function so the compiler knows the type.
JS.structuredClone binds the global, and JS.structuredCloneTransfer transfer value moves objects such as an ArrayBuffer into the copy instead of copying them.
Typed-array views
A numeric F# array is a typed array in JavaScript. An ArraySegment over one is a typed-array view that shares its memory: ArraySegment<byte>(bytes, 4, 16) compiles to bytes.subarray(4, 20). Array.ofArrayBuffer buffer views a whole ArrayBuffer as a numeric array without a copy, and array.AsArrayBuffer() gives the array's buffer.
open System
open Fable.Core
open Fickl.Core
let header (bytes: byte[]) = ArraySegment<byte>(bytes, 4, 16)
let floatsOf (buffer: JS.ArrayBuffer) : float[] = Array.ofArrayBuffer buffer
let bufferOf (bytes: byte[]) = bytes.AsArrayBuffer()
import { asArrayBuffer } from "@fable-org/fable-library-js/TypedArray.js";
export function header(bytes) {
return bytes.subarray(4, 20);
}
export function floatsOf(buffer) {
return new Float64Array(buffer);
}
export function bufferOf(bytes) {
return asArrayBuffer(bytes);
}
import type { float64, uint8 } from "@fable-org/fable-library-ts/Int32.ts";
import type { MutableArray } from "@fable-org/fable-library-ts/Util.ts";
import { asArrayBuffer } from "@fable-org/fable-library-ts/TypedArray.ts";
export function header(bytes: MutableArray<uint8>): Uint8Array {
return (bytes as unknown as Uint8Array).subarray(4, 20);
}
export function floatsOf(buffer: ArrayBuffer): MutableArray<float64> {
return new Float64Array(buffer);
}
export function bufferOf(bytes: MutableArray<uint8>): ArrayBuffer {
return asArrayBuffer(bytes);
}
Array.ofArrayBuffer picks the typed array from the element type, new Float64Array(buffer) here. The buffer's length must be a multiple of the element size, and writes to either the buffer or the array show in the other. AsArrayBuffer returns the array's own buffer when the array covers all of it, as an array F# created does, and otherwise a copy of the part it covers. JS.Constructors.BigUint64Array binds the array uint64[] compiles to.
Import attributes
[<ImportWith("type", "json")>] adds an import attribute to an imported value, function or class, for JSON and CSS modules. JsInterop.importDynamicWith does the same for a dynamic import, with the attributes as a list literal of key and value pairs.
open Fable.Core
open Fickl.Core
type Config =
abstract name: string
abstract port: int
[<ImportDefault("./config.json"); ImportWith("type", "json")>]
let config: Config = jsNative
let address () = $"{config.name}:{config.port}"
let load () =
JsInterop.importDynamicWith<{| ``default``: Config |}> "./config.json" [ "type", "json" ]
import config from "../config.json" with { type: "json" };
export function address() {
return `${config.name}:${config.port}`;
}
export function load() {
return import("./config.json", {
with: {
type: "json",
},
});
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import config from "../config.json" with { type: "json" };
export interface Config {
get name(): string,
get port(): int32
}
export function address(): string {
return `${config.name}:${config.port}`;
}
export function load(): Promise<{ default: Config }> {
return import("./config.json", {
with: {
type: "json",
},
});
}
The static import compiles to import config from "..." with { type: "json" }, and the dynamic one to import("./config.json", { with: { type: "json" } }). The keys of importDynamicWith must be constants. A JSON or CSS module is the default export of the promised namespace object.
The static import's path is rewritten relative to the output folder, like any [<Import>]; the path of importDynamicWith is written as you give it, as with importDynamic. Each import may carry several ImportWith attributes, and the same path with other attributes is a separate declaration. A type that is only used as a type is imported with import type, which takes no attributes.
TypeScript needs module set to esnext, preserve or nodenext for import attributes.