-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Actor select #5299
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
Open
Uniqen
wants to merge
6
commits into
statelyai:main
Choose a base branch
from
Uniqen:actor-select
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+311
−0
Open
Actor select #5299
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
05670ce
Add actor select function simliar to @xstate/store
dementors-magnus 76f8bd8
Add select function to the ActorRef type
dementors-magnus da482f7
chore: remove unused generic types
dementors-magnus 824b3b6
Merge branch 'statelyai:main' into actor-select
Uniqen e1f2808
restore generic type for snapshot to avoid typecheck error
dementors-magnus 5ba73cb
Merge branch 'statelyai:main' into actor-select
Uniqen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'xstate': minor | ||
--- | ||
|
||
Add actor select function simliar to @xstate/store |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
import { assign, SnapshotFrom } from '../src'; | ||
import { createMachine } from '../src/index.ts'; | ||
import { createActor } from '../src/index.ts'; | ||
|
||
describe('select', () => { | ||
it('should get current value', () => { | ||
const machine = createMachine({ | ||
types: {} as { context: { data: number } }, | ||
context: { data: 42 }, | ||
initial: 'G', | ||
states: { | ||
G: { | ||
on: { | ||
INC: { | ||
actions: assign({ data: ({ context }) => context.data + 1 }) | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
|
||
const service = createActor(machine).start(); | ||
const selection = service.select(({ context }) => context.data); | ||
|
||
expect(selection.get()).toBe(42); | ||
|
||
service.send({ type: 'INC' }); | ||
|
||
expect(selection.get()).toBe(43); | ||
}); | ||
|
||
it('should subscribe to changes', () => { | ||
const machine = createMachine({ | ||
types: {} as { context: { data: number } }, | ||
context: { data: 42 }, | ||
initial: 'G', | ||
states: { | ||
G: { | ||
on: { | ||
INC: { | ||
actions: assign({ data: ({ context }) => context.data + 1 }) | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
|
||
const callback = vi.fn(); | ||
const service = createActor(machine).start(); | ||
const selection = service.select(({ context }) => context.data); | ||
selection.subscribe(callback); | ||
|
||
service.send({ type: 'INC' }); | ||
|
||
expect(callback).toHaveBeenCalledTimes(1); | ||
expect(callback).toHaveBeenCalledWith(43); | ||
}); | ||
|
||
it('should not notify if selected value has not changed', () => { | ||
const machine = createMachine({ | ||
types: {} as { context: { data: number; other: string } }, | ||
context: { data: 42, other: 'foo' }, | ||
initial: 'G', | ||
states: { | ||
G: { | ||
on: { | ||
INC: { | ||
actions: assign({ data: ({ context }) => context.data + 1 }) | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
|
||
const callback = vi.fn(); | ||
const service = createActor(machine).start(); | ||
const selection = service.select(({ context }) => context.other); | ||
selection.subscribe(callback); | ||
|
||
service.send({ type: 'INC' }); | ||
|
||
expect(callback).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should support custom equality function', () => { | ||
const machine = createMachine({ | ||
types: {} as { | ||
context: { age: number; name: string }; | ||
events: | ||
| { | ||
type: 'UPDATE_NAME'; | ||
name: string; | ||
} | ||
| { | ||
type: 'UPDATE_AGE'; | ||
age: number; | ||
}; | ||
}, | ||
context: { age: 42, name: 'John' }, | ||
initial: 'G', | ||
states: { | ||
G: { | ||
on: { | ||
UPDATE_NAME: { | ||
actions: assign({ name: ({ event }) => event.name }) | ||
}, | ||
UPDATE_AGE: { | ||
actions: assign({ age: ({ event }) => event.age }) | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
|
||
const service = createActor(machine).start(); | ||
|
||
const callback = vi.fn(); | ||
const selector = ({ context }: SnapshotFrom<typeof machine>) => ({ | ||
name: context.name, | ||
age: context.age | ||
}); | ||
const equalityFn = (a: { name: string }, b: { name: string }) => | ||
a.name === b.name; // Only compare names | ||
|
||
service.select(selector, equalityFn).subscribe(callback); | ||
|
||
service.send({ type: 'UPDATE_AGE', age: 66 }); | ||
expect(callback).not.toHaveBeenCalled(); | ||
|
||
service.send({ type: 'UPDATE_NAME', name: 'Jane' }); | ||
expect(callback).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should unsubscribe correctly', () => { | ||
const machine = createMachine({ | ||
types: {} as { context: { data: number } }, | ||
context: { data: 42 }, | ||
initial: 'G', | ||
states: { | ||
G: { | ||
on: { | ||
INC: { | ||
actions: assign({ data: ({ context }) => context.data + 1 }) | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
|
||
const service = createActor(machine).start(); | ||
|
||
const callback = vi.fn(); | ||
const selection = service.select(({ context }) => context.data); | ||
const subscription = selection.subscribe(callback); | ||
|
||
subscription.unsubscribe(); | ||
service.send({ type: 'INC' }); | ||
|
||
expect(callback).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should handle updates with multiple subscribers', () => { | ||
interface PositionContext { | ||
position: { | ||
x: number; | ||
y: number; | ||
}; | ||
} | ||
|
||
const machine = createMachine({ | ||
types: {} as { | ||
context: { | ||
user: { age: number; name: string }; | ||
position: { | ||
x: number; | ||
y: number; | ||
}; | ||
}; | ||
events: | ||
| { | ||
type: 'UPDATE_USER'; | ||
user: { age: number; name: string }; | ||
} | ||
| { | ||
type: 'UPDATE_POSITION'; | ||
position: { | ||
x: number; | ||
y: number; | ||
}; | ||
}; | ||
}, | ||
context: { position: { x: 0, y: 0 }, user: { name: 'John', age: 30 } }, | ||
initial: 'G', | ||
states: { | ||
G: { | ||
on: { | ||
UPDATE_USER: { | ||
actions: assign({ user: ({ event }) => event.user }) | ||
}, | ||
UPDATE_POSITION: { | ||
actions: assign({ position: ({ event }) => event.position }) | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
|
||
const store = createActor(machine).start(); | ||
|
||
// Mock DOM manipulation callback | ||
const renderCallback = vi.fn(); | ||
store | ||
.select(({ context }) => context.position) | ||
.subscribe((position) => { | ||
renderCallback(position); | ||
}); | ||
|
||
// Mock logger callback for x position only | ||
const loggerCallback = vi.fn(); | ||
store | ||
.select(({ context }) => context.position.x) | ||
.subscribe((x) => { | ||
loggerCallback(x); | ||
}); | ||
|
||
// Simulate position update | ||
store.send({ | ||
type: 'UPDATE_POSITION', | ||
position: { x: 100, y: 200 } | ||
}); | ||
|
||
// Verify render callback received full position update | ||
expect(renderCallback).toHaveBeenCalledTimes(1); | ||
expect(renderCallback).toHaveBeenCalledWith({ x: 100, y: 200 }); | ||
|
||
// Verify logger callback received only x position | ||
expect(loggerCallback).toHaveBeenCalledTimes(1); | ||
expect(loggerCallback).toHaveBeenCalledWith(100); | ||
|
||
// Simulate another update | ||
store.send({ | ||
type: 'UPDATE_POSITION', | ||
position: { x: 150, y: 300 } | ||
}); | ||
|
||
expect(renderCallback).toHaveBeenCalledTimes(2); | ||
expect(renderCallback).toHaveBeenLastCalledWith({ x: 150, y: 300 }); | ||
expect(loggerCallback).toHaveBeenCalledTimes(2); | ||
expect(loggerCallback).toHaveBeenLastCalledWith(150); | ||
|
||
// Simulate changing only the y position | ||
store.send({ | ||
type: 'UPDATE_POSITION', | ||
position: { x: 150, y: 400 } | ||
}); | ||
|
||
expect(renderCallback).toHaveBeenCalledTimes(3); | ||
expect(renderCallback).toHaveBeenLastCalledWith({ x: 150, y: 400 }); | ||
|
||
// loggerCallback should not have been called | ||
expect(loggerCallback).toHaveBeenCalledTimes(2); | ||
|
||
// Simulate changing only the user | ||
store.send({ | ||
type: 'UPDATE_USER', | ||
user: { name: 'Jane', age: 25 } | ||
}); | ||
|
||
// renderCallback should not have been called | ||
expect(renderCallback).toHaveBeenCalledTimes(3); | ||
|
||
// loggerCallback should not have been called | ||
expect(loggerCallback).toHaveBeenCalledTimes(2); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TSnapshot
is already available in the scope here (it's an existing type parameter ofActorRef
), so u dont need it here - onlyTSelected
is neededUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get compilation error when I remove TSnapshot and then run
pnpm typecheck
, see GitHub Actions. Not sure on how to resolve it. Reverting to use TSnapshot for now.I looked into adding a generic for TSnapshot on Actor, see below. But that ended up in a lot of changes not really in the scope of this PR. @Andarist any suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Andarist Any idea how to resolve this without a major refactor? Is it required in order to merge the PR?