Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
Allow useComponent to suspend (#7712)
Browse files Browse the repository at this point in the history
* Allow useComponent to suspend

* Remove unnecessary root.stop() calls

* Update SystemFunctions.tsx

* Update ComponentFunctions.ts
  • Loading branch information
speigg authored Mar 10, 2023
1 parent 1299bf1 commit 7acd42f
Show file tree
Hide file tree
Showing 25 changed files with 89 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ export const PositionalAudioComponent = defineComponent({
},

reactor: function ({ root }) {
if (!hasComponent(root.entity, PositionalAudioComponent)) throw root.stop()

const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility)
const audio = useComponent(root.entity, PositionalAudioComponent)
const mediaElement = useComponent(root.entity, MediaElementComponent)
Expand Down
27 changes: 7 additions & 20 deletions packages/engine/src/audio/systems/PositionalAudioSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getComponent,
hasComponent,
removeQuery,
useComponent,
useOptionalComponent
} from '../../ecs/functions/ComponentFunctions'
import { startQueryReactor } from '../../ecs/functions/SystemFunctions'
Expand Down Expand Up @@ -115,27 +116,13 @@ export default async function PositionalAudioSystem() {
[PositionalAudioComponent, TransformComponent],
function PositionalAudioPannerReactor(props) {
const entity = props.root.entity

const mediaElement = useOptionalComponent(entity, MediaElementComponent)
const panner = useHookstate(null as ReturnType<typeof addPannerNode> | null)

const mediaElement = useComponent(entity, MediaElementComponent)
const positionalAudio = useComponent(entity, PositionalAudioComponent)
useEffect(() => {
if (mediaElement?.value && !panner.value) {
const el = getComponent(entity, MediaElementComponent).element
const audioGroup = AudioNodeGroups.get(el)
if (audioGroup) panner.set(addPannerNode(audioGroup, getComponent(entity, PositionalAudioComponent)))
} else if (panner.value) {
const el = getComponent(entity, MediaElementComponent).element
const audioGroup = AudioNodeGroups.get(el)
if (audioGroup) {
removePannerNode(audioGroup)
panner.set(null)
}
}
}, [mediaElement])

if (!hasComponent(entity, PositionalAudioComponent)) throw props.root.stop()

const audioGroup = AudioNodeGroups.get(mediaElement.element.value)! // is it safe to assume this?
addPannerNode(audioGroup, positionalAudio.value)
return () => removePannerNode(audioGroup)
}, [mediaElement, positionalAudio])
return null
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,9 @@ export const AvatarRigComponent = defineComponent({

reactor: function ({ root }) {
const entity = root.entity

if (!hasComponent(entity, AvatarRigComponent)) throw root.stop()

const debugEnabled = useHookstate(getMutableState(RendererState).debugEnable)
const anim = useComponent(root.entity, AvatarRigComponent)
const pending = useOptionalComponent(root.entity, AvatarPendingComponent)
const anim = useComponent(entity, AvatarRigComponent)
const pending = useOptionalComponent(entity, AvatarPendingComponent)

useEffect(() => {
if (debugEnabled.value && !anim.helper.value && !pending?.value) {
Expand Down
46 changes: 42 additions & 4 deletions packages/engine/src/ecs/functions/ComponentFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { subscribable } from '@hookstate/subscribable'
import * as bitECS from 'bitecs'
import React, { startTransition, useEffect, useLayoutEffect } from 'react'
import React, { experimental_use, startTransition, useEffect, useLayoutEffect } from 'react'
import type from 'react/experimental'

import config from '@etherealengine/common/src/config'
import { DeepReadonly } from '@etherealengine/common/src/DeepReadonly'
Expand Down Expand Up @@ -30,6 +31,8 @@ type OnInitValidateNotState<T> = T extends State<any, {}> ? 'onAdd must not retu
type SomeStringLiteral = 'a' | 'b' | 'c' // just a dummy string literal union
type StringLiteral<T> = string extends T ? SomeStringLiteral : string

const createExistenceMap = () => hookstate({} as Record<Entity, boolean>, subscribable())

export interface ComponentPartial<
ComponentType = any,
Schema extends bitECS.ISchema = {},
Expand Down Expand Up @@ -62,7 +65,7 @@ export interface Component<
onRemove: (entity: Entity, component: State<ComponentType>) => void
reactor?: HookableFunction<React.FC<EntityReactorProps>>
reactorMap: Map<Entity, EntityReactorRoot>
existenceMap: State<Record<Entity, boolean>>
existenceMap: ReturnType<typeof createExistenceMap>
stateMap: Record<Entity, State<ComponentType> | undefined>
valueMap: Record<Entity, ComponentType>
errors: ErrorTypes[]
Expand Down Expand Up @@ -98,7 +101,7 @@ export const defineComponent = <
// We have to create an stateful existence map in order to reactively track which entities have a given component.
// Unfortunately, we can't simply use a single shared state because hookstate will (incorrectly) invalidate other nested states when a single component
// instance is added/removed, so each component instance has to be isolated from the others.
Component.existenceMap = hookstate({} as Record<Entity, boolean>)
Component.existenceMap = createExistenceMap()
Component.stateMap = {}
Component.valueMap = {}
ComponentMap.set(Component.name, Component)
Expand Down Expand Up @@ -386,12 +389,47 @@ export function useQuery(components: QueryComponents) {
return result.value
}

// experimental_use seems to be unavailable in the server environment
function _use(promise) {
if (promise.status === 'fulfilled') {
return promise.value
} else if (promise.status === 'rejected') {
throw promise.reason
} else if (promise.status === 'pending') {
throw promise
} else {
promise.status = 'pending'
promise.then(
(result) => {
promise.status = 'fulfilled'
promise.value = result
},
(reason) => {
promise.status = 'rejected'
promise.reason = reason
}
)
throw promise
}
}

/**
* Use a component in a reactive context (a React component)
*/
export function useComponent<C extends Component<any>>(entity: Entity, Component: C) {
const hasComponent = useHookstate(Component.existenceMap[entity]).value
if (!hasComponent) throw new Error(`${Component.name} does not exist on entity ${entity}`)
// use() will suspend the component (by throwing a promise) and resume when the promise is resolved
if (!hasComponent)
(experimental_use ?? _use)(
new Promise<void>((resolve) => {
const unsubscribe = Component.existenceMap[entity].subscribe((value) => {
if (value) {
resolve()
unsubscribe()
}
})
})
)
return useHookstate(Component.stateMap[entity]) as any as State<ComponentType<C>> // todo fix any cast
}

Expand Down
31 changes: 29 additions & 2 deletions packages/engine/src/ecs/functions/SystemFunctions.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Functions to provide system level functionalities. */

import React from 'react'
import React, { Suspense } from 'react'

import multiLogger from '@etherealengine/common/src/logger'
import { getMutableState, ReactorProps, ReactorRoot, startReactor } from '@etherealengine/hyperflux'
Expand Down Expand Up @@ -323,7 +323,11 @@ function QueryReactor(props: {
return (
<>
{entities.map((entity) => (
<props.ChildEntityReactor key={entity} root={{ ...props.root, entity }} />
<QueryReactorErrorBoundary key={entity}>
<Suspense fallback={null}>
<props.ChildEntityReactor root={{ ...props.root, entity }} />
</Suspense>
</QueryReactorErrorBoundary>
))}
</>
)
Expand All @@ -335,3 +339,26 @@ export const startQueryReactor = (Components: QueryComponents, ChildEntityReacto
return <QueryReactor query={Components} ChildEntityReactor={ChildEntityReactor} root={root} />
})
}

interface ErrorState {
error: Error | null
}

class QueryReactorErrorBoundary extends React.Component<any, ErrorState> {
public state: ErrorState = {
error: null
}

public static getDerivedStateFromError(error: Error): ErrorState {
// Update state so the next render will show the fallback UI.
return { error }
}

public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Uncaught error:', error, errorInfo)
}

public render() {
return this.state.error ? null : this.props.children
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ export const BoundingBoxComponent = defineComponent({
},

reactor: function ({ root }) {
if (!hasComponent(root.entity, BoundingBoxComponent)) throw root.stop()

const debugEnabled = useHookstate(getMutableState(RendererState).debugEnable)
const boundingBox = useComponent(root.entity, BoundingBoxComponent)

Expand Down
2 changes: 0 additions & 2 deletions packages/engine/src/scene/components/AmbientLightComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ export const AmbientLightComponent = defineComponent({
},

reactor: function ({ root }) {
if (!hasComponent(root.entity, AmbientLightComponent)) throw root.stop()

const light = useComponent(root.entity, AmbientLightComponent)

useEffect(() => {
Expand Down
7 changes: 3 additions & 4 deletions packages/engine/src/scene/components/ColliderComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getComponent,
getOptionalComponent,
hasComponent,
useComponent,
useOptionalComponent
} from '../../ecs/functions/ComponentFunctions'
import { Physics } from '../../physics/classes/Physics'
Expand Down Expand Up @@ -102,14 +103,12 @@ export const ColliderComponent = defineComponent({
reactor: function ({ root }) {
const entity = root.entity

const transformComponent = useComponent(entity, TransformComponent)
const colliderComponent = useComponent(entity, ColliderComponent)
const isLoadedFromGLTF = useOptionalComponent(entity, GLTFLoadedComponent)
const transformComponent = useOptionalComponent(entity, TransformComponent)
const colliderComponent = useOptionalComponent(entity, ColliderComponent)
const groupComponent = useOptionalComponent(entity, GroupComponent)

useEffect(() => {
if (!colliderComponent?.value || !transformComponent?.value) return

if (!!isLoadedFromGLTF?.value) {
const colliderComponent = getComponent(entity, ColliderComponent)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ export const DirectionalLightComponent = defineComponent({
},

reactor: function ({ root }) {
if (!hasComponent(root.entity, DirectionalLightComponent)) throw root.stop()

const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility)
const light = useComponent(root.entity, DirectionalLightComponent)

Expand Down
2 changes: 0 additions & 2 deletions packages/engine/src/scene/components/EnvMapBakeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ export const EnvMapBakeComponent = defineComponent({
},

reactor: function ({ root }) {
if (!hasComponent(root.entity, EnvMapBakeComponent)) throw root.stop()

const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility)
const bake = useComponent(root.entity, EnvMapBakeComponent)

Expand Down
8 changes: 3 additions & 5 deletions packages/engine/src/scene/components/GroupComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
hasComponent,
QueryComponents,
removeComponent,
useComponent,
useOptionalComponent
} from '../../ecs/functions/ComponentFunctions'
import { startQueryReactor } from '../../ecs/functions/SystemFunctions'
Expand Down Expand Up @@ -105,13 +106,10 @@ export const startGroupQueryReactor = (
) =>
startQueryReactor([GroupComponent, ...Components], function GroupQueryReactor(props) {
const entity = props.root.entity
// if (!hasComponent(entity, GroupComponent)) throw props.root.stop()

const groupComponent = useOptionalComponent(entity, GroupComponent)

const groupComponent = useComponent(entity, GroupComponent)
return (
<>
{groupComponent?.value?.map((obj, i) => (
{groupComponent.value.map((obj, i) => (
<GroupChildReactor key={obj.uuid} entity={entity} obj={obj} />
))}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ export const HemisphereLightComponent = defineComponent({
},

reactor: function ({ root }) {
if (!hasComponent(root.entity, HemisphereLightComponent)) throw root.stop()

const light = useComponent(root.entity, HemisphereLightComponent)

useEffect(() => {
Expand Down
2 changes: 0 additions & 2 deletions packages/engine/src/scene/components/ImageComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,6 @@ export const SCENE_COMPONENT_IMAGE = 'image'

export function ImageReactor({ root }: EntityReactorProps) {
const entity = root.entity
if (!hasComponent(entity, ImageComponent)) throw root.stop()

const image = useComponent(entity, ImageComponent)
const texture = useHookstate(null as Texture | null)

Expand Down
2 changes: 0 additions & 2 deletions packages/engine/src/scene/components/MediaComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,6 @@ export const MediaComponent = defineComponent({

export function MediaReactor({ root }: EntityReactorProps) {
const entity = root.entity
if (!hasComponent(entity, MediaComponent)) throw root.stop()

const media = useComponent(entity, MediaComponent)
const mediaElement = useOptionalComponent(entity, MediaElementComponent)
const userHasInteracted = useHookstate(getMutableState(EngineState).userHasInteracted)
Expand Down
2 changes: 0 additions & 2 deletions packages/engine/src/scene/components/ModelComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ export const ModelComponent = defineComponent({

function ModelReactor({ root }: EntityReactorProps) {
const entity = root.entity
if (!hasComponent(entity, ModelComponent)) throw root.stop()

const modelComponent = useComponent(entity, ModelComponent)
const groupComponent = useOptionalComponent(entity, GroupComponent)
const model = modelComponent.value
Expand Down
2 changes: 0 additions & 2 deletions packages/engine/src/scene/components/MountPointComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ export const MountPointComponent = defineComponent({
},

reactor: function ({ root }) {
if (!hasComponent(root.entity, MountPointComponent)) throw root.stop()

const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility)
const mountPoint = useComponent(root.entity, MountPointComponent)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,6 @@ export const ParticleSystemComponent = defineComponent({
}),
reactor: function ({ root }: EntityReactorProps) {
const entity = root.entity
if (!hasComponent(entity, ParticleSystemComponent)) throw root.stop()
const componentState = useComponent(entity, ParticleSystemComponent)
const component = componentState.value
const batchRenderer = getBatchRenderer()!
Expand Down
2 changes: 0 additions & 2 deletions packages/engine/src/scene/components/PointLightComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ export const PointLightComponent = defineComponent({
},

reactor: function ({ root }) {
if (!hasComponent(root.entity, PointLightComponent)) throw root.stop()

const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility)
const light = useComponent(root.entity, PointLightComponent)

Expand Down
2 changes: 0 additions & 2 deletions packages/engine/src/scene/components/PortalComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,6 @@ export const PortalComponent = defineComponent({
},

reactor: function ({ root }) {
if (!hasComponent(root.entity, PortalComponent)) throw root.stop()

const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility)
const portalComponent = useComponent(root.entity, PortalComponent)

Expand Down
1 change: 0 additions & 1 deletion packages/engine/src/scene/components/PrefabComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export const PrefabComponent = defineComponent({

reactor: function ({ root }: EntityReactorProps) {
const entity = root.entity
if (!hasComponent(entity, PrefabComponent)) throw root.stop()
const assembly = getComponent(entity, PrefabComponent)
const assemblyState = useComponent(entity, PrefabComponent)

Expand Down
2 changes: 0 additions & 2 deletions packages/engine/src/scene/components/ScenePreviewCamera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ export const ScenePreviewCameraComponent = defineComponent({
},

reactor: function ({ root }) {
if (!hasComponent(root.entity, ScenePreviewCameraComponent)) throw root.stop()

const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility)
const camera = useComponent(root.entity, ScenePreviewCameraComponent)

Expand Down
2 changes: 0 additions & 2 deletions packages/engine/src/scene/components/SpawnPointComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ export const SpawnPointComponent = defineComponent({
},

reactor: function ({ root }) {
if (!hasComponent(root.entity, SpawnPointComponent)) throw root.stop()

const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility)
const spawnPoint = useComponent(root.entity, SpawnPointComponent)

Expand Down
2 changes: 0 additions & 2 deletions packages/engine/src/scene/components/SpotLightComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@ export const SpotLightComponent = defineComponent({
},

reactor: function ({ root }) {
if (!hasComponent(root.entity, SpotLightComponent)) throw root.stop()

const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility)
const light = useComponent(root.entity, SpotLightComponent)

Expand Down
1 change: 0 additions & 1 deletion packages/engine/src/scene/components/VideoComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ export const SCENE_COMPONENT_VIDEO = 'video'

function VideoReactor({ root }: EntityReactorProps) {
const entity = root.entity
if (!hasComponent(entity, VideoComponent)) throw root.stop()

const video = useComponent(entity, VideoComponent)
const mediaUUID = video.mediaUUID.value ?? ''
Expand Down
Loading

0 comments on commit 7acd42f

Please sign in to comment.