@mikrojs/native 0.6.0-pr-72.g464c29c → 0.6.1
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/mikrojs.h +2 -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 +6 -8
- package/runtime/ble/types.ts +7 -14
- package/runtime/internal.d.ts +8 -2
- package/runtime/observable/lazy.ts +39 -0
- package/runtime/observable/native-observable.node-shim.ts +220 -0
- package/runtime/observable/observable.ts +1 -0
- package/runtime/observable/operators.ts +144 -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 +8 -17
- package/runtime/wifi/wifi.ts +7 -28
- package/src/mik_app_config.cpp +4 -1
- package/src/mik_observable.cpp +950 -0
- package/src/mik_repl.cpp +9 -4
- 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/lazy 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)
|
|
@@ -30,7 +30,8 @@ typedef struct MIKConfig {
|
|
|
30
30
|
uint32_t mem_reserved;
|
|
31
31
|
uint32_t fs_read_max; /* 0 = keep runtime default (65536) */
|
|
32
32
|
char entry_point[128];
|
|
33
|
-
char wifi_country[3];
|
|
33
|
+
char wifi_country[3]; /* Two-letter country code + NUL, e.g. "NO" */
|
|
34
|
+
char wifi_hostname[64]; /* DHCP hostname; empty = use mikrojs-<device-id> default */
|
|
34
35
|
} MIKConfig;
|
|
35
36
|
|
|
36
37
|
void MIK_DefaultConfig(MIKConfig* config);
|
|
@@ -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.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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.6.
|
|
81
|
+
"@mikrojs/quickjs": "0.6.1"
|
|
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 {lazyEvent} from 'mikrojs/observable/lazy'
|
|
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'
|
|
@@ -219,13 +221,9 @@ const peripheral: Peripheral = {
|
|
|
219
221
|
return ok(handle)
|
|
220
222
|
},
|
|
221
223
|
|
|
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
|
-
},
|
|
224
|
+
onConnect: lazyEvent<ConnectionInfo>(native, 'connect'),
|
|
225
|
+
onDisconnect: lazyEvent<ConnectionInfo>(native, 'disconnect'),
|
|
226
|
+
onMtu: lazyEvent<MtuInfo>(native, 'mtu'),
|
|
229
227
|
}
|
|
230
228
|
|
|
231
229
|
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
|
@@ -11,6 +11,10 @@ declare module 'mikrojs/kv/shared' {
|
|
|
11
11
|
export {KVError, makeCreateValue, type NativeKvFns} from './kv/shared.js'
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
declare module 'mikrojs/observable/lazy' {
|
|
15
|
+
export {lazyEvent} from './observable/lazy.js'
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
declare module 'native:cbor' {
|
|
15
19
|
import type {CborError} from '@mikrojs/native/runtime/cbor/types'
|
|
16
20
|
import type {Result} from 'mikrojs/result'
|
|
@@ -25,6 +29,10 @@ declare module 'native:result' {
|
|
|
25
29
|
export function err<E>(error: E): ErrResult<E>
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
declare module 'native:observable' {
|
|
33
|
+
export {Observable} from '@mikrojs/native/runtime/observable/types'
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
declare module 'native:sys' {
|
|
29
37
|
import type {JsMemoryUsage} from './sys/types.js'
|
|
30
38
|
export function evalScript(code: string): Promise<{value: unknown}>
|
|
@@ -407,7 +415,6 @@ declare module 'native:wifi' {
|
|
|
407
415
|
// Network config
|
|
408
416
|
mac(): R<string>
|
|
409
417
|
getHostname(): string | undefined
|
|
410
|
-
setHostname(hostname: string): R<void>
|
|
411
418
|
getIpConfig(): R<{ip: string; netmask: string; gateway: string; dns: string} | undefined>
|
|
412
419
|
setIpConfig(opts: {
|
|
413
420
|
ip?: string
|
|
@@ -446,7 +453,6 @@ declare module 'native:wifi' {
|
|
|
446
453
|
getPowerSave(): string
|
|
447
454
|
setPowerSave(mode: string): R<void>
|
|
448
455
|
getCountry(): string | undefined
|
|
449
|
-
setCountry(cc: string): R<void>
|
|
450
456
|
}
|
|
451
457
|
|
|
452
458
|
export declare const Wifi: {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {Observable} from 'native:observable'
|
|
2
|
+
|
|
3
|
+
import type {Subscriber} from './types.js'
|
|
4
|
+
|
|
5
|
+
/* Lazy-attach Observable: native.on is only called once a JS subscriber
|
|
6
|
+
* appears, and native.off runs when the last one unsubscribes. Code paths
|
|
7
|
+
* that import the host module (wifi/ble/...) but don't subscribe take the
|
|
8
|
+
* exact same internal-RAM path as before observables existed — important
|
|
9
|
+
* because mbedTLS handshake needs ~16 KB contiguous internal SRAM and each
|
|
10
|
+
* eager closure pinned at module load chips into that headroom. */
|
|
11
|
+
|
|
12
|
+
interface NativeEventSource {
|
|
13
|
+
on(event: string, listener: (...args: unknown[]) => void): void
|
|
14
|
+
off(event: string, listener: (...args: unknown[]) => void): void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function lazyEvent<T>(source: NativeEventSource, eventName: string): Observable<T> {
|
|
18
|
+
const subscribers: Subscriber<T>[] = []
|
|
19
|
+
function handler(value: unknown): void {
|
|
20
|
+
/* Snapshot so unsubscribes during dispatch don't shift indices. */
|
|
21
|
+
const snapshot = subscribers.slice()
|
|
22
|
+
for (const s of snapshot) {
|
|
23
|
+
if (!s.closed) s.next(value as T)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return new Observable<T>((sub) => {
|
|
27
|
+
subscribers.push(sub)
|
|
28
|
+
if (subscribers.length === 1) {
|
|
29
|
+
source.on(eventName, handler)
|
|
30
|
+
}
|
|
31
|
+
sub.addTeardown(() => {
|
|
32
|
+
const i = subscribers.indexOf(sub)
|
|
33
|
+
if (i >= 0) subscribers.splice(i, 1)
|
|
34
|
+
if (subscribers.length === 0) {
|
|
35
|
+
source.off(eventName, handler)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Host-side shim for `native:observable`, used only in vitest (Node) where
|
|
2
|
+
// the mikrojs C runtime isn't available. Keep in sync with mik_observable.cpp.
|
|
3
|
+
//
|
|
4
|
+
// Mirrors the locked design in .claude/plans/observable.md:
|
|
5
|
+
// - subscribe() returns a Subscription with unsubscribe() (no AbortSignal)
|
|
6
|
+
// - no error channel — throws inside dispatch or teardown are caught at the
|
|
7
|
+
// boundary, isolated to the offending subscriber, and re-thrown
|
|
8
|
+
// asynchronously via setTimeout(0) so the synchronous producer keeps
|
|
9
|
+
// running but the bug eventually surfaces (and on device, halts the
|
|
10
|
+
// runtime via the existing unhandled-rejection path)
|
|
11
|
+
// - sync emission allowed
|
|
12
|
+
// - pipe-only composition (operators live in operators.ts)
|
|
13
|
+
// - withEmitters() factory: {observable, next, complete}
|
|
14
|
+
|
|
15
|
+
/* Catch a thrown error and re-throw it on the next tick. The synchronous
|
|
16
|
+
* caller keeps going (other subscribers receive the value, remaining
|
|
17
|
+
* teardowns run); the error eventually surfaces as an uncaught exception. */
|
|
18
|
+
function panicAsync(err: unknown): void {
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
throw err
|
|
21
|
+
}, 0)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class Subscriber<T> {
|
|
25
|
+
closed = false
|
|
26
|
+
private next_fn: ((v: T) => void) | undefined
|
|
27
|
+
private complete_fn: (() => void) | undefined
|
|
28
|
+
private teardowns: Array<() => void> = []
|
|
29
|
+
|
|
30
|
+
constructor(observer: unknown) {
|
|
31
|
+
if (observer == null) return
|
|
32
|
+
if (typeof observer === 'function') {
|
|
33
|
+
this.next_fn = observer as (v: T) => void
|
|
34
|
+
} else if (typeof observer === 'object') {
|
|
35
|
+
const o = observer as {next?: (v: T) => void; complete?: () => void}
|
|
36
|
+
if (typeof o.next === 'function') this.next_fn = o.next
|
|
37
|
+
if (typeof o.complete === 'function') this.complete_fn = o.complete
|
|
38
|
+
} else {
|
|
39
|
+
throw new TypeError('subscribe: observer must be a function, object, undefined, or null')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
next(value: T): void {
|
|
44
|
+
if (this.closed) return
|
|
45
|
+
if (this.next_fn) {
|
|
46
|
+
try {
|
|
47
|
+
this.next_fn(value)
|
|
48
|
+
} catch (err) {
|
|
49
|
+
panicAsync(err)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
complete(): void {
|
|
55
|
+
if (this.closed) return
|
|
56
|
+
this.closed = true
|
|
57
|
+
if (this.complete_fn) {
|
|
58
|
+
try {
|
|
59
|
+
this.complete_fn()
|
|
60
|
+
} catch (err) {
|
|
61
|
+
panicAsync(err)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
this.runTeardowns()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
addTeardown(fn: () => void): void {
|
|
68
|
+
if (typeof fn !== 'function') {
|
|
69
|
+
throw new TypeError('addTeardown: argument must be a function')
|
|
70
|
+
}
|
|
71
|
+
if (this.closed) {
|
|
72
|
+
try {
|
|
73
|
+
fn()
|
|
74
|
+
} catch (err) {
|
|
75
|
+
panicAsync(err)
|
|
76
|
+
}
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
this.teardowns.push(fn)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Used by Subscription.unsubscribe — silent (no observer.complete call).
|
|
83
|
+
closeSilently(): void {
|
|
84
|
+
if (this.closed) return
|
|
85
|
+
this.closed = true
|
|
86
|
+
this.runTeardowns()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private runTeardowns(): void {
|
|
90
|
+
const list = this.teardowns
|
|
91
|
+
this.teardowns = []
|
|
92
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
93
|
+
try {
|
|
94
|
+
list[i]!()
|
|
95
|
+
} catch (err) {
|
|
96
|
+
panicAsync(err)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class Subscription {
|
|
103
|
+
constructor(private subscriber: Subscriber<unknown>) {}
|
|
104
|
+
unsubscribe(): void {
|
|
105
|
+
this.subscriber.closeSilently()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type SubscribeCallback<T> = (sub: Subscriber<T>) => void
|
|
110
|
+
|
|
111
|
+
export class Observable<Ok, Err = never> {
|
|
112
|
+
// The shim doesn't actually use `_phantom` at runtime; the type parameters are
|
|
113
|
+
// purely for type-level alignment with the public interface.
|
|
114
|
+
declare readonly _phantom: [Ok, Err]
|
|
115
|
+
#cb: SubscribeCallback<unknown>
|
|
116
|
+
|
|
117
|
+
constructor(cb: SubscribeCallback<unknown>) {
|
|
118
|
+
if (typeof cb !== 'function') {
|
|
119
|
+
throw new TypeError('Observable: constructor requires a function argument')
|
|
120
|
+
}
|
|
121
|
+
this.#cb = cb
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
subscribe(observer?: unknown): Subscription {
|
|
125
|
+
const sub = new Subscriber<unknown>(observer)
|
|
126
|
+
try {
|
|
127
|
+
this.#cb(sub)
|
|
128
|
+
} catch (err) {
|
|
129
|
+
sub.closeSilently()
|
|
130
|
+
throw err
|
|
131
|
+
}
|
|
132
|
+
return new Subscription(sub)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
pipe(...ops: Array<(o: Observable<unknown, unknown>) => Observable<unknown, unknown>>) {
|
|
136
|
+
let current: Observable<unknown, unknown> = this as unknown as Observable<unknown, unknown>
|
|
137
|
+
for (const op of ops) {
|
|
138
|
+
if (typeof op !== 'function') {
|
|
139
|
+
throw new TypeError('pipe: arguments must be operator functions')
|
|
140
|
+
}
|
|
141
|
+
current = op(current)
|
|
142
|
+
}
|
|
143
|
+
return current
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static from(src: unknown): Observable<unknown, unknown> {
|
|
147
|
+
if (src instanceof Observable) return src
|
|
148
|
+
if (
|
|
149
|
+
src != null &&
|
|
150
|
+
(typeof src === 'object' || typeof src === 'function') &&
|
|
151
|
+
typeof (src as {then?: unknown}).then === 'function'
|
|
152
|
+
) {
|
|
153
|
+
// Promise-shaped
|
|
154
|
+
return new Observable<unknown>((sub) => {
|
|
155
|
+
;(src as PromiseLike<unknown>).then((value) => {
|
|
156
|
+
if (sub.closed) return
|
|
157
|
+
sub.next(value)
|
|
158
|
+
if (sub.closed) return
|
|
159
|
+
sub.complete()
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
if (
|
|
164
|
+
src != null &&
|
|
165
|
+
(typeof src === 'object' || typeof src === 'string') &&
|
|
166
|
+
typeof (src as {[Symbol.iterator]?: unknown})[Symbol.iterator] === 'function'
|
|
167
|
+
) {
|
|
168
|
+
return new Observable<unknown>((sub) => {
|
|
169
|
+
for (const value of src as Iterable<unknown>) {
|
|
170
|
+
if (sub.closed) return
|
|
171
|
+
sub.next(value)
|
|
172
|
+
}
|
|
173
|
+
if (!sub.closed) sub.complete()
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
throw new TypeError('Observable.from: source must be a Promise, Iterable, or Observable')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
static withEmitters<Ok, Err = never>(): {
|
|
180
|
+
observable: Observable<Ok, Err>
|
|
181
|
+
next: (value: unknown) => void
|
|
182
|
+
complete: () => void
|
|
183
|
+
} {
|
|
184
|
+
const subs: Array<Subscriber<unknown>> = []
|
|
185
|
+
let completed = false
|
|
186
|
+
|
|
187
|
+
const observable = new Observable<unknown>((sub) => {
|
|
188
|
+
if (completed) {
|
|
189
|
+
sub.complete()
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
subs.push(sub)
|
|
193
|
+
sub.addTeardown(() => {
|
|
194
|
+
const i = subs.indexOf(sub)
|
|
195
|
+
if (i >= 0) subs.splice(i, 1)
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const next = (value: unknown) => {
|
|
200
|
+
if (completed) return
|
|
201
|
+
// Snapshot to be resilient against mid-dispatch unsubscribes.
|
|
202
|
+
const snapshot = subs.slice()
|
|
203
|
+
for (const s of snapshot) {
|
|
204
|
+
if (!s.closed) s.next(value)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const complete = () => {
|
|
209
|
+
if (completed) return
|
|
210
|
+
completed = true
|
|
211
|
+
const snapshot = subs.slice()
|
|
212
|
+
subs.length = 0
|
|
213
|
+
for (const s of snapshot) {
|
|
214
|
+
if (!s.closed) s.complete()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {observable: observable as unknown as Observable<Ok, Err>, next, complete}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {Observable} from 'native:observable'
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/* Operators for `Observable.pipe(...)`. Each is a factory returning a
|
|
2
|
+
* function `(source) => Observable`. Composition is pure pipe — no method
|
|
3
|
+
* chaining on Observable itself.
|
|
4
|
+
*
|
|
5
|
+
* M0 scope: operators apply to non-fallible streams (`Err = never`). For
|
|
6
|
+
* fallible streams (`Observable<Ok, Err>` with Err != never), corresponding
|
|
7
|
+
* Result-aware operators (`mapOk`, `filterOk`, ...) ship when a concrete
|
|
8
|
+
* consumer asks. Today no module produces fallible event streams.
|
|
9
|
+
*
|
|
10
|
+
* Errors: throws inside operator transforms or finalize callbacks are caught
|
|
11
|
+
* at the dispatch boundary, isolated to that subscriber, and re-thrown
|
|
12
|
+
* asynchronously via setTimeout(0). The synchronous producer keeps going
|
|
13
|
+
* (the bad value is dropped, sibling subscribers untouched), and on device
|
|
14
|
+
* the eventual uncaught throw halts the runtime via the existing
|
|
15
|
+
* unhandled-rejection path. Stream errors are panics.
|
|
16
|
+
*
|
|
17
|
+
* See `.claude/plans/observable.md` for the full design.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {Observable as NativeObservable} from 'native:observable'
|
|
21
|
+
|
|
22
|
+
import type {Observable as ObservableT} from './types.js'
|
|
23
|
+
|
|
24
|
+
/* `native:observable` resolves only inside the runtime build; outside
|
|
25
|
+
* (twoslash, host typecheck without internal.d.ts) it falls back to `any`,
|
|
26
|
+
* which collapses pipe/operator inference at use sites. Pin the type to the
|
|
27
|
+
* declared class in `./types.ts` so consumers always see the typed shape. */
|
|
28
|
+
const Observable = NativeObservable as unknown as typeof ObservableT
|
|
29
|
+
type Observable<Ok, Err = never> = ObservableT<Ok, Err>
|
|
30
|
+
|
|
31
|
+
/* Catch a thrown error and re-throw it on the next tick. The synchronous
|
|
32
|
+
* caller keeps going; the error eventually surfaces as an uncaught
|
|
33
|
+
* exception. */
|
|
34
|
+
function panicAsync(err: unknown): void {
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
throw err
|
|
37
|
+
}, 0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Map values through a transform. Throws inside `fn` are caught and
|
|
41
|
+
* scheduled to re-throw on the next tick (panic). The bad value is dropped
|
|
42
|
+
* for that subscription; sibling subscriptions are unaffected. */
|
|
43
|
+
export const map =
|
|
44
|
+
<A, B>(fn: (value: A) => B) =>
|
|
45
|
+
(source: Observable<A>): Observable<B> =>
|
|
46
|
+
new Observable<B>((sub) => {
|
|
47
|
+
const upstream = source.subscribe({
|
|
48
|
+
next: (value) => {
|
|
49
|
+
let next: B
|
|
50
|
+
try {
|
|
51
|
+
next = fn(value)
|
|
52
|
+
} catch (err) {
|
|
53
|
+
panicAsync(err)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
sub.next(next)
|
|
57
|
+
},
|
|
58
|
+
complete: () => sub.complete(),
|
|
59
|
+
})
|
|
60
|
+
sub.addTeardown(() => upstream.unsubscribe())
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/* Pass through values matching `pred`. `pred` errors panic asynchronously. */
|
|
64
|
+
export const filter =
|
|
65
|
+
<A>(pred: (value: A) => boolean) =>
|
|
66
|
+
(source: Observable<A>): Observable<A> =>
|
|
67
|
+
new Observable<A>((sub) => {
|
|
68
|
+
const upstream = source.subscribe({
|
|
69
|
+
next: (value) => {
|
|
70
|
+
let keep: boolean
|
|
71
|
+
try {
|
|
72
|
+
keep = pred(value)
|
|
73
|
+
} catch (err) {
|
|
74
|
+
panicAsync(err)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
if (keep) sub.next(value)
|
|
78
|
+
},
|
|
79
|
+
complete: () => sub.complete(),
|
|
80
|
+
})
|
|
81
|
+
sub.addTeardown(() => upstream.unsubscribe())
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
/* Take at most n values, then complete. n <= 0 completes immediately. */
|
|
85
|
+
export const take =
|
|
86
|
+
(n: number) =>
|
|
87
|
+
<A>(source: Observable<A>): Observable<A> =>
|
|
88
|
+
new Observable<A>((sub) => {
|
|
89
|
+
if (n <= 0) {
|
|
90
|
+
sub.complete()
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
let remaining = n
|
|
94
|
+
const upstream = source.subscribe({
|
|
95
|
+
next: (value) => {
|
|
96
|
+
if (remaining <= 0) return
|
|
97
|
+
remaining--
|
|
98
|
+
sub.next(value)
|
|
99
|
+
if (remaining === 0) sub.complete()
|
|
100
|
+
},
|
|
101
|
+
complete: () => sub.complete(),
|
|
102
|
+
})
|
|
103
|
+
sub.addTeardown(() => upstream.unsubscribe())
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
/* Stop emitting when `notifier` emits its first value. Notifier completing
|
|
107
|
+
* without emitting is NOT a trigger — primary keeps going. */
|
|
108
|
+
export const takeUntil =
|
|
109
|
+
(notifier: Observable<unknown, unknown>) =>
|
|
110
|
+
<A>(source: Observable<A>): Observable<A> =>
|
|
111
|
+
new Observable<A>((sub) => {
|
|
112
|
+
const upstream = source.subscribe({
|
|
113
|
+
next: (value) => sub.next(value),
|
|
114
|
+
complete: () => sub.complete(),
|
|
115
|
+
})
|
|
116
|
+
const notifierSub = notifier.subscribe({
|
|
117
|
+
next: () => sub.complete(),
|
|
118
|
+
})
|
|
119
|
+
sub.addTeardown(() => {
|
|
120
|
+
notifierSub.unsubscribe()
|
|
121
|
+
upstream.unsubscribe()
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
/* Run `fn` when the subscription ends for any reason (unsubscribe or
|
|
126
|
+
* natural completion). Throws inside `fn` are caught and panic
|
|
127
|
+
* asynchronously, so subsequent teardowns still run. RxJS naming. */
|
|
128
|
+
export const finalize =
|
|
129
|
+
(fn: () => void) =>
|
|
130
|
+
<A>(source: Observable<A>): Observable<A> =>
|
|
131
|
+
new Observable<A>((sub) => {
|
|
132
|
+
const upstream = source.subscribe({
|
|
133
|
+
next: (value) => sub.next(value),
|
|
134
|
+
complete: () => sub.complete(),
|
|
135
|
+
})
|
|
136
|
+
sub.addTeardown(() => {
|
|
137
|
+
upstream.unsubscribe()
|
|
138
|
+
try {
|
|
139
|
+
fn()
|
|
140
|
+
} catch (err) {
|
|
141
|
+
panicAsync(err)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
})
|
|
@@ -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
|
+
}
|