relay-runtime
Version:
A core runtime for building GraphQL-driven applications.
507 lines (475 loc) • 16.2 kB
Flow
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall relay
*/
'use strict';
import type {HandlerProvider} from '../handlers/RelayDefaultHandlerProvider';
import type {GraphQLResponse, PayloadData} from '../network/RelayNetworkTypes';
import type {INetwork} from '../network/RelayNetworkTypes';
import type {ActiveState} from '../store/OperationExecutor';
import type {GetDataID} from '../store/RelayResponseNormalizer';
import type {
ExecuteMutationConfig,
LogFunction,
MissingFieldHandler,
MutableRecordSource,
MutationParameters,
NormalizeResponseFunction,
OperationAvailability,
OperationDescriptor,
OperationLoader,
OptimisticResponseConfig,
OptimisticUpdateFunction,
RelayFieldLogger,
SelectorStoreUpdater,
SingularReaderSelector,
Snapshot,
Store,
StoreUpdater,
TaskScheduler,
} from '../store/RelayStoreTypes';
import type {Disposable} from '../util/RelayRuntimeTypes';
import type {RenderPolicy} from '../util/RelayRuntimeTypes';
import type {ActorIdentifier} from './ActorIdentifier';
import type {
IActorEnvironment,
IMultiActorEnvironment,
MultiActorStoreUpdater,
} from './MultiActorEnvironmentTypes';
const RelayDefaultHandlerProvider = require('../handlers/RelayDefaultHandlerProvider');
const RelayObservable = require('../network/RelayObservable');
const defaultGetDataID = require('../store/defaultGetDataID');
const defaultRelayFieldLogger = require('../store/defaultRelayFieldLogger');
const normalizeResponse = require('../store/normalizeResponse');
const OperationExecutor = require('../store/OperationExecutor');
const RelayModernStore = require('../store/RelayModernStore');
const RelayRecordSource = require('../store/RelayRecordSource');
const ActorSpecificEnvironment = require('./ActorSpecificEnvironment');
export type MultiActorEnvironmentConfig = $ReadOnly<{
createConfigNameForActor?: ?(actorIdentifier: ActorIdentifier) => string,
createNetworkForActor: (actorIdentifier: ActorIdentifier) => INetwork,
createStoreForActor?: ?(actorIdentifier: ActorIdentifier) => Store,
defaultRenderPolicy?: ?RenderPolicy,
getDataID?: GetDataID,
handlerProvider?: HandlerProvider,
isServer?: ?boolean,
logFn?: ?LogFunction,
missingFieldHandlers?: ?$ReadOnlyArray<MissingFieldHandler>,
normalizeResponse?: NormalizeResponseFunction,
operationLoader?: ?OperationLoader,
relayFieldLogger?: ?RelayFieldLogger,
scheduler?: ?TaskScheduler,
shouldProcessClientComponents?: ?boolean,
treatMissingFieldsAsNull?: boolean,
}>;
class MultiActorEnvironment implements IMultiActorEnvironment {
+_actorEnvironments: Map<ActorIdentifier, IActorEnvironment>;
+_createConfigNameForActor: ?(actorIdentifier: ActorIdentifier) => string;
+_createNetworkForActor: (actorIdentifier: ActorIdentifier) => INetwork;
+_createStoreForActor: ?(actorIdentifier: ActorIdentifier) => Store;
+_defaultRenderPolicy: RenderPolicy;
+_getDataID: GetDataID;
+_handlerProvider: HandlerProvider;
+_isServer: boolean;
+_logFn: LogFunction;
+_missingFieldHandlers: $ReadOnlyArray<MissingFieldHandler>;
+_normalizeResponse: NormalizeResponseFunction;
+_operationExecutions: Map<string, ActiveState>;
+_operationLoader: ?OperationLoader;
+_relayFieldLogger: RelayFieldLogger;
+_scheduler: ?TaskScheduler;
+_shouldProcessClientComponents: ?boolean;
+_treatMissingFieldsAsNull: boolean;
constructor(config: MultiActorEnvironmentConfig) {
this._actorEnvironments = new Map();
this._operationLoader = config.operationLoader;
this._createNetworkForActor = config.createNetworkForActor;
this._scheduler = config.scheduler;
this._getDataID = config.getDataID ?? defaultGetDataID;
this._handlerProvider = config.handlerProvider
? config.handlerProvider
: RelayDefaultHandlerProvider;
this._logFn = config.logFn ?? emptyFunction;
this._operationExecutions = new Map();
this._relayFieldLogger = config.relayFieldLogger ?? defaultRelayFieldLogger;
this._shouldProcessClientComponents = config.shouldProcessClientComponents;
this._treatMissingFieldsAsNull = config.treatMissingFieldsAsNull ?? false;
this._isServer = config.isServer ?? false;
this._missingFieldHandlers = config.missingFieldHandlers ?? [];
this._createStoreForActor = config.createStoreForActor;
this._createConfigNameForActor = config.createConfigNameForActor;
this._defaultRenderPolicy = config.defaultRenderPolicy ?? 'partial';
this._normalizeResponse = config.normalizeResponse ?? normalizeResponse;
}
/**
* This method will create an actor specific environment. It will create a new instance
* and store it in the internal maps. If will return a memoized version
* of the environment if we already created one for actor.
*/
forActor(actorIdentifier: ActorIdentifier): IActorEnvironment {
const environment = this._actorEnvironments.get(actorIdentifier);
if (environment == null) {
const newEnvironment = new ActorSpecificEnvironment({
configName: this._createConfigNameForActor
? this._createConfigNameForActor(actorIdentifier)
: null,
actorIdentifier,
multiActorEnvironment: this,
logFn: this._logFn,
relayFieldLogger: this._relayFieldLogger,
store:
this._createStoreForActor != null
? this._createStoreForActor(actorIdentifier)
: new RelayModernStore(RelayRecordSource.create()),
network: this._createNetworkForActor(actorIdentifier),
handlerProvider: this._handlerProvider,
defaultRenderPolicy: this._defaultRenderPolicy,
missingFieldHandlers: this._missingFieldHandlers,
});
this._actorEnvironments.set(actorIdentifier, newEnvironment);
return newEnvironment;
} else {
return environment;
}
}
check(
actorEnvironment: IActorEnvironment,
operation: OperationDescriptor,
): OperationAvailability {
if (
this._missingFieldHandlers == null ||
this._missingFieldHandlers.length === 0
) {
return actorEnvironment.getStore().check(operation, {
handlers: [],
defaultActorIdentifier: actorEnvironment.actorIdentifier,
getSourceForActor: actorIdentifier => {
return this.forActor(actorIdentifier).getStore().getSource();
},
getTargetForActor: () => {
return RelayRecordSource.create();
},
});
}
return this._checkSelectorAndHandleMissingFields(
actorEnvironment,
operation,
this._missingFieldHandlers,
);
}
_checkSelectorAndHandleMissingFields(
actorEnvironment: IActorEnvironment,
operation: OperationDescriptor,
handlers: $ReadOnlyArray<MissingFieldHandler>,
): OperationAvailability {
const targets: Map<ActorIdentifier, MutableRecordSource> = new Map([
[actorEnvironment.actorIdentifier, RelayRecordSource.create()],
]);
const result = actorEnvironment.getStore().check(operation, {
handlers,
defaultActorIdentifier: actorEnvironment.actorIdentifier,
getSourceForActor: actorIdentifier => {
return this.forActor(actorIdentifier).getStore().getSource();
},
getTargetForActor: actorIdentifier => {
let target = targets.get(actorIdentifier);
if (target == null) {
target = RelayRecordSource.create();
targets.set(actorIdentifier, target);
}
return target;
},
});
for (const [actorIdentifier, target] of targets) {
if (target.size() > 0) {
this._scheduleUpdates(() => {
const publishQueue = this.forActor(actorIdentifier).getPublishQueue();
publishQueue.commitSource(target);
publishQueue.run();
});
}
}
return result;
}
subscribe(
actorEnvironment: IActorEnvironment,
snapshot: Snapshot,
callback: (snapshot: Snapshot) => void,
): Disposable {
// TODO: make actor aware
return actorEnvironment.getStore().subscribe(snapshot, callback);
}
retain(
actorEnvironment: IActorEnvironment,
operation: OperationDescriptor,
): Disposable {
// TODO: make actor aware
return actorEnvironment.getStore().retain(operation);
}
applyUpdate(
actorEnvironment: IActorEnvironment,
optimisticUpdate: OptimisticUpdateFunction,
): Disposable {
const publishQueue = actorEnvironment.getPublishQueue();
const dispose = () => {
this._scheduleUpdates(() => {
publishQueue.revertUpdate(optimisticUpdate);
publishQueue.run();
});
};
this._scheduleUpdates(() => {
publishQueue.applyUpdate(optimisticUpdate);
publishQueue.run();
});
return {dispose};
}
revertUpdate(
actorEnvironment: IActorEnvironment,
update: OptimisticUpdateFunction,
): void {
const publishQueue = actorEnvironment.getPublishQueue();
this._scheduleUpdates(() => {
publishQueue.revertUpdate(update);
publishQueue.run();
});
}
replaceUpdate(
actorEnvironment: IActorEnvironment,
update: OptimisticUpdateFunction,
replacement: OptimisticUpdateFunction,
): void {
const publishQueue = actorEnvironment.getPublishQueue();
this._scheduleUpdates(() => {
publishQueue.revertUpdate(update);
publishQueue.applyUpdate(replacement);
publishQueue.run();
});
}
applyMutation<TMutation: MutationParameters>(
actorEnvironment: IActorEnvironment,
optimisticConfig: OptimisticResponseConfig<TMutation>,
): Disposable {
const subscription = this._execute(actorEnvironment, {
createSource: () => RelayObservable.create(_sink => {}),
isClientPayload: false,
operation: optimisticConfig.operation,
optimisticConfig,
updater: null,
}).subscribe({});
return {
dispose: () => subscription.unsubscribe(),
};
}
commitUpdate(
actorEnvironment: IActorEnvironment,
updater: StoreUpdater,
): void {
const publishQueue = actorEnvironment.getPublishQueue();
this._scheduleUpdates(() => {
publishQueue.commitUpdate(updater);
publishQueue.run();
});
}
commitPayload(
actorEnvironment: IActorEnvironment,
operationDescriptor: OperationDescriptor,
payload: PayloadData,
): void {
this._execute(actorEnvironment, {
createSource: () => RelayObservable.from({data: payload}),
isClientPayload: true,
operation: operationDescriptor,
optimisticConfig: null,
updater: null,
}).subscribe({});
}
lookup(
actorEnvironment: IActorEnvironment,
selector: SingularReaderSelector,
): Snapshot {
// TODO: make actor aware
return actorEnvironment.getStore().lookup(selector);
}
execute(
actorEnvironment: IActorEnvironment,
{
operation,
}: {
operation: OperationDescriptor,
},
): RelayObservable<GraphQLResponse> {
return this._execute(actorEnvironment, {
createSource: () =>
actorEnvironment
.getNetwork()
.execute(
operation.request.node.params,
operation.request.variables,
operation.request.cacheConfig || {},
null,
),
isClientPayload: false,
operation,
optimisticConfig: null,
updater: null,
});
}
executeSubscription<TMutation: MutationParameters>(
actorEnvironment: IActorEnvironment,
{
operation,
updater,
}: {
operation: OperationDescriptor,
updater?: ?SelectorStoreUpdater<TMutation['response']>,
},
): RelayObservable<GraphQLResponse> {
return this._execute(actorEnvironment, {
createSource: () =>
actorEnvironment
.getNetwork()
.execute(
operation.request.node.params,
operation.request.variables,
operation.request.cacheConfig || {},
null,
),
isClientPayload: false,
operation,
optimisticConfig: null,
updater,
});
}
executeMutation<TMutation: MutationParameters>(
actorEnvironment: IActorEnvironment,
{
operation,
optimisticResponse,
optimisticUpdater,
updater,
uploadables,
}: ExecuteMutationConfig<TMutation>,
): RelayObservable<GraphQLResponse> {
let optimisticConfig;
if (optimisticResponse || optimisticUpdater) {
optimisticConfig = {
operation: operation,
response: optimisticResponse,
updater: optimisticUpdater,
};
}
return this._execute(actorEnvironment, {
createSource: () =>
actorEnvironment.getNetwork().execute(
operation.request.node.params,
operation.request.variables,
{
...operation.request.cacheConfig,
force: true,
},
uploadables,
),
isClientPayload: false,
operation,
optimisticConfig,
updater,
});
}
executeWithSource(
actorEnvironment: IActorEnvironment,
config: {
operation: OperationDescriptor,
source: RelayObservable<GraphQLResponse>,
},
): RelayObservable<GraphQLResponse> {
return this._execute(actorEnvironment, {
createSource: () => config.source,
isClientPayload: false,
operation: config.operation,
optimisticConfig: null,
updater: null,
});
}
isRequestActive(
_actorEnvironment: IActorEnvironment,
requestIdentifier: string,
): boolean {
const activeState = this._operationExecutions.get(requestIdentifier);
return activeState === 'active';
}
isServer(): boolean {
return this._isServer;
}
_execute<TMutation: MutationParameters>(
actorEnvironment: IActorEnvironment,
{
createSource,
isClientPayload,
operation,
optimisticConfig,
updater,
}: {
createSource: () => RelayObservable<GraphQLResponse>,
isClientPayload: boolean,
operation: OperationDescriptor,
optimisticConfig: ?OptimisticResponseConfig<TMutation>,
updater: ?SelectorStoreUpdater<TMutation['response']>,
},
): RelayObservable<GraphQLResponse> {
return RelayObservable.create<GraphQLResponse>(sink => {
const executor = OperationExecutor.execute<TMutation>({
actorIdentifier: actorEnvironment.actorIdentifier,
getDataID: this._getDataID,
isClientPayload,
operation,
operationExecutions: this._operationExecutions,
operationLoader: this._operationLoader,
operationTracker: actorEnvironment.getOperationTracker(),
optimisticConfig,
getPublishQueue: (actorIdentifier: ActorIdentifier) => {
return this.forActor(actorIdentifier).getPublishQueue();
},
scheduler: this._scheduler,
shouldProcessClientComponents: this._shouldProcessClientComponents,
sink,
// NOTE: Some product tests expect `Network.execute` to be called only
// when the Observable is executed.
source: createSource(),
getStore: (actorIdentifier: ActorIdentifier) => {
return this.forActor(actorIdentifier).getStore();
},
treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
updater,
log: this._logFn,
normalizeResponse: this._normalizeResponse,
});
return () => executor.cancel();
});
}
_scheduleUpdates(task: () => void) {
const scheduler = this._scheduler;
if (scheduler != null) {
scheduler.schedule(task);
} else {
task();
}
}
commitMultiActorUpdate(updater: MultiActorStoreUpdater): void {
for (const [actorIdentifier, environment] of this._actorEnvironments) {
environment.commitUpdate(storeProxy => {
updater(actorIdentifier, environment, storeProxy);
});
}
}
getScheduler(): ?TaskScheduler {
return this._scheduler;
}
}
function emptyFunction() {}
module.exports = MultiActorEnvironment;