@thi.ng/rstream 8.2.13 → 8.2.14

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/subscription.js CHANGED
@@ -8,283 +8,222 @@ import { comp } from "@thi.ng/transducers/comp";
8
8
  import { map } from "@thi.ng/transducers/map";
9
9
  import { push } from "@thi.ng/transducers/push";
10
10
  import { isReduced, unreduced } from "@thi.ng/transducers/reduced";
11
- import { CloseMode, State, } from "./api.js";
11
+ import {
12
+ CloseMode,
13
+ State
14
+ } from "./api.js";
12
15
  import { __optsWithID } from "./idgen.js";
13
16
  import { LOGGER } from "./logger.js";
14
- /**
15
- * Creates a new {@link Subscription} instance, the fundamental datatype and
16
- * building block provided by this package.
17
- *
18
- * @remarks
19
- * Most other types in rstream, including {@link Stream}s, are `Subscription`s
20
- * and all can be:
21
- *
22
- * - connected into directed graphs (sync or async & not necessarily DAGs)
23
- * - transformed using transducers (incl. support for early termination)
24
- * - can have any number of subscribers (optionally each w/ their own
25
- * transducers)
26
- * - recursively unsubscribe themselves from parent after their last subscriber
27
- * unsubscribed (configurable)
28
- * - will go into a non-recoverable error state if none of the subscribers has
29
- * an error handler itself
30
- * - implement the
31
- * [`IDeref`](https://docs.thi.ng/umbrella/api/interfaces/IDeref.html)
32
- * interface
33
- *
34
- * If a transducer is provided (via the `xform` option), all received values
35
- * will be first processed by the transducer and only its transformed result(s)
36
- * (if any) will be passed to downstream subscribers. Any uncaught errors
37
- * *inside* the transducer will cause this subscription's error handler to be
38
- * called and will stop this subscription from receiving any further values (by
39
- * default, unless overridden).
40
- *
41
- * Subscription behavior can be customized via the additional (optional) options
42
- * arg. See {@link CommonOpts} and {@link SubscriptionOpts} for further details.
43
- *
44
- * @example
45
- * ```ts
46
- * // as reactive value mechanism (same as with stream() above)
47
- * s = subscription();
48
- * s.subscribe(trace("s1"));
49
- * s.subscribe(trace("s2"), { xform: tx.filter((x) => x > 25) });
50
- *
51
- * // external trigger
52
- * s.next(23);
53
- * // s1 23
54
- * s.next(42);
55
- * // s1 42
56
- * // s2 42
57
- * ```
58
- *
59
- * @param sub -
60
- * @param opts -
61
- */
62
- export const subscription = (sub, opts) => new Subscription(sub, opts);
63
- export class Subscription {
64
- wrapped;
65
- id;
66
- closeIn;
67
- closeOut;
68
- parent;
69
- __owner;
70
- xform;
71
- cacheLast;
72
- last = SEMAPHORE;
73
- state = State.IDLE;
74
- subs = [];
75
- constructor(wrapped, opts) {
76
- this.wrapped = wrapped;
77
- opts = __optsWithID(`sub`, {
78
- closeIn: CloseMode.LAST,
79
- closeOut: CloseMode.LAST,
80
- cache: true,
81
- ...opts,
82
- });
83
- this.parent = opts.parent;
84
- this.id = opts.id;
85
- this.closeIn = opts.closeIn;
86
- this.closeOut = opts.closeOut;
87
- this.cacheLast = opts.cache;
88
- opts.xform && (this.xform = opts.xform(push()));
89
- }
90
- deref() {
91
- return this.last !== SEMAPHORE ? this.last : undefined;
92
- }
93
- getState() {
94
- return this.state;
95
- }
96
- setState(state) {
97
- this.state = state;
98
- }
99
- subscribe(sub, opts = {}) {
100
- this.ensureState();
101
- let $sub;
102
- if (sub instanceof Subscription && !opts.xform) {
103
- sub.ensureState();
104
- // ensure sub is still unattached
105
- assert(!sub.parent, `sub '${sub.id}' already has a parent`);
106
- sub.parent = this;
107
- $sub = sub;
108
- }
109
- else {
110
- $sub = new Subscription(sub, { ...opts, parent: this });
111
- }
112
- this.subs.push($sub);
113
- this.setState(State.ACTIVE);
114
- $sub.setState(State.ACTIVE);
115
- this.last != SEMAPHORE && $sub.next(this.last);
116
- return $sub;
117
- }
118
- transform(...args) {
119
- let sub;
120
- let opts;
121
- if (isPlainObject(peek(args))) {
122
- opts = args.pop();
123
- sub = { error: opts.error };
124
- }
125
- return this.subscribe(sub, __optsWithID("xform", args.length > 0
126
- ? {
127
- ...opts,
128
- // @ts-ignore
129
- xform: comp(...args),
130
- }
131
- : opts));
132
- }
133
- /**
134
- * Syntax sugar for {@link Subscription.transform} when using a single
135
- * [`map()`](https://docs.thi.ng/umbrella/transducers/functions/map.html)
136
- * transducer only. The given function `fn` is used as `map`'s
137
- * transformation fn.
138
- *
139
- * @param fn -
140
- * @param opts -
141
- */
142
- map(fn, opts) {
143
- return this.transform(map(fn), opts || {});
144
- }
145
- unsubscribe(sub) {
146
- return sub ? this.unsubscribeChild(sub) : this.unsubscribeSelf();
147
- }
148
- unsubscribeSelf() {
149
- LOGGER.debug(this.id, "unsub self");
150
- this.parent && this.parent.unsubscribe(this);
151
- this.state < State.UNSUBSCRIBED && (this.state = State.UNSUBSCRIBED);
152
- this.release();
153
- return true;
154
- }
155
- unsubscribeChild(sub) {
156
- LOGGER.debug(this.id, "unsub child", sub.id);
157
- const idx = this.subs.indexOf(sub);
158
- if (idx >= 0) {
159
- this.subs.splice(idx, 1);
160
- if (this.closeOut === CloseMode.FIRST ||
161
- (!this.subs.length && this.closeOut !== CloseMode.NEVER)) {
162
- this.unsubscribe();
163
- }
164
- return true;
165
- }
166
- return false;
167
- }
168
- next(x) {
169
- if (this.state >= State.DONE)
170
- return;
171
- this.xform ? this.dispatchXform(x) : this.dispatch(x);
172
- }
173
- done() {
174
- LOGGER.debug(this.id, "entering done()");
175
- if (this.state >= State.DONE)
176
- return;
177
- if (this.xform) {
178
- if (!this.dispatchXformDone())
179
- return;
180
- }
181
- this.state = State.DONE;
182
- // attempt to call .done in wrapped sub
183
- if (this.dispatchTo("done")) {
184
- // disconnect from parent & internal cleanup
185
- this.state < State.UNSUBSCRIBED && this.unsubscribe();
186
- }
187
- LOGGER.debug(this.id, "exiting done()");
188
- }
189
- error(e) {
190
- // only the wrapped sub's error handler gets a chance
191
- // to deal with the error
192
- const sub = this.wrapped;
193
- const hasErrorHandler = sub && sub.error;
194
- hasErrorHandler &&
195
- LOGGER.debug(this.id, "attempting wrapped error handler");
196
- // flag success if error handler returns true
197
- // (i.e. it could handle/recover from the error)
198
- // else detach this entire sub by going into error state...
199
- return (hasErrorHandler && sub.error(e)) || this.unhandledError(e);
200
- }
201
- unhandledError(e) {
202
- // ensure error is at least logged to console
203
- // even if default NULL_LOGGER is used...
204
- (LOGGER !== NULL_LOGGER ? LOGGER : console).warn(this.id, "unhandled error:", e);
17
+ const subscription = (sub, opts) => new Subscription(sub, opts);
18
+ class Subscription {
19
+ constructor(wrapped, opts) {
20
+ this.wrapped = wrapped;
21
+ opts = __optsWithID(`sub`, {
22
+ closeIn: CloseMode.LAST,
23
+ closeOut: CloseMode.LAST,
24
+ cache: true,
25
+ ...opts
26
+ });
27
+ this.parent = opts.parent;
28
+ this.id = opts.id;
29
+ this.closeIn = opts.closeIn;
30
+ this.closeOut = opts.closeOut;
31
+ this.cacheLast = opts.cache;
32
+ opts.xform && (this.xform = opts.xform(push()));
33
+ }
34
+ id;
35
+ closeIn;
36
+ closeOut;
37
+ parent;
38
+ __owner;
39
+ xform;
40
+ cacheLast;
41
+ last = SEMAPHORE;
42
+ state = State.IDLE;
43
+ subs = [];
44
+ deref() {
45
+ return this.last !== SEMAPHORE ? this.last : void 0;
46
+ }
47
+ getState() {
48
+ return this.state;
49
+ }
50
+ setState(state) {
51
+ this.state = state;
52
+ }
53
+ subscribe(sub, opts = {}) {
54
+ this.ensureState();
55
+ let $sub;
56
+ if (sub instanceof Subscription && !opts.xform) {
57
+ sub.ensureState();
58
+ assert(!sub.parent, `sub '${sub.id}' already has a parent`);
59
+ sub.parent = this;
60
+ $sub = sub;
61
+ } else {
62
+ $sub = new Subscription(sub, { ...opts, parent: this });
63
+ }
64
+ this.subs.push($sub);
65
+ this.setState(State.ACTIVE);
66
+ $sub.setState(State.ACTIVE);
67
+ this.last != SEMAPHORE && $sub.next(this.last);
68
+ return $sub;
69
+ }
70
+ transform(...args) {
71
+ let sub;
72
+ let opts;
73
+ if (isPlainObject(peek(args))) {
74
+ opts = args.pop();
75
+ sub = { error: opts.error };
76
+ }
77
+ return this.subscribe(
78
+ sub,
79
+ __optsWithID(
80
+ "xform",
81
+ args.length > 0 ? {
82
+ ...opts,
83
+ // @ts-ignore
84
+ xform: comp(...args)
85
+ } : opts
86
+ )
87
+ );
88
+ }
89
+ /**
90
+ * Syntax sugar for {@link Subscription.transform} when using a single
91
+ * [`map()`](https://docs.thi.ng/umbrella/transducers/functions/map.html)
92
+ * transducer only. The given function `fn` is used as `map`'s
93
+ * transformation fn.
94
+ *
95
+ * @param fn -
96
+ * @param opts -
97
+ */
98
+ map(fn, opts) {
99
+ return this.transform(map(fn), opts || {});
100
+ }
101
+ unsubscribe(sub) {
102
+ return sub ? this.unsubscribeChild(sub) : this.unsubscribeSelf();
103
+ }
104
+ unsubscribeSelf() {
105
+ LOGGER.debug(this.id, "unsub self");
106
+ this.parent && this.parent.unsubscribe(this);
107
+ this.state < State.UNSUBSCRIBED && (this.state = State.UNSUBSCRIBED);
108
+ this.release();
109
+ return true;
110
+ }
111
+ unsubscribeChild(sub) {
112
+ LOGGER.debug(this.id, "unsub child", sub.id);
113
+ const idx = this.subs.indexOf(sub);
114
+ if (idx >= 0) {
115
+ this.subs.splice(idx, 1);
116
+ if (this.closeOut === CloseMode.FIRST || !this.subs.length && this.closeOut !== CloseMode.NEVER) {
205
117
  this.unsubscribe();
206
- this.state = State.ERROR;
207
- return false;
208
- }
209
- dispatchTo(type, x) {
210
- let s = this.wrapped;
211
- if (s) {
212
- try {
213
- s[type] && s[type](x);
214
- }
215
- catch (e) {
216
- // give wrapped sub a chance to handle error
217
- // (if that failed then we're already in error state now & terminate)
218
- if (!this.error(e))
219
- return false;
220
- }
221
- }
222
- // process other child subs
223
- const subs = type === "next" ? this.subs : [...this.subs];
224
- for (let i = subs.length; i-- > 0;) {
225
- s = subs[i];
226
- try {
227
- s[type] && s[type](x);
228
- }
229
- catch (e) {
230
- if (type === "error" || !s.error || !s.error(e)) {
231
- // if no or failed handler, go into error state
232
- return this.unhandledError(e);
233
- }
234
- }
118
+ }
119
+ return true;
120
+ }
121
+ return false;
122
+ }
123
+ next(x) {
124
+ if (this.state >= State.DONE)
125
+ return;
126
+ this.xform ? this.dispatchXform(x) : this.dispatch(x);
127
+ }
128
+ done() {
129
+ LOGGER.debug(this.id, "entering done()");
130
+ if (this.state >= State.DONE)
131
+ return;
132
+ if (this.xform) {
133
+ if (!this.dispatchXformDone())
134
+ return;
135
+ }
136
+ this.state = State.DONE;
137
+ if (this.dispatchTo("done")) {
138
+ this.state < State.UNSUBSCRIBED && this.unsubscribe();
139
+ }
140
+ LOGGER.debug(this.id, "exiting done()");
141
+ }
142
+ error(e) {
143
+ const sub = this.wrapped;
144
+ const hasErrorHandler = sub && sub.error;
145
+ hasErrorHandler && LOGGER.debug(this.id, "attempting wrapped error handler");
146
+ return hasErrorHandler && sub.error(e) || this.unhandledError(e);
147
+ }
148
+ unhandledError(e) {
149
+ (LOGGER !== NULL_LOGGER ? LOGGER : console).warn(
150
+ this.id,
151
+ "unhandled error:",
152
+ e
153
+ );
154
+ this.unsubscribe();
155
+ this.state = State.ERROR;
156
+ return false;
157
+ }
158
+ dispatchTo(type, x) {
159
+ let s = this.wrapped;
160
+ if (s) {
161
+ try {
162
+ s[type] && s[type](x);
163
+ } catch (e) {
164
+ if (!this.error(e))
165
+ return false;
166
+ }
167
+ }
168
+ const subs = type === "next" ? this.subs : [...this.subs];
169
+ for (let i = subs.length; i-- > 0; ) {
170
+ s = subs[i];
171
+ try {
172
+ s[type] && s[type](x);
173
+ } catch (e) {
174
+ if (type === "error" || !s.error || !s.error(e)) {
175
+ return this.unhandledError(e);
235
176
  }
236
- return true;
237
- }
238
- dispatch(x) {
239
- LOGGER.debug(this.id, "dispatch", x);
240
- this.cacheLast && (this.last = x);
241
- this.dispatchTo("next", x);
242
- }
243
- dispatchXform(x) {
244
- let acc;
245
- try {
246
- acc = this.xform[2]([], x);
247
- }
248
- catch (e) {
249
- // error in transducer can only be handled by the wrapped
250
- // subscriber's error handler (if avail)
251
- this.error(e);
252
- // don't dispatch value(s)
253
- return;
254
- }
255
- if (this.dispatchXformVals(acc)) {
256
- isReduced(acc) && this.done();
257
- }
258
- }
259
- dispatchXformDone() {
260
- let acc;
261
- try {
262
- // collect remaining values from transducer
263
- acc = this.xform[1]([]);
264
- }
265
- catch (e) {
266
- // error in transducer can only be handled by the wrapped
267
- // subscriber's error handler (if avail)
268
- return this.error(e);
269
- }
270
- return this.dispatchXformVals(acc);
271
- }
272
- dispatchXformVals(acc) {
273
- const uacc = unreduced(acc);
274
- for (let i = 0, n = uacc.length; i < n && this.state < State.DONE; i++) {
275
- this.dispatch(uacc[i]);
276
- }
277
- return this.state < State.ERROR;
278
- }
279
- ensureState() {
280
- if (this.state >= State.DONE) {
281
- illegalState(`operation not allowed in state ${this.state}`);
282
- }
283
- }
284
- release() {
285
- this.subs.length = 0;
286
- delete this.parent;
287
- delete this.xform;
288
- delete this.last;
289
- }
177
+ }
178
+ }
179
+ return true;
180
+ }
181
+ dispatch(x) {
182
+ LOGGER.debug(this.id, "dispatch", x);
183
+ this.cacheLast && (this.last = x);
184
+ this.dispatchTo("next", x);
185
+ }
186
+ dispatchXform(x) {
187
+ let acc;
188
+ try {
189
+ acc = this.xform[2]([], x);
190
+ } catch (e) {
191
+ this.error(e);
192
+ return;
193
+ }
194
+ if (this.dispatchXformVals(acc)) {
195
+ isReduced(acc) && this.done();
196
+ }
197
+ }
198
+ dispatchXformDone() {
199
+ let acc;
200
+ try {
201
+ acc = this.xform[1]([]);
202
+ } catch (e) {
203
+ return this.error(e);
204
+ }
205
+ return this.dispatchXformVals(acc);
206
+ }
207
+ dispatchXformVals(acc) {
208
+ const uacc = unreduced(acc);
209
+ for (let i = 0, n = uacc.length; i < n && this.state < State.DONE; i++) {
210
+ this.dispatch(uacc[i]);
211
+ }
212
+ return this.state < State.ERROR;
213
+ }
214
+ ensureState() {
215
+ if (this.state >= State.DONE) {
216
+ illegalState(`operation not allowed in state ${this.state}`);
217
+ }
218
+ }
219
+ release() {
220
+ this.subs.length = 0;
221
+ delete this.parent;
222
+ delete this.xform;
223
+ delete this.last;
224
+ }
290
225
  }
226
+ export {
227
+ Subscription,
228
+ subscription
229
+ };
package/sync-raf.js CHANGED
@@ -2,73 +2,42 @@ import { isNode } from "@thi.ng/checks/is-node";
2
2
  import { State } from "./api.js";
3
3
  import { __optsWithID } from "./idgen.js";
4
4
  import { Subscription } from "./subscription.js";
5
- /**
6
- * Similar to (in in effect the same as the **now deprecated**)
7
- * {@link sidechainPartitionRAF}, however more performant & lightweight.
8
- * Synchronizes downstream processing w/ `requestAnimationFrame()`. The returned
9
- * subscription delays & debounces any high frequency intra-frame input values
10
- * and passes only most recent one downstream during next RAF event processing.
11
- *
12
- * This example uses thi.ng/atom as state container. Also see {@link fromAtom}.
13
- *
14
- * See {@link sidechainTrigger} from a similar & more general construct.
15
- *
16
- * @example
17
- * ```ts
18
- * const atom = defAtom("alice");
19
- *
20
- * // any changes to the atom will only be received by this subscription
21
- * // during next RAF update cycle
22
- * syncRAF(fromAtom(atom)).subscribe({
23
- * next({ name }) { document.body.innerText = name; }
24
- * });
25
- *
26
- * // trigger update
27
- * atom.reset("bob");
28
- * ```
29
- *
30
- * @param src -
31
- * @param opts -
32
- */
33
- export const syncRAF = (src, opts) => src.subscribe(new SyncRAF(__optsWithID(`syncraf-${src.id}`, opts)));
34
- /**
35
- * See {@link syncRAF} for details.
36
- */
37
- export class SyncRAF extends Subscription {
38
- queued;
39
- raf;
40
- constructor(opts) {
41
- super(undefined, opts);
42
- }
43
- next(x) {
44
- if (this.state >= State.DONE)
45
- return;
46
- this.queued = x;
47
- if (!this.raf) {
48
- const update = () => {
49
- if (this.state < State.DONE)
50
- super.next(this.queued);
51
- this._clean();
52
- };
53
- this.raf = isNode()
54
- ? setTimeout(update, 16)
55
- : requestAnimationFrame(update);
56
- }
57
- }
58
- done() {
59
- this._clean();
60
- super.done();
61
- }
62
- error(e) {
5
+ const syncRAF = (src, opts) => src.subscribe(new SyncRAF(__optsWithID(`syncraf-${src.id}`, opts)));
6
+ class SyncRAF extends Subscription {
7
+ queued;
8
+ raf;
9
+ constructor(opts) {
10
+ super(void 0, opts);
11
+ }
12
+ next(x) {
13
+ if (this.state >= State.DONE)
14
+ return;
15
+ this.queued = x;
16
+ if (!this.raf) {
17
+ const update = () => {
18
+ if (this.state < State.DONE)
19
+ super.next(this.queued);
63
20
  this._clean();
64
- return super.error(e);
21
+ };
22
+ this.raf = isNode() ? setTimeout(update, 16) : requestAnimationFrame(update);
65
23
  }
66
- _clean() {
67
- if (this.raf) {
68
- isNode()
69
- ? clearTimeout(this.raf)
70
- : cancelAnimationFrame(this.raf);
71
- }
72
- this.raf = this.queued = undefined;
24
+ }
25
+ done() {
26
+ this._clean();
27
+ super.done();
28
+ }
29
+ error(e) {
30
+ this._clean();
31
+ return super.error(e);
32
+ }
33
+ _clean() {
34
+ if (this.raf) {
35
+ isNode() ? clearTimeout(this.raf) : cancelAnimationFrame(this.raf);
73
36
  }
37
+ this.raf = this.queued = void 0;
38
+ }
74
39
  }
40
+ export {
41
+ SyncRAF,
42
+ syncRAF
43
+ };