Collection.js
The complete source of src/Collection.js, exactly as it ships, in reading order. Every line is present; the file is split at its own section banners.
Collection.js - generic array / optional / union access (the library layer)
// ============================================================================
// Collection.js - generic array / optional / union access (the library layer)
// ============================================================================
//
// The generic collection surface the reactive program reaches for arrays, optionals and
// union tags. Generics are explicit and curried (Fn(Types)(values)); value inputs use
// Reactive.Argument. The application rarely writes generics directly - it dots into
// record fields and union options; these functions are what array/optional access go
// through (there is no `[]` in the proxy).
//
// Collection.Array(ElementType)(builder) construct an array; builder calls Element/Elements
// Collection.Array.Element(value) add one element (mints its stable key)
// Collection.Array.Elements(otherArray) splice a whole array in (preserving keys)
// Collection.Array.At(ElementType)(array, index) the element at index (read, first cut)
// Collection.Array.Empty(ElementType)() an empty array
// Collection.Optional.If(optional, callback) render callback(payload) when present
// Collection.Optional.Switch(body) one-of over optionals; body has Switch.Case
// Collection.Optional.Switch.Case(optional, cb) first present case renders (union branch)
// Collection.Union.Tag(union) READ-ONLY active variant name (not two-way)
//
// Array elements are always {key, value} internally, keys minted as stable ids so a loop
// yields a stable-id tree (Each keys its branches by them) and drivers diff by them. The
// app never sees the keys.
// ----------------------------------------------------------------------------
Reactive.Module("Collection", () => {
const ReactiveArray = Reactive._internal.ReactiveArray;
const ReactiveArrayElement = Reactive._internal.ReactiveArrayElement;
// The native active variant name of a union value {tag: payload}: the single present key.
function activeTag(union) {
if (union == null) {
return undefined;
}
for (const key of Object.keys(union)) {
if (union[key] !== undefined && union[key] !== null) {
return key;
}
}
return undefined;
}
// ---- Array ----------------------------------------------------------------
// A stack of open array builders; Element/Elements push into the innermost one.
const builderStack = [];
function currentBuilder(who) {
if (builderStack.length === 0) {
throw new Error("Collection.Array." + who + " called outside a Collection.Array builder");
}
return builderStack[builderStack.length - 1];
}
// Collection.Array(ElementType) yields an object that is BOTH the array TYPE (a descriptor,
// usable in Reactive.Argument / Reactive.Field) AND a CONSTRUCTOR: call it with a builder
// closure to construct an array value.
Collection.Array = function (ElementType) {
const arrayType = function (builder) {
builderStack.push([]);
try {
builder();
return new ReactiveArray(builderStack[builderStack.length - 1]);
} finally {
builderStack.pop();
}
};
arrayType.__reactiveType = { kind: "array", name: "Array", elementType: ElementType, fields: [] };
return arrayType;
};
// Element(value) - add one element; mints its stable key (position/branch based, stable
// across renders so identity survives inserts and reorders).
Collection.Array.Element = function (value) {
currentBuilder("Element").push(new ReactiveArrayElement(Reactive.StableKey(), value));
};
// Elements(otherArray) - splice a whole array in at this position, preserving each
// element's existing key.
Collection.Array.Elements = function (otherArray) {
const native = otherArray instanceof Reactive._internal.ReactiveValue
? (otherArray.optionalGettable == null ? null : otherArray.optionalGettable.value)
: otherArray;
const target = currentBuilder("Elements");
if (native != null) {
for (const element of native.elements) {
target.push(new ReactiveArrayElement(element.key, element.value));
}
}
};
// At(ElementType)(array, index) - the element value at index. Read-only first cut; a
// reversible element accessor (set element -> rebuild array) is a later refinement.
Collection.Array.At = function (ElementType) {
return function (array, index) {
return Reactive.Calculate(() => {
const native = Reactive.Value(array);
const idx = Reactive.Value(index);
return native.elements[idx].value;
});
};
};
// Empty(ElementType)() - an empty array of the given element type.
Collection.Array.Empty = function (ElementType) {
return function () {
return new ReactiveArray([]);
};
};
// Each(array, callback) - render callback once per element, one branch scope keyed by the
// element's stable key (so identity survives inserts/reorders and drivers diff by it), with
// the element value handed to the callback as its single Reactive.Argument. While the array
// is read-locked, nothing renders. This is the loop the data-driven tabs go through.
Collection.Array.Each = function (array, callback) {
Reactive.Each(array, (element) => {
Reactive._internal.invokeCallbackWithArguments(callback, [element]);
});
};
// ---- Optional -------------------------------------------------------------
Reactive.Module("Optional", () => {
// If(optional, callback) - when the optional is present, render callback with the
// payload as its single Reactive.Argument input; otherwise render nothing. Wrapped in
// Reactive.If so the branch has stable identity.
Reactive.Function("If", () => {
const optional = Reactive.Argument(Reactive.Any);
const callback = Reactive.Argument(Reactive.Callback());
const value = Reactive.Value(optional);
const present = value !== undefined && value !== null;
Reactive.If(present, () => {
Reactive._internal.invokeCallbackWithArguments(callback, [optional]);
});
});
// Switch(body) - a one-of over optionals. `body` declares the cases with
// Collection.Optional.Switch.Case(optional, callback); the FIRST case whose optional is
// present renders (its present value handed to the callback as its single Reactive.Argument),
// the rest render nothing. This is how you branch a tagged union WITHOUT reifying its tag as
// a string: each variant IS an optional field (param.payload.vec3), so you pass the field,
// not a name. A union has exactly one present variant, so "first wins" is unambiguous there.
//
// Switch is READ-ONLY: it does not change which case is active. CHANGING variant needs a
// VALUE for the new variant, which cannot be genericised (the default/carry-over is
// domain-specific), so it is done by the app with an explicit Reactive.Reverse that
// constructs the new option's value - not by an automatic reverse of the Switch.
Reactive.Function("Switch", () => {
const body = Reactive.Argument(Reactive.Callback());
// Shared across the cases of THIS Switch run: whether an earlier case already matched, so
// only the first present one renders. Reset each render; the cases run in order in body().
const match = { done: false };
// Case(optional, callback) - one branch. Inner to Switch so it closes over `match` (the
// no-ambient pattern). Accessed as Collection.Optional.Switch.Case in the body.
Reactive.Function("Case", () => {
const optional = Reactive.Argument(Reactive.Any);
const callback = Reactive.Argument(Reactive.Callback());
const value = Reactive.Value(optional);
const present = value !== undefined && value !== null;
const take = present && !match.done;
Reactive.If(take, () => {
Reactive._internal.invokeCallbackWithArguments(callback, [optional]);
});
match.done = match.done || present;
});
body();
});
});
// ---- Union ----------------------------------------------------------------
Reactive.Module("Union", () => {
// Tag(union) - a READ-ONLY derivation of the active variant NAME (the single present key),
// for display or logic. It is NOT two-way: you cannot switch variant by setting a string,
// because the new variant needs a value (see Collection.Optional.Switch). To branch on the
// variant, use Collection.Optional.Switch over the union's option fields; to switch it, use
// an explicit Reactive.Reverse in the app that builds the new option's value.
Reactive.Function("Tag", () => {
const union = Reactive.Argument(Reactive.Any);
return Reactive.Calculate(() => {
return activeTag(Reactive.Value(union));
});
});
});
});