@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 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.5.1",
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.5.1"
81
+ "@mikrojs/quickjs": "0.6.0-pr-70.gc88e298"
80
82
  },
81
83
  "devDependencies": {
82
84
  "@swc/core": "^1.15.30",
@@ -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
- on<K extends keyof PeripheralEventMap>(event: K, listener: PeripheralEventMap[K]) {
223
- native.on(event, listener as (...args: unknown[]) => void)
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}
@@ -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
- * Register a listener for a peripheral lifecycle event. Listeners are
185
- * called on the JS loop thread in registration order. Safe to register
186
- * before calling `advertise()`.
187
- */
188
- on<K extends keyof PeripheralEventMap>(event: K, listener: PeripheralEventMap[K]): void
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
@@ -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
+ }
@@ -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` handler is set when a packet arrives,
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
- onMessage: ((msg: Uint8Array, from: PeerAddress) => void) | null
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>