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));
      });
    });

  });

});