Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support for system as actor #4677

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fcd1bca
feat(core): add support for subscribing to system registration events
cevr Jan 13, 2024
ce1cdc7
refactor: put systemId in event, use single set for all subscriptions…
cevr Jan 14, 2024
ace2404
make event type consistent with others
cevr Jan 14, 2024
5c94a82
make implementation consistent with createActor
cevr Jan 15, 2024
d9bd505
update changeset
cevr Jan 15, 2024
69fe023
reuse subscribable type
cevr Jan 15, 2024
afb5807
make system subscribable, and use snapshot
cevr Jan 25, 2024
4315d52
initialize as empty actors first
cevr Jan 25, 2024
5894a57
Merge remote-tracking branch 'upstream/main' into cevr/system-subscribe
cevr Jan 25, 2024
2a68b5b
update changeset
cevr Jan 25, 2024
8d289c3
copy snapshot
cevr Jan 25, 2024
0cbb0c0
ensure new references
cevr Jan 25, 2024
3326fb3
allow useSelector to subscribe to actor system
cevr Jan 25, 2024
dd87510
remove log
cevr Jan 25, 2024
81cf130
update system snapshot on _set, ensure subscribers are only called on…
cevr Jan 25, 2024
61f0b77
add changeset for @xstate/react
cevr Jan 25, 2024
6840952
remove unused imports
cevr Jan 26, 2024
ae2bfee
ensure snapshot is updated for scheduled events as well
cevr Jan 26, 2024
2dff805
collapse test
cevr Jan 26, 2024
6f1dd44
add nested child state to useSelector test
cevr Jan 26, 2024
2f416a1
Merge remote-tracking branch 'upstream/main' into cevr/system-subscribe
cevr Feb 20, 2024
2f0fbd9
Merge remote-tracking branch 'upstream/main' into cevr/system-subscribe
cevr Mar 5, 2024
3fbe432
revert unintended changes
cevr Mar 5, 2024
2c12777
revert unintended changes
cevr Mar 5, 2024
98ac5ee
Merge remote-tracking branch 'upstream/main' into cevr/system-subscribe
cevr Apr 7, 2024
e7d105b
Merge remote-tracking branch 'upstream/main' into cevr/system-subscribe
cevr Apr 23, 2024
299d1da
revert unintended changes
cevr Apr 23, 2024
d98a89f
Merge remote-tracking branch 'upstream/main' into cevr/system-subscribe
cevr May 24, 2024
60d3db7
revert change
cevr May 24, 2024
8eec984
Merge remote-tracking branch 'upstream/main' into cevr/system-subscribe
cevr Jul 29, 2024
fe762fc
Merge remote-tracking branch 'upstream/main' into cevr/system-subscribe
cevr Aug 16, 2024
841d26c
Merge branch 'main' into cevr/system-subscribe
cevr Aug 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/cuddly-days-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@xstate/react': minor
---

`useSelector` now also allows actor.system to be passed in. This works exactly how normally passing an actor would, any changes that cause the selector to change will be automatically up to date.

ex:

```tsx
const deepChildC = useSelector(actorRef.system, (system) => system.actors.c);
```
32 changes: 32 additions & 0 deletions .changeset/tasty-baboons-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'xstate': minor
---

added support for `actor.system.subscribe` to subscribe to changes to the system snapshot.
`actor.system.subscribe` returns a `Subscription` object you can `.unsubscribe` to at any time.

ex:

```js
// observer object
const subscription = actor.system.subscribe({
next: (snapshot) => console.log(snapshot),
error: (err) => console.error(err),
complete: () => console.log('done')
});

// observer parameters
const subscription = actor.system.subscribe(
(snapshot) => console.log(snapshot),
(err) => console.error(err),
() => console.log('done')
);

// callback function
const subscription = actor.system.subscribe((snapshot) =>
console.log(snapshot)
);

// unsubscribe
subscription.unsubscribe();
```
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,15 @@ Read [📽 the slides](http://slides.com/davidkhourshid/finite-state-machines) (

## Packages

| Package | Description |
| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| 🤖 `xstate` | Core finite state machine and statecharts library + interpreter |
| [📉 `@xstate/graph`](https://github.com/statelyai/xstate/tree/main/packages/xstate-graph) | Graph traversal and model-based testing utilities using XState |
| [⚛️ `@xstate/react`](https://github.com/statelyai/xstate/tree/main/packages/xstate-react) | React hooks and utilities for using XState in React applications |
| [💚 `@xstate/vue`](https://github.com/statelyai/xstate/tree/main/packages/xstate-vue) | Vue composition functions and utilities for using XState in Vue applications |
| [🎷 `@xstate/svelte`](https://github.com/statelyai/xstate/tree/main/packages/xstate-svelte) | Svelte utilities for using XState in Svelte applications |
| [🥏 `@xstate/solid`](https://github.com/statelyai/xstate/tree/main/packages/xstate-solid) | Solid hooks and utilities for using XState in Solid applications |
| [🔍 `@statelyai/inspect`](https://github.com/statelyai/inspect) | Inspection utilities for XState |
| Package | Description |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| 🤖 `xstate` | Core finite state machine and statecharts library + interpreter |
| [📉 `@xstate/graph`](https://github.com/statelyai/xstate/tree/main/packages/xstate-graph) | Graph traversal and model-based testing utilities using XState |
| [⚛️ `@xstate/react`](https://github.com/statelyai/xstate/tree/main/packages/xstate-react) | React hooks and utilities for using XState in React applications |
| [💚 `@xstate/vue`](https://github.com/statelyai/xstate/tree/main/packages/xstate-vue) | Vue composition functions and utilities for using XState in Vue applications |
| [🎷 `@xstate/svelte`](https://github.com/statelyai/xstate/tree/main/packages/xstate-svelte) | Svelte utilities for using XState in Svelte applications |
| [🥏 `@xstate/solid`](https://github.com/statelyai/xstate/tree/main/packages/xstate-solid) | Solid hooks and utilities for using XState in Solid applications |
| [🔍 `@statelyai/inspect`](https://github.com/statelyai/inspect) | Inspection utilities for XState |

## Finite State Machines

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type {
export { setup } from './setup.ts';
export { type Spawner } from './spawn.ts';
export { getStateNodes } from './stateUtils.ts';
export type { ActorSystem } from './system.ts';
export type { ActorSystem, AnyActorSystem, SystemSnapshot } from './system.ts';
export { toPromise } from './toPromise.ts';
export * from './types.ts';
export {
Expand Down
127 changes: 111 additions & 16 deletions packages/core/src/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
AnyActorRef,
Observer,
Snapshot,
Subscribable,
HomomorphicOmit,
EventObject,
AnyTransitionDefinition,
Expand Down Expand Up @@ -38,6 +39,11 @@ interface Scheduler {
cancelAll(actorRef: AnyActorRef): void;
}

export interface SystemSnapshot {
_scheduledEvents: Record<ScheduledEventId, ScheduledEvent>;
actors: Record<string, AnyActorRef>;
}

type ScheduledEventId = string & { __scheduledEventId: never };

function createScheduledEventId(
Expand All @@ -47,7 +53,8 @@ function createScheduledEventId(
return `${actorRef.sessionId}.${id}` as ScheduledEventId;
}

export interface ActorSystem<T extends ActorSystemInfo> {
export interface ActorSystem<T extends ActorSystemInfo>
extends Subscribable<SystemSnapshot> {
/** @internal */
_bookId: () => string;
/** @internal */
Expand All @@ -74,13 +81,9 @@ export interface ActorSystem<T extends ActorSystemInfo> {
event: AnyEventObject
) => void;
scheduler: Scheduler;
getSnapshot: () => {
_scheduledEvents: Record<string, ScheduledEvent>;
};
getSnapshot: () => SystemSnapshot;
/** @internal */
_snapshot: {
_scheduledEvents: Record<ScheduledEventId, ScheduledEvent>;
};
_snapshot: SystemSnapshot;
start: () => void;
_clock: Clock;
_logger: (...args: any[]) => void;
Expand All @@ -101,6 +104,7 @@ export function createSystem<T extends ActorSystemInfo>(
const keyedActors = new Map<keyof T['actors'], AnyActorRef | undefined>();
const reverseKeyedActors = new WeakMap<AnyActorRef, keyof T['actors']>();
const inspectionObservers = new Set<Observer<InspectionEvent>>();
const systemObservers = new Set<Observer<SystemSnapshot>>();
const timerMap: { [id: ScheduledEventId]: number } = {};
const { clock, logger } = options;

Expand All @@ -121,11 +125,27 @@ export function createSystem<T extends ActorSystemInfo>(
startedAt: Date.now()
};
const scheduledEventId = createScheduledEventId(source, id);
system._snapshot._scheduledEvents[scheduledEventId] = scheduledEvent;
const snapshot = system.getSnapshot();
updateSnapshot({
_scheduledEvents: {
...snapshot._scheduledEvents,
[scheduledEventId]: scheduledEvent
},
actors: { ...snapshot.actors }
});

const timeout = clock.setTimeout(() => {
delete timerMap[scheduledEventId];
delete system._snapshot._scheduledEvents[scheduledEventId];
const {
_scheduledEvents: { [scheduledEventId]: _, ..._scheduledEvents },
actors
} = system.getSnapshot();
updateSnapshot({
_scheduledEvents: {
..._scheduledEvents
},
actors: { ...snapshot.actors }
});

system._relay(source, target, event);
}, delay);
Expand All @@ -137,7 +157,16 @@ export function createSystem<T extends ActorSystemInfo>(
const timeout = timerMap[scheduledEventId];

delete timerMap[scheduledEventId];
delete system._snapshot._scheduledEvents[scheduledEventId];
const {
_scheduledEvents: { [scheduledEventId]: _, ..._scheduledEvents },
actors
} = system.getSnapshot();
updateSnapshot({
_scheduledEvents: {
..._scheduledEvents
},
actors: { ...actors }
});

if (timeout !== undefined) {
clock.clearTimeout(timeout);
Expand Down Expand Up @@ -168,14 +197,36 @@ export function createSystem<T extends ActorSystemInfo>(
);
};

function updateSnapshot(snapshot: SystemSnapshot) {
system._snapshot = snapshot;
systemObservers.forEach((listener) => {
listener.next?.(snapshot);
});
}

const system: ActorSystem<T> = {
_snapshot: {
_scheduledEvents:
(options?.snapshot && (options.snapshot as any).scheduler) ?? {}
(options?.snapshot && (options.snapshot as any).scheduler) ?? {},
actors: {}
},

_bookId: () => `x:${idCounter++}`,
_register: (sessionId, actorRef) => {
children.set(sessionId, actorRef);
const systemId = reverseKeyedActors.get(actorRef);
if (systemId !== undefined) {
const currentSnapshot = system.getSnapshot();
if (currentSnapshot.actors[systemId as any] !== actorRef) {
updateSnapshot({
_scheduledEvents: { ...currentSnapshot._scheduledEvents },
actors: {
...currentSnapshot.actors,
[systemId]: actorRef
}
});
}
}
return sessionId;
},
_unregister: (actorRef) => {
Expand All @@ -185,11 +236,40 @@ export function createSystem<T extends ActorSystemInfo>(
if (systemId !== undefined) {
keyedActors.delete(systemId);
reverseKeyedActors.delete(actorRef);
const {
_scheduledEvents,
actors: { [systemId]: _, ...actors }
} = system.getSnapshot();
updateSnapshot({
_scheduledEvents: { ..._scheduledEvents },
actors
});
}
},
get: (systemId) => {
return keyedActors.get(systemId) as T['actors'][any];
},
subscribe: (
nextListenerOrObserver:
| ((event: SystemSnapshot) => void)
| Observer<SystemSnapshot>,
errorListener?: (error: any) => void,
completeListener?: () => void
) => {
const observer = toObserver(
nextListenerOrObserver,
errorListener,
completeListener
);

systemObservers.add(observer);

return {
unsubscribe: () => {
systemObservers.delete(observer);
}
};
},
_set: (systemId, actorRef) => {
const existing = keyedActors.get(systemId);
if (existing && existing !== actorRef) {
Expand All @@ -200,6 +280,16 @@ export function createSystem<T extends ActorSystemInfo>(

keyedActors.set(systemId, actorRef);
reverseKeyedActors.set(actorRef, systemId);
const currentSnapshot = system.getSnapshot();
if (currentSnapshot.actors[systemId as any] !== actorRef) {
updateSnapshot({
_scheduledEvents: { ...system._snapshot._scheduledEvents },
actors: {
...system._snapshot.actors,
[systemId]: actorRef
}
});
}
},
inspect: (observerOrFn) => {
const observer = toObserver(observerOrFn);
Expand All @@ -225,15 +315,20 @@ export function createSystem<T extends ActorSystemInfo>(
scheduler,
getSnapshot: () => {
return {
_scheduledEvents: { ...system._snapshot._scheduledEvents }
_scheduledEvents: { ...system._snapshot._scheduledEvents },
actors: { ...system._snapshot.actors }
};
},

start: () => {
const scheduledEvents = system._snapshot._scheduledEvents;
system._snapshot._scheduledEvents = {};
for (const scheduledId in scheduledEvents) {
const { _scheduledEvents } = system.getSnapshot();
updateSnapshot({
_scheduledEvents: {},
actors: { ...system._snapshot.actors }
});
for (const scheduledId in _scheduledEvents) {
const { source, target, event, delay, id } =
scheduledEvents[scheduledId as ScheduledEventId];
_scheduledEvents[scheduledId as ScheduledEventId];
scheduler.schedule(source, target, event, delay, id);
}
},
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { PromiseActorLogic } from './actors/promise.ts';
import { Guard, GuardPredicate, UnknownGuard } from './guards.ts';
import type { Actor, ProcessingStatus } from './createActor.ts';
import { Spawner } from './spawn.ts';
import { AnyActorSystem, Clock } from './system.js';
import { ActorSystem, AnyActorSystem, Clock } from './system.js';
import { InspectionEvent } from './inspection.ts';

export type Identity<T> = { [K in keyof T]: T[K] };
Expand Down Expand Up @@ -2296,7 +2296,9 @@ export type SnapshotFrom<T> = ReturnTypeOrValue<T> extends infer R
infer _TSystem
>
? TSnapshot
: never
: R extends ActorSystem<infer _>
? ReturnType<R['getSnapshot']>
: never
: never;

export type EventFromLogic<TLogic extends AnyActorLogic> =
Expand Down
61 changes: 61 additions & 0 deletions packages/core/test/system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,4 +535,65 @@ describe('system', () => {

expect(spy).toHaveBeenCalledTimes(1);
});

it('should allow subscriptions to a system', () => {
const aSystemId = 'a_child';
const bSystemId = 'b_child';
const machine = createMachine({
initial: 'a',
states: {
a: {
invoke: {
src: createMachine({}),
systemId: aSystemId
},
on: {
to_b: 'b'
}
},
b: {
invoke: {
src: createMachine({}),
systemId: bSystemId
},
on: {
to_a: 'a'
}
}
}
});

const actorRef = createActor(machine).start();

const keysOverTime: string[][] = [
Object.keys(actorRef.system.getSnapshot().actors)
];
const unsubscribedKeysOverTime: string[][] = [
Object.keys(actorRef.system.getSnapshot().actors)
];

const subscription = actorRef.system.subscribe((event) => {
keysOverTime.push(Object.keys(event.actors));
});

actorRef.system.subscribe((event) => {
unsubscribedKeysOverTime.push(Object.keys(event.actors));
});

actorRef.send({ type: 'to_b' });
expect(keysOverTime).toEqual([['a_child'], [], ['b_child']]);
expect(unsubscribedKeysOverTime).toEqual([['a_child'], [], ['b_child']]);

subscription.unsubscribe();
actorRef.send({ type: 'to_a' });

expect(keysOverTime).toEqual([['a_child'], [], ['b_child']]);
expect(unsubscribedKeysOverTime).toEqual([
['a_child'],
[],
['b_child'],
[],
['a_child']
]);
});
});
Loading