Skip to content

Commit

Permalink
bump patch version to 0.3.2
Browse files Browse the repository at this point in the history
updated docs as an excuse to publish the next version onto jsr.io, as the previous one didn't have its readme rendered at all in the homepage.
  • Loading branch information
omar-azmi committed Mar 5, 2024
1 parent c030ba1 commit 19cb66e
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 13 deletions.
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@oazmi/tsignal",
"version": "0.3.1",
"version": "0.3.2",
"description": "a topological order respecting signals library inspired by SolidJS",
"author": "Omar Azmi",
"license": "Anti-Competition License",
Expand Down
47 changes: 46 additions & 1 deletion src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,41 @@
* @module
*/

import { DEBUG, bindMethodToSelfByName, bind_array_clear, bind_array_pop, bind_array_push, bind_map_clear, bind_map_delete, bind_map_get, bind_map_set, bind_set_add, bind_set_clear, bind_set_delete, bind_set_has } from "./deps.ts"
import {
DEBUG,
bindMethodToSelfByName,
bind_array_clear,
bind_array_pop,
bind_array_push,
bind_map_clear,
bind_map_delete,
bind_map_get,
bind_map_set,
bind_set_add,
bind_set_clear,
bind_set_delete,
bind_set_has
} from "./deps.ts"
import { hash_ids } from "./funcdefs.ts"
import { EffectFn, MemoFn, SimpleSignalInstance } from "./signal.ts"
import { EqualityFn, FROM_ID, HASHED_IDS, ID, Signal, SignalClass, SignalUpdateStatus, TO_ID, UNTRACKED_ID } from "./typedefs.ts"

/** if {@link Context | `ctx`} is your context, then these function members are available under {@link Context.dynamic | `ctx.dynamic`}. <br>
* the purpose of these functions is to dynamically change an existing signal's `value` (or cached value), equality `equals` checking function, or its recomputation `fn` function. <br>
* to use any of these, you must have the signal's {@link Signal.id | original id}, which is typically provided by the {@link SignalClass.create | create function's} first element in the tuple.
* // TODO-DOC: give an example
*/
export interface Context_Dynamic {
setValue: <T>(id: ID, new_value: T) => void,
setEquals: <T>(id: ID, new_equals: EqualityFn<T>) => void,
setFn: <T>(id: ID, new_fn: MemoFn<T> | EffectFn) => void,
}

/** if {@link Context | `ctx`} is your context, then these function members are available under {@link Context.batch | `ctx.batch`}. <br>
* the purpose of these functions is to widthold any signals from firing until the batching is complete, and then all signals are fired together in one single propagation cycle. <br>
* this is useful in circumstances where you have a array or dictionary like signal which must undergo many mutations all at once.
* // TODO-DOC: give an example
*/
export interface Context_Batch {
startBatching: () => number
endBatching: () => void
Expand All @@ -22,6 +46,27 @@ export interface Context_Batch {
) => T
}

/** a signal context class is required to create and register signals.
* without it, signals won't be able to communicate with each other,
* and it would not be possible to build a signals dependency graph.
*
* @example
* ```ts
* const ctx = new Context() // create signal context
* const createState = ctx.addClass(StateSignal_Factory) // feed the state signal factory function to get a signal generator
* const createMemo = ctx.addClass(MemoSignal_Factory) // feed the memo signal factory function to get a signal generator
*
* const [idNum, getNum, setNum] = createState<number>(0)
* const [idNum2, getNum2] = createMemo((memo_id) => {
* const num2 = getNum(memo_id) ** 2
* console.log("recomputing number squared:", num2)
* return num2
* }, { defer: false })
*
* setNum(2) // console.log: "recomputing number squared: 4"
* setNum(4) // console.log: "recomputing number squared: 16"
* ```
*/
export class Context {
readonly addEdge: (src_id: FROM_ID, dst_id: TO_ID) => boolean
readonly delEdge: (src_id: FROM_ID, dst_id: TO_ID) => boolean
Expand Down
21 changes: 19 additions & 2 deletions src/funcdefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@
* @module
*/

import { DEBUG, THROTTLE_REJECT, bind_set_delete, bind_set_has, noop, throttle } from "./deps.ts"
import { DEBUG, THROTTLE_REJECT, noop, throttle } from "./deps.ts"
import { EqualityCheck, EqualityFn, FROM_ID, HASHED_IDS, ID, Signal, TO_ID, UNTRACKED_ID } from "./typedefs.ts"

/** test if `value1 === value2` */
export const default_equality = (<T>(v1: T, v2: T) => (v1 === v2)) satisfies EqualityFn<any>
/** an equality function that always returns `false`, so that the signal is always propagated (never the same/equal). <br>
* this is useful in circumstances where the value is an object, and has undergone mutation, but is still the same object.
*/
export const falsey_equality = (<T>(v1: T, v2: T) => false) satisfies EqualityFn<any>
/** parses the equality function types and primitives accepted by {@link signal!SimpleSignalConfig.equals | `equals` config field },
* and spits out its function equivalent.
*/
export const parseEquality = <T>(equals: EqualityCheck<T>) => (equals === false ? falsey_equality : (equals ?? default_equality)) satisfies EqualityFn<any>

/** transforms a regular equality check function ({@link SimpleSignalConfig.equals}) into a one that throttles when called too frequently. <br>
/** transforms a regular equality check function ({@link signal!SimpleSignalConfig.equals}) into a one that throttles when called too frequently. <br>
* this means that a signal composed of this as its `equals` function will limit propagating itself further, until at least `delta_time_ms`
* amount of time has passed since the last time it was potentially propagated.
*
Expand Down Expand Up @@ -43,15 +50,24 @@ export const throttlingEquals = <T>(delta_time_ms: number, base_equals?: Equalit
}
}

/** this is the special hashing function used by the {@link context!Context}'s dfs-traversal function to cache its output. <br>
* this hashing function is invariant of the order in which the `ids` are provided.
* but its collision rate has yet to be tested, although I'm faily certain that it is quite resistant for multiple ids under the value `10000`.
*/
export const hash_ids = (ids: ID[]): HASHED_IDS => {
const sqrt_len = ids.length ** 0.5
return ids.reduce((sum, id) => sum + id * (id + sqrt_len), 0)
}

/** TODO: add to `kitchensink_ts` if important, then cleanup */
/*
export const intersection_of_sets = <T>(set1: Set<T>, set2: Set<T>): Array<T> => {
return [...set1].filter(bind_set_has(set2))
}
*/

/** TODO: add to `kitchensink_ts` if important, then cleanup */
/*
export const symmetric_difference_of_sets = <T>(set1: Set<T>, set2: Set<T>): [uniques1: Set<T>, uniques2: Set<T>] => {
const
union_arr = intersection_of_sets(set1, set2),
Expand All @@ -61,6 +77,7 @@ export const symmetric_difference_of_sets = <T>(set1: Set<T>, set2: Set<T>): [un
union_arr.forEach(bind_set_delete(uniques2))
return [uniques1, uniques2]
}
*/

export const log_get_request = /* @__PURE__ */ DEBUG.LOG ? (all_signals_get: (id: ID) => Signal<any> | undefined, observed_id: FROM_ID, observer_id?: TO_ID | UNTRACKED_ID) => {
const
Expand Down
14 changes: 9 additions & 5 deletions src/jsx-runtime/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,23 @@ type IntrinsicSVGElements = { [tagName in keyof SVGElementTagNameMap]: Attribute
* ```
*/
export namespace JSX {
/** {@inheritDoc JSX} */
export type IntrinsicElements = IntrinsicHTMLElements & IntrinsicSVGElements
}


/** a reactive text can either be a stringifyable signal `Accessor`, or a regular stringifyable value (which will get upgraded to an accessor). */
export type ReactiveText = Stringifyable | Accessor<Stringifyable>
/** the `props` of an `HTMLElement` are simply their attributes */
export type ElementAttrs = { [attribute_key: Attr["name"]]: ReactiveText }
export type SingleComponentGenerator<P = {}> = (props?: P) => Element
export type FragmentComponentGenerator<P = {}> = (props?: P) => Element[]
type SingleComponentGenerator<P = {}> = (props?: P) => Element
type FragmentComponentGenerator<P = {}> = (props?: P) => Element[]
/** a function that generates one `HTMLElement` or an array of them. */
export type ComponentGenerator<P = {}> = SingleComponentGenerator<P> | FragmentComponentGenerator<P>
export type ElementChild = ReactiveText | Node
export type ElementChildren = Array<ElementChild | ElementChild[]>
type ElementChild = ReactiveText | Node
type ElementChildren = Array<ElementChild | ElementChild[]>
/** function signature of the fragment element generator (array of elements). */
export type HyperScript_CreateFragment = (props: object, ...elements: Node[]) => (typeof elements)
/** function signature of the hyperscript DOM generator function. */
export interface HyperScript_CreateElement {
(html_tag: string, attrs?: ElementAttrs | null, ...children: ElementChildren): HTMLElement
<P = {}>(component: SingleComponentGenerator<P>, props?: P | null, ...children: ElementChildren): Element
Expand Down
1 change: 0 additions & 1 deletion src/mod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/** a low-calorie clone of the popular reactivity library [SolidJS](https://github.com/solidjs/solid). <br>
* but it handles signals in dependency-topological-order, and is much faster, and less ambiguous in terms of reactivity behavior.
* @module
*/

export { AsyncStateSignal_Factory } from "./async_signal.ts"
Expand Down
3 changes: 2 additions & 1 deletion src/record_signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Accessor, EqualityCheck, EqualityFn, ID, SignalUpdateStatus, TO_ID, UNT
// - `DictMemo<K, V>(fn: (observed_id?: ID) => [changed_value: V, changed_key: K, dict: DictMemo<K, V>["dict"])] ) extends Dict<K, V> //with computation + memorization`
// TODO: once you implement `ListState<V>`, use it inplace of `RecordStateSignal` in `/examples/2/` (the todos app)


/** the configuration options used by most record signal constructors. */
export interface RecordSignalConfig<K extends PropertyKey, V> {
/** give a name to the signal for debugging purposes */
name?: string
Expand All @@ -33,6 +33,7 @@ export interface RecordSignalConfig<K extends PropertyKey, V> {
defer?: boolean
}

/** the configuration options used by memo record signal constructor {@link RecordMemoSignal_Factory | `RecordMemoSignal`}. */
export interface RecordMemoSignalConfig<K extends PropertyKey, V> extends RecordSignalConfig<K, V> {
/** initial value declaration for reactive signals. <br>
* its purpose is only to be used as a previous value (`prev_value`) for the optional `equals` equality function,
Expand Down
39 changes: 37 additions & 2 deletions src/signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { Context } from "./context.ts"
import { DEBUG, StaticImplements, bindMethodToSelfByName, isFunction } from "./deps.ts"
import { log_get_request, parseEquality } from "./funcdefs.ts"
import { Accessor, EqualityCheck, EqualityFn, ID, Setter, SignalClass, SignalUpdateStatus, TO_ID, UNTRACKED_ID, Updater } from "./typedefs.ts"
import { Accessor, EqualityCheck, EqualityFn, ID, Setter, Signal, SignalClass, SignalUpdateStatus, TO_ID, UNTRACKED_ID, Updater } from "./typedefs.ts"

// TODO: add `SimpleSignalConfig.deps: ID[]` option to manually enforce dependance on certain signal ids. this can be useful when you want a
// signal to defer its first run, yet you also want that signal to react to any of its dependencies, before this signal ever gets run.
Expand All @@ -15,6 +15,7 @@ import { Accessor, EqualityCheck, EqualityFn, ID, Setter, SignalClass, SignalUpd
// on the other hand, `ondelete` will be called right before the signal and its dependency graph-edges are deleted.
// this is in contrast to `ctx.onInit`, which runs based on the non-zero-ablity of `id`, and `ctx.onDelete`, which runs after the graph-edges have been deleted.

/** the configuration options used by most signal constructors, and especially the basic/primitive ones. */
export interface SimpleSignalConfig<T> {
/** give a name to the signal for debugging purposes */
name?: string
Expand All @@ -34,6 +35,7 @@ export interface SimpleSignalConfig<T> {
defer?: boolean
}

/** the configuration options used by most primitive derived/computed signal constructors. */
export interface MemoSignalConfig<T> extends SimpleSignalConfig<T> {
/** initial value declaration for reactive signals. <br>
* its purpose is only to be used as a previous value (`prev_value`) for the optional `equals` equality function,
Expand All @@ -42,10 +44,19 @@ export interface MemoSignalConfig<T> extends SimpleSignalConfig<T> {
value?: T
}

/** an arbitrary instance of a simple/primitive signal. */
export type SimpleSignalInstance = InstanceType<ReturnType<typeof SimpleSignal_Factory>>

/** the base signal class inherited by most other signal classes. <br>
* its only function is to:
* - when {@link Signal.get | read}, it return its `this.value`, and register any new observers (those with a nonzero runtime-id {@link Signal.rid | `Signal.rid`})
* - if {@link Signal.set | set} to a new value, compare it to its previous value through its `this.equals` function,
* and return a boolean specifying whether or not the old and new values are the same.
* - when {@link Signal.run | ran}, it will always return `0` (unchanged), unless it is forced, in which case it will return a `1`.
*/
export const SimpleSignal_Factory = (ctx: Context) => {
const { newId, getId, setId, addEdge } = ctx
/** {@inheritDoc SimpleSignal_Factory} */
return class SimpleSignal<T> implements StaticImplements<SignalClass, typeof SimpleSignal> {
declare id: ID
declare rid: ID | UNTRACKED_ID
Expand Down Expand Up @@ -108,8 +119,10 @@ export const SimpleSignal_Factory = (ctx: Context) => {
}
}

/** creates state signals, which when {@link Signal.set | set} to a changed value, it will fire an update to all of its dependent/observer signals. */
export const StateSignal_Factory = (ctx: Context) => {
const runId = ctx.runId
/** {@inheritDoc StateSignal_Factory} */
return class StateSignal<T> extends ctx.getClass(SimpleSignal_Factory)<T> {
declare value: T
declare fn: never
Expand Down Expand Up @@ -147,7 +160,11 @@ export const StateSignal_Factory = (ctx: Context) => {
/** type definition for a memorizable function. to be used as a call parameter for {@link createMemo} */
export type MemoFn<T> = (observer_id: TO_ID | UNTRACKED_ID) => T | Updater<T>

/** creates a computational/derived signal that only fires again if at least one of its dependencies has fired,
* and after the {@link SimpleSignalInstance.fn | recomputation} (`this.fn`), the new computed value is different from the old one (according to `this.equals`).
*/
export const MemoSignal_Factory = (ctx: Context) => {
/** {@inheritDoc MemoSignal_Factory} */
return class MemoSignal<T> extends ctx.getClass(SimpleSignal_Factory)<T> {
declare fn: MemoFn<T>
declare prerun: never
Expand Down Expand Up @@ -187,8 +204,22 @@ export const MemoSignal_Factory = (ctx: Context) => {
}
}


/** similar to {@link MemoSignal_Factory | `MemoSignal`}, creates a computed/derived signal, but it only recomputes if:
* - it is dirty (`this.dirty = 1`)
* - AND some signal/observer/caller calls this signal to {@link Signal.get | get} its value.
*
* this signal becomes dirty when at least one of its dependencies has fired an update. <br>
* after which, it will remain dirty unless some caller requests its value, after which it will become not-dirty again.
*
* this signal also always fires an update when at least one of its dependencies has fired an update.
* and it abandons checking for equality all together, since it only recomputes after a get request,
* by which it is too late to signal no update in the value (because its observer is already running).
*
* this signal becomes pointless (in terms of efficiency) once a {@link MemoSignal_Factory | `MemoSignal`} depends on it.
* but it is increadibly useful (i.e. lazy) when other {@link LazySignal_Factory | `LazySignal`s} depend on one another.
*/
export const LazySignal_Factory = (ctx: Context) => {
/** {@inheritDoc LazySignal_Factory} */
return class LazySignal<T> extends ctx.getClass(SimpleSignal_Factory)<T> {
declare fn: MemoFn<T>
declare dirty: 0 | 1
Expand Down Expand Up @@ -241,8 +272,12 @@ export type EffectFn = (observer_id: TO_ID | UNTRACKED_ID) => void | undefined |
*/
export type EffectEmitter = () => boolean

/** extremely similar to {@link MemoSignal_Factory | `MemoSignal`}, but without a value to output, and also has the ability to fire on its own.
* TODO-DOC: explain more
*/
export const EffectSignal_Factory = (ctx: Context) => {
const runId = ctx.runId
/** {@inheritDoc EffectSignal_Factory} */
return class EffectSignal extends ctx.getClass(SimpleSignal_Factory)<void> {
declare fn: EffectFn
declare prerun: never
Expand Down
5 changes: 5 additions & 0 deletions src/typedefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ export type EqualityFn<T> = (prev_value: T | undefined, new_value: T) => boolean
*/
export type EqualityCheck<T> = undefined | false | EqualityFn<T>

/** the {@link Signal.id | `id`} of a signal is simply a number. */
export type ID = number
/** another way of saying the dependency (or source) signal id. */
export type FROM_ID = ID
/** another way of saying the observer (or destination) signal id. */
export type TO_ID = ID
/** the {@link Signal.rid | runtime id (`rid`)} of a signal becomes zero after its first run (because it has captured all of its dependencies). */
export type UNTRACKED_ID = 0
/** the hash value generated by the dfs-signal-dag-traversal hash function. */
export type HASHED_IDS = number

/** type definition for an incremental updater function. */
Expand Down

0 comments on commit 19cb66e

Please sign in to comment.