@mikrojs/native 0.5.1 → 0.6.0-pr-70.gc88e298
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/include/mikrojs/private.h +3 -0
- package/package.json +4 -2
- 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/ble/ble.ts +16 -8
- package/runtime/ble/types.ts +7 -14
- package/runtime/internal.d.ts +4 -0
- package/runtime/observable/native-observable.node-shim.ts +212 -0
- package/runtime/observable/observable.ts +1 -0
- package/runtime/observable/operators.ts +129 -0
- package/runtime/observable/types.ts +80 -0
- package/runtime/udp/types.ts +15 -2
- package/runtime/udp/udp.ts +45 -9
- package/runtime/wifi/types.ts +6 -15
- package/runtime/wifi/wifi.ts +24 -16
- package/src/mik_observable.cpp +882 -0
- package/src/mikrojs.cpp +1 -0
package/CMakeLists.txt
CHANGED
|
@@ -50,6 +50,7 @@ set(MIKROJS_CORE_SOURCES
|
|
|
50
50
|
src/mik_repl.cpp
|
|
51
51
|
src/mik_app_config.cpp
|
|
52
52
|
src/mik_udp.cpp
|
|
53
|
+
src/mik_observable.cpp
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
add_library(mikrojs STATIC
|
|
@@ -80,7 +81,7 @@ endif()
|
|
|
80
81
|
include(cmake/mikrojs_bytecode.cmake)
|
|
81
82
|
mikrojs_generate_bytecode(
|
|
82
83
|
RUNTIME_DIR "${CMAKE_CURRENT_SOURCE_DIR}/runtime"
|
|
83
|
-
MODULES cbor env result schema fs http/helpers http/request i2c kv/nvs kv/rtc kv/shared neopixel pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi
|
|
84
|
+
MODULES cbor env result schema fs http/helpers http/request i2c kv/nvs kv/rtc kv/shared neopixel observable observable/operators pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi
|
|
84
85
|
MODULE_PREFIX "mikrojs"
|
|
85
86
|
SYMBOL_PREFIX "mikrojs"
|
|
86
87
|
TARGET gen_bytecode
|
|
@@ -161,6 +162,7 @@ if(BUILD_TESTING)
|
|
|
161
162
|
test/stream_test.cpp
|
|
162
163
|
test/runtime_recycle_test.cpp
|
|
163
164
|
test/udp_test.cpp
|
|
165
|
+
test/observable_test.cpp
|
|
164
166
|
)
|
|
165
167
|
|
|
166
168
|
target_link_libraries(mikrojs_tests PRIVATE mikrojs)
|
|
@@ -215,6 +215,9 @@ JSModuleDef* mik__result_init(JSContext* ctx);
|
|
|
215
215
|
/* UDP module (mik_udp.cpp) */
|
|
216
216
|
JSModuleDef* mik__udp_init(JSContext* ctx);
|
|
217
217
|
|
|
218
|
+
/* Observable module (mik_observable.cpp) */
|
|
219
|
+
JSModuleDef* mik__observable_init(JSContext* ctx);
|
|
220
|
+
|
|
218
221
|
bool mik__repl_is_evaluating(void);
|
|
219
222
|
|
|
220
223
|
/* REPL protocol mode (mik_repl.cpp) — used by mik_console.cpp, mik_stdio.cpp */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikrojs/native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-pr-70.gc88e298",
|
|
4
4
|
"description": "Mikro.js C++ runtime library and Node.js native addon",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esp32",
|
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
"./runtime/kv/shared": "./runtime/kv/shared.ts",
|
|
57
57
|
"./runtime/kv/types": "./runtime/kv/types.ts",
|
|
58
58
|
"./runtime/neopixel/types": "./runtime/neopixel/types.ts",
|
|
59
|
+
"./runtime/observable/operators": "./runtime/observable/operators.ts",
|
|
60
|
+
"./runtime/observable/types": "./runtime/observable/types.ts",
|
|
59
61
|
"./runtime/pin/types": "./runtime/pin/types.ts",
|
|
60
62
|
"./runtime/pwm/types": "./runtime/pwm/types.ts",
|
|
61
63
|
"./runtime/reader/types": "./runtime/reader/types.ts",
|
|
@@ -76,7 +78,7 @@
|
|
|
76
78
|
"cmake-js": "^8.0.0",
|
|
77
79
|
"node-addon-api": "^8.7.0",
|
|
78
80
|
"node-gyp-build": "^4.8.4",
|
|
79
|
-
"@mikrojs/quickjs": "0.
|
|
81
|
+
"@mikrojs/quickjs": "0.6.0-pr-70.gc88e298"
|
|
80
82
|
},
|
|
81
83
|
"devDependencies": {
|
|
82
84
|
"@swc/core": "^1.15.30",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/runtime/ble/ble.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {Observable} from 'mikrojs/observable'
|
|
1
2
|
import {err, ok} from 'mikrojs/result'
|
|
2
3
|
import {Ble as NativeBle} from 'native:ble'
|
|
3
4
|
|
|
@@ -9,8 +10,9 @@ import type {
|
|
|
9
10
|
BleError,
|
|
10
11
|
Characteristic,
|
|
11
12
|
CharacteristicProperty,
|
|
13
|
+
ConnectionInfo,
|
|
14
|
+
MtuInfo,
|
|
12
15
|
Peripheral,
|
|
13
|
-
PeripheralEventMap,
|
|
14
16
|
Service,
|
|
15
17
|
} from './types.js'
|
|
16
18
|
import {parseUuid} from './uuid.js'
|
|
@@ -140,6 +142,16 @@ function normalizeServices(services: Service[]) {
|
|
|
140
142
|
|
|
141
143
|
const native = new NativeBle()
|
|
142
144
|
|
|
145
|
+
/* Per-event multicast sources backed by a single native.on registration each.
|
|
146
|
+
* See observable.md → Module integration. */
|
|
147
|
+
const _onConnect = Observable.withEmitters<ConnectionInfo>()
|
|
148
|
+
const _onDisconnect = Observable.withEmitters<ConnectionInfo>()
|
|
149
|
+
const _onMtu = Observable.withEmitters<MtuInfo>()
|
|
150
|
+
|
|
151
|
+
native.on('connect', (info) => _onConnect.next(info as ConnectionInfo))
|
|
152
|
+
native.on('disconnect', (info) => _onDisconnect.next(info as ConnectionInfo))
|
|
153
|
+
native.on('mtu', (info) => _onMtu.next(info as MtuInfo))
|
|
154
|
+
|
|
143
155
|
const ble: Ble = {
|
|
144
156
|
get name(): string {
|
|
145
157
|
return native.getName()
|
|
@@ -219,13 +231,9 @@ const peripheral: Peripheral = {
|
|
|
219
231
|
return ok(handle)
|
|
220
232
|
},
|
|
221
233
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
off<K extends keyof PeripheralEventMap>(event: K, listener: PeripheralEventMap[K]) {
|
|
227
|
-
native.off(event, listener as (...args: unknown[]) => void)
|
|
228
|
-
},
|
|
234
|
+
onConnect: _onConnect.observable,
|
|
235
|
+
onDisconnect: _onDisconnect.observable,
|
|
236
|
+
onMtu: _onMtu.observable,
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
export {ble, peripheral}
|
package/runtime/ble/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {Observable} from '../observable/types.js'
|
|
1
2
|
import type {Result} from '../result/types.js'
|
|
2
3
|
|
|
3
4
|
/** Advertising interval range in milliseconds. */
|
|
@@ -171,23 +172,15 @@ export interface MtuInfo {
|
|
|
171
172
|
mtu: number
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
export interface PeripheralEventMap {
|
|
175
|
-
connect: (info: ConnectionInfo) => void
|
|
176
|
-
disconnect: (info: ConnectionInfo) => void
|
|
177
|
-
mtu: (info: MtuInfo) => void
|
|
178
|
-
}
|
|
179
|
-
|
|
180
175
|
export interface Peripheral {
|
|
181
176
|
/** Start advertising. Returns a handle whose `stop()` ends this session. */
|
|
182
177
|
advertise(options?: AdvertiseOptions): Promise<Result<AdvertiseHandle, BleError>>
|
|
183
|
-
/**
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
/** Remove a previously-registered listener. */
|
|
190
|
-
off<K extends keyof PeripheralEventMap>(event: K, listener: PeripheralEventMap[K]): void
|
|
178
|
+
/** Emits when a central connects. Subscribers are dispatched on the JS loop thread. */
|
|
179
|
+
readonly onConnect: Observable<ConnectionInfo>
|
|
180
|
+
/** Emits when a central disconnects. */
|
|
181
|
+
readonly onDisconnect: Observable<ConnectionInfo>
|
|
182
|
+
/** Emits when a connected central renegotiates its ATT MTU. */
|
|
183
|
+
readonly onMtu: Observable<MtuInfo>
|
|
191
184
|
}
|
|
192
185
|
|
|
193
186
|
export declare const ble: Ble
|
package/runtime/internal.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ declare module 'native:result' {
|
|
|
25
25
|
export function err<E>(error: E): ErrResult<E>
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
declare module 'native:observable' {
|
|
29
|
+
export {Observable} from '@mikrojs/native/runtime/observable/types'
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
declare module 'native:sys' {
|
|
29
33
|
import type {JsMemoryUsage} from './sys/types.js'
|
|
30
34
|
export function evalScript(code: string): Promise<{value: unknown}>
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/* console.error inside dispatch matches the mik_call_handler precedent in
|
|
3
|
+
* the C runtime (log + continue, isolated to that subscriber). */
|
|
4
|
+
|
|
5
|
+
// Host-side shim for `native:observable`, used only in vitest (Node) where
|
|
6
|
+
// the mikrojs C runtime isn't available. Keep in sync with mik_observable.cpp.
|
|
7
|
+
//
|
|
8
|
+
// Mirrors the locked design in .claude/plans/observable.md:
|
|
9
|
+
// - subscribe() returns a Subscription with unsubscribe() (no AbortSignal)
|
|
10
|
+
// - no error channel — throws caught + logged at the dispatch boundary,
|
|
11
|
+
// isolated to that subscriber
|
|
12
|
+
// - sync emission allowed
|
|
13
|
+
// - pipe-only composition (operators live in operators.ts)
|
|
14
|
+
// - withEmitters() factory: {observable, next, complete}
|
|
15
|
+
|
|
16
|
+
class Subscriber<T> {
|
|
17
|
+
closed = false
|
|
18
|
+
private next_fn: ((v: T) => void) | undefined
|
|
19
|
+
private complete_fn: (() => void) | undefined
|
|
20
|
+
private teardowns: Array<() => void> = []
|
|
21
|
+
|
|
22
|
+
constructor(observer: unknown) {
|
|
23
|
+
if (observer == null) return
|
|
24
|
+
if (typeof observer === 'function') {
|
|
25
|
+
this.next_fn = observer as (v: T) => void
|
|
26
|
+
} else if (typeof observer === 'object') {
|
|
27
|
+
const o = observer as {next?: (v: T) => void; complete?: () => void}
|
|
28
|
+
if (typeof o.next === 'function') this.next_fn = o.next
|
|
29
|
+
if (typeof o.complete === 'function') this.complete_fn = o.complete
|
|
30
|
+
} else {
|
|
31
|
+
throw new TypeError('subscribe: observer must be a function, object, undefined, or null')
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
next(value: T): void {
|
|
36
|
+
if (this.closed) return
|
|
37
|
+
if (this.next_fn) {
|
|
38
|
+
try {
|
|
39
|
+
this.next_fn(value)
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(err)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
complete(): void {
|
|
47
|
+
if (this.closed) return
|
|
48
|
+
this.closed = true
|
|
49
|
+
if (this.complete_fn) {
|
|
50
|
+
try {
|
|
51
|
+
this.complete_fn()
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(err)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.runTeardowns()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
addTeardown(fn: () => void): void {
|
|
60
|
+
if (typeof fn !== 'function') {
|
|
61
|
+
throw new TypeError('addTeardown: argument must be a function')
|
|
62
|
+
}
|
|
63
|
+
if (this.closed) {
|
|
64
|
+
try {
|
|
65
|
+
fn()
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(err)
|
|
68
|
+
}
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
this.teardowns.push(fn)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Used by Subscription.unsubscribe — silent (no observer.complete call).
|
|
75
|
+
closeSilently(): void {
|
|
76
|
+
if (this.closed) return
|
|
77
|
+
this.closed = true
|
|
78
|
+
this.runTeardowns()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private runTeardowns(): void {
|
|
82
|
+
const list = this.teardowns
|
|
83
|
+
this.teardowns = []
|
|
84
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
85
|
+
try {
|
|
86
|
+
list[i]!()
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(err)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class Subscription {
|
|
95
|
+
constructor(private subscriber: Subscriber<unknown>) {}
|
|
96
|
+
unsubscribe(): void {
|
|
97
|
+
this.subscriber.closeSilently()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type SubscribeCallback<T> = (sub: Subscriber<T>) => void
|
|
102
|
+
|
|
103
|
+
export class Observable<Ok, Err = never> {
|
|
104
|
+
// The shim doesn't actually use `_phantom` at runtime; the type parameters are
|
|
105
|
+
// purely for type-level alignment with the public interface.
|
|
106
|
+
declare readonly _phantom: [Ok, Err]
|
|
107
|
+
#cb: SubscribeCallback<unknown>
|
|
108
|
+
|
|
109
|
+
constructor(cb: SubscribeCallback<unknown>) {
|
|
110
|
+
if (typeof cb !== 'function') {
|
|
111
|
+
throw new TypeError('Observable: constructor requires a function argument')
|
|
112
|
+
}
|
|
113
|
+
this.#cb = cb
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
subscribe(observer?: unknown): Subscription {
|
|
117
|
+
const sub = new Subscriber<unknown>(observer)
|
|
118
|
+
try {
|
|
119
|
+
this.#cb(sub)
|
|
120
|
+
} catch (err) {
|
|
121
|
+
sub.closeSilently()
|
|
122
|
+
throw err
|
|
123
|
+
}
|
|
124
|
+
return new Subscription(sub)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
pipe(...ops: Array<(o: Observable<unknown, unknown>) => Observable<unknown, unknown>>) {
|
|
128
|
+
let current: Observable<unknown, unknown> = this as unknown as Observable<unknown, unknown>
|
|
129
|
+
for (const op of ops) {
|
|
130
|
+
if (typeof op !== 'function') {
|
|
131
|
+
throw new TypeError('pipe: arguments must be operator functions')
|
|
132
|
+
}
|
|
133
|
+
current = op(current)
|
|
134
|
+
}
|
|
135
|
+
return current
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static from(src: unknown): Observable<unknown, unknown> {
|
|
139
|
+
if (src instanceof Observable) return src
|
|
140
|
+
if (
|
|
141
|
+
src != null &&
|
|
142
|
+
(typeof src === 'object' || typeof src === 'function') &&
|
|
143
|
+
typeof (src as {then?: unknown}).then === 'function'
|
|
144
|
+
) {
|
|
145
|
+
// Promise-shaped
|
|
146
|
+
return new Observable<unknown>((sub) => {
|
|
147
|
+
;(src as PromiseLike<unknown>).then((value) => {
|
|
148
|
+
if (sub.closed) return
|
|
149
|
+
sub.next(value)
|
|
150
|
+
if (sub.closed) return
|
|
151
|
+
sub.complete()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
src != null &&
|
|
157
|
+
(typeof src === 'object' || typeof src === 'string') &&
|
|
158
|
+
typeof (src as {[Symbol.iterator]?: unknown})[Symbol.iterator] === 'function'
|
|
159
|
+
) {
|
|
160
|
+
return new Observable<unknown>((sub) => {
|
|
161
|
+
for (const value of src as Iterable<unknown>) {
|
|
162
|
+
if (sub.closed) return
|
|
163
|
+
sub.next(value)
|
|
164
|
+
}
|
|
165
|
+
if (!sub.closed) sub.complete()
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
throw new TypeError('Observable.from: source must be a Promise, Iterable, or Observable')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static withEmitters<Ok, Err = never>(): {
|
|
172
|
+
observable: Observable<Ok, Err>
|
|
173
|
+
next: (value: unknown) => void
|
|
174
|
+
complete: () => void
|
|
175
|
+
} {
|
|
176
|
+
const subs: Array<Subscriber<unknown>> = []
|
|
177
|
+
let completed = false
|
|
178
|
+
|
|
179
|
+
const observable = new Observable<unknown>((sub) => {
|
|
180
|
+
if (completed) {
|
|
181
|
+
sub.complete()
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
subs.push(sub)
|
|
185
|
+
sub.addTeardown(() => {
|
|
186
|
+
const i = subs.indexOf(sub)
|
|
187
|
+
if (i >= 0) subs.splice(i, 1)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const next = (value: unknown) => {
|
|
192
|
+
if (completed) return
|
|
193
|
+
// Snapshot to be resilient against mid-dispatch unsubscribes.
|
|
194
|
+
const snapshot = subs.slice()
|
|
195
|
+
for (const s of snapshot) {
|
|
196
|
+
if (!s.closed) s.next(value)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const complete = () => {
|
|
201
|
+
if (completed) return
|
|
202
|
+
completed = true
|
|
203
|
+
const snapshot = subs.slice()
|
|
204
|
+
subs.length = 0
|
|
205
|
+
for (const s of snapshot) {
|
|
206
|
+
if (!s.closed) s.complete()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {observable: observable as unknown as Observable<Ok, Err>, next, complete}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {Observable} from 'native:observable'
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/* console.error inside operator dispatch is intentional: it matches the
|
|
3
|
+
* existing mik_call_handler precedent in the C layer (log + continue rather
|
|
4
|
+
* than panic). See .claude/plans/observable.md → Errors section. */
|
|
5
|
+
|
|
6
|
+
/* Operators for `Observable.pipe(...)`. Each is a factory returning a
|
|
7
|
+
* function `(source) => Observable`. Composition is pure pipe — no method
|
|
8
|
+
* chaining on Observable itself.
|
|
9
|
+
*
|
|
10
|
+
* M0 scope: operators apply to non-fallible streams (`Err = never`). For
|
|
11
|
+
* fallible streams (`Observable<Ok, Err>` with Err != never), corresponding
|
|
12
|
+
* Result-aware operators (`mapOk`, `filterOk`, ...) ship when a concrete
|
|
13
|
+
* consumer asks. Today no module produces fallible event streams.
|
|
14
|
+
*
|
|
15
|
+
* See `.claude/plans/observable.md` for the full design.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/* Importing from `mikrojs/observable` (not `native:observable`) so this
|
|
19
|
+
* module typechecks from any package that already re-exports it. At runtime
|
|
20
|
+
* `mikrojs/observable` resolves to the bytecode bundle which re-exports the
|
|
21
|
+
* native class; at typecheck it resolves to the ambient `declare class` in
|
|
22
|
+
* runtime/observable/types.ts. */
|
|
23
|
+
import {Observable} from 'mikrojs/observable'
|
|
24
|
+
|
|
25
|
+
/* Map values through a transform. Throws inside `fn` are caught at the
|
|
26
|
+
* dispatch boundary; the bad value is dropped for that subscription, others
|
|
27
|
+
* are unaffected. */
|
|
28
|
+
export const map =
|
|
29
|
+
<A, B>(fn: (value: A) => B) =>
|
|
30
|
+
(source: Observable<A>): Observable<B> =>
|
|
31
|
+
new Observable<B>((sub) => {
|
|
32
|
+
const upstream = source.subscribe({
|
|
33
|
+
next: (value) => {
|
|
34
|
+
let next: B
|
|
35
|
+
try {
|
|
36
|
+
next = fn(value)
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error(err)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
sub.next(next)
|
|
42
|
+
},
|
|
43
|
+
complete: () => sub.complete(),
|
|
44
|
+
})
|
|
45
|
+
sub.addTeardown(() => upstream.unsubscribe())
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
/* Pass through values matching `pred`. `pred` errors are caught + logged. */
|
|
49
|
+
export const filter =
|
|
50
|
+
<A>(pred: (value: A) => boolean) =>
|
|
51
|
+
(source: Observable<A>): Observable<A> =>
|
|
52
|
+
new Observable<A>((sub) => {
|
|
53
|
+
const upstream = source.subscribe({
|
|
54
|
+
next: (value) => {
|
|
55
|
+
let keep: boolean
|
|
56
|
+
try {
|
|
57
|
+
keep = pred(value)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error(err)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
if (keep) sub.next(value)
|
|
63
|
+
},
|
|
64
|
+
complete: () => sub.complete(),
|
|
65
|
+
})
|
|
66
|
+
sub.addTeardown(() => upstream.unsubscribe())
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
/* Take at most n values, then complete. n <= 0 completes immediately. */
|
|
70
|
+
export const take =
|
|
71
|
+
(n: number) =>
|
|
72
|
+
<A>(source: Observable<A>): Observable<A> =>
|
|
73
|
+
new Observable<A>((sub) => {
|
|
74
|
+
if (n <= 0) {
|
|
75
|
+
sub.complete()
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
let remaining = n
|
|
79
|
+
const upstream = source.subscribe({
|
|
80
|
+
next: (value) => {
|
|
81
|
+
if (remaining <= 0) return
|
|
82
|
+
remaining--
|
|
83
|
+
sub.next(value)
|
|
84
|
+
if (remaining === 0) sub.complete()
|
|
85
|
+
},
|
|
86
|
+
complete: () => sub.complete(),
|
|
87
|
+
})
|
|
88
|
+
sub.addTeardown(() => upstream.unsubscribe())
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/* Stop emitting when `notifier` emits its first value. Notifier completing
|
|
92
|
+
* without emitting is NOT a trigger — primary keeps going. */
|
|
93
|
+
export const takeUntil =
|
|
94
|
+
(notifier: Observable<unknown, unknown>) =>
|
|
95
|
+
<A>(source: Observable<A>): Observable<A> =>
|
|
96
|
+
new Observable<A>((sub) => {
|
|
97
|
+
const upstream = source.subscribe({
|
|
98
|
+
next: (value) => sub.next(value),
|
|
99
|
+
complete: () => sub.complete(),
|
|
100
|
+
})
|
|
101
|
+
const notifierSub = notifier.subscribe({
|
|
102
|
+
next: () => sub.complete(),
|
|
103
|
+
})
|
|
104
|
+
sub.addTeardown(() => {
|
|
105
|
+
notifierSub.unsubscribe()
|
|
106
|
+
upstream.unsubscribe()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
/* Run `fn` when the subscription ends for any reason (unsubscribe or
|
|
111
|
+
* natural completion). Throws inside `fn` are caught + logged so other
|
|
112
|
+
* teardowns still run. RxJS naming. */
|
|
113
|
+
export const finalize =
|
|
114
|
+
(fn: () => void) =>
|
|
115
|
+
<A>(source: Observable<A>): Observable<A> =>
|
|
116
|
+
new Observable<A>((sub) => {
|
|
117
|
+
const upstream = source.subscribe({
|
|
118
|
+
next: (value) => sub.next(value),
|
|
119
|
+
complete: () => sub.complete(),
|
|
120
|
+
})
|
|
121
|
+
sub.addTeardown(() => {
|
|
122
|
+
upstream.unsubscribe()
|
|
123
|
+
try {
|
|
124
|
+
fn()
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.error(err)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type {Result} from '../result/types.js'
|
|
2
|
+
|
|
3
|
+
/* Push-shaped, composable event stream. See observable.md (worktree branch)
|
|
4
|
+
* for the full design. */
|
|
5
|
+
|
|
6
|
+
export type NextArg<Ok, Err> = [Err] extends [never] ? Ok : Result<Ok, Err>
|
|
7
|
+
|
|
8
|
+
export type Observer<Ok, Err = never> = [Err] extends [never]
|
|
9
|
+
? {next?: (value: Ok) => void; complete?: () => void}
|
|
10
|
+
: {next?: (value: Result<Ok, Err>) => void; complete?: () => void}
|
|
11
|
+
|
|
12
|
+
export type NextFn<Ok, Err = never> = [Err] extends [never]
|
|
13
|
+
? (value: Ok) => void
|
|
14
|
+
: (value: Result<Ok, Err>) => void
|
|
15
|
+
|
|
16
|
+
export interface Subscriber<Ok, Err = never> {
|
|
17
|
+
next(value: NextArg<Ok, Err>): void
|
|
18
|
+
complete(): void
|
|
19
|
+
addTeardown(fn: () => void): void
|
|
20
|
+
readonly closed: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SubscribeCallback<Ok, Err> = (subscriber: Subscriber<Ok, Err>) => void
|
|
24
|
+
|
|
25
|
+
export interface Subscription {
|
|
26
|
+
unsubscribe(): void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type OperatorFunction<TIn, EIn, TOut, EOut> = (
|
|
30
|
+
source: Observable<TIn, EIn>,
|
|
31
|
+
) => Observable<TOut, EOut>
|
|
32
|
+
|
|
33
|
+
export declare class Observable<Ok, Err = never> {
|
|
34
|
+
constructor(cb: SubscribeCallback<Ok, Err>)
|
|
35
|
+
subscribe(observer?: Observer<Ok, Err> | NextFn<Ok, Err>): Subscription
|
|
36
|
+
|
|
37
|
+
pipe(): Observable<Ok, Err>
|
|
38
|
+
pipe<A>(op1: OperatorFunction<Ok, Err, A, Err>): Observable<A, Err>
|
|
39
|
+
pipe<A, B>(
|
|
40
|
+
op1: OperatorFunction<Ok, Err, A, Err>,
|
|
41
|
+
op2: OperatorFunction<A, Err, B, Err>,
|
|
42
|
+
): Observable<B, Err>
|
|
43
|
+
pipe<A, B, C>(
|
|
44
|
+
op1: OperatorFunction<Ok, Err, A, Err>,
|
|
45
|
+
op2: OperatorFunction<A, Err, B, Err>,
|
|
46
|
+
op3: OperatorFunction<B, Err, C, Err>,
|
|
47
|
+
): Observable<C, Err>
|
|
48
|
+
pipe<A, B, C, D>(
|
|
49
|
+
op1: OperatorFunction<Ok, Err, A, Err>,
|
|
50
|
+
op2: OperatorFunction<A, Err, B, Err>,
|
|
51
|
+
op3: OperatorFunction<B, Err, C, Err>,
|
|
52
|
+
op4: OperatorFunction<C, Err, D, Err>,
|
|
53
|
+
): Observable<D, Err>
|
|
54
|
+
pipe<A, B, C, D, E>(
|
|
55
|
+
op1: OperatorFunction<Ok, Err, A, Err>,
|
|
56
|
+
op2: OperatorFunction<A, Err, B, Err>,
|
|
57
|
+
op3: OperatorFunction<B, Err, C, Err>,
|
|
58
|
+
op4: OperatorFunction<C, Err, D, Err>,
|
|
59
|
+
op5: OperatorFunction<D, Err, E, Err>,
|
|
60
|
+
): Observable<E, Err>
|
|
61
|
+
pipe<A, B, C, D, E, F>(
|
|
62
|
+
op1: OperatorFunction<Ok, Err, A, Err>,
|
|
63
|
+
op2: OperatorFunction<A, Err, B, Err>,
|
|
64
|
+
op3: OperatorFunction<B, Err, C, Err>,
|
|
65
|
+
op4: OperatorFunction<C, Err, D, Err>,
|
|
66
|
+
op5: OperatorFunction<D, Err, E, Err>,
|
|
67
|
+
op6: OperatorFunction<E, Err, F, Err>,
|
|
68
|
+
): Observable<F, Err>
|
|
69
|
+
|
|
70
|
+
static from<X, E>(p: Promise<Result<X, E>>): Observable<X, E>
|
|
71
|
+
static from<T>(p: Promise<T>): Observable<T, never>
|
|
72
|
+
static from<T>(it: Iterable<T>): Observable<T, never>
|
|
73
|
+
static from<Ok, Err>(o: Observable<Ok, Err>): Observable<Ok, Err>
|
|
74
|
+
|
|
75
|
+
static withEmitters<Ok, Err = never>(): {
|
|
76
|
+
observable: Observable<Ok, Err>
|
|
77
|
+
next: (value: NextArg<Ok, Err>) => void
|
|
78
|
+
complete: () => void
|
|
79
|
+
}
|
|
80
|
+
}
|
package/runtime/udp/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {Observable} from '../observable/types.js'
|
|
1
2
|
import type {Result} from '../result/types.js'
|
|
2
3
|
|
|
3
4
|
export type UdpFamily = 'ipv4' | 'ipv6'
|
|
@@ -31,19 +32,31 @@ export type UdpError =
|
|
|
31
32
|
| {name: 'Closed'}
|
|
32
33
|
| {name: 'NotBound'}
|
|
33
34
|
|
|
35
|
+
/** Inbound datagram delivered via `socket.onMessage`. */
|
|
36
|
+
export interface UdpMessage {
|
|
37
|
+
msg: Uint8Array
|
|
38
|
+
from: PeerAddress
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
export interface UdpSocket {
|
|
35
42
|
readonly port: number
|
|
36
43
|
readonly family: UdpFamily | 'dual'
|
|
37
44
|
/**
|
|
38
45
|
* Counter incremented each time an inbound datagram is dropped:
|
|
39
46
|
* - the per-socket queue is full,
|
|
40
|
-
* - no `onMessage`
|
|
47
|
+
* - no subscriber is attached to `onMessage` when a packet arrives,
|
|
41
48
|
* - or the datagram is larger than the 1500-byte receive buffer
|
|
42
49
|
* (typical Ethernet MTU; covers CoAP / mDNS / SNTP / DNS).
|
|
43
50
|
*/
|
|
44
51
|
dropped: number
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Stream of inbound datagrams. Subscribing attaches the native dispatch
|
|
55
|
+
* once the first subscriber is added; the last `unsubscribe()` detaches
|
|
56
|
+
* it again. Multiple subscribers fan out from one native registration.
|
|
57
|
+
* Completes when `close()` is called.
|
|
58
|
+
*/
|
|
59
|
+
readonly onMessage: Observable<UdpMessage>
|
|
47
60
|
|
|
48
61
|
send(data: Uint8Array | string, to: PeerAddress): Promise<Result<void, UdpError>>
|
|
49
62
|
joinMulticastGroup(group: MulticastGroup): Result<void, UdpError>
|