Skip to content

Commit

Permalink
Recursive persistence of invoked actors (#3743)
Browse files Browse the repository at this point in the history
* Add PersistedState

* behavior.at (WIP)

* Use invoke action instead

* Ensure that persisted state isn't lost

* Allow machine.at(...) to take optional arg

* Cleanup

* Renaming

* Fix types

* No more arguments for .start()

* Improve types/tests

* Add input with loose types

* Update packages/core/src/StateMachine.ts

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

* Move input closer to the interpreter (#3803)

* Update packages/core/src/types.ts

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

* Remove UseMachineOptions (React)

* Remove options from other libraries

* Remove public usage of .at(...) in favor of { state: ... }

* Remove .at() (mostly)

* Lint

* Remove input from this PR

* Oops

* Remove machine.at

* Remove state

* Use `machine.getPersistedState`

* public preInitialState -> private _preInitialState

* Remove _preInitialState

* Clarifying comment

* getPersistedState should use initial state

* Use .js at the end

* start returns void

* Add todo

* Lint

* Attempt to fix types

* Ugh types

* Address comments

* Add changeset

* Update .changeset/shy-cobras-ring.md

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

* Remove constraint

* Update packages/core/src/State.ts

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

* Don't do the hacky thing

* Renaming etc

* Change PersistedMachineState children shape

* Small tweaks

* Change test, remove action unshifting for starting actors

* Add skipped test

* Do better here (PersistedMachineState)

* Remove comment

* Fix promise test

* Fix everything

* Add src

* Fix types

* Rename PersistedFrom -> PersistedStateFrom

* Sigh, types

* self != this

* Add test case: initial state of a child is available before starting the parent

* Apply suggestion

* Update packages/core/src/StateNode.ts

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

* Update packages/core/src/StateMachine.ts

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

* Update packages/core/src/types.ts

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

* Made the test work

* fixed types

* Initialize the `Interpreter`'s state eagerly (#3874)

* Initialize the `Interpreter`'s state eagerly

* Fixed parent assignment on children

* remove what was supposed to be removed

* pass parent to the spawner within assign

* pass parent to the initial context factory

* Reassign the old state after stopping the machine in React hooks

* fixed things

* fixed tests

* move a comment back to its original place to minimize the diff

* Remove `persisted: true` check

* Do not restart a completed observable actor

* Update packages/core/test/actor.test.ts

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

* Update packages/core/test/actor.test.ts

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

* Update packages/core/test/actor.test.ts

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

* Update packages/core/test/actor.test.ts

* Add event observable test

* Fix types

* Change back

* Add changeset

* Add another changeset

---------

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
davidkpiano and Andarist committed Mar 14, 2023
1 parent 2d25c9a commit 30c561e
Show file tree
Hide file tree
Showing 44 changed files with 952 additions and 484 deletions.
24 changes: 24 additions & 0 deletions .changeset/fast-rockets-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'xstate': major
---

Restoring persisted state is now done by passing the state into the `state: ...` property of the `interpret` options argument:

```diff
-interpret(machine).start(state);
+interpret(machine, { state }).start();
```

The persisted state is obtained from an actor by calling `actor.getPersistedState()`:

```ts
const actor = interpret(machine).start();

const persistedState = actor.getPersistedState();

// ...

const restoredActor = interpret(machine, {
state: persistedState
}).start();
```
10 changes: 10 additions & 0 deletions .changeset/heavy-ties-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'xstate': major
---

Invoked actors can now be deeply persisted and restored. When the persisted state of an actor is obtained via `actor.getPersistedState()`, the states of all invoked actors are also persisted, if possible. This state can be restored by passing the persisted state into the `state: ...` property of the `interpret` options argument:

```diff
-interpret(machine).start(state);
+interpret(machine, { state }).start();
```
2 changes: 2 additions & 0 deletions .changeset/shy-cobras-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,9 @@ const lightMachine = createMachine({

const currentState = 'green';

const nextState = lightMachine.transition(currentState, { type: 'TIMER' })
.value;
const nextState = lightMachine.transition(currentState, {
type: 'TIMER'
}).value;

// => 'yellow'
```
Expand Down Expand Up @@ -278,8 +279,9 @@ const lightMachine = createMachine({

const currentState = 'yellow';

const nextState = lightMachine.transition(currentState, { type: 'TIMER' })
.value;
const nextState = lightMachine.transition(currentState, {
type: 'TIMER'
}).value;
// => {
// red: 'walk'
// }
Expand Down Expand Up @@ -374,8 +376,9 @@ const wordMachine = createMachine({
}
});

const boldState = wordMachine.transition('bold.off', { type: 'TOGGLE_BOLD' })
.value;
const boldState = wordMachine.transition('bold.off', {
type: 'TOGGLE_BOLD'
}).value;

// {
// bold: 'on',
Expand Down
19 changes: 10 additions & 9 deletions docs/fr/guides/communication.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,23 +131,24 @@ The resolved data is placed into a `'done.invoke.<id>'` event, under the `data`
If a Promise rejects, the `onError` transition will be taken with a `{ type: 'error.platform' }` event. The error data is available on the event's `data` property:

```js
const search = (context, event) => new Promise((resolve, reject) => {
if (!event.query.length) {
return reject('No query specified');
// or:
// throw new Error('No query specified');
}
const search = (context, event) =>
new Promise((resolve, reject) => {
if (!event.query.length) {
return reject('No query specified');
// or:
// throw new Error('No query specified');
}

return resolve(getSearchResults(event.query));
});
return resolve(getSearchResults(event.query));
});

// ...
const searchMachine = createMachine({
id: 'search',
initial: 'idle',
context: {
results: undefined,
errorMessage: undefined,
errorMessage: undefined
},
states: {
idle: {
Expand Down
19 changes: 10 additions & 9 deletions docs/zh/guides/communication.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,23 +131,24 @@ const userMachine = createMachine({
如果 Promise 拒绝,则将使用 `{ type: 'error.platform' }` 事件进行 `onError` 转换。 错误数据在事件的 `data` 属性中可用:

```js
const search = (context, event) => new Promise((resolve, reject) => {
if (!event.query.length) {
return reject('No query specified');
// or:
// throw new Error('No query specified');
}
const search = (context, event) =>
new Promise((resolve, reject) => {
if (!event.query.length) {
return reject('No query specified');
// or:
// throw new Error('No query specified');
}

return resolve(getSearchResults(event.query));
});
return resolve(getSearchResults(event.query));
});

// ...
const searchMachine = createMachine({
id: 'search',
initial: 'idle',
context: {
results: undefined,
errorMessage: undefined,
errorMessage: undefined
},
states: {
idle: {
Expand Down
26 changes: 24 additions & 2 deletions packages/core/src/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
EventObject,
HistoryValue,
MachineContext,
PersistedMachineState,
Prop,
SCXML,
StateConfig,
Expand Down Expand Up @@ -124,7 +125,7 @@ export class State<
return stateValue;
}

const _event = initEvent as SCXML.Event<TEvent>;
const _event = initEvent as SCXML.Event<TEvent>; // TODO: fix

const configuration = getConfiguration(
getStateNodes(machine.root, stateValue)
Expand Down Expand Up @@ -167,7 +168,7 @@ export class State<
this.configuration =
config.configuration ??
Array.from(getConfiguration(getStateNodes(machine.root, config.value)));
this.transitions = config.transitions;
this.transitions = config.transitions as any;
this.children = config.children;

this.value = getStateValue(machine.root, this.configuration);
Expand Down Expand Up @@ -287,3 +288,24 @@ export function cloneState<TState extends AnyState>(
state.machine
) as TState;
}

export function getPersistedState<TState extends AnyState>(
state: TState
): PersistedMachineState<TState> {
const { configuration, transitions, tags, machine, children, ...jsonValues } =
state;

const childrenJson: Partial<PersistedMachineState<any>['children']> = {};

for (const id in children) {
childrenJson[id] = {
state: children[id].getPersistedState?.(),
src: children[id].src
};
}

return {
...jsonValues,
children: childrenJson
} as PersistedMachineState<TState>;
}
Loading

0 comments on commit 30c561e

Please sign in to comment.