npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@starbeam/timeline

v0.8.9

Published

`@starbeam/timeline` is part of Starbeam, a library for building and using reactive objects in any framework.

Downloads

38

Readme

Purpose

@starbeam/timeline is part of Starbeam, a library for building and using reactive objects in any framework.

Primitive

@starbeam/timeline is stable, with the same semver policy as Starbeam.

That said, it is not intended to be used directly by application code. Rather, it is one of the core parts of the Starbeam composition story. You can use it to better understand how Starbeam works, or to build your own Starbeam libraries.

📙 Philosophy

Higher-level libraries like @starbeam/universal build on lower-level primitives. These are not privileged internal APIs, and they are not marked as unstable. We believe that you, the people building Starbeam's library ecosystem, are just as innovative as Starbeam's creators. We avoid including "for me but not for thee" APIs in our composition abstractions. Go forth and build!

Timeline and Lifetime

At a fundamental level, Starbeam reactivity is made up of mutation events that happen to a data universe at a point on a timeline.

The data universe is broken up into two kinds of cells: data cells and formulas.

Two Phases: Action and Render

The Starbeam reactivity system is a perpetual cycle between two phases: Action and Render. These phases run in a cycle for as long as the program is running.

The Action Phase

Code in the Action phase is quite powerful. It can mutate data cells as much as it wants, and it can immediately get the up-to-date values of formulas. Code in the Action phase can also read from the rendered output.

In exchange for all of that power, code in the Action phase cannot directly write to the rendered output, and it will need to wait until the next Action phase to see how the mutations to the data universe reflected onto the rendered output.

The Render Phase

Code in the Render phase is considerably less powerful. It may read from the data universe and write to the rendered output, but it may not write to the data universe.

ℹ️ The Complete Rendering Process

When using Starbeam to reflect the data universe into a Browser DOM, rendering involves multiple iterations of the Action / Render cycle. We call the entirety of this process the Rendering Process.

These steps allow you to implement framework-agnostic [resources] that can correctly use the DOM as data source. They are universal, which means that you can write code in terms of Starbeam's APIs, and it will run inside of the framework of your choice with a Starbeam adapter.

📒 Note

Each of these three steps is an Action / Render cycle. During the Action phase, application code can mutate the data universe and read from the DOM. During the Render phase, your framework will update the DOM from the changes you made during the Action phase. Typically, the three cycles of the rendering phase happen in quick succession, but you should not rely on this. Your framework may choose to do other work between the phases, and modern frameworks commonly do so in order to provide an optimal experience for your users.

Also, while application code will typically have an opportunity to run inside of each step of the Rendering Process, your framework may choose to [deactivate] or [unmount] the component before the Ready step. If application code sets up some state that needs to be torn down, it should not rely on the Measurement or Ready steps running. Instead, finalizers registered with an appropriate lifetime (see below) are guaranteed to run even if the Measurement or Ready steps do not.

In practice, these considerations are bundled together into the high-level "Stateful Formula" construct provided by @starbeam/reactive.

Timeline

The Timeline in @starbeam/timeline coordinates these phases.

It starts out in the Actions phase, which allows free access to the data universe. As soon as a data cell in the data universe is mutated, the Timeline schedules a Render phase using the configured scheduler. By default, this will schedule a Render phase during a microtask checkpoint, which occurs asynchronously, but before the next paint.

Scheduling

The Timeline can be configured with a Coordinator (see @starbeam/schedule), which controls the exact details of the timeline's timing.

The default behavior automatically schedules the next Render phase using a microtask checkpoint, which means that it will happen asynchronously, but before the next time the browser paints the page. The purpose of the Coordinator is to allow you to make multiple mutations to the data universe before a Render phase occurs.

You can specify a custom Coordinator to use an alternative strategy. For example, if you are writing a single-file demo, the entire file will finish running before a microtask checkpoint. You could create an API to use a single-file demo that automatically schedules Render phases at appropriate times.

Finally, you can also explicitly schedule a Render phase, which will supersede the Coordinator's policy and simply wait until you're ready to render.

import { TIMELINE } from "@starbeam/timeline";
import { Cell } from "@starbeam/reactive";

const person = reactive({
  id: null,
  name: "@tomdale",
});
const name = Cell("Tom");
const userId = Cell(null);

function multiStepProcess(name, url) {
  const render = TIMELINE.manualRenderPhase();

  person.name = name;

  fetch(url).then((data) => {
    person.id = data.id;
    render();
  });
}

Moving Along the Timeline

The Timeline is a representation of discrete time, where each mutation to a data cell is given a unique, monotonically increasing timestamp.

Every time you mutate a data cell, the Timeline assigns increments the "current timestamp" by 1, and assigns that timestamp to the mutation.

"Discrete time" just means that there are specific points in time that we are interested in, and that "nothing interesting" happens in between those points.

When you are in an Action phase, this happens automatically.

On the other hand, Render phases are frozen in time. They may not move the timeline forward. In practice, this means that formulas are read-only and may not mutate the data universe.

💡 Note

This has nothing to do with the location of callbacks in the code. For example, it is quite normal for event handlers to occur inside initialization (the code that computes the initial state of the DOM from the data universe). However, the event handlers do not run during initialization, but rather at a later time, in response to hardware events triggered by the user.

By definition, such events happen in the Action phase, even though the function that they call was created during initialization.

Formula Validation

TODO: Describe the validation process

Subscription

TODO: Describe how to subscribe to changes in a formula

Structured Finalizer

As we discussed, the timeline describes changes in the data universe and helps a consumer coordinate the two-phase process of reflecting the data universe onto the output. Both data cells and formulas are pure data: they can be automatically cleaned up by the garbage collector when nobody retains a reference to them.

On the other hand, you may encounter objects in the real world that require you to tear them down when you're done using them, and you may want to convert those objects into data in the data universe. That's where the structured finalizer comes in.

The structured finalizer allows you to set up a stateful connection to some external data, such as a WebSocket, ResizeObserver or even a fetch request, associate it with an owner, and automatically finalize the connection when the owner is finalized.

For example, a component may set up a ResizeObserver to keep track of the size of one of the elements it creates. When the component is deactivated, the component wants to finalize the ResizeObserver so that it doesn't leak.

Composition is King

Starbeam uses the structured finalizer approach to make finalization composable. Instead of making the component responsible for setting up the ResizeObserver and specifying how to finalize the ResizeObserver when the component it finalized, it can delegate responsibility to an ElementSize resource:

  1. Create an object that represents the ResizeObserver.
  2. Convert events on the ResizeObserver into data in the data universe.
  3. Specify what should happen when that object is finalized.
  4. Link that object to the component.

Example: The Resource Pattern

Let's see how this all fits together. We'll use the resource pattern from @starbeam/reactive to create an ElementSize resource.

import { Resource } from "@starbeam/reactive";

export function ElementSize(element: Element) {
  return Resource((resource) => {
    const size = reactive(getSize(element));

    const observer = new ResizeObserver();

    observer.observe(element, () => {
      const { width, height } = getSize(element);

      size.width = width;
      size.height = height;
    });

    resource.on.finalize(() => observer.disconnect());

    return size;
  });
}

function getSize(element: Element) {
  const rect = element.getBoundingClientRect();

  return {
    width: rect.width,
    height: rect.height,
  };
}

Let's look at it one piece at a time.

First, we create a vanilla getSize function to get the width and heigh from an element.

function getSize(element: Element) {
  const rect = element.getBoundingClientRect();

  return {
    width: rect.width,
    height: rect.height,
  };
}

Next, we create a function that will take an element and set up the resource.

export function ElementSize(element: Element) {
  return Resource((resource) => {
    // ...
  });
}

This function operates on a fixed element, such as the top-level element of a component. The function calls the Resource function, the built-in constructor for the resource pattern. Let's see how it works.

First, we create a reactive object with the element's width and height.

const size = reactive(getSize(element));

Next, we create a ResizeObserver and observe the element.

const observer = new ResizeObserver();

observer.observe(element, () => {
  const { width, height } = getSize(element);

  size.width = width;
  size.height = height;
});

When the ResizeObserver fires, we update the width and height properties of the reactive object. Importantly, the ResizeObserver's callback runs in the Action phase, like all asynchronous callbacks invoked by the browser. This means that we can freely mutate anything in the data universe. Any part of the rendered output that cares about the reactive object will run in the next Render phase, which Starbeam will automatically schedule.

Ok, that's great, ResizeObserver requires us to disconnect from it when we no longer need it. If we don't disconnect, the observer will leak. No problem! That's the whole point of the Resource API. Let's tell Starbeam what to do when the resource is finalized.

resource.on.finalize(() => observer.disconnect());

This code is not responsible for attaching the ElementSize resource to any particular owner. That will happen inside the framework adapters, which know how to turn your framework's concept of component into a Starbeam owner.

return size;

Finally, we return the size object. The Resource function returns an object with an owner() method on it, which the caller can use to link the resource to an owner. The owner() method returns the object with reactive width and height properties.

interface Resource<T> {
  owner(parent: object): T;
}

Once linked, ElementSize is a regular formula that can be used as part of other formulas.

📒 Framework-Specific Details

Starbeam's framework adapters provide a way to attach a function that takes an Element (called an "element modifier") to an element when the framework has created it using framework-specific APIs.

For example, you would use the ref API to attach a modifier in React, while you would use the use: directive syntax to attach a modifier in Svelte. Check out the framework-specific documentation for more details.

If all of this is getting too abstract, let's take a look at how you would actually use ElementSize in React.

function Box({ children }) {
  return useReactiveElement((element) => {
    const div = ref(HTMLDivElement);
    const size = element.useModifier(div, ElementSize);

    return () => (
      <>
        {size.match({
          rendering: () => null,
          attached: (size) => `${size.width}x${size.height}`,
        })}
        <div ref={div}>{children}</div>
      </>
    );
  });
}

@starbeam/react provides a React-specific way to create a ref to put into your JSX, and then use the ElementSize modifier with that ref. @starbeam/react takes care of interacting with React to get the element, and invokes the ElementSize modifier once the element is in the DOM.

Since the React ref API requires you to complete a full render cycle in order to attach the ref, the useModifier API in @starbeam/react returns a value that can either be rendering, because it's the first render, or attached, once the element is in the DOM. You can use the match API to decide what to do on the first render.

Critically, while the useReactiveElement, ref and useModifier APIs come from @starbeam/react, they interact with the universal ElementSize modifier that we wrote without having to know anything about React at all.

Terms

The Concept of "Lifecycle"

TODO: Describe the difference between a general concept of "lifecycle hooks", as presented by other frameworks, and how we think about the interaction between phasing and finalization in Starbeam.