diff --git a/libs/permissions/permissionLogic/src/lib/actorIds.ts b/libs/permissions/permissionLogic/src/lib/application/actorIds.ts similarity index 53% rename from libs/permissions/permissionLogic/src/lib/actorIds.ts rename to libs/permissions/permissionLogic/src/lib/application/actorIds.ts index eb2d916..f7db2cd 100644 --- a/libs/permissions/permissionLogic/src/lib/actorIds.ts +++ b/libs/permissions/permissionLogic/src/lib/application/actorIds.ts @@ -1,9 +1,19 @@ export const ActorSystemIds = { + application: 'applicationMachineId', + // Top level system like stuff + systemManagement: 'systemManagementMachineId', + + // Permissions permissionMonitoring: 'permissionMonitoringMachineId', permissionReporting: 'permissionReportingMachineId', permissionCheckerAndRequester: 'permissionCheckerAndRequesterMachineId', + // Lifecycle reporting lifecycleReporting: 'lifecycleReportingMachineId', + // Root of features machine features: 'featuresMachineId', + // Features + counting: 'countingMachineId', + someFeature: 'someFeatureMachineId', } as const; diff --git a/libs/permissions/permissionLogic/src/lib/application/application.machine.ts b/libs/permissions/permissionLogic/src/lib/application/application.machine.ts new file mode 100644 index 0000000..1af7c9d --- /dev/null +++ b/libs/permissions/permissionLogic/src/lib/application/application.machine.ts @@ -0,0 +1,28 @@ +import { setup } from 'xstate'; +import { ActorSystemIds } from '../application/actorIds'; +import { featuresMachine } from '../features/features.machine'; +import { systemManagementMachine } from '../systemManagement/systemManagement.machine'; + +export const applicationMachine = setup({ + types: {} as { + children: { + [ActorSystemIds.features]: 'featuresMachine'; + [ActorSystemIds.systemManagement]: 'topLevelSystemStuff'; + }; + }, + actors: { + featuresMachine: featuresMachine, + topLevelSystemStuff: systemManagementMachine, + }, +}).createMachine({ + invoke: [ + { + systemId: ActorSystemIds.features, + src: 'featuresMachine', + }, + { + systemId: ActorSystemIds.systemManagement, + src: 'topLevelSystemStuff', + }, + ], +}); diff --git a/libs/permissions/permissionLogic/src/lib/features/counting/counting.machine.ts b/libs/permissions/permissionLogic/src/lib/features/counting/counting.machine.ts new file mode 100644 index 0000000..2938f87 --- /dev/null +++ b/libs/permissions/permissionLogic/src/lib/features/counting/counting.machine.ts @@ -0,0 +1,98 @@ +import { assign, raise, sendTo, setup } from 'xstate'; +import { + Permissions, + PermissionStatus, + PermissionStatuses, +} from '../../permission.types'; +import { permissionReportingMachine } from '../../permissionReporting/permissionReporting.machine'; + +export const countingMachineThatNeedsPermissionAt3 = setup({ + actors: { + permissionReportingMachine, + }, + types: { + context: {} as { count: number; permissionStatus: PermissionStatus }, + events: { type: 'count.inc' }, + }, +}).createMachine({ + /** @xstate-layout N4IgpgJg5mDOIC5QGMD2BXAdgFwJaagEFMIAFMAJwFtdZZdVNYA6NLPA5sTAQwCMANpADEbHM3zIA2gAYAuolAAHVPTyNFIAB6IA7ACYANCACeiAIwBWc8xl27AZhm6rDpw4C+H42I5ES5NS09IwsvvhQXLyCIr4SmNLmCkggKmoMmJo6CPoAHA7M1pYAbM4AnLoOLpZGpoj61sxllg7mVZbN5gYALLle3iCYqBBwmuEExGSUNHQZ8ClpuOqZKdkAtPrdzMWWuboyZeZlZd1lDsUOxmYIG1v2Mm0uus1l+vpePhg4EZOBMyFMVhfPyaRbLLL1bpXRAXZinY4nCoOMoPGTdYofEDjfxTIKzUJA9gRKL8IQQUGqJYZCEIcz6GyWXTdfQHBo7GrQnI1OEtR4daw9PoDbG-abBOaE76cCC0UmQCnpDSrRA1XLMLplYq5Rn6E7o4qchr6HmtGTWc7dbqtTEigJi-GAgAWPBIAgif3FoQVVKVoGy+gKDm6dJRuTslhkF10nIcuTVEZkbhkuU1Vt0uW6NuBPzteIBLGdrvd9vzEggQm94OVCHT23ZZv2lUeBrqCG6umKtjs+mKums7e6JUzwuzE1z-wlhfLxbzEp4yDwADcwJXqdWXMwgyHk+HI1VOS0tgm8marMdzEKvEA */ + type: 'parallel', + id: 'countingAndPermissions', + context: { + count: 0, + permissionStatus: PermissionStatuses.unasked, + }, + + states: { + counting: { + initial: 'enabled', + states: { + enabled: { + always: [ + { + target: 'disabled', + guard: ({ context }) => context.count >= 3, + }, + ], + on: { + 'count.inc': [ + { + actions: assign({ count: ({ context }) => context.count + 1 }), + }, + ], + }, + }, + disabled: { + id: 'countingDisabled', + on: { + 'permission.granted.bluetooth': { target: 'enabled' }, + 'permission.denied.bluetooth': { target: 'bluetoothDenied' }, + 'user.didTapBluetoothRequestPermission': { + actions: raise({ + type: 'permissionWasRequested', + // @ts-expect-error TODO make this type safe + permission: Permissions.bluetooth, + }), + }, + }, + }, + bluetoothDenied: { + on: { + 'permission.granted.bluetooth': { target: 'enabled' }, + 'user.didTapBluetoothRequestPermission': { + actions: raise({ + type: 'permissionWasRequested', + // @ts-expect-error TODO make this type safe + permission: Permissions.bluetooth, + }), + }, + }, + }, + }, + }, + + handlingPermissions: { + on: { + permissionWasRequested: { + actions: [ + sendTo('permissionHandler', ({ event }) => { + return { + type: 'requestPermission', + // @ts-expect-error TODO make this type safe + permission: event.permission, + }; + }), + ], + }, + }, + invoke: { + id: 'permissionHandler', + src: 'permissionReportingMachine', + input: ({ self }) => ({ + permissions: [Permissions.bluetooth], + parent: self, + }), + }, + }, + }, +}); diff --git a/libs/permissions/permissionLogic/src/lib/features/features.machine.ts b/libs/permissions/permissionLogic/src/lib/features/features.machine.ts new file mode 100644 index 0000000..eff22e7 --- /dev/null +++ b/libs/permissions/permissionLogic/src/lib/features/features.machine.ts @@ -0,0 +1,28 @@ +import { 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'; + }; + }, + actors: { + countingMachine: countingMachineThatNeedsPermissionAt3, + someFeatureMachine: someFeatureMachine, + }, +}).createMachine({ + invoke: [ + { + systemId: ActorSystemIds.counting, + src: 'countingMachine', + }, + { + systemId: ActorSystemIds.someFeature, + src: 'someFeatureMachine', + }, + ], +}); diff --git a/libs/permissions/permissionLogic/src/lib/features/someFeature/someFeature.machine.ts b/libs/permissions/permissionLogic/src/lib/features/someFeature/someFeature.machine.ts new file mode 100644 index 0000000..5cf6d7d --- /dev/null +++ b/libs/permissions/permissionLogic/src/lib/features/someFeature/someFeature.machine.ts @@ -0,0 +1,72 @@ +import { raise, sendTo, setup } from 'xstate'; +import { permissionReportingMachine } from '../../permissionReporting/permissionReporting.machine'; +import { Permissions } from '../../permission.types'; + +export const someFeatureMachine = setup({ + actors: { + permissionReportingMachine, + }, +}).createMachine({ + /** @xstate-layout N4IgpgJg5mDOIC5SwPYFswDEwEMAuArgE5gCyOAxgBYCWAdmAJIQB0ANilFPVAMQBUAbQAMAXUSgADilg08NFHQkgAHogCcANgAsLbZuHqAzAA4TAdm2GArEYA0IAJ6Ijw83vOv1AJmEBGa28THwBfEIdUDGx8YjJKWgZmFgAzFBQWWDwcIjxeKBQAFRQAdRw5HkwUIgAFMCI0GlhZRRFxJBBpWXlFZTUEb1cWdVs-XyNjYWtrP20HZwRNb3dF41MjJe9fPzCI9CxcQhJyanomVlT0gHcy+TooSpq6hqaFOl5JJ8bmuhYoIhw6HhICwAEZsAhgPBpPBUVrKTrlHrtPomIyaFjmYLCCxBdSTEx+OaIczWdH6byaSmGExubTbcIgSL7GJHeKnJIXFjXcp3B61epfV7vT4vRQsCBgOg0YFgiFQlAwuHtBHdJTIxCo9GYvE4rHWAlEhZBPSLTTmPxUqZGaw7Rl7aKHOInRLnNJcm4VKr857fXgEWB1cU0CAFHCSABC4Mh0KoACUwABHCGZb2ClpieEyRFq0B9bQmbwmizmbzGSwDayG2xGIYU7x+C3mTFmoy2pkO2LHBJnFJu2XRhVUAAikulEGFAtFPz+AKBrH78sVGeVWdVvUQM001hY3m0m2Em2tQQLhs01oxozNB806lWmjb9oOnbZLt76QXMZHUsgfoDRCDIZhpGcoxvGSZwHgqZTkqUirq864IA2fjCLWfhmJimq2OYVZGLoJLDNo1j+KMwiTA+URPqyzo9lQAIQGwPBQd8sATj6rylLAYHJnOMEdHBSK5hq6i6HuaHmn41pWPqhoEiw0y3gpdJmIE5hhAydAoBK8DtO2lFOt2zCZl08HqggAC0u6GmZ27qDSpH2Q5pHaORzKOl27KsBwXA8EZ2YIdo2FOIgUm1osvhuOo5ikcMLkdlRBmuigvlrqZllBQg2j6HodYNp41i2d4sV6e5r6cpk2R4MlJmCQgJansIugrNayEmHuwhnkVLL6R5b7ujy9xeiK3xVQJqiIFMuh+MM6w6FNSw3qe55GOYd7GFSwitgyuldSVPach+g4AOL-ICkAjTmY2IZYujLURB4FrZrWnnuLBNUsyGEZM9K7BRO0vntfZRouw6jmdK7GaNfQkShzX6iSpImNY+iLei9YUlFFK3kYaKdW5-1JLRdD0YxQ2vNpsEQxdeaBfMaGFpFRijIEpGaGhm1hEAA */ + id: 'someFeatureMachineId', + type: 'parallel', + states: { + foo: { + initial: 'start', + states: { + start: { + entry: raise({ type: 'goToWaitingForPermission' }), + on: { goToWaitingForPermission: 'waitingForPermission' }, + }, + waitingForPermission: { + on: { + 'permission.granted.bluetooth': { target: 'bluetoothGranted' }, + 'permission.denied.bluetooth': { target: 'bluetoothDenied' }, + 'user.didTapBluetoothRequestPermission': { + actions: raise({ + type: 'permissionWasRequested', + permission: Permissions.bluetooth, + }), + }, + }, + }, + bluetoothGranted: { + type: 'final', + }, + bluetoothDenied: { + on: { + 'permission.granted.bluetooth': { target: 'bluetoothGranted' }, + 'user.didTapBluetoothRequestPermission': { + actions: raise({ + type: 'permissionWasRequested', + permission: Permissions.bluetooth, + }), + }, + }, + }, + }, + }, + handlingPermissions: { + on: { + permissionWasRequested: { + actions: [ + sendTo('permissionHandler', ({ event }) => { + return { + type: 'requestPermission', + permission: event.permission, + }; + }), + ], + }, + }, + invoke: { + id: 'permissionHandler', + src: 'permissionReportingMachine', + input: ({ self }) => ({ + permissions: [Permissions.bluetooth], + parent: self, + }), + }, + }, + }, +}); diff --git a/libs/permissions/permissionLogic/src/lib/permission-logic.spec.ts b/libs/permissions/permissionLogic/src/lib/permission-logic.spec.ts index 95bb08a..933aa71 100644 --- a/libs/permissions/permissionLogic/src/lib/permission-logic.spec.ts +++ b/libs/permissions/permissionLogic/src/lib/permission-logic.spec.ts @@ -1,17 +1,13 @@ // permissionMonitoringMachine.test.ts -import { ActorSystemIds } from './actorIds'; +import { ActorSystemIds } from './application/actorIds'; import { createSkyInspector } from '@statelyai/inspect'; import { WebSocket } from 'ws'; import { AnyActorRef, - AnyEventObject, - assign, createActor, - enqueueActions, InspectionEvent, log, - raise, sendTo, setup, waitFor, @@ -20,7 +16,6 @@ import { Permission, PermissionMonitoringMachineEvents, Permissions, - PermissionStatus, PermissionStatuses, } from './permission.types'; import { permissionCheckerAndRequesterMachine } from './permissionCheckAndRequestMachine'; @@ -28,206 +23,34 @@ import { EmptyPermissionSubscriberMap, permissionMonitoringMachine, } from './permissionMonitor.machine'; +import { someFeatureMachine } from './features/someFeature/someFeature.machine'; +import { countingMachineThatNeedsPermissionAt3 } from './features/counting/counting.machine'; -const permissionReportingMachine = setup({ - types: { - input: {} as { - permissions: Array; - parent: AnyActorRef; - }, - context: {} as { - permissions: Array; - parent: AnyActorRef; - }, - }, - actions: { - sendSubscriptionRequestForStatusUpdates: sendTo( - ({ system }) => { - const actorRef: AnyActorRef = system.get( - ActorSystemIds.permissionMonitoring - ); - return actorRef; - }, - ({ self, context }) => ({ - type: 'subscribeToPermissionStatuses', - permissions: context.permissions, - self, - }) - ), - // satisfies /*TODO type these events to the receiving machine event type*/ AnyEventObject); - checkedSendParent: enqueueActions( - ({ context, enqueue }, event: AnyEventObject) => { - if (!context.parent) { - console.log( - 'WARN: an attempt to send an event to a non-existent parent' - ); - return; - } - - enqueue.sendTo(context.parent, event); - } - ), - }, -}).createMachine({ - description: - "This actor's job is to report permission statuses to the actors that have invoked it. We abstract away this functionality so that it is reusable by any actor that needs it and so they don't need to know how permissions are checked. This keeps control centralized and easy to modify the behavior of.", - context: ({ input }) => ({ - permissions: input.permissions, - parent: input.parent, - }), - entry: [ - 'sendSubscriptionRequestForStatusUpdates', - log('subscribe to status updates'), - ], - on: { - requestPermission: { - actions: [ - sendTo( - ({ system }) => { - return system.get(ActorSystemIds.permissionCheckerAndRequester); - }, - ({ event }) => ({ - type: 'triggerPermissionRequest', - permission: event.permission, - }) - ), - ], - }, - permissionStatusChanged: { - description: - 'Whenever the Permission Monitoring machine reports that a permission status has changed, we receive this event and can process and share with our siblings.', - // We eventually want to communicate this to the actors that have invoked us - actions: [ - log( - ({ event }) => - event.permission + ' status << { + it('should increment count to 3, ask for permission, and continue counting to 5 when permission is granted', async () => { + const permissionMonitorActor = createActor( + permissionMonitoringMachine.provide({ + actors: { + features: countingMachineThatNeedsPermissionAt3, }, - ], - }, - }, -}); + }), + { + systemId: ActorSystemIds.permissionMonitoring, + } + ).start(); -const forever = 2 ^ (28 - 1); -const countingMachineThatNeedsPermissionAt3 = setup({ - actors: { - permissionReportingMachine, - }, - types: { - context: {} as { count: number; permissionStatus: PermissionStatus }, - events: { type: 'count.inc' }, - }, -}).createMachine({ - /** @xstate-layout N4IgpgJg5mDOIC5QGMD2BXAdgFwJaagEFMIAFMAJwFtdZZdVNYA6NLPA5sTAQwCMANpADEbHM3zIA2gAYAuolAAHVPTyNFIAB6IA7ACYANCACeiAIwBWc8xl27AZhm6rDpw4C+H42I5ES5NS09IwsvvhQXLyCIr4SmNLmCkggKmoMmJo6CPoAHA7M1pYAbM4AnLoOLpZGpoj61sxllg7mVZbN5gYALLle3iCYqBBwmuEExGSUNHQZ8ClpuOqZKdkAtPrdzMWWuboyZeZlZd1lDsUOxmYIG1v2Mm0uus1l+vpePhg4EZOBMyFMVhfPyaRbLLL1bpXRAXZinY4nCoOMoPGTdYofEDjfxTIKzUJA9gRKL8IQQUGqJYZCEIcz6GyWXTdfQHBo7GrQnI1OEtR4daw9PoDbG-abBOaE76cCC0UmQCnpDSrRA1XLMLplYq5Rn6E7o4qchr6HmtGTWc7dbqtTEigJi-GAgAWPBIAgif3FoQVVKVoGy+gKDm6dJRuTslhkF10nIcuTVEZkbhkuU1Vt0uW6NuBPzteIBLGdrvd9vzEggQm94OVCHT23ZZv2lUeBrqCG6umKtjs+mKums7e6JUzwuzE1z-wlhfLxbzEp4yDwADcwJXqdWXMwgyHk+HI1VOS0tgm8marMdzEKvEA */ - type: 'parallel', - id: 'countingAndPermissions', - context: { - count: 0, - permissionStatus: PermissionStatuses.unasked, - }, - - states: { - counting: { - initial: 'enabled', - states: { - enabled: { - on: { - 'count.inc': [ - { - guard: ({ context }) => context.count < 3, - actions: assign({ count: ({ context }) => context.count + 1 }), - }, + const state = permissionMonitorActor.getSnapshot(); + expect( + state.context.permissionSubscribers[Permissions.bluetooth].length + ).toEqual(1); - { - target: ['disabled'], - }, - ], - }, - }, - disabled: { - id: 'countingDisabled', - on: { - 'permission.granted.bluetooth': { target: 'enabled' }, - 'permission.denied.bluetooth': { target: 'bluetoothDenied' }, - 'user.didTapBluetoothRequestPermission': { - actions: raise({ - type: 'permissionWasRequested', - // @ts-expect-error TODO make this type safe - permission: Permissions.bluetooth, - }), - }, - }, - }, - bluetoothDenied: { - on: { - 'permission.granted.bluetooth': { target: 'enabled' }, - 'user.didTapBluetoothRequestPermission': { - actions: raise({ - type: 'permissionWasRequested', - // @ts-expect-error TODO make this type safe - permission: Permissions.bluetooth, - }), - }, - }, - }, - }, - }, - - handlingPermissions: { - on: { - permissionWasRequested: { - actions: [ - sendTo('permissionHandler', ({ event }) => { - return { - type: 'requestPermission', - // @ts-expect-error TODO make this type safe - permission: event.permission, - }; - }), - ], - }, - }, - invoke: { - id: 'permissionHandler', - src: 'permissionReportingMachine', - input: ({ self }) => ({ - permissions: [Permissions.bluetooth], - parent: self, - }), - }, - }, - }, -}); + const featureMachineActor = + permissionMonitorActor.getSnapshot().children.featuresMachineId; + expect(featureMachineActor?.getSnapshot().value).toStrictEqual({ + counting: 'enabled', + handlingPermissions: {}, + }); -describe('Counting Machine That Needs Permission At 3', () => { - it('should not increment count beyond 3, but rather ask permission', async () => { const countingActor = createActor( countingMachineThatNeedsPermissionAt3, {} @@ -238,21 +61,39 @@ describe('Counting Machine That Needs Permission At 3', () => { countingActor.send({ type: 'count.inc' }); expect(countingActor.getSnapshot().context.count).toBe(3); expect(countingActor.getSnapshot().value).toStrictEqual({ - counting: 'enabled', + counting: 'disabled', handlingPermissions: {}, }); countingActor.send({ type: 'count.inc' }); + expect(countingActor.getSnapshot().context.count).toBe(3); expect(countingActor.getSnapshot().value).toStrictEqual({ counting: 'disabled', handlingPermissions: {}, }); - expect(countingActor.getSnapshot().context.count).toBe(3); - - countingActor.send({ type: 'count.inc' }); - expect(countingActor.getSnapshot().context.count).toBe(3); - // await new Promise((resolve) => setTimeout(resolve, prettyMuchForever)); + // Configure the permission actor to grant permission + // const permissionCheckerActor = + // countingActor.getSnapshot().children + // .permissionCheckerAndRequesterMachineId!; + // permissionCheckerActor.send({ + // type: 'triggerPermissionRequest', + // permission: Permissions.bluetooth, + // }); + + // await waitFor(permissionCheckerActor, (state) => state.value === 'idle'); + // + // expect(countingActor.getSnapshot().context.permissionStatus).toBe(PermissionStatuses.granted); + // + // // Send 'count.inc' events to increment the count to 5 + // countingActor.send({ type: 'count.inc' }); + // countingActor.send({ type: 'count.inc' }); + // + // expect(countingActor.getSnapshot().context.count).toBe(5); + // expect(countingActor.getSnapshot().value).toStrictEqual({ + // counting: 'finished', + // handlingPermissions: 'idle', + // }); }); // prettyMuchForever it('should start in idle state', async () => { @@ -466,74 +307,6 @@ describe('Permission Monitoring Machine', () => { ); }); - const someFeatureMachine = setup({ - actors: { - permissionReportingMachine, - }, - }).createMachine({ - /** @xstate-layout N4IgpgJg5mDOIC5SwPYFswDEwEMAuArgE5gCyOAxgBYCWAdmAJIQB0ANilFPVAMQBUAbQAMAXUSgADilg08NFHQkgAHogCcANgAsLbZuHqAzAA4TAdm2GArEYA0IAJ6Ijw83vOv1AJmEBGa28THwBfEIdUDGx8YjJKWgZmFgAzFBQWWDwcIjxeKBQAFRQAdRw5HkwUIgAFMCI0GlhZRRFxJBBpWXlFZTUEb1cWdVs-XyNjYWtrP20HZwRNb3dF41MjJe9fPzCI9CxcQhJyanomVlT0gHcy+TooSpq6hqaFOl5JJ8bmuhYoIhw6HhICwAEZsAhgPBpPBUVrKTrlHrtPomIyaFjmYLCCxBdSTEx+OaIczWdH6byaSmGExubTbcIgSL7GJHeKnJIXFjXcp3B61epfV7vT4vRQsCBgOg0YFgiFQlAwuHtBHdJTIxCo9GYvE4rHWAlEhZBPSLTTmPxUqZGaw7Rl7aKHOInRLnNJcm4VKr857fXgEWB1cU0CAFHCSABC4Mh0KoACUwABHCGZb2ClpieEyRFq0B9bQmbwmizmbzGSwDayG2xGIYU7x+C3mTFmoy2pkO2LHBJnFJu2XRhVUAAikulEGFAtFPz+AKBrH78sVGeVWdVvUQM001hY3m0m2Em2tQQLhs01oxozNB806lWmjb9oOnbZLt76QXMZHUsgfoDRCDIZhpGcoxvGSZwHgqZTkqUirq864IA2fjCLWfhmJimq2OYVZGLoJLDNo1j+KMwiTA+URPqyzo9lQAIQGwPBQd8sATj6rylLAYHJnOMEdHBSK5hq6i6HuaHmn41pWPqhoEiw0y3gpdJmIE5hhAydAoBK8DtO2lFOt2zCZl08HqggAC0u6GmZ27qDSpH2Q5pHaORzKOl27KsBwXA8EZ2YIdo2FOIgUm1osvhuOo5ikcMLkdlRBmuigvlrqZllBQg2j6HodYNp41i2d4sV6e5r6cpk2R4MlJmCQgJansIugrNayEmHuwhnkVLL6R5b7ujy9xeiK3xVQJqiIFMuh+MM6w6FNSw3qe55GOYd7GFSwitgyuldSVPach+g4AOL-ICkAjTmY2IZYujLURB4FrZrWnnuLBNUsyGEZM9K7BRO0vntfZRouw6jmdK7GaNfQkShzX6iSpImNY+iLei9YUlFFK3kYaKdW5-1JLRdD0YxQ2vNpsEQxdeaBfMaGFpFRijIEpGaGhm1hEAA */ - id: 'someFeatureMachineId', - type: 'parallel', - states: { - foo: { - initial: 'start', - states: { - start: { - entry: raise({ type: 'goToWaitingForPermission' }), - on: { goToWaitingForPermission: 'waitingForPermission' }, - }, - waitingForPermission: { - on: { - 'permission.granted.bluetooth': { target: 'bluetoothGranted' }, - 'permission.denied.bluetooth': { target: 'bluetoothDenied' }, - 'user.didTapBluetoothRequestPermission': { - actions: raise({ - type: 'permissionWasRequested', - permission: Permissions.bluetooth, - }), - }, - }, - }, - bluetoothGranted: { - type: 'final', - }, - bluetoothDenied: { - on: { - 'permission.granted.bluetooth': { target: 'bluetoothGranted' }, - 'user.didTapBluetoothRequestPermission': { - actions: raise({ - type: 'permissionWasRequested', - permission: Permissions.bluetooth, - }), - }, - }, - }, - }, - }, - handlingPermissions: { - on: { - permissionWasRequested: { - actions: [ - sendTo('permissionHandler', ({ event }) => { - return { - type: 'requestPermission', - permission: event.permission, - }; - }), - ], - }, - }, - invoke: { - id: 'permissionHandler', - src: 'permissionReportingMachine', - input: ({ self }) => ({ - permissions: [Permissions.bluetooth], - parent: self, - }), - }, - }, - }, - }); describe('Single Subscriber', () => { it('should allow subscriptions from a subscriber to a single permission', () => { const actor = createActor( diff --git a/libs/permissions/permissionLogic/src/lib/permissionMonitor.machine.ts b/libs/permissions/permissionLogic/src/lib/permissionMonitor.machine.ts index 68c2041..a21d0b9 100644 --- a/libs/permissions/permissionLogic/src/lib/permissionMonitor.machine.ts +++ b/libs/permissions/permissionLogic/src/lib/permissionMonitor.machine.ts @@ -8,7 +8,7 @@ import { sendTo, setup, } from 'xstate'; -import { ActorSystemIds } from './actorIds'; +import { ActorSystemIds } from './application/actorIds'; import { stubApplicationLifecycleReportingActorLogic } from './lifecycle/lifecycle.stubs'; import { PermissionSubscriberMap } from './permission-logic.spec'; import { InitialPermissionStatusMap } from './permission.fixtures'; @@ -19,6 +19,8 @@ import { Permissions, } from './permission.types'; import { permissionCheckerAndRequesterMachine } from './permissionCheckAndRequestMachine'; +import { countingMachineThatNeedsPermissionAt3 } from './features/counting/counting.machine'; +import { someFeatureMachine } from './features/someFeature/someFeature.machine'; export const EmptyPermissionSubscriberMap: PermissionSubscriberMap = Object.values(Permissions).reduce( diff --git a/libs/permissions/permissionLogic/src/lib/permissionReporting/permissionReporting.machine.ts b/libs/permissions/permissionLogic/src/lib/permissionReporting/permissionReporting.machine.ts new file mode 100644 index 0000000..4447873 --- /dev/null +++ b/libs/permissions/permissionLogic/src/lib/permissionReporting/permissionReporting.machine.ts @@ -0,0 +1,116 @@ +import { + AnyActorRef, + AnyEventObject, + enqueueActions, + log, + sendTo, + setup, +} from 'xstate'; +import { Permission, Permissions } from '../permission.types'; +import { ActorSystemIds } from '../application/actorIds'; + +export const permissionReportingMachine = setup({ + types: { + input: {} as { + permissions: Array; + parent: AnyActorRef; + }, + context: {} as { + permissions: Array; + parent: AnyActorRef; + }, + }, + actions: { + sendSubscriptionRequestForStatusUpdates: sendTo( + ({ system }) => { + const actorRef: AnyActorRef = system.get( + ActorSystemIds.permissionMonitoring + ); + return actorRef; + }, + ({ self, context }) => ({ + type: 'subscribeToPermissionStatuses', + permissions: context.permissions, + self, + }) + ), + // satisfies /*TODO type these events to the receiving machine event type*/ AnyEventObject); + checkedSendParent: enqueueActions( + ({ context, enqueue }, event: AnyEventObject) => { + if (!context.parent) { + console.log( + 'WARN: an attempt to send an event to a non-existent parent' + ); + return; + } + + enqueue.sendTo(context.parent, event); + } + ), + }, +}).createMachine({ + description: + "This actor's job is to report permission statuses to the actors that have invoked it. We abstract away this functionality so that it is reusable by any actor that needs it and so they don't need to know how permissions are checked. This keeps control centralized and easy to modify the behavior of.", + context: ({ input }) => ({ + permissions: input.permissions, + parent: input.parent, + }), + entry: [ + 'sendSubscriptionRequestForStatusUpdates', + log('subscribe to status updates'), + ], + on: { + requestPermission: { + actions: [ + sendTo( + ({ system }) => { + return system.get(ActorSystemIds.permissionCheckerAndRequester); + }, + ({ event }) => ({ + type: 'triggerPermissionRequest', + permission: event.permission, + }) + ), + ], + }, + permissionStatusChanged: { + description: + 'Whenever the Permission Monitoring machine reports that a permission status has changed, we receive this event and can process and share with our siblings.', + // We eventually want to communicate this to the actors that have invoked us + actions: [ + log( + ({ event }) => + event.permission + ' status <<