Skip to content

Commit 5552c0d

Browse files
authored
Inspector v2: Switch some Observable to IReadonlyObservable (#16742)
This PR just switches some contracts to expose `IReadonlyObservable` instead of `Observable` (for API clarity, and also to simplify some code since `IReadonlyObservable<T>` is covariant). Also small change to `IReadonlyObservable<T>` to make `T = unknown` by default (so for cases where you don't care about the payload, you can just use `IReadonlyObservable` without any type params).
1 parent 973b4fe commit 5552c0d

File tree

8 files changed

+42
-62
lines changed

8 files changed

+42
-62
lines changed

packages/dev/core/src/Misc/observable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export class Observer<T> implements IObserver {
132132
/**
133133
* An interface that defines the reader side of an Observable (receive notifications).
134134
*/
135-
export interface IReadonlyObservable<T> {
135+
export interface IReadonlyObservable<T = unknown> {
136136
/**
137137
* Create a new Observer with the specified callback
138138
* @param callback the callback that will be executed for that Observer

packages/dev/inspector-v2/src/hooks/observableHooks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// eslint-disable-next-line import/no-internal-modules
2-
import type { Observable } from "core/index";
2+
import type { IReadonlyObservable } from "core/index";
33

44
import type { ObservableCollection } from "../misc/observableCollection";
55

@@ -49,7 +49,7 @@ export function useEventfulState<T>(accessor: () => T, element: HTMLElement | nu
4949
* @returns The current value of the accessor.
5050
*/
5151
// eslint-disable-next-line @typescript-eslint/naming-convention
52-
export function useObservableState<T>(accessor: () => T, ...observables: Array<Observable<any> | null | undefined>): T {
52+
export function useObservableState<T>(accessor: () => T, ...observables: Array<IReadonlyObservable | null | undefined>): T {
5353
const [current, setCurrent] = useState(accessor);
5454

5555
useEffect(() => {

packages/dev/inspector-v2/src/misc/observableCollection.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// eslint-disable-next-line import/no-internal-modules
2-
import type { IDisposable } from "core/index";
2+
import type { IDisposable, IReadonlyObservable } from "core/index";
33

44
import { Observable } from "core/Misc/observable";
55

@@ -9,11 +9,14 @@ import { Observable } from "core/Misc/observable";
99
export class ObservableCollection<T> {
1010
private readonly _items: T[] = [];
1111
private readonly _keys: symbol[] = [];
12+
private readonly _observable = new Observable<void>();
1213

1314
/**
1415
* An observable that notifies observers when the collection changes.
1516
*/
16-
public readonly observable = new Observable<void>();
17+
public get observable(): IReadonlyObservable<void> {
18+
return this._observable;
19+
}
1720

1821
/**
1922
* The items in the collection.
@@ -31,14 +34,14 @@ export class ObservableCollection<T> {
3134
const key = Symbol();
3235
this._items.push(item);
3336
this._keys.push(key);
34-
this.observable.notifyObservers();
37+
this._observable.notifyObservers();
3538

3639
return {
3740
dispose: () => {
3841
const index = this._keys.indexOf(key);
3942
this._items.splice(index, 1);
4043
this._keys.splice(index, 1);
41-
this.observable.notifyObservers();
44+
this._observable.notifyObservers();
4245
},
4346
};
4447
}

packages/dev/inspector-v2/src/modularity/serviceContainer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import type { IService, ServiceDefinition } from "./serviceDefinition";
55

66
import { SortGraph } from "../misc/graphUtils";
77

8+
// Service definitions should strongly typed when they are defined to avoid programming errors. However, they are tracked internally
9+
// in a weakly typed manner so they can be handled generically.
810
export type WeaklyTypedServiceDefinition = Omit<ServiceDefinition<IService<symbol>[] | [], IService<symbol>[] | []>, "factory"> & {
911
/**
1012
* A factory function responsible for creating a service instance.
1113
*/
1214
factory: (...args: any) => ReturnType<ServiceDefinition<IService<symbol>[] | [], IService<symbol>[] | []>["factory"]>;
1315
};
1416

17+
// This sorts a set of service definitions based on their dependencies (e.g. a topological sort).
1518
function SortServiceDefinitions(serviceDefinitions: WeaklyTypedServiceDefinition[]) {
1619
const sortedServiceDefinitions: typeof serviceDefinitions = [];
1720
SortGraph(

packages/dev/inspector-v2/src/modularity/serviceDefinition.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,25 @@ export interface IService<ContractIdentity extends symbol> {
2222
readonly [Contract]?: ContractIdentity;
2323
}
2424

25+
// This utility type extracts the service identity (unique symbol type) from a service contract. That is, it extracts T from IService<T>.
2526
type ExtractContractIdentity<ServiceContract extends IService<symbol>> = ServiceContract extends IService<infer ContractIdentity> ? ContractIdentity : never;
2627

28+
// This utility type extracts the contract identities from an array of service contracts. That is, it extracts [T1, T2, ...] from [IService<T1>, IService<T2>, ...].
2729
type ExtractContractIdentities<ServiceContracts extends IService<symbol>[]> = {
2830
[Index in keyof ServiceContracts]: ExtractContractIdentity<ServiceContracts[Index]>;
2931
};
3032

33+
// This utility type converts a union of types into an intersection of types. That is, it converts T1 | T2 | ... into T1 & T2 & ...
34+
// This is specifically used to determine the type that a service factory function returns when it produces multiple services.
3135
type UnionToIntersection<Union> = (Union extends any ? (k: Union) => void : never) extends (k: infer Intersection) => void ? Intersection : never;
3236

3337
type MaybePromise<T> = T | Promise<T>;
3438

3539
/**
3640
* A factory function responsible for creating a service instance.
3741
* Consumed services are passed as arguments to the factory function.
38-
* The returned value must implement all produced services.
42+
* The returned value must implement all produced services, and may IDisposable.
43+
* If not services are produced, the returned value may implement IDisposable, otherwise it may return void.
3944
*/
4045
export type ServiceFactory<Produces extends IService<symbol>[], Consumes extends IService<symbol>[]> = (
4146
...dependencies: [...Consumes, abortSignal?: AbortSignal]

packages/dev/inspector-v2/src/services/panes/scene/nodeExplorerService.tsx

Lines changed: 19 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// eslint-disable-next-line import/no-internal-modules
2-
import type { Observer, Scene } from "core/index";
2+
import type { IReadonlyObservable, Node, Scene } from "core/index";
33

44
import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
55
import type { ISceneExplorerService } from "./sceneExplorerService";
@@ -35,55 +35,24 @@ export const NodeHierarchyServiceDefinition: ServiceDefinition<[], [ISceneExplor
3535
<></>
3636
),
3737
watch: (scene, onAdded, onRemoved) => {
38-
const observers: Observer<unknown>[] = [];
39-
40-
observers.push(
41-
scene.onNewMeshAddedObservable.add((mesh) => {
42-
onAdded(mesh);
43-
}) as Observer<unknown>
44-
);
45-
46-
observers.push(
47-
scene.onNewTransformNodeAddedObservable.add((node) => {
48-
onAdded(node);
49-
}) as Observer<unknown>
50-
);
51-
52-
observers.push(
53-
scene.onNewCameraAddedObservable.add((camera) => {
54-
onAdded(camera);
55-
}) as Observer<unknown>
56-
);
57-
58-
observers.push(
59-
scene.onNewLightAddedObservable.add((light) => {
60-
onAdded(light);
61-
}) as Observer<unknown>
62-
);
63-
64-
observers.push(
65-
scene.onMeshRemovedObservable.add((mesh) => {
66-
onRemoved(mesh);
67-
}) as Observer<unknown>
68-
);
69-
70-
observers.push(
71-
scene.onTransformNodeRemovedObservable.add((node) => {
72-
onRemoved(node);
73-
}) as Observer<unknown>
74-
);
75-
76-
observers.push(
77-
scene.onCameraRemovedObservable.add((camera) => {
78-
onRemoved(camera);
79-
}) as Observer<unknown>
80-
);
81-
82-
observers.push(
83-
scene.onLightRemovedObservable.add((light) => {
84-
onRemoved(light);
85-
}) as Observer<unknown>
86-
);
38+
const observers = [
39+
...(
40+
[
41+
scene.onNewMeshAddedObservable,
42+
scene.onNewTransformNodeAddedObservable,
43+
scene.onNewCameraAddedObservable,
44+
scene.onNewLightAddedObservable,
45+
] as IReadonlyObservable<Node>[]
46+
).map((observable) => observable.add((node) => onAdded(node))),
47+
...(
48+
[
49+
scene.onMeshRemovedObservable,
50+
scene.onTransformNodeRemovedObservable,
51+
scene.onCameraRemovedObservable,
52+
scene.onLightRemovedObservable,
53+
] as IReadonlyObservable<Node>[]
54+
).map((observable) => observable.add((node) => onRemoved(node))),
55+
] as const;
8756

8857
return {
8958
dispose: () => {

packages/dev/inspector-v2/src/services/sceneContext.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// eslint-disable-next-line import/no-internal-modules
2-
import type { Nullable, Observable, Scene } from "core/index";
2+
import type { IReadonlyObservable, Nullable, Scene } from "core/index";
33

44
import type { IService } from "../modularity/serviceDefinition";
55

@@ -17,5 +17,5 @@ export interface ISceneContext extends IService<typeof SceneContextIdentity> {
1717
/**
1818
* Observable that fires whenever the current scene changes.
1919
*/
20-
readonly currentSceneObservable: Observable<Nullable<Scene>>;
20+
readonly currentSceneObservable: IReadonlyObservable<Nullable<Scene>>;
2121
}

packages/dev/inspector-v2/src/services/selectionService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// eslint-disable-next-line import/no-internal-modules
2-
import type { Nullable } from "core/index";
2+
import type { IReadonlyObservable, Nullable } from "core/index";
33

44
import type { IService, ServiceDefinition } from "../modularity/serviceDefinition";
55

@@ -19,7 +19,7 @@ export interface ISelectionService extends IService<typeof SelectionServiceIdent
1919
/**
2020
* An observable that notifies when the selected entity changes.
2121
*/
22-
readonly onSelectedEntityChanged: Observable<void>;
22+
readonly onSelectedEntityChanged: IReadonlyObservable<void>;
2323
}
2424

2525
export const SelectionServiceDefinition: ServiceDefinition<[ISelectionService], []> = {

0 commit comments

Comments
 (0)