@real-router/rx 0.2.2 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/rx",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "type": "commonjs",
5
5
  "description": "Reactive Observable API for Real-Router — state$, events$, operators, and TC39 Observable support",
6
6
  "main": "./dist/cjs/index.js",
@@ -8,7 +8,6 @@
8
8
  "types": "./dist/esm/index.d.mts",
9
9
  "exports": {
10
10
  ".": {
11
- "development": "./src/index.ts",
12
11
  "types": {
13
12
  "import": "./dist/esm/index.d.mts",
14
13
  "require": "./dist/cjs/index.d.ts"
@@ -18,7 +17,8 @@
18
17
  }
19
18
  },
20
19
  "files": [
21
- "dist"
20
+ "dist",
21
+ "src"
22
22
  ],
23
23
  "repository": {
24
24
  "type": "git",
@@ -43,7 +43,7 @@
43
43
  "homepage": "https://github.com/greydragon888/real-router",
44
44
  "sideEffects": false,
45
45
  "dependencies": {
46
- "@real-router/core": "^0.45.1"
46
+ "@real-router/core": "^0.45.2"
47
47
  },
48
48
  "scripts": {
49
49
  "test": "vitest",
@@ -52,7 +52,7 @@
52
52
  "build": "tsdown --config-loader unrun",
53
53
  "type-check": "tsc --noEmit",
54
54
  "lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
55
- "lint:package": "bash ../../scripts/publint-filter.sh",
55
+ "lint:package": "publint",
56
56
  "lint:types": "attw --pack .",
57
57
  "build:dist-only": "tsdown --config-loader unrun"
58
58
  }
@@ -0,0 +1,281 @@
1
+ import type {
2
+ Observer,
3
+ Subscription,
4
+ ObservableOptions,
5
+ SubscribeFn,
6
+ Operator,
7
+ } from "./types";
8
+
9
+ declare global {
10
+ interface SymbolConstructor {
11
+ readonly observable: symbol;
12
+ }
13
+ }
14
+
15
+ export class RxObservable<T> {
16
+ #subscribeFn: SubscribeFn<T>;
17
+
18
+ constructor(subscribeFn: SubscribeFn<T>) {
19
+ this.#subscribeFn = subscribeFn;
20
+ }
21
+
22
+ subscribe(
23
+ observerOrNext: Observer<T> | ((value: T) => void),
24
+ options?: ObservableOptions,
25
+ ): Subscription {
26
+ const observer: Observer<T> =
27
+ typeof observerOrNext === "function"
28
+ ? { next: observerOrNext }
29
+ : observerOrNext;
30
+
31
+ const { signal } = options ?? {};
32
+
33
+ if (signal?.aborted) {
34
+ return {
35
+ unsubscribe: () => {},
36
+ closed: true,
37
+ };
38
+ }
39
+
40
+ let closed = false;
41
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- matches SubscribeFn return type
42
+ let teardown: void | (() => void);
43
+
44
+ const safeNext = (value: T) => {
45
+ if (closed) {
46
+ return;
47
+ }
48
+
49
+ try {
50
+ observer.next?.(value);
51
+ } catch (error) {
52
+ safeError(error);
53
+ }
54
+ };
55
+
56
+ const safeError = (err: unknown) => {
57
+ if (closed) {
58
+ return;
59
+ }
60
+
61
+ try {
62
+ if (observer.error) {
63
+ observer.error(err);
64
+ } else {
65
+ console.error("Unhandled error in RxObservable:", err);
66
+ }
67
+ } catch {
68
+ // Errors in error handler are caught silently
69
+ }
70
+ };
71
+
72
+ const safeComplete = () => {
73
+ if (closed) {
74
+ return;
75
+ }
76
+
77
+ closed = true;
78
+
79
+ try {
80
+ observer.complete?.();
81
+ } catch {
82
+ // Errors in complete handler are caught silently
83
+ }
84
+ };
85
+
86
+ const subscription: Subscription = {
87
+ unsubscribe: () => {
88
+ if (closed) {
89
+ return;
90
+ }
91
+
92
+ closed = true;
93
+
94
+ if (abortHandler) {
95
+ signal?.removeEventListener("abort", abortHandler);
96
+ }
97
+
98
+ if (teardown) {
99
+ try {
100
+ teardown();
101
+ } catch {
102
+ // Teardown errors are caught silently
103
+ }
104
+ }
105
+ },
106
+ get closed() {
107
+ return closed;
108
+ },
109
+ };
110
+
111
+ let abortHandler: (() => void) | undefined;
112
+
113
+ if (signal) {
114
+ abortHandler = () => {
115
+ subscription.unsubscribe();
116
+ };
117
+ signal.addEventListener("abort", abortHandler);
118
+ }
119
+
120
+ try {
121
+ teardown = this.#subscribeFn({
122
+ next: safeNext,
123
+ error: safeError,
124
+ complete: safeComplete,
125
+ });
126
+ } catch (error) {
127
+ safeError(error);
128
+ }
129
+
130
+ return subscription;
131
+ }
132
+
133
+ pipe(): this;
134
+ pipe<A>(op1: Operator<T, A>): RxObservable<A>;
135
+ pipe<A, B>(op1: Operator<T, A>, op2: Operator<A, B>): RxObservable<B>;
136
+ pipe<A, B, C>(
137
+ op1: Operator<T, A>,
138
+ op2: Operator<A, B>,
139
+ op3: Operator<B, C>,
140
+ ): RxObservable<C>;
141
+ pipe<A, B, C, D>(
142
+ op1: Operator<T, A>,
143
+ op2: Operator<A, B>,
144
+ op3: Operator<B, C>,
145
+ op4: Operator<C, D>,
146
+ ): RxObservable<D>;
147
+ pipe<A, B, C, D, E>(
148
+ op1: Operator<T, A>,
149
+ op2: Operator<A, B>,
150
+ op3: Operator<B, C>,
151
+ op4: Operator<C, D>,
152
+ op5: Operator<D, E>,
153
+ ): RxObservable<E>;
154
+ pipe<A, B, C, D, E, F>(
155
+ op1: Operator<T, A>,
156
+ op2: Operator<A, B>,
157
+ op3: Operator<B, C>,
158
+ op4: Operator<C, D>,
159
+ op5: Operator<D, E>,
160
+ op6: Operator<E, F>,
161
+ ): RxObservable<F>;
162
+ pipe<A, B, C, D, E, F, G>(
163
+ op1: Operator<T, A>,
164
+ op2: Operator<A, B>,
165
+ op3: Operator<B, C>,
166
+ op4: Operator<C, D>,
167
+ op5: Operator<D, E>,
168
+ op6: Operator<E, F>,
169
+ op7: Operator<F, G>,
170
+ ): RxObservable<G>;
171
+ pipe<A, B, C, D, E, F, G, H>(
172
+ op1: Operator<T, A>,
173
+ op2: Operator<A, B>,
174
+ op3: Operator<B, C>,
175
+ op4: Operator<C, D>,
176
+ op5: Operator<D, E>,
177
+ op6: Operator<E, F>,
178
+ op7: Operator<F, G>,
179
+ op8: Operator<G, H>,
180
+ ): RxObservable<H>;
181
+ pipe<A, B, C, D, E, F, G, H, I>(
182
+ op1: Operator<T, A>,
183
+ op2: Operator<A, B>,
184
+ op3: Operator<B, C>,
185
+ op4: Operator<C, D>,
186
+ op5: Operator<D, E>,
187
+ op6: Operator<E, F>,
188
+ op7: Operator<F, G>,
189
+ op8: Operator<G, H>,
190
+ op9: Operator<H, I>,
191
+ ): RxObservable<I>;
192
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
193
+ pipe(...operators: Operator<any, any>[]): any {
194
+ if (operators.length === 0) {
195
+ return this;
196
+ }
197
+
198
+ // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias -- pipe pattern
199
+ let result: RxObservable<unknown> = this;
200
+
201
+ for (const operator of operators) {
202
+ result = operator(result);
203
+ }
204
+
205
+ return result;
206
+ }
207
+
208
+ [Symbol.observable](): this {
209
+ return this;
210
+ }
211
+
212
+ ["@@observable"](): this {
213
+ return this;
214
+ }
215
+
216
+ async *[Symbol.asyncIterator](): AsyncIterableIterator<T> {
217
+ let resolve: (() => void) | null = null;
218
+ let latestValue: T | undefined;
219
+ let hasValue = false;
220
+ let completed = false;
221
+ let error: unknown = null;
222
+
223
+ const subscription = this.subscribe({
224
+ next: (value) => {
225
+ latestValue = value;
226
+ hasValue = true;
227
+ if (resolve) {
228
+ const resolveCallback = resolve;
229
+
230
+ resolve = null;
231
+ resolveCallback();
232
+ }
233
+ },
234
+ /* v8 ignore start -- v8 coverage can't track branches inside suspended async generators */
235
+ error: (err) => {
236
+ error = err;
237
+ completed = true;
238
+ if (resolve) {
239
+ const resolveCallback = resolve;
240
+
241
+ resolve = null;
242
+ resolveCallback();
243
+ }
244
+ },
245
+ complete: () => {
246
+ completed = true;
247
+ if (resolve) {
248
+ const resolveCallback = resolve;
249
+
250
+ resolve = null;
251
+ resolveCallback();
252
+ }
253
+ },
254
+ /* v8 ignore stop */
255
+ });
256
+
257
+ try {
258
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- v8 ignore affects analysis
259
+ while (!completed) {
260
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- v8 ignore affects analysis
261
+ if (hasValue) {
262
+ const value = latestValue as T;
263
+
264
+ hasValue = false;
265
+ yield value;
266
+ } else {
267
+ await new Promise<void>((_resolve) => {
268
+ resolve = _resolve;
269
+ });
270
+
271
+ if (error !== null) {
272
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
273
+ throw error;
274
+ }
275
+ }
276
+ }
277
+ } finally {
278
+ subscription.unsubscribe();
279
+ }
280
+ }
281
+ }
package/src/events$.ts ADDED
@@ -0,0 +1,133 @@
1
+ import { events } from "@real-router/core";
2
+ import { getPluginApi } from "@real-router/core/api";
3
+
4
+ import { RxObservable } from "./RxObservable";
5
+
6
+ import type {
7
+ Router,
8
+ State,
9
+ NavigationOptions,
10
+ RouterError,
11
+ } from "@real-router/core";
12
+
13
+ export type RouterEvent =
14
+ | { type: "ROUTER_START" }
15
+ | { type: "ROUTER_STOP" }
16
+ | { type: "TRANSITION_START"; toState: State; fromState: State | undefined }
17
+ | {
18
+ type: "TRANSITION_LEAVE_APPROVE";
19
+ toState: State;
20
+ fromState: State | undefined;
21
+ }
22
+ | {
23
+ type: "TRANSITION_SUCCESS";
24
+ toState: State;
25
+ fromState: State | undefined;
26
+ options: NavigationOptions;
27
+ }
28
+ | {
29
+ type: "TRANSITION_ERROR";
30
+ toState: State | undefined;
31
+ fromState: State | undefined;
32
+ error: RouterError;
33
+ }
34
+ | { type: "TRANSITION_CANCEL"; toState: State; fromState: State | undefined };
35
+
36
+ export function events$(router: Router): RxObservable<RouterEvent> {
37
+ return new RxObservable<RouterEvent>((observer) => {
38
+ const api = getPluginApi(router);
39
+ const unsubscribes: (() => void)[] = [];
40
+
41
+ /* eslint-disable unicorn/prefer-single-call -- individual pushes for partial registration safety */
42
+ try {
43
+ unsubscribes.push(
44
+ api.addEventListener(events.ROUTER_START, () => {
45
+ observer.next?.({ type: "ROUTER_START" });
46
+ }),
47
+ );
48
+ unsubscribes.push(
49
+ api.addEventListener(events.ROUTER_STOP, () => {
50
+ observer.next?.({ type: "ROUTER_STOP" });
51
+ }),
52
+ );
53
+ unsubscribes.push(
54
+ api.addEventListener(
55
+ events.TRANSITION_START,
56
+ (toState: State, fromState: State | undefined) => {
57
+ observer.next?.({ type: "TRANSITION_START", toState, fromState });
58
+ },
59
+ ),
60
+ );
61
+ unsubscribes.push(
62
+ api.addEventListener(
63
+ events.TRANSITION_LEAVE_APPROVE,
64
+ (toState: State, fromState: State | undefined) => {
65
+ observer.next?.({
66
+ type: "TRANSITION_LEAVE_APPROVE",
67
+ toState,
68
+ fromState,
69
+ });
70
+ },
71
+ ),
72
+ );
73
+ unsubscribes.push(
74
+ api.addEventListener(
75
+ events.TRANSITION_SUCCESS,
76
+ (
77
+ toState: State,
78
+ fromState: State | undefined,
79
+ options: NavigationOptions,
80
+ ) => {
81
+ observer.next?.({
82
+ type: "TRANSITION_SUCCESS",
83
+ toState,
84
+ fromState,
85
+ options,
86
+ });
87
+ },
88
+ ),
89
+ );
90
+ unsubscribes.push(
91
+ api.addEventListener(
92
+ events.TRANSITION_ERROR,
93
+ (
94
+ toState: State | undefined,
95
+ fromState: State | undefined,
96
+ error: RouterError,
97
+ ) => {
98
+ observer.next?.({
99
+ type: "TRANSITION_ERROR",
100
+ toState,
101
+ fromState,
102
+ error,
103
+ });
104
+ },
105
+ ),
106
+ );
107
+ unsubscribes.push(
108
+ api.addEventListener(
109
+ events.TRANSITION_CANCEL,
110
+ (toState: State, fromState: State | undefined) => {
111
+ observer.next?.({ type: "TRANSITION_CANCEL", toState, fromState });
112
+ },
113
+ ),
114
+ );
115
+ /* eslint-enable unicorn/prefer-single-call */
116
+ /* v8 ignore start -- defensive: partial listener registration failure */
117
+ } catch (error) {
118
+ // Clean up any listeners that were successfully registered
119
+ for (const unsub of unsubscribes) {
120
+ unsub();
121
+ }
122
+
123
+ throw error;
124
+ }
125
+ /* v8 ignore stop */
126
+
127
+ return () => {
128
+ for (const unsub of unsubscribes) {
129
+ unsub();
130
+ }
131
+ };
132
+ });
133
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ export { RxObservable } from "./RxObservable";
2
+
3
+ export {
4
+ map,
5
+ filter,
6
+ debounceTime,
7
+ distinctUntilChanged,
8
+ takeUntil,
9
+ } from "./operators";
10
+
11
+ export { state$ } from "./state$";
12
+
13
+ export type { SubscribeState } from "./state$";
14
+
15
+ export { events$ } from "./events$";
16
+
17
+ export type { RouterEvent } from "./events$";
18
+
19
+ export { observable } from "./observable";
20
+
21
+ export type {
22
+ Observer,
23
+ Subscription,
24
+ ObservableOptions,
25
+ SubscribeFn,
26
+ Operator,
27
+ UnaryFunction,
28
+ } from "./types";
@@ -0,0 +1,28 @@
1
+ import { state$, type SubscribeState } from "./state$";
2
+
3
+ import type { RxObservable } from "./RxObservable";
4
+ import type { Router } from "@real-router/core";
5
+
6
+ /**
7
+ * Creates a TC39-compliant Observable from a router instance.
8
+ *
9
+ * This is a semantic wrapper over `state$()` that provides TC39 Observable interop.
10
+ * Use this when you need RxJS compatibility via `from(observable(router))`.
11
+ *
12
+ * @param router - Router instance to observe
13
+ * @returns RxObservable that emits state changes
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { from } from 'rxjs';
18
+ * import { observable } from '@real-router/rx';
19
+ *
20
+ * const router$ = from(observable(router));
21
+ * router$.subscribe(({ route, previousRoute }) => {
22
+ * console.log('Navigation:', previousRoute?.name, '→', route.name);
23
+ * });
24
+ * ```
25
+ */
26
+ export function observable(router: Router): RxObservable<SubscribeState> {
27
+ return state$(router);
28
+ }
@@ -0,0 +1,37 @@
1
+ import { RxObservable } from "../RxObservable";
2
+
3
+ import type { Observer } from "../types";
4
+
5
+ /**
6
+ * Creates a stateless operator. Wires error/complete/teardown automatically.
7
+ * The `next` callback receives each value and the downstream observer.
8
+ */
9
+ export function createOperator<T, R>(
10
+ next: (value: T, observer: Observer<R>) => void,
11
+ ): (source: RxObservable<T>) => RxObservable<R> {
12
+ return (source: RxObservable<T>) =>
13
+ new RxObservable<R>((observer) => {
14
+ const subscription = source.subscribe({
15
+ next: (value) => {
16
+ next(value, observer);
17
+ },
18
+ error: (error) => observer.error?.(error),
19
+ complete: () => observer.complete?.(),
20
+ });
21
+
22
+ return () => {
23
+ subscription.unsubscribe();
24
+ };
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Creates a stateful operator. The subscribeFn sets up its own subscriptions
30
+ * and returns a teardown function.
31
+ */
32
+ export function createStatefulOperator<T, R>(
33
+ subscribeFn: (source: RxObservable<T>, observer: Observer<R>) => () => void,
34
+ ): (source: RxObservable<T>) => RxObservable<R> {
35
+ return (source: RxObservable<T>) =>
36
+ new RxObservable<R>((observer) => subscribeFn(source, observer));
37
+ }
@@ -0,0 +1,70 @@
1
+ import { RxObservable } from "../RxObservable";
2
+
3
+ import type { Operator } from "../types";
4
+
5
+ export function debounceTime<T>(duration: number): Operator<T, T> {
6
+ if (!Number.isFinite(duration) || duration < 0) {
7
+ throw new RangeError(
8
+ `debounceTime: duration must be a non-negative finite number, got ${duration}`,
9
+ );
10
+ }
11
+
12
+ return (source: RxObservable<T>) =>
13
+ new RxObservable<T>((observer) => {
14
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
15
+ let latestValue: T;
16
+ let hasValue = false;
17
+
18
+ const subscription = source.subscribe({
19
+ next: (value) => {
20
+ latestValue = value;
21
+ hasValue = true;
22
+
23
+ if (timeoutId !== undefined) {
24
+ clearTimeout(timeoutId);
25
+ }
26
+
27
+ timeoutId = setTimeout(() => {
28
+ /* v8 ignore start -- defensive: timer fired after flush */
29
+ if (hasValue) {
30
+ observer.next?.(latestValue);
31
+ hasValue = false;
32
+ }
33
+ /* v8 ignore stop */
34
+
35
+ timeoutId = undefined;
36
+ }, duration);
37
+ },
38
+ error: (error) => {
39
+ if (timeoutId !== undefined) {
40
+ clearTimeout(timeoutId);
41
+ timeoutId = undefined;
42
+ }
43
+
44
+ observer.error?.(error);
45
+ },
46
+ complete: () => {
47
+ if (timeoutId !== undefined) {
48
+ clearTimeout(timeoutId);
49
+ timeoutId = undefined;
50
+ }
51
+
52
+ if (hasValue) {
53
+ observer.next?.(latestValue);
54
+ hasValue = false;
55
+ }
56
+
57
+ observer.complete?.();
58
+ },
59
+ });
60
+
61
+ return () => {
62
+ if (timeoutId !== undefined) {
63
+ clearTimeout(timeoutId);
64
+ timeoutId = undefined;
65
+ }
66
+
67
+ subscription.unsubscribe();
68
+ };
69
+ });
70
+ }
@@ -0,0 +1,40 @@
1
+ import { createStatefulOperator } from "./createOperator";
2
+
3
+ import type { Operator } from "../types";
4
+
5
+ export function distinctUntilChanged<T>(
6
+ comparator?: (a: T, b: T) => boolean,
7
+ ): Operator<T, T> {
8
+ return createStatefulOperator<T, T>((source, observer) => {
9
+ let hasLast = false;
10
+ let last: T;
11
+ const compare = comparator ?? ((prev: T, curr: T) => prev === curr);
12
+
13
+ const subscription = source.subscribe({
14
+ next: (value) => {
15
+ if (!hasLast) {
16
+ hasLast = true;
17
+ last = value;
18
+ observer.next?.(value);
19
+
20
+ return;
21
+ }
22
+
23
+ try {
24
+ if (!compare(last, value)) {
25
+ last = value;
26
+ observer.next?.(value);
27
+ }
28
+ } catch (error) {
29
+ observer.error?.(error);
30
+ }
31
+ },
32
+ error: (error) => observer.error?.(error),
33
+ complete: () => observer.complete?.(),
34
+ });
35
+
36
+ return () => {
37
+ subscription.unsubscribe();
38
+ };
39
+ });
40
+ }
@@ -0,0 +1,21 @@
1
+ import { createOperator } from "./createOperator";
2
+
3
+ import type { Operator } from "../types";
4
+
5
+ export function filter<T, S extends T>(
6
+ predicate: (value: T) => value is S,
7
+ ): Operator<T, S>;
8
+
9
+ export function filter<T>(predicate: (value: T) => boolean): Operator<T, T>;
10
+
11
+ export function filter<T>(predicate: (value: T) => boolean): Operator<T, T> {
12
+ return createOperator<T, T>((value, observer) => {
13
+ try {
14
+ if (predicate(value)) {
15
+ observer.next?.(value);
16
+ }
17
+ } catch (error) {
18
+ observer.error?.(error);
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,9 @@
1
+ export { map } from "./map";
2
+
3
+ export { filter } from "./filter";
4
+
5
+ export { debounceTime } from "./debounceTime";
6
+
7
+ export { distinctUntilChanged } from "./distinctUntilChanged";
8
+
9
+ export { takeUntil } from "./takeUntil";
@@ -0,0 +1,15 @@
1
+ import { createOperator } from "./createOperator";
2
+
3
+ import type { Operator } from "../types";
4
+
5
+ export function map<T, R>(project: (value: T) => R): Operator<T, R> {
6
+ return createOperator<T, R>((value, observer) => {
7
+ try {
8
+ const result = project(value);
9
+
10
+ observer.next?.(result);
11
+ } catch (error) {
12
+ observer.error?.(error);
13
+ }
14
+ });
15
+ }
@@ -0,0 +1,99 @@
1
+ import { RxObservable } from "../RxObservable";
2
+
3
+ import type { Operator } from "../types";
4
+
5
+ export function takeUntil<T>(notifier: RxObservable<unknown>): Operator<T, T> {
6
+ return (source: RxObservable<T>) =>
7
+ new RxObservable<T>((observer) => {
8
+ // eslint-disable-next-line prefer-const -- assigned after usage in complete()
9
+ let sourceSubscription: ReturnType<typeof source.subscribe> | undefined;
10
+ // eslint-disable-next-line prefer-const -- assigned after usage in complete()
11
+ let notifierSubscription:
12
+ | ReturnType<typeof notifier.subscribe>
13
+ | undefined;
14
+ let completed = false;
15
+
16
+ const complete = () => {
17
+ /* v8 ignore start -- defensive: race condition guard */
18
+ if (completed) {
19
+ return;
20
+ }
21
+ /* v8 ignore stop */
22
+
23
+ completed = true;
24
+
25
+ // sourceSubscription may be undefined if notifier emits synchronously
26
+ if (sourceSubscription) {
27
+ sourceSubscription.unsubscribe();
28
+ }
29
+ // notifierSubscription may be undefined if we're inside notifier.subscribe() call
30
+ if (notifierSubscription) {
31
+ notifierSubscription.unsubscribe();
32
+ }
33
+
34
+ observer.complete?.();
35
+ };
36
+
37
+ notifierSubscription = notifier.subscribe({
38
+ next: () => {
39
+ complete();
40
+ },
41
+ error: (error) => {
42
+ /* v8 ignore start -- defensive: notifier error after completion */
43
+ if (completed) {
44
+ return;
45
+ }
46
+ /* v8 ignore stop */
47
+
48
+ completed = true;
49
+
50
+ // sourceSubscription may be undefined if notifier errors synchronously
51
+ if (sourceSubscription) {
52
+ sourceSubscription.unsubscribe();
53
+ }
54
+
55
+ observer.error?.(error);
56
+ },
57
+ });
58
+
59
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive
60
+ if (completed) {
61
+ return;
62
+ }
63
+
64
+ sourceSubscription = source.subscribe({
65
+ next: (value) => {
66
+ /* v8 ignore start -- defensive: emission after completion */
67
+
68
+ if (!completed) {
69
+ observer.next?.(value);
70
+ }
71
+ /* v8 ignore stop */
72
+ },
73
+ error: (error) => {
74
+ /* v8 ignore start -- defensive: race condition guard */
75
+ if (completed) {
76
+ return;
77
+ }
78
+ /* v8 ignore stop */
79
+
80
+ completed = true;
81
+
82
+ // notifierSubscription is always defined here (notifier subscribes before source)
83
+ notifierSubscription.unsubscribe();
84
+
85
+ observer.error?.(error);
86
+ },
87
+ complete: () => {
88
+ complete();
89
+ },
90
+ });
91
+
92
+ return () => {
93
+ // Both guaranteed defined: notifier subscribes first (line 37),
94
+ // early return on line 60 if sync complete/error, source subscribes after (line 64)
95
+ sourceSubscription.unsubscribe();
96
+ notifierSubscription.unsubscribe();
97
+ };
98
+ });
99
+ }
package/src/state$.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { events } from "@real-router/core";
2
+ import { getPluginApi } from "@real-router/core/api";
3
+
4
+ import { RxObservable } from "./RxObservable";
5
+
6
+ import type { Router, State, SubscribeState } from "@real-router/core";
7
+
8
+ export type { SubscribeState } from "@real-router/core";
9
+
10
+ export function state$(
11
+ router: Router,
12
+ options?: { replay?: boolean },
13
+ ): RxObservable<SubscribeState> {
14
+ const { replay = true } = options ?? {};
15
+
16
+ return new RxObservable<SubscribeState>((observer) => {
17
+ const api = getPluginApi(router);
18
+ const unsubscribe = api.addEventListener(
19
+ events.TRANSITION_SUCCESS,
20
+ (toState: State, fromState: State | undefined) => {
21
+ observer.next?.({ route: toState, previousRoute: fromState });
22
+ },
23
+ );
24
+
25
+ if (replay) {
26
+ const currentState = router.getState();
27
+
28
+ if (currentState) {
29
+ queueMicrotask(() => {
30
+ observer.next?.({ route: currentState, previousRoute: undefined });
31
+ });
32
+ }
33
+ }
34
+
35
+ return unsubscribe;
36
+ });
37
+ }
package/src/types.ts ADDED
@@ -0,0 +1,27 @@
1
+ // Types for @real-router/rx
2
+
3
+ // Import for type reference
4
+ import type { RxObservable } from "./RxObservable";
5
+
6
+ export interface Observer<T> {
7
+ next?: (value: T) => void;
8
+ error?: (err: unknown) => void;
9
+ complete?: () => void;
10
+ }
11
+
12
+ export interface Subscription {
13
+ unsubscribe: () => void;
14
+ readonly closed: boolean;
15
+ }
16
+
17
+ export interface ObservableOptions {
18
+ signal?: AbortSignal;
19
+ replay?: boolean;
20
+ }
21
+
22
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- void required: subscribe fn may have no return statement
23
+ export type SubscribeFn<T> = (observer: Observer<T>) => void | (() => void);
24
+
25
+ export type Operator<T, R> = (source: RxObservable<T>) => RxObservable<R>;
26
+
27
+ export type UnaryFunction<T, R> = (source: T) => R;