@mikrojs/native 0.20.1 → 0.21.0-next.20260913195055
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/CMakeLists.txt +3 -1
- package/cmake/mikrojs_bytecode.cmake +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/result/native-result.node-shim.js.map +1 -1
- package/dist/runtime/result/types.d.ts +5 -5
- package/dist/runtime/result/types.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/include/mikrojs/mikrojs.h +26 -2
- package/include/mikrojs/ota_env.h +8 -6
- package/include/mikrojs/ota_policy.h +16 -0
- package/include/mikrojs/private.h +13 -0
- package/include/mikrojs/utils.h +24 -0
- package/package.json +10 -10
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/gpio/types.ts +116 -0
- package/runtime/i2c/types.ts +25 -31
- package/runtime/i2s/types.ts +23 -12
- package/runtime/internal.d.ts +1 -174
- package/runtime/kv/shared.ts +13 -11
- package/runtime/neopixel/types.ts +24 -6
- package/runtime/observable/native-observable.node-shim.ts +73 -37
- package/runtime/observable/native-operators.node-shim.ts +417 -0
- package/runtime/observable/observable.ts +76 -1
- package/runtime/observable/operators.ts +156 -89
- package/runtime/observable/types.ts +61 -6
- package/runtime/pwm/types.ts +31 -11
- package/runtime/result/native-result.node-shim.ts +1 -1
- package/runtime/result/types.ts +5 -5
- package/runtime/sleep/types.ts +10 -8
- package/runtime/spi/types.ts +16 -14
- package/runtime/uart/types.ts +27 -15
- package/src/gpio_claim.cpp +63 -0
- package/src/mik_abort.cpp +1 -2
- package/src/mik_console.cpp +192 -115
- package/src/mik_http_client.cpp +3 -14
- package/src/mik_inspect.cpp +86 -9
- package/src/mik_observable.cpp +1028 -80
- package/src/mik_ota_client.cpp +5 -2
- package/src/mik_ota_policy.cpp +65 -18
- package/src/mik_result.cpp +10 -9
- package/src/mikrojs.cpp +26 -13
- package/src/modules.cpp +17 -6
- package/src/timers.cpp +6 -2
- package/src/utils.cpp +106 -0
- package/runtime/i2c/i2c.ts +0 -35
- package/runtime/i2s/i2s.ts +0 -46
- package/runtime/neopixel/neopixel.ts +0 -38
- package/runtime/pin/pin.ts +0 -51
- package/runtime/pin/types.ts +0 -49
- package/runtime/pwm/pwm.ts +0 -32
- package/runtime/spi/spi.ts +0 -31
- package/runtime/uart/uart.ts +0 -52
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
// Host-side shim for `mikro/observable/operators`, used only in vitest (Node)
|
|
2
|
+
// where the mikrojs C runtime isn't available. Keep in sync with the
|
|
3
|
+
// operators section of mik_observable.cpp: same argument checks, same
|
|
4
|
+
// subscription order, same completion rules.
|
|
5
|
+
|
|
6
|
+
import {Observable} from './native-observable.node-shim.js'
|
|
7
|
+
import type {Subscriber} from './types.js'
|
|
8
|
+
|
|
9
|
+
type Obs = Observable<unknown, unknown>
|
|
10
|
+
type Sub = Subscriber<unknown>
|
|
11
|
+
type Op = (source: Obs) => Obs
|
|
12
|
+
type Fn = (...args: unknown[]) => unknown
|
|
13
|
+
type EdgeOptions = {leading?: boolean; trailing?: boolean}
|
|
14
|
+
|
|
15
|
+
function isObservable(value: unknown): value is Obs {
|
|
16
|
+
return value instanceof Observable
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requireFunction(name: string, fn: unknown): Fn {
|
|
20
|
+
if (typeof fn !== 'function') throw new TypeError(`${name}: argument must be a function`)
|
|
21
|
+
return fn as Fn
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function requireCount(name: string, count: unknown): number {
|
|
25
|
+
if (typeof count !== 'number') throw new TypeError(`${name}: count must be a number`)
|
|
26
|
+
/* Non-finite or huge values saturate, NaN counts as 0. */
|
|
27
|
+
return count > 0 ? Math.min(count, 0x7fffffff) : 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requireObservable(name: string, source: unknown): Obs {
|
|
31
|
+
if (!isObservable(source)) throw new TypeError(`${name}: source must be an Observable`)
|
|
32
|
+
return source
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Subscribe `sub` to `source`: values go to `next`, completion to `complete`
|
|
36
|
+
* (by default completing `sub`), and the upstream ends with `sub`. */
|
|
37
|
+
function forward(
|
|
38
|
+
source: Obs,
|
|
39
|
+
sub: Sub,
|
|
40
|
+
next: (value: unknown) => void,
|
|
41
|
+
complete: () => void = () => sub.complete(),
|
|
42
|
+
): void {
|
|
43
|
+
Observable.forward(source, sub, {next, complete})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* An operator: `setup` runs per subscription with the checked source. */
|
|
47
|
+
function operator(name: string, setup: (source: Obs, sub: Sub) => void): Op {
|
|
48
|
+
return (source) => {
|
|
49
|
+
const checked = requireObservable(name, source)
|
|
50
|
+
return new Observable((sub) => setup(checked, sub))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function map(fn: unknown): Op {
|
|
55
|
+
const project = requireFunction('map', fn)
|
|
56
|
+
return operator('map', (source, sub) => forward(source, sub, (value) => sub.next(project(value))))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function filter(fn: unknown): Op {
|
|
60
|
+
const predicate = requireFunction('filter', fn)
|
|
61
|
+
return operator('filter', (source, sub) =>
|
|
62
|
+
forward(source, sub, (value) => {
|
|
63
|
+
if (predicate(value)) sub.next(value)
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function tap(fn: unknown): Op {
|
|
69
|
+
const effect = requireFunction('tap', fn)
|
|
70
|
+
return operator('tap', (source, sub) =>
|
|
71
|
+
forward(source, sub, (value) => {
|
|
72
|
+
effect(value)
|
|
73
|
+
sub.next(value)
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function take(count: unknown): Op {
|
|
79
|
+
const limit = requireCount('take', count)
|
|
80
|
+
return operator('take', (source, sub) => {
|
|
81
|
+
if (limit === 0) {
|
|
82
|
+
sub.complete()
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
let remaining = limit
|
|
86
|
+
forward(source, sub, (value) => {
|
|
87
|
+
if (remaining <= 0) return
|
|
88
|
+
remaining--
|
|
89
|
+
sub.next(value)
|
|
90
|
+
if (remaining === 0) sub.complete()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function skip(count: unknown): Op {
|
|
96
|
+
const limit = requireCount('skip', count)
|
|
97
|
+
return operator('skip', (source, sub) => {
|
|
98
|
+
let remaining = limit
|
|
99
|
+
forward(source, sub, (value) => {
|
|
100
|
+
if (remaining > 0) {
|
|
101
|
+
remaining--
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
sub.next(value)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function scan(fn: unknown, seed: unknown): Op {
|
|
110
|
+
const step = requireFunction('scan', fn)
|
|
111
|
+
return operator('scan', (source, sub) => {
|
|
112
|
+
let acc = seed
|
|
113
|
+
forward(source, sub, (value) => {
|
|
114
|
+
acc = step(acc, value)
|
|
115
|
+
sub.next(acc)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function distinctUntilChanged(fn?: unknown): Op {
|
|
121
|
+
const equals =
|
|
122
|
+
fn === undefined
|
|
123
|
+
? (a: unknown, b: unknown) => a === b
|
|
124
|
+
: requireFunction('distinctUntilChanged', fn)
|
|
125
|
+
return operator('distinctUntilChanged', (source, sub) => {
|
|
126
|
+
let has = false
|
|
127
|
+
let last: unknown
|
|
128
|
+
forward(source, sub, (value) => {
|
|
129
|
+
if (has && equals(last, value)) return
|
|
130
|
+
has = true
|
|
131
|
+
last = value
|
|
132
|
+
sub.next(value)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function startWith(value: unknown): Op {
|
|
138
|
+
return operator('startWith', (source, sub) => {
|
|
139
|
+
sub.next(value)
|
|
140
|
+
if (!sub.closed) forward(source, sub, (v) => sub.next(v))
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function finalize(fn: unknown): Op {
|
|
145
|
+
const teardown = requireFunction('finalize', fn) as () => void
|
|
146
|
+
return operator('finalize', (source, sub) => {
|
|
147
|
+
/* Registered first, so it runs after the upstream unsubscribe. */
|
|
148
|
+
sub.addTeardown(teardown)
|
|
149
|
+
forward(source, sub, (value) => sub.next(value))
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function takeUntil(until: unknown, options?: {inclusive?: boolean}): Op {
|
|
154
|
+
if (typeof until === 'function') {
|
|
155
|
+
const predicate = until as Fn
|
|
156
|
+
const inclusive = options?.inclusive ?? true
|
|
157
|
+
return operator('takeUntil', (source, sub) =>
|
|
158
|
+
forward(source, sub, (value) => {
|
|
159
|
+
const stop = predicate(value)
|
|
160
|
+
if (!stop || inclusive) sub.next(value)
|
|
161
|
+
if (stop) sub.complete()
|
|
162
|
+
}),
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
if (!isObservable(until)) {
|
|
166
|
+
throw new TypeError('takeUntil: argument must be an Observable or a predicate')
|
|
167
|
+
}
|
|
168
|
+
return operator('takeUntil', (source, sub) => {
|
|
169
|
+
/* Notifier first, as in RxJS: one that fires during its own subscribe
|
|
170
|
+
* ends the stream before the source is subscribed. */
|
|
171
|
+
forward(
|
|
172
|
+
until,
|
|
173
|
+
sub,
|
|
174
|
+
() => sub.complete(),
|
|
175
|
+
() => {},
|
|
176
|
+
)
|
|
177
|
+
forward(source, sub, (value) => sub.next(value))
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function mergeWith(...others: unknown[]): Op {
|
|
182
|
+
for (const other of others) {
|
|
183
|
+
if (!isObservable(other)) throw new TypeError('mergeWith: arguments must be Observables')
|
|
184
|
+
}
|
|
185
|
+
return operator('mergeWith', (source, sub) => {
|
|
186
|
+
let open = others.length + 1
|
|
187
|
+
const complete = () => {
|
|
188
|
+
if (--open === 0) sub.complete()
|
|
189
|
+
}
|
|
190
|
+
for (const stream of [source, ...(others as Obs[])]) {
|
|
191
|
+
forward(stream, sub, (value) => sub.next(value), complete)
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function withLatestFrom(other: unknown): Op {
|
|
197
|
+
if (!isObservable(other)) throw new TypeError('withLatestFrom: arguments must be Observables')
|
|
198
|
+
return operator('withLatestFrom', (source, sub) => {
|
|
199
|
+
let has = false
|
|
200
|
+
let latest: unknown
|
|
201
|
+
forward(
|
|
202
|
+
other,
|
|
203
|
+
sub,
|
|
204
|
+
(value) => {
|
|
205
|
+
has = true
|
|
206
|
+
latest = value
|
|
207
|
+
},
|
|
208
|
+
() => {},
|
|
209
|
+
)
|
|
210
|
+
forward(source, sub, (value) => {
|
|
211
|
+
if (has) sub.next([value, latest])
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function combineLatest(sources: unknown): Obs {
|
|
217
|
+
if (!Array.isArray(sources) || !sources.every(isObservable)) {
|
|
218
|
+
throw new TypeError('combineLatest: argument must be an array of Observables')
|
|
219
|
+
}
|
|
220
|
+
return new Observable((sub) => {
|
|
221
|
+
const values = new Array<unknown>(sources.length)
|
|
222
|
+
const seen = new Array<boolean>(sources.length).fill(false)
|
|
223
|
+
let missing = sources.length
|
|
224
|
+
let open = sources.length
|
|
225
|
+
if (open === 0) {
|
|
226
|
+
sub.next([])
|
|
227
|
+
sub.complete()
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
sources.forEach((source, i) => {
|
|
231
|
+
/* A source that completed without a value already closed `sub`. */
|
|
232
|
+
if (sub.closed) return
|
|
233
|
+
forward(
|
|
234
|
+
source,
|
|
235
|
+
sub,
|
|
236
|
+
(value) => {
|
|
237
|
+
values[i] = value
|
|
238
|
+
if (!seen[i]) {
|
|
239
|
+
seen[i] = true
|
|
240
|
+
missing--
|
|
241
|
+
}
|
|
242
|
+
if (missing === 0) sub.next(values.slice())
|
|
243
|
+
},
|
|
244
|
+
() => {
|
|
245
|
+
if (!seen[i] || --open === 0) sub.complete()
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function pipe(...ops: unknown[]): Op {
|
|
253
|
+
for (const op of ops) {
|
|
254
|
+
if (typeof op !== 'function') throw new TypeError('pipe: arguments must be operator functions')
|
|
255
|
+
}
|
|
256
|
+
return (source) => source.pipe(...(ops as Op[]))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function switchMap(fn: unknown): Op {
|
|
260
|
+
const project = requireFunction('switchMap', fn)
|
|
261
|
+
return operator('switchMap', (source, sub) => {
|
|
262
|
+
let inner: {close(): void} | undefined
|
|
263
|
+
let innerOpen = false
|
|
264
|
+
let sourceDone = false
|
|
265
|
+
/* Registered first, like the native state teardown: the inner ends after
|
|
266
|
+
* the source upstream. */
|
|
267
|
+
sub.addTeardown(() => inner?.close())
|
|
268
|
+
forward(
|
|
269
|
+
source,
|
|
270
|
+
sub,
|
|
271
|
+
(value) => {
|
|
272
|
+
inner?.close()
|
|
273
|
+
const stream = project(value)
|
|
274
|
+
if (!isObservable(stream)) {
|
|
275
|
+
throw new TypeError('switchMap: project must return an Observable')
|
|
276
|
+
}
|
|
277
|
+
innerOpen = true
|
|
278
|
+
const started = Observable.start(stream, {
|
|
279
|
+
next: (v: unknown) => sub.next(v),
|
|
280
|
+
complete: () => {
|
|
281
|
+
/* A queued completion of an inner already switched away drains
|
|
282
|
+
* here too; only the current inner's completion counts. */
|
|
283
|
+
if (inner !== started) return
|
|
284
|
+
innerOpen = false
|
|
285
|
+
if (sourceDone) sub.complete()
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
inner = started
|
|
289
|
+
},
|
|
290
|
+
() => {
|
|
291
|
+
sourceDone = true
|
|
292
|
+
if (!innerOpen) sub.complete()
|
|
293
|
+
},
|
|
294
|
+
)
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function requireDuration(name: string, what: string, ms: unknown): number {
|
|
299
|
+
if (typeof ms !== 'number') throw new TypeError(`${name}: ${what} must be a number`)
|
|
300
|
+
return ms > 0 ? Math.min(ms, 0x7fffffff) : 0
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function timer(delayMs: unknown, periodMs?: unknown): Obs {
|
|
304
|
+
if (typeof delayMs !== 'number' || (periodMs !== undefined && typeof periodMs !== 'number')) {
|
|
305
|
+
throw new TypeError('timer: delay and period must be numbers')
|
|
306
|
+
}
|
|
307
|
+
return new Observable((sub) => {
|
|
308
|
+
let count = 0
|
|
309
|
+
let id = setTimeout(() => {
|
|
310
|
+
sub.next(count++)
|
|
311
|
+
/* The first value may have closed the chain (take(1), firstValueFrom):
|
|
312
|
+
* the teardown has already run, so an interval opened now would leak. */
|
|
313
|
+
if (sub.closed) return
|
|
314
|
+
if (periodMs === undefined) {
|
|
315
|
+
sub.complete()
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
id = setInterval(() => sub.next(count++), periodMs)
|
|
319
|
+
}, delayMs)
|
|
320
|
+
/* Timeouts and intervals share one id space and one clear function. */
|
|
321
|
+
sub.addTeardown(() => clearInterval(id))
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function edges(
|
|
326
|
+
options: unknown,
|
|
327
|
+
leading: boolean,
|
|
328
|
+
trailing: boolean,
|
|
329
|
+
): {leading: boolean; trailing: boolean} {
|
|
330
|
+
if (typeof options !== 'object' || options === null) return {leading, trailing}
|
|
331
|
+
const given = options as EdgeOptions
|
|
332
|
+
return {leading: given.leading ?? leading, trailing: given.trailing ?? trailing}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function debounceTime(ms: unknown, options?: unknown): Op {
|
|
336
|
+
const window = requireDuration('debounceTime', 'duration', ms)
|
|
337
|
+
const {leading, trailing} = edges(options, false, true)
|
|
338
|
+
return operator('debounceTime', (source, sub) => {
|
|
339
|
+
let id: ReturnType<typeof setTimeout> | undefined
|
|
340
|
+
let pending = false
|
|
341
|
+
let last: unknown
|
|
342
|
+
const flush = () => {
|
|
343
|
+
id = undefined
|
|
344
|
+
if (!pending) return
|
|
345
|
+
pending = false
|
|
346
|
+
sub.next(last)
|
|
347
|
+
}
|
|
348
|
+
sub.addTeardown(() => {
|
|
349
|
+
if (id !== undefined) clearTimeout(id)
|
|
350
|
+
})
|
|
351
|
+
forward(
|
|
352
|
+
source,
|
|
353
|
+
sub,
|
|
354
|
+
(value) => {
|
|
355
|
+
const quiet = id === undefined
|
|
356
|
+
if (id !== undefined) clearTimeout(id)
|
|
357
|
+
id = setTimeout(flush, window)
|
|
358
|
+
if (quiet && leading) {
|
|
359
|
+
sub.next(value)
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
last = value
|
|
363
|
+
pending = trailing
|
|
364
|
+
},
|
|
365
|
+
() => {
|
|
366
|
+
if (id !== undefined) clearTimeout(id)
|
|
367
|
+
flush()
|
|
368
|
+
sub.complete()
|
|
369
|
+
},
|
|
370
|
+
)
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function throttleTime(ms: unknown, options?: unknown): Op {
|
|
375
|
+
const window = requireDuration('throttleTime', 'duration', ms)
|
|
376
|
+
const {leading, trailing} = edges(options, true, false)
|
|
377
|
+
return operator('throttleTime', (source, sub) => {
|
|
378
|
+
let id: ReturnType<typeof setTimeout> | undefined
|
|
379
|
+
let pending = false
|
|
380
|
+
let last: unknown
|
|
381
|
+
const open = () => {
|
|
382
|
+
id = setTimeout(() => {
|
|
383
|
+
id = undefined
|
|
384
|
+
if (!pending) return
|
|
385
|
+
pending = false
|
|
386
|
+
sub.next(last)
|
|
387
|
+
if (!sub.closed) open()
|
|
388
|
+
}, window)
|
|
389
|
+
}
|
|
390
|
+
sub.addTeardown(() => {
|
|
391
|
+
if (id !== undefined) clearTimeout(id)
|
|
392
|
+
})
|
|
393
|
+
forward(
|
|
394
|
+
source,
|
|
395
|
+
sub,
|
|
396
|
+
(value) => {
|
|
397
|
+
if (id !== undefined) {
|
|
398
|
+
last = value
|
|
399
|
+
pending = trailing
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
if (leading) {
|
|
403
|
+
sub.next(value)
|
|
404
|
+
} else {
|
|
405
|
+
last = value
|
|
406
|
+
pending = trailing
|
|
407
|
+
}
|
|
408
|
+
if (!sub.closed) open()
|
|
409
|
+
},
|
|
410
|
+
() => {
|
|
411
|
+
if (id !== undefined) clearTimeout(id)
|
|
412
|
+
if (pending) sub.next(last)
|
|
413
|
+
sub.complete()
|
|
414
|
+
},
|
|
415
|
+
)
|
|
416
|
+
})
|
|
417
|
+
}
|
|
@@ -1 +1,76 @@
|
|
|
1
|
-
|
|
1
|
+
import {from, Observable, of} from 'native:mikro/observable'
|
|
2
|
+
|
|
3
|
+
import type {Subscription} from './types.js'
|
|
4
|
+
|
|
5
|
+
export {from, Observable, of}
|
|
6
|
+
|
|
7
|
+
/* Resolve with the first value, or with undefined if the source completes
|
|
8
|
+
* without one. Unsubscribes as soon as it settles. */
|
|
9
|
+
export function firstValueFrom<T>(source: Observable<T>): Promise<T | undefined> {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
/* Assigned after subscribe returns; a synchronous source settles before that. */
|
|
12
|
+
let sub: Subscription | undefined = undefined
|
|
13
|
+
let settled = false
|
|
14
|
+
const settle = (value: T | undefined) => {
|
|
15
|
+
if (settled) return
|
|
16
|
+
settled = true
|
|
17
|
+
resolve(value)
|
|
18
|
+
sub?.unsubscribe()
|
|
19
|
+
}
|
|
20
|
+
sub = source.subscribe({next: settle, complete: () => settle(undefined)})
|
|
21
|
+
if (settled) sub.unsubscribe()
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* withEmitters plus replay of the latest value to each new subscriber. */
|
|
26
|
+
function replayLatest<T>() {
|
|
27
|
+
const live = Observable.withEmitters<T>()
|
|
28
|
+
let hasValue = false
|
|
29
|
+
let done = false
|
|
30
|
+
let value: T
|
|
31
|
+
const observable = new Observable<T>((sub) => {
|
|
32
|
+
if (hasValue) sub.next(value)
|
|
33
|
+
if (sub.closed) return
|
|
34
|
+
const upstream = live.observable.subscribe({
|
|
35
|
+
next: (v) => sub.next(v),
|
|
36
|
+
complete: () => sub.complete(),
|
|
37
|
+
})
|
|
38
|
+
sub.addTeardown(() => upstream.unsubscribe())
|
|
39
|
+
})
|
|
40
|
+
return {
|
|
41
|
+
observable,
|
|
42
|
+
next: (v: T) => {
|
|
43
|
+
if (done) return
|
|
44
|
+
hasValue = true
|
|
45
|
+
value = v
|
|
46
|
+
live.next(v)
|
|
47
|
+
},
|
|
48
|
+
complete: () => {
|
|
49
|
+
done = true
|
|
50
|
+
live.complete()
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* A state container. Values pushed through `next` run through `pipeline`;
|
|
56
|
+
* the result is what subscribers see, current value first. The pipeline
|
|
57
|
+
* provides the initial value (startWith) and can fold inputs (scan).
|
|
58
|
+
* `complete` stops everything: the pipeline is unsubscribed, merged sources
|
|
59
|
+
* included, and the output completes. */
|
|
60
|
+
export function state<In, Out>(
|
|
61
|
+
pipeline: (source: Observable<In>) => Observable<Out>,
|
|
62
|
+
): readonly [value: Observable<Out>, set: (value: In) => void, complete: () => void] {
|
|
63
|
+
const input = Observable.withEmitters<In>()
|
|
64
|
+
const output = replayLatest<Out>()
|
|
65
|
+
const run = pipeline(input.observable).subscribe({next: output.next, complete: output.complete})
|
|
66
|
+
/* Writes after the container ends are dropped, as everywhere in the stream
|
|
67
|
+
* protocol: complete() closes the input, and a pipeline that ended on its
|
|
68
|
+
* own has already unsubscribed from it. A runaway source writing into a
|
|
69
|
+
* dead container shows up as a timer or subscription leak, not here. */
|
|
70
|
+
const complete = () => {
|
|
71
|
+
input.complete()
|
|
72
|
+
run.unsubscribe()
|
|
73
|
+
output.complete()
|
|
74
|
+
}
|
|
75
|
+
return [output.observable, input.next, complete]
|
|
76
|
+
}
|