@continuum-js/std 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +168 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @continuum-js/std
|
|
2
|
+
|
|
3
|
+
Стандартная библиотека переиспользуемых FRP-комбинаторов Continuum — то, что
|
|
4
|
+
нужно каждый день: тайминг, асинхронные данные, форма потоков, хелперы для
|
|
5
|
+
поведений. Всё построено поверх `@continuum-js/frp` без доступа к внутренностям,
|
|
6
|
+
которых ядро не отдаёт наружу.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { resource, debounce /* … */ } from "@continuum-js/std";
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Тайминг
|
|
13
|
+
|
|
14
|
+
Мост к настенным часам (`setTimeout`/`setInterval`). Каждый возвращает событие,
|
|
15
|
+
которое гасит свой таймер в `dispose()`.
|
|
16
|
+
|
|
17
|
+
| | |
|
|
18
|
+
| ----------------- | --------------------------------------------------------------- |
|
|
19
|
+
| `debounce(e, ms)` | эмит после `ms` тишины, всплеск коалесится в последнее значение |
|
|
20
|
+
| `throttle(e, ms)` | пропустить первое происшествие, затем игнорировать `ms` |
|
|
21
|
+
| `delay(e, ms)` | сдвинуть каждое происшествие на `ms` позже |
|
|
22
|
+
| `interval(ms)` | источник, тикающий `1, 2, 3, …` каждые `ms` |
|
|
23
|
+
|
|
24
|
+
## Форма потоков
|
|
25
|
+
|
|
26
|
+
Производные события над графом (glitch-free, по рангам).
|
|
27
|
+
|
|
28
|
+
| | |
|
|
29
|
+
| ------------------------ | -------------------------------------------------- |
|
|
30
|
+
| `filterMap(e, f)` | map с отбрасыванием `null`/`undefined` |
|
|
31
|
+
| `pairwise(e)` | `[prev, curr]`, начиная со второго происшествия |
|
|
32
|
+
| `partition(e, pred)` | разбить поток на `[подходящие, остальные]` |
|
|
33
|
+
| `count(e)` | `Behavior<number>` — сколько раз событие случилось |
|
|
34
|
+
| `sampleWith(trigger, b)` | значение `b` в момент каждого `trigger` |
|
|
35
|
+
|
|
36
|
+
## Поведения
|
|
37
|
+
|
|
38
|
+
| | |
|
|
39
|
+
| ------------------- | -------------------------------------------------------------- |
|
|
40
|
+
| `previous(b, init)` | поведение, отстающее на шаг (значение до последнего изменения) |
|
|
41
|
+
| `distinctB(b, eq?)` | подавляет обновления, равные текущему значению |
|
|
42
|
+
|
|
43
|
+
## Асинхронные данные (HTTP)
|
|
44
|
+
|
|
45
|
+
`perform` — граница IO (§6.5): эффект бежит после закрытия момента, результат
|
|
46
|
+
возвращается в сеть как данные, ошибка завёрнута в `Result`, а не выброшена.
|
|
47
|
+
|
|
48
|
+
- **`resource(trigger, fetcher): Behavior<Async<T>>`** — конечный автомат
|
|
49
|
+
`idle → loading → ok | error`. Запросы нумеруются, поэтому запоздавший ответ
|
|
50
|
+
на устаревший запрос отбрасывается (last-request-wins) — декларативное решение
|
|
51
|
+
классического бага гонки ответов.
|
|
52
|
+
- **`Async<T>`** — жизненный цикл запроса как первоклассные данные.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const query = debounce(input.updates, 300).filter((s) => s.length > 0);
|
|
56
|
+
const users = resource(query, (q) =>
|
|
57
|
+
fetch(`/api?q=${q}`).then((r) => r.json()),
|
|
58
|
+
);
|
|
59
|
+
// users: Behavior<Async<User[]>> — рисуй по users.status
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Живой пример — [`examples/data`](../../examples/data).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Event, Behavior } from "@continuum-js/frp";
|
|
2
|
+
/**
|
|
3
|
+
* Emit only after `ms` of quiet, coalescing a burst into its last value.
|
|
4
|
+
* (Trailing debounce.)
|
|
5
|
+
*/
|
|
6
|
+
export declare function debounce<A>(e: Event<A>, ms: number): Event<A>;
|
|
7
|
+
/**
|
|
8
|
+
* Emit the leading occurrence immediately, then ignore further ones for `ms`.
|
|
9
|
+
* (Leading throttle / rate limit.)
|
|
10
|
+
*/
|
|
11
|
+
export declare function throttle<A>(e: Event<A>, ms: number): Event<A>;
|
|
12
|
+
/** Shift every occurrence later by `ms`, preserving order and multiplicity. */
|
|
13
|
+
export declare function delay<A>(e: Event<A>, ms: number): Event<A>;
|
|
14
|
+
/** A source event ticking `1, 2, 3, …` every `ms`. Stops on `dispose()`. */
|
|
15
|
+
export declare function interval(ms: number): Event<number>;
|
|
16
|
+
/** Map, dropping occurrences whose result is `null`/`undefined`. */
|
|
17
|
+
export declare function filterMap<A, B>(e: Event<A>, f: (a: A) => B | null | undefined): Event<B>;
|
|
18
|
+
/** Pair each occurrence with the previous one; emits from the 2nd occurrence. */
|
|
19
|
+
export declare function pairwise<A>(e: Event<A>): Event<[A, A]>;
|
|
20
|
+
/** Split a stream by a predicate into `[matching, rest]`. */
|
|
21
|
+
export declare function partition<A>(e: Event<A>, pred: (a: A) => boolean): [Event<A>, Event<A>];
|
|
22
|
+
/** A behavior of how many times the event has occurred. */
|
|
23
|
+
export declare function count(e: Event<unknown>): Behavior<number>;
|
|
24
|
+
/** Sample `b` at each occurrence of `trigger`, discarding the trigger's value. */
|
|
25
|
+
export declare function sampleWith<A, B>(trigger: Event<A>, b: Behavior<B>): Event<B>;
|
|
26
|
+
/** A behavior lagging one step behind `b` (its value before the latest change). */
|
|
27
|
+
export declare function previous<A>(b: Behavior<A>, init: A): Behavior<A>;
|
|
28
|
+
/** A behavior that suppresses updates equal to the current value (default `Object.is`). */
|
|
29
|
+
export declare function distinctB<A>(b: Behavior<A>, eq?: (x: A, y: A) => boolean): Behavior<A>;
|
|
30
|
+
/** The lifecycle of an asynchronous request as first-class data. */
|
|
31
|
+
export type Async<T> = {
|
|
32
|
+
status: "idle";
|
|
33
|
+
} | {
|
|
34
|
+
status: "loading";
|
|
35
|
+
} | {
|
|
36
|
+
status: "ok";
|
|
37
|
+
value: T;
|
|
38
|
+
} | {
|
|
39
|
+
status: "error";
|
|
40
|
+
error: unknown;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Turn a stream of requests into a behavior tracking the async lifecycle
|
|
44
|
+
* (`idle → loading → ok | error`). Requests are stamped with a sequence number,
|
|
45
|
+
* so a slow response to a superseded request is ignored (last-request-wins) —
|
|
46
|
+
* the classic out-of-order-response bug, solved declaratively.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resource<A, T>(trigger: Event<A>, fetcher: (arg: A) => Promise<T>): Behavior<Async<T>>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Continuum standard library (std).
|
|
2
|
+
// Reusable combinators built purely on the frp core — the everyday toolkit for
|
|
3
|
+
// real apps: timing, async data, stream shaping, behavior helpers. Nothing here
|
|
4
|
+
// touches internals the core doesn't already expose.
|
|
5
|
+
import { Event, Behavior, newEvent, perform } from "@continuum-js/frp";
|
|
6
|
+
// ===========================================================================
|
|
7
|
+
// Timing — bridges to the wall clock via setTimeout/setInterval. Each returns
|
|
8
|
+
// an event that tears its timer down on `dispose()`.
|
|
9
|
+
// ===========================================================================
|
|
10
|
+
/**
|
|
11
|
+
* Emit only after `ms` of quiet, coalescing a burst into its last value.
|
|
12
|
+
* (Trailing debounce.)
|
|
13
|
+
*/
|
|
14
|
+
export function debounce(e, ms) {
|
|
15
|
+
const [out, fire] = newEvent();
|
|
16
|
+
let timer;
|
|
17
|
+
const un = e.listen((a) => {
|
|
18
|
+
if (timer !== undefined)
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
timer = setTimeout(() => {
|
|
21
|
+
timer = undefined;
|
|
22
|
+
fire(a);
|
|
23
|
+
}, ms);
|
|
24
|
+
});
|
|
25
|
+
out.onDispose(() => {
|
|
26
|
+
un();
|
|
27
|
+
if (timer !== undefined)
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
});
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Emit the leading occurrence immediately, then ignore further ones for `ms`.
|
|
34
|
+
* (Leading throttle / rate limit.)
|
|
35
|
+
*/
|
|
36
|
+
export function throttle(e, ms) {
|
|
37
|
+
const [out, fire] = newEvent();
|
|
38
|
+
let blocked = false;
|
|
39
|
+
let timer;
|
|
40
|
+
const un = e.listen((a) => {
|
|
41
|
+
if (blocked)
|
|
42
|
+
return;
|
|
43
|
+
blocked = true;
|
|
44
|
+
fire(a);
|
|
45
|
+
timer = setTimeout(() => {
|
|
46
|
+
blocked = false;
|
|
47
|
+
}, ms);
|
|
48
|
+
});
|
|
49
|
+
out.onDispose(() => {
|
|
50
|
+
un();
|
|
51
|
+
if (timer !== undefined)
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
});
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** Shift every occurrence later by `ms`, preserving order and multiplicity. */
|
|
57
|
+
export function delay(e, ms) {
|
|
58
|
+
const [out, fire] = newEvent();
|
|
59
|
+
const timers = new Set();
|
|
60
|
+
const un = e.listen((a) => {
|
|
61
|
+
const id = setTimeout(() => {
|
|
62
|
+
timers.delete(id);
|
|
63
|
+
fire(a);
|
|
64
|
+
}, ms);
|
|
65
|
+
timers.add(id);
|
|
66
|
+
});
|
|
67
|
+
out.onDispose(() => {
|
|
68
|
+
un();
|
|
69
|
+
for (const id of timers)
|
|
70
|
+
clearTimeout(id);
|
|
71
|
+
timers.clear();
|
|
72
|
+
});
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** A source event ticking `1, 2, 3, …` every `ms`. Stops on `dispose()`. */
|
|
76
|
+
export function interval(ms) {
|
|
77
|
+
const [out, fire] = newEvent();
|
|
78
|
+
let n = 0;
|
|
79
|
+
const id = setInterval(() => fire(++n), ms);
|
|
80
|
+
out.onDispose(() => clearInterval(id));
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
// ===========================================================================
|
|
84
|
+
// Stream shaping — derived events over the graph (glitch-free, rank-ordered).
|
|
85
|
+
// ===========================================================================
|
|
86
|
+
/** Map, dropping occurrences whose result is `null`/`undefined`. */
|
|
87
|
+
export function filterMap(e, f) {
|
|
88
|
+
const out = new Event(e.rank + 1);
|
|
89
|
+
out.consume(e, (t, a) => {
|
|
90
|
+
const b = f(a);
|
|
91
|
+
if (b != null)
|
|
92
|
+
out.send_(t, b);
|
|
93
|
+
});
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/** Pair each occurrence with the previous one; emits from the 2nd occurrence. */
|
|
97
|
+
export function pairwise(e) {
|
|
98
|
+
const out = new Event(e.rank + 1);
|
|
99
|
+
let hasPrev = false;
|
|
100
|
+
let prev;
|
|
101
|
+
out.consume(e, (t, a) => {
|
|
102
|
+
if (hasPrev)
|
|
103
|
+
out.send_(t, [prev, a]);
|
|
104
|
+
prev = a;
|
|
105
|
+
hasPrev = true;
|
|
106
|
+
});
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/** Split a stream by a predicate into `[matching, rest]`. */
|
|
110
|
+
export function partition(e, pred) {
|
|
111
|
+
return [e.filter(pred), e.filter((a) => !pred(a))];
|
|
112
|
+
}
|
|
113
|
+
/** A behavior of how many times the event has occurred. */
|
|
114
|
+
export function count(e) {
|
|
115
|
+
return e.accum(0, (_a, n) => n + 1);
|
|
116
|
+
}
|
|
117
|
+
/** Sample `b` at each occurrence of `trigger`, discarding the trigger's value. */
|
|
118
|
+
export function sampleWith(trigger, b) {
|
|
119
|
+
return trigger.snapshot(b, (_a, v) => v);
|
|
120
|
+
}
|
|
121
|
+
// ===========================================================================
|
|
122
|
+
// Behavior helpers.
|
|
123
|
+
// ===========================================================================
|
|
124
|
+
/** A behavior lagging one step behind `b` (its value before the latest change). */
|
|
125
|
+
export function previous(b, init) {
|
|
126
|
+
// At the instant of an update, `b` still samples its pre-commit (prior) value,
|
|
127
|
+
// since `hold` commits at the moment boundary.
|
|
128
|
+
return b.updates.snapshot(b, (_new, old) => old).hold(init);
|
|
129
|
+
}
|
|
130
|
+
/** A behavior that suppresses updates equal to the current value (default `Object.is`). */
|
|
131
|
+
export function distinctB(b, eq = Object.is) {
|
|
132
|
+
const out = new Event(b.updates.rank + 1);
|
|
133
|
+
let prev = b.sampleNoTrans();
|
|
134
|
+
b.updates.listen_(out, (t, a) => {
|
|
135
|
+
if (!eq(prev, a)) {
|
|
136
|
+
prev = a;
|
|
137
|
+
out.send_(t, a);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return new Behavior(() => b.sampleNoTrans(), out);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Turn a stream of requests into a behavior tracking the async lifecycle
|
|
144
|
+
* (`idle → loading → ok | error`). Requests are stamped with a sequence number,
|
|
145
|
+
* so a slow response to a superseded request is ignored (last-request-wins) —
|
|
146
|
+
* the classic out-of-order-response bug, solved declaratively.
|
|
147
|
+
*/
|
|
148
|
+
export function resource(trigger, fetcher) {
|
|
149
|
+
const requests = trigger.accumE({ seq: 0, arg: null }, (arg, prev) => ({
|
|
150
|
+
seq: prev.seq + 1,
|
|
151
|
+
arg,
|
|
152
|
+
}));
|
|
153
|
+
const latest = requests.map((r) => r.seq).hold(0);
|
|
154
|
+
const responses = perform(requests, (r) => fetcher(r.arg).then((value) => ({ seq: r.seq, value })));
|
|
155
|
+
const settled = responses
|
|
156
|
+
.snapshot(latest, (res, latestSeq) => {
|
|
157
|
+
if (res.ok) {
|
|
158
|
+
if (res.value.seq !== latestSeq)
|
|
159
|
+
return null; // superseded — ignore
|
|
160
|
+
return { status: "ok", value: res.value.value };
|
|
161
|
+
}
|
|
162
|
+
return { status: "error", error: res.error };
|
|
163
|
+
})
|
|
164
|
+
.filter((s) => s !== null);
|
|
165
|
+
const loading = requests.mapTo({ status: "loading" });
|
|
166
|
+
// loading (request moment) and settled (later moment) never coincide.
|
|
167
|
+
return loading.orElse(settled).hold({ status: "idle" });
|
|
168
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@continuum-js/std",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Continuum standard library — reusable FRP combinators (async, timing, streams)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/denislibs/continuum.git",
|
|
9
|
+
"directory": "packages/std"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@continuum-js/frp": "0.3.0"
|
|
34
|
+
}
|
|
35
|
+
}
|