From 04bb6f822d2699bf665a4af9c42f729d5ed972e4 Mon Sep 17 00:00:00 2001 From: Farzad Yousefzadeh Date: Thu, 9 Oct 2025 18:43:53 +0300 Subject: [PATCH 1/2] Expose subscription as an inspection event --- packages/core/src/createActor.ts | 14 +++++-- packages/core/src/inspection.ts | 15 ++++++-- packages/core/src/types.ts | 1 + packages/core/src/utils.ts | 6 ++- packages/core/test/inspect.test.ts | 62 +++++++++++++++++++++++++++++- 5 files changed, 89 insertions(+), 9 deletions(-) diff --git a/packages/core/src/createActor.ts b/packages/core/src/createActor.ts index 3da242f7ab..0abfbe1386 100644 --- a/packages/core/src/createActor.ts +++ b/packages/core/src/createActor.ts @@ -415,23 +415,31 @@ export class Actor public subscribe( nextListener?: (snapshot: SnapshotFrom) => void, errorListener?: (error: any) => void, - completeListener?: () => void + completeListener?: () => void, + observerId?: string ): Subscription; public subscribe( nextListenerOrObserver?: | ((snapshot: SnapshotFrom) => void) | Observer>, errorListener?: (error: any) => void, - completeListener?: () => void + completeListener?: () => void, + observerId?: string ): Subscription { const observer = toObserver( nextListenerOrObserver, errorListener, - completeListener + completeListener, + observerId ); if (this._processingStatus !== ProcessingStatus.Stopped) { this.observers.add(observer); + this._actorScope.system._sendInspectionEvent({ + type: '@xstate.subscription', + actorRef: this, + subscriptionId: observer.id + }); } else { switch ((this._snapshot as any).status) { case 'done': diff --git a/packages/core/src/inspection.ts b/packages/core/src/inspection.ts index 82af76dbd9..a5907917b9 100644 --- a/packages/core/src/inspection.ts +++ b/packages/core/src/inspection.ts @@ -10,9 +10,10 @@ export type InspectionEvent = | InspectedEventEvent | InspectedActorEvent | InspectedMicrostepEvent - | InspectedActionEvent; + | InspectedActionEvent + | InspectedSubscriptionEvent; -interface BaseInspectionEventProperties { +interface BaseInspectionEventProperties { rootId: string; // the session ID of the root /** * The relevant actorRef for the inspection event. @@ -20,8 +21,10 @@ interface BaseInspectionEventProperties { * - For snapshot events, this is the `actorRef` of the snapshot. * - For event events, this is the target `actorRef` (recipient of event). * - For actor events, this is the `actorRef` of the registered actor. + * - For subscription events, this is the `actorRef` of the actor that is being + * subscribed to. */ - actorRef: ActorRefLike; + actorRef: T; } export interface InspectedSnapshotEvent extends BaseInspectionEventProperties { @@ -54,6 +57,12 @@ export interface InspectedEventEvent extends BaseInspectionEventProperties { event: AnyEventObject; // { type: string, ... } } +export interface InspectedSubscriptionEvent + extends BaseInspectionEventProperties { + type: '@xstate.subscription'; + subscriptionId: string | undefined; +} + export interface InspectedActorEvent extends BaseInspectionEventProperties { type: '@xstate.actor'; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 733f92574e..062aa3792b 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1905,6 +1905,7 @@ export type Observer = { next?: (value: T) => void; error?: (err: unknown) => void; complete?: () => void; + id?: string; }; export interface Subscription { diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index a7152562dc..92b2d36f9e 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -234,7 +234,8 @@ export function normalizeTarget< export function toObserver( nextHandler?: Observer | ((value: T) => void), errorHandler?: (error: any) => void, - completionHandler?: () => void + completionHandler?: () => void, + observerId?: string ): Observer { const isObserver = typeof nextHandler === 'object'; const self = isObserver ? nextHandler : undefined; @@ -244,7 +245,8 @@ export function toObserver( error: (isObserver ? nextHandler.error : errorHandler)?.bind(self), complete: (isObserver ? nextHandler.complete : completionHandler)?.bind( self - ) + ), + id: observerId }; } diff --git a/packages/core/test/inspect.test.ts b/packages/core/test/inspect.test.ts index b7e2283adf..0bec54bf18 100644 --- a/packages/core/test/inspect.test.ts +++ b/packages/core/test/inspect.test.ts @@ -11,7 +11,10 @@ import { raise, setup } from '../src'; -import { InspectedActionEvent } from '../src/inspection'; +import { + InspectedActionEvent, + InspectedSubscriptionEvent +} from '../src/inspection'; function simplifyEvents( inspectionEvents: InspectionEvent[], @@ -65,6 +68,14 @@ function simplifyEvents( action: inspectionEvent.action }; } + + if (inspectionEvent.type === '@xstate.subscription') { + return { + type: inspectionEvent.type, + actorSystemId: inspectionEvent.actorRef?.systemId, + subscriptionId: inspectionEvent.subscriptionId + }; + } }); } @@ -1044,6 +1055,55 @@ describe('inspect', () => { `); }); + it('should inspect subscriptions', () => { + const events: InspectedSubscriptionEvent[] = []; + + const machine = setup({}).createMachine({}); + + const actor = createActor(machine, { + inspect: (ev) => { + if (ev.type === '@xstate.subscription') { + events.push(ev); + } + }, + systemId: 'actor1' // making sure that the systemId is included in the event + }); + + actor.start(); + actor.subscribe( + (snapshot) => { + console.log('sub1', snapshot); + }, + undefined, + undefined, + 'sub1' + ); + actor.subscribe( + (snapshot) => { + console.log('sub2', snapshot); + }, + undefined, + undefined, + 'sub2' + ); + + expect(simplifyEvents(events, (ev) => ev.type === '@xstate.subscription')) + .toMatchInlineSnapshot(` + [ + { + "actorSystemId": "actor1", + "subscriptionId": "sub1", + "type": "@xstate.subscription", + }, + { + "actorSystemId": "actor1", + "subscriptionId": "sub2", + "type": "@xstate.subscription", + }, + ] + `); + }); + it('@xstate.microstep inspection events should report no transitions if an unknown event was sent', () => { const machine = createMachine({}); expect.assertions(1); From 901335e6a0275517ad8f88b58e7a577d764007ac Mon Sep 17 00:00:00 2001 From: Farzad Yousefzadeh Date: Thu, 9 Oct 2025 19:02:02 +0300 Subject: [PATCH 2/2] changeset --- .changeset/many-teeth-cough.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .changeset/many-teeth-cough.md diff --git a/.changeset/many-teeth-cough.md b/.changeset/many-teeth-cough.md new file mode 100644 index 0000000000..261326fddc --- /dev/null +++ b/.changeset/many-teeth-cough.md @@ -0,0 +1,30 @@ +--- +'xstate': minor +--- + +Exposes actor subscription as an inspection event. + +```ts +import { createMachine, createActor } from 'xstate'; + +const machine = createMachine({ + // ... +}); + +const actor = createActor(machine, { + inspect: (event) => { + // event.type === '@xstate.subscription' + // event.actorRef === actor to which the subscription belongs to + // event.subscriptionId === 'my-observer-id' + } +}); + +actor.subscribe( + (snapshot) => { + console.log(snapshot); + }, + undefined, + undefined, + 'my-observer-id' +); +```