Skip to content

API Design Changes #2

@samholmes

Description

@samholmes

Call Stack (Context)

Ability to query the "context" from within a signal using when. The context contains a stack of values pushed onto the stack during compute of a signal. For example, invoking a signal b that is a dependency for a will cause b to be pushed to the context stack when computing a.

const c = on()
const b = on()
const a = on(() => {
  // touch another signal
  c()
  if (when(b)) {
    return true
  }
  return false
})

The when function returns a boolean; true if the value is in the stack, and false if it's not. Any value queried by when is auto-tracked.

Context or "call stack" could be better named. The idea is that when gives you access to state of the stack trace. This isn't something you can necessarily do in a typical function scope unless the function is defined within a closure of another. Instead, this stack is an alternative way of passing hidden state other than parameters for added "context awareness". This may be an anti-pattern, but it makes for some convenient ways to emulate other kinds of signal behaviors.

Disable Auto-Tracking with off

Instead of a .peek() method, we can use off(signal)(params) to wrap a signal to disable auto-tracking. This change fits nicely with when where we may want to "peek" into some context value: when(off(b)).

Stateless by Default

All signals with a compute function are stateless. State will be solely managed by own. If a compute function doesn't set a value to own (e.g. own(value)), then no state is kept. The function can return a different value from the internal state that is kept by own. This allows for private state:

// Return a boolean when setting the signal state, but return the state when computing the signal
const s = on((v) => {
  return v != null ? own(v) % 2 === 0 : v
})

s(2) // returns true
s() // returns 2
s(1) // returns false
s() // returns 1

This added flexibility gives the user finer grain control over state management for all kinds of purposes: caching for performance, deriving a computed state from memory. The user can make more decisions on how they want to manage memory vs compute resources.

Because own takes a default value or initialization value as it's argument, how then do you update the value of own? The answer is that the state that own returns is not mutable, but it's properties may be. Use an mutable object and mutate in place for mutable internal state. An alternative to this idea is to make the argument a set state (but only if it's !==):

const s = on((v) => {
  return own(own() ?? v ?? initialValue)
}

This is not pretty, but doable. Alternatively, a function could be passed:

// Function always returns its state
own(v => v ?? initialValue)

This function will always update the internal state. This is similar to React's setState function, so this may be a plus because of familiarity.

Note on type inferrence

One challenge to this design may be with proper type checking on own considering that it might have a different type from the signal. Perhaps the use of this could solve instead of own:

// Invent some utility type called Self
const s = on<T>((this: Self<I>, internal: I = this()) => ...)
// Or make the internal state function be an untracked signal
const s = on<T>((this: Signal<I>, internal: I = this()) => ...)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions