Skip to content

Commit

Permalink
Split signals API and utils to separate exports
Browse files Browse the repository at this point in the history
  • Loading branch information
pjeby committed Jul 14, 2024
1 parent 76272bb commit f2be965
Show file tree
Hide file tree
Showing 14 changed files with 239 additions and 150 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### 0.0.6 (unreleased)

- Moved main signals API to a separate export (`uneventful/signals`) and exposed the utils module as an export (`uneventful/utils`).
- Expanded and enhanced the `RuleFactory` interface:
- `rule.stop` can now be saved and then called from outside a rule
- `rule.detached(...)` is a new shorthand for `detached.run(rule, ...)`
Expand Down
25 changes: 21 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,28 @@
"homepage": "https://uneventful.js.org",
"license": "ISC",
"type": "module",
"files": ["./dist/*"],
"types": "./dist/mod.d.ts",
"files": [
"./dist/*"
],
"exports": {
"import": {
"types": "./dist/mod.d.ts",
"default": "./dist/mod.mjs"
".": {
"import": {
"types": "./dist/mod.d.ts",
"default": "./dist/mod.mjs"
}
},
"./signals": {
"import": {
"types": "./dist/signals.d.ts",
"default": "./dist/signals.mjs"
}
},
"./utils": {
"import": {
"types": "./dist/utils.d.ts",
"default": "./dist/utils.mjs"
}
}
},
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion specs/cells.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { log, see, describe, expect, it, useRoot, spy } from "./dev_deps.ts";
import { runRules, value, cached, rule, CircularDependency, WriteConflict } from "../mod.ts";
import { runRules, value, cached, rule, CircularDependency, WriteConflict } from "../src/signals.ts";
import { defer } from "../src/defer.ts";
import { current } from "../src/ambient.ts";

Expand Down
5 changes: 3 additions & 2 deletions specs/jobs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { log, see, describe, expect, it, useClock, clock, useRoot, noClock, logUncaught } from "./dev_deps.ts";
import {
start, Suspend, Request, to, resolve, reject, resolver, rejecter, Yielding, must, until, fromIterable,
IsStream, value, cached, runRules, backpressure, sleep, isHandled, Connection, detached, makeJob,
start, Suspend, Request, to, resolve, reject, resolver, rejecter, Yielding, must, fromIterable,
IsStream, backpressure, sleep, isHandled, Connection, detached, makeJob,
CancelError, throttle, next
} from "../src/mod.ts";
import { value, cached, runRules, until } from "../src/signals.ts";
import { runPulls } from "../src/scheduling.ts";
import { catchers, defaultCatch } from "../src/internals.ts";

Expand Down
3 changes: 2 additions & 1 deletion specs/rules.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { log, see, describe, it, useRoot, msg, expect } from "./dev_deps.ts";
import { runRules, value, rule, must, DisposeFn, detached, SchedulerFn } from "../mod.ts";
import { must, DisposeFn, detached } from "../mod.ts";
import { runRules, value, rule, SchedulerFn } from "../src/signals.ts";

describe("@rule.method", () => {
useRoot();
Expand Down
8 changes: 4 additions & 4 deletions specs/signals.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { log, see, describe, expect, it, useRoot, useClock, clock } from "./dev_deps.ts";
import {
runRules, value, cached, rule, peek, WriteConflict, Signal, Writable, must, recalcWhen,
DisposeFn, RecalcSource, mockSource, lazy, detached, each, sleep,
SignalImpl, ConfigurableImpl, action
} from "../mod.ts";
runRules, value, cached, rule, peek, WriteConflict, Signal, Writable, SignalImpl, ConfigurableImpl, action
} from "../src/signals.ts";
import { recalcWhen } from "../src/sinks.ts";
import { must, DisposeFn, RecalcSource, mockSource, lazy, detached, each, sleep } from "../src/mod.ts";
import { current } from "../src/ambient.ts";
import { nullCtx } from "../src/internals.ts";
import { defaultQ } from "../src/scheduling.ts";
Expand Down
6 changes: 5 additions & 1 deletion specs/tracking.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { afterEach, beforeEach, clock, describe, expect, it, log, see, spy, useClock, useRoot } from "./dev_deps.ts";
import { current, freeCtx, makeCtx, swapCtx } from "../src/ambient.ts";
import { rule, runRules, noop, CleanupFn, Job, start, isJobActive, must, detached, makeJob, getJob, isCancel, isValue, restarting, isHandled, JobResult, nativePromise, Suspend, getResult } from "../mod.ts";
import {
CleanupFn, Job, JobResult, Suspend, detached, getJob, getResult, isCancel, isHandled, isJobActive, isValue, makeJob,
must, nativePromise, noop, restarting, start
} from "../mod.ts";
import { rule, runRules } from "../src/signals.ts";
import { Cell } from "../src/cells.ts";

describe("makeJob()", () => {
Expand Down
22 changes: 22 additions & 0 deletions src/call-or-wait.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Job, Yielding } from "./types.ts";
import { start } from "./jobutils.ts";
import { isValue, isError, markHandled } from "./results.ts";
import { isFunction } from "./utils.ts";
import { connect, Source } from "./streams.ts";

export function callOrWait<T>(
source: any, method: string, handler: (job: Job<T>, val: T) => void, noArgs: (f?: any) => Yielding<T> | void
) {
if (source && isFunction(source[method])) return source[method]() as Yielding<T>;
if (isFunction(source)) return (
source.length === 0 ? noArgs(source) : false
) || start<T>(job => {
connect(source as Source<T>, v => handler(job, v)).do(r => {
if (isValue(r)) job.throw(new Error("Stream ended"));
else if (isError(r)) job.throw(markHandled(r));
});
});
mustBeSourceOrSignal();
}

export function mustBeSourceOrSignal() { throw new TypeError("not a source or signal"); }
13 changes: 12 additions & 1 deletion src/mod.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
/**
* This is the default export of uneventful, which contains the API for jobs and
* streams, as well as any signals-related APIs that don't depend on the signals
* framework (e.g. {@link recalcWhen}, which does nothing if the signals framework
* isn't in use, and doesn't cause it to be imported).
*
* For the rest of the signals API, see the
* [uneventful/signals](uneventful_signals.html) export.
*
* @module uneventful
*/

export { defer } from "./defer.ts";
export * from "./types.ts";
export * from "./results.ts";
export * from "./tracking.ts";
export * from "./async.ts";
export * from "./signals.ts";
export * from "./streams.ts"
export * from "./sources.ts";
export * from "./sinks.ts";
Expand Down
101 changes: 47 additions & 54 deletions src/signals.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
/**
* The Signals API for uneventful.
*
* @module uneventful/signals
*/

import { current, freeCtx, makeCtx, swapCtx } from "./ambient.ts";
import { PlainFunction, Yielding, RecalcSource, AnyFunction } from "./types.ts";
import { PlainFunction, Yielding, AnyFunction, Job } from "./types.ts";
import { Cell } from "./cells.ts";
import { rule } from "./rules.ts";
import { reject, resolve } from "./results.ts";
import { UntilMethod } from "./sinks.ts";
import { SignalSource, Source } from "./streams.ts";
import { SignalSource, Source, Stream } from "./streams.ts";
import { callOrWait } from "./call-or-wait.ts";
import { CallableObject, apply } from "./utils.ts";
import { defer } from "./defer.ts";
import { next } from "./sinks.ts"; // needed for documentation link

export type * from "./rules.ts"
export { rule, runRules } from "./rules.ts"
Expand Down Expand Up @@ -241,58 +249,6 @@ export function peek<F extends PlainFunction>(fn: F, ...args: Parameters<F>): Re
try { return fn(...args); } finally { freeCtx(swapCtx(old)); }
}

/**
* Arrange for the current signal or rule to recalculate on demand
*
* This lets you interop with systems that have a way to query a value and
* subscribe to changes to it, but not directly produce a signal. (Such as
* querying the DOM state and using a MutationObserver.)
*
* By calling this with a {@link Source} or {@link RecalcSource}, you arrange
* for it to be subscribed, if and when the call occurs in a rule or a cached
* function that's in use by a rule (directly or indirectly). When the source
* emits a value, the signal machinery will invalidate the caching of the
* function or rule, forcing a recalculation and subsequent rule reruns, if
* applicable.
*
* Note: you should generally only call the 1-argument version of this function
* with "static" sources - i.e. ones that won't change on every call. Otherwise,
* you will end up creating new signals each time, subscribing and unsubscribing
* on every call to recalcWhen().
*
* If the source needs to reference some object, it's best to use the 2-argument
* version (i.e. `recalcWhen(someObj, factory)`, where `factory` is a function
* that takes `someObj` and returns a suitable {@link RecalcSource}.)
*
* @remarks
* recalcWhen is specifically designed so that using it does not pull in any
* part of Uneventful's signals framework, in the event a program doesn't
* already use it. This means you can use it in library code to provide signal
* compatibility, without adding bundle bloat to code that doesn't use signals.
*
* @category Signals
*/
export function recalcWhen(src: RecalcSource): void;
/**
* Two-argument variant of recalcWhen
*
* In certain circumstances, you may wish to use recalcWhen with a source
* related to some object. You could call recalcWhen with a closure, but that
* would create and discard signals on every call. So this 2-argument version
* lets you avoid that by allowing the use of an arbitrary object as a key,
* along with a factory function to turn the key into a {@link RecalcSource}.
*
* @param key an object to be used as a key
*
* @param factory a function that will be called with the key to obtain a
* {@link RecalcSource}. (Note that this factory function must also be a static
* function, not a closure, or the same memory thrash issue will occur!)
*/
export function recalcWhen<T extends WeakKey>(key: T, factory: (key: T) => RecalcSource): void;
export function recalcWhen<T extends WeakKey>(fnOrKey: T | RecalcSource, fn?: (key: T) => RecalcSource) {
current.cell?.recalcWhen<T>(fnOrKey as T, fn);
}

/**
* Wrap a function (or decorate a method) so that signals it reads are not added
* as dependencies to the current rule (if any). (Basically, it's shorthand for
Expand Down Expand Up @@ -352,3 +308,40 @@ export function action<F extends AnyFunction, D extends {value?: F}>(fn: F, _ctx
try { return apply(fn, this, arguments); } finally { freeCtx(swapCtx(old)); }
}
}

/**
* Wait for and return the next truthy value (or error) from a data source (when
* processed with `yield *` within a {@link Job}).
*
* This differs from {@link next}() in that it waits for the next "truthy" value
* (i.e., not null, false, zero, empty string, etc.), and when used with signals
* or a signal-using function, it can resume *immediately* if the result is
* already truthy. (It also supports zero-argument signal-using functions,
* automatically wrapping them with {@link cached}(), as the common use case for
* until() is to wait for an arbitrary condition to be satisfied.)
*
* @param source The source to wait on, which can be:
* - An object with an `"uneventful.until"` method returning a {@link Yielding}
* (in which case the result will be the the result of calling that method)
* - A {@link Signal}, or a zero-argument function returning a value based on
* signals (in which case the job resumes as soon as the result is truthy,
* perhaps immediately)
* - A {@link Source} (in which case the job resumes on the next truthy value
* it produces
*
* (Note: if the supplied source is a function with a non-zero `.length`, it is
* assumed to be a {@link Source}.)
*
* @returns a Yieldable that when processed with `yield *` in a job, will return
* the triggered event, or signal value. An error is thrown if event stream
* throws or closes early, or the signal throws.
*
* @category Signals
* @category Scheduling
*/

export function until<T>(source: UntilMethod<T> | Stream<T> | (() => T)): Yielding<T> {
return callOrWait<T>(source, "uneventful.until", waitTruthy, recache);
}
function recache<T>(s: () => T) { return until(cached(s)); }
function waitTruthy<T>(job: Job<T>, v: T) { v && job.return(v); }
Loading

0 comments on commit f2be965

Please sign in to comment.