Skip to content

Commit

Permalink
Scheduler (#4497)
Browse files Browse the repository at this point in the history
* system.scheduler

* Relay

* Remove cancel from Actor

* Remove delayedEventsMap

* Remove delaySend

* Fix this

* Add failing test

* This fixes timer restoration in React but is really hacky

* small tweaks

* tweak expected call in a test

* Fixed `to` param retries

* delete before relaying

* Fixed `SimulatedClock`

* reset `_flushing` at the end of the flush 🤦‍♂️

* Initial draft for rehydratable timers

* Fixed the React test case

* Bake scheduler into system

* Fix types

* Remove xstate.system for now

* Treat system like actor

* Make `system` property required on the `ActorRef`

* sYmMeTrY

* Move scheduler to a separate variable to improve readability

* `defer` schedules and cancels

* Unique IDs

* Change the signature of `schedule` (#4562)

* move test

* Update packages/core/src/system.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* remove unused `getSystemPath`

* improve compat between mismatched versions of `xstate` and `@xstate/react`

* Changesets

---------

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
davidkpiano and Andarist committed Dec 6, 2023
1 parent 8081801 commit d7f2202
Show file tree
Hide file tree
Showing 22 changed files with 484 additions and 230 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-papayas-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'xstate': minor
---

Internal: abstract the scheduler for delayed events so that it is handled centrally by the `system`.
5 changes: 5 additions & 0 deletions .changeset/itchy-moles-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xstate/react': patch
---

Fix an issue where `after` transitions do not work in React strict mode. Delayed events (including from `after` transitions) should now work as expected in all React modes.
1 change: 0 additions & 1 deletion docs/about/goals.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,4 @@ If you're deciding if you should use XState, [John Yanarella](https://github.com
> The front-end development world is the wild west, and it could stand to learn from what other engineering disciplines have known and employed for years.
>
> 3. It has **passed a critical threshold of maturity** as of version 4, particularly given the introduction of [the visualizer](https://statecharts.github.io/xstate-viz). And that's just the tip of the iceberg of where it could go next, as it (and [its community](https://github.com/statelyai/xstate/discussions)) introduces tooling that takes advantage of how a statechart can be visualized, analyzed, and tested.
>
> 4. The **community** that is growing around it and the awareness it is bringing to finite state machines and statecharts. If you read back through this gitter history, there's a wealth of links to research papers, other FSM and Statechart implementations, etc. that have been collected by [Erik Mogensen](https://twitter.com/mogsie) over at [statecharts.github.io](https://statecharts.github.io).
31 changes: 17 additions & 14 deletions docs/recipes/svelte.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,21 @@ export const toggleMachine = createMachine({

```html
<script>
import {interpret} from 'xstate';
import {toggleMachine} from './machine';
import { interpret } from 'xstate';
import { toggleMachine } from './machine';
let current;
let current;
const toggleService = interpret(toggleMachine)
.onTransition((state) => {
current = state;
}).start()
const toggleService = interpret(toggleMachine)
.onTransition((state) => {
current = state;
})
.start();
</script>

<button on:click={() => toggleService.send({ type: 'TOGGLE' })}>
{current.matches('inactive') ? 'Off' : 'On'}
<button on:click="{()" ="">
toggleService.send({ type: 'TOGGLE' })}> {current.matches('inactive') ? 'Off'
: 'On'}
</button>
```

Expand All @@ -81,14 +83,15 @@ The toggleService has a `.subscribe` function that is similar to Svelte stores,

```html
<script>
import {interpret} from 'xstate';
import {toggleMachine} from './machine';
import { interpret } from 'xstate';
import { toggleMachine } from './machine';
const toggleService = interpret(toggleMachine).start();
const toggleService = interpret(toggleMachine).start();
</script>

<button on:click={() => toggleService.send({ type: 'TOGGLE' })}>
{$toggleService.matches('inactive') ? 'Off' : 'On'}
<button on:click="{()" ="">
toggleService.send({ type: 'TOGGLE' })}> {$toggleService.matches('inactive') ?
'Off' : 'On'}
</button>
```

Expand Down
41 changes: 31 additions & 10 deletions packages/core/src/SimulatedClock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Clock } from './interpreter.ts';
import { Clock } from './system.ts';

export interface SimulatedClock extends Clock {
start(speed: number): void;
Expand All @@ -15,13 +15,17 @@ export class SimulatedClock implements SimulatedClock {
private timeouts: Map<number, SimulatedTimeout> = new Map();
private _now: number = 0;
private _id: number = 0;
private _flushing = false;
private _flushingInvalidated = false;

public now() {
return this._now;
}
private getId() {
return this._id++;
}
public setTimeout(fn: (...args: any[]) => void, timeout: number) {
this._flushingInvalidated = this._flushing;
const id = this.getId();
this.timeouts.set(id, {
start: this.now(),
Expand All @@ -31,6 +35,7 @@ export class SimulatedClock implements SimulatedClock {
return id;
}
public clearTimeout(id: number) {
this._flushingInvalidated = this._flushing;
this.timeouts.delete(id);
}
public set(time: number) {
Expand All @@ -42,18 +47,34 @@ export class SimulatedClock implements SimulatedClock {
this.flushTimeouts();
}
private flushTimeouts() {
[...this.timeouts]
.sort(([_idA, timeoutA], [_idB, timeoutB]) => {
if (this._flushing) {
this._flushingInvalidated = true;
return;
}
this._flushing = true;

const sorted = [...this.timeouts].sort(
([_idA, timeoutA], [_idB, timeoutB]) => {
const endA = timeoutA.start + timeoutA.timeout;
const endB = timeoutB.start + timeoutB.timeout;
return endB > endA ? -1 : 1;
})
.forEach(([id, timeout]) => {
if (this.now() - timeout.start >= timeout.timeout) {
this.timeouts.delete(id);
timeout.fn.call(null);
}
});
}
);

for (const [id, timeout] of sorted) {
if (this._flushingInvalidated) {
this._flushingInvalidated = false;
this._flushing = false;
this.flushTimeouts();
return;
}
if (this.now() - timeout.start >= timeout.timeout) {
this.timeouts.delete(id);
timeout.fn.call(null);
}
}

this._flushing = false;
}
public increment(ms: number): void {
this._now += ms;
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/actions/cancel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ function resolveCancel(
}

function executeCancel(actorScope: AnyActorScope, resolvedSendId: string) {
(actorScope.self as AnyActor).cancel(resolvedSendId);
actorScope.defer(() => {
actorScope.system.scheduler.cancel(actorScope.self, resolvedSendId);
});
}

export interface CancelAction<
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/actions/raise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,12 @@ function executeRaise(
delay: number | undefined;
}
) {
if (typeof params.delay === 'number') {
(actorScope.self as AnyActor).delaySend(
params as typeof params & { delay: number }
);
const { event, delay, id } = params;
if (typeof delay === 'number') {
actorScope.defer(() => {
const self = actorScope.self;
actorScope.system.scheduler.schedule(self, self, event, delay, id);
});
return;
}
}
Expand Down
37 changes: 20 additions & 17 deletions packages/core/src/actions/send.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import isDevelopment from '#is-development';
import { XSTATE_ERROR } from '../constants.ts';
import { createErrorActorEvent } from '../eventUtils.ts';
import {
ActionArgs,
ActorRef,
AnyActorScope,
AnyActorRef,
AnyActorScope,
AnyEventObject,
AnyActor,
AnyMachineSnapshot,
Cast,
DelayExpr,
EventFrom,
EventObject,
InferEvent,
MachineContext,
NoInfer,
ParameterizedObject,
SendExpr,
SendToActionOptions,
SendToActionParams,
SpecialTargets,
UnifiedArg,
ParameterizedObject,
NoInfer
UnifiedArg
} from '../types.ts';
import { XSTATE_ERROR } from '../constants.ts';

function resolveSendTo(
actorScope: AnyActorScope,
Expand Down Expand Up @@ -145,20 +143,25 @@ function executeSendTo(
delay: number | undefined;
}
) {
if (typeof params.delay === 'number') {
(actorScope.self as AnyActor).delaySend(
params as typeof params & { delay: number }
);
return;
}

// this forms an outgoing events queue
// thanks to that the recipient actors are able to read the *updated* snapshot value of the sender
actorScope.defer(() => {
const { to, event } = params;
actorScope?.system._relay(
const { to, event, delay, id } = params;
if (typeof delay === 'number') {
actorScope.system.scheduler.schedule(
actorScope.self,
to,
event,
delay,
id
);
return;
}
actorScope.system._relay(
actorScope.self,
to,
// at this point, in a deferred task, it should already be mutated by retryResolveSendTo
// if it initially started as a string
to as Exclude<typeof to, string>,
event.type === XSTATE_ERROR
? createErrorActorEvent(actorScope.self.id, (event as any).data)
: event
Expand Down
15 changes: 7 additions & 8 deletions packages/core/src/actors/callback.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { XSTATE_STOP } from '../constants.ts';
import { AnyActorSystem } from '../system.ts';
import {
ActorLogic,
EventObject,
AnyActorSystem,
AnyEventObject,
ActorSystem,
ActorRefFrom,
Snapshot,
AnyActorRef,
NonReducibleUnknown
AnyEventObject,
EventObject,
NonReducibleUnknown,
Snapshot
} from '../types';
import { XSTATE_STOP } from '../constants.ts';

interface CallbackInstanceState<TEvent extends EventObject> {
receivers: Set<(e: TEvent) => void> | undefined;
Expand All @@ -28,7 +27,7 @@ export type CallbackSnapshot<TInput> = Snapshot<undefined> & {
export type CallbackActorLogic<
TEvent extends EventObject,
TInput = NonReducibleUnknown
> = ActorLogic<CallbackSnapshot<TInput>, TEvent, TInput, ActorSystem<any>>;
> = ActorLogic<CallbackSnapshot<TInput>, TEvent, TInput, AnyActorSystem>;

export type CallbackActorRef<
TEvent extends EventObject,
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/actors/observable.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { XSTATE_STOP } from '../constants';
import { AnyActorSystem } from '../system.ts';
import {
Subscribable,
ActorLogic,
EventObject,
Subscription,
AnyActorSystem,
ActorRefFrom,
EventObject,
NonReducibleUnknown,
Snapshot,
NonReducibleUnknown
Subscribable,
Subscription
} from '../types';

const XSTATE_OBSERVABLE_NEXT = 'xstate.observable.next';
Expand Down
9 changes: 4 additions & 5 deletions packages/core/src/actors/promise.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { XSTATE_STOP } from '../constants';
import { XSTATE_STOP } from '../constants.ts';
import { AnyActorSystem } from '../system.ts';
import {
ActorLogic,
ActorRefFrom,
ActorSystem,
AnyActorSystem,
NonReducibleUnknown,
Snapshot
} from '../types';
} from '../types.ts';

export type PromiseSnapshot<TOutput, TInput> = Snapshot<TOutput> & {
input: TInput | undefined;
Expand All @@ -19,7 +18,7 @@ export type PromiseActorLogic<TOutput, TInput = unknown> = ActorLogic<
PromiseSnapshot<TOutput, TInput>,
{ type: string; [k: string]: unknown },
TInput, // input
ActorSystem<any>
AnyActorSystem
>;

export type PromiseActorRef<TOutput> = ActorRefFrom<
Expand Down
13 changes: 6 additions & 7 deletions packages/core/src/actors/transition.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { AnyActorSystem } from '../system.ts';
import {
ActorLogic,
ActorRefFrom,
ActorScope,
ActorSystem,
EventObject,
ActorRefFrom,
AnyActorSystem,
Snapshot,
NonReducibleUnknown
} from '../types';
NonReducibleUnknown,
Snapshot
} from '../types.ts';

export type TransitionSnapshot<TContext> = Snapshot<undefined> & {
context: TContext;
Expand Down Expand Up @@ -87,7 +86,7 @@ export type TransitionActorRef<
export function fromTransition<
TContext,
TEvent extends EventObject,
TSystem extends ActorSystem<any>,
TSystem extends AnyActorSystem,
TInput extends NonReducibleUnknown
>(
transition: (
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export type {
InspectedActorEvent,
InspectedEventEvent,
InspectedSnapshotEvent,
InspectionEvent
InspectionEvent,
ActorSystem
} from './system.ts';

export { and, not, or, stateIn } from './guards.ts';
Expand Down

0 comments on commit d7f2202

Please sign in to comment.