Skip to content

Commit

Permalink
Refactored out the parallel states and fixed tests
Browse files Browse the repository at this point in the history
  • Loading branch information
technoplato committed Apr 22, 2024
1 parent 8504178 commit 417ae0e
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 112 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assign, raise, sendTo, setup } from 'xstate';
import { assign, log, not, raise, sendTo, setup } from 'xstate';
import {
Permission,
PermissionStatus,
Expand All @@ -22,100 +22,121 @@ export const countingMachineThatNeedsPermissionAt3 = setup({
},

guards: {
requiresPermission: ({ context }) =>
isPermissionRequiredToContinue: ({ context }) =>
context.count >= 3 &&
context.permissionStatus !== PermissionStatuses.granted,
countingCompleted: ({ context }) => context.count >= 5,
isCountingCompleted: ({ context }) => context.count >= 5,
},

types: {
context: {} as { count: number; permissionStatus: PermissionStatus },
events: {} as
events:
{} as // TODO pull these out into their own discrete types and do a union here
| { type: 'count.inc' }
| { type: 'permissionWasRequested'; permission: Permission }
| {
type: 'user.didTapBluetoothRequestPermission';
permission: Permission;
},
| { type: 'permissions.bluetooth.revoked' }
| { type: 'permissions.bluetooth.granted' }
| { type: 'permissions.bluetooth.denied' }
| { type: 'permissionWasRequested'; permission: Permission }
| {
type: 'user.didTapBluetoothRequestPermission';
permission: Permission;
},
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOkwHsBXfAFwKhLH3QCMAbSAYguppIMwBtAAwBdRKAAO5WLjrl8EkAA9EAWgCMATgDMJACw6ArMOE6AbACZLRnVoDs+gDQgAnuq1aSN+wA4Hlhr6hlr6RgC+4S5oWHiEpDy09IzM7Fwi4kgg0rLyilmqCIEaJDoaRhrCGlbGxtUu7kX6JbYVRlYRUSAxOATEZFRJ+AxMrBwQnIIamVIycrgKSoXFpeWV1Za1FeYNiJZaviS+Rkb65r72ndEYvfEDvMkQuLBjXJJgAE6oz7IKJFAfdC0SAkdiUMA0cjkGjYDJKHLzRYFPblEimcwmPz2Lb1NyIYzmEgaS5+DQ2SLXWJ9BKDOjDEhPF5pCbvL4-Bb4BlMXAgsEQqEwuFZBF5JYooxo4QY4RYnE7PEIfRaYSrcldHpxfqJOkMRmvCaUWCfBm4CAAFXQkgAQmxwZDodgAEpgACO4NgNAACp9vrBfvghbNchyxUVUejMRc5bsECdLESdL4qlp2sTzPZzBTujdNTSHvS+faYQARblvH3sv4AoE0Xm2-kOwPZOai5FFbEGYlac6XGMYry2cxhexStMZrMa6n3IYMQsC7Cl-A8g1Gj4m82Wm12+fOt1wL0Vv0cpsikNtywd5r2bsXIwx+yBUrmbQZmqtSwTnNT7BAiBsejemyR4KLAnCsr6-oAOroLAu7urWEAni2Z6gIUSp6OY6LaCcxxBMIzgKloliEhoGxmBmyq+FRkRdPg5AQHASiTvE8LIUiqHqJY-gGMYpgWNYtgOARjRqBeejKjoD7aL46aeCmn5Unc2r0KxwbsSonHNDxJhmFYNh2I4MaaDJJBGEmSbdlhDgKbcWq0skozMqpiL5BxCCaERpRGAcMoOL4F7+L4MaJgYJwWNUxymMRMk2bm046iaTLjM5rZucIMb6NYBj+GZlijhm47ql+Sn2QW9ZFguZYQClKEaQg6UKichIOFo1TNJs76xVOyn0gAZgQzzYJANXqYUDWNOYrUGD2ZwaMcOjCBeXV3D++B-gBh7+vAwpsa5dWaJU3jYoY5TmGU6zCYg-jxqYSpJnYQTeRoNHhEAA */
type: 'parallel',
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAYgAcwAnVXWWXAe31gDoAjAGwFcwAXBhr2wtKYAG4MA1pADaABgC6iUGQb1ejfMpAAPRAFoAjADYArCwAshgJwAmABwBmC6YDst03YA0IAJ6JHQzlLazkrR1tHOUjHVwBfOJ80LDxCIhYwfHROSBJMBi58XhYCTHklJBBVdU1tPQR7IJZHY3tTW1tDQwtHa1NTH38EDosWe3s5Q1dJ02Mw21d4xJBknAJiDKyciBJy7WrcDSY6xGtrexZDRrlo+wtbMPvBxAXDFmiruVMJszlxhKSGDWaU22Q4uRkhgqKjUh1qlXqZwuVyCt3uj1szwQzmCrnsxiseIJ1kcpgBKyBqQ22HQ+AgHAIUAAClQaHRNLByKzaPQmCwoJRabxIOxuHwBEI9pUDkctAjEEFDOZ+u4LCS7v0OliOm8bMYHN97rYLHJnOTVlT0jS6Qz8MzueymJyKNQeZoWBBMrgRZwePxBNgpTCasd5QhumdmmZQk5rMZrPcLNr+s17LZ44Z0+FFubKet0r7xQGACJe3Iutm8-D8wVFH1i-2SxT7WGyk7htXWKOeP69eOJrGolh40kefGTOTGbq5lL50V+iXYUv4b07LiwKge3AQAAq6DIACEG4uAEpgACOPFgvBZrsd+CDVVb8NA9Sm7jGcjOU46E08Sb8BU3GHVxPBsNNHFiCwLCWQFZxBQtG2wM8JGkHYKzdPkBSFesFwDR8ZRfXREFMFoWDsaD4wcCI8VcLFAlcYdHHxRZwlMcCLBnYENkQ09xCkXJ103CBtz3Q9jwDM9LzgG8HSrAjn1DV8FScFhbHOQxmMmB4E2sbUphYYxAlCTx3FA4xjC4y15yLIQUIEtcN0oLdd33AA5dAxFwKB0GFHcGAAZT4DQ7VgBSQzlZThgM9w7msTTmNCLpjEHDpmjcExpjOVwpgJBJlnwBhPXgSoLXzFsIvbfROkcSwbGolx3E8TFAIQfR2ijKdzkCdSVVcTjljKkFMjBSAKrhJTiIaZj3jjWJnEg1pWnojxLlMNUv2iGMpisudrXpRlb0rDlxrbMNMwTd5JiiHVZnjbUIkuUlQm6rTJ1gil4J4iShGXVdTqIt98TeexznY6YLHGfU6Nasd3lMSdvnjAlNJcXaEJ+5D+LQgHJvqdNjDGIJ2i-eMvlCAChgY8jJwJSHGr6SzBrzEEADMCFobAxulRTIqmi7RhuTTok6O69NayDGJcTt02+frAjJfKgA */
// type: 'parallel',
context: {
count: 0,
permissionStatus: PermissionStatuses.unasked,
},

invoke: {
id: 'permissionReportingCounting',
systemId: 'permissionReportingCounting',
src: 'permissionReportingMachine',
input: ({ self }) => ({
permissions: [Permissions.bluetooth],
parent: self,
}),
},
on: {
'permissions.bluetooth.revoked': {
target: '.bluetoothRevoked',
guard: not('isPermissionRequiredToContinue'),
},
},

initial: 'enabled',

states: {
counting: {
initial: 'enabled',
states: {
enabled: {
always: [
{
target: 'disabled',
guard: 'requiresPermission',
},
{
target: 'finished',
guard: 'countingCompleted',
},
],
on: {
'count.inc': {
actions: 'incrementCount',
},
},
enabled: {
always: [
{
target: 'handlingPermissions',
guard: 'isPermissionRequiredToContinue',
},
disabled: {
id: 'countingDisabled',
on: {
'permission.granted.bluetooth': {
target: 'enabled',
actions: 'assignBluetoothStatusGranted',
},
'permission.denied.bluetooth': { target: 'bluetoothDenied' },
'user.didTapBluetoothRequestPermission': {
actions: 'triggerBluetoothPermissionRequest',
},
},
{
target: 'finished',
guard: 'isCountingCompleted',
},
bluetoothDenied: {
on: {
'permission.granted.bluetooth': { target: 'enabled' },
'user.didTapBluetoothRequestPermission': {
actions: 'triggerBluetoothPermissionRequest',
},
},
},
finished: {
type: 'final',
],
on: {
'count.inc': {
actions: 'incrementCount',
},
},
},

handlingPermissions: {
on: {
permissionWasRequested: {
'user.didTapBluetoothRequestPermission': {
actions: sendTo('permissionReportingCounting', ({ event }) => {
return {
type: 'requestPermission',
permission: Permissions.bluetooth,
};
}),
},

'permissions.bluetooth.granted': {
target: 'enabled',
actions: ['assignBluetoothStatusGranted'],
},
'permissions.bluetooth.denied': { target: 'bluetoothDenied' },
},
},

bluetoothDenied: {
on: {
'permissions.bluetooth.granted': {
target: 'enabled',
actions: [
sendTo('permissionReportingCounting', ({ event }) => {
return {
type: 'requestPermission',
permission: event.permission,
};
'assignBluetoothStatusGranted',
log(({ event }) => {
console.log(JSON.stringify(event, null, 2));
}),
],
},
'user.didTapBluetoothRequestPermission': {
actions: 'triggerBluetoothPermissionRequest',
},
},
invoke: {
id: 'permissionReportingCounting',
systemId: 'permissionReportingCounting',
src: 'permissionReportingMachine',
input: ({ self }) => ({
permissions: [Permissions.bluetooth],
parent: self,
}),
},

bluetoothRevoked: {
on: {
'permissions.bluetooth.granted': { target: 'enabled' },
'user.didTapBluetoothRequestPermission': {
actions: 'triggerBluetoothPermissionRequest',
},
'user.didTapNavigateToSettings': {
actions: 'triggerNavigateToSettings',
},
},
},

finished: {
type: 'final',
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -61,40 +61,33 @@ describe('Counting Machine That Needs Permission At 3', () => {
const state: PermissionMonitoringSnapshot =
permissionMonitorActor?.getSnapshot();

console.log({
v: state.context.permissionSubscribers[Permissions.bluetooth].map(
(s) => s.id
),
});

expect(
state.context.permissionSubscribers[Permissions.bluetooth]?.length
).toEqual(2);
// We should be able to find the permission coordinator for the Counting
// feature in the Permission Monitor's subscription map
const countingMachinePermissionCoordinator =
state.context.permissionSubscribers[Permissions.bluetooth]?.some(
(subscriber) => subscriber.id === 'permissionReportingCounting'
);
expect(countingMachinePermissionCoordinator).toBeDefined();

const countingActor = applicationActor.system.get(
ActorSystemIds.counting
);

expect(countingActor?.getSnapshot().value).toStrictEqual({
counting: 'enabled',
handlingPermissions: {},
});
expect(countingActor?.getSnapshot().value).toStrictEqual('enabled');

countingActor.send({ type: 'count.inc' });
countingActor.send({ type: 'count.inc' });
countingActor.send({ type: 'count.inc' });
expect(countingActor.getSnapshot().context.count).toBe(3);
expect(countingActor.getSnapshot().value).toStrictEqual({
counting: 'disabled',
handlingPermissions: {},
});
expect(countingActor.getSnapshot().value).toStrictEqual(
'handlingPermissions'
);

countingActor.send({ type: 'count.inc' });
expect(countingActor.getSnapshot().context.count).toBe(3);
expect(countingActor.getSnapshot().value).toStrictEqual({
counting: 'disabled',
handlingPermissions: {},
});
expect(countingActor.getSnapshot().value).toStrictEqual(
'handlingPermissions'
);

// Configure the permission actor to grant permission
const permissionCheckerActor = applicationActor.system.get(
Expand All @@ -108,10 +101,7 @@ describe('Counting Machine That Needs Permission At 3', () => {
(state) => state.value === 'idle'
);

expect(countingActor.getSnapshot().value).toStrictEqual({
counting: 'enabled',
handlingPermissions: {},
});
expect(countingActor.getSnapshot().value).toStrictEqual('enabled');

expect(countingActor.getSnapshot().context.permissionStatus).toBe(
PermissionStatuses.granted
Expand All @@ -122,9 +112,9 @@ describe('Counting Machine That Needs Permission At 3', () => {
countingActor.send({ type: 'count.inc' });

expect(countingActor.getSnapshot().context.count).toBe(5);
expect(countingActor.getSnapshot().value.counting).toStrictEqual(
'finished'
);
expect(countingActor.getSnapshot().value).toStrictEqual('finished');
countingActor.send({ type: 'count.inc' });
expect(countingActor.getSnapshot().context.count).toBe(5);
///* Required for debugging with stately inspector */ await new Promise((resolve) => setTimeout(resolve, vLongTime));
},
vLongTime
Expand All @@ -136,10 +126,7 @@ describe('Counting Machine That Needs Permission At 3', () => {
const countingActor = createActor(
countingMachineThatNeedsPermissionAt3
).start();
expect(countingActor.getSnapshot().value).toStrictEqual({
counting: 'enabled',
handlingPermissions: {},
});
expect(countingActor.getSnapshot().value).toStrictEqual('enabled');
});

it('should increment count', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { log, setup } from 'xstate';
import { ActorSystemIds } from '../application/actorIds';
import { countingMachineThatNeedsPermissionAt3 } from './counting/counting.machine';
import { someFeatureMachine } from './someFeature/someFeature.machine';

export const featuresMachine = setup({
types: {} as {
children: {
[ActorSystemIds.counting]: 'countingMachine';
[ActorSystemIds.someFeature]: 'someFeatureMachine';
// [ActorSystemIds.someFeature]: 'someFeatureMachine';
};
},
actors: {
countingMachine: countingMachineThatNeedsPermissionAt3,
someFeatureMachine: someFeatureMachine,
// someFeatureMachine: someFeatureMachine,
},
}).createMachine({
entry: log('Features started'),
Expand All @@ -23,10 +22,10 @@ export const featuresMachine = setup({
systemId: ActorSystemIds.counting,
src: 'countingMachine',
},
{
id: ActorSystemIds.someFeature,
systemId: ActorSystemIds.someFeature,
src: 'someFeatureMachine',
},
// {
// id: ActorSystemIds.someFeature,
// systemId: ActorSystemIds.someFeature,
// src: 'someFeatureMachine',
// },
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import { stubApplicationLifecycleReportingActorLogic } from '../../lifecycle/lif
import { InitialPermissionStatusMap } from '../../permission.fixtures';
import { Permission } from '../../permission.types';
import { permissionCheckerAndRequesterMachine } from '../checkAndRequest/permissionCheckAndRequestMachine';
import {
PermissionMonitoringMachineEvents,
PermissionSubscriberMap,
} from './permissionMonitor.types';
import {
EmptyPermissionSubscriberMap,
PermissionsMonitoringMachineContext,
} from './permissionMonitor.fixtures';
import {
PermissionMonitoringMachineEvents,
PermissionSubscriberMap,
} from './permissionMonitor.types';

export const permissionMonitoringMachine = setup({
types: {} as {
Expand All @@ -42,6 +42,8 @@ export const permissionMonitoringMachine = setup({
actions: {
assignPermissionCheckResultsToContext: assign({
permissionsStatuses: ({ event }) => {
console.log(JSON.stringify(event, null, 2));

assertEvent(event, 'allPermissionsChecked');
return event.statuses;
},
Expand All @@ -51,6 +53,10 @@ export const permissionMonitoringMachine = setup({
// TODO this should only send permission updates for the recently modified permissions
// and is currently sending updates to all permissions to everyone
Object.keys(context.permissionSubscribers).forEach((permission) => {
console.log(JSON.stringify({ permission }, null, 2));

console.log(JSON.stringify(context.permissionsStatuses, null, 2));

context.permissionSubscribers[permission].forEach(
(actorRef: AnyActorRef) => {
enqueue.sendTo(actorRef, {
Expand All @@ -65,6 +71,8 @@ export const permissionMonitoringMachine = setup({
),
assignPermissionRequestResultToContext: assign({
permissionsStatuses: ({ event, context }) => {
console.log(JSON.stringify(event, null, 2));

assertEvent(event, 'permissionRequestCompleted');
return {
...context.permissionsStatuses,
Expand Down Expand Up @@ -104,6 +112,8 @@ export const permissionMonitoringMachine = setup({
const { permissions } = event;
const { permissionSubscribers } = context;

console.log(JSON.stringify({ event }, null, 2));

// Create a new permissionSubscribers object to avoid mutating the original
const updatedPermissionSubscribers: PermissionSubscriberMap = {
...permissionSubscribers,
Expand All @@ -126,6 +136,8 @@ export const permissionMonitoringMachine = setup({
}
});

console.log(JSON.stringify({ updatedPermissionSubscribers }, null, 2));

return {
permissionSubscribers: updatedPermissionSubscribers,
};
Expand Down
10 changes: 10 additions & 0 deletions libs/permissions/permissionLogic/src/lib/permission/reporting/asdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Feature needs to control

in what state permission is required
when the thing is actually requeted



Prereq machine needs to control
subscribe to permission updates from permission monitor machine
checking and updating its own context / context of the parent with up to pre req statuses
Loading

0 comments on commit 417ae0e

Please sign in to comment.