@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.
Files changed (56) hide show
  1. package/CMakeLists.txt +3 -1
  2. package/cmake/mikrojs_bytecode.cmake +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/runtime/result/native-result.node-shim.js.map +1 -1
  6. package/dist/runtime/result/types.d.ts +5 -5
  7. package/dist/runtime/result/types.d.ts.map +1 -1
  8. package/dist/types.d.ts +1 -1
  9. package/dist/types.d.ts.map +1 -1
  10. package/include/mikrojs/mikrojs.h +26 -2
  11. package/include/mikrojs/ota_env.h +8 -6
  12. package/include/mikrojs/ota_policy.h +16 -0
  13. package/include/mikrojs/private.h +13 -0
  14. package/include/mikrojs/utils.h +24 -0
  15. package/package.json +10 -10
  16. package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
  17. package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
  18. package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
  19. package/runtime/gpio/types.ts +116 -0
  20. package/runtime/i2c/types.ts +25 -31
  21. package/runtime/i2s/types.ts +23 -12
  22. package/runtime/internal.d.ts +1 -174
  23. package/runtime/kv/shared.ts +13 -11
  24. package/runtime/neopixel/types.ts +24 -6
  25. package/runtime/observable/native-observable.node-shim.ts +73 -37
  26. package/runtime/observable/native-operators.node-shim.ts +417 -0
  27. package/runtime/observable/observable.ts +76 -1
  28. package/runtime/observable/operators.ts +156 -89
  29. package/runtime/observable/types.ts +61 -6
  30. package/runtime/pwm/types.ts +31 -11
  31. package/runtime/result/native-result.node-shim.ts +1 -1
  32. package/runtime/result/types.ts +5 -5
  33. package/runtime/sleep/types.ts +10 -8
  34. package/runtime/spi/types.ts +16 -14
  35. package/runtime/uart/types.ts +27 -15
  36. package/src/gpio_claim.cpp +63 -0
  37. package/src/mik_abort.cpp +1 -2
  38. package/src/mik_console.cpp +192 -115
  39. package/src/mik_http_client.cpp +3 -14
  40. package/src/mik_inspect.cpp +86 -9
  41. package/src/mik_observable.cpp +1028 -80
  42. package/src/mik_ota_client.cpp +5 -2
  43. package/src/mik_ota_policy.cpp +65 -18
  44. package/src/mik_result.cpp +10 -9
  45. package/src/mikrojs.cpp +26 -13
  46. package/src/modules.cpp +17 -6
  47. package/src/timers.cpp +6 -2
  48. package/src/utils.cpp +106 -0
  49. package/runtime/i2c/i2c.ts +0 -35
  50. package/runtime/i2s/i2s.ts +0 -46
  51. package/runtime/neopixel/neopixel.ts +0 -38
  52. package/runtime/pin/pin.ts +0 -51
  53. package/runtime/pin/types.ts +0 -49
  54. package/runtime/pwm/pwm.ts +0 -32
  55. package/runtime/spi/spi.ts +0 -31
  56. 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
- export {Observable} from 'native:mikro/observable'
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
+ }